
很多人一听说多GPU训练第一反应是把我的模型放到好几张卡上不就行了吗但真正调过的人都知道卡越多坑越多。跑起来吞吐不升反降、显存不足报错、卡间通信卡死这类问题我见过太多次。问题的根源通常不在“模型有多大”而在“你用了什么方式把模型放到多张卡上”。放法决定了通信开销通信开销决定了训练吞吐的天花板。这次我把Hugging Face生态下最常用的四种多GPU并行方案——DP、PP、TP、ZeRO——逐个讲透。每种方案解决什么问题、付出什么代价、落地时容易踩哪些坑我都会结合实操经验展开。这篇文章适合正在用Transformers训练模型、但发现单卡显存或速度不够的人也适合看过一些并行概念、却始终没搞清DDP和DP区别、ZeRO三个Stage到底切了什么的人。看完之后你应该能根据自己手里的GPU数量和模型大小直接选出合适的方案。1. DP和DDP看似只差一个字母性能差出一个数量级很多初学者第一次接触多卡训练用的都是torch.nn.DataParallel也就是DP。后来看到各种博客说要用DDP又不知道两者到底差在哪。这里先把这个基础问题彻底说清楚因为后面对PP、TP、ZeRO的理解都建立在“数据和梯度怎么在卡间流动”上。1.1 DataParallel的硬伤GPU0承载了所有“汇总工作”DP的实现逻辑很直观主进程把模型复制到每一张卡上每次训练把一个batch拆成若干小份分发下去各卡独立前向、反向。问题出在反向之后——每张卡算出的梯度需要汇聚到一起求平均这个汇总是由GPU0统一完成的。也就是说每一步训练GPU0都要把所有卡的梯度收回来、算出平均值、再广播回去。如果模型有10亿参数这个收发量会非常可观。更糟糕的是前向计算时模型输入也要从GPU0分发出去损失也要汇总回GPU0。整张卡的显存被这些临时数据压得很满同时GPU0的计算和通信负载远高于其他卡。实际训练时你会经常看到GPU0显存快满了其他卡还有大量剩余。这种负载不均衡导致DP在多卡场景下的加速比非常差通常4张卡能跑出2.5倍左右的吞吐就已经不错了。这里还要点名一个隐蔽问题DP用的是多线程受Python GIL和CUDA thread竞争影响通信和计算更难重叠。这也是为什么社区一致建议DP只适合用来验证代码逻辑真正训练一律换成DDP。1.2 DDP的梯度同步通信量、bucket与常用配置DDP的全称是DistributedDataParallel它改用多进程方式每个GPU对应一个独立进程每个进程都持有一份完整模型副本。这带来一个关键变化——每张卡各自完成前向和反向梯度算好后不需要把原始梯度发给某个中心节点而是通过All-Reduce操作直接在所有卡之间求出平均梯度。每个进程得到的最终梯度是一致的于是本地更新自己的参数副本即可。All-Reduce要传多少数据以最常见的Ring-AllReduce算法为例假设模型参数量为P梯度是fp32每张卡的通信量大约是2 × P × 4字节。这个系数2来自在环上既能接收邻居数据、也能向后发送数据的过程。对于7B模型一轮梯度同步要传约56GB数据听起来吓人但实际是通过多个通信桶并行执行的而且现代NCCL在NVLink和InfiniBand上的带宽能跑到几十GB/s所以瓶颈并没有想象中那么夸张但也不能忽视。DDP之所以比DP快核心就两点没有了“GPU0单点汇聚”的瓶颈梯度通信被拆成很多小桶和反向计算重叠进行显卡在算梯度的时候通信已经在传上一层算好的梯度了。实际训练时有几个配置直接影响DDP性能我列在这里供参考--gradient_as_bucket_viewTrue可以避免每次反向都重复创建梯度张量减少显存碎片。find_unused_parametersTrue当模型里有参数不参与Loss计算时比如某些辅助头必须开启否则会报错或同步卡死。static_graphTrue如果模型结构固定不变告诉DDP不再检测计算图变化能省下一些额外开销。还有一个容易忽略的点DDP每个进程是独立吃数据的所以batch_size要理解为“单卡batch”。全局有效batch等于单卡batch × GPU数量 × 梯度累积步数。很多人调了半天学习率其实只是没搞清这个换算关系。2. 管线并行PP按层切开后最值得算清楚的一笔账是气泡当模型大到单卡显存放不下的时候DP/DDP就不成立了——DDP要求每张卡都有一份完整模型副本。这时候你有两条路把模型按层切开放在不同GPU上这是管线并行PP把单个计算算子切开放在不同GPU上这是张量并行TP。先看PP。2.1 按层切分后显存压力如何被均摊PP的做法很直观假设模型一共48层Transformer你有4张卡就把第1-12层放到GPU0第13-24层放到GPU1依此类推。每张卡只需要保存自己负责的那一段权重、梯度和优化器状态显存压力瞬间降至原来的四分之一左右。听起来很完美但有一个致命问题数据是顺序流过这些层的。GPU0算完第12层之前GPU1只能干等等GPU0把中间激活传给GPU1GPU1开始算第13-24层这时候GPU0又没事干了。如果只是朴素地一批一批过整个训练过程里大部分GPU都处于闲置状态利用率低到让人崩溃。解决思路是引入微批次。把一个大的batch切分成若干个micro-batch比如一个batch包含32个样本就切成8个micro-batch每个micro-batch包含4个样本。GPU0算完第一个micro-batch就传给GPU1同时立刻开始算第二个micro-batch。这样不同GPU就能像流水线一样重叠工作。2.2 微批次与气泡占比的计算流水线重叠工作后仍然存在“气泡”。所谓气泡就是流水线启动阶段和排空阶段产生的空闲时间。一个经典公式可以帮我们估算气泡占比设P为管线阶段数M为微批次数量气泡率 ≈ (P - 1) / (M P - 1)这里给了我们两个直观结论管道切得越深P越大气泡越大微批次越多M越大气泡越小。假设4张卡切成4段微批次16个气泡率约 (4-1)/(163) ≈ 15.8%如果把微批次增到32个气泡率降到约8.6%。所以PP不是简单“把模型切开就完事”微批次数量要仔细调。但微批次也不是越大越好因为每个micro-batch都会产生中间激活这些激活在显存里要保留到反向计算时使用。micro-batch越多同时存活的激活越多显存压力越大。实践中需要配合梯度检查点gradient checkpointing来降低激活占用或者把micro-batch控制在合理范围。2.3 1F1B调度让前向和反向重叠起来上面讨论的还是“先全部前向、再全部反向”的朴素流水线也就是GPU会把所有micro-batch的前向都跑完才开始反向。这种方案显存峰值很高因为反向需要用到所有micro-batch的中间激活。更高效的做法是1F1B调度即一个micro-batch前向完成后马上开始另一个micro-batch的反向。这种“前向与反向交错执行”的调度能让显存峰值显著下降同时气泡率也和之前公式基本一致。目前主流框架比如Megatron-LM默认就是1F1B或类似变种。我在第一次调PP时踩过一个典型的坑手工切分模型层时忽略Embedding和最后的输出层。有些模型的Embedding和输出头是共享权重的如果这两个部分被分到不同GPU上就不只是通信问题了权重同步会变得非常麻烦甚至导致精度对不上。遇到这种情况建议把共享权重固定在某个GPU上或者用同步机制保证两处参数一致。3. 张量并行TP算子内部分片互联越强越能发挥威力PP是把模型按“层”纵切TP则是把每一层的计算“横切”——同一个线性层算子的权重被切到多张卡上同时计算最后再合并结果。3.1 两个线性层的切分套路以及为什么需要两次All-ReduceTransformer里最核心的计算是线性层Y XW。一个25B模型权重绝大多数都集中在线性层上。TP的基本思路就是这样切分线性层若把权重矩阵按列切开每张卡负责输出维度的其中一部分那么每张卡算出的只是部分输出必须在所有卡之间做一次All-Reduce把部分输出拼成完整输出。若把权重矩阵按行切开每张卡拿到完整的输出维度但是只吃输入的一部分这种情况下前向计算里每个卡也会只处理部分输入同样需要一次All-Reduce来汇总跨卡的部分和。Megatron-LM的经典做法是第一个线性层比如QKV投影按列切分得到的输出不用立刻合并接着做attention计算最后需要输出时再用第二个线性层按行切分来抵消通信。这样每个Transformer block里大约只需要做两次All-Reduce。这个安排不是随便定的是为了让通信次数尽量少同时保证功能完全等价。3.2 通信是TP的生命线NVLink决定了上限TP最大的特点是通信频率极高。普通Transformer里几乎每个线性层都要伴随一次All-Reduce而PP只需要在层边界通信。这意味着TP对卡间带宽的敏感度非常高高到跨机器基本做不了。我在实际项目里测试过一台8卡A100服务器卡间走NVLink带宽约600GB/sTP并行跑起来非常顺一旦尝试把TP组跨到两台机器走的是InfiniBand或RoCE带宽可能降到几十GB/s训练速度直接断崖下跌。原因是TP在每层计算里都有同步点跨机延迟会被放大几十倍。所以TP的使用边界非常明确单机内、并且卡的互联是NVLink或者PCIe Switch这种高带宽低延迟的拓扑才适合TP。如果你手里是多台机器优先考虑PP或者ZeRO把TP范围控制在单机内。3.3 组合并行里的分工DP、PP、TP各管哪一层实际训练超大模型时通常不是只用一种并行技术而是三者组合。经典的分工方式是TP负责单机内的算子切分让模型单卡显存放不下也能跑PP负责跨机/跨卡的层切分进一步降低不同节点间的通信压力DP则把上述并行组复制成多份用来扩大总batch size加速训练。这样做的原因很简单DP通信频率低适合跨机TP通信频率高只放单机PP介于两者之间既跨机又不会太频繁通信。所以3D并行的顺序通常是先TP内部再PP节点间最后DP组间。4. ZeRO把优化器状态拆出去显存焦虑的真正解法再来看ZeRO。说实话现在用Hugging Face训练大多数人首选不是PP/TP而是ZeRO因为它的易用性和显存收益太明显了。4.1 模型状态不止是模型参数Adam的12字节开销要算清楚很多人以为显存大头是模型参数其实训练过程中显存消耗主要来自四部分模型参数、梯度、优化器状态、中间激活。其中优化器状态经常被忽略但它恰恰是大模型训练的显存黑洞。以最常见的AdamW为例混合精度训练下每个参数需要维护的状态包括fp32的master weight4字节一阶动量m4字节二阶动量v4字节加上fp16模型参数本身2字节和梯度2字节一个参数量为P的模型仅模型状态就需要约16P字节。也就是说70B的模型光参数梯度优化器状态就要超过1.1TB显存。这还没算激活和临时buffer。所以70B模型在8卡80G的H100上全参数微调纯靠DDP是不可行的因为8卡总共只有640G远不够。4.2 ZeRO的三个Stage分别切掉了什么ZeRO的出发点很聪明DDP里每张卡都保存一份完整的参数、梯度和优化器状态但训练时它们不一定需要同时存在。于是它把“模型状态”按三种维度切分对应三个StageZeRO-1只切优化器状态。每张卡只保存一部分优化器状态梯度算完后需要做Reduce-Scatter把梯度分片同步到对应卡上。显存约降到原来的1/4不算激活时。ZeRO-2切优化器状态 梯度。反向计算时梯度不再每张卡都存全量而是按卡分片存。显存进一步降到1/8左右。ZeRO-3切优化器状态 梯度 参数。前向/反向时每层参数用All-Gather临时广播到所有卡上用完再丢弃。显存随卡数线性下降。为了直观我列个表ZeRO Stage切掉内容每个参数的理论显存开销需要的主要通信原语Stage 1优化器状态约22P/N相关优化器状态Reduce-ScatterStage 2优化器状态 梯度约2少量梯度Reduce-ScatterStage 3优化器状态 梯度 参数约2 每层临时参数All-Gather Reduce-Scatter注意通信量并不因为切了东西变小。Stage 2相比DDP通信次数一样只是每次传的数据从“全量梯度”变成了“分片后的梯度”。Stage 3则更糟前向要All-Gather参数反向要All-Gather梯度再来Reduce-Scatter更新分片通信量大约是DDP的1.5倍。4.3 ZeRO-3慢在哪以及FSDP为什么还在流行很多人开了ZeRO-3之后发现训练速度骤降原因就在这里每层计算前都要All-Gather一次完整权重如果模型有50层一个micro-batch就要发起50次以上集合通信。通信延迟成为主要瓶颈尤其是在小batch和高微批次数量下。PyTorch原生的FSDPFully Sharded Data Parallel底层思想和ZeRO-3一致但FSDP做了一些更细的优化比如参数预抓取、按层手动切分、通信与计算重叠等。所以现在很多项目选择FSDP而不是DeepSpeed ZeRO-3不是FSDP显存省得更多而是它在通信重叠上更容易调到理想状态。我的建议是单机8卡以内、卡间带宽好优先用ZeRO-2显存实在不够、或卡数多到需要用ZeRO-3/FSDP时再切过去并且一定要配合开启梯度检查点否则中间激活会先把你卡爆。另外ZeRO还有一个Offload的变体把优化器状态和梯度放到CPU内存GPU只留参数和计算流。这适合单卡显存小但CPU内存充足的机器。缺点是CPU与GPU之间PCIe拷贝会成为瓶颈训练速度会下降但至少能让你在16G消费级显卡上跑7B模型的全参数微调。5. Hugging Face生态里的落地姿势Trainer、DeepSpeed与实际选型原理讲完落到Hugging Face里到底怎么操作这个更贴近日常。5.1 Trainer与Accelerate最省心的DDP入口Hugging Face的Trainer已经内置了DDP支持。只要用torchrun启动并设置--num_processes或直接用Accelerate插件Trainer就会自动把训练逻辑切成多进程。一个最简起动命令是torchrun --nproc_per_node4 train.py \ --model_name_or_path meta-llama/Llama-2-7b-chat-hf \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --output_dir ./output配合Accelerate更省心。accelerate config可以交互式设置分布式参数它会自动处理RANK、LOCAL_RANK、WORLD_SIZE这些环境变量。很多新人在裸写DDP时报错80%是环境变量没设对用Accelerate能直接规避。注意一点Trainer的--per_device_train_batch_size是单卡batch不是总batch。我在项目里常看到有人以为设成8就是总batch结果8卡变成总batch 64学习率跟着就炸了。建议先算清楚总batchper_device_batch × num_gpus × grad_accum。5.2 DeepSpeed配置实战一个能直接跑的JSONTransformers Trainer对DeepSpeed的支持很成熟只要传入一个DeepSpeed配置文件比如ds_config.json{ zero_optimization: { stage: 2, offload_optimizer: { device: cpu, pin_memory: true }, allgather_partitions: true, allgather_bucket_size: 5e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 5e8, contiguous_gradients: true }, gradient_accumulation_steps: 8, gradient_clipping: 1.0, train_batch_size: 64, train_micro_batch_size_per_gpu: 4, fp16: { enabled: true, auto_cast: true, loss_scale: 0, initial_scale_power: 16 } }然后在启动命令里挂上deepspeed --num_gpus4 train.py \ --deepspeed ds_config.json \ --model_name_or_path meta-llama/Llama-2-7b-chat-hf \ --per_device_train_batch_size 4这里面三个batch字段一定不能写错。train_batch_size是总batch必须等于train_micro_batch_size_per_gpu × GPU数 × 梯度累积。很多人DFS某个字段没对齐会莫名其妙的OOM或者收敛异常。另外如果模型里有不参与Loss的额外参数DeepSpeed配置里要加zero_allow_untested_optimizer: true否则会拒绝启动。5.3 PP和TP在HF生态的现状需要借助Megatron不得不承认Hugging Face Trainer对PP和TP的集成远不如ZeRO那么顺手。Trainer本身没有把Megatron-LM的TP切分逻辑完整封装进去要真正跑起PPTP主流路线是用NVIDIA的Megatron-LM或NeMo框架做模型切分或者用transformers加载已切分好的模型权重比如Hugging Face Hub上很多大模型权重本身就是按TP切好发布的或者借助llama-recipes这类专门为LLaMA优化的工具。如果你只是想快速对比效果可以在Trainer之外配合pipeline_parallel模块手动切分但工程复杂度会上升一个量级。我的观点是小团队微调大模型能用ZeRO/FSDP解决就别上PP/TP否则调试成本和维护成本都很高。PP/TP更适合超大模型预训练场景也就是你明确知道自己要跑数十B乃至上百B参数的那种情况。5.4 一张表看懂选型逻辑我自己在实践里基本按下面这个决策逻辑走你的情况推荐方案原因单卡放得下模型想加快速度DDPTrainer默认最简单加速比接近线性单卡放不下但有8卡单机显存够ZeRO-2 / FSDP通信量低显存省得多稳定单卡放不下卡也不多4卡显存很紧ZeRO-3 梯度检查点最大限度压显存模型几十B以上跨多节点PP TP DPMegatron3D并行是确定性出路消费级显卡只想微调ZeRO-Offload或QLoRA显存和速度折中这个表省去了很多纠结。记住一句话显存不够先开ZeRO通信太慢再调并行度别一上来就上最高配置。关于加速比还有一个经验判断如果你8卡训练比4卡快了不到1.5倍先怀疑通信占据主要瓶颈用nvidia-smi观察卡间通信是否长期跑满或者用NCCL的NCCL_DEBUGINFO日志看每个集合通信的平均耗时。多数情况是batch size太小、梯度同步频繁导致的。把梯度累积调整到合适范围、开启overlap_comm通常能立竿见影。从我个人的实操感受来说多GPU训练选型最忌讳的是“贵就是好全都要上”。大部分开源模型用单机8卡配合DDP或者ZeRO-2加上混合精度和梯度检查点就能跑得非常顺。真正需要把PP/TP/ZeRO全部组合起来的场景至少是百亿参数以上普通项目千万不要轻易碰。最后再分享一个实用技巧无论你选哪种并行方案在正式跑长训练之前先用一个很小的数据子集、跑二三十个step同时盯住nvidia-smi的显存和GPU利用率。如果显存曲线稳定、利用率高、日志里没有NCCL超时警告再放心跑全量数据。这个“最小冒烟测试”能帮你提前暴露大多数分布式配置问题省下的时间远超那几分钟测试开销。多GPU训练的核心从来不是方案听起来有多先进而是它能在你的实际硬件上稳定跑出应有的吞吐。