JCuda - 初期化およびデバイスメモリの使い方

ホーム 目次に戻る

JCuda を使用するにあたって、ライブラリの初期化およびコードの読み込みが必要です。
また、デバイスメモリは GPU 側のみに存在する特別なメモリであるため、特別な方法で取得・初期化する必要があります。

この記事ではライブラリの初期化、デバイスメモリの確保まで説明します。

なお、JCuda を利用したサンプルプログラムが GitHub にありますので、併せてご参照ください。


JCuda の初期化

JCuda の初期化については2つポイントがあります。1つはライブラリ自身の初期化で、もう1つは CUDA を実行する GPU の選択です。

JCuda ライブラリを使用するには、以下のライブラリをインポートする必要があります。

import static jcuda.driver.JCudaDriver.*;
import jcuda.*;
import jcuda.driver.*;
JCuda ライブラリの初期化コードを以下に示します。
// 例外を有効化する
JCudaDriver.setExceptionEnabled(true);

// CUDA ドライバを初期化
cuInit(0);

// コンテキストを作成
CUcontext pctx = new CUcontext();

// Device(id=0: つまりデフォルト) の stream=0 をコンテキストに結びつける
CUdevice dev = new CUdevice();
cuDeviceGet(dev, 0);
cuCtxCreate(pctx, 0, dev);

// PTX ファイルを読み込む
String ptxfilename = preparePtxFile(CUFILENAME);
CUmodule module = new CUmodule();
cuModuleLoad(module, ptxfilename);
ここで preparePtxFile は .cu ファイルから .ptx ファイルを作成するための便利関数です。
実装については GitHub を参照してください。

ここまで来ると、module を使って .cu ファイル上 __global__ 宣言したエントリを取得することができます。

CUfunction entry = new CUfunction();
cuModuleGetFunction(entry, module, KERNELNAME);

// 以降、entry を使用する。他の変数は解放して構わない。

デバイスメモリの作成、初期化、および取得

デバイスメモリはバイト単位で管理されます。そのため、メモリの型に合わせたバイト数を与えるグローバルスタティックメンバー Sizeof.* が提供されています。
対象クラスバイト数
BYTEbyte1
SHORTshort2
INTint4
FLOATfloat4
LONGlong8
DOUBLEdouble8
POINTERPointer8
デバイスメモリを確保するには CUdeviceptr インスタンスを作成し、それを引数として cuMemAlloc() 関数を使用します。例えば float 型のメモリを SIZE 個確保するには
CUdeviceptr devMemFloat = new CUdeviceptr();
cuMemAlloc(devMemFloat, Sizeof.FLOAT * SIZE);
とします。ホストメモリは通常の Java のメモリ確保方法です。ホストメモリとデバイスメモリの間の転送は
// ホストメモリの確保
float[] hostMem = new float[SIZE];

// ホストメモリからデバイスメモリへの転送
cuMemcpyHtoD(devMemFloat, Pointer.to(hostmem), Sizeof.FLOAT * SIZE);

// デバイスメモリからホストメモリへの転送
cuMemcpyDtoH(Pointer.to(hostmem), devMemFloat, Sizeof.FLOAT * SIZE);
となります。
ここで Pointer.to() はホストメモリ配列の先頭アドレスを与える Pointerクラスのスタティックメソッドです。

カーネルの呼び出し

カーネルには以下のパラメータがあります。

Grid 次元数

CUDA の block 数の大きさを決定します。 3次元のパラメータで、使用しない次元は 1 を指定します。

Thread 次元数

CUDA の thread 数の大きさを決定します。 3次元のパラメータで、使用しない次元は 1 を指定します。
例えば1次元配列で大きさが SIZE だとした場合、NTHREAD を Thread 次元数として Grid 次元数は (SIZE + NTHREAD - 1) / NTHREAD となります。
NTHREAD には上限があり、またどのような値がよいかは実験的に決める必要があります。

引数ポインタ

__global__ 関数に与える引数を指定します。JCuda は CUDA driver モードで動作するため、引数並びへのポインタが入力値として必要になります。
例えば、入力データ、入力サイズ、出力データをそれぞれ input, size, output と定義していたとすると
Pointer kp = Pointer.to(Pointer.to(input), Pointer.to(new int[]{size}), Pointer.to(output);
とします。2つの点に注意してください。
  1. Pointer.to(args, ...) は Pointer インスタンスしか引数に取らないため、デバイスメモリも Pointer.to() で Pointer 型に変換してください
  2. 非配列変数を直接指定することはできないため、new cls[]{初期値} として値を与えます。
    この場合でも .cu プログラムでは配列ではなく、通常の引数が使用可能です。
上記の kp に対応する .cu 側のシグニチャは
extern "C" void kernel(float *input, int size, float *output);
となります。

共有メモリサイズ

shared memory の大きさをバイト単位で与えます。共有メモリを使用しない場合は 0 を設定しておきます。

使用ストリーム

カーネルを実行する CUDA stream を指定します。デフォルトの stream を使用する場合は null を指定します。

カーネルの呼び出し

カーネルの呼び出しは cuLaunchKernel で行います。
cuLaunchKernel(function,
  (SIZE + NTHREAD - 1) / NTHREAD, 1, 1, // Grid
  NTHREAD, 1, 1, // Block
  0, null, // 共有メモリサイズ、使用 stream
  kp, null // カーネルパラメータ、拡張パラメータ(常に null)
);

Copyright (c) 2017-2019 by TeqStock.tokyo