9.4 流水线并行:把网络切成段
9.3 的张量并行把层内切开,通信频繁;换一种更粗的粒度——按层分段,把模型的第 1–8 层放在卡 1、第 9–16 层放在卡 2……每张卡只负责一段,段与段之间只传边界上的激活。这就是流水线并行(pipeline parallelism, PP)。
它的通信最少(只传边界张量)、对链路要求最低(段间点对点即可,可跨机),却引入一个全新的问题:同一时刻只有一段在算。为了让每一段都能持续工作,必须把一个小批切成多个 micro-batch 依次送入——像工厂流水线一样「错峰」。错峰带来的开头的填充(fill)与结尾的排空(drain)就是气泡(bubble)。
本节回答:
- 朴素调度下气泡占多大比例?公式怎么推出来?
- 三种减小气泡的手段各自改变公式里的哪个量?
- 1F1B 为什么能大幅降低峰值显存?
- micro-batch 数和批大小、梯度累积怎么联动?
9.4.1 流水线并行与 micro-batch 切分
把模型的 层按顺序切成 段(stage),第 段放在第 张卡上。为填满流水线,把一个小批拆成 个 micro-batch 依次送入:
micro-batch 1 在第 1 段算完就传给第 2 段,同时 micro-batch 2 进入第 1 段——各段并行工作。
代价是首尾的空转:开头需要 个时间段把流水线「填充」起来(后面的段还没数据),结尾需要 个时间段「排空」(前面的段已经没数据)。这些空档称为气泡(bubble):
把每个 stage 想成一条产线上的工位:工位 1 做完传给工位 2,同时工位 1 立刻开始做下一个工件。稳态时所有工位都忙;但开工那一刻只有工位 1 有活干,收工那一刻只有最后一个工位有活干。
流水线并行的取舍因此很清晰:
- 换来:每卡只存 份模型(第 段只存自己那几层)→ 解决「整体放不下」;
- 付出:① 开工与收工的空转(气泡);② 卡与卡之间传激活的点对点通信;③ 每段的层数不均衡时,最慢的段拖住所有人。
注意它不解决「单层放不下」(层没有被切开),也不解决激活总量——它只是把激活分散到各段,让每段只需保留自己那部分的中间结果。
9.4.2 气泡公式的推导
设流水线有 段,每段处理一个 micro-batch 的耗时相同,共 个 micro-batch,调度方式为最朴素的 GPipe 风格(先把 个 micro-batch 的前向全部做完,再做全部反向)。则:
- 总时间段数(墙钟时间的最小单位)为 ;
- 其中被气泡占用的部分对应 个「本可工作但没工作」的时间段;
- 气泡占比
(i) 每张卡在稳态下必须完成的「工作量」。 每个 stage 都要处理全部 个 micro-batch(前向一次、反向一次)。把「一个 micro-batch 走完一段」记为一个时间片,则每个 stage 的有效工作量为 个时间片(这里把一次前向或一次反向都折算成 1 个单位;真实比例是前向约占 1/3、反向约占 2/3,见下面的注意)。
(ii) 总时间由「最后一个 stage 的完工时刻」决定。 采用 GPipe 的「先全前向、再全反向」:
- 第 1 个 micro-batch 到达最后一段需要 个时间片(依次流过 段);此后每隔 1 个时间片就有下一个 micro-batch 进入最后一段;
- 因此前向阶段结束时,最后一段才刚处理完 个 micro-batch 的前向,耗时 个时间片;
- 反向按相反顺序进行,同样需要 个时间片(可以从最后一段往前推,也可以用对称性论证);
- 但前向与反向可以部分重叠(最后一段做反向时,第 1 段仍在做前向),因此整条流水线从第一个 micro-batch 进入第 1 段,到最后一个 micro-batch 的反向完成,总共恰好需要 (更严格的证明:所有 stage 的完工时刻 ,其中第 段的启动延迟为 ,故最慢的末段决定了 ;GPipe 调度把每段的空闲时间压到最小,故取等号。)
(iii) 气泡 = 总时间 − 有效工作量。 每张卡的有效时间是 ,而墙钟时间是 ,因此每张卡空闲(气泡) 个时间片:
两个极限的读法: 时气泡 (足够多的 micro-batch 能把首尾空转摊薄到零); 时气泡 (几乎全程空转,等于白买卡)。
注意(口径说明):本推导把前向与反向折算成等长的单位时间片,这是教学口径。实际反向约为前向的两倍耗时,因此更精确的公式会出现前向 / 反向的时间比因子(不同文献的系数略有差异,具体系数需按你的实现实测校准);但结论方向完全一致:气泡随 增大、随 减小,且形式为 。
∎段、 个 micro-batch。求气泡占比与理论利用率;若把 提到 64,结果如何?
(1)、。 也就是说:即使通信为零、负载完美均衡,每张卡也有 30.4% 的时间在等。
(2)。
(3)反过来问: 要多大才能把气泡压到 10% 以下? 解 。即 才把气泡压到 10% 以内。这条经验关系( 取 的若干倍)在配配置时非常常用。
(4)若 (再多一倍段): 时气泡为 ,翻了一倍多。加段必加 micro-batch。
∎- 「1F1B 能消除气泡」。不能。1F1B 改变的是调度顺序与峰值显存,气泡的总量在朴素口径下不变(它让前向与反向交错,缩短的是激活驻留时间)。消除气泡要靠增大 、交错调度或 zero-bubble 拆分。
- 「气泡率就是吞吐损失率」。气泡率是下界之外的空转;实际吞吐还要再乘上通信、负载不均衡、算子效率等因子。反过来也不能说「利用率 = 1 − 气泡率」就等于实测 MFU。
- 「段数 越大越省显存所以越多越好」。 增大确实降低每卡模型状态(),但气泡按 变差,且段间的激活传输次数增加。 的正确取法是「刚好让每段放得下」,而不是尽量大。
9.4.3 三种减小气泡的手段
| 手段 | 机制 | 对公式的影响 | 代价 |
|---|---|---|---|
| ① 增大 (micro-batch 数) | 把同一批数据切得更细,让流水线被更多小批填满 | 分母 变大 → 气泡下降 | 每个 micro-batch 的矩阵乘更小 → 算子效率下降;激活数量增多;梯度累积步数上升 |
| ② 交错调度(interleaved / virtual pipeline) | 每张卡持有多个不连续的层块(如卡 1 持有第 1 块与第 5 块),于是任意时刻总有一块处于「可算」状态 | 等效段数变为 ( 为每卡块数),气泡约按 下降 | 通信次数增加(每块边界都要传);显存略增 |
| ③ zero-bubble(拆分反向) | 把反向拆成「对输入的梯度」与「对权重的梯度」两部分,其中一部分可被推迟到气泡时段执行 | 把本会空转的时段填上有用的计算 | 实现复杂;峰值显存与调度约束增加;效果随层性质不同 |
特别说明 zero-bubble:它的核心观察是「反向中计算权重梯度()的那部分不依赖其他 micro-batch 的结果,因此可以推迟到流水线的空隙里做」;而计算输入梯度()的部分必须立刻做(因为要传给前一段)。把反向拆成 与 两步后,气泡能被大量有效的 计算填掉。这是一项 2024 年提出的调度改进(见本节延伸阅读),属于拓展内容:真实实现的效果与配置强相关,具体收益需按你的框架与模型实测。
朴素调度里,每张卡只负责一段连续层。气泡的根源是「第 1 段在等反向、其余段在等前向」。交错调度给每张卡分配多个不连续的块:卡 1 拿第 1 块和第 5 块。当流水线填到后半程时,卡 1 手上的第 5 块还能继续做前向,于是原本的空档被填上了。
代价是通信次数:原来只传 个边界,现在有 个边界要传( 为每卡的块数)。所以它适合「通信便宜、气泡贵」的场景(例如段间通信量小、机器负载均衡差)。反过来,若段间通信已经很吃紧,交错调度会先恶化通信。没有免费的调度——这一点与张量并行里「能靠更大批解决就不要加 TP」的权衡完全同型。
9.4.4 micro-batch 数与显存、批大小的联动
设数据并行度为 、每卡的单次 micro-batch 为 、每卡每个 micro-batch 的层块数为 、梯度累积步数为 。则一次优化器步骤处理的样本数为
关键点:流水线的 个 micro-batch 是对已经分到本卡的那个小批的再切分——也就是说,切 份不改变本卡拿到的样本总数,只改变「一次同时推进多少份」。所以在「每卡一批」的常见口径下,上式中的 就是本卡数据分区内的批大小,而 只影响显存与气泡;只有当实现把「 段各过一遍」的采样算作 个样本时(GPipe 论文的记账方式),才会有额外的 因子。配置前必须核对你的框架如何定义「全局批」,否则会算错学习率缩放(此项标注为需核实)。
工程上的取法通常是「先定等效批与 ,再让 吃掉剩下的预算」,因为 是唯一既影响气泡、又能通过梯度累积灵活调节的量:
| 你想做的事 | 调哪个 | 会影响什么 |
|---|---|---|
| 压气泡 | 增大 | 算子变小、激活总量上升 |
| 降激活显存 | 减小 或 | 气泡变大;等效批用 补偿 |
| 减少激活驻留的时间 | 用 1F1B 调度 | 峰值显存下降,不影响气泡 |
| 段数太多导致气泡大 | 适当减少 ,把层放不下的部分交给 TP / ZeRO | 改变整体策略配平(9.6.5) |
GPipe 调度是「 个前向全做完,再做 个反向」。这意味着所有 micro-batch 的中间激活都要一直留着,直到反向轮到它——峰值激活显存约等于 份。
1F1B(One-Forward-One-Backward) 在预热(前 个时段)之后,让每段交替执行「一个前向、一个反向」:
- 一旦某段完成一个 micro-batch 的反向,它的激活就可以立刻释放;
- 于是同时驻留的激活只有约 份(而不是 份);
- 前向与反向交错还让通信更容易与计算重叠(前向的发送可与反向的计算并行)。
所以:1F1B 不改变气泡公式,但显著改善「同样的卡数能塞下多大的 micro-batch / 多少个 micro-batch」。在大模型训练里它基本是默认调度。
目标:每卡一次更新处理的样本数(局部批);硬件 8 张卡;模型切 段、数据并行 (每卡分到 条样本);每卡 最多取 2(受显存限制,)。要求气泡降到 15% 以内。求 与 。
(1)气泡约束。 解 : 此时气泡 ✔(若取 ,气泡 ,略超)。
(2)等效批约束。 由 : 不是整数。这是真实配平行程里最常见的摩擦:把某个约束放松一格。取 、: 此时气泡 (略超 15%,工程上通常可接受;若要严格达标可把 提到 20 并让 不可行,故改为把 调到 或调整局部批为 )。
(3)显存检查(1F1B 口径)。 同时驻留的激活约 份 micro-batch:同时在场 token 数 。这里每段同时推进的份数取 (1F1B 下每段交错持有的 micro-batch 数),故 token 数 。按 9.1.3 的口径(每层每 token 每 hidden 元素约 34 字节),本段 8 层(、)、: 模型状态每卡 :若 ,则为 GB。合计约 31 GB + 通信缓冲 → 80 GB 卡上仍然舒适,但已不是「可以忽略」的量;若不重算、或把 提到 8192,这一项就会成为配平的主要约束(这就是为什么长序列训练几乎总要开激活重计算)。口径说明:同时在场 token 数随调度实现而异,此处取 1F1B 的常见量级,精确值需实测。
(4)结论:,气泡 15.8%,每卡模型状态 14 GB、激活数 GB 量级。
∎(1)画出 、 的 GPipe 调度,标出气泡; (2)估算 段、 层、、、、bf16 时,流水线并行的每卡每步通信量,并与数据并行对比。
(1)甘特图(▉ 前向、▨ 反向、░ 气泡)。 行是段(stage),列是时间片:
t → 1 2 3 4 5 6 7 8 9 10 11 12 13
段1 ▉F1 ▉F2 ▉F3 ▉F4 ░ ░ ░ ▨B4 ▨B3 ▨B2 ▨B1 ░ ░
段2 ░ ▉F1 ▉F2 ▉F3 ▉F4 ░ ░ ░ ▨B4 ▨B3 ▨B2 ▨B1 ░
段3 ░ ░ ▉F1 ▉F2 ▉F3 ▉F4 ░ ░ ░ ▨B4 ▨B3 ▨B2 ▨B1
段4 ░ ░ ░ ▉F1 ▉F2 ▉F3 ▉F4 ░ ░ ░ ▨B4 ▨B3 ▨B2
气泡总数:段1 有 3 个(第 5–7)、段2 有 4 个、段3 有 4 个、段4 有 4 个;
用公式核验:墙钟 M+P-1 = 4+3 = 7 个阶段 × 2(前向段与反向段)… 教学口径下
气泡占比 = (P-1)/(M+P-1) = 3/7 = 42.9%。
(严格地说,GPipe 的总时间片数为 (前向与反向各一轮),每段有效工作 ,空闲 ,比值仍是 ,与 结论 9.4.1 一致。)
(2)PP 的通信量。 段边界传的是该 stage 输出的激活,形状 : 每步每个边界要传前向 1 次 + 反向 1 次(反向传的是梯度激活),故每卡涉及的量级约为 ;以 计,约 量级。
与数据并行对比:DDP 的梯度 All-Reduce 是 GB/卡/步(例 9.2.2)。所以流水线并行的通信量比数据并行小一个数量级——这才是它有资格跨机的原因。代价换到了气泡与调度复杂度上。
判据:当「段间链路带宽 / 单步时间」不足以承载 GB 级通信时,PP 仍可运行;而 TP 的通信量(例 9.3.1 的 22 GB/卡/步)与 DP 同量级却更频繁、延迟更敏感,所以必须留在节点内。按「对链路的要求由高到低」排序是 TP > DP > PP(TP 要求最苛刻)。
∎# ── 流水线并行的接口骨架(概念正确、字段随版本变化,需核实)──────────
import torch.nn as nn
from torch.distributed.pipelining import SplitPoint, pipeline, ScheduleGPipe, Schedule1F1B
# ① 切分:按层边界把模型拆成 P 段(这里是概念写法)
def build_stages(model, num_stages):
# 真实实现里用 SplitPoint / 逐段包装把子模块分配到各 stage
...
# ② 描述 micro-batch 的形状(不是数据本身,只给形状与 dtype)
example_args = (torch.zeros(4, 1024, dtype=torch.long, device="cuda"),) # (B_micro, T)
# ③ 组装流水线
pipe = pipeline(module=model, mb_args=example_args, split_spec=None, num_stages=8)
# ④ 选择调度:GPipe(先全前向再全反向,峰值显存高)
# 1F1B(交错,峰值显存低,大模型默认)
schedule = Schedule1F1B(pipe, n_microbatches=16) # M = 16,气泡 ≈ (8-1)/(16+8-1) = 30%
# 调参方向:增大 n_microbatches 压气泡;减小它换算子效率与显存
for step, batch in enumerate(loader):
losses = [] # 收集各 micro-batch 的损失
schedule.step(batch) # 内部完成各段的前向/反向与 p2p 通信
optimizer.step()
optimizer.zero_grad(set_to_none=True)
# ── 传统写法:Megatron 系的等价开关(字段名以你的版本为准,需核实)──
# --pipeline-model-parallel-size 4 # P = 4
# --num-micro-batches 16 # M = 16
# --virtual-pipeline-model-parallel-size 2 # V = 2(交错调度)
切分段时只在层边界切会带来段间不均衡:Transformer 的层是同构的,所以按层数均分通常均衡;但首尾段带有 embedding 与 输出头(lm head),它们参数量大(词表 )、计算轻,会把首尾段变成显存瓶颈或计算瓶颈。
检查清单:
- 参数量均衡:把 embedding / lm head 的参数量单独列出,看首尾段是否超出其他段。
- 计算量均衡:按「每段的前向 FLOPs」而不是「层数」比较(首尾段多了 embedding 查表与输出投影)。
- 激活均衡:首段的输入激活是 embedding 输出(),末段的激活要支撑 softmax over 词表(,可能极大 —— 这项常被忽略,是末段 OOM 的常见原因)。
- 不均衡的处方:把 embedding / lm head 与相邻段共享放置、或在首尾段多放/少放若干层来抵消;框架里常有对应的「首尾段特殊配置」开关(具体字段需核实)。
- 「流水线并行能解决单层放不下」。不能。它切的是层,一个层本身仍然必须放进一张卡(除非叠加张量并行)。
- 「 越大越省显存,所以越大越好」。省的是每段的模型状态(),但气泡按 变差; 的正确取法是「刚好让每段放得下」。
- 「micro-batch 就是数据并行的批」。两者不同:micro-batch 是同一批数据在流水线内部的细分,用于填空档;数据并行的批是分给不同卡的独立数据。二者可同时存在,等效批是它们的乘积(从「等效批」倒推 micro-batch 数)。
- 「1F1B 就是 GPipe 换个名字」。GPipe 需要保留所有 micro-batch 的激活(峰值 );1F1B 只需约 份,且更容易与通信重叠。在 、 时两者峰值激活差距可达数倍。
- 「气泡是通信造成的」。气泡是调度造成的(首尾填充/排空),与通信无关;通信是另一项独立的开销。把两者混为一谈会调错方向。
- 「 越大越好」。 增大压气泡,但每个 micro-batch 的矩阵尺寸变小,GPU 越难跑满;同时梯度累积次数增加、单步墙钟变长。 的甜点通常在「气泡降到 10%–20%」附近,而不是无限增大。
Q1 写出气泡占比公式,并说明 、 各自变大时它怎么变。
Q2 、:算出气泡占比。要把气泡压到 10% 以下, 至少取多少?
Q3 GPipe 与 1F1B 的核心差别是什么?为什么 1F1B 的峰值激活显存更低?
Q4 某任务 、、实测扩展效率只有 0.45。已确认显存充足。给出两个最可能的瓶颈与对应的检查动作。
参考要点
Q1 。 增大 → 分子变大 → 气泡变差; 增大 → 分母变大 → 气泡变好(趋近 0)。
Q2 :。要 :。
Q3 GPipe 把所有 micro-batch 的前向全做完再统一做反向,因此每个 micro-batch 的中间激活必须一直保留到它的反向开始 → 峰值激活约 ;1F1B 在预热后交替执行一前一后,反向一旦完成就立刻释放激活 → 同时驻留约 份。1F1B 还让前向通信与反向计算更容易重叠。
Q4 最可能:① 段间负载不均衡——某一段(常见是带 embedding 或 lm head 的首尾段)计算量/显存明显更大,拖住整条线;检查各段的前向 FLOPs、参数量与激活规模是否一致,必要时调整切分点。② micro-batch 太小导致算子效率低—— 时每个 micro-batch 的矩阵乘很小,GPU 利用率下降;检查 profiler 中的 kernel 时长分布与 GPU 利用率曲线,试着减少 或增大 再比较。也可答「段间点对点通信未重叠」「 过大导致气泡与通信双增」。
- Huang et al., GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism(2019,arXiv:1811.06965)——micro-batch 填充流水线与气泡的原始分析(本节公式的来源)。
- Narayanan et al., PipeDream: Generalized Pipeline Parallelism for DNN Training(2019,arXiv:1806.03377)——异步流水线与权重的多版本管理(另一种思路:用异步换气泡)。
- Narayanan et al., Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM(2021,arXiv:2104.04473)——1F1B、交错调度与 TP×PP×DP 的组合分析,气泡公式在其中的表达与本节一致。
- Qi et al., Zero Bubble Pipeline Parallelism(2024,arXiv:2401.10241)——把反向拆成输入梯度与权重梯度两步、用后者填气泡的调度;本节的 zero-bubble 小结即出自此文。
- PyTorch 官方
torch.distributed.pipelining文档(截至 2025 年)——ScheduleGPipe/Schedule1F1B等接口语义(无对应论文,以官方文档为准)。
本节小结:流水线并行按层分段,用 micro-batch 错峰填满流水线;朴素调度下气泡占比 (推导把它拆成「有效工作量 」与「墙钟 」两个量), 越大、 越小越好。三种压气泡手段分别是增大 、交错调度(等效段数变 )、zero-bubble(拆分反向填补空档),各自有明确代价。1F1B 不消灭气泡,但把峰值激活从 降到 ,是大模型的默认调度。下一节转向另一条线索:不切分、只把每一项做小——混合精度、激活重计算与量化。