Skip to content

原文:第 9 章 说明:忠实翻译原网页内容,并补入与经典文献、业界系统的对照。术语首次出现给出英文锚点。

第 9 章 分布式训练与数据并行

本章在体系中的位置

这是「大模型训练」这块拼图的第一块。第 1 章 1.7 节算过一笔账:训练需求十年涨了约七个数量级,单卡性能只涨约三个——这个扩展缺口(scaling gap)本课程给了三个答案,第 9 章就是第一个:数据并行(data parallelism),把一批数据切开、让每张卡各持一份模型副本,再把梯度同步起来。数据并行简单,但它会在每张卡上复制同样的参数、梯度与优化器状态,于是撞上本章的主角——内存墙(memory wall)。撞墙后的出路在第 10 章(模型并行与专家并行):不再复制,而是把模型本身切开。而本章讲透的 all-reduce、all-gather 这些集合通信原语,正是第 12 章分布式训练基础设施(NVLink、RDMA、网络拓扑)必须服务好的核心负载。读完本章,你应该能回答一个问题:为什么大模型训练的第一个瓶颈不是算力,而是内存与通信。

现代大语言模型(LLM)已经大到不可思议:参数量从十亿一路涨到万亿。模型确实越训越强,但这份能力是拿「单机装不下、单卡算不动」换来的。一台机器再也扛不住这类模型的存储与计算需求,于是分布式训练(distributed training)从「可选项」变成了「必选项」。

这一章给出 LLM 分布式训练的系统性图景,路线很直:先量化模型到底有多大——参数规模、训练计算量、硬件供给;再拆解训练时内存都花在了哪儿——引出内存墙,以及随之而来的通信瓶颈;然后介绍两种核心的数据并行范式——参数服务器与 all-reduce 集合通信,并把 All-Reduce、All-Gather 这些操作逐个讲透;最后落回内存优化——ZeRO 分片与激活重计算。每一节都先算账,再讲技术。


9.1 大模型的量化基础

这一节回答一个前置问题:大模型到底「大」在哪儿? 答案用三本账概括:参数账、算力账、硬件账。

9.1.1 Transformer 中的参数规模

一个参数(parameter)是什么。 神经网络里的参数,就是权重矩阵里的一个标量值,在训练过程中被学习出来。模型的参数量,就是它包含的所有可学习标量的个数。Transformer 的参数量会随规模迅速膨胀,来源有三个核心组件:

  • 嵌入层(embedding layer),参数量为

一个词表 50,000、嵌入维度 12,000 的配置,仅这一层就数亿参数。

  • 多头注意力(multi-head attention,MHA),每个头需要 Query、Key、Value 三套投影矩阵:

  • 层堆叠(layer stacking),总参数量随层数 线性增长。

前馈网络(feed-forward network,MLP) 的参数:

用两条常用的设计规则化简:(1) (前馈中间层把隐藏维度放大四倍);(2) (所有注意力头的维度加起来等于隐藏维度)。

于是整个 Transformer 的参数量收敛成一个很紧凑的公式:

这个公式解释了模型怎么一路涨到数千亿参数。拿 GPT-3 175B 验证一下:,代入得

正好是 1750 亿。以后你看到「某模型多少层、隐藏多大」,用这个公式十秒就能估出参数量——这是所有后续内存与通信估算的起点。

9.1.2 训练计算量(FLOPs)

参数再多,算力跟不跟得上?训练总计算量有个著名的经验近似:

其中 是参数量, 是训练 token 数。系数 6 不是魔法,是两笔账相加:

  • 前向传播:每个参数- token 组合约 2 次 FLOP。一次矩阵乘法里,每个权重被读出来、乘一次、累加一次,大致两笔运算;
  • 反向传播:约 4 次 FLOP。既要算损失对激活的梯度,也要算对权重的梯度,还要把误差信号回传——工作量大约是前向的两倍。

一个 1750 亿参数的模型,在 3000 亿 token 上训练:

这个数字大得离谱,但请先记住它——下一节要拿它除以天数,看硬件够不够。顺带一提,「6ND」如今是业界标配的口径:Chinchilla 这类缩放定律研究,就是在同一框架上讨论「多少参数配多少 token 才算训得值」。

9.1.3 硬件供给

GPU 的理论峰值很高,但实际利用率总被三样东西拖住:通信开销、内存带宽、同步延迟。业界用模型 FLOPs 利用率(Model FLOPs Utilization,MFU)来衡量「理论算力被用出了几成」——第 2 章 2.2 节讲过它怎么分解成三个因子——典型值在 50% 左右。

除以 30 天,得到约 FLOP/s 的持续算力需求;按 50% MFU 折算,需要约 FLOP/s 的峰值算力。一块 A100 的 FP16 张量核峰值是 FLOP/s,所以 30 天训完 175B,理论上至少需要数百张 A100,还要让它们一刻不停地高效运转

「30 天」不是随便说的,它是「训得起」与「训不起」的分界线。 而「上千张 GPU 高效运行」这句话里,真正的难点不在「上千张」,而在「高效」——如何让通信、同步、负载均衡不把算力白白浪费掉,正是本章后面所有内容要解决的事。


9.2 内存墙

算力账算完,还有第二本账:内存账。就算你凑齐了算力,显存也不够装。这一节把训练时的内存逐项拆开。

9.2.1 内存分解

训练需要静态动态两类内存:

  • 静态内存(static memory):参数(FP16)、梯度(FP16)、优化器状态(FP32);
  • 动态内存(dynamic memory):激活值(activation),随批大小和序列长度变化。

以 1750 亿参数模型为例,静态账是这样的:

  • 参数 + 梯度:
  • Adam 优化器状态:
  • 总计 TB。

其中「12 字节」不是拍脑袋,它来自 Adam 为每个参数维护的三份 FP32 量:主权重副本、一阶矩、二阶矩,各 4 字节。这就是下一节要讲的 Adam。

9.2.2 Adam 优化器

Adam(Adaptive Moment Estimation,自适应矩估计)是深度学习里用得最多的优化器之一。它的思路是:在梯度下降的基础上,为每个参数维护历史梯度信息,让更新更稳、更适应参数自身的尺度。原始出处是 Kingma & Ba 2015 年的 ICLR 论文。

标准梯度下降只用当前梯度更新参数:

其中 是当前参数, 是学习率, 是梯度。Adam 在标准梯度下降之上,为每个参数多维护两个量。

第一个是一阶矩估计(first moment estimate),记作

它本质上是动量(momentum):把梯度方向在时间上平滑,让优化器在损失面上走得连贯、少震荡。

第二个是二阶矩估计(second moment estimate),记作

它追踪梯度平方,用来给每个参数自适应地调节学习率:梯度大而嘈杂的参数,实际更新步幅被压小;梯度一直很小的参数,反而能拿到相对更大的步幅。

因为 都从零初始化,训练早期会偏向零,Adam 于是做偏差校正(bias correction)

最终更新写成:

其中 是防止除零的小常数。惯例参数

内存代价藏在细节里。 与参数一一对应,实践中还要保留一份 FP32 主权重保证数值精度——每个参数因此要多占 12 字节。这就是 9.2.1 里「Adam 状态 2.1 TB」的来源。优化器状态,是静态内存里最大的一块,没有之一。

9.2.3 内存墙的现实

GPU 显存容量的增长速度,远远跟不上模型规模的增长速度。拿一张现代 80 GB HBM 的 GPU 来说,要装下 2.8 TB 的训练状态,最少需要

一台常见的 8 卡节点只有 640 GB,连零头都不够。这不是「优化一下就行」的边际问题,而是一道墙——内存墙(memory wall)。它直接宣判:单节点训练大模型不可行,分布式是唯一的路。术语「内存墙」最早来自 Wulf & McKee 1995 年的经典短文,第 2 章在访存层级的语境里引用过;到了这里,它又多了一重含义:显存容量本身,就是模型规模的墙。

9.2.4 通信成为新瓶颈

算力可以靠堆卡堆出来,内存可以靠分片分出去——但跨节点训练一旦开始,系统就变成通信受限(communication-bound)。几个数字就能说明问题:

  • 每次迭代要同步的梯度:175B 模型就是 350 GB(FP16);
  • 节点间带宽(InfiniBand)约 50 GB/s,节点内带宽(NVLink)约 1.8 TB/s,差一个数量级还多;
  • 计算性能的增长速度,远快于网络带宽的增长速度。

把两个数相除,画面立刻具体起来:350 GB 的梯度,走 InfiniBand 光传输就要约 7 秒;走 NVLink 只要约 0.2 秒。当梯度同步以百 GB 计、而网络带宽只有计算带宽的零头时,通信优化就不再是锦上添花,而是命门。 这也是第 12 章要花大力气讲 NVLink、RDMA 和网络拓扑的原因。


9.3 数据并行与集合通信

算力账、内存账都算完了,接下来看技术。这一节先花一点篇幅复习神经网络怎么学,然后讲数据并行、参数服务器,再系统介绍集合通信。

9.3.1 回顾:前向与反向传播

在进入分布式架构之前,先花一分钟复习神经网络是怎么学习的。

前向传播(forward propagation):数据从输入层出发,穿过一个或多个隐藏层到达输出层。每层用学到的权重变换输入,网络最后给出一个预测;训练时拿预测和正确答案比较,用损失函数(loss)算出差距。目标很朴素:调整参数,让损失变小。

反向传播(backward propagation):用链式法则(chain rule)计算损失对每个参数的梯度。如果一个权重通过多条路径影响损失,梯度必须把所有路径的贡献都加起来。举例:假设权重 影响两个后续激活,它对损失的总体影响,两条路都得算到。反向传播自动完成这件事——把误差信号沿计算图从后往前传播,逐层合并贡献。

分布式训练不改变数学目标。 每个 worker 照样做前向和反向传播,区别只在于:工作被切分到了多个设备上,切分之后产生的梯度或激活必须跨设备通信。通信,是本章从 9.3 到 9.4 贯穿始终的主线。

9.3.2 数据并行的基本思想

最简单、也最常见的分布式训练策略,是数据并行(data parallelism)。步骤只有六步:

  1. 每张 GPU worker 保存一份完整的模型副本;
  2. 训练批次被切成更小的微批次(micro-batch);
  3. 每张 GPU 处理不同的微批次;
  4. 每张 GPU 用自己那份数据算出局部梯度(local gradients);
  5. GPU 之间同步(平均)梯度;
  6. 每张 GPU 用同一份平均后的梯度更新参数。

直觉。 这像一百个同学分头做同一套卷子:每人手里都有一份标准答案册(模型副本),各自只做分到的题目(微批次),做完对答案(同步梯度),然后把正确解法抄回自己的答案册(同一份更新)。答案册始终一致,是因为每一步都对过答案。

数据并行吸引人,是因为它概念简单、计算利用率高——有 100 张卡,就能把批吞吐放大到接近 100 倍。但它有两个硬伤:

  • 内存不高效。 每张 GPU 都持有完整模型副本和完整优化器状态。模型小还好说;模型上千亿参数时,不配合分片手段就根本装不下。
  • 同步是第二瓶颈。 每次反向传播后,所有 GPU 必须对梯度求平均。哪张卡慢了,其他卡就得等——这就是落后者问题(straggler problem)。硬件抖动、网络波动、负载不均,都会制造落后者。

停下来想想

数据并行的同步开销不随步数增长,而随参数量和带宽变化:模型翻倍,每步要同步的字节数就翻倍。这正是后文 ZeRO 分片梯度、Ring All-Reduce 摊薄流量的动机。

9.3.3 参数服务器架构

分布式训练的早期方案是参数服务器(parameter server)架构。机器分成两种角色:

  • Worker:负责前向和反向计算;
  • 参数服务器:负责存放模型权重,以及优化器状态(如果有)。

一轮典型的训练步骤:

  1. Worker 读取当前模型权重;
  2. Worker 在本地数据上做前向传播;
  3. Worker 用反向传播算出梯度;
  4. Worker 把梯度推(push)给参数服务器;
  5. 参数服务器聚合梯度、更新权重;
  6. Worker 拉(pull)回更新后的权重,进入下一轮。

SGD 式的参数更新写成:

其中 是学习率, 是聚合后的梯度。

参数服务器模型简单,早期分布式机器学习系统里很流行——经典出处是 Li 等 2014 年的 OSDI 论文 Scaling Distributed Machine Learning with the Parameter Server。但它有严重的通信瓶颈:许多 worker 向少数服务器发梯度,形成多对一流量;服务器再把更新后的权重回发给所有 worker,形成一对多流量。两类流量都会在交换机上打架(后文叫 incast 拥塞),带宽利用率被拉低。正因为这些瓶颈,现代大规模 GPU 训练大多转向去中心化的集合通信,而不是中心化的参数服务器。

分布式训练还有同步与异步之分,这个选择同样影响通信模式。

同步训练(synchronous training):所有 worker 完成本地计算后,才做全局更新。好处是每张卡用的都是同一份模型状态;坏处是快的卡必须等慢的卡——一张卡慢一点,其他几十张卡的算力就空转。

异步训练(asynchronous training):worker 不等彼此,算完梯度就提交更新、立刻继续下一轮。硬件利用率高了,但引入陈旧性(staleness):慢 worker 用旧版本模型算梯度,等它的梯度真正生效时,模型早已变了。陈旧梯度会伤害收敛,对大型神经网络尤其明显。

这就是为什么大语言模型训练几乎一律选同步训练 + 高度优化的通信。 后面 9.3.4 到 9.3.7 的所有通信技巧,都是为了让「同步」这个决定不付出惨痛代价。

9.3.4 集合通信基础

《Encyclopedia of Parallel Computing》给集合通信(collective communication)下的定义是:一组处理单元(或节点)之间、在全部或部分成员间传输数据的通信,传输过程可以伴随求和、求平均、归约(reduction)等运算。 在分布式训练里,集合通信不可或缺——许多 GPU 要合作训练一个模型,就必须高效地交换梯度、参数、激活或中间结果。

为什么用集合通信?三个好处:

  • 简化的编程接口。 开发者不用手写同步和搬数逻辑,直接调用 BroadcastReduceAll-ReduceAll-GatherReduce-Scatter 这些标准原语即可;
  • 可扩展性。 集群从几百张卡涨到几千张时,手动管理通信会疯掉。集合通信库(NCCL、MPI 等)被设计成在大规模下高效搬数,还自带可靠性、监控、容错;
  • 计算与通信分离。 做模型的研究人员只管算法,系统工程师去优化网络性能、拓扑与硬件利用率。

在训练里它长什么样? 如果你用过 PyTorch 的 DistributedDataParallel(DDP),你已经用过集合通信了。DDP 不依赖传统参数服务器,而是通过集合操作同步梯度,后端通常是 NVIDIA 的 NCCL 库。这套思想和 MPI(Message Passing Interface,消息传递接口)标准一脉相承:MPI 提供 Send/Receive 这类点对点操作,但分布式训练更需要组通信——比如每张 GPU 算完局部梯度后,要在所有 GPU 之间求平均。这件事通常用 All-Reduce 完成。

All-Reduce 还兼任同步屏障。 All-Reduce 把各节点的值组合起来,再把最终结果分发给每个节点。数据并行里,它保证每张 GPU 在优化器更新前拿到同一份平均梯度。因为所有 GPU 都必须参与,All-Reduce 客观上也是同步屏障(synchronization barrier):节点 0 在 完成反向传播,节点 2 到 才完成,节点 0 就得干等——这段等待是纯浪费。硬件抖动、网络波动、负载不均都会造成这种延迟,一个慢 worker 就成了落后者(straggler),拉低整个集群的效率。所以批大小要平衡、负载要均匀,这不是洁癖,是性能。

按数据流分类:

  • 一对多Broadcast
  • 多对一GatherReduce
  • 多对多All-GatherReduce-ScatterAll-Reduce

多对一操作容易造成 incast 拥塞——太多流量同时涌向一个节点或一个交换机端口,这和参数服务器的瓶颈一模一样。现代 GPU 集群因此针对多对多操作做了大量优化:NCCL 会把张量切成小块,用 ring、tree 或拓扑感知算法在 NVLink、InfiniBand 等高速链路上搬运。这些操作里,All-Reduce 对数据并行最重要——它驱动梯度同步,它的效率很大程度上决定一个 AI 集群被用出了几成。

9.3.5 集合通信操作详解

这一节逐个过一遍最常见的集合操作:BroadcastGatherScatterReduceAll-ReduceAll-GatherReduce-ScatterAll-to-All。记一个口诀:数据从一个点出去叫 Broadcast;收拢到一个点叫 Gather/Reduce;人人都有份的叫 All-*。

Broadcast。 把数据从选中的一张 GPU(叫 root)发给组内所有其他 GPU。假设有四张 GPU,rank 0 到 rank 3,初始只有 rank 2 持有数据块 。Broadcast 完成后,四张卡都持有完全相同的四份数据。关键想法:把一张 GPU 的数据复制给所有 GPU。

Gather。 和 Broadcast 方向相反——把分散的数据收拢到一张 GPU 上。假设四张卡各持一块:GPU0 有 ,GPU1 有 ,GPU2 有 ,GPU3 有 。选 GPU0 为 root,GPU1/2/3 各自把自己的分片发给 GPU0,完成后 GPU0 持有完整的 ,其他卡没有完整结果。关键想法:把分布式数据分片收集到一张选定 GPU 上。 当数据算好、存好之后只需要一个地方拿全量结果时,Gather 就有用。

Scatter。 Gather 的逆操作——从一张 GPU 把分片分发给多张 GPU。假设 GPU0 初始持有被切成四份的完整数据集 ,其他卡为空。Scatter 时 GPU0 把每份发给一张卡:GPU1 拿 、GPU2 拿 、GPU3 拿 、GPU0 自己留下 。和 Broadcast 不同,不是人人拿到全部,而是人人拿到不同的一份。关键想法:把一张 GPU 的数据切开,分给不同 GPU。 大张量要切到多卡并行处理时用得上。

Reduce。 不只是搬数据,还在搬运过程中做数学运算。假设四张卡各持对应的数据块:rank 0 有 ,rank 1 有 ……这些块可以理解为各卡独立算出的部分梯度。Reduce 对对应块做聚合操作——Sum、Min、Max、Average 都行。以求和为例,所有 块相加得到 同理。若 rank 2 是 root,最终归约结果只存在 rank 2 上。关键想法:把各 GPU 的对应数据组合起来,结果存在一张选定 GPU 上。

All-Reduce。 Reduce 的加强版——最终结果对每张 GPU 都可用。初始状态和 Reduce 一样,每张卡持有自己的本地分片;聚合计算照做,只是把完整结果回发给所有卡。关键想法:所有 GPU 的数据合起来,最终结果人人都有。 这是数据并行的命脉:每张卡从自己的微批次算出梯度,All-Reduce 把它们平均,让所有模型副本用同一份全局梯度更新,从而保持同步。

All-Gather。 Gather 的加强版。假设 rank 0 有 、rank 1 有 、rank 2 有 、rank 3 有 ,每卡只有全局数据的一块。All-Gather 后,每张卡都持有完整的 关键想法:收集分布的分片,把完整结果放到每张 GPU 上。 当每卡只有张量的一部分、但下一步计算需要完整张量时用得上。

Reduce-Scatter。 Reduce 后接 Scatter 的组合。先按对应块归约(得到 等四项),然后不把完整结果放一张卡、也不复制给所有卡,而是把归约后的分片散布出去:rank 0 拿归约后的 块,rank 1 拿 块,rank 2 拿 块,rank 3 拿 块。关键想法:聚合数据,再把一个归约分片分给每张 GPU。 它对内存高效的分布式训练特别有用——没有任何一张 GPU 需要存完整归约张量。Ring All-Reduce 和 ZeRO 都建立在它之上。

All-to-All。 这里讨论过的最一般、也最复杂的模式:每张 GPU 给其他每张 GPU 都发数据,是一次协调的全面交换。假设 GPU0 有 、GPU1 有 ……目标是把数据按类别重新组织:概念上,GPU 把自己的第 块发给 GPU ——这就像在网络上做一次矩阵转置。完成后 GPU0 持有所有 块(),GPU1 持有所有 块……关键想法:彻底重新洗牌,每张 GPU 既发给所有人、也从所有人接收。 在需要把数据完全重排的场景里它是关键,尤其是混合专家(mixture-of-experts,MoE)模型——token 要被路由到不同的专家网络上。

9.3.6 Ring All-Reduce

分布式训练里最重要的集合操作是 All-Reduce,实现它的算法有好几种:Ring All-ReduceTree All-Reduce拓扑感知 All-Reduce。其中 Ring 最常见、也最容易理解。

把 GPU 排成一个环。 假设有四张 GPU:A、B、C、D,数据沿一个方向在环上流动:

这种结构避开了集中式参数服务器的瓶颈,并且用上了所有链路的带宽。每张卡只和邻居通信,网络流量被均匀摊开,没有单点热点。反向传播结束后,每张卡用自己那份微批次算出了局部梯度;为了让模型同步,这些局部梯度要跨卡求和(或平均)。为了高效,每张卡把自己的梯度张量切成 等份——四张卡就切四块:

  • GPU 0 持有
  • GPU 1 持有
  • GPU 2 持有
  • GPU 3 持有

目标:每张卡最终拿到完整归约梯度——对每个分片 ,即 。Ring All-Reduce 分两阶段完成:

阶段 1:Reduce-Scatter。 真正的归约(求和)发生在这里。每一步,每张卡向邻居发一个块、从上一个邻居收一个块,收到后把外来块加到自己对应的本地块上。

第一步:GPU A 发 给 B,B 发 给 C,C 发 给 D,D 发 给 A。各卡收下后立刻累加:GPU B 得到 ,GPU C 得到 ,GPU D 得到 ,GPU A 得到 。所有卡同时收发加——通信沿着环摊开,没有哪张卡或哪条链路成为中心瓶颈。

下一步,把部分和继续沿环传:GPU B 把 发给 C,C 加上自己的 ,得到 。如此往复,一共 步(四张卡就是三步)。结束时,每张卡恰好持有一个完整归约好的分块:某卡持有 ,另一卡持有 ……完整归约结果已经存在,只是分布在各卡手里——每人只有最终答案的一块。

阶段 2:All-Gather。 不再需要加法,计算已完成,只剩把归约好的块分享给每张卡。每张卡把自己的完成块沿环传一圈,再经过 步,所有人都持有全部完成块。此时每张卡拿到同一份完全同步的梯度张量,可以应用同一份优化器更新,进入下一轮训练。

通信成本。 为 GPU 数、 为梯度张量总大小。每个块大小 。Reduce-Scatter 需要 步,All-Gather 需要 步,合计 步。每步每卡发一个 大小的块,所以每张卡的总通信量:

很大时,,每卡通信量逼近 这是 Ring All-Reduce 的核心优势:无论加多少张卡,每张卡搬的数据量基本不变。 这个结论出自 Patarasuk & Yuan 2009 年的经典论文 Optimal All-Reduce Algorithms for Mesh-Connected Computer Architectures,也是 ring 算法后来统治大规模训练的原因。

为什么它有效? 因为它避开了参数服务器的流量模式。参数服务器是很多人涌向一个中心,产生 incast 瓶颈;Ring 则把通信均匀摊开,每卡只和邻居说话,每一步所有卡都活跃、所有链路并行使用,网络利用率平衡,没有单点过载。

一句话记住它

All-Reduce = Reduce-Scatter + All-Gather。 第一阶段做归约,第二阶段分发结果。两个阶段加起来,让成千上万张卡在大规模训练里高效同步梯度。

9.3.7 替代 All-Reduce 算法

Tree All-Reduce。 Ring 之外的另一条路:GPU 排成锦标赛对阵图而不是一圈。向上阶段:第一轮 GPU 0 和 GPU 1、GPU 2 和 GPU 3 两两配对,各对相加数据、留下「赢家」;赢家再配对(如 GPU 1 发给 GPU 3),最后树顶的 GPU 3 持有完整归约结果。向下阶段:把最终答案沿树传回去——GPU 3 发给 GPU 1,再往下传给 GPU 0 和 GPU 2。

性能权衡:二叉树下,轮数约 ,上下两阶段合计约 。代价是每轮的数据量——朴素的 Tree 每轮常要传整个载荷 (或部分链路空闲),总流量约 越大,每卡流量越大,靠近树根的卡忙、叶子卡闲。这种缺乏重叠、流量随规模增长的特质,正是大张量下 Ring 在带宽效率上胜过朴素 Tree 的原因。

Butterfly All-Reduce。 得名于图中交叉的通信模式。相比 Tree,它保证每轮所有 GPU 都参与:第一步 GPU 0 与 GPU 1 配对、GPU 2 与 GPU 3 配对(交换 + 相加);第二步伙伴换成更远的距离(GPU 0 与 GPU 2、GPU 1 与 GPU 3)。经过约 步,完整归约结果处处可得。

性能权衡:轮数仍约 ;但因为省掉了单独的向下广播阶段,每卡总流量相对朴素 Tree 改善,总体通信约 。不过流量仍然随 增长——所以 GPU 数量大、带宽主导时,Ring 依然受欢迎。

Rabenseifner 算法:「两个世界最好的一面」。 它把 Butterfly 的步数效率和 Ring 的带宽效率结合了起来。

  • 聚合阶段(上排):像 Butterfly 一样两两配对,但每次发送的不是完整的 ,而是越来越小的半个:先约 ,再 ,再 ……聚合结束时计算已完成,但结果散布在各 GPU 上,没有全量复制。
  • 收集阶段(下排):同样的过程反向执行,把最终结果重新分配,让每张卡拿到它需要的东西。
  • 为什么高效:轮数约 (聚合 + 收集);由于载荷每轮减半,总流量构成几何级数,带宽接近平坦的最优水平关键结论:Rabenseifner 达到与 Ring 相当的带宽最优,而通信步数比 Ring 少得多。

- 模型挑选算法。 光看「总字节数」不够,还得数消息条数。经典 - 模型把一次通信的时间写成:

  • 启动延迟(startup latency),每条消息/每轮的固定开销;
  • 单位数据耗时,即带宽的倒数;
  • :消息大小。

Ring vs Rabenseifner: Ring 用约 轮,把 付了很多次——消息巨大时,Ring 的带宽效率占优;但 GPU 很多或块很小时,启动开销就疼了。Rabenseifner 用约 轮, 付得少得多,而流量仍大致维持 Ring 级的带宽效率。(Rabenseifner 算法出自 Rabenseifner 2004 年 ICCS 的论文 Optimization of Collective Reduction Operations。)

实操建议: 小集群、超大张量、要简单有效——用 Ring;大规模系统、或消息块很小、想少几次通信轮——用 Rabenseifner。

拓扑感知 All-Reduce。 到目前为止,我们都假设所有 GPU 之间连接一样快。真实的训练集群不是这样,all-reduce 的复杂度很大程度来自数据中心网络拓扑。

硬件层级: 服务器内部,GPU 用极快的 NVLink 互联;服务器之间,通信走慢得多的网络(InfiniBand/Ethernet 上的 RDMA)。如果在 1000+ 张卡上拉一个超大的 Ring,整个系统会被最慢的机间链路拖死。

解法是层次化 All-Reduce(hierarchical All-Reduce),拆成三段:

  1. 节点内归约:每台服务器内部的 GPU 用快速 NVLink 归约,每台留一张「主卡」持有服务器级部分结果;
  2. 节点间 all-reduce:只有各主卡通过 RDMA 跨服务器通信——穿越慢速网络的流量被压到最少;
  3. 节点内广播:主卡把最终结果广播回服务器内的其他 GPU。

这就是分布式训练能扩展到数千块芯片、而网络不成瓶颈的实际原因。


9.4 内存优化

数据并行把算力摊出去了,但每张卡都揣着一整头大象。这一节解决「大象」怎么处理:先是切分(ZeRO),再是往便宜的地方放(Offload/Infinity),最后是激活怎么办(重计算)。

9.4.1 内存优化的动机

一个经典段子精准概括了处境:

怎么把大象装进冰箱?——把大象切成块,把块分到不同的冰箱里。

  • 冰箱:有限的 GPU 显存(常见约 80 GB HBM);
  • 大象:训练一个 LLM 所需的全部内存足迹。

模型涨到十亿、万亿参数后,这头「大象」任何一张 GPU 都装不下,训练立刻以 Out-of-Memory(OOM)报错收场。核心挑战变成:怎么重组内存使用,让模型状态能装进整个集群?

标准数据并行(DP)里,每张 GPU 保存一份完全相同的训练状态副本:参数梯度优化器状态(优化器用的大块辅助缓冲,比如 Adam 的动量与方差)。拿 DP 去训现代 LLM,等于想把整头大象塞进每一台冰箱。小模型可以,现代 LLM 不行——复制会把内存消耗放大到没有任何一张 GPU 装得下自己的那份。

于是需要新思路:与其在每张 GPU 上复制全部模型状态,不如把它切分(partition / shard)到多张 GPU 上。训练内存被三个对象主导:

  1. 参数
  2. 梯度
  3. 优化器状态

为模型总参数量, 为 GPU/设备数。以 Adam 为例:每个参数要额外维护一份 FP32 主权重和两个矩估计,共 12 字节;而参数本体(FP16)只有 2 字节——优化器状态比参数本体大好几倍,课件里常用的粗口径说法是「12–14 倍」。不管怎么数,结论都一样:内存的大头在优化器状态,削减它的冗余收益最大。

9.4.2 ZeRO:零冗余优化器

ZeRO(Zero Redundancy Optimizer) 的目标,就是把 DP 里的冗余存储清掉——通过把模型状态分片到各 GPU 上。它给出一个渐进的阶段谱系:

  • ZeRO-1:分片优化器状态
  • ZeRO-2:分片优化器状态 + 梯度
  • ZeRO-3:分片优化器状态 + 梯度 + 参数(完全分片)

各阶段的每 GPU 内存。 按 ZeRO 论文(Rajbhandari 等,SC 2020)的标准账本,每参数共 16 字节:FP16 参数 2B + FP16 梯度 2B + 优化器状态 12B。

ZeRO-1(优化器状态分片):

参数和梯度仍全量复制,只有优化器状态被切分。

ZeRO-2(优化器状态 + 梯度分片):

参数仍复制,梯度和优化器状态都切分——同步/更新期间每卡持有的内存进一步下降。(14 的来历: 的梯度 + 的优化器状态。)

ZeRO-3(完全分片):

参数、梯度、优化器状态全部切分。 越大,每卡足迹缩得越狠——远超单卡显存的模型也能训了。

根本权衡。 ZeRO-1 和 ZeRO-2 主要是省内存,整体计算结构和 DP 基本一致。ZeRO-3 省得最狠,但它显著改变了计算方式:没有任何一张 GPU 永久持有完整参数,每步计算前,GPU 必须先临时组装出自己需要的参数分片——多出来的通信,就是拿内存换的代价。

ZeRO-1 / ZeRO-2:训练步骤里到底改了什么?

一个关键设计选择:模型参数仍然在每张 GPU 上复制。 这保持了前向/反向计算的简单——每张卡本地就有完整参数,标准数学照跑。变的主要是梯度同步和更新逻辑:不再做一次整体 All-Reduce,而是用数学上等价的分解:

  • ReduceScatter:只留自己需要的。 每张卡贡献自己算出的梯度,参与全局归约,但只收到结果梯度的一个分片——没有任何一张卡需要存下全部参数的完整梯度张量,这对内存友好。
  • 更新:用梯度分片更新对应的参数/优化器分片。 拿到梯度分片后,每卡更新自己负责的那部分优化器状态和参数。
  • AllGather:下轮需要时再凑齐。 ZeRO-1/2 下,为了下一轮复制式的计算,再做一次 All-Gather,让所有卡重新持有一致的参数副本。

ZeRO-1 vs ZeRO-2 小结: ZeRO-1 切分优化器状态——梯度仍需同步,但优化器内存被分摊;ZeRO-2 更进一步,在同步过程中切分梯度——每卡只收到自己参数/优化器分片更新所需的梯度分片,内存压力更小。两者的核心收益,都是优化器相关状态与同步缓冲区的内存削减。

ZeRO-3:完全分片,为什么前向/反向必须变。

ZeRO-3 才是真正颠覆性的那一步。参数不再复制,被永久分片到各 GPU 上。于是朴素的 DP 故事讲不下去了:一张 GPU 如果没持有某层的参数,就没法算那一层。 所以 GPU 必须按需(just-in-time)动态取回参数分片。

  • 前向:逐层组装参数。 对每一层(或每个参数块),先确定哪个 GPU 持有所需分片,做一次类 All-Gather 操作,让每张卡临时拿到算这一层所需的参数;算完立刻丢弃临时缓冲区,释放内存;再处理下一层。这就是 ZeRO-3 分片的运作含义:参数存在于集群各处,只在计算时临时拼装。
  • 反向:同样的问题,重复的参数传输。 反向传播有同样的约束——某层梯度的计算需要对应的参数值(或更早保存的激活)。参数被分片,反向也得动态取参数分片、算局部梯度、释放临时缓冲区。
  • ReduceScatter:梯度聚合保持分片。 局部梯度算完后,ZeRO-3 用 ReduceScatter 做全局聚合,但结果仍以梯度分片形式分布——没有哪张卡成为梯度内存热点。
  • 更新:只更新自己那部分,没有最终 AllGather。 因为参数和优化器状态都被分片,更新步骤天然分布式:每卡只更新自己拥有的参数/优化器分片,之后不需要重建完整参数集。这个「更新后不做全量参数 AllGather」的缺席,正是 ZeRO-3 扩展性最好的关键原因。

实现上的实际考量。 主要的实现是微软的 DeepSpeed 库,它通常在两处细化上面的理想化通信模式:

  1. 优先 AllGather,而不是朴素的 broadcast。 broadcast 让一个设备扛所有重通信,容易成瓶颈;AllGather 把通信负载更均匀地摊到各设备。
  2. 层内分区(intra-layer partitioning)。 不是把整层分给不同 GPU(那会造成空转),而是把层内的大张量运算切开,让 GPU 在下一轮通信进行时保持工作,同时所有 GPU 的前向/反向保持同步。

这些工程决策在保住 ZeRO-3 内存效率的同时,减少了实际停顿。

9.4.3 进一步优化:ZeRO-Offload / ZeRO-Infinity / ZeRO++

有时连 ZeRO-3 都装不下训练状态——尤其是 GPU 数量少、模型又大的时候。

ZeRO-Offload 的答案:把最占内存的部分(通常是优化器状态)搬到 CPU 内存。它利用了硬件的失衡:GPU 有内存(HBM)但容量有限;CPU 有内存(系统 RAM)但访问慢。分工变成:

  • GPU 专注前向/反向计算;
  • 反向算出梯度后,把梯度传给 CPU(通常走 PCIe);
  • CPU 用系统 RAM 里的优化器状态做更新;
  • 更新后的参数传回 GPU,供下一轮前向使用。

这防住了 OOM,但代价明显:CPU↔GPU 通信比 GPU↔GPU 慢得多。训练记录会显示 GPU 利用率下降——GPU 在等 CPU 侧的更新和参数回传时空转。Offload 是一个容量工具:它抬高你能训的模型上限,却降低训练速度。它是「容量 vs 速度」的权衡工具。

ZeRO-Infinity。 Offload 的前提是 CPU 内存够用。模型大到连 CPU RAM 都不够呢?ZeRO-Infinity 把内存层级再向下延伸一层,用 NVMe SSD 兜底:

从存储容量的角度看,这几乎是「无限」容量——GPU 内存和 CPU 内存都不够的规模也能训。但 NVMe 的延迟比 HBM/RAM 高得多、带宽低得多,所以 ZeRO-Infinity 依赖精细的数据编排,让 GPU 始终有活干、把空闲压到最小。它的核心讯息始终一致:

记住这句话

ZeRO 系系统,是用通信与数据移动的复杂度,换取内存容量。

ZeRO++:低带宽场景。 跨广域网、或互连带宽有限的分布式集群里,传统的集合通信(AllGather、ReduceScatter)变得昂贵。ZeRO++ 针对低带宽场景提出三项优化:

  • 量化权重通信:All-Gather 前把 FP16 权重转成 INT8,数据量减半;收集完再反量化回 FP16。对训练来说精度损失可接受。
  • 量化梯度通信:对 Reduce-Scatter 的梯度做类似量化,并结合逐块(block-wise)量化策略压低精度损失。
  • 层次化分区:把参数分片层级结构化,最小化节点间通信——节点内走快速互连,沿层级逐级通信,减少跨节点 all-gather 的流量。

结果是:通信量下降,而训练收敛保持。

9.4.4 激活重计算

参数、梯度、优化器状态只是「静态内容」。HBM 里还有一个同样庞大、但动态的消费者:激活值(activations)——前向传播时每一层产生的中间输出。在 Transformer 里,激活包括 Query/Key/Value 投影、注意力分数、各种前馈层的输出等。它们「动态」,是因为内存足迹不只由模型架构决定,还强烈依赖批大小序列长度这些运行时因素。

为什么必须存激活? 因为反向传播需要它们。由链式法则,损失对某个权重的梯度

既需要从后一层回传的梯度 ,也需要前向传播时进入该层的确切输入 。所以前向算出的激活必须存在 HBM 里,直到反向传播算到那一层——丢掉它们,梯度计算根本无从谈起。

量化激活内存。 激活的内存足迹大到吓人——即使其他模型状态被激进分片,激活也常常超出单卡容量。看一个典型 Transformer 层(FP16)。沿用标准记号:

  • 批大小(batch size)
  • 序列长度(sequence length)
  • 隐藏维度(hidden size)
  • 注意力头数(attention heads)
  • 层数(number of layers)

逐项追踪单层内的内存累积:

  • 自注意力块的输入 :形状 ,占 字节;
  • 注意力块内部:Q/K/V 投影与注意力分数计算产生大量中间激活。注意力分数相关的项记作 ,随序列长度的平方增长——序列一长它就是主导项,后面靠 FlashAttention 这类注意力优化缓解);其余线性中间结果约
  • MLP 块:Linear(h→4h) 输出加 ,GeLU 输出加 Linear(4h→h) 输出加

把这些项归拢,单个 Transformer 层的激活内存约为

注意这里所有字母都是逐项相乘:(即 ,写法不同而已)。其中 项随 增长,长序列时主导; 主要是各线性层的中间输出。

举例用的规模:一批共 万 token(这正是 GPT-3 175B 的训练批大小——注意 是批大小,320 万指的是整批的 token 总数 ,不是 本身),序列长 ,则批大小 个序列;再取 层。在这个量级下,把所有层的激活加在一起,轻松超过数百 GB、逼近 TB。即使 ZeRO-3 把参数、梯度、优化器状态都分片了,光激活就可能触发 OOM。

激活重计算(Activation Recomputation)/ 梯度检查点(Gradient Checkpointing)。 存不下所有激活时,就用计算换内存。最激进的形式是完全激活重计算(full activation recomputation):前向时只保存整个网络的初始输入,其余中间激活几乎全部丢弃;反向传播需要某个没存下的激活时,就从最近保存点(或原始输入)现场重算。内存占用降到最低,代价是训练时间增加约 30–40%——相当于把前向传播又跑了一遍。对以周、月计的 LLM 训练,这笔开销相当可观。

策略性重计算(strategic / selective checkpointing) 在保住内存收益的同时,尽量少加训练时间。图上两种颜色的含义:

  • 浅绿块:激活在反向传播时重算——前向不保存其中间输出;
  • 紫色块:激活在前向时完整保存——中间输出存在;
  • 红色三角:一个保存下来的激活(检查点)——策略性存储点,用来避免重算它之前的一切。

完全检查点(full checkpointing):把每个大矩形(含 LayerNorm、MHA、MLP 的完整 Transformer 块)当作一个整体,只在每个块的入口保存激活(红色三角),块内所有中间激活(浅绿)都不存。反向传播需要某块内部的激活时,从最近的检查点重算整个块的前向。内存省得最多,代价是整块整块地重算。

选择性检查点(selective checkpointing):粒度更细。对块内某些子模块(如 MHA 这类激活特别大、或重算很贵的部分)选择完整保存(紫色);对其他子模块(如某些 LayerNorm、MLP,浅绿)允许反向时重算。这个粒度通常通过框架配置暴露出来,比如 DeepSpeed 的 activations_checkpoint_granularity = selective | full总的原则一句话:重算那些内存占用大、但计算量轻的层。


9.5 本章小结

把这一章压缩成一句话:大模型训练的根本约束不在计算,而在内存和通信。 这不是口号,而是前面每笔账的结论——算力可以靠堆卡堆出来,但每张卡都必须装下一份模型状态(内存墙),每步训练都得同步几十到几百 GB 的梯度(通信墙),而带宽的增长永远追不上模型规模的膨胀。从参数服务器到 Ring All-Reduce,从 ZeRO 的三级分片到激活重计算,所有技术其实都在这条边界上做同一类事:要么让数据少搬一点(分片、量化、层次化),要么让搬数据的同时算点东西(Ring 的流水、重计算的错峰),要么把数据放到更便宜的地方去(Offload、Infinity)。 这三者的平衡点,就是第 10 章模型/专家并行、第 12 章基础设施继续展开的战场。

延伸阅读