Upload
others
View
13
Download
0
Embed Size (px)
Citation preview
CUDA画像処理入門
エヌビディアジャパン CUDAエンジニア 森野慎也
GTC Japan 2014
CUDAを用いた画像処理
画像処理をCUDAで並列化
— 基本的な並列化の考え方
— 目標 : 妥当なNaïveコードが書ける
最適化の初歩がわかる
— ブロックサイズ
— メモリアクセスパターン
RGB → Y(輝度) 変換
カラー画像から、グレイスケールへの変換
— Y = 0.299 × R + 0.587 × G + 0.114 × B
CUDAにおける画像処理の基礎
2次元メモリ確保API
— Pitchを考慮
— cudaMallocPitch()、cudaMemcpy2D()
並列化
— CUDAの並列度 : 数万以上欲しい…
Keplerでの目安 : CUDA Core数 x 10 程度 (最低限)
PITCHを考慮したメモリレイアウト
RGBA(8 bit, uchar4)の配列
index = x + y * pitchInPixel
width
pitchInPixel = pitchInByte / sizeof(uchar4)
(x, y)
2次元メモリ 確保・転送
cudaError_t cudaMallocPitch ( void** devPtr, size_t* pitch, size_t width, size_t height )
— widthバイトのメモリを、height行分、取得する。
— 行は、pitchバイトで整列する。
cudaError_t cudaMemcpy2D ( void* dst, size_t dpitch, const void* src, size_t spitch, size_t width, size_t height,cudaMemcpyKind kind )
— dstで示されるメモリ (dpitchバイトで整列)に、srcで示されるメモリ (spitchバイトで整列) を、width (バイト) x height (行)、コピーする。
サンプルコード
uchar4 *src, *dImage;
size_t spitch, dPitch, dPitchInPixel;
// ピッチつきで、メモリをアロケート
cudaMallocPitch(&dImage, *dPitch, width * sizeof(uchar4), height);
dPitchInPixel = dPitch / sizeof(uchar4);
// ピッチを変換しつつ、ホスト→デバイスへと、メモリ転送
cudaMemcpy2D(dImage, dPitch, src, sPitch, width * sizeof(uchar4), height,
cudaMemcpyHostToDevice);
画像処理における並列化の基本
基本 : 1 ピクセルに対して、 1スレッドを対応させる
— ピクセル数分、スレッドが走る。例 : 262,144 (= 512 x 512) スレッド
スレッドは、処理対象のピクセルを持つ。
— 自分の位置 (x, y) を知ることが必要
2DでのBLOCK・THREADの割り当て
Thread : 「2次元」でピクセルに対応。
Block : 「2次元」で定義。一定のサイズのタイル。
Grid : 必要数のBlockを「2次元」に敷き詰める。
1 Block
1 Pixel = 1 Thread
(x, y) =
(Global ID X, Global ID Y)
2DでのBLOCK・THREADの割り当て
GlobalID は、(x, y, z)方向に計算できる
— GlobalID(x) = blockDim.x * blockIdx.x + threadIdx.x
— GlobalID(y) = blockDim.y * blockIdx.y + threadIdx.y
— GlobalID(z) = blockDim.z * blockIdx.z + threadIdx.z
blockDim.x * blockIdx.xthreadIdx.x
blockDim.y * blockIdx.y
threadIdx.y
RGB → Y 変換 カーネル
__global__
void RGBToYKernel(uchar4 *dDst, const uchar4 *dSrc, int width, int height, int pitch){
int gidx = blockDim.x * blockIdx.x + threadIdx.x;
int gidy = blockDim.y * blockIdx.y + threadIdx.y;
if ((gidx < width) && (gidy < height)) {
int pos = gidx + gidy * pitch;
// Y = 0.299 R + 0.587 G + 0.114 B
uchar4 value = src[pos];
float Y = 0.299f * value.x + 0.587f * value.y + 0.114f * value.z;
unsigned char y = (unsigned char)min(255, (int)Y);
dDst[pos ] = pixel;
}
}
カーネル呼び出し (GRIDサイズ指定)
/* value、radixで割って、切り上げる */int divRoundUp(int value, int radix) {
return (value + radix – 1) / radix;
}
/* gridDim, blockDimを、2次元(x, y方向)に初期化 */dim3 blockDim(64, 2);
/* divRoundUp()は、切り上げの割り算 */dim3 gridDim(divRoundUp(width, blockDim.x), divRoundUp(height, blockDim.y));
RGBToYKernel(dDst, dSrc, …);
悪い並列化の例
GPUの並列化としては、NG。非常に低速。
— 並列度が低い
— メモリアクセスパターンが悪い
ただし… CPU的発想としてはふつう。
Thread 0
Thread 1
Thread 2
Thread 3
…
…
ここはポイント!コアレス(COALESCED)アクセス
連続するスレッドが、連続するメモリにアクセスする。— threadIdx.xに対して、連続。
Memory :
0 1 2 3 4 5 6 7 8 …
threadIdx.x
…
Thread :
再掲 : 2DでのBLOCK・THREADの割り当て
GlobalID は、(x, y, z)方向に計算できる
— GlobalID(x) = blockDim.x * blockIdx.x + threadIdx.x
— GlobalID(y) = blockDim.y * blockIdx.y + threadIdx.y
— GlobalID(z) = blockDim.z * blockIdx.z + threadIdx.z
blockDim.x * blockIdx.xthreadIdx.x
blockDim.y * blockIdx.y
threadIdx.y
動かしてみる
FAQ : BLOCKDIMの決め方
1. Occupancy (占有率) を 100 %にする
2. Blockあたりのスレッド数は、なるべく小さく。
3. 横方向は、コアレスアクセス。なるべく、長くする。
BLOCKDIMの決め方 (OCCUPANCY から)
SMXあたり、2048 Thread走らせたい。
— Occupancy (占有率) = 100 %
Occupancy = 100 % を満たす、Blockあたりのスレッド数は、
2048 Thread / 16 Block = 128 Thread / Block2048 Thread / 8 Block = 256 Thread / Block2048 Thread / 4 Block = 512 Thread / Block2048 Thread / 2 Block = 1024 Thread / Block
項目 値
最大のBlock数 / SMX 16
最大のThread数 / SMX 2048
最大のThread数 / Block 1024
BLOCKDIMの決め方(BLOCKの粒度から)
Grid = 4096 Thread の実行例を考えてみる
— Block : 256 Thread、1024 Threadで比較
— 3 SMX / GPU、1 SMXあたり 1 Blockが実行可能とする
1024 Thread / Block
Block
Block
Block
BlockSMX 0
SMX 1
SMX 2
256 Thread / Block
SMX 0 Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
Block
SMX 1
SMX 2
Block
t
t
Blockサイズは小さいほうが得 → 128 Threads / Block
BLOCKDIMの決め方 (SMXの構造から)
Warp Scheduler x 4 :
— 1 clockあたり、4 Warpに対する命令発行
— Blockのサイズは、 128 Thread の倍数が望ましい。(128 Thread = 32 Thread/Warp x 4 Warp)
タイルは横長がよい
タイルの横幅は、32(Warpの幅)の倍数がよい。
32より小さい場合、16、もしくは、8 を使う。
Memory :
threadIdx.x
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31Thread :
RGB→Y変換時のバンド幅 : TESLA K20CblockDim.x
1 2 4 8 16 32 64 128 256 512 1024blo
ckD
im.y
1 1.4 2.8 5.6 11.2 22.1 43.9 78.5 119.8 119.3 115.4 87.7
2 2.6 5.2 10.4 20.6 40.7 77.9 119.8 119.4 115.3 87.4 -
4 4.8 9.6 19.2 37.8 74.0 119.4 118.2 114.2 87.3 - -
8 8.4 16.7 33.3 69.6 115.0 117.9 111.9 87.1 - - -
16 13.4 26.3 60.6 106.7 115.0 114.3 87.2 - - - -
32 17.7 40.4 81.1 103.9 110.9 86.9 - - - - -
64 20.7 41.7 79.8 99.0 83.5 - - - - - -
128 20.7 41.6 75.6 75.3 - - - - - - -
256 20.7 41.0 60.3 - - - - - - - -
512 20.5 37.6 - - - - - - - - -
1024 19.1 - - - - - - - - - -
値: バンド幅 (GB/sec)Tesla K20c (ECC off)
Occupancy < 100 %
blockDim.x < 8
まとめ
画像処理におけるCUDA
— Pitchを考慮したメモリレイアウト
— 2次元のGridの呼び出し
正しいNaïveコード (カーネル) の書き方
— コアレスアクセス
— ピクセルごとに、スレッドを割り当てる
並列度は、数万以上。
— Blockサイズは、128が適当 (単純なカーネルの場合)