要让脚本用上加速卡(GPU)的算力,就得去调它那套底层的运行时 API。这一步经常是"代码看着对、编译也能过,一跑就报一堆看不懂的错误码"。错误码不会告诉你原因,只能对照文档一项项核。
现象
调用某加速卡的运行时 API 时,错误花样百出,而且每一个都指向不同的地方:
-
链接时报"未定义引用"(undefined reference),编译不通过:
undefined reference to `xxxMalloc' -
运行时返回一个数字错误码 201,不知道是什么意思。
-
分配小额度显存正常,分配 4 GiB 以上却失败,返回错误码 1。
-
传设备指针时提示签名不匹配。
同一个程序里能同时撞上好几类,让人怀疑是不是环境整个坏了。
根因
这些错误看着杂乱,其实是一堆 ABI / 签名细节叠加的结果,每一条都有明确的机制:
其一,报"未定义引用"是因为没有链接运行时库。 "undefined reference" 是链接阶段的错误,意思是"符号找不到"。运行时 API 的函数符号都在运行时库里,编译时只把头文件 #include 进来(只提供声明,不提供实现),如果链接命令里没带上那个运行时库(-l<runtime>),链接器就找不到函数体,报未定义引用。
其二,返回 201 是因为没有先创建执行上下文。 加速卡的运行时有一套"初始化顺序":通常要先调用初始化函数,再创建执行上下文(context),然后才能做后续的资源分配、加载。没建上下文就去调需要上下文的函数,返回的就是这个 201 错误码——它表示"当前处于无效的上下文状态"。
其三,大额度分配返回 1 是因为用错了 32 位尺寸版本的函数。 分配显存的函数有"基础版"和"版本二"(形如 xxxMalloc 与 xxxMalloc_v2)两种。基础版的尺寸参数是 32 位的,能表达的地址空间有限,当请求超过某个大小(比如 4 GiB)时,尺寸在 32 位里溢出或超限,于是失败返回 1。大额分配必须用 _v2 版本的函数,它接受 64 位尺寸。
其四,设备指针要用 64 位整数传递。 有些运行时的函数签名要求设备指针以 64 位整型(而非通用指针类型)传入,版本之间签名不一致,传错类型就对不上。
解决
按 ABI 要求逐项修正:
1. 显式链接运行时库。 在编译/链接命令里补上库:
gcc app.c -o app -L/<运行时库路径> -l<runtime> -lcuda_driver
(具体库名以官方文档为准)链接后再编,未定义引用就会消失。
2. 遵守初始化顺序:先建上下文。 在调用任何需要上下文的 API 之前,依次完成初始化、创建上下文:
xxxInit(0);
xxxContext ctx;
xxxCtxCreate(&ctx, 0, device); /* 必须先建上下文,才能做后续操作 */
此后那些返回 201 的调用就会正常。
3. 大额分配改用 _v2 版本函数。 把尺寸类型也改成 64 位:
size_t size = (size_t)4ULL * 1024 * 1024 * 1024; /* 4 GiB,用 64 位尺寸 */
void* ptr;
xxxMalloc_v2(&ptr, size); /* 大分配用 _v2 */
4. 设备指针按 64 位整型传递。 对照当前版本头文件里的函数签名,把设备指针以 uint64_t 传入:
kernel<<<..., >>>((uint64_t)device_ptr);
延伸与预防
一句总结:底层 API 的错误码不会告诉你"你少链了一个库",只能对照文档一项项核。
处理这类问题的关键,是把"一个数字错误码"翻译成"一条明确的规则"。建议养成几个习惯:
- 建一份错误码对照表。 把遇到过的错误码和它的真实含义记下来(201 = 缺上下文、1 = 参数/尺寸问题……),下次再见到就不用重新查。错误码的官方定义通常都在文档的"Error Codes"一节。
- 分清错误的"阶段"。 "未定义引用"是链接期问题,"返回非零错误码"是运行期问题,编译告警是编译期问题。先判断错误发生在哪个阶段,排查范围立刻缩小一大截。
- 核对 ABI 三件套:链接了什么库、函数签名(参数类型/宽度)对不对、调用顺序对不对。上面四类问题,全部落在这三件里。
- 小额度能通、大额度失败,优先怀疑位宽。 这是"32 位装不下、要用 64 位版本"的经典信号——不止显存分配,文件大小、偏移量、缓冲区长度都可能是同一个套路。
- 升级驱动/运行时后重新核对一遍。 这套底层 API 的签名在不同大版本间会变,锁定版本并记录"哪个版本对应哪套签名",能避免升级后一夜之间全崩。