OpenCLとMPIを併用すると、複数個のNVIDIAまたはAMDのグラフィックスボードを用いて並列計算することができます。
1台のコンピュータが複数個のグラフィックスボードを持つ場合(マルチGPU)と、
複数台のコンピュータが1個以上のグラフィックスボードを持つ場合に対応することができます。
グラフィックスボードではOpenCLを用いて高速に計算を行い、プロセス間の通信はMPIを用いて行います。
プログラミング方法と作業手順はCUDA+MPIと同じなので11.も参考にしてください。
リスト12-1にベクトルの和をOpenCL+MPIで並列計算するプログラムを示します。
なお、カーネルプログラムvadd.clはリスト9-2と同じです。
リスト12-1 OpenCL+MPIプログラム(ocl_mpi_vadd.c)
1 /*
2 ocl_mpi_vadd.c (OpenCL + MPI)
3
4 Compile + Link:
5 > cl.exe /O2 ocl_mpi_vadd.c OpenCL.lib msmpi.lib
6
7 Usage:
8 > mpiexec -n <proc> ocl_mpi_vadd <n> <loop> <platform> <device> ...
9 */
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <time.h>
15
16 #ifdef __APPLE__
17 #include <OpenCL/opencl.h>
18 #else
19 #include <CL/cl.h>
20 #endif
21
22 #ifdef MPI
23 #include <mpi.h>
24 #endif
25
26 #define MAX_PLATFORMS (10)
27 #define MAX_DEVICES (10)
28 #define MAX_SOURCE_SIZE (100000)
29
30 static int setup_ocl(cl_uint, cl_uint, char *);
31 static void vadd_calc(void);
32 static void vadd(void);
33
34 cl_command_queue Queue;
35 cl_kernel k_vadd;
36 int N;
37 float *h_A, *h_B, *h_C;
38 cl_mem d_A, d_B, d_C;
39 int OCL;
40
41 int main(int argc, char **argv)
42 {
43 int *platform = NULL;
44 int *device = NULL;
45 int comm_size = 1;
46 int comm_rank = 0;
47 int n = 1000;
48 int nloop = 1000;
49
50 clock_t t0 = 0, t1 = 0;
51 #ifdef MPI
52 MPI_Status status;
53 int g_ret;
54 #endif
55
56 // initialize (MPI)
57 #ifdef MPI
58 MPI_Init(&argc, &argv);
59 MPI_Comm_size(MPI_COMM_WORLD, &comm_size);
60 MPI_Comm_rank(MPI_COMM_WORLD, &comm_rank);
61 #endif
62
63 // arguments
64 platform = (int *)calloc(comm_size, sizeof(int));
65 device = (int *)calloc(comm_size, sizeof(int));
66 if (argc >= 3 + 2 * comm_size) {
67 n = atoi(argv[1]);
68 nloop = atoi(argv[2]);
69 for (int i = 0; i < comm_size; i++) {
70 platform[i] = atoi(argv[3 + 2 * i]);
71 device[i] = atoi(argv[4 + 2 * i]);
72 }
73 }
74 OCL = (platform[0] >= 0);
75
76 // local size : N
77 N = (n + (comm_size - 1)) / comm_size;
78
79 // alloc host memory
80 size_t size = N * sizeof(float);
81 h_A = (float *)malloc(size);
82 h_B = (float *)malloc(size);
83 h_C = (float *)malloc(size);
84
85 // setup problem
86 for (int i = 0; i < N; i++) {
87 int gid = (comm_rank * N) + i;
88 if (gid < n) {
89 h_A[i] = gid + 1.0f;
90 h_B[i] = gid + 1.0f;
91 }
92 }
93
94 // setup OpenCL
95 if (OCL) {
96 char msg[BUFSIZ];
97 int ret = setup_ocl((cl_uint)platform[comm_rank], (cl_uint)device[comm_rank], msg);
98 #ifdef MPI
99 // show device name
100 if (comm_rank == 0) {
101 printf("%d: %s\n", 0, msg);
102 for (int i = 1; i < comm_size; i++) {
103 MPI_Recv(msg, BUFSIZ, MPI_CHAR, i, 0, MPI_COMM_WORLD, &status);
104 printf("%d: %s\n", i, msg);
105 }
106 fflush(stdout);
107 }
108 else {
109 MPI_Send(msg, BUFSIZ, MPI_CHAR, 0, 0, MPI_COMM_WORLD);
110 }
111 // error check
112 MPI_Allreduce(&ret, &g_ret, 1, MPI_INT, MPI_LOR, MPI_COMM_WORLD);
113 if (g_ret) {
114 MPI_Finalize();
115 exit(1);
116 }
117 #else
118 printf("%s\n", msg);
119 if (ret) {
120 exit(1);
121 }
122 #endif
123 }
124
125 // timer
126 if (comm_rank == 0) {
127 t0 = clock();
128 }
129
130 // copy host to device
131 if (OCL) {
132 clEnqueueWriteBuffer(Queue, d_A, CL_TRUE, 0, size, h_A, 0, NULL, NULL);
133 clEnqueueWriteBuffer(Queue, d_B, CL_TRUE, 0, size, h_B, 0, NULL, NULL);
134 }
135
136 // run
137 for (int loop = 0; loop < nloop; loop++) {
138 vadd_calc();
139 }
140
141 // copy device to host
142 if (OCL) {
143 clEnqueueReadBuffer(Queue, d_C, CL_TRUE, 0, size, h_C, 0, NULL, NULL);
144 }
145
146 // timer
147 if (comm_rank == 0) {
148 t1 = clock();
149 }
150
151 // sum
152 double sum = 0;
153 for (int i = 0; i < N; i++) {
154 sum += h_C[i];
155 }
156
157 // reduction (MPI)
158 #ifdef MPI
159 double l_sum = sum;
160 MPI_Reduce(&l_sum, &sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
161 #endif
162
163 // output
164 if (comm_rank == 0) {
165 double exact = n * (n + 1.0);
166 double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC;
167 printf("N=%d L=%d %.6e(%.6e) err=%.1e %dGPUs %.3f[sec]\n",
168 n, nloop, sum, exact, fabs((sum - exact) / exact), comm_size, sec);
169 fflush(stdout);
170 }
171
172 #ifdef MPI
173 MPI_Finalize();
174 #endif
175
176 // release
177 if (OCL) {
178 clReleaseMemObject(d_A);
179 clReleaseMemObject(d_B);
180 clReleaseMemObject(d_C);
181 clReleaseKernel(k_vadd);
182 clReleaseCommandQueue(Queue);
183 }
184
185 // free
186 free(h_A);
187 free(h_B);
188 free(h_C);
189
190 return 0;
191 }
192
193 // setup OpenCL
194 static int setup_ocl(cl_uint platform, cl_uint device, char *msg)
195 {
196 cl_context context = NULL;
197 cl_program program = NULL;
198 cl_platform_id platform_id[MAX_PLATFORMS];
199 cl_device_id device_id[MAX_DEVICES];
200
201 FILE *fp;
202 char *source_str;
203 char str[BUFSIZ];
204 size_t source_size, ret_size, size;
205 cl_uint num_platforms, num_devices;
206 cl_int ret;
207
208 // alloc
209 source_str = (char *)malloc(MAX_SOURCE_SIZE * sizeof(char));
210
211 // platform
212 clGetPlatformIDs(MAX_PLATFORMS, platform_id, &num_platforms);
213 if (platform >= num_platforms) {
214 sprintf(msg, "error : platform = %d (limit = %d)", platform, num_platforms - 1);
215 return 1;
216 }
217
218 // device
219 clGetDeviceIDs(platform_id[platform], CL_DEVICE_TYPE_ALL, MAX_DEVICES, device_id, &num_devices);
220 if (device >= num_devices) {
221 sprintf(msg, "error : device = %d (limit = %d)", device, num_devices - 1);
222 return 1;
223 }
224
225 // device name (option)
226 clGetDeviceInfo(device_id[device], CL_DEVICE_NAME, sizeof(str), str, &ret_size);
227 sprintf(msg, "%s (platform = %d, device = %d)", str, platform, device);
228
229 // context
230 context = clCreateContext(NULL, 1, &device_id[device], NULL, NULL, &ret);
231
232 // command queue
233 Queue = clCreateCommandQueue(context, device_id[device], 0, &ret);
234
235 // source
236 if ((fp = fopen("vadd.cl", "r")) == NULL) {
237 sprintf(msg, "kernel source open error");
238 return 1;
239 }
240 source_size = fread(source_str, 1, MAX_SOURCE_SIZE, fp);
241 fclose(fp);
242
243 // program
244 program = clCreateProgramWithSource(context, 1, (const char **)&source_str, (const size_t *)&source_size, &ret);
245 if (ret != CL_SUCCESS) {
246 sprintf(msg, "clCreateProgramWithSource() error");
247 return 1;
248 }
249
250 // build
251 if (clBuildProgram(program, 1, &device_id[device], NULL, NULL, NULL) != CL_SUCCESS) {
252 sprintf(msg, "clBuildProgram() error");
253 exit(1);
254 }
255
256 // kernel
257 k_vadd = clCreateKernel(program, "vadd", &ret);
258 if (ret != CL_SUCCESS) {
259 sprintf(msg, "clCreateKernel() error");
260 return 1;
261 }
262
263 // memory object
264 size = N * sizeof(float);
265 d_A = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
266 d_B = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
267 d_C = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
268
269 // release
270 clReleaseProgram(program);
271 clReleaseContext(context);
272
273 // free
274 free(source_str);
275
276 return 0;
277 }
278
279 // entry point
280 static void vadd_calc(void)
281 {
282 if (OCL) {
283 size_t global_item_size, local_item_size;
284
285 // args
286 clSetKernelArg(k_vadd, 0, sizeof(cl_mem), (void *)&d_A);
287 clSetKernelArg(k_vadd, 1, sizeof(cl_mem), (void *)&d_B);
288 clSetKernelArg(k_vadd, 2, sizeof(cl_mem), (void *)&d_C);
289 clSetKernelArg(k_vadd, 3, sizeof(int), (void *)&N);
290
291 // work item
292 local_item_size = 256;
293 global_item_size = ((N + local_item_size - 1) / local_item_size)
294 * local_item_size;
295
296 // run
297 clEnqueueNDRangeKernel(Queue, k_vadd, 1, NULL, &global_item_size, &local_item_size, 0, NULL, NULL);
298 }
299 else {
300 // non-OpenCL code
301 vadd();
302 }
303 }
304
305 // non-OpenCL code
306 static void vadd(void)
307 {
308 for (int i = 0; i < N; i++) {
309 h_C[i] = h_A[i] + h_B[i];
310 }
311 }
ソースコードの説明
77行目:全体の問題サイズnから各プロセスの問題サイズNを求めます。
80-83行目:各プロセスは部分配列を持ちます。
これによってプロセス数を増やせばそれに比例して扱える問題サイズが大きくなります。
87行目:各プロセスのランクと問題サイズから全体のインデックスを求めます。
コンパイル・リンク方法
コンパイル・リンク方法は以下の通りです。
> cl /Ox /DMPI vadd_ocl_mpi.c OpenCL.lib msmpi.lib
プログラムの実行方法
プログラムの実行方法は以下の通りです。
1台のコンピュータに複数のグラフィックスボードがあるとき:
> mpiexec -n GPU数 vadd_ocl_mpi 配列の大きさ 繰り返し回数 platform番号 device番号 ...
"platform番号 device番号"はGPUの数だけ必要です。
例えば以下のようになります。
> mpiexec -n 2 vadd_ocl_mpi 100000000 1000 0 0 0 1 (2GPUで計算するとき)
複数台のコンピュータを使用するとき:
> mpiexec -hosts ホスト数 ホスト名 プロセス数 ... vadd_ocl_mpi 配列の大きさ 繰り返し回数 platform番号 device番号 ...
"ホスト名 プロセス数"はホストの数だけ必要であり、
"platform番号 device番号"は全プロセスの数だけ必要です。
例えば以下のようになります。
> mpiexec -hosts 2 localhost 1 PC2 1 vadd_ocl_mpi 100000000 1000 0 0 0 0 (2台で計算するとき)
繰り返し回数は計算時間の測定誤差を小さくするためです。
platform番号とdevice番号は環境によって異なるので、
メッセージを見て適当に設定してください。
mpiexecの使い方と実行環境の設定については7.5を参考にしてください。
表12-1にベクトル和の計算時間を示します。
配列の大きさ(=N)と繰り返し回数(=L)の積は一定(=1011)です。
従って全体の演算量は同じです。
本環境は1GPUで1~2個のプロセスを起動しているので、
1プロセスと2プロセスの計算時間が変わらなければ、
2GPUが実装された環境では1GPUの2倍速くなることが予想できます。
No.1-3では2プロセスの計算時間は1プロセスより少し大きいので、
2GPUでは2倍弱速くなると予想できます。
No.4の2プロセスで計算時間が大幅に増えている理由は不明です。
表12-1の2プロセスの計算時間は表11-1の2プロセスとほぼ同じです。
すなわちCUDA+MPIとOpenCL+MPIの性能はほぼ同じと言えます。
| No. | 配列の大きさN | 繰り返し回数L | 1プロセス | 2プロセス |
|---|---|---|---|---|
| 1 | 100,000,000 | 1,000 | 5.22秒 | 6.99秒 |
| 2 | 10,000,000 | 10,000 | 5.02秒 | 6.85秒 |
| 3 | 1,000,000 | 100,000 | 5.21秒 | 6.71秒 |
| 4 | 100,000 | 1,000,000 | 8.97秒 | 20.37秒 |