NVIDIA OptiX 光线追踪引擎是一个可在 GPU 上实现最优光线追踪性能的应用框架。使用 OptiX 的应用可能会以难以诊断的方式失败:无效的 API 参数、黑屏,或埋藏在数千并发线程下的 GPU 端错误。

NVIDIA OptiX 工具包 (OTK) 中的调试功能可以提供帮助。OTK 是一个包含一组支持 GPU 光线追踪应用常见工作流的实用程序的 GitHub 仓库。OTK 使用 BSD 3-clause 风格的许可证,因此您可以自由复制和修改任何代码。

本文将介绍 OTK 的两个调试功能:对 OptiX 和 CUDA API 错误码进行一致性检查,以及有针对性的设备端调试打印。OTK 包含一个示例程序 DemandPbrtScene,展示了在实际场景中使用的设备端调试打印。

OptiX 日志和验证是如何工作的?

在介绍 OTK 提供的帮助之前,了解 OptiX 日志的一些背景知识会很有用。当您创建 OptiX 设备上下文时,需要提供一个选项结构体,用于配置日志回调和验证模式。当验证模式设置为 OPTIX_DEVICE_CONTEXT_VALIDATION_MODE_ALL 时,OptiX 可以协助验证 API 函数的输入。

OptiX 会将验证错误的易读消息写入日志。日志输出应该是您查找 API 错误(如 OPTIX_ERROR_INVALID_VALUE)的首选位置。验证确实会增加 API 的额外开销。建议在调试和测试构建中启用所有验证,而在发布构建中省略验证。更多详情,请参阅 OptiX SDK 示例

如何一致地检查返回码以发现错误

正如本节详述的,最好尽可能早地检测错误,以免后续故障掩盖原始问题。

API 机制

OptiX 中的大多数函数返回一个 OptixResult 错误码,当该码非零时表示发生错误。CUDA 运行时 APICUDA 驱动 API 遵循类似的模式。这三个 API 都支持以下功能:

  • 用于错误码的独立枚举类型;例如 OptixResult
  • 返回错误码符号名称的字符串的函数;例如 OPTIX_ERROR_INVALID_VALUE
  • 返回错误码易读错误消息的函数;例如 Invalid value

这三个 API 的函数签名略有不同,但机制相同。

错误检查策略

在每个 API 调用点手动处理这些错误码既繁琐又容易出错。最好使用宏或函数调用来强制执行一致的错误处理策略。OTK 提供了在检测到错误时实现两种策略的宏:

通过重用提供的部分机制并创建适当的宏,很容易实现其他策略。

宏机制的最小化使用

此错误检查机制仅在最小程度上使用宏,并委托内联函数执行实际工作。您可以在内联函数定义中设置断点,以便在函数检测到错误时让调试器停止执行。

宏的存在是为了提供导致错误代码的诊断信息:

  • expr:宏提供的参数的字符串形式。这是计算得出错误码的表达式。
  • __FILE__:宏被调用的源文件名。
  • __LINE__:宏被调用的源文件中的行号。

来自宏调用点的信息将被传递给执行实际错误检查的内联函数。

跨 API 的统一错误检查

内联模板函数 checkError 检查状态码以发现错误,并在检测到故障时创建诊断消息。对于这三个 API,将错误码简单地强制转换为 bool 就足以指示错误。这三个 API 都使用零状态码表示成功,非零表示失败。

错误消息的格式如下:

file(line): expr failed with error nnn (name): message

其中 expr 是已计算的表达式,nnn 是状态码强制转换为 int 的结果,name 是状态码的符号名称,message 是易读的错误消息。如果名称或消息为空,函数将省略它们。

内联模板函数 makeErrorString 负责构建此消息。它调用内联模板函数 getErrorNamegetErrorMessage 来构建组合消息。

每个 API 都有不同的 API 状态码类型,因此您可以特化模板函数以执行适当的 API 调用来获取扩展的错误信息。

用法

OTK 为每个 API 提供一个头文件,其中包含所描述模板函数的必要特化(表 1)。

表 1. 错误检查头文件

只需包含您正在使用的 API 的头文件,并在所有调用点使用单个宏 OTK_ERROR_CHECK。以下示例使用了所有三个 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 设备代码中的问题,您可以采取几种方法。几个显而易见的选择浮现在脑海中:

  • 对设备代码的调试构建使用 CUDA 调试器
  • 使用 printf 从设备代码的发布构建中获取信息

许多应用在调试模式下编译时运行得太慢,影响交互式调试器的使用。

printf 风格调试的主要困难是 GPU 上有太多线程同时运行,以至于最终会淹没在输出的“消防水带”中。此外,问题可能是在与应用进行一定交互之后才出现的。问题在视觉上显现之前的调试输出只是噪音,会妨碍找到所需信息。

DebugLocation

头文件 <OptiXToolkit/ShaderUtil/DebugLocation.h> 提供了一种可重用的调试输出机制。结构体 DebugLocation 控制行为:

struct DebugLocation
{
    bool  enabled;
    bool  dumpSuppressed;
    bool  debugIndexSet;
    uint3 debugIndex;
};

enabled 成员打开或关闭整个机制。dumpSuppressed 成员即使在机制启用时也关闭调试输出。debugIndexSet 成员表示已在 debugIndex 中存储了有效的启动索引。

当以下条件为真时,机制将输出调试信息:

  • enabled 为真
  • dumpSuppressed 为假
  • debugIndexSet 为真,并且
  • 当前启动索引与 debugIndex 匹配

在 OptiX 管线的启动参数中包含 DebugLocation 结构体实例,以实现调试输出的交互式控制。

debugInfoDump 函数

模板函数 debugInfoDump 提供了发出调试信息的接口:

template <typename Callback>
static __forceinline__ __device__
bool debugInfoDump( const DebugLocation& debug,
                    const Callback &callback )

Callback 模板参数应为符合以下条件的 structclass

struct Callback
{
    void setColor( float red, float green, float blue );
    void dump( const uint3& index );
};

setColor 方法用于在调试位置周围绘制一个可视框,以便于识别屏幕上转储信息的点。典型用法是为与当前启动索引对应的输出像素设置颜色。如果不需要调试位置的可视指示,该方法可以为空。

dump 方法用于在提供的启动索引处打印应用认为相关的任何信息。

显示调试位置

启用时,回调结构上的 setColor 方法会在屏幕上绘制一个框以指示当前调试位置,即使在转储输出被抑制时也是如此。禁用时框会被隐藏。

调试位置的红色像素位于一个像素宽的黑色框内,该黑色框本身位于一个像素宽的白色框内。这提供了一个高对比度的指示器,指示转储消息发生的位置。如果输出缓冲区不是传统的颜色缓冲区,您可以自由地将提供的红、绿、蓝值映射到某些不同的值进行可视化。

单次触发模式

为了避免淹没在调试输出中,最好让调试输出处于 单次触发 模式,即在用户控制下转储一次输出。您可以按如下方式排序此模式:

  1. 启用 DebugLocation 机制
  2. 正常启动
  3. 当用户交互式选择调试位置时,将 dumpSuppressed 设置为 truedebugIndexSet 设置为 true,并将 debugIndex 设置为所选位置
  4. 后续启动会显示调试位置,但机制不提供转储
  5. 用户与应用交互以将其操作到适当状态,可能在此过程中移动调试位置
  6. 当用户指示想要当前位置的调试信息时,将 dumpSuppressed 设置为 false
  7. 启动以获取调试输出
  8. 启动后将 dumpSuppressed 恢复为 true

DemandPbrtScene 示例

OTK 中的 DemandPbrtScene 示例 展示了 pbrt 版本 3 场景 的按需加载几何体。它使用了 DebugLocation 机制,包括单次触发行为以及调试输出和调试位置的交互式切换。它使用 ImGui 作为 UI 框架。

显示调试输出控件和图像右侧高亮调试像素的截图
图 1. DemandPbrtScene 调试控件与高亮调试像素

要从 OTK 运行此示例,您需要 pbrt-v3 的场景文件

OptiX 工具包为常见的 OptiX 开发问题提供了可重用的调试和测试实用程序:一致的 API 错误检查和有针对性的设备端调试输出。代码可通过 NVIDIA/optix-toolkit GitHub 仓库获取。

准备好开始了吗?从 GitHub 下载 OptiX 工具包,首先在调试构建中启用 OptiX 验证,使用 OTK_ERROR_CHECK 包装您的 CUDA 和 OptiX 调用,并使用 DebugLocation 在开发早期隔离 GPU 端错误。OTK 采用宽松的 BSD 3-clause 风格许可证,因此您可以直接将这些实用程序复制、改编并集成到您自己的 OptiX 应用中。