第 14 讲 · 分布式训练(一):大规模训练概览、多 GPU 通信与数据并行 · Distributed Training I: Communication and Data Parallelism

Large Language Model Systems · Lecture 14。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 14 · Mon 3/9 · Distributed Model Training(slides:llmsys-14-distributed-training.pdf,51 页)。 阅读材料:NCCL 用户文档(集合通信原语);Patarasuk & Yuan (2009) Bandwidth optimal all-reduce algorithms for clusters of workstations;Li et al. (2014) Scaling Distributed Machine Learning with the Parameter Server;原课 HW5 Problem 1(数据并行)。 依据说明:本讲覆盖 slides 的三部分:大规模训练概览(模型与数据规模的增长、DeepSeek-V3 与 LLaMA 3.1 的训练资源、可扩展训练的几种策略、参数服务器)、多 GPU 通信(NCCL、broadcast / reduce / all-reduce / reduce-scatter / all-gather 及其接口、CUDA 里的数据指针、点对点通信、用环实现、环上广播与切分成 S 段的流水线、NCCL 代码示例、用 NCCL 实现参数服务器)、基于 all-reduce 的数据并行(流程、朴素 all-reduce、环形 all-reduce 的逐步过程与 MPI 代码、参数服务器 vs all-reduce)。token 数(LLaMA 3.1 约 15.6T、DeepSeek-V3 14.8T)取自两份技术报告。集合通信的逐轮模拟与流量统计、α-β 模型的比较、gloo 的实测与拟合、HW5 规格的切分函数、模拟的数据并行与单进程的对比、样本数不等的偏差、梯度累积、异步 SGD 的过时梯度、加速比模型,都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture14/(5 个脚本 + collectives.py、data_parallel.py;都在 3 秒以内) 预计阅读时间:2.5–3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

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

  1. 用 6ND 和”每个参数 16 字节”估算大模型训练的算力与显存,说出为什么必须分布式,并区分”切数据”和”切模型”两类并行各解决什么问题。
  2. 说出 NCCL 的五个集合通信原语(broadcast、reduce、all-reduce、reduce-scatter、all-gather)的语义和缓冲区大小,以及 all-reduce = reduce-scatter + all-gather。
  3. 一步一步地走一遍环形 all-reduce,算出每个设备发送 2(K − 1)/K·N 字节,解释它为什么”带宽最优”、代价是什么。
  4. 用 α-β 模型比较不同的通信算法,知道消息大小怎样决定谁更快;理解流水线广播的 (K − 2 + S)·N/(S·B)。
  5. 实现数据并行的三个部件(切数据、平均梯度、训练一步),解释它为什么等价于单卡大批量、为什么通常不是逐位相同,并避开”样本数不等”的坑。
  6. 比较参数服务器与 all-reduce、同步与异步,估算数据并行的加速比。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
训练资源的估算:6ND、每参数 16 字节(第 1 讲 §5)DDP 的梯度分桶与通信 / 计算重叠(第 15 讲)
网络的延迟与带宽(P4)流水线并行、张量并行(第 16 讲);MoE 的专家并行(第 17 讲)
浮点加法不满足结合律(P2 §3)ZeRO 把优化器状态、梯度、参数切开(第 18 讲)
Mesh 与集合通信的初步印象(第 12 讲 §5)

0.3 本讲地图 Map

§1  为什么要分布式:规模、显存墙与算力墙;切数据 vs 切模型;参数服务器        ← 代码 01
§2  多 GPU 通信:NCCL 的五个原语;点对点                                      ← 代码 02
§3  用环来实现:环形 all-reduce;流水线广播;α-β 模型                         ← 代码 03、04
§4  数据并行:切数据、平均梯度、训练一步;等价性与坑;同步 vs 异步;加速比      ← 代码 05
§5 小结   §6 常见误区   §7 练习   §8 延伸阅读   §9 下一讲预告

1. 为什么要分布式 · Why distributed training

1.1 规模

slides 第 5–6 页:模型从 GPT-1(2018)到 GPT-4(约 1.8T)、DeepSeek-V3(671B),训练数据从几十亿 token 涨到 LLaMA 3.1、DeepSeek-V3 的十几万亿。slides 第 7 页给了两次真实训练的资源:

  • DeepSeek-V3(671B):2,048 块 H800,约 2 个月,共 266.4 万 GPU 小时。
  • LLaMA 3.1(405B):16,000 块 H100,共 3,084 万 GPU 小时。

用第 1 讲的 6ND 反推(01 号脚本第 1 部分;token 数取自技术报告):

模型6ND每块 GPU 实际MFU(≈)墙钟时间
DeepSeek-V3(每 token 激活 37B)3.29e24343 TFLOP/s35%54 天(2,048 块 H800)
LLaMA 3.1 405B3.79e25341 TFLOP/s35%78 天(16,384 块 H100)

两次训练每块 GPU 都只跑出峰值的三分之一多一点。剩下的去哪了?一部分是第 10–13 讲的单卡效率问题,更大一部分是多卡之间的通信与等待——第 14–18 讲的主题。(DeepSeek-V3 的总参数是 LLaMA 3.1 的 1.7 倍,GPU 小时却只有 1/12:MoE 每个 token 只激活 37B 参数,第 17 讲。)

1.2 两道墙

如果只有一块 GPU(01 号脚本第 2 部分):

模型训练状态(16 B / 参数)至少几块 80 GB 的卡一块卡按 40% MFU 要算
LLaMA 3.1 405B6.5 TB813,039 年
GPT-3 175B2.8 TB3525 年

显存墙:参数、梯度、Adam 状态就要几个 TB,差两个数量级(还没算激活);算力墙:即使放得下,也要算上千年。必须同时切分模型(放得下)和切分数据(算得完)。

1.3 切数据,还是切模型

slides 第 8 页把可扩展训练的策略分成两类:

策略切的是什么每块卡上的模型要通信什么
数据并行(单机 / 分布式)、参数服务器数据完整一份梯度(每步)
流水线并行(第 16 讲)模型的层1/P 的层段与段之间的激活
张量并行(第 16 讲)每个矩阵每个矩阵的 1/T每层的激活

纯数据并行不减少每块卡的显存:每个 worker 都要一整份模型(405B 时仍是 6.5 TB)。它解决的是”算得完”。模型并行解决”放得下”:张量 8 路 × 流水线 16 段,405B 的训练状态每份约 51 GB。真实的大模型训练都是几种并行的组合(3D 并行)。

1.4 经典做法:参数服务器

slides 第 9 页:

  1. 拉取(pull):server 把最新参数发给每个 worker;
  2. 每个 worker 拿到一份数据;
  3. 算本地梯度;
  4. 推送(push):worker 把梯度发给 server;
  5. server 汇总梯度、更新参数。

用 NCCL 实现时(slides 第 32 页),拉取就是 ncclBroadcast,推送就是 ncclReduce;而且要同步两次:worker 等 server 发参数,server 等 K 个 worker 的梯度都到齐。01 号脚本第 4 部分:每个 worker 每步收发 2N,与 K 无关;server 收发 2K·N,随 K 线性增长——它是整个系统的瓶颈。


2. 多 GPU 通信 · Multi-GPU communication

2.1 NCCL

slides 第 11 页:NCCL(NVIDIA Collective Communication Library)提供 GPU 之间的通信接口,包括集合通信和点对点的 send / recv;支持 PCIe、NVLink、InfiniBand、IP socket 等互连;每个操作都绑定在一个 CUDA 流上(和内核一样异步执行,第 3 讲)。数据指针可以是设备显存、注册过的主机内存(cudaHostRegister),或者统一内存(slides 第 18 页)。

2.2 五个原语

slides 第 12–17 页。02 号脚本第 1 部分用 4 个设备、每个 4 个数演示了每个原语调用前后的缓冲区:

原语做什么每个设备的输入 / 输出
Broadcastroot 的 N 个数拷到所有设备N / N
Reduce求和(或 max、min),结果只写到 rootN / N(只有 root)
AllReduce求和,每个设备都拿到结果(= Reduce + Broadcast)N / N
ReduceScatter求和后切成 K 段,第 r 段给 rank rN / N/K
AllGather每个设备的 N 个数拼成 K·N 个,人人一份N / K·N

它们都是”集合”操作:组里的每个设备都必须调用同一个函数(参数一致),否则谁也完成不了——分布式训练里最常见的死锁,就是某个 rank 少调了一次集合通信。

02 号脚本第 2 部分验证了三个恒等式:AllReduce = Reduce + Broadcast = ReduceScatter + AllGather(slides 第 17 页),以及参数服务器 = Broadcast(拉)+ Reduce(推)。第二种拆法是环形 all-reduce 的基础,也是 ZeRO 的基础(第 18 讲:只做 reduce-scatter,每个设备只需要自己负责的那 1/K 梯度)。

2.3 点对点

slides 第 19 页:ncclSend / ncclRecv 用 ncclGroupStart() / ncclGroupEnd() 包起来一起发起——否则每个设备都先阻塞在 send 上、等对方 recv,环上就互相等待。02 号脚本第 3 部分演示了”每个设备发给右边、从左边收”的整体移一位:环形算法的每一轮就是一次这样的移位;流水线并行在相邻两段之间传激活,用的也是点对点通信。

slides 第 31 页的 NCCL 代码示例:先用 ncclCommInitRank 建立通信器(一个进程管多块 GPU 时要用 group API 包起来),再对每块 GPU 调用 ncclAllReduce(sendbuff, recvbuff, size, ncclFloat, ncclSum, comm, stream),最后 cudaStreamSynchronize 等通信完成。


3. 用环来实现 · Ring algorithms

3.1 环上广播:为什么要切成小段

slides 第 20–30 页:NCCL 用环来移动数据。在 K 个设备的环上广播 N 字节(带宽 B):

  • 不切分:数据一跳一跳地传,第 2 个设备收完才能传给第 3 个……共 K − 1 跳,时间 (K − 1)·N/B。
  • 切成 S 段:第一段走完 K − 1 跳,后面的段一拍一个地跟上。总拍数 K − 2 + S,每拍只传 N/S:

T=(K−2+S)⋅NS⋅B→S≫KNBT = (K - 2 + S)\cdot\frac{N}{S\cdot B} \xrightarrow{S \gg K} \frac{N}{B}

和设备数几乎无关。03 号脚本第 3 部分的逐拍模拟(每拍每条链路传一段)与公式完全一致。但每条消息都有启动延迟 α:S 太大时延迟累积,总时间 (K − 2 + S)(α + N/(S·B)) 有一个最佳的 S——K = 8、N = 64 MB、B = 25 GB/s、α = 5 μs 时约 64 段(3.15 ms;不切分 17.96 ms)。NCCL 内部就是把数据切成固定大小的块在环上流水线传。

3.2 环形 all-reduce

slides 第 36–46 页:朴素的 all-reduce(都发给一个设备、求和、再广播)让那个设备的链路成为瓶颈。环形 all-reduce 把每个设备的数据切成 K 段,分两个阶段:

  1. reduce-scatter(K − 1 轮):第 i 轮,设备 r 把第 (r − i) mod K 段发给右边的邻居,邻居把它加到自己的同一段上。K − 1 轮后,设备 r 的第 (r + 1) mod K 段是所有设备这一段的总和。
  2. all-gather(K − 1 轮):第 i 轮,设备 r 把第 (r + 1 − i) mod K 段(已经求完和)发给右边,邻居直接覆盖。K − 1 轮后人人都有全部的总和。

03 号脚本第 1 部分用符号把 4 个 worker(数据 a0..a3、b0..b3、c0..c3、d0..d3)的每一轮打印出来,和 slides 逐页一致。例如 reduce-scatter 结束时:

worker 0:  a0        a1+b1+c1+d1  a2+c2+d2  a3+d3
worker 1:  a0+b0     b1           a2+b2+c2+d2 ...

每一轮里 K 条链路同时在传,每条只传 N/K。所以每个设备一共发送

2(K−1)⋅NK=2(K−1)KN≈2N2(K-1)\cdot\frac{N}{K} = \frac{2(K-1)}{K}N \approx 2N

slides 第 47–48 页的 MPI 代码就是这两个循环:reduce-scatter 里 send_chunk = (rank − i) mod size、recv_chunk = (rank − i − 1) mod size,收到后 reduce;all-gather 里 send_chunk = (rank − i + 1) mod size,用 MPI_Sendrecv 同时收发。

3.3 和其他算法比

03 号脚本第 2 部分(每个设备 256 KiB):

K环形:最忙设备发送 / N轮数全发给 0 号二叉树参数服务器:server 收发 / N
21.0021.001.002
81.75147.003.008
641.9712663.006.0064
  • 环形:每个设备发约 2N,与 K 无关、所有设备一样忙。这是 all-reduce 的下限(每个设备的数据要被所有人看到一次,每个人都要拿到完整的结果),所以叫带宽最优。
  • 全发给 0 号、参数服务器:中心节点的流量随 K 线性增长。二叉树:最忙设备发 log₂K 份,但轮数只有 2log₂K。
  • 环形的代价是轮数 2(K − 1):每轮都要付一次启动延迟。另外它的求和顺序是”沿着环”的,和 np.sum 不同,浮点结果差在最后几位(float32 时 K = 64 约 1e-5)。

3.4 α-β 模型

一条消息的时间 ≈ α(启动延迟)+ n/β(带宽)。三种算法:

Tring=2(K−1)(α+NKβ),Ttree=2⌈log⁡2K⌉(α+Nβ),Tnaive=2(α+(K−1)Nβ)T_{\text{ring}} = 2(K-1)\Big(\alpha + \frac{N}{K\beta}\Big),\quad T_{\text{tree}} = 2\lceil\log_2 K\rceil\Big(\alpha + \frac{N}{\beta}\Big),\quad T_{\text{naive}} = 2\Big(\alpha + \frac{(K-1)N}{\beta}\Big)

04 号脚本第 1 部分(K = 8、α = 5 μs、β = 25 GB/s):1–10 KB 的小消息”全发给 0 号”最快(只有 2 轮),100 KB 时二叉树,≥ 1 MB 时环形;环形超过二叉树的分界约 235 KB。训练里的梯度动辄几百 MB,属于带宽主导,所以环形及其变体是数据并行的主力;NCCL 会按消息大小和设备数在环、树等算法之间选择。

⏱ 04 号脚本第 2 部分在本机用 torch.distributed(gloo 后端)起 2 个、4 个进程做真正的 all-reduce,1 KiB 到 16 MiB:时间是一条”截距 + 斜率 × 大小”的直线,1 KiB 和 16 KiB 几乎一样慢。按环形模型拟合,4 个进程时每轮延迟约 130 μs、有效带宽约 4 GB/s——进程之间走操作系统的回环网络,延迟比 NVLink / InfiniBand 大两个数量级。数字不重要,形状重要:任何互连都是”固定延迟 + 按字节付费”。

第 3 部分把它放回训练里:8 块卡做数据并行、每卡每步 2048 个 token,梯度 all-reduce 和一步前向 + 反向的时间比与模型大小无关(两者都正比于参数量),只取决于互连和每卡的批:NVLink 级的互连上通信也要占 43%,InfiniBand 级 82%,以太网级 97%(都是不重叠时)。


4. 数据并行 · Data parallelism

4.1 流程

slides 第 35 页:每个 worker 都有一份完整的模型;① 切数据,每个 worker 一份;② 各自算本地梯度;③ all-reduce 求平均梯度;④ 每个 worker 用同样的平均梯度更新参数。HW5 Problem 1 要实现三个函数。

切数据(partition_indices,HW5 1.1):用局部的 random.Random(seed) 打乱(不碰全局随机状态),按”整除 + 余数”切——rank r 拿 size // world_size 个,前 size % world_size 个 rank 各多一个。按比例切片会悄悄丢掉余下的样本。所有 rank 用同一个种子各自算出同一份切分、只取自己那份,不需要通信(05 号脚本第 1 部分)。

平均梯度(average_gradients,HW5 1.2):对每个参数的梯度 dist.all_reduce(…, op=SUM) 再除以 world_size(gloo 不支持 AVG);没有参与前向的参数梯度是 None,要跳过。(更快的做法是把所有梯度拼成一块、只 all-reduce 一次——第 15 讲的桶。)

训练一步(train_step,HW5 1.3):清梯度 → 前向 + 损失 → 反向 → 平均梯度 → 优化器更新。

4.2 为什么等价于单卡大批量,又为什么不是逐位相同

整批的平均梯度 = 各份平均梯度的平均(前提是每份一样大)。每个 rank 都用同一个平均梯度更新,所以 K 份副本始终一致,不需要再同步参数。05 号脚本第 2 部分:K = 2、4、8 个 rank 训练 20 步,各副本逐位相同;和单进程整批训练的差别只有 5.6e-17(float64)——数学上等价,但求和顺序不同(先 rank 内、再 rank 间,环形还按段走不同的顺序),浮点加法不满足结合律,所以通常不是逐位相同。HW5 的检查本来就是”足够接近”。

坑:样本数不相等(05 号脚本第 3 部分):10 个样本分给 4 个 rank 是 3、3、2、2。直接平均各 rank 的平均梯度,2 个样本的 rank 里每个样本的权重是 3 个样本的 1.5 倍——与整批梯度的相对差 21%,不是舍入误差。修正:按样本数加权,或者损失用求和、all-reduce 之后除以全局样本数(相对差 2.9e-16)。语言模型里每条序列的 token 数不同,同样要按 token 数归一化。

梯度累积(05 号脚本第 4 部分):一个 rank 把一批分成 4 个微批,梯度在 .grad 里累加、最后才更新,与 4 个 rank 的数据并行差 2.8e-17。数据并行是”空间上”分一批,梯度累积是”时间上”分一批;两者组合时,每个 rank 先本地累积、只在最后一个微批之后才 all-reduce——第 15 讲 DDP 的 no_sync()。

4.3 参数服务器 vs all-reduce;同步 vs 异步

slides 第 49 页:参数服务器每步要同步两次(参数、梯度);all-reduce 式的数据并行让每个 worker 自己更新参数——“冗余吗?本地更新比跨 GPU 传数据快得多”。

参数服务器还可以异步:不等所有 worker,谁的梯度先到就先更新。代价是梯度过时:K 个 worker 同时在算时,一份梯度到达之前服务器已经被更新了约 K − 1 次。05 号脚本第 5 部分(8 个 worker、400 次梯度计算):

学习率同步异步 τ = 0异步 τ = 7异步 τ = 24
0.50.14540.14500.14590.2676
1.00.19090.12000.1303发散
2.00.37790.09930.5413发散

梯度不过时(τ = 0,相当于顺序的小批量 SGD)时异步甚至更快(更新次数多 8 倍);过时以后越来越差,学习率大时发散。现代的大模型训练几乎都用同步的 all-reduce:结果可复现、和单卡大批量等价、调参经验可以迁移。

4.4 加速比

不重叠时,K 块卡的一步 = T₁/K + T_通信,加速比 = T₁ / (T₁/K + T_通信)。05 号脚本第 6 部分(单卡 20 ms,环形 all-reduce,α = 5 μs、β = 25 GB/s):梯度 10 MB 时 8 块卡 6.12 倍;100 MB 时只有 2.09 倍;400 MB 时 2 块卡反而只有 0.77 倍——通信比单卡计算还慢。HW5 要求 2 块 GPU 至少快 1.4 倍,需要 T_通信 ≤ T₁/1.4 − T₁/2 ≈ 0.21·T₁。K 增大时计算按 1/K 缩短、通信几乎不变,加速比饱和——第 15 讲的 DDP 让通信和反向计算重叠,把 T_通信藏起来一大部分。


5. 小结 · Summary

  1. 大模型训练同时撞上显存墙(几 TB 的训练状态)和算力墙(单卡上千年),必须切数据、切模型;真实训练每块 GPU 只跑出约 35% 的峰值,通信与等待是主要损失之一。
  2. NCCL 的五个原语:broadcast、reduce、all-reduce、reduce-scatter、all-gather;all-reduce = reduce + broadcast = reduce-scatter + all-gather。
  3. 环形 all-reduce:2(K − 1) 轮,每个设备发 2(K − 1)/K·N ≈ 2N,与 K 无关、负载均衡,带宽最优;代价是轮数多、延迟大。流水线广播把数据切成 S 段,时间 (K − 2 + S)·N/(S·B) → N/B。
  4. α-β 模型:小消息延迟主导(少轮数的算法快),大消息带宽主导(环形快);分界在几百 KB 量级。
  5. 数据并行:切数据、平均梯度、各自更新;等价于单卡大批量(相等大小的分片时),但求和顺序不同所以不是逐位相同;样本数不等要加权;梯度累积是时间维度上的数据并行。
  6. 参数服务器的中心节点是瓶颈;异步更新带来过时的梯度;不重叠时加速比受通信限制而饱和。

6. 常见误区 · Common pitfalls

  • “数据并行能让大模型放进显存。” 每块卡仍是一整份模型;要省显存得靠模型并行或 ZeRO。
  • “环形 all-reduce 的通信量随设备数增长。” 每个设备发 2(K − 1)/K·N,几乎不变;增长的是轮数(延迟)。
  • “all-reduce 越大越慢,所以要切成越小越好。” 切得太小,每条消息的启动延迟会累积起来。
  • “数据并行的结果和单卡逐位相同。” 数学上等价,浮点上求和顺序不同,通常差在最后几位。
  • “各 rank 的平均梯度再平均就是整批的平均梯度。” 只有每个 rank 的样本数相同时才成立。
  • “异步 SGD 没有等待,所以一定更快。” 梯度过时会拖慢收敛甚至发散。
  • “每个 worker 都做一遍参数更新是浪费。” 本地更新比在 GPU 之间传参数快得多。
  • “加了 GPU 就一定更快。” 通信比计算还慢时,2 块卡可能比 1 块还慢。

7. 练习 · Exercises

  1. 一个 70B 参数的模型用 2T token 训练,按 6ND 和每块 GPU 350 TFLOP/s,需要多少 GPU 小时?1024 块 GPU 要几天?
  2. 4 个设备的 reduce-scatter 之后,每个设备的输出缓冲区多大(输入 N 个数)?all-gather 的输出呢?
  3. 写出 K = 3 时环形 all-reduce 的全部 4 轮(数据 a0 a1 a2、b0 b1 b2、c0 c1 c2),每轮之后每个设备的状态。
  4. 证明环形 all-reduce 每个设备发送的字节数是 2(K − 1)N/K。
  5. K = 16、N = 1 GB、β = 25 GB/s、α = 10 μs:环形 all-reduce 要多久?其中延迟占多少?
  6. 环上广播 N = 100 MB,K = 4,B = 10 GB/s:不切分要多久?切成 100 段呢?每条消息另有 α = 20 μs 时,S = 100 好不好?
  7. α = 5 μs、β = 25 GB/s、K = 8:多大的消息时,环形与”全发给 0 号”一样快?
  8. 参数服务器有 1 台 server、16 个 worker,参数 2 GB。server 的网卡 25 GB/s,每步至少要多少通信时间?换成 16 个设备的环形 all-reduce 呢?
  9. 一批 1000 个样本切给 3 个 rank(334、333、333),损失用平均。直接平均三个 rank 的平均梯度,样本的权重偏差多大?
  10. 用 4 个 rank 的数据并行、每个 rank 每步 32 个样本,等价的单卡批大小是多少?如果每个 rank 再做 2 步梯度累积呢?
  11. 单卡一步 100 ms,梯度 1 GB,β = 25 GB/s,α 忽略:2、4、8 块卡不重叠时的加速比是多少?
  12. 为什么 all-reduce 之后不需要再广播参数?在什么情况下各副本的参数会悄悄变得不一样?

参考答案

  1. 6 × 70e9 × 2e12 = 8.4e23 FLOP;÷ 350e12 ≈ 2.4e9 GPU 秒 ≈ 66.7 万 GPU 小时;÷ 1024 ≈ 651 小时 ≈ 27 天。
  2. reduce-scatter 输出 N/4;all-gather(每个设备输入 N/4)输出 N;一般地 all-gather 的输出是输入的 K 倍。
  3. 第 1 轮(reduce-scatter):0 把 a0 给 1,1 把 b1 给 2,2 把 c2 给 0 → 0:[a0, a1, a2+c2],1:[a0+b0, b1, b2],2:[c0, b1+c1, c2]。第 2 轮:0 把 a2+c2 给 1,1 把 a0+b0 给 2,2 把 b1+c1 给 0 → 0 的段 1 = a1+b1+c1,1 的段 2 = a2+b2+c2,2 的段 0 = a0+b0+c0。all-gather 第 1 轮:0 把段 1 给 1,1 把段 2 给 2,2 把段 0 给 0;第 2 轮再转一次,人人齐全。
  4. 两个阶段各 K − 1 轮,每轮每个设备发一段(N/K 字节):2(K − 1)·N/K。
  5. 2 × 15 × (10 μs + 1e9 / (16 × 25e9) s) = 30 × (10 μs + 2.5 ms) = 75.3 ms,延迟只占 0.3 ms(0.4%)。
  6. 不切分:3 × 100 MB / 10 GB/s = 30 ms;S = 100:(4 − 2 + 100) × 1 MB / 10 GB/s = 10.2 ms;加 α:102 × (20 μs + 0.1 ms) = 12.2 ms,仍然好得多。
  7. 令 2·7·(α + N/(8β)) = 2(α + 7N/β):14α + 1.75N/β = 2α + 14N/β ⇒ 12α = 12.25N/β ⇒ N = 12α·β/12.25 ≈ 122 KB。小于它时”全发给 0 号”更快。
  8. server 收 16 × 2 GB、发 16 × 2 GB:至少 64 GB / 25 GB/s = 2.56 s(收发可并行时 1.28 s)。环形:每个设备发 2 × 15/16 × 2 GB = 3.75 GB,3.75 / 25 = 0.15 s。
  9. 整批平均:每个样本权重 1/1000。三个 rank 平均:rank 0 的样本权重 1/(3 × 334) = 1/1002,另两个 rank 1/999。偏差只有 0.2%–0.1%——样本数接近时影响很小,但 10 个样本分 4 份时就是 21%(05 号脚本)。
  10. 128;再加 2 步梯度累积:256。
  11. T_通信 ≈ 2(K − 1)/K × 1 GB / 25 GB/s:K = 2 时 40 ms,加速比 100 / (50 + 40) = 1.11×;K = 4:60 ms,100 / 85 = 1.18×;K = 8:70 ms,100 / 82.5 = 1.21×——通信主导,加速比几乎不增长。
  12. 每个副本初始参数相同、每一步用同一个平均梯度、做同样的确定性更新,所以参数自动保持一致。会悄悄变得不一样的情况:初始参数不同(各 rank 用了不同的随机种子初始化,又没有从 rank 0 广播一次);某个参数的梯度没有参与 all-reduce(只在某些 rank 上用到);更新里有各 rank 不同的随机性(例如往梯度里加噪声却用了不同的种子);或者 all-reduce 的结果在不同 rank 上舍入不同(某些非确定性的实现)。DDP 在构造时会先从 rank 0 广播一次参数(第 15 讲)。

8. 延伸阅读 · Further reading

  • NCCL 文档:“Collective Operations”、“Point-to-point communication”。
  • Patarasuk & Yuan (2009) Bandwidth optimal all-reduce algorithms for clusters of workstations:环形 all-reduce 的带宽最优性。
  • Thakur, Rabenseifner & Gropp (2005) Optimization of Collective Communication Operations in MPICH:各种集合通信算法与 α-β 分析。
  • Li et al. (2014) Scaling Distributed Machine Learning with the Parameter Server(OSDI)。
  • Sergeev & Del Balso (2018) Horovod: fast and easy distributed deep learning in TensorFlow:把环形 all-reduce 带进深度学习。
  • Goyal et al. (2017) Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour:数据并行的大批量训练与学习率缩放。
  • Dean et al. (2012) Large Scale Distributed Deep Networks(DistBelief):异步 SGD 与参数服务器。

9. 下一讲预告 · Next lecture

本讲的数据并行是”算完所有梯度、再一起 all-reduce”:反向传播的时候网络闲着,all-reduce 的时候 GPU 闲着。第 15 讲读 PyTorch DDP 的论文(Li et al., VLDB 2020):DDP 怎样在反向传播进行到一半时就开始通信——把梯度分进一个个”桶”,某个桶里的梯度一旦全部算完就立刻发起它的 all-reduce,和剩下的反向计算重叠;桶多大合适(太小延迟主导,太大重叠不起来);no_sync() 怎样配合梯度累积;没有参与前向的参数怎么办;以及 autograd 钩子、Reducer 这些实现细节。我们会用时间线模拟桶大小与重叠的关系,并用 gloo 在本机真正起几个进程跑 DDP,和单进程训练对比。

← 返回课程目录