资讯动态

PyTorch与nvidia-smi GPU编号不一致?三层映射排查与避坑指南

发布时间:2026/10/16 11:20:26 来源:尧图企业网站定制
第一次遇到这个问题时我差点以为是显卡烧了。深夜跑一个分布式训练任务启动脚本一切正常日志明确打出“使用 GPU 0、1、2、3 共四张卡”模型也正常开始迭代。但我习惯性地看了一眼 nvidia-smi结果直接愣住显示的是 GPU 2、3、4、5 在疯狂跑满显存而 0 号和 1 号卡干干净净空在那里。我脑子里瞬间闪过一堆念头日志里的 GPU 0 到底是物理上的哪张卡分布式组里每个进程看到的编号为什么不一致按物理编号挂载的任务到底跑到了哪里那天晚上我花了大半夜把 PyTorch cuda 编号与 nvidia-smi 不一致这个问题从现象到原理全部拆了个遍最后发现原因并不玄学核心在于“物理编号、驱动枚举、运行时可见编号”三层映射被打乱了。这个坑在多卡机器、容器环境、远程服务集群里极其常见尤其是当 CUDA_VISIBLE_DEVICES 被反复设置、容器内部和宿主机交替使用、或者换卡之后没有清理环境变量的时候十次里有七八次都会踩中。这篇文章就是要给你一条完整的排查链路从理解 nvidia-smi 和 CUDA 各自编号的底层逻辑到用一段脚本一分钟定位错位根因再到几个高频场景的避坑方案。适合刚配好多卡环境的新手也适合被 CUDA_VISIBLE_DEVICES 折磨过的资深调参工程师。不涉及特别深的 CUDA 底层只要你用过 nvidia-smi、会跑 Python就能跟着一步步查清问题。1. 现象与根因为什么两张“编号表”会对不上1.1 一次真实踩坑现场还原先还原一下当时的具体场景。机器上一共八张卡物理位置从 0 到 7。我用的训练脚本在入口处写了这样一段逻辑# 启动脚本片段 os.environ[CUDA_VISIBLE_DEVICES] 2,3,4,5,6,7然后通过分布式启动器拉起 6 个进程。日志里每个进程输出的 device 编号分别是 0、1、2、3、4、5看起来完全正确模型也确实跑起来了。但此时 nvidia-smi 里看到的是另一幅画面GPU 2 到 7 这六张卡被六个进程各占用一张显存使用率在 80% 以上。看起来好像对得上因为我把“2,3,4,5,6,7”传给 CUDA_VISIBLE_DEVICES物理上确实就是 2 到 7 号卡在工作。真正的混乱出现在另一种情况脚本里只写了0,1,2,3但 nvidia-smi 显示的是 GPU 1、2、5、6 在跑。为什么会这样因为服务器上的另一个服务在启动时提前设置了CUDA_VISIBLE_DEVICES1,2,5,6并且这个环境变量通过某种方式被继承到了我的进程里。我的脚本又覆盖了一层最终进程实际看到的环境变量是1,2,5,6于是日志里的 CUDA 编号 0 对应物理卡 1CUDA 编号 1 对应物理卡 2依次类推。表面上看代码没有报错编号也连续但作业实际跑在了完全不同的物理卡上。这种错位最迷惑的地方在于它不会报任何错误。只有在显存占满导致 OOM、或者你拿 nvidia-smi 去看进程归属时才发现日志里说的 GPU 和物理 GPU 完全是两回事。1.2 编号不一致的根源三层映射在作怪要理解为什么两边会对不上先得搞清楚 GPU 编号到底有几套“身份”。第一套是物理拓扑编号也就是显卡插在 PCIe 插槽上的顺序。nvidia-smi 在没有其他配置干扰时默认按驱动枚举顺序显示编号通常和物理位置基本一致但不保证永远一致比如换卡、驱动升级、BIOS 设置变更后枚举顺序就可能变化。第二套是驱动枚举编号也就是 nvidia-smi 右下角那串 ID或者nvidia-smi -L打出来的 GPU 编号。驱动在初始化时为每张卡分配一个索引这个索引会受总线地址、驱动版本、上电顺序影响。第三套是 CUDA 运行时可见编号也就是 PyTorch 中torch.cuda.current_device()返回的那个编号。CUDA 运行时在初始化时会把“当前可见的物理设备”重新编成一套从 0 开始的连续编号。而“可见”这件事恰恰由环境变量CUDA_VISIBLE_DEVICES说了算。问题就出在这个“重编号”操作上。划重点CUDA_VISIBLE_DEVICES不仅仅起到“屏蔽部分 GPU”的作用它还会把可见的 GPU 重新排列成一套从 0 开始的新编号。当这个环境变量里的顺序和物理顺序不一致时PyTorch 里的编号就必然和 nvidia-smi 不一致。比如设置CUDA_VISIBLE_DEVICES3,1那么 PyTorch 中的 cuda:0 是物理卡 3cuda:1 才是物理卡 1。反过来nvidia-smi 里的 GPU 0 可能是物理卡 0但如果你没把它放进可见列表它就永远不会出现在 PyTorch 的编号表里。这一来一去两个表格自然对不上。后面的所有排查思路都围绕一个目标把这三层映射关系彻底打通。2. 机制深挖nvidia-smi 与 PyTorch 各自编号的底层逻辑2.1 nvidia-smi 的编号由谁决定要真正理解错位现象得先知道 nvidia-smi 的编号从哪来。nvidia-smi 的索引是 NVIDIA 驱动层的 GPU 枚举结果这个枚举顺序主要取决于设备的总线地址Bus ID以及驱动加载时的探测顺序。大多数情况下nvidia-smi 输出的编号和物理卡槽位是匹配的因为驱动会按 PCIe 总线地址从小到大排序。但有一种情况会让这个编号“不稳定”就是在驱动模块重新加载、显卡固件更新、或者物理拔插之后枚举顺序可能发生改变。比如一次重启后原来排在第 3 号的卡可能变成第 0 号。此时如果代码里硬编码了“用 GPU 3”实际切到的可能就是另一张卡。所以生产环境里有个基本共识不要信任 nvidia-smi 的数字编号要信任 UUID。nvidia-smi -L会输出每张卡的 UUID这个 UUID 是每张显卡烧录进去的全局唯一标识不受枚举顺序变化影响。排查编号错乱问题时UUID 就是我们最强的锚点。来看一个典型的输出GPU 0: GeForce RTX 3090 (UUID: GPU-4b7d...) GPU 1: GeForce RTX 3090 (UUID: GPU-1a2b...) GPU 2: GeForce RTX 4090 (UUID: GPU-9c3d...)同样的信息也可以通过查询命令拿到nvidia-smi --query-gpuindex,uuid,name --formatcsv输出结果类似0, GPU-4b7d..., GeForce RTX 3090 1, GPU-1a2b..., GeForce RTX 3090 2, GPU-9c3d..., GeForce RTX 4090这张表就是驱动层的“档案”。不管上层怎么设置 CUDA_VISIBLE_DEVICES驱动层这张表始终反映真实物理设备。2.2 PyTorch / CUDA 的编号是如何重排的CUDA 运行时本身也维护一套编号但它不是直接使用驱动索引而是经过一个“可见性过滤 重排序”的过程。打个比方。nvidia-smi 像是一栋楼的楼层编号0 层、1 层、2 层是固定的建筑实体而 CUDA_VISIBLE_DEVICES 像是一个访客的通行证上面写了“只允许你访问 2 层、5 层、7 层”。访客为了方便记忆会把这些楼层重新编号为 A、B、C。对于访客来说A 就是它口中的 0B 就是它口中的 1。这个“重新编号”在 CUDA 里的实现方式就是按照CUDA_VISIBLE_DEVICES里出现的物理编号顺序从左到右依次分配新的 CUDA 编号 0、1、2…具体规则不复杂如果CUDA_VISIBLE_DEVICES0,1,2那么 CUDA 编号 0 物理 0编号 1 物理 1一一对应。如果CUDA_VISIBLE_DEVICES2,0,1那么 CUDA 编号 0 物理 2编号 1 物理 0编号 2 物理 1。这种配置在意图上可能是想“让卡 2 优先”但如果你没意识到重编号写代码时就会出错。如果CUDA_VISIBLE_DEVICES1,1,3同一个设备重复出现CUDA 行为通常是取第一个出现的位置后面的重复会忽略。如果CUDA_VISIBLE_DEVICES未设置CUDA 默认使用当前所有 GPU此时 CUDA 编号通常和驱动枚举编号一致但不保证永远一致因为驱动升级后仍可能改变枚举顺序。理解了这个机制很多诡异现象就说得通了。比如你设置了CUDA_VISIBLE_DEVICES4,5,6,7然后代码里用torch.device(cuda:2)你以为用的是物理卡 2 号实际上用的是物理卡 6 号。如果你再把这个配置写进分布式训练脚本每个进程拿到一套不同的“可见编号”整个集群的作业调度就全乱了。2.3 CUDA_VISIBLE_DEVICES 是被忽略的环境变量吗很多时候编号错乱的“罪魁祸首”不是 CUDA 本身而是环境变量没有像你预期的那样传递。最常见的情况有四种第一种是 shell 环境变量被覆盖。你在命令行里设置了CUDA_VISIBLE_DEVICES2,3,4,5但启动脚本内部又设置了os.environ[CUDA_VISIBLE_DEVICES] 0,1,2,3后者生效。第二种是继承污染。父进程设置了CUDA_VISIBLE_DEVICES1,2,5,6子进程没有显式处理直接继承了旧值。然后在脚本里面你又给它追加了内容最后形成一串看起来莫名其妙的值。第三种是跨容器环境。容器内启动 Python 时宿主机注入的环境变量可能具有不同的 GPU 编号集合比如容器内的 CUDA 编号和宿主机的编号不一致这种场景在虚拟化 GPU 服务中尤其明显。第四种是空白字符串。CUDA_VISIBLE_DEVICES表示屏蔽所有 GPU此时torch.cuda.device_count()返回 0但如果某个脚本对它做了错误判断就可能产生莫名的“找不到设备”报错。这里给一个实际检查的小技巧。在 Python 里直接打出来看import os print(os.environ.get(CUDA_VISIBLE_DEVICES, 未设置))如果输出的是未设置说明 CUDA 会用默认视图如果输出一串编号就要仔细核对它的顺序是否就是你想要的那一串。3. 实操篇一次定位 GPU 编号错位的完整排查流程3.1 第一步先用 nvidia-smi 拿到物理基线排查的第一步永远是先避开 Python单独看驱动层到底有几张卡、各自的 UUID 和总线信息是什么。这一步很简单nvidia-smi -L nvidia-smi --query-gpuindex,uuid,name,memory.total --formatcsv把输出保存下来后面所有判断都基于这张表。同时顺手看一眼nvidia-smi的实时监控页记下每张卡当前的进程和占用情况。此时不要启动任何新的训练任务保持现场干净否则你无法分辨哪些进程是本来就该存在的。我在实际操作中会把输出写到一个临时文件里然后做一件事把每张卡的 UUID 复制出来和主板上的物理插槽位置做一一对应。为什么要这么做因为有些服务器机箱里的卡是竖插的从 GPU 0 到 GPU 7 从左到右排列但换了驱动版本之后驱动枚举可能从右往左了。先确认物理对应关系后面判断错位时才不会自乱阵脚。3.2 第二步用 Python 打印 PyTorch 视角的设备列表接下来在终端里用交互式 Python 跑一段最简单的探查代码手动设置一个临时CUDA_VISIBLE_DEVICES来观察变化import os import torch # 为了让结果清晰先手动设一个明确的可见列表 os.environ[CUDA_VISIBLE_DEVICES] 0,2,4 print(可见 GPU 数量:, torch.cuda.device_count()) for i in range(torch.cuda.device_count()): props torch.cuda.get_device_properties(i) print(fcuda:{i} - name{props.name}, 显存{props.total_memory / 1024 / 1024:.0f}MB)这里的核心观测点是当设置CUDA_VISIBLE_DEVICES0,2,4时如果输出里 cuda:0 对应 0 号裁剪名称、cuda:1 对应 2 号卡名称、cuda:2 对应 4 号卡名称说明映射符合预期。但如果 cuda:0 打印出的名称和物理 0 号卡不一致说明它的“物理身份”已经被换过了。更精确的做法是让 Python 直接输出设备的 UUID。PyTorch 本身没有直接的“获取 UUID”接口但可以通过请求nvidia-smi并和 torch 输出做交叉对比来实现。3.3 第三步交叉对比脚本一分钟锁定错位关系下面这段脚本是我在实际排查中反复使用的小工具核心思路就是让 PyTorch 把每个 CUDA 编号对应的 GPU 信息拉出来同时调 nvidia-smi 把每个物理编号对应的信息拉出来让它们通过 UUID 对齐。import subprocess import torch def get_smi_table(): 返回列表每项是 {smi_index, uuid, name} cmd [ nvidia-smi, --query-gpuindex,uuid,name, --formatcsv,noheader, ] out subprocess.check_output(cmd, textTrue) rows [] for line in out.strip().splitlines(): parts [p.strip() for p in line.split(,)] rows.append({smi_index: int(parts[0]), uuid: parts[1], name: parts[2]}) return rows def get_cuda_visible_uuid(): 对每个 CUDA 可见编号通过 nvidia-smi 的进程查询反查 uuid。 更直接的方式是用 cuda-python 或者 pynvml但为了通用性这里用命令行。 smi_table get_smi_table() # 先拿到 PyTorch 的设备名 result [] for i in range(torch.cuda.device_count()): name torch.cuda.get_device_name(i) total_mem torch.cuda.get_device_properties(i).total_memory # 用显存大小 名称做弱匹配不够精确但能辅助判断 result.append((i, name, total_mem)) return smi_table, result smi_table, torch_devices get_cuda_visible_uuid() print( nvidia-smi 物理视角 ) for row in smi_table: print(f物理GPU {row[smi_index]}: {row[name]} ({row[uuid]})) print() print( PyTorch CUDA 视角 ) for i, name, mem in torch_devices: print(fcuda:{i}: {name}, 显存{mem / 1024 / 1024:.0f} MB)但注意这段代码还做不到完全精确的 UUID 匹配因为get_device_name无法直接对应到 UUID。如果你想精确锁定可以用 Python 绑定的pynvml库通过 NVML 拿到每个 CUDA 索引对应的 UUID 字段。这个方法我在后面会细说。3.4 第四步用 pynvml 精确读出 CUDA 索引对应的 UUIDpynvml是 NVIDIA 官方提供的 Python 绑定库可以直接访问 NVML 接口。它比命令行更可靠效率也更高。安装方式pip install nvidia-ml-py然后运行下面的脚本import pynvml import torch pynvml.nvmlInit() # 拿到物理设备数量 device_count pynvml.nvmlDeviceGetCount() print(NVML 物理设备数量:, device_count) # 建立 smi_index - uuid 的映射 smi_uuid_map {} for i in range(device_count): handle pynvml.nvmlDeviceGetHandleByIndex(i) uuid pynvml.nvmlDeviceGetUUID(handle) name pynvml.nvmlDeviceGetName(handle) smi_uuid_map[i] {uuid: uuid, name: name} print(f物理GPU {i}: {name} ({uuid})) # 对每个 PyTorch CUDA 可见设备找到对应的物理 index print() print( CUDA 编号与物理编号映射 ) for cuda_idx in range(torch.cuda.device_count()): # 通过设置 CUDA_VISIBLE_DEVICES 环境变量并重新初始化 CUDA 上下文 # 无法直接在运行时改变映射所以这里用另一种方法用 torch 当前可见列表反查 ...实际操作中有一个更笨但很稳妥的方式通过给每个 CUDA 编号制造一个明显的特征然后在 nvidia-smi 里观察。比如让每个进程分别占住一张卡并给它分配大块显存再通过 nvidia-smi 查看哪张物理卡有进程在跑。不过这个方法比直接调 NVML 慢一些。更推荐的做法是在代码里用torch.cuda.current_device()结合CUDA_VISIBLE_DEVICES的顺序做一次手动推导import os visible os.environ.get(CUDA_VISIBLE_DEVICES, 0,1,2,3) visible_list [int(x.strip()) for x in visible.split(,) if x.strip()] print(可见列表(物理编号):, visible_list) for i in range(torch.cuda.device_count()): if i len(visible_list): print(fcuda:{i} - 物理GPU {visible_list[i]})这段代码在绝大多数情况下能直接告诉你映射关系。它的原理就是 2.2 节讲的“按可见列表从左到右重编号”机制。3.5 第五步拿到结论后如何判断根因做完上述检查你会得出几种典型结论如果 PyTorch 的 cuda:0 对应物理 GPU 0cuda:1 对应 1且环境变量既没有隐藏也没有重排那说明代码和使用者都正常是 nvidia-smi 的枚举顺序发生了不连续变化这种情况要重点检查驱动和物理拓扑。如果 PyTorch 的编号与CUDA_VISIBLE_DEVICES的列表顺序完全一致只是和 nvidia-smi 不一致那么错位原因就是“环境变量重排”解决方案是把环境变量按目标顺序设置或者去掉环境变量让 CUDA 用默认视图。如果 Python 中torch.cuda.device_count()小于 nvidia-smi 里的实际卡数说明某些卡被隐藏了通常是CUDA_VISIBLE_DEVICES只包含了一部分卡。如果 Python 中设备数量为 0 但 nvidia-smi 显示有卡那就检查CUDA_VISIBLE_DEVICES是否被设成了空字符串或者不存在的编号。把结论写下来就等于完成了整个定位过程。剩下的就是该怎么修的问题。4. 高频错乱场景与避坑方案4.1 场景一分布式训练启动后不同进程看到的编号互相冲突这个问题在多进程训练里非常常见。比如你用分布式数据并行框架启动 8 个进程每个进程通过local_rank决定自己的 GPU 编号。常见的错误写法是device torch.device(fcuda:{local_rank})如果启动器已经把local_rank设置成与 CUDA 可见编号一致那没问题。但如果你手动设置了CUDA_VISIBLE_DEVICES为3,4,5,6然后又通过local_rank去加偏移比如cuda:{local_rank 3}这就会把 cuda:3 指向物理卡 6而 cuda:0 指向物理卡 3编号彻底乱掉。正确姿势是让local_rank直接对应 CUDA 可见编号不要自己去加物理偏移。也就是说进程启动后先拿到本机的local_rank然后直接用torch.device(fcuda:{local_rank})。同时在全链路里只设置一次CUDA_VISIBLE_DEVICES不要在 Python 内部再覆盖。4.2 场景二设置了 CUDA_VISIBLE_DEVICES 后训练日志仍显示“cuda:0”但实际跑在另一张卡这种情况多数发生在你同时使用了多个设置来源比如 Docker 启动参数里传入NVIDIA_VISIBLE_DEVICES或NVIDIA_VISIBLE_DEVICES已被映射成CUDA_VISIBLE_DEVICES然后脚本里又覆盖了一遍。最终生效的是脚本里最后一次赋值。这时候优先做的事不是去改代码而是直接打印环境变量import os print(os.environ.get(CUDA_VISIBLE_DEVICES))如果打印出来的是2,3,4,5你代码里又写死torch.device(cuda:0)那它实际就是物理卡 2。解决方式是明确区分两个概念CUDA 编号是“可见列表内的位置”物理编号是“驱动程序里的原始索引”。代码里尽量不要直接写死物理编号而是通过环境变量引导。4.3 场景三显存够用但总是 OOM一查发现两张卡重复被用这种问题在多人共享服务器上特别容易发生。有人设置了CUDA_VISIBLE_DEVICES5,6,7,8然后又用“nvidia-smi 里看 8 号卡”的判断标准去查看任务结果发现总是有进程占用同一张卡。实际上很可能是A 同学设置了CUDA_VISIBLE_DEVICES0,1B 同学设置了CUDA_VISIBLE_DEVICES1,0两个任务在 PyTorch 里看到的 cuda:0 可能不同但他们都在物理卡 0 上跑了重活。两边日志里都是 cuda:0互相不知道对方占的是哪张物理卡。解决思路是统一 GPU 分配规范所有人在同一个服务器上使用一套编号语义要么全部按 nvidia-smi 物理编号要么全部使用 CUDA 编号并严格禁止一边用物理编号一边用 CUDA 编号的混用。最可靠的做法是上线前人工检查一次nvidia-smi的进程列表和任务日志做一次简单的 UUID 核对。4.4 场景四宿主机和容器内的 GPU 编号不一致容器场景有个额外的干扰项宿主机上的卡被某些虚拟化服务重新索引后容器里看到的编号可能是一套完全不同的子集。比如宿主机有 8 张卡容器里被分配了 2、5、7 三张但容器内 CUDA 运行时可能显示为 0、1、2。如果你在容器内直接拿 cuda:1 当作宿主机物理卡的 GPU 1那就找错对象了。针对这种情况建议在容器启动时显式指定可见卡列表并让容器内 Python 打印环境变量和 torch 设备列表确认映射关系后再训练。可以在 Docker 启动命令中通过环境变量CUDA_VISIBLE_DEVICES指定宿主机物理卡号然后在容器内打印确认。4.5 场景五重启后 GPU 编号漂移这种场景最容易让人措手不及。前一天代码跑得好好的第二天重启机器发现 nvidia-smi 里同型号两张卡的顺序对调了。原代码里写死cuda:0实际上切到了原来那张卡但因为显存占用正常一时半会儿看不出问题。等模型性能出现细微差异才会意识到选错了卡。面对这种问题的长期对策是在代码中不依赖裸编号而是通过 UUID 来选定 GPU。你可以维护一个配置文件把“业务用途标识”映射到“UUID”启动时去 nvidia-smi 里动态解析 UUID 对应的当前物理编号然后再注入环境变量。这里给一段实用代码按 UUID 挑选 GPUimport subprocess import os import torch def get_uuid_to_index_map(): cmd [nvidia-smi, --query-gpuindex,uuid, --formatcsv,noheader] out subprocess.check_output(cmd, textTrue) mapping {} for line in out.strip().splitlines(): parts [p.strip() for p in line.split(,)] mapping[parts[1]] int(parts[0]) return mapping # 假设你想把任务固定到某块特定 UUID 的显卡上 TARGET_UUID GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx uuid_map get_uuid_to_index_map() if TARGET_UUID not in uuid_map: raise RuntimeError(目标显卡不存在) target_physical_index uuid_map[TARGET_UUID] # 再把物理编号设成可见列表 os.environ[CUDA_VISIBLE_DEVICES] str(target_physical_index) print(目标物理编号:, target_physical_index)这段代码跑起来后PyTorch 里的 cuda:0 就一定是 UUID 匹配的那张卡跟 nvidia-smi 的枚举顺序完全无关。这样即使驱动升级、物理插槽位置调整也不会影响你选卡。5. 从根子上规避一套比较稳的 GPU 编号设计原则5.1 原则一所有入口统一走环境变量我踩过几次坑后的体会是PyTorch 代码里不应该出现任何“裸编号”语言。比如torch.device(cuda:2)这种写法只要你有两台以上的卡就埋下了隐患。正确做法是入口脚本接收CUDA_VISIBLE_DEVICES参数然后所有程序内部全部使用cuda:{相对编号}绝不直接写死物理编号。推荐的标准流程是在项目管理配置或启动脚本里定义好要用的物理卡列表。把这个列表一次性设置给CUDA_VISIBLE_DEVICES。Python 内部通过torch.device(fcuda:{rank})使用相对编号。日志和环境变量输出时额外打印CUDA_VISIBLE_DEVICES的真实值方便事后追溯。这样做的好处是代码逻辑、日志记录、运行环境三者之间的语义完全一致。排查问题时只需要看环境变量和日志不需要再去猜“cuda:2”到底指向哪张物理卡。5.2 原则二重要任务必须做“显式归属校验”在正式训练开始前做一个 GPU 归属校验步骤能省下非常多排查时间。这个校验的思路很简单在代码初始化设备之后打印当前 CUDA 编号、物理编号、UUID 三者的映射并和预期配置做比较。import os import torch import subprocess def verify_gpu_mapping(expected_physical_gpus): visible os.environ.get(CUDA_VISIBLE_DEVICES, ) visible_list [int(x.strip()) for x in visible.split(,) if x.strip()] print(环境变量 CUDA_VISIBLE_DEVICES:, visible) for i in range(torch.cuda.device_count()): name torch.cuda.get_device_name(i) current_cuda_idx i physical_cuda_idx visible_list[i] if i len(visible_list) else None print(fcuda:{current_cuda_idx} - 物理索引 {physical_cuda_idx}, 型号 {name}) # 用 nvidia-smi 确认当前有没有重复占用 out subprocess.check_output( [nvidia-smi, --query-gpuindex,memory.used, --formatcsv,noheader], textTrue, ) print(nvidia-smi 当前显存占用情况:) print(out)默认情况下这段代码能在任务启动早期暴露错位问题。我通常还会在训练脚本第一轮迭代之后从 nvidia-smi 主动拉一次“当前进程占用的 GPU 索引”和历史日志做对照。一旦发现不一致立刻终止而不是等训练跑完免得浪费大量算力。5.3 原则三生产环境引入作业调度机制如果服务器上常驻多个用户或多项任务强烈建议在代码上再加一层“作业级 GPU 分配管理”而不是依赖每个人自己设置环境变量。常见做法是把 GPU 资源划分做成一张文本表映射“任务名 → 物理卡 UUID 列表”然后由一个统一脚本读取这张表设置环境变量并拉起任务。# gpu_tasks.conf 示例 # task_name physical_gpu_uuids train_a GPU-1111,GPU-2222 train_b GPU-3333,GPU-4444 eval_c GPU-5555启动脚本只负责解析这个配置并生成严格有序的CUDA_VISIBLE_DEVICES。这样每个任务从开始就有明确的物理卡归属不需要任何人去记忆物理编号与 CUDA 编号的转换规则。即使某天驱动枚举顺序变了只要 UUID 不变任务依然会落到正确的卡上。这套机制是我的第一推荐因为它在源头上消灭了大部分“编号不一致”的发作场景。剩下的偶发问题也只能是环境变量被外部覆盖而那个问题可以在启动脚本里强制覆盖环境变量加上一次终检来兜住。5.4 原则四日志里一定要留“证据链”最后一条原则最便宜但最容易忽略把所有和 GPU 相关的关键信息写进日志。日志里至少包含三样东西启动时CUDA_VISIBLE_DEVICES的原始值。启动时torch.cuda.device_count()和每张 CUDA 编号对应卡的名称、显存信息。启动脚本所在的宿主机名称和进程 PID。这样一旦出了问题直接看日志头部就能还原整套映射关系不需要再去现场复现。特别是多人协作的场景别人可能改过环境变量也可能在启动脚本里临时加了覆盖逻辑没有日志就什么都说不清。我在一次排查中就是靠日志里的“PID 时间戳”才从 nvidia-smi 的进程列表里反推出某个历史进程到底跑在哪张物理卡上节约了大量时间。提示写日志时别忘了“时间戳”。如果你在凌晨发现问题而去翻半天前的启动日志没有时间戳是根本定位不到当时的 CPU/内存/GPU 状态的。6. 最后的几句实在话这个坑看起来像一个小小的环境变量问题但它背后牵扯到的是“物理设备、驱动枚举、运行时编号”三层概念的边界。我个人的建议很简单不要在代码里和 GPU 编号玩猜谜游戏。你花半小时做的 UUID 校验和日志打印可能在未来几个月里帮你省下几个通宵。如果这篇文章只留一段话我会说先让环境变量成为你唯一的编号来源再用 UUID 兜底物理身份最后把所有证据写进日志。做到这三点PyTorch 和 nvidia-smi 的编号就不会再有机会“打架”。如果你现在正被编号问题折磨别慌按第 3 节的五步流程走一遍大概率十分钟内就能定位根因。修好后记得把排查脚本留在项目仓库里给后来人留一条活路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑