错误处理与调试#

GPU kernel 的失败方式与 CPU 代码不同。CUDA 工具链目前不支持 异常或栈展开,kernel 输出中没有栈回溯,也没有 println!。 当出现问题时,结果是静默数据损坏、硬件陷阱,或者主机上的 一个难以理解的驱动错误。本章涵盖 cuda-oxide 用于诊断和修复 kernel 问题的工具。

Kernel 出错时会发生什么#

GPU 错误分为三类:

故障模式

你看到的表现

示例

静默损坏

错误结果,无错误提示

竞争条件、差一索引

硬件陷阱

主机收到 CUDA_ERROR_ILLEGAL_INSTRUCTION

gpu_assert! 失败、panic、越界访问

启动失败

立即返回 DriverError

Grid 维度错误、缺少模块、资源不足

CUDA 工具链目前不暴露异常机制(硬件可以支持,但 nvcc/ptxas 没有 将其连接起来)。陷阱指令会杀死 kernel 并污染 CUDA context—— 同一 context 上的后续操作将失败,直到你处理或重新创建该 context。

gpu_printf! -- 从 GPU 打印#

gpu_printf! 让你可以从设备代码打印值以进行快速调试。它 使用 CUDA 内置的 vprintf 机制:

use cuda_device::{kernel, thread, gpu_printf, DisjointSlice};

#[kernel]
pub fn debug_kernel(data: &[f32], mut out: DisjointSlice<f32>) {
    let idx = thread::index_1d();
    if idx.get() < 4 {
        gpu_printf!("Thread {} sees value {}\n", idx.get(), data[idx.get()]);
    }
    if let Some(out_elem) = out.get_mut(idx) {
        *out_elem = data[idx.get()] * 2.0;
    }
}

重要细节#

  • 刷新需要同步。 输出在 GPU 上缓冲,仅在 stream 或设备同步 之后(例如 to_host_vecctx.synchronize())才会出现在 主机上。

  • 缓冲区大小。 默认的 printf 缓冲区是 1 MiB。如果许多线程打印, 输出可能会被截断。通过 cudaDeviceSetLimit(cudaLimitPrintfFifoSize, size) 增大。

  • 线程顺序。 来自不同线程的输出以任意顺序出现。

  • 性能。 Printf 跨线程串行化——避免在热路径中使用。 将其用于调试,而不是日志记录。

  • 格式转换。 该宏在编译时将 Rust {} 格式说明符转换为 C printf 等价物(%d%f 等)。

为什么不使用 println!Debug#

标准 Rust 格式化(fmt::Displayfmt::Debugformat!println!) 需要动态分发、字符串分配和 I/O——这些在 GPU 上都不存在。 gpu_printf! 通过直接降低为 CUDA vprintf 调用避免了所有这些。

gpu_assert!trap()#

对于设备端的致命错误检查,使用 gpu_assert!debug::trap()

use cuda_device::{kernel, thread, debug, gpu_assert, DisjointSlice};

#[kernel]
pub fn checked_kernel(data: &[f32], len: u32, mut out: DisjointSlice<f32>) {
    let idx = thread::index_1d();
    gpu_assert!(idx.get() < len as usize);   // 如果为 false 则触发陷阱

    if let Some(out_elem) = out.get_mut(idx) {
        *out_elem = data[idx.get()];
    }
}

内建函数

做什么

主机影响

gpu_assert!(condition)

条件为 false 时触发陷阱

CUDA_ERROR_ILLEGAL_INSTRUCTION

debug::trap()

无条件触发陷阱

CUDA_ERROR_ILLEGAL_INSTRUCTION

debug::breakpoint()

发出 brkpt 指令

在 cuda-gdb 中暂停;无调试器时崩溃

陷阱与检查模式#

用于捕获设备端错误的常见工作流:

// 启动 kernel
module.vecadd(&stream, config, &a, &b, &mut c).expect("Launch failed");

// 同步并检查陷阱
stream.synchronize().expect("Kernel trapped -- check gpu_assert! conditions");

如果 gpu_assert! 触发,同步会返回一个错误。错误消息 不会告诉你哪个断言失败了,所以在断言旁边使用 gpu_printf! 来缩小问题范围。

主机端错误处理#

DriverError#

同步启动路径返回 Result<(), DriverError>DriverError 包装了一个 CUDA 驱动结果码:

match module.vecadd(&stream, config, &a, &b, &mut c) {
    Ok(()) => { /* 启动成功 */ }
    Err(e) => eprintln!("Launch failed: {e}"),
}

DeviceError#

异步路径({kernel}_async / DeviceOperation)使用 DeviceError, 它包装了驱动错误以及 context 和调度失败:

use cuda_async::error::DeviceError;

let result: Result<Vec<f32>, DeviceError> = operation.sync();

DeviceError 变体包括 DriverContextKernelCacheSchedulingLaunchInternal

CudaContext::check_err#

在一系列操作之后,在 context 上调用 check_err() 来暴露任何 可能已被记录的异步错误:

ctx.check_err().expect("Asynchronous GPU error detected");

cargo oxide debug -- cuda-gdb 集成#

cargo oxide debug 使用调试信息构建你的 kernel 并启动 cuda-gdb:

cargo oxide debug vecadd          # 标准 GDB
cargo oxide debug vecadd --tui    # 带 TUI 的 GDB
cargo oxide debug vecadd --cgdb   # cgdb 前端

断点工作流#

  1. 使用调试方式构建:cargo oxide debug <example>

  2. 在你的 kernel 上设置断点:break vecadd

  3. 运行:run

  4. 检查线程:cuda threadcuda blockcuda warp

  5. 打印变量:print idxprint *c_elem

对于编程式断点,在你的 kernel 代码中使用 debug::breakpoint()。 当 cuda-gdb 命中 brkpt 指令时,它会暂停执行并让你检查 GPU 状态。

小技巧

如果没有附加调试器,debug::breakpoint()崩溃 kernel。 使用编译时标志进行保护,或者仅在调试会话期间使用。

cargo oxide doctor -- 环境验证#

在调试 kernel 故障之前,验证你的环境是否正确设置:

cargo oxide doctor

Doctor 检查:

检查项

验证内容

Rust 工具链

带有必需组件的 Nightly 编译器

CUDA 工具包

找到 nvcc 且版本兼容

libNVVM

libnvvm.so(CUDA 工具包)可加载 -- 用于 libdevice 数学 kernel

nvJitLink

libnvJitLink.so(CUDA 工具包)可加载 -- 用于 libdevice 数学 kernel

libdevice

libdevice.10.bc 可发现 -- 用于 libdevice 数学 kernel

LLVM

llc(21+)可用于 PTX 生成

Codegen 后端

找到 librustc_codegen_cuda.so(运行 cargo oxide setup 来构建)

仅当 kernel 调用 CUDA libdevice 数学函数(sincosexppowsqrt、...)时,libNVVM / nvJitLink / libdevice 检查才会触发。 如果你的 kernel 是纯算术,这三项失败是无害的。它们都随 CUDA 工具包 一起提供——无需单独下载。如果任何检查失败,doctor 会打印该组件的 标准安装位置。

cargo oxide pipeline -- 检查编译过程#

当 kernel 产生错误结果但没有错误提示时,检查编译流水线 以查看究竟生成了什么代码:

cargo oxide pipeline vecadd

这将打印完整的流水线输出:

  1. MIR 收集 -- 收集器找到了哪些函数

  2. dialect-mir -- 建模 Rust MIR 的 pliron IR(mem2reg 前后)

  3. dialect-llvm -- 建模 LLVM IR 的 pliron IR(mir-lower 后)

  4. 文本 LLVM IR -- 序列化的 .ll 文件

  5. 最终 PTX -- 生成的汇编代码

环境变量#

进行更有针对性的检查:

变量

效果

CUDA_OXIDE_VERBOSE=1

详细编译器输出

CUDA_OXIDE_SHOW_RUSTC_MIR=1

在导入前转储 rustc MIR

使用 Nsight Compute 进行性能分析#

对于性能调试,NVIDIA 的 Nsight Computencu)提供 roofline 分析、内存吞吐量和 occupancy 指标:

ncu --set full ./target/release/my_example

cuda-oxide kernel 可以使用 debug::prof_trigger::<N>() 发出分析器触发器, 该触发器生成 pmevent 指令,Nsight Compute 和 Nsight Systems 可以捕获该指令用于时间线标注。

参见

Nsight Compute 文档 完整的性能分析工具包。

常见陷阱#

陷阱

症状

修复方法

输出缓冲区竞争条件

错误结果,非确定性

使用 DisjointSlice 代替原始 *mut T

缺少 sync_threads()

读取到过期的共享内存数据

在写入和读取之间添加屏障

shared_mem_bytes 不正确

LAUNCH_OUT_OF_RESOURCES 或垃圾数据

确保 LaunchConfig 与实际 DynamicSharedArray 使用量匹配

裸指针越界

陷阱或静默损坏

使用 DisjointSlice::get_mut 进行边界检查

Kernel 中的 panic!("message")

编译错误(fmt 不可用)

使用 gpu_assert!debug::trap()

启动后忘记同步

主机读取到过期数据

调用 to_host_vecstream.synchronize().sync()

PTX 为错误架构构建

NO_BINARY_FOR_GPU

使用 cargo oxide build --arch sm_XX 重新构建

gpu-programming/images/debug-workflow.svg

调试决策树:kernel 问题分为三类(编译错误、运行时陷阱、静默损坏), 每类有不同的诊断工具。常见修复方法显示在底部。#