【Bug已解决】[Build] CMake variableCMAKE_CUDA_ARCHinterpretation is not standard 解决方案
一、现象长什么样
在编译 ONNX Runtime 的 GPU 版本时,通过 CMake 传入 CUDA 架构参数,发现 ORT 对CMAKE_CUDA_ARCH(以及它自己内部用的CMAKE_CUDA_ARCHITECTURES变体)的解析方式和 CMake 官方约定不一致,导致要么编出的二进制不包含目标架构、要么触发 NVCC 报错。现象:
# 现象 A:传 "75" 期望编 sm_75,结果只编了 PTX 没编 cubin # CMake 官方语义:CMAKE_CUDA_ARCHITECTURES=75 表示 "75-real;75-virtual" # 但 ORT 内部把 "75" 当成 “只生成 virtual PTX”,运行时退化到 JIT # 现象 B:传 "75;80" 解析错位 # ORT 按逗号/分号切分后,把 "80" 当成了 "80-real" 却丢了对应 virtual, # 链接时报 “no kernel image available for sm_80” # 现象 C:传 "75-real;80" 直接报错 # NVCC: Invalid device architecture '75-real' —— ORT 没按 CMake 标准 # 把 "-real"/"-virtual" 后缀翻译成 nvcc 的 "-gencode arch=compute_75,code=sm_75"最坑的是现象 A:编译“成功”了,但部署到目标卡(如 T4 sm_75)时因为没有 cubin 只能 JIT 编译 PTX,首次推理巨慢,且某些环境 JIT 被禁用就直接不能跑。
二、背景
CMake 从 3.18 起对 CUDA 架构有标准解释:CMAKE_CUDA_ARCHITECTURES接受形如75、75;80、75-real;80-virtual的值。CMake 会把它翻译成一系列-gencode arch=compute_XX,code=sm_XX(real,生成 cubin)和code=compute_XX(virtual,生成 PTX)。
ONNX Runtime 的构建脚本(在cmake/下)历史上自己实现了一套 CUDA 架构解析,没有复用 CMake 的CMAKE_CUDA_ARCHITECTURES标准机制,而是读一个自定义的CMAKE_CUDA_ARCH变量并按自己的规则切分。这套自定义规则和 CMake 标准有出入:它把裸数字75仅当作 virtual(PTX),而 CMake 标准是real+virtual;它对-real/-virtual后缀的处理也没有正确转成 nvcc 的 gencode。于是“传同样的字符串,ORT 编出来和官方 CMake 项目不一样”。
这是构建系统审查里典型的坑:重复造轮子解析标准变量,且解析语义和官方不一致。
三、根因
自造解析而非复用
CMAKE_CUDA_ARCHITECTURES:ORT 读自定义CMAKE_CUDA_ARCH,自己按;切、自己判断 real/virtual,没用 CMake 的cuda_select_nvcc_arch_flags或标准展开。裸数字只当 virtual:CMake 标准里
75=real(75) + virtual(75),ORT 却只生成virtual(75)(PTX),缺 cubin → 现象 A。-real/-virtual后缀没正确翻译:没把75-real映射成arch=compute_75,code=sm_75,直接把75-real喂给 nvcc → 现象 C。
本质:是构建脚本用自造的非标准解析处理 CUDA 架构,与 CMake 官方语义脱节,导致 cubin/PTX 生成不对。
四、最小可运行复现
下面用 Python 模拟“ORT 自造解析 vs CMake 标准解析”的差异(可运行,复现语义错位):
def ort_parse_buggy(spec: str): """buggy: ORT 自造解析 —— 裸数字只当 virtual,后缀直接透传。""" flags = [] for item in spec.split(";"): if item.endswith("-real"): arch = item[:-5] flags.append(f"-gencode arch=compute_{arch},code=sm_{arch}") elif item.endswith("-virtual"): arch = item[:-8] flags.append(f"-gencode arch=compute_{arch},code=compute_{arch}") else: # 裸数字:ORT 只生成 virtual(错!CMake 标准是 real+virtual) flags.append(f"-gencode arch=compute_{item},code=compute_{item}") return flags def cmake_standard_parse(spec: str): """CMake 标准:裸数字 = real + virtual。""" flags = [] for item in spec.split(";"): if item.endswith("-real"): arch = item[:-5] flags.append(f"-gencode arch=compute_{arch},code=sm_{arch}") elif item.endswith("-virtual"): arch = item[:-8] flags.append(f"-gencode arch=compute_{arch},code=compute_{arch}") else: # 裸数字:real + virtual 都生成 flags.append(f"-gencode arch=compute_{item},code=sm_{item}") flags.append(f"-gencode arch=compute_{item},code=compute_{item}") return flags print("ORT '75':", ort_parse_buggy("75")) # ['-gencode arch=compute_75,code=compute_75'] ← 只有 PTX,缺 cubin print("CMake '75':", cmake_standard_parse("75")) # 含 code=sm_75(cubin)+ code=compute_75(PTX)ORT '75'只有 PTX 没有 cubin,证明现象 A 的退化。
五、解决方案(第一层:最小直接修复)
最小修复:把裸数字按 CMake 标准展开成 real+virtual,并正确翻译-real/-virtual后缀:
def parse_cuda_arch_fixed(spec: str): """修正:与 CMake 标准一致。""" flags = [] for item in spec.split(";"): if item.endswith("-real"): arch = item[:-5] flags.append(f"-gencode arch=compute_{arch},code=sm_{arch}") elif item.endswith("-virtual"): arch = item[:-8] flags.append(f"-gencode arch=compute_{arch},code=compute_{arch}") else: # 裸数字:real + virtual flags.append(f"-gencode arch=compute_{item},code=sm_{item}") flags.append(f"-gencode arch=compute_{item},code=compute_{item}") return flags对应的 CMake 侧也应改为复用 CMake 标准变量:
# 推荐:直接用 CMake 标准机制,不要再读自定义 CMAKE_CUDA_ARCH set(CMAKE_CUDA_ARCHITECTURES "75;80" CACHE STRING "" FORCE) # ORT 的 nvcc 调用走 CMake 自动展开的 CUDA_ARCHITECTURES,不再自造这一层改动最小:把裸数字补上 real 分支、去掉自造变量改用标准CMAKE_CUDA_ARCHITECTURES,cubin 恢复生成。但依赖“每处解析都改对”,下看第二层。
六、解决方案(第二层:结构性改进)
把“CUDA 架构字符串 → nvcc gencode 列表”的解析规则固化成单一事实来源。下面这个 dataclass 集中管理解析契约,CMake 侧和 Python 侧校验都引用同一套规则:
from dataclasses import dataclass, field from typing import List import re @dataclass class CudaArchParsePolicy: """单一事实来源:CUDA 架构字符串的标准解析(对齐 CMake 语义)。""" _cache: dict = field(default_factory=dict) def to_gencode(self, spec: str) -> List[str]: if spec in self._cache: return self._cache[spec] flags: List[str] = [] for item in spec.split(";"): item = item.strip() if not item: continue if item.endswith("-real"): arch = item[:-5] flags.append(f"-gencode arch=compute_{arch},code=sm_{arch}") elif item.endswith("-virtual"): arch = item[:-8] flags.append(f"-gencode arch=compute_{arch},code=compute_{arch}") else: # 裸数字 / 裸 "XX":real + virtual if not re.fullmatch(r"\d+", item): raise ValueError(f"invalid CUDA arch token: {item}") flags.append(f"-gencode arch=compute_{item},code=sm_{item}") flags.append(f"-gencode arch=compute_{item},code=compute_{item}") self._cache[spec] = flags return flags def has_cubin(self, spec: str) -> bool: """断言:解析结果必须至少包含一个 real (code=sm_XX) 项。""" return any("code=sm_" in f for f in self.to_gencode(spec)) # 用法 policy = CudaArchParsePolicy() print(policy.to_gencode("75")) assert policy.has_cubin("75") # 现在为 True这一层的关键收益:
- 标准语义:裸数字永远 real+virtual,杜绝现象 A;
- 校验:
has_cubin断言解析结果含 cubin,缺了立刻发现; - 单一事实来源:所有架构解析规则收口在
CudaArchParsePolicy,CMake 和校验脚本共享同一逻辑。
七、解决方案(第三层:断言 / CI 守护)
把第二层钉成 pytest,挂进 CI,确保解析与 CMake 标准一致、必有 cubin:
import pytest from your_package.cuda_arch import CudaArchParsePolicy def test_bare_number_has_cubin(): # 断言 1:裸数字 "75" 必须生成 real (cubin) policy = CudaArchParsePolicy() flags = policy.to_gencode("75") assert any("code=sm_75" in f for f in flags) assert policy.has_cubin("75") def test_real_virtual_suffix_translated(): # 断言 2:"75-real;80-virtual" 正确翻译 policy = CudaArchParsePolicy() flags = policy.to_gencode("75-real;80-virtual") assert "-gencode arch=compute_75,code=sm_75" in flags assert "-gencode arch=compute_80,code=compute_80" in flags def test_invalid_token_rejected(): # 断言 3:非法 token 必须报错 policy = CudaArchParsePolicy() with pytest.raises(ValueError): policy.to_gencode("abc") def test_multi_arch_all_have_cubin(): # 断言 4:多架构每个都含 cubin policy = CudaArchParsePolicy() for f in policy.to_gencode("70;75;80"): assert "code=sm_" in f四条断言从“裸数字有 cubin”“后缀翻译”“非法拒绝”“多架构全有 cubin”四面把解析回归钉死在 CI。
八、排查清单
编译 ONNX Runtime GPU 版、遇到架构相关问题时:
- 编译“成功”但部署到目标卡首次推理极慢 / 报
no kernel image available?多半是只生成了 PTX 没 cubin(现象 A)。 - 检查传入的是自定义
CMAKE_CUDA_ARCH还是标准CMAKE_CUDA_ARCHITECTURES?前者要警惕自造解析。 - 裸数字
75是否生成了code=sm_75(cubin)?只有code=compute_75就是 bug。 - 用第二层
CudaArchParsePolicy:解析规则收口,且has_cubin校验。 - 加第三层 pytest,断言“裸数字有 cubin、后缀翻译、非法拒绝、多架构全有 cubin”。
- 优先复用 CMake 标准
CMAKE_CUDA_ARCHITECTURES,不要再自造解析变量。
九、小结
ONNX Runtime 的CMAKE_CUDA_ARCH解析 bug 本质是构建脚本用自造的非标准解析处理 CUDA 架构,把裸数字只当 virtual(只生成 PTX 缺 cubin),且-real/-virtual后缀没正确翻译成 nvcc gencode,导致编译“成功”却部署时退化到 JIT 或直接报无 kernel 镜像。修复分三层——第一层把裸数字按 CMake 标准展开成 real+virtual 并改用标准CMAKE_CUDA_ARCHITECTURES;第二层用CudaArchParsePolicy这个 dataclass 把解析规则收口成单一事实来源并加has_cubin校验;第三层用四条 pytest 把“裸数字有 cubin、后缀翻译、非法拒绝、多架构全有 cubin”钉死在 CI。核心心法:CUDA 架构解析必须对齐 CMake 官方语义(裸数字 = real+virtual),且必须有 cubin 校验,否则编译通过却部署失效。