■
id:ColMuskaと組んで出たGPU Challengeが終わったので忘れないうちにCUDAプログラミングのメモを書き残しておこう。内容は保証できないけど。
基本的には本家CUDAのProgramming Guideを見ればいい。Runtime/Driver APIを参照するときはReference Manualを見ればリストが載ってる。
CUDA Toolkit 10.1 Update 1 Download | NVIDIA Developer
日本語ならここが結構丁寧に説明されてる。
http://journal.mycom.co.jp/special/2008/cuda/index.html
CUDAの用語がごっちゃになって結構めんどい。
スレッド関連
スレッドブロック
スレッドのまとまり。1次元あたり512スレッドの3次元まで可。スレッドブロック単位で8つのストリーミングプロセッサ(SP)をもつマルチプロセッサに投げられる。マルチプロセッサの数は品種で違う。
グリッド
スレッドブロックのまとまりで1次元あたり65535スレッドブロックの2次元まで可。
ワープ
平たく言うと同一スレッドブロック内の32スレッド。SPのクロックがメインクロックの3倍強なので各SPが4回別のスレッドに同じ命令を実行することになり、マルチプロセッサ単位では1メインクロックあたり32スレッド消化することになる。
メモリ関連
ローカルメモリ
各スレッドごとのメモリ。カーネル内で普通に宣言すればレジスタに入るんだけどでかい構造体とか配列はローカルメモリにのる。HW的には基板上のデバイスメモリに入る上にキャッシュが無いので遅い。よっぽどでかいもの入れない限り心配しなくてよさげ?一応CUDAのアセンブラ的なものを見れば確認できるらしい。
コンスタントメモリ
デバイスメモリ上。キャッシュがある。ホストからのみ書き込みできる。使ってないからよくわからない。
テクスチャメモリ
デバイスメモリ上。キャッシュがある。2次元的にデータを取り出したりできるらしい。謎。
コアレスアクセス
corelessじゃなくてcoalesce。「合体」という意味らしい。スレッドが1データずつずれてグローバルメモリにアクセスすると半ワープ分を一斉に取ってくることで時間を抑える。つまり、自分のスレッド番号をidxとしたとき、
for(i = 0; i < 5; i++) dest = global[idx*5+i];
よりも
for(i = 0; i < 5; i++) dest = global[idx*i+idx];
の方が速くなる。普通の演算命令が4〜数十クロックかかるのに対してオンチップメモリからデータを取ってくるのは400〜600クロックもかかるのでかなり大事。
方針としてはなるべくグローバルメモリを使わず、どうしても使わなきゃいけないところはコアレスする。同スレッドが複数回使うならレジスタに入れて、違うスレッドで同じデータを使うならシェアードメモリに移してから作業する。場合によるけどこれで結構速くなる。
ただ難しいのがスレッドブロック間の通信ができないこと。例えばデータをidxからidx+1に引き渡したいとき、スレッドブロック内でならシェアードメモリを使えばいいんだけど、どうしてもスレッドブロックの境目でグローバルメモリを使わないといけない。だったらはじめからグローバルメモリ使ってコアレスアクセスすればいいじゃんってなる。スレッドブロックを1つだけにするとマルチプロセッサを1つしか使わないのでハードウェアリソースもったいなさすぎ。結局最初に配られたツールキットの半分の速度にもならず。入賞は厳しいなぁ。
でも冬学期にやったMPIプログラミングとはまた違う考え方をしなきゃいけなかったりで勉強になりました。CUDA専門にやってる人すげー。
もう1つ。文章力のなさに絶望した…w