ZeRO十年演进:分布式训练显存优化从分片到卸载的完整拆解
大模型训练一年多过一年显存却永远不够用。凡是跑过几十亿参数模型的人八成都被“out of memory”折磨过。明明手里掐着八张卡模型一加载还没开始训练就爆了显存。这一代的分布式训练技术里能实打实解决这个问题的就是ZeRO——一个把显存分配逻辑彻底重做的分布式优化器方案。从它早期提出到现在整个技术脉络已经走了将近十年的演进路程从最初的数据并行优化到后来的全面状态分片、CPU卸载、通信压缩每一代版本背后都对应着训练场景的一次升级。这篇文章不绕弯子直接把ZeRO这十年的技术演进拆开讲透它到底解决了什么问题三个阶段的原理分别是什么每一代版本做了什么以及落到实际训练里配置怎么写、参数怎么调、坑怎么躲。1. 大模型训练里的显存危机ZeRO到底解决什么问题1.1 显存都去哪了把账单算明白要理解ZeRO先得把训练过程里显存的去向算清楚。训练一个模型显存主要消耗在两个地方一是模型状态也就是参数、梯度、优化器状态这三样东西二是中间激活值也就是前向传播过程中每一层计算出来的临时张量。先说模型状态。以Adam优化器为例每训练一个参数需要保存的东西包括参数本身FP16下2字节、参数的FP32副本4字节、梯度FP16下2字节、以及Adam优化器维护的一阶动量m和二阶动量v各自FP32下4字节。把这些加起来单个参数在训练过程中合计占用约16字节。一个100亿参数的模型纯模型状态就需要160GB的显存这还没算中间激活值。一张主流显卡显存在80GB左右单卡训练直接出局就算拿四张卡做普通数据并行每张卡也要保存完整的模型副本总显存需求还是160GB四张卡每张吃40GB依然非常紧张。更麻烦的是这四张卡上存放的是完全相同的参数副本每次反向传播还要做梯度同步意味着每张卡上有一份冗余的模型状态纯粹是重复存储。中间激活值这件事更头疼。它的大小跟批量大小、序列长度、模型深度直接相关在训练长序列大模型时激活值占用的显存甚至会超过模型状态。有一个粗略估算方式单层Transformer的激活值大小约等于批量大小乘以序列长度乘以隐藏维度乘以层数再乘以一个常数实际跑起来经常是几十GB级别的消耗。1.2 普通数据并行为什么掰不动这个局数据并行是分布式训练里最直觉的方案每张卡放一份完整模型副本各吃各的数据前向反向各自计算最后把梯度做一次AllReduce同步。它在小模型时代够用但到了大模型时代就暴露出两个致命问题。第一是存储冗余。每张卡都持有完整的参数、梯度、优化器状态卡越多总冗余越严重。8张卡训练一个10B模型模型状态总量是160GB但被复制了8份总容量需求是1280GB有效利用率只有12.5%。第二是通信瓶颈。每轮迭代都要做梯度AllReduce通信量与模型参数量成正比。模型一大通信时间就会超过计算时间GPU大部分时间在等待梯度同步计算利用率直线下降。1.3 ZeRO的第一性原理消除冗余而不是增加硬件ZeRO的核心思路很朴素既然数据并行是在复制模型状态那不如把模型状态切开每张卡只存一份分片需要用到某个参数时再通过通信把它取回来。这个思路本质上跟集合通信里的AllGather和ReduceScatter是一脉相承的——要减少冗余存储就得用通信换显存。ZeRO把这个思路做成了三个阶段分别对应三条演进路线优化器状态分片、梯度分片、参数分片。三个阶段可以单独启用也可以叠加使用。每个阶段分掉的“冗余”不同显存节省幅度也不同通信开销也不一样这就成了整个技术演进的核心骨架。后面所有版本从ZeRO第一阶段到ZeRO第三阶段再到Offload和通信压缩都是在这条主线上做延展。2. ZeRO三阶段原理拆解从优化器状态到全面分片2.1 ZeRO-1先拿最大的蛋糕开刀三个状态里谁占的显存最大答案是优化器状态。还是以Adam为例单个参数对应FP32的模型副本、一阶动量m、二阶动量v这三项加起来12字节占单个参数显存开销的75%。把优化器状态切开分到各卡上是最具性价比的第一步。ZeRO-1的做法是把优化器状态按参数量平均切成N份每张卡只负责更新自己手里的那部分参数。前向和反向计算流程不变但反向算出的梯度要先做一次ReduceScatter把属于自己的那份梯度汇总到对应的卡上然后每张卡用自己手里的梯度去更新自己负责的参数分片更新完成后做AllGather把完整参数广播回所有卡。通过这套机制优化器状态的显存占用从N份降为1份通信量跟标准数据并行的AllReduce相比略有增加但省下的显存是实打实的大头。2.2 ZeRO-2把梯度也分掉ZeRO-1省掉了优化器状态但每张卡上依然持有完整的梯度和参数副本。梯度虽然只占单个参数4字节但模型一大同样吃显存。ZeRO-2的逻辑是既然ReduceScatter已经把梯度汇总到各自负责的卡上了那不如在汇总的同时把梯度就地分片只保留自己需要的那部分其余的全丢掉。这样做的直接结果是每张卡只需要维护自己参数分片对应的梯度分片梯度存储也从N份降为1份。到了这个阶段模型状态里只剩下参数依然是完整副本但参数在三个状态里占比最小FP16下只有2字节/参数冗余已经不严重了。ZeRO-2在节省显存的同时通信量跟ZeRO-1基本一致没有额外开销所以实际场景里启用率非常高。2.3 ZeRO-3把参数也切片打破单卡墙到了ZeRO-3事情就彻底不一样了。这里不再要求每张卡持有完整参数副本而是把参数也切成N份前向计算时每个算子需要用到哪部分参数就临时AllGather出来用用完就丢。整个训练过程中没有任何一张卡拥有全部模型权重。这意味着模型的存储上限不再受单卡显存约束只受所有卡显存总和约束。8张80GB的卡理论上可以存接近640GB的模型状态训练千亿参数模型在卡数够多的情况下成为可能。代价是通信量大幅上升每一层前向和反向都要做一次参数AllGatherZeRO-3的通信量相比数据并行明显增加。社区里形容这种模式叫“每层都通信”事实上是把AllReduce的通信模式改成了更细粒度的多次AllGather网络延迟立刻成了瓶颈。2.4 三阶段对比一张表看懂选择逻辑对比项ZeRO-1ZeRO-2ZeRO-3分片对象优化器状态优化器状态梯度参数梯度优化器状态显存节省幅度约4倍相对普通DP约8倍相对普通DP约N倍卡数线性扩展通信开销变化略增与ZeRO-1相近明显增加适用模型规模10亿级百亿级千亿级及以上典型启用方式单机多卡单机多卡/多机多机大规模集群选哪个阶段取决于模型规模和硬件条件。我用一句话概括卡少模型小优先ZeRO-2模型大到单卡装不下直接ZeRO-3如果还想进一步压低显存就得考虑把部分计算搬离GPU了这就是下一代的Offload技术。3. 十年演进路线从ZeRO-1到ZeRO每代版本在卷什么3.1 早期探索从模型并行到显存冗余消除ZeRO不是一个凭空冒出来的想法。它的理念源头可以追溯到深度学习分布式训练的早期探索。在ZeRO之前应对大模型的标准方案主要是张量并行和流水线并行。张量并行把一个层的矩阵运算切到多张卡上流水线并行把不同层分配到不同卡上。这两种方案的问题是切分逻辑复杂通信模式固定扩展性受限而且对某些算子比如LayerNorm特别不友好。ZeRO的贡献在于它用“通信换显存”的统一思路把模型并行的显存收益用数据并行的代码框架实现了出来。这一点对工业界极其重要因为它意味着不用改动模型结构就能让普通数据并行代码吃掉模型并行的显存红利。这可以说是分布式训练从“结构改造”转向“系统优化”的分水岭。3.2 ZeRO-Offload让CPU成为第二战场解决显存还有一个思路不用都堆在GPU上把一部分数据挪到CPU内存。ZeRO-Offload的核心做法是把优化器状态和梯度计算放到CPU端执行GPU只负责前向反向计算和参数存储。Adam优化器的更新计算是一个典型的CPU友好任务它不需要很高的带宽但需要大量的显存。把这块挪出去GPU显存压力立刻就下来了。代价是训练速度的下降。CPU计算优化器状态更新比GPU慢很多而且CPU和GPU之间的PCIe数据传输是绕不开的开销。实际经验是这种模式适合显存实在不够但又想跑更大模型的场景可以接受训练速度下降来换取模型规模的提升。后来演进出ZeRO-Infinity更进一步把模型状态卸载到NVMe硬盘上用SSD当显存用原理一样只是存储层级更深瓶颈更明显。3.3 ZeRO通信才是最大的敌人当显存问题逐步缓解后ZeRO演进的下一个矛盾变成了通信。ZeRO-3把参数切成N份导致每个算子都需要AllGather拿参数通信量比普通数据并行高出不少。在千卡以上集群里通信等待时间已经超过计算时间GPU利用率被打到很低。ZeRO的应对策略有三个方向。一是量化通信把AllGather的参数从FP16压缩成INT8传输完再反量化回FP16通信量直接砍半。二是分层通信在NVLink域内先把参数从各卡聚合到节点内再通过IB网络做节点间传输减少跨节点通信次数。三是引入本地计算模式通过ZeRO-CM计算通信重叠把参数通信和反向计算重叠起来让GPU在等待通信的同时继续做计算而不是干等。这三个方向分别对应通信压缩、通信拓扑优化和计算通信流水化成了ZeRO演进后期的主旋律。3.4 演进路线总览版本阶段核心策略主要收益主要瓶颈ZeRO-1/2优化器状态梯度分片显存节省数倍通信开销略增ZeRO-3全状态分片模型规模突破单卡上限通信量显著增加ZeRO-OffloadCPU/NVMe卸载进一步降低GPU显存需求传输带宽受限ZeRO通信量化拓扑优化计算重叠降低跨节点通信压力实现复杂度高4. 实操落地DeepSpeed里写配置、调参数、算显存的完整指南4.1 配置ZeRO的完整写法在工程上ZeRO最常用的落地形态是某深度学习加速框架里的ZeRO实现。跑一个ZeRO-2的训练任务配置写起来并不复杂。核心是在训练配置里加入ZeRO配置段设置阶段等级、分片大小和通信相关参数。一个常用的ZeRO-2配置大概是这样的格式zero_optimization: stage: 2 allgather_partitions: true allgather_bucket_size: 5e8 reduce_scatter: true reduce_bucket_size: 5e8 overlap_comm: true contiguous_gradients: true cpu_offload: false几个关键参数的作用分别是allgather_bucket_size和reduce_bucket_size控制通信张量的打包大小数值越大通信次数越少但临时显存占用越高overlap_comm开启梯度通信与反向计算的重叠能明显提升GPU利用率contiguous_gradients把梯度放在连续内存中便于通信后端高效处理。实际调参时这几个值不是越大越好要根据模型大小和卡数去找平衡点。ZeRO-3需要在配置里把stage改成3同时新增stage3_gather_16bit_weights_on_model_save这类参数控制保存模型时是否重新收集完整权重。这个参数很多人第一次跑会踩坑忘了开启保存出来的模型是不完整的参数分片无法直接用于推理。4.2 显存占用估算公式跑之前先算一笔账配置写得对不对提前用估算公式验证一下显存需求能省去很多试错成本。假设模型参数量为P使用Adam优化器全ZeRO-3加CPU Offload混合模式下的显存需求一般按以下思路估算。GPU端显存主要包括参数分片FP162P/N字节、梯度分片FP162P/N字节、以及临时AllGather缓冲区。CPU端显存包括优化器状态的完整副本FP32参数副本4P 动量8P合计12P字节和完整参数副本FP162P字节。举个例子一个130亿参数的模型在8卡ZeRO-3模式下每卡GPU端需要存储约3.25GB参数加3.25GB梯度临时缓冲区按5亿字节算合计约11.5GB加上激活值后大约20GB。这个容量在80GB显卡上完全没问题。而如果把stage降到1只分片优化器状态每卡就要保存完整的13亿参数和梯度再加上完整模型副本显存压力就差很多了。这类估算公式在工程上非常实用能在改动配置前就知道方案是否可行。4.3 训练速度关键参数调优心得ZeRO配置跑通了不算完性能调优才是见功夫的地方。我在多次实践中总结下来最影响训练速度的几个点依次是通信bucket大小、overlap开关、梯度累积步数、以及CPU offload的占比。通信bucket大小这个东西设小了通信次数会非常频繁小张量传输的网络效率极低GPU利用率能掉到50%以下设大了临时候冲区会占用大量显存在ZeRO-2配大模型时反而会挤占模型存储空间。我的一般经验是单卡显存80GB、模型10B以下bucket取5e8是稳妥的起点模型更大就降到2e8甚至1e8优先保证模型能加载进去。overlap_comm这个开关开启后反向传播计算和梯度通信并行执行理论上能省大量等待时间。但我实测发现它在NVLink环境下的收益明显在跨机IB网络环境下反而会因为通信抢占计算资源造成抖动。跨机训练时我会先关闭它跑一个step对比时间再决定是否开启。梯度累积步数的调整要结合ZeRO的通信模式来看。梯度累积相当于把多个micro-batch的梯度加起来再统一更新这会降低通信频率对ZeRO-3这种通信密集的模式特别友好。但要注意累积步数过大会导致优化器更新频率过低影响收敛效果一般不超过8步。4.4 几个容易踩的配置坑第一个坑是zero_force_dense_cpu_mem这个选项在Offload模式下容易误设设错了会导致CPU内存被模型参数占满直接OOM。第二个坑是混合精度与ZeRO的配合ZeRO分片的是FP16参数但优化器状态更新需要FP32所以开启ZeRO时必须同时开启混合精度否则FP32的Master Weight会重复存储显存反而浪费。第三个坑是checkpoint保存不完整ZeRO-3模式下如果用默认配置保存模型各卡只保存自己的参数分片合并模型时一旦忘记开启权重收集选项就是静默错误跑完几十个小时训练拿到一个缺斤少两的权重文件非常崩溃。5. 常见问题与排查技巧实录5.1 显存OOM先分清是模型状态还是激活值训练中途OOM是最常见的问题但OOM的原因分两类处理方式完全不同。如果是刚启动就OOM那八成是模型状态分配失败优先检查ZeRO阶段配置是否正确、bucket大小是否过大、是否同时开启了Offload和全量模型存储。如果跑了几个step之后才OOM那大概率是激活值爆炸处理方式是减小批量大小、开启激活值检查点activation checkpointing或者梯度累积。我之所有一次把批量大小调到很大跑不到第十个step就OOM了一开始还以为是模型太大后来用激活值统计工具一查原来单个batch的激活值占了40GB。把activation checkpointing打开之后激活值从40GB降到了8GB代价是增加了约30%的前向计算时间但总归是能跑了。5.2 通信卡死网络拓扑和bucket大小的锅ZeRO-3模式下通信次数非常多卡死问题也最频发。我遇到过的卡死场景分两种一种是程序挂起、GPU利用率归零但显存不释放这通常是集合通信等待不匹配导致的最常见的原因是部分卡上的bucket大小不一致或者启用了overlap_comm时通信和计算不同步。另一种是网络拥塞导致的超时常见于跨机场景IB网络配置不对或者交换机拥塞控制策略不合理。排查思路是先看日志里能不能定位到具体卡再看时间戳是否集中在某个AllGather操作附近。定位到通信阶段后先把overlap_comm关掉试跑如果问题消失就说明是通信和计算的竞争问题如果还在再检查所有节点上的配置是否完全一致。一致性是分布式训练的命根子这一点上我栽过跟头。5.3 收敛效果变差分片不该影响精度但会影响种子ZeRO分片改变的是参数的存储位置和更新方式数学上没有改变优化器的更新公式理论上收敛行为和单卡一致。但实践中确实出现过切换ZeRO后收敛曲线变差的情况。我排查下来最可能的原因是开启ZeRO后每个step的计算顺序发生了改变浮点运算的累积顺序不同导致结果有微小差异如果随机种子没有在分片逻辑下正确同步每张卡的数据顺序还会不同相当于全局batch被随机打乱了自然影响收敛。解决办法是确保所有卡上随机种子一致开启seed同步选项同时在切换ZeRO等级后用一小段数据跑几次对比实验确认loss曲线没有明显偏离后再启动全量训练。这不丢人我在这个阶段翻过车训练跑了大半天才发现收敛不对只能重来。5.4 常见问题速查表现象可能原因排查/解决路径启动即OOM模型状态分配失败bucket过大Offload误配降低bucket检查Offload选项用显存估算公式验证中途OOM激活值过大减小batch开启activation checkpointing训练卡死通信等待不匹配跨机网络拥塞关闭overlap_comm检查各卡配置一致性收敛变差随机种子不同步浮点计算顺序改变同步seed小数据对比测试保存的模型不可用ZeRO-3下没有收集完整权重开启stage3_gather_16bit_weights_on_model_saveCPU内存OOMOffload数据超限检查CPU可用内存调整Offload比例6. 这十年走下来的一些实在体会从ZeRO开始只分优化器状态到后来的全分片、卸载、压缩这个技术演进路径反映的是分布式训练核心矛盾的迁移先是显存容量问题然后是通信效率问题再往后是算力利用率问题。每一步演进都踩在真实生产痛点上没有一步是凭空造概念。我个人在实际操作中的体会是ZeRO不是银弹它是一套需要结合硬件和场景来做权衡的方案组合。单机八卡优先考虑ZeRO-2加适当bucket配置卡数多了、模型上百亿了才需要ZeRO-3加通信优化显存实在不够再考虑Offload但一定要接受训练速度下降的现实。没有绝对最优的配置只有最贴合你当前资源状况的配置。最后再分享一个小技巧每换一次ZeRO等级或者每改一次分片配置我都会先用一个小规模实验比如几百万参数的模型跑通验证显存和通信行为再切到全量模型。这一步花不了多少时间但能省下很多浪费在排错上的天数。ZeRO发展到现在配置面板越来越复杂底层逻辑其实还是那句话把状态切开把通信压住让GPU除了算模型就是算模型别让显存和等待卡住你的迭代效率。