vLLM 推理内核深度解析

第14章 分布式并行:TP / PP / EP / DP

作者 杨艺韬 · 10,286 字

第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 调度、微批次化、气泡率公式 PP1PP1+mb\frac{PP-1}{PP-1+mb}
  • 理解 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/s1×(基准)
GPU 间(同机,NVLink 5)NVSwitch900 GB/s0.27×
GPU 间(同机,PCIe 5)PCIe128 GB/s0.04×
机间(InfiniBand NDR)400 Gbps50 GB/s0.015×
机间(以太网 100G)100 GbE12.5 GB/s0.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 数据并行
切分对象单层权重(按列 / 按行)不同层分到不同 rankMoE 的 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/RowParallelLinearworker/worker.py + distributed/parallel_state.py send/recvmodel_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 计算本身和单卡相同

关键抽象:分布式边界正好落在 ExecutorWorker 之间——上面(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)。核心洞察:矩阵乘法 Y=XWY = XW 可以按 W 的列或行切分,再通过通信合并结果

14.2.1 列并行(Column Parallel)

把 W 沿”输出维度”(列)切成 P 份:W=[W1W2WP]W = [W_1 | W_2 | \dots | W_P]

每个 GPU 拿一片 WiW_i 的列,输入 XX 在所有 GPU 上相同(已经通过前面的 AllReduce 同步)。计算:

Y=XW=[XW1XW2XWP]Y = X W = [X W_1 | X W_2 | \dots | X W_P]

输出 YY 天然按列切分——GPU i 持有 YY 的第 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 份:W=[W1;W2;;WP]W = [W_1; W_2; \dots; W_P]

输入 X 也按列切分成 P 份:X=[X1X2XP]X = [X_1 | X_2 | \dots | X_P]

Y=XW=X1W1+X2W2++XPWPY = X W = X_1 W_1 + X_2 W_2 + \dots + X_P W_P

每个 GPU 计算自己的 XiWiX_i W_i,结果是完整 YY 的部分和。最后必须通过 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):

per AllReduce=1×8192×2=16 KB\text{per AllReduce} = 1 \times 8192 \times 2 = 16 \text{ KB}

80 层 × 2 次 = 160 次 AllReduce / step / request。每次 AllReduce 的启动开销(latency-bound part)在 NVLink 上约 1-2 μs:

160×2μs=320μs=0.32 ms160 \times 2 \mu s = 320 \mu s = 0.32 \text{ ms}

相比 decode step 本身的 20-80 ms,只占 0.5-1.6%。TP 在 NVLink 域内几乎免费

但在跨机器(InfiniBand)场景下,AllReduce 启动开销变成 10-50 μs:

160×30μs=4.8 ms160 \times 30 \mu s = 4.8 \text{ ms}

占 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:1×8192×2=16 KB1 \times 8192 \times 2 = 16 \text{ KB},每 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 占比:

bubble ratio=PP1PP1+M\text{bubble ratio} = \frac{PP - 1}{PP - 1 + M}
  • 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:

per all_to_all32×7168×1×8=1.8 MB\text{per all\_to\_all} \approx 32 \times 7168 \times 1 \times 8 = 1.8 \text{ MB}

每 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 的好处

  1. 零通信开销——各个 rank 完全独立,没有任何同步点
  2. 线性扩展吞吐——N 个副本 = N 倍吞吐(只要 LB 够聪明)
  3. 独立扩缩容——忙时 HPA 加副本,闲时缩回
  4. 故障隔离——一个 rank 崩不影响其他 rank

14.5.2 DP 的代价

  1. 显存冗余——每个副本都要完整装一份模型
  2. 没有前缀缓存共享——副本间的 KV 不互通
  3. 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_groupnew_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_implall_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)

三点非显然的细节——

  1. 注释里 ExternalDP 是第 4 维——-1 代表 verl 集成时的外层 DP(跨 vLLM 实例),本进程组不参与,但 rank 张量要预留一维给它
  2. 只建了 TP/PP/DP 三个 group,没有 EP——EP 是 MoE 模型才有的运行时拓扑、由 init_model_parallel_group 在 MoE 层里另行创建、不属于”基础 3 维”
  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.py151抽象接口 DeviceCommunicatorBase——定义 all_reduce / all_gather / broadcast / send / recv 五个抽象方法
cuda_communicator.py131NVIDIA GPU 后端——wrap torch.distributed + 集成 CustomAllreduce
cpu_communicator.py139CPU 推理后端——走 Gloo backend
tpu_communicator.py93Google TPU 后端——走 XLA
hpu_communicator.py45Intel Gaudi 后端
xpu_communicator.py54Intel Xe GPU 后端
neuron_communicator.py19AWS Trainium/Inferentia 后端——19 行是全仓最小的 communicator、因为 Neuron SDK 自己处理了几乎所有通信
custom_all_reduce.py301小数据 P2P AllReduce——≤ 8 卡、数据 ≤ 8MB 时,绕过 NCCL、用 CUDA IPC + 自实现 reduce kernel、比 NCCL 快 2x
custom_all_reduce_utils.py257custom AR 的 P2P capability 探测 + IPC handle 管理
pynccl.py + pynccl_wrapper.py217 + 340直接调 NCCL C API(绕过 torch.distributed)——vLLM 特殊场景用、避免 torch 的 stream 同步开销
shm_broadcast.py557本机 broadcast 走 POSIX shm ring buffer——不走 NCCL、因为 NCCL 的 broadcast 对小消息开销过大
cuda_wrapper.py179CUDA runtime API 的 ctypes wrapper(用于检测 P2P 能力等)

两条非显然的设计原则——

  1. 小消息不走 NCCL——shm_broadcast.pyShmRingBuffer共享内存 ring buffer + reader-flag 字节实现无锁本机 broadcast——因为 TP 组里 broadcast 一个 sampling 参数只有几十字节、走 NCCL 会每次 10+ us 启动开销、走 shm 只要 sub-us
  2. 小数据 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 时自动启用,不需要用户配置。

跨 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_SIZEShandwritten 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-161capture 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/最上:业务层 connectorbase.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_pipemooncake_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_modelcollective_rpcsample_tokensshutdown)、只是内部实现换成了 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)mpRay 纯粹负担、开销多余
单机大模型(405B FP8)mp同上
跨机 PPraymp 不支持跨机、ray 是唯一选择
跨机 DPmp + 外层 LBrayDP 本不用 ray;但如果要统一运维平面 ray 更顺
verl 这类 RL framework 集成rayverl 本身基于 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~150GroupCoordinator 方法的门面封装:tensor_model_parallel_all_reduce
distributed/utils.py~200stateless init、split_tensor_along_last_dim 等工具
distributed/device_communicators/base_device_communicator.py151DeviceCommunicatorBase 抽象
distributed/device_communicators/cuda_communicator.py131NVIDIA GPU 实现
distributed/device_communicators/cpu_communicator.py139CPU 推理实现
distributed/device_communicators/tpu_communicator.py93TPU 实现
distributed/device_communicators/custom_all_reduce.py340小数据 AllReduce 专用——world_size ∈ {2,4,6,8}、数据 ≤ 8 MB
distributed/device_communicators/custom_all_reduce_utils.py257P2P capability 探测 + IPC handle
distributed/device_communicators/pynccl.py217直接 NCCL C API(绕过 torch.distributed)
distributed/device_communicators/pynccl_wrapper.py340NCCL ctypes 封装
distributed/device_communicators/shm_broadcast.py557本机 broadcast 走 POSIX shm ring buffer
distributed/device_communicators/cuda_wrapper.py179CUDA runtime ctypes wrapper
distributed/kv_transfer/kv_pipe/*.py~620字节流传输层(pynccl_pipe / mooncake_pipe)
distributed/kv_transfer/kv_lookup_buffer/*.py~570KV 查找缓冲层
distributed/kv_transfer/kv_connector/*.py~750业务 connector 层(simple / mooncake_store / lmcache)
executor/ray_distributed_executor.py~520Ray 后端 Executor(跨机)
v1/executor/multiproc_executor.py~600mp 后端 Executor(单机)

账本的三个启示——

  1. parallel_state.py 一个文件 2000 行承载了整个四维并行元数据——想改 vLLM 并行语义、这里是唯一入口
  2. device_communicators/ 13 个文件里 custom_all_reduce(340)+ shm_broadcast(557)= 897 行在干”绕过 NCCL”——说明 vLLM 对 NCCL 启动延迟极其敏感、小消息场景愿意自己造轮子
  3. 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、没识别到 IBNCCL_DEBUG=INFO 看是否出现 Using network SocketNCCL_IB_DISABLE=0 + NCCL_SOCKET_IFNAME=ens3(替换成实际 IB 接口)
2decode 延迟突增 10×、概率复现NCCL 退回 ring 算法(通常因为 SHARP 失效)NCCL_DEBUG=INFO 日志里 COLLNET 是否 enabledNCCL_COLLNET_ENABLE=1、确认 Hopper 且网络路径支持
3跨机 PP 吞吐只有理论值 30%GPU 利用率被 P2P send/recv 启动开销吃掉nsys profile 看 kernel gap增加 max_num_seqs、提高 micro-batch 数量降低 bubble
4TP=8 启动时提示 “too many open files”shm_broadcast 分配了大量 fd、超过 ulimitulimit -n 查看ulimit -n 65536 或启动前 echo 'ulimit -n 65536' >> /etc/security/limits.conf
5Ray 启动超时、Actor 永远不 readyplacement group PACK 策略等不到足够 GPUray status 看资源占用确认 ray start --num-gpus=8、清理旧的 dangling actor
6DP=N 下前缀缓存命中率暴跌LB 没做 sessionAffinity、同一 session 落到不同副本Grafana 看 hit rate per rank在 nginx 里按 $remote_addr 或 session cookie 做 sticky
7EP 启用后 all_to_all 报 OOMtop_k 太大 + batch 太大、临时 buffer 超过 GPU 剩余显存OOM 栈显示在 fused_moe_kernelmax_num_seqs 或升级 PyTorch 到支持 DeepEP(更省显存的 MoE kernel)
8跨机 TP 下 token 延迟 300 ms 以上用户错误地把 TP 切到了跨机nvidia-smi topo -m 看网络拓扑绝对禁止跨机 TP——改成机内 TP + 机间 PP
9custom_all_reduce 不生效、NCCL 慢两张卡之间没 NVLink / 走了 PCIenvidia-smi topo -m 看是否 NV8换机器、或接受 NCCL 性能(没救)
10decode 阶段偶现 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 公式 PP1PP1+M\frac{PP-1}{PP-1+M};跨机部署的标准手段
  • 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