OpenTKとC#でOpenCLプログラミング

C#でお手軽にOpenCLを使ったプログラムを作れないかと思い、OpenTKを使用。

OpenTK 3.xではOpenCLの機能がClooとして独立したようですが、現行のOpenTK 4.xでは再びOpenCLが統合されています。

まずはNugetパッケージマネージャからOpenTK 4.7.xをプロジェクトに追加します。

サンプルプログラムはhttps://github.com/opentk/opentk/tree/master/tests/OpenToolkit.OpenCL.Testsにあるので参照。

using OpenTK.Compute.OpenCL;
using System.Text;
CL.GetPlatformIds(0, null, out uint platformCount);
CLPlatform[] platformIds = new CLPlatform[platformCount];
CL.GetPlatformIds(platformCount, platformIds, out _);
Console.WriteLine(platformIds.Length);
foreach (CLPlatform platform in platformIds)
{
    Console.WriteLine(platform.Handle);
    CL.GetPlatformInfo(platform, PlatformInfo.Name, out byte[] val);
    Console.WriteLine(System.Text.Encoding.ASCII.GetString(val));
}
foreach (IntPtr platformId in platformIds)
{
    CL.GetDeviceIds(new CLPlatform(platformId), DeviceType.All, out CLDevice[] deviceIds);
    CLContext context = CL.CreateContext(IntPtr.Zero, (uint)deviceIds.Length, deviceIds, IntPtr.Zero, IntPtr.Zero, out CLResultCode result);
    if (result != CLResultCode.Success)
    {
        throw new Exception("The context couldn't be created.");
    }
    string code = @"
                __kernel void add(__global float* A, __global float* B,__global float* result, const float mul)
                {
                    int i = get_global_id(0);
                    for (int j =0;j < 60000;j++)
                    {
                        for (int k =0;k < 60000;k++)
                        {
                            result[i] = (A[i] + B[i])*mul;
                        }
                    }
                }";
    CLProgram program = CL.CreateProgramWithSource(context, code, out result);
    CL.BuildProgram(program, (uint)deviceIds.Length, deviceIds, null, IntPtr.Zero, IntPtr.Zero);
    CLKernel kernel = CL.CreateKernel(program, "add", out result);
    int arraySize = 20;
    float[] A = new float[arraySize];
    float[] B = new float[arraySize];
    for (int i = 0; i < arraySize; i++)
    {
        A[i] = 1;
        B[i] = i;
    }
    CLBuffer bufferA = CL.CreateBuffer(context, MemoryFlags.ReadOnly | MemoryFlags.CopyHostPtr, A,
        out result);
    CLBuffer bufferB = CL.CreateBuffer(context, MemoryFlags.ReadOnly | MemoryFlags.CopyHostPtr, B,
        out result);
    float[] pattern = new float[] { 1, 3, 5, 7 };
    CLBuffer resultBuffer = new CLBuffer(CL.CreateBuffer(context, MemoryFlags.WriteOnly,
        new UIntPtr((uint)(arraySize * sizeof(float))), IntPtr.Zero, out result));
    try
    {
        CL.SetKernelArg(kernel, 0, bufferA);
        CL.SetKernelArg(kernel, 1, bufferB);
        CL.SetKernelArg(kernel, 2, resultBuffer);
        CL.SetKernelArg(kernel, 3, -1f);
        CLCommandQueue commandQueue = new CLCommandQueue(
                CL.CreateCommandQueueWithProperties(context, deviceIds[0], IntPtr.Zero, out result));
        CL.EnqueueFillBuffer(commandQueue, bufferB, pattern, UIntPtr.Zero, (UIntPtr)(arraySize * sizeof(float)), null,
            out _);
        //CL.EnqueueNDRangeKernel(commandQueue, kernel, 1, null, new UIntPtr[] {new UIntPtr((uint)A.Length)},
        //	null, 0, null,  out CLEvent eventHandle);
        CL.EnqueueNDRangeKernel(commandQueue, kernel, 1, null, new UIntPtr[] { new UIntPtr((uint)A.Length) },
            null, 0, null, out CLEvent eventHandle);
        CL.Finish(commandQueue);
        CL.SetEventCallback(eventHandle, (int)CommandExecutionStatus.Complete, (waitEvent, data) =>
        {
            float[] resultValues = new float[arraySize];
            CL.EnqueueReadBuffer(commandQueue, resultBuffer, true, UIntPtr.Zero, resultValues, null, out _);
            StringBuilder line = new StringBuilder();
            foreach (float res in resultValues)
            {
                line.Append(res);
                line.Append(", ");
            }
            Console.WriteLine(line.ToString());
        });
        //get rid of the buffers because we no longer need them
        CL.ReleaseMemoryObject(bufferA);
        CL.ReleaseMemoryObject(bufferB);
        CL.ReleaseMemoryObject(resultBuffer);
        //Release the program kernels and queues
        CL.ReleaseProgram(program);
        CL.ReleaseKernel(kernel);
        CL.ReleaseCommandQueue(commandQueue);
        CL.ReleaseContext(context);
        CL.ReleaseEvent(eventHandle);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
        throw;
    }
}

GPU上で実行されるプログラムはC言語で記述できます。

拍子抜けするぐらい簡単にOpenCLを使用したプログラムを作れるんですね。

どうしてもGPUへのデータ転送がボトルネックになりやすいので、高速化するのは中々難しいですが、機会があれば活用したいところ。

Docker上で実行しているNextCloudで”Updates between multiple major versions and downgrades are unsupported. Update failed.”が発生してアップデートに失敗する。

Dockerを使ってNextCloudを運用して居る。久しぶりにNextCloudバージョン画面を見たところ、「保守終了したバージョン」との警告が。あわててアップデートを行ったところ、ずっとメンテナンスモードから戻らなくなってしまった。そう、うっかりとversion 23→version 25にアップデートを掛けてしまったため、処理が正常に終了せずに環境が壊れてしまったのだ。

以下のコマンドを実行してメンテナンスモードを強制的に解除してNextCloudをWERBで開くがUpgradeの確認画面になり、そしてUpgradeは失敗してしまう。

$ sudo docker exec -u www-data <container name> php occ maintenance:mode --off

以下のコマンドを実行してコマンドラインからUpgradeを実施するが、「Updates between multiple major versions and downgrades are unsupported. Update failed.」のエラーになってしまう。

$ sudo docker exec -u www-data <container name> php occ upgrade

リカバリ方法がないかと探していたところ「How to fix an accidental Nextcloud docker image update」にたどり着いたので、こちらの方法でリカバリを試みることにする。

まずhtml/config/config.phpを開き、正確なバージョンを確認する。ここでは「’version’ => ‘23.0.0.10’,」とあるとおり、23.0.0.10が正確なージョンになる。

  'datadirectory' => '/var/www/html/data',
  'dbtype' => 'mysql',
  'version' => '23.0.0.10',
  'overwrite.cli.url' => 'https://example.net',
  'dbname' => 'nextcloud',

次にhtml/version.phpを編集する。既にversion 25のファイルに置き換わってしまっているため、これを書き換えて23→24へのアップグレードを通すために、$OC_Versionをarray(25,0,2,3)からarray(23,0,0,10)に、$OC_VersionStringを’25.0.2’から’23.0.0’に、array (‘24.0’ => true, ‘25.0’ => true,)をarray (‘23.0’ => true, ‘24.0’ => true,)に、下記の通りに書き換えます。

<?php
$OC_Version = array(23,0,0,10);
$OC_VersionString = '23.0.0';
$OC_Edition = '';
$OC_Channel = 'stable';
$OC_VersionCanBeUpgradedFrom = array (
  'nextcloud' =>
  array (
    '23.0' => true,
    '24.0' => true,
  ),
  'owncloud' =>
  array (
    '10.11' => true,
  ),
);
$OC_Build = '2022-12-08T11:32:17+00:00 9c791972de9c2561a9b36c1a60e48c25315ecca5';
$vendor = 'nextcloud';

続いてdocker-compose.ymlを編集します。下記のようにnextcloud:24のようにアップデート先となる旧バージョンのdockerイメージを指定します。

  nextcloud:
    image: nextcloud:24

これで23→24にアップデートする環境が整うので、以下のコマンドを実行してNextCloudを起動し、23から24へのアップデートを実行します。

$ sudo docker-compose up --build -d
$ sudo docker exec -u www-data <container name> php occ upgrade

version 24へのアップデートが正常に終了したら、再びdocker-compose.ymlを編集して元に戻します。その後に再び下記のコマンドを実行すれば、24から25へのバージョンアップを行えます。

$ sudo docker-compose up --build -d
$ sudo docker exec -u www-data <container name> php occ upgrade