先从一个让人抓狂的报错说起:你在工作站上从源码编译 PyTorch,一路绿灯,结果把编译好的包拷贝到另一台机器上跑推理,立刻弹出CUDA error: no kernel image is available for execution on the device。为什么?因为编译时没有把目标 GPU 的 计算能力(Compute Capability) 设置对,或者只对着当前这台机器的显卡进行优化,换一台架构不同的 GPU 就当场翻车。如果你平时还要编 caffe、TensorFlow,类似的坑我踩过不止一次:Unsupported gpu architecture 'compute_75'、黑屏、算力跑不满、跑一半提示找不到镜像……说到底,问题都出在同一个地方——没搞清楚 Compute Capability 和 NVCC 编译参数之间的那点关系。
这篇博文我尽量讲透,内容包括三块:计算能力是什么、为什么编译深度学习框架时必须指定它,以及 caffe、PyTorch、TensorFlow 三套主流程框架在编译时到底怎么填参数才不踩坑。如果你正在自己折腾"源码编译 + 多卡环境 + 不同代际 GPU 混跑",这篇文章应该能帮你省下半天排查时间。
1. 先吃透Compute Capability:它决定NVCC为你生成哪种机器码
1.1 用一张表看懂架构、型号与算力编号
Compute Capability 是 NVIDIA 给自家 GPU 定义的一个版本号,格式是X.Y,主版本号X代表核心架构代际,次版本号Y代表同一代架构里的增强版本。它不直接对应显卡的市场型号(比如 RTX 3080、A100 这种商业名),而是对应 GPU 芯片的微架构版本。
我整理了一张常用对照表,编译框架前先对着它确认一下手上的卡属于哪一代:
| 架构代号 | Compute Capability | 代表GPU型号 | 关键特性 |
|---|---|---|---|
| Kepler | 3.0 / 3.5 / 3.7 | K80、K40 | 老古董,基本可以放弃 |
| Maxwell | 5.0 / 5.2 / 5.3 | GTX 750 Ti、GTX 980 | 消费级,无 Tensor Core |
| Pascal | 6.0 / 6.1 / 6.2 | P100、GTX 1080、Titan X | 16nm 时代,FP16 弱 |
| Volta | 7.0 | V100 | 第一代 Tensor Core |
| Turing | 7.5 | RTX 2080 Ti、Quadro RTX 系列 | Tensor Core + RT Core |
| Ampere | 8.0 / 8.6 | A100、RTX 3090、A6000 | 第三代 Tensor Core |
| Ada Lovelace | 8.9 | RTX 4090、RTX 4080 | 第四代 Tensor Core |
| Hopper | 9.0 | H100 | Transformer Engine,FP8 |
注意,同一架构下不同 Acceleration 卡的算力编号也可能不一样,比如 Ampere 家族的 A100 是 8.0,而 RTX 3090 是 8.6;另外还有 A30、A10 这些分支,各自对应不同的小版本。编译前精确到小数点后一位去查,别想当然。
1.2 arch 与 code 的区别:为什么不能只填一个数字
NVCC 在编译 CUDA 代码时有两个关键参数,长得像双胞胎,但作用完全不同:
arch=compute_XX:指定 PTX 中间代码的目标虚拟架构。code=sm_XX:指定最终 SASS 机器码的目标物理架构。
翻译成大白话:compute_XX是给虚拟机器看的,用来生成可移植的 PTX 代码,之后可以通过 JIT 机制在更新的 GPU 上转成 SASS 运行;code=sm_XX是给真实硬件用的,直接生成只能在对应架构上跑的机器码。
很多人以为填一个数字就够了,实际上正确的用法经常是成对出现:
-gencode arch=compute_80,code=sm_80上面这一行表示:既生成sm_80的 SASS 机器码,也保留compute_80的 PTX 中间码。保留 PTX 的意义在于——如果用户后来把程序跑在一张支持 8.0 以上、能兼容向下加载 PTX 的新卡上,驱动会现场把 PTX 编译成 SASS,不至于直接报"No kernel image"。
我见过不少同学的编译脚本里只写了arch=compute_80,没写code=sm_80,结果编出来的库只能在部分环境里正常跑,换卡就崩。这个细节非常关键,下面三套框架的配置全部要围绕"arch 和 code 一起指定"来展开。
2. 编译前,先把你的GPU底牌摸清楚
2.1 三种机器组合下的编译策略
动手编译之前,先回答一个问题:这套东西编出来是只在自己固定的一台机器上用,还是要分发到实验室/公司里多台不同显卡的机器上跑?不同答案对应完全不同的策略:
- 只在本机固定使用:只写本机 GPU 对应的算力值,编译最快,包体最小,性能最优。
- 多台机器但架构相同:比如两台都是 RTX 3090,写 8.6 即可,不要画蛇添足加别的。
- 多台机器且架构不同:必须打包多个算力值,比如
8.0;8.6;8.9,甚至还要主动加上compute_XX的 PTX 兼容层。 - 面向未知环境的分发:保守做法是把目标支持范围内的主要算力值全部编进去,代价是编译时间和包体积明显上升。
实际工程里,我比较推荐第三种方案里的折中写法:把你要支持的每个主要架构按arch=compute_XY,code=sm_XY写全,同时至少保留一个arch=compute_XY,code=compute_XY作为 PTX 兜底,让老卡/新卡都能通过 JIT 跑起来。
2.2 查询 GPU 计算能力的几种靠谱方法
直接跑nvidia-smi只能看到 GPU 名称、驱动、显存用量,它不会直接显示 Compute Capability。我常用的是下面这几种查法:
第一种,用 CUDA 自带的 deviceQuery 示例程序:
cd /usr/local/cuda/samples/1_Utilities/deviceQuery && make ./deviceQuery输出里有一行CUDA Capability Major/Minor version number,直接告诉你是几段几。
第二种,如果你懒得编译示例,就用 Python 的 PyTorch 查:
import torch print(torch.cuda.get_device_capability()) # 例如输出 (8, 9) 就代表 compute_89第三种,NVIDIA 官网每代 GPU 的支持列表里也有,但页面更新慢,不如上面两条快。
2.3 别忽略CUDA Toolkit版本的天花板
还有个容易忽略的因素:Compute Capability 不是想编多新就能编多新,老版本的 CUDA Toolkit 根本不认识新 GPU 的算力编号。你拿 CUDA 10.2 去编译针对compute_89的代码,NVCC 直接给你一句Unsupported gpu architecture 'compute_89'。
反过来也一样,新版本 CUDA 也在逐步放弃极老架构的支持。比如 CUDA 12.x 系列对 Kepler 之前的卡基本不提供完整支持,较新版本里 Maxwell/Pascal 的编译支持也在缩水。
所以编译前先确认三件事的版本兼容三角:CUDA Toolkit 版本、GPU 算力版本、深度学习框架要求的 CUDA 版本范围。这三者必须同时满足,缺一个都会导致编译链直接断掉。
3. caffe编译:在Makefile.config里手工指定CUDA_ARCH
3.1 老版 caffe 的CUDA_ARCH 配置写法
caffe 是个"上了年纪"的框架,不同的维护分支配置方式差别很大。最经典的 BVLC 版本使用 Makefile.config 管理编译选项,里面对计算能力的配置集中在一块CUDA_ARCH变量里,默认长这样:
CUDA_ARCH := -gencode arch=compute_61,code=sm_61 \ -gencode arch=compute_61,code=compute_61默认值停留在 compute_61 是一个非常典型的坑——哪怕你现在用的是 A100 或 RTX 4090,caffe 默认配置也根本不会帮你生成对应的 SASS。所以动手前八成要改。
比如你在实验室一台 RTX 3090(compute_86)和一台 RTX 4090(compute_89)上都要跑代码,建议改成这样:
CUDA_ARCH := -gencode arch=compute_70,code=sm_70 \ -gencode arch=compute_80,code=sm_80 \ -gencode arch=compute_86,code=sm_86 \ -gencode arch=compute_89,code=sm_89 \ -gencode arch=compute_89,code=compute_89这里为什么要塞一个compute_70进去?因为有些老模型在 V100 上还有业务,顺手保留;caffe 是老框架,老卡概率不小,加一行成本很低。
如果你用的是基于 CMake 的 caffe 分支,配置入口不一样,会在cmake/Cuda.cmake里读一个CUDA_ARCH_LIST变量,写法更接近 PyTorch 的列表风格:
set(CUDA_ARCH_LIST "7.5 8.0 8.6")3.2 验证 caffe 编译结果是否真的生效
caffe 编完后怎么确认 CUDA_ARCH 真的生效了?别只凭"编过了"猜测。跑一下自带的测试集:
./build/test/test_all.testbin --gtest_filter=*TestForward*如果 GTest 里跟 cuDNN 相关的用例能跑通,基本说明架构没问题;如果报CUDA error: no kernel image is available,那就回头去检查CUDA_ARCH里有没有包含当前 GPU,或者compute_XX,code=compute_XX的 PTX 兜底。
另外要注意,caffe 对 cuDNN 版本很敏感,如果你改了架构导致编译通过但运行时 cuDNN 报错,先别急着怀疑算力设置,先查 cuDNN 版本匹配不匹配。
3.3 一个关于cuDNN版本与算力的交叉坑
我经历过一次很诡异的问题:RTX 3090 上编好的 caffe,跑 CPU 数据层一切正常,模型一上 GPU 就报Check failed: ... CUDNN_STATUS_EXECUTION_FAILED。折腾了很久,最后发现是 cuDNN 版本太老,对 compute_86 的支持有缺陷,换了新版 cuDNN 后立刻正常。
这类问题很容易被误判为编译架构问题。所以我现在的习惯是,编译前把 cuDNN 版本、CUDA 版本、计算能力三项做成一个对照表,每换一台新卡都先查一遍版本兼容性,再动编译脚本。
4. PyTorch源码编译:TORCH_CUDA_ARCH_LIST的正确姿势
4.1 从源码编译时的标准配置流程
PyTorch 源码编译比 caffe 现代得多,计算能力统一通过环境变量TORCH_CUDA_ARCH_LIST控制,格式是用分号分隔的算力列表。我第一次从源码编译 PyTorch 是在一张 RTX 3080 的机器上,当时什么都不懂,直接一路默认,编译了三小时,最后跑模型非常卡,查了才知道编译产物里根本没有针对 Ampere 8.6 的机器码。
正确做法是在运行python setup.py之前就设置好环境变量。比如最常见的多卡混用配置:
export TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;9.0" python setup.py develop如果你是单纯为当前机器编译,可以一步到位写成:
export TORCH_CUDA_ARCH_LIST="8.9" python setup.py install还有一个偷懒写法是直接设置TORCH_CUDA_ARCH_LIST="ALL",表示支持所有架构。听起来很爽,但实际编译时间会非常夸张,而且生成出来的.so文件体积巨大。除非你是要打个包含所有显卡的 wheel 包给别人分发,否则不建议这么做。
4.2 TORCH_CUDA_ARCH_LIST 格式细节与常见报错
这个环境变量的格式比较灵活,支持空格、分号、逗号分隔,但里面有几个隐藏的细节值得注意:
- 如果填的是
8.6,PyTorch 的构建脚本会自动帮你展开成-gencode arch=compute_86,code=sm_86,你不需要自己写-gencode。 - 如果填
8.6+PTX,则表示额外保留compute_86的 PTX 中间码,相当于code=compute_86的效果。 - 填
7.5和填7.5+PTX的区别:前者只能在 Turing 架构上直接运行,后者还可以靠 JIT 在 Ampere、Ada 等更新的架构上运行。
编译过程中如果你看到类似下面这种报错:
Unsupported gpu architecture 'compute_86'先别慌,这不是你环境变量写错了,而是 CUDA Toolkit 版本太低,不认识 8.6。解决方式是升级 CUDA 或降低请求算力值,比如改成 7.5 看看能不能过。只有版本匹配问题,没有别的玄学原因。
4.3 设置过全的代价:编译时间与包体量实测
很多人一开始为了"稳妥",会把TORCH_CUDA_ARCH_LIST填成"3.5;5.0;6.0;6.1;7.0;7.5;8.0;8.6;9.0"。我做过一次实测,同样的 PyTorch 源码,只给 8.6 编译大约 1 到 1.5 小时;给全量 9 个架构编译,时间直接翻到 4 小时以上,生成的libtorch_cuda.so体积也从大约 1.2 GB 涨到 2.5 GB 左右。
更重要的是,PyTorch 编译过程中每个架构都会触发一次针对性的代码生成,全量配置下你还要考虑 CPU 内存占用。有一台 32 GB 内存的机器,编译到一半直接 OOM 卡死,就是因为同时编太多架构把内存撑爆了。所以我的建议一直很明确:能用"8.6"解决问题,就别写"8.0;8.6";要面向多卡分发,也只挑真正相关的一两个低版本架构做兼容,不要搞全家桶。
5. TensorFlow编译:configure阶段就把TF_CUDA_COMPUTE_CAPABILITIES填对
5.1 从 ./configure 到 bazel 构建的完整设置
TensorFlow 的源码编译和前面两个不太一样,它依赖 bazel,而架构配置主要发生在./configure交互式问答阶段。执行:
./configure中间会问你一串问题,其中有一句是:
Please specify a list of comma-separated CUDA compute capabilities you want to build with.这里填的是逗号分隔列表,不是分号。如果你没填,有一个默认值:
7.5,8.0这个默认值正好不含 8.6 和 8.9,意味着你在 RTX 30/40 系列显卡上编出来的 TensorFlow,默认配置下会跑不了。我第一次编 TensorFlow 就用默认值过去了,结果跑任何 op 都是no kernel image,那感觉真是痛苦。
正确操作是直接用环境变量预设答案,免交互:
export TF_CUDA_COMPUTE_CAPABILITIES="7.5,8.0,8.6,8.9" ./configure注意这里还有一个TF_NEED_CUDA环境变量要顺手设成1,否则 configure 会跳过了 CUDA 选项。完整脚本可以写成:
export TF_NEED_CUDA=1 export TF_CUDA_COMPUTE_CAPABILITIES="7.5,8.6,8.9" export TF_CUDA_VERSION=12.2 export TF_CUDNN_VERSION=8 ./configure5.2 bazel 编译阶段如何观察到底编了哪些架构
TensorFlow 一旦开始 bazel 构建,再改架构就麻烦了,因为 bazel 会缓存所有编译中间产物。你以为改了环境变量重新 build 就行,实际上它可能直接复用缓存,你的新架构设置根本没生效。
我踩过一次:第一次用默认值编译到一半,发现不对,改了TF_CUDA_COMPUTE_CAPABILITIES后重新跑 build,它很快跳过了所有步骤,直接输出完成了。我兴高采烈地拿生成的 pip 包去测试,结果还是no kernel image。最后查了很久才发现需要先清掉 bazel 缓存。
正确姿势是:
export TF_CUDA_COMPUTE_CAPABILITIES="8.6" bazel clean --expunge ./configure bazel build //tensorflow/tools/pip_package:build_pip_packagebazel clean --expunge会把整个构建缓存删干净,相当于把所有编译中间变量全部重置,然后再重新 configure 一次,确保新的算力配置进入 bazel 的构建配置链。
5.3 利用 cuobjdump 反查二进制中的架构镜像
编译完的 TensorFlow 也可以像 PyTorch 一样验证。我用的是 CUDA 自带的cuobjdump工具来列出.so文件里包含的所有 SASS 架构信息:
cuobjdump --list-elf tensorflow/python/_pywrap_tensorflow_internal.so | grep -E "sm_|compute_"输出的内容是一串sm_86、sm_89、compute_89之类的标签。如果你能从中找到目标 GPU 对应的sm_XX,说明这次编译的架构设置真的进去了;如果找不到,就算编译成功,部署到目标机器也大概率跑不起来。
这里多一句嘴:我用的是 Linux 环境,Windows 下 TensorFlow 源码编译虽然能进行,但 bazel 缓存清理和 cuobjdump 都容易遇到路径权限问题,非常折腾。非必要不建议在 Windows 上从源码编译类库,直接使用官方 wheel 更省事。
6. 算力错配的运行时症状与一条亲测有效的排查链路
6.1 三种最常见的错误信息,请先对号入座
把三种框架都编过一遍之后,我发现运行时算力错配的表现其实很有规律。常见的基本就三种:
| 错误现象 | 直接原因 | 对应配置问题 |
|---|---|---|
no kernel image is available for execution on the device | 当前 GPU 找不到对应 SASS/PTX | 算力列表没包含当前卡 |
Unsupported gpu architecture 'compute_XX' | NVCC 不认这个虚拟架构名 | CUDA Toolkit 版本过老 |
| 能跑但性能明显偏低,GPU 利用率只在 50% 上下波动 | 走了 PTX 到 SASS 的 JIT 路径而不是原生机器码 | 只有 PTX 兜底,没有针对该架构的原生编译 |
第三条容易被忽略。我在一台 A100 上测过:直接编sm_80,同一个模型每秒能处理约 300 张图;但如果你只给compute_80的 PTX,运行时靠驱动 JIT 再编一次,性能会掉到 250 张左右,而且每次新进程启动都要重复编译一次,白等几秒钟。所以生产环境务必针对硬件架构给原生 SASS,JIT 只是兜底,不是主力。
6.2 从报错到修复的一次完整排查记录
拿我之前在混合环境的服务器上遇到的问题举例。那台机器有两张显卡,一张 RTX 3080(compute_86),一张 GTX 1080 Ti(compute_61)。我从源码编译好了 PyTorch,在 3080 上跑一切正常,切到 1080 Ti 上跑,启动即报:
RuntimeError: CUDA error: no kernel image is available for execution on the device第一反应检查TORCH_CUDA_ARCH_LIST,发现里面只写了"8.6",显然没覆盖 6.1。于是改成:
export TORCH_CUDA_ARCH_LIST="6.1;8.6"重新编译后,再在 1080 Ti 上跑同一个脚本,顺利通过。但诡异的是,跑着跑着发现 1080 Ti 上有些自定义的 CUDA 算子还是报错,看报错位置是我手工写的一个extension.cpp在编译时用的也是同一套TORCH_CUDA_ARCH_LIST,但那个算子代码里用了__shfl_xor_sync这类新特性,在 6.1 上根本不支持,编译却通过了,运行时才炸。
这个案例给了我一个重要教训:算力配置不只是影响框架本体,还影响你所有基于这套框架编译的自定义 CUDA 算子。算子源码本身也要做架构差异兼容,或者直接把低架构排除在新特性之外。如果你拿别人写好的自定义算子源码,在混合 GPU 环境编译,最好先看看算子代码对低版本架构的兼容性,别只盯着框架的算力参数。
6.3 一个保底建议:统一算力基线,减少无谓分支
如果你维护多台机器组成的训练集群,我的真实建议是不要试图同时支持跨度极大的架构组合。比如机器里有 K80(3.7),又有 H100(9.0),把 3.7 和 9.0 同时编进一个二进制,不仅编译产物非常巨大,代码运行时还可能因为分支兼容逻辑产生各种预想不到的行为。
更好的做法是给机器分层:老卡专门编一套支持 3.5/3.7 的二进制放在老的静态服务器上,跑推理;新卡统一编 8.0/8.6/8.9/9.0 的一套二进制,跑训练。所谓"一次编译到处跑"在深度学习的 GPU 生态里真的很难实现,平时尽量不跟自己过不去。
最后再说一个关于PTX与JIT实操习惯的建议
回到开头那句no kernel image。实际上只要你在编译时给arch=compute_XX,code=compute_XX留下了 PTX 兜底,很多错配问题驱动都能帮你温柔地接住,不至于直接崩溃。但 JIT 的启动开销和性能折损是实实在在的,因此我现在的习惯是:主力架构写 SASS,兼容架构写 PTX。
举个例子,假如主开发机是 RTX 4090(8.9),但测试机上还有一批老的 T4(7.5),那我会这样配:
export TORCH_CUDA_ARCH_LIST="7.5 8.9+PTX"这样的意思是:给 7.5 生成原生二进制用于老卡兼容,给 8.9 生成 SASS 加一份 PTX 兜底,将来如果这套库被拷贝到更新的 Hopper 架构机器上,也能靠 PTX 顺利跑起来,不会抓瞎。
这套思路我同样用在 caffe 和 TensorFlow 上。区别不过是格式:caffe 写在 Makefile.config 的CUDA_ARCH里,TensorFlow 填在TF_CUDA_COMPUTE_CAPABILITIES的环境变量中,而 PyTorch 认的是TORCH_CUDA_ARCH_LIST。框架在变,工具链在升级,核心逻辑永远绕不开那句话——编译前先看一眼 Compute Capability,别让编译过程在机器码这一步就压错了赌注。