GPU の HWアーキテクチャと高性能化手法 須田礼仁 ( 東京大学 ) 2011/03/22 GPU 高性能プログラミング GPU のハードウェアを理解する CUDA のソフトウェアを理解する CUDA でプログラムを書くのは難しくないが, CUDA で高速なプログラムを書くのは難しい どうすれば遅くなるかを理解する! 効果が大きいものから順に説明します 1
高性能プログラミングの手順 1. 現在のコードの, どこに時間がかかっているのか? A B C A B C A B C 2. なぜ遅いのか, どこまで速くなりうるか? 行列積 N = 2000, 演算器 500Gflops, メモリ 200GB/s 演算 2N 3 = 16Gflop = 32ms データ ( キャッシュサイズ ) 8 x 3N 2 = 96MB = 0.48ms データ ( キャッシュサイズ 0) 8 x 3N 3 = 192GB = 960ms 3. どうすれば高速化できるか? CUDA の所要時間計測 cudaevent を使う cudaevent_t start, stop; cudaeventcreate(&start); cudaeventcreate(&stop); cudaeventrecord(start, 0); ここに計算をする cudaeventrecord(stop, 0); cudaeventsynchronize(stop); float elapsedtime; cudaeventelapsedtime(&elapsedtime, start, stop); elapsedtime に ms 単位の値が入る ; 精度は us レベル clock 関数を使う ; うまく使うとクロックサイクルレベル CPU 側で計測する 2
高性能化のポイント 1 GPU プロセッサの構成 GPU HW の 3 階層 Core (CUDA Core, 以前は Stream Processor, SP とも呼ばれていた ) SIMD 演算の 1 要素分の計算する演算器 (Multiprocessor, Streaming Multiprocessor, SM) あたりの Core 数は Compute Capability 依存 Device (GPU) Device あたりの 数は GPU モデルによる Core Core Device Core Core Core Core Core Core 例 :GeForce GT120 4 per GPU 8 Core per 32 Core per GPU Compute Capability 1.1 3
いろんな GPU モデル Device Core Core Device Device Core GeForce 9300M GS Tesla C2050 GTX 560 Ti 自分の GPU の情報を知るには? プログラムの中で :cudagetdeviceproperties サンプルプログラムの devicequery を実行する./deviceQuery Starting... CUDA Device Query (Runtime API) version (CUDART static linking) There is 1 device supporting CUDA Device 0: "GeForce GTX 580" CUDA Driver Version: 4.0 CUDA Runtime Version: 4.0 CUDA Capability Major/Minor version number: 2.0 Total amount of global memory: 1609760768 bytes (16) Multiprocessors x (32) CUDA Cores/: 512 CUDA Cores Total amount of constant memory: 65536 bytes Total amount of shared memory per block: 49152 bytes Total number of registers available per block: 32768 Warp size: 32 Maximum number of threads per block: 1024 Maximum sizes of each dimension of a block: 1024 x 1024 x 64 Maximum sizes of each dimension of a grid: 65535 x 65535 x 65535 Maximum memory pitch: 2147483647 bytes Texture alignment: 512 bytes Clock rate: 1.54 GHz Concurrent copy and execution: Yes # of Asynchronous Copy Engines: 1 Run time limit on kernels: Yes Integrated: No Support host page-locked memory mapping: Yes Compute mode: Default (multiple host threads can use this device simultaneously) Concurrent kernel execution: Yes Device has ECC support enabled: No Device is using TCC driver mode: No devicequery, CUDA Driver = CUDART, CUDA Driver Version = 4.0, CUDA Runtime Version = 4.0, NumDevs = 1, Device = GeForce GTX 580 PASSED Press <Enter> to Quit... ----------------------------------------------------------- 4
CUDA SW の 3 階層 Grid Block Block Block Block Threads Device Cores Cores Gid Grid, Block, Thread が Device,, Core にほぼ対応 各 block は同じ数の thread を含む 全 thread 数 = block 数 block あたりの thread 数 高性能化のポイント 2 BLOCK の実行方式 5
Block と Thread の違い 1 Grid 中の Block 1 Block 中の Thread 同時に実行されることも, 逐次に実行されることもある 非常に多数作成できる ( 最大 65535 block) 同期や通信ができない 必ず同時に実行される 数が制限されている (512 または 1024) 同期や通信ができる Block の実行方式 はいくつかの block を同時に実行できる 実行しきれない block は逐次に実行される Device Block Block Block Block Block Block Block Block Block Block Block Block Block Block 6
1 で同時に走る block 数 各 block が消費する資源量による 1 で同時に走る block 数上限 (= 8) 1 で同時に走る thread 数上限 (= 768/1024/1536) 1 の shared memory サイズ (= 16KB / 48KB) 1 のレジスタ数 (= 8K/16K/32K) 例 :block あたり 128 thread および 3KB の shared memory 使用, thread あたり 40 レジスタ使用の場合同時に走るblock 数 = min{ 8, 1024 / 128, 16K / 3K, 16K / (40*128) } = min{ 8, 8, 5, 3} = 3 上の例で,thread あたりのレジスタ数が 32 ですめば, 4 つの block が同時に走る 33% の速度向上 Thread あたりのレジスタ数の制御 1. NVCC コンパイラオプション -maxrregcount 数 2. カーネル関数の launch bounds global void launch_bounds (256) kernel( ) { } 少なくとも 256 thread 1 block で動作できるレジスタ数に抑える global void launch_bounds (256, 4) kernel( ) { } 少なくとも 256 thread 4 block で動作できるレジスタ数に抑える 両方に指定すると launch bounds が優先 スピルコードが増える危険性 ; 最適値は非自明 7
Block 数 同時に走る block 数が多くても, 性能ロスは少ない Context switch はハードウェアで実行され, 極めて小さい 多くの場合, 最適な同時実行 block 数は,2 以上 同時に走る block 数を増やすには Block あたりの thread 数は適切な大きさに レジスタや shared memory は使いすぎない Block 数は ( 数 ) ( 同時に走る block 数 ) の倍数がよい Block 数,thread 数と性能 1.60E+12 ops fl 1.40E+12 1.20E+12 1.00E+12 8.00E+11 6.00E+11 4.00E+11 2.00E+11 Thread 数 1 64 256 1024 resident block 8 8 6 1 0.00E+00 0 50 100 150 200 250 300 350 400 Block 数 GTX 580, 16 x 32 core, float FMA, 100 times unrolled, peak 1.58 Tflops, max 1.49 Tflops 8
高性能化のポイント 3 スレッドの実行方式 Warp 32 thread で 1 つの warp を構成,SIMD 実行 x = a + b x = a + b a = p[i] a = p[i] y = x * z y = x * z x = a + b x = a + b a = p[i] a = p[i] y = x * z y = x * z Thread 0 ~ 31 Thread 64 ~ 95 Thread 32 ~ 63 time 9
命令の遅延 各命令は 1 サイクルでは終わらない 命令の遅延 ある命令を開始してから, その結果に依存する命令を開始できるまでの時間 x = a * b; y = x + c; x = a * b; y = x + c; 簡単な命令でも, 数サイクル ~ 20 サイクル以上 メモリアクセスは, 数百サイクル パイプライン実行 命令遅延を隠蔽するパイプラインが実装されている Warp 0 計算メモリアクセス計算メモリアクセス Warp 1 Warp 2 Warp 3 Warp 5 計算 計算 メモリアクセス 計算 メモリアクセス 計算 計算 メモリアクセス 計算 メモリアクセス メモリアクセス 計算 計算 同じ block の warp でも, 異なる block の warp でも, 同じように遅延隠蔽ができる 同時に走る warp 数 (= block あたりの warp 数 同時に走る block 数 ) が多いほど遅延が隠蔽できる 10
Block 数,thread 数と性能 ( 再掲 ) ops fl 1.60E+12 1.40E+12 十分な性能を出すのに 48 block (24 resident warp per ) が必要 Occupancy = 0.5 1.20E+12 1.00E+12 8.00E+11 6.00E+11 Thread 数 256 4.00E+11 2.00E+11 0.00E+00 0 50 100 150 200 250 300 350 400 Block 数 GTX 580, 16 x 32 core, float FMA, 100 times unrolled, peak 1.58 Tflops, max 1.49 Tflops 5.00E+10 独立な計算系列 x0 = b0 + a0 * x0; x1 = b1 + a1 * x1; x2 = b2 + a2 * x2; x3 = b3 + a3 * x3; x4 = b4 + a4 * x4; x5 = b5 + a5 * x5; ops fl 4.50E+10 4.00E+10 3.50E+10 3.00E+10 2.50E+10 2.00E+10 1.50E+10 独立な計算 1 2 3 6 1.00E+10 5.00E+09 0.00E+00 Thread 数 = 1 0 50 100 150 200 250 300 350 400 Block 数 11
6 系列の独立な計算,64 thread ops fl 1.60E+12 1.40E+12 48 block x 64 thread で 6 resident warp Occupancy = 0.125 1.20E+12 1.00E+12 8.00E+11 6.00E+11 4.00E+11 Thread 数 th=64, op=6 th=256, op=1 2.00E+11 0.00E+00 0 50 100 150 200 250 300 350 400 GTX 580, 16 x 32 core, float FMA, 100 times unrolled, peak 1.58 Tflops, max 1.55 Tflops Block 数 高性能化のポイント 4 SIMD と条件分岐 12
SIMD 実行 1 つの warp 内の thread は,SIMD 実行 Single Instruction Multiple Data 分岐があっても, 両方を実行する if (b > 0) a += b; else a -= b; p = b > 0; a += b; a -= b; p = b > 0; a += b; a -= b; p = b > 0; a += b; a -= b; p = b > 0; a += b; a -= b; p = b > 0; a += b; a -= b; Warp 内のすべての thread が同じ方向に分岐した時は, 片方だけしか実行されません 分岐は苦手 条件分岐が入れ子になると, それぞれ分岐してゆく if (k > 0) { if (j > 0) do_a; else do_b; } else { if (j > 0) do_c; else do_d; } 1 warp は 32 thread なので, 最悪の場合 32 通りの異なるパスを実行,1/32 の性能 対策は, アルゴリズム上の工夫ということに j = threadidx.x; for (i=0; i< n[j]; i++) x += f(a[i], b[j]); n[j] はできるだけ均一のほうがよい 13
分岐は苦手 ( その 2) ループが遅い アンローリング for (i=0; i< n; i++) a[i] = c * b[i]; ループは n 回まわる for (i=0; i+5 < n; i+= 6) { a[i] = c * b[i]; a[i+1] = c * b[i+1]; a[i+2] = c * b[i+2]; a[i+3] = c * b[i+3]; a[i+4] = c * b[i+4]; a[i+5] = c * b[i+5]; } for (; i< n; i++) a[i] = c * b[i]; ループはほぼ n/6 回まわる 0 1 2 3 4 5 6 7 8 9 10 11 12 13 0 1 2 3 4 5 6 7 8 9 10 11 12 13 #pragma unroll 6 でやってくれる ループアンローリングと性能 1.60E+12 ops fl 1.40E+12 1.20E+12 1.00E+12 8.00E+11 6.00E+11 Unroll 段数 100 25 5 1 4.00E+11 2.00E+11 0.00E+00 0 5 10 15 20 25 30 35 Block 数 GTX 580, 16 x 32 core, float addition 100000 times, peak 1.58 Tflops, max 1.53 Tflops 14
高性能化のポイント 5 デバイスメモリへのアクセス Register Constant cache Shared memory / L1 cache GPU 側のメモリの種類 GPU chip L2 cache Texture cache, surface cache Global memory Register Constant cache Shared memory / L1 cache Register: カーネル関数で宣言されたスカラー変数 Local memory: スピルなどが global memory と同じ所に入る Shared memory, constant memory: 特別に宣言する Global memory: 大域変数,host で割り当てた配列 Texture, Surface: 省略 15
Coalesced access Coalesced access 連続した thread が連続したメモリアドレスをアクセスする メモリアクセスが一度ですむ Block SIMD における 連続アクセス Threads Thread に対するデータ分散は サイクリック分割 が基本 キャッシュがなくてもあってもだいたい同じ Global memory メモリアクセス性能 16 block x 1024 thread x 16K word を読み出す device float x_dev[n_itr][n_thr][n_blk]; ] device float x_dev[n_thr][n_itr][n_blk]; Co oalesced access device float x_dev[n_itr][n_blk][n_thr]; device float x_dev[n_blk][n_itr][n_thr]; device float x_dev[n_thr][n_blk][n_itr]; device float x_dev[n_blk][n_thr][n_itr]; Byte/s 0.00E+00 1.00E+11 2.00E+11 16
データ構造 ポインタを手繰るのは苦手 ループが遅い, パイプライン化できない ランダムアクセスになりがち => できれば ( 構造が静的など ) 配列に 構造体の配列か, 配列の構造体か struct { struct { double a, b; double a[n], b[n]; } data[n]; } data; GPU では, 配列の構造体のほうが有利 75.9 GB/s 84.9 GB/s 計算 :thread ごとに a と b の総和 Shared memory ソフトウェアで明示的に使うキャッシュ 使い方は省略 L1 cache のおかげで, 必要性が低下 最近のバージョンではめいっぱいまで使える L1 cache 48KB + Shared memory 16KB にするには cudafuncsetcacheconfig(kernel, cudafunccachepreferl1) ; Shared memory が 16KB を超えると,L1 48KB + shared 16 KB に L1 cache 16KB + Shared memory 48KB にするには cudafuncsetcacheconfig(kernel, cudafunccacheprefershared) ; nvcc で Xptxas dlcm=cg で L1 cache を使わなくなるが, shared memory のサイズは 48 KB のまま 17
高性能化のポイント 6 カーネルと転送の粒度 CPU-GPU データ通信バンド幅 PCI Express Gen 2.0 x16 理論ピークバンド幅 8GB/s ( 実測 6GB/s 程度 ) 例 : Tesla C2050 計算 :1000x1000 行列の和 C = A + B 倍精度ピーク演算性能 515 Gflops 3.9 us Device memory ピークバンド幅 144 GB/s 167 us => GPU での実効性能 2Mflop / 171 us 12 Gflops PCI Express ピークバンド幅 8GB/s 3000 us => CPU GPU CPU での実効性能 0.67 Gflops CPU-GPU 間のデータ転送は最小限に! GPU C = A + B C A B Dev mem GPU C = A + B C A B Dev mem 12 G flops 0.67 Gflops C A B Host mem 18
CPU-GPU データ転送遅延 最小サイズの転送に 7 us 前後かかる N 回の転送で合計 X バイトのデータを送る時間 ~ T(N, X) = N * 7*10 6 + X / (5*10 9 ) N X N * 10-5 X / (5*10 9 ) T(N,X) 1 10 6 7*10-6 2*10-4 207 us 10 10 6 7*10-5 2*10-4 270 us 100 10 6 7*10-4 2*10-4 900 us 1000 10 6 7*10-3 2*10-4 7.2 ms データはできるだけまとめて転送する! もしくは mapped memory を使う Host 側のメモリ Pageable memory: 普通に malloc したもの Page-locked memory y( または pinned memory) Pageable memory より転送が高速 ( 最大 2 倍ぐらい違う ) データ転送とカーネル実行が並列化できる 作り方 malloc ではなく cudahostalloc 関数を使う malloc で割り当てて,cudaHostRegister で pinned にする Mapped memory GPU から直接アクセスできる CPU メモリ cudahostalloc や cudahostregister でパラメタを指定 細かいアクセスが沢山ある場合にちょっと有利 19
Host と Device 間の通信 遅延時間 Pinned to Device Pageable to Device Device to Pinned Device to Pageable 6.17 GB/s 569GB/s 5.69 6.06 GB/s 5.29 GB/s スループット Pinned to Device Pageable to Device Device to Pinned Device to Pageable 6.52 us 4.75 us 6.54 us 6.61 us その他のヒント 20
NVCC: CUDA compiler driver -arch sm_20 Compute Capability を指定 ( この場合 2.0, デフォルトは 1.0) -maxrregcount 引数 カーネル関数あたりのレジスタ数の上限を指定 -Xptxas -v レジスタ数,local memory, shared memory の使用量を表示 -use_fast_math 高速 低精度の数学関数 ( 単精度 ) を使用 -deviceemu デバイスエミュレーションコードを生成 -Xcompiler 引数 バックエンドコンパイラのオプションを指定 -run ファイル 引数のオブジェクトファイルを実行する デバッグのヒント エラーは毎回チェックするべき #define chk do_check( FILE, LINE ) void do_check(char *file, int line) { cudaerror_t er = cudagetlasterror(); if (er!= cudasuccess) { fprintf(stderr, "%s %d: %s n", file, line, cudageterrorstring(er)); exit(1); } } そこらじゅうに chk; を書いておく Device memory と host memory が混乱しがち device float a_dev; Host からは両方が見える int main(void) { float a_hos; cudamemcpytosymbol(a_dev, &a_hos, sizeof(float), 0,...);... 21