开发工具与技巧

使用 NVIDIA OptiX 工具包调试光线追踪应用

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

NVIDIA OptiX 工具包 (OTK) 中的调试工具可以提供帮助。OTK 是一个 GitHub 资源库,其中包含一组实用程序,支持 GPU 光线追踪应用中常见的工作流。OTK 具有 BSD 3 条款样式许可证,因此您可以自由复制和修改任何代码。

本文将介绍两种 OTK 调试工具:一致检查 OptiX 和 CUDA API 错误代码,以及定向设备端调试打印。OTK 包含 DemandPbrtScene 示例程序,该程序展示了在上下文中使用的设备端调试打印。

OptiX 日志记录和验证的工作原理是什么?

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

OptiX 会将人类可读的验证错误消息写入日志。日志输出应该是您查找 OPTIX_ERROR_INVALID_VALUE 等 API 错误的首要位置。验证确实会在 API 中增加一些额外成本。建议在调试和测试版本中启用所有验证,并省略版本验证。有关更多详细信息,请参阅 OptiX SDK 示例

如何始终如一地检查退货代码是否有错误

在后续故障隐藏原始问题之前,最好尽可能早地检测到本节中详述的错误。 

API 机制

OptiX 中的大多数函数都会返回 OptixResult 错误代码,该代码若为零,则表示错误。CUDA 运行时 API 和 CUDA 驱动程序 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) 。

标题 API
<OptiXToolkit/Error/cuErrorCheck.h> CUDA 驱动 API
<OptiXToolkit/Error/cudaErrorCheck.h> CUDA 运行时 API
<OptiXToolkit/Error/optixErrorCheck.h> OptiX API
表 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 为 true
  • dumpSuppressed 为 false
  • debugIndexSet 为 true
  • 当前启动索引与 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 设置为 true,将 debugIndexSet 设置为 true,并将 debugIndex 设置为所选位置
  4. 后续启动会显示调试位置,但该机制不会提供转储文件
  5. 用户与应用程序交互,将其操作为适当状态,可能会在此过程中移动调试位置
  6. 当用户表示需要当前位置的调试信息时,请将 dumpSuppressed 设置为 false
  7. 启动以获取调试输出
  8. 启动后,将 dumpSuppressed 设置回 true

DemandPbrtScene 示例

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

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

开始使用 NVIDIA OptiX 工具套件进行调试

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

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

标签