尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ZeRO技术深度解析:如何通过参数分片优化大模型分布式训练显存

ZeRO技术深度解析:如何通过参数分片优化大模型分布式训练显存 1. 项目概述从单卡到多卡的效率瓶颈与ZeRO的破局思路搞深度学习模型训练的朋友尤其是最近在折腾大语言模型或者多模态大模型的肯定对“显存不够”这四个字深恶痛绝。模型参数动辄百亿、千亿哪怕用上了最新的旗舰显卡单卡显存也很快见底。分布式训练自然就成了必由之路但传统的并行策略比如数据并行只是把数据分到不同卡上每张卡依然要保存一份完整的模型参数和优化器状态显存浪费严重模型并行虽然能把模型切分开但通信开销和编程复杂度又让人头疼。这时候微软开源的ZeROZero Redundancy Optimizer技术就进入了我们的视野。它本质上不是一种全新的并行方式而是一套基于参数分片Parameter Sharding的深度优化器状态与梯度管理策略。我第一次接触ZeRO是在尝试训练一个参数量超过30B的模型时当时用传统数据并行8张A10080G都跑得磕磕绊绊显存利用率极低。换上ZeRO之后同样的硬件不仅能跑起来训练速度还有了明显提升。简单来说ZeRO的核心思想就是“消除冗余”它把原本在每个GPU上都完整保存的优化器状态、梯度和模型参数巧妙地切分到不同的GPU上每个GPU只负责维护其中一部分从而将显存占用近乎线性地降低让我们能用更少的卡训练更大的模型。这篇文章我就结合自己踩过的坑和实战经验来深度拆解一下ZeRO。我们会聊清楚它到底是怎么工作的三种不同的分片阶段Stage 1, 2, 3分别解决了什么问题在实际部署时该如何选择和配置以及那些官方文档里不会写的“坑”和调优技巧。无论你是刚入门分布式训练的新手还是正在为显存瓶颈发愁的老手相信都能从中找到有用的信息。2. ZeRO核心原理深度拆解分而治之的显存艺术要理解ZeRO必须先从传统数据并行的显存浪费说起。在标准的数据并行中假设我们有N张GPU。每张GPU上都加载了一份完整的模型副本参数前向传播时各自计算一部分数据一个mini-batch的损失反向传播时各自计算得到一份针对完整模型参数的梯度。之后通过All-Reduce操作通常是求和平均将所有GPU上的梯度同步每张卡都得到一份全局一致的梯度。最后每张卡用自己的优化器如Adam和这份全局梯度独立更新自己那份完整的模型参数。这个过程存在巨大的显存冗余模型参数Parameters存储了N份完全相同的副本。梯度Gradients在反向传播后、All-Reduce前每张卡也存了一份针对完整参数的梯度又是N份。优化器状态Optimizer States这是大头。以常用的Adam优化器为例它需要为每个参数维护动量momentum和方差variance两个状态大小通常是参数量的2倍如果用混合精度可能还有一份FP32的模型参数副本。这部分显存在每张卡上也是完整保存的。ZeRO的智慧就在于它通过分片彻底消除了这些冗余。它定义了三个渐进式的优化阶段Stage每个阶段分片不同的内容以达到不同的显存节省效果。2.1 ZeRO Stage 1分片优化器状态P_{os}这是ZeRO的入门阶段也是性价比最高的阶段。它的做法非常直观既然优化器状态占了大头那就把它切分开。具体实现原理假设我们有Nd张GPU。ZeRO会将整个模型的优化器状态比如Adam的动量和方差均匀地分割成Nd个分片。GPU 0负责维护第0个分片对应的所有参数的优化器状态GPU 1负责第1个分片以此类推。注意模型参数和梯度在每张卡上依然是完整保存的。工作流程前向传播每张卡用完整的模型参数副本独立计算自己分配到的数据。反向传播每张卡计算得到完整的梯度。梯度同步与参数更新这里就是关键。系统不再进行传统的All-Reduce来同步完整的梯度。取而代之的是一种称为Reduce-Scatter的操作。Reduce-Scatter可以理解为先Reduce规约再Scatter散射。首先所有GPU将自己计算出的完整梯度进行按位相加Reduce得到一个全局的完整梯度。然后这个全局梯度被均匀地切分成Nd块每块被散射Scatter到对应的GPU上。例如负责优化器状态分片0的GPU会收到全局梯度中对应分片0的那部分。每个GPU在收到属于自己的那部分全局梯度后就用自己维护的那部分优化器状态例如分片0的动量和方差来更新自己负责的那部分模型参数。参数同步由于每张卡只更新了一部分参数为了进行下一轮前向计算需要让所有卡都拿到更新后的完整参数。这时通过All-Gather操作完成。每张卡把自己更新后的那部分参数广播出去并从其他所有卡收集其余部分的参数最终每张卡又重建了一份完整的、已更新的模型参数。显存节省分析优化器状态被分到了Nd张卡上因此每张卡上的优化器状态显存占用减少为原来的1/Nd。模型参数和梯度依然是完整的所以这两部分没有节省。对于Adam优化器假设参数量为Ψ混合精度训练下优化器状态FP32的参数、动量、方差大小约为12Ψ字节。分片后每张卡的优化器状态显存从12Ψ降至12Ψ / Nd。这是一个巨大的提升通信开销仅比传统数据并行增加了一次Reduce-Scatter和一次All-Gather通常是可以接受的。注意在实际使用DeepSpeedZeRO的主流实现框架时Stage 1的配置非常简单但务必理解其背后的通信模式变化。它不再是一个简单的“包装器”而是改变了训练的数据流。2.2 ZeRO Stage 2分片优化器状态与梯度P_{osg}Stage 2在Stage 1的基础上更进一步除了优化器状态梯度也被分片存储。具体实现原理梯度分片与优化器状态分片是对齐的。也就是说负责优化器状态分片k的GPU也负责存储全局梯度中分片k的部分。工作流程前向传播不变每张卡有完整参数。反向传播这里出现变化。在反向传播计算梯度的过程中每计算完一层网络的梯度系统就会立即执行一次Reduce-Scatter操作。这次操作的目标不是完整的梯度而是当前这一层参数的梯度。通过这种方式每层参数的梯度在计算完成后就被规约并分片存储到了对应的GPU上。这个过程被称为“梯度分区”或“梯度切片”。参数更新由于每张卡已经拥有了它负责的那部分梯度分片以及对应的优化器状态分片因此可以直接进行参数更新。这一步是本地操作无需额外通信。参数同步和Stage 1一样通过All-Gather操作同步更新后的完整参数。显存节省分析梯度也被分片存储因此每张卡上的梯度显存占用也从完整的Ψ假设梯度精度与参数一致降低到了Ψ / Nd。对于大模型梯度显存也是一笔不小的开销例如100B参数的模型FP16梯度就需要约200GB显存。Stage 2在Stage 1节省优化器状态的基础上进一步节省了梯度显存。通信开销与Stage 1类似但反向传播中的Reduce-Scatter操作更频繁每层一次可能会对性能有细微影响但通常仍远优于显存不足带来的换页开销。2.3 ZeRO Stage 3分片优化器状态、梯度与模型参数P_{osgp}Stage 3是ZeRO的完全体也是最激进的模式。它将模型参数本身也进行分片。每张GPU只保存整个模型参数的一个分片。具体实现原理模型参数的分片也与优化器状态、梯度的分片对齐。GPU k只保存模型参数分片k以及对应的优化器状态分片k和梯度分片k。工作流程前向传播由于每张卡只有部分参数要进行计算就必须先获取其他参数。这通过All-Gather操作实现。在计算某一层之前所有GPU通过All-Gather临时重建该层的完整参数。计算完成后立即释放掉从其他GPU收集来的参数只保留自己分片的部分。这个过程被称为“参数卸载”。反向传播与Stage 2类似计算梯度并立即进行Reduce-Scatter将梯度分片存储。同样在计算需要用到非本地参数时会通过All-Gather临时获取。参数更新本地进行无需通信。参数同步由于参数本身就是分片存储的且更新是本地化的因此Stage 3在参数更新后不需要进行All-Gather来同步完整参数。每张卡已经持有了自己负责的那部分最新参数。只有在需要 checkpoint保存模型或者进行推理时才需要通过Gather操作将分散的参数收集起来。显存节省分析这是显存节省的终极形态。模型参数、梯度、优化器状态的显存占用都降到了原来的1/Nd。理论上可以用Nd张GPU来训练一个原本需要Nd倍单卡显存的模型。但代价是通信开销的显著增加前向和反向传播中几乎每一层都需要进行All-Gather和Reduce-Scatter操作通信量巨大。三种阶段的对比总结特性传统数据并行ZeRO Stage 1 (P_os)ZeRO Stage 2 (P_osg)ZeRO Stage 3 (P_osgp)模型参数存储每卡完整副本每卡完整副本每卡完整副本每卡仅存储分片梯度存储每卡完整副本每卡完整副本每卡仅存储分片每卡仅存储分片优化器状态存储每卡完整副本每卡仅存储分片每卡仅存储分片每卡仅存储分片显存节省程度无优化器状态降至 1/Nd优化器状态梯度降至 1/Nd全部参数梯度状态降至 1/Nd通信开销每步一次All-Reduce每步一次Reduce-Scatter 一次All-Gather反向传播中每层一次Reduce-Scatter 每步一次All-Gather前向/反向中每层都可能需要All-Gather/Reduce-Scatter适用场景模型较小显存充足优化器状态是瓶颈如Adam大模型梯度和优化器状态都是瓶颈极大模型显存是绝对瓶颈愿意用通信换显存3. 基于DeepSpeed的ZeRO实战配置与调优理解了原理我们来看怎么用。目前ZeRO最成熟、最流行的实现是微软的DeepSpeed库。它与PyTorch和Hugging Face Transformers集成得非常好。下面我以Transformers训练脚本为例分享具体的配置和调优经验。3.1 基础环境搭建与依赖安装首先确保你的环境有PyTorch、CUDA。然后安装DeepSpeed。建议使用pip安装并指定与你的CUDA版本对应的版本。# 例如对于CUDA 11.x pip install deepspeed # 或者从源码安装以获得最新特性 # pip install githttps://github.com/microsoft/DeepSpeedDeepSpeed的训练需要通过一个配置文件ds_config.json来驱动。这个文件定义了ZeRO的阶段、各种参数和优化设置。3.2 ZeRO Stage 1 2 配置详解Stage 1和2的配置相对简单也是大多数场景下的首选。下面是一个典型的ds_config.json示例{ train_batch_size: auto, train_micro_batch_size_per_gpu: 4, gradient_accumulation_steps: auto, zero_optimization: { stage: 1, // 或 2 allgather_partitions: true, allgather_bucket_size: 5e8, // 500MB通信桶大小 overlap_comm: true, // 重叠通信与计算 reduce_scatter: true, reduce_bucket_size: 5e8, // 500MB contiguous_gradients: true // 连续梯度内存提升效率 }, fp16: { enabled: true, loss_scale: 0, loss_scale_window: 1000, initial_scale_power: 16, hysteresis: 2, min_loss_scale: 1 }, optimizer: { type: AdamW, params: { lr: 5e-5, betas: [0.9, 0.999], eps: 1e-8, weight_decay: 0.01 } }, scheduler: { type: WarmupLR, params: { warmup_min_lr: 0, warmup_max_lr: 5e-5, warmup_num_steps: 1000 } } }关键参数解析与调优经验stage: 设为1或2。对于大多数10B~100B参数量的模型Stage 2是甜点。它在显存节省和通信开销之间取得了很好的平衡。除非你的模型非常小否则不建议只用Stage 1因为Stage 2节省的梯度显存非常可观而额外开销很小。allgather_bucket_size和reduce_bucket_size: 这是ZeRO通信的核心调优参数。DeepSpeed将梯度/参数分组到“桶”中进行通信。桶大小直接影响通信效率。值太小通信次数频繁启动开销大。值太大单次通信数据量大可能导致内存峰值过高且延迟长。经验值通常设置在2e8到1e9之间即200MB到1GB。可以从5e8500MB开始尝试。监控GPU显存使用情况如果发现显存占用有异常的周期性峰值可能是桶设置过大。在NVIDIA A100/H100等高性能互联NVLink, NVSwitch的机器上可以适当调大。在普通PCIe互联的机器上建议保守一些。overlap_comm:务必设置为true。这允许通信All-Gather/Reduce-Scatter与计算反向传播重叠进行是提升训练吞吐量的关键优化。DeepSpeed会智能地将通信操作插入到反向计算图中尽可能隐藏通信延迟。contiguous_gradients:建议设置为true。它会将梯度在内存中连续存储减少内存碎片提升Reduce-Scatter操作的效率。对于大多数模型都有正向收益。train_micro_batch_size_per_gpu: 这是每张GPU每次前向-反向计算处理的样本数。这是决定显存占用的最主要因素。你需要根据单卡显存在开启ZeRO后通过尝试找到这个值的上限。通常从一个很小的值如1或2开始逐步增加直到GPU显存使用率达到80%-90%。gradient_accumulation_steps: 梯度累积步数。当micro_batch_size * gradient_accumulation_steps等于你想要的全局有效批次大小时训练最稳定。DeepSpeed可以设置为auto它会根据其他设置自动计算。启动命令示例deepspeed --num_gpus8 your_training_script.py \ --deepspeed ds_config.json \ --other_training_args ...3.3 ZeRO Stage 3 进阶配置与“踩坑”指南Stage 3的配置更为复杂调优不当极易导致性能反而下降。以下是一个强化版的Stage 3配置示例{ zero_optimization: { stage: 3, offload_optimizer: { device: cpu, // 可选项将优化器状态卸载到CPU内存 pin_memory: true }, offload_param: { device: cpu, // 可选项将参数分片卸载到CPU内存 pin_memory: true }, overlap_comm: true, contiguous_gradients: true, sub_group_size: 1e9, // 参数All-Gather的子组大小 reduce_bucket_size: auto, stage3_prefetch_bucket_size: auto, stage3_param_persistence_threshold: auto, stage3_max_live_parameters: 1e9, stage3_max_reuse_distance: 1e9, stage3_gather_16bit_weights_on_model_save: true // 保存模型时收集FP16权重 }, fp16: { enabled: true, loss_scale: 0, initial_scale_power: 16 } }Stage 3专属调优参数与“避坑”心得offload_optimizer和offload_param: 这是Stage 3的“大杀器”也是“双刃剑”。它们允许将优化器状态和参数分片卸载到CPU内存从而进一步突破GPU显存限制实现“用CPU内存换GPU显存”。device: cpu: 启用CPU卸载。pin_memory: true: 使用锁页内存可以加速CPU到GPU的数据传输强烈建议开启。代价数据在CPU和GPU之间频繁移动会带来巨大的PCIe带宽开销可能严重拖慢训练速度。仅当GPU显存完全无法容纳模型时例如在消费级显卡上尝试超大模型才考虑使用。在服务器级多卡环境下优先通过增加GPU数量来解决问题而不是依赖卸载。stage3_prefetch_bucket_size:这是Stage 3最重要的性能调优参数之一。它控制“参数预取”的桶大小。在Stage 3中为了隐藏从其他GPU收集参数的通信延迟DeepSpeed会尝试在计算需要用到某些参数之前就提前发起All-Gather请求去获取它们。调大此值预取更积极可能更好地隐藏通信延迟。调小此值预取更保守降低内存峰值。建议从默认值或auto开始通过观察训练吞吐量和GPU显存波动来调整。如果训练速度慢且GPU利用率低等待通信可以尝试逐步调大。如果出现OOM内存溢出则需调小。stage3_param_persistence_threshold: 参数持久化阈值。有些参数例如嵌入层会被频繁访问。这个参数单位是元素数量指定如果一个参数张量的元素数量小于此阈值它就不会被分片而是持久化保存在每张GPU上。这可以减少对微小参数的频繁通信。经验对于像词表嵌入vocab_embedding这样的大层将其排除在分片之外可能有益。可以尝试将其设置为嵌入层参数的大小。但需要测试因为这会增加每卡的显存占用。stage3_gather_16bit_weights_on_model_save:务必设为true。否则保存的checkpoint只会包含当前GPU上的参数分片而不是完整的模型导致无法用于推理或恢复训练。Stage 3使用警告在我个人的多次实践中Stage 3的训练吞吐量Throughput samples/sec通常只有Stage 2的50%甚至更低尤其是在网络带宽受限或模型层数很多、参数很分散的情况下。决策流程应该是先尝试Stage 2如果显存仍然不足考虑增加GPU数量如果硬件受限再考虑Stage 3如果Stage 3仍不足最后才考虑启用CPU卸载。4. 实战问题排查与性能优化技巧即使配置正确在实际运行中也可能遇到各种问题。这里记录几个我踩过的典型深坑和解决方法。4.1 常见错误与解决方案速查表问题现象可能原因排查步骤与解决方案训练启动即报OOM内存不足1.micro_batch_size设置过大。2. ZeRO配置未生效。3. 模型本身有非参数的大缓存。1. 将micro_batch_size设为1确保能启动。2. 检查DeepSpeed日志确认ZeRO已初始化查找ZERO_OPTIMIZATION日志行。3. 检查模型代码是否有大的缓冲区如Attention的K/V缓存未纳入ZeRO管理。对于自定义层可能需要用deepspeed.zero.Init()上下文管理器初始化。训练中途随机OOM1. ZeRO通信桶bucket_size设置过大。2. Stage 3的预取桶prefetch_bucket_size过大。3. 激活值Activations内存过高。1. 逐步减小reduce_bucket_size和allgather_bucket_size如从5e8降到2e8。2. 在Stage 3中减小stage3_prefetch_bucket_size。3. 考虑启用激活检查点Gradient Checkpointing这能大幅降低激活内存代价是增加约30%的计算量。在Transformers中通常对模型设置model.gradient_checkpointing_enable()。训练速度极慢GPU利用率低1. 通信成为瓶颈特别是Stage 3。2. CPU卸载导致PCIe带宽瓶颈。3.overlap_comm未生效或效果差。1. 使用nvidia-smi或dcgm监控GPU利用率和网络带宽。如果利用率低且带宽吃满则是通信瓶颈。考虑换用Stage 2或优化网络使用InfiniBand。2. 如果启用了offload_param或offload_optimizer尝试禁用看速度是否飞升。如果是说明硬件不适合CPU卸载。3. 确保overlap_comm: true。对于Stage 3尝试调整sub_group_size和prefetch参数。保存的模型无法加载1. Stage 3下保存的是分片未自动聚合。2. Checkpoint文件不完整。1. 确保配置中stage3_gather_16bit_weights_on_model_save: true。2. 使用DeepSpeed提供的zero_to_fp32.py脚本将分片checkpoint合并成完整模型python zero_to_fp32.py ./checkpoint_dir ./full_model.bin。Loss变为NaN或训练不稳定1. FP16混合精度训练下梯度溢出。2. 学习率过高。3. 梯度累积步数设置不当。1. DeepSpeed的FP16配置中loss_scale: 0表示启用动态损失缩放Dynamic Loss Scaling这是最稳定的。观察日志中是否有[deepscale] OVERFLOW!提示它会在溢出时跳过更新并调整scale。2. 适当降低学习率。3. 确保gradient_accumulation_steps与micro_batch_size的乘积是一个稳定的有效批次大小。4.2 性能监控与调优实战纸上得来终觉浅调优必须靠监控。以下是我常用的工具链DeepSpeed内置日志在ds_config.json中增加wall_clock_breakdown: true和flops_profiler: { enabled: true, profile_step: 10 }。运行后DeepSpeed会输出详细的时间分解前向、反向、优化器、通信各占多少时间和FLOPS分析精准定位瓶颈。系统监控GPUnvidia-smi -l 1实时查看显存、利用率、功耗。网络对于多机训练使用iftop或nethogs监控节点间网络流量。ZeRO Stage 3的通信压力很大网络必须足够快建议100Gb InfiniBand。一个关键的隐藏参数steps_per_print在DeepSpeed配置的scheduler同级可以设置steps_per_print: 10。这会让DeepSpeed每隔10步打印一次详细的性能数据包括时间开销、损失缩放因子等对于初期调试非常有帮助。4.3 与Transformers Trainer的无缝集成如果你使用Hugging Face的Trainer集成DeepSpeed非常简单无需大幅修改训练循环。准备好ds_config.json文件。在启动命令中加上--deepspeed ds_config.json。确保在训练脚本中模型通过deepspeed.zero.Init()上下文或在Trainer内正确初始化。一个常见陷阱Trainer会自动处理数据加载和设备放置。但在使用ZeRO Stage 3时如果自定义了模型结构尤其是自己写的层必须确保这些层的参数是在DeepSpeed初始化之后创建的或者用deepspeed.zero.Init()包裹否则这些参数不会被ZeRO管理导致显存泄漏。我曾在自定义的位置编码层上栽过跟头就是因为这个原因。最后关于硬件选择虽然ZeRO的设计目标是降低单卡显存需求但这并不意味着用消费级显卡如RTX 4090就能轻松训练千亿模型。Stage 3带来的通信开销在PCIe Gen4 x16的带宽下会成为不可承受之重。ZeRO真正发挥威力的场景是在拥有高速互联NVLink/NVSwitch的多张数据中心级GPU如A100/H100集群上。在这种情况下ZeRO能让你将宝贵的HBM显存用于存储更大的模型和批次数据而不是冗余的状态从而最大化硬件利用率。理解其原理合理选择阶段细致调优参数才能让这项强大的技术真正为你的大模型训练赋能。
返回列表