对应 Maps to:CMU 15-779(Fall 2025,Zhihao Jia),Lecture 12 · Wed 10/8 · ML Parallelization: Data Parallelism。 必读论文:S. Rajbhandari, J. Rasley, O. Ruwase, Y. He, ZeRO: Memory Optimizations Toward Training Trillion Parameter Models(SC 2020);S. Rajbhandari, O. Ruwase, J. Rasley, S. Smith, Y. He, ZeRO-Infinity: Breaking the GPU Memory Wall for Extreme Scale Deep Learning(SC 2021)。选读:J. Ren et al., ZeRO-Offload: Democratizing Billion-Scale Model Training(ATC 2021)。 依据说明:本讲以原课 slides
12-ML-parallelization-part1.pdf的前半为主线:
- DNN 训练流程复习;
- 数据并行;参数服务器及其低效;
- AllReduce 的四种做法(朴素、环形、树形、蝶形)和比较表,以及”环形为什么更可扩展”的讨论;
- 数据并行的显存问题;大模型训练的挑战表。
slides 后半的 ZeRO 三个阶段和 FSDP 放在第 13 讲。ZeRO、ZeRO-Infinity 是第 12、13 讲共同的必读论文:本讲精读 ZeRO 里和数据并行有关的部分(显存去哪儿了、数据并行的通信量),第 13 讲精读 ZeRO 的三个阶段和 ZeRO-Infinity。论文数字只做转述。
collectives.py(设备列表上的集合通信 + α-β 计时)、减半—加倍与分层 AllReduce、梯度分桶的时间线模拟以及全部实验,都是本讲义自己设计的;减半—加倍、分层、分桶(PyTorch DDP)是 slides 之外的补充。 配套代码 Code:code/lecture12/(4 个脚本 + 共享模块collectives.py;只需 numpy;每个脚本 ≤ 2 秒) 预计阅读时间:3 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标
学完这一讲,你应该能够:
- 用”梯度对样本可加”解释数据并行为什么等价于大 batch 的 SGD,并说出三种会悄悄破坏等价的错误。
- 说清楚参数服务器为什么扩展不了,以及”分片 + 每台机器兼任服务器”怎样变成了 reduce-scatter + all-gather。
- 手算朴素、环形、树形、蝶形 AllReduce 的轮数、总通信量和每个设备的通信量,并和 slides 的表对照。
- 用 α-β 模型判断:给定消息大小和卡数,哪种 AllReduce 最快;理解”延迟受限”和”带宽受限”两个区间。
- 估算数据并行的扩展效率;解释梯度分桶为什么能把通信藏进反向传播,桶为什么不能太大也不能太小。
- 用 16 字节 / 参数算出数据并行的显存需求,说明为什么它在几十亿参数时就放不下,从而引出 ZeRO。
0.2 先修与本讲范围
| 默认你已经会 | 本讲只给直觉,后面再展开 |
|---|---|
| 第 1–2 讲:训练的前向 / 反向 / 更新、6ND 算力估算、混合精度 | ZeRO 的三个阶段、FSDP(第 13 讲) |
| 第 3 讲:带宽、延迟的概念 | 张量并行、流水线并行(第 14 讲) |
| 第 7 讲:重叠计算和通信的动机 | 自动并行(第 15 讲) |
| 基本的 SGD / Adam | 大 batch 训练的优化问题(学习率缩放等) |
0.3 本讲地图
§1 训练回顾:为什么可以并行
§2 数据并行:等价性与常见错误 ← 代码 01
§3 参数服务器:集中式聚合的瓶颈 ← 代码 02
§4 AllReduce 的几种算法:轮数与通信量 ← 代码 03(演示 1–2)
§5 α-β 模型:谁在什么时候最快 ← 代码 03(演示 3–5)
§6 扩展效率与通信重叠:梯度分桶 ← 代码 04(演示 1–2)
§7 数据并行的显存问题 ← 代码 04(演示 3)
§8 论文精读:ZeRO(显存分析与数据并行的通信);ZeRO-Infinity(预览)
§9 小结 §10 常见误区 §11 练习 §12 延伸阅读 §13 下一讲预告
0.4 关于 collectives.py
每块”设备”就是列表里的一个 NumPy 数组,通信就是把数组的一部分从一个设备复制到另一个。每一轮里所有设备同时收发,代码记下每次传输(谁发给谁、多少字节),结果和”直接把所有数组加起来”逐元素核对。
计时用 α-β 模型:发一条 n 字节的消息耗时 α + n/B。
- α 是固定开销(启动、同步、链路延迟),取几微秒;
- B 是带宽:节点内按 H100 NVLink 每个方向 450 GB/s,节点间按每卡一张 400 Gb/s 网卡(50 GB/s)。
一轮的时间 = 这一轮的最大 α + 最忙的那个设备的收发时间。模拟器的时间和闭式公式逐一核对过,完全一致。真实的 NCCL 还有分块流水、拓扑感知、多种算法自动选择,这里都没有建模。
1. 训练回顾:为什么可以并行 · Why training can be parallelized
slides 先复习了 DNN 训练的三步,每一次迭代:
- 前向传播:把一个 batch 的样本送进模型,算出预测和损失;
- 反向传播:从损失往回,算出每个可训练权重的梯度;
- 权重更新:用梯度更新权重,例如 w ← w − η·g。
关键在于第二步的梯度是每个样本梯度之和:
求和可以拆开算:把 n 个样本分给 N 块 GPU,各算一部分的和,最后再加起来。这就是 slides 问”怎样并行化 DNN 训练”的第一个答案——数据并行。
后面几讲会看到另外两种切法:
- 切模型的每一层(张量并行);
- 把不同的层放在不同的卡上(流水线并行)。
2. 数据并行:等价性与常见错误 · Data parallelism
2.1 三步
slides 第 9 页:
- 把训练数据分成若干个 batch,每块 GPU 拿一份;
- 每块 GPU 用完整的模型副本算自己那份数据的梯度;
- 把各 GPU 的梯度聚合起来(求和或平均),每块 GPU 用聚合后的梯度更新自己的模型副本。
因为每块 GPU 拿到的是同一个聚合结果、用同样的更新规则,所有副本在每一步之后仍然完全一样,下一步又从同一个模型出发。
2.2 代码验证(代码 01)
两层 MLP,4 类合成数据,float64:
| 检查 | 结果 |
|---|---|
| 4 个 worker 各算 64 个样本,环形 AllReduce 后 ÷ 256,vs 一块卡算 256 个样本的梯度 | 最大差 1.8e-15 |
| 训练 200 步后,4 个副本之间 | 逐位相同 |
| 训练 200 步后,副本 vs 单卡 | 差 1.1e-16 |
差别只来自浮点加法的顺序。数据并行不改变训练算法,只要全局 batch 和学习率不变。
2.3 三种悄悄破坏等价的错误(代码 01 演示 3)
- 各 worker 样本数不同,却对”本地平均梯度”再取简单平均。样本数 [100, 60, 60, 36] 时,和全局梯度差 5.0e-2:小的分片被过度加权。按样本数加权(或先求和、再除以全局样本数)差 8.9e-16。PyTorch DDP 默认对梯度取平均,所以要让每个 worker 的 batch 一样大。
- 忘了 AllReduce:每个 worker 只用本地梯度更新,副本立刻分开。
- 只同步了一部分参数:例如一个参数没有注册进 DDP,副本同样会分开。
后两种情况下,副本之间的差在第 10、50、200 步分别是 0.041、0.033、0.025(以及 0.044、0.037、0.026)。在这个简单的问题上它们收敛到相近的解,差距在缩小,但不回到 0——已经不是单卡的那个算法了。这类 bug 不会报错,只会让结果悄悄偏离。
BatchNorm 是另一个经典例子:每个 worker 用本地 batch 的统计量,和单卡大 batch 的结果不同,需要 SyncBatchNorm。
2.4 每一步要通信多少
梯度和参数一样大。BF16 梯度:
- GPT-2(1.5B)3 GB;
- LLaMA-2-7B 14 GB;
- GPT-3(175B)350 GB。
用环形 AllReduce 时,每块卡每步要发约 2 倍梯度大小的数据(§4)。
通信量和模型大小成正比,和 batch 无关;计算量和 batch 成正比。 这一点决定了数据并行的扩展性(§6)。
3. 参数服务器:集中式聚合的瓶颈 · Parameter server
3.1 做法
最直接的聚合方式是设一个中心:
- worker 把梯度推(push)给参数服务器;
- 服务器把它们加起来、更新参数;
- worker 再把新参数拉(pull)回来。
早期的大规模训练系统(如 DistBelief、Li Mu 等人的 Parameter Server)就是这样做的。参数服务器还支持异步更新:worker 不等别人,拿到参数就算,代价是用到的参数可能是”过时的”。
3.2 问题:所有流量都过服务器的网卡(代码 02)
slides 第 10–11 页的批评是:集中式通信让所有 worker 都和参数服务器通信,worker 一多就扩展不了。代码里每张网卡 50 GB/s,梯度 1 GB:
| worker 数 | 1 台服务器 | 8 台服务器 | 每台机器兼任服务器 | 环形 AllReduce |
|---|---|---|---|---|
| 8 | 0.32 s | 0.04 s | 0.040 s | 0.035 s |
| 64 | 2.56 s | 0.32 s | 0.040 s | 0.041 s |
| 256 | 10.24 s | 1.28 s | 0.040 s | 0.045 s |
8 个 worker、1 台服务器时:
- 每个 worker 的网卡只发、收各 1 M(M = 梯度大小);
- 服务器的网卡要收 8 M、发 8 M。
时间和 worker 数成正比。
分片(把参数切给 k 台服务器)能分摊 k 倍,但仍随 N 线性增长。
把分片推到极致:每台机器都兼任服务器,各负责 1/N 的参数。这时:
- “推”变成了每台机器把第 j 段发给机器 j、机器 j 把收到的加起来——这正是 reduce-scatter;
- “拉”变成了每台机器把自己负责的那段发给所有人——这正是 all-gather。
时间约 2M/B,和环形 AllReduce 几乎一样。去中心化的 AllReduce 就是从这里来的。
4. AllReduce 的几种算法 · AllReduce algorithms
AllReduce:N 个设备各有一个数组,结束时每个设备都得到所有数组的(逐元素)和。
4.1 朴素
每个设备把整个数组发给其他所有设备,然后各自求和。
- 1 轮;
- 每个设备发 (N − 1)M;
- 总通信量 N(N − 1)M。
slides:同样有可扩展性问题,每个设备都要和所有其他设备通信。
4.2 环形(ring)
把 N 个设备连成环,每个设备的数组切成 N 段。
- 聚合(reduce-scatter):N − 1 轮,每轮每个设备把一段(M/N)发给下一个设备,对方加到自己的那段上。结束时每个设备手里有一段是所有设备那一段的和。
- 广播(all-gather):再 N − 1 轮,每轮把完整的段转给下一个设备。
代码 03 演示 1 用 N = 4 逐轮打印了每个设备每一段里”加了谁”:reduce-scatter 第 3 轮后,设备 i 的第 (i + 1) mod 4 段是 0+1+2+3;all-gather 后每个设备都得到 1111(= 1 + 10 + 100 + 1000)。
统计:
- 2(N − 1) 轮;
- 每个设备发 2(N − 1)/N · M ≈ 2M;
- 总通信量 2(N − 1)M(slides 写作 2MN)。
4.3 树形(tree)
把设备组织成二叉树:
- 聚合:叶子往上,每个节点把整个数组(M)发给父节点,log N 轮;
- 广播:从根往下,log N 轮。
统计:
- 2 log N 轮;
- 总通信量 2(N − 1)M(每个非根节点上、下各传一次 M);
- 但中间节点每轮都要收、发整个 M。
4.4 蝶形(butterfly)
log N 轮。第 k 轮,设备 i 和 i XOR 2^k 交换整个数组并相加(递归加倍,recursive doubling)。
- 每个设备每轮发 M;
- 总通信量 NM·log N,最大;
- 轮数最少。
4.5 补充:减半—加倍(Rabenseifner)
slides 之外,MPI 库里常用的一个算法:
- reduce-scatter 用递归减半(第 k 轮和距离 N/2^k 的伙伴交换一半的数据);
- all-gather 用递归加倍。
它兼得两者:2 log N 轮(和树形一样少),每个设备只发 2(N − 1)/N · M(和环形一样少)。要求 N 是 2 的幂。
4.6 比较(代码 03 演示 2)
六种算法都在设备列表上实现,和直接求和比较误差都在 1e-14 以内。N = 16 时:
| 朴素 | 参数服务器 | 环形 | 树形 | 蝶形 | 减半—加倍 | |
|---|---|---|---|---|---|---|
| 轮数 | 1 | 2 | 30 | 8 | 4 | 8 |
| 总通信量 | 240M | 32M | 30M | 30M | 64M | 30M |
| slides 公式 | N(N−1)M | 2NM | 2NM | 2NM | NM log N | — |
| 每个设备最多发 | 15M | 16M(服务器) | 1.88M | 4M | 4M | 1.88M |
只看总通信量会漏掉一半的故事:环形、树形、减半—加倍的总量相同,但分布不同。环形把负载均匀地摊给每个设备;树形让中间节点更忙。时间还取决于轮数,这就要用 α-β 模型。
4.7 slides 的讨论题:环形为什么更高效、更可扩展?
slides 给出的三点:
- 延迟最好:每个 worker 每轮发 M/N,共 2N 轮,传输时间 M/N × 2N / 带宽 = 2M/带宽;
- 负载均衡:每个 worker 的负载一样;
- 更可扩展:每个 worker 发的量(约 2M)和 worker 数无关。
相比之下:
- 参数服务器:所有 worker 发 M 给服务器、再收 M,传输时间 M·N/带宽;
- 树形:每个 worker 每轮发 M,共 2 log N 轮,传输时间 2M·log N/带宽。
需要澄清的是 slides 说的”延迟”:
- 它指的是传输时间(带宽项);
- 按 α-β 模型,环形的启动延迟 2(N − 1)α 反而是几种算法里最大的,所以小消息时环形最慢(§5)。
5. α-β 模型:谁在什么时候最快 · Which algorithm when
5.1 公式
平坦拓扑(所有链路相同,α、B)下,代码 formula_time 的公式:
| 算法 | 时间 |
|---|---|
| 朴素 | α + (N − 1)M/B |
| 参数服务器 | 2(α + NM/B) |
| 环形 | 2(N − 1)(α + M/(NB)) |
| 树形 | 2 log N (α + M/B) |
| 蝶形 | log N (α + M/B) |
| 减半—加倍 | 2 log N · α + 2(N − 1)/N · M/B |
每个公式都由两部分组成:
- 延迟项:轮数 × α;
- 带宽项:关键路径上的字节数 / B。
5.2 消息大小(代码 03 演示 3)
8 卡 NVLink(α = 5 µs,450 GB/s)上,最快的算法标 *:
| 消息 | 朴素 | 环形 | 树形 | 蝶形 | 减半—加倍 |
|---|---|---|---|---|---|
| 4 KB | *5.1 µs | 70.0 µs | 30.1 µs | 15.0 µs | 30.0 µs |
| 1 MB | *20.6 µs | 73.9 µs | 43.3 µs | 21.7 µs | 33.9 µs |
| 16 MB | 253.9 µs | 132.2 µs | 243.3 µs | 121.7 µs | *92.2 µs |
| 1 GB | 15.56 ms | 3.96 ms | 13.36 ms | 6.68 ms | *3.92 ms |
- 小消息:延迟受限,轮数最少的最快。模型里朴素(1 轮)甚至赢了,因为模型允许一个设备在一轮里同时给 N − 1 个设备发消息、只付一次 α;真实系统里发 N − 1 条消息要付 N − 1 次开销。64 卡、64 KB 时最快的是蝶形。
- 大消息:带宽受限,每个设备发得最少的最快:环形和减半—加倍(≈ 2M)。蝶形每轮发整个 M,慢 log N / 2 倍;朴素发 (N − 1)M,最慢。
- 交叉点:环形和蝶形的交叉点在 8 卡 NVLink 上约 19.8 MB,64 卡 InfiniBand 上约 14.9 MB。
真实的 NCCL 会根据消息大小和拓扑在环、树等算法之间选择,并把大消息切成小块在环上流水传输以隐藏延迟。
5.3 卡数(代码 03 演示 4)
1 GB 的梯度,InfiniBand(α = 10 µs,50 GB/s),平坦拓扑:
| 卡数 | 参数服务器 | 朴素 | 树形 | 蝶形 | 环形 | 减半—加倍 |
|---|---|---|---|---|---|---|
| 8 | 0.32 s | 0.14 s | 120 ms | 60 ms | 35.1 ms | 35.1 ms |
| 64 | 2.56 s | 1.26 s | 240 ms | 120 ms | 40.6 ms | 39.5 ms |
| 1024 | 41 s | 20.5 s | 400 ms | 200 ms | 60.4 ms | 40.2 ms |
- 环形的带宽项趋近 2M/B = 40 ms,和卡数无关——这就是 slides 说的”可扩展”。
- 卡数很多时,环形的启动延迟 2(N − 1)α 变得可观(1024 卡时 20.5 ms);减半—加倍只有 2 log N · α,仍是 40.2 ms。这是超大规模时要用树形或分层算法的原因之一。
5.4 多节点:分层 AllReduce(代码 03 演示 5)
真实的集群是两层的:
- 节点内 8 块 GPU 用 NVLink(450 GB/s);
- 节点之间每块 GPU 一张网卡(50 GB/s)。
一个跨节点的大环里,每一轮都有消息过网卡,整个环按网卡的速度走,NVLink 被浪费了。
分层 AllReduce 分三步:
- 节点内 reduce-scatter(快),每块 GPU 只剩 M/8 需要跨节点;
- 8 块 GPU 各自用自己的网卡,和其他节点上”负责同一段”的 GPU 做环形 AllReduce;
- 节点内 all-gather。
4 个节点 × 8 卡时,分层比一个 32 卡的大环快 4.8–4.9 倍(20 轮 vs 62 轮)。
6. 扩展效率与通信重叠 · Scaling efficiency and overlap
6.1 扩展效率(代码 04 演示 1)
一个 13 亿参数的模型(BF16 梯度 2.6 GB):
- 计算时间 = 6 × 参数数 × 每卡 token 数 / (H100 峰值 × 45% MFU);
- 通信时间用分层环形 AllReduce;
- 效率 = 计算 / (计算 + 通信),这里先假设不重叠。
| 每卡 token、网卡 | 8 卡 | 64 卡 | 512 卡 | 4096 卡 |
|---|---|---|---|---|
| 2,048,50 GB/s | 78% | 62% | 60% | 52% |
| 2,048,12.5 GB/s | 78% | 39% | 36% | 33% |
| 16,384,50 GB/s | 97% | 93% | 92% | 90% |
| 16,384,12.5 GB/s | 97% | 84% | 82% | 80% |
读法:
- **一个节点内(8 卡,只走 NVLink)**效率都很高;一出节点,梯度就要过网卡。
- 每卡 token 少时,计算只有 36 ms,2.6 GB 的梯度过 50 GB/s 网卡要几十毫秒。
- 每卡 token 多 8 倍时,计算多 8 倍、通信不变,效率回到九成以上。
数据并行”喜欢大 batch”,但全局 batch = 每卡 batch × 卡数,太大会影响收敛(需要调学习率、热身等),这是数据并行扩展的另一个上限。
6.2 梯度分桶与重叠(代码 04 演示 2,本讲义补充)
反向传播是从最后一层往前算的。最后一层的梯度早就算完了,不必等整个反向结束才开始通信。PyTorch DDP 的做法(Li et al., VLDB 2020)是:
- 把参数张量按反向中就绪的顺序分进若干个桶(bucket);
- 一个桶里的梯度全部算完,就立刻在通信流上发起这个桶的 AllReduce;
- 通信和后面层的反向计算重叠。
代码模拟 64 卡、1.36 B 参数(384 个参数张量)的一步:
| 桶大小 | 0.1 MB | 1 MB | 5 MB | 25 MB | 100 MB | 500 MB | 一个桶 |
|---|---|---|---|---|---|---|---|
| 桶数 | 192 | 192 | 144 | 72 | 24 | 6 | 1 |
| 反向后还要等 | 13.1 ms | 13.1 ms | 3.0 ms | 0.5 ms | 1.1 ms | 2.9 ms | 22.7 ms |
| 效率 | 85% | 85% | 96% | 99% | 98% | 96% | 77% |
- 一个桶 = 不重叠:反向结束后还要等整整一次 AllReduce(22.7 ms)。
- 桶太小:每个桶的 AllReduce 都要付固定开销(分层环形 14 + 14 轮,约 0.21 ms)。192 个桶光固定开销就有 40 ms。
- 桶太大:开始得太晚,最后一个大桶要等反向快结束才发出。
DDP 默认的桶是 25 MB:大到能摊薄每次 AllReduce 的固定开销,小到能尽早开始通信。
另一个常见技巧是梯度累积:多个 micro-batch 只在最后一次反向时通信(DDP 的 no_sync),用更多计算摊薄通信。
7. 数据并行的显存问题 · The memory problem
slides 第 27 页:数据并行里每块 GPU 都存一整份模型,模型大到放不进一块卡就没法训练。到底要多少显存?
7.1 每个参数 16 字节
混合精度 + Adam 训练(ZeRO 论文的统计):
| 内容 | 每个参数 | 说明 |
|---|---|---|
| FP16 参数 | 2 B | 前向、反向用 |
| FP16 梯度 | 2 B | 反向产生 |
| FP32 参数副本 | 4 B | 优化器更新用(混合精度需要高精度的主权重) |
| FP32 动量(Adam 一阶矩) | 4 B | |
| FP32 方差(Adam 二阶矩) | 4 B | |
| 合计 | 16 B | ZeRO 记为 2Ψ + 2Ψ + KΨ,K = 12 |
slides 的统计把 FP32 的梯度副本也算进优化器状态(FP32 状态 16 B,总共 20 B),所以写”10 亿参数 → 20 GB/卡”。两种算法的差别只在于是否保留 FP32 梯度。
7.2 slides 的表(代码 04 演示 3)
| 模型 | 参数 | 模型状态(16 B/参数) |
|---|---|---|
| BERT-Large | 0.32 B | 5.12 GB |
| GPT-2 | 1.5 B | 24 GB |
| Turing-NLG | 17.2 B | 275 GB |
| GPT-3 | 175 B | 2,800 GB |
slides 的表正好是 16 B/参数。GPT-2 的 FP16 权重只有 3 GB,模型状态却要 24 GB——优化器状态占了大头。
一块 80 GB 的卡(A100 / H100)只放模型状态,最多 5 B 参数;V100 32 GB 最多 2 B。
这还没算:
- 激活:ZeRO 论文估计 GPT-2 1.5B、序列 1K、batch 32 的激活约 60 GB;用激活重计算(第 2 讲)可以降到约 8 GB,代价是多算约 33%。
- 临时缓冲区和显存碎片。
ZeRO 论文因此说,普通的数据并行在 32 GB 的 GPU 上训练超过 1.4 B 参数的模型就会显存不足。
7.3 冗余
数据并行让 N 块卡存了 N 份完全相同的优化器状态、梯度和参数。这是纯粹的冗余:每块卡只用自己的那份梯度做一次一模一样的更新。
ZeRO(Zero Redundancy Optimizer)的想法就从这里来:把这三样东西切成 N 份,每块卡只存 1/N。下一讲会看到,前两个阶段的通信量和普通数据并行完全一样。
8. 论文精读 · Paper reading
8.1 ZeRO: Memory Optimizations Toward Training Trillion Parameter Models(Rajbhandari et al., SC 2020)——本讲读与数据并行有关的部分
1. 问题:
- 模型从 BERT-Large(0.3B)、GPT-2(1.5B)走向 Megatron-LM(8.3B)、T5(11B)乃至万亿参数,单卡放不下。
- 普通数据并行不减少每卡显存。
- 模型并行(按层内切分)在节点内高效,跨节点时效率急剧下降:论文实测 40B 参数的 Megatron-LM 跨两个 DGX-2 节点,每块 V100 只有约 5 TFLOP/s,不到峰值的 5%。
2. 想法:先回答”显存都去哪儿了”,把训练的显存分成两类:
- 模型状态:优化器状态、梯度、参数,混合精度 Adam 共 16Ψ 字节;
- 剩余状态:激活、临时缓冲区、显存碎片。
对模型状态,ZeRO-DP 消除数据并行中的冗余(第 13 讲详述三个阶段);对剩余状态,ZeRO-R 做激活分片、固定大小的缓冲区、碎片整理。
论文还分析了数据并行的通信量:最先进的 AllReduce 实现分两步,reduce-scatter 和 all-gather 各传 Ψ 个元素,所以普通数据并行每个设备的通信量是 2Ψ。这正是本讲环形 AllReduce 的 2(N − 1)/N · M ≈ 2M。ZeRO 的前两个阶段只把 AllReduce 拆成”reduce-scatter 梯度 + all-gather 参数”,通信量仍是 2Ψ。
3. 验证:
- 在 400 块 GPU 上训练 100B 以上参数的模型,达到 15 PFLOP/s 的吞吐和超线性的扩展。论文说这比当时最好的方法模型大 8 倍、性能高 10 倍。
- 不用模型并行就能训练 13B 参数的模型(比 Megatron GPT 8.3B 和 T5 11B 还大)。
- Turing-NLG(17B)就是用它训练的。
4. 从系统的角度看:ZeRO 的出发点是一个简单的记账——16 字节 / 参数,其中 12 字节是只在更新时用到的优化器状态。它把问题从”怎样并行地算”转成”怎样不重复地存”,并且证明消除冗余不一定增加通信。这个分析框架(模型状态 vs 剩余状态、通信量按 Ψ 计)是理解后面所有大模型训练系统的基础。
5. 局限(本讲关心的部分):
- 数据并行本身不减少激活和计算,大模型仍需要激活重计算、张量 / 流水线并行配合;
- 全局 batch 随卡数增长,受收敛性限制;
- 第三阶段(切参数)的通信量增加到 3Ψ(第 13 讲)。
8.2 ZeRO-Infinity(Rajbhandari et al., SC 2021)——预览
论文指出 GPU 显存墙:三年里最大的稠密模型增长了 1000 多倍,而单卡显存只从 16 GB 增长到 80 GB(5 倍)。即使用 3D 并行:
- 光是放下一个万亿参数的模型就要 800 块 V100;
- 微调 GPT-3 也要 8 个 DGX-2 节点(128 块 GPU)。
ZeRO-Infinity 把 GPU、CPU、NVMe 的存储都用起来,并设计了一系列技术让训练不被 CPU / NVMe 的带宽拖慢:
- 在 512 块 V100 上训练 32 万亿参数的模型,是 3D 并行的 50 倍;
- 吞吐超过 25 PFLOP/s(峰值的 40%);
- 能在单个 DGX-2 节点上微调万亿参数的模型。
细节放在第 13 讲。
8.3 本讲的补充阅读:PyTorch DDP
S. Li et al., PyTorch Distributed: Experiences on Accelerating Data Parallel Training(VLDB 2020)介绍了 DDP 的工程设计:
- 梯度分桶(默认 25 MB);
- 通信与反向计算的重叠;
- 跳过同步(
no_sync,用于梯度累积); - 按参数就绪顺序分桶、处理没有用到的参数,等等。
§6.2 的模拟就是它的简化版。
9. 小结 · Summary
- 数据并行成立的全部理由是梯度对样本可加。代码验证了 4 卡和单卡的梯度只差 1e-15,训练 200 步后 4 个副本逐位相同。样本数不均、漏掉同步会悄悄破坏等价。
- 参数服务器把所有流量集中到服务器的网卡上,时间随 worker 数线性增长(1 GB 梯度、256 个 worker 要 10 秒)。分片到每台机器后,就变成了 reduce-scatter + all-gather。
- AllReduce 算法:
- 朴素:N(N−1)M,1 轮;
- 环形:2(N−1)M,2(N−1) 轮,每设备 ≈ 2M;
- 树形:2(N−1)M,2 log N 轮;
- 蝶形:NM log N,log N 轮;
- 减半—加倍:2(N−1)M,2 log N 轮,每设备 ≈ 2M。
- α-β 模型:小消息延迟受限,轮数少的赢;大消息带宽受限,环形 / 减半—加倍赢(交叉点十几到二十 MB)。环形的带宽项和卡数无关(→ 2M/B),但启动延迟 2(N − 1)α 随卡数增长;多节点用分层 AllReduce(快约 5 倍)。
- 扩展效率由”计算 ∝ batch、通信 ∝ 模型大小”决定。梯度分桶把通信藏进反向传播:25 MB 的桶让效率从 77% 升到 99%。
- 显存:混合精度 Adam 每个参数 16 字节,80 GB 的卡最多放 5 B 参数(不算激活)。N 份完全相同的模型状态是冗余,引出 ZeRO。
10. 常见误区 · Common misconceptions
- “数据并行会改变训练结果。” 不会(只差浮点加法的顺序),前提是全局 batch、学习率不变,并且所有参数都正确同步。代码里 200 步后副本和单卡只差 1e-16。
- “环形 AllReduce 总是最快的。” 只在大消息时。小消息时它的 2(N − 1) 轮启动延迟最多,反而最慢(8 卡 4 KB 时 70 µs,蝶形 15 µs)。NCCL 会按消息大小和拓扑选择算法。
- “环形 AllReduce 的通信量随卡数线性增长。” 总通信量 2(N − 1)M 随卡数增长,但每个设备只发 ≈ 2M,和卡数无关。每块卡有自己的链路,所以时间不随卡数增长(带宽项)。
- “桶越大越好(通信次数越少)。” 桶太大,通信开始得太晚,没法和反向传播重叠;整个梯度一个桶就是完全不重叠(效率 77%),25 MB 时 99%。
- “80 GB 的卡能训练 40 B 参数的模型(FP16 权重 80 GB)。” 训练不止要存权重:混合精度 Adam 每个参数 16 字节,80 GB 只够 5 B 参数,还没算激活。
- “参数服务器已经过时了,没有任何意义。” 它的集中式版本扩展不了,但”把参数分片给不同的机器、各自负责一部分的更新”正是 reduce-scatter / all-gather,也是 ZeRO 的思路。
11. 练习 · Exercises(含参考答案)
练习 1 N = 8 个设备、每个 M = 1 GB,分别算朴素、环形、树形、蝶形 AllReduce 的总通信量、轮数、每个设备发送的量。
答案:
算法 总通信量 轮数 每设备发送 朴素 8 × 7 × 1 = 56 GB 1 7 GB 环形 2 × 7 × 1 = 14 GB 14 1.75 GB 树形 14 GB 6 根和中间节点最多 3 GB 蝶形 8 × 1 × 3 = 24 GB 3 3 GB
练习 2 用 α-β 模型推导环形和蝶形 AllReduce 的交叉点:消息多大时两者一样快?代入 N = 8、α = 5 µs、B = 450 GB/s。
答案:
- 2(N − 1)(α + m/(NB)) = log₂N (α + m/B),整理得 m = (2(N − 1) − log₂N)·α / (log₂N/B − 2(N − 1)/(NB))。
- N = 8:分子 (14 − 3) × 5 µs = 55 µs;分母 (3 − 1.75)/(450 × 10⁹) ≈ 2.78 × 10⁻¹² s/B。
- 所以 m ≈ 19.8 MB,和代码 03 一致。
练习 3 解释为什么”每台机器兼任参数服务器、各负责 1/N 的参数”等价于 reduce-scatter + all-gather,并算出每台机器发送的字节数。
答案:
- 推:机器 i 把它的梯度第 j 段(M/N)发给机器 j,共发 (N − 1)M/N;机器 j 把收到的 N − 1 段和自己的相加——这就是 reduce-scatter 的结果。
- 拉:机器 j 把它负责的那段(已求和)发给其他 N − 1 台,又发 (N − 1)M/N——这就是 all-gather。
- 合计 2(N − 1)/N · M,和环形 AllReduce 相同;区别只在”同时发给所有人”(一轮,靠交换机)还是”沿环转发”(N − 1 轮)。
练习 4 4 个节点 × 8 卡,梯度 M。分层 AllReduce 里每块 GPU 跨节点发送多少字节?和一个 32 卡的大环相比呢?
答案:
- 分层:节点内 reduce-scatter 后每块 GPU 只剩 M/8 要跨节点;在 4 个节点之间做环形 AllReduce,每块 GPU 发 2 × 3/4 × M/8 = 3M/16 ≈ 0.19M,并且 8 块 GPU 的网卡同时工作。
- 大环:每块 GPU 发 2 × 31/32 × M ≈ 1.94M,而且每一轮都卡在跨节点的那几条链路上,整个环按网卡速度走。
练习 5 一个 7 B 参数的模型用数据并行训练,混合精度 Adam。每块 80 GB 的卡上模型状态要多少显存?放得下吗?
答案:16 × 7 = 112 GB > 80 GB,放不下,还没算激活。需要 ZeRO(切开优化器状态、梯度、参数)或模型并行。按 slides 的 20 B/参数算是 140 GB。
练习 6 每卡 token 数从 2,048 增加到 16,384(8 倍),代码 04 演示 1 里 64 卡、50 GB/s 的效率从 62% 变成 93%。推导:如果通信时间为 C、计算时间为 T,效率是多少?token 翻 8 倍后呢?
答案:
- 效率 = T / (T + C)。62% 意味着 C ≈ 0.61 T。
- token 翻 8 倍后计算变成 8T,通信不变,效率 = 8T / (8T + 0.61T) ≈ 93%。
练习 7 为什么 PyTorch DDP 按”反向传播中梯度就绪的顺序”而不是”参数注册的顺序”分桶?如果反过来会怎样?
答案:桶要等里面所有梯度都算完才能发起 AllReduce。按就绪顺序分桶,最早就绪的梯度在同一个桶里,桶能尽早发出、和后面的反向重叠。如果按注册顺序(前向顺序),第一个桶里是最先注册的参数(第一层),它们的梯度最后才算出来,于是所有桶都要等到反向快结束,重叠就没有了。
练习 8 (开放)数据并行的通信量和 batch 无关,而计算量和 batch 成正比。为什么我们不能一直加大 batch 来提高效率?有哪些办法在不加大全局 batch 的情况下提高扩展效率?
参考思路:
- 全局 batch 太大会影响收敛:需要相应调大学习率,超过某个”临界 batch”后收益递减,样本效率下降。
- 不加大 batch 的办法:
- 通信和计算重叠(分桶);
- 分层 AllReduce、更好的网络;
- 梯度压缩(低精度、稀疏化);
- 减少每块卡要同步的参数量:张量 / 流水线并行把模型切开,每块卡的梯度只是一部分;
- ZeRO 不减少通信量,但让更大的模型能用数据并行。
12. 延伸阅读 · Further reading
- S. Li et al., PyTorch Distributed: Experiences on Accelerating Data Parallel Training(VLDB 2020)——DDP 的分桶与重叠。
- A. Sergeev, M. Del Balso, Horovod: fast and easy distributed deep learning in TensorFlow(2018)——把环形 AllReduce 带进深度学习的开源框架。
- R. Thakur, R. Rabenseifner, W. Gropp, Optimization of Collective Communication Operations in MPICH(IJHPCA 2005)——环形、减半—加倍等集合通信算法与 α-β 分析。
- M. Li et al., Scaling Distributed Machine Learning with the Parameter Server(OSDI 2014);J. Dean et al., Large Scale Distributed Deep Networks(NeurIPS 2012,DistBelief)——参数服务器。
- P. Goyal et al., Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour(2017)——大 batch 下的学习率线性缩放与热身。
13. 下一讲预告 · Next lecture
数据并行的每块卡都存了一整份 16 字节 / 参数的模型状态,N 块卡就是 N 份冗余。第 13 讲《零冗余与 PyTorch FSDP》介绍 ZeRO 的三个阶段:
- 第 1 阶段切优化器状态(显存约 4 倍节省);
- 第 2 阶段再切梯度(约 8 倍);
- 第 3 阶段连参数也切(N 倍)。
前两个阶段的通信量和普通数据并行一样是 2Ψ,第三阶段增加到 3Ψ。我们会在模拟设备上真的跑 ZeRO 1 / 2 / 3,和普通数据并行逐位对齐,并为每块卡记显存和通信的账。然后看 PyTorch FSDP 怎样把它落地(分片单元、预取、编程接口),以及 ZeRO-Offload / ZeRO-Infinity 怎样再把 CPU 内存和 NVMe 用起来。