📑 本页目录(点开跳转)
13 · 流水线并行
⏱ 26 分钟 | ⭐ 通信最省,但有个叫"气泡"的敌人
🎯 一句话
把模型按层切段,每张卡负责几层,数据像流水线一样流过去。 它的通信量比张量并行小两个数量级,所以它是唯一能舒服跨机的模型并行 —— 但它天生会浪费一部分算力,而这一章就是在和那个浪费搏斗。
🏭 一、基本结构
通信量对比(batch=8, seq=2048, hidden=8192):
张量并行:每层 4 次 AllReduce → 80 层共 86 GB/步
流水线并行:每个切分点传一次激活
3 个切分点 × 268MB × 2(前向+反向)≈ 1.6 GB/步
⭐ 差了 50 倍以上 —— 这就是它能跨机的原因
🫧 二、气泡:流水线的原罪
朴素做法(一个 batch 走完再来下一个):
时间 ────────────────────────────────────►
GPU0 [F1] [B1]
GPU1 [F1] [B1]
GPU2 [F1] [B1]
GPU3 [F1][B1]
░░░░░ ← 大量空闲(气泡)░░░░░
⭐ 任何时刻只有【一张卡】在干活,其他 3 张在等 💀
→ 4 卡的效率还不如 1 卡
解法:把 batch 切成 micro-batch
GPipe(把 batch 切成 4 个 micro-batch):
GPU0 F1 F2 F3 F4 B4 B3 B2 B1
GPU1 F1 F2 F3 F4 B4 B3 B2 B1
GPU2 F1 F2 F3 F4 B4 B3 B2 B1
GPU3 F1 F2 F3 F4 B4 B3 B2 B1
░░░░░░░░░ ░░░░░░░░░░
↑ 开头的气泡 ↑ 结尾的气泡
$$\text{气泡占比} = \frac{P-1}{M+P-1}$$
(P = 流水线级数,M = micro-batch 数)
P=4, M=4 → 气泡 3/7 = 43% 💀
P=4, M=16 → 气泡 3/19 = 16% ⚠️
P=4, M=64 → 气泡 3/67 = 4.5% ✅
P=8, M=64 → 气泡 7/71 = 10%
🔑 黄金法则:M ≥ 4P,最好 M ≥ 8P。 但 micro-batch 越多,需要缓存的激活也越多 —— 这就是下面 1F1B 要解决的。
⚡ 三、1F1B:现代标准调度
GPipe 的问题:所有 F 做完才开始 B
→ 【M 个 micro-batch 的激活】全部要缓存 💀
✅ 1F1B(One Forward One Backward):
进入稳定态后,每做一次前向就【立刻】做一次反向
→ 反向做完就能释放那个 micro-batch 的激活
⭐ 激活显存从 O(M) 降到 O(P)
而气泡率【完全不变】—— 纯赚 ⭐
1F1B 时间轴(P=4):
GPU0 F1 F2 F3 F4 B1 F5 B2 F6 B3 ...
GPU1 F1 F2 F3 B1 F4 B2 F5 B3 ...
GPU2 F1 F2 B1 F3 B2 F4 B3 ...
GPU3 F1 B1 F2 B2 F3 B3 ...
↑ 最深的卡最早开始反向
💡 注意 GPU3(最后一级)几乎没有等待 —— 它的前向一做完就能立刻反向。 而 GPU0 要等最久。这种不均衡是流水线的固有特征。
⭐ 交错式 1F1B(Interleaved)
想法:让每张卡负责【不连续的多段】层
GPU0: 层 1-10 和 层 41-50
GPU1: 层 11-20 和 层 51-60
...
→ 流水线"变长"了(虚拟级数 = P × v)
→ 气泡占比降到 (P-1)/(v·M+P-1) ⭐ 再降 v 倍
⚠️ 代价:通信次数变成 v 倍
⚖️ 四、切分策略:负载均衡是关键
⚠️ 流水线的速度取决于【最慢的那一级】
常见的不均衡来源:
├─ 第一级要算 Embedding(词表很大)
├─ 最后一级要算 LM Head + 损失(词表 × hidden,很重)⭐
└─ 有些层结构不同(比如 MoE 层)
❌ 均匀切:每卡 20 层
→ 最后一级因为 LM Head 慢 30%
→ 【所有卡都被它拖慢 30%】
✅ 按实测耗时切:
GPU0: 22 层(含 Embedding)
GPU1: 21 层
GPU2: 21 层
GPU3: 16 层 + LM Head ⭐ 层数少一些来补偿
🔑 实践建议:先跑一次 profiling,测出每层的实际耗时,再切。 均匀切层数几乎总是次优的。
🔨 五、代码
# PyTorch 原生(2.x)
from torch.distributed.pipelining import pipeline, ScheduleGPipe, Schedule1F1B
pipe = pipeline(module=model, mb_args=(example_input,),
split_spec={"layers.20": SplitPoint.BEGINNING,
"layers.40": SplitPoint.BEGINNING,
"layers.60": SplitPoint.BEGINNING})
stage = pipe.build_stage(rank, device)
sched = Schedule1F1B(stage, n_microbatches=64) # ⭐ M ≥ 4P
for batch in loader:
if rank == 0: sched.step(batch)
elif rank == last: losses = sched.step(target=labels)
else: sched.step()
💡 DeepSpeed 和 Megatron 也都提供流水线 —— Megatron 的
--num-layers-per-virtual-pipeline-stage就是交错式 1F1B。
⚠️ 六、四个坑
| 坑 | 说明 |
|---|---|
| micro-batch 太少 | 气泡吃掉一半算力 → M ≥ 4P ⭐ |
| 切分点不均衡 | 被最慢一级拖住 → 按实测耗时切 ⭐ |
| BatchNorm | 跨 micro-batch 的统计会错 → 用 LayerNorm |
| 调试困难 | 报错栈横跨多个进程 |
💥 一个容易忽略的点:流水线并行改变了梯度累积的语义。 M 个 micro-batch 的梯度是累加的,所以等效 batch = micro_batch × M, 学习率要按这个来调,而不是按 micro_batch。
📊 七、三种并行的对比(重要)
| 数据并行 | 张量并行 | 流水线并行 | |
|---|---|---|---|
| 切什么 | 数据 | 层内的矩阵 | 层(按深度) |
| 通信频率 | 每步一次 | 每层 4 次 | 每个切分点一次 |
| 通信量 | 2× 参数量 | 很大 | 很小 ⭐ |
| 能跨机 | ✅ | ❌ | ✅ ⭐ |
| 省参数显存 | ❌ | ✅ | ✅ |
| 主要缺点 | 不省显存 | 必须机内 | 气泡 |
| 单层装不下 | ❌ | ✅ | ❌ |
⭐ 三者互补,不是三选一 —— 第 14 章讲怎么组合。
🔗 和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 第 5 章 跨机带宽小 | 流水线能跨机的原因 ⭐ |
| 第 12 章 TP 通信 86GB/步 | 对比:PP 只要 1.6GB |
| 第 9 章 梯度累积 | micro-batch 就是它 |
| 第 14 章 3D 并行 | PP 放在跨机维度 |
| 《Kaggle竞赛方法论》02 训练策略与正则化 梯度累加 | 那里它只是"省显存的招";在流水线里它是气泡公式的 M,直接决定效率 ⭐ |
| 《大模型全景导论》02 Transformer 原理 一层长什么样 | 切分点必须落在层边界上;embedding / lm_head 比中间层重得多,是负载不均的主因 |
✅ 检查点
- 流水线并行怎么切?为什么它的通信量这么小?
- 什么是气泡?朴素做法下 4 卡的效率如何?
- 气泡占比的公式是什么?P=4、M=4 和 M=64 分别是多少?
- 黄金法则是什么?为什么不能无限增大 M?
- 1F1B 相比 GPipe 改进了什么?气泡率变了吗?
- 交错式 1F1B 的思路和代价是什么?
- 为什么均匀切层数几乎总是次优的?
- 流水线并行怎么改变了学习率的调法?
- 三种并行里,哪些能跨机?哪个能救"单层装不下"?
👀 答案
- 按层切段,每卡负责几层,数据像流水线流过。通信量小是因为卡之间只在切分点传递激活——3 个切分点约 1.6GB/步,而张量并行是 86GB/步,差 50 倍以上。
- 气泡是流水线填充和排空阶段的空闲。朴素做法下任何时刻只有一张卡在干活,4 卡的效率还不如 1 卡。
- (P−1)/(M+P−1)。P=4,M=4 → 3/7 = 43%;P=4,M=64 → 3/67 = 4.5%。
- M ≥ 4P(最好 8P)。不能无限增大是因为 micro-batch 越多需要缓存的激活越多(GPipe 下是 O(M))。
- 进入稳定态后每做一次前向立刻做一次反向,反向完就释放该 micro-batch 的激活 → 激活显存从 O(M) 降到 O(P),而气泡率完全不变,纯赚。
- 让每卡负责不连续的多段层,虚拟级数变成 P×v,气泡降到 (P−1)/(v·M+P−1),再降 v 倍。代价:通信次数变成 v 倍。
- 因为流水线的速度取决于最慢的一级,而第一级有 Embedding、最后一级有 LM Head(词表×hidden,很重)。均匀切会让最后一级拖慢所有卡。应该先 profiling 测每层实际耗时再切。
- M 个 micro-batch 的梯度是累加的,所以等效 batch = micro_batch × M,学习率要按等效 batch 调,不是按 micro_batch。
- 数据并行和流水线并行能跨机,张量并行不能。只有张量并行能救"单层装不下"。
🛑 可以停在这里
⚡ 走神救援
流水线并行 = 按层切段,数据像流水线流过;⭐通信量只有张量并行的 1/50(1.6GB vs 86GB 每步,因为只在切分点传激活)→ 唯一能舒服跨机的模型并行。💀原罪是气泡:朴素做法下任何时刻只有一张卡干活,4 卡还不如 1 卡。解法是切 micro-batch,⭐气泡占比 = (P−1)/(M+P−1)(P=4,M=4 → 43%;M=64 → 4.5%)→ ⭐黄金法则 M ≥ 4P,但 M 越大缓存的激活越多。⭐1F1B:稳定态后每做一次前向立刻做一次反向,反向完就释放激活 → 激活显存从 O(M) 降到 O(P),而气泡率完全不变,纯赚(最后一级几乎不等待,GPU0 等最久,这种不均衡是固有的)。交错式 1F1B:每卡负责不连续的多段,虚拟级数 P×v,气泡再降 v 倍,代价是通信次数 v 倍。⭐负载均衡是关键——速度取决于最慢的一级:第一级有 Embedding、最后一级有 LM Head(很重),⭐均匀切层数几乎总是次优,要先 profiling 测每层耗时再切(最后一级少给几层来补偿)。⚠️四个坑:M 太少、切分不均衡、BatchNorm 跨 micro-batch 统计会错(用 LayerNorm)、调试困难(先单卡跑通);💥还有一个容易忽略的:等效 batch = micro_batch × M,学习率要按等效 batch 调。三种并行对比:数据并行不省显存但能跨机、张量并行通信巨大必须机内但唯一能救"单层装不下"、流水线通信极小能跨机但有气泡——⭐三者互补不是三选一。
下一节 👉 14-并行策略怎么组合.md ⭐⭐