はじめに
CUDA & OpenCL Advent Calendar 2014の5日目です。
前回、WebCLのデモを動かすでWebCLの環境を導入して動作確認をしましたので、今回はWebCLのプログラムを少し書いてみたいと思います。
Local Memoryの使用など程よくテクニカルな行列積をお題にしようと思います。
なお、OpenCLの知識は前提とさせてもらいます。
PlatformとDeviceの取得
WebCLでは通常のOpenCLと同様に、動作させている環境で使用可能な全てのOpenCL Deviceを選択して使用することができます。
複数のOpenCL Platformがインストールされている場合ももちろん全て使用することができます。
早速、PlatformとDeviceを取得するコードを書いてみます。
function getDevices() {
var result = [];
webcl.getPlatforms().forEach(function(platform) {
platform.getDevices().forEach(function(device) {
result.push([platform, device]);
});
});
return result;
}
WebCL対応の環境であれば、webcl
というオブジェクトがグローバルにアクセスできるようになります。
webcl.getPlatforms()
でプラットフォームの配列を取得できます。
取得したplatform
からplatform.getDevices()
でデバイスの配列を取得します。
上のコードでは、[platform, device]
というペアの配列を作成しています。
Contextの取得
お次はContextです。
var context;
context = webcl.createContext(); // default context
context = webcl.createContext(device);
webcl.createContext(device)
を呼び出すだけです。
引数のdevice
を与えると、デフォルトのDeviceが選択されます。
Kernelの取得
WebCLでのDevice側のプログラムは、OpenCLと同じくOpenCL C言語がそのまま使われます。
行列積を実装するにあたって、カーネル関数はNVIDIAのサンプルからそのまま持ってきてみましょう。
HTMLまたはJSのソース中に埋め込んでもいいですし、AJAXで取得するようにしても構いません。
カーネル関数のソースコードを文字列としてkernelSource
に取得できたら、以下のようにしてKernelを取得します。
var program = context.createProgram(kernelSource);
program.build([device], '-D BLOCK_SIZE=16');
var kernel = program.createKernel('matrixMul');
context.createProgram(kernelSource)
でProgramを取得し、program.build(devices, options)
でビルドします。
今回持ってきたカーネル関数ではBLOCK_SIZE
という定数が必要になっていますので、options
に渡すことで対応しています。
あとは、必要なKernelをprogram.createKernel(kernelName)
で取得します。
ちなみに、OpenCLでのclCreateProgramWithBinary
みたいなものは無いようです。
Bufferの作成
デバイス側のメモリを用意します。
var devA = context.createBuffer(WebCL.MEM_READ_ONLY, bufASize);
var devB = context.createBuffer(WebCL.MEM_READ_ONLY, bufBSize);
var devC = context.createBuffer(WebCL.MEM_WRITE_ONLY, bufCSize);
context.createBuffer
でサクッと用意します。
bufSize
はバイト数なので、配列の要素数に型のバイト数をかけておく必要があります。
OpenCLの定数関係はWebCL
オブジェクトの中にあります。
また、ホスト側はJavaScriptのTyped Arrayを使います。
var A = new Float32Array(ASize);
var B = new Float32Array(BSize);
var C = new Float32Array(CSize);
CommandQueue
CommandQueueを取得します。
var queue = context.createCommandQueue(device);
ホスト・デバイス間のメモリのやり取りは以下の様な感じになります。
queue.enqueueWriteBuffer(devA, false, 0, bufASize, A);
queue.enqueueWriteBuffer(devB, false, 0, bufBSize, B);
Kernel呼び出し
ようやく準備が整ってきたのでKernelを呼び出します。
まずは、引数をセットします。
kernel.setArg(0, devC);
kernel.setArg(1, devA);
kernel.setArg(2, devB);
kernel.setArg(3, new Uint32Array([floatSize * blockSize * blockSize]));
kernel.setArg(4, new Uint32Array([floatSize * blockSize * blockSize]));
kernel.setArg(5, new Uint32Array([wa]));
kernel.setArg(6, new Uint32Array([hb]));
kernel.setArg(7, new Uint32Array([ha]));
このカーネル関数では、1〜3番目の引数がGlobal Memory、4、5番目がLocal Memory、6〜8番目をPrivate Memoryとして受け取ります。
1〜3番目にはBufferをそのまま渡してやります。
4、5番目では、要素数1のUint32ArrayにLocal Memoryのサイズを入れてやることでそのサイズのLocal Memoryが確保されます。
6〜8番目でも、スカラー値の受け渡しに要素数1のTyped Arrayを使います。
そしていよいよ呼び出しです。
queue.enqueueNDRangeKernel(kernel, 2, null, [wa, hb], [blockSize, blockSize]);
queue.finish();
1番目の引数がKernel、2番目の引数がワークアイテムの次元数、4番目と5番目の引数でそれぞれグローバルとローカルのワークアイテム数を指定します。
デモ
以上のコードを整理してゴチャゴチャ付け足して動かせるようにしました。
http://likr.github.io/webcl20141205/
Block Size * Scale
次の正方行列の積を計算します。
Repeat
回計算した平均時間を表示します。
比較のために、適当に実装したJSでの行列積も置いておきます。
OpenCL Device
でJS
を選ぶとこれになります。
function matMulJS(A, B, C, wa, ha, hb) {
var i, j, k;
for (i = 0; i < wa; ++i) {
for (j = 0; j < hb; ++j) {
var val = 0;
var iA = wa * i;
var iB = j;
for (k = 0; k < ha; ++k) {
val += A[iA] * B[iB];
iA += 1;
iB += ha;
}
C[i * wa + j] = val;
}
}
}
申し訳程度に実行結果のスクショを載せておきます。
NVIDIAとIntelのOpenCL Platformがインストールされています。
WebCLはメモリ転送も含めて時間を測っているので、サイズが小さいとJS実装よりも遅くなっているんですが、十分に大きいサイズになると数十倍程度の差がでてきます。
おわりに
今回はようやくまともにWebCLプログラムを書いて実行しましたが、十分に大きいデータの処理ではCPU、GPUともにWebCLで性能的なメリットが有ることが確認できました。
また、OpenCLのカーネル関数が修正もなくそのままWebCLで動かせることも確認できました。
WebCLのAPIはOpenCLと若干名前が違ったりしていますが、OpenCLの経験がある人にとってはそれ程苦労なくWebCLプログラムを書くことができるように思います。
もう一回WebCLについて書く予定ですが、WebGL Resource Sharingあたりを触れたいと思います。
Advent Calendarの次の担当は@duxcaさんです。