错误处理与调试#
GPU kernel 的失败方式与 CPU 代码不同。CUDA 工具链目前不支持
异常或栈展开,kernel 输出中没有栈回溯,也没有 println!。
当出现问题时,结果是静默数据损坏、硬件陷阱,或者主机上的
一个难以理解的驱动错误。本章涵盖 cuda-oxide 用于诊断和修复 kernel
问题的工具。
Kernel 出错时会发生什么#
GPU 错误分为三类:
故障模式 |
你看到的表现 |
示例 |
|---|---|---|
静默损坏 |
错误结果,无错误提示 |
竞争条件、差一索引 |
硬件陷阱 |
主机收到 |
|
启动失败 |
立即返回 |
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_vec或ctx.synchronize())才会出现在 主机上。缓冲区大小。 默认的 printf 缓冲区是 1 MiB。如果许多线程打印, 输出可能会被截断。通过
cudaDeviceSetLimit(cudaLimitPrintfFifoSize, size)增大。线程顺序。 来自不同线程的输出以任意顺序出现。
性能。 Printf 跨线程串行化——避免在热路径中使用。 将其用于调试,而不是日志记录。
格式转换。 该宏在编译时将 Rust
{}格式说明符转换为 C printf 等价物(%d、%f等)。
为什么不使用 println! 或 Debug?#
标准 Rust 格式化(fmt::Display、fmt::Debug、format!、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()];
}
}
内建函数 |
做什么 |
主机影响 |
|---|---|---|
|
条件为 false 时触发陷阱 |
|
|
无条件触发陷阱 |
|
|
发出 |
在 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 变体包括 Driver、Context、KernelCache、Scheduling、
Launch 和 Internal。
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 前端
断点工作流#
使用调试方式构建:
cargo oxide debug <example>在你的 kernel 上设置断点:
break vecadd运行:
run检查线程:
cuda thread、cuda block、cuda warp打印变量:
print idx、print *c_elem
对于编程式断点,在你的 kernel 代码中使用 debug::breakpoint()。
当 cuda-gdb 命中 brkpt 指令时,它会暂停执行并让你检查 GPU 状态。
小技巧
如果没有附加调试器,debug::breakpoint() 会崩溃 kernel。
使用编译时标志进行保护,或者仅在调试会话期间使用。
cargo oxide doctor -- 环境验证#
在调试 kernel 故障之前,验证你的环境是否正确设置:
cargo oxide doctor
Doctor 检查:
检查项 |
验证内容 |
|---|---|
Rust 工具链 |
带有必需组件的 Nightly 编译器 |
CUDA 工具包 |
找到 |
libNVVM |
|
nvJitLink |
|
libdevice |
|
LLVM |
|
Codegen 后端 |
找到 |
仅当 kernel 调用 CUDA libdevice 数学函数(sin、cos、exp、pow、
sqrt、...)时,libNVVM / nvJitLink / libdevice 检查才会触发。
如果你的 kernel 是纯算术,这三项失败是无害的。它们都随 CUDA 工具包
一起提供——无需单独下载。如果任何检查失败,doctor 会打印该组件的
标准安装位置。
cargo oxide pipeline -- 检查编译过程#
当 kernel 产生错误结果但没有错误提示时,检查编译流水线 以查看究竟生成了什么代码:
cargo oxide pipeline vecadd
这将打印完整的流水线输出:
MIR 收集 -- 收集器找到了哪些函数
dialect-mir-- 建模 Rust MIR 的 pliron IR(mem2reg前后)dialect-llvm-- 建模 LLVM IR 的 pliron IR(mir-lower后)文本 LLVM IR -- 序列化的
.ll文件最终 PTX -- 生成的汇编代码
环境变量#
进行更有针对性的检查:
变量 |
效果 |
|---|---|
|
详细编译器输出 |
|
在导入前转储 rustc MIR |
使用 Nsight Compute 进行性能分析#
对于性能调试,NVIDIA 的 Nsight Compute(ncu)提供
roofline 分析、内存吞吐量和 occupancy 指标:
ncu --set full ./target/release/my_example
cuda-oxide kernel 可以使用 debug::prof_trigger::<N>() 发出分析器触发器,
该触发器生成 pmevent 指令,Nsight Compute 和 Nsight Systems
可以捕获该指令用于时间线标注。
参见
Nsight Compute 文档 完整的性能分析工具包。
常见陷阱#
陷阱 |
症状 |
修复方法 |
|---|---|---|
输出缓冲区竞争条件 |
错误结果,非确定性 |
使用 |
缺少 |
读取到过期的共享内存数据 |
在写入和读取之间添加屏障 |
|
|
确保 |
裸指针越界 |
陷阱或静默损坏 |
使用 |
Kernel 中的 |
编译错误(fmt 不可用) |
使用 |
启动后忘记同步 |
主机读取到过期数据 |
调用 |
PTX 为错误架构构建 |
|
使用 |
调试决策树:kernel 问题分为三类(编译错误、运行时陷阱、静默损坏), 每类有不同的诊断工具。常见修复方法显示在底部。#