1. 问题背景与核心矛盾拆解
gpu_burn 这个工具在 GPU 压力测试和稳定性验证圈子里算是老面孔了,它的原理并不复杂——通过反复执行大规模的矩阵乘法(GEMM)运算,把 GPU 的计算单元和显存子系统推到接近满载的状态,从而在短时间内暴露散热、供电、驱动兼容性等潜在问题。很多做深度学习训练、高性能计算集群运维、甚至显卡售后检测的朋友,都会把它当作标准工具之一。
但最近一年,随着 CUDA 13 的逐步铺开,尤其是搭配 Ubuntu 26.04 这类较新的发行版,不少人在编译 gpu_burn 时开始撞墙。最典型的报错集中在cuCtxCreate这个 API 上——编译阶段直接报未定义引用,或者参数数量不匹配。这个问题表面上看是"编译不过",实际上背后牵扯到 CUDA 驱动 API 的版本演进、gpu_burn 上游代码的维护节奏、以及新老 GPU 架构在上下文管理上的差异。
我先把这个问题的核心矛盾说清楚:gpu_burn 的源码长期依赖 CUDA Driver API 中cuCtxCreate的旧版签名,而 CUDA 13 对这套上下文创建接口做了调整,同时新版驱动对计算上下文的管理策略也有变化。这就导致在老环境里能顺利编译的代码,到了 CUDA 13 环境下要么找不到符号,要么参数对不上。
这篇文章适合三类人看:第一类是在 Ubuntu 26.04 上装了 CUDA 13、想跑 gpu_burn 却编译失败的人;第二类是需要维护 GPU 测试工具链、想搞清楚 Driver API 版本兼容逻辑的运维或开发;第三类是对 CUDA 底层上下文管理机制感兴趣、想借这个案例理解 API 演进规律的技术爱好者。不管你是哪一类,我都会从原理讲到实操,把每一步的"为什么"讲透,让你不只是抄个命令,而是真正理解这套东西怎么运转。
1.1 为什么 CUDA 13 会让老代码编译失败
要理解这个问题,得先搞清楚 CUDA 的两套 API 体系。CUDA 分为 Runtime API(以cuda开头,比如cudaMalloc)和 Driver API(以cu开头,比如cuMemAlloc)。gpu_burn 用的是 Driver API,因为 Driver API 更底层、控制粒度更细,适合做这种极限压测。
cuCtxCreate是 Driver API 里用来创建一个 CUDA 计算上下文的函数。所谓"上下文",你可以把它类比成进程和操作系统之间的关系——GPU 上跑的任何计算任务,都需要先有一个上下文作为"容器",里面管理着显存分配、模块加载、流(stream)等资源。在 CUDA 的早期版本里,cuCtxCreate的签名是这样的:
CUresult cuCtxCreate(CUcontext *pctx, unsigned int flags, CUdevice dev);到了后来,NVIDIA 引入了CUcontext的"新式"创建方式,增加了CUexecAffinityParam这类参数,用于更精细地控制执行亲和性。CUDA 13 进一步收紧了这套接口,把一些旧的、不带亲和性参数的调用方式标记为过时甚至移除。gpu_burn 的代码如果还停留在旧签名上,编译器在链接阶段就会报undefined reference to 'cuCtxCreate'或者too few arguments to function 'cuCtxCreate'。
这里有个容易被忽略的点:CUDA 13 的驱动库(libcuda.so)和 CUDA Toolkit 自带的 stub 库(libcuda.so 的 stub 版本)在符号导出上可能不一致。你编译时链接的是 Toolkit 里的 stub,运行时加载的是真实驱动,如果两者版本对不上,就会出现"编译过了但运行崩"或者"编译就过不了"的诡异现象。这也是为什么很多人换了 CUDA 版本后,同样的代码表现完全不同。
1.2 gpu_burn 上游代码的维护现状
gpu_burn 这个项目本身更新频率不算高,它的核心代码多年来变化不大。这就带来一个现实问题:上游代码对新 CUDA 版本的适配往往滞后。CUDA 13 发布后,gpu_burn 的 master 分支并没有第一时间跟进cuCtxCreate的签名变更,导致大量用户在新环境下编译失败。
我在实际排查时发现,社区里流传的解决方案大致分三派:一派是直接改源码,把cuCtxCreate的调用改成新签名;一派是用宏定义做条件编译,根据 CUDA 版本号走不同的分支;还有一派是干脆降级 CUDA 到 12.x。这三种方案各有优劣,后面我会逐一分析,并给出我实测下来最稳的那套做法。
需要强调的是,改源码不是简单地"把参数补上"就完事。cuCtxCreate的签名变化背后,是上下文管理语义的变化。如果你只是机械地补参数,可能会引入资源泄漏或者上下文创建失败的问题。所以理解每个参数的含义,比记住怎么改更重要。
2. 环境准备与编译前必做的检查
在动手改代码之前,有几项环境检查是必须做的。我见过太多人一上来就改源码,结果折腾半天发现是环境本身没配对。这一章我把编译前的准备工作拆成几个关键步骤,每一步都说明为什么要做、怎么做、以及怎么验证做对了。
2.1 确认 CUDA 版本与驱动版本的匹配关系
第一件事是搞清楚你机器上到底装了什么。很多人以为装了 CUDA 13 Toolkit 就是"CUDA 13 环境",但实际上 CUDA 环境由两部分组成:Toolkit(编译工具链、头文件、库)和 Driver(运行时驱动)。这两者的版本必须匹配,否则会出现各种奇怪的问题。
在 Ubuntu 26.04 上,你可以用以下命令查看:
# 查看 Toolkit 版本 nvcc --version # 查看驱动版本 nvidia-smi # 查看 CUDA 运行时版本(驱动支持的版本) nvidia-smi | grep "CUDA Version"这里有个关键点:nvidia-smi显示的 "CUDA Version" 是驱动支持的最高 CUDA 版本,不是当前安装的 Toolkit 版本。比如你看到 "CUDA Version: 13.0",说明驱动至少支持到 13.0,但你的 Toolkit 可能是 12.4。这两个版本如果不一致,编译时用的头文件(来自 Toolkit)和运行时加载的驱动库就可能对不上。
我建议的做法是:Toolkit 版本不要超过驱动支持的版本。如果你的驱动只支持到 12.6,那就别装 13.0 的 Toolkit,否则编译出来的东西运行时大概率出问题。反过来,驱动版本高于 Toolkit 版本通常是安全的,因为驱动向下兼容。
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| Toolkit 版本 | nvcc --version | 显示 13.x |
| 驱动版本 | nvidia-smi顶部 | 显示 5xx.xx 以上 |
| 驱动支持的最高 CUDA | nvidia-smi右上角 | 大于等于 Toolkit 版本 |
| 头文件路径 | ls /usr/local/cuda/include/cuda.h | 文件存在 |
| 库文件路径 | ls /usr/local/cuda/lib64/libcuda.so | 文件存在 |
如果发现 Toolkit 和驱动版本不匹配,优先考虑升级驱动,而不是降级 Toolkit。升级驱动在 Ubuntu 上相对简单,用系统自带的驱动管理工具或者 NVIDIA 官方仓库都能搞定。
2.2 定位 gpu_burn 源码中所有 cuCtxCreate 调用点
环境确认无误后,下一步是找到源码里所有需要改的地方。gpu_burn 的代码量不大,核心文件通常就几个。用 grep 快速定位:
grep -rn "cuCtxCreate" /path/to/gpu_burn/你会看到类似这样的输出:
gpu_burn.c:123: CUresult res = cuCtxCreate(&ctx, 0, dev);注意这里的调用是旧签名:三个参数,第二个是 flags,第三个是 device。CUDA 13 里这个签名已经不被推荐,需要用新的形式。但具体怎么改,取决于你用的 CUDA 13 具体小版本,因为 NVIDIA 在不同小版本里对 API 的处理略有差异。
我建议在改之前,先查一下你本地 CUDA 13 头文件里cuCtxCreate的声明:
grep -n "cuCtxCreate" /usr/local/cuda/include/cuda.h你会看到类似这样的声明(具体以你本地为准):
CUresult CUDAAPI cuCtxCreate(CUcontext *pctx, unsigned int flags, CUdevice dev); CUresult CUDAAPI cuCtxCreate_v2(CUcontext *pctx, unsigned int flags, CUdevice dev); CUresult CUDAAPI cuCtxCreate_v3(CUcontext *pctx, CUexecAffinityParam *params, int numParams, unsigned int flags, CUdevice dev);看到没?CUDA 13 里其实保留了cuCtxCreate的旧签名,但可能通过宏或者版本控制做了重定向。关键是要搞清楚你的代码实际链接到的是哪个符号。如果头文件里cuCtxCreate被定义成了cuCtxCreate_v3,那你的三参数调用就会报参数数量错误。
提示:不要盲目相信网上搜到的"改法",不同 CUDA 13 小版本的头文件定义可能不同。一定要以你本地
/usr/local/cuda/include/cuda.h的实际内容为准。
2.3 检查 GPU 架构与编译目标是否匹配
还有一个容易被忽视的点是 GPU 架构。CUDA 13 对计算能力(Compute Capability)的支持范围做了调整,一些老架构可能不再被支持。如果你编译时指定的架构和实际 GPU 不匹配,即使编译过了,运行时也可能报错。
查看你的 GPU 架构:
nvidia-smi --query-gpu=name,compute_cap --format=csv然后在编译时用-arch参数指定对应的架构。比如你的 GPU 是 Compute Capability 8.9,就加-arch=sm_89。gpu_burn 的 Makefile 里通常有架构相关的配置,需要根据实际情况调整。
这里有个经验:CUDA 13 默认可能不再包含一些老架构的编译目标。如果你用的是比较老的卡(比如 Pascal 架构的 sm_61),可能需要显式加上-arch=sm_61,否则编译器会报"unsupported architecture"。反过来,如果你用的是新卡(比如 Hopper 的 sm_90),也要确保 CUDA 13 支持这个架构。
3. 核心修改方案与实操步骤
环境检查做完,进入正题。这一章我会给出三种修改方案,从最简单到最彻底,你可以根据自己的情况选择。每种方案我都会说明适用场景、具体操作、以及潜在风险。
3.1 方案一:条件编译适配新旧签名
这是我最推荐的方案,因为它既解决了当前问题,又保留了向后兼容性。核心思路是用预处理宏判断 CUDA 版本,根据版本走不同的调用分支。
首先,在源码开头(或者一个公共头文件里)加上版本判断:
#include <cuda.h> #if CUDA_VERSION >= 13000 // CUDA 13 及以上,使用新签名 #define GPU_BURN_CTX_CREATE(ctx, flags, dev) \ cuCtxCreate_v3(ctx, NULL, 0, flags, dev) #else // CUDA 12 及以下,使用旧签名 #define GPU_BURN_CTX_CREATE(ctx, flags, dev) \ cuCtxCreate(ctx, flags, dev) #endif然后把源码里所有的cuCtxCreate(&ctx, 0, dev)替换成GPU_BURN_CTX_CREATE(&ctx, 0, dev)。
这里解释一下cuCtxCreate_v3的参数:第一个是上下文指针,第二个是执行亲和性参数数组(传 NULL 表示不指定),第三个是参数个数(传 0),第四个是 flags,第五个是 device。这样调用等价于旧版的cuCtxCreate(ctx, flags, dev),语义上是一致的。
为什么推荐这个方案?因为它不破坏对老 CUDA 版本的兼容。如果你以后需要在 CUDA 12 的机器上编译同一份代码,条件编译会自动走旧分支,不需要再改回来。对于需要维护多环境的人来说,这是最省心的做法。
注意:
CUDA_VERSION这个宏定义在cuda.h里,值是类似 13000 这样的整数(主版本 13,次版本 0,补丁 0)。确保你在包含cuda.h之后再使用这个宏,否则会报未定义。
3.2 方案二:直接修改为 v3 签名
如果你确定只在 CUDA 13 环境下使用,不需要兼容老版本,那可以直接把调用改成 v3 签名,省去宏定义的麻烦:
// 原来的代码 CUresult res = cuCtxCreate(&ctx, 0, dev); // 改成 CUresult res = cuCtxCreate_v3(&ctx, NULL, 0, 0, dev);这个方案简单直接,但缺点是失去了向后兼容性。如果哪天你需要把代码拿到 CUDA 12 的机器上编译,就得再改回来。所以只建议在单一环境、短期使用的场景下采用。
另外要注意,cuCtxCreate_v3这个符号在 CUDA 13 的 stub 库里是否导出,需要验证。可以用nm命令检查:
nm -D /usr/local/cuda/lib64/stubs/libcuda.so | grep cuCtxCreate如果看到cuCtxCreate_v3的符号,说明可以链接。如果只有cuCtxCreate,那可能需要用方案一或者方案三。
3.3 方案三:降级 CUDA Toolkit 到 12.x
这是最"怂"但也最省事的方案。如果你的项目不强制要求 CUDA 13,完全可以把 Toolkit 降级到 12.x,这样 gpu_burn 的原始代码不用改就能编译。
在 Ubuntu 26.04 上降级 CUDA Toolkit 的步骤大致是:先卸载当前的 CUDA 13,然后从 NVIDIA 官方仓库安装 12.x 版本。具体命令因安装方式而异,这里不展开。需要提醒的是,降级 Toolkit 不一定需要降级驱动,因为驱动通常向下兼容。但如果你的驱动是最新的、只支持 CUDA 13,那就得连驱动一起降。
这个方案的缺点是:你放弃了 CUDA 13 带来的新特性和性能优化。如果你的 GPU 是新架构(比如 Blackwell),CUDA 13 可能有针对性的优化,降级后可能发挥不出全部性能。所以这个方案适合"只想赶紧把测试跑起来、不追求最新特性"的场景。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 条件编译 | 多环境维护 | 兼容新旧版本 | 需要改代码结构 |
| 直接改 v3 | 单一 CUDA 13 环境 | 简单直接 | 失去向后兼容 |
| 降级 Toolkit | 不强制 CUDA 13 | 不改代码 | 放弃新特性 |
3.4 编译参数与链接选项的调整
改完代码,编译参数也得跟着调整。gpu_burn 的 Makefile 里通常有NVCC、CFLAGS、LDFLAGS这些变量。在 CUDA 13 环境下,我建议加上以下参数:
NVCC = nvcc CFLAGS = -O3 -arch=sm_89 -I/usr/local/cuda/include LDFLAGS = -L/usr/local/cuda/lib64 -lcuda -lcudart几个关键点:
-arch=sm_89要换成你实际的 GPU 架构,前面查过了。-lcuda链接的是 Driver API 库,gpu_burn 必须要有。-lcudart链接 Runtime API 库,有些版本需要。- 如果编译时报找不到
cuCtxCreate_v3,可能需要显式链接 stub 库:-L/usr/local/cuda/lib64/stubs -lcuda。
还有一个坑:Ubuntu 26.04 的默认 GCC 版本可能比较新,而 CUDA 13 对 GCC 版本有要求。如果 GCC 太新,nvcc 可能报"unsupported GNU version"。解决办法是用-allow-unsupported-compiler参数,或者安装一个兼容的 GCC 版本。我实测下来,CUDA 13 对 GCC 13 和 GCC 14 的支持都还行,但如果你的系统默认是 GCC 15,可能就需要额外处理。
4. 常见编译错误与排查实录
即使按照上面的步骤操作,实际编译过程中还是可能遇到各种报错。这一章我把踩过的坑整理成速查表,每个问题都给出排查思路和解决方法。
4.1 undefined reference to 'cuCtxCreate' 的三种可能
这个报错是最常见的,但原因可能不止一种。我遇到过的情况包括:
第一种:链接时没加-lcuda。这是最基础的错误,检查 Makefile 里的 LDFLAGS 是否包含-lcuda。如果没有,加上即可。
第二种:链接的是 stub 库但符号不匹配。CUDA Toolkit 里的libcuda.so其实是个 stub(桩),真正的实现在驱动里。如果 stub 库的版本和头文件不匹配,就会出现符号找不到。解决办法是确保-L指向的路径和头文件路径来自同一个 Toolkit。
第三种:CUDA 13 移除了旧符号。如果 CUDA 13 彻底移除了cuCtxCreate的旧符号,只保留cuCtxCreate_v3,那你的旧代码就会报未定义引用。这时候必须改代码,用方案一或方案二。
排查顺序建议:先看 LDFLAGS,再看库路径,最后看头文件里的符号定义。用nm命令检查库里的符号是最直接的:
nm -D /usr/local/cuda/lib64/stubs/libcuda.so | grep -i ctxcreate4.2 too few arguments to function 'cuCtxCreate' 的应对
这个报错说明头文件里的cuCtxCreate已经是新签名(参数更多),但你的代码还在用旧签名调用。这时候编译器认为你少传了参数。
解决方法就是前面说的,改成新签名调用。但要注意,不要直接把参数补成 5 个就完事,要理解每个参数的含义。比如cuCtxCreate_v3的第二个参数是CUexecAffinityParam *,传 NULL 表示不指定亲和性,这是最安全的做法。如果你传了一个未初始化的指针,运行时可能直接崩溃。
还有一种情况是:头文件里cuCtxCreate被定义成了宏,展开后变成了cuCtxCreate_v3,但你的代码里又手动写了cuCtxCreate_v3,导致重复展开。这种问题比较隐蔽,需要看预处理后的代码:
nvcc -E gpu_burn.c | grep cuCtxCreate4.3 运行时 cuCtxCreate 返回 CUDA_ERROR_UNKNOWN 的排查
编译过了,运行时却报CUDA_ERROR_UNKNOWN(错误码 999),这是最让人头疼的。这个错误码是个"万能错误",什么原因都可能。
我遇到过的原因包括:
- 驱动版本太老,不支持 CUDA 13 的上下文创建方式。解决方法是升级驱动。
- GPU 被其他进程占用,比如另一个 CUDA 程序正在跑。用
nvidia-smi查看是否有残留进程,必要时 kill 掉。 - 显存不足,创建上下文时需要分配一些显存,如果显存被占满就会失败。这种情况在跑完其他任务后容易出现,重启或者清理显存可以解决。
- 权限问题,某些环境下普通用户无法创建 CUDA 上下文,需要 root 权限或者调整设备权限。
排查这类问题的通用思路是:先用一个最简单的 CUDA 程序验证环境是否正常。比如写一个只调用cuInit和cuCtxCreate的小程序,如果它也失败,说明是环境问题;如果它成功,说明是 gpu_burn 代码的问题。
// minimal_ctx_test.c #include <cuda.h> #include <stdio.h> int main() { CUresult res = cuInit(0); if (res != CUDA_SUCCESS) { printf("cuInit failed: %d\n", res); return 1; } CUdevice dev; res = cuDeviceGet(&dev, 0); if (res != CUDA_SUCCESS) { printf("cuDeviceGet failed: %d\n", res); return 1; } CUcontext ctx; res = cuCtxCreate_v3(&ctx, NULL, 0, 0, dev); if (res != CUDA_SUCCESS) { printf("cuCtxCreate failed: %d\n", res); return 1; } printf("Context created successfully\n"); cuCtxDestroy(ctx); return 0; }编译这个测试程序:
nvcc -o minimal_ctx_test minimal_ctx_test.c -lcuda如果这个程序能跑通,说明环境没问题,问题在 gpu_burn 的代码逻辑上。
4.4 常见问题速查表
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| undefined reference to cuCtxCreate | 没链接 -lcuda | LDFLAGS 加 -lcuda |
| undefined reference to cuCtxCreate_v3 | stub 库版本旧 | 更新 Toolkit 或改用旧签名 |
| too few arguments | 头文件是新签名 | 改用 v3 签名调用 |
| CUDA_ERROR_UNKNOWN | 驱动旧/显存不足/权限 | 升级驱动、清理显存、检查权限 |
| unsupported GNU version | GCC 版本太新 | 加 -allow-unsupported-compiler |
| unsupported architecture | 架构参数不对 | 用 -arch=sm_XX 指定正确架构 |
提示:遇到问题时,先用最小测试程序验证环境,再排查业务代码。这个思路能帮你快速定位问题边界,避免在错误的方向上浪费时间。
5. 实操心得与性能验证
改完代码、编译通过只是第一步,真正要确认的是 gpu_burn 能不能正常跑、跑出来的结果是否可信。这一章我分享一些实操中的经验和验证方法。
5.1 编译成功后的首次运行检查
第一次运行改好的 gpu_burn,不要一上来就跑满负载。先用小规模、短时间跑一下,确认基本功能正常:
./gpu_burn -d 10 -m 1024这里的-d 10表示跑 10 秒,-m 1024表示用 1024MB 显存。先用小参数验证,没问题再逐步加大。
运行过程中用另一个终端监控 GPU 状态:
nvidia-smi -l 1观察几个关键指标:GPU 利用率是否接近 100%、温度是否在合理范围、功耗是否达到预期、有没有报错。如果 GPU 利用率上不去,可能是矩阵规模太小或者上下文创建有问题。
我实测下来,CUDA 13 环境下 gpu_burn 的性能和 CUDA 12 基本持平,没有明显的性能回退。这说明 API 签名的变化主要是接口层面的调整,底层计算逻辑没有变。如果你的测试结果显示性能明显下降,那可能是编译参数没优化好,检查一下-O3和架构参数。
5.2 多卡环境下的上下文管理注意事项
如果你在多卡机器上跑 gpu_burn,上下文管理会更复杂。每张卡都需要独立的上下文,而且要注意上下文和设备的绑定关系。
gpu_burn 通常支持指定 GPU 编号,比如-i 0,1表示用第 0 和第 1 张卡。在多卡场景下,每张卡的上下文创建都要走一遍cuCtxCreate,如果某张卡创建失败,整个程序可能就挂了。
我的建议是:多卡测试前,先用nvidia-smi确认所有卡都正常识别,没有掉卡或者报错。然后逐卡测试,确认每张卡单独都能跑通,再一起跑。这样出问题时容易定位是哪张卡的问题。
还有一个细节:CUDA 13 对上下文的生命周期管理更严格。旧代码里可能存在上下文没及时销毁的情况,在 CUDA 12 下可能只是资源泄漏,在 CUDA 13 下可能直接导致后续创建失败。所以改代码时,顺便检查一下cuCtxDestroy的调用是否配对。
5.3 验证修改是否真正生效的方法
怎么确认你的修改真的生效了,而不是碰巧编译过了?我通常用这几个方法验证:
方法一:检查二进制文件里的符号引用。用nm或objdump看编译出来的可执行文件引用了哪个符号:
nm gpu_burn | grep cuCtxCreate如果显示U cuCtxCreate_v3,说明确实用了新签名;如果显示U cuCtxCreate,说明还是旧签名。
方法二:用ldd检查动态库依赖:
ldd gpu_burn | grep cuda确认链接的是正确的 CUDA 库路径。
方法三:跑一个完整的压力测试,观察是否有异常。如果上下文创建有问题,通常在测试开始阶段就会暴露,比如报错退出或者 GPU 利用率异常。
5.4 长期维护的建议
如果你需要长期维护这套工具链,我有几个建议:
第一,把 CUDA 版本判断逻辑封装成独立的头文件,比如cuda_compat.h,所有版本相关的适配都放在里面。这样以后 CUDA 14、15 出来时,只需要改这一个文件。
第二,在 CI 里加多版本编译测试。如果你有条件,配置一个 CI 流程,分别在 CUDA 12 和 CUDA 13 环境下编译,确保代码在两个版本下都能过。这样能提前发现兼容性问题。
第三,关注 gpu_burn 上游的更新。虽然上游更新慢,但一旦官方适配了 CUDA 13,你就可以直接用官方代码,省去自己维护的成本。可以订阅项目的 release 通知或者定期看 commit 记录。
第四,记录你的修改。在代码里加注释,说明为什么这么改、对应哪个 CUDA 版本、参考了什么资料。过几个月再回头看,这些注释能帮你快速回忆起来。
我个人在实际操作中的体会是,CUDA 生态的版本兼容问题本质上是个"接口演进"问题,几乎每个大版本都会遇到。与其每次临时救火,不如建立一套自己的兼容性处理规范。比如统一用条件编译、统一封装兼容层、统一在 CI 里验证。这套方法不仅适用于 gpu_burn,也适用于其他依赖 CUDA Driver API 的工具。
最后再分享一个小技巧:如果你不确定某个 CUDA API 在新版本里的签名,除了看头文件,还可以用nvcc --help或者查 NVIDIA 官方的 API 文档。文档里通常会标注每个 API 从哪个版本开始引入、哪个版本废弃。养成查文档的习惯,比在网上搜零散的解决方案靠谱得多。