大模型规模化训练 · 流水线并行

流水线并行:从 GPipe 到 DualPipe

切层、气泡、在途激活,以及把气泡一点点挤掉的几代调度

按 → 或点击翻页 · 带 交互 的页面可以直接拖动

路线

目录

  1. 为什么要按层切模型,切完之后多了什么通信
  2. 气泡(bubble):定义与 \((p-1)/m\) 的推导
  3. AFAB / GPipe:先全前向再全反向,显存随 \(m\) 涨
  4. 1F1B:气泡不变,在途激活封顶为 \(p\)
  1. 交错(interleaved):每卡 \(v\) 段,气泡 \(\div v\),通信 \(\times v\)
  2. Zero Bubble:把反向拆成 B 和 W,用 W 填洞
  3. DualPipe:双向喂数据,再把 EP 的 all-to-all 藏进计算
  4. 一个能拖动所有参数的调度模拟器 + 选型

记号:\(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\) 级的反向:

$$t_{pb} = (p-1)\,(t_f + t_b)$$

「气泡有多大」有两种常见说法,读论文时别混:

气泡率:相对理想时间

$$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\) 不能想多大就多大。

AFAB / GPipe

All-Forward-All-Backward

先把 \(m\) 个 micro-batch 的前向全做完,再做全部反向。实现最简单:训练循环仍然是「前向 → 反向」两段。点一个格子可以高亮那个 micro-batch 走过的路径。

AFAB 的气泡

气泡被 \(m\) 摊薄

开头仍然是 \(p-1\) 个前向的爬坡,结尾仍然是 \(p-1\) 个反向的退坡,气泡的绝对量没变:\(t_{pb} = (p-1)(t_f+t_b)\)。

但理想时间变成了 \(m\) 倍:\(t_{id} = m(t_f+t_b)\)。于是

$$r_{bubble} = \frac{(p-1)(t_f+t_b)}{m\,(t_f+t_b)} = \frac{p-1}{m},\qquad \text{空闲占比} = \frac{p-1}{m+p-1}$$

直觉:爬坡和退坡是固定成本,\(m\) 越大,中间「所有卡都在忙」的部分越长,固定成本被摊得越薄。GPipe 的实验里,\(m \ge 4p\) 时气泡已经几乎可以忽略。

例:\(p=8\):\(m=8\) 时 \(r=0.875\)(空闲 47%);\(m=32\) 时 \(r\approx0.22\)(空闲 18%);\(m=64\) 时空闲 10%。

AFAB 的代价

所有 micro-batch 的激活同时在途

反向要用前向存下的激活。AFAB 里第一个反向开始之前,\(m\) 个 micro-batch 的前向都做完了——每张卡要同时存 \(m\) 份本级激活。

每层激活的经典估算(Korthikanti et al.,16-bit 激活、无 TP/SP、无重计算):

$$M_{layer} \approx s\,b\,h\left(34 + \frac{5\,a\,s}{h}\right)\ \text{bytes}$$

\(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 会改变常数,不改变量级。

GPipe 的补丁

重计算(activation recomputation)

用时间换显存(GPipe 的做法):前向只存每级边界的输入,反向前再把本级前向算一遍。

$$\text{峰值激活} = O\!\left(N + \frac{L}{K}\cdot\frac{N}{M}\right)$$

\(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}}\) 时最小。

1F1B

尽早开始反向

问题出在「所有前向做完才开始反向」。只要最后一级做完某个 micro-batch 的前向,它就可以立刻做那个 micro-batch 的反向,把激活释放掉。

第 \(r\) 级(从 0 数)的程序分三段:

  1. warm-up:先做 \(p-r-1\) 个前向(越靠前的级做得越多);
  2. steady:一个前向、一个反向交替(one-forward-one-backward);
  3. cool-down:把剩下 \(p-r-1\) 个 micro-batch 的反向做完。

最早出现在 PipeDream(异步版本,靠 weight stashing 保存多版本权重);带 flush 的同步版本叫 PipeDream-Flush,就是 Megatron-LM 默认的调度。

1F1B · 交互

看它的时间线

右侧小柱是播放光标所在时刻每张卡的在途激活(前向已做、反向未做的 micro-batch 数),播放结束后显示每卡的峰值。拖大 \(m\):气泡率下降,而在途激活的峰值始终是 \(p\)。

AFAB vs 1F1B

同样的气泡,不一样的在途激活

两条时间线一样长:开头 \(p-1\) 步爬坡、结尾 \(p-1\) 步退坡都还在,每卡空闲 \((p-1)(t_f+t_b)\)。差别在右边——AFAB 每张卡都要攒满 \(m=8\) 份激活,1F1B 的第 \(r\) 级最多 \(p-r\) 份。

1F1B 的账

气泡没变,显存封顶

为什么气泡一样大

对第 0 级:它在 \((p-1)\) 个前向之后才能收到第一个反向;结尾最后一个反向要等后面 \(p-1\) 级依次退回来。不管中间怎么排,开头和结尾的等待都在。数一下就是 \((p-1)(t_f+t_b)\),于是仍然

$$r_{bubble} = \frac{p-1}{m}$$

但在途激活从 \(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 更扛得住;它的主要敌人是气泡,不是带宽。

PP × DP

在固定 GPU 数和全局 batch 下

卡数 \(n\) 固定、不用 TP 时,PP 度和 DP 度此消彼长:\(p = n/d\)。全局 batch 有 \(b' = B/b\) 个 micro-batch,分给 \(d\) 条流水线,每条 \(m = b'/d\)。代入气泡率:

$$\frac{p-1}{m} = \frac{n/d - 1}{b'/d} = \frac{n-d}{b'}$$

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\) 步,但每步变短了:

$$t^{int}_{pb} = \frac{(p-1)(t_f+t_b)}{v},\qquad r_{bubble} = \frac{1}{v}\cdot\frac{p-1}{m}$$

代价一:每个 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 份)。

交错 1F1B · 交互

拖 \(v\) 看气泡缩小

颜色深浅区分同一张卡上的不同段。把「p2p 延迟」拉起来:每跨一次级边界多等 \(c\),交错因为跨边界次数多,受伤更重。这就是「通信 \(\times v\)」在时间线上的样子。

深度优先 vs 广度优先

同一时刻先做哪个?

交错之后,一张卡上同时可能有「早的 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\) 能看到峰值激活变大。

DFS ↔ BFS · 交互

\(N\) 从 \(p\) 拉到 \(m\)

气泡几乎不变,峰值在途激活随 \(N\) 增大(\(N=m\) 时退化成「交错版 AFAB」)。BFS 换来的是 FSDP 下权重聚合次数更少——这部分收益模拟器里看不到。

PP 的通信

p2p 流量有多小?

每个 micro-batch、每个级边界:前向 send 一份激活 \([b,s,h]\),反向 send 一份同形状的梯度。bf16 下各 \(2bsh\) 字节,一个 micro-batch 在一个边界上合计 \(4bsh\) 字节。

能否被计算藏住?下一级在这段时间要算 \(l\) 层,前向每层约 \(24bsh^2\) FLOPs(只算矩阵乘,GPT 式 4h MLP):

$$\frac{t_{comm}}{t_{comp}} \approx \frac{2bsh / BW}{24\,bsh^2\,l / F} = \frac{F}{12\,h\,l\,BW}$$

\(b, s\) 约掉了,只剩 \(h\)、每级层数和算力/带宽比。代入默认值:H100、50 GB/s、\(h=8192\)、\(l=10\),比值只有约 2%——p2p 通信很容易被计算藏住。

t_comm / t_comp
每 mb 每边界 (s=4096,b=1)

默认:H100 稠密 bf16 峰值 989 TFLOPs、单卡 50 GB/s 的 IB(400 Gb/s)。

Zero Bubble · 关键观察

反向其实是两个矩阵乘

x(上一层) F: y = xW y ∂L/∂y B: ∂L/∂y · Wᵀ W: xᵀ · ∂L/∂y ∂L/∂x 传给上一级 → 关键路径 ∂L/∂W 只在 optimizer step 前需要

对 \(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 成了可以随意挪动、用来填洞的「零件」。

Zero Bubble · 账本

F、B、W 各花多少

一层 Transformer(GPT 式,\(4h\) MLP,\(a\) 个头),只计矩阵乘:

PassFLOPs需保留的激活
Fsbh(24h+4s)0
Bsbh(24h+8s)sb(34h+5as)
Wsbh(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 指完整反向。

Zero Bubble · 拆开之后

把 W 挪出关键路径

B 只等后一级的 B,链条变短,每个 F、B 都能更早开始;W 不在任何人的依赖链上,推迟到尾部原来空等的位置。总工作量没变(每卡 24 个单位),每卡的空闲从 \(3(p-1)=9\) 降到 \(p-1=3\)。这就是下一页的 ZB-H1。

ZB-H1 · 交互

显存不变,气泡变成 1/3

绿色是 W。ZB-H1 沿用 1F1B 的 warm-up,第 \(r\) 级把 W 推迟 \(r\) 轮开始,于是 B 能更早启动、尾部的空洞由迟到的 W 填上。在途激活峰值仍是 \(p\)。

ZB-H1 的推导

为什么正好少了 \(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。

ZB-H2 · 交互

多放前向,梯形变平行四边形

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。

Zero Bubble · 落地

自动调度与 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。

DualPipe · 动机

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 带宽。

DualPipe · 块内重叠

把一对前向/反向块拆开重排

计算流 通信流 (不按比例)前向块的 dispatch/combine 落在反向块的计算期间,反向块的 dispatch/combine 落在前向块的计算期间。 实际实现还手工调节通信与计算各占多少 SM,并插入 PP 通信;调好之后 all-to-all 和 PP 通信都能被完全藏住。

每个块分成四部分:attention、all-to-all dispatch、MLP、all-to-all combine;反向块里的 attention 和 MLP 再按 Zero Bubble 拆成 B 与 W。一个前向块和一个不相关的反向块(不同 micro-batch、不同方向)没有数据依赖,它们的计算可以去填对方的通信时间。

DualPipe · 双向

从流水线的两端同时喂数据

把模型切成 \(PP\) 段。卡 \(r\) 同时存正向流水线的第 \(r\) 段和反向流水线的第 \(PP-1-r\) 段——也就是存两份模型参数(\(2\times\))。这个代价可以接受:EP 很大时每卡的专家参数本来就很少,多一份不显著。

\(m\) 个 micro-batch 对半分给两个方向。中间稳态里,每张卡交替执行「一个方向的前向 + 另一个方向的反向」,这一对正好可以做块内重叠。

DualPipe · 交互

DualPipe 的时间线

这张时间线按 DualPipe 开源实现 step() 的 8 个阶段生成(不含通信)。半紫半橙的长格是重叠的 F&B 对;浅色是反方向的 micro-batch(编号带 ′)。默认的 \(PP=8, m=20\) 就是 DeepSeek-V3 报告配图的设置。

DualPipe · 账本

气泡、参数、激活

记号:\(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)mp−1常配重计算
1F1B(p−1)(t_f+t_b)pp−1需分级调度,首级显存最重
交错 1F1B(p−1)(t_f+t_b)/v≈p(1+(p−1)/(pv))vp−1m 为 p 的倍数;通信 ×v
ZB-H1(p−1)(T_F+T_B−T_W)pp−1拆 B/W;W 暂存占显存
ZB-H2(p−1)(T_F+T_B−2T_W)2p−1p−1需绕过 optimizer 同步
DualPipe(p/2−1)(F&B+B−3W)p+1p−12× 参数;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 的流水线并行各节读起。

系列 · 大模型规模化训练

下一站:MoE 与专家并行

DualPipe 要藏的 all-to-all,到底在传什么

← 回到博客