如果你在多卡服务器上跑深度学习任务多半迟早会撞上一个诡异现象nvidia-smi 里清清楚楚显示 GPU 0 是 A800GPU 1 是 4090可代码里torch.device(cuda:0)加载完模型显存却涨在另一张卡上。更烦的是有时你按照 nvidia-smi 的编号在脚本里写CUDA_VISIBLE_DEVICES2结果程序跑起来用的却是物理 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(ftorch 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-gpuindex,name,pci.bus_id,uuid --formatcsv输出大概是这样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_ORDERPCI_BUS_ID最简单、最省事的修复方法是把 CUDA 的枚举顺序改成和nvidia-smi一致的 PCI 顺序。在运行训练脚本之前执行export CUDA_DEVICE_ORDERPCI_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_ORDERPCI_BUS_ID export CUDA_VISIBLE_DEVICES2,3 python train.py然后 PyTorch 里cuda:0对应物理第 2 张cuda:1对应物理第 3 张。torch.cuda.device_count()返回 2进程内不存在编号 2 或 3因为可见设备被重新编号了。这看起来很简单但很多人只设置了CUDA_VISIBLE_DEVICES2,3忘了设置CUDA_DEVICE_ORDERPCI_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(fGPU mapping mismatch: expected {expected}, got {actual_physical_ids})注意这个函数不能放在 import torch 之后才设置环境变量所以如果你要用它做兜底就要保证环境变量实际上已经由外部脚本设置好这里的setdefault只是防御手段。更可靠的做法是在所有 import 之前就手动设置而不是依赖这个函数。4. 容器和集群场景里的编号错乱案例4.1 Docker 容器内看到的编号为什么和宿主机不一样Docker 容器里的 GPU 设备隔离机制经常让人误会。当你用--gpus all启动容器时容器内默认能看到宿主机上所有 GPU编号通常和宿主机一致但仍可能被 CUDA 枚举顺序影响。而当你用--gpus device2,3或者设置NVIDIA_VISIBLE_DEVICES2,3启动容器宿主机上的物理 GPU 2 和 3 会被映射到容器内的设备 0 和 1。容器内执行nvidia-smi看到的就只有这两张卡索引从 0 开始和宿主机上的全局编号完全不同。这种“编号变化”其实不是错误而是隔离的预期结果。问题出在很多人拿着宿主机上的编号去容器内调试。比如你在容器内看到nvidia-smi只有 GPU 0 和 GPU 1想指定容器内的 GPU 1结果发现你的 PyTorch 程序里cuda:1又可能因为默认枚举顺序而对应另一张卡。在容器里所有上层环境变量都被继承下来或者被容器 runtime 重写所以更需要显式设置CUDA_DEVICE_ORDERPCI_BUS_ID来保证容器内 CUDA 枚举顺序和容器内nvidia-smi一致。我的建议是凡是进容器第一时间执行env | grep -E CUDA|NVIDIA看看 runtime 帮你设了什么变量。搞清楚CUDA_VISIBLE_DEVICES和NVIDIA_VISIBLE_DEVICES的实际值再开始排查。4.2 多容器并发使用指定物理卡的最佳实践在多容器共享 GPU 的服务器上最常见的场景是每个容器固定分一张卡。为了避免容器之间抢卡启动命令要同时控制两层顺序。推荐在启动脚本里这样写export CUDA_DEVICE_ORDERPCI_BUS_ID export NVIDIA_VISIBLE_DEVICES2 export CUDA_VISIBLE_DEVICES0 docker run ... \ -e CUDA_DEVICE_ORDERPCI_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_DEVICES0就不会出现越界。之所以容器内还要再设置一次CUDA_DEVICE_ORDERPCI_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_GPUS2,3等环境变量但不会自动设置CUDA_DEVICE_ORDER。如果你在多卡并行任务里用torchrun或DistributedDataParallel每个进程分配的LOCAL_RANK和CUDA_VISIBLE_DEVICES里的顺序并不总是对应。比如分配了物理卡 3 和 1CUDA_VISIBLE_DEVICES可能写的是1,3这样进程的 local rank 0 对应物理 1local 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(fnvml index {i}: {name}, uuid{uuid}, pci_bus_id{pci_info.busId}) pynvml.nvmlShutdown()注意这里的i是 NVML 枚举的物理设备索引和nvidia-smi的 index 对应。如果环境里启用了 MIGnvmlDeviceGetCount返回的可能不包含 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(ftorch:{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_ORDERPCI_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(fGPU {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_ORDERPCI_BUS_ID设置了CUDA_VISIBLE_DEVICES2但跑的不是物理 GPU 2CUDA_VISIBLE_DEVICES的索引受CUDA_DEVICE_ORDER影响两个变量同时设置先export CUDA_DEVICE_ORDERPCI_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_ORDERPCI_BUS_ID。这是成本最低、收益最直接的规则能避免绝大多数编号错乱问题。第二训练代码里永远不要直接写死cuda:2这种全局编号。用os.environ[CUDA_VISIBLE_DEVICES]来限定卡然后在程序里统一使用 0 开始的相对编号。第三养成分层验证的习惯先看nvidia-smi -L的物理设备再看 PyTorch 读取到的 PCI bus id最后看 NVML 的 UUID。三层信息对齐了再开始跑长任务。我在实际项目里就是靠这个流程把多卡机器上各种“灵异现象”都归到了环境变量或枚举顺序上。下次你再遇到 PyTorch 编号和 nvidia-smi 对不上不用急着怀疑硬件先跑一遍 2.1 节的打印代码再对照本节的速查表大概率十分钟内就能找到症结。
阅读完成 · 觉得有帮助?