第 8 讲:并行 II —— 流水线、张量、序列、专家并行与 3D 并行

Language Modeling from Scratch · Lecture 08。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:Stanford CS336 Spring 2026 Lecture 8(Parallelism,Tatsunori Hashimoto 的幻灯片),以及 Lecture 7 可执行讲义中张量并行、流水线并行的最简实现。Spring 2025 的第 7、8 讲内容相近,有公开录像。 配套代码:code/lecture08/ 预计阅读时间:3–4 小时 硬件说明:不需要 GPU。多进程代码沿用第 7 讲的 dist_utils.py,用 CPU 进程模拟多块 GPU。


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 说出数据并行(含 ZeRO)的局限,以及”模型并行”与 ZeRO-3 的根本区别:传激活值 vs 传参数。
  2. 画出流水线并行的 GPipe、1F1B 时间线,推导气泡比例 (p−1)/(m+p−1)(p-1)/(m+p-1),说明 1F1B 为什么省显存,并了解交错调度、零气泡调度。
  3. 写出 Megatron 张量并行的 MLP 和注意力(按列切、按行切、f / g 算子),算出每层通信量 8bsh(t−1)/t8bsh(t-1)/t,解释为什么张量并行只在机器内部用。
  4. 推导激活值公式 sbh(34+5as/h)sbh(34 + 5as/h),说明序列并行怎样在不增加通信的前提下把剩下的激活值也切开。
  5. 解释上下文并行(环形注意力)如何用在线 softmax 合并部分结果,以及因果掩码下的负载均衡问题。
  6. 解释专家并行的两次 all-to-all,以及负载不均衡的后果。
  7. 用经验法则和粗略的估算,为给定的模型和集群选择 3D 并行配置,并读懂 Llama 3、DeepSeek-V3 等模型的并行配置。

0.2 先修知识

知识点用在哪里最低要求
第 7 讲全文集合通信原语、环形 all-reduce 的通信量、DDP、ZeRO、dist_utils.launch
第 3 讲第 3、4 节Transformer 层的结构(注意力、MLP、归一化、残差)
第 4 讲第 6 节MoE 的路由、负载均衡
第 5 讲第 5 节FlashAttention 的在线 softmax
第 2 讲第 4 节激活值显存、激活检查点(重计算)

0.3 本讲地图

第1节 从数据并行到模型并行
第2节 流水线并行:按层切(GPipe、1F1B、气泡、交错与零气泡调度)
第3节 张量并行:按矩阵切(Megatron:列并行 + 行并行)
第4节 激活值显存与序列并行
第5节 上下文并行:环形注意力
第6节 专家并行:all-to-all
第7节 组合起来:3D / 4D 并行,经验法则与实际案例

1. 从数据并行到模型并行

1.1 数据并行还剩下什么问题?

上一讲的数据并行 + ZeRO 已经很强大了,但 CS336 课件指出了两类问题:

计算扩展的问题。数据并行的卡数不能超过批次大小;而且批次越大,收益递减(超过临界批量大小后,更大的批次几乎不能再减少训练步数,第 9 讲)。另外,每块卡每步要处理几千个 token,才能把跨机器的梯度通信藏住(第 7 讲 6.6 节)。

显存扩展的问题。ZeRO-1、ZeRO-2 不能让参数本身随卡数减少;ZeRO-3 在原理上可以,但它不减少激活值,而且每一步都要把整个模型的参数传两遍。

1.2 模型并行:切模型,传激活值

模型并行(model parallelism)的思路:

  • 和 ZeRO-3 一样,把参数切开,分到不同的 GPU 上;
  • 但和 ZeRO-3 不同,它不传参数,传激活值。每块 GPU 永久保存自己那部分参数,数据(激活值)流过它的时候就地计算。

模型有好几种”切法”,对应本讲的几种并行:

切法名字传什么
按深度(层)切流水线并行(PP)阶段之间的激活值(点对点)
按宽度(每层的矩阵)切张量并行(TP)每层内部的部分和(all-reduce)
按专家切专家并行(EP)派发给各专家的 token(all-to-all)
按序列长度切序列并行(SP)、上下文并行(CP)激活值的各段序列 / K、V 块

2. 流水线并行:按层切

2.1 最朴素的按层切

把一个 LL 层的模型切成 pp 段(阶段,stage),GPU ii 负责第 ii 段。前向时,GPU 0 算完把激活值发给 GPU 1,GPU 1 算完发给 GPU 2……反向时反过来,激活值的梯度一段一段传回去。

问题一眼就能看出来:任何时刻只有一块 GPU 在干活。01_pipeline_schedules.py 画出的时间线(每个字符是一个时间单位,数字是前向,字母是反向,前向 1 格、反向 2 格,. 是空闲):

朴素(m = 1):气泡 75%
  阶段 0:0.........aa
  阶段 1:.0......aa..
  阶段 2:..0...aa....
  阶段 3:...0aa......

pp 块 GPU,每块只有 1/p1/p 的时间在工作。这些空闲的时间叫气泡(bubble)。

2.2 GPipe:切成微批次

解决办法(GPipe,Huang et al. 2019):把批次切成 mm 个微批次(micro-batch),像工厂流水线一样一个接一个地送进去。GPU 0 算完第 1 个微批次,发给 GPU 1,马上开始第 2 个:

GPipe(m = 8):气泡 27.3%
  阶段 0:01234567.........aabbccddeeffgghh
  阶段 1:.01234567......aabbccddeeffgghh..
  阶段 2:..01234567...aabbccddeeffgghh....
  阶段 3:...01234567aabbccddeeffgghh......

开头和结尾各剩下一个”三角形”的空闲区。算一下它有多大:流水线要走 m+p−1m + p - 1 个”格子”才能让所有微批次通过所有阶段,每个阶段真正干活的是其中 mm 个。所以:

气泡比例=p−1m+p−1(CS336 课件的写法:气泡时间有用时间=p−1m)\text{气泡比例} = \frac{p-1}{m+p-1} \qquad\left(\text{CS336 课件的写法:}\frac{\text{气泡时间}}{\text{有用时间}} = \frac{p-1}{m}\right)

模拟器对多种 pp、mm 验证了这个公式(完全一致):

ppmm = 148163264
475.0%42.9%27.3%15.8%8.6%4.5%
887.5%63.6%46.7%30.4%17.9%9.9%

**结论:微批次数 mm 必须远大于阶段数 pp。**这就是 CS336 课件强调的”流水线并行需要大批次”。

2.3 1F1B:同样的气泡,更少的激活显存

GPipe 有一个隐藏的代价:每个微批次的前向做完后,它的激活值要一直保存到它的反向做完。GPipe 先做完全部 mm 个前向,所以每个阶段同时要存 mm 份激活值。

1F1B(one-forward-one-backward,也叫 PipeDream-Flush)调整了顺序:阶段 ss 先做 p−s−1p - s - 1 个前向”热身”,之后每做一个前向,就做一个反向:

1F1B(m = 8):气泡 27.3%,各阶段最多同时保存 [4, 3, 2, 1] 份激活值
  阶段 0:0123......aa4bb5cc6dd7ee.ff.gg.hh
  阶段 1:.012....aa3bb4cc5dd6ee7ff.gg.hh..
  阶段 2:..01..aa2bb3cc4dd5ee6ff7gg.hh....
  阶段 3:...0aa1bb2cc3dd4ee5ff6gg7hh......

总耗时和气泡与 GPipe 完全相同,但每个阶段最多同时存 pp 份激活值(而不是 mm 份)。mm 越大,省得越多。这让我们可以放心地调大 mm 来压缩气泡。

2.4 真的跑一个流水线

02_pipeline_parallel.py 用 4 个进程运行一个 8 层 MLP 的流水线(每个进程 2 层)。CS336 课上的代码只实现了前向,这里把反向也补上了:

# 前向:从上一个阶段收激活值,算完发给下一个阶段
inp = torch.empty(...); dist.recv(inp, src=rank - 1); inp.requires_grad_(True)
out = stage(inp)
dist.send(out.detach(), dst=rank + 1)

# 反向:从下一个阶段收"输出的梯度",反向传播,把"输入的梯度"发给上一个阶段
grad_out = torch.empty(...); dist.recv(grad_out, src=rank + 1)
out.backward(grad_out)
dist.send(inp.grad, dst=rank - 1)

正确性:mm = 1、4、8 时,流水线算出的梯度与单进程整批计算的差异都在 10−910^{-9} 以下。

实测气泡(8 层 × Linear(1024),全局批次 1024,本机 CPU;多进程计时每次运行会有小的波动,看趋势即可):

mm微批次大小每步耗时平均忙碌比例公式 m/(m+p−1)m/(m+p-1)
1102433.0 ms22%25%
251228.7 ms37%40%
425627.4 ms53%57%
812831.5 ms64%73%
166450.9 ms70%84%

两个发现:

  1. 忙碌比例的趋势与公式一致,只是略低(公式忽略了通信和调度开销)。
  2. 但总耗时并不是 mm 越大越好:mm = 4 时最快。mm 太大,每个微批次太小,矩阵乘法的效率下降、固定开销占比上升。所以流水线需要的不只是”很多微批次”,而是”很多个足够大的微批次”——也就是很大的总批次。

一个计时的坑:这个文件最初的版本测出来,无论 mm 多大,忙碌比例都是 25%。原因是每个进程第一次做反向传播时要做几十毫秒的一次性初始化,而流水线的反向是一个阶段接一个阶段进行的,这些一次性开销被串了起来,完全掩盖了真正的气泡。解决办法就是第 6 讲说的”先热身再计时”:同一个训练步跑 3 次,只计最后一次。

2.5 更好的调度

交错调度(interleaved 1F1B,Narayanan et al. 2021):每块 GPU 不再负责连续的一段层,而是负责 vv 个不相邻的”虚拟阶段”(例如 GPU 0 负责第 1、9、17……块)。每个虚拟阶段更薄,流水线填满得更快,气泡缩小到约 (p−1)/(v⋅m)(p-1)/(v\cdot m)。代价是阶段之间的通信次数变成 vv 倍。CS336 课件的说法:更”疯狂”的流水线模式可以用通信带宽换利用率。

零气泡调度(zero bubble,Qi et al. 2023):把反向拆成两部分——

  • B:算输入(激活值)的梯度,下一个阶段在等它,必须尽快做;
  • W:算权重的梯度,没有人等它,什么时候做都行。

把 W 塞进原本的气泡里,气泡几乎可以完全消除。DeepSeek-V3 的 DualPipe 调度也用了这种拆分,并把 MoE 的 all-to-all 通信和计算重叠起来。

2.6 流水线并行:为什么用、什么时候用

流水线看起来很麻烦(气泡、需要大批次、实现复杂),为什么还要用?CS336 课件给出两个理由:

  1. 省显存:每块 GPU 只存 L/pL/p 层的参数(和对应的优化器状态)。
  2. 通信性质好:只在相邻阶段之间传激活值(大小 b×s×hb \times s \times h),是点对点的,量很小。相比之下,FSDP 每步要传两遍全部参数。

所以:**流水线并行通常用在较慢的链路上(机器之间),用来获得随机器数线性减少的显存。**它的缺点是气泡、对批次大小的要求、以及实现的复杂度(CS336 课件的总结表里,它的”易用性”是”难”)。


3. 张量并行:按矩阵切

3.1 矩阵乘法的两种切法

能不能不按深度切,而是按宽度切——把每一层的矩阵切开?矩阵乘法本身提供了两种切法:

按列切:  X [A1,A2]=[XA1,  XA2]按行切:  [Y1,Y2][B1B2]=Y1B1+Y2B2\text{按列切:}\; X\,[A_1, A_2] = [XA_1,\; XA_2] \qquad\qquad \text{按行切:}\; [Y_1, Y_2]\begin{bmatrix}B_1\\B_2\end{bmatrix} = Y_1B_1 + Y_2B_2

按列切时,每块 GPU 算出输出的一部分列,不需要通信;按行切时,每块 GPU 算出一个部分和,要 all-reduce 加起来。

3.2 Megatron 的 MLP:先列后行

Transformer 的 MLP 是 Z=GeLU(XA) BZ = \mathrm{GeLU}(XA)\,B。Megatron-LM(Shoeybi et al. 2019)的切法:AA 按列切,BB 按行切:

Z=∑i=1tGeLU(XAi) BiZ = \sum_{i=1}^{t} \mathrm{GeLU}(XA_i)\,B_i

因为 GeLU 是逐元素的,每块 GPU 拿着完整的若干列 XAiXA_i,可以直接算 GeLU;然后乘上 BiB_i 得到一个部分和。整个 MLP 只需要在最后做一次 all-reduce。

用两个”算子”描述通信(CS336 课件的图):

算子前向反向
ff(MLP 入口)恒等:每块 GPU 都拿到完整的 XXall-reduce:XX 的梯度是各块 GPU 贡献之和
gg(MLP 出口)all-reduce:把部分和加起来恒等

03_tensor_parallel.py 用两个 torch.autograd.Function 实现了 ff 和 gg:

class CopyToTP(torch.autograd.Function):      # f
    def forward(ctx, x):  return x.view_as(x)
    def backward(ctx, g): dist.all_reduce(g); return g

class ReduceFromTP(torch.autograd.Function):  # g
    def forward(ctx, x):  dist.all_reduce(x); return x
    def backward(ctx, g): return g

为什么必须”先列后行”?文件里有一个反例:如果 AA 按行切,每块 GPU 得到的是预激活值的部分和,而 GeLU(a+b)≠GeLU(a)+GeLU(b)\mathrm{GeLU}(a + b) \ne \mathrm{GeLU}(a) + \mathrm{GeLU}(b)。各块单独算 GeLU 再相加,与正确结果的最大差异是 0.82,错得离谱。按行切就必须先 all-reduce 出完整的预激活值,多一次通信。

3.3 注意力:按头切

多头注意力天然可以切:各个头的计算完全独立。Q、K、V 的投影矩阵按列切(每块 GPU 负责 H/tH/t 个头),输出投影按行切。一个头的 Q、K、V 都在同一块 GPU 上,注意力本身不需要任何通信。

CS336 课件总结的 Transformer 块切法:

  • 按列切:QKV 投影、MLP 的上投影(SwiGLU 的两个上投影矩阵);
  • 按行切:注意力输出投影、MLP 的下投影;
  • 复制:归一化层、MoE 的路由器等小参数。

03_tensor_parallel.py 演示 1 搭了一个”注意力 + MLP”(带残差)的简化 Transformer 层,4 路张量并行:输出、输入梯度、每一片参数的梯度都与单进程一致(最大差异约 10−610^{-6})。

3.4 通信量:为什么只在机器内部用

一个 Transformer 层有 2 个 gg、2 个 ff:前向 2 次 all-reduce、反向 2 次,每次的大小都是整个激活值 b×s×hb \times s \times h。代码统计:前向、反向各恰好 2×s×d2 \times s \times d 个元素。

按第 7 讲的环形 all-reduce,每次每个 rank 发送约 2(t−1)/t2(t-1)/t 倍的数据,所以:

张量并行每层的通信量=8 bsh t−1tvs.流水线每个阶段边界:bsh\text{张量并行每层的通信量} = 8\,bsh\,\frac{t-1}{t} \qquad\text{vs.}\qquad \text{流水线每个阶段边界:}bsh

张量并行的通信比流水线大得多,而且是阻塞的:每一层都要等 all-reduce 完成才能继续算下一步。所以 CS336 课件的结论是:在 GPU 上,张量并行只在一台机器内部用(最多 8 块),因为那里有高速的 NVLink。

3.5 张量并行 vs 流水线并行

张量并行流水线并行
气泡没有(网络够快就不用等)有,需要大量微批次
实现相对简单,容易”包装”现有模型复杂,要改训练循环
批次要求不需要大批次需要大批次
通信量每层 8bsh(t−1)/t8bsh(t-1)/t,all-reduce每个阶段边界 bshbsh,点对点
适合的链路低延迟、高带宽(NVLink)较慢的链路(机器之间)

4. 激活值显存与序列并行

4.1 显存是动态的

到目前为止,我们主要在讨论参数的显存。但 CS336 课件提醒:显存不只是静态的参数,还有激活值,而且它可能很大。训练过程中,显存占用随前向传播不断上升(存激活值),随反向传播不断下降(用完释放)。张量并行和流水线并行能线性地减少参数显存,那激活值呢?

4.2 一层要存多少激活值?

Korthikanti et al. 2022 给出了 GPT 式 Transformer 层的公式(16 位激活值、dropout 掩码每个元素 1 字节):

每层激活值(字节)=sbh(34+5ash)\text{每层激活值(字节)} = sbh\left(34 + \frac{5as}{h}\right)

其中 ss 是序列长度,bb 是微批次大小,hh 是隐藏维度,aa 是头数。04_activation_memory.py 逐项拆解了它(以 GPT-3 175B 为例:ss = 2048,hh = 12288,aa = 96,bb = 1):

部分存的是什么大小谁能切开它
LayerNorm两个 LayerNorm 的输入4sbh4sbh序列并行
注意力QKV 投影的输入2sbh2sbh序列并行
Q 和 K4sbh4sbh张量并行
softmax 输出(每个头 s×ss\times s)2as2b2as^2b张量并行
softmax 后的 dropout 掩码as2bas^2b张量并行
dropout 输出2as2b2as^2b张量并行
V、输出投影的输入4sbh4sbh张量并行
输出后的 dropout 掩码sbhsbh序列并行
MLP第一个线性层的输入2sbh2sbh序列并行
GeLU 的输入、输出(4h 维)16sbh16sbh张量并行
输出后的 dropout 掩码sbhsbh序列并行
合计34sbh+5as2b34sbh + 5as^2b

GPT-3 的这一层:0.86+2.01=2.870.86 + 2.01 = 2.87 GB,其中注意力矩阵(5as2b5as^2b)占了大头(5as/h=80>345as/h = 80 > 34)。96 层、一个 2048 长的序列,就要 275 GB 的激活值。

4.3 张量并行切不动的那 10·sbh

张量并行把”在列并行 / 行并行矩阵乘法内部”的激活值切成了 1/t1/t。但有一些激活值每块 GPU 都有完整的一份:LayerNorm 的输入、dropout 掩码、送进列并行矩阵乘法的输入。于是:

张量并行后每层=sbh(10+24t+5asht)\text{张量并行后每层} = sbh\left(10 + \frac{24}{t} + \frac{5as}{ht}\right)

那个 10 sbh10\,sbh 不随 tt 减少。CS336 课件说,“这些项会随着规模继续增长”。

4.4 序列并行:把剩下的也切开

观察:那 10 sbh10\,sbh 全都是逐 token 的运算(LayerNorm、dropout、残差相加)——每个 token 只看自己。所以可以沿序列把它们切开:每块 GPU 只负责 s/ts/t 个 token。

于是在张量并行区域的入口和出口,通信算子换成了:

算子前向反向
gg(进入张量并行区域)all-gather:把各段序列拼成完整序列reduce-scatter
gˉ\bar g(离开张量并行区域)reduce-scatter:部分和相加,并沿序列切开all-gather

关键:all-gather + reduce-scatter 的通信量 = 一次 all-reduce(第 7 讲)。序列并行不增加通信,却把整层激活值都变成了 1/t1/t:

张量并行 + 序列并行后每层=sbht(34+5ash)\text{张量并行 + 序列并行后每层} = \frac{sbh}{t}\left(34 + \frac{5as}{h}\right)

03_tensor_parallel.py 演示 3 实现了”序列并行 + 张量并行”的 MLP 子层(RMSNorm → all-gather → 列并行 → 行并行 → reduce-scatter → 残差)。输出、输入梯度、MLP 参数梯度都与单进程一致。一个细节:RMSNorm 的增益 gg 在每块 GPU 上都有一份,但每块 GPU 只看到了一段序列,所以 gg 的梯度要在张量并行组内再 all-reduce 一次。

4.5 重计算

再加上选择性重计算(不保存注意力内部的 s×ss\times s 矩阵,反向时重算。它们占显存多、算起来便宜,FlashAttention 本来就这样做),注意力矩阵那一项也没了。GPT-3 175B,tt = 8(04_activation_memory.py 演示 2):

方式公式每层96 层合计
不做并行sbh(34+5as/h)sbh(34 + 5as/h)2869 MB275.4 GB
张量并行sbh(10+24/t+5as/(ht))sbh(10 + 24/t + 5as/(ht))579 MB55.6 GB
+ 序列并行sbht(34+5as/h)\frac{sbh}{t}(34 + 5as/h)359 MB34.4 GB
+ 选择性重计算sbht⋅34\frac{sbh}{t}\cdot 34107 MB10.3 GB
+ 完全重计算sbht⋅2\frac{sbh}{t}\cdot 26 MB0.6 GB

CS336 课件还引用了 Narayanan et al. 2021 的发现:激活重计算可以”自己付账”——它多花了计算,但省下的显存允许更大的微批次,整体吞吐反而可能更高。

**【边界】**这个公式是论文中 GPT-3 式架构(有 dropout、LayerNorm、4h 的 GeLU MLP)的结果。现代模型(没有 dropout、RMSNorm、SwiGLU、GQA、FlashAttention)的常数不同,但”哪些项被切、哪些项随 s2s^2 增长”的结构是一样的。


5. 上下文并行:环形注意力

5.1 序列太长怎么办?

04_activation_memory.py 演示 3:张量 + 序列并行(tt = 8)时,序列从 2K 增长到 128K,每层的 34sbh/t34sbh/t 那一项也从 0.11 GB 涨到 6.85 GB,而注意力矩阵那一项(如果不重算)的占比从 70% 涨到 99%。长上下文训练中,一块 GPU 可能连一个序列都放不下。

上下文并行(context parallelism,CP)把整个序列切开,pp 块 GPU 各负责 s/ps/p 个 token——不只是逐 token 的运算,而是整个层(包括注意力)。

5.2 注意力怎么办:环形注意力

MLP、归一化只需要本地的 token;但注意力中,每个 query 要看它之前的所有 key / value,而它们分散在不同的 GPU 上。

环形注意力(Ring Attention,Liu et al. 2023):

  1. 每块 GPU 保存自己那段的 Q、K、V;
  2. pp 块 GPU 排成一个环,K、V 块沿环传递:第 rr 步,GPU ii 手里是 GPU (i−r)(i-r) 的 K、V;
  3. 每拿到一块,就算”自己的 Q 对这块 K、V”的部分注意力,并用第 5 讲 FlashAttention 的在线 softmax 合并:
lse=log⁡ ⁣(else1+else2),o=o1 else1−lse+o2 else2−lse\mathrm{lse} = \log\!\left(e^{\mathrm{lse}_1} + e^{\mathrm{lse}_2}\right),\qquad o = o_1\, e^{\mathrm{lse}_1 - \mathrm{lse}} + o_2\, e^{\mathrm{lse}_2 - \mathrm{lse}}
  1. pp 步之后,每块 GPU 都看过了所有 K、V,结果与完整注意力完全相同。

通信(把 K、V 传给下一块 GPU)可以和计算(当前块的注意力)重叠。每一步的计算 ∝c2\propto c^2、通信 ∝c\propto c(cc 是每块 GPU 的 token 数),计算 / 通信 = 2c2c 次运算每元素——序列越长,越容易藏住通信。

05_ring_attention.py 在单进程里模拟 4 块 GPU,环形注意力与完整因果注意力的差异是 1.1×10−151.1\times10^{-15}(float64 下只有舍入误差)。

5.3 因果掩码下的负载均衡

因果注意力中,靠前的 token 只看很少的 key,靠后的 token 看很多。如果按顺序切,GPU 0 几乎不用算,GPU 3 要算全部:

切法各 GPU 要算的 (query, key) 对数整体效率
按顺序切2080、6176、10272、1436857%
之字形切8224、8224、8224、8224100%

之字形切法:把序列切成 2p2p 小段,GPU ii 拿第 ii 段和第 2p−1−i2p-1-i 段(一前一后),工作量就完全相同了。Llama 3 的上下文并行也把序列切成 2×2\timesCP 段、每个 rank 拿两段。

另一种上下文并行的方案是 DeepSpeed-Ulysses:在注意力前后各做一次 all-to-all,把”按序列切”换成”按头切”。每块 GPU 拿到完整的序列、但只算一部分头,算完再换回来。


6. 专家并行:all-to-all

6.1 思路

MoE(第 4 讲)的专家参数非常多(DeepSeek-V3 每层 256 个路由专家),而每个 token 只用其中几个。专家并行:把专家分到不同的 GPU 上。CS336 课件的说法是”不切矩阵乘法,而是切专家、路由激活值”。

每块 GPU 有自己那批 token(由数据并行分配),但 token 需要的专家可能在别的 GPU 上:

  1. 路由:用(复制的)路由器为每个 token 选出 top-k 专家;
  2. 派发:用 all-to-all 把每个 token 发到它的专家所在的 GPU(第 7 讲第 3 节);
  3. 计算:每块 GPU 用本地专家处理收到的 token;
  4. 收回:再用一次 all-to-all 把结果发回原来的 GPU,按路由权重加权求和。

反向传播时,这两次 all-to-all 反过来再各做一次。

06_expert_parallel.py 用 4 个进程、8 个专家(每个进程 2 个)、top-2 路由实现了这个过程,结果与单进程 MoE 一致(差异约 10−710^{-7})。每个 token 的每个 (token, 专家) 对要传一个 DD 维向量:通信量与 token 数 × k × D 成正比,与专家的参数量无关。

6.2 为什么用专家并行,而不是张量并行?

CS336 课件:专家并行在 MLP 上的行为很像张量并行——都需要高带宽,都减少每块 GPU 上的参数和激活值。但张量并行把每个矩阵乘法切碎,小矩阵乘法效率低;专家并行里每个专家都是一个完整的矩阵乘法,只是处理的 token 少一些,计算效率更高。

6.3 负载不均衡

专家并行最大的麻烦是负载不均衡。文件里的实验:

路由器各 rank 收到的 (token, 专家) 对最忙的 rank / 平均
随机初始化517、461、592、4781.16 倍
偏爱专家 0、1(都在 rank 0 上)1835、53、100、603.58 倍

每一层都要等最慢的 rank 算完,其他 rank 大部分时间在空等。这就是 MoE 训练必须做负载均衡(第 4 讲的辅助损失、DeepSeek-V3 的偏置调整)和设置容量因子的原因之一。

6.4 与其他并行方式组合

CS336 课件提到几个复杂之处:

  • 专家并行通常与数据并行”共享”GPU:同一组 GPU 在注意力部分是数据并行,在 MoE 部分是专家并行(所以 EP ≤ DP);
  • MoE 只替换 MLP,不替换注意力。注意力适合较大的 TP(不能用 EP),MLP 则更适合用 EP 而不是 TP。Megatron 因此让注意力和 MoE 部分使用不同的并行配置(注意力用 TP / CP / DP,专家用 ETP / EP / EDP);
  • DeepSeek-V3:16 路流水线并行、跨 8 个节点的 64 路专家并行、ZeRO-1 数据并行,不用张量并行。

7. 组合起来:3D / 4D 并行

7.1 各种并行方式一览(CS336 课件的总结表)

方式通信 / 同步每卡参数显存每卡激活值显存主要带宽开销能扩展全局批次吗易用性
DDP / ZeRO-1每步梯度 all-reduce不减少(ZeRO-1 只切优化器状态)不减少梯度,O(参数量)能,随 DP 线性很容易
FSDP / ZeRO-3可与计算重叠参数、梯度、优化器状态 ~1/DP不减少参数,比 DDP 多能中等
流水线并行阶段间传激活值,有气泡~1/PP取决于流水线缓冲阶段间激活值不能,需要微批次难
张量并行阻塞的激活值通信~1/TP配合 SP 时 ~1/TP每层的激活值集合通信不能难
序列 / 上下文并行每层交换序列分片不减少~1/SP 或 1/CP激活值 / KV不能难
专家并行每个 MoE 层 all-to-all专家权重 ~1/EP不减少token 路由 all-to-all不能,需要每个专家有足够的 token难

7.2 经验法则

CS336 课件给出的简单规则:

  1. 先让模型放得下:
    • 张量并行(或专家并行)用到一台机器的 GPU 数为止;
    • 还放不下,就在机器之间加流水线并行(或者用 ZeRO-3,取决于网络带宽)。
  2. 然后用完所有 GPU:剩下的全部用来做数据并行。
  3. 批次太小时:用梯度累积换取更高的通信效率。

Narayanan et al. 2021(Megatron-LM 的大规模训练论文)的实验印证了这些规则:张量并行先用到 8,然后不再增加;流水线并行随模型增大而增加,直到模型放得下;数据并行的路数随模型增大而减少(最大的模型只剩 6)。精心配置的 3D 并行可以让每块 GPU 的利用率几乎不随 GPU 数下降;在 64 台机器上,8 × 8 的(张量 × 流水线)配置最好。

7.3 用规划器看 Llama 3 405B

07_parallel_planner.py 把本讲的公式组合成一个粗略的规划器,对每种 (TP, PP, DP) 估算显存、气泡和通信。以 Llama 3 405B(405.8B 参数)在 16384 块 H100 上为例(序列 8192,全局批次 1600 万 token,微批次 1 条序列)。Llama 3 的实际配置是 TP 8、PP 16、DP 128:

配置 (TP, PP, DP)显存(完全重计算)气泡TP 通信 / 计算DP 通信 / 计算
(8, 4, 512)55.3 GB43%21%40%
(8, 8, 256)29.9 GB47%21%20%
(8, 16, 128) ← Llama 317.2 GB48%21%10%
(4, 16, 256)29.9 GB65%9%20%
(16, 4, 256)27.8 GB27%402%20%

从表里能读出几件事:

  1. TP ≤ 8:tt = 8 在 NVLink 内,通信约占计算的 21%;tt = 16 跨出了机器,通信是计算的 4 倍。
  2. 气泡的”死结”:tt = 8 时 p×dp \times d = 2048,恰好等于每步的序列数 2048。于是每条流水线的微批次数 mm = 序列数 / dd = pp,气泡 (p−1)/(2p−1)(p-1)/(2p-1) 无论 pp 取多少都接近 50%。要减小气泡,只能加大全局批次(但受临界批量大小的限制)、或者用交错调度等更好的调度。Llama 3 论文提到,他们在交错调度的基础上做了修改,让微批次数可以更灵活地设置。
  3. 激活值是大头:如果用选择性重计算(只不存注意力矩阵),这个粗略模型估算的激活值超过了 80 GB(1F1B 的第一个阶段要同时存 pp 个微批次的激活值)。这个模型用的是 GPT-3 式的常数 34,高估了 Llama(没有 dropout),但它说明激活值必须认真规划。

对于小模型(Llama 3 8B,64 块 GPU,全局批次 400 万 token),规划器的结论很简单:t=p=1t = p = 1,全部做数据并行(加 ZeRO-1)。能用数据并行解决的,就不要引入模型并行的复杂度。

**【边界】**这是一个用来建立直觉的粗略模型:没有考虑通信与计算的重叠、交错调度、各种实现细节。它能告诉你”哪些配置明显不行、为什么”,但不能代替真实的测量。

7.4 实际模型用了什么

CS336 课件整理的配置(07_parallel_planner.py 演示 3):

模型DPTPEPPPCP
Llama 3 405B1288—161(长上下文阶段 16)
Gemma 2768(ZeRO-3)8———
DeepSeek-V3?(ZeRO-1)16416—
Mixtral 8x22B(Megatron)24841
Qwen 3(Megatron)?23281

课件总结的规律:TP 一般 ≤ 8;EP 可以很大(但很难做好);长上下文阶段用很大的 CP。

另外,在上万块 GPU 的规模上,硬件故障是常态。据 Llama 3 论文,在 54 天的预训练期间一共发生了 466 次作业中断,大部分由硬件问题(GPU、显存等)引起。大规模训练系统必须能快速检测故障、从检查点恢复。


8. 本讲小结

  1. 模型并行切模型、传激活值;ZeRO-3 切模型状态、传参数。
  2. 流水线并行按层切:气泡比例 (p−1)/(m+p−1)(p-1)/(m+p-1),需要 m≫pm \gg p 的大批次;1F1B 把激活显存从 mm 份降到 pp 份;交错调度、零气泡调度进一步减小气泡。通信是相邻阶段间的点对点传输,适合跨机器。
  3. 张量并行按矩阵切:先列后行,一个 MLP 或注意力块只需一次 all-reduce;每层通信 8bsh(t−1)/t8bsh(t-1)/t 且阻塞,只在机器内部用(t≤8t \le 8)。
  4. 激活值每层 sbh(34+5as/h)sbh(34 + 5as/h)。张量并行切不动 10sbh10sbh;序列并行用 all-gather / reduce-scatter 代替 all-reduce,不增加通信,把整层激活值切成 1/t1/t;选择性重计算去掉注意力矩阵。
  5. 上下文并行:环形注意力用在线 softmax 合并各块结果;之字形切分解决因果掩码的负载不均衡。
  6. 专家并行:两次 all-to-all;通信与专家参数量无关;负载不均衡是主要挑战。
  7. 3D 并行的经验法则:TP 用到机器内 GPU 数,PP(或 ZeRO-3)跨机器直到放得下,剩下的给 DP;批次、气泡、激活值、通信要一起权衡。

9. 与作业 2 的关系

作业 2 的分布式部分以数据并行为主(第 7 讲),但通常也会有关于多种并行方式组合的资源核算问题,例如:

  • 给定一个大模型和一个集群,用 FSDP / TP / PP 时每块卡要存多少参数、激活值;
  • 计算张量并行、流水线并行的通信量,判断哪种组合在给定带宽下可行;
  • 估算流水线气泡,以及需要多大的批次才能让气泡足够小。

本讲的 04_activation_memory.py 和 07_parallel_planner.py 就是为这类问题准备的工具。


10. 练习

练习 1(气泡):8 个流水线阶段,希望气泡比例不超过 10%,至少需要多少个微批次?如果每个微批次是 4 条长度 4096 的序列,全局批次至少多少 token?

参考答案

(p−1)/(m+p−1)≤0.1(p-1)/(m+p-1) \le 0.1,即 7≤0.1(m+7)7 \le 0.1(m+7),m≥63m \ge 63。全局批次(每条流水线)至少 63×4×4096≈10363 \times 4 \times 4096 ≈ 103 万 token;如果还有 dd 路数据并行,再乘以 dd。

练习 2(1F1B 的显存):pp = 8、mm = 64 时,GPipe 和 1F1B 的第一个阶段各要同时保存多少个微批次的激活值?最后一个阶段呢?

参考答案

GPipe:每个阶段都是 64 个。1F1B:第一个阶段 8 个(p−sp - s,ss = 0),最后一个阶段 1 个。这也说明 1F1B 下各阶段的显存不均衡,第一个阶段最紧张——有些实现会给第一个阶段少分几层。

练习 3(张量并行的通信):一个 Transformer 层,bb = 1,ss = 8192,hh = 8192,BF16,tt = 8。(a) 每层每步(前向 + 反向)每块 GPU 发送多少字节?(b) 用 450 GB/s 的 NVLink 要多久?(c) 这一层的计算(整层约 24bsh224bsh^2 次前向运算,前向 + 反向约 3 倍,分给 8 块 GPU)在 H100 上(MFU 50%)每块 GPU 要多久?通信占计算的多少?(d) 如果这 8 块 GPU 分在两台机器上(每台 4 块),跨机器的通信走 50 GB/s 的 InfiniBand 呢?

参考答案

(a) 8×8192×8192×7/8×28 \times 8192 \times 8192 \times 7/8 \times 2 字节 ≈ 0.94 GB。(b) 约 2.1 ms。(c) 整层 24×8192×81922×3≈3.96×101324 \times 8192 \times 8192^2 \times 3 ≈ 3.96\times10^{13} 次运算,每块 GPU 算 1/8,约 4.9×10124.9\times10^{12} 次,÷ (989×1012×0.5)(989\times10^{12} \times 0.5) ≈ 10 ms。通信约占计算的 21%,与 07_parallel_planner.py 算出的比例一致。(这里的 24bsh224bsh^2 只算了矩阵乘法,4h 的 MLP。)(d) 环上有跨机器的链路,通信速度受 InfiniBand 限制:约 0.94 GB / 50 GB/s ≈ 19 ms,差不多是计算时间的 2 倍,而且是阻塞的。这就是张量并行要留在机器内部的原因。

练习 4(为什么 f 的反向是 all-reduce):用链式法则说明,Megatron MLP 中 XX 的梯度为什么等于各块 GPU 上 ∂L/∂X\partial L/\partial X 的和。

参考答案

Z=∑iGeLU(XAi)BiZ = \sum_i \mathrm{GeLU}(XA_i)B_i,XX 通过每一项影响 ZZ。所以 ∂L∂X=∑i∂L∂Z∂Zi∂X\frac{\partial L}{\partial X} = \sum_i \frac{\partial L}{\partial Z}\frac{\partial Z_i}{\partial X},第 ii 项只有 GPU ii 能算(它有 AiA_i、BiB_i)。把各 GPU 算出的这一项加起来,正是一次 all-reduce。

练习 5(序列并行):序列并行把 all-reduce 换成了 all-gather + reduce-scatter。(a) 为什么通信量不变?(b) 它省下了哪些激活值?(c) RMSNorm 的增益参数为什么要额外 all-reduce 梯度?

参考答案

(a) 第 7 讲:all-reduce = reduce-scatter + all-gather,环形实现下通信量相同(各约 (t−1)/t(t-1)/t 倍的数据)。(b) 张量并行区域之外的逐 token 激活值:归一化的输入 / 输出、dropout 掩码、残差流,以及送进列并行矩阵乘法之前的那份输入——每块 GPU 只存 s/ts/t 个 token。(c) 增益参数在每块 GPU 上都有一份(复制),但每块 GPU 只在自己那段序列上计算了它的梯度,完整的梯度是所有序列段的梯度之和。

练习 6(环形注意力):(a) 用在线 softmax 的合并公式,手算两块的例子:第一块分数 [1,2][1, 2]、值 [10,20][10, 20];第二块分数 [3][3]、值 [30][30](每个值是一个标量)。验证合并结果等于直接对 [1,2,3][1,2,3] 做 softmax 加权。(b) 为什么环形注意力的通信能被计算藏住?

参考答案

(a) 第一块:lse1=log⁡(e1+e2)≈2.3133\mathrm{lse}_1 = \log(e^1 + e^2) ≈ 2.3133,o1=(10e1+20e2)/(e1+e2)≈17.311o_1 = (10e^1 + 20e^2)/(e^1+e^2) ≈ 17.311。第二块:lse2=3\mathrm{lse}_2 = 3,o2=30o_2 = 30。合并:lse=log⁡(e2.3133+e3)≈3.4076\mathrm{lse} = \log(e^{2.3133} + e^3) ≈ 3.4076,o=17.311×e2.3133−3.4076+30×e3−3.4076≈17.311×0.3348+30×0.6652≈25.75o = 17.311 \times e^{2.3133-3.4076} + 30 \times e^{3-3.4076} ≈ 17.311 \times 0.3348 + 30 \times 0.6652 ≈ 25.75。直接计算:softmax([1,2,3])([1,2,3]) = [0.0900,0.2447,0.6652][0.0900, 0.2447, 0.6652],加权和 =0.9+4.894+19.957≈25.75= 0.9 + 4.894 + 19.957 ≈ 25.75 ✓。(b) 每一步的计算 ∝c2d\propto c^2 d,通信 ∝cd\propto cd,cc 越大,计算越多于通信;而且下一块 K、V 的传输可以在算当前块时同时进行。

练习 7(专家并行的通信):DeepSeek-V3 式的 MoE 层,隐藏维度 7168,每个 token 选 8 个专家,一块 GPU 上有 4096 个 token,EP = 64。估算这块 GPU 每个 MoE 层前向要发送多少字节(BF16)。反向呢?

参考答案

派发:约 4096×8×63/64×7168×24096 \times 8 \times 63/64 \times 7168 \times 2 字节 ≈ 462 MB;收回同样多。前向约 0.92 GB,反向再来一次,共约 1.85 GB。(DeepSeek-V3 限制每个 token 最多发往 4 个节点,并在节点内部用 NVLink 转发,以减少跨机器的流量;这正是”通信层次”原则的应用。)

练习 8(规划)★:用 07_parallel_planner.py,把 Llama 3 405B 的全局批次改成 3200 万 token,看看 (8, 16, 128) 的气泡变成多少。为什么不直接这样做?

参考答案

序列数变成 4096,mm = 4096 / 128 = 32,气泡 = 15/47 ≈ 32%。不直接这样做,是因为全局批次受临界批量大小的限制:超过它以后,更大的批次几乎不减少所需的训练步数,等于浪费计算(第 9 讲)。Llama 3 的批次是逐步加大的(训练初期更小)。

练习 9(动手): (a) 在 01_pipeline_schedules.py 里实现交错调度,验证气泡。 (b) 在 02_pipeline_parallel.py 里实现 1F1B(提示:用 isend / irecv 避免死锁)。 (c) 在 06_expert_parallel.py 里加上容量因子,统计偏心路由器下丢弃了多少 token。


11. 延伸阅读

  • CS336 Spring 2026 Lecture 8(stanford-cs336/lectures 中的 lecture_08.pdf)与 Lecture 7(lecture_07.py)。
  • Shoeybi et al.,Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism(2019):张量并行。
  • Narayanan et al.,Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM(2021):3D 并行、交错调度、配置经验。
  • Korthikanti et al.,Reducing Activation Recomputation in Large Transformer Models(2022):激活值公式、序列并行、选择性重计算。
  • Huang et al.,GPipe(2019);Narayanan et al.,PipeDream(2019);Qi et al.,Zero Bubble Pipeline Parallelism(2023)。
  • Liu et al.,Ring Attention with Blockwise Transformers for Near-Infinite Context(2023);Jacobs et al.,DeepSpeed Ulysses(2023)。
  • Lepikhin et al.,GShard(2020):专家并行与 all-to-all。
  • Meta,The Llama 3 Herd of Models(2024)第 3.3 节;DeepSeek-AI,DeepSeek-V3 Technical Report(2024):真实系统的并行配置、DualPipe、故障处理。
  • Google,How to Scale Your Model(Scaling Book):各种并行方式的系统讲解。

12. 下一讲预告

第 5–8 讲回答了”怎么把训练跑快、怎么用上成千上万块 GPU”。但还有一个更根本的问题:拿到这些算力之后,到底该训练一个多大的模型、用多少数据?

CS336 课件的开场问题:“朋友给了你一万块 B200、用一个月,请你训练一个最好的开源语言模型。你怎么做?“基础设施(作业 2)和数据(作业 4)都准备好了,可是模型多大?宽还是深?学习率多少?在大模型上试错太贵了。

下一讲(规模定律 I)的答案是:在小模型上做实验,找到简单的幂律,再外推到大模型。

  • 为什么损失随数据量、参数量、计算量呈幂律下降?
  • Kaplan(2020)和 Chinchilla(2022)为什么得出了不同的结论?“每个参数约 20 个 token”是怎么来的?
  • IsoFLOP 分析怎么做?我们会在笔记本电脑上做一个真实的小型 IsoFLOP 实验,并用它预测更大预算下的结果。

(按照大纲,第 10 讲”推理”也属于系统单元。如果你想趁热打铁,也可以先读第 10 讲,再回来读第 9 讲。)

下一讲之前,建议你:跑通 code/lecture08/ 下的 7 个脚本,完成练习 1、3、5、6。

← 返回课程目录