跳转至

四:ZeRO 优化与显存卸载

为什么需要 ZeRO?它针对的是数据并行中的什么冗余?

在标准数据并行训练中,每张GPU都拥有完整的模型副本。这个“完整”意味着每张卡都存储着完全相同的数据——模型参数、梯度、优化器状态(Adam的m和v)。当我们用8张卡训练同一个7B模型时,整个集群存储的模型状态总量是单卡需求的8倍,而其中7份是冗余的。这种冗余在小型模型上不是问题,但当模型参数量达到数十亿、数百亿时,单张GPU的80GB显存很快就不够用了。

具体来看这种冗余造成的浪费:7B模型在FP16混合精度下,参数占14GB,梯度占14GB,优化器状态(FP32的m和v)占56GB,FP32主权重占28GB,模型状态合计112GB。8张卡集群总存储达896GB,但有效信息只有112GB,冗余率高达87.5%。更致命的是,激活值还需要额外显存,导致单卡根本无法容纳完整状态。

ZeRO(Zero Redundancy Optimizer)正是针对这一痛点设计的。它的核心理念是消除数据并行中的内存冗余——不再让每张卡都存储完整的模型状态,而是将这些状态分布到所有卡上,每张卡只持有自己负责的那一部分。当需要访问不在本地的数据时,通过高速的集合通信(如AllGather、ReduceScatter)从其他卡获取。

这个思想的精妙之处在于:它既不改变数据并行的计算模式(每张卡仍然处理不同的微批次数据),也不改变模型结构,只是在底层改变了数据的存储和流动方式。通过消除冗余,ZeRO使得在相同硬件上训练大10倍甚至100倍的模型成为可能。DeepSpeed的实践表明,ZeRO-3可以在8张A100上训练13B甚至更大规模的模型,而标准数据并行连7B都无法承载。

ZeRO 的三个阶段分别分片了什么?用表格总结。

ZeRO的三个优化阶段(Stage 1、Stage 2、Stage 3)分别针对模型状态的不同部分进行分片,每一阶段都是在前一阶段基础上的进一步压缩。

查看内嵌表格

内存占用分解(以7B模型,FP16混合精度,8卡训练为例):

查看内嵌表格

从表中可以清晰看到,随着ZeRO阶段提升,单卡显存占用递减。ZeRO-1通过分片优化器状态,将最大的一笔开销(84GB)压缩到原来的1/8;ZeRO-2进一步分片梯度,减少了一半的剩余显存;ZeRO-3彻底分片参数,使得单卡模型状态总占用仅为14GB,比完整存储节省了87.5%。但通信量也从ZeRO-1的相对轻量,递增到ZeRO-3的显著增加——这正是用通信换显存的经典权衡。

ZeRO-1 是如何分片优化器状态的?每个 GPU 存 1/N 的优化器状态,更新时怎么办?

ZeRO-1将优化器状态(Adam的m和v、FP32主权重)按参数维度均匀切分到所有数据并行的GPU上。假设有8张卡,每张卡只存储1/8的优化器状态,也只负责更新对应分片的参数。但前向和反向传播仍需要完整的模型参数,这就需要在计算和更新之间建立一套精确的通信机制。

前向和反向传播阶段:与标准数据并行完全相同。每张卡都持有完整的FP16模型参数(ZeRO-1不改变参数的存储方式),处理自己的微批次数据,独立执行前向和反向传播,计算完整的梯度。这个阶段ZeRO-1不做任何干预,训练吞吐与标准DP完全一致。

优化器更新阶段:这是ZeRO-1发挥作用的关键时刻。每张卡拥有完整的梯度,但只拥有1/N的优化器状态。为了正确更新,需要经过三个步骤:

第一步:ReduceScatter梯度分片。各卡将本地计算的完整梯度进行ReduceScatter操作。这个操作先按参数分片维度将所有卡的梯度求和(Reduce),然后将归约后的结果按分片规则“散落”(Scatter)到各卡——每张卡只得到自己负责那部分参数的全局梯度。例如,卡0只保留参数分区0的梯度,卡1只保留分区1的梯度。通信数据量等于梯度总大小。

第二步:本地更新。每张卡用自己分到的梯度,更新本地持有的1/N优化器状态(m、v),并进一步更新对应的FP32主权重。这一步完全在本地进行,无需通信。

第三步:AllGather参数同步。更新完成后,每张卡拥有更新后的参数分片。为了让所有卡重新获得完整的模型参数(用于下一轮前向传播),需要执行AllGather——每张卡将自己更新后的参数分片广播给所有其他卡,同时接收其他卡的分片。通信数据量等于参数总大小。

简单来说,ZeRO-1用一次ReduceScatter和一次AllGather,换取了优化器状态显存的8倍压缩。这两次通信的总数据量约为28GB(对于7B模型),而标准DP仅需一次梯度AllReduce(14GB),通信量翻了一倍。但因为优化器状态通常是最大的显存消耗源(占112GB中的84GB),这个代价在大模型训练中是完全值得的。

在 ZeRO-1 下,进行一次优化器更新需要哪些通信操作?

一次完整的ZeRO-1参数更新涉及两个集合通信操作,它们前后衔接,缺一不可:

操作一:ReduceScatter(梯度归约与分片)

这是更新流程的起点。此时每张卡都刚刚完成反向传播,持有基于本地微批次数据计算出的完整梯度。但每张卡的本地梯度只是全局梯度的一个部分贡献(因为它只处理了部分数据),需要将所有卡的梯度求和才能得到真正的全局梯度。

ReduceScatter完美地完成了“求和+按需分发”两个任务。它首先将所有卡的梯度在对应参数位置上求和(Reduce),然后将求和结果按照优化器状态的分片规则切分并分发(Scatter)——每张卡只收到自己负责更新的那部分参数的全局梯度。例如8卡训练7B模型,每张卡最终只保留约1/8的梯度。

这次通信的数据量等于梯度总大小:7B参数 × 2字节(FP16)= 14GB。通信后,每张卡的梯度显存占用从14GB降至1.75GB,释放的显存可以被后续计算复用。

操作二:AllGather(参数收集与同步)

每张卡用自己分到的梯度,更新本地优化器状态和对应的FP32主权重后,需要将更新后的FP16参数同步给所有其他卡,以便下一轮前向传播时所有卡都拥有完整的模型参数。

AllGather就是完成这个“广播+收集”的操作。每张卡贡献自己更新后的参数分片(约1/N的参数),同时从其他卡接收它们的分片。AllGather完成后,每张卡重新拥有完整的模型参数。

这次通信的数据量等于参数总大小:同样14GB。两次通信合计28GB,而标准DP仅需一次梯度AllReduce(14GB)。

ZeRO-2 分片梯度,梯度同步的过程是怎样的?Reduce-Scatter 替代 AllReduce。

ZeRO-2在ZeRO-1基础上进一步对梯度进行分片。在标准数据并行中,梯度通过AllReduce同步——AllReduce将所有卡的梯度求和,然后将完整结果返回给每张卡。但在ZeRO-2中,因为每张卡只负责更新部分参数,它并不需要完整梯度,只需要自己负责的那部分。因此,ZeRO-2用ReduceScatter替代了AllReduce。

梯度同步过程:

反向传播逐层进行。当某一层(比如第N个Transformer层)的梯度计算完成后,ZeRO-2不会等待所有层的梯度都算完,而是立即对该层梯度发起ReduceScatter。这个操作将该层梯度在所有DP卡之间求和,然后按参数分片规则将结果切分——每张卡只保留自己负责更新参数的那部分梯度,其余直接丢弃。

这个“逐层立即处理”的模式带来了两个重要好处:

  • 通信与计算重叠:当第N层的ReduceScatter在后台异步执行时,反向传播可以继续向前推进,计算第N-1层的梯度。两者在时间上重叠,通信延迟被部分隐藏。

  • 显存立即可释放:每张卡不再需要为所有参数保存完整梯度。一层梯度被ReduceScatter处理后,该层完整梯度即可释放,只保留分片后的小部分。整个反向传播结束后,每张卡只持有约1/N的梯度。

从通信量来看,ReduceScatter的总通信量仍然等于梯度总量(14GB),与AllReduce相同。但AllReduce需要将完整结果返回每张卡,而ReduceScatter按需分发,更契合分片存储的需求,避免了不必要的显存占用。

为什么 ZeRO-2 的梯度分片能进一步节省显存?

答案非常直接:因为不再需要为所有参数存储完整的梯度。

在标准DP或ZeRO-1中,梯度是完整存储的——每张卡都要为模型的每一个参数保留一份梯度,直到优化器更新完成。对于一个7B模型,这意味着一笔14GB的固定显存开销,且在整个反向传播和更新期间持续占用。

ZeRO-2改变了这个模式。梯度不再是一份持久保留的完整副本,而是在反向传播过程中被逐层“消费”掉:

  • 当某一层梯度计算完成后,立即通过ReduceScatter归约并分片。

  • 归约完成后,该层的完整梯度即被释放,每张卡只保留自己分片的一小部分。

  • 当所有层都处理完毕,每张卡的梯度总占用仅为完整梯度的1/N(8卡时为1.75GB)。

从显存角度看,梯度占用从14GB降至1.75GB,释放了12.25GB。这笔空间可以用来增大微批次大小、延长序列长度,或者配合Gradient Checkpointing进一步压缩激活值。加上ZeRO-1已经节省的优化器状态(从84GB降至10.5GB),模型状态总显存从112GB降至26.25GB,降幅达76.6%。这就是ZeRO-2能比ZeRO-1训练更大模型的原因。

ZeRO-3 分片参数,前向传播时如何获取所需参数?AllGather。

ZeRO-3是分片最彻底的阶段,连模型参数本身也被切分到各卡。每张卡不再持久存储完整的模型,只持有1/N的参数分片。当需要计算某一层时,必须临时从其他卡获取该层的完整参数。

前向传播的参数获取流程:

设模型有L层,DP组有N张卡。每张卡持久存储的参数量为P/N。

步骤一:参数收集(AllGather)。当计算进行到第i层时,该层的完整参数分布在所有N张卡上。此时发起一次AllGather——每张卡贡献自己持有的该层参数分片,同时从其他N-1张卡接收它们持有的分片。AllGather完成后,每张卡都拥有了该层的完整权重。

步骤二:计算前向传播。使用刚刚AllGather得到的完整参数,结合输入激活值(来自上一层的输出),执行该层的前向计算,产生输出激活值。

步骤三:参数释放。该层计算完成后,输出激活值已经产生并保留(用于反向传播)。此时该层的完整参数不再被需要,因此立即释放AllGather的临时权重,只保留自己原本负责的参数分片。释放的显存可以被下一层的AllGather复用。

这个过程在每一层重复:AllGather→计算→释放→下一层。前向传播的总通信量等于完整模型参数的总大小(因为每一层都被AllGather了一次),但这部分显存占用只是临时的,每层计算完后立即归还,不同层之间可以复用同一块显存缓冲区。

解释 ZeRO-3 参数分片下的“参数收集”与“参数释放”时机。

ZeRO-3通过精确控制参数的“收集”和“释放”时机,实现了显存的极致利用。这个时机控制就像一个精密的仓储管理系统:

参数收集(AllGather)时机:恰好在某一层开始计算之前。这个时机必须精确——不能太早,否则临时参数占用显存时间过长,挤压激活值和梯度存储空间;不能太晚,否则计算无法进行,GPU会空等。通常这个时机设置在层计算的入口处,比如在Transformer层的forward()函数被调用时立即触发。

参数释放时机:恰好在某一层计算完成之后。一旦该层的前向输出已经计算完毕,输入的完整参数就不再被需要,可以立即释放。释放后,对应的显存可以被下一层的AllGather复用。这个释放操作通常紧跟在层输出的产生之后。

反向传播中的特殊处理:反向传播同样需要完整参数来计算权重梯度和输入梯度。但与前向不同,反向传播中各卡已经持有完整的激活值(从前向保留),但参数仍然被分片。因此,反向传播时同样需要再次AllGather每层的完整参数。计算完该层梯度后,完整参数同样立即释放。

精妙的显存复用:因为参数是“即用即取,用完即弃”,不同层的AllGather可以复用同一块显存缓冲区。整个训练过程中,参数收集的峰值显存仅相当于最大单层的参数量,而非整个模型。对于7B模型(32层),单层参数约0.44GB,远小于完整模型的14GB。这就是ZeRO-3能将单卡持久参数显存从14GB压缩到1.75GB(持久存储)+ 单层临时缓冲(~0.44GB)的关键原因。

ZeRO-3 的后向传播中,梯度如何计算并分片?

反向传播在ZeRO-3中同样需要先收集完整参数,计算梯度,然后分片。这个过程与前向传播对称但更复杂:

步骤一:收集参数。对于每一层(从后往前,即从输出层向输入层),先执行AllGather获取该层的完整参数。这次AllGather的通信量等于该层参数大小。

步骤二:计算完整梯度。使用该层完整参数和对应的激活值(前向时保留),计算该层参数的本地梯度。此时计算出的梯度是完整的(针对该层所有参数),但这是基于本地微批次数据的局部梯度,还不是全局梯度。

步骤三:ReduceScatter分片梯度。为了得到全局梯度并分片,立即对该层梯度执行ReduceScatter。这个操作将所有DP卡的该层梯度求和(Reduce),然后按分片规则将求和结果分散(Scatter)——每张卡只保留自己负责更新的那部分参数的全局梯度。例如,卡0只保留参数分区0的全局梯度,卡1只保留分区1的。

步骤四:释放参数和多余梯度。ReduceScatter完成后,该层的完整参数和未被保留的梯度立即释放。只有各卡负责分片的梯度被保留,用于后续优化器更新。

步骤五:本地更新。每张卡用自己分到的全局梯度,更新本地持有的优化器状态(m和v)和对应的参数分片。这一步无需通信。

这个过程逐层进行,每层都需要一次AllGather(参数收集)和一次ReduceScatter(梯度分片)。整个反向传播的总通信量 = 全模型参数的AllGather + 全模型梯度的ReduceScatter,约等于前向通信量的两倍。

比较 ZeRO-1/2/3 的通信量:各自需要多少字节的通信?

以7B模型,FP16混合精度,8卡数据并行为例,各阶段通信量如下表:

查看内嵌表格

注1:ZeRO-1的梯度同步仍使用AllReduce(因为梯度尚未分片),通信量14 GB;参数AllGather通信量14 GB,合计28 GB。ZeRO-2用ReduceScatter替代AllReduce,通信量同为14 GB,加上参数AllGather 14 GB,合计28 GB。ZeRO-3没有更新后的参数AllGather,但增加了前向和反向的参数AllGather各14 GB,加上梯度ReduceScatter 14 GB,合计42 GB。

注2:标准数据并行仅需一次梯度AllReduce,通信量为14 GB。因此ZeRO-1/2的总通信量约为标准DP的2倍,ZeRO-3约为3倍。

ZeRO-3 的通信量约为数据并行的 1.5 倍,这个 1.5 倍怎么来的?

“1.5倍”这个数字来源于对实际训练吞吐量的测量,而非纯通信数据量。理解这个差异需要从两个层面展开:

绝对通信量层面:

如上表所示,ZeRO-3的总通信量(42 GB)是标准DP(14 GB)的3倍,而非1.5倍。如果只看字节数,ZeRO-3的通信开销是相当高的。但通信量不等于训练时间,因为通信可以和计算重叠。

等效训练速度层面:

在实际训练中,ZeRO的通信可以通过多种优化手段被部分隐藏:

  • 通信与计算重叠:参数AllGather可以在前一层计算时预取,梯度ReduceScatter可以与后续层计算重叠。DeepSpeed实现了精密的通信调度,使得相当一部分通信时间被计算时间覆盖。

  • 通信粒度优化:ZeRO-3的AllGather和ReduceScatter被拆分为细粒度操作(逐层进行),穿插在计算中,不会像标准DP那样集中式AllReduce阻塞训练。

  • 参数量与激活值的比例:在实际大模型训练中,模型参数量(决定通信量)与激活值计算量(决定计算时间)的比例并非线性。对于大隐藏维度、深层的模型,计算时间占比更高,通信相对占比下降。例如,增大隐藏维度时,计算量FLOPs随参数量平方增长,而通信量仅随参数量线性增长。

因此,经过深度优化后(如DeepSpeed的ZeRO-3实现),实际训练速度损失约为标准DP的30%~50%,即总训练时间大约是标准DP的1.3~1.5倍。这个“1.5倍”是训练吞吐量的等效比值,反映的是通信开销对端到端性能的实际影响,而非字节级的通信量比值。

这也是为什么ZeRO-3虽然绝对通信量翻了3倍,但在实践中仍然被广泛采用——因为它换来了单卡可训练模型规模的N倍提升(N为GPU数),这种规模扩展能力在大模型时代是决定性的优势。

ZeRO 可以与 TP/PP 结合吗?如果可以,结合方式是怎样的?

可以,且这是训练千亿级模型的标准做法。 ZeRO 属于数据并行(DP)层面的优化,TP 和 PP 属于模型并行层面,三者正交互补。结合的总体原则是:先按 TP/PP 切分模型,再对每个模型副本应用 ZeRO-DP。

具体结合方式分为两步:

第一步:确定模型副本的“逻辑设备”

  • 假设总 GPU 数为 1024,单机 8 卡。

  • 首先设定 TP=8(占用 1 个节点内全部 8 卡),PP=8(需要 8 个 TP 组串联,占用 8 个节点,共 64 卡)。这 64 卡共同构成一个模型副本。

  • 总 GPU 数 / 模型副本所需 GPU 数 = 1024 / 64 = 16,即可以容纳 16 个完整模型副本。

第二步:在模型副本之间应用 ZeRO-DP

  • 这 16 个模型副本之间就是数据并行组(DP group)。ZeRO 的优化器状态、梯度、参数分片都在这个 DP 组内进行。

  • ZeRO-1/2/3 的阶段选择只影响 DP 组内每张卡的显存占用和通信量。

通信层次:

  • TP 通信:在单节点内,每层前向/反向的 AllReduce(激活值切分后的合并)。

  • PP 通信:节点之间,层边界的点对点 Send/Recv(激活值和梯度)。

  • ZeRO-DP 通信:跨所有节点,每步训练后的梯度 ReduceScatter 或 AllReduce,以及参数 AllGather(取决于 ZeRO 阶段)。

这种 “TP 节点内、PP 跨节点、ZeRO-DP 全局” 的三维拓扑是目前最有效的混合并行方案。

当同时使用 ZeRO-3 和 TP 时,参数分片是在 TP 内还是跨 TP 组?

当 ZeRO-3 与 TP 结合时,参数分片是跨 TP 组(即跨数据并行副本),而不是在 TP 组内部。

原因解析:

  • TP 组内部的 GPU 共同拥有完整的一层参数(虽然被切分到各卡,但逻辑上属于同一个模型副本)。如果 ZeRO-3 的分片再在 TP 组内进行,就会破坏 TP 计算的完整性——TP 需要每张卡持有该层的完整逻辑参数(虽然物理上切分,但通过 AllGather 可组装)。ZeRO-3 的参数分片是在 DP 维度上进行的,即在不同的模型副本之间。

  • 一个 TP 组内的所有 GPU 共同构成一个“大 GPU”,它们对外表现为一个完整的模型副本。ZeRO-3 把这个副本的参数再切分到不同的 TP 组上。

举例:设 TP=4,DP=8(即有 8 个 TP 组,每个 TP 组 4 卡)。ZeRO-3 会把整个模型的参数均匀分片到这 8 个 TP 组上。也就是说,每个 TP 组持久存储的参数是总参数的 1/8。当某个 TP 组需要计算某层时,它要从其他 7 个 TP 组 AllGather 收集该层的完整参数(这是跨节点的通信)。而 TP 组内部,各卡仍然通过 TP 的 f/g 操作(节点内通信)协同计算该层。

因此,ZeRO-3 的分片粒度是模型副本级别,TP 的分片粒度是层内矩阵级别,二者不冲突。

为什么推荐在单机内使用 TP,跨机使用 ZeRO-DP?

这完全是由通信带宽决定的。

TP 的通信模式:每层前向和反向都需要 AllReduce(或 AllGather/ReduceScatter),频率极高(96 层模型每步 192 次),数据量也大(与激活值成正比)。节点内的 NVLink 带宽可达 900 GB/s 每对 GPU,总带宽可达数 TB/s,能够轻松支撑这种高频大流量的通信。

跨机通信的瓶颈:跨节点网络(如 InfiniBand HDR 200 Gb/s ≈ 25 GB/s 每链路)的带宽远低于 NVLink,且延迟高。如果 TP 跨机,每层 AllReduce 都将成为瓶颈,GPU 大量时间空闲等待通信,训练效率极低。

ZeRO-DP 的通信模式:ZeRO-DP 的通信发生在整个反向传播结束或参数更新时,频率低(每步 1 次或 2 次),且可以通过分桶、异步等方式与计算重叠,对跨机带宽的敏感度远低于 TP。ZeRO-3 虽然也有每层 AllGather,但 DeepSpeed 通过预取、通信融合等优化,可以将通信开销部分隐藏。

因此,最佳实践是:让通信密集的 TP 留在单机内,让通信稀疏、可重叠的 ZeRO-DP 负责跨机扩展。流水线并行(PP)的通信是层间点对点,频率也低,同样适合跨机。

什么是 ZeRO-Offload?它把什么卸载到 CPU?对显存的节省效果。

ZeRO-Offload 是 ZeRO 的扩展,当 GPU 显存即使经过 ZeRO-3 分片仍然不足时,将优化器状态和优化器计算卸载到 CPU 内存,进一步打破显存墙。

卸载的内容:

  • FP32 优化器状态(Adam 的 momentum 和 variance):这是显存中最大的冗余部分。原本在 ZeRO-1 中它们被分片到各 GPU,ZeRO-Offload 进一步把它们全部移到 CPU 内存。

  • FP32 主权重(master weights):同样移到 CPU。

  • FP16 参数和梯度:仍保留在 GPU 显存中,因为前向和反向计算需要它们高速访问。

  • 优化器更新计算:Adam 的数学运算(更新 m, v, 以及计算新权重)也移到 CPU 执行,因为更新所需的数据(梯度、主权重、优化器状态)都在 CPU 上。

显存节省效果:极端情况下,GPU 显存仅需存储FP16 参数、梯度和激活值。以 7B 模型为例,FP16 参数 14 GB + 梯度 14 GB + 激活值(通过 Gradient Checkpointing 压缩后约 8 GB)≈ 36 GB,远低于单卡 80 GB 容量。这使得在消费级 GPU(如 24 GB)上也能训练大模型成为可能。

ZeRO-Offload 如何利用 CPU 进行优化器更新?数据流向是怎样的?

ZeRO-Offload 将优化器更新的全部计算放在 CPU 上,GPU 只专注于前向和反向传播。数据流向经过精心设计,以最小化 PCIe 传输瓶颈。

具体流程(以单卡为例):

  1. 前向和反向传播(GPU):
  2. GPU 持有完整的 FP16 参数。前向计算使用这些参数,产生激活值。
  3. 反向传播逐层计算 FP16 梯度。梯度暂时驻留在 GPU 显存中。

  4. 梯度传输到 CPU:

  5. 每一层(或一组层)的梯度计算完成后,立即异步拷贝到 CPU 内存(通过 PCIe)。GPU 并不等待 CPU 完成,而是继续反向传播下一层。

  6. CPU 执行优化器更新:

  7. CPU 上维护着 FP32 主权重和优化器状态(m, v)。
  8. CPU 接收梯度后,将其从 FP16 转换为 FP32,然后执行标准的 Adam 更新公式:更新 m 和 v,再更新主权重。
  9. 更新完成后,CPU 得到新的 FP32 主权重,再将其转换为 FP16。

  10. 更新后的参数传回 GPU:

  11. 新的 FP16 权重通过 PCIe 异步拷贝回 GPU,覆盖原来的旧权重。
  12. 这个过程同样不需要 GPU 等待,可以与后续层的反向传播或下一轮前向传播重叠。

数据流向图:

GPU: [FP16参数] ←───────────── 更新后FP16参数 ← CPU
 |                               ↑
 ├─ 前向传播 → 激活值 → 反向传播 → FP16梯度 → 异步拷贝到CPU
 |                               ↓
 └─ 激活值释放               CPU: [FP32主权重, m, v] → Adam更新

通过异步传输和计算与拷贝重叠,ZeRO-Offload 将 PCIe 带宽的瓶颈影响降到最低。

解释 ZeRO-Offload 中的“计算-通信重叠”和“单拷贝”设计。

计算-通信重叠:

  • 传统数据加载中,CPU 和 GPU 之间的数据传输往往是阻塞的。ZeRO-Offload 使用CUDA Stream和异步内存拷贝,将梯度从 GPU 到 CPU 的传输、优化器更新、以及参数从 CPU 回传 GPU 的操作,与 GPU 上的计算(下一层的反向传播或下一轮的前向传播)并发执行。

  • 例如,当 GPU 正在计算第 N 层的反向梯度时,CPU 已经在处理第 N+1 层的优化器更新。两者流水线化,使得 CPU 的慢速计算和 PCIe 传输被部分隐藏在 GPU 计算时间之后。

单拷贝设计:

  • ZeRO-Offload 在 CPU 和 GPU 之间只保留一份数据副本,避免不必要的重复拷贝。例如,FP16 参数在 GPU 上,FP32 主权重在 CPU 上,不会在 GPU 上同时保留一份 FP32 主权重。梯度从 GPU 传输到 CPU 后,GPU 上的梯度即可释放,不保留冗余。

  • 这种精简的内存管理减少了内存占用,也减少了数据移动的次数,从而提升效率。

通过这两项设计,尽管引入了 CPU 参与计算,但训练速度的下降被控制在 30%~50% 左右,而不是数倍的性能损失。

ZeRO-Infinity 与 ZeRO-Offload 的区别?它支持卸载到什么介质?

ZeRO-Offload 仅支持卸载到 CPU 内存,当模型大到连 CPU 内存都不够时(例如训练 100B+ 模型,优化器状态可能需要数百 GB),ZeRO-Infinity 登场了。

ZeRO-Infinity 将卸载能力扩展到NVMe 固态硬盘,构建了一个 GPU 显存 → CPU 内存 → NVMe 硬盘的三级存储体系。它突破了单服务器物理内存的上限,使得在有限硬件上训练万亿参数模型成为可能。

查看内嵌表格

ZeRO-Infinity 如何进行显存和 CPU/NVMe 之间的动态数据转移?

ZeRO-Infinity 像一个智能交通调度系统,实时预测模型的计算需求,提前将数据从慢速存储(NVMe)搬运到快速存储(CPU 内存或 GPU 显存),并在不需要时移回慢速存储。

动态转移策略:

  1. 预取:在 GPU 计算当前层时,后台线程根据计算图分析,提前从 NVMe 将下一层所需的参数、梯度或优化器状态加载到 CPU 内存缓冲区。当 GPU 需要时,数据已在 CPU 内存中,只需一次 PCIe 传输即可到达 GPU。

  2. 换出:当 GPU 或 CPU 内存空间不足时,将当前最不急需的数据(如已经反向传播完的层的优化器状态)异步写入 NVMe,释放空间给即将需要的数据。

  3. 带宽优化:ZeRO-Infinity 针对 NVMe 的特性进行了专门的 I/O 优化,包括批量顺序读写、并行化 SSD 访问、对齐块大小等,以最大化吞吐量。它还会智能地在 CPU 内存中缓存热点数据,减少 NVMe 访问次数。

通过这种动态、预取式的数据调度,ZeRO-Infinity 将原本不可行的 NVMe 训练变为可能,尽管速度会慢很多(可能是纯 GPU 训练的 2~10 倍),但对于极限规模的实验,这是有价值的。

使用 ZeRO-Offload 训练时,CPU 内存需求有多大?如何估算?

CPU 内存主要存储FP32 优化器状态和 FP32 主权重。估算方法如下:

对于参数量为 P 的模型:

  • FP32 优化器状态(m 和 v):2 × P × 4 bytes = 8P bytes

  • FP32 主权重:P × 4 bytes = 4P bytes

  • 合计:12P bytes

以 7B 模型为例:12 × 7 × 10^9 ≈ 84 GB。此外,还需要一些临时缓冲区和操作系统开销,通常建议预留 1.5~2 倍的安全余量,即至少需要 128 GB 以上的 CPU 内存。

如果使用多卡数据并行,CPU 内存需求与 GPU 数量无关(每张卡都维护完整的优化器状态),但 ZeRO-1/2 的分片同样可以应用到 CPU 卸载上,即 DeepSpeed 可以配置 CPU 优化器状态分片,此时单机 CPU 内存需求将除以节点内卡数。

ZeRO-Offload 对训练速度的影响通常是多少?为什么会变慢?

通常使训练速度下降 30%~70%(即吞吐量降至原来的 30%~70%)。变慢的根本原因是 PCIe 带宽限制。

GPU 显存带宽高达 2 TB/s(如 A100),而 PCIe 4.0 x16 单向带宽仅 ~32 GB/s,差距达 60 倍以上。每次训练步需要将梯度从 GPU 传到 CPU(14 GB for 7B),再将更新后参数从 CPU 传回 GPU(14 GB),总传输量 28 GB,理想情况下仅传输时间就需要约 0.9 秒。相比之下,GPU 内部计算时间可能远小于此。因此,数据传输成为瓶颈,GPU 的计算速度被慢速 PCIe 链路拖慢。

尽管采用了异步传输和计算重叠,但无法完全消除这种速度差异,因为重叠需要计算时间和传输时间大致匹配,而通常传输时间远超单层计算时间。增大批次大小可以部分提高 GPU 利用率,但总体上速度损失是卸载的必然代价。

在训练时,如何调整 ZeRO stage 和 offload 参数以获得最佳性价比?

“性价比”指在给定硬件预算下,达到最高的训练吞吐或训练最大规模模型。调整策略如下:

步骤一:优先使用 ZeRO-2(或 ZeRO-1)

  • 如果单卡显存足以容纳模型参数 + 梯度 + 激活值(但不足以容纳优化器状态),则使用 ZeRO-1 或 ZeRO-2。它们通信量小,训练速度接近标准 DP。

  • ZeRO-2 通常是最佳平衡点,节省了梯度显存,而额外通信开销可以忽略。

步骤二:显存仍不足,启用 ZeRO-3

  • 当 ZeRO-2 下激活值或参数显存溢出时,升级到 ZeRO-3。其参数分片大幅削减显存,但通信量增加约 50%。如果希望保速,可配合梯度检查点进一步压缩激活值。

步骤三:CPU Offload 作为最后手段

  • 如果 ZeRO-3 仍 OOM,或硬件显存太小(如消费级 24 GB),开启 offload_optimizeroffload_param

  • 优先卸载优化器(offload_optimizer),因为优化器状态最大,且更新计算可以移出 GPU,速度损失通常在 30%-40%。卸载参数(offload_param)损失更大(50%+),仅当显存极度紧缺时使用。

步骤四:权衡 Micro Batch Size 和 Gradient Accumulation

  • 卸载后,GPU 显存压力减小,可以适当增大 micro batch size,提高 GPU 利用率,补偿部分速度损失。

推荐配置模板(以 8×A100 80GB 训练 13B 模型为例):

  • 高性价比:stage=2, offload_optimizer=false,TP=1, PP=1,ZeRO-2 即可。

  • 平衡:stage=3, offload_optimizer=false,TP=1, ZeRO-3,显存充裕。

  • 极限大模型(如 30B):stage=3, offload_optimizer=true,TP=2,PP=2,结合卸载与模型并行。

通过“先 ZeRO-2,再 ZeRO-3,最后 Offload”的渐进式策略,可以在保持训练速度的前提下,最大化利用现有硬件。

什么是 ZeRO++?它针对什么问题?

ZeRO++ 是 ZeRO 系列优化的进阶版本,针对的核心痛点是跨机通信带宽限制。ZeRO-3 虽然通过分片参数实现了极致的显存压缩,但其代价是每层前向和反向都需要 AllGather 完整参数,跨机通信量约为标准数据并行的 3 倍。在节点内部,NVLink 高带宽可以轻松承载这一通信负载,但当训练扩展到数百甚至上千张 GPU、跨越数十个节点时,跨机网络(InfiniBand)带宽成为瓶颈。节点间的通信时间可能超过计算时间,导致 GPU 大量空闲,训练吞吐严重下降。

ZeRO++ 从三个维度对通信进行极致优化:

  • 量化通信:通过将传输的梯度和参数量化为低精度(如 INT4/INT8),成倍压缩通信数据量。

  • 分层通信:利用节点内高带宽和节点间低带宽的异构特性,将通信分两层处理,减少跨机数据量。

  • 细粒度调度:通过更精细的通信与计算重叠、预取策略,进一步隐藏通信延迟。

目标是让 ZeRO-3 在大规模跨机训练时,通信开销接近甚至低于传统数据并行,从而在保持极致显存节省的同时实现高扩展性。

此外,ZeRO++ 还专门针对量化精度损失、异构网络拓扑以及参数更新的延迟隐藏做了专门设计,使得原本在 256 卡以上规模出现的通信瓶颈得到了大幅缓解。在实践中,使用 ZeRO++ 可以在千卡集群上达到与 ZeRO-2 相近的 MFU(Model FLOPs Utilization),而同时享受 ZeRO-3 的显存节省效果。

ZeRO++ 的 qgZ(量化梯度)是如何压缩梯度的?效果如何?

qgZ(quantized gradient ZeRO) 是 ZeRO++ 中对梯度通信的量化压缩技术。其核心思想是:在反向传播完成后,各卡在发送梯度进行 ReduceScatter 之前,先将 FP16 梯度量化为低精度(如 INT4),然后再传输。接收方在收到量化梯度后,将其反量化为 FP16,再进行累加。

具体实现:

  1. 分块量化:将梯度张量划分为多个小块(如每 64 个元素一组),每个块独立计算自己的缩放因子(块内最大值)。这种分块策略能够适应梯度在不同区域分布的差异,避免单一缩放因子导致的精度损失。

  2. 量化与传输:块内每个梯度值除以缩放因子,四舍五入到 INT4 表示范围,然后发送。通信数据量仅为原始 FP16 的 1/4。

  3. 反量化与累加:接收方将收到的 INT4 梯度乘以缩放因子,恢复为 FP16,然后进行 ReduceScatter 的累加操作。

效果:

  • 通信量压缩至原来的 1/4 ~ 1/8(取决于量化位数,INT4为1/4,INT8为1/2)。

  • 由于梯度在训练过程中本身就存在噪声,小幅度的量化误差通常不会影响收敛。实践中,qgZ 可以在几乎不损失模型精度的情况下,将跨机梯度通信开销降低 50%~70%。

  • 在千卡集群的实测中,启用 qgZ 后,总训练吞吐提升了约 1.3x~1.5x,因为跨机梯度通信不再是瓶颈,GPU 空闲率大幅下降。

需要注意的是,量化操作本身会引入额外的计算开销(量化/反量化),但由于这些操作可以在 GPU 上高效并行,且与通信流水线重叠,因此额外开销通常在 5% 以内,远低于其节省的通信时间。

ZeRO++ 中的 hpZ(分层分区 ZeRO)如何减少跨机通信?

hpZ(hierarchical partition ZeRO) 利用了节点内外带宽的异构性,将参数分片和通信分为两个层次。

核心设计:

  • 节点内:由于 NVLink 带宽极高,可以在节点内维护完整参数副本,但节点内的 GPU 之间仍然通过 ZeRO-3 分片存储参数(以减少单卡显存)。这个副本被称为“二级分片”。

  • 节点间:跨节点的通信不再以单张 GPU 为单位,而是以节点为单位。每个节点作为一个整体参与全局的参数分片和收集。节点内部通信使用全精度(FP16/BF16),而节点间通信可以使用压缩/量化数据。

具体流程(以 AllGather 参数为例):

  1. 节点内每个 GPU 持有该层参数的一部分(1/N 节点内)。首先在节点内进行一次 AllGather(利用 NVLink 高带宽),使得节点内的一个指定 GPU(或节点内的一个代理)获得该层的完整参数。

  2. 该代理 GPU 将完整参数量化为低精度(如 INT8),然后通过跨机网络发送给其他节点的代理 GPU。

  3. 接收节点内的代理 GPU 收到量化参数后,反量化为完整精度,再通过节点内 AllGather(或 Broadcast)将完整参数分发给节点内其他 GPU。

这样,跨机通信的数据量从全模型参数降低为节点数 × 量化后的参数,且跨机通信次数大幅减少。由于节点内 NVLink 带宽比跨机带宽高数十倍,节点内多出的 AllGather 开销极低,而节省的跨机通信时间显著。

hpZ 的效果:在 32 节点(每节点 8 卡)的集群中,hpZ 可将 ZeRO-3 的跨机通信量降低约 50%~70%,具体取决于量化策略和模型大小。结合 qgZ,ZeRO++ 的总通信量接近甚至低于传统 ZeRO-2 的跨机通信量,同时保持 ZeRO-3 的显存优势。

简述 ZeRO++ 对 ZeRO-3 参数收集通信的优化(量化权重/梯度)。

ZeRO-3 的参数收集涉及两个方向的通信:前向和反向时的参数 AllGather(权重收集),以及梯度 ReduceScatter(梯度聚合)。ZeRO++ 对这两个过程都进行了量化优化:

  • 权重收集的量化:在 AllGather 之前,将待发送的 FP16 参数量化为 INT8(或更低)。接收方反量化回 FP16。由于模型权重在训练过程中变化相对平滑,量化误差对前向和反向计算的影响极小。此优化可将参数收集的跨机通信量压缩至 1/2。

  • 梯度聚合的量化(即 qgZ):在 ReduceScatter 之前,将 FP16 梯度量化为 INT4。由于梯度噪声较大,INT4 的精度损失在反向传播中可被自然吸收。通信量压缩至 1/4。

  • 分层通信的量化(hpZ):在跨机传输前,先将节点内完整参数或梯度收集到代理 GPU,再进行量化传输。

综合这些优化,ZeRO++ 将 ZeRO-3 原本约为标准 DP 1.5 倍(实际吞吐等效值)的通信开销进一步降低,使得在超大规模集群上使用 ZeRO-3 训练几乎不再有额外的通信惩罚。

DeepSpeed 配置中,zero_optimization 的 stage 如何设置?

在 DeepSpeed 配置文件的 zero_optimization 部分,stage 参数接受 0、1、2、3 四个值:

  • stage=0:禁用 ZeRO,所有状态(参数、梯度、优化器)每张卡都保存完整副本,相当于标准数据并行。

  • stage=1:分片优化器状态。单卡只存储 1/N 的 FP32 主权重和 Adam 状态,梯度仍为全量。更新后通过 AllGather 收集完整参数。

  • stage=2:进一步分片梯度。反向传播时对梯度做 ReduceScatter,每卡只保留负责参数的那部分梯度,节省梯度显存。

  • stage=3:进一步分片模型参数。每卡只持久存储 1/N 的参数,前向和反向通过 AllGather 临时收集完整参数,显存压缩到极致。

如何选择 stage:

  1. 如果单卡能容纳完整模型状态,且希望最小通信开销,用 stage=0 或 1。

  2. 大多数 7B~13B 模型在 8 卡 A100 上,stage=2 是性能与显存的最佳平衡。

  3. 需要训练更大模型或使用更长序列时,升级到 stage=3。

  4. 在跨节点环境下,若 ZeRO-3 通信成为瓶颈,可考虑使用 ZeRO++ 的 stage=3 变体。

DeepSpeed 还支持 offload_optimizeroffload_param 等选项,可与任意 stage 组合使用。

“overlap_comm” 参数的作用是什么?开启后有何效果?

overlap_comm 是 DeepSpeed ZeRO 配置中的一个布尔参数,默认为 true。它控制梯度通信与反向传播计算的重叠。

作用:在反向传播过程中,当某一层的梯度计算完成后,正常情况下需要等待该层梯度完成跨 GPU 的 ReduceScatter 或 AllReduce 之后才能继续处理下一层。overlap_comm 开启后,DeepSpeed 会在该层梯度计算完毕的瞬间,立即将梯度通信操作投递到独立的 CUDA Stream 中异步执行,而主计算流继续反向传播下一层。当主计算流推进到需要使用通信结果时(如优化器更新前),再同步等待通信完成。

效果:

  • 隐藏通信延迟:由于通信与计算并行,大部分梯度通信时间被后续层的反向计算时间所覆盖,几乎不增加额外的训练步时。

  • 提升吞吐:在通信瓶颈明显的配置下(如跨节点 ZeRO-3),开启此选项可提升 10%~20% 的训练吞吐。

  • 代价:额外的异步操作需要显存来存储中间缓冲区和通信队列,通常少量增加显存占用(< 5%),在显存充裕时可放心开启。

注意,overlap_comm 只有在使用 ZeRO stage=1 以上时才有意义,因为它主要针对的是梯度通信。

“contiguous_gradients” 在 ZeRO 中是什么?为什么要设置?

contiguous_gradients 是 DeepSpeed 中的一个配置项,默认为 true。它决定是否将多个参数的梯度在通信前拼接成连续的内存块,然后再进行 AllReduce 或 ReduceScatter。

为什么需要:

  • 深度网络中的参数梯度通常分散存储在不同的张量中,大小、形状各不相同。如果逐个发起通信,会产生大量极小的通信操作,严重浪费网络带宽(因为通信启动开销远大于小数据传输时间)。

  • 开启 contiguous_gradients 后,DeepSpeed 会预分配一个大的连续缓冲区,将不同参数的梯度拷贝到这个缓冲区中,拼接成一个大的连续张量,然后对这个大张量执行一次集合通信。

  • 通信完成后,再将缓冲区中的梯度拷贝回各个参数的梯度存储位置。

效果:

  • 大幅减少通信启动次数,提高跨机网络带宽利用率(可达 80% 以上)。

  • 略微增加 CPU 和 GPU 内存拷贝的开销,但这些拷贝操作可以与计算并行,通常不会成为瓶颈。

在大规模分布式训练中,此选项建议始终开启,因为它对通信效率的提升非常显著。

在 DeepSpeed 中,如何配置“sub_group_size”以控制通信组大小?

sub_group_size 是 DeepSpeed 中用于分层 AllReduce 的参数。它控制在进行梯度通信时,首先在多大的 GPU 子组内做 Reduce,然后再跨子组做 AllReduce。

配置方式:在 DeepSpeed 配置文件中添加:

"zero_optimization": {
    "stage": 2,
    "reduce_bucket_size": 5e8,
    "allgather_bucket_size": 5e8,
    "sub_group_size": 1e9
}

作用:

  • 如果 sub_group_size 设置为一个正整数(如 8),则 DeepSpeed 会将所有 GPU 划分为大小为 8 的子组,首先在每个子组内部进行梯度 Reduce,然后将子组间的结果进行 AllReduce。

  • 这种分层通信可以降低通信的总延迟,特别适用于跨机通信场景。子组内的 Reduce 可以使用节点内高带宽(NVLink),子组间的 AllReduce 跨机进行,但此时的数据量已经减少(因为已经在子组内归约过了)。

  • 设置 sub_group_size 为节点内 GPU 数量(如 8),可以实现最优的节点内通信与节点间通信的平衡。

注意事项:

  • sub_group_size 应能被总 GPU 数整除。

  • 这个参数主要用于 stage=2 时优化梯度通信。对于 stage=3,DeepSpeed 有更复杂的通信调度策略。

什么是“梯度累积”与 ZeRO 的交互?梯度分片在累积后如何处理?

梯度累积 是为了在显存受限时模拟大批次训练的技术:模型对多个 micro-batch 分别执行前向和反向,每个 micro-batch 的梯度被累加到梯度缓冲区中,直到累积足够的步数后才执行一次优化器更新。

在标准数据并行中,梯度累积只是延迟了 AllReduce 的执行——通常在累积的最后一步才进行梯度同步,以减少通信次数。但在 ZeRO 中,由于梯度被分片(stage=2)或参数被分片(stage=3),梯度累积的交互更为复杂:

ZeRO-2 下的梯度累积:

  • 每个 micro-batch 的反向传播会产生局部梯度。ZeRO-2 会在每个 micro-batch 反向结束后,立即对该 micro-batch 的梯度进行 ReduceScatter(因为梯度需要被分片存储以节省显存)。

  • 为了模拟梯度累积,DeepSpeed 会为每个参数分片维护一个梯度累积缓冲区(位于 FP16 显存)。每次 ReduceScatter 得到的梯度会累加到该缓冲区中,而不是直接用于更新。

  • 当累积步数达到目标后,使用累积缓冲区中的总梯度来更新优化器状态和参数。

ZeRO-3 下的梯度累积:

  • 与 ZeRO-2 类似,但还需要处理参数的 AllGather。每个 micro-batch 前向和反向都需要临时收集完整参数。梯度同样通过 ReduceScatter 分片并累积。

关键点:

  • 梯度累积不会破坏 ZeRO 的显存节省效果,因为分片的梯度和参数仍然只占 1/N,累积缓冲区也只保存分片部分的梯度。

  • 但使用梯度累积时,需要注意gradient_accumulation_steps 应被正确设置,并且与 ZeRO 的通信调度兼容。DeepSpeed 会自动处理这些细节。

当使用 ZeRO-3 时,如何保存和加载模型权重?多个分片如何合并?

ZeRO-3 下,每张 GPU 只持有 1/N 的模型参数(分片存储)。保存模型时,如果直接按分片保存,将产生 N 个分片文件,无法直接用于推理或迁移。

保存方法:

  • 分片保存:每张卡将自己持有的参数分片保存为一个独立的 checkpoint 文件(通常包含模型参数和优化器状态)。这种保存速度最快,适合断点续训。

  • 合并保存:在保存前,通过 AllGather 将各卡的参数分片收集到一张卡上(通常是 rank 0),合并为完整的模型权重,然后保存为标准的 HuggingFace 或 PyTorch 格式。这种保存会产生大量通信(全模型参数一次 AllGather),但方便后续使用。

DeepSpeed 提供的工具:

  • 训练时设置 "stage3_gather_16bit_weights_on_model_save": true,DeepSpeed 会在保存模型时自动收集合并 FP16 权重,并保存为单个文件。

  • 如果训练时没有合并,后续可以使用 DeepSpeed 提供的脚本 zero_to_fp32.py 进行离线合并。

加载方法:

  • 从分片 checkpoint 恢复训练:加载各卡对应的分片文件和优化器状态,重新初始化 ZeRO-3 的进程组,然后继续训练。

  • 加载为完整模型:将所有分片文件放在同一目录,使用 zero_to_fp32.py 或 HuggingFace 的 from_pretrained 加载合并后的权重。

DeepSpeed 中“zero_to_fp32.py”脚本的作用和使用场景。

zero_to_fp32.py 是 DeepSpeed 官方提供的工具脚本,用于将 ZeRO stage 3 训练保存的分片 checkpoint 合并为单个完整的 FP32 模型权重文件。

使用场景:

  • 训练完成或中断后,希望将模型导出为标准的 PyTorch 或 HuggingFace 格式,以便用于推理、微调或分享。

  • 训练时未开启 stage3_gather_16bit_weights_on_model_save,而保存的是分片 checkpoint。

脚本用法:

python zero_to_fp32.py . pytorch_model.bin
  • 第一个参数是包含分片 checkpoint 的目录(通常有 zero_pp_rank_* 子目录)。

  • 第二个参数是输出的合并后的模型文件名(可选,默认 pytorch_model.bin)。

脚本原理:

  • 读取每个 rank 保存的分片文件。

  • 根据 ZeRO-3 的参数分片元信息,将各分片的权重拼接回完整的张量。

  • 将所有层的权重组装成完整的模型 state_dict,并保存。

注意事项:

  • 合并过程需要在 CPU 或单个 GPU 上加载所有分片,因此需要足够的内存来容纳完整模型。对于超大模型(如 175B),合并可能需要在内存充足的 CPU 节点上进行。

  • 脚本只合并权重,不合并优化器状态。如需恢复训练,应直接加载分片 checkpoint。

ZeRO-3 训练的模型能否直接用于推理?需要额外合并步骤。

ZeRO-3 训练的模型不能直接用于推理,因为推理通常需要完整的模型权重,而 ZeRO-3 的权重是分片存储在多张 GPU 上的。

为什么不能直接推理:

  • 推理时通常不依赖分布式通信环境,或者只需要单卡推理。ZeRO-3 的模型状态分散在多个设备,单卡无法独立完成前向传播。

  • 即使推理时可以组建 ZeRO-3 的通信组,每层都需要临时 AllGather 参数,这会引入不可接受的延迟,且显存节省不再是优先目标。

合并步骤:

  1. 使用 DeepSpeed 的 zero_to_fp32.py 脚本将分片 checkpoint 合并为单个完整的 FP32 权重文件。

  2. 将合并后的权重加载到标准的模型结构中(如 HuggingFace Transformers 的 from_pretrained)。

  3. 根据推理需求,可以将模型转换为 FP16 或 INT8 量化版本以节省显存,然后进行单卡或多卡推理(不需要 ZeRO)。

特殊场景:

  • 如果推理也需要超大规模模型,可以使用 推理版的模型并行(如 Megatron 的 TP),但与 ZeRO-3 的分片格式不兼容,需要先合并再重新切分。

  • 未来 ZeRO 的推理模式或许可以直接加载分片权重,但目前主流方案还是先合并。

总之,ZeRO-3 为训练而生,它的分片机制不适合推理的低延迟要求。合并步骤虽然多了一步,但能将训练成果无缝转化为可部署的标准模型。

为什么说 ZeRO 是一种“数据并行的内存高效变体”?

数据并行(DP)的核心是每张GPU持有完整的模型副本,独立处理不同的输入数据,然后同步梯度,保证参数一致。在这个过程中,每张卡上的模型参数、梯度和优化器状态(Adam的m和v等)完全相同,形成了巨大的冗余。当模型较小时,这种冗余只是浪费;当模型大到数十亿参数时,单卡显存根本无法容纳这些完整状态,DP完全不可行。

ZeRO(Zero Redundancy Optimizer)正是针对这一冗余而设计的。它在保持DP训练流程基本不变的前提下,将模型状态分片(partition)到所有DP组内的GPU上,每张卡只持有1/N的状态。当一个GPU需要某些不在本地的数据时,通过高效的集合通信(AllGather、ReduceScatter)瞬时获取,用后即释放。这种设计使得整个集群的模型状态容量等于所有GPU显存之和,而单卡显存需求仅约为完整状态的1/N。

之所以称ZeRO为“数据并行的内存高效变体”,是因为它没有改变DP的三大核心特征:

  1. 独立的数据处理:每张卡仍独立处理不同的微批次数据。

  2. 全局梯度同步:仍然使用全局梯度来保证模型一致性,只是同步方式从AllReduce变成了ReduceScatter+AllGather等组合。

  3. 参数最终一致:经过通信后,所有卡上的模型参数逻辑上仍然保持一致。

ZeRO仅仅是通过分片消除了状态冗余,让DP能够在更大模型上运行。随着ZeRO阶段从1提升到3,分片粒度越来越细,单卡内存占用也越来越接近理论下界(完整状态的1/N)。因此,ZeRO可以视为在DP框架内,通过内存管理革新实现的“无损压缩”,是DP适应大模型时代的必然产物。

ZeRO 和模型并行 (TP/PP) 相比,哪个更适合扩展到数千卡?

这取决于“扩展”的维度。ZeRO本质上是横向扩展(scale out),TP/PP则是纵向扩展(scale up),两者互补而非替代。

ZeRO/DP的扩展特性:

  • 增加GPU数量,直接在DP组中增加数据副本,线性提升整体吞吐。

  • 通信模式(AllReduce、ReduceScatter)可以很好地适应大规模集群的拓扑,通过梯度分桶、通信与计算重叠等技术,将通信开销隐藏在计算之后。

  • 即使扩展到数千卡,只要网络拓扑合理(如InfiniBand全互联),ZeRO仍能保持较高的扩展效率。尤其在ZeRO++等优化后,跨节点通信量大幅压缩,扩展瓶颈进一步缓解。

TP/PP的扩展特性:

  • TP:将单层参数切分,每层都需要高频AllReduce(每层前向+反向各至少1次),通信量与激活值大小成正比。跨节点网络带宽远低于节点内NVLink,因此TP一旦跨机,通信立刻成为瓶颈,扩展性极差。TP通常限制在节点内(如8卡)。

  • PP:将模型按层切分,通信是层间点对点的,频率低,可跨节点。但受限于模型层数,PP并行度通常不超过几十,不能单独扩展到数千卡。

因此,要扩展到数千卡,必须以ZeRO/DP为主。TP/PP用于解决单卡无法容纳单层参数或激活值的问题,是“为了让DP能跑起来”的辅助手段。实际大模型训练中,常采用混合策略:节点内TP=8(利用NVLink),跨节点PP=8~16(降低单卡层数),剩余的卡全部用于ZeRO/DP,形成“DP(最大维度)+ PP + TP”的3D并行。ZeRO提供了扩展到万卡级的关键能力,而TP/PP则解决了单卡的存储与计算极限。

在单卡上使用 ZeRO-Offload 有用吗?为什么?

非常有用,在单卡场景下是突破显存瓶颈的关键工具。

ZeRO的分片机制需要多张GPU协同工作,但ZeRO-Offload不同,它不依赖多卡。其核心是将优化器状态、FP32主权重以及优化器更新计算迁移到CPU内存和CPU计算单元上。即使只有一张GPU,GPU仍然需要执行前向和反向计算,但不再需要在显存中保留庞大的优化器状态。

对于单卡训练7B或13B模型:

  • FP32优化器状态(m和v)加上FP32主权重,参数量约为模型的12倍。7B模型这部分约84GB,13B模型约156GB,远超消费级GPU的24GB甚至企业级80GB。

  • 开启ZeRO-Offload后,GPU只需存储FP16参数、梯度(可临时保留然后卸载)和激活值(可配合梯度检查点压缩),总显存可降至30~50GB,其余全部放在CPU内存中。

  • 优化器更新也在CPU上完成,GPU专注于矩阵计算。

尽管会因PCIe带宽限制(约32GB/s vs 2TB/s的GPU带宽)导致训练速度下降30%~70%,但对于硬件资源匮乏的场景(如单卡微调、学术研究),这是唯一能让大模型跑起来的途径。因此,单卡ZeRO-Offload极其有用。

微调时,ZeRO 的 stage 选择与全量训练有何不同?

微调(Fine-tuning)时,模型的绝大部分参数可能是冻结的(如LoRA),或者我们只更新全部参数(全量微调),两种情况的ZeRO需求差异很大。

  • 全量微调:所有参数都参与训练,梯度、优化器状态与预训练时完全相同。因此ZeRO的stage选择与预训练一致:通常需要ZeRO-2分片梯度和优化器状态,甚至ZeRO-3分片参数,才能将单卡显存降至可接受范围。

  • 参数高效微调(PEFT,如LoRA):可训练参数仅为全量的千分之一,梯度和优化器状态极小(几乎忽略不计)。显存的主要消耗是只读的预训练参数(FP16副本)。此时ZeRO-3的参数分片可直接将这些只读参数分布到多卡,极大节省显存。而ZeRO-1/2对梯度和优化器状态的分片收益微乎其微,因为这部分本来就不大。因此,LoRA微调时,如果单卡显存不够放只读参数,直接上ZeRO-3是最佳选择;如果显存充足,甚至完全不需要ZeRO。

所以微调时的stage选择更灵活:对于全量微调,stage与预训练一致;对于PEFT,按需使用ZeRO-3即可,ZeRO-1/2非必需。

LoRA + ZeRO 的组合效果如何?是否冲突?

完全不冲突,而且组合起来威力强大。

LoRA冻结了绝大部分原始参数,只有极少低秩适配矩阵需要梯度,因此梯度和优化器状态占比极小。但原始参数的FP16副本仍然占用大量显存(7B约14GB,13B约26GB)。如果单卡放不下这些只读参数,或者想增大batch size、加长序列,就必须解决参数存储问题。

ZeRO-3恰好能分片这些只读参数,每卡只存1/N,例如8卡训练,每卡仅需约1.75GB存放7B模型参数。这样,LoRA解决了“梯度和优化器状态”的显存,ZeRO-3解决了“预训练参数存储”的显存。两者各自独立地压缩显存的不同部分,不存在冲突。实践中,LoRA + ZeRO-3可以在单张24GB显卡上微调65B甚至更大的模型;若再加上ZeRO-Offload,门槛还能进一步降低。

因此,LoRA与ZeRO-3是绝佳搭档,尤其在消费级硬件上微调超大模型时。

使用 QLoRA 时,是否需要 ZeRO?可以结合 Zero-Offload。

QLoRA通过4-bit量化将预训练模型权重压缩到原来的1/4,显存占用已大幅降低。同时LoRA的可训练参数极少,梯度和优化器状态也很小。多数情况下,QLoRA已能在单卡(如24GB)上微调65B模型,不需要额外使用ZeRO。

但如果想进一步扩展batch size或序列长度,或者硬件显存特别紧张(如12GB),QLoRA可以结合ZeRO-Offload。ZeRO-Offload将优化器状态卸载到CPU内存,进一步释放GPU显存给激活值。但注意,QLoRA本身已经用分页优化器(Paged Optimizer)管理显存,与ZeRO-Offload功能重叠,通常不需要同时开启。若必须叠加,建议先尝试增加CPU内存并开启offload_optimizer,避免通信开销过大。

结论:QLoRA优先,显存仍不够时再叠加ZeRO-Offload,ZeRO-3通常不必要,因为量化已解决参数存储问题。

ZeRO-3 中每个 GPU 上都有自己的参数分片,那 Embedding 层如何处理?

Embedding层(词嵌入矩阵)通常参数量巨大(词表大小 × 隐藏维度),且访问模式稀疏(每次只查少数token)。在ZeRO-3中,Embedding层的处理方式通常有两种:

  1. 作为普通参数分片:将Embedding矩阵按词表维度切分到各GPU。前向时,通过AllGather收集当前batch中实际需要的token对应的完整权重(或直接通信需要激活的嵌入向量),然后再做查表。这种方式节省了显存,但引入了额外通信。DeepSpeed默认采用这种方法。

  2. 特殊处理(数据并行):因为Embedding的输入数据是全局分布的(不同GPU处理不同样本),查表操作本身不需要完整的矩阵,且Embedding的梯度通常很大。一些框架(如Megatron-LM)会将Embedding层作为数据并行,每卡保留完整副本,不参与ZeRO-3的参数分片。这样避免了AllGather通信,但增加了显存占用。

在DeepSpeed ZeRO-3中,默认是对Embedding进行分片的,同时也支持配置stage3_param_persistence_threshold等参数来将某些小参数或Embedding层排除在分片之外。用户可以根据词表大小和通信带宽权衡决定。

对于共享参数,ZeRO 如何处理?例如 weight tying。

Weight tying(如输入嵌入和输出嵌入共享权重)让模型的两个不同层引用同一个参数张量。在分片存储时,必须保证这个共享参数只被存储一次,并且在更新和通信时保持一致。

DeepSpeed的处理机制:

  • 在模型初始化时,识别出所有共享参数(通过Python的id()is判断)。

  • 为共享参数分配唯一的参数ID,并在ZeRO分片中只保留一份优化器状态和分片。

  • 前向和反向传播时,所有引用该参数的位置都使用同一个参数分片,通过AllGather获取完整参数时,只会产生一次通信。

  • 保存checkpoint时,DeepSpeed会记录共享关系,合并权重时确保只生成一个张量,不会重复保存。

如果处理不当,会导致训练时梯度更新重复应用,或者checkpoint中权重不一致。DeepSpeed内部对常见共享模式(如HuggingFace的tie_weights())有良好支持,用户通常无需干预。

分析 ZeRO 各阶段对激活值显存没有影响,为什么还需要梯度检查点?

ZeRO的三个阶段只针对模型状态(参数、梯度、优化器)进行分片,完全不涉及激活值(activations)。激活值是前向传播产生的中间张量,用于反向传播计算梯度。其大小由模型结构、序列长度和batch size决定。

在长序列训练或深层网络中,激活值可能成为显存的最大瓶颈。例如,注意力矩阵的显存复杂度为O(L²),对于序列长度8K,注意力矩阵单独就可能占用几十GB。即使ZeRO-3将模型状态压缩到极致,激活值仍然可以轻松撑爆显存。

梯度检查点(Gradient Checkpointing) 则是专门压缩激活值的技术:它在前向时丢弃大部分中间激活,反向时再重新计算。与ZeRO形成正交互补:

  • ZeRO负责降低“参数、梯度、优化器”的存储;

  • 检查点负责降低“激活值”的存储。

只有两者结合,才能在保持模型能力和序列长度的同时,将总显存控制在合理范围。大模型长序列训练的标准配置就是:ZeRO-3 + Gradient Checkpointing + FlashAttention。

ZeRO 与梯度检查点结合,能否实现“1卡训练 13B”?

理论上可以,但需要借助CPU Offload,且速度极慢。

分析13B模型单卡训练显存:

  • FP16参数:26 GB

  • FP16梯度:26 GB(反向传播后暂存)

  • 优化器状态(FP32 m, v, master):156 GB

  • 激活值(通过梯度检查点压缩后):约15~25 GB

单卡80GB A100,仅模型状态(未分片)就达到26+26+156=208GB,远超显存。必须将优化器状态卸载到CPU(ZeRO-Offload),此时GPU只需承担参数26GB + 梯度26GB + 激活值15~25GB,总计67~77GB,勉强在80GB以内。如果再加上一些临时缓冲区和框架开销,极可能OOM,因此实际中往往需要更激进的激活值压缩(如FlashAttention)或降低batch size。

但即使能跑起来,由于优化器更新需要频繁通过PCIe与CPU交换数据,单步训练时间将延长数倍,整体训练时间可能比多卡ZeRO-3方案慢5~10倍。因此,“1卡训练13B”虽然技术上可行,但通常不推荐,除非资源极度受限且对时间不敏感。相较之下,8卡ZeRO-3训练13B就非常舒适,单卡显存仅约30~40GB。

为什么 ZeRO-3 不把激活值也分片?序列并行 (SP) 正好做这件事。

激活值的生命周期和通信模式与参数/梯度截然不同:

  • 生命周期短:激活值在每一层产生并立刻在下一层被消费,不能像参数那样持久分片。

  • 依赖关系复杂:每一层的激活值大小取决于输入,各层之间必须严格串行,分片后需要频繁地收集和释放,通信开销将难以承受。

  • ZeRO的设计哲学:专注于消除数据并行中的“静态”冗余(模型状态),而激活值属于“动态”临时数据,用其他专门策略处理更高效。

序列并行(SP) 正是为激活值分片而生的。它将激活张量的序列维度切分到多卡上,每卡只负责一部分token的计算。在注意力层,通过高效的AllGather或Ring Attention交换K和V,完成后再切回分片。SP通常与TP结合,将通信限制在节点内,利用高带宽NVLink,效率很高。因此,激活值的分片由SP负责,ZeRO专注于模型状态,各司其职,协同工作。

比较 DeepSpeed ZeRO 与 PyTorch FSDP 在实现和性能上的异同。

查看内嵌表格

总结:两者原理相似,DeepSpeed ZeRO在超大模型训练和卸载方面更加成熟,FSDP则作为PyTorch的原生方案,与PyTorch本身的发展紧密绑定,使用更灵活。选择哪一个往往取决于团队的熟悉度和基础设施。许多项目会同时使用两者,例如用DeepSpeed训练,用FSDP加载推理。

FSDP 中的“sharding_strategy”参数与 ZeRO 阶段的对应。

FSDP的sharding_strategy参数控制分片程度,与ZeRO的阶段有直接对应关系:

查看内嵌表格

实际上FSDP没有严格对应的ZeRO-1(只分片优化器状态),但可以通过结合SHARD_GRAD_OP和优化器状态管理来近似。用户可以根据模型大小和集群情况直接选择上述策略。

FSDP 的“full_reshard”和“grad_reshard”的区别。

这两个术语通常指分片策略的细粒度行为:

  • full_reshard:在完成一个micro-batch的前向和反向之后,立即将当前层的参数重新分片(即丢弃临时收集的完整参数),恢复到只有参数分片的状态,以释放显存。这是FSDP在FULL_SHARD模式下的默认行为,也是显存节省的关键。

  • grad_reshard:在反向传播过程中,对每层计算出的梯度立即进行ReduceScatter,分散到各设备,并释放完整梯度。在SHARD_GRAD_OP模式下,梯度会被重新分片,但参数仍保持完整。

简单说,full_reshard既分片参数也分片梯度,grad_reshard只分片梯度。在显存极度紧张时选择full_reshard;在希望平衡通信和显存时可选grad_reshard。

使用 FSDP 训练时,如何通过“limit_all_gathers”优化通信?

limit_all_gathers=True是FSDP中的一个关键优化参数。它的作用是:阻止参数AllGather的重叠执行,从而减少同时驻留在显存中的完整参数数量,降低显存峰值。

工作原理:

  • 在标准FSDP中,当一层正在进行前向计算时,下一层的参数可以提前通过AllGather收集(预取),以隐藏通信延迟。但这会导致同一时刻有多层的完整参数同时驻留在显存中,增加峰值显存。

  • 开启limit_all_gathers后,FSDP将严格控制预取,只允许在当前层计算完成后,才触发下一层的AllGather。这样,同一时刻显存中最多只有当前层的完整参数,峰值显存大幅下降。

效果:显存峰值降低约20%~40%,尤其是对于层数多、激活值大的模型。代价是通信无法完全与计算重叠,可能有微小速度损失。在显存紧张时,此选项非常有效。

在 FSDP 中,什么是“state_dict_type”?如何处理分片状态的保存。

state_dict_type是FSDP中控制如何保存和加载模型状态(state_dict)的配置。它决定了保存的state_dict是分片的还是完整的,以及如何在各rank间协调。

常见选项:

  • StateDictType.FULL_STATE_DICT:将所有rank的参数聚合,保存为一个完整的state_dict文件(通常只在rank 0保存)。这需要一次全局通信,但生成的文件可以直接用于推理或调优。

  • StateDictType.SHARDED_STATE_DICT:每个rank保存自己持有的参数分片,通信最少,保存速度最快。恢复时每个rank加载自己的分片。需要所有rank的目录都存在且一致。

  • StateDictType.LOCAL_STATE_DICT:只保存本地的参数(包括当前未分片的完整参数),用于节点内断点续训等特殊场景。

使用示例:

from torch.distributed.fsdp import FullyShardedDataParallel as FSDP, StateDictType
save_policy = {StateDictType.SHARDED_STATE_DICT}
with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT):
    state = model.state_dict()
    # state 现在包含各rank的分片,需要每个rank保存
    torch.save(state, f"checkpoint_rank_{rank}.pt")

恢复时:

with FSDP.state_dict_type(model, StateDictType.SHARDED_STATE_DICT):
    state = torch.load(f"checkpoint_rank_{rank}.pt")
    model.load_state_dict(state)

如果要将分片状态合并为完整权重,可使用FULL_STATE_DICT或在保存后使用FSDP.summon_full_params手动合并。

FSDP通过这套灵活的state_dict_type机制,让开发者能根据场景(断点续训 vs 模型发布)自由选择高效的保存方式。