ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式训练并行策略全解析:DDP、ZeRO、张量、流水线、上下文

分布式训练并行策略全解析:DDP、ZeRO、张量、流水线、上下文 1. 分布式训练到底在解决什么问题先说个最直观的感受。单卡训练大模型就像一个人搬砖砖多了你只能一趟一趟跑。分布式训练的本质不是“把砖分给更多人搬”这么简单而是要解决三件事显存放不下、计算来不及、通信太昂贵。这三件事对应到技术选型上就是并行策略的分水岭。很多人第一次接触分布式训练容易被一堆名词绕晕DDP、DeepSpeed ZeRO、张量并行、流水线并行、上下文并行。它们听起来都是“并行”但解决的问题完全不同。DDP和ZeRO解决的是显存和通信的平衡问题张量并行解决的是单层计算超出一个GPU显存的问题流水线并行解决的是把网络切段后如何让GPU别闲着的问题上下文并行则是大模型序列特别长时针对KV Cache和注意力计算的专门优化。这篇文章按照我在实际项目中踩坑的顺序来梳理先从最基础的DDP讲起再引入ZeRO对显存分配的优化然后分别拆解张量并行、流水线并行各自的适用场景最后聊上下文并行——这是处理超长序列时最容易被忽略但收益极高的一种并行方式。整个过程会配合具体数值、配置示例和我在训练过程中遇到的真实问题尽量做到看完能直接上手选型。我这里先给出一张整体的选型对照表后面每个章节都会展开细说并行方式解决的核心问题适用场景主要代价DDP多卡数据并行加速显存够放下模型追求吞吐梯度通信开销随卡数增长ZeRO显存不够放不下完整模型单卡显存放不下但想用数据并行增加通信量换取显存张量并行单层过大放不进单卡单卡显存放不下单层权重每层都需要通信通信频率极高流水线并行网络过深导致层切分层数多且张量并行收益下降流水线气泡导致GPU空转上下文并行序列过长导致注意力KV爆炸超长序列输入需要特定的注意力实现配合这张表是我在实际做方案选型时反复对照的。下面逐个展开。2. DDP分布式训练的入门地基2.1 DDP的核心原理和为什么它能扩展DDP全程是Distributed Data Parallel分布式数据并行。它的设计思路非常朴素每个GPU上都放一份完整的模型副本把训练数据切成多份分给不同GPU每个GPU独立做前向和反向计算算完之后把梯度做一次全局同步再各自更新参数。这个方案的优点在于实现简单、计算效率高。因为每个GPU上的计算完全独立除了梯度同步那一刻其余时间都在并行工作理论上加速比非常线性。我曾经用4卡A100训练一个7B规模的模型DDP的加速比能做到3.7倍到3.9倍通信开销占比不到百分之十这个效率在工业场景里已经很理想了。DDP能扩展的关键在于梯度通信使用了Ring AllReduce算法。这个算法的聪明之处在于它把梯度同步拆成了scatter-reduce和all-gather两个阶段每个GPU只和相邻的GPU通信通信总量不随GPU数量增加而爆炸而是基本恒定。这保证了DDP在8卡、16卡甚至更大规模下依然有不错的扩展性。但DDP有一个硬伤每张卡都要放一份完整的模型、优化器状态和梯度。以7B模型为例参数用FP16存储约14GB梯度也是14GBAdam优化器状态是FP32的参数、一阶动量、二阶动量每项都需要28GB加起来总共大约141484112GB。单卡显存低于120GB基本跑不动这就是DDP在超大模型面前的失效点。2.2 DDP的实操配置与踩坑记录DDP在PyTorch里实现非常成熟底层封装了NCCL通信几个关键步骤就能跑起来import torch import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group(backendnccl) torch.cuda.set_device(local_rank) model DDP(model, device_ids[local_rank]) # 数据加载时设置 train_sampler torch.utils.data.distributed.DistributedSampler(train_dataset) dataloader DataLoader(train_dataset, samplertrain_sampler, batch_sizeper_gpu_batch) # 每轮epoch需要设置sampler的epoch保证shuffle生效这里有一个很容易踩的坑DistributedSampler如果不设置set_epoch每个epoch数据的shuffle顺序是完全一样的模型会反复看到相同顺序的数据训练效果会变差。我当时排查了很久发现验证集loss震荡不降最后定位到就是这个原因。另一个坑是batch_size的语义。DDP下每个进程的batch_size是单卡batch全局batch等于单卡batch乘以卡数。很多人在调整学习率时搞混了这个概念。经验法则是全局batch翻倍学习率也要相应调大但具体倍数需要根据优化器和模型规模微调。提示DDP只适合模型参数梯度优化器状态能塞进单卡显存的场景。如果显存差一点点可以考虑梯度累积或者混合精度训练但如果差太多就需要下面说的ZeRO了。3. ZeRO把显存分配变成一门艺术3.1 ZeRO的三个阶段和显存优化原理ZeRO是DeepSpeed提出的显存优化方案全称是Zero Redundancy Optimizer。它的核心思想我在实战中理解成一句话把原本每张卡都重复存一份的数据拆开分到不同卡上用的时候再聚合。ZeRO分三个阶段每个阶段砍掉一部分冗余第一阶段把优化器状态分片到各卡。7B模型的Adam状态占84GB分到8卡后每卡只存10.5GB。这一阶段通信量基本不变因为通信的内容仍然是梯度只是参数更新时每卡只更新自己那部分分片所以性价比极高。第二阶段再把梯度分片。原本每卡需要存14GB完整梯度分片后每卡只存1.75GB。梯度分片会引入额外的通信因为每卡在反向传播时只保留自己负责的那部分梯度其他部分在AllReduce过程中就被丢弃了。第三阶段把模型参数也分片。这是最彻底的一步每卡只存自己负责的那部分参数。但在前向和反向计算时需要用到全部参数因此每层计算前都要做一次广播拉取参数。三个阶段通信量的对比我直接给数据以7B模型、8卡为例单卡显存占用从最初的112GB可以降到大约28GB但通信量相对DDP分别增加了约1.5倍、2倍和2.5倍。所以ZeRO是一个典型的用通信换显存的方案不是无代价的。3.2 ZeRO Stage 2和3的选型与配置实战DeepSpeed在配置ZeRO时只需要改一个json文件{ zero_optimization: { stage: 2, offload_optimizer: { device: cpu, pin_memory: true }, overlap_comm: true, contiguous_gradients: true } }这里有一个非常实用的经验在单机8卡场景ZeRO Stage 2通常是最优选择因为通信开销小、显存节省明显。只有跨多机或单卡显存实在紧张到需要把参数也分片时才优先考虑Stage 3。我实际测过一个13B模型在8张80GB A100上的表现Stage 2能跑但单卡显存占用超过75GB余量太小。换成Stage 3后显存占用降到50GB左右但每步训练时间大约增加了百分之二十。如果追求训练吞吐可以加一个参数overlap_comm: true让通信和计算重叠能把这部分损失拉回一半以上。还有一个小技巧当模型小但batch想开很大时可以试一下zero_optimization里的reduce_bucket_size和allgather_bucket_size参数默认值是5e8单位是字节。增大这个值比如到2e9能减少通信次数对训练吞吐有正向帮助但会小幅增加显存峰值需要取舍。注意很多人误以为ZeRO Stage 3可以无限扩展卡数。实际上当卡数超过64张后通信瓶颈会非常明显这时候更合适的做法是结合张量并行降通信量。这一点在下一节展开。4. 张量并行把一个大层切开4.1 为什么需要张量并行和DDP、ZeRO的区别在哪当单卡连一个transformer层都放不下时ZeRO也无能为力因为ZeRO解决的是“多卡各存一份”的冗余问题但模型单层本身就超出单卡显存时必须把层内部的计算切分到多卡。这种切分方式就是张量并行核心思想是把矩阵乘按行或按列拆分到不同GPU上。以Transformer里的MLP层为例假设hidden_size是8192FFN中间维度是32768权重矩阵大约是8192乘32768。如果单卡放不下这个矩阵就把权重按列切成两份每份是8192乘16384分别放在两张卡上。前向时输入各送一份到两张卡各自做矩阵乘再拼接结果。这个操作在Megatron-LM里的实现叫Column Parallel。对应地如果输入维度太大可以按行切分权重每张卡得到部分输出再做AllReduce求和这叫Row Parallel。实际使用中通常第一层用Column Parallel第二层用Row Parallel配合起来还能省掉一次全量通信。张量并行和DDP、ZeRO最大的区别在于通信频率。DDP每步只同步一次梯度ZeRO每层通信几次参数张量并行则是在每一层的每个矩阵乘里都要做通信。因此张量并行对通信延迟极其敏感只适合机内NVLink这类高带宽低延迟的互联环境。跨机器做张量并行性能会非常难看。4.2 张量并行的切分维度和实际部署建议张量并行里最关键的参数是切分维度。以4卡为例张量并行度设为4那么每个隐藏层被切成4份每张卡只做四分之一的矩阵乘。切分后两个问题会出现一是每层的LayerNorm和Dropout需要对全局结果做计算因此需要一次AllReduce来聚合二是在Transformer的注意力层QKV线性投影也可以按列切分但多头注意力中不同头的计算天然独立所以注意力头的分配可以专门优化。Megatron里的做法是让张量并行度整除注意力头数这样每个GPU负责部分头避免了不必要的跨卡通信。实际操作中我建议遵循几个原则。第一张量并行度一般不要超过8因为通信量随切分数量上升很快超过8后单步延迟会急剧增加。第二张通量并行度需要能整除hidden_size和头数否则只能padding参数浪费显存。第三张量并行通常和ZeRO搭配使用但要避免冲突最稳妥的组合是“张量并行 ZeRO Stage 1”因为Stage 2以上的梯度分片和张量并行的gradient reduce逻辑需要仔细对齐容易出错。提示张量并行适合模型超大、单层放不下的场景。如果你用Megatron框架它会自动处理好切分和通信但你要理解每个GPU上实际存储的权重形状是原权重的1/tp_size否则调试shapes时会一脸懵。5. 流水线并行切段的艺术与气泡难题5.1 流水线并行的核心思想与气泡问题如果模型层数非常深张量并行已经不能进一步降低显存占用那么把网络的层切成多段、分配到不同GPU上就轮到流水线并行了。它和前两者的区别在于DDP、ZeRO是数据维度上的并行张量并行是层内的并行流水线并行是层间的并行。最简单的流水线实现是F-then-B模式先算完所有层的前向再算所有层的反向。这样每个GPU有一段时间在等前面的GPU算完这就是流水线气泡Bubble。气泡比例大约等于(p-1)/(pn)其中p是流水线级数n是micro-batch数量。当p8、n16时气泡大约占百分之三十非常浪费算力。为了降低气泡GPipe和PipeDream提出了不同的方案。GPipe把大batch切成多个micro-batch让后几级GPU提前开始计算气泡显著降低。PipeDream更进一步提出1F1B模式让前向和反向交错执行。在1F1B模式下气泡率大幅降低这也是当时为什么说它比GPipe更实用。5.2 流水线并行的配置策略和性能调优经验流水线并行的配置主要看两个维度流水线级数和每个GPU上的micro-batch数量。级数通常等于GPU数量除以张量并行度比如16卡、张量并行4流水线级数就是4。micro-batch数量则取决于显存余量越大气泡越小但显存占用越高。我建议的经验值是每个流水线段内至少4到8个micro-batch否则气泡大。另外如果模型层数不是流水线级数的整倍数需要处理“层不整除”的问题多出来的层放在最后一段或者通过修改embedding的切分方式避免通信量过大的不均衡。一个容易被忽略的细节是不同段的计算负载可能不均衡。比如前几层是embedding和浅层attention相对轻量中间层FFN计算更重。负载不均衡会拖慢整条流水线的节奏。这时候可以考虑调整层分配比例让重负载的段少分几层轻负载的段多分几层虽然实现起来麻烦收益却不小。注意流水线并行的反向传播中中间激活值的存储策略非常关键。GPipe为了省显存默认不存所有层的中间激活重计算只提一次但1F1B模式下反向传播时前后层状态交错显存管理复杂度上升建议直接用框架默认的重计算策略不要自己瞎调。6. 上下文并行超长序列的隐藏推手6.1 上下文并行解决的独特瓶颈KV Cache与注意力计算大模型的输入序列越来越长从早期的2K、4K一路做到128K、1M。序列长了之后即使参数不多注意力计算也会遇到显存和计算瓶颈。这里说的不是参数显存而是KV Cache和注意力分数矩阵。序列长度为L注意力头数为H每个头的维度为D那么注意力分数矩阵的形状是H × L × L。当L128K时L×L接近1.6万兆任何一个GPU都装不下。这是上下文并行存在的根本原因它的作用是沿着序列长度维度切分把不同序列片段分给不同GPU计算各自的注意力分数再通过通信得到完整的注意力输出。需要一个特殊技巧配合标准的注意力实现里每个query需要和所有key做点积如果query在GPU A、key在GPU B就必须把key从GPU B传到GPU A通信成本随序列长度线性增长。那篇著名的FlashAttention论文和后来的Ring Attention研究核心思路是把KV分块流转让每个GPU轮流持有不同块的KV计算完一块就传给下一个GPU这样通信量只随序列长度线性增长而非平方增长。6.2 上下文并行的实现方案与工程注意事项上下文并行在实际工程里的实现路线主要有两条。一条是sliding window的注意力近似只在窗口内计算注意力完全避免全局KV传输代价是损失长距离依赖建模能力。另一条是全量注意力的Ring Attention/环形传输精度无损但工程复杂度高。我实测下来在现代大模型训练中如果序列长度超过32K上下并行的收益就很可观了。以8卡训练128K序列为例采用Ring Attention后单卡KV Cache占用降到原来的八分之一注意力部分的FLOPs也能利用多卡并行消掉一部分训练一个step的时间能缩短百分之二十到三十。工程上有几个注意事项。第一上下文并行度一般小于等于张量并行度否则会破坏注意力头分布的局部性。第二需要在注意力实现里开启attn_mask的切分版否则每张卡都在算完整序列的mask显存直接爆炸。第三序列并行需要在batch内做动态padding否则长短不一的样本会让计算极度不均匀这一点在超长序列场景下尤其明显。提示很多训练框架比如Megatron和DeepSpeed已经原生支持了上下文并行相关的序列并行实现。如果你的框架不支持可以拆分为“张量切分KV环形传输”自己写一个简化版但建议先确认序列长度是否真的有这个必要否则增加的美观复杂度很可能换来一堆通信bug。7. 主流框架与工具选型分析聊完四种并行方式项目落地时最常见的疑问就是该用PyTorch原生的DDP还是DeepSpeed还是Megatron-LM我按场景分一下。如果模型在几十亿参数级别、单卡能塞下、主要追求速度和简单用PyTorch原生的DDP加AMP混合精度就够了这套组合最简单也最稳定。如果模型上百亿参数单机多卡显存不够我首选DeepSpeed ZeRO Stage 2或3配合offload和重计算工程成熟度很高。如果模型进入千亿参数量级并且团队有分布式系统经验这时候Megatron-LM的张量并行加流水线并行是主流路线。现在很多大厂训练方案都是Megatron和DeepSpeed混合使用用Megatron管理张量并行和流水线并行用DeepSpeed的ZeRO做优化器状态分片和通信优化。打个比方这就像装修房子Megatron负责主体结构DeepSpeed负责水电和软装各管一段。选型时另外一个维度是易用性。DeepSpeed用起来相对无脑这个json配置一改就完事Megatron则需要改模型定义和训练循环学习曲线陡峭不少。但Megatron对通信的精细控制好性能天花板也更高。还要注意框架间的兼容性。比如DeepSpeed在ZeRO Stage 3下和PyTorch的torch.compile存在一些兼容问题我试过几次都报奇怪的CUDA错误后来关掉torch.compile才正常。所以选型时不要只看单点性能要考虑整条链路的稳定性。8. 并行策略的组合套路与实战配置示例几十亿参数用DDP几百亿用ZeRO千亿级别张量并行加流水线并行。但真实项目中往往不是选一种而是要组合使用。我以一个130B模型、8卡A100 80GB、训练序列长度4K的典型配置为例给出一套经过验证的组合方案。首先是并行策略分配。8卡全部参与训练张量并行度设为4流水线并行度设为2ZeRO阶段选1上下文并行不开。这样一来模型参数被4路张量并行切分层被切成2段做流水线优化器状态用ZeRO Stage 1全局分片整体单卡显存占用可以控制在65GB左右。对应的DeepSpeed配置文件长这样{ train_batch_size: 32, gradient_accumulation_steps: 4, train_micro_batch_size_per_gpu: 4, zero_optimization: { stage: 1, reduce_bucket_size: 2e8 }, tensor_parallel: { enabled: true, tensor_parallel_size: 4 }, pipeline_parallel: { enabled: true, pipeline_parallel_size: 2 } }这里的train_batch_size32是全局batch由micro-batch 4、梯度累积4、数据并行不额外扩大得到。如果后续要增加batch优先调大梯度累积而不是单卡micro-batch这样可以降低显存峰值。组合使用的顺序建议是先开张量并行到单层能放下再开流水线并行到模型能分段放下最后用ZeRO进一步压显存。不要反过来因为ZeRO的通信量在高并行度下增长快先切分参数能减少通信总量。还有一点心得调试组合并行时不要一上来就上完整配置。先用单卡跑通逻辑再逐步开张量、流水线、ZeRO每开一个验证一次数值一致性。这样做能快速定位问题避免了几个并行策略叠加后翻车、完全不知道是哪一环出错的局面。9. 常见问题排查与避坑指南9.1 显存分配与通信相关的典型问题训练大模型翻车百分之八十的问题出在显存和通信上。我把我遇到过的高频问题列成了一张速查表现象可能原因排查思路单卡显存超出但报错含糊ZeRO配置未生效或重计算未开检查zero stage是否正确加载开启activation_checkpointing训练速度越来越慢通信量和计算量不匹配某卡的AllReduce阻塞用NCCL的ncclDebug打开日志看卡间通信耗时张量并行后loss不下降切分维度不对权重初始化没有对齐验证前向输出和单卡一致对比几个中间tensor流水线并行时某卡显存异常高层分配不均匀导致负载倾斜计算各段的激活显存重新分配层数上下文并行后KV Cache仍溢出未启用KV分块或mask未切分检查是否开启了Ring Attention模式确认mask形状梯度数值异常NaN/Inf混合精度下loss scaling失效检查grad_scaler状态必要时改用BF16训练这里我想重点讲两个我个人体会特别深的坑。第一个是很多人在开ZeRO Stage 2时忘了同时开启contiguous_gradients导致梯度通信时频繁分配和释放显存碎片训练过程中显存峰值忽高忽低最后很容易OOM。开启这个参数后梯度会先拷贝到连续缓冲区通信效率明显提升显存也稳定了。第二个坑是流水线并行下梯度累积的同步问题。如果全局batch算错了比如梯度累积步数设错会导致每一步更新时梯度没有完全累积模型参数一直在小幅抖动。排查这个问题的办法很简单把梯度累积设为1跑几个step看loss是否正常下降再逐步加回去。9.2 关于NCCL通信环境的那点事NCCL在分布式训练里承担了全部GPU间通信。很多看起来像模型代码的bug根源其实是NCCL通信环境没配好。第一次跑多机训练时我遇到过卡间通信极慢、速度只有单机的一半不到。排查后发现是NCCL_SOCKET_IFNAME没设置NCCL走了管理网络而不是高速数据网络。解决办法是在启动脚本里加export NCCL_SOCKET_IFNAMEeth0 # 换成实际高速网卡名 export NCCL_IB_DISABLE0 # 确保IB/RoCE网络开启 export NCCL_DEBUGINFO # 定位阶段可开线上关闭还有一类典型问题是AllReduce卡死。通常是因为某张卡提前退出了训练循环其他卡还在等它的梯度。这种情况多半是数据加载不均匀或者某卡异常退出。建议在每个step的日志里打印local_rank和step一旦卡死能快速定位是哪张卡的问题。重要多机多卡训练时NVLink和PCIe的带宽差异极大一定要确认GPU间通信走的是NVLink/IB否则再好的并行策略也会被通信拖垮。用nvidia-smi topo -m可以查看卡间拓扑。10. 基于个人经验的分布式训练选型心法如果在训练前能想清楚模型规模、单卡显存、卡间带宽这三件事选型就成功了一大半。我习惯按下面几条经验快速决策。第一条小模型、单机多卡、显存充足直接用DDP加AMP不加任何额外复杂度。很多人一上来就上ZeRO结果训练速度慢了、代码复杂度上升收益却几乎没有纯属自找麻烦。第二条中等规模模型、显存吃紧先开启重计算和混合精度无脑试一下ZeRO Stage 2。如果这个组合能跑通性能大概率是最优解。第三条超大模型、多机多卡张量并行和流水线并行是绕不开的。建议这样排列优先级先通过张量并行把单层缩小再用流水线并行把层数分配开最后用ZeRO Stage 1压优化器状态。这是我在多次试验后一个比较稳定好用的顺序。每次训练任务开始我也会先做一个五到十分钟的连通性测试用一个小模型跑几十个step对比单卡和分布式下的loss曲线。只要曲线能重合基本可以确定并行策略没有数值问题。这个习惯帮我避开了很多后期才发现的大坑。分布式训练这个领域无脑追求新方案并不高明关键是理解自己手上资源和模型的约束再针对性组合。希望这篇文章能帮你少走一些弯路。
返回列表