对应: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 学完本讲,你应该能够
- 说出数据并行(含 ZeRO)的局限,以及”模型并行”与 ZeRO-3 的根本区别:传激活值 vs 传参数。
- 画出流水线并行的 GPipe、1F1B 时间线,推导气泡比例 ,说明 1F1B 为什么省显存,并了解交错调度、零气泡调度。
- 写出 Megatron 张量并行的 MLP 和注意力(按列切、按行切、f / g 算子),算出每层通信量 ,解释为什么张量并行只在机器内部用。
- 推导激活值公式 ,说明序列并行怎样在不增加通信的前提下把剩下的激活值也切开。
- 解释上下文并行(环形注意力)如何用在线 softmax 合并部分结果,以及因果掩码下的负载均衡问题。
- 解释专家并行的两次 all-to-all,以及负载不均衡的后果。
- 用经验法则和粗略的估算,为给定的模型和集群选择 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 最朴素的按层切
把一个 层的模型切成 段(阶段,stage),GPU 负责第 段。前向时,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......
块 GPU,每块只有 的时间在工作。这些空闲的时间叫气泡(bubble)。
2.2 GPipe:切成微批次
解决办法(GPipe,Huang et al. 2019):把批次切成 个微批次(micro-batch),像工厂流水线一样一个接一个地送进去。GPU 0 算完第 1 个微批次,发给 GPU 1,马上开始第 2 个:
GPipe(m = 8):气泡 27.3%
阶段 0:01234567.........aabbccddeeffgghh
阶段 1:.01234567......aabbccddeeffgghh..
阶段 2:..01234567...aabbccddeeffgghh....
阶段 3:...01234567aabbccddeeffgghh......
开头和结尾各剩下一个”三角形”的空闲区。算一下它有多大:流水线要走 个”格子”才能让所有微批次通过所有阶段,每个阶段真正干活的是其中 个。所以:
模拟器对多种 、 验证了这个公式(完全一致):
| = 1 | 4 | 8 | 16 | 32 | 64 | |
|---|---|---|---|---|---|---|
| 4 | 75.0% | 42.9% | 27.3% | 15.8% | 8.6% | 4.5% |
| 8 | 87.5% | 63.6% | 46.7% | 30.4% | 17.9% | 9.9% |
**结论:微批次数 必须远大于阶段数 。**这就是 CS336 课件强调的”流水线并行需要大批次”。
2.3 1F1B:同样的气泡,更少的激活显存
GPipe 有一个隐藏的代价:每个微批次的前向做完后,它的激活值要一直保存到它的反向做完。GPipe 先做完全部 个前向,所以每个阶段同时要存 份激活值。
1F1B(one-forward-one-backward,也叫 PipeDream-Flush)调整了顺序:阶段 先做 个前向”热身”,之后每做一个前向,就做一个反向:
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 完全相同,但每个阶段最多同时存 份激活值(而不是 份)。 越大,省得越多。这让我们可以放心地调大 来压缩气泡。
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)
正确性: = 1、4、8 时,流水线算出的梯度与单进程整批计算的差异都在 以下。
实测气泡(8 层 × Linear(1024),全局批次 1024,本机 CPU;多进程计时每次运行会有小的波动,看趋势即可):
| 微批次大小 | 每步耗时 | 平均忙碌比例 | 公式 | |
|---|---|---|---|---|
| 1 | 1024 | 33.0 ms | 22% | 25% |
| 2 | 512 | 28.7 ms | 37% | 40% |
| 4 | 256 | 27.4 ms | 53% | 57% |
| 8 | 128 | 31.5 ms | 64% | 73% |
| 16 | 64 | 50.9 ms | 70% | 84% |
两个发现:
- 忙碌比例的趋势与公式一致,只是略低(公式忽略了通信和调度开销)。
- 但总耗时并不是 越大越好: = 4 时最快。 太大,每个微批次太小,矩阵乘法的效率下降、固定开销占比上升。所以流水线需要的不只是”很多微批次”,而是”很多个足够大的微批次”——也就是很大的总批次。
一个计时的坑:这个文件最初的版本测出来,无论 多大,忙碌比例都是 25%。原因是每个进程第一次做反向传播时要做几十毫秒的一次性初始化,而流水线的反向是一个阶段接一个阶段进行的,这些一次性开销被串了起来,完全掩盖了真正的气泡。解决办法就是第 6 讲说的”先热身再计时”:同一个训练步跑 3 次,只计最后一次。
2.5 更好的调度
交错调度(interleaved 1F1B,Narayanan et al. 2021):每块 GPU 不再负责连续的一段层,而是负责 个不相邻的”虚拟阶段”(例如 GPU 0 负责第 1、9、17……块)。每个虚拟阶段更薄,流水线填满得更快,气泡缩小到约 。代价是阶段之间的通信次数变成 倍。CS336 课件的说法:更”疯狂”的流水线模式可以用通信带宽换利用率。
零气泡调度(zero bubble,Qi et al. 2023):把反向拆成两部分——
- B:算输入(激活值)的梯度,下一个阶段在等它,必须尽快做;
- W:算权重的梯度,没有人等它,什么时候做都行。
把 W 塞进原本的气泡里,气泡几乎可以完全消除。DeepSeek-V3 的 DualPipe 调度也用了这种拆分,并把 MoE 的 all-to-all 通信和计算重叠起来。
2.6 流水线并行:为什么用、什么时候用
流水线看起来很麻烦(气泡、需要大批次、实现复杂),为什么还要用?CS336 课件给出两个理由:
- 省显存:每块 GPU 只存 层的参数(和对应的优化器状态)。
- 通信性质好:只在相邻阶段之间传激活值(大小 ),是点对点的,量很小。相比之下,FSDP 每步要传两遍全部参数。
所以:**流水线并行通常用在较慢的链路上(机器之间),用来获得随机器数线性减少的显存。**它的缺点是气泡、对批次大小的要求、以及实现的复杂度(CS336 课件的总结表里,它的”易用性”是”难”)。
3. 张量并行:按矩阵切
3.1 矩阵乘法的两种切法
能不能不按深度切,而是按宽度切——把每一层的矩阵切开?矩阵乘法本身提供了两种切法:
按列切时,每块 GPU 算出输出的一部分列,不需要通信;按行切时,每块 GPU 算出一个部分和,要 all-reduce 加起来。
3.2 Megatron 的 MLP:先列后行
Transformer 的 MLP 是 。Megatron-LM(Shoeybi et al. 2019)的切法: 按列切, 按行切:
因为 GeLU 是逐元素的,每块 GPU 拿着完整的若干列 ,可以直接算 GeLU;然后乘上 得到一个部分和。整个 MLP 只需要在最后做一次 all-reduce。
用两个”算子”描述通信(CS336 课件的图):
| 算子 | 前向 | 反向 |
|---|---|---|
| (MLP 入口) | 恒等:每块 GPU 都拿到完整的 | all-reduce: 的梯度是各块 GPU 贡献之和 |
| (MLP 出口) | all-reduce:把部分和加起来 | 恒等 |
03_tensor_parallel.py 用两个 torch.autograd.Function 实现了 和 :
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
为什么必须”先列后行”?文件里有一个反例:如果 按行切,每块 GPU 得到的是预激活值的部分和,而 。各块单独算 GeLU 再相加,与正确结果的最大差异是 0.82,错得离谱。按行切就必须先 all-reduce 出完整的预激活值,多一次通信。
3.3 注意力:按头切
多头注意力天然可以切:各个头的计算完全独立。Q、K、V 的投影矩阵按列切(每块 GPU 负责 个头),输出投影按行切。一个头的 Q、K、V 都在同一块 GPU 上,注意力本身不需要任何通信。
CS336 课件总结的 Transformer 块切法:
- 按列切:QKV 投影、MLP 的上投影(SwiGLU 的两个上投影矩阵);
- 按行切:注意力输出投影、MLP 的下投影;
- 复制:归一化层、MoE 的路由器等小参数。
03_tensor_parallel.py 演示 1 搭了一个”注意力 + MLP”(带残差)的简化 Transformer 层,4 路张量并行:输出、输入梯度、每一片参数的梯度都与单进程一致(最大差异约 )。
3.4 通信量:为什么只在机器内部用
一个 Transformer 层有 2 个 、2 个 :前向 2 次 all-reduce、反向 2 次,每次的大小都是整个激活值 。代码统计:前向、反向各恰好 个元素。
按第 7 讲的环形 all-reduce,每次每个 rank 发送约 倍的数据,所以:
张量并行的通信比流水线大得多,而且是阻塞的:每一层都要等 all-reduce 完成才能继续算下一步。所以 CS336 课件的结论是:在 GPU 上,张量并行只在一台机器内部用(最多 8 块),因为那里有高速的 NVLink。
3.5 张量并行 vs 流水线并行
| 张量并行 | 流水线并行 | |
|---|---|---|
| 气泡 | 没有(网络够快就不用等) | 有,需要大量微批次 |
| 实现 | 相对简单,容易”包装”现有模型 | 复杂,要改训练循环 |
| 批次要求 | 不需要大批次 | 需要大批次 |
| 通信量 | 每层 ,all-reduce | 每个阶段边界 ,点对点 |
| 适合的链路 | 低延迟、高带宽(NVLink) | 较慢的链路(机器之间) |
4. 激活值显存与序列并行
4.1 显存是动态的
到目前为止,我们主要在讨论参数的显存。但 CS336 课件提醒:显存不只是静态的参数,还有激活值,而且它可能很大。训练过程中,显存占用随前向传播不断上升(存激活值),随反向传播不断下降(用完释放)。张量并行和流水线并行能线性地减少参数显存,那激活值呢?
4.2 一层要存多少激活值?
Korthikanti et al. 2022 给出了 GPT 式 Transformer 层的公式(16 位激活值、dropout 掩码每个元素 1 字节):
其中 是序列长度, 是微批次大小, 是隐藏维度, 是头数。04_activation_memory.py 逐项拆解了它(以 GPT-3 175B 为例: = 2048, = 12288, = 96, = 1):
| 部分 | 存的是什么 | 大小 | 谁能切开它 |
|---|---|---|---|
| LayerNorm | 两个 LayerNorm 的输入 | 序列并行 | |
| 注意力 | QKV 投影的输入 | 序列并行 | |
| Q 和 K | 张量并行 | ||
| softmax 输出(每个头 ) | 张量并行 | ||
| softmax 后的 dropout 掩码 | 张量并行 | ||
| dropout 输出 | 张量并行 | ||
| V、输出投影的输入 | 张量并行 | ||
| 输出后的 dropout 掩码 | 序列并行 | ||
| MLP | 第一个线性层的输入 | 序列并行 | |
| GeLU 的输入、输出(4h 维) | 张量并行 | ||
| 输出后的 dropout 掩码 | 序列并行 | ||
| 合计 |
GPT-3 的这一层: GB,其中注意力矩阵()占了大头()。96 层、一个 2048 长的序列,就要 275 GB 的激活值。
4.3 张量并行切不动的那 10·sbh
张量并行把”在列并行 / 行并行矩阵乘法内部”的激活值切成了 。但有一些激活值每块 GPU 都有完整的一份:LayerNorm 的输入、dropout 掩码、送进列并行矩阵乘法的输入。于是:
那个 不随 减少。CS336 课件说,“这些项会随着规模继续增长”。
4.4 序列并行:把剩下的也切开
观察:那 全都是逐 token 的运算(LayerNorm、dropout、残差相加)——每个 token 只看自己。所以可以沿序列把它们切开:每块 GPU 只负责 个 token。
于是在张量并行区域的入口和出口,通信算子换成了:
| 算子 | 前向 | 反向 |
|---|---|---|
| (进入张量并行区域) | all-gather:把各段序列拼成完整序列 | reduce-scatter |
| (离开张量并行区域) | reduce-scatter:部分和相加,并沿序列切开 | all-gather |
关键:all-gather + reduce-scatter 的通信量 = 一次 all-reduce(第 7 讲)。序列并行不增加通信,却把整层激活值都变成了 :
03_tensor_parallel.py 演示 3 实现了”序列并行 + 张量并行”的 MLP 子层(RMSNorm → all-gather → 列并行 → 行并行 → reduce-scatter → 残差)。输出、输入梯度、MLP 参数梯度都与单进程一致。一个细节:RMSNorm 的增益 在每块 GPU 上都有一份,但每块 GPU 只看到了一段序列,所以 的梯度要在张量并行组内再 all-reduce 一次。
4.5 重计算
再加上选择性重计算(不保存注意力内部的 矩阵,反向时重算。它们占显存多、算起来便宜,FlashAttention 本来就这样做),注意力矩阵那一项也没了。GPT-3 175B, = 8(04_activation_memory.py 演示 2):
| 方式 | 公式 | 每层 | 96 层合计 |
|---|---|---|---|
| 不做并行 | 2869 MB | 275.4 GB | |
| 张量并行 | 579 MB | 55.6 GB | |
| + 序列并行 | 359 MB | 34.4 GB | |
| + 选择性重计算 | 107 MB | 10.3 GB | |
| + 完全重计算 | 6 MB | 0.6 GB |
CS336 课件还引用了 Narayanan et al. 2021 的发现:激活重计算可以”自己付账”——它多花了计算,但省下的显存允许更大的微批次,整体吞吐反而可能更高。
**【边界】**这个公式是论文中 GPT-3 式架构(有 dropout、LayerNorm、4h 的 GeLU MLP)的结果。现代模型(没有 dropout、RMSNorm、SwiGLU、GQA、FlashAttention)的常数不同,但”哪些项被切、哪些项随 增长”的结构是一样的。
5. 上下文并行:环形注意力
5.1 序列太长怎么办?
04_activation_memory.py 演示 3:张量 + 序列并行( = 8)时,序列从 2K 增长到 128K,每层的 那一项也从 0.11 GB 涨到 6.85 GB,而注意力矩阵那一项(如果不重算)的占比从 70% 涨到 99%。长上下文训练中,一块 GPU 可能连一个序列都放不下。
上下文并行(context parallelism,CP)把整个序列切开, 块 GPU 各负责 个 token——不只是逐 token 的运算,而是整个层(包括注意力)。
5.2 注意力怎么办:环形注意力
MLP、归一化只需要本地的 token;但注意力中,每个 query 要看它之前的所有 key / value,而它们分散在不同的 GPU 上。
环形注意力(Ring Attention,Liu et al. 2023):
- 每块 GPU 保存自己那段的 Q、K、V;
- 块 GPU 排成一个环,K、V 块沿环传递:第 步,GPU 手里是 GPU 的 K、V;
- 每拿到一块,就算”自己的 Q 对这块 K、V”的部分注意力,并用第 5 讲 FlashAttention 的在线 softmax 合并:
- 步之后,每块 GPU 都看过了所有 K、V,结果与完整注意力完全相同。
通信(把 K、V 传给下一块 GPU)可以和计算(当前块的注意力)重叠。每一步的计算 、通信 ( 是每块 GPU 的 token 数),计算 / 通信 = 次运算每元素——序列越长,越容易藏住通信。
05_ring_attention.py 在单进程里模拟 4 块 GPU,环形注意力与完整因果注意力的差异是 (float64 下只有舍入误差)。
5.3 因果掩码下的负载均衡
因果注意力中,靠前的 token 只看很少的 key,靠后的 token 看很多。如果按顺序切,GPU 0 几乎不用算,GPU 3 要算全部:
| 切法 | 各 GPU 要算的 (query, key) 对数 | 整体效率 |
|---|---|---|
| 按顺序切 | 2080、6176、10272、14368 | 57% |
| 之字形切 | 8224、8224、8224、8224 | 100% |
之字形切法:把序列切成 小段,GPU 拿第 段和第 段(一前一后),工作量就完全相同了。Llama 3 的上下文并行也把序列切成 CP 段、每个 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 上:
- 路由:用(复制的)路由器为每个 token 选出 top-k 专家;
- 派发:用 all-to-all 把每个 token 发到它的专家所在的 GPU(第 7 讲第 3 节);
- 计算:每块 GPU 用本地专家处理收到的 token;
- 收回:再用一次 all-to-all 把结果发回原来的 GPU,按路由权重加权求和。
反向传播时,这两次 all-to-all 反过来再各做一次。
06_expert_parallel.py 用 4 个进程、8 个专家(每个进程 2 个)、top-2 路由实现了这个过程,结果与单进程 MoE 一致(差异约 )。每个 token 的每个 (token, 专家) 对要传一个 维向量:通信量与 token 数 × k × D 成正比,与专家的参数量无关。
6.2 为什么用专家并行,而不是张量并行?
CS336 课件:专家并行在 MLP 上的行为很像张量并行——都需要高带宽,都减少每块 GPU 上的参数和激活值。但张量并行把每个矩阵乘法切碎,小矩阵乘法效率低;专家并行里每个专家都是一个完整的矩阵乘法,只是处理的 token 少一些,计算效率更高。
6.3 负载不均衡
专家并行最大的麻烦是负载不均衡。文件里的实验:
| 路由器 | 各 rank 收到的 (token, 专家) 对 | 最忙的 rank / 平均 |
|---|---|---|
| 随机初始化 | 517、461、592、478 | 1.16 倍 |
| 偏爱专家 0、1(都在 rank 0 上) | 1835、53、100、60 | 3.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 课件给出的简单规则:
- 先让模型放得下:
- 张量并行(或专家并行)用到一台机器的 GPU 数为止;
- 还放不下,就在机器之间加流水线并行(或者用 ZeRO-3,取决于网络带宽)。
- 然后用完所有 GPU:剩下的全部用来做数据并行。
- 批次太小时:用梯度累积换取更高的通信效率。
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 GB | 43% | 21% | 40% |
| (8, 8, 256) | 29.9 GB | 47% | 21% | 20% |
| (8, 16, 128) ← Llama 3 | 17.2 GB | 48% | 21% | 10% |
| (4, 16, 256) | 29.9 GB | 65% | 9% | 20% |
| (16, 4, 256) | 27.8 GB | 27% | 402% | 20% |
从表里能读出几件事:
- TP ≤ 8: = 8 在 NVLink 内,通信约占计算的 21%; = 16 跨出了机器,通信是计算的 4 倍。
- 气泡的”死结”: = 8 时 = 2048,恰好等于每步的序列数 2048。于是每条流水线的微批次数 = 序列数 / = ,气泡 无论 取多少都接近 50%。要减小气泡,只能加大全局批次(但受临界批量大小的限制)、或者用交错调度等更好的调度。Llama 3 论文提到,他们在交错调度的基础上做了修改,让微批次数可以更灵活地设置。
- 激活值是大头:如果用选择性重计算(只不存注意力矩阵),这个粗略模型估算的激活值超过了 80 GB(1F1B 的第一个阶段要同时存 个微批次的激活值)。这个模型用的是 GPT-3 式的常数 34,高估了 Llama(没有 dropout),但它说明激活值必须认真规划。
对于小模型(Llama 3 8B,64 块 GPU,全局批次 400 万 token),规划器的结论很简单:,全部做数据并行(加 ZeRO-1)。能用数据并行解决的,就不要引入模型并行的复杂度。
**【边界】**这是一个用来建立直觉的粗略模型:没有考虑通信与计算的重叠、交错调度、各种实现细节。它能告诉你”哪些配置明显不行、为什么”,但不能代替真实的测量。
7.4 实际模型用了什么
CS336 课件整理的配置(07_parallel_planner.py 演示 3):
| 模型 | DP | TP | EP | PP | CP |
|---|---|---|---|---|---|
| Llama 3 405B | 128 | 8 | — | 16 | 1(长上下文阶段 16) |
| Gemma 2 | 768(ZeRO-3) | 8 | — | — | — |
| DeepSeek-V3 | ?(ZeRO-1) | 1 | 64 | 16 | — |
| Mixtral 8x22B(Megatron) | 2 | 4 | 8 | 4 | 1 |
| Qwen 3(Megatron) | ? | 2 | 32 | 8 | 1 |
课件总结的规律:TP 一般 ≤ 8;EP 可以很大(但很难做好);长上下文阶段用很大的 CP。
另外,在上万块 GPU 的规模上,硬件故障是常态。据 Llama 3 论文,在 54 天的预训练期间一共发生了 466 次作业中断,大部分由硬件问题(GPU、显存等)引起。大规模训练系统必须能快速检测故障、从检查点恢复。
8. 本讲小结
- 模型并行切模型、传激活值;ZeRO-3 切模型状态、传参数。
- 流水线并行按层切:气泡比例 ,需要 的大批次;1F1B 把激活显存从 份降到 份;交错调度、零气泡调度进一步减小气泡。通信是相邻阶段间的点对点传输,适合跨机器。
- 张量并行按矩阵切:先列后行,一个 MLP 或注意力块只需一次 all-reduce;每层通信 且阻塞,只在机器内部用()。
- 激活值每层 。张量并行切不动 ;序列并行用 all-gather / reduce-scatter 代替 all-reduce,不增加通信,把整层激活值切成 ;选择性重计算去掉注意力矩阵。
- 上下文并行:环形注意力用在线 softmax 合并各块结果;之字形切分解决因果掩码的负载不均衡。
- 专家并行:两次 all-to-all;通信与专家参数量无关;负载不均衡是主要挑战。
- 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?
参考答案
,即 ,。全局批次(每条流水线)至少 万 token;如果还有 路数据并行,再乘以 。
练习 2(1F1B 的显存): = 8、 = 64 时,GPipe 和 1F1B 的第一个阶段各要同时保存多少个微批次的激活值?最后一个阶段呢?
参考答案
GPipe:每个阶段都是 64 个。1F1B:第一个阶段 8 个(, = 0),最后一个阶段 1 个。这也说明 1F1B 下各阶段的显存不均衡,第一个阶段最紧张——有些实现会给第一个阶段少分几层。
练习 3(张量并行的通信):一个 Transformer 层, = 1, = 8192, = 8192,BF16, = 8。(a) 每层每步(前向 + 反向)每块 GPU 发送多少字节?(b) 用 450 GB/s 的 NVLink 要多久?(c) 这一层的计算(整层约 次前向运算,前向 + 反向约 3 倍,分给 8 块 GPU)在 H100 上(MFU 50%)每块 GPU 要多久?通信占计算的多少?(d) 如果这 8 块 GPU 分在两台机器上(每台 4 块),跨机器的通信走 50 GB/s 的 InfiniBand 呢?
参考答案
(a) 字节 ≈ 0.94 GB。(b) 约 2.1 ms。(c) 整层 次运算,每块 GPU 算 1/8,约 次,÷ ≈ 10 ms。通信约占计算的 21%,与 07_parallel_planner.py 算出的比例一致。(这里的 只算了矩阵乘法,4h 的 MLP。)(d) 环上有跨机器的链路,通信速度受 InfiniBand 限制:约 0.94 GB / 50 GB/s ≈ 19 ms,差不多是计算时间的 2 倍,而且是阻塞的。这就是张量并行要留在机器内部的原因。
练习 4(为什么 f 的反向是 all-reduce):用链式法则说明,Megatron MLP 中 的梯度为什么等于各块 GPU 上 的和。
参考答案
, 通过每一项影响 。所以 ,第 项只有 GPU 能算(它有 、)。把各 GPU 算出的这一项加起来,正是一次 all-reduce。
练习 5(序列并行):序列并行把 all-reduce 换成了 all-gather + reduce-scatter。(a) 为什么通信量不变?(b) 它省下了哪些激活值?(c) RMSNorm 的增益参数为什么要额外 all-reduce 梯度?
参考答案
(a) 第 7 讲:all-reduce = reduce-scatter + all-gather,环形实现下通信量相同(各约 倍的数据)。(b) 张量并行区域之外的逐 token 激活值:归一化的输入 / 输出、dropout 掩码、残差流,以及送进列并行矩阵乘法之前的那份输入——每块 GPU 只存 个 token。(c) 增益参数在每块 GPU 上都有一份(复制),但每块 GPU 只在自己那段序列上计算了它的梯度,完整的梯度是所有序列段的梯度之和。
练习 6(环形注意力):(a) 用在线 softmax 的合并公式,手算两块的例子:第一块分数 、值 ;第二块分数 、值 (每个值是一个标量)。验证合并结果等于直接对 做 softmax 加权。(b) 为什么环形注意力的通信能被计算藏住?
参考答案
(a) 第一块:,。第二块:,。合并:,。直接计算:softmax = ,加权和 ✓。(b) 每一步的计算 ,通信 , 越大,计算越多于通信;而且下一块 K、V 的传输可以在算当前块时同时进行。
练习 7(专家并行的通信):DeepSeek-V3 式的 MoE 层,隐藏维度 7168,每个 token 选 8 个专家,一块 GPU 上有 4096 个 token,EP = 64。估算这块 GPU 每个 MoE 层前向要发送多少字节(BF16)。反向呢?
参考答案
派发:约 字节 ≈ 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, = 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。