CUDA CとMPIを使ったマルチGPU計算の例はよく見かけますが、ノード跨ぎのMPIでのマルチGPUプログラム例はあまり見たことがないので書いてみました。
実施した環境
- Cray CS-Storm
- CPU: Xeon Gold 6150 x2 (36core)
- Mem: 768 GiB
- GPU: NVIDIA V100(PCIE) x10
- I/F: Infiniband
- OS: RHEL/CentOS 7系
スパコン上では下記の環境設定コマンドを実行しました。
通常のクラスターであれば、cuda 10.2とgcc 6.1.0の環境と同等です。
$ module switch cudatoolkit/9.0.176 cudatoolkit/10.2.89
複数のGPUでの実行
例えば、下記のコードを実行するとノード内の搭載されいているすべてのGPUデバイス名を表示することが可能です。
CUDA Runtime APIでは、cudaSetDevice(int id)関数で任意のidのGPUデバイスをターゲットとして設定可能です。
int ngpus;
cudaGetDeviceCount(&ngpus);
for (int igpu = 0; igpu < ngpus; igpu++) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, igpu);
printf("Using Device %d : %s\n", igpu, deviceProp.name);
// igpuのデバイスでデバイスコードkernelが実行される
cudaSetDevice(igpu);
kernel<<<grid, block>>>(...);
}
igpuをfor文で逐次に与えていますが、これをスレッドやMPIプロセスに展開することでマルチGPU処理が可能となります。
今回はこのコードをMPIプログラミングで複数ノードでのマルチGPU処理に対応させてみます。
1MPIプロセスに1GPUを割り当て、複数ノードで並列にデバイス情報を取得してみます。
やりたいことのイメージは下記です。
MPIプロセスとノード番号
MPIプロセスは、複数ノードの場合でも通し番号です。
デバイスIDと対応させるにはノード毎に0番から(GPU番号-1)を対応させるため、MPIプロセスがノード内で何番目かを算出するため、ノード数の情報が必要です。
MPIでノード数を取得する機能を調べてみたのですが、下記のstackoverflowでも質問でていましたが、どうやらないようでした。
各MPIプロセスで自分が実行されているノードのホスト名を取得し、ユニークなホスト名をカウントすることで対応します。下記のように実装しました。
// Get the hostname of current node
MPI_Get_processor_name(hostname, &ilen);
// Gather the hostnames from all processes
char all_hostnames[nprocs][MPI_MAX_PROCESSOR_NAME];
MPI_Allgather(hostname, MPI_MAX_PROCESSOR_NAME, MPI_CHAR, all_hostnames, MPI_MAX_PROCESSOR_NAME, MPI_CHAR, MPI_COMM_WORLD);
// Count unique hostnames to determine the number of nodes
num_nodes = 0;
num_nodes++;
for (int i = 1; i < nprocs; i++) {
int is_unique = 1;
for (int j = 0; j < i; j++) {
if (strcmp(all_hostnames[i], all_hostnames[j]) == 0) {
is_unique = 0;
break;
}
}
if (is_unique) {
num_nodes++;
}
}
if (rank == 0) {
printf("number of nodes: %d on %s \n", num_nodes, hostname);
}
ノード数がわかれば、下記のようにして各プロセスにgpuID 0番から(GPU数-1)を対応させることができます。
// Assign GPU to each MPI process
int igpu = rank%(nprocs/num_nodes);
ソースコード全体
上記の内容に、エラー処理などを加えて下記のようにしました。
#include <mpi.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <cuda.h>
#include <cuda_runtime_api.h>
#define CUDACHECK(msg) { \
const cudaError_t error = msg; \
if (error != cudaSuccess) { \
fprintf(stderr, "Fatal error: %s (%s at %s:%d)\n", \
msg, cudaGetErrorString(error), __FILE__, __LINE__); \
fprintf(stderr, "*** FAILED - ABORTING\n"); \
exit(1); \
} \
}
int main(int argc, char *argv[])
{
int rank, nprocs, ilen;
int ngpus, num_nodes;
char hostname[MPI_MAX_PROCESSOR_NAME];
double tstart = 0.0, tend = 0.0;
MPI_Status reqstat;
MPI_Request send_request;
MPI_Request recv_request;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// Get the hostname of current node
MPI_Get_processor_name(hostname, &ilen);
// Gather the hostnames from all processes
char all_hostnames[nprocs][MPI_MAX_PROCESSOR_NAME];
MPI_Allgather(hostname, MPI_MAX_PROCESSOR_NAME, MPI_CHAR, all_hostnames, MPI_MAX_PROCESSOR_NAME, MPI_CHAR, MPI_COMM_WORLD);
// Count unique hostnames to determine the number of nodes
num_nodes = 0;
num_nodes++;
for (int i = 1; i < nprocs; i++) {
int is_unique = 1;
for (int j = 0; j < i; j++) {
if (strcmp(all_hostnames[i], all_hostnames[j]) == 0) {
is_unique = 0;
break;
}
}
if (is_unique) {
num_nodes++;
}
}
if (rank == 0) {
printf("number of nodes: %d\n", num_nodes);
}
// Get the number of gpus on the current node
CUDACHECK(cudaGetDeviceCount(&ngpus));
if (rank%(nprocs/num_nodes) == 0) { // First process number of each node
printf("number of GPUs: %d on %s \n",ngpus, hostname);
}
// Check the number of GPUs and processes
if(ngpus < nprocs/num_nodes) {
if(rank == 0) printf("This test requires MPI processes (%d) < number of GPUs (%d)\n",nprocs/num_nodes,ngpus);
MPI_Finalize();
exit(EXIT_FAILURE);
}
// Assign GPU to each MPI process
int igpu = rank%(nprocs/num_nodes);
CUDACHECK(cudaSetDevice(igpu));
// Get GPU device property
struct cudaDeviceProp deviceProp;
CUDACHECK(cudaGetDeviceProperties(&deviceProp, igpu))
printf("Using Device %d: %s on %s (process rank: %d)\n", igpu, deviceProp.name, hostname, rank);
// ここで任意のkernel関数を起動するとプロセス毎に割り当てられたGPUで実行される
// 簡略化のためkernel関数の記述は省略
// kernel<<<grid, block>>>(...);
MPI_Finalize();
return EXIT_SUCCESS;
}
コンパイル
OpenMPI環境
参考に示すOpenMPI環境では下記のコマンドでビルドしました。
nvcc -ccbin mpicc -O2 -gencode arch=compute_70,code=compute_70 -I${CUDA_HOME}/include -L${CUDA_HOME}/lib64 -lcudart -lmpi -o mpi_CUDA_device_check mpi_CUDA_device_check.c
Intel MPI環境
nvccのオプション-ccbinを用いてバックエンドをインテルコンパイラに設定してビルドできます。
nvcc -ccbin mpiicc -m64 -gencode arch=compute_70,code=compute_70 -L${CUDA_HOME}/lib64 -lcudart -lmpi -o mpi_CUDA_device_check-intel mpi_CUDA_device_check.c
実行&結果
実行方法
ノードあたり10プロセス(10GPU)、2ノードで実行してみます。
mpirun -np 20 -N 10 -hostfile $PBS_NODEFILE --oversubscribe ./mpi_CUDA_device_check
CS-Storm OpenMPIでのPBSスクリプト例
#!/bin/sh
#PBS -l select=2
#PBS -q DA_002g
#PBS -N mpi_CUDA_devcheck
module switch cudatoolkit/9.0.176 cudatoolkit/10.2.89
module unload intel
module load gcc/6.1.0
UCXROOT=/usr/local/app/OpenMPI/ucx1.10.0
MPIROOT=/usr/local/app/OpenMPI/openmpi-4.1.0
PATH=$UCXROOT/bin:$MPIROOT/bin:$PATH
LD_LIBRARY_PATH=$UCXROOT/lib:$MPIROOT/lib:$LD_LIBRARY_PATH
MANPATH=$MPIROOT/share/man:$MANPATH
export MPIROOT PATH LD_LIBRARY_PATH MANPATH
export OMPI_MCA_btl_openib_allow_ib=1
export OMPI_MCA_btl_openib_if_include="mlx5_0:1"
export OMP_NUM_THREADS=1
cd $PBS_O_WORKDIR
mpirun -np 20 -N 10 -hostfile $PBS_NODEFILE --oversubscribe ./mpi_CUDA_device_check
CS-Storm IntelMPIでのPBSスクリプト例
#!/bin/sh
#PBS -l select=2
#PBS -q DA_002g
#PBS -N mpi_CUDA_devcheck
module switch cudatoolkit/9.0.176 cudatoolkit/10.2.89
module switch intel/17.0.4 intel/18.0.3
export OMP_NUM_THREADS=1
cd $PBS_O_WORKDIR
mpirun -np 20 -ppn 10 -hostfile $PBS_NODEFILE ./mpi_CUDA_device_check-intel
実行結果
実行結果は下記のようになっており、ノード跨ぎで20GPUのデバイス情報が取得できています。
各プロセス毎に出力しているため、順番はランダムです。
number of nodes: 2
Using Device 5: Tesla V100-PCIE-16GB on cgpu24 (process rank: 5)
Using Device 5: Tesla V100-PCIE-16GB on cgpu25 (process rank: 15)
Using Device 7: Tesla V100-PCIE-16GB on cgpu24 (process rank: 7)
Using Device 2: Tesla V100-PCIE-16GB on cgpu24 (process rank: 2)
Using Device 8: Tesla V100-PCIE-16GB on cgpu24 (process rank: 8)
Using Device 1: Tesla V100-PCIE-16GB on cgpu24 (process rank: 1)
Using Device 1: Tesla V100-PCIE-16GB on cgpu25 (process rank: 11)
number of GPUs: 10 on cgpu24
Using Device 0: Tesla V100-PCIE-16GB on cgpu24 (process rank: 0)
Using Device 8: Tesla V100-PCIE-16GB on cgpu25 (process rank: 18)
Using Device 3: Tesla V100-PCIE-16GB on cgpu24 (process rank: 3)
Using Device 3: Tesla V100-PCIE-16GB on cgpu25 (process rank: 13)
Using Device 9: Tesla V100-PCIE-16GB on cgpu24 (process rank: 9)
Using Device 6: Tesla V100-PCIE-16GB on cgpu24 (process rank: 6)
Using Device 4: Tesla V100-PCIE-16GB on cgpu24 (process rank: 4)
Using Device 6: Tesla V100-PCIE-16GB on cgpu25 (process rank: 16)
Using Device 7: Tesla V100-PCIE-16GB on cgpu25 (process rank: 17)
number of GPUs: 10 on cgpu25
Using Device 0: Tesla V100-PCIE-16GB on cgpu25 (process rank: 10)
Using Device 4: Tesla V100-PCIE-16GB on cgpu25 (process rank: 14)
Using Device 9: Tesla V100-PCIE-16GB on cgpu25 (process rank: 19)
Using Device 2: Tesla V100-PCIE-16GB on cgpu25 (process rank: 12)
まとめ
MPIとCUDA Cで複数ノードによる複数GPUの使用方法を記載してみました。
cudaSetDevice()関数に与えるデバイスIDとプロセスIDの割り当てに少しコツが必要です。
実際の計算ではこれに加えて、メモリ転送処理などが必要になります。
GPU Directを用いた方法など、そのうち記載してみたいと思います。
参考