☰
PyTorch中CUDA设备编号与nvidia-smi不一致的排查与修复指南
2026/10/16 20:17:54 网站建设 项目流程

如果你在多卡服务器上跑深度学习任务,多半迟早会撞上一个诡异现象:nvidia-smi 里清清楚楚显示 GPU 0 是 A800,GPU 1 是 4090,可代码里torch.device('cuda:0')加载完模型,显存却涨在另一张卡上。更烦的是,有时你按照 nvidia-smi 的编号在脚本里写CUDA_VISIBLE_DEVICES=2,结果程序跑起来用的却是物理 0 号卡。这种 PyTorch CUDA 编号与 nvidia-smi 不一致的问题,通常不是硬件故障,而是 CUDA 枚举顺序和环境变量的锅。这篇文章把我踩过的那一堆坑整理成指南,从根因、定位到容器场景下的处理,最后给出一套可复用的物理设备识别方案。

1. 问题现象与根因分析

1.1 一个让我排查了两天的“灵异事件”

先说个真实案例。某实验室有一台四卡机器,三张老一点的卡加一张新卡,nvidia-smi显示的顺序是物理插槽从近到远排列的,0 到 3 分别是老卡、新卡、老卡、老卡。我想把主实验跑在新卡上,于是代码里写了torch.device('cuda:1'),结果启动一看,显存占用最高的却是物理 0 号老卡。当时第一反应是驱动坏了,重装驱动、清 CUDA 缓存、甚至把 PyTorch 版本都换了一遍,问题依旧。最后发现,代码里读到的 "cuda:1" 根本不是nvidia-smi里的 GPU 1,而是 CUDA 按“计算能力”重新排过序之后的第二个位置。也就是说,索引从这个程序一开始的解读方式就错了,后面所有排查都建立在错误前提下,自然怎么折腾都不对。

这种问题最坑的地方在于:程序不会报错。CUDA 会认为一切正常,该分配设备就分配设备,该跑就跑。只有当你在日志里加了一行设备名和显存打印,才发现真实设备和你设想的不一样。如果做的是多卡并行训练,还会出现某张卡被反复占用、另一张卡吃不满的情况,性能曲线异常还很难定位。

1.2 CUDA 可见设备的枚举顺序规则

要搞明白为什么编号会错乱,必须先知道 CUDA 在给设备编号时用的什么规则。CUDA 运行时有一个环境变量叫CUDA_DEVICE_ORDER,它决定了可见设备的默认枚举顺序,取值有两个:FASTEST_FIRST和PCI_BUS_ID。

默认情况下,CUDA 用的是FASTEST_FIRST,也就是按照设备的计算能力从高到低排列,计算能力相同的再按 PCI 总线 ID 排序。这样做的好处是,如果你不关心具体用哪张卡,只想要“最好”的那张,CUDA 自动帮你选了。但对于多卡混插、有特定物理卡需求的训练任务,这个默认规则就会带来灾难性的误解。比如一台机器上有一张新卡和三张老卡,新卡的计算能力最高,CUDA 就会把它编为 0 号设备,哪怕它在nvidia-smi里的物理编号是 2。你心里想用的是“物理 2 号卡”,代码里写cuda:0,结果确实也是同一张卡,一切正常;但如果你习惯照着nvidia-smi的编号来写cuda:2,实际用的就是另一张老卡了。

当把CUDA_DEVICE_ORDER设为PCI_BUS_ID后,CUDA 会按照 PCI 总线号的顺序给设备编号,这个顺序通常和nvidia-smi保持一致。这里的“通常”不是说绝对,因为驱动版本、主板插槽顺序甚至 BIOS 设置都可能影响 PCI 枚举,但至少绝大多数服务器上,设置之后 PyTorch 的编号就和nvidia-smi的编号一一对应了。

1.3 为什么 nvidia-smi 与 PyTorch 会出现错位

nvidia-smi是 NVIDIA 驱动自带的工具,它的设备列表顺序直接来自驱动枚举,基本就是物理 PCI 拓扑顺序,从 0 开始排。而 PyTorch 在调用 CUDA Runtime API 时,设备编号默认按FASTEST_FIRST排列。一个按物理顺序,一个按计算能力顺序,只要机器上有不同型号的显卡,两者几乎必然错位。

即使所有显卡型号相同,也有可能出现错位。因为FASTEST_FIRST在计算能力相同的情况下会退回 PCI 总线 ID 排序,而如果主板上有 PCIE 插槽直连 CPU 和通过 PCIE Switch 扩展的区别,驱动枚举顺序和 CUDA 枚举顺序也可能不完全同步。所以“全是同款卡就不会错”这个想法是靠不住的。

除了枚举顺序,还有一层干扰来自环境变量CUDA_VISIBLE_DEVICES。它可以把部分设备“屏蔽”给某个进程。一旦你设置了它,PyTorch 里看到的设备编号就会在可见设备范围内重新从 0 开始,这时的 0 就不一定是全局物理设备 0。很多人把这两层变量混在一起,导致排查更加困难。建议先理解这样一条线:物理设备由驱动号唯一标识,PyTorch 里能看到的设备是 CUDA 枚举后的结果,CUDA_VISIBLE_DEVICES是在 CUDA 枚举结果之上再做一次过滤。先搞清楚每一层发生了什么,再谈定位和修复。

2. 快速定位:让 PyTorch 开口告诉你它看到了什么

2.1 用三行代码打印设备映射

遇到编号疑似错乱时,第一步不是改配置,而是让 PyTorch 自己报一遍它看到的设备。在 Python 里跑下面这段代码,可以看到每个 CUDA 设备在当前进程中的编号、名称和 PCI 总线 ID。

import torch if torch.cuda.is_available(): print("device_count:", torch.cuda.device_count()) for i in range(torch.cuda.device_count()): props = torch.cuda.get_device_properties(i) print(f"torch index {i}: {props.name}, pci_bus_id={props.pci_bus_id}") else: print("CUDA is not available")

注意这段代码必须在没有手动设置CUDA_VISIBLE_DEVICES的可交互环境里跑,否则打印出来的就是从过滤后的设备列表中枚举的结果。不过即使被过滤过,这段代码仍然能告诉你“当前进程内”的真实设备信息。如果你只想复现问题,就保持环境变量原样运行,然后和宿主机的nvidia-smi输出对比。

torch.cuda.get_device_properties(i)返回的对象里有几个关键字段:name是设备型号,total_memory是总显存大小,pci_bus_id是类似00000000:3B:00.0的 PCI 地址。在多卡机器上,pci_bus_id是最可靠的关联线索。

2.2 与 nvidia-smi 的 PCI 地址对应

拿到 PyTorch 的 PCI 总线 ID 后,再用nvidia-smi查一下每张物理卡的对应关系。命令行一条搞定:

nvidia-smi --query-gpu=index,name,pci.bus_id,uuid --format=csv

输出大概是这样:

index, name, pci.bus_id, uuid 0, NVIDIA A800, 00000000:3B:00.0, GPU-xxxxxxxx 1, NVIDIA RTX 4090, 00000000:5D:00.0, GPU-yyyyyyyy 2, NVIDIA A800, 00000000:AF:00.0, GPU-zzzzzzzz

把nvidia-smi输出的pci.bus_id和 PyTorch 打印的props.pci_bus_id对照一下,就能画出“PyTorch 逻辑索引 → 物理设备”的映射表。比如 PyTorch 的 0 号设备pci_bus_id是00000000:5D:00.0,而nvidia-smi里对应行是 index 1 的 RTX 4090,那就说明 PyTorch 的cuda:0其实是物理 GPU 1。

这个对照过程最好写进自动化脚本里,不要每次靠肉眼比对。因为人眼在五六张卡以上的服务器上很容易看岔。脚本里只需要把两边的 (index, pci_bus_id) 都建出来,用 bus id 做 key 合并,就能自动生成映射表。

2.3 如果 pci_bus_id 为空或不存在怎么办

在某些旧版 PyTorch 里,torch.cuda.get_device_properties(i)返回的对象没有pci_bus_id字段。遇到这种情况别慌,有两条路可以走。

第一条路是升级 PyTorch。新版 PyTorch 在 CUDA 设备属性里已经包含 PCI 信息,这属于基础功能升级,不用为了一个字段去折腾新的 CUDA 版本。如果项目里用的老代码不宜轻易升级,可以专门开一个虚拟环境装新版 PyTorch 来做一次设备映射诊断,不需要把整个训练环境换掉。

第二条路是直接用设备型号和显存大小做粗粒度匹配。比如打印torch.cuda.get_device_name(i)和torch.cuda.get_device_memory_info(i)? 不过要注意,torch.cuda.memory_allocated是当前进程的显存占用,不是设备总显存。获取总显存仍然要用get_device_properties(i).total_memory。如果机器上有同型号多张卡,这种方法无法区分具体是哪一张,但这已经能帮你发现明显错位。更精确的方案需要用到后面会说的 NVML 工具,通过 NVML 读取每个物理设备的唯一 UUID,再做交叉验证。

3. 最直接的解决方案:统一排序规则

3.1 设置 CUDA_DEVICE_ORDER=PCI_BUS_ID

最简单、最省事的修复方法是把 CUDA 的枚举顺序改成和nvidia-smi一致的 PCI 顺序。在运行训练脚本之前,执行:

export CUDA_DEVICE_ORDER=PCI_BUS_ID

也可以在 Python 脚本的最顶部,注意是“最顶部”,比如放在任何 import 之前设置:

import os os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID'

为什么要强调“最顶部”?因为 CUDA 运行时会读取这个环境变量并缓存枚举顺序。如果已经import torch甚至已经执行过任何 CUDA 初始化,再改os.environ就不会生效了。更隐蔽的是,有些库会在 import 时隐式引入 torch,比如某些数据加载库、可视化库,所以最稳妥的做法是把它放在脚本的第一行,前面连注释都不要放,确保它是在任何其他模块加载之前执行的。

设置之后,PyTorch 里的设备编号基本就和nvidia-smi一致了。你可以再用 2.1 节的打印代码验证一遍。如果还是不一致,说明驱动或者主板层面的 PCI 枚举顺序和nvidia-smi展示顺序不一样,这种极端情况需要用 NVML 的 UUID 做最终裁决。

3.2 其实你只需要 CUDA_VISIBLE_DEVICES 就够了

很多人被编号错乱困扰,其实是因为太依赖 “全局编号” 了。实际上,哪怕你完全不关心 CUDA 枚举顺序,只要明确告诉进程“你只能用哪几张卡”,并在代码里始终使用过滤后的相对编号,就不会踩坑。

假设物理机上有 8 张卡,你希望用物理 PCI 顺序下的第 2 和第 3 张卡。正确的启动方式是:

export CUDA_DEVICE_ORDER=PCI_BUS_ID export CUDA_VISIBLE_DEVICES=2,3 python train.py

然后 PyTorch 里cuda:0对应物理第 2 张,cuda:1对应物理第 3 张。torch.cuda.device_count()返回 2,进程内不存在编号 2 或 3,因为可见设备被重新编号了。

这看起来很简单,但很多人只设置了CUDA_VISIBLE_DEVICES=2,3,忘了设置CUDA_DEVICE_ORDER=PCI_BUS_ID。在这种情况下,CUDA_VISIBLE_DEVICES里的 “2” 和 “3” 并不是物理 PCI 顺序下的 2 和 3,而是默认按计算能力排序后的第 2 和第 3 个设备。这就是为什么有些人明明写对了物理卡号,实际还是跑错卡的原因。所以这两个变量要一起用。

3.3 在 PyTorch 代码中主动做一次“映射校准”

环境变量虽然好使,但团队协作时不一定每个人都记得 export。为了让问题在第一时间暴露,我建议在训练入口加一段“映射校准”逻辑,启动时自动打印当前的物理设备对应关系,如果和预期不符就抛出异常或至少打个醒目的警告。

下面是一个可以直接放在项目里的工具函数:

import os import torch def verify_gpu_mapping(expected_physical_ids): os.environ.setdefault('CUDA_DEVICE_ORDER', 'PCI_BUS_ID') # setdefault 不能覆盖已经设置的值,主要起兜底作用 torch.cuda.init() actual_physical_ids = [] for i in range(torch.cuda.device_count()): props = torch.cuda.get_device_properties(i) print(f"[Mapping] torch index {i} -> pci_bus_id {props.pci_bus_id} ({props.name})") # pci_bus_id 形如 00000000:3B:00.0,可以提取坐标段 bus_id = props.pci_bus_id.split(':')[1] if ':' in props.pci_bus_id else props.pci_bus_id actual_physical_ids.append(bus_id.lower()) if expected_physical_ids: expected = [x.lower() for x in expected_physical_ids] if actual_physical_ids != expected: raise RuntimeError(f"GPU mapping mismatch: expected {expected}, got {actual_physical_ids}")

注意这个函数不能放在 import torch 之后才设置环境变量,所以如果你要用它做兜底,就要保证环境变量实际上已经由外部脚本设置好,这里的setdefault只是防御手段。更可靠的做法是在所有 import 之前就手动设置,而不是依赖这个函数。

4. 容器和集群场景里的编号错乱案例

4.1 Docker 容器内看到的编号为什么和宿主机不一样

Docker 容器里的 GPU 设备隔离机制经常让人误会。当你用--gpus all启动容器时,容器内默认能看到宿主机上所有 GPU,编号通常和宿主机一致,但仍可能被 CUDA 枚举顺序影响。而当你用--gpus '"device=2,3"'或者设置NVIDIA_VISIBLE_DEVICES=2,3启动容器,宿主机上的物理 GPU 2 和 3 会被映射到容器内的设备 0 和 1。容器内执行nvidia-smi看到的就只有这两张卡,索引从 0 开始,和宿主机上的全局编号完全不同。

这种“编号变化”其实不是错误,而是隔离的预期结果。问题出在很多人拿着宿主机上的编号去容器内调试。比如你在容器内看到nvidia-smi只有 GPU 0 和 GPU 1,想指定容器内的 GPU 1,结果发现你的 PyTorch 程序里cuda:1又可能因为默认枚举顺序而对应另一张卡。在容器里,所有上层环境变量都被继承下来,或者被容器 runtime 重写,所以更需要显式设置CUDA_DEVICE_ORDER=PCI_BUS_ID来保证容器内 CUDA 枚举顺序和容器内nvidia-smi一致。

我的建议是:凡是进容器,第一时间执行env | grep -E "CUDA|NVIDIA",看看 runtime 帮你设了什么变量。搞清楚CUDA_VISIBLE_DEVICES和NVIDIA_VISIBLE_DEVICES的实际值,再开始排查。

4.2 多容器并发使用指定物理卡的最佳实践

在多容器共享 GPU 的服务器上,最常见的场景是每个容器固定分一张卡。为了避免容器之间抢卡,启动命令要同时控制两层顺序。

推荐在启动脚本里这样写:

export CUDA_DEVICE_ORDER=PCI_BUS_ID export NVIDIA_VISIBLE_DEVICES=2 export CUDA_VISIBLE_DEVICES=0 docker run ... \ -e CUDA_DEVICE_ORDER=PCI_BUS_ID \ -e NVIDIA_VISIBLE_DEVICES=$NVIDIA_VISIBLE_DEVICES \ -e CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES \ my_training_image

这里有两个变量值得注意。NVIDIA_VISIBLE_DEVICES是 NVIDIA Container Runtime 用的,决定哪些物理设备可以被容器访问,值对应宿主机上的物理设备索引,通常按宿主机nvidia-smi的编号来。CUDA_VISIBLE_DEVICES是 CUDA 层用的,决定容器内进程实际可见的设备列表。如果容器 runtime 已经把物理 2 映射成容器内设备 0,那你只需要在容器内让 CUDA_VISIBLE_DEVICES=0,就不会出现越界。之所以容器内还要再设置一次CUDA_DEVICE_ORDER=PCI_BUS_ID,是为了保证 CUDA 枚举顺序不因默认规则而打乱容器内设备顺序。

这套组合拳打完之后,容器内nvidia-smi、PyTorch 的cuda:0、以及宿主机物理 GPU 2,三者就完全对齐了。排查容器问题的时候,不要只看当前容器内的输出,要把宿主机nvidia-smi的全局编号也打出来,两边一对比立刻就能看清。

4.3 集群调度场景中的隐藏陷阱

使用工作负载管理器(比如常见的 Slurm、SGE 等)时,调度器往往会在作业脚本里自动设置CUDA_VISIBLE_DEVICES,让你只能用被分配的那几张卡。此时作业进程里nvidia-smi通常只会显示被分配的 GPU,且索引从 0 开始。这样做本身没问题,坑的是有些同学之前在单机上习惯用全局编号写代码,到了集群里仍然写cuda:1、cuda:2,结果直接在越界处报错。

更隐蔽的是,有些调度器会设置SLURM_STEP_GPUS=2,3等环境变量,但不会自动设置CUDA_DEVICE_ORDER。如果你在多卡并行任务里用torchrun或DistributedDataParallel,每个进程分配的LOCAL_RANK和CUDA_VISIBLE_DEVICES里的顺序并不总是对应。比如分配了物理卡 3 和 1,CUDA_VISIBLE_DEVICES可能写的是1,3,这样进程的 local rank 0 对应物理 1,local rank 1 对应物理 3。千万不要想当然地认为 local rank 和物理卡号顺序一致。

建议在分布式训练脚本里增加一条调试环境变量的输出:

echo "CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES" echo "SLURM_LOCALID=$SLURM_LOCALID" nvidia-smi -L

用CUDA_VISIBLE_DEVICES的值去反推物理卡顺序,再结合 NVML UUID 确认,就能避免很多分布式环境里的“卡没用好”问题。

5. 进阶:用 NVML 做精确的物理设备识别

5.1 引入 pynvml 获取 UUID 和总线信息

环境变量可以让绝大多数设备编号对齐,但有一类场景必须借助更底层的工具:当机器上有多张同型号显卡,或者启用了 MIG、虚拟化,又或者驱动版本和 PyTorch 版本互相不配合时,靠名称和 PCI 地址都能判断,但最可靠的唯一标识是 GPU 的 UUID。

NVML 是 NVIDIA 官方提供的管理库,Python 里可以通过nvidia-ml-py包使用。安装命令:

pip install nvidia-ml-py

安装后导入pynvml。写一段小代码,把所有物理 GPU 的信息列出来:

import pynvml pynvml.nvmlInit() count = pynvml.nvmlDeviceGetCount() print("NVML device count:", count) for i in range(count): handle = pynvml.nvmlDeviceGetHandleByIndex(i) name = pynvml.nvmlDeviceGetName(handle) uuid = pynvml.nvmlDeviceGetUUID(handle) pci_info = pynvml.nvmlDeviceGetPciInfo(handle) print(f"nvml index {i}: {name}, uuid={uuid}, pci_bus_id={pci_info.busId}") pynvml.nvmlShutdown()

注意这里的i是 NVML 枚举的物理设备索引,和nvidia-smi的 index 对应。如果环境里启用了 MIG,nvmlDeviceGetCount返回的可能不包含 MIG 实例,需要换用 MIG 相关 API。对于绝大多数普通 GPU 服务器,上面这个代码已经够用。

5.2 实现 PyTorch 索引到物理 UUID 的映射工具

有了 NVML 提供的物理设备 UUID 和 PCI 总线 ID,就可以把 PyTorch 的逻辑索引映射到物理设备。核心思路是:循环 PyTorch 可见设备,取每个设备的pci_bus_id,再到 NVML 设备列表里找相同 PCI 地址的设备,然后记录它的物理索引和 UUID。

下面是一段可以直接用的工具代码:

import torch import pynvml def build_torch_to_physical_map(): if not torch.cuda.is_available(): return {} torch.cuda.init() pynvml.nvmlInit() nvml_list = [] for i in range(pynvml.nvmlDeviceGetCount()): handle = pynvml.nvmlDeviceGetHandleByIndex(i) pci_info = pynvml.nvmlDeviceGetPciInfo(handle) nvml_list.append({ 'index': i, 'handle': handle, 'name': pynvml.nvmlDeviceGetName(handle), 'uuid': pynvml.nvmlDeviceGetUUID(handle), 'pci_bus_id': pci_info.busId.lower(), }) mapping = {} for torch_idx in range(torch.cuda.device_count()): props = torch.cuda.get_device_properties(torch_idx) torch_pci = props.pci_bus_id.lower() matched = None for item in nvml_list: if item['pci_bus_id'] == torch_pci: matched = item break if matched is not None: mapping[torch_idx] = { 'physical_index': matched['index'], 'name': matched['name'], 'uuid': matched['uuid'], 'pci_bus_id': matched['pci_bus_id'], } else: mapping[torch_idx] = { 'physical_index': None, 'name': torch.cuda.get_device_name(torch_idx), 'uuid': None, 'pci_bus_id': torch_pci, } pynvml.nvmlShutdown() return mapping if __name__ == '__main__': for k, v in build_torch_to_physical_map().items(): print(f"torch:{k} -> physical:{v['physical_index']} uuid:{v['uuid']} name:{v['name']}")

这段代码有几个细节值得解释。第一,比较 PCI 地址前统一转成小写,避免大小写不一致导致匹配失败。第二,如果 PyTorch 版本很老拿不到pci_bus_id,这个工具会走到else分支,返回 None,相当于降级到仅显示名称。第三,在调用torch.cuda.init()之后再启动 NVML,可以保证 PyTorch 和 NVML 的上下文一致,减少误匹配。

有了这个映射函数,你在训练脚本里就能直接打日志:每个进程用的 PyTorch 编号对应的物理 UUID 是什么。下次哪张卡有问题,拿着 UUID 去nvidia-smi一搜,跑不了。

5.3 案例:训练时自动选择最空闲的物理卡

这个工具最实用的场景之一是写一个“空闲卡选择器”。多人共用一台服务器时,经常需要找一张显存占用最低的卡跑新任务。如果你只靠nvidia-smi肉眼挑卡,再手动设CUDA_VISIBLE_DEVICES,很容易因为编号错乱选错卡。写个自动选择的小脚本就稳很多。

思路是:在导入 torch 之前,用 NVML 扫描所有物理 GPU 的显存占用,选出可用显存最大的物理卡索引,然后设置CUDA_DEVICE_ORDER=PCI_BUS_ID和CUDA_VISIBLE_DEVICES=该物理卡索引,再导入 torch,开始训练。

示例代码:

import os import pynvml def pick_freest_gpu(): pynvml.nvmlInit() best_free = -1 best_idx = -1 count = pynvml.nvmlDeviceGetCount() for i in range(count): handle = pynvml.nvmlDeviceGetHandleByIndex(i) mem = pynvml.nvmlDeviceGetMemoryInfo(handle) print(f"GPU {i}, free memory: {mem.free / 1024**3:.1f} GiB") if mem.free > best_free: best_free = mem.free best_idx = i pynvml.nvmlShutdown() return best_idx if __name__ == '__main__': os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID' gpu_idx = pick_freest_gpu() print("Picked physical GPU", gpu_idx) os.environ['CUDA_VISIBLE_DEVICES'] = str(gpu_idx) import torch print("torch device_count:", torch.cuda.device_count()) print("device name:", torch.cuda.get_device_name(0))

这段代码看起来简单,但有一个关键点:所有环境变量必须在import torch之前设置。因为import torch之后 CUDA Runtime 可能已经初始化,之后再设置CUDA_VISIBLE_DEVICES是无效的。所以这个文件里pick_freest_gpu()被放在import torch之前,利用 NVML 做选择,不依赖 PyTorch。

实际使用时,可以把这段逻辑放到训练脚本的最前面,也可以在 shell 里先运行一个小程序输出选择的卡号,再构建训练命令。我更推荐后者,因为这样能避免在一个进程里既做设备选择又启动分布式训练带来的复杂度。

6. 常见问题速查表与避坑心得

6.1 问题速查表(对照)

我把实践中遇到的问题整理成一张表,方便你遇到相似现象时直接对照:

现象可能原因处理方法
PyTorchcuda:0与 nvidia-smi GPU 0 不是同一张卡CUDA 默认按计算能力排序,而 nvidia-smi 按 PCI 物理顺序设置CUDA_DEVICE_ORDER=PCI_BUS_ID
设置了CUDA_VISIBLE_DEVICES=2,但跑的不是物理 GPU 2CUDA_VISIBLE_DEVICES的索引受CUDA_DEVICE_ORDER影响两个变量同时设置,先export CUDA_DEVICE_ORDER=PCI_BUS_ID
容器内nvidia-smi只有 0、1,但宿主机有 4 张卡容器 runtime 通过NVIDIA_VISIBLE_DEVICES做了设备隔离分清宿主机物理号和容器内相对号,用 UUID 确认映射
训练日志里设备名和 nvidia-smi 不一致机器启用了 MIG,或者设备被虚拟化使用 NVML 的 UUID 识别,不要只看设备名
代码里get_device_properties没有pci_bus_idPyTorch 版本太旧升级 PyTorch,或用 pynvml 读取物理 PCI 信息
换了新驱动之后设备顺序变了驱动枚举顺序随版本可能变化不要只依赖环境变量,训练前打印 PCI bus id 映射

这张表不是万能药,但覆盖了我遇到过的 90% 场景。核心思想始终是:不要凭“感觉”指定设备,而要让程序把映射打出来给你看。

6.2 几个我踩过的坑

第一个坑:环境变量写在.bashrc里,但用nohup启动任务时没有重新加载。.bashrc只在交互式登录时生效,很多后台任务启动脚本是干净环境,里面的CUDA_DEVICE_ORDER根本不存在。我后来改成在每次训练启动命令前显式写export,并且加了条env | grep CUDA的日志,确保环境确实带着配置。

第二个坑:在 Jupyter Notebook 里调试多卡问题时,直接在同一个 kernel 里设置os.environ['CUDA_DEVICE_ORDER'],但之前某个 cell 早就执行过import torch了。环境变量没有生效,我还以为是变量名拼错了。后来养成的习惯是:涉及 CUDA 顺序的调试,要么重启 kernel 并在最顶部的 cell 设置,要么干脆退出 Python 重新用脚本执行。

第三个坑:调 NVML 时不分系统和虚拟环境,直接pip install pynvml,结果装到了一个已经废弃的第三方包上,API 全对不上。正确做法是安装nvidia-ml-py,导入名仍然是pynvml。如果环境里已经有冲突,先pip uninstall pynvml nvidia-ml-py再重新装。

第四个坑:在CUDA_VISIBLE_DEVICES已经生效的情况下,用 NVML 的nvmlDeviceGetHandleByIndex(i)去匹配 PyTorch 的索引。这会得到完全错误的映射,因为 NVML 的i是物理全量索引,而 PyTorch 的i是可见设备列表中过滤后的索引。必须通过 PCI 地址或 UUID 做关联,不能直接按数字位置对应。

6.3 给新手的最终建议

如果你刚接触多卡训练,我建议把下面三条写进项目规范里。

第一,所有涉及 GPU 的任务,入口脚本第一件事就是export CUDA_DEVICE_ORDER=PCI_BUS_ID。这是成本最低、收益最直接的规则,能避免绝大多数编号错乱问题。第二,训练代码里永远不要直接写死cuda:2这种全局编号。用os.environ['CUDA_VISIBLE_DEVICES']来限定卡,然后在程序里统一使用 0 开始的相对编号。第三,养成分层验证的习惯:先看nvidia-smi -L的物理设备,再看 PyTorch 读取到的 PCI bus id,最后看 NVML 的 UUID。三层信息对齐了,再开始跑长任务。

我在实际项目里就是靠这个流程,把多卡机器上各种“灵异现象”都归到了环境变量或枚举顺序上。下次你再遇到 PyTorch 编号和 nvidia-smi 对不上,不用急着怀疑硬件,先跑一遍 2.1 节的打印代码,再对照本节的速查表,大概率十分钟内就能找到症结。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询