WebCLプログラミング入門


# はじめに

CUDA & OpenCL Advent Calendar 2014の5日目です。
前回、[WebCLのデモを動かす](http://qiita.com/_likr/items/bf135a7d0f523ecf6944)でWebCLの環境を導入して動作確認をしましたので、今回はWebCLのプログラムを少し書いてみたいと思います。
Local Memoryの使用など程よくテクニカルな行列積をお題にしようと思います。
なお、OpenCLの知識は前提とさせてもらいます。

# PlatformとDeviceの取得

WebCLでは通常のOpenCLと同様に、動作させている環境で使用可能な全てのOpenCL Deviceを選択して使用することができます。
複数のOpenCL Platformがインストールされている場合ももちろん全て使用することができます。

早速、PlatformとDeviceを取得するコードを書いてみます。

```javascript
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です。

```javascript
var context;
context = webcl.createContext(); // default context
context = webcl.createContext(device);
```

`webcl.createContext(device)`を呼び出すだけです。
引数の`device`を与えると、デフォルトのDeviceが選択されます。

# Kernelの取得

WebCLでのDevice側のプログラムは、OpenCLと同じくOpenCL C言語がそのまま使われます。
行列積を実装するにあたって、カーネル関数は[NVIDIAのサンプル](https://developer.nvidia.com/opencl#oclMatrixMul)からそのまま持ってきてみましょう。
HTMLまたはJSのソース中に埋め込んでもいいですし、AJAXで取得するようにしても構いません。

カーネル関数のソースコードを文字列として`kernelSource`に取得できたら、以下のようにしてKernelを取得します。

```javascript
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の作成

デバイス側のメモリを用意します。

```javascript
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を使います。

```javascript
var A = new Float32Array(ASize);
var B = new Float32Array(BSize);
var C = new Float32Array(CSize);
```

# CommandQueue

CommandQueueを取得します。

```javascript
var queue = context.createCommandQueue(device);
```

ホスト・デバイス間のメモリのやり取りは以下の様な感じになります。

```javascript
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を使います。

そしていよいよ呼び出しです。

```javascript
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`を選ぶとこれになります。

```javascript
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;
    }
  }
}
```

申し訳程度に実行結果のスクショを載せておきます。

![webcl.png](https://qiita-image-store.s3.amazonaws.com/0/4249/f8099896-642c-393b-77ba-697c01b0fb7a.png "webcl.png")

NVIDIAとIntelのOpenCL Platformがインストールされています。
WebCLはメモリ転送も含めて時間を測っているので、サイズが小さいとJS実装よりも遅くなっているんですが、十分に大きいサイズになると数十倍程度の差がでてきます。


# おわりに

今回はようやくまともにWebCLプログラムを書いて実行しましたが、十分に大きいデータの処理ではCPU、GPUともにWebCLで性能的なメリットが有ることが確認できました。
また、OpenCLのカーネル関数が修正もなくそのままWebCLで動かせることも確認できました。
WebCLのAPIはOpenCLと若干名前が違ったりしていますが、OpenCLの経験がある人にとってはそれ程苦労なくWebCLプログラムを書くことができるように思います。
もう一回WebCLについて書く予定ですが、WebGL Resource Sharingあたりを触れたいと思います。

Advent Calendarの次の担当は@duxcaさんです。