5870性能評価(5)

話としては、昨日*1の続き。そしてついでに性能評価にからめてみる。

すっごい遅い件

シェーダのこの部分

#ifdef GI_IS_B
    uint3       rgb = uint3(Gid.x, Gid.y, Gid.z*NUM_THREAD_X*NUM_THREAD_Y*NUM_THREAD_Z+GI);
#else
    uint3       rgb = uint3(Gid.x*NUM_THREAD_X*NUM_THREAD_Y*NUM_THREAD_Z+GI, Gid.y, Gid.z);
#endif

元々はGI_IS_B=1側のコードを書いていたんだけど、結論から言うとこれはバグで本当はelse側のコードを意図していた。
で、まぁ性能評価に使えるのでせっかくだからifdefにしてみたわけだけど。

それで性能評価としては以下の2つのdefineを用意した。

define名 定義した場合の動作 設定意図 定義しない場合の動作
THREAD_COVERED_ALL 1つのスレッドグループで256スレッドとして、RGB3次元のうち1次元分の処理を1つのスレッドグループで処理してしまう。 1つのSIMD Engineでできるだけ処理を集中させた方が速いのか、スレッドグループを大量に増やして細切れにSIMD Engineに割り当ててもらう方が良いのかを見極める。 1スレッドグループには64スレッド(1Wavefront)を割り当てる。
GI_IS_B スレッドの割り当てをB軸とする。1つのスレッドグループに割り当たる64スレッド(or256スレッド)は1つのSIMD Engineで処理されるが、この一度に処理される対象をB軸とする。残りの軸はスレッドグループとしてバラバラに処理される。 メモリアクセスパターンの良し悪しによる影響を見る。 R軸がスレッド割り当てとなる。

測定条件

測定はTHREAD_COVERED_ALLありなし、GI_IS_Bありなしの組み合わせ4パターンでComputeShaderをぶん回してデータを読み出して照合するまでの時間を計測した。ソースコードは昨日の記事を参照。
処理レイテンシを観測するためComputeShaderは以下の回数だけパターンを振り分けて繰り返し呼び出す。

  • 1回
  • 10回
  • 50回
  • 100回
  • 200回
  • 300回
  • 500回

測定は各条件で5回ずつ実施し、平均値を採用する。

測定結果

測定結果は以下のようになった。
縦軸が処理時間(単位は秒)、横軸が繰り返し数、青字のBがGI_IS_Bあり、赤字のRがGI_IS_Bなし。
「すべてスレッドで出力」がTHREAD_COVERED_ALLあり、「スレッドとグループで分担」がTHREAD_COVERED_ALLなし。
ただし、GI_IS_Bありは重すぎたので200回以上の繰り返しは測定なしとした。

ちなみに繰り返し数が1回の場合は測定値がぶれるので平均値は参考値の扱いで。

数値から見える傾向

まずGI_IS_Bありは重い。激しく。
そしてスレッドはTHREAD_COVERED_ALLなしの方が軽い。
繰り返し数を増やすと処理時間も増える。微妙にリニアじゃない気がするけど概ねリニアっぽく見える。
処理のレイテンシが2sec程度ある。

GI_IS_Bが重い原因(想定)

まず3Dテクスチャのメモリレイアウトから。

D3D11_MAPPED_SUBRESOURCEを見る限り、ぶっちゃけ普通に3次元配列になってるっぽい。
つまりアドレスの若番から(0, 0, 0)〜(255, 0, 0)とXが0〜255で続いて次にYが増えて(0, 1, 0)〜(255, 1, 0)、(0, 2, 0)〜(255, 2, 0)、...、(255, 255, 0)
で、最後にZが増える感じ。(0, 0, 1)〜(255, 0, 1)、...
ようするにZが増えるとアドレスは256×256×4=0x40000も一気に飛ぶことになる。

で、ComputeShaderでのスレッドも隣同士はXが連続している。なので、今回のようにスレッド番号(x, y, z)を(RGB値として解釈はするけど)そのままアドレスとして使う場合、連続したアドレスになるのはXの方だった。
で、この連続したスレッド番号はGI(SV_GroupIndex)でもらえる値なので、このGIをX座標に使わないとアドレスが連続しないことになっていた。
図にするとこんな感じ。

書き込み先アドレスがバラバラなのでまとめてDRAMへ書けない状態になっていたのではと思う。

THREAD_COVERED_ALLが遅い件

THREAD_COVERED_ALLについては、思い当たる節がないので測定結果からの推測以上の何かはないんだけど。
スレッドとして分割するよりもスレッドグループとして分割する方が処理時間が短くてすむってことは、きっと何かブロッキングされる要因があってそのレイテンシを大量のスレッドグループで隠すことができてる。ってことになる。
1つ気になるのは今回の処理はスレッドグループに分割してもスレッドグループは4倍にしか増えないんだよね。

THREAD_COVERED_ALL スレッドグループ数 スレッド数
defineなし 256×256×4=256K 64
defineあり 256×256×1=64K 256

SIMD Engineさんは20人しかいないだけだし、64Kスレッドグループは1人あたり3,276.8個ずつ処理しないと終わらないわけで十分数はそろってる認識なんだけどなぁ。
もちろん4倍になると1人あたり13,107.2個と圧倒的な物量になるので多少のレイテンシは完全に隠れちゃうってのは理解できるんだけど、逆に3000cycleで隠せないレイテンシってどんだけーって疑問を覚えるよね。

あと、もう一つ気になることが。レイテンシの隠匿って別にスレッドグループじゃなくても大量のスレッドがあれば大量のWavefrontによって達成できると思うんだけど。もちろん各Wavefront間では同じ命令群を発射してしかもアクセスするメモリアドレスも近くになるだろうし、何かストールする様な原因があったらどのWavefrontも結局次々とストールへ突入することも考えられる。
でも、今回はスレッドグループといっても同じ命令を実行するからあまり変わらない気がするんだよね。その辺を考えるともしかしたらアクセスするメモリアドレスが近いことが逆にストール発生の原因になっているのかもしれない。あり得るとするとキャッシュラインとかメモリバンクとかがもろかぶりすることでアービトレーションが走る羽目になってるとか。その辺の原因だとするとスレッドグループに分けた方が早くなる可能性はあると思う。あえて近いアドレスを吐く処理同士が時間的に離れて実行されることで競合が解消されて早くなるとか。
ま、裏もとってないし妄想のレベルだけど。どうやって検証したものかな。

考察

コンピューティングパワー

今回のプログラムは先日*2見たとおり、DirectXアセンブリレベルで53命令、5870の機械語レベルだと29?30?命令あるわけで1秒間には300〜600回くらい実行できる計算になる。実際にどの程度なのかは命令のスループットに依存するんだけど、そのあたりを机上では調査しきれなかった。
計測結果はほかの要因が絡んでいる様で正直"どうだ"って言えるほどのわかりやすい結果ではなかった。強引に結論を出すなら500回繰り返しでちょうど3秒ほどかかっていて、1回繰り返しでも2秒近くかかることから500回で1秒と言えなくもないけど。

バスアクセス量

今回のプログラムはただとにかく3Dテクスチャへ出力するだけなのでバスへの負荷は軽いはず。普通に考えると1回実行すると256×256×256×4byteで64MBしか出力しないはず。500回繰り返しても32GBほどにしかならない。すくなくとも5870の性能限界を超えることは考えられない。
ただ、1回繰り返しでも2秒ほど時間がかかるということは何かあるってことになる。繰り返し数を増やしてもベースの2秒は変わらないかの様に見えるので何かのレイテンシが2秒なのかもしれない。といっても2秒も必要になるレイテンシは心当たりがないのでこれは別途何らかの手段で解き明かしたいなぁ。

今回机上で検証できなかったこと

今回の測定結果を検証しようとしていろいろ調査したんだけど、5870のVRAMへのアクセス経路についてはっきりした情報を入手できなかった。
たとえば

  1. 3Dテクスチャへのwrite経路にキャッシュがあるのか無いのか
  2. VRAMとアドレスのマッピングはどうなっているのか(バンクインターリーブ的な観点で)

といった情報が見つからなかった。

ただ、おそらくRenderTargetViewを通していないので今回のプログラムはROPを経由せずに直接Memory Export Bufferへ各SIMD Engineから集中砲火されてくるはず。
ATIのスライド*3だと単に「近いwriteはくっつける。32個の64bit値を出せる」としか書かれていない。

  • Coalesces and combine writes
  • Scatter up to 32 64bit values per clock

西川さんの記事*4だと

Memory Export Bufferは,SIMD Engine群から出力されてくる,言ってみればバラバラの演算結果データを取りまとめてメモリに書き出すための一時バッファなのだ。1クロックで32bitデータを64個出力するポテンシャルがある。

との解説がある。(64と32がひっくり返ってる?まぁスループットは変わらないよね)
自分でも想像図を書いてみた。

今回のプログラムはfloat4を集中砲火するので128bitもの巨大データだ。この場合はどうなののか?2cycleかけて32個writeしてくれるんだろうか。

VRAMのアドレスマッピングもわからない。5870は8個のGDDR5 SDRAMに4つのメモリコントローラが接続されているみたい。メモリは1GBあるのでチップ1個たたりに換算すると128MBあるので今回の3Dテクスチャ64MBはまるまる1つ入ってしまう計算になる。
仮に1つのチップに全データが入っちゃうと集中砲火によってwrite待ちが出てしまってスペック上の最高性能が出ない。メモリインターリーブ的にうまく8チップにアドレスを分散してくれていると連続する64MB分のアドレスwriteしてもスペック上の最高性能がたたき出せるはず。
もしかしたら2秒の謎の原因がメモリインターリーブがないせいかもしれない。そのあたりが情報不足で検証できなかった。個人的には繰り返し数を増やしても処理時間が激しく延びないので2秒の謎は別件だと思っているけど。