9.6 通信、容错与工程实践
把 9.2–9.5 的手段都用上之后,训练能跑起来了。但「能跑」与「跑得快、跑得久」之间还有一大段工程距离:通信能不能藏进计算里、坏了能不能快速恢复、两次实验的结果能不能说得清差异从哪来。
这一节面向的是「已经会用并行、但还没把集群用顺」的读者。四个主题:
- 通信:集合通信有哪些原语、各自的量级是多少、怎么估时间?
- 重叠与压缩:为什么「通信与计算重叠」是吞吐的第一杠杆,梯度压缩能帮上什么忙?
- 容错:万卡规模下故障是常态,checkpoint 策略与恢复时间怎么设计?
- 配平:5D 并行怎么把各轴并行度分下去?确定性训练牺牲了什么?
9.6.1 集合通信原语与通信量的量级
设参与通信的卡数为 、消息大小为 字节、每卡发送量为 :
| 原语 | 语义 | 每卡发送量(环状实现, 较大时) | 在训练里用在哪 |
|---|---|---|---|
| Broadcast | 一张卡的数据发给所有卡 | 参数初始化、把超参同步给所有 rank | |
| Reduce | 所有卡的数据求和,结果落在一张卡 | 中心化聚合(参数服务器风格) | |
| All-Reduce | 所有卡求和,每张卡都拿到结果 | DDP 的梯度同步 | |
| Reduce-Scatter | 求和后按卡分片,每卡只拿自己那 | ZeRO-2/3 与 FSDP 的梯度归约 | |
| All-Gather | 各卡的分片拼成完整张量,每卡都拿到完整结果 | FSDP 计算前聚合参数、序列并行的重排 | |
| All-to-All | 每卡向所有卡各发一段(等价于一次转置) | 专家并行与序列并行(Ulysses 换头) | |
| Send / Recv(P2P) | 点对点 | (只在相邻两卡之间) | 流水线并行的段边界 |
两条恒等式随时可用:All-Reduce Reduce-Scatter All-Gather(这就是 ZeRO-2 能用更便宜的原语替换 All-Reduce 的原因);All-Gather 与 Reduce-Scatter 互为「转置」操作(发送量与接收量互换)。
朴素的 All-Reduce 想法是「每张卡都向其他人收集一遍」,那是 的量级。环状算法把它改成接力:
- 把张量切成 块,每张卡从右手邻居收一块、加到自己的对应块上、再传给左手邻居——转 圈后,每一块的总和正好落在某一张卡上(这是 reduce-scatter);
- 第二轮沿环把「已经算好的块」传开,每张卡收齐 块就得到完整结果(这是 all-gather)。
两轮各 ,合计 。关键洞察是「流量守恒」:每个字节至少要被每张卡看见一次,所以 是下界;环状算法只用了两倍的下界,而且并发走满所有链路,不依赖任何中心节点。
工程含义:通信时间由「带宽」与「延迟」共同决定。大消息时 由带宽决定;小消息时固定延迟(每次通信的握手、kernel 启动)占主导。这解释了为什么「把多个小消息打包成大消息」(梯度分桶)能显著提速。
张卡,消息大小 GB(7B 模型的 bf16 梯度)。链路为节点间 InfiniBand 400 Gb/s( GB/s)。分别估算 All-Reduce 与 Reduce-Scatter 的每卡时间,并说明 ZeRO-2 的收益;再说明若走 NVLink( GB/s)会怎样。
All-Reduce(DDP 口径)。
Reduce-Scatter(ZeRO-2 口径)。 省一半——因为归约后的完整结果不再需要广播给所有卡(每卡只需要自己分片的那段)。
换 NVLink。 All-Reduce 的 s,省 6 倍。但真实节点内很少需要跨卡 All-Reduce 整个模型:ZeRO 与 FSDP 的分片通信天然发生在节点内,只有数据并行的跨节点梯度同步才走 IB。
量级小结表(同一 GB):
| 场景 | 原语 | 每卡流量 | IB 时间 | NVLink 时间 |
|---|---|---|---|---|
| DDP 梯度同步 | All-Reduce | 24.5 GB | 0.49 s | 0.08 s |
| ZeRO-2 梯度归约 | Reduce-Scatter | 12.25 GB | 0.25 s | 0.04 s |
| FSDP 参数聚合(每层) | All-Gather | 单层量级(KB–MB) | 延迟主导 | 延迟主导 |
结论:大消息看带宽、小消息看延迟。跨机的 GB 级 All-Reduce 是秒级开销,必须靠「更大的计算量摊薄」或「与计算重叠」来隐藏,而不是指望链路变快。
∎9.6.2 通信与计算重叠
| 做法 | 机制 | 适用于 | 关键参数 |
|---|---|---|---|
| 梯度分桶 + 异步归约 | 反向每算完一层就把该层梯度放入一个桶,桶满立刻发起 All-Reduce(不等整网反向结束),与后续层的计算重叠 | DDP / ZeRO | 桶大小(bucket size);太小 → 消息碎、延迟占比高;太大 → 重叠窗口不足 |
| 参数预取 | FSDP / PP 在算第 层时,提前发起第 层的 All-Gather / 发送 | ZeRO-3 / FSDP / 流水线 | 预取深度(多留几份参数 → 显存换重叠率) |
| 计算分块(chunking) | 把一个大矩阵乘切成小块,每块算完就可参与通信(如 FlashAttention 的分块 + 环状传 KV) | 长序列注意力、序列并行 | 块大小与通信/计算比 |
| 通信算子融合 | 把 All-Reduce 与相邻的逐元素算子(如加法、缩放)合成一个内核,省一次读写 | ZeRO、TP | 框架是否内置(Megatron / DeepSpeed 均有) |
判定方法:理想重叠下,;无重叠时 。用 profiler 把二者时间线画出来,看通信内核是否与计算内核在时间上重叠——这是判断「重叠有没有生效」的唯一可靠方法。
GPU 的计算单元(Tensor Core / SM)与网卡(NIC)是两组独立的硬件。如果训练是「算一会儿 → 等通信 → 再算」,那么网卡忙的时候计算单元空着,两者交替各闲一半。
重叠的本质就是给这两种资源同时派活:把计算切成能独立进行的块,让「第 块的通信」与「第 块的计算」并行。
它有一个硬约束:重叠需要多留一点显存(预取的参数、分桶的梯度缓冲、双缓冲的激活)。所以「显存换重叠」与「重叠换吞吐」是同一件事的两面。当显存极度紧张时,重叠率必然下降——这也解释了为什么小显存卡上跑多卡训练,扩展效率总是比大显存卡低。
某配置每步 s, s。求无重叠、理想完全重叠、以及实际重叠率 60% 三种情况下的单步时间与扩展效率差异(假设 8 卡)。
(1)无重叠。 s。
(2)理想完全重叠。 s —— 通信完全被计算盖住。
(3)实际重叠率 60%。 被隐藏的通信为 s,暴露的为 s:
对加速比的影响(设单卡基线 与这些数字同比例):不重叠时 8 卡的有效算力利用率相当于 而重叠率 60% 时升到 ,理想重叠时接近 。
结论:重叠率是仅次于「气泡」的吞吐杀手,而且它是纯工程问题(分桶、预取、融合),不需要改算法。所以调优顺序里它排得很前。
∎当跨节点带宽成为硬瓶颈时,可以从「少传」入手:
- 低精度通信:梯度用 bf16(甚至 FP8)传输——DDP 默认已是 bf16;再往下压需要用误差反馈(把量化误差累加到下一步)来保持收敛。
- 梯度稀疏化:只传超过阈值的梯度分量 + 索引。理论上压缩率高,但索引开销与负载不均衡常常抵消收益,在稠密大模型训练中不如量化稳定。
- ZeRO++ 的三种组合(DeepSpeed 提出的系统优化,见延伸阅读):① 分层分区——把每卡的梯度分片再切细,使 All-Gather 尽量发生在节点内(把跨机流量降下来);② 量化梯度——跨机通信降到低比特;③ 二级分区——用节点内的另一份副本做聚合。三者都是「用节点内带宽换取跨机带宽」这一思路的具体化。
注意:压缩会改变梯度,从而影响收敛;引入前必须做对照实验(同样的等效批与学习率,比较损失曲线),不能只看吞吐数字。本段属拓展内容,具体收益强依赖拓扑与实现,需按你的环境实测(标注为需核实)。
9.6.3 checkpoint 策略与恢复时间
| 策略 | 做法 | 保存时间 | 恢复粒度 | 适用 |
|---|---|---|---|---|
| 全量(每卡存全模型) | 每卡各自 dump 完整模型状态 | 慢(每卡都写 GB 级) | 最简单 | 小模型、少卡 |
| 分片(sharded) | 每卡只写自己那 的模型状态,附带元数据描述分片布局 | 快(总量 = 模型状态 / 卡数) | 需要相同或可重排的分片布局 | 大模型默认 |
| 异步(async) | 写盘与训练并行,写的是某一时刻的一致性快照 | 对训练几乎无阻塞 | 稍复杂(需要快照机制) | 长时间训练 |
| 重算(recompute-on-load) | 只存最小的种子 / 输入,恢复时重算 | 最快(存得最少) | 最慢(恢复时要重算) | 有确定性数据管线的场景 |
选择判据:用「期望损失」比较——单次故障的平均损失 ≈ (checkpoint 间隔) 训练吞吐,而 checkpoint 的固定开销 ≈ 训练时间 ×(保存时间 / 间隔)。两者加总最小处即为最优间隔(与经典的最优 checkpoint 间隔分析同型)。
,混合精度 Adam(模型状态 GB),用 张卡训练。存储为并行文件系统,聚合写带宽约 GB/s(128 卡共享)。若单次故障平均损失 5 分钟的进度,恢复(含重启、加载、重建数据管线)耗时约 分钟。
(1)求分片 checkpoint 的数据量与保存时间; (2)若每 2 小时存一次,求「每小时因故障 + checkpoint 造成的平均额外时间」。
(1)数据量与时间。 分片写入的总量 = 模型状态总量 = GB(每卡 GB)。 (若用全量策略: GB, 2 小时 —— 完全不可行,这就是「分片是默认」的原因。)
(2)每小时的额外时间。 设故障率为每小时 次: 取 (每两小时一次故障,见 9.6.4 的量级参考): 即近 8% 的训练时间花在故障与保存上——这在大规模训练里是必须计入预算的成本,而不是意外。
(3)最优间隔的直觉。 间隔 (小时)时,期望故障损失 分钟(平均等到间隔中点),保存开销 分钟。对 求导取零:。 时 小时 —— 间隔越短不一定越好,因为保存本身也要时间(当然真实系统还会加上「必须能在可接受时间内恢复」的硬约束)。
∎9.6.4 大规模故障率与容错设计
把单个组件的故障率想成 (次/小时)。系统里有 个组件(GPU、网卡、光模块、电源、交换机端口),整体故障率近似 ,平均无故障时间(MTBF)≈ 。
数量级感受:假设单卡的小时故障率 ,则
| 卡数 | 期望 MTBF | 换算 |
|---|---|---|
| 8 | 小时 | 基本无感 |
| 512 | 小时 | 近一个月一次 |
| 8192 | 小时 | 一到两天一次 |
| 16384 | 小时 | 几乎每天 |
读法:故障率随规模线性增长,而训练时长不变(大模型要跑几周到几个月),于是「每天都坏」成为常态。公开的工程记载(如 Llama 3 的技术报告,2024,arXiv:2407.21783)也报告了万卡级训练中远高于单机的计划外中断率 —— 具体数字随集群与统计口径差异很大,引用时需核对原始报告的统计窗口(标注为需核实)。
因此容错不是「以防万一」,而是与吞吐并列的设计目标:检错(谁坏了、有没有静默数据损坏)→ 恢复(多快能重启)→ 一致性(恢复后训练是否等价于没坏过)。
| 层次 | 要做的事 | 常见做法 | 代价 |
|---|---|---|---|
| 检错 | 快速发现「哪张卡坏了」而不是整机挂掉 | 心跳 / NCCL 超时、健康检查脚本(跑一个小的 all-reduce + 矩阵乘自测) | 检测延迟通常几分钟 |
| 恢复 | 重启后把状态恢复到一致点 | 分片 checkpoint + 元数据(分片布局、数据管线位置、优化器步数) | 加载时间随模型大小上升 |
| 弹性 | 坏卡就绪后重新组队继续 | 在支持弹性训练的框架里重配进程组(各框架支持度不同,需核实) | 需要重排分片与数据分片 |
| 静默损坏 | 检测「没报错但算错了」 | 抽样校验(周期性比对手工计算的损失值 / 梯度范数是否落在期望范围) | 额外计算极少,但需要对照基准 |
| 确定性 | 让「恢复后」与「未中断」一致 | 固定数据顺序与种子(9.6.5) | 牺牲部分吞吐与灵活性 |
- 「有 checkpoint 就不怕」。checkpoint 只解决「丢多少进度」,不解决一致性:恢复后若数据顺序被打乱、或 optimizer 步数对不上,训练是「悄悄换了实验条件」。元数据必须和权重一起存。
- 「异步 checkpoint 一定更快」。它把写盘与计算重叠,但需要快照一致性机制(否则存下来的是不同时刻的混合状态);在写的过程中若发生故障,可能得到不可用的文件。通常做法是「周期性同步 + 更频繁异步」。
- 「坏卡换掉重启就行」。还要检查数据分片与随机状态是否重放正确;若用固定的 global batch 索引恢复,需要框架支持「按步数而不是按数据流位置」恢复。
- 「故障率与卡数无关,是概率运气问题」。故障率随组件数线性增长,是统计必然;把大规模训练的失败归因于「运气不好」会导致完全错误的预算规划。
- 「确定性训练只是加个种子」。要同时固定:数据顺序、随机初始化、dropout/采样、算子实现(有些算子没有确定实现)、归约顺序。缺任何一项,两次运行都会有微小差异,且并行度不同时差异更明显。
9.6.5 确定性训练与 5D 并行配平
在 3D(DP × TP × PP)之上再加两轴:
| 轴 | 切什么 | 通信特征 | 常用取值 |
|---|---|---|---|
| DP(数据并行) | 数据 | 每步一次大 All-Reduce() | 最外层,尽可能大 |
| TP(张量并行) | 层内权重与激活 | 每层若干次 All-Reduce,延迟敏感 | ≤ 单机 GPU 数(常 4 或 8) |
| PP(流水线并行) | 层 | 段间 P2P,量小 | 2–16,配合 若干倍 |
| EP(专家并行,MoE 专用) | 专家(FFN 的多个分支) | All-to-All,量中等、模式复杂 | 与专家总数、拓扑强相关 |
| CP / SP(上下文 / 序列并行) | 序列维(激活与 KV) | 环状或 all-to-all(注意力需交换) | 长序列才需要 |
配平原则:① 通信最密集的轴放在最快的链路(TP 在 NVLink 内);② 通信最少的轴放最外层(DP 跨机);③ 各轴乘积必须等于总卡数;④ 每一轴都要守住自己的整除约束(TP 要整除头数与 FFN 隐维;PP 要整除层数;EP 要整除专家数)。
硬约束(必须满足,否则跑不起来):
- 显存:每卡模型状态 + 激活 + 通信缓冲 ≤ 卡容量;
- 整除性:TP / PP / EP 的并行度必须能整除对应的结构量;
- 拓扑:TP 不出节点。
软目标(在满足硬约束的可行解里挑):
- 扩展效率最大化:等价于「让最贵的通信尽量小、尽量被重叠」;
- 气泡最小: 足够大;
- 批量与学习率的兼容:全局批不要大到需要改优化器配方。
所以实践里的流程是:列出可行解 → 按软目标排序 → 用一小段基准测试(benchmark)实测两三个候选 → 定稿。没有人靠纯算就能定下最终配置——因为通信时间取决于实际拓扑与内核质量。
下表是示例量级,用于建立「各轴怎么分」的直觉;真实取值必须按模型结构、显存与拓扑重算:
| 模型规模(示例) | 总卡 4096 的一种分法 | 说明 |
|---|---|---|
| 7B | TP 1 × PP 2 × DP 2048 | 7B 单卡放得下(ZeRO-2 后 GB),几乎不需要 TP/PP;此规模通常不用 4096 卡 |
| 70B | TP 4 × PP 8 × DP 128 | TP 留在节点内(4 卡一组);PP 8 段配 ;DP 承担主要并行度 |
| 400B+(稠密) | TP 8 × PP 16 × DP 32(× 可用 CP 若干) | TP 吃满单机 8 卡;PP 16 段对气泡要求高;DP 变小后 ZeRO 分片更关键 |
| MoE 类(总参数百 B、激活数十 B) | TP 8 × PP 8 × EP 8 × DP 8 | EP 引入 All-to-All,需与 TP 共享拓扑预算;具体配平强依赖专家数与路由设计,需按报告核对 |
读表要点:随着模型变大,TP 与 PP 先增大(吃紧的是单卡显存与层数),DP 缩小;到了 MoE,多出一个「用 All-to-All 换稀疏激活」的维度。每一行的乘积都等于 4096(MoE 行中 ,EP 与 DP 共享剩余的卡)。注意:这些分解是教学示例,表达的是「各轴怎么分」的推理方式,不是对任何具体训练报告配置的复述——真实配置受模型结构(专家数、层数、头数)、显存与拓扑共同约束,引用具体报告的配平必须回原报告核对(标注为需核实)。
第一步:决定你要哪一种「确定性」。
| 口径 | 含义 | 可行性 |
|---|---|---|
| 逐位可复现(bitwise) | 同配置两次运行结果完全相同 | 单机单卡可能;多卡 + 高性能算子几乎不可得(浮点归约顺序、非确定性算子) |
| 统计可复现 | 指标差异在预期噪声内(如损失曲线形态一致) | 工程上实际追求的目标 |
| 同并行度内可复现 | 相同卡数与拓扑下可重复 | 通常可行(固定种子 + 确定性算子) |
第二步:把随机性来源逐个钉死。
- 数据顺序:固定 sampler 与
worker_init_fn;记录「已消费样本数」作为 checkpoint 的一部分(否则恢复后数据流会重置)。 - 权重初始化:固定种子;注意并行度不同时初始化的划分方式也可能不同(每卡初始化自己那部分),所以「同一种子、不同 TP 度」不产生同一模型。
- Dropout / 采样:掩码要与并行切分一致(定义 9.3.2 的注记)。
- 算子确定性:开启确定性算法开关(如
torch.use_deterministic_algorithms(True)),并接受由此带来的性能损失与被禁用的算子(哪些算子受影响随版本变化,需核实)。 - 归约顺序:跨卡归约的实现顺序由拓扑决定;不同卡数下浮点求和顺序不同,这就是「逐位可复现」多卡下不可得的根本原因(见 9.1.6 陷阱 9 关于浮点结合律的讨论)。
第三步:把「不确定但可控」的部分写进实验记录:卡数、并行配置、桶大小、精度、算子开关——这些都属于 工具箱 要求的「一次训练的最小记录集」。
- 「扩展效率低就是硬件不行」。先按顺序查:批太小(算力利用率低)→ 通信未重叠 → 气泡太大 → 段间负载不均 → 最后才怀疑硬件。
- 「通信量算出来很小,所以通信不是瓶颈」。小消息受延迟支配,量小不等于时间短。分桶、融合、增大单次消息都是针对延迟的手段。
- 「重叠是框架自动做好的」。默认开关往往只覆盖一部分;桶大小、预取深度、是否融合都需要按模型调。
- 「checkpoint 越频繁越安全」。保存本身要时间(例 9.6.3:7B 约 1 分钟/次),频率过高反而净损失。
- 「万卡训练主要成本是算力」。故障恢复、通信开销、气泡、调参实验都要计入;工程界普遍认为非计算开销占比相当可观(具体比例随配置差异大,不作定量断言)。
- 「恢复训练只要把权重读回来」。数据流位置、优化器步数、随机状态、(若用)学习率调度器的位置都必须恢复,否则等价于换了个实验。
- 「确定性只是可复现性要求,不影响其他」。开确定性算子通常要牺牲吞吐(禁用最快的内核),所以它是「可复现性 vs 速度」的显式取舍,而不是免费的开关。
Q1 写出 All-Reduce 与 Reduce-Scatter 的每卡流量(用 、 表示),并说明为什么 ZeRO-2 能用后者替代前者。
Q2 某步 s、 s,实际暴露的通信时间 4.8 s。求实际单步时间与重叠率。
Q3 万卡集群的 MTBF 为什么远小于单卡?给出「组件数 × 单组件故障率」的推导思路。
Q4 为什么多卡训练难以做到「逐位可复现」?至少给出两个来源。
参考要点
Q1 All-Reduce:;Reduce-Scatter:。ZeRO-2 中梯度也被分片,每卡只需要自己那 份的和,不需要让所有卡都拿到完整的平均梯度,所以可以用「只归约不广播」的 Reduce-Scatter 替代需要「归约 + 广播」的 All-Reduce,流量因此约减半。
Q2 隐藏的通信 s,重叠率 。单步时间 s(若不重叠则为 s)。
Q3 整体故障率近似为各组件故障率之和:,故 ,随 线性缩短。单卡 8 张时以万小时计,8192 张时降到几十小时量级——而训练时长不变(数周),于是故障成为常态而非意外。
Q4 两个来源:① 浮点归约顺序不同(环状 versus 树状、不同卡数、不同切分)导致求和结果在最低位不同,误差随后被放大;② 非确定性算子(原子加、某些卷积/归约实现)本身不保证顺序固定。其他可接受答案:不同并行度下 dropout 掩码划分不同、不同内核实现的累加精度不同(如 bf16 累加 vs fp32 累加)、数据加载的多 worker 顺序。
- Narayanan et al., Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM(2021,arXiv:2104.04473)——通信量分析与重叠策略的经典参考。
- Zhao et al., PyTorch Distributed: Experiences on Accelerating Data Parallel Training(2020,arXiv:2006.15704)——DDP 的梯度分桶与重叠实现。
- Wang et al., ZeRO++: Extremely Efficient Collective Communication for Giant Model Training(2023,arXiv:2306.10209)——分层分区、量化梯度与二级分区的系统优化。
- PyTorch 官方文档:
torch.distributed.checkpoint(分片存取)、torch.use_deterministic_algorithms(确定性开关)(截至 2025 年,无对应论文,以官方文档为准)。 - Llama 3 技术报告(2024,arXiv:2407.21783)——万卡级训练的中断率、并行配置与工程实践的公开记载(统计口径需核对)。
- DeepSeek-V3 技术报告(2024,arXiv:2412.19437)——MoE 训练的并行配平与通信-计算重叠(DualPipe)工程记载。
本节小结:集合通信的量级可以用三行公式记住——All-Reduce 、Reduce-Scatter / All-Gather 、P2P 只有 ;大消息看带宽、小消息看延迟。吞吐的第一杠杆是重叠(分桶、预取、分块、融合),但它要以显存为代价。规模上去以后,故障是与吞吐并列的设计目标:MTBF 随组件数线性缩短,checkpoint 的最优间隔由「期望故障损失」与「保存开销」共同决定,而恢复的一致性(数据流、步数、随机状态)常被忽略。最后,5D 配平是「先满足硬约束、再优化软目标」的流程,确定性训练则是「可复现性 vs 吞吐」的显式取舍。下一节把这些全部落到一个具体问题:手上有 1–8 张卡,到底该怎么配。