LoginSignup
8
1

More than 3 years have passed since last update.

本当は「D言語をVimで書くには?」みたいな話をしようと思っていたのですが、自分のブログで既にそんなかんじの話を書いてしまっていたのを忘れていて、急遽話題を変更しました。

DComputeとは

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

前提条件

  • LDCがインストール済み
  • DUBがインストール済み(多分LDCをインストールすると勝手についてくる)
  • CUDA または OpenCL(≧ 2.1)がインストール済み

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を編集します。

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側のファイルです。

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とでもしましょうか。

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が吐き出されるので読み込みませます。

次に計算の入力データを作ります。
ここでは乱数で適当なベクトルabを作っています。
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バックエンドの場合は自前で宣言すれば使えます。

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が出てしまいます。
これについては使おうとしているファイル内で自前で宣言してやることで解決します。

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

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

まとめ

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

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1