大模型训练容错实战:异步 checkpoint、故障节点剔除与确定性重放
本文把「训练跑到一半挂了怎么办」当成一等工程问题来讲:一个正确可恢复的 checkpoint 到底要存什么、异步快照怎么做到不阻塞训练、慢节点(straggler)怎么被识别和踢出、以及为什么只有「确定性重放」能让你在千卡集群上复现 loss spike 而不是靠运气。读完你应该能照着文末的脚本搭出一套能扛硬件故障、能快速恢复、且训练结果可逐位复现的容错流水线。
关键词:fault tolerance、checkpoint、异步快照、数据并行冗余、straggler、确定性重放、训练恢复、epoch 一致性、硬件故障率
一、写在前面:为什么现在必须聊这件事
做过大模型预训练的人都有一个共同的噩梦:跑了 3 天、花了几十万电费,突然一个节点 HBM 报了不可纠正的 ECC 错误,整条 all-reduce 链路卡死,job 被调度器杀掉。你翻开最后一次 checkpoint,发现它写到了一半就被中断毁了,于是只能从上上一份、也就是 6 小时前的快照回退。这种情况在千卡以上规模不是小概率事件,而是排期表里必须预留的常驻项。
很多人对容错的理解还停留在「每隔几小时存一次权重」。这套做法在单机或百卡规模能凑合,但到了万卡规模会直接破产:同步 checkpoint 会让所有 rank 停下来等写盘,一次 70B 模型的全量快照可能阻塞几十秒甚至几分钟,等于主动把集群利用率往下砸;而硬件故障的频率又高到不能赌「下一次挂在下一次存盘之后」。Meta 在 Llama 3 405B 的 54 天预训练里记录了 466 次作业中断,其中 419 次是意外中断,平均大约每 3 小时一次(来源:Meta Llama 3 技术报告,被多篇 2026 年工程分析引用)。xAI Colossus 那种约 100K H100 的集群,按单设备故障率外推,有人估算约每 30 分钟就要面对一次故障。
所以这篇不聊怎么把模型切分到更多卡上(那是并行策略的事,本仓另文已覆盖),也不聊 NCCL 超时怎么调(那是通信排障的事)。本文只回答一个被很多教程绕开的问题:训练过程中如何不因为单点故障前功尽弃、如何快速恢复、以及如何让恢复后的结果和没挂过一样可复现。这三个目标分别对应三套机制:异步 checkpoint、故障节点剔除、确定性重放。下面逐层拆。
二、技术背景与现状梳理
2.1 故障不是异常,是稳态
先摆一组 2026 年公开数据,目的是把「故障率」从直觉变成可计算的量。核心规律是:同步训练作业只要有一个参与设备挂掉就得停,所以集群的 MTBF(平均无故障时间)近似随 GPU 数量线性下降。单张 H100 的 MTBF 大约在 50000 小时量级,看起来很稳;但 1024 卡集群的聚合 MTBF 掉到约 49 小时,8192 卡掉到约 6 小时。更大的集群只会更糟。
更关键的是故障构成。2026 年一篇覆盖 100K+ GPU 的容错论文指出,硬件相关故障仍占中断的约 78%,但构成变了:纯粹的 GPU 计算故障占比降到约 7.4%,HBM 内存问题成了头号原因。Llama 3 的意外中断里,GPU 相关(含 NVLink)占 30.1%,HBM3 占 17.2%,网络交换机和线缆占 8.4%。也就是说,一半以上的故障来自最底层的硬件和互联,软件 bug 反而不是主角。
这带来一个反直觉的结论:「不挂」是幻想,「挂了能快速恢复」才是工程。容错设计的投入产出比,取决于你把恢复时间从「小时级」压到「分钟级甚至秒级」能省下多少 GPU 小时。Epoch AI 对 Llama 3.1 405B 的建模显示,checkpoint 与故障恢复合计约占训练总时长的 2.1%,相当于整轮训练损失约 1% 的 MFU(模型算力利用率),前提是 checkpoint 间隔约 4 分钟、且写盘足够快。这个账在 16384 卡还扛得住,再往上放大就会吃紧。
2.2 容错的三条主线
业界把训练容错大致归成三类手段,本文聚焦后两者,第一个作为基础:
下表先把故障按可处理性分个类,后面所有机制都围绕这三类展开。
| 可预测故障(Predictable) | 视监控质量而定 | GPU 温度持续走高、可纠正 ECC 计数爬升、风扇减速 | 否,可提前排空 | 预测性排空 + 计划内换件 |
| 瞬时/可恢复故障(Transient) | 约 20%~30% | NCCL watchdog 超时、软件 bug、链路闪断 | 否,reset 即可 | 自动重启 / hang 检测 |
| 永久硬件故障(Permanent) | 约 70%+(硬件相关合计) | XID 79 GPU 掉 PCIe 总线、HBM 不可纠正 ECC、交换机死 | 是,需换卡/换件 | 故障节点剔除 + checkpoint 恢复 |
注:占比为多篇 2026 年公开资料的区间估计,不同集群差异很大,不要当成精确常数。
三、核心原理拆解
3.1 一个「正确」的 checkpoint 到底要存什么
这是最容易被写错的地方。很多人只存 model.state_dict(),恢复后发现 loss 曲线对不上,还以为是学习率没设对。生产级 checkpoint 必须同时捕获以下 5 类状态,缺一类都会让恢复后的轨迹和无中断轨迹分叉:
- 模型参数(weights):这个都知道。
- 优化器状态(optimizer state):Adam 的一阶矩 m 和二阶矩 v 加起来约等于参数规模的 2 倍。只存权重不存 m/v,恢复后相当于优化器从零开始,轨迹必然发散。
- 学习率调度器状态(lr scheduler step):cosine 衰减走到第几步决定了当前 lr,漏了会让恢复瞬间 lr 跳变。
- 每个设备的 RNG 状态:dropout、数据打乱、attention 里的随机性都依赖它。PyTorch 的 torch.default_generator、CUDA 的若干 generator、numpy、python random 各自独立,必须逐一存。
- 数据加载位置(data cursor):当前读到数据集第几个样本。漏了会导致某些 batch 被重复训练、某些被跳过,破坏 epoch 一致性。
一句话准则:恢复后的第一步,必须和无中断跑法在逐位上接得上。这也是为什么社区里那句「只存权重不够」是面试题常客——正确答案就是上面这 5 类。
3.2 异步 checkpoint 的执行模型
同步 checkpoint 的流程是:所有 rank 停算 → 各自把状态序列化 → 写分布式存储 → 全部写完才继续。问题在「全部写完才继续」,万卡下这一步的尾延迟由最慢的 rank 和存储的写带宽决定,阻塞时间可能达到分钟级。
异步 checkpoint 把这个阻塞拆开:rank 先把状态拷到 CPU 内存(或本机 NVMe 的临时区),GPU 立刻继续算;后台线程再分片把 CPU 侧的状态刷到持久存储。这样前台训练只被「拷到 CPU」这一步短暂停顿,通常 < 2 秒,I/O 和计算重叠。代价是内存占用更高(要留一份状态的副本),以及实现更复杂(不能让训练继续改了还没拷走的那份状态——通常做法是先 detach().clone() 或 copy_ 到固定缓冲区)。
PyTorch 的 torch.distributed.checkpoint(DCP)原生支持分片 checkpoint:每个 rank 只写自己那一份 shard,不再有「单点全量快照」,恢复时还能按新拓扑重新映射 shard(比如 512 卡跑挂了,用 384 卡接着跑,DCP 负责把 shard 重新分配)。这是千卡以上训练的事实标准。
TierCheck(2026-05 的 arXiv 论文,覆盖到 400 亿参数)把这件事再推一层:本地/对等内存里放轻量差分 checkpoint 做快速局部恢复,重型 base checkpoint 异步迁移到远端持久存储,且保证跨层全局一致性而不阻塞训练,端到端 checkpoint 时间压到 10 秒以内,支持高频 checkpoint。
3.3 故障节点剔除:从检测到踢出
straggler(慢节点)和彻底挂掉的节点是两件事,但处理链路相似:
- 检测:靠心跳(heartbeat)+ 步级进度。每个 rank 定期上报「我到了第几 step、这步花了多久」。如果某个 rank 的 step 明显落后其他 rank 一个阈值(比如落后 3 个 step 或单步耗时超过中位数 3 倍),且 NCCL watchdog 开始报超时,基本可以判定它成了瓶颈或要挂。
- 隔离:调度器(Kubernetes kubelet liveness probe,每 10 秒一次)发现节点失联后,30~60 秒内驱逐 pod,杀掉训练进程。
- 替补:新 pod 在健康节点起来(30~120 秒),拉最新 checkpoint,重新做 rendezvous(集合通信的握手),从断点续训。
真正麻烦的是「半死不活」的节点:GPU 没掉总线,但时钟被 thermal throttle 砍到很低,或者某条 NVLink lane 在闪。因为 all-reduce 要等最慢的 rank,整个集群都被它拖死。这种 silent straggler 不会触发 XID 错误,只能靠「步耗时监控 + 自动排空」抓出来。2026 年有运维报告称,系统化的 DCGM 监控把故障相关停机时间砍掉了最高约 70%。
弹性训练(elastic training)让这套剔除更进一步:TorchElastic(PyTorch 1.9 起核心模块)支持 world-size 变化——坏几张卡,剩下的卡重新 rendezvous 接着跑,不要求备件齐活。Ray Train 用 num_workers=(min, max) 元组表达弹性区间,max_failures 控制重试次数。前提是你的训练代码能容忍 world-size 变化(数据分片要按当前 worker 数重新划分)。
数据并行冗余(data-parallel redundancy)是另一路:把优化器状态或整份模型状态冗余存放在对等内存或备卡上,单点挂掉时直接从副本重建内存状态,恢复从「分钟级拉盘」降到「秒级内存复制」。DeadPool 的 in-memory checkpoint + 运行时 communicator 重建就是这一思路,永久节点故障恢复 < 40 秒。
3.4 确定性重放:让 bug 能被复现
非确定性训练最大的坑不是结果不同,而是问题会消失。一个 loss spike 在非确定性跑法里可能第 10000 步出现,你重启 job,因为随机数流变了,spike 跑到第 12000 步甚至不出现,你根本没法定位。NVIDIA Megatron-Core 团队在 2026-09 的讨论里把这件事讲透了:万亿参数预训练要可调试,必须做 bitwise 级确定性。
bitwise 确定性要求两条保证:
- 独立可复现(independent reproducibility):两次从同一初始状态启动的跑法,必须步步逐位相同。
- checkpoint 逐位续跑(bitwise checkpoint resume):存盘再恢复,轨迹必须和无中断跑法完全一致。
要做到这两点,光设 torch.manual_seed 不够。CUDA 卷积算子(cuDNN)有自己内部的随机状态,cublas 的 workspace 配置、非确定性算子、use_deterministic_algorithms(True) 都要开;浮点非结合律(不同设备上的 reduce 顺序不同)也会让梯度低位 bit 漂。更隐蔽的是异步集合通信:分布式 reduce 的归约顺序跨 run 不一致,轨迹就漂。
确定性重放的真正价值是给了一个「强诊断信号」:一旦软件栈确定,两次重复跑的任何不一致都直接指向沉默的 checkpoint 损坏、异常的软件行为或可疑硬件。它既是一个回归测试,也是一套机群健康探针。
四、环境与依赖准备
下面给一套能在 2026 年主流环境跑起来的依赖与硬件门槛。版本号是社区实测相对稳的组合,不要混用大版本跨度的 CUDA 和 PyTorch。
| Python | 3.11 或 3.12 | 3.13 部分算子支持不全,谨慎 |
| PyTorch | 2.4+(建议 2.5/2.6) | DCP 在 2.x 已较成熟 |
| CUDA | 12.4 或 12.6 | 与驱动、Transformer Engine 对齐 |
| Transformer Engine | 1.10+ | 混合精度确定性路径依赖它 |
| NVIDIA Resiliency Extension (NVRx) | 2026 年实验性 | 自动重启 / hang 检测,May 2026 仍标 experimental |
| torch/distributed/checkpoint | 随 PyTorch 自带 | 分片 checkpoint 核心 |
| 本地存储 | 节点内 NVMe,≥ 模型状态 2 倍 | 异步 checkpoint 的临时落点 |
| 远端存储 | 对象存储(S3/GCS)带版本控制 | 跨节点故障的最终防线 |
| 监控 | DCGM + Prometheus + Alertmanager | 抓 ECC、温度、NVLink 带宽 |
硬件门槛方面,单机多卡就能验证本文脚本;要压真实容错,建议至少 2 节点 8 卡起,否则弹性 rendezvous 和 shard 重映射看不出效果。节点内 NVMe 的写带宽直接决定异步 checkpoint 的「前台停顿」时长,建议实测顺序写 > 2 GB/s。
依赖安装(以 pip 为例):
# 建议放在容器镜像里 pin 死版本,不要运行时随机装
pip install torch==2.6.0 –index-url https://download.pytorch.org/whl/cu124
pip install transformer-engine notebooks # transformer-engine 按需
pip install ray[train] # 弹性训练可选
pip install boto3 # 异步复制到对象存储
一个容易踩的坑:确定性格子要先验证你用的算子支不支持 use_deterministic_algorithms。不支持的算子开了会直接抛异常,需要 warn_only=True 先扫一遍。
五、完整实操
下面 5 段脚本都可直接运行(带 if __name__ == "__main__" 或 shebang)。前 3 段是本地即可验证的小工具,后 2 段是接入训练循环的核心逻辑。
5.1 确定性种子与算子锁定(本地可跑)
这段是确定性重放的地基。注意 CUBLAS_WORKSPACE_CONFIG 和 use_deterministic_algorithms 缺一不可,且确定性模式会牺牲约 10%~15% 性能——这是为可复现性付的税。
import os
import random
import numpy as np
import torch
def set_deterministic_seed(seed: int = 42) –> None:
"""锁死所有随机源,让同机同数据两次跑结果逐位一致。"""
# 1) 框架级种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# 2) CUDA 算子内部随机状态(cuDNN 有独立 RNG)
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# 3) 强制确定性算子;不支持的先 warn 不报错,方便排查
torch.use_deterministic_algorithms(True, warn_only=True)
if __name__ == "__main__":
set_deterministic_seed(42)
# 用一个随机初始化小矩阵验证:两次独立 forward 必须逐位相同
x = torch.randn(1024, 1024, device="cuda" if torch.cuda.is_available() else "cpu")
w = torch.randn(1024, 1024, device=x.device)
out = (x @ w).sum()
print(f"deterministic check value = {out.item():.8f}")
# 再跑一次,若完全一致说明种子链路生效
5.2 完整可恢复 checkpoint 的保存(含 5 类状态)
这段用 torch.distributed.checkpoint 做分片保存,并显式把 RNG、data cursor、lr scheduler 一起存。异步部分用后台线程把 CPU 副本刷到对象存储,前台只被 copy_ 到固定缓冲区短暂阻塞。
import os
import threading
import torch
import torch.distributed as dist
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.stateful import Stateful
from torch.utils.data import DataLoader
class TrainState(Stateful):
"""把训练要恢复的全部状态打包成一个 Stateful,DCP 负责分片。"""
def __init__(self, model, optimizer, lr_scheduler, data_loader):
self.model = model
self.optimizer = optimizer
self.lr_scheduler = lr_scheduler
self.data_loader = data_loader
def state_dict(self):
return {
"model": self.model,
"optimizer": self.optimizer,
"lr_scheduler": self.lr_scheduler,
"data_loader": self.data_loader,
}
def load_state_dict(self, state_dict):
self.model.load_state_dict(state_dict["model"])
self.optimizer.load_state_dict(state_dict["optimizer"])
self.lr_scheduler.load_state_dict(state_dict["lr_scheduler"])
self.data_loader.load_state_dict(state_dict["data_loader"])
def async_flush_to_object_store(local_path: str, bucket: str, key: str):
"""后台线程:把本地 NVMe 上的 checkpoint 异步复制到对象存储。"""
def _copy():
import boto3
client = boto3.client("s3")
client.upload_file(local_path, bucket, key)
t = threading.Thread(target=_copy, daemon=True)
t.start()
return t
def save_checkpoint(model, optimizer, lr_scheduler, data_loader,
step: int, local_dir: str, bucket: str):
"""保存一个含全部状态的分布式分片 checkpoint(异步刷远端)。"""
os.makedirs(local_dir, exist_ok=True)
local_path = os.path.join(local_dir, f"ckpt_step_{step}")
state = TrainState(model, optimizer, lr_scheduler, data_loader)
# DCP 分片写:每个 rank 只写自己那份,互不阻塞
dcp.save(state.state_dict(), checkpoint_id=local_path)
# 前台已可继续;后台异步复制到对象存储做跨节点容灾
async_flush_to_object_store(local_path, bucket, f"ckpts/ckpt_step_{step}")
print(f"[rank {dist.get_rank()}] checkpoint saved at step {step}")
if __name__ == "__main__":
# 本地单卡 demo:用普通 state_dict 验证 5 类状态都能落盘
model = torch.nn.Linear(64, 64)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
sd = {
"model": model.state_dict(),
"optimizer": opt.state_dict(),
"rng": torch.get_rng_state(),
"step": 0,
}
torch.save(sd, "/tmp/demo_ckpt.pt")
print("demo checkpoint written with weights + optimizer + rng + step")
5.3 straggler 检测与故障节点剔除(本地可跑)
这段不依赖 GPU,用模拟的 rank 步耗时演示「如何判定慢节点并踢出」。生产里把 report_step_time 接到训练循环、把 evict_rank 接到调度器即可。
import time
import statistics
class StragglerMonitor:
def __init__(self, step_lag_threshold: int = 3, slow_ratio: float = 3.0):
self.step_lag_threshold = step_lag_threshold
self.slow_ratio = slow_ratio
self.last_step = {} # rank -> 最近到达的 step
self.step_cost = {} # rank -> 最近单步耗时(秒)
def report_step_time(self, rank: int, step: int, cost_s: float):
self.last_step[rank] = step
self.step_cost[rank] = cost_s
def detect_stragglers(self):
if not self.last_step:
return []
max_step = max(self.last_step.values())
median_cost = statistics.median(self.step_cost.values())
bad = []
for rank, step in self.last_step.items():
lag = max_step – step
cost = self.step_cost[rank]
if lag >= self.step_lag_threshold or (cost > median_cost * self.slow_ratio):
bad.append(rank)
return bad
def evict_rank(self, rank: int):
# 生产里这一步改为:通知调度器排空该节点 + 触发 elastic rendezvous
print(f"evicting rank {rank}: drain node and re-rendezvous survivors")
if __name__ == "__main__":
mon = StragglerMonitor()
base = time.time()
for rank in range(8):
mon.report_step_time(rank, 1000, 1.0)
# rank 5 落后且单步慢 -> 应被判定为 straggler
mon.report_step_time(5, 994, 4.2)
stragglers = mon.detect_stragglers()
print("detected stragglers:", stragglers)
for r in stragglers:
mon.evict_rank(r)
5.4 弹性训练启动脚本(bash)
用 torchrun 的弹性模式,允许 world-size 在 [MIN, MAX] 间浮动,节点挂掉后幸存者重新 rendezvous 接着跑。注意 rdzv_backend 用 etcd 或 c10d,这里是 c10d 简化版。
#!/usr/bin/env bash
# 弹性训练启动:坏几张卡不终止整作业,幸存者重新 rendezvous
set -euo pipefail
MIN_SIZE=4
MAX_SIZE=8
ETCD_ADDR="${ETCD_ADDR:-localhost:2379}"
torchrun \\
–nnodes=1 \\
–nproc-per-node=8 \\
–rdzv-backend=etcd \\
–rdzv-endpoint="${ETCD_ADDR}" \\
–rdzv-id=train-$(date +%s) \\
–max-restarts=3 \\
–elastic-training=1 \\
–min-nodes=1 \\
train.py
5.5 checkpoint 完整性校验(本地可跑)
恢复前必须校验,否则「写到一半的 checkpoint」会让你从坏快照续训、悄无声息地污染整轮结果。这段对 checkpoint 目录算哈希并比对元数据里的 step。
import hashlib
import json
import os
import sys
def sha256_of_dir(path: str) –> str:
h = hashlib.sha256()
for root, _, files in os.walk(path):
for f in sorted(files):
fp = os.path.join(root, f)
with open(fp, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def verify_checkpoint(ckpt_dir: str, expected_step: int) –> bool:
meta = os.path.join(ckpt_dir, "metadata.json")
if not os.path.exists(meta):
print("FAIL: metadata.json missing")
return False
with open(meta) as fh:
info = json.load(fh)
if info.get("step") != expected_step:
print(f"FAIL: step mismatch want={expected_step} got={info.get('step')}")
return False
digest = sha256_of_dir(ckpt_dir)
if info.get("sha256") != digest:
print("FAIL: checksum mismatch, checkpoint may be corrupted mid-write")
return False
print(f"OK: checkpoint step={expected_step} sha256={digest[:12]}…")
return True
if __name__ == "__main__":
if len(sys.argv) != 3:
print("usage: python checkpoint_integrity.py <ckpt_dir> <expected_step>")
sys.exit(1)
ok = verify_checkpoint(sys.argv[1], int(sys.argv[2]))
sys.exit(0 if ok else 1)
5.6 恢复演练:定期「演习」而非「等出事才发现」
一个你从没恢复过的 checkpoint 不是 checkpoint,是一个假设。下面这段 bash 把「杀掉 job → 拉最新已校验 checkpoint → 续训」做成一个可周期性跑的演练,用来验证恢复链路真的通。
#!/usr/bin/env bash
# 恢复演练:验证 checkpoint 恢复链路端到端可用,而不是等真故障时才发现坏
set -euo pipefail
CKPT_DIR="${CKPT_DIR:-/local_nvme/ckpt}"
EXPECTED_STEP="${EXPECTED_STEP:-0}" # 由训练侧写进 metadata.json
echo "[drill] verifying latest checkpoint integrity …"
python checkpoint_integrity.py "${CKPT_DIR}/latest" "${EXPECTED_STEP}"
echo "[drill] simulating node failure: killing training job …"
pkill -f "torchrun train.py" || true
sleep 5
echo "[drill] resuming from latest verified checkpoint …"
torchrun –nnodes=1 –nproc-per-node=8 \\
–rdzv-backend=c10d –rdzv-endpoint=localhost:29500 \\
train.py –resume "${CKPT_DIR}/latest"
echo "[drill] OK: recovery path exercised, no manual surprise"
5.7 checkpoint 元数据示例(json)
TrainState 落盘时建议同时写一份 metadata.json,供 5.5 节的完整性校验和演练脚本读取。结构如下:
{
"step": 250,
"epoch": 1,
"sha256": "a1b2c3d4e5f6…",
"global_batch_size": 1024,
"framework": "pytorch-2.6",
"shards": ["rank0.pt", "rank1.pt", "rank2.pt", "rank3.pt"],
"written_at": "2026-10-08T14:32:00Z"
}
5.8 把上述机制接进训练循环(yaml 配置)
一份把 checkpoint 间隔、异步、弹性、确定性开关都暴露出来的训练配置,方便不同规模切换策略。
training:
model: llama–7b
global_batch_size: 1024
max_steps: 100000
fault_tolerance:
deterministic: true # 开了牺牲 ~10-15% 性能换逐位可复现
checkpoint:
interval_steps: 250 # 约 4 分钟一次,对齐 Epoch AI 推荐区间
async: true # 前台只拷到 CPU,后台刷盘
local_dir: /local_nvme/ckpt
remote_bucket: s3://my–ckpts
keep_last: 3 # 至少留一份,防写到一半损坏
elastic:
min_workers: 4
max_workers: 8
max_failures: 3
straggler:
step_lag_threshold: 3
slow_ratio: 3.0
heartbeat_sec: 10
六、结果对比与数据分析
6.1 checkpoint 策略对比
不同 checkpoint 策略的取舍非常直接:阻塞时间、恢复耗时、存储成本三者在打架。下面这张表把常见方案摆一起。
| 同步全量 checkpoint | 是,全 rank 等 | 数十秒~数分钟 | 取决于快照大小 | 无 | < 百卡,调试用 |
| 异步分片 checkpoint(DCP) | 否,仅拷 CPU 短暂停 | < 2 秒 | 拉盘 5~20 分钟 | +1 份状态副本 | 千卡~万卡 |
| 分层 checkpoint(TierCheck 式) | 否,差分+异步 | < 10 秒 | 局部秒级/远端分钟级 | +差分+远端 | 万卡~十万卡 |
| 内存冗余热替换(DeadPool 式) | 否,零开销 | 0 | < 40 秒 | +冗余副本显存/内存 | 超大规模固定拓扑 |
6.2 恢复耗时估算
一次故障后的端到端 RTO(恢复时间目标)由多段串行组成。下面按「512 卡、50 GB checkpoint」估算,数字来自 2026 年公开工程资料的区间,具体看你的网络和存储带宽。
| 故障检测 | 心跳 / NCCL watchdog 超时 | 10~60 秒 | DCGM 主动健康检查 |
| 资源调度 | 新 pod 调度到健康节点 | 30~120 秒 | 预留备件池 |
| checkpoint 下载 | 从对象存储拉 50 GB | 5~20 分钟 | 本地 NVMe 缓存 / 内存副本 |
| 梯度重同步 | 与其他 rank 对齐 buffer | 5~10 分钟 | 拓扑感知 shard 映射 |
| 重算损失 | 上次 checkpoint 到故障点的步数 | 视间隔而定 | 缩短 checkpoint 间隔 |
| 合计 RTO | — | 约 45~90 分钟 | 冗余方案可压到 < 1 分钟 |
关键结论:RTO 的主要开销在「拉盘 + 重算」,不在「检测」。所以异步 checkpoint 的价值不是让检测变快,而是让你敢把间隔缩到几分钟——间隔短,重算损失就小。Epoch AI 估算 Llama 3 上最优间隔约 4 分钟,整体恢复开销控制在 2.1% 训练时长内。
6.3 把故障率变成可算的账
假设你有 N 张 GPU,单卡 MTBF 为 T 小时,则集群聚合 MTBF ≈ T / N。若 checkpoint 间隔为 I 分钟,每次故障平均损失 ≈ I 分钟 + RTO。于是「因故障浪费的训练占比」≈ (I + RTO) / (集群 MTBF 换算成的分钟数)。举例:N=16384、T=50000 小时 → 集群 MTBF ≈ 3 小时 = 180 分钟;若 I=4 分钟、RTO=60 分钟,浪费占比 ≈ 64/180 ≈ 35%?这个估算偏悲观,因为它假设每次故障都走到完整 RTO——实际上异步 + 本地缓存能把大多数瞬时故障的 RTO 压到分钟级。真实公开数字(Llama 3)是约 2.1%,差距来自「高频 checkpoint + 快写盘 + 大部分故障可瞬时恢复」。这张账表的用途是让你知道:瓶颈永远在 I 和 RTO 的乘积相对集群 MTBF 的比例,优化任一项都直接省 GPU 小时。
七、踩坑记录与排查手册
下面 6 条都来自真实工程场景(部分为社区公开复盘 + 本人在小集群上的复现)。每条固定四段:现象 / 定位 / 原因 / 解决。
7.1 只存权重,恢复后 loss 曲线对不上
- 现象:从 checkpoint 恢复后,loss 比中断前高了一截,且之后收敛轨迹和「没挂过」的参考跑法完全分叉,像换了个随机种子。
- 定位:对比恢复前后的 optimizer.state_dict(),发现 m/v 矩全丢了;再查 lr scheduler 的 step 也是初始值。
- 原因:只 torch.save(model.state_dict()),没存 Adam 的 m/v 和调度器状态,优化器等于从零开始,轨迹必然发散。
- 解决:改用 5.1/5.2 节的 TrainState 打包法,把 optimizer、lr_scheduler、RNG、data_loader 一起进 DCP。恢复后第一步 loss 必须和中断前最后一步逐位接上,否则就是状态漏存。
7.2 异步 checkpoint 恢复了「写到一半」的脏数据
- 现象:恢复时加载报 EOFError 或加载成功但 loss 直接炸成 NaN,且只有部分 rank 报坏。
- 定位:用 5.5 节的 verify_checkpoint 校验,发现 sha256 不匹配;查写盘日志,故障恰好发生在后台刷盘线程把本地 shard 复制到一半时。
- 原因:异步复制和「保留至少一份旧 checkpoint」没配合好,新快照覆盖/破坏了唯一一份可用副本。
- 解决:写盘用「写临时目录 → 校验通过 → 原子 rename → 再删旧版」;remote_bucket 开版本控制,本地 keep_last: 3。恢复永远从「已校验通过的版本」拉,绝不从正在写的目录拉。
7.3 非确定性导致 loss spike 重启后消失,无法复现
- 现象:训练第 10000 步附近出现 loss spike,重启 job 想复现调试,spike 跑到第 13000 步甚至不出现,没法定位。
- 定位:确认代码只设了 torch.manual_seed,没管 cuDNN 内部 RNG 和 cublas workspace;分布式 reduce 顺序跨 run 不一致。
- 原因:浮点非结合律 + 异步集合通信的归约顺序不确定,让两次跑的轨迹在低位 bit 上漂,spike 出现位置随之漂。
- 解决:开 torch.backends.cudnn.deterministic=True、CUBLAS_WORKSPACE_CONFIG=:4096:8、use_deterministic_algorithms(True, warn_only=True),并固定 NCCL 算法(如 NCCL_ALGO=Ring 在调试阶段)。确定性模式牺牲约 10%~15% 性能,但换来 spike 在同一步复现,才能做受控排查。
7.4 半死不活的 straggler 拖垮整集群却不报错
- 现象:集群整体吞吐掉到预期的 60%,但没有任何 XID 错误、没有节点掉线,DCGM 里某卡温度正常只是时钟被 throttle。
- 定位:在监控里画各 rank 单步耗时,发现 rank 5 的 step cost 是中位数的 4 倍;NCCL watchdog 开始偶发超时。
- 原因:该 GPU 的冷却出问题(CDU 流量不足),thermal throttle 砍时钟,但它没掉总线,所以不会被「节点失联」检测抓到。all-reduce 等最慢 rank,整集群被拖死。
- 解决:上步耗时监控(5.3 节 StragglerMonitor),slow_ratio > 3.0 即触发排空;同时把「恒定负载下温度曲线走高」关联成冷却问题告警,而不是 GPU 问题。
7.5 弹性恢复后数据被重复训练或跳过
- 现象:world-size 从 8 变 6 恢复后,loss 正常但评测指标异常,怀疑部分数据没训到或训了两遍。
- 定位:检查 DataLoader 的 sampler,发现用的是固定 rank/world_size 分片,world-size 变了之后分片边界错位,首尾样本重叠或缺失。
- 原因:弹性训练改变了 worker 数,但数据分片没按「当前 world-size + 全局样本游标」重新划分,只按 rank 取模。
- 解决:DataLoader 状态必须进 checkpoint(5.2 节的 data_loader 字段),恢复时从 data cursor 续读;分片用 DistributedSampler 并显式传 num_replicas=当前 world_size。弹性变更后重新创建 sampler,而不是复用旧分片。
7.6 确定性开了但性能掉 30%,业务方不接受
- 现象:为可复现开满确定性开关,吞吐从 100 tokens/s/GPU 掉到 70,训练排期直接爆。
- 定位:profile 发现 use_deterministic_algorithms(True) 强制了若干慢算子(如特定 attention kernel 退化为确定性实现),且 cublas workspace 锁死失去自动调优。
- 原因:确定性要求归约顺序固定,部分高效非确定性 kernel 被禁用;cudnn.benchmark=False 也关掉了卷积算法自动选择。
- 解决:不要在全部训练开满。折中:只在「需要调试 / 需要审计 / 需要 A/B 对比」的跑法开确定性;常规探索阶段用统计可复现(固定种子 + checkpoint 里锁关键 RNG)即可。NVIDIA Megatron-Core 的做法是做成端到端可开关的能力,平时关、出问题开,而不是一刀切。
八、优化方案与进阶打法
8.1 分层 checkpoint:把存储位置和故障类型对齐
TierCheck 的核心洞察是「故障是异质的,存储不该是单层的」。本地 NVMe 扛节点内软件崩溃(秒级恢复),对等内存扛单卡故障(内存副本秒级重建),远端对象存储扛节点/机架级故障(分钟级拉盘)。做法是:轻量差分 checkpoint 放本地和对等内存,重型 base checkpoint 异步迁远端,且跨层全局一致性不阻塞训练。你在 5.2 节的基础上加一层「本地差分 + 远端 base」即可:每 K 次存 base,中间步只存相对 base 的差分,写盘量骤降,checkpoint 间隔能压到分钟甚至秒级。
8.2 内存冗余热替换:把恢复从「拉盘」变成「拷贝」
DeadPool 思路是在对等内存/备卡上放优化器状态的副本,单点永久故障时用 communicator 重建协议把备卡顶上,不终止整作业。代价是显存/内存多一份副本——65B 模型优化器状态冗余一份不是小数。是否值得取决于你的故障频率:如果集群 MTBF 短到分钟级(十万卡规模),省下的 RTO 远超冗余成本;百卡规模则不划算。
8.3 每步指纹:把确定性变成可观测指标
不要只靠「打印 loss 相同」来判断确定性——两个跑法可能打印出相同的四舍五入 loss,但梯度低位 bit 已经漂了。NVIDIA 的做法是每步对数值状态(loss、load balancing loss、参数哈希)打指纹,两次跑指纹一致才算 bitwise 相同。你可以每 N 步算一次参数 tensor 的 xxhash 或低精度摘要存进监控,作为「训练轨迹健康」的 SLI。一旦两次重复跑指纹不一致,立刻定位到是哪一层、哪一步开始漂——这比事后看 loss 曲线灵敏得多。
8.4 预测性排空:从「坏了再踢」到「要坏先换」
可预测故障(温度走高、ECC 可纠正计数爬升)是免费的早期信号。在监控里给这类指标设趋势阈值,达到就主动排空节点、安排计划内换件,把「意外中断」变成「零计算的维护」。2026 年有运维数据称系统监控把故障相关停机砍了最高约 70%,性价比远高于任何算法层优化。
九、适用场景与选型建议
9.1 什么情况该上全套容错
- 千卡以上同步预训练:故障频率高到不能赌,异步 checkpoint + 弹性 + straggler 监控是标配。
- 需要审计 / 合规的训练(金融、医疗):必须确定性重放 + checkpoint 哈希留痕,否则无法证明「模型是怎么训出来的」。
- 长周期、高成本跑法:一次中断损失以 GPU 小时计,容错投入回报极高。
- 异构硬件混跑:不同卡浮点路径不同,更需要确定性开关锁死轨迹。
9.2 什么情况不该用(重点)
- 百卡以下、跑几小时的实验:同步 checkpoint 足够,上异步 + 分层 + 弹性是过度工程,反而增加出 bug 面。
- 纯探索阶段的快速迭代:确定性模式牺牲 10%~15% 性能,会拖慢试错速度,先用统计可复现即可。
- 推理服务(非训练):容错形态是「优雅排空降级」而不是 checkpoint/restart,套用本文机制是错配。
- 对延迟极敏感、显存已经吃满的训练:内存冗余热替换要再占一份状态副本,显存爆了就别上。
- 数据本身不可复现的管线(实时流式数据、带墙钟时间戳的特征):确定性重放无从谈起,先解决数据快照再谈容错。
- regulated 域直接 resume 不校验:金融/医疗域恢复前必须人工校验 checkpoint 哈希 + 看前 10 步梯度是否合理,自动 resume 不合规。
9.3 选型速查表
| < 百卡 / 短实验 | 同步 checkpoint,间隔放宽 | 分层、内存冗余、弹性 |
| 百卡~千卡 | 异步分片 checkpoint + straggler 监控 | 内存冗余热替换(显存不值) |
| 千卡~万卡 | 异步 + 弹性 + 确定性(调试期开) | 纯同步 checkpoint |
| 万卡~十万卡 | 分层 checkpoint + 内存冗余 + 每步指纹 | 任何单点全量快照 |
| 合规/审计域 | 确定性重放 + 哈希留痕 + 人工校验 | 自动 resume 不校验 |
十、总结与展望
训练容错不是「存个盘」那么简单,它是一套把「硬件必然故障」当成输入条件的系统工程:异步 checkpoint 解决「存盘不阻塞训练」,故障节点剔除解决「坏卡不拖死整集群」,确定性重放解决「恢复后结果和没挂过一样、且 bug 能被复现」。三件事各自独立,又共享同一个前提——一个正确的 checkpoint 必须包含权重、优化器状态、lr 调度、RNG、数据位置这 5 类状态,少一类都会让轨迹分叉。
展望上看,两个方向在收敛:一是把容错从「应用层自己搞」下沉到框架和机群(NVIDIA Resiliency Extension、Mission Control 自治恢复引擎把恢复提速宣称 10 倍),二是把恢复从「拉盘重算」推向「内存副本热替换」(DeadPool 的 < 40 秒恢复)。但无论上层怎么演进,工程师手里的三个杠杆不会变:缩短 checkpoint 间隔、压低 RTO、锁死确定性。把这三件事的账算清楚,比追任何新框架都实在。
最后一句实话:容错做得好,训练挂了没人知道;做得差,挂一次就是几天白跑。这份投入在千卡以上规模是必选项,不是加分项。
参考素材与延伸阅读
- Scaling Bitwise-Deterministic Pretraining for a Trillion-Parameter Nemotron Model with NVIDIA Megatron Core — NVIDIA Megatron-LM Discussion,2026-09-18
- AI Infra 系列:一万张 GPU 怎么排班,调度器才是训练场的隐形老板 — CSDN,2026-09
- Common Approaches to Fault Tolerant AI Training & Selection Framework — Clockwork,2026-09
- The Facility Is in Your Training Loop — The Data Scientist,2026-09
- GPU Failure Rates in 2026: What UK Buyers Must Plan For — Servnet UK,2026-08
- GPU Fault Tolerance in Distributed Training: A Technical Guide — Lyceum Technology,2026-09
- Fault-tolerant checkpointing for thousand-GPU pre-training — Tezvyn,2026-09
- What is Deterministic Training? (2026 Guide) — DataOpsSchool,2026-09


