☰
深度学习训练显存占用分析与DP/MP/PP分布式策略选型指南
2026/10/26 11:01:56 网站建设 项目流程

显存不够用这件事,几乎每个跑过深度学习训练的人都撞上过。我见过太多人第一次拿到一个稍微大点的模型,兴冲冲地把 batch size 往上一调,结果终端直接甩出一行红色的显存溢出报错,然后就开始漫无目的地试——调小 batch、加梯度累积、换混合精度,折腾一整天也没搞清楚显存到底被谁吃掉了。这篇内容我想把这件事讲透:深度学习模型训练显存占用分析到底该怎么算、怎么拆,以及当单卡装不下时,DP、MP、PP 三种分布式训练策略各自解决什么问题、怎么选、怎么落地。适合刚接触大模型训练、被显存问题卡住的中级开发者,也适合想系统梳理分布式训练知识地图的从业者。我不会只给你结论,而是把每一步的推导过程、参数计算、踩坑经验都摊开讲,让你看完能自己动手算、动手配。

1. 先把显存这笔账算清楚:训练时到底谁在吃显存

很多人对显存的认知停留在“模型有多大就占多大”,这个理解在推理阶段勉强成立,在训练阶段则完全不够用。训练时的显存占用是一个由多个部分叠加而成的账本,任何一项算漏了,你的预估就会和实际差出一大截。我习惯把这笔账拆成四块:模型参数、梯度、优化器状态、激活值。前三个是静态开销,和 batch size 基本无关;第四个是动态开销,随 batch size 和序列长度线性甚至平方级增长。搞不清这四块的比例,你就不知道该往哪个方向优化。

1.1 静态开销:参数、梯度、优化器状态三件套

先看最容易被低估的静态部分。假设模型有 P 个参数,用 fp16 存储,那参数本身占 2P 字节。梯度通常和参数同精度,又是 2P 字节。优化器状态是大头——如果你用 Adam,它要为每个参数维护一阶矩和二阶矩两个动量,fp32 存储的话就是 8P 字节,再加上 fp32 的模型参数副本(混合精度训练时保留一份 fp32 主权重)4P 字节。这么一算,Adam + 混合精度的标准配置下,静态开销大约是16P 字节。

我用一个具体数字让你有感觉:一个 7B 参数的模型,P = 7×10⁹,静态开销就是 16 × 7×10⁹ ≈ 112 GB。注意,这还没算激活值,也没算任何临时缓冲区。也就是说,光是把 7B 模型用 Adam 训起来,静态部分就要一百多 G,单张 80G 的卡根本放不下。这就是为什么现在训大模型几乎必然要上分布式——不是想炫技,是物理上装不下。

这里有个常见误区要提醒:有人觉得用 fp16 存参数就能省一半,确实参数和梯度省了,但优化器状态和主权重还是 fp32,省下来的比例没有想象中大。真正想压静态开销,要么换优化器(比如 Adafactor 对二阶矩做分解近似,能把优化器状态从 8P 压到接近 2P),要么上 ZeRO 这类分片策略把状态切到多卡上。这个后面讲 DP 的时候会展开。

1.2 动态开销:激活值才是真正的隐形杀手

静态开销虽然大,但它是可预测的,算一遍就知道。真正让新手翻车的是激活值——它跟 batch size、序列长度、网络深度都挂钩,而且中间还有不少临时张量。激活值占用的粗略估算方式是:每层保存的激活张量数量 × 每张量元素数 × 精度字节数 × 层数 × batch size。

以 Transformer 为例,一个注意力层里需要保存的中间结果包括 Q、K、V 投影后的张量、注意力分数矩阵、softmax 输出、FFN 中间激活等。注意力分数矩阵的大小是 batch × heads × seq_len × seq_len,注意这里是 seq_len 的平方。序列长度从 512 涨到 2048,这一项就涨 16 倍。这就是为什么长上下文训练对显存的压力是爆炸性的,也是 FlashAttention 这类技术存在的意义——它通过分块计算避免把完整的 seq_len × seq_len 矩阵物化到显存里,直接把这一项的峰值占用压下去。

我实测过一个中等规模的 Transformer,batch size 从 8 加到 16,静态开销纹丝不动,但激活值几乎翻倍,总占用从 30G 出头直接冲到 55G 以上。所以当你遇到显存溢出,第一反应应该是看激活值,而不是怀疑模型参数算错了。降低激活值的手段有梯度检查点(用计算换显存,只保存部分层的激活,反向传播时重算)、减小 batch、缩短序列、换更省显存的注意力实现,这几招的组合基本能覆盖大部分场景。

1.3 一份可直接套用的显存估算表

我把上面这些整理成一张表,你可以对着自己的模型填数字。这张表的价值在于让你在动手跑之前就能判断“单卡到底行不行”,而不是跑起来报错了再猜。

开销类型计算方式是否随 batch 变化典型优化手段
模型参数P × 精度字节否混合精度、参数量化
梯度P × 精度字节否梯度分片、梯度累积
优化器状态P × 状态数 × 4 字节否换优化器、ZeRO 分片
激活值与层数、batch、seq_len 相关是梯度检查点、FlashAttention
临时缓冲区视算子而定部分相关算子融合、减少中间变量

提示:估算时务必留出 15% 到 20% 的余量。CUDA 上下文、cuDNN 工作空间、通信缓冲区都会额外占显存,算得太满一定会溢出。

2. 单卡装不下之后:三种分布式策略的分工逻辑

搞清楚显存账本,接下来就是“装不下怎么办”。分布式训练不是一招鲜,DP、MP、PP 三种策略切的是不同的维度,解决的问题也不一样。很多人一上来就听说“大模型要用 3D 并行”,但不知道这三个 D 各自管什么,配置的时候瞎组合,结果通信开销爆炸、GPU 利用率上不去。我先把三者的分工讲清楚,再逐个展开。

2.1 一张图看懂 DP、MP、PP 切的是什么

用一句话概括:DP 切数据,MP 切模型宽度(层内),PP 切模型深度(层间)。DP 是每张卡都有完整的模型副本,但喂不同的数据,梯度汇总后同步;MP 是把单个层内部的矩阵运算拆到多卡上,比如一个大的矩阵乘法按列或按行切开;PP 是把不同的层分给不同的卡,像流水线一样前向传递。

这个区别决定了它们的适用场景。DP 实现简单、对模型改动小,但每张卡都要放完整模型,显存瓶颈没解决,只是加速了训练;MP 能真正把单层的大矩阵拆开,适合单层就大到一张卡放不下的情况,但通信极其频繁,对卡间带宽要求高;PP 把层分开,显存压力随卡数线性下降,但会引入流水线气泡,需要精心调度才能把利用率拉满。实际训大模型时,这三者往往是组合使用的,比如“PP 切层 + MP 切层内 + DP 做数据并行”的混合模式。

2.2 DP 数据并行:最易上手但显存不省

DP 的逻辑最直观:N 张卡,每张卡放一份完整模型,把一个大 batch 切成 N 份,各卡独立前向反向,算完梯度后做一次 all-reduce 求平均,再各自更新参数。它的优点是实现简单,PyTorch 的 DistributedDataParallel 几行代码就能用起来,对模型代码几乎零侵入。

但它的硬伤也很明显:每张卡都要存完整的模型、梯度、优化器状态。前面算过 7B 模型静态开销 112G,DP 之后每张卡还是 112G,一点没省。所以 DP 解决的是“训练太慢”而不是“显存不够”。它的通信量是每步 2P 字节的 all-reduce,参数量越大通信越重,卡多了之后通信可能成为瓶颈。

DP 真正有价值的进化是 ZeRO。ZeRO 的思路是把原本每张卡都冗余存储的优化器状态、梯度、参数分片到各卡上,用的时候再通过通信拼起来。ZeRO-1 只切优化器状态,ZeRO-2 再切梯度,ZeRO-3 连参数都切。切到 ZeRO-3 时,单卡显存占用能降到接近 1/N,这时候 DP 才真正具备了训大模型的能力。代价是通信量增加,尤其是 ZeRO-3 每层前向反向都要通信拉参数,对带宽敏感。

2.3 MP 模型并行:切层内矩阵,通信是命门

MP 针对的是“单个层内部就大到放不下”的场景。最典型的是 Transformer 里的 FFN 层和注意力投影矩阵,参数量巨大。张量并行(Tensor Parallelism)的做法是把一个大的权重矩阵按列或按行切分到多张卡上,每张卡只算一部分,然后通过 all-reduce 或 all-gather 把结果拼起来。

以矩阵乘法 Y = XW 为例,如果 W 按列切成 [W1, W2],那 Y = [XW1, XW2],两张卡各算一半,最后 concat 即可,这是列并行;如果按行切,则需要先对 X 做切分,算完再 all-reduce 求和,这是行并行。实际实现里通常把注意力的 QKV 投影做列并行、输出投影做行并行,FFN 的第一层列并行、第二层行并行,这样一前一后正好把通信藏在计算里,减少同步次数。

MP 的命门是通信。每一层前向反向都要做 all-reduce,通信频率极高,所以它强烈依赖卡间高带宽互联。如果卡间带宽不够,MP 的加速比会惨不忍睹,甚至比单卡还慢。我的经验是:MP 的并行度不要超过单机内的卡数(通常 8 卡以内),跨机做 MP 基本是自找麻烦,除非你有极好的互联条件。

2.4 PP 流水线并行:切层间,气泡是主要敌人

PP 把模型的不同层分给不同的卡,比如 24 层的模型分到 4 张卡上,每张卡管 6 层。前向时数据从卡 0 流向卡 3,反向时梯度倒着流回来。它的显存优势很直接:每张卡只存自己那几层的参数、梯度和优化器状态,显存压力随卡数线性下降。

但 PP 有个天然的效率问题——流水线气泡。因为前向和反向是分阶段进行的,在流水线填充和排空的阶段,部分卡是空闲的。假设切成 4 段,气泡占比在最朴素的实现里能到 1/4 甚至更高。解决办法是微批(micro-batch)调度,把一个 batch 再切成多个小批,让不同卡尽量同时有活干,主流的有 GPipe 的填充-排空调度和 1F1B(一前一后交替)调度。1F1B 能把气泡压到比较低,是目前更常用的方案。

PP 的另一个考量是层怎么切。不能简单地按层数平均分,因为不同层的计算量和显存占用不一样,Embedding 层和最后的输出层往往特别大。实践中要做负载均衡,让每张卡的计算时间和显存占用尽量接近,否则最慢的那张卡决定整体速度。

3. 从估算到落地:一套可复现的配置流程

理论讲完,到了动手环节。这一节我给你一套从显存估算到分布式配置的完整流程,你可以照着走一遍。我会用一个假设的模型配置做例子,所有数字都是演示用的,你替换成自己的即可。

3.1 第一步:用脚本实测单卡显存基线

不要纯靠手算,手算用来做初筛,实测才是准的。最省事的办法是在单卡上跑一个极小 batch 的前向反向,用 PyTorch 的显存统计接口把峰值打出来。下面这段代码可以直接用:

import torch from torch.utils.checkpoint import checkpoint def measure_peak_memory(model, input_ids, use_checkpoint=False): torch.cuda.reset_peak_memory_stats() torch.cuda.synchronize() outputs = model(input_ids) loss = outputs.sum() if use_checkpoint: loss = checkpoint(lambda x: model(x).sum(), input_ids) loss.backward() torch.cuda.synchronize() peak = torch.cuda.max_memory_allocated() / 1024**3 print(f"峰值显存: {peak:.2f} GB") return peak

跑的时候 batch size 设成 1,先测一个基线,然后逐步加到 2、4、8,观察峰值增长的斜率。如果斜率接近线性,说明激活值是主导;如果几乎不涨,说明静态开销占大头。这个斜率信息直接告诉你该优先优化哪一块。我一般会同时测一遍开梯度检查点和不开的对比,心里对“用多少计算换多少显存”有个数。

3.2 第二步:根据瓶颈选策略组合

拿到基线数据后,按下面的决策逻辑选策略。这套逻辑是我踩了不少坑之后总结的,能覆盖大多数情况:

  • 如果单卡能放下但训练慢:优先 DP(DDP),简单直接,加速比接近线性。
  • 如果单卡放不下、但单层放得下:优先 PP,把层切开,显存线性下降,配合微批调度压气泡。
  • 如果单层就放不下(超大 FFN 或超宽注意力):必须上 MP,把层内矩阵切开。
  • 如果以上都不够、或者想极致压显存:DP + ZeRO-3,或者 PP + MP + DP 的 3D 组合。

举个具体例子:假设模型 13B 参数,静态开销约 208G,单张 80G 卡放不下。单层最大的是 FFN,约 4B 参数,单层静态约 64G,勉强能放进一张 80G 卡。这种情况我会选 PP=4,每张卡管 1/4 的层,单卡静态降到 52G 左右,再留出激活值的空间,配合梯度检查点,基本能跑起来。如果 FFN 再大一点单层就超了,那就得 PP=2 + MP=2 组合。

3.3 第三步:配置并行度与全局 batch 的换算

这一步最容易出错。分布式训练里有个关键概念:全局 batch = 每卡 micro-batch × 梯度累积步数 × DP 并行度。注意 MP 和 PP 不增加数据并行维度,它们切的是模型,不切数据。很多人配完并行度发现训练效果不对,一查是全局 batch 算错了。

假设你要的全局 batch 是 512,用 DP=8、PP=4、MP=2,总共 64 张卡。那每卡 micro-batch × 梯度累积 = 512 / 8 = 64。如果每卡 micro-batch 设 4,梯度累积就是 16。这个换算关系要写死在配置里,改任何一个参数都要重新核对全局 batch,否则学习率调度会对不上。

注意:PP 的微批数量要大于等于流水线段数,否则流水线填不满,气泡会非常大。比如 PP=4,微批数至少设 4,最好设 8 或更多,让流水线充分流动。

3.4 第四步:通信与计算的重叠调优

配置跑通只是第一步,跑得快才是目标。分布式训练的性能调优核心是让通信和计算重叠起来,别让卡闲着等数据。几个实操要点:

  • DP 的 all-reduce 可以设置成和反向传播重叠,PyTorch DDP 默认会做梯度分桶,桶大小可以调,桶太小通信次数多,桶太大重叠效果差,一般设 25MB 左右比较平衡。
  • PP 用 1F1B 调度,确保前向和反向交替进行,减少气泡。
  • MP 的通信尽量用 NCCL 的 ring 算法,并且确保同一组 MP 的卡在物理上靠近(同一台机器内),减少跨机通信。

我实测过一个 PP=4 + DP=4 的配置,一开始没做重叠,GPU 利用率只有 60% 出头;把微批数从 4 提到 16、开启 1F1B 调度后,利用率拉到 85% 以上。这个提升幅度说明调度配置的重要性不亚于并行策略本身。

4. 踩坑实录:分布式训练常见问题与排查表

分布式训练的坑特别多,而且报错信息往往很隐晦。这一节我把实际遇到过的高频问题整理成速查表,附上排查思路,你遇到问题时可以对着查。

4.1 显存溢出但估算明明够用

这是最常见的困惑:手算显存够,跑起来还是 OOM。原因通常有几个。一是没算临时缓冲区和 CUDA 上下文,实际占用比理论值高 15% 以上;二是激活值的估算漏了某些中间张量,尤其是自定义算子里的临时变量;三是 PyTorch 的缓存分配器会保留已释放的显存块,max_memory_allocated和max_memory_reserved是两回事,看后者会偏大。排查时先看 reserved 和 allocated 的差值,如果差值很大,说明是碎片问题,可以调PYTORCH_CUDA_ALLOC_CONF的max_split_size_mb来缓解。

4.2 多卡训练速度不升反降

卡加多了反而更慢,八成是通信成了瓶颈。先确认并行策略选对没有:如果模型单卡放得下却用了 MP,通信开销白白增加;如果跨机做了 MP,带宽不够必然拖慢。再看通信量,DP 的 all-reduce 通信量是 2P 字节,参数量大时很可观,可以上 ZeRO 或者梯度压缩。最后检查有没有做通信计算重叠,没重叠的话卡都在干等。

4.3 流水线气泡过大导致利用率低

PP 配置后 GPU 利用率上不去,先看微批数够不够。微批数小于流水线段数时,流水线根本填不满。再看调度策略,朴素的 GPipe 调度气泡大,换 1F1B。还有一个容易忽略的点是层切分不均衡,某张卡分到的层计算量大,它就成了瓶颈,其他卡都在等它。用 profiler 看每张卡的耗时,不均衡就重新调整层的分配。

问题现象可能原因排查与解决
OOM 但估算够缓冲区/碎片/漏算激活看 reserved vs allocated,调分配器参数
加卡变慢通信瓶颈、策略选错检查并行策略,开启通信计算重叠
PP 利用率低微批不足、层切分不均增加微批数,用 profiler 查负载均衡
loss 不收敛全局 batch 算错、学习率没同步调核对全局 batch 换算,按比例调学习率
梯度异常混合精度溢出、all-reduce 精度问题开 loss scaling,检查通信精度设置

4.4 混合精度下的数值稳定性问题

用 fp16 做分布式训练时,梯度溢出和 NaN 是常客。fp16 的动态范围窄,大梯度容易溢出成 inf。解决办法是开 loss scaling,PyTorch 的 AMP 有动态 scaling 机制,会自动调整 scale 因子。但分布式场景下要注意,各卡的 scale 要同步,否则 all-reduce 时会出现精度不一致。另外 bf16 的动态范围和 fp32 一样宽,现在新硬件上我更倾向用 bf16,省去了 loss scaling 的麻烦,数值稳定性好很多。

5. 策略选型的经验判断:什么场景用什么组合

讲完原理和实操,最后聊聊选型判断。这部分没有标准答案,更多是经验权衡,我把自己总结的几条判断原则分享出来,供你参考。

5.1 按模型规模和硬件条件快速定位

我习惯用一个简单的二维判断:横轴是模型能否单卡放下,纵轴是单层能否单卡放下。两个都能放下,DP 就够了;模型放不下但单层放得下,上 PP;单层都放不下,必须 MP。如果三者都不够,就是 3D 并行组合。硬件条件方面,卡间带宽好(同机 NVLink 之类)可以多用 MP,带宽差就多用 PP 和 DP,把通信频率降下来。

还有一个现实约束是卡的数量。PP 的并行度受层数限制,层数不够切太细没意义;MP 的并行度受单层矩阵维度限制,切太细每张卡算的太少,通信占比反而高。所以并行度不是越大越好,要匹配模型结构。

5.2 显存优化的优先级排序

如果目标是“用最少的改动把显存压下来”,我建议按这个优先级来:先上梯度检查点(改动小、效果直接,用计算换显存),再上混合精度(bf16 优先),然后考虑换省显存的优化器(Adafactor 之类),最后才动分布式策略。分布式策略的配置复杂度和调试成本都高,能靠单卡优化解决就别急着上分布式。我见过不少人一上来就搞 3D 并行,结果配置错误排查了两天,其实开个梯度检查点就够用了。

5.3 一个容易被忽略的细节:检查点与并行的交互

梯度检查点和 PP、MP 组合时有一些交互要注意。开了梯度检查点后,前向时不保存中间激活,反向时重算,这会改变计算图的结构,在某些 PP 调度下可能影响流水线的重叠效果。另外检查点的重算会增加计算量,如果本来计算就是瓶颈,开了反而更慢。我的做法是先测不开检查点的显存和速度,再测开的,对比之后决定。通常显存紧张时开,显存宽裕时不开。

提示:调试分布式配置时,先用小模型和小数据跑通流程,确认并行度、全局 batch、通信都正常,再换大模型。直接上大模型调试,一次失败要等很久,效率极低。

我在实际项目里最深的一个体会是:显存分析和分布式配置这件事,纸面上算得再准,都不如动手实测一遍。理论估算帮你缩小范围、选对方向,但真正的数字只有跑起来才知道。每次换模型或换硬件,我都会重新跑一遍基线测量,因为缓存分配器的行为、通信库的版本差异,都会让实际结果和上次不一样。把测量脚本固化成流程的一部分,比记住任何公式都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询