node-ffi で OpenCL を使う2 - 演算の実行

node-ffi で OpenCL を使う」 に続き、Node.js を使って OpenCL の演算を実施してみます。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20160725/

はじめに

演算の実行には ref-array モジュールを使った方が便利なため、node-ffi をインストールした環境へ追加でインストールしておきます。

ref-array インストール例
> npm install ref-array

OpenCL の演算実行サンプル

今回は配列の要素を 3乗する OpenCL のコード(以下)を Node.js から実行する事にします。

cube.cl
__kernel void cube(
    __global float* input,
    __global float* output,
    const unsigned int count)
{
    int i = get_global_id(0);

    if (i < count) {
        output[i] = input[i] * input[i] * input[i];
    }
}

サンプルコード概要

上記 cube.cl を実行する Node.js サンプルコードの全体像です。(OpenCLAPI は try-finally 内で呼び出しています)

OpenCL 演算の入力値として data 変数の値を使用します。OpenCL のコードはファイルから読み込んで code 変数へ設定しています。

OpenCL APIclCreateXXX で作成したリソースは clReleaseXXX で解放するようなので、解放処理を都度 releaseList へ追加しておき、finally で実行するようにしています。

なお、OpenCL API のエラーコード取得には以下の 2通りがあります。(使用する関数による)

  • 関数の戻り値でエラーコードを取得
  • 関数の引数(ポインタ)でエラーコードを取得
calc.js (全体)
'use strict';

const fs = require('fs');
const ref = require('ref');
const ArrayType = require('ref-array');
const ffi = require('ffi');

const CL_DEVICE_TYPE_DEFAULT = 1;

const CL_MEM_READ_WRITE = (1 << 0);
const CL_MEM_WRITE_ONLY = (1 << 1);
const CL_MEM_READ_ONLY = (1 << 2);
const CL_MEM_USE_HOST_PTR = (1 << 3);
const CL_MEM_ALLOC_HOST_PTR = (1 << 4);
const CL_MEM_COPY_HOST_PTR = (1 << 5);

const intPtr = ref.refType(ref.types.int32);
const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');
const StringArray = ArrayType('string');

const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL';

// 使用する OpenCL の関数定義
const openCl = ffi.Library(clLib, {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    'clGetDeviceIDs': ['int', ['size_t', 'ulong', 'uint', sizeTPtr, uintPtr]],
    'clCreateContext': ['pointer', ['pointer', 'uint', sizeTPtr, 'pointer', 'pointer', intPtr]],
    'clReleaseContext': ['int', ['pointer']],
    'clCreateProgramWithSource': ['pointer', ['pointer', 'uint', StringArray, sizeTPtr, intPtr]],
    'clBuildProgram': ['int', ['pointer', 'uint', sizeTPtr, 'string', 'pointer', 'pointer']],
    'clReleaseProgram': ['int', ['pointer']],
    'clCreateKernel': ['pointer', ['pointer', 'string', intPtr]],
    'clReleaseKernel': ['int', ['pointer']],
    'clCreateBuffer': ['pointer', ['pointer', 'ulong', 'size_t', 'pointer', intPtr]],
    'clReleaseMemObject': ['int', ['pointer']],
    'clSetKernelArg': ['int', ['pointer', 'uint', 'size_t', 'pointer']],
    'clCreateCommandQueue': ['pointer', ['pointer', 'size_t', 'ulong', intPtr]],
    'clReleaseCommandQueue': ['int', ['pointer']],
    'clEnqueueReadBuffer': ['int', ['pointer', 'pointer', 'bool', 'size_t', 'size_t', 'pointer', 'uint', 'pointer', 'pointer']],
    'clEnqueueNDRangeKernel': ['int', ['pointer', 'pointer', 'uint', sizeTPtr, sizeTPtr, sizeTPtr, 'uint', 'pointer', 'pointer']]
});

// エラーチェック
const checkError = (err, title = '') => {
    if (err instanceof Buffer) {
        // ポインタの場合はエラーコードを取り出す
        err = intPtr.get(err);
    }

    if (err != 0) {
        throw new Error(`${title} Error: ${err}`);
    }
};

// 演算対象データ
const data = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9];

const functionName = process.argv[2]
// OpenCL コードの読み込み(ファイルから)
const code = fs.readFileSync(process.argv[3]);

const releaseList = [];

try {
    ・・・ OpenCL API の呼び出し処理 ・・・

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

clCreateProgramWithSourceOpenCL のコードを渡す際に、ref-array で作成した String の配列 StringArray を使っています。

OpenCL 処理部分

今回は以下のような OpenCL API を使っています。

番号 概要 OpenCL 関数名
(1) プラットフォーム取得 clGetPlatformIDs
(2) バイス取得 clGetDeviceIDs
(3) コンテキスト作成 clCreateContext
(4) コマンドキュー作成 clCreateCommandQueue
(5) プログラム作成 clCreateProgramWithSource
(6) プログラムのビルド clBuildProgram
(7) カーネル作成 clCreateKernel
(8) 引数用のバッファ作成 clCreateBuffer
(9) 引数の設定 clSetKernelArg
(10) 処理の実行 clEnqueueNDRangeKernel
(11) 結果の取得 clEnqueueReadBuffer

OpenCL のコードを実行するには (6) のように API を使ってビルドする必要があります。

Node.js と OpenCL 間で配列データ等をやりとりするには (8) で作ったバッファを使います。(入力値をバッファへ書き込んで、出力値をバッファから読み出す)

また、今回は clEnqueueNDRangeKernel を使って実行しましたが、clEnqueueTask を使って実行する方法もあります。

calc.js (OpenCL 処理部分)
・・・
try {
    const platformIdsPtr = ref.alloc(sizeTPtr);
    // (1) プラットフォーム取得
    let res = openCl.clGetPlatformIDs(1, platformIdsPtr, null);

    checkError(res, 'clGetPlatformIDs');

    const platformId = sizeTPtr.get(platformIdsPtr);

    const deviceIdsPtr = ref.alloc(sizeTPtr);
    // (2) デバイス取得 (デフォルトをとりあえず使用)
    res = openCl.clGetDeviceIDs(platformId, CL_DEVICE_TYPE_DEFAULT, 1, deviceIdsPtr, null);

    checkError(res, 'clGetDeviceIDs');

    const deviceId = sizeTPtr.get(deviceIdsPtr);

    const errPtr = ref.alloc(intPtr);
    // (3) コンテキスト作成
    const ctx = openCl.clCreateContext(null, 1, deviceIdsPtr, null, null, errPtr);

    checkError(errPtr, 'clCreateContext');
    releaseList.push( () => openCl.clReleaseContext(ctx) );
    // (4) コマンドキュー作成
    const queue = openCl.clCreateCommandQueue(ctx, deviceId, 0, errPtr);

    checkError(errPtr, 'clCreateCommandQueue');
    releaseList.push( () => openCl.clReleaseCommandQueue(queue) );

    const codeArray = new StringArray([code.toString()]);
    // (5) プログラム作成
    const program = openCl.clCreateProgramWithSource(ctx, 1, codeArray, null, errPtr);

    checkError(errPtr, 'clCreateProgramWithSource');
    releaseList.push( () => openCl.clReleaseProgram(program) );
    // (6) プログラムのビルド
    res = openCl.clBuildProgram(program, 1, deviceIdsPtr, null, null, null)

    checkError(res, 'clBuildProgram');
    // (7) カーネル作成
    const kernel = openCl.clCreateKernel(program, functionName, errPtr);

    checkError(errPtr, 'clCreateKernel');
    releaseList.push( () => openCl.clReleaseKernel(kernel) );

    const FixedFloatArray = ArrayType('float', data.length);
    // 入力データ
    const inputData = new FixedFloatArray(data);

    const bufSize = inputData.buffer.length;
    // (8) 引数用のバッファ作成(入力用)し inputData の内容を書き込む
    const inClBuf = openCl.clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, bufSize, inputData.buffer, errPtr);

    checkError(errPtr, 'clCreateBuffer In');
    releaseList.push( () => openCl.clReleaseMemObject(inClBuf) );

    // (8) 引数用のバッファ作成(出力用)
    const outClBuf = openCl.clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, bufSize, null, errPtr);

    checkError(errPtr, 'clCreateBuffer Out');
    releaseList.push( () => openCl.clReleaseMemObject(outClBuf) );

    const inClBufRef = inClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 0, inClBufRef.length, inClBufRef);

    checkError(res, 'clSetKernelArg 0');

    const outClBufRef = outClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 1, outClBufRef.length, outClBufRef);

    checkError(res, 'clSetKernelArg 1');

    const ct = ref.alloc(ref.types.uint32, data.length);

    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 2, ct.length, ct);

    checkError(res, 'clSetKernelArg 2');

    const globalPtr = ref.alloc(sizeTPtr);
    sizeTPtr.set(globalPtr, 0, data.length);
    // (10) 処理の実行
    res = openCl.clEnqueueNDRangeKernel(queue, kernel, 1, null, globalPtr, null, 0, null, null);

    checkError(res, 'clEnqueueNDRangeKernel');

    const resData = new FixedFloatArray();

    // (11) 結果の取得 (outClBuf の内容を resData へ)
    res = openCl.clEnqueueReadBuffer(queue, outClBuf, true, 0, resData.buffer.length, resData.buffer, 0, null, null);

    checkError(res, 'clEnqueueReadBuffer');

    // 結果出力
    for (let i = 0; i < resData.length; i++) {
        console.log(resData[i]);
    }

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

動作確認

今回は以下の Node.js を使って WindowsLinux の両方で動作確認します。

  • Node.js v6.3.1

(a) Windows で実行

node-ffi で OpenCL を使う」 で構築した環境へ ref-array をインストールして実行しました。

実行結果 (Windows
> node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562

(b) Linux で実行

前回 の Docker イメージを使って実行します。

calc.js と cube.cl を /vagrant/work へ配置し、Docker コンテナからは /work でアクセスできるようにマッピングしました。

Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash
Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
・・・
# source ~/.bashrc

# nvm install v6.3.1
・・・
# npm install -g node-gyp
・・・
# cd /work
# npm install ffi ref-array
・・・

/work 内で実行します。

実行結果 (Linux
# node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562