本当は「D言語をVimで書くには?」みたいな話をしようと思っていたのですが、自分のブログで既にそんなかんじの話を書いてしまっていたのを忘れていて、急遽話題を変更しました。
DComputeとは
DComputeとはD言語のライブラリで、D言語で書いたカーネルをCUDAやOpenCLをバックエンドとして実行できるようにできます。
他のライブラリとは違い実装がコンパイラにまで食い込んでいるため、3つあるD言語のコンパイラの1つであるLDCでしか動きません。
「本当に使ってる奴おるんか?」というくらいバグにまみれた怪しいライブラリですが、根気よく付き合うと幸せな気持ちになりながらGPGPUができます。
ちなみに私はこれを使って研究しています。(つまり頑張れば普通に使える)
前提条件
OpenCLのほうは普段使っていないのでちょっと保証はできないです。
この記事中ではCUDAを使う前提で進めていきます。
いちいち用意するのが面倒な場合はコンテナイメージが用意してあるので、よければご活用ください。
導入
まずdub
で適当にプロジェクトを作ります
> 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のバグにより動かないので私がforkしたやつを使ってください。
まずcloneします。
> git clone https://github.com/Sobaya007/dcompute.git
次に先程作ったプロジェクト内のdub.sdl
を編集します。
name "dcompute-test"
description "A minimal D application."
authors "sobaya"
copyright "Copyright © 2019, sobaya"
license "proprietary"
dependency "dcompute" path="さっきcloneしたやつのpath"
これでdcomputeが使えます。とりあえず実行してみる。
> 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側のファイルです。
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
とでもしましょうか。
@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バックエンドの場合は自前で宣言すれば使えます。
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
が出てしまいます。
これについては使おうとしているファイル内で自前で宣言してやることで解決します。
pragma(LDC_intrinsic, "llvm.nvvm.barrier0")
void barrier0();
他にも似たような関数があるのですが、だいたいこれで解決します。
...もっと頑張ってほしいなぁ。
まとめ
いかがでしたか?
これでD言語で何不自由なくGPGPUできますね!
皆さんも良きD言語ライフをお送りください!