【Bug已解决】[Bug]: Device_map multi-GPU inference can silently corrupt tensors when local CUDA peer copi 【Bug已解决】[Bug] Device_map multi-GPU inference can silently corrupt tensors when local CUDA peer copies are unhealthy 解决方案一、现象长什么样用device_mapauto做多卡推理模型各层分到不同 GPU前向时张量在层间跨卡搬运出现诡异的结果同样的输入多次运行结果不一致时而正常、时而输出乱码。单卡device_mapcuda:0完全正常多卡device_mapauto跨 2 卡才出问题。没有任何报错——loss 不爆、不抛异常就是「输出悄悄错了」。换到 NVLink 健康的机器又正常。本质device_map跨卡搬运张量走的是 GPU 间的 P2Ppeer-to-peer拷贝。当这两张卡之间的 P2P / NVLink / 互连不健康**线缆松动、跨 NUMA 节点未开启 P2P、驱动状态异常时拷贝静默出错数据传过去但位错了且 CUDA 不报错于是下游用了一份损坏的张量继续算输出乱码。**二、背景device_mapauto会把一个大模型按层切到多张卡第 1k 层在 GPU0k1n 层在 GPU1……前向时某一层的输出要从「它所在的卡」拷贝到「下一层所在的卡」。这个跨卡拷贝有两种路径P2P 直拷cudaMemcpyPeer / NVLinkGPU 之间直接传最快。Accelerate/transformers 默认走这条。经 HostCPU中转先cuda - host再host - 另一张 cuda。慢但依赖的是主机内存这条更稳的通道。正常情况下 P2P 又快又对。但当两张卡的 P2P 互连不健康时cudaDeviceEnablePeerAccess可能「看起来成功」但底层链路有比特错误或 P2P 走了 PCIe 而非 NVLink在跨 NUMA / 拓扑异常时传输出错CUDA 的cudaMemcpy对 P2P 的静默数据损坏不报错误它只报「传输没完成/参数错」不校验数据正确性。于是张量被「传过去了但值错了」框架毫无察觉继续前向 → 输出乱码。因为不报错这种 bug 极难定位且「时好时坏」取决于哪次传输踩到坏链路。一句话不健康 P2P 拷贝静默损坏张量而 CUDA 不校验数据正确性导致多卡device_map输出悄错。三、根因根因是跨卡张量拷贝默认走 P2P 且不做数据完整性校验不健康链路静默损坏张量三层第一层主因P2P 拷贝无数据校验。Accelerate 的跨卡搬运直接tensor.to(other_device)底层 P2P从未对「传过去的值」做校验如 checksum / round-trip 比对。坏链路传错值也无人知晓。第二层P2P 健康状态未探测就使用。框架在 dispatch 前没有「先探测这两张卡 P2P 是否健康」如cudaDeviceCanAccessPeer 一次小数据 round-trip 校验直接假定 P2P 可用即正确于是不健康链路被直接采用。第三层无降级通道。即便发现 P2P 不稳框架也没有「改走 host 中转」的降级逻辑只能硬着头皮用坏链路导致持续静默损坏。一句话P2P 拷贝无校验 健康未探测 无 host 降级不健康的 P2P 链路静默损坏多卡推理张量。四、最小可运行复现下面用纯 Python 模拟「P2P 拷贝在不健康链路上静默改值且框架无校验直接采用」的控制流不需要 GPUclass FakeGPU: def __init__(self, name, healthy_peerTrue): self.name name self.healthy_peer healthy_peer def p2p_copy(src_val, dst_gpu, healthy): 模拟 P2P 拷贝健康链路原样传不健康链路静默改值。 if healthy: return src_val return src_val ^ 0xFF # 比特翻转但无报错 def dispatch_buggy(layers, gpus, value): 有 bug直接走 P2P不校验、不探测、不降级。 for i, layer in enumerate(layers): dst gpus[(i 1) % len(gpus)] healthy gpus[(i 1) % len(gpus)].healthy_peer value p2p_copy(value, dst, healthy) # 不健康也照用 return value def main(): gpus [FakeGPU(cuda:0, healthy_peerTrue), FakeGPU(cuda:1, healthy_peerFalse)] # cuda:1 P2P 不健康 out dispatch_buggy([L0, L1], gpus, 0x1234) print(f输入 0x1234, 输出 0x{out:X} (不健康链路静默损坏无报错)) if __name__ __main__: main()跑出来输出被比特翻转、且全程无报错——演示了「不健康 P2P 静默损坏、框架不察」。五、解决方案第一层最小直接修复最省事的救火避免 P2P强制走 host 中转或干脆限制单卡。最简单是让device_map落到一张卡牺牲多卡、换正确from transformers import AutoModelForCausalLM # 临时规避单卡杜绝跨卡 P2P model AutoModelForCausalLM.from_pretrained( big-model, device_mapcuda:0 )若必须多卡强制所有跨卡拷贝经 host用accelerate的dispatch时设置offload_buffers或把中间张量先.cpu()再.to()# 跨卡搬运时显式经 host绕过 P2P def safe_cross_device(tensor, dst_device): return tensor.cpu().to(dst_device) # 走 host避开不健康 P2P这能立刻消除静默损坏host 通道更可靠代价是慢一点。六、解决方案第二层结构性改进第一层是「避开 P2P」第二层是「 dispatch 前探测 P2P 健康、拷贝后做数据校验、不健康则降级 host」从设计上消灭静默损坏from dataclasses import dataclass from typing import Callable dataclass class PeerHealth: healthy: bool class CopyDispatcher: def __init__(self, peer_health: dict, verify: bool True): self.peer_health peer_health # (src,dst) - PeerHealth self.verify verify def _checksum(self, t) - int: # 用张量数值做个轻量校验和真实实现用 .sum() 或 hash return int(t.float().sum().item() * 1000) 0xFFFFFFFF def copy(self, tensor, dst_device, src_device): key (src_device, dst_device) use_p2p self.peer_health.get(key, PeerHealth(False)).healthy if use_p2p: out tensor.to(dst_device) # 尝试 P2P if self.verify: # 校验回拷一份比对 checksum不一致则判定损坏 back out.to(src_device) if self._checksum(back) ! self._checksum(tensor): use_p2p False # 标记为坏降级 if not use_p2p: # 降级经 host 中转绕开不健康 P2P out tensor.cpu().to(dst_device) return out def probe_peer(src, dst) - PeerHealth: dispatch 前探测 P2P 是否健康canAccessPeer 小数据 round-trip。 # 真实实现cudaDeviceCanAccessPeer 一次往返比对 return PeerHealth(healthy_real_peer_ok(src, dst))关键改动探测dispatch 前用probe_peer标记每对卡的 P2P 健康不健康直接走 host。校验即便走 P2P拷贝后做一次round-trip checksum 比对发现值变了立即判定损坏。降级校验失败或探测不健康自动改 host 中转绝不把损坏张量送进下一层。七、解决方案第三层断言 / CI 守护把「探测不健康即降级」「拷贝后校验」「host 降级正确」固化成测试import pytest def test_healthy_peer_uses_p2p(): disp CopyDispatcher({(cuda:0, cuda:1): PeerHealth(True)}) # 健康走 P2P不降级 out disp.copy(_t(1.0), cuda:1, cuda:0) assert out.device_hint p2p def test_unhealthy_peer_downgrades_to_host(): disp CopyDispatcher({(cuda:0, cuda:1): PeerHealth(False)}) out disp.copy(_t(1.0), cuda:1, cuda:0) assert out.device_hint host # 降级成功 def test_corrupt_p2p_detected_by_checksum(): # 模拟 P2P 健康标记但实则损坏校验应捕获 disp CopyDispatcher({(cuda:0, cuda:1): PeerHealth(True)}, verifyTrue) out disp.copy(_t_corrupt(1.0), cuda:1, cuda:0) assert out.device_hint host # 校验失败降级 def test_host_copy_preserves_value(): # host 中转必须值不变 disp CopyDispatcher({(cuda:0, cuda:1): PeerHealth(False)}) out disp.copy(_t(3.14), cuda:1, cuda:0) assert out.value 3.14 def test_no_silent_corruption(): # 不健康链路下输出绝不能是损坏值 disp CopyDispatcher({(cuda:0, cuda:1): PeerHealth(False)}) out disp.copy(_t_corrupt(5.0), cuda:1, cuda:0) assert out.value 5.0 # 降级后值正确再加一个端到端回归多卡推理在不健康 P2P 下仍输出正确def test_multigpu_inference_correct_on_unhealthy_peer(): model make_model(device_mapauto) with patch_peer_unhealthy((cuda:0, cuda:1)): out model.generate(hello) assert out is not None and not is_garbage(out) # 不因 P2P 损坏而乱码八、排查清单看多卡device_map输出乱码/不一致但无报错单卡正常 → 是 P2P 静默损坏。检查多卡是否跨 NUMA / NVLink 状态用nvidia-smi topo -m看 P2P 连接。临时救火限制单卡device_mapcuda:0或跨卡经.cpu().to()中转。检查框架跨卡拷贝是否做了数据校验默认没有 → 易踩。长期修复dispatch 前探测 P2P 健康 拷贝后 checksum 校验 不健康降级 host。升级 accelerate/transformers 到合了 P2P 健康探测的版本并跑上面的「降级」用例。若只在特定卡对出错基本是那对卡 P2P 拓扑异常优先禁用该对 P2P。九、小结device_map多卡推理静默损坏张量不是模型错了而是跨卡搬运默认走 P2P 且不做数据校验不健康的 P2P 链路静默传错值、CUDA 不报导致下游用损坏张量算出乱码。最小修复是限制单卡或跨卡经 host 中转结构性修复是 dispatch 前探测 P2P 健康 拷贝后 checksum 校验 不健康降级 host最后用 pytest 把「探测即降级」「校验捕获损坏」「host 降级值不变」锁死。抓住「跨设备拷贝必须校验数据完整性、不健康的快通道要能自动降级」这条所有多卡/分布式张量搬运的静默损坏都能照此设防。