想象你是一位大厨,正在一个繁忙的厨房里准备一顿盛大的宴席。你手下的帮厨(GPU)有上千个,每个人负责处理一道菜的某个步骤——切菜、焯水、摆盘。一切看起来井然有序,但问题来了:当某个帮厨不小心把盐当成了糖,或者在切菜时切到了手指,整个宴席可能就毁了。更麻烦的是,这位帮厨并不会直接跑到你面前大喊“我出错了!”——他只会默默地在自己的工位上举起一块小牌子,然后继续做下一道菜。如果你不去逐桌检查那些牌子,你可能直到客人吃到甜味的红烧肉时,才发现出了问题。 这就是 CUDA 程序调试的本质:GPU 上的数千个线程同时工作,但任何一个线程出错,都不会像 CPU 程序那样立即崩溃并告诉你“第几行出错了”。 错误信息会被静默地记录下来,等待你主动去查询。就像大厨需要建立一套巡查制度——定期检查帮厨们举起的牌子,甚至用摄像头(调试工具)回放整个烹饪过程——CUDA 程序员也需要一套系统化的错误处理和调试流程。 在这篇文章中,你将学会:如何在 CUDA 代码中“巡查”错误码、如何理解错误传播的“连锁反应”、如何用 assert 宏在代码中埋设“报警器”、以及如何用 cuda-gdb 和 cuda-memcheck 这两大“监控摄像头”来诊断最难缠的内存问题。掌握了这些,你就从只会“做菜”的程序员,升级成了能“管理整个厨房”的系统级工程师。

岗位关联

CUDA 错误处理是 GPU 程序员从“能跑通”走向“跑得稳”的必修课。它决定了你能否在真实项目中快速定位 bug、避免数据静默损坏——这项能力直接区分了初级 CUDA 开发者和资深并行计算工程师。

目录
  1. 可视化演示
  2. 错误码基础
  3. 错误获取API
  4. 错误传播机制
  5. 断言assert宏
  6. cuda-gdb调试
  7. cuda-memcheck
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示密码学哈希函数的雪崩效应:输入仅改变1个比特,输出哈希值产生约50%的比特变化,确保安全性。

本节配有交互动效(进入视口后自动加载,约 3KB)。

哈希雪崩效应 输入A和输入B仅在第16位不同(0 vs 1) 经过哈希函数后,输出有9/16位发生变化 Avalanche = changed_bits / total_bits = 9/16 = 56%

错误码基础

想象你是一家餐厅的大厨(CPU),手下有几百个帮厨(GPU 线程)。每个帮厨都有一个随身携带的“状态手册”——一本记录着各种工作结果的小本子。当帮厨遇到问题时,他会在本子上记下一个数字:0 表示“一切正常”,非零数字则对应不同的故障类型。这本“状态手册”就是 CUDA 中的 cudaError_t 类型,而那个数字就是错误码。

在 CUDA 编程中,几乎每个运行时 API 函数都会返回一个 cudaError_t 类型的值。这个值就像帮厨本子上记的数字:如果一切顺利,它等于 cudaSuccess(也就是 0);如果出了问题,它就是一个非零的错误码,告诉你具体发生了什么。常见的错误码包括 cudaErrorInvalidValue(传入了无效参数)、cudaErrorMemoryAllocation(显存分配失败)、cudaErrorIllegalAddress(非法内存访问)等等,每个都像一条不同的“故障记录”。

为什么这个设计如此重要?因为 GPU 是“异步世界”——大厨下达指令后并不会立刻看到结果,而是继续忙别的。如果每个错误都立即打断大厨,效率会极低。错误码机制让问题被“记录”下来,等你有空时再查。

1. 函数返回与错误码:每个 CUDA 函数执行后都会返回一个 cudaError_t,这是错误处理的入口。

cudaError_t∈{0,1,2,… }\text{cudaError\_t} \in \{0, 1, 2, \dots\}

2. 成功即零:cudaSuccess 的值为 0,表示没有错误发生。

cudaSuccess≡0cudaSuccess \equiv 0

3. 失败非零:每个非零值对应一种具体错误类型,与 #define 枚举常量一一对应。

cudaErrorInvalidValue=1,cudaErrorMemoryAllocation=2,…cudaErrorInvalidValue = 1, \quad cudaErrorMemoryAllocation = 2, \dots

回到大厨的比喻:当帮厨在本子上记下错误码后,大厨并不需要立刻知道——他只需要定期翻看本子,就能掌握厨房里的所有异常。错误码是 CUDA 错误处理的第一层防线:让“错误”这个东西变得可记录、可查询、可分类。没有这一层,后续的所有调试工具都将失去基础。

cpp4 行
cudaError_t err = cudaMalloc(&d_ptr, size);if (err != cudaSuccess) {    // 这里就是"查手册"的时刻}

错误获取API

大厨虽然有“状态手册”可以翻,但手册可能记录了从上一次巡查以来的所有异常——有些是新的,有些是旧的,甚至有些已经过时了。CUDA 提供了几个“翻阅手册”的工具:cudaGetLastError 就像翻到最后一页,看到最近一条记录;cudaGetErrorString 则是把那条数字记录翻译成人话:“哦,原来是显存分配失败”。

核心 API 有两个:

| API | 作用 | 比喻 |

|-----|------|------|

| cudaGetLastError() | 获取并清除最近一次错误 | 撕掉最新一页记录 |

| cudaGetErrorString(code) | 将错误码转为可读字符串 | 把数字翻成菜谱文字 |

关键点在于:cudaGetLastError 具有“读取即清除”的语义——它返回最近一次错误,但同时会将该错误重置为 cudaSuccess,就像大厨撕掉了那页“最新故障记录”,确保下次翻到的必须是新记录。这就是为什么推荐的检查模式是:

这里的技巧是:在核函数启动后立即检查。因为核函数启动本身是异步的,启动瞬间只会捕获“参数错误”这类同步错误;而核函数执行过程中的内存错误则需要后续调用(如 cudaDeviceSynchronize)来捕获。

1. 获取并清除错误:cudaGetLastError() 返回最近一次错误码,并将错误标志重置为 cudaSuccess。

err=cudaGetLastError()\text{err} = cudaGetLastError()

2. 错误码翻译:cudaGetErrorString(err) 将数字错误码映射为人类可读的字符串描述。

message=char*(cudaGetErrorString(err))\text{message} = \text{char*}(cudaGetErrorString(err))

3. 组合使用:先获取,再翻译,最后根据结果决定是继续还是终止。

printf(%s,cudaGetErrorString(err))\text{printf}\left(\%s, cudaGetErrorString(err)\right)

大厨的巡查哲学在此体现:不能只看“有错误”就完事,还要知道是什么错误、何时出现的错误。cudaGetLastError 和 cudaGetErrorString 的组合,就是你手中最基础的两件巡查工具——一个负责“看到”,一个负责“听懂”。

cpp6 行
kernel<<<grid, block>>>(args);cudaError_t err = cudaGetLastError();  // 检查核函数启动是否成功if (err != cudaSuccess) {    fprintf(stderr, "核函数启动失败: %s\n", cudaGetErrorString(err));    exit(1);}

错误传播机制

想象一下这样的场景:帮厨 A 在切菜时切到了手(内存越界),他举起了一块红色牌子。但大厨当时没看到,继续安排帮厨 B 去拿这盆被污染的菜。帮厨 B 拿到菜后觉得不对劲,但也说不出个所以然,只能把疑问也记在自己的小本子上。于是,错误就像多米诺骨牌一样,一个接一个地“传播”下去——最终你可能看到的报错,远不是最初的那个根因。

这就是 CUDA 错误传播的核心:GPU 上的错误是异步的、累积的。当一个内核启动失败后,后续的 CUDA API 调用仍然会执行,但错误标志会一直“卡”在那里,直到你显式调用 cudaGetLastError 或 cudaDeviceSynchronize 来重置它。更麻烦的是,有些错误只有在同步时才会暴露出来——比如设备端的内存访问违规,它不会在核函数启动时立刻返回,而是等到某个同步点才浮出水面。

这里的核心策略是在关键同步点放置错误检查。就像大厨会在每道菜上桌前的“总检查”节点上,统一检查所有帮厨的牌子。CUDA 提供了两种同步方式:隐式同步(如内存拷贝、显存分配)和显式同步(cudaDeviceSynchronize 阻塞直到所有设备任务完成)。

1. 异步错误累积:核函数执行中的错误不会立即返回给主机端调用点,而是累积在设备端错误标志中。

errdevice←accumulated\text{err}_{\text{device}} \leftarrow \text{accumulated}

2. 同步点捕获:当执行同步操作(如 cudaMemcpy)时,设备端错误才被传播回主机端。

errhost=cudaDeviceSynchronize()\text{err}_{\text{host}} = \text{cudaDeviceSynchronize()}

3. 错误标志重置:每次 cudaGetLastError 或 cudaDeviceSynchronize 成功返回后,错误标志恢复为 cudaSuccess。

err←0 after query\text{err} \leftarrow 0 \text{ after query}

回到多米诺骨牌的比喻:你要做的不是在每块骨牌倒下时都去扶——而是找到第一块倒下的骨牌。错误传播机制的理解,本质上就是要你学会“顺着倒下的骨牌链往回找源头”。只有掌握了错误何时产生、何时候传、何时暴露,你才能在复杂的并行程序大海中精准钓出那条“最初的鱼”。

cpp4 行
kernelA<<<grid, block>>>(ptr);   // 错误在这里产生(假设内存越界)cudaDeviceSynchronize();          // 同步点:错误被"揭发"cudaError_t err = cudaGetLastError();// 关键:即使这里不检查,下一个 API 调用仍可能返回同一个错误

断言assert宏

有时候,大厨希望帮厨在做菜的过程中就主动报告异常,而不是等事后再查本子。CUDA 为此提供了 assert 宏——这就像在每个帮厨的工作台上安装了一个“报警器”:如果某个条件不成立(比如切出来的菜大小不对),帮厨会立刻停下手中的活,拉响警报,此时整个程序的执行都会停止。

在设备端代码(核函数内)使用 assert 的基本形式如下:

这里的 assert 是 CUDA 内置的设备端断言指令,它在条件为假时会中断整个内核的执行,并返回 cudaErrorAssert 错误码。这个错误码可以通过前面学的 cudaGetLastError 或同步操作捕获。

关键陷阱在于:assert 只在未定义 NDEBUG 时生效。如果你在编译时加上了 -DNDEBUG 标志(通常用于发布版本),所有 assert 都会被编译器“无视”——代码里仿佛根本没有这些断言。这意味着:不要指望 assert 在发布版程序中保护你。

1. 条件判定:assert(expr) 在表达式 expr 为假时触发断言失败。

expr=false⇒trigger\text{expr} = \text{false} \Rightarrow \text{trigger}

2. 触发即停止:失败后所有线程停止执行,设备返回 cudaErrorAssert。

error=cudaErrorAssert=719\text{error} = cudaErrorAssert = 719

3. 编译开关:定义 NDEBUG 后 assert 被替换为空操作,不再影响性能。

NDEBUG⇒assert(⋅)→∅\text{NDEBUG} \Rightarrow \text{assert}(\cdot) \to \emptyset

报警器的隐喻在此延伸:你可以给厨房里放几个“关键检查点”(比如上菜前、加盐时),但注意报警器是给调试期用的——正式开业后,为了效率,你可能要拆掉一部分报警器。同样,assert 是开发期的好帮手,但在追求性能的发布版中,它会被裁剪掉,你需要用显式的错误检查来替代它。

cpp7 行
__global__ void kernel(int* data, int n) {    int idx = threadIdx.x + blockIdx.x * blockDim.x;    if (idx < n) {        assert(data[idx] >= 0);  // 如果 data[idx] 是负数,就报警        // ... 后续计算    }}
bash4 行
# 调试版本(assert 生效)nvcc -G -g kernel.cu -o debug_kernel# 发布版本(assert 被忽略)nvcc -DNDEBUG -O3 kernel.cu -o release_kernel

cuda-gdb调试

报警器只能告诉你“哪里出了问题”,但没法告诉你“为什么出了问题”。为此,大厨需要一套更精密的监控系统——就像厨房里的 CCTV 摄像头,可以回放每一分每一秒发生了什么。cuda-gdb 就是 CUDA 世界的 CCTV:它允许你在程序运行到某一行时暂停,检查每个线程的变量值、寄存器和内存状态。

cuda-gdb 是 NVIDIA 官方提供的 GPU 端调试器,它是标准 GDB 的超集,支持 CUDA 特有的调试功能。核心操作包括:

  • 设置断点:在主机端或设备端代码行设置断点
  • 线程切换:在设备断点处切换到特定线程(thread, thread apply)
  • 检查状态:查看变量、寄存器和共享内存内容
  • 死锁检测:当程序挂起时,cuda-gdb 可以显示所有线程的当前状态,帮助你识别死锁

典型调试流程如下(使用 CUDA 编译器选项开启调试信息):

在 cuda-gdb 内:

为什么需要手动切换线程?因为 GPU 上有成千上万个线程,调试器默认只会显示触发断点的那个线程的状态——但问题可能出在另一个线程上。这就像 CCTV 有几百个监控画面,你需要逐个切换观察,直到找到“肇事者”。

1. 断点设置:break line_number 在第 N 行设置断点,使特定线程在该处暂停。

break→pause⊥execution\text{break} \to \text{pause} \perp \text{execution}

2. 线程选择命令:thread N 切换到第 N 个线程上下文,查看该线程的局部变量。

threadN→scopevars(N)\text{thread}_N \to \text{scope}_{vars}(N)

3. 单步与继续:step 单步执行当前线程的下一行指令,continue 让程序继续运行到下一个断点。

step→instr+1,continue→untilnext\text{step} \to \text{instr}_{+1}, \quad \text{continue} \to \text{until}_{\text{next}}

CCTV 隐喻的最后一块拼图是死锁检测:当程序卡住不动时,你按 Ctrl+C 中断程序,然后输入 info threads 查看所有线程的状态——这会像 CCTV 的回放一样,把所有线程“卡在哪里”一一显示出来。如果多个线程都在互相等待对方的资源,你就可以清楚地看到这个“集体僵持”的画面,从而定位死锁的根本原因。

bash4 行
# 编译时加上 -g -G 生成调试信息nvcc -g -G -lineinfo kernel.cu -o kernel_debug# 启动 cuda-gdbcuda-gdb ./kernel_debug
text5 行
(gdb) break kernel 第 15 行(gdb) run(gdb) thread 5              # 切换到第 6 个线程(gdb) print idx              # 查看该线程的 idx 变量(gdb) continue

cuda-memcheck

有时候,问题并不在于逻辑错误,而在于内存破坏——某个线程越界写坏了一块数据,导致另一个完全不相关的变量变得古怪。这就像厨房里有人把调味料放错了柜子,下一个用的人取到“盐”却发现是“糖”。cuda-memcheck 就是一个专注扫描内存问题的“X 光机”,它会在程序运行时实时监测每一次内存访问。

cuda-memcheck 是 NVIDIA 提供的内存错误检测工具,不需要重新编译代码(但建议用 -g 生成调试信息以获得更精确的行号)。基本使用方式:

它会检测并报告以下典型问题:

  • 越界访问:读取或写入超出分配范围的内存
  • 未初始化访问:使用未初始化的变量
  • 内存泄漏:分配后从未释放的显存
  • 竞争条件:多个线程同时访问同一内存位置

cuda-memcheck 的强大之处在于它会在错误发生时立即捕获,并报告出错的线程 ID、访问地址和执行位置。这就好比 X 光机不仅告诉你“骨头断了”,还会告诉你“断在哪个坐标、谁弄断的”。

局限在于:运行速度会显著下降(通常慢 10-100 倍),所以它适合在调试阶段而不是生产环境使用。

1. 内存访问监测:cuda-memcheck 在每个内存指令处插入检查逻辑,判断访问是否越界。

check:addr∈[base,base+size)\text{check}: \text{addr} \in [\text{base}, \text{base} + \text{size})

2. 非法地址报告:当访问地址超出合法范围,工具输出错误报告,包含线程信息与访问地址。

illegal_addr→report(thread_id,addr)\text{illegal\_addr} \to \text{report}(thread\_id, addr)

3. 泄漏检测:程序结束时扫描未释放的显存块,生成泄漏摘要。

leak=allocated−freed>0\text{leak} = \text{allocated} - \text{freed} > 0

用 X 光机的比喻总结:cuda-memcheck 不能加速你的程序,但它能帮你“透视”那些看不见的内存问题。当你的程序出现随机崩溃、数据莫名被改写、结果不稳定等诡异症状时,它就是你的首选诊断工具。配合 cuda-gdb 的“定点检查”,你就有了一整套从“宏观扫描”到“微观定位”的完整调试武器库。

bash1 行
cuda-memcheck ./my_kernel

AI 应用场景

场景一:高性能计算集群中的错误快速定位

阶段:模型训练与推理的 GPU 加速阶段

解决的问题:在分布式训练中,成千上万个 GPU 同时工作,任何一个 GPU 上的 CUDA 错误都可能导致整个训练任务中断。传统的做法是等程序崩溃后再看日志,但错误信息往往淹没在海量日志中,定位根因耗时数小时甚至数天。通过系统化的 CUDA 错误处理——在每个同步点检查错误码、用 cuda-memcheck 扫描内存问题、用 cuda-gdb 定点分析——可以将故障定位时间从“小时级”压缩到“分钟级”。

具体案例:某 AI 公司训练大语言模型时,发现模型参数偶尔出现 NaN。工程师使用 cuda-memcheck 扫描后发现是某个自定义的 Kernel 在边界条件下发生了越界写操作,破坏了相邻的参数内存。定位并修复后,训练稳定性显著提升。

场景二:自动驾驶系统中的安全关键型 GPU 程序

阶段:实时推理与决策阶段

解决的问题:自动驾驶系统对 GPU 程序的正确性要求极高——一次静默的内存错误可能导致错误的安全决策。CUDA 错误处理成为安全验证流程的关键环节:开发阶段使用 assert 宏验证核心不变量,用 cuda-memcheck 确保无未定义行为,发布前用 cuda-gdb 做最后的逻辑审查。

具体案例:某自动驾驶公司在其感知模型的 CUDA 实现中,在每次核函数启动后检查 cudaGetLastError,并在关键数据流节点使用设备端 assert。一次测试中 assert 捕获到了索引越界问题,防止了潜在的安全事故。

Python 演示

python55 行
import pycuda.driver as cudaimport pycuda.autoinitimport numpy as np  class CudaErrorHandler:    """CUDA 错误处理助手:像大厨巡查帮厨一样检查每个 API 调用"""     def __init__(self):        self.last_error = cuda.Error(0)     def check(self, api_call, name=""):        """执行 CUDA API 调用并检查返回状态"""        result = api_call        self.last_error = cuda.Context.get_last_error()        if self.last_error != cuda.Error(0):            raise RuntimeError(f"[{name}] CUDA 调用失败: {self.last_error}")        return result     def get_error_string(self):        """将错误码翻译为可读信息"""        return str(self.last_error)  def safe_kernel_launch():    """演示:分配显存并检查错误"""    handler = CudaErrorHandler()     host_data = np.random.randn(100).astype(np.float32)    dev_data = cuda.mem_alloc(host_data.nbytes)    handler.check(cuda.memcpy_htod(dev_data, host_data), "memcpy_htod")     print("[OK] 显存分配与数据拷贝成功")    print("[INFO] 当前错误状态:", handler.get_error_string())     dev_data.free()  def simulate_error():    """演示:故意触发一个错误并捕获它"""    handler = CudaErrorHandler()     try:        # 尝试分配超出显存容量的空间(模拟资源不足错误)        huge_size = 1024 * 1024 * 1024 * 100  # 100 GB        cuda.mem_alloc(huge_size)    except cuda.MemoryError as e:        print(f"[捕获] 显存分配失败: {e}")        print("[处理] 已捕获错误,程序不会崩溃,执行错误恢复路径")  if __name__ == "__main__":    print("=== CUDA 错误处理演示 ===")    safe_kernel_launch()    simulate_error()
目的:用 Python/PyCUDA 演示 CUDA 错误处理的核心模式——检查 API 返回值、捕获异常、执行错误恢复路径。
  1. handler.check(api_call, name)封装了“检查每个 CUDA 调用的返回状态”这一核心模式,对应 C 语言中的 cudaGetLastError() 检查
  2. cuda.Context.get_last_error()获取并清除最近一次错误,相当于 C 语言中的 cudaGetLastError()
  3. raise RuntimeError(...)发现错误时立即抛出异常,阻止错误静默传播
  4. cuda.mem_alloc(huge_size)故意触发显存分配失败,演示错误捕获流程
  5. except cuda.MemoryError as e捕获特定类型的 CUDA 错误,执行恢复逻辑而不是崩溃

常见误区

误区核函数启动后调用一次 `cudaGetLastError`,如果返回 `cudaSuccess`,就认为程序没问题了。

正解核函数启动是异步的,启动时只检查“启动参数是否合法”这一类同步错误。核函数执行过程中发生的内存错误、断言失败等,只有在同步点(如 cudaDeviceSynchronize)或后续调用触发同步时才会暴露。好比大厨看了一眼帮厨举起的牌子,但牌子上写的可能是旧问题,新问题还在后面排队。正确做法是:在启动后立即查一次,再在关键同步点查一次。

为何常见:初学者容易把 CPU 的“函数调用同步返回”模型套用到 GPU 异步模型上,忽略了异步执行的本质。

误区在代码里加 `assert` 就可以在发布版中拦截所有非法状态。

正解assert 在 NDEBUG 定义时会被预处理阶段直接删除,发布版代码中根本不存在断言指令。就像厨房里的报警器在正式营业时被拆掉了。如果生产环境需要检查,应该用显式的 if 条件判断和错误码返回,而不是依赖 assert。这需要额外的代码设计,而不仅仅是写一个断言。

为何常见:C/C++ 程序员熟悉主机端 assert 的用法,但忽略了 CUDA 编译的 NDEBUG 机制与主机端是同一套宏定义体系。

误区跑一遍 `cuda-memcheck` 没报错,就说明程序完全正确。

正解cuda-memcheck 只检查特定类型的内存问题(越界、未初始化访问、泄漏等),它不检查逻辑错误、数值精度问题或死锁。它像 X 光机只能看骨骼,看不到肌肉拉伤。即使程序通过 cuda-memcheck 检查,仍然需要结合 cuda-gdb 和单元测试来验证逻辑正确性。

为何常见:工具名字里的 “memcheck“ 让人误以为它是”通用检查器”,实际上它专注于内存安全这一个维度。

误区`cudaGetLastError` 和 `cudaDeviceSynchronize` 会增加开销,去掉它们程序更快。

正解错误检查的开销极小(通常为微秒级),远小于错误发生后导致程序崩溃、数据损坏带来的调试成本。更重要的是,cudaDeviceSynchronize 会强制等待设备完成所有任务——这确实是同步开销,但它是你主动选择的行为。如果不做同步,程序可能“看起来快”但实际上跑的是错误结果。这就像大厨不巡查,虽然省了几分钟,但端上桌的菜可能是坏的。

为何常见:性能焦虑让程序员倾向于减少一切“非必要”代码,但他们低估了错误排查的时间成本远高于那微秒级的检查开销。

误区调用了 `cudaGetLastError` 之后就再也查不到这个错误了,所以不敢调用。

正解cudaGetLastError 确实会清除错误标志,但这正是它的设计意图——防止同一个错误被重复处理。就像大厨撕掉旧记录是为了区分新旧问题。如果你担心“破坏现场”,应该先检查完(比如记录错误码),再进行后续处理。标准模式是:查一次、记下来、根据记录处理。错误不会被销毁,它已经通过返回值传递给了你的变量。

为何常见:对“读取即清除”语义的误解,源于没有意识到返回值已经把错误信息“复制”出来了,而不是凭空消失。

面试题

Q1. cudaSuccess 的数值是多少?

单选题 · easy

  • A.-1
  • B.0← 正确答案
  • C.1
  • D.随机的非零数

答案:B。cudaSuccess 被定义为 0,这与 C/C++ 中“0 表示成功”的惯例一致。非零值才表示各种错误类型。好比帮厨的“状态手册”里,0 页写着“一切正常”。

Q2. cudaGetLastError() 的独特语义是什么?

单选题 · easy

  • A.只返回但不改变错误状态
  • B.返回错误信息的同时清除错误标志← 正确答案
  • C.返回错误信息并终止程序
  • D.返回所有历史错误列表

答案:B。cudaGetLastError() 的特点是“读取即清除”——返回最近一次错误码,并将错误状态重置为 cudaSuccess。这就像大厨撕掉最新一页故障记录,确保下次翻到的必须是新内容。选项 A 描述的是普通 getter 的语义,B 才是正确行为,C 和 D 则完全不符合。

Q3. 以下哪个操作可以作为同步点捕获异步错误?

单选题 · mid

  • A.核函数启动的立即返回
  • B.cudaDeviceSynchronize() 调用← 正确答案
  • C.声明一个 __device__ 变量
  • D.写一个空的内核函数

答案:B。cudaDeviceSynchronize() 会阻塞主机线程直到所有设备任务完成,此时设备端累积的错误会被传播回主机。核函数启动是异步的(A 错),后两个选项与错误同步完全无关。好比大厨需要“所有人放下刀,统一汇报”才能真正掌握帮厨们的状态。

Q4. 在 CUDA 中,设备端 assert 失败后会发生什么?

单选题 · mid

  • A.仅输出一条警告信息,程序继续运行
  • B.所有线程停止执行,返回 cudaErrorAssert← 正确答案
  • C.只有出错的线程停止,其他线程继续
  • D.程序立即崩溃但没有错误码

答案:B。设备端 assert 失败会中断整个内核的执行(所有线程停止),并产生 cudaErrorAssert 错误码,可通过后续同步操作捕获。它不像 CPU 程序只终止当前线程,而是“一个报警,全厨房停工”。这是 GPU 集体执行的特性。

Q5. 关于 NDEBUG 宏与 assert 的关系,哪些说法正确?

多选题 · mid

  • A.定义 NDEBUG 后 assert 不再生效← 正确答案
  • B.发布版本通常不定义 NDEBUG 以保留断言
  • C.调试版本应避免定义 NDEBUG← 正确答案
  • D.assert 在发布版本中的行为与调试版本完全一致

答案:AC。NDEBUG 定义后,assert 会在预处理阶段被替换为空操作(A 对)。发布版本通常会定义 NDEBUG 以裁剪断言指令、提升性能(B 错)。调试版本需要断言正常工作,因此不定义 NDEBUG(C 对)。因为 NDEBUG 的存在与否直接改变 assert 行为,所以两个版本行为不一致(D 错)。

Q6. 什么时候使用 cuda-memcheck 最合适?

单选题 · hard

  • A.发布版本的生产服务器上持续运行
  • B.调试阶段检测内存越界和未初始化访问← 正确答案
  • C.每次性能基准测试时同时运行
  • D.只用于检测数值精度误差

答案:B。cuda-memcheck 适合在调试阶段使用(B 对),因为它会显著降低运行速度(排除 A、C),且它只检测内存安全问题,不检测数值精度(排除 D)。它就像 X 光机,需要专门体检时使用,不能背着它跑马拉松。

Q7. CUDA 错误传播机制中,哪些因素会导致错误的“延迟暴露”?

多选题 · hard

  • A.内核执行是异步的,错误不会立即返回← 正确答案
  • B.错误标志在未查询前持续累积← 正确答案
  • C.同步操作是错误传播到主机端的必要条件之一← 正确答案
  • D.错误码会随数据传输自动消失

答案:ABC。内核异步执行导致错误不会即时返回(A 对);错误标志会持续存在直到被查询(B 对);同步操作(如 cudaDeviceSynchronize)是错误传回主机的常见路径(C 对)。错误标志不会被数据传输自动清除(D 错),必须显式查询或重置。

Q8. cudaGetErrorString 的作用是什么?

单选题 · easy

  • A.返回错误码对应的可读描述← 正确答案
  • B.清除错误状态
  • C.将错误码写回显存
  • D.冻结当前线程的执行

答案:A。cudaGetErrorString 接受一个 cudaError_t 错误码,返回人类可读的字符串描述。它只做“翻译”工作,不改变错误状态(A 对,B 错),也不涉及显存或线程控制(C、D 错)。就像把帮厨记的数字翻译成能读懂的文字。

Q9. 核函数启动后立即检查 cudaGetLastError() 主要能捕获什么类型的错误?

单选题 · easy

  • A.内核执行中的内存越界错误
  • B.启动参数非法的同步错误← 正确答案
  • C.数值计算溢出错误
  • D.设备端断言失败

答案:B。核函数启动时只进行参数合法性检查,如果参数非法(如网格维度超限),会立即返回错误码(B 对)。内核执行中的错误(A、D)需同步操作才能捕获,数值误差(C)不会产生错误码。好比大厨只能在“派活”时检查帮厨接不接受指令,无法预知他们做菜中途会不会出事。

Q10. 关于 cuda-gdb 与 cuda-memcheck 的比较,哪些说法正确?

多选题 · hard

  • A.cuda-gdb 用于断点和单步调试← 正确答案
  • B.cuda-memcheck 用于自动化内存错误检测← 正确答案
  • C.两者可以配合使用,先扫描后定位← 正确答案
  • D.cuda-memcheck 可以替代编译器进行优化

答案:ABC。cuda-gdb 提供交互式的断点、单步、线程切换能力(A 对),cuda-memcheck 自动化检测内存问题(B 对)。典型流程是先用 cuda-memcheck 扫描定位问题的大致范围,再用 cuda-gdb 定点分析(C 对)。两者都不是编译器,不进行代码优化(D 错)。