NVIDIA OptiX でレイトレーシングアプリをデバッグ
NVIDIA は、GPU でのレイトレーシング開発で発生する複雑なデバッグ課題を解決するための「OptiX Toolkit」の主要機能であるエラーコードの一貫した検証とデバイス側デバッグ印刷の詳細を紹介している。
キーポイント
OptiX デバッグの難易度とツール必要性
GPU の数千もの並列スレッドに埋もれたバグや無効な API 引数、黒いフレーム出力など、OptiX アプリケーションのデバッグは極めて困難であり、専用のデバッグ機能が必要である。
OptiX Toolkit (OTK) の概要とライセンス
OTK は GitHub で公開されているユーティリティセットで、GPU レイトレーシングアプリケーションの一般的なワークフローをサポートし、BSD 3 クロースタイルライセンスにより自由にコピーや修正が可能である。
API エラーコードの一貫した検証
エラーを早期に検知して後続の失敗が元の問題を隠蔽するのを防ぐため、OptiX および CUDA API の戻り値を体系的にチェックするメカニズムが紹介されている。
デバイス側デバッグ印刷とログ機能
OptiX デバイスコンテキスト作成時に設定可能なログコールバックや検証モード(OPTIX_DEVICE_CONTEXT_VALIDATION_MODE_ALL)を活用し、人間 readable なエラーメッセージを取得する手法が解説されている。
統一されたエラーコード処理パターン
OptiX、CUDA Runtime API、および CUDA Driver API はすべて、エラーコードの列挙型、文字列表記取得関数、および人間 readable なメッセージ取得関数を共通してサポートしています。
OTK による一貫したエラーチェックポリシー
各呼び出し手で手動でエラーをチェックする手間を省くため、OTK は例外をスローする「OTK_ERROR_CHECK」またはエラーメッセージを出力して継続する「OTK_ERROR_CHECK_NOTHROW」という2つのマクロを提供しています。
デバッグに有利なマクロとインライン関数の設計
実際の処理はインライン関数に委譲しているため、エラー検出時にブレークポイントを設定して実行を停止させることが可能で、マクロ自体はエラー発生のコード箇所に関する診断情報を提供します。
重要な引用
Applications using OptiX can fail in ways that are difficult to diagnose: an invalid API argument, a black frame, or a GPU-side bug buried under thousands of concurrent threads.
OTK is licensed with a BSD 3-clause style license, so you are free to copy and modify any of the code.
It's best to detect errors as early as possible as detailed in this section, before subsequent failures mask the original problem.
Handling these error codes manually at every API call site is tedious and error-prone.
The macros exist to provide diagnostic information about the code that caused the error.
Error messages are formatted as follows: file(line): expr failed with error nnn (name): message
影響分析・編集コメントを表示
影響分析
この記事は、高性能レンダリングやリアルタイムレイトレーシングを開発するエンジニアにとって、デバッグプロセスを効率化し、複雑な GPU バグの特定時間を短縮するための具体的なガイドラインを提供します。特に大規模並列処理環境でのトラブルシューティングにおいて、標準的なログ出力だけでなく、ツールを活用した体系的アプローチの重要性を再認識させる内容です。
編集コメント
GPU におけるレイトレーシング開発は、その複雑さゆえにデバッグが最大のボトルネックになりがちですが、このツールキットの活用により、開発者の負担を大幅に軽減できる可能性があります。特に並列処理特有のバグを特定する際の体系的なアプローチは、実務経験豊富なエンジニアにとっても有益な知見です。
NVIDIA OptiX は、GPU 上で最適なレイ tracing パフォーマンスを実現するためのアプリケーションフレームワークです。OptiX を利用するアプリケーションは、診断が難しい形で失敗することがあります。例えば、無効な API 引数の指定や黒いフレームの出力、あるいは数千もの並行スレッドに埋もれた GPU 側のバグなどが挙げられます。
NVIDIA OptiX Toolkit (OTK) のデバッグ機能は、こうした課題を解決する助けになります。OTK は GitHub リポジトリ上に公開されており、GPU レイ tracing アプリケーションで一般的なワークフローをサポートするユーティリティ群を含んでいます。ライセンスは BSD 3 クロースタイルであり、コードの複製や改変も自由に行えます。
本記事では、OTK の 2 つのデバッグ機能について解説します。1 つ目は OptiX および CUDA API のエラーコードを常にチェックする仕組み、2 つ目はデバイス側での特定の箇所に絞ったデバッグ出力です。OTK には、デバイス側のデバッグ出力が実際にどう使われるかを示すサンプルプログラム DemandPbrtScene も含まれています。
OptiX のログと検証はどのように動作するのか?
OTK が提供する支援について詳しく語る前に、OptiX のログ機能に関する背景知識をいくつか確認しておきましょう。OptiX デバイスコンテキストを作成する際、オプション構造体を指定することで、ログコールバックの設定や検証モードの切り替えが可能です。検証モードを「OPTIX_DEVICE_CONTEXT_VALIDATION_MODE_ALL」に設定すると、API 関数への入力値の検証を OptiX が支援してくれます。
OptiX は検証エラーに関する人間が読みやすいメッセージをログに出力します。API エラー(OPTIX_ERROR_INVALID_VALUE など)が発生した際は、まずこのログを確認するのが最も効果的です。
ただし、検証機能には若干のオーバーヘッドがかかります。デバッグビルドやテストビルドではすべての検証機能を有効にし、リリースビルドではこれを無効化することが推奨されます。詳細については OptiX SDK サンプル をご覧ください。
エラーコードを確実に確認する方法
このセクションで詳述しているように、エラーはできるだけ早期に検出することが重要です。後続の失敗が元の問題を隠してしまい、原因特定が難しくなるのを防ぐためです。
API の仕組み
OptiX のほとんどの関数は、エラーコードを示す OptixResult を返します。この値がゼロ以外の場合、エラーが発生していることを意味します。CUDA ランタイム API や CUDA ドライバー API も同様のパターンを採用しています。
これら 3 つの API は、以下の機能を共通してサポートしています:
- エラーコード用の列挙型(例:
OptixResult) - エラーコードに対応する文字列名を返す関数(例:
OPTIX_ERROR_INVALID_VALUE) - エラーコードに対する人間が読みやすいメッセージを返す関数(例:"Invalid value")
これら 3 つの API では関数のシグネチャにわずかな違いがありますが、エラー処理の仕組み自体は同じです。
これらのエラーコードを API 呼び出しの都度手動で処理するのは、手間がかかるうえにミスも起こりやすいものです。エラー処理の一貫したポリシーを適用するためには、マクロや関数呼び出しを利用するのが良いでしょう。
OTK では、エラー検出時に2つのポリシーを実装するマクロを提供しています。
- OTK_ERROR_CHECK(expr): 例外をスローします
- OTK_ERROR_CHECK_NOTHROW(expr): std::cerr にメッセージを出力し、処理を続行します
提供されている機能の一部を再利用して適切なマクロを作成すれば、他のポリシーも容易に実装できます。
マクロ機構の最小限の利用
このエラーチェック機構は、実際の処理をインライン関数に委譲するために、マクロの使用を最小限に抑えています。インライン関数の定義にブレークポイントを設定しておけば、関数がエラーを検出した際にデバッガーが実行を停止させることができます。
マロが存在する目的は、エラーの原因となったコードに関する診断情報を提供することです。
- expr: マクロに渡された引数の文字列表現。これはエラーコードを評価する式そのものです。
- __FILE__: マクロが呼び出されたソースファイル名。
- __LINE__: マクロが呼び出された行番号。
これらのマクロ呼び出し元からの情報は、実際のエラーチェックを行うインライン関数に渡されます。
API 横断的な統一エラーチェック
インラインテンプレート関数 checkError は、エラーのステータスコードを確認し、失敗を検知した場合は診断メッセージを生成します。今回取り上げる 3 つの API では、エラーコードを bool に単純に変換するだけでエラーの有無を示すことができます。これら 3 つの API はいずれも、成功にはステータスコード 0 を、失敗には非ゼロ値を使用しています。
エラーメッセージは以下の形式でフォーマットされます。
file(line): expr failed with error nnn (name): message
ここで、expr は評価された式、nnn はステータスコードを int に変換した結果、name はステータスコードの記号名、message は人間が読みやすいエラーメッセージです。name または message のいずれかが空の場合、その部分は省略されます。
このメッセージを構築するのは、インラインテンプレート関数 makeErrorString です。makeErrorString は、getErrorName と getErrorMessage という 2 つのインラインテンプレート関数を呼び出して、結合されたメッセージを作成します。
各 API にはステータスコード専用の型が用意されているため、テンプレート関数を特殊化して、拡張エラー情報を取得するための適切な API 呼び出しを実行できます。
Usage
OTK は、記述したテンプレート関数の必要な特殊化を提供するヘッダーを API ごとに 1 つずつ用意しています(表 1)。
*表 1. エラーチェック用ヘッダー*
使用している API のヘッダーをインクルードし、すべての呼び出し箇所で単一のマクロ OTK_ERROR_CHECK を使用するだけで済みます。以下の例では、3 つの API すべてを使用しています。
OTK_ERROR_CHECK( cudaSetDevice( m_deviceIndex ) );
OTK_ERROR_CHECK( cuCtxGetCurrent( &m_cudaContext ) );
OTK_ERROR_CHECK( cuStreamCreate( &m_stream, CU_STREAM_DEFAULT ) );
OTK_ERROR_CHECK( optixInit() );
ターゲットを絞ったデバイス側デバッグ出力の実行方法
グラフィックアプリケーションで黒い画面が表示される原因は、コードの書き方が多岐にわたるため特定が難しいという問題があります。
OptiX のデバイスコードで発生した問題をデバッグするには、いくつかのアプローチが取れます。まず思い浮かぶのは以下の2つです。
- デバイスコードをデバッグビルドでコンパイルし、CUDA デバッガを使用する
- デバイスコードのリリースビルドから printf を使って情報を取得する
多くのアプリケーションは、デバッグモードでコンパイルすると動作が極端に遅くなるため、インタラクティブなデバッガの使用が困難になります。
printf 形式でのデバッグにおける主な課題は、GPU で同時に実行されるスレッド数が膨大であるため、出力情報が洪水のように溢れかえってしまう点です。さらに、問題が発生するのはアプリケーションとの特定の操作が行われた後だけの場合もあります。視覚的な不具合が現れる前のデバッグ出力は、必要な情報を見つけるためのノイズに過ぎず、邪魔になるだけです。
DebugLocation
ヘッダーファイル <OptiXToolkit/ShaderUtil/DebugLocation.h> には、デバッグ出力を再利用可能な仕組みとして提供されています。この構造体 DebugLocation が動作を制御します。
struct DebugLocation
{
bool enabled;
bool dumpSuppressed;
bool debugIndexSet;
uint3 debugIndex;
};
有効化メンバーは、この機構全体をオンまたはオフに切り替えます。dumpSuppressed メンバーは、機構が有効な状態であってもデバッグ出力を抑制する役割を持ちます。また、debugIndexSet メンバーは、debugIndex に有効な起動インデックスが格納されていることを示します。
この機構がデバッグ情報を出力するのは、以下のすべての条件が満たされた場合です。
- enabled が true であること
- dumpSuppressed が false であること
- debugIndexSet が true であること
- 現在の起動インデックスが debugIndex と一致すること
対話式でデバッグ出力を制御するためには、OptiX パイプラインの起動パラメータに DebugLocation 構造体のインスタンスを含める必要があります。
デバッグ情報のダンプ関数
template 関数 debugInfoDump は、デバッグ情報を出力するためのインターフェースを提供します:
template <typename Callback>
static __forceinline__ __device__
bool debugInfoDump( const DebugLocation& debug,
const Callback &callback )
Callback テンプレートパラメータには、以下の構造体またはクラスに準拠したものを指定する必要があります。
struct Callback
{
void setColor( float red, float green, float blue );
void dump( const uint3& index );
};
setColor メソッドは、デバッグ対象の位置を視覚的にボックスで囲み、画面内のどの地点に対して情報をダンプしているかを容易に識別できるようにするためのものです。一般的な使用法では、現在の起動インデックスに対応する出力ピクセルの色を設定します。もしデバッグ位置の視覚的な表示が必要ない場合は、このメソッドは空の実装でも問題ありません。
dump メソッドは、アプリケーションが必要と考える情報を指定された起動インデックスに基づいて出力するために使用されます。
デバッグ位置の表示
この機能を有効にすると、コールバック構造体の setColor メソッドが画面にボックスを描画し、現在のデバッグ位置を示します。これは dump 出力を抑制している場合でも同様です。無効化された場合はボックスは非表示になります。
デバッグ位置には赤いピクセルがあり、その周囲には幅 1 ピクセルの黒枠、さらにその外側に幅 1 ピクセルの白枠が描かれます。これにより、dump メッセージが発生している場所を高いコントラストで明確に示すことができます。出力バッファが従来のカラーバッファでない場合でも、提供される赤・緑・青の値を視覚化に適した独自の値に自由にマッピングできます。
1 回限りのモード (One-shot mode)
デバッグ出力が多量になりすぎないよう、ユーザー操作に応じて一度だけ出力を行う one-shot#One-shot_timer) モードを利用すると便利です。このモードは以下のようにシークエンスできます。
- デバッグ位置 (DebugLocation) メカニズムを有効にする
- 通常通り起動する
- ユーザーが対話的にデバッグ位置を選択した時点で、dumpSuppressed を true に設定し、debugIndexSet を true にして debugIndex に選択された位置を指定する
- その後の起動ではデバッグ位置が表示されますが、メカニズムによる dump 出力は行われません
- ユーザーはアプリケーションと対話しながら適切な状態に操作します。この過程でデバッグ位置を移動させることも可能です
ユーザーが現在の位置に関するデバッグ情報を必要と示した場合は、dumpSuppressed を false に設定します。
その後、デバッグ出力を取得するために実行を開始し、完了後に dumpSuppressed を再び true に戻してください。
DemandPbrtScene の例
OTK 内の DemandPbrtScene の例 は、pbrt バージョン 3 のシーン に対するデマンドローディング(必要時の読み込み)機能のジオメトリを実装しています。この例では、DebugLocation メカニズムを活用しており、一度きりの動作やデバッグ出力のインタラクティブな切り替え、さらにデバッグ位置の選択といった機能を備えています。UI フレームワークには ImGui を採用しています。
image*図 1. デバッグピクセルが強調表示された DemandPbrtScene のデバッグコントロール*
この例を実行するには、pbrt-v3 用のシーンファイル が必要です。
NVIDIA OptiX Toolkit を用いたデバッグの開始
OptiX Toolkit は、一般的な OptiX 開発の問題に対する再利用可能なデバッグおよびテストユーティリティを提供します。具体的には、一貫性のある API エラーチェックと、ターゲットを絞ったデバイス側のデバッグ出力機能が含まれています。これらのコードは、NVIDIA/optix-toolkit の GitHub リポジトリから入手可能です。
さあ、始めましょうか。GitHub から OptiX Toolkit をダウンロード して、デバッグビルドで OptiX の検証機能を有効化してください。CUDA や OptiX の呼び出しには OTK_ERROR_CHECK をラップし、開発の早い段階で GPU 側のバグを特定するために DebugLocation を活用しましょう。
OTK は BSD 3 クロースタイルの寛容なライセンスの下で提供されているため、これらのユーティリティをコピーして適応させ、独自の OptiX アプリケーションに直接統合することができます。
原文を表示
NVIDIA OptiX ray tracing engine is an application framework for achieving optimal ray tracing performance on the GPU. Applications using OptiX can fail in ways that are difficult to diagnose: an invalid API argument, a black frame, or a GPU-side bug buried under thousands of concurrent threads.
Debugging facilities in the NVIDIA OptiX Toolkit (OTK) can help. OTK is a GitHub repository containing a set of utilities supporting workflows common in GPU ray tracing applications. OTK is licensed with a BSD 3-clause style license, so you are free to copy and modify any of the code.
This post covers two OTK debugging facilities: consistent checking of OptiX and CUDA API error codes and targeted device-side debug printing. OTK includes an example program, DemandPbrtScene, that shows device-side debug printing used in context.
How do OptiX logging and validation work?
Before getting to the assistance provided by OTK, some background on the OptiX log would be useful. When you create an OptiX device context, you supply an options structure that allows you to configure a log callback and a validation mode. OptiX can assist in validating inputs to API functions when the validation mode is set to OPTIX_DEVICE_CONTEXT_VALIDATION_MODE_ALL.
OptiX writes human-readable messages for validation errors to the log. The log output should be the first place you look for API errors like OPTIX_ERROR_INVALID_VALUE. Validation does impose some additional cost in the API. It’s recommended to enable all validation in debug and testing builds and omitting validation for release builds. For more details, see the OptiX SDK samples.
How to consistently check return codes for errors
It’s best to detect errors as early as possible as detailed in this section, before subsequent failures mask the original problem.
API mechanisms
Most functions in OptiX return an OptixResult error code that, when non-zero, indicates an error. The CUDA runtime API and the CUDA driver API follow a similar pattern. All three APIs support the following:
- A distinct enumerated type for the error code; for example OptixResult
- A function to return a symbolic name for an error code as a string; for example OPTIX_ERROR_INVALID_VALUE
- A function to return a human-readable error message for an error code; for example, Invalid value
The signatures of these functions are slightly different for the three APIs, but the mechanisms are the same.
Error checking policy
Handling these error codes manually at every API call site is tedious and error-prone. It is better to use macros or function calls to enforce a consistent policy for handling errors. OTK provides macros that implement two policies when an error is detected:
- OTK_ERROR_CHECK( expr ): Throw an exception
- OTK_ERROR_CHECK_NOTHROW( expr ): Print a message to std::cerr and continue
Other policies are easily implemented by reusing some of the provided machinery and creating an appropriate macro.
Minimal use of macro machinery
This error checking mechanism uses macros to a minimal extent and delegates to inline functions to do the actual work. You can set breakpoints in the inline function definitions to have the debugger stop execution when the function detects an error.
The macros exist to provide diagnostic information about the code that caused the error:
- expr: A string form of the supplied argument to the macro. This is the expression that evaluates to the error code.
- __FILE__: The name of the source file where the macro was invoked.
- __LINE__: The line number within the source file where the macro was invoked.
This information from the macro call site is passed to the inline function that does the actual error checking.
Unified error checking across APIs
The inline template function checkError checks the status code for an error and creates a diagnostic message if it detects a failure. In the case of these three APIs, a simple cast of the error code to bool suffices to indicate an error. All three APIs use a status code of zero to indicate success and non-zero to indicate failure.
Error messages are formatted as follows:
file(line): expr failed with error nnn (name): message
Where expr is the evaluated expression, nnn is the result of casting the status code to an int, name is the symbolic name of the status code, and message is the human-readable error message. If either the name or message are empty, the function omits them.
The inline template function makeErrorString is responsible for building this message. It calls the inline template functions getErrorName and getErrorMessage to build the combined message.
Each API has a distinct type for API status codes, so you can specialize the template functions to perform the appropriate API call to get the extended error information.
Usage
OTK provides one header per API that provides the necessary specializations of the template functions described (Table 1).
Simply include the headers for the APIs you are using and use the single macro OTK_ERROR_CHECK around all call sites. The following example uses all three APIs.
OTK_ERROR_CHECK( cudaSetDevice( m_deviceIndex ) );
OTK_ERROR_CHECK( cuCtxGetCurrent( &m_cudaContext ) );
OTK_ERROR_CHECK( cuStreamCreate( &m_stream, CU_STREAM_DEFAULT ) );
OTK_ERROR_CHECK( optixInit() );
How to perform targeted device-side debug printing
The problem with graphics applications is that there are too many ways to code up a black screen.
To debug problems in your OptiX device code, you can take several approaches. A couple of obvious choices spring to mind:
- Use the CUDA debugger on a debug build of the device code
- Get information from a release build of the device code with printf
Many applications run too slowly when compiled in debug mode, impairing the use of interactive debuggers.
The main difficulty with printf-style debugging is that there are so many threads running concurrently on the GPU that you can end up drowning in a firehose of output. In addition, it could be that the issue only arises after a certain amount of interaction with the application. Debug output prior to the problem manifesting visually is just noise that gets in the way of finding the desired information.
DebugLocation
The header <OptiXToolkit/ShaderUtil/DebugLocation.h> provides a reusable mechanism for debug output. The structure DebugLocation controls the behavior:
struct DebugLocation
{
bool enabled;
bool dumpSuppressed;
bool debugIndexSet;
uint3 debugIndex;
};
The enabled member turns the entire mechanism on or off. The dumpSuppressed member turns off the debug output even if the mechanism is enabled. The debugIndexSet member indicates that a valid launch index has been stored in debugIndex.
The mechanism will output debug information when the following conditions are true:
- enabled is true
- dumpSuppressed is false
- debugIndexSet is true, and
- The current launch index matches debugIndex
Include an instance of the DebugLocation structure in the launch parameters of the OptiX pipeline for interactive control of debug output.
The debugInfoDump function
The template function debugInfoDump provides the interface for emitting debug information:
template <typename Callback>
static __forceinline__ __device__
bool debugInfoDump( const DebugLocation& debug,
const Callback &callback )
The Callback template parameter should be a struct or class matching the following:
struct Callback
{
void setColor( float red, float green, float blue );
void dump( const uint3& index );
};
The setColor method is used to draw a visual box around the debug location for easy identification of the point on the screen for which information is dumped. The typical usage is to set the color for the output pixel corresponding to the current launch index. If no visual indication of the debug location is desired, the method can simply be empty.
The dump method is used to print whatever information the application considers relevant at the supplied launch index.
Displaying the debug location
When enabled, the setColor method on the callback structure draws a box on screen to indicate the current debug location, even when dump output is suppressed. The box is hidden when disabled.
A red pixel at the debug location sits inside a one-pixel-wide black box, itself inside a one-pixel-wide white box. This provides a high-contrast indicator of the location where dump messages are occurring. If the output buffer is not a traditional color buffer, you are free to map the supplied red, green, and blue values to some distinctive value for visualization.
One-shot mode
To avoid drowning in debug output, it’s useful to have the debug output in a one-shot#One-shot_timer) mode, where the output is dumped once in response to user control. You can sequence this mode as follows:
- Enable the DebugLocation mechanism
- Launch as usual
- When the user interactively selects the debug location, set dumpSuppressed to true, debugIndexSet to true, and debugIndex to the selected location
- Subsequent launches display the debug location, but the mechanism provides no dumps
- The user interacts with the application to manipulate it into the appropriate state, possibly moving the debug location along the way
- When the user indicates that they want debug information for the current location, set dumpSuppressed to false
- Launch to get the debug output
- Set dumpSuppressed back to true after launch
DemandPbrtScene example
The DemandPbrtScene example in OTK demonstrates demand loaded geometry for pbrt version 3 scenes. It uses the DebugLocation mechanism, including one-shot behavior and interactive toggling of debug output and interactive selection of the debug position. It uses ImGui as the UI framework.

To run this example from OTK, you will need a scene file for pbrt-v3.
Get started debugging with NVIDIA OptiX Toolkit
The OptiX Toolkit provides reusable debugging and testing utilities for common OptiX development problems: consistent API error checking and targeted device-side debug output. The code is available through the NVIDIA/optix-toolkit GitHub repo.
Ready to get started? Download the OptiX Toolkit from GitHub and start by enabling OptiX validation in your debug builds, wrapping your CUDA and OptiX calls with OTK_ERROR_CHECK, and using DebugLocation to isolate GPU-side bugs earlier in development. OTK is available under a permissive BSD 3-clause-style license, so you can copy, adapt, and integrate these utilities directly into your own OptiX applications.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み