ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DeepSpeed ZERO技术解析:大模型分布式训练显存优化实战指南

DeepSpeed ZERO技术解析:大模型分布式训练显存优化实战指南 1. 项目概述大模型时代的效率革命如果你最近在搞大模型相关的项目无论是微调、推理还是应用开发大概率会碰到一个让人头疼的问题显存。动辄几十亿、上百亿参数的模型哪怕只是加载到GPU里看一眼都可能直接把你的显存撑爆。更别提同时训练多个模型或者进行复杂的多任务学习了。传统的分布式训练方法比如数据并行Data Parallelism虽然能把数据分到多张卡上但每张卡上依然要保存一份完整的模型副本。模型一大这招就不好使了。这就是“ZERO”系列技术诞生的背景。它不是什么具体的算法模型而是一套由微软DeepSpeed团队提出的、用于极致优化大规模模型训练内存和效率的分布式训练策略。你可以把它理解为一套“组合拳”专门对付大模型训练中的显存“怪兽”。掌握ZERO意味着你能用有限的硬件资源比如几块消费级显卡去挑战以前需要昂贵计算集群才能完成的任务。这不仅仅是技术上的优化更是成本控制和研发效率的革命。无论是学生、研究者还是工程师只要你的工作与大模型沾边ZERO就是你绕不开的必修课。2. ZERO核心思想与三级策略深度解析ZERO的核心思想非常直观既然完整的模型参数、梯度和优化器状态太占地方那我们就把它们“拆开”分散到不同的GPU上去。通过精密的通信和协调在需要的时候再把它们组合起来。这种“分而治之”的思路直接击中了分布式训练的痛点。ZERO主要分为三个等级ZERO-1, ZERO-2, ZERO-3它们像俄罗斯套娃一样层层递进优化得越来越彻底当然实现的复杂度和通信开销也会相应增加。2.1 ZERO-1优化器状态分区这是入门级也是性价比最高的一级。它的目标很明确干掉最占地方的“元凶”——优化器状态。为什么是优化器状态以最常用的Adam优化器为例。对于每一个模型参数Adam需要维护两个状态一阶动量m和二阶动量v。假设模型有Ψ个参数使用FP16混合精度训练那么参数本身占用2Ψ字节FP16。梯度占用2Ψ字节FP16。Adam状态包括FP32格式的参数副本、一阶动量、二阶动量共3 * 4Ψ 12Ψ字节。计算一下比例12Ψ / (2Ψ 2Ψ 12Ψ) 12Ψ / 16Ψ 75%。看到了吗优化器状态吃掉了高达75%的显存ZERO-1就专门对付它。具体如何操作假设我们有Nd张GPU数据并行维度。在传统数据并行中每张卡都有一份完整的优化器状态。ZERO-1的做法是分区将完整的优化器状态均匀地分割成Nd份。分发每个GPU只保存其中一份。例如GPU0保存第1到第Ψ/Nd个参数的优化器状态GPU1保存下一份以此类推。通信与更新前向和反向传播时每张卡都有完整的参数和梯度与传统DP一样。在优化器更新参数时情况变了。每个GPU只负责更新自己“管辖”的那部分参数。因为它只拥有那部分参数的优化器状态。更新完成后它需要通过“全体收集”All-Gather操作将自己更新好的那部分参数广播给所有其他GPU确保所有GPU上的参数保持一致。效果与权衡显存节省优化器状态的显存占用直接降为原来的1/Nd。这是巨大的提升。通信开销引入了额外的All-Gather通信来同步参数。但通常优化器步骤的计算量不大通信开销相对可控性价比极高。实操心得ZERO-1几乎是所有大模型训练的起点。在DeepSpeed中你只需要在配置文件中将stage设置为1并指定optimizer和scheduler就能轻松启用。它带来的显存收益是立竿见影的而增加的通信成本在大多数网络环境下都是可以接受的。对于很多场景仅用ZERO-1就能让你把模型规模扩大近一倍。2.2 ZERO-2梯度分区解决了优化器状态下一个目标就是梯度。在反向传播结束后每张卡上都会计算出完整的梯度张量。ZERO-2在ZERO-1的基础上进一步对梯度进行分区。工作原理梯度计算与分区反向传播过程中每张卡计算出完整的梯度后立即对其进行分区Nd份然后通过“规约散射”Reduce-Scatter操作。Reduce-Scatter操作这个操作可以理解为两步的合并。首先所有GPU将各自梯度对应的分区进行“规约”Reduce通常是求和得到全局梯度的分区。然后这个结果被“散射”Scatter到对应的GPU上。最终每个GPU只持有全局梯度的一部分。优化器更新由于ZERO-1已经将优化器状态分区每个GPU正好用自己持有的那一份梯度去更新自己持有的那一份优化器状态和参数。参数同步更新后同样通过All-Gather操作同步所有参数。效果与权衡显存节省梯度显存占用也降为原来的1/Nd。结合ZERO-1现在参数和梯度是完整的各占2Ψ字节但优化器状态和梯度都只有1/Nd。显存节省进一步扩大。通信开销引入了Reduce-Scatter操作。与ZERO-1的All-Gather相比Reduce-Scatter的通信量是相同的都是传输Ψ个元素但算法不同。通常现代GPU集群使用NCCL后端对这两种集体操作都有高度优化实际开销增加并不显著。注意事项ZERO-2的通信发生在反向传播结束后这可能会略微增加反向传播阶段的时间。但在整体训练流程中由于显存大幅降低你可能可以使用更大的批量大小batch size来弥补甚至获得更高的吞吐量。需要根据实际硬件和模型进行 profiling 来权衡。2.3 ZERO-3参数分区这是最激进、也是最彻底的优化级别。ZERO-3的思想是既然优化器状态和梯度都分区了为什么不把模型参数本身也分区呢让每张GPU在平时只保存整个模型的1/Nd。工作原理前向传播当某层需要计算时通过All-Gather操作从所有GPU上收集该层所需的完整参数。计算完成后立即释放掉从其他GPU收集来的参数只保留自己负责的那一部分。这被称为“参数卸载”。反向传播与正向类似需要时收集参数计算梯度。梯度计算完成后同样通过Reduce-Scatter操作将梯度分区并分发。优化器更新每个GPU用自己分区的梯度更新自己分区的优化器状态和参数。效果与权衡显存节省达到极致。每张GPU上持久化保存的只有1/Nd的参数 1/Nd的梯度 1/Nd的优化器状态。模型显存占用几乎与GPU数量成线性反比。通信开销急剧增加。因为在前向和反向的每一层都可能需要进行All-Gather和Reduce-Scatter操作。通信频率和总量远高于前两个阶段。计算粒度ZERO-3通常与模型并行Model Parallelism或流水线并行Pipeline Parallelism结合使用以更细的粒度如层内进行参数分区从而减少每次通信的数据量这也就是所谓的“ZERO-Infinity”或“ZERO”所做的进一步优化。踩坑实录ZERO-3虽然省显存但绝不是“无脑开”。通信开销可能成为严重的性能瓶颈尤其是在节点间网络带宽不足的情况下。开启ZERO-3后训练速度可能会显著下降。我们的经验是只有当模型大到连ZERO-2都无法加载时才考虑ZERO-3并且一定要搭配高速互联如NVLink, InfiniBand和细致的性能剖析。3. 实战使用DeepSpeed配置与调优ZERO理论懂了关键还得落地。微软的DeepSpeed库是目前实现和使用ZERO最方便、最强大的工具。下面我们以一个具体的例子看看如何配置和调优。3.1 基础配置与启动假设我们有一个基于Hugging Face Transformers的模型训练脚本train.py。使用DeepSpeed的第一步是创建一个配置文件ds_config.json。一个启用ZERO-1的基础配置{ train_batch_size: 32, gradient_accumulation_steps: 1, fp16: { enabled: true, loss_scale: 0, loss_scale_window: 1000, initial_scale_power: 16 }, zero_optimization: { stage: 1, // 启用ZERO-1 allgather_partitions: true, allgather_bucket_size: 5e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 5e8 }, 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: ZERO的阶段12或3。allgather_bucket_size和reduce_bucket_size: 通信桶大小。将大量小张量的通信聚合成少量大张量的通信能极大提升效率。一般设置为5e8500MB左右是个不错的起点。overlap_comm: 是否重叠通信和计算。开启后在通信进行的同时GPU可以继续做其他计算能有效隐藏通信延迟。强烈建议开启。启动训练的命令也很简单deepspeed --num_gpus4 train.py --deepspeed ds_config.json3.2 进阶调优与参数解读当你需要启用ZERO-2或ZERO-3时配置需要更精细的调整。ZERO-2配置示例zero_optimization: { stage: 2, contiguous_gradients: true, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 5e8, allgather_bucket_size: 5e8, cpu_offload: false // 谨慎开启见下文 }contiguous_gradients: 在反向传播前将梯度缓冲区置为连续内存。这能提升Reduce-Scatter操作的效率建议开启。ZERO-3配置示例zero_optimization: { stage: 3, contiguous_gradients: true, stage3_max_live_parameters: 1e9, stage3_max_reuse_distance: 1e9, stage3_prefetch_bucket_size: 5e8, stage3_param_persistence_threshold: 1e6, overlap_comm: true, reduce_bucket_size: 5e8, allgather_bucket_size: 2e8, // ZERO-3下可以调小一些 cpu_offload: false }ZERO-3的配置参数更为复杂stage3_max_live_parameters: 控制同一时刻驻留在GPU上的参数数量上限。调小可以省显存但可能增加通信次数。stage3_prefetch_bucket_size: 参数预取桶大小。DeepSpeed会尝试在需要参数之前提前发起All-Gather通信与计算重叠。这个参数控制预取的量。stage3_param_persistence_threshold: 参数持久化阈值。小于此大小的参数张量不会被分区/卸载而是常驻在所有GPU上。这对于偏置bias、层归一化LayerNorm参数等小张量很有效能避免为它们付出昂贵的通信代价。关于CPU Offload配置中有一个cpu_offload选项。当设置为true时可以将优化器状态、梯度甚至参数卸载到CPU内存。这能进一步释放GPU显存让你跑起更大的模型。血泪教训CPU Offload是一把双刃剑。虽然显存省了但数据在CPU和GPU之间的传输PCIe带宽会成为巨大瓶颈训练速度可能下降一个数量级。除非你的模型真的巨大无比且训练时间不是首要考虑因素例如只为获取一个预训练权重否则不要轻易开启。我们的原则是优先用尽GPU显存和通信优化最后才考虑CPU Offload。3.3 性能监控与瓶颈分析开启DeepSpeed后如何知道性能瓶颈在哪DeepSpeed提供了丰富的日志和性能分析工具。查看日志在训练命令中加入--deepspeed_config ds_config.json --deepspeed 21 | tee train.log日志中会包含各个阶段的耗时。使用Timeline在配置文件中启用wall_clock_breakdown: true和flops_profiler: {enabled: true}可以生成更详细的时间线分析看清是计算耗时多还是通信耗时多。调整桶大小reduce_bucket_size和allgather_bucket_size是最关键的调优参数。如果通信耗时占比高可以尝试增大它们但不要超过单个张量的最大限制。如果GPU显存利用率低可以尝试减小它们。这是一个需要反复试验的过程。结合NVProf/Nsight Systems对于更深度的性能分析可以结合NVIDIA的性能分析工具查看CUDA Kernel执行和通信操作的具体耗时。4. 常见问题排查与实战技巧在实际部署中你会遇到各种各样的问题。这里记录了一些典型场景和解决方法。4.1 内存溢出OOM问题即使开了ZERO也可能OOM。现象训练刚开始或中途报CUDA out of memory。排查思路检查配置阶段确认stage设置正确。ZERO-3比ZERO-2省显存。检查激活值内存ZERO主要优化参数、梯度、优化器状态。但前向传播中产生的激活值Activations也可能占用大量显存。可以启用激活值检查点Activation Checkpointing或梯度检查点用计算换内存。在DeepSpeed配置中可以通过activation_checkpointing部分配置。检查批量大小train_batch_size是全局批量大小。DeepSpeed会自动根据GPU数量计算每张卡的本地批量大小。如果你手动设置了gradient_accumulation_steps确保train_batch_size per_gpu_batch_size * num_gpus * gradient_accumulation_steps。检查模型大小用torch.cuda.max_memory_allocated()在关键位置打印显存使用定位内存峰值。4.2 训练速度慢或不稳定现象开启ZERO后迭代时间变长或Loss曲线震荡剧烈。排查思路通信瓶颈这是ZERO-2/3最常见的问题。使用wall_clock_breakdown分析时间。如果通信占比过高如30%尝试增大reduce_bucket_size和allgather_bucket_size。确保使用了overlap_comm: true。检查硬件单机多卡确保使用NVLink多机确保使用InfiniBand等高速网络。精度问题FP16混合精度训练可能不稳定特别是当模型中有非常小或非常大的梯度值时。可以尝试使用DeepSpeed的fp16: {loss_scale: 0}动态损失缩放。或者考虑使用BFloat16如果硬件支持其数值范围比FP16更稳健。优化器状态同步在ZERO-1/2下确保优化器步骤后参数同步正确。可以定期检查不同GPU上同一参数的数值是否一致。4.3 保存与加载检查点这是一个容易踩坑的地方。在ZERO-3下模型参数是分区的不能直接用torch.save(model.state_dict(), ...)。正确做法使用DeepSpeed提供的engine.save_checkpoint()和engine.load_checkpoint()API。保存engine.save_checkpoint(save_dir, tag, client_state{...})会以分布式的方式保存所有分区。加载load_checkpoint(save_dir, tag, load_optimizer_statesTrue, load_lr_scheduler_statesTrue)。关键点保存的检查点目录结构是特定的不要手动修改。加载时需要先初始化DeepSpeed引擎再用该引擎加载。4.4 与其它并行策略的混合使用在实际的超大模型训练中ZERO常与模型并行MP、流水线并行PP结合。与模型并行结合DeepSpeed支持自动化的张量并行Tensor Parallelism在配置中通过tensor_parallel: {tp_size: 2}来设置。ZERO负责数据并行维度的内存优化MP负责模型层内的切分。此时ZERO的Nd指的是数据并行组的规模它会自动调整。与流水线并行结合通过pipeline_parallel: {pp_size: 2}启用。流水线并行将模型按层切分到不同GPUZERO则在每个流水线阶段内进行数据并行优化。配置相对复杂需要仔细规划micro-batch和梯度累积步骤。我个人在多次项目中的体会是ZERO不是一个“设置完就忘”的黑盒。它是一套需要你根据具体模型、硬件和数据流进行精细调优的工具集。从ZERO-1开始逐步推进密切监控日志和性能指标小步快跑地调整参数是掌握它的不二法门。当你看到原本需要8张A100才能训练的模型在4张3090上稳定跑起来时那种感觉就是工程师的快乐。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进