流水线并行:从 GPipe 到 DualPipe
切层、气泡、在途激活,以及把气泡一点点挤掉的几代调度
按 → 或点击翻页 · 带 交互 的页面可以直接拖动
目录
- 为什么要按层切模型,切完之后多了什么通信
- 气泡(bubble):定义与 \((p-1)/m\) 的推导
- AFAB / GPipe:先全前向再全反向,显存随 \(m\) 涨
- 1F1B:气泡不变,在途激活封顶为 \(p\)
- 交错(interleaved):每卡 \(v\) 段,气泡 \(\div v\),通信 \(\times v\)
- Zero Bubble:把反向拆成 B 和 W,用 W 填洞
- DualPipe:双向喂数据,再把 EP 的 all-to-all 藏进计算
- 一个能拖动所有参数的调度模拟器 + 选型
记号:\(p\) 流水线级数(卡数),\(m\) 每步的 micro-batch 数,\(v\) 每卡的模型段数;\(t_f, t_b\) 是一个 micro-batch 在一级上的前向、反向耗时。
单节点装不下模型时
张量并行(TP)在每层里通信好几次,只适合节点内 NVLink;一跨节点,带宽掉一个数量级。
但 70B 量级的模型,光训练状态就超过一个 8 卡节点:混合精度 + Adam 每个参数约 16 字节(bf16 权重 2 + bf16 梯度 2 + fp32 主权重 4 + Adam 两个矩 8)。
流水线并行(PP)的思路最朴素:把层切开,第 1–10 层放卡 0,第 11–20 层放卡 1……每张卡只存 \(1/p\) 的层。
账算一下(Llama 3 70B 的形状)
80 层, d=8192
70×10⁹ × 16 B ≈ 1.12 TB
8 × 80 GB = 640 GB < 1.12 TB
切成 \(p=8\) 级(每级 10 层)后,每卡的权重 + 梯度 + 优化器状态 ≈ 140 GB;再配上 ZeRO-1 把优化器状态按 DP 切开,就装得下了。
激活值是另一回事:它随 micro-batch 数和调度方式变化,才是 PP 调度真正要管的东西。
每一级是一段连续的层
通信只发生在级与级的边界,而且是点对点(send/recv):一个 micro-batch 过一次边界只传一份 \([b,s,h]\) 张量。对比 TP 每层都要做 all-reduce,PP 对带宽的要求低得多——这正是它适合跨节点的原因。
前向一路往右,反向一路往左
# 第 i 级的一次训练(伪代码)
x = recv(from=i-1) if i > 0 else batch
acts = []
for layer in my_layers:
acts.append(x) # 留给反向用
x = layer(x)
if i < p-1: send(x, to=i+1)
else: loss = L(x); g = dL/dx
g = recv(from=i+1) if i < p-1 else g
for layer, a in reversed(zip(my_layers, acts)):
g, dW = layer.vjp(a, g) # 对输入、对权重求导
accumulate(dW)
if i > 0: send(g, to=i-1)
第 \(i\) 级的反向,必须等第 \(i+1\) 级把梯度传回来;而第 \(i+1\) 级的前向,必须等第 \(i\) 级把激活传过来。
所以一个 batch 只走一遍时,任意时刻只有一张卡在干活。
还要注意 acts:前向时存下来、反向时才释放的激活值。一个 micro-batch 的前向做完、反向没做的这段时间,它就「在途」(in flight),占着显存。后面所有调度都在和这两件事较劲:空闲和在途激活。
一个 batch 走一遍:大部分时间在等
灰色格子就是气泡。紫色是前向 F,橙色是反向(这里按 \(t_b \approx 2t_f\) 画)。\(p\) 级的情况下,每张卡只有 \(1/p\) 的时间在算。
气泡时间与气泡率
理想情况下(完美并行),一级处理一个 batch 只要 \(t_{id} = t_f + t_b\)。
实际上,每张卡都要等前面 \(p-1\) 级的前向,再等后面 \(p-1\) 级的反向:
「气泡有多大」有两种常见说法,读论文时别混:
气泡率:相对理想时间
$$r_{bubble} = \frac{t_{pb}}{t_{id}}$$一个 batch 时 \(= p-1\):多花了 \(p-1\) 倍的理想时间。后面默认用这个。
空闲占比:占总时间
$$\frac{t_{pb}}{t_{pb}+t_{id}}$$一个 batch 时 \(=(p-1)/p\),即每张卡有多少时间在等。
把 batch 切成 micro-batch
把一个 batch 切成 \(m\) 份。卡 1 在处理 micro-batch 1 的时候,卡 0 已经可以开始 micro-batch 2 了。
数学上这就是梯度累积:每个 micro-batch 的梯度加起来,等全部 \(m\) 个做完再做一次优化器 step——结果和直接用整个 batch 一样(同步语义,不引入权重版本不一致)。
代价:每步结束要等流水线排空(pipeline flush),然后所有级同步地做 optimizer step。
和全局 batch 的关系
$$m = \frac{B}{b\cdot d}$$\(B\) 全局 batch(样本数),\(b\) micro-batch 大小,\(d\) 数据并行度。\(B\) 往往由优化需求定死,所以 \(m\) 不能想多大就多大。
All-Forward-All-Backward
先把 \(m\) 个 micro-batch 的前向全做完,再做全部反向。实现最简单:训练循环仍然是「前向 → 反向」两段。点一个格子可以高亮那个 micro-batch 走过的路径。
气泡被 \(m\) 摊薄
开头仍然是 \(p-1\) 个前向的爬坡,结尾仍然是 \(p-1\) 个反向的退坡,气泡的绝对量没变:\(t_{pb} = (p-1)(t_f+t_b)\)。
但理想时间变成了 \(m\) 倍:\(t_{id} = m(t_f+t_b)\)。于是
直觉:爬坡和退坡是固定成本,\(m\) 越大,中间「所有卡都在忙」的部分越长,固定成本被摊得越薄。GPipe 的实验里,\(m \ge 4p\) 时气泡已经几乎可以忽略。
例:\(p=8\):\(m=8\) 时 \(r=0.875\)(空闲 47%);\(m=32\) 时 \(r\approx0.22\)(空闲 18%);\(m=64\) 时空闲 10%。
所有 micro-batch 的激活同时在途
反向要用前向存下的激活。AFAB 里第一个反向开始之前,\(m\) 个 micro-batch 的前向都做完了——每张卡要同时存 \(m\) 份本级激活。
每层激活的经典估算(Korthikanti et al.,16-bit 激活、无 TP/SP、无重计算):
\(5as/h\) 那项是注意力分数矩阵;用 FlashAttention 不落地这块后,约剩 \(34\,sbh\)。
按 70B 形状估一下
s=4096, b=1, h=8192
34·s·b·h ≈ 1.14 GB / 层 / mb
p=8 → 每级 10 层 ≈ 11.4 GB / mb
AFAB、\(m=32\):每卡约 365 GB,不可能。
所以「加大 \(m\) 摊薄气泡」在 AFAB 里直接撞上显存墙。
34sbh 对应 GPT 式结构(4h MLP、多头注意力);SwiGLU、GQA 会改变常数,不改变量级。
重计算(activation recomputation)
用时间换显存(GPipe 的做法):前向只存每级边界的输入,反向前再把本级前向算一遍。
\(N\) 是 batch 大小,\(M\) 是 micro-batch 数(\(N/M\) 就是 micro-batch 大小),\(K\) 是级数,\(L/K\) 是每级层数。第一项是所有 micro-batch 的边界输入,第二项是正在反向的那一个 micro-batch 的完整中间激活。
代价是每个 micro-batch 多一次前向:按 \(t_b \approx 2t_f\),计算量从 \(3t_f\) 涨到 \(4t_f\),理论上 +33%,实测一般在 20% 左右。
折中:\(l\) 层里只存 \(c\) 个 checkpoint(各占 \(A^{in}\)),反向时一次重建一段 \(l/c\) 层(每层 \(A^{mid}\)):显存 \(c A^{in} + \frac{l}{c}A^{mid}\),在 \(c=\sqrt{l A^{mid}/A^{in}}\) 时最小。
尽早开始反向
问题出在「所有前向做完才开始反向」。只要最后一级做完某个 micro-batch 的前向,它就可以立刻做那个 micro-batch 的反向,把激活释放掉。
第 \(r\) 级(从 0 数)的程序分三段:
- warm-up:先做 \(p-r-1\) 个前向(越靠前的级做得越多);
- steady:一个前向、一个反向交替(one-forward-one-backward);
- cool-down:把剩下 \(p-r-1\) 个 micro-batch 的反向做完。
最早出现在 PipeDream(异步版本,靠 weight stashing 保存多版本权重);带 flush 的同步版本叫 PipeDream-Flush,就是 Megatron-LM 默认的调度。
看它的时间线
右侧小柱是播放光标所在时刻每张卡的在途激活(前向已做、反向未做的 micro-batch 数),播放结束后显示每卡的峰值。拖大 \(m\):气泡率下降,而在途激活的峰值始终是 \(p\)。
同样的气泡,不一样的在途激活
两条时间线一样长:开头 \(p-1\) 步爬坡、结尾 \(p-1\) 步退坡都还在,每卡空闲 \((p-1)(t_f+t_b)\)。差别在右边——AFAB 每张卡都要攒满 \(m=8\) 份激活,1F1B 的第 \(r\) 级最多 \(p-r\) 份。
气泡没变,显存封顶
为什么气泡一样大
对第 0 级:它在 \((p-1)\) 个前向之后才能收到第一个反向;结尾最后一个反向要等后面 \(p-1\) 级依次退回来。不管中间怎么排,开头和结尾的等待都在。数一下就是 \((p-1)(t_f+t_b)\),于是仍然
但在途激活从 \(m\) 降到 \(p\)
第 \(r\) 级最多同时有 \(p-r\) 个 micro-batch 在途(warm-up 的 \(p-r-1\) 个 + steady 里刚做完前向的 1 个),与 \(m\) 无关。
所以 1F1B 的真正收益是:可以放心加大 \(m\),由此间接压低气泡。
副作用:显存不均,第 0 级在途最多(再加上 embedding),最先撞到显存上限。Llama 3 训练时为此给首尾两级各少放了一层。
1F1B 的扩展性
在 H100 集群上实测 1F1B:当 \(m \le p-1\) 时,气泡非常伤,PP 越大性能越差;当 \(m=32 \gg p-1\) 时小 PP 度的性能明显改善,但 PP 很大时仍然受限。
一个值得记住的对比:micro-batch 较少时,从 1 个节点(\(p=8\))扩到 2 个节点(\(p=16\)),性能只降了约 14%;而张量并行在类似的跨节点场景通常降约 43%。
结论和直觉一致:PP 的 p2p 流量小,碰到较慢的节点间网络时比 TP 更扛得住;它的主要敌人是气泡,不是带宽。
在固定 GPU 数和全局 batch 下
卡数 \(n\) 固定、不用 TP 时,PP 度和 DP 度此消彼长:\(p = n/d\)。全局 batch 有 \(b' = B/b\) 个 micro-batch,分给 \(d\) 条流水线,每条 \(m = b'/d\)。代入气泡率:
DP 度 \(d\) 越大,气泡越小;全局 batch 越大,气泡也越小。PP 只应该用到「模型装得下」为止,剩下的卡给 DP。
micro-batch 大小 \(b\) 也有张力:\(b\) 大 → GEMM 算术强度高,但 \(m\) 小 → 气泡大。总时间约为 \((b'/b+p-1)(t_f(b)+t_b(b))\):前一个因子是「\(m\) 个 micro-batch 加 \(p-1\) 步爬坡」,后一个是单个 micro-batch 的耗时,扫一遍 \(b\) 取最小即可。
经验法则(Megatron-LM)
1. TP 用到单节点 GPU 数 \(g\) 为止;跨节点再用 PP。
2. 总模型并行度 \(t\cdot p\) 取刚好装得下模型的值,其余用 DP 扩。
3. 最优 micro-batch 大小取决于模型的吞吐/显存特性和 \(p, d, B\)。
每张卡放多段不连续的层
把模型切成 \(v\cdot p\) 段(model chunk),第 \(k\) 段放在卡 \(k \bmod p\) 上。一个 micro-batch 的前向会绕流水线转 \(v\) 圈,所以也叫 looping pipeline。例:16 层、4 卡、\(v=2\):卡 0 放第 1–2 层和第 9–10 层。
气泡 \(\div\,v\),通信 \(\times\,v\)
每一段只有原来 \(1/v\) 的层,所以一段的前向/反向时间是 \(t_f/v,\ t_b/v\)。爬坡和退坡仍然是 \(p-1\) 步,但每步变短了:
代价一:每个 micro-batch 跨越的级边界从 \(p-1\) 个变成 \(vp-1\) 个,p2p 通信量约 \(\times v\)。Megatron 用 scatter/gather 优化抵消:发送端把张量按 TP 度 \(t\) 切开,各走一块 IB 网卡,接收端用 NVLink all-gather 还原,每对相邻级间的 IB 流量降为 \(bsh/t\)。
代价二:Megatron 的实现要求 \(m\) 是 \(p\) 的整数倍;第 0 级的在途激活也会略多于 \(p\)(下一页模拟器可以看到,\(p=4,v=2\) 时是 5.5 份)。
拖 \(v\) 看气泡缩小
颜色深浅区分同一张卡上的不同段。把「p2p 延迟」拉起来:每跨一次级边界多等 \(c\),交错因为跨边界次数多,受伤更重。这就是「通信 \(\times v\)」在时间线上的样子。
同一时刻先做哪个?
交错之后,一张卡上同时可能有「早的 micro-batch 在后面的段」和「晚的 micro-batch 在前面的段」两种活。
深度优先(DFS):优先让早的 micro-batch 走完,尽快关闭前向-反向环。Megatron 的交错调度以 \(p\) 个 micro-batch 为一组轮转。
广度优先(BFS):先让所有 micro-batch 过完这一段再进下一段。它和 FSDP 搭配很好:每段权重只需 all-gather 一次就服务所有 micro-batch,梯度 reduce 也能更好地和计算重叠。代价是在途激活接近 AFAB。
Llama 3 的做法:把「同一段连续做几个 micro-batch」记为 \(N\),DFS 对应 \(N=p\),BFS 对应 \(N=m\),Llama 3 训练时让 \(N\) 可调,在两者之间找通信与显存的平衡点。
他们还:首尾两级各少放一层 Transformer(首段只剩 embedding,末段只剩输出投影和 loss)来配平;用异步 p2p;主动释放不再用的张量。气泡率写作 \(\frac{PP-1}{V\cdot M}\)。
下一页的模拟器里,交错模式有一个 \(N\) 滑块(要求 \(N\ge p\)、\(m\) 为 \(N\) 的倍数),把 \(N\) 拉到 \(m\) 能看到峰值激活变大。
\(N\) 从 \(p\) 拉到 \(m\)
气泡几乎不变,峰值在途激活随 \(N\) 增大(\(N=m\) 时退化成「交错版 AFAB」)。BFS 换来的是 FSDP 下权重聚合次数更少——这部分收益模拟器里看不到。
p2p 流量有多小?
每个 micro-batch、每个级边界:前向 send 一份激活 \([b,s,h]\),反向 send 一份同形状的梯度。bf16 下各 \(2bsh\) 字节,一个 micro-batch 在一个边界上合计 \(4bsh\) 字节。
能否被计算藏住?下一级在这段时间要算 \(l\) 层,前向每层约 \(24bsh^2\) FLOPs(只算矩阵乘,GPT 式 4h MLP):
\(b, s\) 约掉了,只剩 \(h\)、每级层数和算力/带宽比。代入默认值:H100、50 GB/s、\(h=8192\)、\(l=10\),比值只有约 2%——p2p 通信很容易被计算藏住。
默认:H100 稠密 bf16 峰值 989 TFLOPs、单卡 50 GB/s 的 IB(400 Gb/s)。
反向其实是两个矩阵乘
对 \(y = xW\):反向要算两样东西。
B(backward for input):\(\partial L/\partial x = \partial L/\partial y \cdot W^\top\)。上一级的反向等着它——在关键路径上。
W(backward for weights):\(\partial L/\partial W = x^\top \partial L/\partial y\)。没人等它,只要在 optimizer step 前算完。
框架习惯把两者打包成一个 backward(对 DP 很友好:第 \(i\) 层梯度的通信可以和第 \(i-1\) 层反向重叠),但在 PP 里这平白拉长了依赖链。Sea AI Lab 的 Zero Bubble(Qi et al.)把它们拆开调度:W 可以放在同级 B 之后的任何位置,于是 W 成了可以随意挪动、用来填洞的「零件」。
F、B、W 各花多少
一层 Transformer(GPT 式,\(4h\) MLP,\(a\) 个头),只计矩阵乘:
| Pass | FLOPs | 需保留的激活 |
|---|---|---|
| F | sbh(24h+4s) | 0 |
| B | sbh(24h+8s) | sb(34h+5as) |
| W | sbh(24h) | 32sbh |
于是 \(T_W \lt T_F \lt T_B\),\(T_B + T_W = 2T_F\):B 和 W 合起来正是我们熟悉的「反向 ≈ 2 倍前向」。
记号:\(M_B\)/\(M_W\) 是一次 B/W 需要占着的激活显存。B 做完会释放一部分,但要为 W 留下 \(\partial L/\partial z\) 之类的东西,所以 \(M_W \lt M_B\)(但不小:没有 \(5as\) 项时 \(32/34\))。
为了画手工调度,先假设 \(T_F = T_B = T_W\),每个都是 1 个单位。这时 1F1B 的完整反向 = B + W = 2 个单位,正好对应 \(t_b \approx 2t_f\)。
记号提醒:这里的 B 只指「对输入的反向」;DualPipe 那张表里的 B 指完整反向。
把 W 挪出关键路径
B 只等后一级的 B,链条变短,每个 F、B 都能更早开始;W 不在任何人的依赖链上,推迟到尾部原来空等的位置。总工作量没变(每卡 24 个单位),每卡的空闲从 \(3(p-1)=9\) 降到 \(p-1=3\)。这就是下一页的 ZB-H1。
显存不变,气泡变成 1/3
绿色是 W。ZB-H1 沿用 1F1B 的 warm-up,第 \(r\) 级把 W 推迟 \(r\) 轮开始,于是 B 能更早启动、尾部的空洞由迟到的 W 填上。在途激活峰值仍是 \(p\)。
为什么正好少了 \(T_W\)
1F1B 里,第 0 级每次等待反向,等的是「后面 \(p-1\) 级各做一次完整反向」,即 \((p-1)(T_B+T_W)\) 的链条;拆开后链条上只剩 B,W 被挪到链条之外。
而尾部,原先 1F1B 的退坡时间里各级在空等,现在用推迟的 W 填进去。三种调度的账(ZB-H1 里第 \(i\) 级(从 1 数)的峰值激活是 \((p-i+1)M_B + (i-1)M_W\),不超过 \(pM_B\)):
| 调度 | 气泡 | 峰值激活 |
|---|---|---|
| 1F1B | (p−1)(T_F+T_B+T_W) | p·M_B |
| ZB-H1 | (p−1)(T_F+T_B−T_W) | p·M_B |
| ZB-H2 | (p−1)(T_F+T_B−2T_W) | (2p−1)·M_B |
\(T_F=T_B=T_W\) 时:1F1B 是 \(3(p-1)\),ZB-H1 是 \((p-1)\)——正好 1/3;ZB-H2 是 0。
多放前向,梯形变平行四边形
warm-up 里多做前向(第 \(r\) 级最多 \(2(p-r)-1\) 个在途),填掉第一个 B 之前的洞;W 往后挪,越靠后的级尾巴越长。单看一次迭代仍有起止的斜边,但看「稳态周期」:每张卡从第一个任务到最后一个任务之间没有空隙。
绕开 optimizer step 的同步
平行四边形要真正「零气泡」,下一次迭代第 0 级的前向必须能紧接着开始,不等最后一级做完——可惯例做法在 optimizer step 前有一次全流水线 all-reduce:全局梯度范数(梯度裁剪)和全局 NaN/Inf 检查(混合精度)。
Zero Bubble 的办法是事后验证:每级从前一级收到部分归约的全局状态,合上自己的,传给下一级,并按这个部分状态先做 step;完整的全局状态在下一次迭代的 warm-up 中从最后一级传回第一级,若发现上一步不该那么更新(例如其实触发了裁剪或有 NaN),就回滚并重做。
理由:这些全局检查绝大多数时候不起作用,为它每步同步一次不划算。
模拟器里的两个「气泡」
迭代内气泡:迭代时长 − 每卡工作量,假设迭代末全局同步。
稳态气泡:去掉同步、相邻迭代可以咬合时,周期 = 各卡「首个任务开始到最后任务结束」的最大跨度,减去工作量。
对 1F1B 两者相同;对 ZB-H2,后者在 \(T_F=T_B=T_W\) 时为 0。
自动调度与 ZB-V
真实模型里 \(T_F, T_B, T_W\) 不相等,还有 p2p 通信时间 \(T_{comm}\),手工调度会留下洞。于是改成自动生成调度,启发式是:
- warm-up 在显存上限内尽量多放 F;
- 之后 1F-1B 交替,出现大于 \(T_W\) 的空隙就塞 W,显存到上限时也用 W 回收;
- F 和 B 用完后把剩余 W 依次做完。
也可写成整数线性规划(ILP)求解。按激活上限分两档:ZB-1p(\(pM_B\),与 1F1B 相同)和 ZB-2p(\(2pM_B\))。实测相近显存下吞吐比 1F1B 最多高 23%,放宽显存限制后最多高 31%。
ZB-V:每卡恰好 2 段,段的分配走「V 形」:0→p−1 再 p−1→0(16 层 4 卡时卡 0 放 1–2 层和 15–16 层)。于是一个 micro-batch 的前向和反向都从同一张卡开始,第 0 级不必等反向从最后一级传回。
在 \(T_F=T_B=T_W\) 下 ZB-V 以 \(pM_B\) 的峰值激活做到零气泡(约为 ZB-H2 的一半),且各卡显存天然均衡。
模拟器里的 ZB-H2 也是这样贪心生成的:B 优先,F 受在途上限约束,空隙塞 W。
DeepSeek-V3 的难处:通信和计算 1:1
DeepSeek-V3 在 2048 张 H800 上训练:16 路 PP、64 路专家并行(EP,跨 8 个节点)、ZeRO-1 数据并行,没用 TP。
跨节点 EP 意味着每个 MoE 层都有两次 all-to-all(dispatch 把 token 送到专家所在的卡,combine 再送回来)。这让计算和通信的时间大约是 1:1:单纯减少气泡已经不够,必须把通信藏起来。
DualPipe 的两个想法:①在一对「前向块 + 反向块」内部重叠计算和通信;②双向流水线,让每张卡总有一对可以重叠的前向/反向。
硬件背景
节点内 NVLink 160 GB/s,节点间 IB 50 GB/s,约 3.2 倍。
节点受限路由:每个 token 最多发往 4 个节点。先走 IB 到目标节点上同号卡,再经 NVLink 转发——IB 和 NVLink 的传输完全重叠。
只用 20 个 SM 就能吃满 IB 和 NVLink 带宽。
把一对前向/反向块拆开重排
每个块分成四部分:attention、all-to-all dispatch、MLP、all-to-all combine;反向块里的 attention 和 MLP 再按 Zero Bubble 拆成 B 与 W。一个前向块和一个不相关的反向块(不同 micro-batch、不同方向)没有数据依赖,它们的计算可以去填对方的通信时间。
从流水线的两端同时喂数据
把模型切成 \(PP\) 段。卡 \(r\) 同时存正向流水线的第 \(r\) 段和反向流水线的第 \(PP-1-r\) 段——也就是存两份模型参数(\(2\times\))。这个代价可以接受:EP 很大时每卡的专家参数本来就很少,多一份不显著。
\(m\) 个 micro-batch 对半分给两个方向。中间稳态里,每张卡交替执行「一个方向的前向 + 另一个方向的反向」,这一对正好可以做块内重叠。
DualPipe 的时间线
这张时间线按 DualPipe 开源实现 step() 的 8 个阶段生成(不含通信)。半紫半橙的长格是重叠的 F&B 对;浅色是反方向的 micro-batch(编号带 ′)。默认的 \(PP=8, m=20\) 就是 DeepSeek-V3 报告配图的设置。
气泡、参数、激活
记号:\(F\) 前向块,\(B\) 完整反向块,\(W\) 权重反向块,\(F\&B\) 一对重叠的前向+反向块的耗时。
| 方法 | 气泡 | 每卡参数 | 每卡激活 |
|---|---|---|---|
| 1F1B | (PP−1)(F+B) | 1× | PP |
| ZB1P | (PP−1)(F+B−2W) | 1× | PP |
| DualPipe | (PP/2−1)(F&B+B−3W) | 2× | PP+1 |
换回 Zero Bubble 记号(\(B_{zb}\) 只是输入梯度):ZB1P 一行 \(= (PP-1)(T_F+T_{B}-T_W)\),就是 ZB-H1。不建模通信时 \(F\&B = F + B\);取 \(T_F=T_B^{zb}=T_W=1\):1F1B 气泡 \(3(PP-1)\),ZB1P \((PP-1)\),DualPipe \((PP/2-1)\cdot 2 = PP-2\)。上一页模拟器读出的气泡就是这几个数。
真正的收益在通信:前两者的 \(F\)、\(B\) 里还包着暴露的 all-to-all,而 DualPipe 的 \(F\&B\) 把它藏了。另外,它只要求 PP 级数和 micro-batch 数为偶数(实现里还要求 \(m \ge 2PP\)),不像 Chimera 那样要求 \(m\) 被级数整除;\(m\) 增大时气泡和激活都不再增长。
DualPipeV:对半折叠
DualPipe 的两个方向是对称的,存两份参数有点浪费。把它从中间对折(Sea AI Lab 称为 cut-in-half),得到一个 V 形调度:DualPipeV。
同样 \(PP\) 个流水线段,DualPipeV 只用 \(PP/2\) 张卡,每卡 2 段(和 ZB-V 一样是 V 形放置),气泡与 DualPipe 相同 \((PP/2-1)(F\&B+B-3W)\),激活 \(PP+1\)。
每卡仍然存 2 段的参数,但这 2 段是不同的层:整个系统只有一份模型,而不是 DualPipe 那样两份完整副本。
模拟器里只有 DualPipe 本体,没有 DualPipeV。
自己拖:调度、p、m、v、N、T_F / T_B / T_W、通信延迟
点格子高亮一个 micro-batch;「单步」逐个事件推进,右侧柱是该时刻每卡的在途激活。时间单位为 T_F=1 时的一次前向。
依赖 + 每卡程序顺序 = 时间线
每种调度先生成「每张卡依次执行的任务列表」(AFAB、1F1B、交错按 Megatron-LM 的规则;ZB-H1 让「第 \(r\) 级推迟 \(r\) 轮做 W」;DualPipe 按开源实现 step() 的逻辑)。
然后按「尽早开始」模拟:任务开始时间 = max(本卡上一个任务结束, 依赖完成 + 跨卡延迟 \(c\))。依赖只有三种:
- F(级 s) 依赖 F(级 s−1);
- B(级 s) 依赖 B(级 s+1),最后一级依赖自己的 F;
- W 依赖同级的 B。
ZB-H2 用时间步进的贪心:卡空闲时,B 可做就做 B;否则在途数未达 \(2(p-r)-1\) 且 F 可做就做 F;否则有待算的 W 就做 W。
不建模的东西:通信与计算的带宽竞争、F&B 重叠带来的收益(重叠块时长按 \(T_F+T_B+T_W\) 算)、首尾级的额外 embedding/loss、显存的绝对值。
激活以「一级一个 micro-batch 的激活」为单位(交错时每段 \(1/v\)),待算 W 单独计数——它们也占显存(\(M_W\)),只是比 \(M_B\) 小。
六种调度一张表
| 调度 | 气泡(同步,\(c=0\)) | 峰值在途激活 | p2p 次数/mb | 约束 / 代价 |
|---|---|---|---|---|
| AFAB (GPipe) | (p−1)(t_f+t_b) | m | p−1 | 常配重计算 |
| 1F1B | (p−1)(t_f+t_b) | p | p−1 | 需分级调度,首级显存最重 |
| 交错 1F1B | (p−1)(t_f+t_b)/v | ≈p(1+(p−1)/(pv)) | vp−1 | m 为 p 的倍数;通信 ×v |
| ZB-H1 | (p−1)(T_F+T_B−T_W) | p | p−1 | 拆 B/W;W 暂存占显存 |
| ZB-H2 | (p−1)(T_F+T_B−2T_W) | 2p−1 | p−1 | 需绕过 optimizer 同步 |
| DualPipe | (p/2−1)(F&B+B−3W) | p+1 | p−1 | 2× 参数;p、m 偶数;为 EP 重叠而生 |
交错一行是第 0 级的峰值:\(p + (p-1)/v\) 份(以一级的完整激活为单位),\(p=4,v=2\) 时为 5.5。
PP vs ZeRO-3,以及搭配
| ZeRO-3 (FSDP) | PP | |
|---|---|---|
| 每卡存 | 一层的一部分 | 完整的若干层 |
| 通信传 | 权重 | 激活 |
| 与模型的耦合 | 无关 | 无关 |
| 实现难点 | 切分与通信管理 | 高效调度 |
| 喜欢 | 大 mbs、长序列藏通信 | 大梯度累积藏气泡 |
两者都把模型参数分散到多张卡上。它们可以组合,但实践中少见:两边的通信都要靠大全局 batch 来摊薄,叠在一起对 batch 的要求太高。真要组合,ZeRO-3 应在整串 micro-batch 期间保留聚合好的权重,而不是每个 micro-batch 都重新 all-gather 一遍。
PP + ZeRO-1/2 没有新问题,DeepSeek-V3 就是 PP + ZeRO-1。TP(+SP)与 PP 互补:TP 放节点内,PP 放跨节点。Llama 3 405B 的组合是 [TP, CP, PP, DP] 由内到外。
怎么选调度
- 先问要不要 PP:模型(+优化器状态)在 TP×ZeRO 下装得进节点,就别用 PP。
- 要用就从 1F1B 起步,把 \(m\) 拉到 \(\gg p\)(\(m \ge 4p\) 是常用的经验线);受全局 batch 限制时提高 DP 度 \(d\) 或减小 \(p\)。
- \(m\) 拉不大、而节点间带宽尚可:上 交错,\(v=2\)–4;注意 \(m\) 要是 \(p\) 的倍数,通信 \(\times v\)。
- 框架支持拆 B/W:ZB-H1 / ZB-1p 几乎是免费午餐(显存同 1F1B);显存有余量再考虑 ZB-2p / ZB-V。
- 大规模跨节点 MoE、通信与计算同量级:DualPipe / DualPipeV 的价值主要在藏 all-to-all。
- 任何调度都别忘了首尾级配平(少放一层)和显存最重的那一级。
参考与延伸阅读
- Ultra-Scale Playbook:Pipeline Parallelism 各节、5D Parallelism、附录 PP communication analysis
- Scaling Book · Training:Pipelining 一节
- GPipe(Huang et al., 2019)
- PipeDream(Harlap et al., 2018)
- Megatron-LM 2021(Narayanan et al.):PipeDream-Flush、交错调度、scatter/gather
- Breadth-First Pipeline Parallelism(Lamy-Poirier)
- The Llama 3 Herd of Models:§3.3.2 Pipeline parallelism improvements
- Zero Bubble Pipeline Parallelism(Qi et al.)
- DeepSeek-V3 Technical Report:§3.2.1–3.2.2
- deepseek-ai/DualPipe(调度代码与 DualPipeV)
- Korthikanti et al., Reducing Activation Recomputation in Large Transformer Models(激活估算)
想看更完整的推导和实测,推荐从 Ultra-Scale Playbook 的流水线并行各节读起。
下一站:MoE 与专家并行
DualPipe 要藏的 all-to-all,到底在传什么