CUDAとMPIを併用すると、1台に複数のグラフィックスボードを持ったコンピュータ(マルチGPU)、
または複数台の1個以上のグラフィックスボードを持ったコンピュータで並列計算することができます。
グラフィックスボードではCUDAを用いて高速に計算を行い、
プロセス間の通信はMPIを用いて行います。
CUDA+MPIのプログラミング方法
CUDA+MPIではMPIを用いて問題をプロセスに分割しCUDAで計算を行います。
host-device memoryでは以下のような手順になります。
CUDA+MPIの作業手順
CUDA+MPIは2段階の並列計算になるので、以下の手順で開発することが必要です。
2.と3.は逆でも構いません。
リスト11-1にベクトル和をCUDA+MPIで並列計算するプログラムを示します。
リスト11-1 CUDA+MPIプログラム (cuda_mpi_vadd.cu)
1 /*
2 cuda_mpi_vadd.cu (CUDA + MPI)
3
4 Compile,Link:
5 > nvcc -O2 -D_MPI -o cuda_mpi_vadd cuda_mpi_vadd.cu cuda_memory.cu msmpi.lib
6
7 Usage:
8 > mpiexec -n <proc> cuda_mpi_vadd [-gpu|-cpu] [-hdm|-um] <n> <loop>
9 > mpiexec -hosts <hosts> <host> <proc> [<host> <proc> ...] cuda_mpi_vadd [-gpu|-cpu] [-hdm|-um] <n> <loop>
10 */
11
12 // GPU/CPU
13 __host__ __device__
14 static void vadd_calc(float a, float b, float *c)
15 {
16 *c = a + b;
17 }
18
19 // GPU
20 __global__
21 static void vadd_gpu(int n, const float *a, const float *b, float *c)
22 {
23 int tid = threadIdx.x + (blockIdx.x * blockDim.x);
24 if (tid < n) {
25 vadd_calc(a[tid], b[tid], &c[tid]);
26 }
27 }
28
29 // CPU
30 static void vadd_cpu(int n, const float *a, const float *b, float *c)
31 {
32 for (int i = 0; i < n; i++) {
33 vadd_calc(a[i], b[i], &c[i]);
34 }
35 }
36
37 // GPU/CPU
38 static void vadd(int gpu, int n, const float *a, const float *b, float *c)
39 {
40 if (gpu) {
41 int block = 256;
42 int grid = (n + (block - 1)) / block;
43 vadd_gpu<<<grid, block>>>(n, a, b, c);
44 }
45 else {
46 vadd_cpu(n, a, b, c);
47 }
48 }
49
50 #include <stdlib.h>
51 #include <stdio.h>
52 #include <string.h>
53 #include <time.h>
54 #ifdef MPI
55 #include <mpi.h>
56 #endif
57
58 static int device_number(int, int, int []);
59 extern void cuda_malloc(int, int, void **, size_t);
60 extern void cuda_free(int, void *);
61 extern void cuda_memcpy(int, void *, const void *, size_t, cudaMemcpyKind);
62
63 int main(int argc, char **argv)
64 {
65 int gpu = 1;
66 int um = 0;
67 int comm_size = 1;
68 int comm_rank = 0;
69 int n = 1000;
70 int nloop = 1000;
71 int nhost = 1;
72 int *ndevice;
73 float *a, *b, *c;
74 clock_t t0 = 0, t1 = 0;
75
76 // initialize (MPI)
77 #ifdef MPI
78 MPI_Init(&argc, &argv);
79 MPI_Comm_size(MPI_COMM_WORLD, &comm_size);
80 MPI_Comm_rank(MPI_COMM_WORLD, &comm_rank);
81 #endif
82
83 // arguments
84 while (--argc) {
85 argv++;
86 if (!strcmp(*argv, "-hosts")) {
87 if (--argc) {
88 nhost = atoi(*++argv);
89 if (nhost < 1) nhost = 1;
90 ndevice = (int *)malloc(nhost * sizeof(int));
91 for (int ihost = 0; ihost < nhost; ihost++) {
92 if (argc > 1) {
93 ndevice[ihost] = atoi(*++argv);
94 argc--;
95 }
96 else {
97 ndevice[ihost] = 1;
98 }
99 }
100 }
101 }
102 else if (!strcmp(*argv, "-gpu")) {
103 gpu = 1;
104 }
105 else if (!strcmp(*argv, "-cpu")) {
106 gpu = 0;
107 }
108 else if (!strcmp(*argv, "-hdm")) {
109 um = 0;
110 }
111 else if (!strcmp(*argv, "-um")) {
112 um = 1;
113 }
114 else if (argc == 2) {
115 n = atoi(*argv);
116 }
117 else if (argc == 1) {
118 nloop = atoi(*argv);
119 }
120 }
121
122 // GPU device
123 if (gpu) {
124 // rank -> device number
125 int device = device_number(comm_rank, nhost, ndevice);
126
127 // GPU info
128 cudaDeviceProp prop;
129 cudaGetDeviceProperties(&prop, device);
130 printf("Rank-%d GPU-%d : %s, C.C.%d.%d, U.M.%s\n", comm_rank, device, prop.name, prop.major, prop.minor, (um ? "ON" : "OFF"));
131 fflush(stdout);
132
133 // set device
134 cudaSetDevice(device);
135 }
136
137 // global size : n
138 // local size : l_n
139 int l_n = (n + (comm_size - 1)) / comm_size;
140
141 // alloc device memory
142 size_t size = l_n * sizeof(float);
143 cuda_malloc(gpu, um, (void **)&a, size);
144 cuda_malloc(gpu, um, (void **)&b, size);
145 cuda_malloc(gpu, um, (void **)&c, size);
146
147 // alloc host memory
148 float *h_a = (float *)malloc(size);
149 float *h_b = (float *)malloc(size);
150
151 // setup problem
152 for (int i = 0; i < l_n; i++) {
153 int gid = (comm_rank * l_n) + i;
154 if (gid < n) {
155 h_a[i] = gid;
156 h_b[i] = gid + 1;
157 }
158 }
159
160 // copy host to device
161 cuda_memcpy(gpu, a, h_a, size, cudaMemcpyHostToDevice);
162 cuda_memcpy(gpu, b, h_b, size, cudaMemcpyHostToDevice);
163
164 // timer
165 #ifdef MPI
166 MPI_Barrier(MPI_COMM_WORLD);
167 #endif
168 if (comm_rank == 0) {
169 t0 = clock();
170 }
171
172 // calculation
173 for (int loop = 0; loop < nloop; loop++) {
174 vadd(gpu, l_n, a, b, c);
175 }
176 if (gpu) cudaDeviceSynchronize();
177
178 // timer
179 #ifdef MPI
180 MPI_Barrier(MPI_COMM_WORLD);
181 #endif
182 if (comm_rank == 0) {
183 t1 = clock();
184 }
185
186 // copy device to host
187 float *h_c = (float *)malloc(size);
188 cuda_memcpy(gpu, h_c, c, size, cudaMemcpyDeviceToHost);
189
190 // local sum
191 double sum = 0;
192 for (int i = 0; i < l_n; i++) {
193 sum += h_c[i];
194 }
195
196 // reduction (MPI)
197 #ifdef MPI
198 double l_sum = sum;
199 MPI_Reduce(&l_sum, &sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
200 #endif
201
202 // output
203 if (comm_rank == 0) {
204 double exact = (double)n * n;
205 double sec = (double)(t1 - t0) / CLOCKS_PER_SEC;
206 printf("nproc=%d n=%d nloop=%d %e(%e) %s[sec]=%.3f\n",
207 comm_size, n, nloop, sum, exact, (gpu ? "GPU" : "CPU"), sec);
208 fflush(stdout);
209 }
210
211 #ifdef MPI
212 MPI_Finalize();
213 #endif
214
215 // free
216 free(h_a);
217 free(h_b);
218 free(h_c);
219 cuda_free(gpu, a);
220 cuda_free(gpu, b);
221 cuda_free(gpu, c);
222
223 return 0;
224 }
225
226 // rank -> device number
227 static int device_number(int comm_rank, int nhost, int ndevice[])
228 {
229 int device = 0;
230
231 if (nhost <= 1) {
232 // single node
233 device = comm_rank;
234 }
235 else {
236 // cluster
237 device = -1;
238 int rank = -1;
239 for (int ihost = 0; ihost < nhost; ihost++) {
240 for (int idevice = 0; idevice < ndevice[ihost]; idevice++) {
241 if (++rank == comm_rank) {
242 device = idevice;
243 break;
244 }
245 }
246 if (device >= 0) {
247 break;
248 }
249 }
250 if (device < 0) device = 0;
251 }
252
253 int num_device;
254 cudaGetDeviceCount(&num_device);
255 if (device >= num_device) device = num_device - 1;
256
257 return device;
258 }
ソースコードの説明
86-101行目, 123-135行目, 227-258行目: MPIではプログラムの知っている情報はプロセス数と自分のランクのみです。
これでは自分に割り当てるデバイス番号がわからないので、
引数の"-hosts"オプションによってランクからデバイス番号が求められるようにしています。
139行目:全体の問題サイズnから各プロセスの問題サイズl_nを求めます。
142-145行目: 各プロセスは部分配列を持ちます。
これによってプロセス数を増やせばそれに比例して扱える問題サイズが大きくなります。
153-154行目: 各プロセスのランクと問題サイズから全体のインデックスを求めます。
全体の問題サイズはプロセス数の倍数とは限りませんので全体のインデックスが全体の問題サイズを超えないように条件判定が必要です。
コンパイル・リンク方法
コンパイル・リンク方法は以下の通りです。
> nvcc -O2 -D_MPI -o cuda_mpi_vadd cuda_mpi_vadd.cu cuda_memory.cu msmpi.lib
VC++では多数の"warning C4819"が出ることがあります。
そのときは以下のようにコンパイルオプションを追加してください。
> nvcc -O2 -D_MPI -Xcompiler "/wd4819" -o cuda_mpi_vadd cuda_mpi_vadd.cu cuda_memory.cu msmpi.lib
プログラムの実行方法
プログラムの実行方法は以下の通りです。
1台に複数のグラフィックスボードがあるとき:
> mpiexec -n プロセス数 cuda_mpi_vadd [-gpu|-cpu] [-hdm|-um] 配列の大きさ 繰り返し回数
複数ノードで計算するとき:
> mpiexec -hosts ホスト数 ホスト名 プロセス数 [[ホスト名 プロセス数] ...] cuda_mpi_vadd -hosts ホスト数 プロセス数 ... [-gpu|-cpu] [-hdm|-um] 配列の大きさ 繰り返し回数
mpiexecの引数の"ホスト名 プロセス数"はホストの数だけ必要です。
"プロセス数"には通常は各ノードのグラフィックスボードの数を指定します。
デバイス番号の小さい順に使用されます。
cuda_mpi_vaddの引数の"ホスト数 プロセス数 ..."にはmpiexecの引数の"ホスト数 ホスト名 プロセス数 ..."からホスト名を除いて数字だけを取り出したものを入力してください。
例えば以下のようになります。
> mpiexec -n 2 cuda_mpi_vadd 100000000 1000 (1台に2GPUがあるとき)
> mpiexec -n 2 cuda_mpi_vadd -um 100000000 1000 (unified memoryを使用するとき)
> mpiexec -n 2 cuda_mpi_vadd -cpu 100000000 1000 (CPUで計算するとき、デバッグ用)
> mpiexec -hosts 2 localhost 1 PC2 1 cuda_mpi_vadd -hosts 2 1 1 100000000 1000 (2台で計算するとき)
> mpiexec -hosts 2 localhost 2 PC2 1 cuda_mpi_vadd -hosts 2 2 1 100000000 1000 (2台で計算しlocalhostに2GPUがあるとき)
繰り返し回数は計算時間の測定誤差を小さくするためです。
mpiexecの使い方と実行環境の設定については7.5を参考にしてください。
表11-1にベクトル和の計算時間を示します。
配列の大きさ(=N)と繰り返し回数(=L)の積は一定(=1011)です。
従って全体の演算量は同じです。
本環境は1GPUで1~2個のプロセスを起動しているので、
1プロセスと2プロセスの計算時間が変わらなければ、
2GPUが実装された環境では1GPUの2倍速くなることが予想できます。
No.1-3では2プロセスの計算時間は1プロセスより少し大きいので、
2GPUでは2倍弱速くなると予想できます。
No.4の2プロセスで計算時間が大幅に増えている理由は不明です。
なお、8章で述べたように、host-device memory と unified memory の計算時間は同じです。
| No. | 配列の大きさN | 繰り返し回数L | 1プロセス | 2プロセス |
|---|---|---|---|---|
| 1 | 100,000,000 | 1,000 | 4.98秒 | 6.79秒 |
| 2 | 10,000,000 | 10,000 | 5.00秒 | 6.82秒 |
| 3 | 1,000,000 | 100,000 | 5.22秒 | 6.89秒 |
| 4 | 100,000 | 1,000,000 | 6.61秒 | 22.02秒 |