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へのデータ転送がボトルネックになりやすいので、高速化するのは中々難しいですが、機会があれば活用したいところ。

SharpZipLibの日本語対応

SharpZipLib 1.2.0以前でtarファイルを作成すると日本語が文字化けしていたけど、何時の間にか日本語に対応していたんですね。TarOutputStreamのConstructorでEncodingを指定できるではないですか。

github(https://github.com/icsharpcode/SharpZipLib)から最新のソースコードをダウンロードしてきて、自らビルドしましょう。

.NET COREでLinuxで動作するサービスを作成する

.NET Core + C#でLinuxのDeamon作れないかなと思ったら、思ったより簡単に作れるようになっていたので覚え書き。Program.csとDaemonService.csの中味はほぼ定型なのでサンプルからそのまま流用します。

以下のサンプルは.NET Core 3.1で記述しています。

// Program.cs

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace DaemonSample
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = new HostBuilder()
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddOptions();
                    services.Configure<DaemonConfig>(hostContext.Configuration.GetSection("Daemon"));

                    services.AddSingleton<IHostedService, DaemonService>();
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                });

            await builder.RunConsoleAsync();
        }
    }
}
// DaemonService.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DaemonSample
{
    class DaemonService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private readonly IOptions<DaemonConfig> _config;
        public DaemonService(ILogger<DaemonService> logger, IOptions<DaemonConfig> config)
        {
            _logger = logger;
            _config = config;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Starting daemon: " + _config.Value.DaemonName);
            _logger.LogInformation("arg1:{0}", _config.Value.arg1);
            _logger.LogInformation("arg2:{0}", _config.Value.arg2);

            // ここでサーバー本体の処理をTaskとして起動します。

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Stopping daemon.");
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _logger.LogInformation("Disposing....");
        }
    }
}
// DaemonConfig.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace DaemonSample
{
    class DaemonConfig
    {
        public string DaemonName { get; set; }
        public string arg1{ get; set; }
        public string arg2{ get; set; }
    }
}

起動時のコマンドラインは下記の通り。コマンドライン引数の受け渡し方が非常に独特です。

/usr/bin/dotnet run --project deamonsample --configuration Release --Daemon:DaemonName="Sample Daemon" Daemon:arg1="argument 1" Daemon:arg2="argument 2"

Demonとして起動するにはsystemdを使用します。/etc/systemd/systemにdeamonsample.serviceファイルを作成し、下記の様に記述します。

[Unit]
Description=deamon sample
After=network.target

[Service]
ExecStart=/usr/bin/dotnet run --project atozipsrv --configuration Release --Daemon:DaemonName="deamonsample " Daemon:arg1="argument 1" Daemon:arg2="argument 2"
WorkingDirectory=/usr/src/deamonsample
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=atozip
Environment=DOTNET_CLI_HOME=/var/src/deamonsample

[Install]
WantedBy=multi-user.target

後は通常のsystemdのDeamon同様に以下のコマンドで起動します。

$ systemctl start deamonsample

参考:Creating a Daemon with .NET Core (Part 1)

Linux上のClosedXMLで画像データを扱うときにSystem.DllNotFoundExceptionが発生する

Linux(Ubuntu 18.04)環境に.NET Core SDK 2.2をインストールし、ClosedXmlで画像を含むExcelファイルを扱おうとしたところ、下記の様な例外が発生して実行出来ません。

System.TypeInitializationException: The type initializer for 'Gdip' threw an exception. ---> System.DllNotFoundException: Unable to load shared library 'libdl' or one of its dependencies. In order to help diagnose loading problems, consider setting th
 e LD_DEBUG environment variable: liblibdl: cannot open shared object file: No such file or directory at Interop.Libdl.dlopen(String fileName, Int32 flag)
     at System.Drawing.SafeNativeMethods.Gdip.LoadNativeLibrary()
     at System.Drawing.SafeNativeMethods.Gdip..cctor()
     --- End of inner exception stack trace ---
     at System.Drawing.SafeNativeMethods.Gdip.GdipLoadImageFromDelegate_linux(StreamGetHeaderDelegate getHeader, StreamGetBytesDelegate getBytes, StreamPutBytesDelegate putBytes, StreamSeekDelegate doSeek, StreamCloseDelegate close, StreamSizeDelegate size, IntPtr& image)
     at System.Drawing.Image.InitFromStream(Stream stream)
     at ClosedXML.Excel.Drawings.XLPicture..ctor(IXLWorksheet worksheet, Stream stream)
     at ClosedXML.Excel.Drawings.XLPictures.Add(Stream stream)
     at ClosedXML.Excel.XLWorkbook.LoadDrawings(WorksheetPart wsPart, IXLWorksheet ws)
     at ClosedXML.Excel.XLWorkbook.LoadSpreadsheetDocument(SpreadsheetDocument dSpreadsheet)
     at ClosedXML.Excel.XLWorkbook.LoadSheets(Stream stream)

.NET Core Runtimeインストール時に依存関係が処理されていないことが原因です。下記のコマンドを実行してGDI+関連のライブラリをインストールすると解決します。

sudo apt install libc6-dev 
sudo apt install libgdiplus
cd /usr/lib
sudo ln -s <a rel="noreferrer noopener" target="_blank" href="http://libgdiplus.so/">libgdiplus.so</a> gdiplus.dll

環境によってはLD_LIBRARY_PATHの設定も必要になるようです。

ASP.NET COREを使ったWEBサービス作成(習作)

習作としてASP.NET COREを使用したWEBサービスを作成してみた。
もちろんLinux(Ubuntu 18.04)上で動作している。

Excelファイルをアップロードすると、適当な文字列部分をバーコード画像に置き換えてくれる。

https://barcode.code-lab.net/