シェーダーとスレッド化

GeForce8800 になって Shader Unit のマルチスレッドがどうこうといわれるように
なったわけですが、スレッド化そのものは ATI(AMD)系だろうとかなり前から
使われているテクニックです。

この辺の話は後藤さんの記事でかなり突っ込んで紹介されています。
http://pc.watch.impress.co.jp/docs/2006/1121/kaigai320.htm

この記事で書かれている3つのスレッディングのうち「カスケード」と呼ばれている
ものは割と初期の段階から用いられています。後藤さんの説明では「メモリアクセス」
のレイテンシを隠蔽するためとなっていますが、これはもともとシェーダーの
パイプラインを埋めるのが目的です。

例えばシェーダー命令で直前の命令と依存関係が発生する場合、アウトオブオーダー
実行でない限りパイプラインの終了を待つためにストールが発生します。

dp3 r0.x, r2, c5
rsq r1.x, r0.x

この例では dp3 の d0.x の演算が完了するまで rsq は実行できません。命令上は
順番並んでいても、パイプライン化が行われているためすべての命令実行は半分以上
がオーバーラップしているからです。

仮にパイプラインステージ数が5段と仮定するとイメージ的には次のようになります。

・依存関係がない場合
dp3 F D E E W _
rsq _ F D E E W

・依存関係がある場合
dp3 F D E E W _
rsq _ F D _ _ E E W

前の命令完了を待つため数サイクル分のストール(パイプラインの空き)が生じます。
そのため動作速度を上げるには、命令を並べ替えて依存関係が発生しないように
プログラムを改良する必要があります。
(なお、この例ではレジスタの読み書きに関するストールなので、実際はバイパス化
でもうちょっと効率が上がるかもしれません)

シェーダーでは依存関係がない複数の演算が同時に大量発生するので、複数のコン
テキストを用いたスレッド化(カスケード化)でパイプラインを埋めることができます。

例えば 3スレッド(カスケード)で頂点演算する場合 V0~V2 の3頂点を同時に処理
します。

V0:dp3 F D E E W _ … (1)
V1:dp3 _ F D E E W _
V2:dp3 _ _ F D E E W _
V0:rsq _ _ _ F D E E W _ … (2)
V1:rsq _ _ _ _ F D E E W _
V2:rsq _ _ _ _ _ F D E E W _

V0,V1,V2 は依存関係が無いことがわかっているためストールは発生しません。

また V0 同士の場合でも、(1) の V0:dp3 演算のあと、(2) の V0:rsq 実行時には
すでに dp3 の演算が完了しているので、命令の並び上直接依存関係があっても見か
け上レイテンシ無しに実行することができます。
この場合特に最適化コンパイラや人間が並び替えで最適化する必要も無く効率が良い
ことがわかります。

レイテンシを考えながら書かなければならない他のハードと比べて、シェーダーの
プログラミングが便利で簡単なのはこのおかげだと思っています。

その代わり1つの実行パイプでもスレッド数分のコンテキストを保持しなければな
らず、またパイプラインが深くなればなるほどレイテンシを隠すためのスレッドが
余計に必要になることがわかります。

ShaderModel2.0 世代の GPU では、このリソース量の上限が実際の演算速度上の制限
になっていたようです。リソースが枯渇すると各スレッドの実行に必要なコンテキス
トの割り当てができず、実行できるスレッド数が減ってしまいます。

演算ユニットやパイプラインに余裕があっても、スレッドを発行できずに動作速度が
頭打ちになってしまうわけです。

昔の GPU で性能を謳う部分に「ハードウエアスレッド○○個」と書かれていたこ
とがありますが、このようにスレッド化はインオーダー実行時のレイテンシを隠す
ための機能なので、その分並列演算能力が増加するわけではありません。

スレッドが無くてもぎりぎりまで最適化したストールが発生しないコードは、
理論上は動作効率が変わらないことになります。
その分人間やコンパイラが苦労わけですが。