WebGLでGPGPU

下のイラスト調のやつを爆速にしようと思って試した。(まだイラスト調のほうは実装してないです)
WebGL GPGPU Sample Code

WebGLで計算を行う方法は、

  • GPU側へ入力データをテクスチャとして転送
  • 出力サイズの画面にフレームバッファオブジェクトを設定して画面サイズの四角を描画
  • このときのフラグメントシェーダーでテクスチャ(入力データ)を参照して計算を行う
  • javascript側から結果を読む

としています。
基本的にはOpenGLでやる場合と同じなので、以下のサイトがとても参考になります。
OpenGLによる実装方法 - Satoshi OHSHIMA's web site
(今落ちてるみたいですが、そのうち復活することを願ってリンクを張っておきます)
GPGPU シェーダ」とかでググれば、古のGPGPU手法に関するPDF等がいっぱいヒットすると思います。


オレオレ計算用ライブラリを作ってそれ使っているのですが、コードは以下。
run1ボタンを押したときがrun1関数、run2ボタンを押したときにrun2関数を呼んでいます。
ちなみにこのためだけにOpenGLを勉強したOpenGL初心者なので、WebGLでイロイロやってるライブラリのほう(./nv_gpu.js)はイロイロ突込みどころがあるかもしれません。

/* WebGL GPGPUでテキトウに計算するサンプル  */

var W = 8;
var H = 8;
var N = 4;
var ctx = null;

/* カーネル1
   output(x,y,z) := uTexUnit1(x,y,z) + uTexUnit2(x,y,z)
 */
var kernel1 =
    "#ifdef GL_ES \n" +
    "precision highp float; \n" +
    "#endif \n" +
    "uniform sampler2D uTexUnit1; \n" + 
    "uniform sampler2D uTexUnit2; \n" +
    "void main(void) { \n" +
    "  vec2 pos = vec2(gl_FragCoord.x / 8.0, gl_FragCoord.y / 8.0); \n" + 
    "  gl_FragColor = texture2D(uTexUnit1, pos) + texture2D(uTexUnit2, pos);\n" +
    "}\n";

function run1()
{
    /* デバイスコンテキストを作成*/
    ctx = ctx || nvjs.gpu.create_context();
    var gl = ctx.gl; // ctx.glがWebGL, ctx.canvasがキャンバス
    
    /* 出力データが WxHxN のプログラムを作成
       Nは4しか動かない....
       データ型はUNSIGNED_BYTESしか動かない...
     */
    var gpu_program = new nvjs.gpu.program(ctx.gl, W, H, N, kernel1);
    
    /* 入力データ
       サイズは2^nでないといけないらしい..
       サイズが合わない場合はパディングするなどする.
    */
    var data1 = new Array(W * H * N);
    var data2 = new Array(W * H * N);
    var i;
    var results;
    
    /* 入力データを作成 */
    for (i = 0; i < data1.length; ++i) {
	data1[i] = Math.floor(i / 4);
    }
    for (i = 0; i < data2.length; ++i) {
	data2[i] = i % 4;
    }
    /* 入力データをテクスチャユニットに設定.
       gl.TEXTURE0はライブラリ用にあけとくので1から.
       uniform変数名は、カーネルのソースを参照.
     */
    gpu_program.set_data("uTexUnit1", gl.TEXTURE1, W, H, N, data1);
    gpu_program.set_data("uTexUnit2", gl.TEXTURE2, W, H, N, data2);
    
    /* 実行 */
    results = gpu_program.execute();
    
    /* 破棄 */
    gpu_program.destroy();
    
    /* 結果を表示 */
    var pre = document.getElementById('results');
    var rs = "";
    for (var j = 0; j < results.length; ++j) {
	if (j % 4 == 0) {
	    rs += "\n [" + (j / 4) + "]: ";
	}
	rs += data1[j] + " + " + data2[j] + " = " + results[j] + ", ";
    }
    pre.innerHTML = rs;
};

/* カーネル2
  output(x,y,0) := (uTexUnit1(x,y,0) + uTexUnit1(x,y,1) + uTexUnit1(x,y,2) + uTexUnit1(x,y,3)) * uAlpha
 */
var kernel2 =
    "#ifdef GL_ES \n" +
    "precision highp float; \n" +
    "#endif \n" +
    "uniform sampler2D uTexUnit1; \n" +
    "uniform float uAlpha; \n" +
    "void main(void) { \n" +
    "  vec2 pos = vec2(gl_FragCoord.x / 8.0, gl_FragCoord.y / 8.0); \n" +
    "  vec4 c = texture2D(uTexUnit1, pos); \n" +
    "  gl_FragColor = vec4(c[0] + c[1] + c[2] + c[3], 0, 0, 0) * uAlpha; \n" +
    "}\n";

function run2()
{
    ctx = ctx || nvjs.gpu.create_context();
    var gl = ctx.gl;
    var gpu_program = new nvjs.gpu.program(ctx.gl, W, H, N, kernel2);
    var data1 = new Array(W * H * N);
    var i;
    var results;
    var uAlpha = 1.5;
    
    for (i = 0; i < data1.length; ++i) {
	data1[i] = Math.floor(i / 4);
    }
    gpu_program.set_data("uTexUnit1", gl.TEXTURE1, W, H, N, data1);
    
    /* 変数を設定 */
    gl.uniform1f(gpu_program.uniform_location("uAlpha"), uAlpha);
    
    results = gpu_program.execute();
    gpu_program.destroy();

    var pre = document.getElementById('results');
    var rs = "";
    for (var j = 0; j < results.length / N; ++j) {
	var i = j * 4;
	rs += "\n [" + (j) + "]: ";
	rs += "( " + data1[i + 0] + " + " + data1[i + 1]+ " + " +
	    data1[i + 2]+ " + " + data1[i + 3] + ") * " + uAlpha +
	    " = " + results[i] + ", ";
    }
    pre.innerHTML = rs;
};

カーネルとかCUDAっぽい用語を使っていますが、GPU側の処理はOpenGLのフラグメントシェーダーを記述するためのGLSLという言語で書いています。
結構簡単に書けてると思います。

これはすごい!!夢が広がリング!!!1
と思ってしまいそうですが、いろいろ制限があってヤバイ。

入出力のデータでgl.UNSIGNED_BYTEしか受け付けない

gl.texImage2Dとgl.readPixelsなんですが、なんかgl.RGBA + gl.UNSINGED_BYTEしか指定できない気がする。(INVALID_ENUMが返って来る)
run2()のときに、計算結果が255より大きくなる値がおかしいのはこのせいです。
OpenGLだとgl.FLOATが指定できるらしいので、そのうちできるようになるのか、このままなのか。
uniformでならfloatも送れるのですが、GPUの読み取り専用メモリはサイズ制限とかイロイロあった気がするので(覚えていないのでテキトウに言っています)。
個人的な用途(イラスト調のやつ)は画像の畳み込みフィルタと画素ベクトルのクラスタリングなので、
0-255で十分使えるのですが、汎用計算と呼ぶには無理がありますね…。
なんでもはできない。UNSIGNED_BYTEでできることだけ。

(本当か?情報あれば求む)