想象一支几百辆货车组成的车队,要从北京一路开到乌鲁木齐。每辆车都装着几十吨货物,任何一辆在半路抛锚,整个车队都得停下来等。更麻烦的是,如果出发前没有记账,就得把所有车开回起点重新出发。大模型分布式训练就是这样的车队:几百张 GPU 并行跑同一个模型,一跑就是好几天。硬件故障、内存溢出、网络抖动随时可能打断训练。断点续训就是给车队准备“沿途货单”——每隔一段时间,把每辆车的载货清单和当前位置记下来。万一抛锚,只需退回到最近一张货单,重新装车继续出发,而不是从头再来。这篇文章就用“长途车队”这个比喻,把检查点保存、故障检测、恢复流程、状态一致性这些概念讲清楚。
掌握断点续训,是大模型训练工程师的必备生存技能。它决定了千卡集群上跑一周的任务,是“一夜回到解放前”,还是“晚点两小时到达”。
可视化演示
展示K-Means算法迭代过程:初始化质心、分配数据点到最近质心、更新质心位置,直至收敛。
本节配有交互动效(进入视口后自动加载,约 3KB)。
K-Means初始化 随机初始化3个质心位置 点击分配按钮将每个点分配到最近质心 argmin_c ||x_i - mu_c||^2 K-Means通过交替执行分配和更新步骤收敛
故障必然性
长途车队最坏的情况不是堵车,而是出发后没人知道哪辆车会在哪个山口抛锚。分布式训练动辄上百张卡,每张卡像一辆货车,持续满载运行几天几夜。就算单卡的故障率很低,一整个车队跑下来,总会出现一次故障——这不是运气问题,是概率问题。
在工程上,我们常把单位时间内单卡发生故障的比率叫做失效率 λ(lambda)。常见故障包括硬件掉卡、显存溢出(OOM)、网络抖动导致的通信超时。若一张卡失效率是 λ,N 张卡同时在线时,集群整体失效率大约是 N 倍。把时间拉长,任务能活下来的概率按指数衰减。
回到车队比喻:车子越多、路程越长,路上“所有人都平安”的愿望就越不现实。所以聪明的车队不会祈祷不抛锚,而是提前准备好货单和备用方案。断点续训,就是为这种必然的意外兜底。
- 单卡失效率 λ一张卡在单位时间(比如一天)内出故障的概率估计值,是可靠性计算的基本单位。
- 集群总失效率 N × λN 张卡同时在跑,整体的故障强度近似放大 N 倍。
- 指数存活概率时间 t 越长,任务完全不故障的概率 e 的负数次幂越低。
检查点组成
车队出发前要记的“货单”,不只是货物清单,还包括每辆车的位置、油量、司机排班。同样,训练检查点不能只保存模型权重。权重只是模型当前“会了什么”,但训练还依赖很多看不见的状态:优化器里的动量、学习率、数据读到哪、随机数种子等。
一个完整检查点通常包含四类内容:模型权重、优化器状态、数据游标、随机数状态。其中权重和优化器状态是体积大头。对 Adam 优化器来说,除了参数本身,还要额外保存一阶动量和二阶动量,因此存储量大约是参数量的三倍左右。数据游标和随机数状态很小,但决定恢复后数据会不会重读或漏读。
货单记得越全,重新装车后就越能无缝出发。只记货物不记车厢安排,出发后就会手忙脚乱。同理,只存权重不存优化器状态,恢复后的训练精度和收敛速度都可能悄悄变差。
- 模型参数量 P模型里需要学习的权重数量,比如 70B 模型 P 约为 700 亿。
- 优化器状态数 OAdam 等优化器为每个参数额外保存的动量变量数,常为 2P。
- 单值字节数 4FP32 浮点数占 4 字节,决定文件总体积。
保存策略
车队不能每开一公里就停下来记一次货单,那样太耽误行程;也不能一路不停,等抛锚时连刚才走的路段都找不到。保存检查点也有同样矛盾:保存太频繁,训练被 IO 拖慢;保存太少,故障后回退的步数太多。
实践中常用三种做法:固定周期保存,比如每隔 1000 步或 15 分钟存一次;异步保存,把检查点写入放到后台线程,避免阻塞训练;临时检查点,用“先写临时文件、再原子重命名”的方式防止写入一半的坏文件。选择保存间隔时,需要同时考虑保存耗时和故障平均间隔。
车队的智慧是“按里程设驿站,既不让大家疲劳,也不让路白跑”。保存间隔要跟着故障频率和存储速度动态调整,才能让“安全”和“速度”同时成立。
- 保存耗时 T_save把一张完整检查点写入存储的耗时,受文件大小和 IO 带宽影响。
- 保存间隔 T_interval两次检查点之间间隔的训练时间。
- 平均故障间隔 M_MTBF集群平均多久故障一次,简称 MTBF。
- 期望丢失时间故障若随机发生,平均落在最近检查点与下一次保存之间的一半位置。
故障检测
车队最怕的不是有人上报“我抛锚了”,而是某个司机突然失联,所有人还傻傻等他。分布式训练也一样,很多故障不伴随着明显的报错日志,进程可能只是“卡住不说话”。工程上靠心跳和超时来识别失联。
所谓心跳,就是每个训练进程定期向调度器报告“我还活着”。如果超过设定时间没有收到心跳,调度器就判定节点失联;再结合训练框架里的通信超时看门狗,比如 NCCL 的 watchdog,就能把“静默卡死”也揪出来。从失联发生到被确认,通常要经过一个判定过程。
车队里每个司机都要定期用对讲机喊一声“我在”。一旦长时间听不到某辆车的声音,车队就要派人回头查看。检测故障的速度,决定了救援能否及时开展,也决定了丢失的路程有多远。
- 心跳周期 T_heartbeat进程每隔多久上报一次状态,周期越短反应越快。
- 超时阈值 T_timeout调度器等多久没听到心跳才认为可疑。
- 确认时间 T_quorum多个监控节点协商确认故障需要的时间,避免单个心跳丢失造成误判。
恢复流程
一旦确认某辆车抛锚,车队不会退回起点,而是找到最近一个货单,按货单重新装车。断点恢复也分三步:先把检查点文件读回内存;然后重建分布式训练环境,包括进程组、RANK 编号、通信端口;最后校验数据状态与全局步数,确认无误后再继续训练。
恢复不是“启动一个全新任务”,而是“回到历史某个确定时刻”。大模型检查点动辄几十 GB 甚至几百 GB,加载耗时不可忽略;并行上下文重建也要重新初始化网络连接。因此恢复时间可以拆成三部分。
货单重新装好后,还要请几个老司机核对一下货物数量和出发位置。只有确认无误,车队才敢重新上路。恢复流程里的校验,就是这种“出发前最后清点”。
- 加载检查点 T_load从共享存储读入权重和优化器状态,受磁盘带宽限制。
- 重建并行态 T_rebuild重新建立 NCCL 通信组、分配进程编号等。
- 校验一致性 T_verify核对步数、数据游标、文件哈希,确保恢复点没有损坏。
状态一致性
车队重新出发时,账目必须对得上:这段路货物不能重复运,也不能漏运。训练恢复最大的隐蔽风险,就是权重虽然读回来了,但步数、学习率、数据游标没有对齐,导致数据重复或漏掉一部分。
比如全局步数必须从检查点记录的 step 继续;学习率调度器要根据当前 step 重新计算学习率,而不是从头开始衰减;数据加载器则要回到检查点保存的 batch 偏移,同时恢复随机数生成器状态,才能保证后面读到的样本顺序和原计划一致。学习率调度常用线性下降:
货单上的每一项都要对齐,司机们才知道现在走到哪一段路、下一站该送什么货。状态一致性的本质,就是让“故障前”和“恢复后”的世界严格接轨,不重不漏。
- 学习率上界 lr_max训练初始最大学习率。
- 学习率下界 lr_min训练结束时最低学习率。
- 当前步数 t恢复时必须使用检查点里的全局步数。
- 总步数 T整个训练计划的总步数,决定衰减速度。
容错设计
最稳妥的车队,不会只依赖一张货单,还会准备备用轮胎、共享仓库和主备调度车。训练系统的容错设计同样围绕存储、算力和调度展开:检查点写入共享存储,让任何一台机器宕机后都能读取;弹性训练支持节点故障后自动缩容或补充新节点;主备冗余则避免调度节点单点失效。
引入这些设计后,系统可用性可以用一个简单指标刻画。MTBF 代表平均多久故障一次,MTTR 代表平均修复时间。可靠设计的目标,就是减少 MTTR,并让故障恢复过程自动化。
车队有了共享仓库和备用车辆,哪怕某辆货车半路抛锚,也能用最短时间把货物转移到备用车上。容错设计的核心不是“不发生故障”,而是“故障发生后很快恢复”。
- 平均故障间隔 M_MTBF系统平均正常运行多久发生一次故障。
- 平均修复时间 M_MTTR从故障发生到恢复训练平均耗时。
- 可用性 A健康时间在总时间中的占比,越接近 1 越好。
AI 应用场景
大模型预训练平台检查点管理
阶段:模型训练与运维
解决的问题:传统训练一旦中断,往往要从零开始,浪费大量 GPU 算力。平台通过自动保存完整检查点,把“崩溃后重来”变成“从最近检查点续跑”。
具体案例:一个 70B 参数模型在 512 张 GPU 上训练,计划跑 30 天。平台每 15 分钟保存一次检查点。某天凌晨训练节点因硬件故障掉线,平台自动检测到心跳丢失后,在健康节点上加载最近检查点,5 分钟后继续训练。原本可能损失数小时算力,现在只损失约 7.5 分钟的最近区间。
云上弹性训练与故障自愈
阶段:分布式训练调度与云原生运维
解决的问题:云上抢占式实例随时可能被回收,传统任务只能被动等待。通过心跳检测、自动保存和弹性扩缩容,可以让训练任务在实例被回收时自动断点续跑。
具体案例:一家企业用云上 Spot 实例训练多模态模型,某实例被系统回收。调度器检测到节点失联后,立即触发检查点保存流程,并在其他可用实例上重新拉起一个 Worker;新 Worker 加载检查点、重建通信组,训练损失曲线在短暂的抖动后继续下降。
Python 演示
import osimport json class FaultTolerantTrainer: def __init__(self, total_steps=100, lr=0.1, ckpt_path="ckpt.json"): self.total_steps = total_steps self.lr = lr self.ckpt_path = ckpt_path self.global_step = 0 self.loss_sum = 0.0 def _dummy_loss(self, step): return 1.0 / (step + 1) def train_step(self): self.loss_sum += self._dummy_loss(self.global_step) self.global_step += 1 def save_ckpt(self): state = { "global_step": self.global_step, "loss_sum": self.loss_sum, "lr": self.lr, } with open(self.ckpt_path, "w", encoding="utf-8") as f: json.dump(state, f) print("save checkpoint at step", self.global_step) def load_ckpt(self): if not os.path.exists(self.ckpt_path): return False with open(self.ckpt_path, "r", encoding="utf-8") as f: state = json.load(f) self.global_step = state["global_step"] self.loss_sum = state["loss_sum"] self.lr = state["lr"] print("load checkpoint, resume from step", self.global_step) return True def train(self, save_every=20, fail_at=45): if self.load_ckpt(): pass while self.global_step < self.total_steps: if self.global_step == fail_at: print("failure occurs, wait for resume...") return self.train_step() if self.global_step % save_every == 0: self.save_ckpt() print("training done, final loss_sum", self.loss_sum) def main(): trainer = FaultTolerantTrainer(total_steps=60, ckpt_path="demo_ckpt.json") trainer.train(save_every=20, fail_at=45) trainer.train(save_every=20, fail_at=999) if __name__ == "__main__": main()
state = {...}把全局步数、累计损失、学习率打包成字典,相当于写一张训练状态“货单”。json.dump(state, f)把货单序列化到磁盘,进程重启后仍可读回。self.global_step = state["global_step"]恢复时把步数指针拨回保存时刻,训练从断点继续。if self.global_step == fail_at:模拟训练中途故障;真实场景由心跳超时触发。trainer.train(save_every=20, fail_at=45)第一次训练在 45 步抛锚,第二次从 40 步接着跑完。
常见误区
误区我以为断点续训只要把模型权重保存下来就行,恢复后模型参数一样就能继续训练。
正解模型权重只是“货物清单”的一部分。优化器的动量、学习率、数据游标、随机数状态都可能影响后续训练。少存任何关键状态,恢复后的损失和收敛行为都可能偏离正常轨迹。
为何常见:权重文件最常见,保存和加载模型权重是最常被提到的 API,容易误以为权重等于全部训练状态。
误区检查点保存得越频繁越好,这样最多只丢一点点训练进度。
正解保存本身会占用 IO 带宽和磁盘空间,频率过高会让训练变慢,甚至因 IO 争抢造成新的不稳定。更合理的做法是根据保存耗时和平均故障间隔做权衡,比如每隔 15-30 分钟或固定步数保存一次。
为何常见:直觉认为“多存几次没坏处”,忽略了保存过程本身是性能开销。
误区只要从 checkpoint 恢复,数据一定不会重复或遗漏。
正解恢复时如果没有恢复数据游标和随机数状态,dataloader 可能从头开始或跳过一截;如果只恢复权重,global_step 也可能和检查点对不上。恢复后要校验步数、数据批次、学习率位置。
为何常见:检查点文件读起来正常,训练日志里 loss 却可能不对;状态不一致是隐性的,不比对很难发现。
误区故障检测主要靠看训练日志,日志里没有 error 就代表没故障。
正解卡住(hang)的进程往往不打印任何错误。工程上靠心跳超时和通信超时判断,“没消息”本身就是坏消息。比如 NCCL 通信看门狗在一定时间内等不到对端就会判定故障。
为何常见:日常排障依赖日志,但分布式环境下“无声卡死”比“主动报错”更常见。
面试题
Q1. 断点续训中最核心的检查点应该包含什么?
- A.只保存模型权重文件
- B.模型、优化器、数据游标等完整状态← 正确答案
- C.只保存训练日志与损失曲线
- D.只保存整份训练数据集
答案:B。检查点要像车队货单一样足够完整。权重只是模型参数,优化器动量、数据游标、随机数状态都会影响恢复后的训练,因此 B 正确。A/C/D 都缺失关键信息。
Q2. 大模型训练中,故障检测最常用机制是什么?
- A.人工查看训练日志中的报错
- B.随机抽样 GPU 温度数据
- C.心跳与超时判断节点失联← 正确答案
- D.每天定时重启训练任务
答案:C。训练节点发生卡死时往往不打印错误,所以工程上依赖心跳上报和超时判定。C 能发现“无声失联”,A/B/D 都不能及时可靠地发现故障。
Q3. 训练在全局第 100 步保存检查点,第 120 步故障,恢复时应从哪一步继续?
- A.第 1 步重新开始
- B.第 100 步继续← 正确答案
- C.第 119 步继续
- D.第 120 步继续
答案:B。故障时刻的状态可能不完整,不能从 119 或 120 步继续。恢复只能回退到最近一个完整保存点,也就是第 100 步。B 正确。
Q4. 以下哪些是合理的检查点保存策略?
- A.每隔固定步数保存一次← 正确答案
- B.用异步方式写入共享存储← 正确答案
- C.让训练每一步都完整落盘
- D.保留最近两份临时检查点← 正确答案
答案:ABD。固定步数保存和异步写入都能控制开销,保留临时检查点可防止写坏。C 每步落盘会造成巨大 IO 开销,不是合理策略。
Q5. 哪些状态需要随检查点一起保存?
- A.优化器动量状态← 正确答案
- B.数据加载游标← 正确答案
- C.随机数生成器状态← 正确答案
- D.GPU 风扇转速记录
答案:ABC。优化器动量决定后续梯度更新,数据游标保证不重不漏,随机数状态保证数据增强和 shuffle 可继续。D 属于硬件状态,不影响训练恢复。
Q6. 使用 Adam 优化器时,检查点文件体积主要被什么占据?
- A.训练日志与错误堆栈
- B.数据集预处理的缓存
- C.模型参数与优化器动量← 正确答案
- D.网络梯度直方图
答案:C。Adam 除了参数外,还要保存一阶动量和二阶动量,所以检查点体积主要来自参数和优化器状态。日志、缓存、直方图都不是训练必要状态。
Q7. 分布式训练中,节点“失联”的最准确语义是什么?
- A.心跳超时未收到该节点回应← 正确答案
- B.训练精度低于历史均值
- C.损失函数值变为负数
- D.显存占用超过百分之八十
答案:A。失联是通信层面的概念,心跳超时即可判定。精度、损失和显存变化都只是训练指标,不能直接用来判断节点是否失联。
Q8. 提升分布式训练容错能力,常见设计包括哪些?
- A.检查点写入共享存储← 正确答案
- B.故障节点自动重启并恢复← 正确答案
- C.测试集复制到每张卡上
- D.由主节点单点保存全部状态
答案:AB。共享存储让任意节点都能读取检查点,自动重启加快恢复。C 与容错无关,D 会造成单点风险,都不是可靠设计。
Q9. 某集群 MTBF 为 100 小时,保存一次检查点耗时 0.5 小时,每 5 小时保存一次。单位训练时间的预期恢复相关开销约为多少?
- A.约 0.5
- B.约 0.125← 正确答案
- C.约 0.025
- D.约 5
答案:B。保存开销比例 = 0.5 / 5 = 0.1;期望丢失时间比例 = 5 / (2 × 100) = 0.025;合计约 0.125。B 正确。
Q10. 单卡日失效率为 0.001,1000 张卡训练 7 天,任务完全不发生故障的概率最接近多少?
- A.约 0.999
- B.约 0.993
- C.约 0.900
- D.约 0.001← 正确答案
答案:D。总失效率 = 1000 × 0.001 = 1(每天),7 天存活概率为 e^{-7} ≈ 0.000912,最接近 0.001。这说明大集群长任务几乎必然遇到故障,断点续训必不可少。