分布式通信原语与 vLLM 并行策略
深入理解 AllReduce, AllToAll, TP, DP, PP, EP
技术分享 Meetup
目录
通信原语
- 6 大通信原语详解
- AllReduce 与 Ring 算法
- AllToAll 全交换
并行策略与通信量
- TP/DP/PP/EP 通信详情
- Prefill-Decode 分离
- 通信量计算公式
📡 分布式通信原语:全景图
| 原语 | 定义 | 类比 |
| Broadcast | 一个节点 → 所有节点 | 广播通知 |
| Reduce | 所有节点 → 一个节点 | 汇总报表 |
| AllReduce | 所有节点同步获得归约结果 | 大家报数,都知道总数 |
| Gather | 收集数据(不合并) | 收作业 |
| AllToAll | 每个节点给所有节点发不同数据 | 互相握手 |
| Send/Recv | 两节点双向通信 | 两人通话 |
💡 理解 AllReduce
AllReduce = Reduce + Broadcast。先汇总,再把结果同步给所有人。
💡 理解 ReduceScatter
ReduceScatter = Reduce + Scatter。归约后分片,每人拿一块。
🔄 AllReduce = 归约 + 同步广播
🌐 AllReduce = 所有机器一起做归约,结果同步给所有人
所有节点不仅参与计算,最后还同步获得相同的结果!
flowchart LR
D0["🖥️ 节点0: 3"] & D1["🖥️ 节点1: 5"] & D2["🖥️ 节点2: 2"] & D3["🖥️ 节点3: 8"] --> AR["🔄 AllReduce
(求和)"]
AR --> R["结果: 18"]
R --> R0["18 ✓"] & R1["18 ✓"] & R2["18 ✓"] & R3["18 ✓"]
通信量公式
Ring AllReduce:
总通信量 ≈ 2M (与机器数无关)
其中 M = 数据大小
🔄 AllToAll = 全交换
📡 AllToAll = 每个人都给每个人发不同的消息
AllReduce 是"大家做同样的事,得到同样的结果"。AllToAll 是"大家交换数据,每个人都给其他人发不同的东西"。
flowchart LR
G0["GPU0
[a0|a1|a2|a3]"] -->|"a1→GPU1"| G1["GPU1
[b0|b1|b2|b3]"]
G1 -->|"b2→GPU2"| G2["GPU2
[c0|c1|c2|c3]"]
G2 -->|"c3→GPU3"| G3["GPU3
[d0|d1|d2|d3]"]
G3 -->|"d0→GPU0"| G0
通信量
总通信量: N² × M 字节
每人发送: (N-1) × M 字节
主要用途
- MoE 模型: Token 路由到专家
- 数据重分区
⚙️ vLLM 并行策略概览
flowchart TB
AR["🔄 AllReduce"] --> TP["📦 TP"]
A2A["📡 AllToAll"] --> EP["👥 EP"]
P2P["📨 Send/Recv"] --> PP["🔗 PP"]
NONE["❌ 无通信"] --> DP["📋 DP"]
subgraph Info["通信模式"]
TPL["Tensor Parallel: 每层2次 AllReduce"]
EPL["Expert Parallel: AllToAll 路由"]
PPL["Pipeline Parallel: P2P 传激活"]
DPL["Data Parallel: 推理无通信"]
end
一句话总结
- TP = AllReduce
- EP = AllToAll
- PP = Send/Recv
- DP 推理 = 无通信
关键问题
- 通信量多大?
- 传什么内容?
- Prefill vs Decode 有何不同?
📦 Tensor Parallel (TP) 通信详情
💡 核心:每层计算后需要 AllReduce 同步
TP 把同一层权重分到不同 GPU,每 GPU 算完后要把结果汇总(AllReduce)。
通信内容
| 阶段 | 通信类型 | 传输内容 |
| Forward | AllReduce | 局部输出 → 完整输出 |
| Backward | AllReduce | 局部梯度 → 完整梯度 |
通信量公式
# Forward (单层)
C_TP_fwd = 2 × B × S × H / P
# Backward (单层)
C_TP_bwd = 2 × C_TP_fwd
B=batch, S=seq_len, H=hidden, P=TP degree
⚠️ 关键:TP 通信量与 batch size × sequence length 成正比,与 GPU 数成反比。
📦 TP: Prefill vs Decode 通信差异
| 维度 | Prefill | Decode |
| 输入形状 | [B, S_prompt, H] | [1, 1, H] 逐 token |
| 通信量 | 大 (S_prompt 长) | 小 (每次 1 token) |
| AllReduce 次数 | 每层 1 次 | 每层 1 次 |
| 瓶颈 | 带宽敏感 | 延迟敏感 |
Prefill 特点
- 一次性处理整个 prompt
- 序列长,通信量大
- 适合高带宽互联 (NVLink)
Decode 特点
- 逐 token 生成
- 每次通信量小但频繁
- 对延迟更敏感
💡 实际影响:Prefill 阶段 TP 通信开销明显,Decode 阶段相对较小。Decode 时如果用 Ring Attention,还需要额外传递 K/V 块。
📋 Data Parallel (DP) 通信详情
💡 核心:推理时无通信,训练时用 AllReduce 同步梯度
每个 GPU 持有完整模型副本,处理不同 batch。
通信内容
| 阶段 | 通信类型 | 传输内容 |
| 推理 | 无 | 无 |
| 训练 | AllReduce | 梯度 |
| DP Attention | AllReduce | K/V 激活 |
通信量
# 梯度同步
C_DP = model_size × bytes
# 7B 模型 (FP16): ~14 GB
# 70B 模型 (FP16): ~140 GB
# DP Attention (K/V)
C_DP_attn = 2 × B × S × H × bytes
✅ DP 推理优势:完全无 GPU 间通信!每个 GPU 独立处理请求,线性扩展吞吐量。
📋 DP Attention 的特殊通信
🔍 问题:跨 GPU 的 Attention 计算需要同步 K/V
当使用 DP Attention 时,不同 GPU 处理不同请求,但 Attention 需要完整的 K/V。
flowchart LR
Q0["🖥️ GPU0 Q"] --> A["🔢 Attention 计算"]
K0["🖥️ GPU0 K"] -->|"AllReduce"| Kfull["完整 K"]
V0["🖥️ GPU0 V"] -->|"AllReduce"| Vfull["完整 V"]
Q1["🖥️ GPU1 Q"] --> A
K1["🖥️ GPU1 K"] -->|"AllReduce"| Kfull
V1["🖥️ GPU1 V"] -->|"AllReduce"| Vfull
为什么需要?
- 不同 GPU 处理不同请求
- Attention 需要跨 batch 的 K/V
- DeepSeek 等 MLA 模型尤其需要
vLLM DP Attention
- KV Cache 分片存储
- 避免完整 KV 在 GPU 间复制
- 节省 MLA 模型内存
🔗 Pipeline Parallel (PP) 通信详情
💡 核心:阶段之间用 Send/Recv 传递激活值
PP 把不同层的 GPU 直连,只在边界传输数据。
flowchart LR
G0["🖥️ GPU0
Layer 0-19"] -->|"激活值"| G1["🖥️ GPU1
Layer 20-39"]
G1 -->|"激活值"| G2["🖥️ GPU2
Layer 40-59"]
G2 -->|"激活值"| G3["🖥️ GPU3
Layer 60-79"]
通信内容
| 方向 | 传输内容 | 大小估算 |
| Forward | 激活值 X_out | B × S × H_next |
| Backward | 梯度 | B × S × H_current |
通信量
# 单层传输
C_PP = B × S × H × bytes
# 仅在阶段边界传输
# 通信量远小于 TP
🔗 PP: Prefill vs Decode 通信差异
💡 Prefill 和 Decode 在 PP 中通信模式类似,但数据量差异大
| 维度 | Prefill | Decode |
| 传输数据 | 完整 prompt 激活 | 单 token 激活 |
| 通信频率 | 一次(完整序列) | N 次(N 个 token) |
| KV Cache | 生成并存储 | 读取历史 + 生成新 |
| 瓶颈 | 计算密集 | 访存密集 |
⚠️ PP 的问题:如果 Prefill 和 Decode 混在同一个 PP 流水线中,会相互阻塞——Prefill 的长序列会等待 Decode 的单 token 交互。
💡 解决方案:PD 分离!把 Prefill 和 Decode 分到不同的 GPU 集群。
👥 Expert Parallel (EP) 通信详情
💡 核心:MoE 模型用 AllToAll 路由 Token 到专家
每个 token 只激活 top-k 个专家,专家分布在不同 GPU。
sequenceDiagram
participant T as 📋 Token
participant R as 🎯 Router
participant G0 as 🖥️ GPU0
participant G1 as 🖥️ GPU1
T->>R: 选择 Expert-0, Expert-1
R->>G0: AllToAll Dispatch
R->>G1: AllToAll Dispatch
G0->>G0: Expert-0 计算
G1->>G1: Expert-1 计算
G0->>T: AllToAll Combine
G1->>T: AllToAll Combine
通信内容
# AllToAll 发送: Token 到 Expert GPU
# AllToAll 接收: 结果返回原 GPU
C_EP = 2 × tokens × topk × expert_size
EP 挑战
- 负载均衡(专家选择动态)
- AllToAll 通信量大
- 需要高速互联
👥 EP: Prefill vs Decode 通信差异
| 维度 | Prefill | Decode |
| Token 数量 | 长序列 (S 个) | 单 token |
| AllToAll 规模 | 大(同时路由多 token) | 小(单 token) |
| 通信量 | 大 | 小 |
| 负载均衡 | 更易不均衡 | 需更好均衡策略 |
📊 EP 通信量公式:
C_EP = 2 × batch × seq × topk × expert_size
# Prefill: seq = prompt_length (可能数千)
# Decode: seq = 1
💡 形象理解
Prefill 像一次派对上把几千人分配到不同桌子;Decode 像每次只来一个人去握手。通信量差异巨大!
🔀 Prefill-Decode (PD) 分离
💡 核心思想:把 Prefill 和 Decode 分到不同的 GPU 集群
因为它们的资源需求和通信模式完全不同!
flowchart LR
subgraph Prefill["📦 Prefill 集群"]
P["高算力 GPU
TP × DP"]
end
subgraph Transfer["⬇️ KV Cache 传输"]
KV["Network"]
end
subgraph Decode["🎯 Decode 集群"]
D["高带宽 GPU
大 Batch DP"]
end
P -->|"KV Cache"| KV
KV -->|"传输"| D
Prefill 集群特点
- 计算密集 (FLOPs 瓶颈)
- 需要 TP 提升算力
- 处理长序列 prompt
Decode 集群特点
- 带宽密集 (Memory BW 瓶颈)
- 需要大 batch DP
- 逐 token 生成
🔀 为什么需要 PD 分离?
Prefill 阶段
| 特性 | 描述 |
| 计算 | 矩阵乘法密集 |
| 序列 | 一次性处理整个 prompt |
| 瓶颈 | 算力 (FLOPs) |
| 内存 | 激活值大 |
Decode 阶段
| 特性 | 描述 |
| 计算 | 轻量但访存密集 |
| 序列 | 逐 token 生成 |
| 瓶颈 | 带宽 (Memory BW) |
| 内存 | KV Cache 访问 |
❌ 传统方式的问题:Prefill 和 Decode 混在同一个集群,互相干扰——长序列 Prefill 会阻塞 Decode 的低延迟响应。
✅ PD 分离的优势:
- 资源隔离:各用最适合的 GPU 配置
- 调度灵活:可分别优化
- 带宽利用率:Decode batch 可放到最大
🔀 PD 分离的节点间通信
📡 主要通信:KV Cache 传输
Prefill 完成后,需要把生成的 KV Cache 传输到 Decode 集群。
KV Cache 大小计算
# 单 token 单层
KV = 2 × num_heads × head_dim
= 2 × H (FP16)
# 完整序列
KV_total = B × S × 2 × H × num_layers
# 示例: LLaMA-7B, 4096 token, FP16
# H=8192, layers=32
# KV = 1 × 4096 × 2 × 8192 × 32
# ≈ 2 GB
通信优化
- 压缩传输: FP8 或更低精度
- 流水线: Prefill 输出后立即开始 Decode
- 部分传输: 只传必要部分
⚠️ 关键瓶颈:KV Cache 跨节点传输量巨大,是 PD 分离的主要开销。需要高速网络 (InfiniBand/NVLink Switch)。
🔀 PD 分离适用的并行策略
📦 Prefill 节点
| 并行方式 | 原因 |
| TP | 计算密集,提升单节点算力 |
| DP | 多 prompt 并行,提高吞吐 |
| PP | 超大模型可分层(较少用) |
🎯 Decode 节点
| 并行方式 | 原因 |
| DP | Decode batch 并行,吞吐关键 |
| EP | MoE 模型专用 |
| 序列并行 | 长上下文减少显存压力 |
💡 推荐配置:
- Prefill: TP × DP (高算力利用率)
- Decode: 纯 DP (最大化 batch size)
📊 通信量综合对比
| 策略 | 通信原语 | 传输内容 | 通信量级 |
| TP | AllReduce | 激活值/梯度 | O(B×S×H/P) |
| DP (训练) | AllReduce | 梯度 | O(model_size) |
| DP (推理) | 无 | 无 | 0 |
| PP | Send/Recv | 激活值 | O(B×S×H) |
| EP | AllToAll | Token + Expert 输出 | O(tokens×topk×expert) |
| PD 分离 | Network | KV Cache | O(B×S×H×layers) |
pie title 带宽需求排序
"TP" : 40
"EP" : 35
"PD分离 KV传输" : 30
"PP" : 20
"DP推理" : 0
🎯 并行策略选择指南
flowchart TD
A{"模型能放入
单GPU?"} -->|是| B{"需要更高
吞吐?"}
A -->|否| C{"是 MoE?"}
B -->|是| D["✅ DP (推理无通信)"]
B -->|否| E["单GPU运行"]
C -->|是| F["✅ EP + TP/DP"]
C -->|否| G{"需要
跨节点?"}
G -->|是| H["✅ TP + PP"]
G -->|否| I["✅ TP (NVLink)"]
📋 PD 分离特殊场景
| 场景 | 推荐方案 |
| 低延迟 Decode | PD 分离 + Decode 纯 DP |
| 长序列 Prefill | PD 分离 + Prefill TP×DP |
| 超长上下文 | PD 分离 + Decode 序列并行 |
💡 选择口诀
单卡能放 → DP(无通信)
大模型 → TP(AllReduce)
MoE → EP(AllToAll)
跨节点 → TP+PP
极低延迟 → PD 分离
📝 总结
📡 通信原语
- AllReduce: 归约 + 同步广播
- AllToAll: 全交换 (MoE 路由)
- Send/Recv: P2P (PP 阶段边界)
⚙️ 并行策略
| 策略 | 通信 | 特点 |
| TP | AllReduce | 层内分片 |
| DP | 无(推理) | 副本扩展 |
| PP | Send/Recv | 流水线 |
| EP | AllToAll | MoE专用 |
🔀 PD 分离核心价值
- Prefill: 计算密集 → TP × DP
- Decode: 带宽密集 → 大 Batch DP
- 分离后各自优化,效率最大化
📊 关键公式
TP: 2×B×S×H/P
EP: 2×tokens×topk×expert
PD: B×S×2×H×layers