
1. 从一次编译成功却跑不起来的诡异故障说起如果你在 Linux 上编译过带 CUDA 的 OpenCV、PyTorch 扩展或者自己写的.cu文件大概率遇到过这种场景nvcc一路绿灯make没有任何报错可程序一运行就抛出no kernel image is available for execution on the device或者干脆在cudaGetDeviceProperties之后直接崩掉。更让人抓狂的是同一份代码在同事的机器上跑得好好的换到你的 4090 或者 4060 Ti 上就翻车。这类问题的根子十有八九不在代码逻辑而在nvcc的-gencode参数没配对。-gencode决定了编译器为哪些 GPU 架构生成机器码配错了要么生成的二进制里根本没有你显卡能执行的指令集要么白白浪费了算力让本该跑满的 kernel 只发挥了一半性能。这篇内容就是把我这些年踩过的-gencode相关的坑从原理到实操完整梳理一遍。不管你是刚在 Ubuntu 24.04 上装完 CUDA Toolkit 的新手还是已经在多版本 CUDA 环境里折腾过的老手只要你的工作涉及用nvcc编译代码这里面的细节都值得过一遍。核心关键词就几个CUDA、-gencode、GPU 算力、nvcc、arch围绕它们把为什么和怎么做讲透。先说结论-gencode的本质是告诉nvcc我要在哪些架构上跑它由arch虚拟架构决定 PTX 版本和code真实架构决定 SASS 机器码两部分组成。理解这两者的区别是解决所有相关问题的钥匙。2. -gencode 到底在干什么arch 与 code 的双层结构2.1 虚拟架构与真实架构的分工很多人第一次看到-gencode archcompute_75,codesm_75这种写法是懵的为什么一个参数要写两遍架构号这得从 CUDA 的编译模型说起。nvcc编译.cu文件时实际上走两条路。第一条路把 CUDA C 代码编译成PTXParallel Thread Execution这是一种中间表示类似 Java 的字节码它对应的是虚拟架构写作compute_XX。第二条路把 PTX 进一步编译成目标 GPU 能直接执行的SASS机器码对应的是真实架构写作sm_XX。archcompute_75说的是我按 7.5 的虚拟架构规则生成 PTXcodesm_75说的是顺便把这段 PTX 编译成 7.5 的真实机器码。两者可以不一致这就引出了后面要讲的向前兼容机制。为什么要有虚拟架构这一层因为 GPU 架构迭代很快从 Maxwell、Pascal、Volta、Turing、Ampere 到 Ada Lovelace、Hopper每一代的指令集都有差异。如果只生成 SASS那这份二进制就锁死在特定架构上而 PTX 可以在运行时由驱动用 JITJust-In-Time方式重新编译成当前 GPU 的机器码从而实现一定程度的向前兼容。2.2 算力编号与显卡型号的对应关系compute_XX和sm_XX里的数字就是所谓的GPU 算力Compute Capability它和显卡型号的对应关系是配-gencode的基础。下面这张表是我整理的高频型号对照建议收藏显卡系列代表型号算力 (Compute Capability)arch/code 写法MaxwellGTX 750 Ti5.0compute_50 / sm_50PascalGTX 10806.1compute_61 / sm_61VoltaTesla V1007.0compute_70 / sm_70TuringRTX 20807.5compute_75 / sm_75AmpereRTX 3090 / A1008.0 / 8.6compute_80 / sm_80、compute_86 / sm_86Ada LovelaceRTX 4090 / 4060 Ti8.9compute_89 / sm_89HopperH1009.0compute_90 / sm_90查自己显卡算力最直接的办法是跑nvidia-smi看型号然后对照官方文档或者在装好 CUDA 后跑deviceQuery这个 sample它会直接打印CUDA Capability Major/Minor version number。我习惯用后者因为它是从驱动实际读出来的最准。注意算力编号是主版本.次版本的形式比如 8.9 对应compute_89中间没有小数点。写成compute_8.9是错的nvcc会直接报错。2.3 不写 -gencode 会发生什么如果你偷懒编译时完全不指定-gencodenvcc会用一套默认值。这套默认值随 CUDA 版本变化通常只覆盖几个主流架构。问题在于如果你的显卡算力不在默认列表里生成的二进制就没有对应的 SASS运行时只能靠 PTX JIT 兜底——前提是你编译时生成了 PTX。更糟的情况是某些构建系统比如 CMake 里配置不当的 OpenCV会显式指定一组-gencode而这组值里恰好没有你的架构。这时候编译照样成功但运行时报no kernel image is available。这个错误的字面意思就是设备上没有可用的 kernel 镜像翻译成人话就是你给我的这份二进制里没有我能执行的代码。3. 适配自己显卡的 -gencode 该怎么写3.1 单卡场景最简写法与推荐写法假设你只有一张 RTX 4090算力 8.9。最直接的写法是nvcc -gencode archcompute_89,codesm_89 -o myapp myapp.cu这样生成的二进制里只有 sm_89 的 SASS在 4090 上跑没问题。但如果你以后换了卡或者要把程序发给别人用这份二进制就废了。所以更稳妥的写法是同时生成 PTXnvcc -gencode archcompute_89,codesm_89 \ -gencode archcompute_89,codecompute_89 \ -o myapp myapp.cu第二行的codecompute_89表示把 PTX 也嵌进二进制。这样即使未来在更高算力的卡上运行驱动也能用这段 PTX 做 JIT 编译向前兼容。代价是二进制体积会大一些但通常可以接受。我个人的习惯是开发阶段只编当前卡的 SASS追求编译速度发布阶段同时嵌入 PTX追求兼容性。这个取舍后面还会细说。3.2 多卡异构场景一次编译覆盖多张卡实验室或公司里经常出现一台机器插多张不同代显卡的情况比如一张 30908.6加一张 40908.9。这时候-gencode要写多组nvcc -gencode archcompute_86,codesm_86 \ -gencode archcompute_89,codesm_89 \ -gencode archcompute_89,codecompute_89 \ -o myapp myapp.cu每组-gencode都会往最终二进制里塞一份对应的代码。注意这里只嵌入了最高算力8.9的 PTX因为 PTX 的向前兼容是单向的——低算力的 PTX 不能在高算力卡上 JIT但高算力的 PTX 可以在更高算力卡上用。所以嵌入最高版本的 PTX 就够了。有个细节容易忽略-gencode的顺序会影响编译产物的组织但一般不影响正确性。不过在某些老版本 CUDA 上顺序错乱可能导致链接阶段找不到符号保险起见按算力从低到高排列。3.3 用 CMake 时怎么传 -gencode现在大部分项目用 CMake 构建直接写nvcc命令的机会不多。CMake 里控制-gencode有几个层次。最简单的是设CMAKE_CUDA_ARCHITECTURESset(CMAKE_CUDA_ARCHITECTURES 86 89)CMake 会自动把它翻译成对应的-gencode。注意这里写的是86、89这种不带前缀的数字不是sm_86。CMake 3.18 以后支持这个变量老版本可能不认。如果你需要更精细的控制比如同时嵌入 PTX可以用CMAKE_CUDA_FLAGS手动追加set(CMAKE_CUDA_FLAGS ${CMAKE_CUDA_FLAGS} -gencode archcompute_89,codesm_89 -gencode archcompute_89,codecompute_89)但要注意如果同时设了CMAKE_CUDA_ARCHITECTURES和手动-gencode可能会冲突导致重复或矛盾的架构列表。我的做法是二选一要么全用 CMake 变量要么全手动别混着来。编译 OpenCV 这种大型项目时-gencode配错是重灾区。OpenCV 的CUDA_ARCH_BIN和CUDA_ARCH_PTX两个 CMake 变量分别对应 SASS 和 PTX。CUDA_ARCH_BIN填8.9CUDA_ARCH_PTX填8.9它内部会转成-gencode。如果只填了CUDA_ARCH_BIN没填 PTX编译出来的 OpenCV 就只能在 8.9 的卡上跑。4. 那些年我踩过的 -gencode 坑与排查链路4.1 no kernel image is available 的完整定位过程这个报错我遇到过至少五次每次原因都不太一样。分享一次最典型的排查过程。当时场景是在 Ubuntu 22.04 上用 CUDA 11.8 编译一个自定义的 PyTorch 扩展机器是 RTX 4090。pip install -e .编译成功import也没问题但一调用自定义算子就报CUDA error: no kernel image is available for execution on the device。第一步确认显卡算力。跑nvidia-smi看到 4090算力应该是 8.9。但 CUDA 11.8 发布时 4090 还没上市它的默认架构列表里没有 sm_89。这就是根因的雏形。第二步确认编译时用了哪些架构。PyTorch 扩展的编译走的是torch.utils.cpp_extension它默认读取环境变量TORCH_CUDA_ARCH_LIST。如果这个变量没设它会用 PyTorch 自己编译时的架构列表。而我装的 PyTorch 是官方 wheel编译时覆盖的架构里同样没有 8.9。第三步验证。在 Python 里跑torch.cuda.get_arch_list()输出里果然没有sm_89。到这一步基本确认编译产物里没有 4090 能执行的 SASS。第四步修复。设置环境变量后重新编译export TORCH_CUDA_ARCH_LIST8.9 pip install -e . --no-build-isolation--no-build-isolation是为了让编译过程能读到当前环境变量否则 pip 会开一个隔离环境变量传不进去。重新编译后再跑问题消失。这个链路的关键在于报错信息只告诉你没有可用镜像不告诉你为什么没有。你得自己顺着显卡算力 → 编译架构列表 → 实际生成的 SASS这条线去查。4.2 编译成功但性能腰斩PTX JIT 的隐性代价另一个坑更隐蔽。有次我把程序发给同事他机器是 A100算力 8.0我本地是 40908.9。我编译时只嵌入了compute_89的 PTX没生成 sm_80 的 SASS。结果程序在他机器上能跑但速度只有我这边的一半。原因就是PTX JIT。A100 的驱动发现二进制里没有 sm_80 的 SASS于是拿compute_89的 PTX 现场 JIT 编译。JIT 编译出来的代码质量通常不如nvcc离线编译的 SASS因为 JIT 时间有限优化级别会降低。而且 JIT 发生在程序启动时还会带来额外的启动延迟。这个坑的教训是PTX 向前兼容是能跑的保障不是跑得好的保障。如果你的程序要在多种架构上部署最好为每种目标架构都生成对应的 SASS而不是指望 PTX JIT 兜底。排查这类问题的方法是看启动日志。CUDA 在 JIT 时会有提示或者用CUDA_FORCE_PTX_JIT1强制走 JIT 对比性能。如果强制 JIT 后性能明显下降说明你之前依赖的就是 JIT。4.3 多版本 CUDA 共存时的架构列表混乱机器上装了 CUDA 11.8 和 CUDA 12.4 两个版本用update-alternatives切换。有次编译一个项目明明nvcc --version显示的是 12.4但生成的二进制在 4090 上跑不了。查了半天发现项目的 CMake 缓存里还留着上次用 11.8 编译时的CMAKE_CUDA_ARCHITECTURES值而 11.8 不支持 8.9。切换 CUDA 版本后没有清缓存CMake 继续用旧值。修复很简单删掉build目录重新cmake。但这个坑提醒我切换 CUDA 版本后一定要清构建缓存。不同 CUDA 版本支持的架构列表不同缓存里的架构值可能在新版本里无效或者反过来新版本支持但缓存里没有。顺便说下各 CUDA 版本对架构的支持边界这个在选版本时很有用CUDA 版本最高支持算力说明CUDA 10.27.5不支持 AmpereCUDA 11.08.0首次支持 AmpereCUDA 11.18.6支持 RTX 30 系CUDA 11.88.9支持 Ada LovelaceCUDA 12.09.0支持 Hopper如果你用的是 4090 或 4060 TiCUDA 版本至少要 11.8。用 11.7 及以下nvcc根本不认识sm_89会直接报Unsupported gpu architecture。4.4 一个反直觉的现象为什么加了 -gencode 反而更慢有次我为了保险把从 5.0 到 8.9 的所有架构全写进-gencode想着这样到哪都能跑。结果编译时间暴涨不说生成的二进制体积翻了好几倍程序启动也变慢了。原因是每组-gencode都会让nvcc完整编译一遍代码生成对应的 SASS。架构越多编译时间越长二进制越大。而程序启动时CUDA 运行时要在这一堆代码里查找匹配当前设备的版本查找开销也随之增加。所以-gencode不是越多越好。只编你实际会用到的架构这是原则。如果确实需要广泛兼容用少量 SASS 一份最高版本 PTX的组合而不是把所有 SASS 都编出来。5. 把 -gencode 用对的几条实操准则5.1 开发、测试、发布三阶段的差异化配置经过这些年的折腾我总结出一套分阶段的配置策略直接可以抄开发阶段只编当前开发机的架构追求编译速度。-gencode archcompute_89,codesm_89测试阶段加上 PTX方便在测试机的不同卡上快速验证。-gencode archcompute_89,codesm_89 \ -gencode archcompute_89,codecompute_89发布阶段覆盖所有目标部署环境的 SASS外加最高版本的 PTX。-gencode archcompute_80,codesm_80 \ -gencode archcompute_86,codesm_86 \ -gencode archcompute_89,codesm_89 \ -gencode archcompute_89,codecompute_89这套策略的核心逻辑是编译速度和运行兼容性是一对矛盾不同阶段侧重不同。开发时你改代码频繁编译慢会严重影响效率发布时你只编一次多花点时间换兼容性完全值得。5.2 用环境变量统一管理架构列表在团队协作里每个人的显卡可能不同硬编码-gencode会导致别人的机器跑不了。我的做法是用环境变量统一管理构建脚本读取变量生成-gencode。对于 PyTorch 扩展直接用官方的TORCH_CUDA_ARCH_LISTexport TORCH_CUDA_ARCH_LIST8.0;8.6;8.9PTX注意PTX后缀它表示同时嵌入该架构的 PTX。这个语法是 PyTorch 特有的很方便。对于 CMake 项目可以自定义一个变量if(DEFINED ENV{CUDA_ARCH_LIST}) set(CMAKE_CUDA_ARCHITECTURES $ENV{CUDA_ARCH_LIST}) else() set(CMAKE_CUDA_ARCHITECTURES 89) endif()这样团队成员各自设自己的环境变量构建脚本不用改。CI 环境里统一设成发布用的架构列表。5.3 验证 -gencode 是否生效的三种方法配完-gencode不能想当然得验证。我常用三种方法从粗到细。方法一看编译日志。nvcc加-v参数会打印详细的编译过程包括为哪些架构生成了代码。构建大型项目时日志里搜gpu architecture能看到实际用的架构列表。方法二用 cuobjdump 检查二进制。这是最直接的方法cuobjdump -lelf myapp它会列出二进制里包含的所有 ELF 段每个段对应一个架构。如果输出里没有你显卡的sm_XX说明没编进去。方法三运行时查询。在代码里调用cudaFuncGetAttributes或者直接看deviceQuery的输出确认运行时实际加载的是哪个版本的代码。这个方法能发现 PTX JIT 的情况——如果实际执行的是 JIT 代码说明没有对应的 SASS。我一般在发布前用方法二过一遍确保目标架构都在。方法三用于排查性能问题确认没有意外走 JIT。5.4 几个容易忽略的边界情况最后说几个边角料但很关键的细节。算力 7.5 和 8.0 之间的兼容性。Turing7.5和 Ampere8.0之间不兼容sm_75 的 SASS 不能在 8.0 的卡上跑反之亦然。这两个架构的指令集差异较大必须分别编译。算力 8.0 和 8.6 的关系。8.6 是 8.0 的超集sm_80 的 SASS 理论上能在 8.6 的卡上跑但性能不是最优。如果目标只有 8.6直接编 sm_86。算力 8.9 的特殊性。Ada Lovelace 的 8.9 和 Hopper 的 9.0 是两代不同的架构sm_89 的代码不能在 9.0 上跑。别看到数字接近就以为兼容。WSL2 环境下的注意事项。在 WSL2 里装 CUDA 编译-gencode的写法和原生 Linux 一样但要注意 WSL2 的驱动是 Windows 侧提供的CUDA Toolkit 是 Linux 侧的两者版本要匹配。如果nvidia-smi在 WSL2 里显示的 CUDA 版本和nvcc --version不一致以nvidia-smi为准因为它反映的是驱动支持的最高版本。conda 环境里的 CUDA。用 conda 装的cudatoolkit和系统装的 CUDA Toolkit 是两回事。conda 的cudatoolkit只包含运行时库不含nvcc。如果你在 conda 环境里编译用的还是系统的nvcc-gencode的行为由系统nvcc决定。这点在排查为什么 conda 里编译的架构不对时很关键。我在实际项目里养成了一个习惯每次新建构建环境第一件事是跑一遍nvcc --version和nvidia-smi把两个版本号记下来再对照架构支持表确认目标架构可用。这个动作花不了一分钟但能省掉后面几个小时的排查。踩过太多次版本不匹配的坑之后这种前置检查已经成了肌肉记忆。