GPGPUライブラリDComputeの紹介

本当は「D言語をVimで書くには？」みたいな話をしようと思っていたのですが、[自分のブログ](https://sobaya007.github.io/2019/07/30/vim%E3%81%A7d%E8%A8%80%E8%AA%9E/)で既にそんなかんじの話を書いてしまっていたのを忘れていて、急遽話題を変更しました。

# DComputeとは
[DCompute](https://github.com/libmir/dcompute)とはD言語のライブラリで、D言語で書いたカーネルをCUDAやOpenCLをバックエンドとして実行できるようにできます。
他のライブラリとは違い実装がコンパイラにまで食い込んでいるため、3つあるD言語のコンパイラの1つである[LDC](https://github.com/ldc-developers/ldc)でしか動きません。
「本当に使ってる奴おるんか？」というくらいバグにまみれた怪しいライブラリですが、根気よく付き合うと幸せな気持ちになりながらGPGPUができます。
ちなみに私はこれを使って研究しています。(つまり頑張れば普通に使える)

# 前提条件
- [LDC](https://github.com/ldc-developers/ldc)がインストール済み
- [DUB](https://code.dlang.org/)がインストール済み(多分LDCをインストールすると勝手についてくる)
- CUDA または OpenCL(≧ 2.1)がインストール済み

OpenCLのほうは普段使っていないのでちょっと保証はできないです。
この記事中ではCUDAを使う前提で進めていきます。

いちいち用意するのが面倒な場合は[コンテナイメージ](https://hub.docker.com/repository/docker/sobaya007/dcompute-cuda)が用意してあるので、よければご活用ください。

# 導入
まず`dub`で適当にプロジェクトを作ります

```bash
> dub init dcompute-test
Package recipe format (sdl/json) [json]: sdl
Name [dcompute-test]: 
Description [A minimal D application.]: 
Author name [sobaya]: 
License [proprietary]: 
Copyright string [Copyright © 2019, sobaya]: 
Add dependency (leave empty to skip) []: 
Successfully created an empty project in '/home/sobaya/dtest/dcompute-test'.
Package successfully created in dcompute-test
```

いろいろ訊かれるけど基本的に連打でOKです。

次に依存ライブラリとしてdcomputeを追加します。
本当は`dub add dcompute`で公式のレポジトリにあるライブラリは入るのですが、`ldc1.18.0`現在、[LDCのバグ](https://sobaya007.github.io/2019/12/01/ldc%E3%81%AE%E3%83%90%E3%82%B0%E3%81%BF%E3%81%A4%E3%81%91%E3%81%9F/)により動かないので私がforkしたやつを使ってください。
まずcloneします。

```bash
> git clone https://github.com/Sobaya007/dcompute.git
```

次に先程作ったプロジェクト内の`dub.sdl`を編集します。

```sdl:dub.sdl
name "dcompute-test"
description "A minimal D application."
authors "sobaya"
copyright "Copyright © 2019, sobaya"
license "proprietary"

dependency "dcompute" path="さっきcloneしたやつのpath"
```

これでdcomputeが使えます。とりあえず実行してみる。

```bash
> dub run --compiler=ldc
Performing "debug" build using ldc for x86_64.
derelict-util 3.0.0-beta.2: target for configuration "library" is up to date.
derelict-cl 3.2.0: target for configuration "library" is up to date.
derelict-cuda 3.1.1: target for configuration "library" is up to date.
taggedalgebraic 0.10.13: target for configuration "library" is up to date.
dcompute 0.1.0+commit.22.geff2671: target for configuration "library" is up to date.
dcompute-test ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
Running ./dcompute-test 
Edit source/app.d to start your project.
```
ここまで行けば普通に動いています。

# 使ってみる
では試しにカーネルをDで書いて動かしてみましょう。
今回は簡単のため単純なベクトルの足し算の例です。

まずはhost側のファイルです。

```d:app.d
import dcompute.driver.cuda;
import std.experimental.allocator : theAllocator;
import std;
import testkernel;

void main() {
    /*  各種初期化 */
    Platform.initialise();

    auto ctx = Context(Platform.getDevices(theAllocator)[0]);
    scope (exit) ctx.detach();

    Program.globalProgram = Program.fromFile("./.dub/obj/kernels_cuda210_64.ptx");

    /* データの準備 */
    auto bufA = Buffer!float(iota(100).map!(_ => uniform(0.0f, 1.0f)).array);
    auto bufB = Buffer!float(iota(100).map!(_ => uniform(0.0f, 1.0f)).array);
    auto bufC = Buffer!float(new float[100]);
    scope (exit) bufA.release();
    scope (exit) bufB.release();
    scope (exit) bufC.release();

    bufA.copy!(Copy.hostToDevice);
    bufB.copy!(Copy.hostToDevice);

    /* カーネル起動 */
    auto q = Queue(/* async = */false);
    q.enqueue!(testKernel)
        ([100, 1, 1], [1,1,1])
        (bufA, bufB, bufC);

    /* 計算結果の確認 */
    bufC.copy!(Copy.deviceToHost);

    foreach (a,b,c; zip(bufA.hostMemory, bufB.hostMemory, bufC.hostMemory)) {
        assert(a + b == c);
    }
}
```

次にdevice側のファイルです。
`app.d`の横に並べる形で`testkernel.d`とでもしましょうか。

```d:testkernel.d
@compute(CompileFor.deviceOnly)
module testkernel;

import ldc.dcompute;
import dcompute.std.index;

@kernel
void testKernel(GlobalPointer!float a, GlobalPointer!float b, GlobalPointer!float c) {
    const idx = GlobalIndex.x;
    c[idx] = a[idx] + b[idx];
}
```

host側ではDComputeの初期化から始まります。
`dcompute`を依存に加えた状態で`dub`でビルドをすると、deviceコードっぽいファイルだけ集められ`.dub/obj/`にptxが吐き出されるので読み込みませます。

次に計算の入力データを作ります。
ここでは乱数で適当なベクトル`a`と`b`を作っています。
hostからdeviceへのデータ転送は明示的に行う必要があります。

ここまで来たら計算を実行します。
DComputeでは計算はQueueに追加していく方式をとっています。
同期/非同期も選ぶことが可能ですが、今回は簡単のため同期型にしました。
カーネルの起動はCUDAやOpenCLと同様にGridサイズとBlockサイズを指定します。

最後に計算結果を確認します。
deviceからhostへのデータ転送もやはり明示的に行う必要があります。

deviceコードは基本的に
- module宣言に`@compute(CompileFor.deviceOnly)`をつける
- kernel関数に`@kernel`をつける
- hostとのデータのやり取りは`GlobalPointer`型で行う
- 自身のindexは`GlobalIndex`で取得する
という点にだけ気をつければ普通にかけます。
ちなみにdeviceでもhostでも同じコードを使いたいという場合は`@compute(CompileFor.hostAndDevice)`というのもあります。

# 実際困る点
と、ここまではdcomputeのドキュメントとかを読んでいればわかることですが、実際はもっと複雑なことがしたくなります。
そこで起きがちな様々なトラブルについてメモ書き程度に書いておきます。

## 数学系の関数がない
これは私がレイトレを書こうと思って詰まったところなのですが、普通に`sin`とか`cos`とかがないので困ります。
実はCUDAバックエンドの場合は自前で宣言すれば使えます。

```d:math.d
pragma(LDC_intrinsic, "llvm.sqrt.f#") T sqrt(T)(T val) if (__traits(isFloating, T));
pragma(LDC_intrinsic, "llvm.nvvm.cos.approx.f") float cos(float val);
pragma(LDC_intrinsic, "llvm.nvvm.sin.approx.f") float sin(float val);
```
これだけはなんとか見つけました。
しかし`tan`とか`abs`とかはどうやってもうまくいかない。
まぁ最悪上記3つだけあれば他の関数は気合でどうにかなります。

## barrierできない
これは`reduce`を実装しようとしたときに起きたものです。
一応DComputeには`barrier0`という関数が宣言されているのですが、使おうとすると`invalid ptx`が出てしまいます。
これについては使おうとしているファイル内で自前で宣言してやることで解決します。

```d
pragma(LDC_intrinsic, "llvm.nvvm.barrier0")
void barrier0();
```

他にも似たような関数があるのですが、だいたいこれで解決します。
...もっと頑張ってほしいなぁ。

# まとめ

いかがでしたか？
これでD言語で~~何不自由なく~~GPGPUできますね！
皆さんも良きD言語ライフをお送りください！