OpenCLメモ (その4) - 転送(コピー)の排除 (USE_HOST_PTR)
CPUメモリとOpenCLバッファ(GPUメモリ)は通常別のアドレス空間であり、転送が必要である。NVIDIA等のdGPUなら、物理的にも別のメモリであるから、転送が必要なのは、まあ、当たり前だろう。
しかし、この転送というのはくせ者で、たいてい結構重い。計算より転送が重いなんていうのは、ありがちな話だ。例えば、以下の例でもピンクの部分が実際の計算だが、その前後の緑/青色の転送のほうが時間がかかってしまっている。
IntelのGPUでOpenCLを使用する場合、そのメモリはCPUと物理的には同一メモリ上にある。そのため、OpenCL 1.2以降で使用可能なUSE_HOSRT_PTRという仕組みを使って、転送を不要にすることができる。これをIntelではZeroCopyと呼んでいる。
参考資料
iSUS - OpenCL* 1.2 の活用: インテル® プロセッサー・グラフィックスでバッファーコピーを最小限に抑えてパフォーマンスを向上する方法
Intel Developer Zone - Getting the Most from OpenCL™ 1.2: How to Increase Performance by Minimizing Buffer Copies on Intel® Processor Graphics
今回はこれを試してみる。
USE_HOST_PTRを使用する場合の手順はこんな感じ。
まず、CPU側のメモリ確保で2つの点で注意する。
・4096バイトでアラインされている
・バッファのサイズは64バイトの倍数
これは、_aligned_malloc()を使ってアライン済みアドレスを取得することで解決できる。
そして、このポインタを使って、OpenCLバッファを作成する。
このように作成した領域について、ホスト(CPU)側から操作する場合には、clEnqueueMapBuffer()を呼び出し、操作し終わったら、clEnqueueUnmapMemObject()を呼び出す必要がある。(これでCPU-GPU間の同期がとられる)
Map / Unmapが少し面倒であるが、このようにすることで転送が完全に省略される。clEnqueueUnmapMemObject()での転送時間が1usととても短くなり、事実上転送は行われていないことがわかる。
これに対し、アライメントが取れていないアドレスをclCreateBufferに渡すと、clEnqueueUnmapMemObject()で転送が起こってしまい、1.3ms前後かかってしまっている。
ZeroCopyを使用するには、今回のように、自分でCPU側のメモリを_aligned_malloc()で確保し、そのアドレスからUSE_HOST_PTR付きのclCreateBuffer()でOpenCLバッファを作る方法のほかに、ALLOC_HOST_PTR付きのclCreateBuffer()でOpenCLバッファを作る方法があり、この場合はOpenCL側でメモリを確保してくれる。
こちらのほうが使いやすい場合もあるかもしれない。
今回のちゃんと動くコードはこちら。
続き>>
OpenCLメモリスト
OpenCLメモ (その1) - かんたんな計算
OpenCLメモ (その2) - Intel GPUの構造
OpenCLメモ (その3) - work sizeの調整
OpenCLメモ (その4) - 転送(コピー)の排除 (USE_HOST_PTR) (いまここ)
OpenCLメモ (その5) - 転送(コピー)の排除 (SVM : OpenCL 2.0)
OpenCLメモ (その6) - imageの使用
OpenCLメモ (その7) [終] - reductionとshared local memory(SLM)の使用
しかし、この転送というのはくせ者で、たいてい結構重い。計算より転送が重いなんていうのは、ありがちな話だ。例えば、以下の例でもピンクの部分が実際の計算だが、その前後の緑/青色の転送のほうが時間がかかってしまっている。
IntelのGPUでOpenCLを使用する場合、そのメモリはCPUと物理的には同一メモリ上にある。そのため、OpenCL 1.2以降で使用可能なUSE_HOSRT_PTRという仕組みを使って、転送を不要にすることができる。これをIntelではZeroCopyと呼んでいる。
参考資料
iSUS - OpenCL* 1.2 の活用: インテル® プロセッサー・グラフィックスでバッファーコピーを最小限に抑えてパフォーマンスを向上する方法
Intel Developer Zone - Getting the Most from OpenCL™ 1.2: How to Increase Performance by Minimizing Buffer Copies on Intel® Processor Graphics
今回はこれを試してみる。
USE_HOST_PTRを使用する場合の手順はこんな感じ。
まず、CPU側のメモリ確保で2つの点で注意する。
・4096バイトでアラインされている
・バッファのサイズは64バイトの倍数
これは、_aligned_malloc()を使ってアライン済みアドレスを取得することで解決できる。
cl_uint optimizedSize
= ceil_int(sizeof(cl_int) * arrayWidth * arrayHeight, 64);
cl_int* inputA = (cl_int*)_aligned_malloc(optimizedSize, 4096);
cl_int* inputB = (cl_int*)_aligned_malloc(optimizedSize, 4096);
cl_int* outputC = (cl_int*)_aligned_malloc(optimizedSize, 4096);
そして、このポインタを使って、OpenCLバッファを作成する。
int CreateBufferArguments(
ocl_args_d_t *ocl,
cl_int* inputA, //CPU側の入力配列へのポインタ
cl_int* inputB, //CPU側の入力配列へのポインタ
cl_int* outputC, //CPU側の出力配列へのポインタ
cl_uint width, cl_uint height //配列のサイズ
) {
cl_int err = CL_SUCCESS;
cl_uint optimizedSize
= ceil_int(sizeof(cl_int) * arrayWidth * arrayHeight, 64);
ocl->srcA = clCreateBuffer(ocl->context,
//CL_MEM_USE_HOST_PTRを追加
CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,
optimizedSize, //配列のサイズ
inputA, //対応するCPUメモリへのポインタ
&err); //エラー情報を受け取る
ocl->srcB = clCreateBuffer(ocl->context,
//CL_MEM_USE_HOST_PTRを追加
CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,
optimizedSize, //配列のサイズ
inputB, //対応するCPUメモリへのポインタ
&err); //エラー情報を受け取る
ocl->dstMem = clCreateBuffer(ocl->context,
//CL_MEM_USE_HOST_PTRを追加
CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR,
optimizedSize, //配列のサイズ
outputC, //対応するCPUメモリへのポインタ
&err); //エラー情報を受け取る
return err;
}
このように作成した領域について、ホスト(CPU)側から操作する場合には、clEnqueueMapBuffer()を呼び出し、操作し終わったら、clEnqueueUnmapMemObject()を呼び出す必要がある。(これでCPU-GPU間の同期がとられる)
//対応するホスト側のポインタを取得する
cl_int *ptrMappedA = (cl_int *)clEnqueueMapBuffer(
ocl.commandQueue, //投入キュー
ocl.srcA, //対象のOpenCLバッファ
CL_FALSE, //終了までブロックするか -> しない
CL_MAP_WRITE, //CPUが書き込むためにMapする
//(読み込みならCL_MAP_READ)
//(両方ならCL_MAP_READ | CL_MAP_WRITE)
0, //オフセット
sizeof(cl_uint) * arrayWidth * arrayHeight, //マップするサイズ
0, //この関数が待機すべきeventの数
NULL, //この関数が待機すべき関数のリストへのポインタ
NULL, //この関数の返すevent
&err);
//終了を待機
err = clFinish(ocl.commandQueue);
//ホスト(CPU)から操作
//(...略...)
err = clEnqueueUnmapMemObject(
ocl.commandQueue, //投入キュー
ocl.srcA, //対象のOpenCLバッファ
ptrMappedA, //取得したホスト側のポインタ
0, //この関数が待機すべきeventの数
NULL, //この関数が待機すべき関数のリストへのポインタ
NULL); //この関数の返すevent
//終了を待機
err = clFinish(ocl.commandQueue);
//デバイス(GPU) kernelを実行
//(...略...)
Map / Unmapが少し面倒であるが、このようにすることで転送が完全に省略される。clEnqueueUnmapMemObject()での転送時間が1usととても短くなり、事実上転送は行われていないことがわかる。
これに対し、アライメントが取れていないアドレスをclCreateBufferに渡すと、clEnqueueUnmapMemObject()で転送が起こってしまい、1.3ms前後かかってしまっている。
ZeroCopyを使用するには、今回のように、自分でCPU側のメモリを_aligned_malloc()で確保し、そのアドレスからUSE_HOST_PTR付きのclCreateBuffer()でOpenCLバッファを作る方法のほかに、ALLOC_HOST_PTR付きのclCreateBuffer()でOpenCLバッファを作る方法があり、この場合はOpenCL側でメモリを確保してくれる。
ocl->srcA = clCreateBuffer(ocl->context,
//CL_MEM_ALLOC_HOST_PTRを追加
CL_MEM_READ_ONLY | CL_MEM_ALLOC_HOST_PTR,
optimizedSize, //配列のサイズ
inputA, //OpenCL側で確保してくれるので不要
&err); //エラー情報を受け取る
こちらのほうが使いやすい場合もあるかもしれない。
今回のちゃんと動くコードはこちら。
続き>>
OpenCLメモリスト
OpenCLメモ (その1) - かんたんな計算
OpenCLメモ (その2) - Intel GPUの構造
OpenCLメモ (その3) - work sizeの調整
OpenCLメモ (その4) - 転送(コピー)の排除 (USE_HOST_PTR) (いまここ)
OpenCLメモ (その5) - 転送(コピー)の排除 (SVM : OpenCL 2.0)
OpenCLメモ (その6) - imageの使用
OpenCLメモ (その7) [終] - reductionとshared local memory(SLM)の使用