DirectXの話 第181

Shader Execution Reordering

22/12/21 up

Shader Execution Reordering

Shader Execution Reordering(SER)はNVIDIA社がGeforce RTX 40シリーズ(Ada Lovelace)で実装したレイトレーシングシェーダの高速化が期待できる技術の1つです。
他にもいくつかの技術がAdaアーキテクチャには追加されていますが、最も実装しやすいのはSERでしょう。

レイトレーシングシェーダの負荷が上がる要因の1つとして、マテリアルの複雑化が挙げられます。

現在のGPUは複数のデータに対して同一の命令を実行することで高速化を図っています。いわゆるSIMDと呼ばれる機能ですね。
この方法は大量に同一の処理を行う場合には有利ですが、反面、データごとに処理が変わってくる場合には不利に働くことがあります。
シェーダが分岐を苦手とする、と言われる理由は主にこれですね。
といっても、小さな、ネストも深くない分岐であれば、昨今のGPUはさほど問題になりません。

しかし、レイトレーシングシェーダはレイがヒットしたマテリアルの違いによってダイバージェンスが発生します。
これは命令的にも発生しますが、データ的にも発生します。
命令的にはHit Groupの違いによるシェーダコード自体の違い(Closest Hit ShaderやAny Hit Shader)があり、データ的には主にテクスチャキャッシュのヒットミスが挙げられます。
残念ながら、現在のGPUレイトレではこれは避けがたい問題です。

この問題を解決するため、多くのゲームエンジンでは各種ソート機能を実装していたりします。
前回(といっても半年以上前)にやったRay Binningにしてもそうですし、UE4で実装されているマテリアルソートも同様です。
しかしこれらは追加のバッファが必要だったり、パスも追加しなければならなかったり、それでいて条件によっては遅くなることもあります。
ハードウェアがやってくれれば楽なのになぁ、と思ったことがある人もいるのではないでしょうか。

で、これをハードウェアでやってしまったのがIntel社とNVIDIA社。
Intelはこれを自動的な機能として実装しました。Intelのハードウェア機能については以下の公式ドキュメントが参考になります。

https://www.intel.com/content/www/us/en/developer/articles/guide/real-time-ray-tracing-in-games.html

この手法ではユーザーが介入する余地はなく、Intel Arcシリーズを使うと自動的に有効になります。
特にユーザーの実装の必要がないというのは簡単ですが、この機能を考慮した高速化を行う必要がある、という欠点もあります。
この記事から見ると、UE4のレイトレリフレクションシェーダは効率がよろしくないように見えます。

NVIDIAはこれに対して、ユーザーによる実装を必要とするSERをハードウェアに実装しました。
やっていることはIntelと基本的に変わらないのではないかと思いますが、ユーザーがシェーダに対して実装するという過程が必要になります。
もちろん、SERを利用しない、ということも可能です。
詳細については以下のホワイトペーパーを参照してください。

https://developer.nvidia.com/sites/default/files/akamai/gameworks/ser-whitepaper.pdf

今回は、このSERの実装方法について解説します。

前準備

SERはNVIDIA社独自の実装となります。そのため、現状ではNVAPIが必要となります。

https://developer.nvidia.com/rtx/path-tracing/nvapi/get-started

2022年12月時点の最新版はR525です。
こちらをダウンロードして、ZIPを展開してください。
いつものGitHubのサンプルには加えていませんので、動作確認を行いたい場合は展開したファイルを External/NVAPI フォルダにコピーしてください。

Monsho/D3D12Samples

また、SERの動作検証にはRTX 4080以上が必要となります。
多分これが一番のネックですね。一番安くて19万円くらい。お高い…

実装

今回の実装は Sample028 に行っています。

まずはシェーダ側から行きましょう。
通常、レイトレーシングを行う場合は TraceRay() 関数を利用しますが、これの代わりに3つの命令を利用します。

まず、NvTraceRayHitObject() 関数でレイトレーシングを行います。
この命令ではレイトレーシングを行い、ヒットしたオブジェクトの情報を取得します。
この命令でヒットしたとしても、この段階ではまだHit Groupのシェーダは実行されません。

次に NvReorderThread() 関数を利用します。
この関数は引数を利用してスレッドをソートします。
いくつかのオーバーロードがあり、最も簡単な形式では、前述した NvTraceRayHitObject() の戻り値である NvHitObject を指定するだけです。

最後に NvInvokeHitObject() 関数を利用します。
この関数を呼び出すと、すでに取得している NvHitObject に対してHit Groupのシェーダを実行するようにします。

実際のコードは以下のようになります。

NvTraceRayHitObject() 関数の引数は、ほぼ TraceRay() 関数と同等です。
違いは引数の最後に出力先となる NvHitObject を指定するだけです。
他の2つの命令は見ての通り。ね、簡単でしょ?

なお、NvInvokeHitObject() で指定するTLASが、NvTraceRayHitObject() 関数と違うものを指定した場合、クラッシュする可能性があるのではないかと考えます。
普通ならこのようなことは行わないと思いますが、複数のTLASを運用する方は注意してください。

さて、これらの関数は当然HLSLの組み込み関数ではありません。
ですので、NVAPIのヘッダをインクルードする必要があります。
インクルード部分の実装は以下のようになります。

インクルードするのは NVAPI/nvHLSLExtns.h ですが、その前に2つの define が入ります。

NV_SHADER_EXTN_SLOT はこの拡張機能を利用する上で必要なUAVを登録するレジスタ番号を指定します。
これは自身のシェーダに合わせて指定しましょう。なお、RootSignatureもこれに合わせる必要がある点に注意してください。
なお、spaceも指定することが可能で、この場合は NV_SHADER_EXTN_REGISTER_SPACE を利用します。
SLOTについては必須ですが、SPACEはオプションです。

NV_HITOBJECT_USE_MACRO_API は利用するDirectX Compilerのバージョンによって切り替えてください。
具体的にはテンプレートが使えるようになっている場合は不要のようですが、私の実装ではこちらを使っています。
この define を使うと、NvTraceRayHitObject() 関数の最後の引数として結果を取得する NvHitObject を指定することになります。
使わない場合、NvTraceRayHitObject() 関数の戻り値が NvHitObject となります。

シェーダはこれで終わりで、次はC++の実装に入ります。
と言ってもこちらはそれほど面倒ではありません。

NVAPIを利用するためのヘッダをインクルードし、SERが利用可能かどうかをチェックします。
RTX40シリーズ、ドライバ最新であればSERサポートされていることが確認できるはずです。
特に難しいことはしていませんが、重要なのは有効であった場合にNVIDIA拡張スロットを設定する NvAPI_D3D12_SetNvShaderTxtnSlotSpaceLocalThread() 関数です。
これはシェーダで指定した NV_SHADER_EXTN_SLOT を設定します。
u7を設定し、spaceは未設定なので、7と0を指定しています。
前述したように、RootSignatureに含める必要がありますので、その点を考慮して設定してください。

なお、このスロットには何かを設定する必要はないようです。
多分、ドライバ側で設定されるのではないかと思います。

結果

映像的には結果に違いはありません。
パフォーマンスもほとんど違いがなく、差をあまりにも感じられなかったので今回はNsightでの比較をしてみます。

SER OFFを1st Capture、ONを2nd Captureとします。
レイトレリフレクションのDispatchRaysは、1stが1.89ms、2ndが1.90msとなっていて、差はありませんでした。
単純にイベントクリックで比較すると前後の描画がマギレてきてしまうので、中間部あたりの1msほどを比較しました。

まずはUnit Throughputから。

上位の傾向は変わっていませんが、SM Throughputが結構上がっています。
メモリに関するL1/L2/VRAMは微増です。

次はOccupancyを見てみましょう。

Occupancyが少し改善されていますので、SM Throughputが上昇したという感じでしょうか。
休止中のSMが少し増えているのも面白いですね。

キャッシュヒット率はどうでしょう?

L2ヒット率はほんの少し改善していますが、誤差といえば誤差かも?
しかしL1ヒット率は逆に悪くなっています。
もともとあまりよろしくない数値ではありますが、更に下がるのはよろしくないですね。

L1 Throughputsから、やはりテクスチャ関連が下がっているのがわかります。
この手のダイバージェンスに対応することが目的の手段なのですが、悪化してるのは解せないですね。

本当にSERが動いているのか疑問はありますが、SM Warp Occupancyの結果ではBarrierによるWarp起動が制限されてるように見えます。
SERは多分スレッドグループ単位でスレッドのソートを行うものと思われるので、GroupSyncのバリアが必要になるかと思います。
その結果がここに出ているのではないでしょうか。

結論

ホワイトペーパーによると、SERを使うことで最大2倍までパフォーマンスは改善するそうです。
しかし、すべてのレイトレでSERがパフォーマンスをよくするとは限らず、場合によっては悪化することもありそうです。
UEのように、マテリアルがユーザ次第で相当数に増えるパターンでは有効かもしれないですが、Hit Groupのシェーダが少ない場合は効果が薄いかもしれないです。

また、現状ではRTX40シリーズ専用ということもあり、今後出てくるかもしれないSERを使ったベストプラクティスがあったとしても使うべきかは疑問です。
高速化が常に期待できるならAda用の設定として入れることも悪くないですが、今のところメリットは少なそうです。

とはいえ、実装も簡単なので、試してみてうまく高速化できるようならそのまま実装してもいいかもです。
無理に実装せず、D3D12が正式対応するのを待つのもありかもしれないです。ただし、対応するかどうかは微妙ですがね。