CUDA CとMPIで複数ノード、複数GPUを使ってみる

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デバイスをターゲットとして設定可能です。

```getDeviceName.cu
    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_CUDA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194778/28a0f9e4-66d0-8fbe-f6df-41fbcff8b215.png)



## MPIプロセスとノード番号

MPIプロセスは、複数ノードの場合でも通し番号です。
デバイスIDと対応させるにはノード毎に0番から(GPU番号-1)を対応させるため、MPIプロセスがノード内で何番目かを算出するため、ノード数の情報が必要です。
MPIでノード数を取得する機能を調べてみたのですが、下記のstackoverflowでも質問でていましたが、どうやらないようでした。

https://stackoverflow.com/questions/8314000/mpi-getting-number-of-nodes-not-processes-in-a-communicator

各MPIプロセスで自分が実行されているノードのホスト名を取得し、ユニークなホスト名をカウントすることで対応します。下記のように実装しました。

```getNumNodes.c
    // 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);
```



## ソースコード全体

上記の内容に、エラー処理などを加えて下記のようにしました。

```mpi_CUDA_device_check.c
#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(call)                                                       \
do {                                                                          \
    cudaError_t __err = (call);                                               \
    if (__err != cudaSuccess) {                                               \
        fprintf(stderr,                                                       \
                "CUDA error %s at %s:%d\n",                                   \
                cudaGetErrorString(__err), __FILE__, __LINE__);               \
        MPI_Abort(MPI_COMM_WORLD, (int)__err);                                \
    }                                                                         \
} while (0)

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スクリプト例
```submit.sh 
#!/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スクリプト例
```submit-intel.sh 
#!/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を用いた方法など、そのうち記載してみたいと思います。

## 参考

https://qiita.com/tarako1889/items/c3d014881e1fa285ca01