
这行报错我太熟悉了凡是折腾过PyTorch多卡训练或者在一台机器上反复切换CUDA环境的人几乎都被它折磨过。乍一看像是“你的程序里有个cuda2但另一个东西在cuda0上”很多人第一反应是去检查机器上有没有插第二张卡结果发现明明只有一张卡报错还照样出现那种心态直接崩掉。先给结论这个报错跟“机器上有没有编号为cuda2的显卡”没有必然关系它真正想表达的是——你的代码里有两个张量或者模型参数被放置在不同的CUDA设备上其中一个在cuda0而另一个期望在cuda2上PyTorch不允许这种跨设备运算。这就是一个典型的设备分配不一致问题。搞明白了这一点你就能少走至少半天弯路。这篇文章我会从报错原理讲起然后给你一套完整的排查流程再给出修复方案和多卡训练下的正确打开方式最后打包几个我在实际工作中踩过的关联坑希望能帮你一次性把这问题摁死。1. 报错解读这行红字到底在说什么1.1 一次让人抓狂的报错现场先还原一下常见场景。你写了个训练脚本在本地单卡上调得顺风顺水推送到服务器上准备上多卡或者换一张大显存的卡结果一跑就弹出来RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda2 and cuda0!如果你运气差一点看到的可能是类似“cuda2 but found one of them on device cuda 0”这种被截断的版本。此时你的第一反应通常是我电脑上根本没有cuda2这块卡啊程序是不是瞎了实际不是。PyTorch里的cuda:2只是一个逻辑编号它不一定对应物理上的第三块显卡。只要你的程序里有一个张量或者模型参数被放置在编号为2的设备上而另一个关键张量还在编号为0的设备上两者一旦发生运算PyTorch就会立刻拦下来抛出这个异常。1.2 设备编号只是逻辑称呼不是物理顺序这里需要好好解释一下CUDA设备编号的机制。在没有做任何设置的情况下cuda:0代表系统当前可见的第一块GPUcuda:1代表第二块。但只要你设置了环境变量局面就会完全不一样export CUDA_VISIBLE_DEVICES2,3这样一来物理上的第3块和第4块显卡在程序里反而会被认成cuda:0和cuda:1。也就是说你需要关注的不是“机器上有几块卡”而是“当前进程里能看到几块卡以及它们的编号映射关系”。很多莫名其妙的设备不匹配报错根源都在这里。比如你在命令行里临时加了CUDA_VISIBLE_DEVICES2程序里某个地方写死了torch.device(cuda:1)这时候进程里其实只有一块可见卡cuda0你强行去访问cuda1轻则直接报“device not found”重则就是这种云里雾里的设备不一致错误。所以看到报错里出现cuda2先别急着去机房看有没有第三张卡先想想你的代码里谁在引用这个编号。1.3 真正的冲突张量被放在了不同的“工位”上用一个生活化的例子帮助理解。你把一批数据张量想象成一组待加工的零件每个零件都会被放到某个工位上cuda:0工位、cuda:1工位而模型就是一台必须同时接触所有零件的机器。现在机器安排在cuda:2工位上你喂给它的一个零件却在cuda:0工位上机器自然没办法同时处理这两个不在同一地方的零件于是报错。更麻烦的是这种不一致往往不是显式的。比如你从DataLoader里取一批数据它们默认放在CPU上你用.to(device)把它们挪到了cuda:0但模型的某个子模块因为加载checkpoint时的逻辑问题留在了cuda:2上。前向传播一执行两个不匹配的张量一相遇报错瞬间爆炸。2. 排查路线把躲在角落里的那个张量揪出来面对这种报错最重要的是不要慌更不要直接去重装CUDA或者重刷驱动。99%的情况下是你的代码逻辑在某个环节放错了地方。下面这套排查流程是我在无数次踩坑后沉淀下来的按这个顺序走基本能定位到具体行。2.1 先搞清楚进程里到底能看见几块卡第一步先确认环境。你可以在脚本最开头加一段诊断代码import torch print(PyTorch版本:, torch.__version__) print(CUDA是否可用:, torch.cuda.is_available()) print(可见GPU数量:, torch.cuda.device_count()) for i in range(torch.cuda.device_count()): print(f cuda:{i} - {torch.cuda.get_device_name(i)}) print(当前设备:, torch.cuda.current_device())这一步能帮你判断是不是环境变量把设备编号搞乱了。如果devices_count()返回1而你的代码里还在用cuda:2那问题就非常明确了有人在代码里写死了设备编号。如果返回数量大于1说明确实有多卡环境那就进入下一步查张量到底分布在哪个设备上。2.2 给每个关键张量“查户口”报错信息虽然只提到“found at least two devices”但没告诉你是哪一行代码触发的。不要慌先做两件事第一仔细看完整堆栈信息找到第一次发生张量运算的地方那行代码就是“案发现场”。第二在“案发现场”附近插入临时打印把参与运算的每个张量的device属性打出来print(input tensor device:, input_tensor.device) print(weight tensor device:, model.fc.weight.device) print(mask tensor device:, mask_tensor.device)记住一条铁律PyTorch中两个张量进行任何运算都必须位于同一块设备上。所以你要做的就是在报错触发的那一行之前逐一确认所有参与运算的张量它device属性是否一致。我遇到过最隐蔽的情况是一个经过torch.where生成的条件掩码张量因为之前某个分支把它放到了CPU上导致后续运算直接炸掉。2.3 别忘了模型、优化器和Dataloader除了显式的张量还有三个容易藏雷的对象模型参数、优化器状态、数据加载器返回的数据。模型参数如果只有一部分模块调用了.to(device)另一部分漏掉了就会造成模型内部参数散落在多个设备上。检查方法是遍历模型参数看它们的device是否一致devices set() for name, param in model.named_parameters(): devices.add(param.device) if len(devices) 1: print(f参数 {name} 在 {param.device}已有设备集合 {devices})优化器状态优化器通常会在第一次step()前根据参数位置创建状态如果你先to(device)再创建优化器一般没问题但如果你顺序反了或者在to(device)之前已经执行过optimizer.step()优化器里的动量缓存可能还在CPU或旧设备上。Dataloader如果你在数据集里手动做了张量变换并且变换后的张量调用了.cuda(0)或者.to(cuda:0)而主程序用的设备是cuda:2那么每个batch都会被送到错误的地方。正确的做法应该是让DataLoader保持返回CPU张量在训练循环开头统一.to(device)。3. 修复方案从“各玩各的”到“统一工位”排查出问题在哪之后修复其实不复杂但有几个设计层面的习惯一定要养成。我下面的方案按推荐程度从高到低排列。3.1 最推荐的做法全程序只维护一个device变量很多人喜欢在脚本里到处写.cuda()这算是最容易埋雷的写法。更好的是在配置阶段就定义好全局设备对象后面所有张量和模型都用它来迁移import torch # 专一device变量不要到处写死 cuda:0 或 cuda:2 device torch.device(cuda if torch.cuda.is_available() else cpu) # 模型 model MyModel().to(device) # 输入数据 for batch in dataloader: inputs, labels batch[0].to(device), batch[1].to(device) outputs model(inputs)这样写有两个好处一是换机器、换卡时只需要改一行二是彻底杜绝了“这个张量在cuda0、那个在cuda2”的错位。如果你需要让程序支持多卡还可以结合环境变量来定义设备import os local_rank int(os.environ.get(LOCAL_RANK, 0)) device torch.device(fcuda:{local_rank} if torch.cuda.is_available() else cpu)3.2 多卡训练下DataParallel和DDP的设备分配差异多卡训练是设备不一致报错的重灾区。PyTorch有两种主流方案DataParallel简称DP和DistributedDataParallel简称DDP。DataParallel用起来很简单model nn.DataParallel(model)它的机制是把输入batch在dim0维度上切成多份分发给所有可见GPU然后在cuda:0上汇总loss。正因为主卡承担了汇总工作所以DataParallel要求所有输入数据的batch size必须大于GPU数量而且主卡默认cuda:0的内存占用会明显高于其他卡。如果你在DataParallel的模型外面又手动把输入挪到了cuda:2就会出现输入在卡2、模型参数分散在所有卡上的诡异局面。DistributedDataParallel是更推荐的多卡方案但它对设备分配的要求更严格。它通过torch.distributed.init_process_group初始化进程组每一个进程负责一块卡设备必须严格对应当前进程的local_rank。不少人在这里犯的错误是启动脚本时传了CUDA_VISIBLE_DEVICES0,1,2,3但在初始化时却用了torch.device(cuda:0)导致所有进程都挤到第一块卡上或者某一次前向传播时张量出现在非当前进程的设备上触发一堆莫名其妙的错误。DDP的标准初始化方式我建议直接抄import torch.distributed as dist import torch.multiprocessing as mp def worker(local_rank): # 通常由torchrun或launch脚本传入 torch.cuda.set_device(local_rank) device torch.device(fcuda:{local_rank}) dist.init_process_group(backendnccl, ranklocal_rank, world_sizeworld_size) model MyModel().to(device) model nn.SyncBatchNorm.convert_sync_batchnorm(model) # 如果需要 ddp_model DDP(model, device_ids[local_rank])核心思想就是“一进程一卡设备编号严格遵循local_rank”。不要在你的代码里用torch.cuda.current_device()或者torch.cuda.device_count()去猜设备否则多进程环境下极容易错位。3.3 从checkpoint恢复训练时最容易翻车我在生产环境里排查过很多次类似的“cuda2 vs cuda0”报错最后发现它们有个共同的隐藏源头模型是从checkpoint恢复的而checkpoint里的参数和设备绑定关系出了问题。比如说你之前在一块卡上保存了模型权重用的是torch.save(model.state_dict(), path)这个没问题因为state_dict只存了数值没有绑设备。但如果你用的是torch.save(model, path)直接保存整个模型对象那么反序列化时模型参数可能会保持在保存时的设备上。如果你换了机器、换了卡号加载出来的模型参数就会被还原到已经失效的cuda:2上。更隐蔽的问题是“先加载后迁移”的顺序搞反了。正确的加载姿势是checkpoint torch.load(path, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model model.to(device)注意两个关键点第一torch.load一定要加map_locationcpu先把所有参数加载到CPU再统一迁移到目标设备。这样能避免“张量在cuda2、模型在cuda0”的尴尬局面。第二如果checkpoint里保存了优化器状态也要在模型to(device)之后重新创建优化器或把优化器状态load_state_dict后再to(device)否则优化器状态可能会留在其他设备上。4. 这些周边坑排查时一起避开设备不一致报错往往不是孤立出现的它通常和你的CUDA环境、显卡驱动、训练框架版本纠缠在一起。我在这里把排查过程中容易一起碰到的坑统一列一下免得你刚解决完这个又被下一个绊倒。4.1 为什么有时候“cuda:1”或者“cuda:2”根本不存在很多人的误区是报错里提到了cuda2那就说明系统里有cuda2可用。但实际上如果你在启动脚本前设置了CUDA_VISIBLE_DEVICES物理设备编号和你进程里看到的逻辑编号完全是两码事。举个例子# 物理上有4块卡但你只想用第1块和第3块 export CUDA_VISIBLE_DEVICES0,2 python train.py在这个进程里物理第3块卡对应的逻辑编号是cuda:1物理第1块卡对应cuda:0。如果你的代码里硬编码了torch.device(cuda:2)即使物理机上有第3块卡进程里也访问不到报错就来了。我自己的习惯是在训练脚本里显式打印一下当前进程可见的设备列表并把它写进日志。这样真正出了问题一看日志就能追溯到是环境变量的问题、还是代码写死设备的问题。另外如果你用nvidia-smi看到的卡和PyTorch里device_count()得到的数量对不上先检查CUDA_VISIBLE_DEVICES这是最常见的差异来源。4.2 其他常见CUDA报错速查除了“cuda2 but found one of them on device cuda 0”我还经常收到另一类报错比如RuntimeError: CUDA error: device-side assert triggered这种报错通常不是设备分配问题而是你的代码在GPU上执行了一些非法操作最常见的就是标签索引越界、类别数和全连接层输出维度不匹配、某些张量包含NaN或Inf。它之所以在设备分配问题中被一起讨论是因为很多人第一次遇到它时误以为是CUDA环境坏了。实际排查手段是先加CUDA_LAUNCH_BLOCKING1环境变量把异步执行变成同步执行这样堆栈信息就能定位到具体代码行而不是在一个普通Kernel执行完毕后才统一报错。再比如nvidia-smi: unable to determine the device handle for GPU 0000:41:00.0: Unknown Error这通常代表驱动和设备的通信出了问题常见的诱因是显卡被其他进程占用到濒临崩溃、GPU掉驱动、或者多卡环境下某块卡的PCIe链路异常。遇到这种问题优先考虑重启机器或者用nvidia-smi -pm 1打开持久化模式试试不要急着重装CUDA。还有一个容易让人误判的CUDA error: out of memory这也不用多说一般就是显存峰值超了。但有一种情况是模型在训练过程中动态创建了某些临时张量并且没有及时释放一步步把显存占满。排查时可以用torch.cuda.max_memory_allocated()查看峰值显存再用torch.cuda.reset_peak_memory_stats()包住某个小循环逐步定位。4.3 环境层面的兼容性排查驱动、CUDA、PyTorch三者缺一不可设备分配报错排查到最后如果代码里真的找不出毛病那就得考虑环境本身的兼容性。这里有一个比较实的体会PyTorch是编译时绑定CUDA版本的你用什么版本的PyTorch最好就安装对应版本的CUDA Runtime虽然PyTorch很多操作是动态加载驱动的但cuda2这种设备编号能出现在报错里说明你的CUDA Runtime至少已经正常枚举出了设备。常见的兼容性问题有这几类驱动版本过低老驱动不支持新版CUDA。nvidia-smi里能看到驱动版本torch.version.cuda里能看到PyTorch自带的CUDA版本如果驱动版本比CUDA最低要求还低很多奇怪的问题都会冒出。可以先运行python -c import torch; print(torch.cuda.is_available())如果返回False十有八九是驱动和CUDA版本不匹配。多个CUDA版本共存导致路径混乱一台机器上可能装了CUDA 11.8、CUDA 12.1~/.bashrc里的LD_LIBRARY_PATH又写得比较随意就容易出现“上一个程序跑得好好的换一个Python环境就崩了”的情况。排查时可以用torch.utils.cpp_extension.CUDA_HOME看看PyTorch认为CUDA在哪再用nvcc -V看看当前命令行实际用的CUDA是哪一套保证两者一致。WSL2下的特殊情况如果是在WSL2里跑CUDA设备是通过宿主机透传进来的这里最容易出问题的是驱动必须装Windows侧而CUDA Toolkit可以装在Linux侧。Windows的驱动和Linux的CUDA Runtime各管一摊一旦版本对不上或者Path没配对设备枚举就会出怪事也可能间接引发设备编号错乱。5. 最后分享一点排查心得这一路排查下来你会发现绝大多数所谓“CUDA设备错误”其实都不是硬件环境坏了而是代码里对设备分配的处理不够严谨。我自己在踩过几次坑之后现在写任何PyTorch训练脚本都会在开头固定好全局device变量所有张量迁移都通过它多卡环境一律用DDP并严格绑定local_rank加载checkpoint统一走map_locationcpu再迁移。这套组合拳打下来“cuda2 but found one of them on device cuda 0”这种报错几乎没再出现过。如果你现在正在被这个报错折磨建议按文章里的顺序走一遍先打印可见GPU数量再逐一张量查设备最后检查模型和优化器。绝大多数情况下问题都会在半小时内水落石出。如果真的把所有代码逻辑都排查干净了还是报错那再去怀疑驱动和CUDA版本也不迟。毕竟环境问题虽然可怕但代码逻辑问题永远是最常见的第一嫌疑犯。