NVIDIAまたはAMDのグラフィックスボード(GPU)が搭載されたコンピュータでは、
その高い演算能力を汎用的な科学技術計算に用いることができます。
そのためのプログラミング言語をOpenCL(Open Computing Language)と呼びます。
CPUがフロントエンドとして動作し、計算の主要部はGPUが行います。
OpenCLプログラムはCPUやCPU内蔵GPUでも動作しますが性能上の利点は少なく主に外付GPUで使用します。
OpenCLプログラム
OpenCLプログラムは通常のC/C++で記述されるホストプログラムと、
OpenCL C言語で記述されるカーネルプログラム(拡張子.cl)から成ります。
ホストプログラムはCPUで実行され、カーネルプログラムはGPUで実行されます。
両者の関係はCUDAと同じです。
OpenCL C言語
カーネルを記述するOpenCL C言語のC/C++との違いは以下の通りです。
オンラインコンパイルとオフラインコンパイル
OpenCLプログラムにはオンラインコンパイルとオフラインコンパイルの2種類のモードがあります。
それぞれの意味は以下の通りです。
OpenCLプログラミングの指針
OpenCLプログラミングでは以下の点が最も重要です。これはCUDAと同じです。
(1)並列計算できるアルゴリズムを採用する。
(2)CPU/GPU間のデータ転送を最小限にする。
(3)カーネルコードではメモリアクセスをスレッド順とする。
これらを満たさないときは速度は数分の一以下になりGPUを使う意味がなくなります。
変数の命名ルール
CPUコードとGPUコードの両方から呼ばれる同じ意味をもつ変数については、
前者の頭には何もつけないか"h_"(host memoryの意味)を付け、
後者の頭に"d_"(device memoryの意味)を付ける方法がよく用いられます。
このようにすればその変数がCPUにあるかGPUにあるか一目でわかります。
またカーネル変数の名前はカーネル関数の名前と1対1に対応させるとわかりやすくなります。
データ並列とタスク並列
並列処理にはデータ並列とタスク並列の2種類がありますが、数値計算では通常前者を使用します。
OpenCLではそのための関数がclEnqueueNDRangeKernel関数です。
ワークアイテム
OpenCLではスレッドのことをワークアイテムと呼び、スレッドの集合をグループと呼びます。
ワークアイテムの大きさはclEnqueueNDRangeKernel関数の引数で指定します。
表9-1にOpenCLとCUDAの対応関係を示します。
カーネル関数でこれらを通して自分のスレッド番号を取得します。
OpenCLではNo.5,No.6も関数が用意されており便利です。
No. | OpenCL ワークアイテム | CUDA execution configuration |
---|---|---|
1 | get_local_id(0) get_local_id(1) get_local_id(2) | threadIdx.x threadIdx.y threadIdx.z |
2 | get_local_size(0) get_local_size(1) get_local_size(2) | blockDim.x blockDim.y blockDim.z |
3 | get_group_id(0) get_group_id(1) get_group_id(2) | blockIdx.x blockIdx.y blockIdx.z |
4 | get_num_groups(0) get_num_groups(1) get_num_groups(2) | gridDim.x gridDim.y gridDim.z |
5 | get_global_id(0) get_global_id(1) get_global_id(2) | threadIdx.x+blockIdx.x*blockDim.x threadIdx.y+blockIdx.y*blockDim.y threadIdx.z+blockIdx.z*blockDim.z |
6 | get_global_size(0) get_global_size(1) get_global_size(2) | blockDim.x*gridDim.x blockDim.y*gridDim.y blockDim.z*gridDim.z |
ホストプログラム
リスト9-1にベクトルの和をOpenCLで並列計算するプログラムを示します。
リスト9-1 OpenCLホストプログラム(ocl_vadd_v1.c)
1 /* 2 ocl_vadd_v1.c (OpenCL, version 1) 3 4 Compile + Link: 5 > cl.exe /O2 ocl_vadd_v1.c OpenCL.lib 6 7 Usage: 8 > ocl_vadd_v1 <n> <loop> <platform> <device> 9 */ 10 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <time.h> 14 #include <CL/cl.h> 15 16 #define MAX_PLATFORMS (10) 17 #define MAX_DEVICES (10) 18 #define MAX_SOURCE_SIZE (100000) 19 20 int main(int argc, char **argv) 21 { 22 // OpenCL 23 cl_context context = NULL; 24 cl_command_queue command_queue = NULL; 25 cl_program program = NULL; 26 cl_kernel kernel = NULL; 27 cl_platform_id platform_id[MAX_PLATFORMS]; 28 cl_device_id device_id[MAX_DEVICES]; 29 30 // memory object 31 cl_mem d_a = NULL; 32 cl_mem d_b = NULL; 33 cl_mem d_c = NULL; 34 35 FILE *fp; 36 char *source_str; 37 size_t source_size; 38 size_t global_item_size, local_item_size; 39 size_t ret_size; 40 cl_uint num_platforms; 41 cl_uint num_devices; 42 cl_int ret; 43 char str[BUFSIZ]; 44 45 cl_uint platform = 0; 46 cl_uint device = 0; 47 int nloop = 1000; 48 int n = 1000; 49 50 // arguments 51 if (argc >= 5) { 52 n = atoi(argv[1]); 53 nloop = atoi(argv[2]); 54 platform = atoi(argv[3]); 55 device = atoi(argv[4]); 56 } 57 58 // alloc 59 source_str = (char *)malloc(MAX_SOURCE_SIZE * sizeof(char)); 60 61 // setup host arrays 62 float *a = (float *)malloc(n * sizeof(float)); 63 float *b = (float *)malloc(n * sizeof(float)); 64 float *c = (float *)malloc(n * sizeof(float)); 65 for (int i = 0; i < n; i++) { 66 a[i] = (float)(1 + i); 67 b[i] = (float)(1 + i); 68 } 69 70 // platform 71 clGetPlatformIDs(MAX_PLATFORMS, platform_id, &num_platforms); 72 if (platform >= num_platforms) { 73 printf("error : platform = %d (limit = %d)\n", platform, num_platforms - 1); 74 exit(1); 75 } 76 77 // device 78 clGetDeviceIDs(platform_id[platform], CL_DEVICE_TYPE_ALL, MAX_DEVICES, device_id, &num_devices); 79 if (device >= num_devices) { 80 printf("error : device = %d (limit = %d)\n", device, num_devices - 1); 81 exit(1); 82 } 83 84 // device name (option) 85 clGetDeviceInfo(device_id[device], CL_DEVICE_NAME, sizeof(str), str, &ret_size); 86 printf("%s\n", str); 87 88 // context 89 context = clCreateContext(NULL, 1, &device_id[device], NULL, NULL, &ret); 90 91 // command queue 92 command_queue = clCreateCommandQueue(context, device_id[device], 0, &ret); 93 94 // source 95 if ((fp = fopen("vadd.cl", "r")) == NULL) { 96 fprintf(stderr, "kernel source open error\n"); 97 exit(1); 98 } 99 source_size = fread(source_str, 1, MAX_SOURCE_SIZE, fp); 100 fclose(fp); 101 102 // program 103 program = clCreateProgramWithSource(context, 1, (const char **)&source_str, (const size_t *)&source_size, &ret); 104 if (ret != CL_SUCCESS) { 105 fprintf(stderr, "clCreateProgramWithSource() error\n"); 106 exit(1); 107 } 108 109 // build 110 if (clBuildProgram(program, 1, &device_id[device], NULL, NULL, NULL) != CL_SUCCESS) { 111 fprintf(stderr, "clBuildProgram() error\n"); 112 exit(1); 113 } 114 115 // kernel 116 kernel = clCreateKernel(program, "vadd", &ret); 117 if (ret != CL_SUCCESS) { 118 fprintf(stderr, "clCreateKernel() error\n"); 119 exit(1); 120 } 121 122 // memory object 123 d_a = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret); 124 d_b = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret); 125 d_c = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret); 126 127 // host to device 128 clEnqueueWriteBuffer(command_queue, d_a, CL_TRUE, 0, n * sizeof(float), a, 0, NULL, NULL); 129 clEnqueueWriteBuffer(command_queue, d_b, CL_TRUE, 0, n * sizeof(float), b, 0, NULL, NULL); 130 131 // args 132 clSetKernelArg(kernel, 0, sizeof(cl_mem), (void *)&d_a); 133 clSetKernelArg(kernel, 1, sizeof(cl_mem), (void *)&d_b); 134 clSetKernelArg(kernel, 2, sizeof(cl_mem), (void *)&d_c); 135 clSetKernelArg(kernel, 3, sizeof(int), (void *)&n); 136 137 // timer 138 clock_t t0 = clock(); 139 140 // work item 141 local_item_size = 256; 142 global_item_size = ((n + local_item_size - 1) / local_item_size) * local_item_size; 143 144 // run 145 for (int loop = 0; loop < nloop; loop++) { 146 clEnqueueNDRangeKernel(command_queue, kernel, 1, NULL, &global_item_size, &local_item_size, 0, NULL, NULL); 147 } 148 149 // device to host 150 clEnqueueReadBuffer(command_queue, d_c, CL_TRUE, 0, n * sizeof(float), c, 0, NULL, NULL); 151 152 // timer 153 clock_t t1 = clock(); 154 double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC; 155 156 // output 157 double sum = 0; 158 for (int i = 0; i < n; i++) { 159 sum += c[i]; 160 } 161 double exact = n * (n + 1.0); 162 printf("n=%d nloop=%d %e(%.6e) cpu[sec]=%.3f\n", 163 n, nloop, sum, exact, cpu); 164 165 // release 166 clFlush(command_queue); 167 clFinish(command_queue); 168 clReleaseMemObject(d_a); 169 clReleaseMemObject(d_b); 170 clReleaseMemObject(d_c); 171 clReleaseKernel(kernel); 172 clReleaseProgram(program); 173 clReleaseCommandQueue(command_queue); 174 clReleaseContext(context); 175 176 // free 177 free(source_str); 178 free(a); 179 free(b); 180 free(c); 181 182 return 0; 183 }
ソースコードの説明
14行目:OpenCLプログラムにはこのinclude文が必須です。
70-120行目:OpenCLに必須の前処理です。OpenCLはいろいろな環境で動くためにこのような処理が必要になります。
コードが煩雑になりますが定型的な処理です。
platform→device→context→command queue→カーネルソースコード→program→build→kernelの順に処理します。
78行目:"CL_DEVICE_TYPE_ALL"を"CL_DEVICE_TYPE_GPU"に変えるとGPUだけが対象デバイスになります。
123-125行目:GPUで使用する配列を確保します。(CUDAのcudaMallocに対応)
128-129行目:ホストからデバイスにメモリーを転送します。(CUDAのcudaMemcpy(...,cudaMemcpyHostToDevice)に対応)
132-135行目:カーネルの引数を代入します。(CUDAにない機能)
141-142行目:ワークアイテム(スレッドの構成)を指定します。global_item_sizeはlocal_item_sizeの整数倍であることが必要です。(CUDAのExecution configurationに対応)
150行目:デバイスからホストにメモリーを転送します。(CUDAのcudaMemcpy(...,cudaMemcpyDeviceToHost)に対応)
168-170行目:GPUで使用した配列を解放します。(CUDAのcudaFreeに対応)
カーネルプログラム
リスト9-2にカーネルプログラムを示します。
オンラインコンパイルではカーネルプログラムを実行プログラムと同じフォルダに置きます。
リスト9-2 OpenCLカーネルプログラム(vadd.cl)
1 __kernel void vadd(global const float *a, global const float *b, global float *c, int n) 2 { 3 int i = get_global_id(0); 4 5 if (i < n) { 6 c[i] = a[i] + b[i]; 7 } 8 }
ソースコードの説明
3行目:globalなスレッド番号を取得します。それがこの場合配列のインデックスになります。
5行目:配列の大きさはスレッド数の倍数とは限らないのでこの条件判定が必要です。
プログラミングの改良
上で示したプログラムはOpenCLを用いた最小のプログラムですが、
大規模なアプリケーションを開発するための出発点としては不適切です。
すわなちOpenCL固有の処理とその他の処理が分離されておらず、
また計算アルゴリズムを検証するための逐次計算が実装されていないために開発効率が悪くなります。
その点を考慮したプログラムをリスト9-3に示します。
なお、カーネルプログラムはリスト9-2と共通です。
リスト9-3 OpenCLホストプログラム(ocl_vadd_v2.c)
1 /* 2 ocl_vadd_v2.c (OpenCL, version 2) 3 4 Compile + Link: 5 > cl.exe /O2 ocl_vadd_v2.c OpenCL.lib 6 7 Usage: 8 > ocl_vadd_v2 <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 #define MAX_PLATFORMS (10) 23 #define MAX_DEVICES (10) 24 #define MAX_SOURCE_SIZE (100000) 25 26 // prototypes 27 static int setup_ocl(cl_uint, cl_uint, char *); 28 static void vadd_calc(void); 29 static void vadd(void); 30 31 // globals 32 cl_command_queue Queue; 33 cl_kernel k_vadd; 34 int N; 35 float *A, *B, *C; 36 cl_mem d_A, d_B, d_C; 37 int OCL; 38 39 int main(int argc, char **argv) 40 { 41 int platform = 0; 42 int device = 0; 43 int nloop = 1000; 44 45 // arguments 46 N = 1000; 47 if (argc >= 5) { 48 N = atoi(argv[1]); 49 nloop = atoi(argv[2]); 50 platform = atoi(argv[3]); 51 device = atoi(argv[4]); 52 } 53 OCL = (platform >= 0); 54 55 // alloc host arrays 56 size_t size = N * sizeof(float); 57 A = (float *)malloc(size); 58 B = (float *)malloc(size); 59 C = (float *)malloc(size); 60 61 // setup problem 62 for (int i = 0; i < N; i++) { 63 A[i] = (float)(1 + i); 64 B[i] = (float)(1 + i); 65 } 66 67 // setup OpenCL 68 if (OCL) { 69 char msg[BUFSIZ]; 70 int ret = setup_ocl((cl_uint)platform, (cl_uint)device, msg); 71 printf("%s\n", msg); 72 if (ret) { 73 exit(1); 74 } 75 } 76 77 // timer 78 clock_t t0 = clock(); 79 80 // copy host to device 81 if (OCL) { 82 clEnqueueWriteBuffer(Queue, d_A, CL_TRUE, 0, size, A, 0, NULL, NULL); 83 clEnqueueWriteBuffer(Queue, d_B, CL_TRUE, 0, size, B, 0, NULL, NULL); 84 } 85 86 // run 87 for (int loop = 0; loop < nloop; loop++) { 88 vadd_calc(); 89 } 90 91 // copy device to host 92 if (OCL) { 93 clEnqueueReadBuffer(Queue, d_C, CL_TRUE, 0, size, C, 0, NULL, NULL); 94 } 95 96 // timer 97 clock_t t1 = clock(); 98 double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC; 99 100 // sum 101 double sum = 0; 102 for (int i = 0; i < N; i++) { 103 sum += C[i]; 104 } 105 106 // output 107 double exact = N * (N + 1.0); 108 printf("N=%d L=%d %.6e(%.6e) %.1e %.3f[sec]\n", 109 N, nloop, sum, exact, fabs((sum - exact) / exact), cpu); 110 111 // release 112 if (OCL) { 113 clReleaseMemObject(d_A); 114 clReleaseMemObject(d_B); 115 clReleaseMemObject(d_C); 116 clReleaseKernel(k_vadd); 117 clReleaseCommandQueue(Queue); 118 } 119 120 // free 121 free(A); 122 free(B); 123 free(C); 124 125 return 0; 126 } 127 128 // setup OpenCL 129 static int setup_ocl(cl_uint platform, cl_uint device, char *msg) 130 { 131 cl_context context = NULL; 132 cl_program program = NULL; 133 cl_platform_id platform_id[MAX_PLATFORMS]; 134 cl_device_id device_id[MAX_DEVICES]; 135 136 FILE *fp; 137 char *source_str; 138 char str[BUFSIZ]; 139 size_t source_size, ret_size, size; 140 cl_uint num_platforms, num_devices; 141 cl_int ret; 142 143 // alloc 144 source_str = (char *)malloc(MAX_SOURCE_SIZE * sizeof(char)); 145 146 // platform 147 clGetPlatformIDs(MAX_PLATFORMS, platform_id, &num_platforms); 148 if (platform >= num_platforms) { 149 sprintf(msg, "error : platform = %d (limit = %d)", platform, num_platforms - 1); 150 return 1; 151 } 152 153 // device 154 clGetDeviceIDs(platform_id[platform], CL_DEVICE_TYPE_ALL, MAX_DEVICES, device_id, &num_devices); 155 if (device >= num_devices) { 156 sprintf(msg, "error : device = %d (limit = %d)", device, num_devices - 1); 157 return 1; 158 } 159 160 // device name (option) 161 clGetDeviceInfo(device_id[device], CL_DEVICE_NAME, sizeof(str), str, &ret_size); 162 sprintf(msg, "%s (platform = %d, device = %d)", str, platform, device); 163 164 // context 165 context = clCreateContext(NULL, 1, &device_id[device], NULL, NULL, &ret); 166 167 // command queue 168 Queue = clCreateCommandQueue(context, device_id[device], 0, &ret); 169 170 // source 171 if ((fp = fopen("vadd.cl", "r")) == NULL) { 172 sprintf(msg, "kernel source open error"); 173 return 1; 174 } 175 source_size = fread(source_str, 1, MAX_SOURCE_SIZE, fp); 176 fclose(fp); 177 178 // program 179 program = clCreateProgramWithSource(context, 1, (const char **)&source_str, (const size_t *)&source_size, &ret); 180 if (ret != CL_SUCCESS) { 181 sprintf(msg, "clCreateProgramWithSource() error"); 182 return 1; 183 } 184 185 // build 186 if (clBuildProgram(program, 1, &device_id[device], NULL, NULL, NULL) != CL_SUCCESS) { 187 sprintf(msg, "clBuildProgram() error"); 188 return 1; 189 } 190 191 // kernel 192 k_vadd = clCreateKernel(program, "vadd", &ret); 193 if (ret != CL_SUCCESS) { 194 sprintf(msg, "clCreateKernel() error"); 195 return 1; 196 } 197 198 // memory object 199 size = N * sizeof(float); 200 d_A = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret); 201 d_B = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret); 202 d_C = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret); 203 204 // release 205 clReleaseProgram(program); 206 clReleaseContext(context); 207 208 // free 209 free(source_str); 210 211 return 0; 212 } 213 214 // entry point 215 static void vadd_calc(void) 216 { 217 if (OCL) { 218 size_t global_item_size, local_item_size; 219 220 // args 221 clSetKernelArg(k_vadd, 0, sizeof(cl_mem), (void *)&d_A); 222 clSetKernelArg(k_vadd, 1, sizeof(cl_mem), (void *)&d_B); 223 clSetKernelArg(k_vadd, 2, sizeof(cl_mem), (void *)&d_C); 224 clSetKernelArg(k_vadd, 3, sizeof(int), (void *)&N); 225 226 // work item 227 local_item_size = 256; 228 global_item_size = ((N + local_item_size - 1) / local_item_size) 229 * local_item_size; 230 231 // run 232 clEnqueueNDRangeKernel(Queue, k_vadd, 1, NULL, &global_item_size, &local_item_size, 0, NULL, NULL); 233 } 234 else { 235 // serial code 236 vadd(); 237 } 238 } 239 240 // serial code 241 static void vadd(void) 242 { 243 for (int i = 0; i < N; i++) { 244 C[i] = A[i] + B[i]; 245 } 246 }
ソースコードの説明
32-37行目:複数の関数から呼ばれる変数(プログラムの核をなす変数)をグローバル変数にします。
50行目:3番目の引数に負の値を代入するとOpenCLを使用しない逐次計算プログラムになります。
68-75行目:OpenCL固有の前処理は一か所に集めます。
215行目:この関数が計算の入り口になります。この中で並列処理と逐次処理を場合分けしています。
221-224行目:引数の内容は計算の途中で変わることがありますのでカーネルを呼び出す直前に設定します。
227-229行目:ここではワークアイテムをカーネルを呼び出す直前に設定していますが、
これは計算の途中で変わることはありませんので外部で設定しても構いません。
241-246行目:逐次コードを先に作成して十分テストし、計算式が正しく実装されていることを確認します。
逐次版と並列版の開発
プログラムの最終目的が並列計算であってもいきなり並列計算プログラムを実装してもうまく動くことはほとんどありません。
最初に並列化を想定したプログラム構造の逐次版を作成し、
計算式が正しく実装されていることを十分確認してから並列版を実装することが開発の近道です。
並列版固有の処理は以下の通りです。
これらはすべて定型的な作業なので注意深く実装すれば間違いは少なくなります。
コンパイル・リンク方法
コンパイル・リンク方法は以下の通りです(VC++の場合)。
> cl.exe /O2 ocl_vadd_v2.c OpenCL.lib
warningが出たときは以下のようにしてください。
> cl.exe /O2 /wd4996 /wd4201 ocl_vadd_v2.c OpenCL.lib
ここで、OpenCL.libはCUDAをインストールしたときにCUDAと同じ場所に保存されています。
プログラムの実行方法
プログラムの実行方法は以下の通りです。
> ocl_vadd_v2.exe 配列の大きさ 繰り返し回数 プラットフォーム番号 デバイス番号
例えば以下のようになります。
> ocl_vadd_v2.exe 1000000 1000 1 0 (プラットフォーム番号=1, デバイス番号=0のとき)
> ocl_vadd_v2.exe 1000000 1000 -1 0 (OpenCLを用いないで逐次計算するとき)
繰り返し回数は計算時間の測定誤差を小さくするためです。
プラットフォーム番号とデバイス番号については表9-3を参考にしてください。
プログラム実行時に C:\Windows\System32\OpenCL.dll を使用しています。
OpenCLプログラムはCPU/CPU内蔵GPU/外付GPUで計算することができます。
(以下では、CPU内蔵GPUをiGPU、外付GPUをdGPUと呼びます)[21]
計算するハードウェアはOpenCLのプラットフォーム番号とデバイス番号によって選択することができます。
表9-2にベクトル和の計算時間を示します。
配列の大きさ(=N)と繰り返し回数(=L)の積は一定(=1010)です。
従って全体の演算量は同じです。
dGPUではNo.1-2のとき計算時間が大幅に短縮されますが、
No.3-4でカーネル起動回数が増えるために計算時間が増えます。
これはCUDAと同じです。
リスト9-3よりカーネル起動回数は繰り返し回数と同じです。
繰り返し回数が1,000,000回のときdGPUで約7秒余分に時間がかかっています。
これからdGPUのカーネル起動のオーバーヘッドは約7μsecと評価することができます。
(通常のアプリケーションではカーネルをこのように多数回呼ぶことは少ないです)
iGPUはCPU1コアより速いですが、
CPUは通常マルチコアで並列計算するのでiGPUでOpenCLプログラムを使用する理由は特にありません。
(iGPUはメモリー容量も小さい)
表9-2の外付GPUの計算時間は表8-3のGPUとほぼ同じです。
すなわちCUDAとOpenCLの性能はほぼ同じと言えます。
No. | 配列の大きさN | 繰り返し回数L | CPU1コア | CPU内蔵GPU (iGPU) | 外付GPU (dGPU) |
---|---|---|---|---|---|
1 | 10,000,000 | 1,000 | 5.79秒 (1.0) | 2.85秒 (2.0) | 0.56秒 (10.3) |
2 | 1,000,000 | 10,000 | 7.80秒 (1.0) | 2.85秒 (2.7) | 0.53秒 (14.7) |
3 | 100,000 | 100,000 | 3.81秒 (1.0) | 2.62秒 (1.5) | 0.93秒 (4.1) |
4 | 10,000 | 1,000,000 | 3.78秒 (1.0) | 2.09秒 (1.8) | 8.03秒 (0.5) |
表9-3に本環境のプラットフォーム番号とデバイス番号を示します。
プラットフォーム番号とデバイス番号は環境によって変わるので、
いろいろ変えて実行し表示されるデバイス名から番号を決定してください。
外付GPUが2個以上あるときは、デバイス番号が順に0,1,...となります。
No. | ハードウェア | プラットフォーム番号 | デバイス番号 |
---|---|---|---|
0 | CPU | - | - |
1 | CPU内蔵GPU | 0 | 0 |
2 | 外付GPU | 1 | 0 |