vLLM 推理内核深度解析
第14章 分布式并行:TP / PP / EP / DP
第14章 分布式并行:TP / PP / EP / DP
“Divide and conquer, but make sure the pieces can talk to each other.” — 分布式系统的第一性原理
本章要点
- 理解分布式推理的根本矛盾:GPU 内部 HBM 带宽 (2 TB/s) vs 跨 GPU NVLink (600 GB/s) vs 跨机 InfiniBand (25 GB/s),三个数量级的带宽鸿沟决定了并行策略的取舍
- 读懂 Megatron 式张量并行的数学构造:列并行(Column Parallel)如何切 QKV 投影,行并行(Row Parallel)如何切 output projection + MLP down projection
- 掌握流水线并行的 1F1B 调度、微批次化、气泡率公式
- 理解 MoE 模型的专家并行(Expert Parallel, EP):为什么它是 DeepSeek-V3 / Mixtral 这类模型的刚需
- 看懂数据并行(DP)——最简单但最赚钱的一级:多副本、无状态同步、线性扩吞吐
- 读懂 V1 的
vllm/distributed/parallel_state.py如何用 4 组 ProcessGroup 管理 TP×PP×EP×DP 的四维拓扑 - 了解 Hopper 上 NCCL 的 SHARP / NVLS 原语,理解为什么 H100 的 all_reduce 比 A100 快 3-5×
- 掌握 Prefill/Decode 分离(Disaggregated Serving)这个 2024-2025 的新范式
- 拿到四个典型部署场景的
vllm serve配置样板 + 实测数字
14.1 分布式推理的根本矛盾:带宽鸿沟
在讲任何具体策略之前,必须先理解我们面对的硬件约束。一台 DGX H100 内部和 DGX 之间,带宽能差两个数量级:
| 层级 | 硬件 | 双向带宽 | 相对基准 |
|---|---|---|---|
| GPU 内 | HBM3 (H100) | 3.35 TB/s | 1×(基准) |
| GPU 间(同机,NVLink 5) | NVSwitch | 900 GB/s | 0.27× |
| GPU 间(同机,PCIe 5) | PCIe | 128 GB/s | 0.04× |
| 机间(InfiniBand NDR) | 400 Gbps | 50 GB/s | 0.015× |
| 机间(以太网 100G) | 100 GbE | 12.5 GB/s | 0.004× |
“分布式推理的第一性原理”就是:GPU 内部的计算和访存是免费的;跨 GPU 的通信必须精打细算;跨机器的通信要当宝贝。
每一种并行策略本质上都是在”把什么放在 GPU 内部计算 vs 什么需要通过通信传递”之间做取舍。四种主流策略(TP / PP / EP / DP)各自的通信特征:
graph TB
subgraph "四种并行的通信指纹"
TP["TP 张量并行<br/>每 Transformer 层 2 次 AllReduce<br/>每次传 tokens × hidden × dtype<br/>适合 NVLink"]
PP["PP 流水线并行<br/>阶段边界 1 次 P2P 传 activations<br/>通信稀疏但有 bubble<br/>适合跨机"]
EP["EP 专家并行<br/>MoE 每层 2 次 all_to_all<br/>通信量 ≈ tokens × hidden × top_k<br/>适合同机 NVLink"]
DP["DP 数据并行<br/>副本间完全独立<br/>零通信<br/>适合任何位置,线性扩吞吐"]
end
style TP fill:#3b82f6,color:#fff,stroke:none
style PP fill:#10b981,color:#fff,stroke:none
style EP fill:#f59e0b,color:#fff,stroke:none
style DP fill:#8b5cf6,color:#fff,stroke:none
读懂这张图就掌握了分布式部署的全局观。接下来我们逐个拆解。
14.1.1 四种并行策略的横向对比表
把 TP/PP/EP/DP 的关键属性放在一张表里对照——这是做部署决策的唯一凭据:
| 维度 | TP 张量并行 | PP 流水线并行 | EP 专家并行 | DP 数据并行 |
|---|---|---|---|---|
| 切分对象 | 单层权重(按列 / 按行) | 不同层分到不同 rank | MoE 的 expert 子模块 | 完整模型 N 份复制 |
| 通信原语 | 每层 2 次 AllReduce | 阶段边界 P2P send/recv | 每层 2 次 all_to_all | 无 |
| 通信频率 | 极高(80 层 × 2 = 160 次/step) | 低(PP-1 次/step) | 中(MoE 层数 × 2) | 零 |
| 单次通信量 | batch × hidden × dtype(KB 级) | batch × hidden × dtype(KB 级) | batch × hidden × top_k(MB 级) | 0 |
| 显存占用 | 1/TP × 模型 | 1/PP × 模型 | 非 expert 部分全量 + 1/EP × expert | 完整模型 × DP |
| 能否跨机 | 否(跨 IB AllReduce 拖垮 decode) | 是(P2P 通信稀疏) | 一般不跨(all_to_all 怕 IB) | 可以(本来就无通信) |
| 气泡/空转 | 无 | 有,公式 (PP-1)/(PP-1+M) | 无(但有负载不均) | 无 |
| 吞吐扩展性 | 亚线性(通信拖累) | 亚线性(bubble 拖累) | 亚线性(all_to_all) | 线性(瓶颈只在前端 LB) |
| 延迟影响 | 降单请求延迟 | 增单请求延迟 | 略增延迟 | 不影响单请求延迟 |
| vLLM 源码位置 | model_executor/layers/linear.py ColumnParallelLinear/RowParallelLinear | worker/worker.py + distributed/parallel_state.py send/recv | model_executor/layers/fused_moe/ | v1/executor/mp_executor.py DPAsyncMPClient |
| 典型使用场景 | 单机大模型(70B / 405B) | 跨机超大模型(671B+) | MoE 模型(DeepSeek-V3、Mixtral) | 高并发无状态 chat |
关键取舍经验法则:
- 降延迟用 TP——TP 把单个 forward 的算力摊到多卡、单 token 延迟降到 1/TP
- 扩模型用 PP——PP 把模型层数分摊、显存占用降到 1/PP、但延迟增加(PP 串行累加)
- 扩吞吐用 DP——DP 线性扩副本、延迟不变、吞吐翻倍
- MoE 用 EP——不用 EP 的话 expert 全复制、显存立刻爆
选型决策树(从上往下问):
flowchart TB
Q1{"单卡显存能放下?"}
Q2{"单机总显存能放下?"}
Q3{"是 MoE 模型?"}
Q4{"需要跨 AZ / 高可用?"}
A1[DP=N 横向扩<br/>单卡单副本,最简单]
A2[TP=单机卡数<br/>纯 TP 吃满 NVLink]
A3[TP=单机卡数 + EP=单机卡数<br/>专家和张量双维切分]
A4[TP × PP × DP<br/>机内 TP,机间 PP,外层 DP]
A5[TP × PP 跨机<br/>DP 默认 1]
Q1 -->|是| A1
Q1 -->|否| Q2
Q2 -->|是| Q3
Q3 -->|是| A3
Q3 -->|否| A2
Q2 -->|否| Q4
Q4 -->|是| A4
Q4 -->|否| A5
style A1 fill:#8b5cf6,color:#fff,stroke:none
style A2 fill:#3b82f6,color:#fff,stroke:none
style A3 fill:#f59e0b,color:#fff,stroke:none
style A4 fill:#10b981,color:#fff,stroke:none
style A5 fill:#10b981,color:#fff,stroke:none
这张决策树串起了全章逻辑——每个叶节点都对应 14.9 节的一份 vllm serve 配置样板。
14.1.2 与第 1 章架构总图的串联
第 1 章给出过 vLLM V1 的整体分层:LLMEngine → EngineCore → Executor → Worker → ModelRunner → AttentionBackend。分布式发生在哪一层?
| 层 | 是否感知分布式 | 注释 |
|---|---|---|
LLMEngine / EngineCore | 不感知 | 只产出 SchedulerOutput、不知道下游有几个 Worker |
Executor | 感知 | MultiprocExecutor / RayDistributedExecutor 的职责就是把任务分发到多 Worker |
Worker | 部分感知 | 知道自己的 tp_rank / pp_rank / dp_rank、但不知道总拓扑 |
ModelRunner | 不感知 | 只负责 forward 一次、分布式是 model 层面(ColumnParallelLinear)的事 |
AttentionBackend | 基本不感知 | 只要 QKV 已经按 TP 切好、attention 计算本身和单卡相同 |
关键抽象:分布式边界正好落在 Executor ↔ Worker 之间——上面(Engine/Scheduler)完全单机视角、下面(Worker/ModelRunner)知道自己是多卡的一份子。这是 vLLM 整个代码库保持清晰的核心工程决策——单机调试和千卡生产共享 99% 代码路径。
对照第 1 章的架构图:TP/PP/EP 在 ColumnParallelLinear + parallel_state.py 级别实现(Worker 内部)、DP 在 Executor 级别实现(Executor 外层)——两种并行在代码栈里处于完全不同的层。读懂这点、第 14 章所有代码导航就都有了锚点。
14.2 张量并行(TP):Megatron 式矩阵切分
张量并行的数学基础来自 Megatron-LM(Shoeybi et al., 2019 / arXiv:1909.08053)。核心洞察:矩阵乘法 可以按 W 的列或行切分,再通过通信合并结果。
14.2.1 列并行(Column Parallel)
把 W 沿”输出维度”(列)切成 P 份:
每个 GPU 拿一片 的列,输入 在所有 GPU 上相同(已经通过前面的 AllReduce 同步)。计算:
输出 天然按列切分——GPU i 持有 的第 i 块列。无需通信。
vLLM 的 ColumnParallelLinear:
# vllm/model_executor/layers/linear.py(概念性简化)
class ColumnParallelLinear(LinearBase):
def __init__(self, input_size: int, output_size: int, ...):
self.tp_size = get_tensor_model_parallel_world_size()
# 每张卡持有 output_size / tp_size 列
self.output_size_per_partition = output_size // self.tp_size
# 权重 shape: [output_size_per_partition, input_size]
self.weight = Parameter(torch.empty(
self.output_size_per_partition, input_size,
dtype=params_dtype,
))
def forward(self, input_):
# 每张卡独立计算自己那片 W_i @ X
output = F.linear(input_, self.weight)
# gather_output 通常为 False —— 保持输出列切分,给下一层用
return output
14.2.2 行并行(Row Parallel)
把 W 沿”输入维度”(行)切成 P 份:
输入 X 也按列切分成 P 份:
每个 GPU 计算自己的 ,结果是完整 的部分和。最后必须通过 AllReduce 把所有 P 片加起来——这就是 TP 每层都要做的那个 AllReduce。
vLLM 的 RowParallelLinear:
class RowParallelLinear(LinearBase):
def __init__(self, input_size: int, output_size: int, ...):
self.tp_size = get_tensor_model_parallel_world_size()
# 每张卡持有 input_size / tp_size 行
self.input_size_per_partition = input_size // self.tp_size
self.weight = Parameter(torch.empty(
output_size, self.input_size_per_partition,
))
def forward(self, input_):
# 每张卡计算自己的 X_i @ W_i
output_parallel = F.linear(input_, self.weight)
# AllReduce 聚合所有 rank 的部分和
output = tensor_model_parallel_all_reduce(output_parallel)
return output
14.2.3 Transformer 层的切分方式
Transformer 的每个 decoder block 由两个子模块构成:Attention 和 MLP。Megatron 的切分:
graph TB
subgraph "Self-Attention"
A1[LayerNorm]
A2["QKV Projection<br/>ColumnParallel<br/>[hidden] → [3 × num_heads/P × head_dim]"]
A3["Attention<br/>(每个 rank 处理自己的 heads)"]
A4["Output Projection<br/>RowParallel<br/>[num_heads/P × head_dim] → [hidden]<br/>+ AllReduce"]
A1 --> A2 --> A3 --> A4
end
subgraph "MLP"
M1[LayerNorm]
M2["Gate/Up Projection<br/>ColumnParallel<br/>[hidden] → [intermediate/P]"]
M3["Activation (SiLU/GELU)"]
M4["Down Projection<br/>RowParallel<br/>[intermediate/P] → [hidden]<br/>+ AllReduce"]
M1 --> M2 --> M3 --> M4
end
A4 --> M1
style A2 fill:#3b82f6,color:#fff,stroke:none
style A4 fill:#10b981,color:#fff,stroke:none
style M2 fill:#3b82f6,color:#fff,stroke:none
style M4 fill:#10b981,color:#fff,stroke:none
关键模式:Column 在先、Row 在后。Column 的输出天然切分,直接进入下一步 Row,中间不需要通信;Row 结束时做一次 AllReduce 把 hidden 重新聚回到所有 rank 上。
每个 Transformer block 有 2 次 AllReduce:一次在 Attention 后,一次在 MLP 后。
14.2.4 AllReduce 的通信量
单次 AllReduce 传 batch_tokens × hidden_dim × dtype_size 字节。对 Llama-3-70B(hidden=8192,FP16)、batch_tokens=1(单 decode step):
80 层 × 2 次 = 160 次 AllReduce / step / request。每次 AllReduce 的启动开销(latency-bound part)在 NVLink 上约 1-2 μs:
相比 decode step 本身的 20-80 ms,只占 0.5-1.6%。TP 在 NVLink 域内几乎免费。
但在跨机器(InfiniBand)场景下,AllReduce 启动开销变成 10-50 μs:
占 step 时间的 10-25%——开始显著拖累。
这就是”TP 只能跨 NVLink,不能跨机”的根本原因。
14.3 流水线并行(PP):阶段切分 + 微批次
14.3.1 核心思想
PP 把模型的不同层放在不同 GPU 上。对 Llama-70B 的 80 层,PP=4 可以切成 [L1-L20, L21-L40, L41-L60, L61-L80]。
每个 decode step 的数据流:
sequenceDiagram
participant G0 as GPU 0 (L1-20)
participant G1 as GPU 1 (L21-40)
participant G2 as GPU 2 (L41-60)
participant G3 as GPU 3 (L61-80)
Note over G0: step t=1
G0->>G1: activations [batch, 20 layers 后]
G1->>G2: activations
G2->>G3: activations
G3-->>G0: sampled tokens (下一步输入)
Note over G0,G3: step t=2, 重复
每个阶段边界发一次点对点消息——体积是 batch_tokens × hidden × dtype_size。对 70B 模型一个 batch:,每 step 4 次(3 个边界 + 最后回绕采样结果)= 64 KB/step。
对比 TP=4 每 step 160 次 AllReduce × 16 KB = 2.5 MB/step。
PP 的通信量比 TP 小 40 倍——这是它能跨机部署的底气。
14.3.2 气泡(Bubble)问题
PP 的阻碍在于 bubble——当一个阶段在计算时其他阶段在等待。看一个 PP=4 naive 调度:
Time: t=0 t=1 t=2 t=3 t=4
GPU 0: forward L1-20 | idle (waiting L21-80 done) | forward step2
GPU 1: idle | forward L21-40 | idle | idle | forward step2
GPU 2: idle | idle | forward L41-60 | idle | forward step2
GPU 3: idle | idle | idle | forward L61-80 | forward step2
GPU 利用率 = 1/4 = 25% (极差!)
解决方法:微批次化(micro-batching)。把一个 batch 切成 M 个 micro-batch 交错流经流水线:
gantt
title PP=4, 4 个 micro-batch 的 1F1B 调度(只示意 forward)
dateFormat X
axisFormat %L
section GPU 0
mb1 :0, 10
mb2 :10, 20
mb3 :20, 30
mb4 :30, 40
section GPU 1
mb1 :10, 20
mb2 :20, 30
mb3 :30, 40
mb4 :40, 50
section GPU 2
mb1 :20, 30
mb2 :30, 40
mb3 :40, 50
mb4 :50, 60
section GPU 3
mb1 :30, 40
mb2 :40, 50
mb3 :50, 60
mb4 :60, 70
在稳态阶段(所有阶段都有活干),GPU 利用率 100%。只有启动期和收尾期有空闲,bubble 占比:
- M=1:bubble = 3/4 = 75% 😱
- M=4:bubble = 3/7 = 43%
- M=8:bubble = 3/11 = 27%
- M=16:bubble = 3/19 = 16%
理论上 M 越大越好。实际限制:每个 micro-batch 至少要有一个 request,否则就是空流水。vLLM 的 V1 借助连续批处理天然提供了 micro-batch 源——正在跑的 64 个 decode 请求可以自然地切成 8 组 × 8 micro-batch。
14.3.3 什么时候用 PP
PP 只在必须跨机、且模型放不下时才用:
- 模型 > 单机 GPU 总显存(比如 DeepSeek-V3 671B 需要 >= 2 台 8×H100 主机)
- 想用更多副本应对极高并发(见 DP)
- 跨地域 / 跨 AZ 部署(需要备份副本)
如果模型能在单机放下,纯 TP 几乎总是更好——PP 的 bubble 再小也是纯损失。
14.4 专家并行(EP):MoE 模型的刚需
14.4.1 MoE 的特殊需求
MoE(Mixture of Experts)架构下每层有若干”专家”子模块。每个 token 只激活 top_k 个专家(通常 k=2 或 k=8)。这导致一个特殊挑战:
- 专家数量可能很多(DeepSeek-V3 有 256 个专家 / MoE 层)
- 每个专家是一个 MLP(参数量大)
- 如果每个 GPU 复制所有专家(就像 TP 那样),显存完全装不下
解法:把不同专家分给不同 GPU——这就是专家并行。
14.4.2 all_to_all 通信
但这带来一个新问题:token 要去它激活的专家所在的 GPU 上算,算完再回来。这需要两次 all_to_all 通信:
graph TB
subgraph "Step 1: Router 决定 token → expert 映射"
R[Router Gate<br/>每 token 选 top_k 专家]
end
subgraph "Step 2: All-to-All Dispatch"
D["把每个 token 的拷贝<br/>发到它对应专家所在的 GPU"]
end
subgraph "Step 3: Expert Forward"
E1[GPU 0: Expert 0,1,...,31 计算]
E2[GPU 1: Expert 32,...,63 计算]
E3[GPU 2: Expert 64,...,95 计算]
E4[GPU 3: Expert 96,...,127 计算]
end
subgraph "Step 4: All-to-All Combine"
C["每个 token 把在各专家的结果拿回原 GPU<br/>加权求和"]
end
R --> D --> E1 & E2 & E3 & E4 --> C
style D fill:#f59e0b,color:#fff,stroke:none
style C fill:#f59e0b,color:#fff,stroke:none
通信量:每 token 在每次 all_to_all 中传 hidden × dtype_size × top_k 字节。对 DeepSeek-V3(hidden=7168, k=8, FP8)、batch=32:
每 MoE 层 2 次 all_to_all = 3.6 MB。DeepSeek-V3 有 58 层 MoE,总 ≈ 209 MB / step。在 NVLink 900 GB/s 下耗时 0.23 ms——完全可以接受。
但如果跨机器(InfiniBand 50 GB/s)就 4 ms——显著拖累。所以 EP 通常只在同机内用。
14.4.3 vLLM 的 EP 支持
V1 对 EP 的支持在 vllm/distributed/device_communicators/ + vllm/model_executor/layers/fused_moe/:
vllm serve deepseek-ai/DeepSeek-V3 \
--tensor-parallel-size 8 \
--enable-expert-parallel \
--max-model-len 8192
启用后,--tensor-parallel-size 8 同时被解释为 EP=8:每张卡拿 256/8 = 32 个专家。非专家的部分(attention、KV Cache、layer norm)还是 TP 切分。
EP 的实现细节非常讲究——vllm/model_executor/layers/fused_moe/ 里有专门的 fused kernel 把 router → dispatch → expert → combine 融合到尽量少的 kernel launch,这在 MoE 推理效率上是决定性的。
14.5 数据并行(DP):最赚钱的那一级
DP 和上面三种完全不同——它不切模型,而是完整地复制多份。N 个 DP rank 各自是一个独立的 vLLM 实例,请求通过前端 Load Balancer 分发到不同 rank。
graph TB
LB[Load Balancer<br/>nginx / envoy / DPAsyncMPClient]
subgraph "DP Rank 0"
R0[完整 Llama-70B<br/>TP=4 in 4 GPUs]
end
subgraph "DP Rank 1"
R1[完整 Llama-70B<br/>TP=4 in 4 GPUs]
end
subgraph "DP Rank 2"
R2[完整 Llama-70B<br/>TP=4 in 4 GPUs]
end
LB --> R0
LB --> R1
LB --> R2
note["DP 之间完全无通信<br/>线性扩展吞吐<br/>支持独立扩缩容"]
style R0 fill:#8b5cf6,color:#fff,stroke:none
style R1 fill:#8b5cf6,color:#fff,stroke:none
style R2 fill:#8b5cf6,color:#fff,stroke:none
style note fill:#10b981,color:#fff,stroke:none
14.5.1 DP 的好处
- 零通信开销——各个 rank 完全独立,没有任何同步点
- 线性扩展吞吐——N 个副本 = N 倍吞吐(只要 LB 够聪明)
- 独立扩缩容——忙时 HPA 加副本,闲时缩回
- 故障隔离——一个 rank 崩不影响其他 rank
14.5.2 DP 的代价
- 显存冗余——每个副本都要完整装一份模型
- 没有前缀缓存共享——副本间的 KV 不互通
- LB 路由决定均衡度——sessionAffinity 处理不好会让前缀缓存命中率降低
14.5.3 DP 与 TP/PP/EP 的叠加
DP 可以叠加在任何一种之上。一个 DP=2 × TP=4 的部署:
- 共 8 张 GPU
- 2 个独立副本
- 每个副本内部做 4 卡 TP
前端把请求分发到两个副本,每个副本内部同步做 TP。这是生产环境最常见的拓扑——DP 扩吞吐,TP 扩单实例能力。
vLLM V1 对 DP 的原生支持在 DPAsyncMPClient(第 6 章讲过的 Executor):
vllm serve meta-llama/Llama-3-70B \
--tensor-parallel-size 4 \
--data-parallel-size 2 \
--distributed-executor-backend mp
自动 fork 2 组 Worker 子进程(每组 4 个 TP rank)。前端 request 通过 --data-parallel-rpc-port 协议路由到合适的 DP rank。
14.6 parallel_state.py:四维拓扑的进程组管理
V1 的 vllm/distributed/parallel_state.py 用多个 torch.distributed.ProcessGroup 管理复杂的并行拓扑。对 DP=2 × PP=2 × TP=4 这样的拓扑(总 16 卡):
Rank layout (total_world_size = 16):
Rank 0-7: DP rank 0, PP=0,1 each with TP=4
Rank 8-15: DP rank 1, PP=0,1 each with TP=4
Process Groups:
TP groups (each of size 4):
[0,1,2,3], [4,5,6,7], [8,9,10,11], [12,13,14,15]
PP groups (each of size 2):
[0,4], [1,5], [2,6], [3,7], [8,12], [9,13], [10,14], [11,15]
DP groups (each of size 2) — used for DP coordination:
[0,8], [4,12], ...
World group: all 16
每个 rank 同时属于多个组:
# vllm/distributed/parallel_state.py(简化)
def init_distributed_environment(...):
# 初始化全局 process group
torch.distributed.init_process_group(backend="nccl", ...)
# 构建子组
for tp_group_ranks in get_tp_groups(world_size, tp_size):
torch.distributed.new_group(tp_group_ranks, backend="nccl")
for pp_group_ranks in get_pp_groups(...):
torch.distributed.new_group(pp_group_ranks, backend="nccl")
# ... EP、DP 类似
AllReduce 调用时指定 group,就只在那个组内做:
# TP 内的 AllReduce —— 只在 TP group 里
output = tensor_model_parallel_all_reduce(output_parallel)
# PP 边界 send/recv —— 只在 PP group 里
send_to_next_pp_rank(activations)
recv_from_prev_pp_rank()
这种”一个 rank 同时是多个组的成员”的设计让多维并行变得可组合。想换成 DP=4 × TP=8?改配置参数,其他一律不动。
14.6.1 GroupCoordinator 真身:device_group + cpu_group 双通道
上一节伪代码直接用了 torch.distributed.new_group 和 new_group。真实 vLLM 不会裸用——它把所有组包在一个叫 GroupCoordinator 的类里(vllm/distributed/parallel_state.py:173),每个逻辑组对应两个真实 process group:
# parallel_state.py:220
for ranks in group_ranks:
device_group = torch.distributed.new_group(
ranks, backend=torch_distributed_backend) # 通常 NCCL,走 GPU
# a group with `gloo` backend, to allow direct coordination between
# processes through the CPU.
cpu_group = torch.distributed.new_group(ranks, backend="gloo")
if self.rank in ranks:
self.ranks = ranks
self.world_size = len(ranks)
self.rank_in_group = ranks.index(self.rank)
self.device_group = device_group
self.cpu_group = cpu_group
双通道设计是关键工程选择:
device_group(NCCL 后端) —— 大张量走 GPU 直通、打满 NVLink/NVSwitch 带宽。这是 AllReduce、Broadcast activations 等数据平面操作的通路。cpu_group(Gloo 后端) —— 小对象走 CPU 端点对点。source 注释原文:“to allow direct coordination between processes through the CPU”——控制平面操作(比如 “所有 rank 同步一个 int 告诉对方 scheduler 这一拍空跑”)用它更便宜。NCCL 大 tensor 优化、Gloo 小对象灵活,没必要把控制信号塞进 NCCL 走 GPU 一圈。
两个组基于同一批 ranks 构建,保证”TP 的 NCCL 组”和”TP 的 Gloo 组”永远覆盖同一组进程——物理拓扑一致、只是用不同 protocol。
14.6.2 三层 rank 体系:global / local / in-group
GroupCoordinator 还暴露了三种不同的 rank 概念——这是多维并行里最容易搞混的部分。源码 docstring(line 187-193)直接给了一张表:
Process | Node | Rank | Local Rank | Rank in Group
0 | 0 | 0 | 0 | 0
1 | 0 | 1 | 1 | 1
2 | 1 | 2 | 0 | 2
3 | 1 | 3 | 1 | 3
rank(全局 rank)——torch.distributed.get_rank()得到的那个、0..world_size-1。唯一标识跨所有节点的一个进程。local_rank——本节点内的 rank,用来绑 GPU 设备 ID(torch.device(f"cuda:{local_rank}"))。进程 0 和进程 2 都有local_rank=0因为它们各自是节点 0 和节点 1 的第一个进程。rank_in_group——本 GroupCoordinator 视角下的 rank。如果这 4 个进程构成一个 TP group、rank_in_group 就是 0/1/2/3;如果它们同时属于另一个 PP group、PP group 的 rank_in_group 可能完全不同。
日常编程错误最多的一处:误用 rank 当成 rank_in_group 去 index 某个本地 tensor。比如 tensor[self.rank] 在 world_size=16 的场景下 rank=14 但 TP group 里该进程的 rank_in_group 只有 2——下标越界。如果你看懂了这三层 rank、多维并行代码就瞬间变得可读。
14.6.3 group name 和 torch.ops.vllm.all_reduce 定制算子
GroupCoordinator.__init__ 里这两行:
self.unique_name = _get_unique_name(group_name)
_register_group(self)
——把每个组注册到一个全局 dict,key 是 unique_name。lookup 入口在 line 100-109:
def all_reduce(tensor: torch.Tensor, group_name: str) -> torch.Tensor:
# 按 group_name 找到对应 GroupCoordinator 再调它的方法
group = get_group_by_name(group_name)
return group._all_reduce_out_place(tensor)
为什么要搞 “按名字 lookup”? 因为 vLLM 把 all_reduce 注册成 torch 自定义算子(torch.ops.vllm.all_reduce)——注册代码在 line 151-154:
op_name="all_reduce",
op_func=all_reduce,
...
fake_impl=all_reduce_fake,
这样 torch.compile 在 trace 模型时能把 all_reduce 当成一个算子符号保留下来——不会把它”穿透”到 NCCL 的 Python wrapper 里把 trace 弄坏。fake_impl(all_reduce_fake)在 symbolic shape inference 时被调用——返回和真实输出同 shape 的 fake tensor、让 torch.compile 的 shape propagator 能顺利算出下游形状。
Process group 对象无法直接序列化进 FX graph(它们是 C++ binding 的 opaque 对象)、但 group_name: str 能序列化——所以算子签名是 (tensor, group_name: str) 而不是 (tensor, process_group)。跟随 name 再在算子实现里 lookup 是**“可序列化 token + 运行时 dict 查找”的标准模式**——和 Python 里 weakref.ref 到对象注册表是同种手法。
这种细节是 vLLM 能利用 PyTorch 2 torch.compile 全图优化但又不放弃 NCCL 通信的工程关键——通信算子和计算算子在同一张 FX graph 里、编译器能看到完整数据流。没这个 trick 你要么牺牲 torch.compile 优化、要么牺牲分布式通信——vLLM 用 GroupCoordinator 的名字注册机制把两者缝合在一起。
14.6.4 initialize_model_parallel() 的 4D 拓扑构造
读源码最震撼的一幕在 parallel_state.py:915-1008——vLLM 用一个 4D 的 torch.arange + transpose + reshape + unbind 把 world_size 切成 TP/PP/DP 三组 group_ranks:
# parallel_state.py:958 注释原文:"the layout order is: ExternalDP x DP x PP x TP"
all_ranks = torch.arange(world_size).reshape(
-1, data_parallel_size, pipeline_model_parallel_size,
tensor_model_parallel_size)
# 切 TP 组:把 TP 维(最后一维)留在最后,flatten 前面所有维度
group_ranks = all_ranks.view(-1, tensor_model_parallel_size).unbind(0)
# 切 PP 组:transpose(2,3) 把 PP 维搬到末尾,再 reshape
group_ranks = all_ranks.transpose(2, 3).reshape(-1, pipeline_model_parallel_size).unbind(0)
# 切 DP 组:transpose(1,3) 把 DP 维搬到末尾
group_ranks = all_ranks.transpose(1, 3).reshape(-1, data_parallel_size).unbind(0)
三点非显然的细节——
- 注释里
ExternalDP是第 4 维——-1代表 verl 集成时的外层 DP(跨 vLLM 实例),本进程组不参与,但 rank 张量要预留一维给它 - 只建了 TP/PP/DP 三个 group,没有 EP——EP 是 MoE 模型才有的运行时拓扑、由
init_model_parallel_group在 MoE 层里另行创建、不属于”基础 3 维” - 只有 TP 组开启
use_message_queue_broadcaster=True——因为 TP 通信最频繁、消息队列 broadcaster(基于 shm_broadcast.py 的 ring buffer)比默认的 torch.distributed.broadcast 快一个数量级——PP/DP 通信稀疏、不值得额外优化
initialize 之后设 3 个全局变量 _TP、_PP、_DP——destroy_model_parallel() 按照与创建相反的顺序销毁(LIFO)——这一点和一般 RAII 不同、是 NCCL 通信组销毁有依赖性的体现。
14.6.5 device_communicators/ 13 个文件:一张接口 + 七种后端
vllm/distributed/device_communicators/ 目录总计 2483 行——按抽象层与后端切开——
| 文件 | 行 | 角色 |
|---|---|---|
base_device_communicator.py | 151 | 抽象接口 DeviceCommunicatorBase——定义 all_reduce / all_gather / broadcast / send / recv 五个抽象方法 |
cuda_communicator.py | 131 | NVIDIA GPU 后端——wrap torch.distributed + 集成 CustomAllreduce |
cpu_communicator.py | 139 | CPU 推理后端——走 Gloo backend |
tpu_communicator.py | 93 | Google TPU 后端——走 XLA |
hpu_communicator.py | 45 | Intel Gaudi 后端 |
xpu_communicator.py | 54 | Intel Xe GPU 后端 |
neuron_communicator.py | 19 | AWS Trainium/Inferentia 后端——19 行是全仓最小的 communicator、因为 Neuron SDK 自己处理了几乎所有通信 |
custom_all_reduce.py | 301 | 小数据 P2P AllReduce——≤ 8 卡、数据 ≤ 8MB 时,绕过 NCCL、用 CUDA IPC + 自实现 reduce kernel、比 NCCL 快 2x |
custom_all_reduce_utils.py | 257 | custom AR 的 P2P capability 探测 + IPC handle 管理 |
pynccl.py + pynccl_wrapper.py | 217 + 340 | 直接调 NCCL C API(绕过 torch.distributed)——vLLM 特殊场景用、避免 torch 的 stream 同步开销 |
shm_broadcast.py | 557 | 本机 broadcast 走 POSIX shm ring buffer——不走 NCCL、因为 NCCL 的 broadcast 对小消息开销过大 |
cuda_wrapper.py | 179 | CUDA runtime API 的 ctypes wrapper(用于检测 P2P 能力等) |
两条非显然的设计原则——
- 小消息不走 NCCL——
shm_broadcast.py的ShmRingBuffer用共享内存 ring buffer + reader-flag 字节实现无锁本机 broadcast——因为 TP 组里 broadcast 一个 sampling 参数只有几十字节、走 NCCL 会每次 10+ us 启动开销、走 shm 只要 sub-us - 小数据 AllReduce 专门走 custom_all_reduce——≤ 8MB 的 AllReduce(TP 每层都触发一次、频率最高)用 CUDA IPC + 一个 handwritten kernel——实测比 NCCL 快 2x——这是 vLLM 能把 70B 模型推理延迟压到 NCCL 水平以下的关键
14.7 Hopper 上的 NCCL 加速:SHARP 与 NVLS
2023-2024 的 Hopper 架构(H100/H200)给 NCCL 带来了两个改变游戏规则的新原语:
14.7.1 SHARP(Scalable Hierarchical Aggregation Reduction Protocol)
传统 all_reduce = reduce-scatter + all_gather,两个阶段,每个阶段的数据都走完一遍网络。N 张 GPU 做 all_reduce,每张 GPU 需要发送 2(N-1)/N × data_size 字节。
SHARP 把 all_reduce 的规约部分offload 到交换机(switch)上——数据在交换机里就聚合了,不需要送回每个 GPU 再聚合。通信量减半,延迟降 40-60%。
NVIDIA 的 NVSwitch(H100 DGX 的互联芯片)原生支持 SHARP。vLLM 在 H100 上跑 all_reduce 时自动启用,不需要用户配置。
14.7.2 NVLS(NVLink SHARP for NVLink domain)
跨 NVSwitch(比如 2 个 DGX 之间)需要 NVLink 跨箱互联(NVLink 5 支持 72 张 GPU 的 NVLink 域)。NVLS 在这个扩展域内也能做 in-network reduction。
结合 SHARP + NVLS,H100 64 卡 all_reduce 的延迟只有 A100 NVLink 域内的 1/3。大模型部署(> 16 卡)从此变得可行。
14.7.3 vLLM 的 NCCL 后端配置
默认 NCCL 已经足够好,但极端场景有几个可调旋钮:
# 启用 SHARP(H100+)
export NCCL_ALGO=Tree,Ring
export NCCL_COLLNET_ENABLE=1
# 禁用某些可能慢的策略
export NCCL_P2P_LEVEL=NVL # 仅通过 NVLink P2P,禁用 PCIe fallback
# debug
export NCCL_DEBUG=INFO # 启动时打印 NCCL 拓扑探测结果
vLLM 在 V1 下面的 NCCL 封装在 vllm/distributed/device_communicators/pynccl.py(对 PyTorch NCCL 的直接 C API 封装),因为某些场景 PyTorch NCCL 的 Python GIL 会拖慢通信。
14.7.4 custom_all_reduce 的边界条件
vllm/distributed/device_communicators/custom_all_reduce.py(截至 main 2026-04,共 340 行)——这是 vLLM 小数据 AllReduce 专用的”逃生通道”。CustomAllreduce 类(line 43-340)只在全部四个条件都满足时才启用:
| 条件 | 取值 | 理由 |
|---|---|---|
world_size | ∈ {2, 4, 6, 8}(_SUPPORTED_WORLD_SIZES) | handwritten kernel 只针对这几种 shape 做了展开 |
| 消息大小 | ≤ 8 MB(max_size=8192*1024) | 超过这个阈值 NCCL ring/tree 算法更优 |
| P2P 能力 | 所有 rank 两两可 cudaIPC | 没 NVLink / 没 P2P fallback 用 NCCL |
| 张量连续 | tensor.is_contiguous() | kernel 只处理线性 memory |
should_custom_ar(line 183-195)是逐次调用检查——每次 AllReduce 都动态判断、不符合就自动退回 torch.distributed.all_reduce。这是保守设计——对用户完全透明、出错就 fallback。
__init__(line 46-149)做了一件非常重的事——预注册所有 rank 的 CUDA IPC handle。每张 GPU 分一块共享 buffer、其他 rank 通过 cudaIpcOpenMemHandle 拿到指针——这样 reduce kernel 可以直接 ld.global 其他卡的显存、不经过任何 host 跳板。
这就是为什么 custom_all_reduce 能比 NCCL 快 2×——NCCL 的 ring 算法每步都要 cudaMemcpyAsync 走 P2P channel、有内核启动 + stream sync 开销;custom 版本只启动一次 kernel、所有 rank 直接读写对方显存。
但也因此有硬限制——kernel 最多处理 8 个 rank、超过会有 shared memory / register 压力、性能反而比 NCCL 差。这就是 _SUPPORTED_WORLD_SIZES 不写死成 {2,4,6,8,10,12,…} 的原因。
14.7.5 CUDA Graph 捕获下的特殊处理
vLLM 的 decode 路径几乎全部跑在 CUDA Graph 里(第 9 章详细讲过 piecewise CUDA graph)——这对分布式通信带来一个挑战:AllReduce 也必须能被 graph 捕获。
NCCL 原生支持 stream capture(cudaStreamBeginCapture 之间发的 NCCL 调用会被记录)——但有个坑:NCCL 在 capture 期间不能动态选算法(tree / ring / NVLS)、必须提前选定。
custom_all_reduce.py:151-161 的 capture context manager 就是为了处理这个:
@contextmanager
def capture(self):
# 进入 capture 前:lock-in 当前 graph 拓扑
# 退出后:注册所有被 capture 进 graph 的 buffer
yield
self.register_graph_buffers()
register_graph_buffers(line 163-181)把 capture 阶段分配的临时 buffer 在重放(replay)时继续使用——否则下次 replay 时 buffer 地址变了、IPC handle 失效。
这些细节对普通用户完全不可见、但它是 vLLM 在 H100 上 decode 延迟能压到 10 ms 级的关键——没有 CUDA Graph 通信至少多 30-50%。
14.8 Prefill / Decode 分离:2024-2025 新范式
传统部署把 prefill 和 decode 放在同一组 GPU 上。2024 年 DistServe、Splitwise 等论文提出了分离架构:
graph LR
subgraph "传统架构"
U1[用户] --> T1[vLLM 引擎<br/>prefill + decode 混合]
end
subgraph "分离架构"
U2[用户] --> LB[Router]
LB --> P[Prefill 专用引擎<br/>高算力 GPU 如 H100]
P -->|KV Cache 传输| D[Decode 专用引擎<br/>高带宽 GPU 如 H200]
D --> U2
end
style P fill:#3b82f6,color:#fff,stroke:none
style D fill:#10b981,color:#fff,stroke:none
14.8.1 动机
- Prefill 是 compute-bound,吃算力(H100 的 TensorCore 利用率可到 80%)
- Decode 是 memory-bound,吃带宽(H200 的 HBM3e 141 GB 带宽 4.8 TB/s 比 H100 翻倍)
- 传统架构下两个形态互相干扰(chunked prefill 虽然缓解了,但 GPU 的两种特性还是没用到极致)
分离后:
- Prefill 集群专门跑 H100(算力型)
- Decode 集群专门跑 H200(带宽型)
- 请求先走 prefill,KV Cache 传给 decode 集群继续生成
14.8.2 KV Cache 传输成本
Llama-70B, context=1000:
KV size = 2 × 80 layers × 8 heads × 128 dim × 1000 tokens × 2 bytes
= 328 MB per request
RDMA 100 Gbps ≈ 12.5 GB/s,传 328 MB = 26 ms。
对比 prefill 本身:A100 上 1000 token 的 prefill 约 50-100 ms。传输耗时约是 prefill 时间的 1/4——可以接受,且能通过 RDMA write 叠加到 prefill 的最后一层 forward 里(overlap)。
14.8.3 vLLM 的实现
vllm/distributed/kv_transfer/ 下是 KV 跨机传输的基础设施:
kv_connector/定义了传输的抽象(类似 Executor 的抽象层)- 具体实现有 MooncakeConnector(基于 mooncake 分布式 KV 存储)、NixlConnector(NVIDIA NIXL)等
这是一个活跃发展的方向,2025 年 vLLM 主线预计会把 Disagg Serving 作为 first-class 功能。
14.8.4 kv_transfer/ 模块的三层拆分
vllm/distributed/kv_transfer/ 总计 2302 行、三个子目录对应三层解耦——
| 子目录 | 层 | 核心文件 | 实现 |
|---|---|---|---|
kv_pipe/ | 最底:字节流传输 | base.py (66) / pynccl_pipe.py (279) / mooncake_pipe.py (279) | 定义 send_tensor / recv_tensor 接口 |
kv_lookup_buffer/ | 中:KV 查找缓冲 | base.py (174) / simple_buffer.py (236) / mooncake_store.py (160) | 按 (prompt_hash, layer_id) 做 insert/drop_select |
kv_connector/ | 最上:业务层 connector | base.py (127) / simple_connector.py (328) / mooncake_store_connector.py (201) / lmcache_connector.py (98) | 拼装 pipe + buffer、提供 send_kv_caches_and_hidden_states 等高层 API |
三层之间的依赖方向——connector → buffer → pipe——上层组合下层、下层不知道上层存在。
为什么做成三层——pynccl_pipe 和 mooncake_pipe 的传输语义不同:前者是同步 send/recv(双方同时在线)、后者是异步 put/get 到远端 KV store(发送方可先离线)——这决定了 buffer 层是必须的(同步语义下 buffer 可省、异步语义下必须 buffer 起来)。如果把 connector 直接写在 pipe 之上、buffer 层就会散落到每个 connector 里、代码重复且难维护。
2024-2025 PD 分离最常用的是 mooncake_store_connector——因为 Moonshot 的 Mooncake 是专门为 KV 迁移设计的分布式 KV store、比直接点对点 pynccl 在跨机多消费者场景下吞吐高 4x(同一份 KV 可被多个 decode 实例同时拉)。
14.8A Ray Distributed Executor:跨机 PP 的执行底座
vllm/executor/ray_distributed_executor.py(截至 main 2026-04,约 520 行)是 vLLM 唯一支持跨机部署的 Executor 实现——和单机 MultiprocExecutor(走 fork + 管道)互补。
14.8A.1 职责边界
RayDistributedExecutor 继承自 Executor——和单机版暴露同样的接口(execute_model、collective_rpc、sample_tokens、shutdown)、只是内部实现换成了 Ray Actor 方式。
关键方法和行号(main 分支,2026-04 快照):
| 方法 | 近似行号 | 作用 |
|---|---|---|
_init_executor | 初始化段 | 建 Ray cluster 连接、placement group、启动 Actor |
_init_workers_ray | 中段 | 按 TP × PP 在 Ray Actor 里创建 Worker 子进程、注入 rank 信息 |
execute_model | 中段 | 把 SchedulerOutput 序列化送入 compiled DAG、取回 sampled tokens |
_compiled_ray_dag | 中后段 | 构造一个静态图(compiled DAG)描述 PP 阶段间的 activation 流向 |
collective_rpc | 尾段 | broadcast 一个方法调用到所有 Worker(metadata 更新、profile、shutdown 都走它) |
reinitialize_distributed | 尾段 | 动态调整 rank 拓扑(scale-out / scale-in 场景) |
14.8A.2 Compiled DAG:为什么不直接 RPC
naive 做法是每个 decode step 都用 Ray RPC 调用每个 Worker 的 execute_model——问题是 Ray RPC 有 1-2 ms 的调度开销、decode 一拍本身只有 20-80 ms、RPC overhead 占 2-10% 不能忍。
解法:Ray 2.4 引入的 compiled DAG——把”rank 0 发 activations 给 rank 1、rank 1 发给 rank 2…”这个模式编成静态 DAG、一次 setup 反复复用、每次只付 50-100 μs 的 task 分发开销。
_compiled_ray_dag 的构造逻辑:
# 概念性伪码,对应 ray_distributed_executor.py _compiled_ray_dag
from ray.dag.input_node import InputNode
from ray.dag.output_node import MultiOutputNode
with InputNode() as input_data:
# 第一阶段 rank 组(PP=0 的所有 TP rank)接收请求
stage_outputs = [w.execute_model.bind(input_data) for w in pp0_workers]
# 后续阶段:每个阶段接收前阶段的 output
for pp_idx in range(1, pp_size):
stage_workers = self.pp_workers[pp_idx]
stage_outputs = [
w.execute_model.bind(prev)
for w, prev in zip(stage_workers, stage_outputs)
]
# 最后阶段输出 sampled tokens
dag = MultiOutputNode(stage_outputs)
self.forward_dag = dag.experimental_compile()
compile 之后 execute_model 只要 self.forward_dag.execute(scheduler_output)——Ray 在后台用 channel-based 数据通路(共享内存或 NCCL)做阶段间传递,不走 Python RPC。
14.8A.3 Placement Group:GPU 调度
Ray 在多机场景下不能让 Worker 随便落地——必须保证 TP=8 的 8 个 Worker 落在同一台机器(吃 NVLink 带宽),PP 的不同阶段落在不同机器(用 IB 通信没毛病)。
_init_executor 做的事:
# 概念性伪码
pg = ray.util.placement_group(
bundles=[{"GPU": 1, "CPU": 1}] * world_size,
strategy="PACK", # 同一 bundle 组尽量挤到一台机
)
# TP 组的 bundle 放在同一 node
# PP 跨 node 由 PACK 策略通过 bundle 排序 + node affinity 实现
这是为什么 --distributed-executor-backend ray 的启动比 mp 慢 30 s-1 min——Ray 要等 placement group ready、才能起 Actor。
14.8A.4 何时选 Ray
| 场景 | 推荐 | 原因 |
|---|---|---|
| 单机多卡(TP) | mp | Ray 纯粹负担、开销多余 |
| 单机大模型(405B FP8) | mp | 同上 |
| 跨机 PP | ray | mp 不支持跨机、ray 是唯一选择 |
| 跨机 DP | mp + 外层 LB 或 ray | DP 本不用 ray;但如果要统一运维平面 ray 更顺 |
| verl 这类 RL framework 集成 | ray | verl 本身基于 ray、复用同一 cluster 最自然 |
默认 backend 选择规则(vllm/config.py 里):world_size ≤ 单机 GPU 数用 mp、否则 ray。用户可以强制 --distributed-executor-backend=ray 覆盖。
14.8B 跨章源码账本:vllm/distributed/ 总览
把整个 vllm/distributed/ 树的文件和行数做一张源码账本——截至 main 2026-04 的快照——方便读者对照阅读时快速定位:
| 路径 | 行 | 职责 |
|---|---|---|
distributed/parallel_state.py | ~2000 | 全章核心——GroupCoordinator + TP/PP/DP 初始化 + EP 创建 |
distributed/communication_op.py | ~150 | 对 GroupCoordinator 方法的门面封装:tensor_model_parallel_all_reduce 等 |
distributed/utils.py | ~200 | stateless init、split_tensor_along_last_dim 等工具 |
distributed/device_communicators/base_device_communicator.py | 151 | DeviceCommunicatorBase 抽象 |
distributed/device_communicators/cuda_communicator.py | 131 | NVIDIA GPU 实现 |
distributed/device_communicators/cpu_communicator.py | 139 | CPU 推理实现 |
distributed/device_communicators/tpu_communicator.py | 93 | TPU 实现 |
distributed/device_communicators/custom_all_reduce.py | 340 | 小数据 AllReduce 专用——world_size ∈ {2,4,6,8}、数据 ≤ 8 MB |
distributed/device_communicators/custom_all_reduce_utils.py | 257 | P2P capability 探测 + IPC handle |
distributed/device_communicators/pynccl.py | 217 | 直接 NCCL C API(绕过 torch.distributed) |
distributed/device_communicators/pynccl_wrapper.py | 340 | NCCL ctypes 封装 |
distributed/device_communicators/shm_broadcast.py | 557 | 本机 broadcast 走 POSIX shm ring buffer |
distributed/device_communicators/cuda_wrapper.py | 179 | CUDA runtime ctypes wrapper |
distributed/kv_transfer/kv_pipe/*.py | ~620 | 字节流传输层(pynccl_pipe / mooncake_pipe) |
distributed/kv_transfer/kv_lookup_buffer/*.py | ~570 | KV 查找缓冲层 |
distributed/kv_transfer/kv_connector/*.py | ~750 | 业务 connector 层(simple / mooncake_store / lmcache) |
executor/ray_distributed_executor.py | ~520 | Ray 后端 Executor(跨机) |
v1/executor/multiproc_executor.py | ~600 | mp 后端 Executor(单机) |
账本的三个启示——
parallel_state.py一个文件 2000 行承载了整个四维并行元数据——想改 vLLM 并行语义、这里是唯一入口device_communicators/13 个文件里 custom_all_reduce(340)+ shm_broadcast(557)= 897 行在干”绕过 NCCL”——说明 vLLM 对 NCCL 启动延迟极其敏感、小消息场景愿意自己造轮子kv_transfer/三层解耦 2302 行独立存在——这是 2024-2025 PD 分离新范式落地的基础设施、不是传统意义的”集合通信”、而是命名 KV 的分布式存储——未来可能会分化成独立子系统
14.9 四种部署场景的配置模板
14.9.1 单机 8 卡,Llama-70B 低延迟 chat
vllm serve meta-llama/Llama-3-70B-Instruct \
--tensor-parallel-size 8 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 128 \
--enable-prefix-caching \
--enable-chunked-prefill
纯 TP=8,在 DGX 的 NVSwitch 下跑得飞起。
14.9.2 2 机 16 卡,DeepSeek-V3 671B
# 两台 8×H100,机内 TP=8 + EP=8,机间 PP=2
vllm serve deepseek-ai/DeepSeek-V3 \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--enable-expert-parallel \
--distributed-executor-backend ray \
--gpu-memory-utilization 0.92 \
--max-model-len 8192
机内 NVLink 做 TP+EP(通信重),机间 InfiniBand 做 PP(通信轻)。这是 670B 量级模型的标准打法。
14.9.3 4 副本高并发 Llama-8B
# 4 副本独立部署,前端 LB 分发
# 每副本 1 卡 FP8
vllm serve meta-llama/Llama-3-8B-Instruct \
--data-parallel-size 4 \
--quantization fp8 \
--gpu-memory-utilization 0.92
DP=4 吞吐翻 4 倍,不同请求落到不同副本。
14.9.4 8 卡单机 Llama-405B FP8
# 405B FP8 = 405 GB,8×H100 恰好能放
vllm serve meta-llama/Llama-3.1-405B-FP8 \
--tensor-parallel-size 8 \
--gpu-memory-utilization 0.95 \
--max-model-len 4096
榨干单机极限。再大就只能上 PP 跨机了。
14.9A 生产踩坑:分布式推理的 10 个故障模式
把过去几年 vLLM GitHub issue 里重复最多的 10 类分布式问题整理成表——每个都给了定位方法和 fix,方便线上救火时直接翻。
| # | 症状 | 根因 | 定位方法 | Fix |
|---|---|---|---|---|
| 1 | 启动卡死在 NCCL init 10 min 不动 | 跨机 NCCL 走了 TCP fallback、没识别到 IB | NCCL_DEBUG=INFO 看是否出现 Using network Socket | 设 NCCL_IB_DISABLE=0 + NCCL_SOCKET_IFNAME=ens3(替换成实际 IB 接口) |
| 2 | decode 延迟突增 10×、概率复现 | NCCL 退回 ring 算法(通常因为 SHARP 失效) | 看 NCCL_DEBUG=INFO 日志里 COLLNET 是否 enabled | 设 NCCL_COLLNET_ENABLE=1、确认 Hopper 且网络路径支持 |
| 3 | 跨机 PP 吞吐只有理论值 30% | GPU 利用率被 P2P send/recv 启动开销吃掉 | nsys profile 看 kernel gap | 增加 max_num_seqs、提高 micro-batch 数量降低 bubble |
| 4 | TP=8 启动时提示 “too many open files” | shm_broadcast 分配了大量 fd、超过 ulimit | ulimit -n 查看 | ulimit -n 65536 或启动前 echo 'ulimit -n 65536' >> /etc/security/limits.conf |
| 5 | Ray 启动超时、Actor 永远不 ready | placement group PACK 策略等不到足够 GPU | ray status 看资源占用 | 确认 ray start --num-gpus=8、清理旧的 dangling actor |
| 6 | DP=N 下前缀缓存命中率暴跌 | LB 没做 sessionAffinity、同一 session 落到不同副本 | Grafana 看 hit rate per rank | 在 nginx 里按 $remote_addr 或 session cookie 做 sticky |
| 7 | EP 启用后 all_to_all 报 OOM | top_k 太大 + batch 太大、临时 buffer 超过 GPU 剩余显存 | OOM 栈显示在 fused_moe_kernel | 降 max_num_seqs 或升级 PyTorch 到支持 DeepEP(更省显存的 MoE kernel) |
| 8 | 跨机 TP 下 token 延迟 300 ms 以上 | 用户错误地把 TP 切到了跨机 | nvidia-smi topo -m 看网络拓扑 | 绝对禁止跨机 TP——改成机内 TP + 机间 PP |
| 9 | custom_all_reduce 不生效、NCCL 慢 | 两张卡之间没 NVLink / 走了 PCIe | nvidia-smi topo -m 看是否 NV8 | 换机器、或接受 NCCL 性能(没救) |
| 10 | decode 阶段偶现 NCCL hang | 一个 rank 的 CUDA Graph 捕获到 AllReduce 失败但没报错 | NCCL_ASYNC_ERROR_HANDLING=1 看错误 | 升级 vLLM(2024 Q3 已 fix)、或关闭 CUDA Graph 临时缓解 |
一条黄金法则:线上分布式问题 80% 是网络拓扑配置错——nvidia-smi topo -m + NCCL_DEBUG=INFO 的组合能定位大多数问题。剩下 20% 是软件 bug,往往跟 PyTorch 某个小版本的 NCCL 组件有关——锁死 PyTorch + NCCL + vLLM 的版本组合是生产稳定性最便宜的手段。
14.9B 版本信息与参考
本章引用的所有 vLLM 代码行号基于 GitHub main 分支 2026-04 快照。不同小版本的差异通常很小(函数位置 ±30 行内)——如果读者正在追最新代码、建议用 git blame 找到本章引用的关键函数、对照当前文件位置。
行号会变、但架构不会变——vLLM 的分布式层从 V0 的 parallel_state.py 移植到 V1 后核心概念(GroupCoordinator、device_group/cpu_group 双通道、custom_all_reduce、shm_broadcast)完全保留。本章花了大量篇幅解释为什么这么设计而不只是代码长什么样——就是为了让读者在未来几年面对 V2 / V3 重构时还能认出同样的模式。
14.10 本章小结
分布式并行是 vLLM 从”单机实验室玩具”走向”千卡 production 系统”的关键:
- 带宽鸿沟:HBM 2 TB/s → NVLink 900 GB/s → InfiniBand 50 GB/s 三级鸿沟决定了一切策略取舍
- TP 张量并行:Megatron 式 Column + Row 切分;每层 2 次 AllReduce;只在 NVLink 内部用
- PP 流水线并行:阶段切分;bubble 公式 ;跨机部署的标准手段
- EP 专家并行:MoE 模型刚需;每层 2 次 all_to_all;同机 NVLink 内执行
- DP 数据并行:复制整模型;零通信;线性扩吞吐;生产最常用
- 四维组合:
parallel_state.py用多个 ProcessGroup 管理 TP × PP × EP × DP 的拓扑 - Hopper 加速:SHARP + NVLS 把 AllReduce 延迟降到 A100 的 1/3
- Prefill/Decode 分离:2024-2025 新范式,用不同硬件特性匹配不同计算形态
- 部署模板:给出 8 卡单机 / 2 机 16 卡 / 4 副本 / 405B 四种典型场景
源码导航
- 并行状态管理:
vllm/distributed/parallel_state.py- 通信操作:
vllm/distributed/communication_op.py- 设备通信器:
vllm/distributed/device_communicators/(NCCL, shm, XLA, HPU 等)- pynccl(直接 NCCL):
vllm/distributed/device_communicators/pynccl.py- MoE 融合 kernel:
vllm/model_executor/layers/fused_moe/- KV 跨机传输:
vllm/distributed/kv_transfer/- Ray Executor(跨机 PP 用):
vllm/v1/executor/ray_distributed_executor.py- 线性层并行实现:
vllm/model_executor/layers/linear.py论文
- Shoeybi et al., “Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism”, 2019 (arXiv:1909.08053)
- Narayanan et al., “Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM”, 2021 (arXiv:2104.04473)
- Huang et al., “GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism”, NeurIPS 2019 (arXiv:1811.06965)
- Zhong et al., “DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving”, OSDI 2024
- Patel et al., “Splitwise: Efficient Generative LLM Inference Using Phase Splitting”, ISCA 2024