第 12 讲 · ML 并行化(一):数据并行 · ML Parallelization Part 1: Data Parallelism

Advanced Topics in Machine Learning Systems (LLM Edition) · Lecture 12。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

对应 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 学习目标

学完这一讲,你应该能够:

  1. 用”梯度对样本可加”解释数据并行为什么等价于大 batch 的 SGD,并说出三种会悄悄破坏等价的错误。
  2. 说清楚参数服务器为什么扩展不了,以及”分片 + 每台机器兼任服务器”怎样变成了 reduce-scatter + all-gather。
  3. 手算朴素、环形、树形、蝶形 AllReduce 的轮数、总通信量和每个设备的通信量,并和 slides 的表对照。
  4. 用 α-β 模型判断:给定消息大小和卡数,哪种 AllReduce 最快;理解”延迟受限”和”带宽受限”两个区间。
  5. 估算数据并行的扩展效率;解释梯度分桶为什么能把通信藏进反向传播,桶为什么不能太大也不能太小。
  6. 用 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 训练的三步,每一次迭代:

  1. 前向传播:把一个 batch 的样本送进模型,算出预测和损失;
  2. 反向传播:从损失往回,算出每个可训练权重的梯度;
  3. 权重更新:用梯度更新权重,例如 w ← w − η·g。

关键在于第二步的梯度是每个样本梯度之和:

∇wL=∑i=1n∇wℓ(xi,yi;w).\nabla_w L = \sum_{i=1}^{n} \nabla_w \ell(x_i, y_i; w).

求和可以拆开算:把 n 个样本分给 N 块 GPU,各算一部分的和,最后再加起来。这就是 slides 问”怎样并行化 DNN 训练”的第一个答案——数据并行。

后面几讲会看到另外两种切法:

  • 切模型的每一层(张量并行);
  • 把不同的层放在不同的卡上(流水线并行)。

2. 数据并行:等价性与常见错误 · Data parallelism

2.1 三步

slides 第 9 页:

  1. 把训练数据分成若干个 batch,每块 GPU 拿一份;
  2. 每块 GPU 用完整的模型副本算自己那份数据的梯度;
  3. 把各 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)

  1. 各 worker 样本数不同,却对”本地平均梯度”再取简单平均。样本数 [100, 60, 60, 36] 时,和全局梯度差 5.0e-2:小的分片被过度加权。按样本数加权(或先求和、再除以全局样本数)差 8.9e-16。PyTorch DDP 默认对梯度取平均,所以要让每个 worker 的 batch 一样大。
  2. 忘了 AllReduce:每个 worker 只用本地梯度更新,副本立刻分开。
  3. 只同步了一部分参数:例如一个参数没有注册进 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
80.32 s0.04 s0.040 s0.035 s
642.56 s0.32 s0.040 s0.041 s
25610.24 s1.28 s0.040 s0.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 段。

  1. 聚合(reduce-scatter):N − 1 轮,每轮每个设备把一段(M/N)发给下一个设备,对方加到自己的那段上。结束时每个设备手里有一段是所有设备那一段的和。
  2. 广播(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)

把设备组织成二叉树:

  1. 聚合:叶子往上,每个节点把整个数组(M)发给父节点,log N 轮;
  2. 广播:从根往下,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 时:

朴素参数服务器环形树形蝶形减半—加倍
轮数1230848
总通信量240M32M30M30M64M30M
slides 公式N(N−1)M2NM2NM2NMNM log N—
每个设备最多发15M16M(服务器)1.88M4M4M1.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 µs70.0 µs30.1 µs15.0 µs30.0 µs
1 MB*20.6 µs73.9 µs43.3 µs21.7 µs33.9 µs
16 MB253.9 µs132.2 µs243.3 µs121.7 µs*92.2 µs
1 GB15.56 ms3.96 ms13.36 ms6.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),平坦拓扑:

卡数参数服务器朴素树形蝶形环形减半—加倍
80.32 s0.14 s120 ms60 ms35.1 ms35.1 ms
642.56 s1.26 s240 ms120 ms40.6 ms39.5 ms
102441 s20.5 s400 ms200 ms60.4 ms40.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 分三步:

  1. 节点内 reduce-scatter(快),每块 GPU 只剩 M/8 需要跨节点;
  2. 8 块 GPU 各自用自己的网卡,和其他节点上”负责同一段”的 GPU 做环形 AllReduce;
  3. 节点内 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/s78%62%60%52%
2,048,12.5 GB/s78%39%36%33%
16,384,50 GB/s97%93%92%90%
16,384,12.5 GB/s97%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)是:

  1. 把参数张量按反向中就绪的顺序分进若干个桶(bucket);
  2. 一个桶里的梯度全部算完,就立刻在通信流上发起这个桶的 AllReduce;
  3. 通信和后面层的反向计算重叠。

代码模拟 64 卡、1.36 B 参数(384 个参数张量)的一步:

桶大小0.1 MB1 MB5 MB25 MB100 MB500 MB一个桶
桶数192192144722461
反向后还要等13.1 ms13.1 ms3.0 ms0.5 ms1.1 ms2.9 ms22.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 BZeRO 记为 2Ψ + 2Ψ + KΨ,K = 12

slides 的统计把 FP32 的梯度副本也算进优化器状态(FP32 状态 16 B,总共 20 B),所以写”10 亿参数 → 20 GB/卡”。两种算法的差别只在于是否保留 FP32 梯度。

7.2 slides 的表(代码 04 演示 3)

模型参数模型状态(16 B/参数)
BERT-Large0.32 B5.12 GB
GPT-21.5 B24 GB
Turing-NLG17.2 B275 GB
GPT-3175 B2,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

  1. 数据并行成立的全部理由是梯度对样本可加。代码验证了 4 卡和单卡的梯度只差 1e-15,训练 200 步后 4 个副本逐位相同。样本数不均、漏掉同步会悄悄破坏等价。
  2. 参数服务器把所有流量集中到服务器的网卡上,时间随 worker 数线性增长(1 GB 梯度、256 个 worker 要 10 秒)。分片到每台机器后,就变成了 reduce-scatter + all-gather。
  3. 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。
  4. α-β 模型:小消息延迟受限,轮数少的赢;大消息带宽受限,环形 / 减半—加倍赢(交叉点十几到二十 MB)。环形的带宽项和卡数无关(→ 2M/B),但启动延迟 2(N − 1)α 随卡数增长;多节点用分层 AllReduce(快约 5 倍)。
  5. 扩展效率由”计算 ∝ batch、通信 ∝ 模型大小”决定。梯度分桶把通信藏进反向传播:25 MB 的桶让效率从 77% 升到 99%。
  6. 显存:混合精度 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 GB17 GB
环形2 × 7 × 1 = 14 GB141.75 GB
树形14 GB6根和中间节点最多 3 GB
蝶形8 × 1 × 3 = 24 GB33 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 用起来。

← 返回课程目录