1. 算法同学为什么必须搞懂分布式并行策略
做算法的同学,尤其是从传统深度学习转到大模型方向的,大概率都经历过这样一个阶段:单卡能跑通的模型,换到多卡环境突然就不会写了。明明只是加了几行torch.distributed的初始化代码,结果 loss 不收敛、梯度对不上、显存还是爆。更让人头疼的是,面试或者组内技术评审的时候,别人张口就是 TP、DP、PP、CP、EP,你只能点头微笑,心里想的是“这些缩写到底在说什么”。
我最早接触分布式训练是在做推荐模型的时候,那时候数据量大,单卡塞不下,用 DataParallel 包一层就能跑。后来转到 LLM 方向,发现事情完全不是那么回事。一个 70B 的模型,光权重就 140GB,加上优化器状态和梯度,单卡 80GB 的 A100 根本装不下。这时候你面对的就不是“怎么跑得更快”的问题,而是“怎么让它跑起来”的问题。
LLM 分布式计算里的 TP、DP、PP、CP、EP,本质上是在回答一个问题:当模型大到一张卡放不下的时候,我们怎么把计算图和参数切开放到多张卡上,同时保证数学等价性和通信效率。这五个策略不是互斥的,实际训练中往往是组合使用,比如 Megatron-LM 里常见的 TP+DP+PP 三维并行,长序列场景再加 CP,MoE 模型再加 EP。
这篇文章我打算用算法同学能听懂的语言,把这五个策略的直觉、切分方式、通信模式、适用场景和踩坑点讲清楚。不会堆太多公式,但关键的数学等价性会点出来。读完之后,你至少能做到:看到一篇论文说“我们用了 TP=8, PP=4, DP=2”,脑子里能浮现出参数是怎么分布的,通信发生在哪一步,瓶颈可能在哪里。
提示:这篇文章假设你已经了解 Transformer 的基本结构,知道什么是 attention、FFN、layer norm,也用过 PyTorch 的 DDP。如果这些还不熟,建议先补一下基础再来看分布式部分。
2. 五个策略的核心直觉:切什么,怎么切
在展开每个策略之前,我想先给一个统一的视角。分布式训练的本质是把计算图、参数、数据这三个东西中的一个或多个切开,分配到不同设备上。不同的策略切的东西不一样,通信模式也不一样。
2.1 用“切蛋糕”类比五种并行
想象你有一个巨大的蛋糕要分给一群人吃。蛋糕就是模型的计算任务,人就是 GPU。
DP 是每个人拿一份完整的蛋糕配方,但各自用不同的原料(数据)做一遍,做完之后大家对一下配方哪里需要调整(梯度 all-reduce)。每个人做的蛋糕是一样的,只是原料不同。
TP 是把蛋糕的某一层切开,比如一个大的矩阵乘法,横着切几刀分给不同的人,每个人算一部分,算完之后把结果拼起来。每个人手里只有蛋糕的一部分,必须频繁沟通才能完成一层。
PP 是把蛋糕按层叠起来,第一层的人做完传给第二层的人,像流水线一样。每个人负责不同的层,中间需要传递激活值。
CP 是把一条很长的蛋糕卷切成几段,每个人负责一段,但做的时候需要看到相邻段的信息,所以要在边界处交换数据。
EP 是把蛋糕里的不同口味(专家)分给不同的人,来什么订单就找对应口味的人做,其他人闲着。
这个类比不完全精确,但能帮你建立第一层直觉。下面逐个展开。
2.2 五个策略的对比总览
| 策略 | 切分对象 | 通信模式 | 通信频率 | 典型场景 | 主要瓶颈 |
|---|---|---|---|---|---|
| DP | 数据 | All-Reduce 梯度 | 每步一次 | 数据量大、模型能单卡放下 | 梯度同步带宽 |
| TP | 参数(层内) | All-Reduce/All-Gather | 每层多次 | 单层参数巨大 | 通信延迟 |
| PP | 层(深度) | Send/Recv 激活 | 每 micro-batch | 层数多、模型深 | 流水线气泡 |
| CP | 序列维度 | Ring All-Gather | 每层 attention | 长序列 | 边界通信 |
| EP | 专家 | All-to-All | 每层 MoE | MoE 模型 | 负载不均 |
这张表建议你先记住,后面每个策略我会展开讲为什么是这样。
3. DP:最熟悉的陌生人,坑比你想的多
Data Parallel 应该是大家最熟悉的。单卡放得下模型,但数据量大,想加速训练,就用 DP。PyTorch 的DistributedDataParallel几乎是标配。
3.1 DP 的工作原理与数学等价性
DP 的核心假设是:每个 GPU 上有一份完整的模型副本,但喂给每个 GPU 的数据不同。前向计算各自独立,反向传播各自算出梯度,然后在更新参数之前,把所有 GPU 的梯度做一次 all-reduce 求平均。
数学上,假设有 N 张卡,每张卡的 batch size 是 B,那么全局 batch size 是 N×B。每张卡算出的梯度是 g_i,all-reduce 之后得到 (1/N)Σg_i,然后用这个平均梯度更新参数。这等价于用一个全局 batch size 为 N×B 的数据做单卡训练。
这里有个细节容易被忽略:loss 的归一化方式。如果你用CrossEntropyLoss默认的 mean reduction,每张卡算的是自己 batch 内的平均 loss,all-reduce 平均之后正好是全局平均。但如果你自己写了 loss 并且用了 sum reduction,就需要手动除以全局 batch size,否则梯度会放大 N 倍。我见过不止一个同学在这里踩坑,loss 曲线看起来正常,但学习率实际上被放大了。
3.2 DP 的通信开销与 bucket 机制
DP 的通信发生在反向传播之后、参数更新之前。朴素实现是等所有梯度算完,一次性 all-reduce。但这样通信和计算是串行的,GPU 在通信时闲着。
PyTorch DDP 的做法是梯度分桶(bucketing)。反向传播是从后往前算的,后面的层先算出梯度。DDP 把梯度按顺序分成若干个 bucket,每个 bucket 满了就立刻发起 all-reduce,不用等所有梯度算完。这样通信和计算可以重叠,隐藏一部分通信时间。
bucket 的大小默认是 25MB,可以通过bucket_cap_mb参数调整。调大有调大的好处:all-reduce 的次数少,每次通信的数据量大,带宽利用率高。但调太大也有问题:第一个 bucket 要等更久才能凑满,重叠效果变差。实测下来,25MB 到 100MB 之间比较合适,具体要看网络带宽和模型结构。
注意:如果你用的是 NVLink 互联的 A100,带宽很高,bucket 可以调大一些。如果是 PCIe 或者跨机通信,bucket 调大反而可能因为等待时间过长而降低重叠效果。
3.3 DP 的显存账:为什么 70B 模型 DP 也救不了
很多人以为 DP 能解决显存问题,其实不能。DP 的每张卡上都有完整的模型副本、完整的优化器状态、完整的梯度。以 Adam 为例,假设模型参数量是 P,混合精度训练下:
- 模型权重(fp16):2P 字节
- 梯度(fp16):2P 字节
- 优化器一阶矩(fp32):4P 字节
- 优化器二阶矩(fp32):4P 字节
- 优化器 master weight(fp32):4P 字节
合计 16P 字节。70B 模型就是 1120GB,除以 80GB 每卡,至少需要 14 张卡才能放下。这还没算激活值。所以 DP 解决的是吞吐问题,不是容量问题。容量问题要靠 TP、PP、CP 这些模型并行策略。
4. TP:把矩阵乘法切开,通信最密集的策略
Tensor Parallel 是我认为最需要算法同学理解清楚的策略,因为它直接改变了模型的计算图,而且通信非常频繁。
4.1 TP 的切分方式:列并行与行并行
TP 的核心思想是把一个大的矩阵乘法切开。以 Transformer 里的 FFN 为例,第一层是Y = XW,其中 X 是 (B, S, H),W 是 (H, 4H)。如果按列切 W,分成 W1 和 W2,每张卡算Y1 = XW1和Y2 = XW2,然后把 Y1 和 Y2 拼起来得到 Y。这叫列并行。
第二层是Z = YV,V 是 (4H, H)。如果 Y 是按列切开的,那么每张卡有 Y1 和 Y2,但 V 需要按行切,V1 和 V2,每张卡算Z1 = Y1V1和Z2 = Y2V2,然后Z = Z1 + Z2。这叫行并行。
列并行之后不需要立刻通信,因为每张卡算出的 Y1 和 Y2 可以直接喂给下一层的行并行。行并行之后需要一次 all-reduce 把 Z1 和 Z2 加起来。这样一对列并行+行并行,只需要一次 all-reduce,通信量是 (B, S, H)。
4.2 Attention 的 TP 切分:多头注意力的天然并行
Attention 的 TP 切分更自然,因为多头注意力本身就是多个头独立计算。假设有 32 个头,TP=8,那么每张卡负责 4 个头。Q、K、V 的投影矩阵按头切分,每张卡算自己那 4 个头的 attention,最后输出投影矩阵按行切分,算完之后 all-reduce。
这里有个关键点:LayerNorm 和 Dropout 在 TP 下需要特殊处理。LayerNorm 需要完整的 hidden state 才能算均值和方差,但 TP 之后每张卡只有部分 hidden state。Megatron-LM 的做法是在 LayerNorm 之前做一次 all-reduce,或者用同步的 LayerNorm。这会增加通信量,所以实际实现中往往把 LayerNorm 放在 all-reduce 之后,避免额外的通信。
4.3 TP 的通信量计算与瓶颈
假设 hidden size 是 H,序列长度是 S,batch size 是 B,TP 度是 T。每层 Transformer 有两次 all-reduce(attention 一次,FFN 一次),每次通信量是 B×S×H×2 字节(fp16)。假设有 L 层,那么每步训练的通信量是 2×L×B×S×H×2 字节。
以 GPT-3 175B 为例,H=12288,L=96,S=2048,B=1,TP=8。每步通信量大约是 2×96×1×2048×12288×2 ≈ 9.7GB。这个通信量非常大,所以 TP 通常只在 NVLink 互联的同一台机器内使用,跨机 TP 会因为网络带宽不足而严重拖慢训练。
实操心得:TP 度一般不超过单机 GPU 数,比如 8 卡机器 TP 最多 8。如果模型太大单机放不下,优先用 PP 跨机,而不是 TP 跨机。
5. PP:流水线并行,气泡是最大的敌人
Pipeline Parallel 的思路很直观:把模型按层切成若干段,每段放在不同的 GPU 上。GPU1 算第 1-12 层,GPU2 算第 13-24 层,以此类推。数据像流水线一样从 GPU1 流到 GPU2。
5.1 PP 的朴素实现与气泡问题
最朴素的 PP 是:GPU1 算完一个 batch 的激活值,传给 GPU2,GPU2 算完传给 GPU3,最后 GPU_n 算完 loss,反向传播再倒着传回去。但这样同一时刻只有一个 GPU 在工作,其他 GPU 都在等,利用率极低。
为了解决这个问题,引入了micro-batch。把一个 mini-batch 切成多个 micro-batch,GPU1 算完 micro-batch 1 就传给 GPU2,然后立刻算 micro-batch 2。这样 GPU1 和 GPU2 可以同时工作。但流水线的开始和结束阶段,仍然有 GPU 在空转,这就是气泡(bubble)。
气泡的大小可以用公式估算:假设有 P 个流水线阶段,M 个 micro-batch,那么气泡占比大约是 (P-1)/(M+P-1)。要减少气泡,就要增大 M,但 M 太大会增加显存占用,因为每个 micro-batch 的激活值都要存着等反向传播。
5.2 1F1B 调度与显存权衡
Megatron-LM 用的是 1F1B(One Forward One Backward)调度。它的做法是:在流水线预热阶段,每个 GPU 连续做若干个前向;进入稳定阶段后,每做一个前向就做一个反向,保持前向和反向的数量平衡。这样显存占用比朴素实现低,因为不需要存所有 micro-batch 的激活值。
1F1B 的显存占用大约是朴素实现的 1/M。但代价是调度逻辑更复杂,而且反向传播的梯度需要累积到对应的 micro-batch 上。实际实现中,PyTorch 的Pipe和 DeepSpeed 的 pipeline 都支持 1F1B。
5.3 PP 的通信量与层切分策略
PP 的通信量比 TP 小很多。每个 micro-batch 在相邻 stage 之间传递一次激活值,通信量是 B×S×H×2 字节。因为只在 stage 边界通信,不像 TP 每层都通信,所以 PP 可以跨机使用,对网络带宽要求不高。
层切分策略上,有两种常见做法:均匀切分和按计算量切分。均匀切分就是每段层数相同,但 Transformer 的第一层和最后一层计算量不同(第一层有 embedding,最后一层有 lm head),可能导致负载不均。按计算量切分需要预估每层的 FLOPs,实际中往往用均匀切分加手动调整。
注意:PP 的 stage 数量不要太多,一般 4 到 16 之间。stage 太多会导致气泡占比增大,而且跨机通信的延迟也会累积。
6. CP:长序列训练的救星,Ring Attention 是核心
Context Parallel 是最近两年随着长上下文模型火起来的。当序列长度达到 128K 甚至 1M 时,attention 的计算量和显存占用都是平方级增长,单卡根本放不下。CP 就是把序列维度切开,每张卡负责一段序列。
6.1 CP 的切分方式与 attention 的边界问题
假设序列长度是 S,CP 度是 C,那么每张卡负责 S/C 个 token。但 attention 的问题是:每个 token 需要和所有 token 计算注意力,不只是自己这一段。所以每张卡在算 attention 时,需要拿到其他卡上的 K 和 V。
Ring Attention 的做法是:每张卡把自己的 K 和 V 发给下一张卡,同时从上一张卡接收 K 和 V,形成一个环。每轮计算一部分 attention,经过 C-1 轮之后,每张卡都见过了所有 K 和 V,attention 结果就完整了。这样通信和计算可以重叠,因为每轮计算当前收到的 K、V 时,可以同时发送自己的 K、V 给下一张卡。
6.2 CP 与 TP 的区别:切序列还是切头
CP 和 TP 在 attention 上的切分方式容易混淆。TP 是把 attention 的头切开,每张卡负责一部分头,每个头仍然看到完整的序列。CP 是把序列切开,每张卡负责一段序列,但所有头都要算。
两者的通信模式也不同。TP 在 attention 之后需要 all-reduce 输出投影的结果。CP 在 attention 过程中需要多轮 ring all-gather 交换 K、V。CP 的通信量是 O(S×H×C),TP 的通信量是 O(S×H×T)。在长序列场景下,CP 的通信量增长更快,但因为它可以和多轮计算重叠,实际效率往往可以接受。
6.3 CP 的显存收益与适用场景
CP 的显存收益主要来自 attention 矩阵的切分。原本需要存 (S, S) 的 attention score 矩阵,CP 之后每张卡只需要存 (S/C, S) 或者更少(如果用 flash attention 就不存完整矩阵)。对于 128K 序列,attention score 矩阵是 128K×128K,fp16 下就是 32GB,单卡根本放不下。CP=8 之后每张卡只需要 4GB,就可以接受了。
CP 适合的场景是长序列训练,比如文档理解、代码生成、多轮对话。如果序列长度只有 2K 或 4K,CP 的收益不大,反而增加了通信开销。
7. EP:MoE 模型的专属并行策略
Expert Parallel 是专门为 MoE(Mixture of Experts)模型设计的。MoE 的核心思想是把 FFN 层替换成多个专家,每个 token 只激活其中几个专家。EP 就是把不同的专家放在不同的 GPU 上。
7.1 MoE 的基本结构与 EP 的切分逻辑
一个典型的 MoE 层有 N 个专家,每个专家是一个 FFN。还有一个 router(门控网络),根据 token 的 hidden state 决定它去哪些专家。比如 top-2 routing,每个 token 选择概率最高的 2 个专家。
EP 的切分逻辑是:把 N 个专家分到 E 张卡上,每张卡负责 N/E 个专家。Router 在每个 GPU 上都有副本,因为每个 token 都需要算 routing。Token 根据 routing 结果被发送到对应的专家所在的 GPU,专家算完之后再把结果发回来。
7.2 All-to-All 通信与负载均衡
EP 的通信模式是 all-to-all。每个 GPU 上的 token 可能要去任意一个专家,所以需要把 token 发送到所有其他 GPU,同时接收来自所有其他 GPU 的 token。这是一个 all-to-all 通信,通信量取决于 routing 的分布。
负载均衡是 EP 最大的挑战。如果 router 倾向于把大部分 token 发给少数几个专家,那么这几个专家所在的 GPU 就会过载,其他 GPU 闲着。为了解决这个问题,MoE 训练中通常会加一个负载均衡 loss,鼓励 router 把 token 均匀分配给各个专家。另外还有 capacity factor 的机制,限制每个专家最多处理多少 token,超出的 token 直接丢弃或者走残差连接。
7.3 EP 与其他策略的组合
EP 通常和 DP、TP 组合使用。比如一个 MoE 模型有 64 个专家,EP=8,那么每张卡 8 个专家。同时 attention 部分可以用 TP=8,数据维度用 DP=2。这样总的 GPU 数是 8×2=16,但专家分布在 8 张卡上,每张卡还有完整的 attention 部分。
EP 的通信量在 MoE 层是 all-to-all,通信量是 O(B×S×H×top_k),其中 top_k 是每个 token 激活的专家数。这个通信量比 TP 小,但比 PP 大。实际中 EP 往往和 TP 在同一个 NVLink 域内,以保证通信带宽。
8. 组合策略实战:三维、四维并行怎么配
实际训练大模型时,很少只用一种策略。Megatron-LM 的经典配置是 TP+DP+PP 三维并行,长序列加 CP,MoE 加 EP。这一节讲怎么组合,以及组合时的优先级。
8.1 组合优先级:先 PP 还是先 TP
我的经验是:先确定 PP,再确定 TP,最后用 DP 填满剩余 GPU。原因是 PP 的通信量最小,可以跨机;TP 的通信量最大,尽量限制在单机内;DP 的通信量中等,可以跨机但需要高带宽。
具体步骤:
- 算模型总显存需求,确定最少需要多少张卡才能放下。
- 如果单机 8 卡放不下,用 PP 跨机。PP 的 stage 数尽量取 2 的幂次,方便切分。
- 在每个 stage 内,如果单卡还是放不下,用 TP。TP 度不超过单机 GPU 数。
- 剩余的 GPU 用 DP。DP 度 = 总 GPU 数 / (PP × TP)。
举个例子:128 张 A100,模型需要至少 32 张卡才能放下。可以配 PP=4,TP=8,DP=4。这样每个 PP stage 有 8 张卡做 TP,4 个 DP 副本。总 GPU 数 = 4×8×4=128。
8.2 通信域的设计与网络拓扑
组合策略下,通信域的设计很关键。TP 的通信最频繁,应该限制在 NVLink 域内。PP 的通信是点对点,可以跨机。DP 的 all-reduce 可以跨机,但最好用高带宽网络。
实际部署时,通常把同一台机器内的 8 张卡作为一个 TP 组,不同机器之间做 PP 或 DP。如果机器有 NVLink switch,可以跨机做 TP,但成本很高。
8.3 一个具体的配置案例与参数计算
假设我们要训练一个 13B 的模型,24 层,hidden size 5120,序列长度 2048,用 32 张 A100 80GB。
显存估算:13B 参数,混合精度下权重 26GB,梯度 26GB,优化器状态 52GB,合计 104GB。单卡 80GB 放不下,至少需要 2 张卡。但还要算激活值,假设 activation checkpointing 之后每层激活值 1GB,24 层就是 24GB。所以每张卡需要 104/TP + 24/PP GB。
如果 TP=4,PP=2,那么每张卡权重+梯度+优化器是 26GB,激活值是 12GB,合计 38GB,可以放下。剩余 GPU 数 = 32/(4×2)=4,所以 DP=4。最终配置:TP=4,PP=2,DP=4。
通信量估算:TP 每层两次 all-reduce,每次 2048×5120×2=20MB,24 层共 960MB。PP 每个 micro-batch 传一次激活,假设 8 个 micro-batch,每次 20MB,共 160MB。DP 每步 all-reduce 梯度,13B×2=26GB。DP 的通信量最大,但频率低,每步一次。
9. 常见问题与排查技巧实录
分布式训练的坑非常多,我整理了一些典型问题和排查思路。
9.1 梯度对不上、loss 不收敛
这是最常见的问题。排查顺序:
- 检查 loss 的 reduction 方式。如果是自定义 loss,确认是否除以了全局 batch size。
- 检查 TP 的 LayerNorm 是否同步。不同步的 LayerNorm 会导致每张卡算出的均值方差不同,梯度对不上。
- 检查 PP 的 micro-batch 梯度累积是否正确。每个 micro-batch 的 loss 要除以 micro-batch 数量再累积。
- 用一个小模型(比如 2 层 Transformer)和单卡结果对比,逐层检查输出是否一致。
9.2 通信卡住、NCCL 超时
NCCL 超时通常是因为某张卡在等待其他卡,但其他卡已经挂了或者卡在某个操作上。排查方法:
- 设置
NCCL_DEBUG=INFO,看日志里哪张卡最后发起通信。 - 检查是否有 GPU 显存溢出导致进程被杀。
- 检查网络是否有丢包或者带宽不足。
- 如果是跨机通信,检查防火墙和路由配置。
9.3 显存溢出但不知道溢在哪
用torch.cuda.memory_summary()看显存分布。常见原因:
- 激活值没做 checkpointing,导致中间激活值占用过大。
- PP 的 micro-batch 太多,每个 micro-batch 的激活值都存着。
- TP 的通信 buffer 太大。
- 优化器状态没有用 ZeRO 或者分片。
9.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss 不收敛 | loss reduction 错误 | 检查 loss 计算 | 除以全局 batch size |
| 梯度对不上 | LayerNorm 不同步 | 对比单卡输出 | 同步 LayerNorm |
| NCCL 超时 | 某卡挂掉 | NCCL_DEBUG=INFO | 检查显存和进程 |
| 显存溢出 | 激活值太大 | memory_summary | activation checkpointing |
| 训练速度慢 | 通信瓶颈 | profile 通信时间 | 调整并行策略 |
| PP 气泡大 | micro-batch 太少 | 计算气泡占比 | 增大 micro-batch |
实操心得:分布式训练调试时,先用小模型和小数据集跑通,确认数学等价性,再上大模型。不要一上来就 100B 模型,出了问题根本没法定位。
10. 我个人在实际操作中的几点体会
最后分享几个我在实际训练中总结的经验,不一定对所有人都适用,但至少能帮你少走点弯路。
第一,TP 的度不要超过 8。我试过 TP=16 跨两台机器,通信延迟直接把训练速度拖慢了一倍。除非你有 NVLink switch 或者 InfiniBand 这种高带宽低延迟网络,否则 TP 就限制在单机内。
第二,PP 的 stage 数尽量取 2 的幂次。因为层数不一定能被 stage 数整除,取 2 的幂次可以让切分更均匀。比如 24 层,PP=4 每段 6 层,PP=6 每段 4 层,但 PP=6 不是 2 的幂次,调度逻辑会更复杂。
第三,CP 和 TP 不要同时用在 attention 上。我见过有人既切头又切序列,结果通信量爆炸。正确的做法是:如果序列长,用 CP;如果头多,用 TP。两者选一个作为 attention 的主要并行方式。
第四,EP 的负载均衡 loss 权重需要调。权重太小,router 会塌缩到少数专家;权重太大,router 会均匀分配但失去专业性。一般从 0.01 开始调,观察专家利用率。
第五,监控通信和计算的重叠率。用 PyTorch profiler 看 all-reduce 和计算的时间线,如果通信和计算没有重叠,说明 bucket 设置或者调度有问题。理想情况下,通信时间应该被计算时间完全掩盖。
这些经验都是踩坑踩出来的,希望能帮你节省一些调试时间。分布式训练没有银弹,每个模型、每个集群的最优配置都不一样,多试多调才是正道。