UE4のRT ReflectionパスのSortMaterialをNSight Graphicsを使って確認する

Unreal Engine 4.22でリアルタイムレイトレーシングの描画パスが追加されました。今回はその中のReflectionの設定のSortMaterailsについての簡単な解説と、ReflectionのRayTraceのパフォーマンスをNSight Graphicsを使って確認します。

注:前回のUE4.22のSortMaterialsの解説に、下記NSightのGPUTraceの解説を付け加えた結果、NSightに関する説明が過半を占めたので、記事のタイトルを変更しました。

実行環境など

Unreal Engine 4.22
RTX2080ti
NSight Graphics 2019.2 と 2019.3

サンプルシーンは、Soul Cityのアセットが並んでいるマップを使用。
適度にマテリアルがあって、オブジェクトが入り組んでないので、BLASの構造がどうなるかわかりやすかったので使用しました。固定カメラを一つ追加しただけで、あとはオリジナルからの変更はありません。

まず、r.RayTracing.Reflections.SortMaterialsの有効/無効を切り替えた場合の処理の変化についてです。

SortMaterialsの設定の無い場合

 SortMaterailsを設定しない場合は、Reflectionレイを計算するため、GBufferのDimensionのDispatchRaysが呼び出され、GBufferの情報を元にReflectionRayを設定して、TraceRayを呼び出してレイトレースを行い、マテリアルを評価するようになっています。マルチバウンスの場合でもClosestHitからのTraceRayの再帰呼び出しは無く、一旦RayGenerationに戻る設計になっています。そして、その結果をDenoiserが処理する形になっています。ReyGenシェーダーは、Engineのシェーダーとして記述されています。
Engine\Shaders\Private\RayTracing\RayTracingReflections.usf

SortMaterialsの設定がある場合

 SortMaterailsを設定した場合は、DispatchRays->Compute->DispatchRaysの3パス構成の後にDenoiser処理という形に変更されます。ReyGenシェーダーは、SortMaterialsの設定がない場合のソースコードは同じなのですが、
DIM_DEFERRED_MATERIAL_MODEマクロにDEFERRED_MATERIAL_MODE_GATHERを設定したものと、DEFERRED_MATERIAL_MODE_SHADEを設定したものを、それぞれ1パスと3パス目に用います。
 DEFERRED_MATERIAL_MODE_GATHERを設定したコードは、Gbufferをサンプリングして、ReflectionRayを設定して、TraceRayを呼び出すのですが、シェーディングは行わず、ReflectionRayがヒットした場合は、HitTとHitした先のマテリアルのID,それから自身のScreenPositionをStructuredBufferに書き出します。
 2パス目のComputeShaderは、GATHERパスが書き出したUAVバッファを、マテリアルIDを基準にソートします。ソートは全画面ではなく、ある程度の画面ブロック(デフォルトでは、64×64)ごとにソートをします。
 3パス目のDEFERRED_MATERIAL_MODE_SHADEを設定したコードは、GBufferにアクセスする前に、2パス目でマテリアルでソートしたStructuredBufferから、ScreenPositionを読み出します。そして、取得したスクリーン位置のG-Bufferの情報を取得して、ReflectionRayを設定します。加えて、Rayの衝突判定範囲のMinTをStructuredBufferから読み出した、HitTの少し手前に設定します。TraceRay呼び出しから先の処理は、SotrMaterialを設定しない場合と同じです。

パフォーマンスについて

以下は、SortMaterialsを設定しない場合(デフォルト)と、した場合の、NSightのRange Profilerのスクリーンショットになります。

r.RayTracing.Reflections.SortMaterials=0

r.RayTracing.Reflections.SortMaterials=1

 まず、SortMaterialsを設定したことで、フレーム全体のレンダリング時間が、32ms程度から、27ms程度に短縮されています。また、”RayTracingReflections”のマーカーの範囲が約22msから、17ms程度になっているのが確認できるので、レンダリング時間の差異は、RTReflectionによるものなのがわかります。このマーカーの範囲の後半には、3パスのDenoiserの処理がありますが、どちらも4ms弱なので、ここに大きな違いは見られません。一方で前半のDispatchRayは、SortMaterialsの設定の無い場合は1パスで、SortMaterialsの設定がある場合は3パスとなっています。これは先ほど説明した通りです。
 SortMaterialsの設定をした場合の1パス目のGATHERパスは、マテリアルの評価を行わないので、1ms強で処理が完了しています。ComputeShaderによるソートも、0.2ms程度で処理が終わっています。これらの部分は、ReflectionRayが最初にHitするマテリアルでソートする分のオーバーヘッドに相当する部分となります。一方で、SortMaterialsを設定した3パス目と、SortMaterialsを設定しない1パス目の処理時間は、マテリアルをソートした場合としない場合の処理時間の差異と考えることができると思います。該当パス全体でアクセスするシェーダーリソースの総量で考えるならば、マテリアルをソートしたほうが、StructuredBufferの読み出しの分だけメモリI/Oは増加するはずですが、実際の処理時間は、ソートしない場合は18msに対して、ソートした方は11msとなっています。

 両者のDispatchRaysのRangeProfilerによるプロファイル結果を比較します。まずSM OverviewのStallの原因をチェックしますと、マテリアルでソートしない方は、SM Warp Stall No Instructionsが25%でトップとなっているのに対して、ソートしたほうは、Stall Long Scoreboardが18%でトップとなっています。SM Warp Stall No Instructionsは、Warpを実行しようとしたときに、Instruction Cacheから命令のFetchができなかった場合に発生するStallです。つまり、Instructionキャッシュのthrashingが発生しているのではと予測できます。一方で、Stall Long Scoreboardは、XBarを跨ぐメモリアクセスのDependencyが未解決のために発生するStallです。こちらは、シェーダーリソース(Texture/StructuredBuffer/UAV)などへのアクセスでキャッシュにヒットしなかった場合に増加するStallと考えられます。ただし、これらの値は、そのシェーダーを実行したときに発生したStallサイクル数の百分率で示される値なので、実際のStallサイクル数ではありません。マテリアルをソートした場合と、していない場合の、L2HitRateとTexHitRateを比較すると、マテリアルをソートしたほうが、HitRateは高いので、おそらくですが、Long ScoreboardによるStallサイクル数は、マテリアルをソートしたほうが少ないのではないかと考えられます。
 また、SM Active Threads Per Instruction Executedは、Warpが実行されたときに実際にActiveだったThreadの割合の平均値になります。実行分岐などでThreadがマスクされると、この値が低下します。ソートしなかった方が50%に対して、ソートした方が73%となっています。つまり同一Warp内のThreadが、より命令を同時に実行できる機会が多かったと考えることができ、簡単に考えるならば、同じ処理量のコードならば、ソートした方が約1.5倍の実行効率になっていると考えることができます。

更新:2019/05/31 NSight Graphics 2019.3 の GPU Trace を試す

 バージョン2019.3より、新しくGPUTraceという機能が入りました。せっかくなので、SortMaterialsをOn/Offした場合のプロファイルを行ってみたいと思います。まず、GPUTraceのキャプチャは対象となるプロセスにNSightを接続する前に選択する必要があります。したがって、FrameDebuggerやRnageProfilerと同時に使用することはできません。

そして、以下がSortMaterialsをOn/Offした場合のGPUTraceの結果です。
SortMaterials – On

SortMaterials – Off

 GPUTraceの特徴としては、各種パフォーマンスカウンタを、フレームのレンダリング中にリアルタイムにキャプチャします。Range Profiler のように、各DrawCallやDispatchごとの計測ではありません。したがって、GraphicsのCommandQueueとComputeのCommandQueueが同時実行されている状態や、resource barrierの状況などがそのままグラフに反映されます。上記の両者のグラフでは、ReflectionのDispatchRayの部分のPerformanceMarkerを選択しているので、始まりと終わりのTimeStampが確認できます。ソートしていない方は約18msで、ソートした方は10msとなっています。しかし。GPUTraceはリアルタイムに各種カウンタを取得するため多少のオーバーヘッドがあるので、TimeStampの計測に使うのには向いていません。あくまで参考値、相対的な指標として捉えるべきです。続いて、右側のSummaryタブには、選択したマーカーの区間の平均値が表示されます。Unit ThroughputとWarp Occupancy(SM Occupancy)の値が確認できます。
 まずSM Occupancyの値ですが、ソートの有無に関わらず、両者ともCompute Warpが41%となっています。これは、GPUが同時に起動できる最大Warp数の41%のWarpが起動しているという意味になります。Active SM unused warpの値は54%となっており、Idle SMは3%となっています。この様子は、左側のグラフのSM Occupancyで確認することができます。選択したマーカーの区間は、オレンジと、暗い灰色でほぼ埋め尽くされています。Active SM unused warpは、Warpのスロットが確保されているにも関わらず、何らかの理由でWarpが起動していない部分です。理由を調べるには、下記のように、SM Warp Can’t Launchのグラフをチェックします。

 上記のように、グラフの上にマウスを合わせれば、その時点でのカウンタの値が確認できます。選択した範囲の平均値を見る場合は、右側のMetricsタブで確認します。パラメータが多いので、フィルタをかけて確認するのが良いと思います。WarpのLaunchに関する項目であれば、”Launch”と入力すれば十分だと思います。話を戻します。Warpが起動できない理由は、SM Compute Warp Can’t Launch, Register Limited.が54%となっています。これは、ComputeShaderが使用するRegisterの数が多いため、ComputeShaderのWarpの起動ができない状態を指します。RayTracingのシェーダは、ユーザー定義のShaderのレジスタ使用数を絞っても、Rayのステートや、コールグラフ、チェック中のAcceleration Structureの状態の保持などで、レジスタ使用数が多い傾向があります。比較的単純なShadow RayのDispatchと比較しても、SM Occupancyが極端に低いわけではないので、Payloadの大きさや、Shaderコードに問題があるわけでは無いようです。SMのOccupancyが低いと、直ぐに何か問題があるのではないかと考えてしまいがちですが、各種カウンタをチェックして正しく状態を理解することが必要です。
 次にUnit Throughputを確認します。ソートしていない場合は、VRAMが44%、次いでL2が25%となっています。ソートしてある方ではVRAMが57%で、L2が29%となっています。SM Throughputは両者とも10%以下となっています。残念ながら、SMの命令発行Stallの原因を示す、Can’t Issue Reasonsがまだ実装されていないため、SMのTrhoughputが低い原因をここで知ることができません。ただし、この記事の冒頭で、Range Profilerを使って調べた結果より、ソートしていない方では、Instructionのキャッシュミスと思われるSMのStallが観測されています。したがって、ソートしたものより、VRAM,L2のThoughputが低いのだと思われます。また、両社とも、VRAMのThroughputが高いので、L2のキャッシュミスが頻発していることが考えられます。L2のキャッシュがヒットしていれば、VRAMに対するリクエストは少なくなるはずです。このあたりは、L2 TrafficやL2 Read Hit Rateなどの項目を確認することで、確認することができます。

 次に、ソートした場合の1パス目にあたる、Gatherパス、つまりマテリアルのIDのみ確認するDispatchRaysのレンジを見てみます。

Timestampを確認すると、約1.2msとなっています。上記のReflectionRayと同じ数のRayを同じ方向に飛ばしているのですが、マテリアルにアクセスしないため、実行時間が大きく異なります。UnitThroughputを確認すると、マテリアルを読んでいる場合と比べ、SM Throughputが30%と高く、L2 Throughputも38%となっています。一方でVRAMは32%です。L2のHitRateを確認すると75%です。マテリアルのテクスチャにアクセスしないため、L2のHitRateが高く、VRAMのThroughputが低いです。また、シェーダーの実行分岐等も少ないと思われ、SM Trhoughputが30%と、Shadingを行ってるDispatchRayに比べて高いのが見て取れます。

まとめ

SortMaterailsを有効することによる、Reflection Rayのパフォーマンスの変化は顕著です。DXRのパフォーマンスは、シェーダーの複雑度とRayのコヒーレンシに大きく依存することがよくわかります。このシーンでは、HitGroupが578個あるので、これと同等以上の規模のシーンでは、おそらくSortMaterailsを有効にしたほうが良いと思われます。ただし、ソートする際の基準となるマテリアルIDは、ReflectionRayが最初に到達したサーフェースのマテリアルなので、マルチバウンスを行う設定では、注意が必要だと思います。