1. 为什么 H100 的 CUDA 版本不能照抄 4090 的作业
上架一台 8 卡 H100 的机器之后,最容易被误解的一件事就是:既然 CUDA 号称向下兼容,那把 4090 上跑得好好的环境原样搬过来不就行了?我见过不止一次这种操作——从另一台 A100 或者 4090 的机器上把 requirements 抄过来,pip install一把梭,torch.cuda.is_available()返回True,随手torch.randn(512, 512).cuda()也不报错,于是所有人以为环境成了。真正的坑在训练脚本跑起来十几分钟之后才露头:loss 掉不下去、精度对不上论文、或者干脆在一个自定义算子那里直接崩掉。
原因在于 H100 用的是 Hopper 架构,计算能力(compute capability)是9.0,而 4090 是 Ada Lovelace 的8.9,A100 是 Ampere 的8.0。这几个数字不是贴在卡上的标签,它直接决定了一个二进制包里有没有塞进对应的 GPU 机器码(kernel image)。CUDA 和 torch 的版本选择,本质上就是在回答一个问题:我装进去的这套东西,里面到底有没有 sm_90 的代码。
1.1 sm_90 这条硬门槛是怎么划出来的
NVIDIA 每一代架构发布,CUDA Toolkit 里就会多一个 target。Hopper 对应的就是sm_90,业界普遍把CUDA 11.8 当作 H100 的起跑线——从这个版本开始,sm_90才被正式纳入支持列表。比它更早的 CUDA 11.x,你可以把 toolkit 装上、nvcc -V也能正常打印,但编译出来的东西里没有 Hopper 的 SASS,跑起来要么退回 JIT 现场编译(慢得让你怀疑人生),要么直接抛no kernel image is available。
这里有个特别容易混淆的点,值得单独拎出来说:能装 ≠ 能跑,能跑 ≠ 跑得快。
- 能装:
pip install torch几乎在任何驱动版本上都可能成功安装,因为 pip 不管你的硬件; - 能跑:
torch.cuda.is_available()为真,只说明驱动和运行时握上手了; - 跑得快:需要 wheel 里真的带
sm_90的优化内核,这跟 CUDA 标签强相关。
我见过太多人卡在第二步就以为完事了。判断标准很简单,下面这段代码的输出才是关键:
import torch print("torch:", torch.__version__) print("built with cuda:", torch.version.cuda) print("cudnn:", torch.backends.cudnn.version()) print("device:", torch.cuda.get_device_name(0)) print("capability:", torch.cuda.get_device_capability(0)) # 期望 (9, 0) print("arch list:", torch.cuda.get_arch_list()) # 期望里面出现 sm_90get_arch_list()这一行是很多教程里压根不写的。它返回的是这个 wheel 编译时打包进去的架构列表。如果里面只有sm_80、sm_86、sm_89,那你在 H100 上就是靠 JIT 兜底,性能损耗在 20% 起步,算子越复杂越明显。
1.2 驱动版本是比 CUDA 版本更靠前的一道闸
新手最容易搞反的一个顺序是:先纠结装哪个 CUDA,装完发现驱动太老,整个白干。正确的思路是看driver 能撑到哪一档 CUDA,然后在那一档里选。
大致门槛如下(Linux 平台,具体以官方 release notes 为准):
| CUDA 运行时版本 | 最低 Linux 驱动版本 | 说明 |
|---|---|---|
| CUDA 11.8 | ≥ 520.61.05 | H100 的最低可用档 |
| CUDA 12.1 | ≥ 530.30.02 | 很多推理框架的默认基线 |
| CUDA 12.4 | ≥ 550.54.14 | 中期比较稳的一档 |
| CUDA 12.6 | ≥ 560.28.03 | 支持新卡的常见选择 |
| CUDA 12.8 | ≥ 570.26 | 较新的 torch wheel 会用到 |
注意:
nvidia-smi右上角显示的 "CUDA Version" 不是你装了什么 CUDA,而是当前驱动最多能支持的 CUDA 版本上限。它显示 12.4,说明你装 CUDA 12.1、11.8 都没问题,但装 12.6 就会在初始化时报错。
在 H100 的实际部署里,我个人的建议是把驱动刷到550 系列以上。原因不是新,而是 CUDA 12.x 的 minor version compatibility 机制:同一大版本内,新驱动可以跑旧运行时,反过来不行。把驱动顶到高位,等于给后面所有版本的切换都留了余地。
1.3 看到 nvidia-smi 之后的三步判断
拿到一台陌生的 H100 机器,别急着 pip,先按这个顺序走一遍:
- 确认卡型和数量:
nvidia-smi -L,确认是 H100 SXM 还是 PCIe,8 卡还是 4 卡。SXM 版本走 NVLink,PCIe 版本走 PCIe Switch,后续多卡通信的参数完全不同。 - 记录驱动版本和 CUDA 上限:
nvidia-smi | head -n 15,把 Driver Version 抄下来,对照上表确定可用区间。 - 确认有没有已存在的 CUDA Toolkit:
ls /usr/local | grep cuda,看看/usr/local/cuda指向哪一版。这一步是为了避免后面编译扩展时,nvcc和 torch 打架。
这三步走完,你才真正知道自己手里有什么牌。跳过它们直接 pip,等于闭着眼睛换轮胎。
2. torch wheel 的命名逻辑:cu118、cu121、cu128 到底差在哪
搞清楚硬门槛之后,下一件事是把 PyTorch 的版本体系吃透。这套命名规则说实话有点绕,但只要理解了一次,后面所有平台都能套用。
2.1 pip 装的 torch 为什么自带一整套 CUDA 运行时
这是最反直觉、也是最重要的一点:用 pip 从官方源装的 torch,会把 CUDA 运行时库一起装进来。
你可以自己验证一下:
pip list | grep nvidia在 cu121 的 torch 环境下,你会看到一长串nvidia-cuda-runtime-cu12、nvidia-cublas-cu12、nvidia-cudnn-cu12、nvidia-nccl-cu12之类的包。它们就是被塞进 site-packages 里的一整套运行时。
这意味着什么?意味着:
- 跑 PyTorch 本身,你系统里装不装 CUDA Toolkit 完全无所谓,只要驱动够新就行;
nvcc -V显示的版本和torch.version.cuda不一致,是正常的,不是 bug;- 只有当你需要编译自定义 CUDA 算子时,系统里的 CUDA Toolkit 才会真正参与进来,那时候版本对齐才成为必须。
我见过太多人在 H100 上折腾一整天,就因为发现nvcc是 11.8 而torch.version.cuda是 12.1,然后开始重装系统级 CUDA,最后把本来好好的环境搞崩。真的没必要,这两个数字本来就允许不同。
2.2 index-url 和三件套版本号的换算规律
PyTorch 官方源按 CUDA 版本分了多个 channel,安装时通过--index-url指定:
# CUDA 12.1 版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # CUDA 12.4 版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 # CPU 版本(H100 上当然不用,但别装错) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cputorch、torchvision、torchaudio三者的版本号有严格的对应关系,不能随便配。规律其实很朴素:torchvision 的次版本号 = torch 的次版本号 + 15。
- torch 2.0.x → torchvision 0.15.x
- torch 2.1.x → torchvision 0.16.x
- torch 2.4.x → torchvision 0.19.x
- torch 2.7.x → torchvision 0.22.x
torchaudio 则基本和 torch 保持同号,2.x 对 2.x。
网上流传着大量"拍脑袋写出来"的安装命令,比如把版本号随手写成一个看起来很长很新的组合,然后--index-url指向某个 cu 目录。这类命令几乎百分百会报Could not find a version that satisfies the requirement。遇到这个报错,先别怀疑网络,先怀疑自己抄的版本号是不是压根不存在,或者三件套没配平。
提示:只装
torch一个包是完全可行的。torchvision 和 torchaudio 只在你要用视觉/音频模型时才需要。单独指定三件套版本,反而更容易因为某一个次版本不存在而整条命令失败。
2.3 一份可以直接对着抄的对应关系表
下面这张表是我在几台 H100 机器上按实际安装结果整理的,可以作为起点。装之前建议还是用pip index versions torch复核一次,避免拿到过期的信息。
| torch 版本 | 官方 wheel 的 CUDA 标签 | torchvision | 对 H100 的适配情况 |
|---|---|---|---|
| 2.0.x | cu118 | 0.15.x | 可用,sm_90 支持有限 |
| 2.1.x | cu118 / cu121 | 0.16.x | 可用,cu121 更推荐 |
| 2.2.x – 2.3.x | cu118 / cu121 | 0.17.x / 0.18.x | 生产上比较常见 |
| 2.4.x – 2.5.x | cu118 / cu121 / cu124 | 0.19.x / 0.20.x | cu124 是甜点档 |
| 2.6.x | cu118 / cu124 / cu126 | 0.21.x | 开始逐步淘汰 cu118 |
| 2.7.x | cu118 / cu126 / cu128 | 0.22.x | 新驱动环境首选 |
| 2.8.x 及以上 | cu126 / cu128 / cu129 | 0.23.x | CUDA 11.8 已不再提供 |
从这张表能看出一个趋势:CUDA 11.8 的 wheel 正在被逐步淘汰。如果你在 H100 上接手了一个老项目,锁死在 torch 1.13 加 cu117,那就只能考虑从源码编译,或者把项目整体升级。这件事越早决策越好,拖到上线前再动,代价会翻好几倍。
3. 从裸机到可跑通:H100 上搭一套 PyTorch 环境的完整动作
前面两章讲的是"怎么选",这一章讲"怎么做"。我按实际落地顺序写一遍,中间会标注哪些地方容易出错。
3.1 拿到机器后的验货清单
第一步永远是确认硬件状态,尤其是二手机或者刚上架的机器。
# 卡型号和数量 nvidia-smi -L # 详情:驱动版本、显存、功耗上限 nvidia-smi -q | grep -E "Product Name|Driver Version|Total|Power Limit|Persistence" # 错误计数(H100 上这一项很重要,XID 错误会导致莫名其妙的崩) nvidia-smi -q -d ECC,PAGE_RETIREMENT # NVLink 拓扑(SXM 版本必看) nvidia-smi topo -mnvidia-smi topo -m的输出里,如果卡间是NV18之类的标记,说明走的是 NVLink;如果显示PHB或者SYS,说明走 PCIe,多卡训练的通信瓶颈会很明显。这个信息直接影响你后面选并行策略和 batch size。
特别提一句 Persistence Mode。在 H100 这种多卡大显存的机器上,不开持久化模式的话,每次进程结束驱动都要重新初始化,多卡加载模型能慢到让你以为卡死了:
sudo nvidia-smi -pm 13.2 conda 环境怎么建才不污染系统
我不建议用系统 Python,也不建议用sudo pip install。H100 的机器一般是多人共用,环境隔离是刚需。
conda create -n h100-torch python=3.10 -y conda activate h100-torch # 换国内镜像加速基础包(可选) pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simplePython 版本我推荐3.10 或 3.11。这两个版本在 H100 生态里的轮子最全,尤其是 FlashAttention、vLLM、DeepSpeed 这类需要编译的库。3.12 也能用,但偶尔会遇到某个包还没出对应 wheel,然后被逼着从源码编译,那是纯粹的浪费时间。
环境建好之后,记得把镜像源改回来再装 torch:
pip install torch torchvision torchaudio \ --index-url https://download.pytorch.org/whl/cu124顺序很重要。先用镜像装基础依赖,再用官方源装 torch,这样既快又不会因为镜像不同步而装到裁剪过的包。
3.3 安装命令与自检脚本
装完之后,跑一遍完整自检。下面这段脚本我基本每个环境都会跑一次:
import torch, time assert torch.cuda.is_available(), "CUDA 不可用,先查驱动" n = torch.cuda.device_count() print(f"GPU 数量: {n}") for i in range(n): p = torch.cuda.get_device_properties(i) print(f"[{i}] {p.name} CC={p.major}.{p.minor} 显存={p.total_memory/1024**3:.1f}GB") print("torch:", torch.__version__, "| cuda:", torch.version.cuda) print("arch list:", torch.cuda.get_arch_list()) # 实测一次矩阵乘,顺便看看有没有明显异常 a = torch.randn(8192, 8192, device="cuda", dtype=torch.float16) b = torch.randn(8192, 8192, device="cuda", dtype=torch.float16) torch.cuda.synchronize(); t0 = time.time() for _ in range(20): c = a @ b torch.cuda.synchronize() dt = (time.time() - t0) / 20 print(f"fp16 8192^3 matmul: {dt*1000:.2f} ms") # 多卡通信自检 if n > 1: import torch.distributed as dist dist.init_process_group("nccl")这段脚本一次性覆盖了四件事:卡能不能用、算力是否正常、编译架构对不对、多卡通信是否通畅。8192³的 fp16 矩阵乘在单张 H100 SXM 上大概在 25–45 毫秒这个量级,如果你测出来是几百毫秒,大概率是没走 Tensor Core 或者退回了 JIT。
4. 装完跑不通的几类高频报错,按这个顺序排查
环境装完只是开始,真正消耗时间的是各种运行期报错。这一章我按"最常见 → 最难缠"的顺序排,每一条都给出定位路径。
4.1 no kernel image is available:内核缺失的定位方法
报错长这样:
RuntimeError: CUDA error: no kernel image is available for execution on the device这个报错基本可以直接锁定为"二进制里没有 sm_90"。排查路径非常清晰:
- 打印
torch.cuda.get_arch_list(),看里面有没有sm_90; - 如果是从源码编译的 torch 或某个扩展,检查编译时有没有设置
TORCH_CUDA_ARCH_LIST; - 检查这个报错是出现在 torch 原生操作上,还是出现在某个第三方 CUDA 扩展上。前者说明 torch 本身装错了,后者只需要重编那个扩展。
重编扩展时,正确的环境变量写法是:
export TORCH_CUDA_ARCH_LIST="9.0" # 如果算子用到了 Hopper 专属指令(wgmma 等),需要带 a 后缀 export TORCH_CUDA_ARCH_LIST="9.0a"9.0和9.0a的区别很多人搞不清。简单说,9.0a是"架构特定"(architecture-specific)的 target,只有它才能用 Hopper 新引入的那些指令。用9.0编出来的代码跑在 H100 上没问题,但拿不到那些新特性带来的加速。所以你看到某个库的安装文档强调"必须用 9.0a",不要以为是笔误。
4.2 编译自定义算子时的 CUDA_HOME 版本错配
另一个高频报错:
The detected CUDA version (11.8) mismatches the version that was used to compile PyTorch (12.4).注意,这个报错只在编译扩展时出现,纯跑推理是不会有这个问题的。原因是扩展编译需要真实的nvcc,而nvcc来自系统或 conda 里的 CUDA Toolkit。
解决办法有两条路:
- 路线 A(推荐):用 conda 装一套和 torch 完全对齐的 toolkit,不碰系统环境:
conda install -c nvidia cuda-toolkit=12.4 -y export CUDA_HOME=$CONDA_PREFIX- 路线 B:直接指定
CUDA_HOME指向某个已装好的 toolkit 目录,然后在PATH里把它的bin放前面。
路线 A 的好处是干净、可复制、跟着环境走。路线 B 适合机器上已经有多套 CUDA 的情况,但PATH的顺序很容易被别的脚本打乱,出问题时很难查。
注意:如果扩展编译卡在
ninja那一步很久没动静,八成是在为多个架构编译。把TORCH_CUDA_ARCH_LIST显式设成9.0,编译时间能从十几分钟降到两三分钟。
4.3 显存、NCCL 和多卡启动参数
H100 单卡 80GB 显存听起来很大,但实际用起来很快见底,尤其是跑长上下文的大模型。常见的三类问题:
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 显存 OOM 但不报具体位置 | 缓存碎片 + 激活值峰值 | 开PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True |
| 多卡启动卡在 init_process_group | NCCL 走了错误网卡 | 设NCCL_SOCKET_IFNAME指定网卡 |
| 训练中途 NCCL timeout | 卡间通信被其他任务抢占 | 检查是否开了 MPS,必要时隔离 GPU |
第一项值得展开说。H100 80GB 显存下,PyTorch 默认的分配器在反复分配释放之后会产生碎片,明明nvidia-smi显示还有 20GB,却报 OOM。打开可扩展内存段之后,这个问题基本消失:
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:TrueNCCL 那一项,H100 机器通常有多张网卡(业务网 + 高速互联网),NCCL 默认可能会挑错。多机训练前,先把NCCL_SOCKET_IFNAME和NCCL_IB_HCA确认一遍,能省下大量"训练启动了三分钟然后 timeout"的时间。
5. 一台机器多个项目:CUDA 多版本共存与容器化落地
H100 的机器通常不归一个人用,也不只跑一个项目。怎么让不同项目各用各的 CUDA,是个绕不开的问题。
5.1 别去动 /usr/local/cuda 这个软链接
很多教程会教你ln -sf /usr/local/cuda-12.4 /usr/local/cuda去切换版本。这个操作在单项目机器上没问题,多项目机器上就是灾难——你切一次,别人正在跑的训练进程可能就崩了。
我的做法是:系统级的/usr/local/cuda保持不动,交给运维统一管。所有版本差异都通过环境变量在进程内解决。
# 需要哪个版本,就在当前 shell 里改,不影响别人 export CUDA_HOME=/usr/local/cuda-12.4 export PATH=$CUDA_HOME/bin:$PATH export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH这三个变量的关系值得记一下:CUDA_HOME是给编译系统找 toolkit 用的,PATH是给nvcc找执行文件用的,LD_LIBRARY_PATH是给运行期找动态库用的。三者缺一个都可能出问题,尤其是第三个,很多"明明 nvcc 对了但运行报找不到 libcudart"的问题都出在这里。
5.2 用 conda 装一套完整的 CUDA Toolkit 用来编译
如果要在一个 conda 环境里从源码编译带 CUDA 的包,最省事的方案是让这个环境自带 toolkit:
conda activate h100-torch conda install -c nvidia cuda-toolkit=12.4 -y export CUDA_HOME=$CONDA_PREFIX这样这套 toolkit 就跟着环境走了。删掉环境就等于删掉 toolkit,不留任何系统残留。对于需要频繁试错、装了一堆编译依赖的项目,这种方式几乎是唯一解。
成本是要多占几个 GB 磁盘。在 H100 这种机器上,磁盘一般不是瓶颈,这笔账很划算。
5.3 验证过的组合要固化下来
环境折腾成功的那一刻,是整件事里最容易被浪费的时刻——很多人就这样让它跑着,过了三个月再想复现,完全想不起来当时装了哪些包。
我的习惯是装完立刻冻结两份清单:
pip freeze > requirements.lock.txt # 只保留直接依赖,方便别人阅读 pip install pipdeptree pipdeptree --warn silence | grep -v "^ " > deps.txtpip freeze出来的清单可以直接用于重建,但可读性差。pipdeptree出来的顶层依赖列表则方便别人快速看懂环境构成。两份都留着,前者用于复现,后者用于沟通。
再进一步,把验证过的组合打成容器镜像,从根上解决"在我机器上是好的"这类问题。镜像里显式写死驱动兼容的上限,比在文档里写一段说明可靠得多。
6. H100 上值得顺手打开的几项设置
环境跑通之后,还有几个开关值得花十分钟调一下,收益往往是两位数百分比的性能提升。
6.1 TF32 相关的精度开关
PyTorch 从 2.x 开始在矩阵乘上默认使用 TF32(在 Ampere 及之后架构上)。TF32 用 19 位有效位做乘法累加,速度比 FP32 快不少,代价是精度下降。
在 H100 上,如果你做的是科学计算、数值模拟这类对精度敏感的任务,需要关掉:
import torch torch.backends.cuda.matmul.allow_tf32 = False torch.backends.cudnn.allow_tf32 = False如果是常规的深度学习训练,开着基本没坏处,甚至能顺带起到一点正则化效果。这个决定没有标准答案,取决于任务本身对数值误差的容忍度。我的做法是先开着跑,如果发现 loss 曲线和论文对不上,再关掉对比一次。
需要注意的是,新版 PyTorch 里这类开关有过调整,不同小版本之间的默认值可能有差异。写训练脚本时最好显式声明,而不是依赖默认行为。
6.2 FlashAttention 与 torch 版本绑定
FlashAttention 在 H100 上的收益非常明显,但它对 torch 和 CUDA 版本相当挑。
几个实际经验:
- FlashAttention 2.x 需要较新的 torch,且对 CUDA 版本有最低要求;
- 版本不匹配时,通常不是报错,而是静默退回普通注意力实现,你以为开了加速其实是白开;
- 一定要在装完之后实测一次显存占用和吞吐,确认真的生效了。
最简单有效的验证方式是跑一个固定规模的注意力计算,对比开与关的显存峰值。如果两者一样,那说明根本没走 FA 的路径。
6.3 TORCH_CUDA_ARCH_LIST 的正确写法
最后回到一个看起来很小但影响很大的点。任何时候你在 H100 上从源码编译东西,都把这一行加上:
export TORCH_CUDA_ARCH_LIST="9.0"不设这个变量,编译系统默认会为一大堆历史架构都编一遍(sm_50到sm_90全来),编译时间翻好几倍,编出来的包还特别大。在确定只用 H100 的场景下,锁定9.0是纯粹的收益。
唯一的例外是,你编出来的 wheel 要分发给不同架构的机器使用。这时候老老实实列出全部目标架构,别偷这个懒。
我自己在 H100 上踩过的最大一个坑,其实跟技术细节关系不大:曾经为了图快,直接复制了别人博客里的安装命令,连版本号都没核对,结果那次排查花了大半天,最后发现问题出在 torchvision 的次版本号少了一位。从那之后,无论多简单的安装,我都会先跑一次pip index versions确认包真实存在,再动手。这个习惯看起来很笨,但它省下来的时间远比它花掉的多。