前回のGPUレイトレーシングについてもう少し補足したほうがよさそうでしたので、補足します。
今回はGPUのジョブの単位、メモリの扱い、画面分割の割り当てを図を用いて説明します。
GPUのジョブの単位
まず、GPUのコアとそのジョブの単位であるグリッド、ブロックとスレッドの関係について列挙します。
- GPUに送られるジョブの単位はグリッド
- グリッドはブロックとスレッドによって構成される
- グリッドのライフタイムは10秒 (10秒ルール)
- 1個のブロックは1個のコアに対してのみ割り当てられる
(注: つまり一個のコアに対しては複数のブロックが割り当てられる、ということ) - ブロックはスレッドで構成され、1個のブロックに最大512スレッド実行できる
- 32スレッドをまとめたものを1warpと呼ぶ
(注:1個のコアには8個のストリーミングプロセッサが積まれている。その1個が4スレッド並列動作をおこなう。
それによって、4*8=32スレッド (1warp) ということ)
これらは図01のような関係となっています。
図01
スレッド1個1個で GPUMain が走り、それらはブロック単位でコアに渡されます。
warp とよばれるスレッドの集合が適宜入れ替えられ、32本ずつ並列動作します。この方法を最大限にいかせるスレッド数として192スレッドという値が出されています。
GPUのメモリについて
今回のトピックで頻繁に使われるグローバルメモリ、シェアードメモリについて説明します。
以下の二つの内容については、私が作成したプログラムの範囲に限定して、のことです。
まず、グローバルメモリとはすべてのコアから参照でき、大容量、低速です。
シェアードメモリに収まりきらないデータや、読み込む規則が定まらないデータなどがおかれます。
次にシェアードメモリとはブロック内でスレッド間でのみ共有できる容量の小さい高速なメモリです。
参照回数が多いオブジェクトや環境情報などがおかれます。
シェアードメモリの内容はブロック内でのみ有効で、同じコア内の別ブロックとは共有できません。
(それに、どのブロックをどのコアに置くといった設定がありません。)
基本的なピクセル並列
X軸の画素をブロック内のスレッド1つ1つに割り当て、Y軸の画素をそれぞれのブロック単位で処理します。
この内容は図02のようになります。
図02
> 図02の方法において、画素をスレッドに割り当てた場合、レイと交差するオブジェクトを左端から右端までメモリに蓄える必要があります。
メモリキャッシュや、不要なロードを抑えるためにもこの方法は適当ではありません。
ただし、この内容は限られた状況、つまり1度にオブジェクトがすべてシェアードメモリに収まるような状況はあまりありません。
ブロック単位のピクセル分割
> ピクセル1個に対しスレッド1個を割り当て、2*2, 4*4, 8*8,... ピクセルのブロックといった感じでマルチコアに割り当てます
混乱を避けるためにあえてこのような言い回しをしました。
図03を見てください。
図03
青い枠でかこった部分 (ピクセルのブロック) をGPUのジョブのブロックとしてマルチコア1個に割り当てます。
このように左上端から右下端までの範囲、といった形でロードするオブジェクトを判別することができます。
またすでに読み込まれているオブジェクトの参照が多く行われるため、高速です。
CPUとGPU間のメモリ遅延
余談につっこみがありましたので一応。
GPUから直接画像をキックという件についてですが、なぜこんな話を出してきたかというとCPU、GPU間のメモリ遅延は空間分割を行い必要なものだけをストア、ロードを繰り返したとしても処理の大半の時間を持って行かれます。
よって実験目的なりなんなりの関数として「一応あってもいい」のではないかと・・・。
実際にソフトウェアとして使用できるかどうかはまた別問題です。