CUDA画像処理最適化入門

CUDA画像処理 最適化入門
エヌビディアジャパン CUDAエンジニア 森野慎也
GTC Japan 2014
CUDA画像処理 最適化入門
 最適化
1. システムレベル: ホットスポットの特定と、改善
2. GPU : CUDAカーネルの最適化。
 今日のシナリオ : システムレベルでは、ホットスポットが特定済み。
— CUDAを使って、どれだけ速くなるか?
— CUDAカーネルの最適化で、どれだけの改善があるのか?
 Nsight Visual Studio Edition
— CUDAカーネルのプロファイリング
題材
 Disparity Map
— ステレオカメラからの深さ検出
L
L
R
R
ずれ量から深さを推定
動かしてみる
元画像 (RGBA8)
輝度画像 (float)
Disparity Map (int)
DISPARITY MAP
L
𝑑
R
 R側の演算範囲を移動
 L、Rが、よく一致する
ずれ量 d を探す
𝑟
𝑏𝑚 𝑥, 𝑦, 𝑑 =
R
L
𝐿 𝑋, 𝑌 − 𝑅 𝑋 − 𝑑, 𝑌
𝑋,𝑌 ∈ 𝐵𝑙𝑜𝑐𝑘(𝑥,𝑦)
 bm(x, y, d)が最小となるdを求め、D(x, y)とする。
BLOCK MATCHING
R
𝑏𝑚 =
S
R
L
ー
𝑑
L
 ブロック内部の画像が似ていると、bmの値は小さくなる。
CPUで普通に実装すると…
 全部で5重ループ
for (x, y : 出力点座標) {
// 2重ループ
for (dTmp : ずらし量 ) {
// ループ
bm = 0;
for (X, Y : ブロック内部の点の座標)
// 2重ループ
bm += SAD(X, Y, dTmp)
if (bm < bmMin) {
dMin = dTmp; bmMin = bm;
}
}
}
CUDA NAÏVE実装
 出力点(x, y)を並列化。カーネル内部は、3重ループ。
__global__
void ssdNaiveKernel(int *disparity, int disparityPitch, …) {
/* 出力点の(X, Y)の二重ループを並列化 */
int gidx = blockDim.x * blockIdx.x + threadIdx.x, gidy = blockDim.y * blockIdx.y + threadIdx.y;
if ((gidx < roiWidth) && (gidy < roiHeight)) {
float minSad = FLT_MAX; int minDisp = 0;
int imgX = gidx + numDisparities, imgY = gidy;
/* ずらし量で、ループ */
for (int iDisp = 0; iDisp < numDisparities; ++iDisp) {
float sad = 0.f;
/* ブロック内部の点の演算。二重ループ */
for (int iy = 0; iy < blockSize; ++iy) {
int offset = imgX + (imgY + iy) * imgPitch;
for (int ix = 0; ix < blockSize; ++ix)
sad += fabs(dR[offset + ix - iDisp] - dL[offset + ix]);
} // iy
if (sad < minSad) {
minSad = sad; minDisp = iDisp;
}
} // iDisp
disparity[gidx + gidy * disparityPitch] = minDisp;
}
}
性能比較
実装
実行時間
性能向上
CPU (1 core)
970 ms
-
CPU (OpenMP, 8 threads)
320 ms
x 3.0
GPU (Naïve) *
28 ms
x 34.6
 CPU → CUDAに載せ替えるだけでも、速い!
— CUDA : 開発を加速するツールとしても有用。
CPU : Core i7 3870 (4 core), GPU : Tesla K20c (ECC off), Image : 384 x 288 pixels
GPUは、カーネルの実行時間のみ計測
最適化へのアプローチ
 データアクセスの効率化
— Naive版を高速化
— 本セッションでのフォーカス。
 またの機会に取り上げます。
— アルゴリズムの検討
— システムレベルの最適化
— IPCの向上 (IPC = Instruction Per Clock)
演算性能 VS メモリバンド幅
演算性能(Single)
メモリバンド幅
演算性能 /バンド幅
(FLOP / B)
Tesla M2090
(Fermi)
1.33 TFLOPS
177.6 GB/sec
6.97
Tesla K40
(Kepler)
4.29 TFLOPS
288 GB/sec
13.9
比率
x 3.2
x 1.6
-
GPU
 演算性能に対して、メモリバンド幅が十分広くはない。
 Fermi → Keplerで、さらに、顕著になった。
プロファイリング
メモリアクセスのモニタリング 実行
NSIGHT : メモリアクセスのモニタリング
 Naïve版をプロファイリング : L2↔SMX のアクセス量が多い
データアクセスの効率化
 低速なパス(L2⇔SM)の利用を減らし、
高速なパス(Shared Memory)を利用する。
Global RO
Tex
高速 (数TB / sec)
PCIe
Host(PC) DRAM
Global Memory
(GPU DRAM)
Global Memory Access
L2 Cache
Shared Memory
Tex L1
演算
Register File
(変数)
CUDA Cores
SM
低速 (~ 10 GB/sec)
Keplerでは、通常、Global Memory Access時のL1 Cacheは、使用されていない。
SHARED MEMORYを使う
R
R−L
L
S
Thread
 1 Line分づつ、読み込み。
 隣り合ったスレッドで、隣り合ったShared Memoryをアクセス。
 値の再利用性が高い。(Blockの横幅分/Thread : 数十回)
実装例 (一部抜粋)
for (int iDisp = 0; iDisp < numDisparities; ++iDisp) {
float sad = 0.f;
for (int iy = 0; iy < blockSize; ++iy) {
__shared__ float shL[128 + 32];
__shared__ float shR[128 + 32];
int posInL = imgX + (imgY + iy) * imgPitch;
if (imgX < width) {
shL[threadIdx.x] = dL[posInL];
shR[threadIdx.x] = dR[posInL - iDisp];
}
else {
shL[threadIdx.x] = shR[threadIdx.x] = 0.f;
}
/* 同期。Shared Memoryの書き込み後には、必須。 */
__syncthreads();
(以下略)
SHARED MEMORY の利用
 L2 : 7.3 GB → 445 MB まで減った!
性能比較
実行時間
L2
アクセス量
CPU (1 core)
970 ms
-
-
-
CPU (OpenMP*)
320 ms
-
-
x 3.0
CUDA Naïve
28 ms
7.27 GB
-
x 34
CUDA1 (Shared Mem)
11 ms
445 MB
15.1 GB
x 88
実装
Shared Mem
性能向上
アクセス量
*OpenMP, 8 threads
CPU : Core i7 3870 (4 core), GPU : Tesla K20c (ECC off), Image : 384 x 288 pixels
もっと、データアクセスを減らす!
 Shared Memoryへのデータ読み込み回数が多い
— R側イメージのピクセルをずらすごとに、読み込み直し。
 Shared Memoryへの読み込み後、複数の「ずれ量」に対して計算。
複数のずれ量を、まとめて計算
R
R−L
L
S
ずれ量
※ 1 Thread分だけ表示
 Rにずれ量を足しつつ、複数のずれ量に対するSADの値を計算
 計算するずれ量の分、再利用回数が増える。
性能比較
実行時間
L2
アクセス量
CPU (1 core)
970 ms
-
-
-
CPU (OpenMP*)
320 ms
-
-
x 3.0
CUDA Naïve
28 ms
7.27 GB
-
x 34
CUDA1 (Shared Mem)
11 ms
445 MB
15.1 GB
x 88
CUDA2 (Shared Mem2)
5.0 ms
60.9 MB
7.7 GB
x 194
実装
Shared Mem
性能向上
アクセス量
*OpenMP, 8 threads
CPU : Core i7 3870 (4 core), GPU : Tesla K20c (ECC off), Image : 384 x 288 pixels
計算方法も見直す
-S line0
-S line1
S(Block)
+S line5
+S line6
 Block Macthingの算出結果の再利用。
— 1 ラインづつ下にBlockを移動。最上列を引き、最下列を足す。
性能比較
実行時間
L2
アクセス量
CPU (1 core)
970 ms
-
-
-
CPU (OpenMP*)
320 ms
-
-
x 3.0
CUDA Naïve
28 ms
7.27 GB
-
x 34
CUDA1 (Shared Mem)
11 ms
445 MB
15.1 GB
x 88
CUDA2 (Shared Mem2)
5.0 ms
60.9 MB
7.7 GB
x 194
CUDA3 (演算見直し)
3.6 ms
10.3 MB
552 MB
x 269
実装
Shared Mem
性能向上
アクセス量
*OpenMP, 8 threads
CPU : Core i7 3870 (4 core), GPU : Tesla K20c (ECC off), Image : 384 x 288 pixels
まとめ
 データアクセスを減らす!
— Nsight Visual Studio Editionによりプロファイリング
— Naïve版では、L2キャッシュからのデータ移動が大きい。
— アクセス量の削減により、5.7倍 ( x 34 → x 269 ) 高速になった。
 もちろん、以下も重要。またの機会に。
— システムレベルの最適化(次のセッションで、一部扱います)
— アルゴリズムレベルの最適化
— IPCの向上