超多数スレッドを管理するギガスレッドエンジン

GPUは超多数のスレッドを並列に実行する。NVIDIAはホストCPUとインタフェースし、多数のスレッドを実行させる機構を「ギガスレッドエンジン」と呼んでいる。

3次元のスレッド配列であるブロックを定義し、ホストCPUからGPUに対しては、そのブロックの3次元配列である「グリッド」という単位で実行を指示する。nThreadsは最大1024スレッドに制限されるが、nBlocks.xは2G、nBlocks.yとnBlocks.zは64Kまでの値が指定できるので、計算上はトータルのスレッド数は1ギガを優に超える。従って、ギガスレッドは誇張ではない。

1つのブロックに含まれるスレッドは1~3次元の配列になっているが、処理上は1次元の配列となっており、最初から32スレッドずつをワープとしてまとめて行く。従って、図3-33のようにブロックに12スレッドしか含まれていない場合は、12スレッドしか使ってないワープが作られ、残りの20スレッド分の命令実行資源が無駄になってしまう。また、スレッド数が32スレッドより大きくとも、最後のワープに空きがでないようにするため、ブロックのスレッド数は32の倍数とすることが望ましい。例えば、33スレッドのブロックを作ってしまうと、2番目のワープは1スレッドだけになってしまい、ピーク性能の33/64の性能しか出せないことになってしまう。

一方、ブロックに含まれるスレッド数が多くなると、各スレッドが使えるレジスタファイルのエントリ数やシェアードメモリが少なくなりすぎて、プログラムの効率が低くなってしまう。このような理由から、1つのブロックに含まれるスレッド数は1024以下に制限されている。つまり、ブロックに含まれるスレッド数は1024以下で、32の倍数が望ましいということになり、思ったより選択の幅が狭い。

nBlocksで指定された複数のブロックは、使用可能なSMに均等になるように割り当てられる。従って、ブロック数が多い場合は、1つのSMに複数のブロック(Warp)が割り当てられるということが起こるが、割り当てはダイナミックで、SMのビジーの程度を見て決定されるので、どのブロックとどのブロックが同じSMで実行されるかは確定できない。従って、異なるブロックのスレッドの間では、シェアードメモリを経由したデータ交換を利用することはできない。

また、Warp Bufferは64エントリであり、それを超えるワープ数のカーネルプログラムは待機しており、実行していたワープが終了してWarp Bufferが空くとギガスレッドエンジンが次に実行するワープを選択してWarp Bufferに入れるという風にスケジュールされると考えられる。

これらのギガスレッドエンジンの処理は、かなり、手続き的な処理であり、ホストCPUで実行されるGPUドライバが行っているか、あるいは、GPUで実行されるとしても、マイクロコードなどで実現されているのではないかと思われる。

なお、nBlocksとnThreadsは最大3要素のベクトルであり、2次元、3次元の表現は、処理するデータの構造に合わせてプログラムを分かりやすくするという目的で使用すれば良い。そして、前述のように、nBlocksはx方向は最大231-1、y方向とz方向は最大65,535と大きな値がとれるようになっており、解くべき問題に合わせて各次元のサイズを選ぶことができる。

SMはWarpを切り替えて処理する

NVIDIAのKepler GPUのSMはワープバッファに入っている最大64のワープを切り替えながら実行して行く。切り替えて実行されるワープに対応して、ワープ番号とプログラムカウンタなどの情報を保持する64エントリのワープバッファが備えられている。

以前の図3-28に示したように、各命令の実行には10~20サイクルかかるので、ある命令の演算結果を入力オペランドとして使う命令は、前の命令の実行開始から10~20サイクル待たないと、前の演算が終わっていないので実行を始められない。この演算レーテンシを隠すために、GPUは別のワープを実行する。原理的に、異なるワープの命令の入力や出力オペランドとはデータの依存関係は無いので、別のワープの命令であれば、依存関係のチェックをする必要はない。

そして、同一ワープ内の命令の依存関係をチェックするため、レジスタファイルの各エントリにバリッドビットを持たせる。レジスタのバリッドビットは、そのエントリに結果を格納する命令が発行されたら、現在の内容は古いものであるので、"0"(Invalid)にし、命令が実行されてそのエントリに演算結果を格納したら"1"(Valid)にする。

図3-38のように、Warp Bufferの各エントリは次に実行する命令の入力オペランドのバリッドビットを監視しており、全ての入力オペランドがバリッドになっていれば命令の実行を開始できるのでReadyを上げる。

どれか1つでも入力オペランドがバリッドになっていない場合はNot Readyで、そのデータは、まだ、計算中(あるいはメモリアクセス待ちなど)でレジスタファイルから読み出すことはできない。

図3-38 WarpのReadyの検出

ワープがReadyになり、その命令が使用する実行ユニットが使用可能であれば、ワープの命令を実行ユニットに発行することができる。

実行可能なワープが複数ある場合にどのようにワープを選択するのかは不明であるが、どのワープも公平に実行されることが望ましいので、ラウンドロビン、あるいは、以前に実行された時からの経過時間が長いワープを優先して選択するという方法が使われていると考えられる。

このようにバリッドビットをチェックすれば、デバイスメモリをアクセスして400~800サイクルかかる命令を実行するスレッドを含むワープの結果を格納するレジスタはなかなかReadyにならず、そのデータを使うケースでは長い待ちが発生してそのワープの実行は進まず、その他の実行時間が短い命令だけからなるワープの実行だけが進んでいく。そして、メモリアクセスが終わると、待たされていたワープも実行対象に戻るという処理が自動的に行われる。