训练无报错却算错:GPU 静默损坏排查
文章在 PyTorch DDP 训练循环中加入三项检查,检测 GPU 返回看似合理但实际错误的数据。结合 SageMaker HyperPod 定位故障节点,并交由自动恢复机制替换。
AI 深度解读
GPU 算错了,训练却没有报错,loss 甚至还能正常跑几个小时。AWS 这篇文章讨论的就是这种静默数据损坏(SDC):错误数值看起来合理,却会通过分布式训练扩散到所有模型副本,最后变成一次昂贵的无效训练。
作者的方案是在 PyTorch DDP 训练循环里加入三道检查,再接上 Amazon SageMaker HyperPod 的节点替换流程。值得看的是检测边界:副本权重一致,不代表计算正确;一次检查通过,也不能证明 checkpoint 干净。
为什么机器没报警,所有副本却一起错了
SDC 的麻烦在于,硬件返回的是「合理但错误」的结果,比如把 4.0000 算成 4.0001。原文将成因归于制造缺陷和芯片老化,尤其是 tensor core 中的乘加计算通路;高温、电压下降或特定操作数可能触发错误。这些算术通路不像内存那样受到 ECC 或奇偶校验保护。
原文引用的规模案例是:Llama 3 405B 在最多 16K H100 GPU 上预训练,54 天内发生 419 次意外任务中断,其中 6 次被归因于 SDC。这个数字只覆盖导致中断且被追溯到 SDC 的事件,不能拿来估算全部静默错误的发生率。
HyperPod 原有的健康监控、深度检查和自动节点恢复依赖可观测的故障。硬件没有报告错误,训练代码就得自己核对计算结果。
DDP 恰好提供了可利用的冗余:各副本处理不同数据,反向传播后通过 all-reduce 平均梯度,再分别调用 optimizer.step()。更新后的权重应该逐位一致。
但假设某个副本把局部梯度 0.40 算成 4.00,错误会被平均到所有副本。即使学习率是 0.1,大家仍然执行相同的错误更新,权重比较照样通过。相反,若错误发生在更新过程或权重存储中,副本之间才会出现差异。
权重一致只能说明副本没有分叉,不能说明它们算对了。
三道检查分别守住哪一步
第一道检查在 all-reduce 前记录局部梯度,识别 NaN、Inf 和溢出。作者通过 DDP communication hook 保存统计信息,再交给标准 allreduce_hook。这样既能发现异常,也能保留异常来自哪个 rank 的线索。
all-reduce 后还要再检查一次,因为归约运算本身也可能产生 NaN 或 Inf。作者复用 clip_grad_norm_ 计算的梯度范数,每步额外执行一次单元素 all-reduce,在更新权重前拦住异常。使用 fp16 和 gradient scaler 时,要排除 scaler 本来就会跳过的溢出步骤;原文说明 bf16 没有这类 scaler 溢出步骤。
第二道检查处理有限但错误的数值:定期让另一台机器上的 GPU 重算同一个 micro-batch。
作者采用环形重放。每个 rank 重算前方第 8 个位置的 rank 的数据,在 p5 实例上相当于跨一个节点。两次计算使用相同权重、输入和随机状态,才有资格比较。如果某个 rank 参与的两组比较都不一致,它就成为嫌疑对象;只有一组不一致,则让第三个节点上的 GPU 参与确认,并让目标 GPU 再算一次,以区分持续性和瞬时性错误。
不能只让嫌疑 GPU 自查,它可能稳定复现同一个错误。
默认模式只逐位比较 loss,因为默认 attention backward 不具备确定性。设置 deterministic_audits=True 后,审计步骤使用确定性算法和 math attention kernel,才能进一步比较梯度指纹。作者模型上的确定性审计步骤耗时是普通步骤的 2.4 倍;没有确定性实现的算子会直接报错。
第三道检查放在 optimizer.step() 之后,比较所有副本的权重指纹。某个权重只有获得超过半数副本的一致支持,才据此指出异类。只有两个副本,或者没有严格多数时,只能报告分歧。
这道检查每 N 步执行,并且必须覆盖每次 checkpoint。作者注入权重错误后,保存前的强制检查成功阻止了写盘;取消它,损坏副本就会被保存,直到 5 步后的下一次重放才被发现。
为什么不看梯度大小,而要重算和取指纹
局部梯度范数看起来更便宜,但各 rank 原本就在处理不同数据。作者在 4 台实例、32 张 H100 上,用 953-million-parameter 模型记录了三次无故障运行,共 22,800 步。
结果很不友好:健康数据在前 800 步就达到 224 个中位数绝对偏差(MAD),完整运行达到 1,240。把阈值设为 k = 336,即早期最高值的 1.5 倍,剩余 22,000 步仍出现 11 次误报。有限错误的异常分数却从未超过 6.9,哪怕把一个 activation 乘以 10,000。
这说明在这组实验里,正常数据差异足以淹没计算错误,调大阈值也救不了检测能力。
重放和权重检查又不能频繁传输整个张量,所以作者用两个 64-bit 累加值生成指纹:一个直接累加原始位模式,一个按位置加权。下面的片段展示了这一做法:
_INT_VIEW = {1: torch.uint8, 2: torch.int16, 4: torch.int32, 8: torch.int64}
@torch.no_grad()
def fingerprints(tensors, chunk=1 << 24):
# ... picks the device and allocates out: one row of two int64 per tensor
for k, t in enumerate(tensors):
if t is None:
continue
bits = t.detach().contiguous().view(-1).view(_INT_VIEW[t.element_size()])
for i in range(0, bits.numel(), chunk): # chunked to bound temporary memory
b = bits[i:i + chunk].to(torch.int64)
pos = torch.arange(i + 1, i + 1 + b.numel(), dtype=torch.int64, device=dev)
out[k, 0] += b.sum()
out[k, 1] += (b * pos).sum()
return out
要看的是 out[k, 1]:交换两个权重,或者对两个值做相互抵消的修改,可能不改变普通求和,加权求和能发现作者测试的这些情况。代码中的设备选择和 out 分配已被原文省略,不能直接复制运行。
对于元素少于 2³²、每个元素不超过 4 bytes 的张量,任意一两个元素发生变化都会改变该指纹。更多元素同时变化仍可能相互抵消,所以指纹相等是有力证据,不能当作数学证明。
作者测得,953-million-parameter 模型每次权重检查耗时 31.1 ms,每 10 步执行一次时,摊销开销约为训练步的 2%。
我的判断
这套方案最有价值的地方,是把检查放在错误还来得及被拦住的位置。计算检查失败就清空梯度,跳过更新;权重检查失败就阻止 checkpoint。只发告警、让任务继续运行,无法实现同样的隔离效果。
但它的适用前提很具体:至少三个 GPU 节点;DDP 每个副本独立执行优化器更新;rebuild_batch(step, rank) 能准确重建输入;训练和重放使用相同的 compute_loss,包括 autocast 和随机行为。它也不是可以直接套给 tensor、pipeline 或 context parallelism 的通用补丁。
作者报告在 4 台 ml.p5.48xlarge、32 张 NVIDIA H100 上注入软件故障,并在三实例配置重复测试。这支持方案对所注入故障有效,尚不能证明它覆盖真实硬件的全部错误模式。原文还明确承认:all-reduce 内部产生的有限数值错误,仍可能通过所有检查。
普通健康检查负责发现硬件报错,计算重放负责核对结果,两者需要配合。后续应把 rank 映射到节点,停止任务、cordon 节点,再结合 DCGM 的 ECC、NVLink 和节流指标判断是否替换。提供的原文在重放依赖说明处截断,后续操作细节没有完整给出。
恢复也有边界。suspect_window 记录上次通过检查之后的可疑区间,但间歇故障可能更早出现。checkpoint 标签只能证明当时检查通过,不能证明状态绝对干净。
能拿来做什么
- 如果团队正在维护 DDP 训练任务,可以从原文的
audit_every=10、check_every=10、ckpt_every=100、peer_offset=8配置理解接入方式,再按实际节点拓扑和耗时调整;确定性审计必须在原始 forward 前决定,不能只在重放时开启。 - 为训练数据保留可重建能力,让
rebuild_batch(step, rank)返回完全相同的 micro-batch,并让训练与重放共用compute_loss。做不到这一点,应先解决复现问题,再谈逐位比较。 - 把权重检查接到 checkpoint 保存入口,并将文件放在 Amazon FSx for Lustre 或 Amazon S3 等共享存储。节点替换会销毁根卷、附加卷和本地 NVMe,恢复方案不能依赖这些位置。
- 如果负责训练平台的全栈或 Node 运维界面,可以把告警步骤、嫌疑 rank、节点和检查记录关联展示,并核对
sagemaker:StartClusterHealthCheck、sagemaker:BatchReplaceClusterNodes权限;界面应保留「无法归因」状态,不能把一次不一致自动翻译成确定的坏节点。