Skip to content
Go back

The Most Common Vulkan Mistakes

· Updated:

slides

拙訳

Vulkan --- The Beginnings

  • OpenGLやDX10以前では、データをアップロードして、ステートを切り替えて、描画コマンドを発行する
    • ドライバは巨大なステートマシンである
      • レンダリング処理はシングルスレッドでおこなわなければならない
    • コマンドのサブミッションは時間のかかる処理である
      • ドライバはフレームの終わりやコマンドバッファが満杯でないとサブミットしない
    • 毎フレームごとに異なるコマンドをサブミットする可能性がある
      • ドライバはあらかじめにコマンドバッファをベイクしたりできない
  • OpenGLやDX10時代のワークアラウンド
    • ストリーミングデータのアップロードに複数のレンダリングスレッドを使う
    • できるだけGPUにデータを生成する
    • ステートの構成によってドローコールをバッチする
    • 一部のハードウェアでは並列計算のためにコンピュートシェーダを使う
    • GPUバブルを減らすためにできるだけ多くのフレームを前もってレンダリングする
  • これらのワークアラウンドは最大の問題を解決するものではない
    • GPUは高度に非同期化されている
    • 数多くの種類のタスクを並列に処理するよう設計されている
      • 計算
      • DMA転送
      • ラスタライゼーション
      • その他(例えば、画像データ変換の高速化)
  • APIの立場からだと、
    • GPUへはひとつのレンダリングスレッドからのみ 処理チャンクwork chunk を実行するよう要求できる
    • アプリケーションは信用されていない --- API呼び出しの検証CPU時間が使われる
  • 実際の懸念事項
    • 段々とCPUに縛られたアプリケーションが市場に現れてきている
      • ドライバスレッドがCPU時間を消費している
      • アプリケーションの複雑さが増加している
    • クロスプラットフォームな方法でこれらに対処するのは簡単ではない
    • Tilers(モバイルのTiled Rendering)はOpenGL ESでフルパワーを活用できていない
    • ベンダ固有の解決法のみが存在する(例えば、Pixel Local Storage)
  • 以下のユースケースには言及しない
    • マルチGPUのサポート
    • VR

Vulkan --- Do I Need It?

  • Vulkanは先程述べた問題のすべてに対処する
    • コマンドキューファミリーの集まりとしてGPUを開示する
    • コマンドバッファ複数のスレッドからキューにサブミットすることができる
    • アプリケーションは以下の責任を持つ
      • 正しいコマンドキュー処理チャンクをサブミットすること
      • GPUジョブの実行同期を取ること
    • メモリヒープの集まりとして利用可能なGPUメモリを開示する
    • アプリケーションは フラッシュflush 、 無効化invalidation 、管理に対する責任を持つ
  • アプリケーション動いているGPUの能力に合わせる必要がある
  • 作法を間違えるmisbehaveと、GPUがハングする
  • Vulkanを必要なケース
    • CPUに縛られたアプリケーション:
      • 情報の大多数がコンピュートやレンダリングで要求される --- ロード時に事前にベイクする
      • たった2つのコマンドでフレームがレンダリングできる!
      • ドライバ側の検証なし = 本当に重要なことにより多くのCPU時間を割ける
    • GPUに縛られたアプリケーション:
      • 以下によりGPU使用率が改善する
        • 関連するキューファミリーにコンピュート及びグラフィクスジョブをサブミットする
        • トランスファーキューでVRAM-VRAM間やRAM-VRAM間のコピー処理を行う
      • 突然のパフォーマンス低下スパイクがなくなる
        • 予測可能な時間で、アプリケーションが指定した情報に従い、すべてのGPU側のキャッシュフラッシュされる
        • ドライバは 当てずっぽうguess work をしなくて良くなる
  • Vulkanを必要とするかもしれないケース
    • 既存のGL4.xやDX11以下のアプリケーション:
      • Vulkanに移行すれば、パフォーマンス的な利益がもたらされるかもしれないし、されないかもしれない
      • かかるCPUパワーが減る可能性は高い
  • Vulkanを必要としないケース
    • 迅速な開発期間を要求するプロトタイプアプリケーション:
      • 検証レイヤはまだ仕様のすべてを網羅していない
      • 多くの間違ったユースケースは依然として発見されていない
      • 急勾配な学習曲線
    • CPUにもGPUにも縛られていないような単純なアプリケーション:
      • 学ぶことを目的とする場合を除いて、Vulkanから利益を得る可能性は低い

Vulkan --- Problematic Areas: Introduction

  • 我々(AMD)のドライバは世に出て数カ月1経った程度
  • トップレベル観察:
    • Vulkanを使うのは、アプリ側でも時間的にも、負担が大きいdemanding
    • アプリケーションがAというGPUで動作しても、BというGPUで動作するとは限らない
    • 基本的な落とし穴:
      • バリア
      • 正しいデータのアップロード
      • イメージ遷移
      • レンダパス
    • ISVは一般的には検証レイヤを使うことに 消極的reluctant である
      • 使ってください。多くの時間を節約してくれます

Vulkan --- Problematic Areas: Command Queues

  • CPU側:
    • Vulkanにレンダリングスレッドはない
    • 複数のスレッドからGPU側のコマンドキュー処理チャンクをサブミットできる
  • GPU側:
    • コマンドキューは実行できるコマンドのタイプでグループ化される
  • 問題点:
    • コマンドキューの数はハードウェア依存
    • キューファミリーの数はハードウェア依存
  • これの何が問題か?
    • 効率的にGPUタスクを配ることは今やVulkanアプリケーションの責任である
    • 解決法はデバイスの能力に応じてアップスケールダウンスケールできなければならない
    • オープンソースな解決法はまだない
    • Vulkan 1.0ではひとつのコンピュート+グラフィクスのキューファミリーのみ保証されている
    • 単純なアプリケーションはユニバーサルキューの存在ひとつsolely に頼る可能性が高いだろう
    • ただし、パフォーマンス最優先で書かれたVulkanアプリケーションではない
  • 解決法:
    • さまざまなVulkan実装であなたのレンダリングエンジンをテストする

Vulkan --- Problematic Areas: Command Buffers

  • Vulkanでは、コマンドバッファは
    • GPU側で実行されるコマンドを保持している
    • アプリケーションによって別途明示しない限り、再利用可能である
  • 問題点:
    • アプリケーションはしばしばフレームごとにコマンドバッファ記録し直す
  • これの何が問題か?
    • 多くのCPU時間を無駄にする
    • 多くの場合では必要ない
  • 解決法:
    • レンダリングロジックに影響を与えるすべてのパラメータイメージストレージバッファユニフォームバッファに移動する
    • 必要であれば、各スワップチェーンイメージごとに一度だけすべてのコマンドバッファ事前にベイクする
    • コマンドバッファの再利用性を改善するなら、インダイレクトディスパッチやインダイレクト描画のコマンドを使う

Vulkan --- Problematic Areas: Memory Management

  • メモリ管理もまたVulkanアプリケーションの責任である:
    • 物理デバイスは1以上のメモリヒープを報告する
    • メモリヒープは:
      • プラットフォーム固有のサイズを持つ
      • device-localであるかもしれないが、必ずしも必要ではない
    • メモリヒープ --- アプリケーションが直接アクセス可能ではない
    • 代わりに、ドライバはハードウェア固有の”メモリタイプの配列を開示している
    • GPUメモリを割り当てるとき、Vulkanアプリケーションはメモリタイプのインデックスを指定する
  • 辛い所は?
    • Vulkanとアプリケーションとの契約はとても薄い
    • 以下が保証されている
      • 少なくとも1つのメモリタイプhost-visibleかつhost-coherentである
      • 少なくとも1つのメモリタイプdevice-localである
    • バッファメモリイメージメモリの割り当てはドライバ固有のメモリタイプ由来でなければならない
    • そのタイプは以下に依存して変化するかもしれない
      • オブジェクトの特性
      • オブジェクトのタイプ
  • 最も辛い所は?
    • ISVはmaxMemoryAllocationCountの制限を無視する傾向にある
    • 同時に生存できる割り当ての限界最大最小数4096である
      • 複雑なアプリケーションなら達するのは非常に容易い
      • デスクトップのVulkan実装で報告される普通の値である
  • 解決法:
    • 利用可能なGPUメモリをアプリケーション側事前に割り当て管理する
    • 小さなメモリ割り当てを避けて大きなものから副割り当てを行う

Vulkan --- Problematic Areas: Descriptor Pools

  • 大多数のシェーダ外部データにアクセスする
  • Vulkanでは:
    • デスクリプタを経由して開示される
    • デスクリプタは直接生成できない
    • 代わりに、アプリケーションによってインスタンス化されたデスクリプタプールから取り出される
  • 問題点:
    • maxSetsがISVが期待しているようには働かない
  • 時折見られる誤解:
    • デスクリプタは”maxSets * {poolSizeCount * pPoolSizes}個割り当てられるよね”
    • “できないの?アンタんとこのドライバはクソだ。Xのドライバではできてるのに!”
  • 正確な理解:
    • 最大maxSetsのデスクリプタセットに最大N事前割り当てされたデスクリプタを配布する

Vulkan --- Problematic Areas: Sparse Descriptor Bindings

  • デスクリプタは後の使用のためにデスクリプタセットにグループ化される
    • デスクリプタタイプとバインディングの関係はデスクリプタセットレイアウトで定義される
    • GPUが消費する実際のバッファイメージコマンドバッファにバインドされる
  • デスクリプタセットレイアウトはvkCreateDescriptorSetLayoutで生成される
  • 問題点:
    • デスクリプタセットレイアウトが以下のデスクリプタセットをどのようにして探すすべきか
      • バインディング0: ストレージバッファ
      • バインディング2: ストレージイメージ
    • バインディング1に対してVkDescriptorSetLayoutBindingのアイテムを含める必要があるか否か?
  • 解決法:
    • このアプリケーションは非効率であり、ダミーバインディングはパフォーマンス悪影響を及ぼす
    • ただし、本当にそのバインディングが必要であるならやってもいい
    • 使わないバインディングのdescriptorCountを0に設定していることを確かめる

Vulkan --- Problematic Areas: Images

  • Vulkanでは、
    • テクスチャステートイメージオブジェクトに格納される
    • テクスチャデータメモリオブジェクトに格納され、イメージオブジェクトにバインドされる
  • イメージオブジェクトはイメージデータの指定の特性により生成される:
    • 以下のようなビットや数値:
      • タイプ(1D、2D、3D)
      • ベースのMIPMAPサイズ
      • MIPMAP数
    • タイリングタイプ
    • 使い方フラグ
    • その他

Vulkan --- Problematic Areas: Image Usage Flags

  • 生成時前もってのイメージの使い方の宣言を要求する
    • 使い方は1つ以上のビットの組み合わせである
  • ドライバはあるイメージの使い方に対するフォーマットのサポートを提供しなくても良い
  • そのときは、使い方設定は以下に制限する:
    • サポートされるメモリタイプ
    • 最大イメージ解像度サンプル数、など
  • 共通の問題点:
    • アプリケーションは不正確なイメージの使い方を指定する
  • :
    • VK_IMAGE_USAGE_TRANSFER_DST_BITを伴ってイメージを生成することを検討する
    • イメージはカラーアタッチメントとして使用してはならない
    • アプリケーションが気にしない
  • 結果:
    • 未定義動作
  • 解決法:
    • 検証が有効化されていれば、この種の問題は簡単に検出できる

Vulkan --- Problematic Areas: Image Tiling

  • タイリング設定はGPUによって使われるイメージデータレイアウトを決定する:
    • Linear: イメージは行優先row-major の配置で、各行は潜在的にパッディングされる
    • Optimal: プラットフォーム固有のデータ配置で、速度に最適化される
  • 線形タイル化linearly-tiledイメージ特性:
    • 最適タイル化optimally-tiledイメージに提供される機能のサブセットをサポートする
    • パフォーマンス的には劣る
  • なぜ線形イメージに悩まされるのか?
    • GPUでレンダリングされたイメージデータを 読み戻すread back のに必要である場合に極めて重要crucial
  • 共通の問題点:
    • ISVはデータを最適タイル化イメージ直接コピーする
  • 典型的なシナリオ:
    • イメージAVK_IMAGE_TILING_OPTIMALのタイリング設定で生成される
    • アプリケーションはイメージAに対してvkGetImageSubresourceLayoutを呼ぶ
    • アプリケーションは”報告された”特徴を用いてデータをアップロードすることを試みる
  • 解決法:
    • 最適タイル化イメージにデータをコピーするためにステージングバッファを使う:
      1. バッファオブジェクト生成して、メモリ領域バインドする
      2. データで埋める
      3. イメージをGENERALTRANSFER_DST_OPTIMALのレイアウトに遷移する
      4. vkCmdCopyBufferToImageの呼び出しによりコピー処理をスケージューリングする
      5. コマンドバッファサブミットして、実行が完了するまで待つ
      6. 一時的バッファオブジェクト解放する
    • 覚えておくこと: バッファからイメージへのコピー処理マルチサンプリングイメージでは働かない
    • そこにデータをアップロードするためには、実際のディスパッチ描画の呼び出しを使う必要があるだろう

Vulkan --- Problematic Areas: Image Layout Transitions

  • GPUは 実行中にon the fly データを圧縮・展開したり再配置したりするかもしれない
    • 帯域幅のプレッシャーが小さくなるので、パフォーマンスがよくなる
    • DX11以下やOpenGL: 透明でヒューリスティック駆動な処理
    • Vulkan: イメージレイアウトの遷移時に起こる
    • : DCC(Delta Color Compression)
  • ハードウェアレベルの最適化:
    • ハードウェアのアーキテクチャハードウェアの世代異なる
    • 一般にベンダ固有である
  • Vulkanでは:
    • イメージは使用前に正しいレイアウト移動されなければならない
    • これは以下によって要求することができる
      • コマンドバッファイメージバリアを注入する
      • 正しいレンダパスサブパスの構成
    • 間違うと、視覚的な破損visual corruptionが起こるかもしれない

(画像)

  • 一般的な問題:
    1. イメージ不正なレイアウト遷移する
      • 例:シェーダで読みたいのに、前のバリアがTRANSFER_DST_OPTIMALである

(画像)

  • 一般的な問題: 2. イメージバリアで定義される前のレイアウトが不正確である。 - 例:前のバリアをSHADER_READ_ONLY_OPTIMALにしたのに、後ろのバリアをTRANSFER_DST_OPTIMALのままになっている

(画像)

  • 一般的な問題: 3. “なあ、AMDさんよ、俺のアプリケーションはY社のドライバでは動くんだが、お前んとこのドライバではダメなんだよなぁ!” - いくつかのベンダはイメージバリアを無視しているけど、我々(AMD)はそれをしていない - すると、間違っているのは誰のドライバでしょうか?:)
  • 解決法:
    • 検証レイヤは絶えず改善している --- 使おう!
    • 色んなVulkan実装でソフトウェアをテストしよう

Vulkan --- Problematic Areas: Image Layout Transitions & Renderpasses

  • 一般的な問題: 4. ISVはレンダパスがとうやってイメージのサブリソースを遷移するかを誤解している - レンダパスはVulkanにおける 新しくnovel複雑な 概念である - ドライバに”時間旅行travel in time”させ、先んじて知らせておくために導入される - どのカラーまたは深度ステンシルアタッチメントラスタライズされ、アクセスされるか(いつどうやって?) - どのイメージのサブリソース範囲同期される必要があるか(いつどうやって?) - どのレイアウトへイメージのサブリソース遷移されるべきか、そしてそれはいつか - これは間違えてしまうほどに情報が膨大である。手動で記述するときは特に :)

レンダパス: 各”レンダリングパス”はユーザ指定のサプバスで記述される)

レンダパス: 実行順はユーザ指定の依存関係から推定される)

レンダパス: イメージ遷移サブパスレンダパスの構成から推定される)

Vulkan --- Problematic Areas: GPU-Side Synchronization

  • 超一般的uber-generalなVulkanのGPU側コマンド実行のルール:
    1. コマンドキューそれぞれ独立に実行する
    2. キューAにサブミットしたとき、コマンドバッファは特定の順序で実行する
    3. 順序はバリアレンダパス同期プリミティブによって強要されない限り、
      • サブミットされたコマンドは並列実行されるかもしれない
      • サブミットされたコマンドは 順序に関係なくout of order実行 されるかもしれない
  • 以下の同期オブジェクト使える:
    • イベント(**キュー内intra-queue**同期)
    • セマフォ(**キュー間inter-queue**同期)
    • フェンス(サブミットしたジョブチャンクが実行し終わるまで、CPUスレッドをブロックする)
  • 問題:
    • ISVは時々毎フレームに同期オブジェクトを生成する
  • 解決法:
    • すべてのコスト避けること!
    • 以下を覚えておく
      1. イベントCPU側GPU側リセットさせることができる
      2. フェンスCPU側リセットさせることができる
      3. セマフォは正常に待った後で自動的リセットさせることができる
    • さらに実行可能であるならば、先んじてスワップチェーン毎のイメージに同期オブジェクト一式をベイクする

Footnotes

  1. 訳注:この文書がまとめられたのは2016年3月頃