C#
OpenCL

C#からOpenCLを便利に使う

いいたいこと

C#向けライブラリを作ったから紹介したい。

C#で使うときの障壁

OpenCL1.2までしか対応してない

.NETから使えるライブラリ(OpenCL.Netとか)も存在するが、開発が止まっていてなおかつOpenCL1.2までしか対応していない。ホスト-デバイス間のメモリ転送とか面倒。

オブジェクト指向っぽくない

私の使い方が下手くそなだけだったらごめんなさい
いちいちカーネルとかコマンドキューとかコンテキストとか、そういったあまり重要でないものを持ちまわるのが面倒くさい。

そもそも定型的なところが多い

しばらく使ってると出てくる定型文を吸収して欲しい。あと、そんなに高度な機能を必要としていないことが多い。

C++向けライブラリをラップする必要がある

自力でラッパー書くのはしんどい。

OpenCL for Netという選択

面倒くさいところを程よくラップしてくれるC#向けOpenCLライブラリ、それがOpenCL for Net

使いやすさのほどはソースを見て欲しい。

unsafe class Program
{
    static void Main(string[] args)
    {
        // シンプルなデバイス情報取得
        foreach (var platformInfo in Platform.GetPlatformInfos())
        {
            Console.WriteLine($"platform index   : {platformInfo.Index}");
            Console.WriteLine($"platform vendor  : {platformInfo.Vendor}");
            Console.WriteLine($"platform name    : {platformInfo.Name}");
            Console.WriteLine($"platform verison : {platformInfo.Version}");
            foreach (var deviceInfo in platformInfo.DeviceInfos)
            {
                Console.WriteLine($"  device index                    : {deviceInfo.Index}");
                Console.WriteLine($"  device name                     : {deviceInfo.Name}");
                Console.WriteLine($"  device max conpute units        : {deviceInfo.MaxComputeUnits}");
                Console.WriteLine($"  device max work item sizes      : {deviceInfo.MaxWorkItemSizes[0]}, {deviceInfo.MaxWorkItemSizes[1]}, {deviceInfo.MaxWorkItemSizes[2]}");
                Console.WriteLine($"  device max work group size      : {deviceInfo.MaxWorkGroupSize}");
                Console.WriteLine($"  device mem alloc size           : {deviceInfo.MaxMemAllocSize}");
                Console.WriteLine($"  device max constant buffer size : {deviceInfo.MaxConstantBufferSize}");
                Console.WriteLine($"  device svm capabilities         : {deviceInfo.SvmCapabilities}");
            }
        }

        // 面倒だけど必要なこいつらを手軽に用意
        var platform = new Platform(0);
        var device = platform.CreateDevice(0);
        var context = device.CreateContext();
        var commandQueue = context.CreateCommandQueue(device);

        var program = context.CreateProgram(source);
        var kernel = program.CreateKernel("testKernel");


        // いわゆるcl_memを用いたプログラム
        var data = new float[] { 3F, 4.5F, 0F, -4.4F };
        var dataSize = sizeof(float) * 4;
        var simpleMemory = context.CreateSimpleMemory(dataSize);
        simpleMemory.Write(commandQueue, true, data, 0, dataSize);
        kernel.NDRange(commandQueue, new long[] { 4 }, simpleMemory, 3F);
        commandQueue.WaitFinish();
        simpleMemory.Read(commandQueue, true, data, 0, dataSize);
        simpleMemory.Release();


        // CL_MEM_COPY_HOST_PTRを利用した場合
        data = new float[] { 3F, 4.5F, 0F, -4.4F };
        dataSize = sizeof(float) * 4;
        simpleMemory = context.CreateSimpleMemory(data, dataSize);
        kernel.NDRange(commandQueue, new long[] { 4 }, simpleMemory, 3F);
        commandQueue.WaitFinish();
        simpleMemory.Read(commandQueue, true, data, 0, dataSize);
        simpleMemory.Release();


        // CL_MEM_ALLOC_HOST_PTRを使用した場合(unsafeが必要)
        dataSize = sizeof(float) * 4;
        var mappingMemory = context.CreateMappingMemory(dataSize);
        var pointer = (float *)mappingMemory.Mapping(commandQueue, true, 0, dataSize);
        pointer[0] = 3F;
        pointer[1] = 4.5F;
        pointer[2] = 0F;
        pointer[3] = -4.4F;
        mappingMemory.UnMapping(commandQueue, pointer);
        kernel.NDRange(commandQueue, new long[] { 4 }, mappingMemory, 3F);
        commandQueue.WaitFinish();
        mappingMemory.Read(commandQueue, true, data, 0, dataSize);
        mappingMemory.Release();


        // CL_MEM_USE_HOST_PTRを使用した場合
        data = new float[] { 3F, 4.5F, 0F, -4.4F };
        dataSize = sizeof(float) * 4;
        mappingMemory = context.CreateMappingMemory(data, dataSize);
        kernel.NDRange(commandQueue, new long[] { 4 }, mappingMemory, 3F);
        commandQueue.WaitFinish();
        mappingMemory.Read(commandQueue, true, data, 0, dataSize);
        mappingMemory.Release();


        // SVM(unsafeが必要)
        dataSize = sizeof(float) * 4;
        var svmBuffer = context.CreateSVMBuffer(dataSize, 0);
        pointer = (float *)svmBuffer.GetSVMPointer();
        pointer[0] = 3F;
        pointer[1] = 4.5F;
        pointer[2] = 0F;
        pointer[3] = -4.4F;
        kernel.NDRange(commandQueue, new long[] { 4 }, svmBuffer, 3F);
        commandQueue.WaitFinish();
        svmBuffer.Release();


        kernel.Release();
        program.Release();

        commandQueue.Release();
        context.Release();
    }

    private static string source = @"
__kernel void testKernel(__global float* array, float rate){
    int idx = get_global_id(0);
    array[idx] *= rate;
}
";

シンプルなプラットフォーム・デバイスの指定

OpenCLを使用するときに毎回必要な定型文がこんなに短くなる。

        // 面倒だけど必要なこいつらを手軽に用意
        var platform = new Platform(0);
        var device = platform.CreateDevice(0);
        var context = device.CreateContext();
        var commandQueue = context.CreateCommandQueue(device);

        var program = context.CreateProgram(source);
        var kernel = program.CreateKernel("testKernel");

簡単なメモリ管理

メモリをSimpleMemoryとMappingMemoryに分類し、メンバ関数で簡単にメモリ読み書きが可能。さらにunsafeが必要ながらもSVMを利用することが可能!

        // cl_mem
        var simpleMemory = context.CreateSimpleMemory(dataSize);

        // CL_MEM_COPY_HOST_PTR
        var simpleMemory = context.CreateSimpleMemory(initialData, dataSize);

        // CL_MEM_ALLOC_HOST_PTR
        var mappingMemory = context.CreateMappingMemory(dataSize);

        // CL_MEM_USE_HOST_PTR
        var mappingMemory = context.CreateMappingMemory(initialData, dataSize);

        // SVM
        var svmBuffer = context.CreateSVMBuffer(dataSize, 0);

柔軟なカーネル呼び出し

カーネル呼び出しに時に合わせて引数をセットすることが可能。一度ワークサイズと引数をセットしたら次回呼び出しからは省略することもできる。

まとめ

つまりとても手軽にOpenCLを利用することができるハッピーライフをお届けします。