GPU 编号错乱踩坑指南:PyTorch cuda 编号与 nvidia-smi 不一致

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(cudaGetDeviceCountcudaSetDevice 等)。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 两套体系的完整对比

graph LR subgraph nvidia_smi_PCI_Bus_ID N0["GPU 0 - Tesla T4 - Bus 5E:00.0"] N1["GPU 1 - RTX 4090 - Bus 86:00.0"] N2["GPU 2 - Tesla T4 - Bus AF:00.0"] N3["GPU 3 - Tesla T4 - Bus D8:00.0"] end subgraph PyTorch_FASTEST_FIRST C0["cuda:0 - RTX 4090 - sm_89"] C1["cuda:1 - Tesla T4 - sm_75"] C2["cuda:2 - Tesla T4 - sm_75"] C3["cuda:3 - Tesla T4 - sm_75"] end N1 -.->|对应| C0 N0 -.->|对应| C1 N2 -.->|对应| C2 N3 -.->|对应| C3

从图中可以清楚地看到,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 的顺序一致。

以本文服务器为例,排序过程如下:

graph TD subgraph "FASTEST_FIRST 排序过程" S1["第一轮:按计算能力分组"] S1 --> G1["sm_89 组:RTX 4090(Bus 86)"] S1 --> G2["sm_75 组:T4 x3(Bus 5E、AF、D8)"] G1 --> R1["cuda:0 = RTX 4090"] G2 --> S2["第二轮:组内按 PCI Bus ID 升序"] S2 --> R2["cuda:1 = T4(Bus 5E,即十进制 94)"] S2 --> R3["cuda:2 = T4(Bus AF,即十进制 175)"] S2 --> R4["cuda:3 = T4(Bus D8,即十进制 216)"] end

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 路径:

flowchart LR subgraph "两条独立的路径" A["nvidia-smi"] --> B["NVML API\nnvidia-smi 直接调用 NVML"] B --> C["按 PCI Bus ID 排序\n始终不变,不受任何环境变量影响"] D["PyTorch"] --> E["CUDA Runtime API\ncudaGetDeviceCount 等"] E --> F["CUDA Driver\ncu* 系列函数"] F --> G["读取 CUDA_DEVICE_ORDER 环境变量\n按对应策略排序"] end

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 完整排查流程图

flowchart TD A["发现推理速度异常"] --> B["运行 nvidia-smi 查看 GPU 状态"] B --> C{"显存占用与预期 GPU 一致?"} C -- "是" --> D["排查其他原因:PCIe 降速 / 散热 / 显存不足"] C -- "否" --> E["打印 torch.cuda.get_device_name() 验证 PyTorch 编号"] E --> F{"cuda 编号与 nvidia-smi 一致?"} F -- "是" --> G["检查代码中的 device 分配逻辑是否正确"] F -- "否" --> H["确认是 GPU 编号错乱问题"] H --> I["设置 export CUDA_DEVICE_ORDER=PCI_BUS_ID"] I --> J["重新启动所有相关服务"] J --> K["验证:再次打印设备名称,并与 nvidia-smi 对比"] K --> L["运行基准测试,确认性能符合预期"]

五、根本原因深度解析

5.1 CUDA Runtime 的完整枚举流程

理解枚举流程,有助于掌握各种环境变量的生效时机:

flowchart TD A["程序调用 torch.cuda.init() 或首次使用 GPU"] --> B["CUDA Runtime 初始化(仅执行一次)"] B --> C{"检查 CUDA_DEVICE_ORDER 环境变量"} C -- "未设置 或 值为 FASTEST_FIRST" --> D["按计算能力降序排序\nsm_89 > sm_86 > sm_80 > sm_75 ..."] C -- "值为 PCI_BUS_ID" --> E["按 PCI 总线地址升序排序\n与 nvidia-smi 保持一致"] D --> F["生成 cuda 设备编号映射表"] E --> F F --> G{"检查 CUDA_VISIBLE_DEVICES 环境变量"} G -- "未设置" --> H["所有 GPU 可见,编号不变"] G -- "已设置,如 '1,3'" --> I["仅指定索引的 GPU 可见\n重新从 0 开始编号"] H --> J["完成设备枚举,映射表固定"] I --> J

关键点 :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 编号的处理方式存在明显差异:

graph TD subgraph "各框架的 GPU 编号默认行为" PT["PyTorch\n默认 FASTEST_FIRST\n受 CUDA_DEVICE_ORDER 影响\n混合 GPU 时需要额外注意"] TF["TensorFlow\n默认 PCI_BUS_ID\n与 nvidia-smi 一致\n通常不需要额外设置"] JAX["JAX\n默认 PCI_BUS_ID\n与 nvidia-smi 一致\n通常不需要额外设置"] end PT --> N1["是主流框架中唯一默认 FASTEST_FIRST 的\n这也是这个坑主要出现在 PyTorch 项目中的原因"] TF --> N2["默认行为符合直觉"] JAX --> N3["默认行为符合直觉"]

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 三种方案对比

graph LR subgraph "方案选择" A["方案一:启动脚本 export\n推荐,灵活可控"] B["方案二:Python 代码开头设置\n适合单文件或小项目"] C["方案三:写入 bashrc / 系统变量\n全局生效"] end A --> RA["每次启动服务自动生效\n不影响其他用户\n可以针对不同服务设置不同策略"] B --> RB["代码可移植\n但容易在多入口项目中遗漏\n必须在所有入口文件的最顶部"] C --> RC["一劳永逸\n但可能影响其他项目\n谨慎用于共享服务器"]

七、常见错误汇总

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 服务器上部署服务时,按以下步骤检查:

flowchart LR A["1. 运行 nvidia-smi\n记录物理 GPU 顺序和 Bus ID"] --> B["2. 打印 torch.cuda.get_device_name()\n确认 PyTorch 的设备编号"] B --> C["3. 对比两者是否一致"] C --> D["4. 不一致则设置\nCUDA_DEVICE_ORDER=PCI_BUS_ID"] D --> E["5. 重新验证,确认一致"] E --> F["6. 加入设备名称验证逻辑\n防止日后配置变更导致静默错误"]

用于验证的完整脚本:

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


十、总结

mindmap root((GPU编号错乱-核心要点)) 根本原因 PyTorch默认FASTEST_FIRST 按计算能力排序cuda编号 nvidia_smi按PCI_Bus_ID排序 使用不同底层API路径 触发条件 混用不同型号GPU 同型号不会触发 迁移服务器易出现 问题表现 任务跑错GPU 性能严重下降 显存占用对不上 修复方法 CUDA_DEVICE_ORDER=PCI_BUS_ID import前生效 export保证子进程继承 启动脚本或代码或bashrc 预防措施 启动脚本首行设置 GPU名称校验 部署前诊断映射 避免依赖编号 框架差异 PyTorch默认FASTEST_FIRST TensorFlow和JAX默认PCI_BUS_ID

一行命令,避免数小时排查:

bash 复制代码
export CUDA_DEVICE_ORDER=PCI_BUS_ID

三条核心原则:

  1. 永远不要假设 cuda:N 等于 nvidia-smiGPU N,混合 GPU 环境下两者很可能不一致。
  2. 在任何多 GPU 项目的启动脚本中,第一行就写 export CUDA_DEVICE_ORDER=PCI_BUS_ID,养成习惯。
  3. 部署前务必用 torch.cuda.get_device_name() 验证设备映射,不能只看 nvidia-smi
相关推荐
吴佳浩1 小时前
GPU 编号进阶:CUDA\_VISIBLE\_DEVICES、多进程与容器化陷阱
人工智能·pytorch·python
小饕2 小时前
苏格拉底式提问对抗315 AI投毒:实操指南
网络·人工智能
卧蚕土豆2 小时前
【有啥问啥】OpenClaw 安装与使用教程
人工智能·深度学习
GoCodingInMyWay2 小时前
开源好物 26/03
人工智能·开源
AI科技星2 小时前
全尺度角速度统一:基于 v ≡ c 的纯推导与验证
c语言·开发语言·人工智能·opencv·算法·机器学习·数据挖掘
zhangfeng11332 小时前
Windows 的 Git Bash 中使用 md5sum 命令非常简单 md5做文件完整性检测 WinRAR 可以计算文件的 MD5 值
人工智能·windows·git·bash
monsion2 小时前
OpenCode 学习指南
人工智能·vscode·架构
藦卡机器人2 小时前
中国工业机器人发展现状
大数据·人工智能·机器人
破阵子443282 小时前
小米AI新模型全面解析:从MiMo-V2系列到使用指南
人工智能