想象一支几百辆货车组成的车队,要从北京一路开到乌鲁木齐。每辆车都装着几十吨货物,任何一辆在半路抛锚,整个车队都得停下来等。更麻烦的是,如果出发前没有记账,就得把所有车开回起点重新出发。大模型分布式训练就是这样的车队:几百张 GPU 并行跑同一个模型,一跑就是好几天。硬件故障、内存溢出、网络抖动随时可能打断训练。断点续训就是给车队准备“沿途货单”——每隔一段时间,把每辆车的载货清单和当前位置记下来。万一抛锚,只需退回到最近一张货单,重新装车继续出发,而不是从头再来。这篇文章就用“长途车队”这个比喻,把检查点保存、故障检测、恢复流程、状态一致性这些概念讲清楚。

岗位关联

掌握断点续训,是大模型训练工程师的必备生存技能。它决定了千卡集群上跑一周的任务,是“一夜回到解放前”,还是“晚点两小时到达”。

目录
  1. 可视化演示
  2. 故障必然性
  3. 检查点组成
  4. 保存策略
  5. 故障检测
  6. 恢复流程
  7. 状态一致性
  8. 容错设计
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示K-Means算法迭代过程:初始化质心、分配数据点到最近质心、更新质心位置,直至收敛。

本节配有交互动效(进入视口后自动加载,约 3KB)。

K-Means初始化 随机初始化3个质心位置 点击分配按钮将每个点分配到最近质心 argmin_c ||x_i - mu_c||^2 K-Means通过交替执行分配和更新步骤收敛

故障必然性

长途车队最坏的情况不是堵车,而是出发后没人知道哪辆车会在哪个山口抛锚。分布式训练动辄上百张卡,每张卡像一辆货车,持续满载运行几天几夜。就算单卡的故障率很低,一整个车队跑下来,总会出现一次故障——这不是运气问题,是概率问题。

在工程上,我们常把单位时间内单卡发生故障的比率叫做失效率 λ(lambda)。常见故障包括硬件掉卡、显存溢出(OOM)、网络抖动导致的通信超时。若一张卡失效率是 λ,N 张卡同时在线时,集群整体失效率大约是 N 倍。把时间拉长,任务能活下来的概率按指数衰减。

回到车队比喻:车子越多、路程越长,路上“所有人都平安”的愿望就越不现实。所以聪明的车队不会祈祷不抛锚,而是提前准备好货单和备用方案。断点续训,就是为这种必然的意外兜底。

R(t)=e−N×λ×t\displaystyle R(t)=e^{-N\times\lambda\times t}
通俗说明:卡越多、训练时间越长,任务“一次故障都不出”的概率越低,所以断点续训不是可选项,而是大规模训练的必选项。
  1. 单卡失效率 λ一张卡在单位时间(比如一天)内出故障的概率估计值,是可靠性计算的基本单位。
    λ\lambda
  2. 集群总失效率 N × λN 张卡同时在跑,整体的故障强度近似放大 N 倍。
    N×λN \times \lambda
  3. 指数存活概率时间 t 越长,任务完全不故障的概率 e 的负数次幂越低。
    e−N×λ×te^{-N \times \lambda \times t}

检查点组成

车队出发前要记的“货单”,不只是货物清单,还包括每辆车的位置、油量、司机排班。同样,训练检查点不能只保存模型权重。权重只是模型当前“会了什么”,但训练还依赖很多看不见的状态:优化器里的动量、学习率、数据读到哪、随机数种子等。

一个完整检查点通常包含四类内容:模型权重、优化器状态、数据游标、随机数状态。其中权重和优化器状态是体积大头。对 Adam 优化器来说,除了参数本身,还要额外保存一阶动量和二阶动量,因此存储量大约是参数量的三倍左右。数据游标和随机数状态很小,但决定恢复后数据会不会重读或漏读。

货单记得越全,重新装车后就越能无缝出发。只记货物不记车厢安排,出发后就会手忙脚乱。同理,只存权重不存优化器状态,恢复后的训练精度和收敛速度都可能悄悄变差。

Sckpt≈(P+O)×4 bytes\displaystyle S_{\text{ckpt}} \approx (P+O)\times 4 \text{ bytes}
通俗说明:检查点文件大小主要看“参数个数 + 优化器状态个数”,再乘上每个数占用的 4 字节浮点宽度。
  1. 模型参数量 P模型里需要学习的权重数量,比如 70B 模型 P 约为 700 亿。
    PP
  2. 优化器状态数 OAdam 等优化器为每个参数额外保存的动量变量数,常为 2P。
    O≈2PO \approx 2P
  3. 单值字节数 4FP32 浮点数占 4 字节,决定文件总体积。
    4 bytes4 \text{ bytes}

保存策略

车队不能每开一公里就停下来记一次货单,那样太耽误行程;也不能一路不停,等抛锚时连刚才走的路段都找不到。保存检查点也有同样矛盾:保存太频繁,训练被 IO 拖慢;保存太少,故障后回退的步数太多。

实践中常用三种做法:固定周期保存,比如每隔 1000 步或 15 分钟存一次;异步保存,把检查点写入放到后台线程,避免阻塞训练;临时检查点,用“先写临时文件、再原子重命名”的方式防止写入一半的坏文件。选择保存间隔时,需要同时考虑保存耗时和故障平均间隔。

车队的智慧是“按里程设驿站,既不让大家疲劳,也不让路白跑”。保存间隔要跟着故障频率和存储速度动态调整,才能让“安全”和“速度”同时成立。

η=TsaveTinterval+Tinterval2×MMTBF\displaystyle \eta = \frac{T_{\text{save}}}{T_{\text{interval}}} + \frac{T_{\text{interval}}}{2 \times M_{\text{MTBF}}}
通俗说明:总开销等于“保存动作本身花的时间占比”加上“故障后平均丢失时间的占比”。间隔太大或太小都会让 η 变大,中间存在一个较优区间。
  1. 保存耗时 T_save把一张完整检查点写入存储的耗时,受文件大小和 IO 带宽影响。
    TsaveT_{\text{save}}
  2. 保存间隔 T_interval两次检查点之间间隔的训练时间。
    TintervalT_{\text{interval}}
  3. 平均故障间隔 M_MTBF集群平均多久故障一次,简称 MTBF。
    MMTBFM_{\text{MTBF}}
  4. 期望丢失时间故障若随机发生,平均落在最近检查点与下一次保存之间的一半位置。
    Tinterval2\frac{T_{\text{interval}}}{2}

故障检测

车队最怕的不是有人上报“我抛锚了”,而是某个司机突然失联,所有人还傻傻等他。分布式训练也一样,很多故障不伴随着明显的报错日志,进程可能只是“卡住不说话”。工程上靠心跳和超时来识别失联。

所谓心跳,就是每个训练进程定期向调度器报告“我还活着”。如果超过设定时间没有收到心跳,调度器就判定节点失联;再结合训练框架里的通信超时看门狗,比如 NCCL 的 watchdog,就能把“静默卡死”也揪出来。从失联发生到被确认,通常要经过一个判定过程。

车队里每个司机都要定期用对讲机喊一声“我在”。一旦长时间听不到某辆车的声音,车队就要派人回头查看。检测故障的速度,决定了救援能否及时开展,也决定了丢失的路程有多远。

Tdetect=Theartbeat+Ttimeout+Tquorum\displaystyle T_{\text{detect}} = T_{\text{heartbeat}} + T_{\text{timeout}} + T_{\text{quorum}}
通俗说明:发现故障的总时间,等于心跳周期、超时阈值和多数派确认三者之和。想更快发现故障,就要把这三段都压短。
  1. 心跳周期 T_heartbeat进程每隔多久上报一次状态,周期越短反应越快。
    TheartbeatT_{\text{heartbeat}}
  2. 超时阈值 T_timeout调度器等多久没听到心跳才认为可疑。
    TtimeoutT_{\text{timeout}}
  3. 确认时间 T_quorum多个监控节点协商确认故障需要的时间,避免单个心跳丢失造成误判。
    TquorumT_{\text{quorum}}

恢复流程

一旦确认某辆车抛锚,车队不会退回起点,而是找到最近一个货单,按货单重新装车。断点恢复也分三步:先把检查点文件读回内存;然后重建分布式训练环境,包括进程组、RANK 编号、通信端口;最后校验数据状态与全局步数,确认无误后再继续训练。

恢复不是“启动一个全新任务”,而是“回到历史某个确定时刻”。大模型检查点动辄几十 GB 甚至几百 GB,加载耗时不可忽略;并行上下文重建也要重新初始化网络连接。因此恢复时间可以拆成三部分。

货单重新装好后,还要请几个老司机核对一下货物数量和出发位置。只有确认无误,车队才敢重新上路。恢复流程里的校验,就是这种“出发前最后清点”。

Trecover=Tload+Trebuild+Tverify\displaystyle T_{\text{recover}} = T_{\text{load}} + T_{\text{rebuild}} + T_{\text{verify}}
通俗说明:恢复总时间 = 读检查点 + 重建分布式状态 + 校验一致性。任何一个环节拖慢,都会延长集群的空等时间。
  1. 加载检查点 T_load从共享存储读入权重和优化器状态,受磁盘带宽限制。
    TloadT_{\text{load}}
  2. 重建并行态 T_rebuild重新建立 NCCL 通信组、分配进程编号等。
    TrebuildT_{\text{rebuild}}
  3. 校验一致性 T_verify核对步数、数据游标、文件哈希,确保恢复点没有损坏。
    TverifyT_{\text{verify}}

状态一致性

车队重新出发时,账目必须对得上:这段路货物不能重复运,也不能漏运。训练恢复最大的隐蔽风险,就是权重虽然读回来了,但步数、学习率、数据游标没有对齐,导致数据重复或漏掉一部分。

比如全局步数必须从检查点记录的 step 继续;学习率调度器要根据当前 step 重新计算学习率,而不是从头开始衰减;数据加载器则要回到检查点保存的 batch 偏移,同时恢复随机数生成器状态,才能保证后面读到的样本顺序和原计划一致。学习率调度常用线性下降:

货单上的每一项都要对齐,司机们才知道现在走到哪一段路、下一站该送什么货。状态一致性的本质,就是让“故障前”和“恢复后”的世界严格接轨,不重不漏。

lrt=lrmin⁡+(lrmax⁡−lrmin⁡)×(1−tT)\displaystyle lr_t = lr_{\min} + (lr_{\max}-lr_{\min}) \times \left(1-\frac{t}{T}\right)
通俗说明:第 t 步的学习率由总步数 T 决定。恢复时把 t 设成检查点里的全局步数,学习率才能和“没故障的世界”保持一致。
  1. 学习率上界 lr_max训练初始最大学习率。
    lrmax⁡lr_{\max}
  2. 学习率下界 lr_min训练结束时最低学习率。
    lrmin⁡lr_{\min}
  3. 当前步数 t恢复时必须使用检查点里的全局步数。
    tt
  4. 总步数 T整个训练计划的总步数,决定衰减速度。
    TT

容错设计

最稳妥的车队,不会只依赖一张货单,还会准备备用轮胎、共享仓库和主备调度车。训练系统的容错设计同样围绕存储、算力和调度展开:检查点写入共享存储,让任何一台机器宕机后都能读取;弹性训练支持节点故障后自动缩容或补充新节点;主备冗余则避免调度节点单点失效。

引入这些设计后,系统可用性可以用一个简单指标刻画。MTBF 代表平均多久故障一次,MTTR 代表平均修复时间。可靠设计的目标,就是减少 MTTR,并让故障恢复过程自动化。

车队有了共享仓库和备用车辆,哪怕某辆货车半路抛锚,也能用最短时间把货物转移到备用车上。容错设计的核心不是“不发生故障”,而是“故障发生后很快恢复”。

A=MMTBFMMTBF+MMTTR\displaystyle A = \frac{M_{\text{MTBF}}}{M_{\text{MTBF}} + M_{\text{MTTR}}}
通俗说明:可用性等于“健康时间”占“健康时间 + 修复时间”的比例。MTTR 越短,系统整体可用性越高。
  1. 平均故障间隔 M_MTBF系统平均正常运行多久发生一次故障。
    MMTBFM_{\text{MTBF}}
  2. 平均修复时间 M_MTTR从故障发生到恢复训练平均耗时。
    MMTTRM_{\text{MTTR}}
  3. 可用性 A健康时间在总时间中的占比,越接近 1 越好。
    MMTBFMMTBF+MMTTR\frac{M_{\text{MTBF}}}{M_{\text{MTBF}} + M_{\text{MTTR}}}

AI 应用场景

大模型预训练平台检查点管理

阶段:模型训练与运维

解决的问题:传统训练一旦中断,往往要从零开始,浪费大量 GPU 算力。平台通过自动保存完整检查点,把“崩溃后重来”变成“从最近检查点续跑”。

具体案例:一个 70B 参数模型在 512 张 GPU 上训练,计划跑 30 天。平台每 15 分钟保存一次检查点。某天凌晨训练节点因硬件故障掉线,平台自动检测到心跳丢失后,在健康节点上加载最近检查点,5 分钟后继续训练。原本可能损失数小时算力,现在只损失约 7.5 分钟的最近区间。

云上弹性训练与故障自愈

阶段:分布式训练调度与云原生运维

解决的问题:云上抢占式实例随时可能被回收,传统任务只能被动等待。通过心跳检测、自动保存和弹性扩缩容,可以让训练任务在实例被回收时自动断点续跑。

具体案例:一家企业用云上 Spot 实例训练多模态模型,某实例被系统回收。调度器检测到节点失联后,立即触发检查点保存流程,并在其他可用实例上重新拉起一个 Worker;新 Worker 加载检查点、重建通信组,训练损失曲线在短暂的抖动后继续下降。

Python 演示

python61 行
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()
目的:用极简例子模拟断点续训——第一次训练在第 45 步“故障”,第二次训练自动加载第 40 步的检查点继续跑,演示保存、加载、续跑三个核心动作。
  1. state = {...}把全局步数、累计损失、学习率打包成字典,相当于写一张训练状态“货单”。
  2. json.dump(state, f)把货单序列化到磁盘,进程重启后仍可读回。
  3. self.global_step = state["global_step"]恢复时把步数指针拨回保存时刻,训练从断点继续。
  4. if self.global_step == fail_at:模拟训练中途故障;真实场景由心跳超时触发。
  5. trainer.train(save_every=20, fail_at=45)第一次训练在 45 步抛锚,第二次从 40 步接着跑完。

常见误区

误区我以为断点续训只要把模型权重保存下来就行,恢复后模型参数一样就能继续训练。

正解模型权重只是“货物清单”的一部分。优化器的动量、学习率、数据游标、随机数状态都可能影响后续训练。少存任何关键状态,恢复后的损失和收敛行为都可能偏离正常轨迹。

为何常见:权重文件最常见,保存和加载模型权重是最常被提到的 API,容易误以为权重等于全部训练状态。

误区检查点保存得越频繁越好,这样最多只丢一点点训练进度。

正解保存本身会占用 IO 带宽和磁盘空间,频率过高会让训练变慢,甚至因 IO 争抢造成新的不稳定。更合理的做法是根据保存耗时和平均故障间隔做权衡,比如每隔 15-30 分钟或固定步数保存一次。

为何常见:直觉认为“多存几次没坏处”,忽略了保存过程本身是性能开销。

误区只要从 checkpoint 恢复,数据一定不会重复或遗漏。

正解恢复时如果没有恢复数据游标和随机数状态,dataloader 可能从头开始或跳过一截;如果只恢复权重,global_step 也可能和检查点对不上。恢复后要校验步数、数据批次、学习率位置。

为何常见:检查点文件读起来正常,训练日志里 loss 却可能不对;状态不一致是隐性的,不比对很难发现。

误区故障检测主要靠看训练日志,日志里没有 error 就代表没故障。

正解卡住(hang)的进程往往不打印任何错误。工程上靠心跳超时和通信超时判断,“没消息”本身就是坏消息。比如 NCCL 通信看门狗在一定时间内等不到对端就会判定故障。

为何常见:日常排障依赖日志,但分布式环境下“无声卡死”比“主动报错”更常见。

面试题

Q1. 断点续训中最核心的检查点应该包含什么?

单选题 · easy

  • A.只保存模型权重文件
  • B.模型、优化器、数据游标等完整状态← 正确答案
  • C.只保存训练日志与损失曲线
  • D.只保存整份训练数据集

答案:B。检查点要像车队货单一样足够完整。权重只是模型参数,优化器动量、数据游标、随机数状态都会影响恢复后的训练,因此 B 正确。A/C/D 都缺失关键信息。

Q2. 大模型训练中,故障检测最常用机制是什么?

单选题 · easy

  • A.人工查看训练日志中的报错
  • B.随机抽样 GPU 温度数据
  • C.心跳与超时判断节点失联← 正确答案
  • D.每天定时重启训练任务

答案:C。训练节点发生卡死时往往不打印错误,所以工程上依赖心跳上报和超时判定。C 能发现“无声失联”,A/B/D 都不能及时可靠地发现故障。

Q3. 训练在全局第 100 步保存检查点,第 120 步故障,恢复时应从哪一步继续?

单选题 · mid

  • A.第 1 步重新开始
  • B.第 100 步继续← 正确答案
  • C.第 119 步继续
  • D.第 120 步继续

答案:B。故障时刻的状态可能不完整,不能从 119 或 120 步继续。恢复只能回退到最近一个完整保存点,也就是第 100 步。B 正确。

Q4. 以下哪些是合理的检查点保存策略?

多选题 · mid

  • A.每隔固定步数保存一次← 正确答案
  • B.用异步方式写入共享存储← 正确答案
  • C.让训练每一步都完整落盘
  • D.保留最近两份临时检查点← 正确答案

答案:ABD。固定步数保存和异步写入都能控制开销,保留临时检查点可防止写坏。C 每步落盘会造成巨大 IO 开销,不是合理策略。

Q5. 哪些状态需要随检查点一起保存?

多选题 · mid

  • A.优化器动量状态← 正确答案
  • B.数据加载游标← 正确答案
  • C.随机数生成器状态← 正确答案
  • D.GPU 风扇转速记录

答案:ABC。优化器动量决定后续梯度更新,数据游标保证不重不漏,随机数状态保证数据增强和 shuffle 可继续。D 属于硬件状态,不影响训练恢复。

Q6. 使用 Adam 优化器时,检查点文件体积主要被什么占据?

单选题 · mid

  • A.训练日志与错误堆栈
  • B.数据集预处理的缓存
  • C.模型参数与优化器动量← 正确答案
  • D.网络梯度直方图

答案:C。Adam 除了参数外,还要保存一阶动量和二阶动量,所以检查点体积主要来自参数和优化器状态。日志、缓存、直方图都不是训练必要状态。

Q7. 分布式训练中,节点“失联”的最准确语义是什么?

单选题 · mid

  • A.心跳超时未收到该节点回应← 正确答案
  • B.训练精度低于历史均值
  • C.损失函数值变为负数
  • D.显存占用超过百分之八十

答案:A。失联是通信层面的概念,心跳超时即可判定。精度、损失和显存变化都只是训练指标,不能直接用来判断节点是否失联。

Q8. 提升分布式训练容错能力,常见设计包括哪些?

多选题 · hard

  • A.检查点写入共享存储← 正确答案
  • B.故障节点自动重启并恢复← 正确答案
  • C.测试集复制到每张卡上
  • D.由主节点单点保存全部状态

答案:AB。共享存储让任意节点都能读取检查点,自动重启加快恢复。C 与容错无关,D 会造成单点风险,都不是可靠设计。

Q9. 某集群 MTBF 为 100 小时,保存一次检查点耗时 0.5 小时,每 5 小时保存一次。单位训练时间的预期恢复相关开销约为多少?

单选题 · hard

  • 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 天,任务完全不发生故障的概率最接近多少?

单选题 · extreme

  • A.约 0.999
  • B.约 0.993
  • C.约 0.900
  • D.约 0.001← 正确答案

答案:D。总失效率 = 1000 × 0.001 = 1(每天),7 天存活概率为 e^{-7} ≈ 0.000912,最接近 0.001。这说明大集群长任务几乎必然遇到故障,断点续训必不可少。