训练集群的工程学:数据流水线与断点续训

训练作业的两条命脉

集群里硬件怎么坏、坏了谁去修,「GPU 集群故障工程」一篇已经交过底;数据该放在共享存储还是本地盘、分几层管,「AI 存储三层策略」一篇也定过规矩。本篇接着往里走一层:站在训练作业自己的视角,看它怎么把数据吃进去、怎么在故障后爬起来——这两件事分别对应数据流水线与断点续训,是训练集群工程的两条命脉。

对很多工程师来说,训练大模型像一只黑盒:丢进数据与配置,转几天吐出权重。把黑盒拆开,里面其实是两条环环相扣的流水线:一条把语料从存储一路搬到 GPU 显存,喂给计算;另一条在计算进行时不断把「进度」固化到磁盘,等着下一次故障来收割。任何一条断掉,几千张卡的时薪就在白白燃烧。

一轮训练的时间都花在哪

理想状态下 GPU 每一步都在做矩阵乘法,现实中却经常「GPU 等数据」。一个训练 step 由前向计算、反向计算、优化器更新三段组成,这些是 GPU 的活;但 step 之间还夹着数据搬运——下一个 batch 的样本要从磁盘读出、解码、tokenize、拼批,再跨 PCIe 拷进显存。任何一环慢了,GPU 就空转,而空转的代价按千卡集群折算非常吓人。

判断瓶颈的办法很朴素:看 GPU 利用率曲线。曲线呈规律锯齿,多半是数据供给断流;曲线平稳但偏低,可能是 batch 太小或通信占比过高。排障顺序也应该先查数据流水线,再谈扩大集群规模——很多「算力不够」的抱怨,最后查出来是「数据不够快」。

数据流水线:从磁盘到显存的三段路

第一段:磁盘读取与文件组织

预训练语料动辄数 TB,而原始网页语料的物理形态常常是海量小文件:单个文档几 KB,清洗前的语料库就是上亿个零散文件。文件系统对付这种规模非常吃力,每次打开文件都有元数据查找开销,顺序吞吐完全发挥不出来。业界的通行做法是把小文件预先打包成固定大小的分片文件——常见几百 MB 一个归档分片,WebDataset 这类格式就是这一思路的代表——训练时顺序读分片、流式解包,把随机读变成顺序读,网络存储的吞吐才真正用得起来。

分片之外还有一条重要的工程纪律:训练要读的数据尽量落在离计算节点近的地方。共享文件系统吞吐再高,也扛不住几千个进程同时随机抽读。常见做法是作业启动前把所需分片预拷到节点本地 NVMe,训练全程只吃本地盘。这次预拷贝的时间要计入作业总时长——存储三层策略定的是「数据放哪一层」,训练侧要补的是「怎么把要用的份提前搬到位」。

顺序之外还有随机:训练要求样本顺序充分打散,否则模型会学到数据的排列偏差。打包成分片后,全局打散通常拆成两级——分片之间的乱序(每个 epoch 打乱分片读取顺序)加分片内部的乱序(读取时打散样本),两级配合近似全局 shuffle。别小看这一步:打散不充分的训练会出现周期性的 loss 波动,排查起来非常费人。

第二段:CPU 预处理与预取

从磁盘读出来的是原始字节,要经过解码、过滤、tokenize、截断、拼批一长串 CPU 工作。这些活绝不能和 GPU 计算串行做,否则 GPU 每步都要停下来等。解法是把预处理交给一组 DataLoader worker 进程异步完成,主进程只消费现成的 batch。PyTorch 里两个参数就是这里的全部玄机:num_workers 决定预处理并行度,通常设为 CPU 核数的一半到全部,实际取值按机器实测;prefetch_factor 默认为 2,表示每个 worker 提前备好两个 batch。备少了 GPU 断粮,备多了内存吃紧,需要结合 batch 大小与样本尺寸权衡。

预取之外,锁页内存值得单独一提。默认的 CPU 内存是可换页的,GPU 从页内存拷数据既慢又可能出问题;把 batch 固定进锁页内存(pin_memory),再用异步拷贝与计算重叠执行,能让 PCIe 上的搬运基本隐藏在计算背后。这几招组合下来,数据供给通常能从「GPU 吃不饱」跨到「GPU 吃撑」。

如果 CPU 预处理仍跟不上,还有两条升级路线:一是把部分预处理挪给 GPU——图像解码与增广的 GPU 化预处理是成熟做法,能卸掉 CPU 的大头;二是把重活前置到数据制备阶段,tokenize 这类一次性工作在进训练前做完,训练时直接读现成的 token 序列,用一次离线成本换每一步的在线开销。预处理算力是训练成本里经常被漏记的一项,扩 GPU 之前先看看 CPU 是不是先到顶了。

第三段:显存侧的最后调度

数据进了锁页内存,最后一道账是 batch 大小与拷贝时机。batch 越大单步算得越满,但显存占用也水涨船高,而且大 batch 对优化器超参有影响,学习率常要跟着调。图像类任务还常把缩放、增广这类逐样本的活挪到 GPU 上做,让 CPU 只干重活。这里没有统一答案,原则只有一条:让三段路各自的吞吐都大于 GPU 的消耗速率,整条流水线才算打通。打通的标志是显存侧始终有下一个 batch 在等着,而不是 GPU 反过来催数据。

断点续训:一次崩溃到底丢了什么

训练状态的完整清单

集群故障是常态而非意外,前述故障工程篇给过量级感:大规模训练期间几乎隔几天就有硬件出状况。断点续训的目标是故障后从最近存档满血复活,而「满血」的前提是存档完整。一份完整的训练状态至少包括四类东西,缺任何一类,恢复都算失败。

模型权重只是最显眼的部分。优化器状态同样不可或缺:以 Adam 为例,每个参数都挂着两个同形状的矩估计量,体积约为权重本身的两倍。只恢复权重不恢复优化器,等于让优化器失忆重启,动量归零,恢复初期会出现明显抖动,严重的甚至把已经收敛的趋势抖回去。第三类是数据进度:全局训到第几步、数据集采样位置在哪。漏了它,恢复后要么从头重吃旧数据,要么跳段留缺口。第四类最隐蔽:随机数状态。dropout 掩码、数据 shuffle、数据增强全依赖随机序列,恢复时随机状态对不上,严格意义上续训已经不是同一条轨迹——多数场景能接受,但做对照实验或复现问题时足以让人抓狂。

还有几样锦上添花的:开了权重指数滑动平均的训练,EMA 副本也要进存档,丢了它评测数字会莫名变差;学习率调度器的内部状态、梯度累积的余量,都属于「丢了不致命、丢了很难查」的灰色地带——建议一并打包,宁可存档大一点。

保存与恢复的完整链路

断点续训不是「存个文件」这么简单,它是一条链:训练进程按周期把状态写入共享存储;调度器发现进程死亡后重新拉起作业;新进程定位最近一份完好的 checkpoint,加载、校验、跳过已完成 step 继续跑。链上每一环都可能出错。写盘写到一半崩溃,会留下残缺文件,所以要先把状态写成临时名,完整后再原子改名,或者配上校验和;调度器重启太慢,恢复期间整组卡一起空等;加载时发现存档损坏,还要能自动回退到更早的一份。

自动重启还要区分故障形态,不能无脑一把梭。显存报错、集合通信超时这类进程级故障,重启作业即可恢复;整机失联要等节点替换或剔除;存储抖动导致的读写卡死,盲目重启只会让新进程又卡在加载上空转烧卡——恢复流程里要有前置检查(存储可达、节点就绪),检查不过就换条路径等待,而不是原地打转。恢复脚本把这些分支写清楚,才能真正做到无人值守。

恢复之后还有一件容易被忽略的事:日志与指标的接续。step 计数、学习率曲线、监控时间序列都要从断点接着走,否则看板上会出现一段莫名其妙的重复曲线,事后排查极易误判。工程纪律是:四类状态打包存、打包恢复;日志接续作为恢复流程的一步写进脚本,而不是靠人记。

数据进度还有两种实现口径值得知道:一是显式记录「吃到第几个样本」,恢复时直接跳位;二是只存随机种子,恢复时用同一种子重放整个采样序列来重建数据顺序。后者省一个状态项,但前提是采样完全由种子决定、数据集本身没变——数据集中途增删,重放就会错位。两种做法都有人用,工程上怕的不是选哪种,而是两种都实现了一半、恢复时互相打架。

分布式 checkpoint 与弹性训练

分片保存与恢复重排

大模型的训练状态大得惊人:数百亿参数的模型,权重加优化器状态动辄 TB 级。让单个进程收集全部状态再写一个大文件,既慢又会把网络与单盘打爆。分布式 checkpoint 的思路是化整为零:每个进程只保存自己名下的分片——权重按并行策略切开,优化器状态跟着参数走,全体并行写入存储,存储侧看到的是一批中等大小的文件,而不是一个巨型文件。

麻烦出在恢复环节:作业重启后,并行拓扑未必与保存时一致。故障换了几块卡、弹性伸缩改变了进程数,都会让分片边界对不上。这时需要重排:加载方按新拓扑去旧分片里读出自己名下的那部分参数,重新拼装。现代训练框架普遍把这一层做了进去,但工程上仍要留心:跨拓扑恢复比同拓扑恢复慢得多也更容易出错,重要训练任务通常固定并行配置,把重排留给真正需要的场景。保存与恢复还要配一对习惯:恢复后先做健全性检查,抽样比对参数形状与统计量;同时滚动保留最近几份存档,并定期把一份归档到远端,防止本地存储随节点一起阵亡。

弹性训练:故障后自动爬起来

把以上环节串成自动链路,就是弹性训练。作业以弹性方式启动(torchrun 这类启动器支持进程组动态重组),配合调度器的备份节点机制——为长训练预留少量备用节点,故障时把坏节点踢出、备用节点顶上,训练组自动重组并从最近存档恢复。目标只有一个:把故障恢复压到分钟级,让硬件故障只损失几分钟进度,而不是半夜把工程师叫起来人肉重启。

弹性不是免费的:进程组重组、通信域重建、数据分片重分配都要时间,恢复期间所有卡一起等。所以弹性训练的调优重点不在「能不能恢复」,而在「恢复得多快、多稳」:心跳间隔决定发现故障的速度,checkpoint 频率决定丢多少进度,两者按实测故障率搭配,才是训练集群真正的核心算术。

恢复时长本身也值得拆开监控:发现(心跳超时判定)、拉起(分配节点、起进程)、加载(读存档做校验)、追赶(对齐数据进度),四段各有各的预算,哪段超了看哪段。把每次故障的恢复时长记录成指标,恢复越来越慢往往说明存储或调度在悄悄劣化——这比单次故障本身更有信号价值。

小结

训练集群的工程学,本质是在和两个敌人作战:一是 IO,别让最贵的 GPU 吃不上饭;二是故障,别让跑了数周的作业一夜归零。数据流水线用分片打包、多进程预取、锁页内存异步拷贝三板斧,把数据喂到 GPU 嘴边;断点续训靠权重、优化器状态、数据进度、随机数状态四类状态一个不落的完整清单,保证故障后满血复活;分布式 checkpoint 用分片保存与重排恢复撑起 TB 级状态;弹性训练再把所有环节串成自动链路,把人从深夜重启作业的命运里解放出来。

这些机制彼此咬合:存档太疏,弹性恢复丢的进度就多;存档太勤,又反过来拖慢训练。保存频率、故障率、存储吞吐三者的平衡,是每个训练平台都要做的算术。存档机制本身的文件构成、代价与取舍,本系列下一篇「一篇读懂 checkpoint」会单独展开。

给实践者一条底线建议:大规模训练正式开跑之前,故意杀掉作业一次,验证整条续训链路真的走得通。没经过演练的断点续训,等于没有断点续训。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?