ARTICLE DETAIL

资讯详情

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

模型装下了GPU却在等?流水线并行气泡与调优实战

模型装下了GPU却在等?流水线并行气泡与调优实战 前阵子帮朋友调一个70B模型的训练脚本。他花了两周总算用流水线并行把模型塞进了8张A100兴冲冲一跑监控面板上的GPU利用率只有30%出头。他跑来找我第一句话就是模型明明装下了为什么GPU还在等这个问题我太熟悉了。Pipeline Parallel流水线并行解决的是大模型装不下的问题可它同时也带来一个隐形代价——气泡bubble和等待。模型装下了只是万里长征第一步能不能让每张卡都忙起来才是真正决定训练吞吐的地方。这篇分享不是基础教程而是围绕“装下了却在等”这件事把流水线并行的原理、调度、坑和调优方法一次性说透。不管你是刚把第一个PP任务跑通还是已经被低利用率折磨到怀疑网卡都建议读完再动手。1. 先把Pipeline Parallel这件事重新梳理一遍1.1 当单卡放不下时为什么你会选PP大模型分布式训练基本有四条路数据并行、ZeRO/FSDP、张量并行Tensor Parallel、流水线并行Pipeline Parallel。数据并行最简单但每张卡都要有一整份参数、梯度和优化器状态ZeRO和FSDP通过切分状态来省显存适合单机多卡和部分跨机场景张量并行把单个层的计算拆到多卡通信密集必须依赖NVLink这类高速互联流水线并行按层把模型切成多段每张卡只负责其中一段通信量很小是可以跨节点跑大模型的关键手段。PP最大的优势就是“显存省、通信压力小、能跨机”所以几乎所有百亿千亿级模型训练的最后一步都会把PP请进来。但请进来之后很多人会发现一个诡异的现象模型确实跑起来了显存也没炸可GPU利用率就是上不去训练吞吐远低于预期。问题不在显存而在时间维度。PP天然会制造一种等待一段数据经过第1个stage计算后第2个stage才能开始第3个stage还要更晚。于是一条流水线上总是有卡在“等别人算完”。这种等待有个专门的名字——bubble气泡。气泡越少流水线越饱满GPU利用率越高气泡越多哪怕显存绰绰有余监控上也是大片空闲。这就是“装下了却在等”最核心的原因。接下来要弄明白的问题是气泡到底有多大怎么算以及怎么压。1.2 模型装下了不等于每张卡都在干活很多人把“模型装下了”和“训练高效”混为一谈。实际上显存满足和计算吞吐是两件独立的事。显存只要放得下参数、梯度和激活值就能跑但计算吞吐要求每一张卡在每一个时钟周期里都有活可干。PP的切分方式决定了它天然存在空闲窗口——前后阶段之间的依赖关系是硬性的后面的stage必须等前面的stage把activation传过来才能开工。这也是为什么PP的调优难度比数据并行高一个数量级。数据并行只要网络不差、负载基本均匀所有卡基本都在忙PP却要把“调度顺序”“micro-batch数量”“通信开销”“负载分布”几件事同时处理好才能真正把卡喂饱。所以当你看到GPU利用率低的时候第一步不是加机器而是先搞清楚你的GPU到底在等什么。2. 气泡不是玄学手把手推导气泡率2.1 一次只喂一个batch的最坏情况为了建立直觉先看最朴素的做法模型切成P个stage每次只处理一个batch不考虑micro-batch也不做梯度累积。设一个batch过一个stage的forward加backward时间为T。那么stage1算完这个batch需要T接着stage2开始再花掉一个T直到stage P算完总时间是P乘T。但整个过程中每个设备只在属于自己的那一段T里是忙碌的其他P减1个T都在等。利用率约等于1除以P。P等于8时利用率只有12.5%左右。也就是说在朴素实现下你的GPU有87.5%的时间在空转。这个数字听起来夸张但它就是很多人在刚接触PP时跑出来的真实数据——因为默认配置下M可能很小甚至等于1。2.2 引入micro-batch后气泡率怎么算工业流水线解决空转的办法是让产线上同时存在多个在制品。训练里对应的做法就是把一个batch切成M个micro-batch一个接一个灌进流水线。此时关键路径如下从第一个micro-batch进入到最后一个micro-batch离开共经历M个micro-batch乘P个stage的时间。理想情况下第一个micro-batch会在P乘T后走完全程之后每隔T就有一个micro-batch完成总时间等于M加P减1乘T。总的工作量是M乘P乘T每个micro-batch都要经过P个stage。空闲时间等于总时间减忙碌时间也就是P减1乘T。所以气泡率的公式是气泡率 (P - 1) / (M P - 1)注意看这个公式的极限M越大气泡率越低但如果M等于1气泡率就是P减1除以P跟我们上面推导的最坏情况一致。M等于32、P等于8时气泡率约等于18%M等于64时约10%。这就是为什么所有PP框架都要求你把batch切成尽量多的micro-batch不只是为了梯度累积更根本的是为了填满流水线。这里我把forward加backward的时间简化成等长的T实际backward往往比forward更耗时所以真实气泡率会有偏差但方向性完全一致。理解这个公式之后你就能明白一个道理PP的气泡是结构性存在的无法靠堆机器消除只能通过调度和参数去压缩。2.3 但micro-batch不是想切多细就切多细micro-batch切得越小能灌进去的M就越多气泡越少。但每个micro-batch照样有forward和backward太小的micro-batch会让kernel launch、通信的开销占比变大而且梯度累积会让参数更新的频率变低如果global batch size不变那M往往取决于batch size和显存。更现实的问题是activation会随着micro-batch数量线性增长。如果你的显存只允许M等于8而P等于8气泡率接近47%利用率很难看。于是就有了“要么加显存、要么减模型、要么上更聪明的调度”的三角制约。这也是为什么后面要花大篇幅讲调度——不同调度策略本质上都是在同一个三角形里找更好的平衡点。3. 同一个气泡不同调度策略的差别在哪3.1 GPipe最直觉但显存是硬伤GPipe是早期经典方案做法分两步先让所有micro-batch依次完成forward把activation全部存下来再统一进入backward阶段。这样气泡率的公式和我们上面推导的一致逻辑清晰理解起来很容易。但它的显存问题很致命每个stage上要保存所有micro-batch的activationM越大显存越容易爆。所以GPipe在实践中往往M不能太大气泡压不下来前段和后段的利用率也难看。它更适合用来理解原理不太适合直接作为现代大模型训练的主调度。如果你在某个老旧框架里看到类似“先全部forward再全部backward”的行为大概率就是GPipe风格调度这时候调参能起的作用非常有限更值得做的是换调度实现。3.2 1F1B为什么要一个前向紧跟一个反向1F1BOne-Forward-One-Backward是目前Megatron-LM等框架的默认调度。和GPipe最大的区别是它不等到所有forward结束再统一backward而是在流水线进入稳定状态后每执行一次forward就尽量执行一次backward。这样带来的好处有两个方面。第一activation的存活时间大幅缩短。一个micro-batch的backward算完后它对应的activation就能释放显存峰值远低于GPipe。换句话说同样的显存容量下1F1B可以塞更多的micro-batch进而压气泡。第二流水线能更早进入稳定状态前中后段设备的空闲分布更均匀。可能有人会问1F1B的气泡率不是和GPipe差不多吗确实在等时长假设下量级接近但工程上它把“尖峰显存”变成了“平均显存”显存效率更高实际能撑住的M更大因此真实吞吐通常明显优于GPipe。如果你是第一次从零搭PP优先选带1F1B调度的框架别再用GPipe的简化实现。3.3 Interleaved pipeline用通信次数换气泡Interleaved pipeline也叫virtual pipeline是1F1B的进阶版。思路是把每个stage再切成更小的virtual stage让同一张物理卡交替处理多个virtual stage。这样流水线里的“节拍”更细气泡更少。代价是p2p通信次数成倍增加对网络的延迟和带宽更敏感。我在实测中得到的经验是在单机NVLink环境下interleaved可以把bubble从20%压到5%左右整体吞吐能提升10%到15%。但换成跨机以太网通信开销增长可能直接把收益吃掉甚至变成负优化。所以一句话总结网络好开网络差关。具体阈值没有统一标准建议先用profiler量一下通信占比超过20%就别硬开。3.4 Zero Bubble气泡也能被“填”起来Zero Bubble零气泡的思路更激进。它的核心洞察是bubble期间GPU并不是完全不能干活只是没有“当前设备负责的micro-batch”可做。于是研究者在调度里把注意力、MLP这类子模块的backward拆开在原本空闲的窗口里插入一些不依赖当前micro-batch的计算最终让气泡接近零。这个思路学术上很漂亮但对框架的依赖调度要求非常高基本不是自己改框架就很难直接用。它的适用场景是大规模研究型训练普通团队还是先老老实实把micro-batch和interleaved调到位再考虑这种进阶方案。至少从我接触过的生产环境来看Zero Bubble目前还不是一个开箱即用的选项。3.5 调度方案怎么选一张表说清楚表格对比一下几种主流调度方案的核心差异方便你按自己的场景快速做判断。调度方式气泡率理想峰值显存通信次数工程实现适用场景GPipe(P-1)/(MP-1)高少简单教学、早期方案1F1B同量级中少中等生产默认推荐Interleaved显著降低中多中等NVLink等高速互联环境Zero Bubble接近0中多且特殊高大规模研究型训练需要说明的是表格里的气泡率是理想模型下的近似值真实环境还会受通信延迟、负载不均衡和框架实现质量的影响。选调度方案的时候不要只看理论值要结合自己的网络拓扑和显存余量做取舍。4. 为什么模型装下了GPU还在等除了气泡还有三个隐形元凶4.1 负载不均衡切层不是切蛋糕气泡是PP的“系统性疾病”负载不均衡则是“个别人的病”。你把一个80层的模型均匀地切成8段每段10层然后以为每段耗时差不多太天真了。不同层的计算量差异非常明显Embedding通常只是查表计算密度低Attention的矩阵乘法规模小一些MLP是计算大头LayerNorm和RMSNorm又是一类轻量操作。此外共享embedding、tied weight、MoE中的expert层都会让某些stage比其他stage重得多。如果某张卡被分到了几个重型层它算得慢前面算完的卡只能等它整体训练速度被拉低。你看到的现象就是某张卡利用率90%其他卡只有50%。这种情况下你要做的是重新切层而不是继续调micro-batch。可以用profiler把每个stage的compute time打出来看看方差是不是过大如果某个stage明显偏慢尝试调整模型切分点把重型层和轻型层搭配分布。4.2 通信等待p2p请求堆积卡就等着PP的p2p通信量确实不大但“不大”也要看网络条件。跨机场景里如果你用的是千兆或万兆以太网一个micro-batch的activation可能有几十MB甚至更大传输耗时可能高到和单stage计算时间一个量级。而且TP和PP经常叠加使用TP内部的all-reduce频率很高和PP的p2p请求挤在同一个NIC或PCIe带宽里往往互相拖慢。如果你用torch profiler看到时间线上有大量空隙且空隙里是recv相关操作大概率就是通信在等。建议优先检查NCCL用的是不是RDMA或者是不是和别的任务共享了网卡。很多“跨机跑得慢”的案例最后都定位到网卡协商失败走了TCP fallback速度直接掉一个数量级。4.3 计算和通信没有重叠也是隐形等待很多PP实现并没有把p2p通信和计算做深度重叠。比如forward算完后才调用send下一个recv的数据要等很久或者backward的梯度要等整个micro-batch算完才发起传输。现代框架Megatron-LM、DeepSpeed做了不少优化比如提前预取、拆分张量的chunk传输、用CUDA stream实现overlap。但如果你是自己写的流水线或者框架版本比较老很可能没有这些优化。检查方法很简单看同一时刻GPU的SM利用率是不是明显低于100%同时NIC或内存拷贝在用。如果计算没吃满网络也没用满大概率就是重叠没做好。这种情况下与其调这调那不如换成成熟框架的PP实现或者手动把send和recv挪到计算之前让它们在CUDA stream上并行。5. 实操记录一个可复现的Pipeline Parallel调优流程5.1 先确认你是不是真的需要PP很多人一看到模型放不下第一反应就是上PP。但PP的调试复杂度高还伴随气泡能不用就不用。在现代框架里可以先试ZeRO-3或FSDP这些方案可以用CPU offload把优化器状态、梯度甚至参数放到内存模型不一定非要用PP。什么情况下才该上PP一是模型大到即使offload也无法接受性能损失二是已经有多机多卡的资源三是需要把PP的显存省出来增加batch size。正确的顺序应该是先估算模型显存再试单卡最大micro-batch再决定是否需要PP以及PP怎么和其他并行组合。否则你可能是用了一个复杂方案解决了一个简单问题白白增加调试成本。5.2 参数配置从公式推算合理的micro-batch和accumulationPP训练时global batch size、micro-batch size、梯度累积和DP之间必须满足下面这个关系global_batch_size micro_batch_size × gradient_accumulation_steps × data_parallel_size其中data_parallel_size等于总卡数除以tensor_model_parallel_size和pipeline_model_parallel_size的乘积。举例来说72卡A100TP等于3PP等于8则DP等于3。想让global batch size等于1024micro-batch size等于4那么gradient accumulation steps等于1024除以4乘3约等于85。这里有个容易犯的错误很多框架配置里micro-batch size和accumulation是全局的不是per-rank的。同时要保证M每个stage实际跑的micro-batch数别太小一般建议M至少是PP的2到3倍。以这个例子来看M等于85气泡率约12%已经算比较理想的状态了。5.3 写出你的第一个配置并跑起来以Megatron-LM风格为例训练70B模型、72卡时一个合理的配置大概是这样的--tensor-model-parallel-size 3 \ --pipeline-model-parallel-size 8 \ --micro-batch-size 4 \ --global-batch-size 1024 \ --num-layers 80 \ --hidden-size 8192 \ --num-attention-heads 64如果要用interleaved再加一个参数--num-layers-per-virtual-pipeline-stage 2第一次跑别直接上生产配置建议先用小模型、小数据比如10层、隐藏层512把整体链路调通再逐步放大。放大时注意保持TP乘PP乘DP等于总卡数否则程序会直接报错或者隐性跑成低效的并行模式。我见过不少同事在这个地方翻车TP和PP改完DP没跟着改结果卡数对不上框架自动降级成纯TP显存直接爆掉。5.4 看时间线profiler是找“等”的第一工具调优PP的过程基本就是“找出时间线里那些空隙来自哪里”的过程。我常用NVIDIA Nsight Systems和PyTorch Profiler一起看先看全局有没有大段空闲再点开空闲看是等待通信、等待计算还是等待barrier。如果某个rank的空闲时间明显多于其他rank从stage切分和负载不均查起如果空闲时间均匀分布那大概率是bubble和通信等待。一个很实用的经验方法把每个stage的compute time单独统计画个柱状图。如果某个stage比其他stage慢30%以上你后面所有调参都白搭。先把负载搞平衡再谈调度。如果每个stage都差不多再去看通信和bubble这样才能真正定位到瓶颈。6. 常见问题与排查技巧实录6.1 Q1模型装下了GPU利用率不到50%这个是最常见的现象。先用profiler分三类如果是均匀的气泡把M增大或者开interleaved如果是不均匀空档查负载不均如果是通信recv等待查网络。不要一上来就调micro-batch先定位。很多时候你调micro-batch调了半天发现瓶颈根本不在气泡而在某个stage太重那就浪费了时间。6.2 Q2跨机跑PP比单机还慢PP跨机意味着每过一个stage就有一轮网络传输。如果机器之间的网络是普通的1G或10G以太网而单机内是NVLink性能差异会非常明显。建议把PP放在机器内部把DP放到跨机维度也就是说每台机器内部各跑一个pipeline多台机器之间做数据并行。这样跨机通信只是梯度同步的all-reduce而不是每个micro-batch都要走一趟网络。6.3 Q3显存没满但报OOM显存峰值和平均值是两回事。GPipe或者1F1B的调度在某个瞬间可能同时保存多个micro-batch的activation即便你看着总显存占用不高也可能因为momentary spike撞到显存上限。解决办法调小micro-batch、打开activation checkpointing、减少virtual pipeline stage数。activation checkpointing会让每次forward多算一次但显存压力大降我一般优先开它。6.4 Q4NCCL一直卡在recv甚至超时检查NCCL环境变量用NCCL_DEBUGINFO看是不是走了TCP fallback而不是RDMA检查网卡绑定是否正确看是否存在多任务抢占网卡。如果是跨机p2p通信慢可以尝试关闭interleaved减少通信次数。另外检查系统防火墙和网卡固件也有必要我踩过几次坑都是网卡驱动版本过旧导致协商失败。6.5 常见症状速查表症状可能原因快速处理GPU利用率低空档均匀气泡过大增加micro-batch数量或调整调度GPU利用率低空档不均匀负载不均按耗时重新切层跨机训练更慢网络带宽不足PP放单机DP跨机显存没满但OOMactivation spike减小micro-batch开activation checkpointing卡在recv或NCCL超时网络或RDMA问题检查NCCL、网卡、拓扑调了这么久流水线并行我最深的一个体会是模型装下只是前提能不能让每张卡都忙起来才是决定训练效率的关键。每次看到“GPU在等”别急着加机器先扪心自问一句等的是谁是调度阶段产生的气泡是某张卡上特别重的层还是网络上迟迟没到的数据。三种等三种解法。从一个简单的profiler开始把每个stage的compute time和通信时间画出来你会发现答案往往比自己想象得简单。千万别一上来就堆配置先把等待这件事看明白了再说。
返回列表