GPU 编号错乱踩坑指南:PyTorch cuda 编号与 nvidia-smi 不一致
本文记录一次多 GPU 服务器上,PyTorch 的 cuda 设备编号与 nvidia-smi 显示编号不一致导致的性能问题,以及排查和解决的完整过程。本篇是系列第一篇,聚焦问题的发现、根因分析、解决方案与常见错误汇总。

作者:吴佳浩
撰稿时间:2026-3-19
最后更新:2026-3-22
测试版本:pytorch 2.8.0
一、问题现象
1.1 背景描述
在一台配备 3 张 Tesla T4 和 1 张 RTX 4090 的服务器上,团队需要同时运行 modelA 服务和 modelB 推理服务。由于 modelB 的计算量远大于 modelA,自然而然地想把 modelB 分配到算力最强的 RTX 4090 上,把三个 modelA 服务分别跑在三张 T4 上。
这个思路完全正确,问题出在"如何指定 GPU"这个细节上。
1.2 nvidia-smi 的显示
服务器上执行 nvidia-smi,输出如下:
diff
+-------+---------------------------+------------------+
| GPU | Name | Bus-Id |
+-------+---------------------------+------------------+
| 0 | Tesla T4 | 00000000:5E:00.0 |
| 1 | NVIDIA GeForce RTX 4090 | 00000000:86:00.0 |
| 2 | Tesla T4 | 00000000:AF:00.0 |
| 3 | Tesla T4 | 00000000:D8:00.0 |
+-------+---------------------------+------------------+
从这个输出来看,GPU 0 是 T4,GPU 1 是 4090,GPU 2 和 GPU 3 是 T4。那么很自然地,开发者在代码中这样写:
python
INFER_DEVICE_MAP = {
"modelA": "cuda:0", # 以为是 T4
"modelB": "cuda:1", # 以为是 4090
}
1.3 实际现象
部署上线后,modelB 的推理速度异常缓慢,与在 T4 上运行时几乎没有区别。4090 的 FP16 算力约为 330 TFLOPS,T4 的 FP16 算力约为 65 TFLOPS,两者相差约 5 倍;在实际矩阵乘法基准测试中,4090 比 T4 快约 13 倍。如果 modelB 真的跑在了 4090 上,推理速度应该有非常明显的提升,而现实中却看不到任何变化。
更诡异的是,用 nvidia-smi 查看显存占用,发现 GPU 1(4090)的显存有占用,但代码明明写的是 cuda:1,按照 nvidia-smi 的显示,cuda:1 就应该是 4090,这怎么解释?
实际情况完全相反 :在 PyTorch 的默认配置下,cuda:1 根本不是 RTX 4090,而是一张 Tesla T4。
二、两套编号体系
这个问题的根源在于:nvidia-smi 和 PyTorch 使用了两套完全不同、彼此独立的 GPU 编号规则。它们是通过不同的底层 API 访问 GPU 的,各自有自己的排序逻辑,在混合 GPU 型号的场景下,两套编号必然产生偏差。
2.1 nvidia-smi 的编号规则
nvidia-smi 工具通过 NVML(NVIDIA Management Library,NVIDIA 管理库)与 GPU 驱动直接通信,获取设备信息。NVML 在枚举 GPU 时,始终按照 PCI 总线地址(PCI Bus ID) 从小到大排序。PCI 总线地址是一个物理硬件地址,格式为 Domain:Bus:Device.Function,例如 00000000:5E:00.0,其中 5E 是总线编号的十六进制表示。
在本文的服务器上:
ini
GPU 0 -> Bus 5E:00.0 (T4) 十六进制 5E = 十进制 94
GPU 1 -> Bus 86:00.0 (4090) 十六进制 86 = 十进制 134
GPU 2 -> Bus AF:00.0 (T4) 十六进制 AF = 十进制 175
GPU 3 -> Bus D8:00.0 (T4) 十六进制 D8 = 十进制 216
按照总线地址从小到大排序:94 < 134 < 175 < 216,所以 T4(Bus 5E)排在第 0 位,4090(Bus 86)排在第 1 位,以此类推。这个顺序反映的是 GPU 插在主板 PCIe 插槽上的物理位置,是稳定不变的物理属性。
关键特性 :nvidia-smi 的编号顺序不受任何软件配置影响,始终与 PCI 总线地址保持一致,是一种稳定的、可信赖的物理编号。
2.2 PyTorch 的默认编号规则
PyTorch 并不直接操作 NVML,它调用的是 CUDA Runtime API(cudaGetDeviceCount、cudaSetDevice 等)。CUDA Runtime 再向下调用 CUDA Driver,而 CUDA Driver 在枚举 GPU 时,会读取一个名为 CUDA_DEVICE_ORDER 的环境变量来决定排序策略。
当 CUDA_DEVICE_ORDER 未设置时,默认使用 FASTEST_FIRST 策略,即按照 GPU 的计算能力(Compute Capability)从高到低排序 。计算能力是 NVIDIA 用来衡量 GPU 架构代际和功能的版本号,格式为 major.minor,例如 sm_89 表示第 8 代第 9 修订版,对应 Ada Lovelace 架构(RTX 4090 所在的架构);sm_75 表示第 7 代第 5 修订版,对应 Turing 架构(Tesla T4 所在的架构)。
显然,sm_89 > sm_75,所以 RTX 4090 的计算能力更强,在 FASTEST_FIRST 策略下会被排到第 0 位。
在本文服务器上,PyTorch 默认的编号结果是:
makefile
cuda:0 -> RTX 4090 (sm_89,Ada Lovelace,计算能力最强)
cuda:1 -> Tesla T4 (sm_75,Turing)
cuda:2 -> Tesla T4 (sm_75,Turing)
cuda:3 -> Tesla T4 (sm_75,Turing)
这就是开发者写 cuda:1 却跑在 T4 上的根本原因。
2.3 两套体系的完整对比
从图中可以清楚地看到,nvidia-smi 里的 GPU 1(4090)实际上对应 PyTorch 里的 cuda:0,而开发者认为的 cuda:1(4090)实际上是 T4。
2.4 为什么同型号服务器不会暴露这个问题
如果服务器上所有 GPU 型号完全相同(比如 4 张 T4),那么 FASTEST_FIRST 策略在第一轮按计算能力分组时,所有 GPU 都在同一组(都是 sm_75),第二轮会在组内按 PCI Bus ID 升序排列。这个结果与 nvidia-smi 的 PCI Bus ID 排序完全一致,所以编号不会出现偏差。
这就是为什么这个问题在同型号服务器上完全暴露不出来,但一旦混用不同型号的 GPU(比如搭配 4090 和 T4,或者 A100 和 A10),编号错乱就会必然出现。这种情况在实际工程中非常常见,比如为了性价比而把旧卡和新卡混搭使用,或者某张卡故障替换后用了不同型号的卡,都会触发这个问题。
三、FASTEST_FIRST 排序机制详解
理解 FASTEST_FIRST 的内部排序逻辑,有助于在遇到更复杂的 GPU 配置时做出正确判断。
3.1 排序的两轮逻辑
CUDA Driver 在 FASTEST_FIRST 模式下的排序遵循以下两轮优先级:
第一轮:按计算能力(Compute Capability)降序
计算能力越高的 GPU,排序越靠前。计算能力高意味着支持更新的 CUDA 特性,通常也意味着更强的算力(但并非绝对,因为同一计算能力下可能有不同规格的产品)。
第二轮:同计算能力的 GPU,按 PCI Bus ID 升序
如果两张 GPU 的计算能力相同(例如都是 sm_75 的 T4),则在这一组内部按照 PCI Bus ID 从小到大排列,与 nvidia-smi 的顺序一致。
以本文服务器为例,排序过程如下:
3.2 计算能力与架构对照
了解常见 GPU 的计算能力,有助于预判 FASTEST_FIRST 的排序结果:
| 架构 | 代表 GPU 型号 | 计算能力 |
|---|---|---|
| Hopper | H100 | sm_90 |
| Ada Lovelace | RTX 4090, RTX 4080 | sm_89 |
| Ampere | A100, A30, RTX 3090 | sm_80 |
| Ampere(消费级) | RTX 3080, A10 | sm_86 |
| Turing | T4, RTX 2080 Ti | sm_75 |
| Volta | V100 | sm_70 |
| Pascal | GTX 1080 Ti, P100 | sm_61/60 |
在混合 GPU 服务器上,计算能力最高的卡会被排到 cuda:0,其余卡按组内 Bus ID 升序排列。
3.3 nvidia-smi 与 PyTorch 的底层路径
这两套编号不一致的根本技术原因,是它们走了两条完全独立的底层 API 路径:
NVML 是一个独立的管理接口,专门用于监控和管理 GPU,与 CUDA 计算路径完全分离。所以不管 CUDA 侧怎么排序,nvidia-smi 看到的顺序永远是 PCI Bus ID 顺序。
四、问题排查的完整过程
4.1 第一步:发现显存占用异常
排查的起点是观察 nvidia-smi 的实时输出。运行 watch -n 1 nvidia-smi,在 modelB 服务工作时查看各 GPU 的显存占用。
预期:cuda:1 对应 nvidia-smi 里的 GPU 1(4090),GPU 1 应该有大量显存占用。 实际:GPU 1(4090)确实有显存占用,但 cuda:1 指的应该也是 4090,这一步看起来没问题。
但仔细想想就会发现矛盾:如果 cuda:1 真的是 4090,性能为什么只有 T4 的水平?于是进入第二步。
4.2 第二步:打印 PyTorch 看到的设备名称
直接用 Python 打印 PyTorch 能看到的每个 cuda 设备名称:
python
import torch
for i in range(torch.cuda.device_count()):
print(f"cuda:{i} -> {torch.cuda.get_device_name(i)}")
输出:
makefile
cuda:0 -> NVIDIA GeForce RTX 4090
cuda:1 -> Tesla T4
cuda:2 -> Tesla T4
cuda:3 -> Tesla T4
这一步彻底揭示了问题:在 PyTorch 的视角里,cuda:0 才是 4090,cuda:1 是 T4 。把 modelB 分配到 cuda:1,跑的是 T4,当然慢。
4.3 第三步:验证 PCI_BUS_ID 模式
为了确认修复思路,在设置 CUDA_DEVICE_ORDER=PCI_BUS_ID 后再次打印:
python
import os
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
import torch
for i in range(torch.cuda.device_count()):
print(f"cuda:{i} -> {torch.cuda.get_device_name(i)}")
输出:
makefile
cuda:0 -> Tesla T4
cuda:1 -> NVIDIA GeForce RTX 4090
cuda:2 -> Tesla T4
cuda:3 -> Tesla T4
此时 PyTorch 的编号与 nvidia-smi 完全一致:cuda:0 是 T4(Bus 5E),cuda:1 是 4090(Bus 86),cuda:2 和 cuda:3 是另外两张 T4。
注意 :os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" 必须在 import torch 之前执行,否则 CUDA Runtime 已经完成初始化,这个设置不会生效。上面的示例代码只是演示效果,在实际项目中必须在文件最开头、import torch 之前设置。
4.4 第四步:性能对比基准测试
为了量化两张卡的实际性能差距,以及验证任务确实跑在了正确的卡上,运行矩阵乘法基准测试:
python
import torch, time
for gpu_id in [0, 1, 2, 3]:
name = torch.cuda.get_device_name(gpu_id)
x = torch.randn(8192, 8192, device=f"cuda:{gpu_id}")
# 预热:让 GPU 进入全速工作状态,排除冷启动的影响
for _ in range(50):
y = x @ x
torch.cuda.synchronize(device=gpu_id)
start = time.time()
for _ in range(100):
y = x @ x
torch.cuda.synchronize(device=gpu_id)
elapsed = time.time() - start
print(f"cuda:{gpu_id} ({name}): {elapsed:.3f}s")
在默认模式(FASTEST_FIRST,未设置 PCI_BUS_ID)下的输出:
makefile
cuda:0 (NVIDIA GeForce RTX 4090): 2.033s
cuda:1 (Tesla T4): 26.447s
cuda:2 (Tesla T4): 27.084s
cuda:3 (Tesla T4): 26.436s
结论非常清晰:4090 比 T4 快约 13 倍 。在这种情况下把 modelB(最消耗算力的任务)放到了 cuda:1(T4),性能损失高达 93%,相当于白白浪费了一张 4090。
这个基准测试同时还说明了另一件事:torch.cuda.synchronize(device=gpu_id) 的重要性。GPU 操作是异步的,如果不调用 synchronize,time.time() 记录的只是 CPU 提交操作的时间,不是 GPU 实际执行完成的时间,会得到错误的计时结果。
4.5 完整排查流程图
五、根本原因深度解析
5.1 CUDA Runtime 的完整枚举流程
理解枚举流程,有助于掌握各种环境变量的生效时机:
关键点 :CUDA Runtime 的初始化是一次性操作 ,在进程生命周期内只发生一次。一旦初始化完成,设备映射表就固定了,之后修改 CUDA_DEVICE_ORDER 环境变量不会有任何效果。这就是为什么必须在 import torch 之前设置。
5.2 为什么 PyTorch 选择 FASTEST_FIRST 作为默认值
从 CUDA 的设计初衷来看,FASTEST_FIRST 是有其合理性的:在早期的 CUDA 应用中,大多数程序只使用一张 GPU,用 cuda:0 指定。如果系统里有多张不同性能的 GPU,把最强的那张放在 cuda:0 可以让程序在不做任何修改的情况下自动用上最快的卡,对单 GPU 程序是友好的。
然而随着多 GPU 应用的普及,以及混合 GPU 配置的出现,这个默认值带来的麻烦远大于便利。TensorFlow 和 JAX 显然也意识到了这一点,所以它们选择默认使用 PCI_BUS_ID 排序,与 nvidia-smi 保持一致,减少了开发者的认知负担。
5.3 各框架的行为差异
不同深度学习框架对 GPU 编号的处理方式存在明显差异:
PyTorch 是目前主流深度学习框架中唯一默认使用 FASTEST_FIRST 的,这也是为什么这个编号错乱的问题几乎专属于 PyTorch 生态。
六、解决方案详解
6.1 方案一:在启动脚本中设置环境变量(推荐)
这是生产环境中最推荐的方案。将环境变量设置写入服务的启动脚本,每次启动服务时自动生效:
bash
#!/bin/bash
# 必须使用 export,不能只写赋值语句
# 原因:export 会将变量标记为需要传递给子进程
# 如果只写 CUDA_DEVICE_ORDER=PCI_BUS_ID(没有 export),
# 这个变量只在当前 shell 生效,python 子进程看不到它
export CUDA_DEVICE_ORDER=PCI_BUS_ID
WORKSPACE="/workspace/coder"
cd ${WORKSPACE}
....(这里各人情况不同作为省略)
这种方式的优点:
- 每次启动服务时自动生效,不需要人工干预
- 不影响同一台服务器上其他用户或其他项目
- 脚本逻辑清晰,一眼就能看到 GPU 编号策略
6.2 方案二:在 Python 代码开头设置
对于单文件项目或者不方便修改启动脚本的场景,可以在 Python 代码的最开头设置:
python
import os
# 这两行必须在 import torch 之前,否则无效
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
# 可以同时设置 CUDA_VISIBLE_DEVICES 来限制可见 GPU
# os.environ["CUDA_VISIBLE_DEVICES"] = "0,1,2,3"
import torch # 在这里 CUDA Runtime 才会初始化,此时能读到上面设置的环境变量
# 之后可以放心地使用 cuda:0、cuda:1 等,它们与 nvidia-smi 一致
device = torch.device("cuda:1") # 此时 cuda:1 就是 nvidia-smi 里的 GPU 1(4090)
这种方式的注意事项:
- 必须是文件的最顶部,不能放在任何 import torch 之后
- 如果项目有多个入口文件,每个文件都要加,或者统一在
__init__.py中处理 - 代码可移植性好,但如果有人在别的地方 import 了 torch 然后再 import 这个文件,设置可能失效
6.3 方案三:写入系统环境变量(全局永久生效)
如果这台服务器上所有 Python 项目都需要这个配置,可以写入 ~/.bashrc 或 /etc/environment:
bash
# 写入当前用户的 bashrc(仅对当前用户生效)
echo 'export CUDA_DEVICE_ORDER=PCI_BUS_ID' >> ~/.bashrc
source ~/.bashrc
# 验证
echo $CUDA_DEVICE_ORDER
# 输出:PCI_BUS_ID
或者写入系统级环境变量文件(对所有用户生效):
bash
# 写入 /etc/environment(系统级,重启后生效)
echo 'CUDA_DEVICE_ORDER=PCI_BUS_ID' | sudo tee -a /etc/environment
# 注意:/etc/environment 里不需要 export 关键字,格式是 KEY=VALUE
这种方式的优缺点:
- 优点:一劳永逸,不需要在每个脚本或代码文件中重复设置
- 缺点:可能影响同一用户或系统上的其他项目;如果其他项目刻意依赖 FASTEST_FIRST 行为,会产生意外影响
6.4 三种方案对比
七、常见错误汇总
7.1 忘记 export,只写赋值
这是最常见的错误,尤其对 shell 不太熟悉的开发者容易犯:
bash
# 错误写法:子进程(python 进程)看不到这个变量
CUDA_DEVICE_ORDER=PCI_BUS_ID
python train.py
# 正确写法
export CUDA_DEVICE_ORDER=PCI_BUS_ID
python train.py
为什么 export 是必要的?在 Linux shell 中,普通的变量赋值(VAR=value)只在当前 shell 进程中有效,不会被子进程继承。只有通过 export 标记的变量,才会被子进程继承到其环境(environment)中。Python 脚本是通过 shell 启动的子进程,它通过 os.environ 读取环境变量,如果父 shell 没有 export,子进程就读不到这个变量。
验证方法:
bash
# 不使用 export
CUDA_DEVICE_ORDER=PCI_BUS_ID
python -c "import os; print(os.environ.get('CUDA_DEVICE_ORDER', 'NOT FOUND'))"
# 输出:NOT FOUND
# 使用 export
export CUDA_DEVICE_ORDER=PCI_BUS_ID
python -c "import os; print(os.environ.get('CUDA_DEVICE_ORDER', 'NOT FOUND'))"
# 输出:PCI_BUS_ID
7.2 在 import torch 之后才设置环境变量
这是在 Python 代码中设置时最容易出现的错误:
python
# 错误:torch 已经在第一行 import 时初始化了 CUDA,
# 下面的 os.environ 设置来不及影响初始化过程
import torch
import os
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" # 已经晚了,无效
# 错误:即使写在同一个代码块里,只要 import torch 在前面,就无效
import torch
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
print(torch.cuda.get_device_name(0)) # 仍然是 FASTEST_FIRST 的排序
python
# 正确:os 的设置必须在 import torch 之前
import os
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
import torch # 此时 CUDA Runtime 初始化,能读到上面设置的值
print(torch.cuda.get_device_name(0)) # 此时是 PCI_BUS_ID 的排序
理解这个错误的关键是:import torch 触发的不只是模块加载,还会执行 CUDA Runtime 的初始化(在内部调用 cudaInit 或类似函数),而初始化时会固定设备映射。一旦初始化完成,后续修改 CUDA_DEVICE_ORDER 对已经运行的进程不再有效。
7.3 直接依赖 nvidia-smi 编号写死设备
python
# 错误:看 nvidia-smi GPU 1 是 4090,就直接写 cuda:1
# 未设置 CUDA_DEVICE_ORDER 时,这样写会跑到 T4 上
model.to("cuda:1")
# 正确做法一:先设置 CUDA_DEVICE_ORDER=PCI_BUS_ID,再按 nvidia-smi 编号写
# 正确做法二:不管有没有设置,先验证设备名称
print(torch.cuda.get_device_name(1)) # 先确认这是不是你要的 GPU
model.to("cuda:1")
python
# 更健壮的写法:通过名称而不是编号来查找设备
def find_device_by_name(keyword: str) -> str:
"""根据 GPU 名称关键词找到对应的 cuda 设备编号"""
for i in range(torch.cuda.device_count()):
name = torch.cuda.get_device_name(i)
if keyword in name:
return f"cuda:{i}"
raise RuntimeError(f"No GPU containing '{keyword}' found")
# 使用示例
reranker_device = find_device_by_name("4090")
embedding_device = find_device_by_name("T4")
这种写法的好处是:即使 GPU 编号因为某种原因发生变化(比如换了一张卡,或者修改了 CUDA_DEVICE_ORDER),代码仍然能找到正确的 GPU。
7.4 最安全的做法是
最安全的做法是:在 Python 代码的最顶部同时也设置一份,作为双重保险:
python
import os
# 双重保险:即使环境变量没有正确传递,代码层面也保证设置正确
os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
import torch
os.environ.setdefault 的语义是:如果环境变量已经设置,不覆盖;如果没有设置,则设置为给定的默认值。这样既尊重外部的显式配置,又提供了一个安全的兜底。
八、部署验证清单
每次在多 GPU 服务器上部署服务时,按以下步骤检查:
用于验证的完整脚本:
python
import os
import torch
def diagnose_gpu_mapping():
"""打印详细的 GPU 映射信息,便于与 nvidia-smi 对比"""
order = os.environ.get("CUDA_DEVICE_ORDER", "NOT SET(默认 FASTEST_FIRST)")
print(f"CUDA_DEVICE_ORDER = {order}")
print(f"GPU 总数: {torch.cuda.device_count()}")
print("-" * 60)
for i in range(torch.cuda.device_count()):
props = torch.cuda.get_device_properties(i)
print(f"cuda:{i}")
print(f" 名称: {props.name}")
print(f" 计算能力: sm_{props.major}{props.minor}")
print(f" 总显存: {props.total_mem / 1024**3:.1f} GB")
print()
if __name__ == "__main__":
diagnose_gpu_mapping()
九、一键诊断脚本
将以下内容保存为 diagnose_gpu.sh,用于在任何服务器上快速诊断 GPU 编号是否一致:
bash
#!/bin/bash
echo "============ 物理 GPU 信息(来自 nvidia-smi,始终按 PCI Bus ID 排序)============"
nvidia-smi --query-gpu=index,name,pci.bus_id,memory.total --format=csv,noheader
echo ""
echo "============ 当前环境变量 ============"
echo "CUDA_DEVICE_ORDER=${CUDA_DEVICE_ORDER:-NOT SET(默认 FASTEST_FIRST)}"
echo "CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-NOT SET(所有 GPU 可见)}"
echo ""
echo "============ PyTorch 当前编号(受 CUDA_DEVICE_ORDER 影响)============"
python -c "
import os, torch
print(f'CUDA_DEVICE_ORDER = {os.environ.get(\"CUDA_DEVICE_ORDER\", \"NOT SET\")}')
print(f'设备总数: {torch.cuda.device_count()}')
for i in range(torch.cuda.device_count()):
p = torch.cuda.get_device_properties(i)
print(f' cuda:{i} -> {p.name} (sm_{p.major}{p.minor}, {p.total_mem/1024**3:.1f}GB)')
"
echo ""
echo "============ 强制 PCI_BUS_ID 模式下的编号(应与 nvidia-smi 一致)============"
CUDA_DEVICE_ORDER=PCI_BUS_ID python -c "
import torch
for i in range(torch.cuda.device_count()):
print(f' cuda:{i} -> {torch.cuda.get_device_name(i)}')
"
echo ""
echo "提示:对比上面两组输出。若 PyTorch 当前编号与 PCI_BUS_ID 模式不同,"
echo " 说明 CUDA_DEVICE_ORDER 未设置或设置不正确,建议添加:"
echo " export CUDA_DEVICE_ORDER=PCI_BUS_ID"
运行 bash diagnose_gpu.sh,输出示例:
ini
============ 物理 GPU 信息(来自 nvidia-smi,始终按 PCI Bus ID 排序)============
0, Tesla T4, 00000000:5E:00.0, 15360 MiB
1, NVIDIA GeForce RTX 4090, 00000000:86:00.0, 49140 MiB
2, Tesla T4, 00000000:AF:00.0, 15360 MiB
3, Tesla T4, 00000000:D8:00.0, 15360 MiB
============ 当前环境变量 ============
CUDA_DEVICE_ORDER=NOT SET(默认 FASTEST_FIRST)
CUDA_VISIBLE_DEVICES=NOT SET(所有 GPU 可见)
============ PyTorch 当前编号(受 CUDA_DEVICE_ORDER 影响)============
CUDA_DEVICE_ORDER = NOT SET
设备总数: 4
cuda:0 -> NVIDIA GeForce RTX 4090 (sm_89, 45.7GB)
cuda:1 -> Tesla T4 (sm_75, 14.3GB)
cuda:2 -> Tesla T4 (sm_75, 14.3GB)
cuda:3 -> Tesla T4 (sm_75, 14.3GB)
============ 强制 PCI_BUS_ID 模式下的编号(应与 nvidia-smi 一致)============
cuda:0 -> Tesla T4
cuda:1 -> NVIDIA GeForce RTX 4090
cuda:2 -> Tesla T4
cuda:3 -> Tesla T4
两组对比一目了然:当前 PyTorch 的 cuda:0 是 4090,而 PCI_BUS_ID 模式下 cuda:0 是 T4,说明编号确实存在偏差,需要设置 CUDA_DEVICE_ORDER=PCI_BUS_ID。
十、总结
一行命令,避免数小时排查:
bash
export CUDA_DEVICE_ORDER=PCI_BUS_ID
三条核心原则:
- 永远不要假设
cuda:N等于nvidia-smi的GPU N,混合 GPU 环境下两者很可能不一致。 - 在任何多 GPU 项目的启动脚本中,第一行就写
export CUDA_DEVICE_ORDER=PCI_BUS_ID,养成习惯。 - 部署前务必用
torch.cuda.get_device_name()验证设备映射,不能只看nvidia-smi。