欢迎光临

PD分离架构(Prefill-Decode Disaggregation)实战详解:原理拆解、vLLM/SGLang部署与多卡推理性能调优全攻略

为什么大模型推理需要 PD 分离架构

在大模型推理服务化的实际工程中,我们经常会遇到一个让人头疼的现象:当一个长上下文请求(比如 8K tokens 的 Prefill)和一批短上下文的 Decode 请求混在一起时,整体吞吐和延迟都会急剧恶化。根本原因是 Prefill 阶段是计算密集型(要做全量 Attention,GPU 算力打满),而 Decode 阶段是访存密集型(每步只生成一个 token,瓶颈在 KV Cache 读取带宽)。把这两个阶段塞进同一个 batch,用同一组 GPU 跑,就会出现互相拖累——Prefill 把算力吃光,Decode 的 token 生成被卡住;Decode 占着显存,Prefill 又排不进队列。

PD 分离架构(Prefill-Decode Disaggregation)就是为解决这个矛盾而生的。它把 Prefill 和 Decode 物理隔离到不同的 GPU 池上:Prefill 节点专门做首 token 计算并把产生的 KV Cache 通过高速互联(NVLink/RDMA/InfiniBand)传给 Decode 节点,Decode 节点拿到 KV Cache 后专心做增量生成。这种架构在 DeepSeek-V3、Mooncake、Splitwise 等论文和系统里被反复验证,TPOT(Time Per Output Token)和 TTFT(Time To First Token)可以同时优化,是 2025 年大模型推理工程最关键的范式之一。

PD分离架构:Prefill与Decode分池部署示意

PD 分离的核心原理:两阶段资源画像

要理解 PD 分离为什么有效,必须先看清 Prefill 和 Decode 两个阶段的资源画像差异。下表对比了 70B 模型(FP16、batch=1、上下文 4K)在 A100-80G 上的典型指标:

指标 Prefill 阶段 Decode 阶段
计算量 (FLOPs) ~28 TFLOP(高) ~0.4 TFLOP(低)
显存读写 (Bytes) ~56 GB ~9 GB/step(KV Cache 主导)
算术强度 (FLOP/Byte) ~500(计算密集) ~45(访存密集)
GPU 算力利用率 60-80% MFU 5-15% MFU
显存带宽利用率 低 高(瓶颈)
单次延迟 TTFT 主导 TPOT 主导

从算术强度可以看出,Prefill 的 FLOP/Byte 远高于 GPU 的 Roofline 拐点(A100 约 100-150),属于计算受限;而 Decode 的算术强度只有 45,低于拐点,属于带宽受限。这意味着:

  • Prefill 节点应配置高算力卡(H100/H200),跑满 Tensor Core。
  • Decode 节点应配置高带宽卡或用更大的 batch 把带宽利用率顶起来。
  • 两者用不同的 batch size、不同的并行度策略,互不干扰。

更关键的是,PD 分离后可以各自做 Continuous Batching:Prefill 池可以连续接新的长 prompt,Decode 池可以堆叠几百路并发输出。这正是 Mooncake 论文里 “split-and-conquer” 的精髓。

KV Cache 传输:PD 分离的真正难点

PD 分离架构听起来很美,但工程落地的最大难点在于:Prefill 阶段产生的 KV Cache 怎么传给 Decode 节点?70B 模型、4K 上下文、batch=1 的 KV Cache 体积大约是:


1
2
3
4
5
6
7
8
9
10
# KV Cache 大小估算(70B, FP16, GQA, 4K ctx)
num_layers = 80
kv_heads   = 8       # GQA, 8 个 KV head
head_dim   = 128
seq_len    = 4096
bytes_per_elem = 2   # FP16

kv_cache_bytes = 2 * num_layers * kv_heads * head_dim * seq_len * bytes_per_elem
# = 2 * 80 * 8 * 128 * 4096 * 2 ≈ 10.5 GB
print(f"单请求 KV Cache: {kv_cache_bytes / 1024**3:.2f} GB")

10.5 GB 的 KV Cache 如果走普通 TCP/Ethernet(25 Gb/s),传输延迟约 3.3 秒,比 Prefill 本身还慢,PD 分离就毫无意义。所以真正的 PD 分离系统必须依赖:

  1. NVLink / NVSwitch:单机内 GPU 间 900 GB/s,10.5 GB 只需 ~12ms。
  2. InfiniBand / RoCE RDMA:跨机 200-400 Gb/s,配合 GPUDirect RDMA 可以 GPU 显存到显存零拷贝。
  3. KV Cache 池化:Mooncake 提出的思路——KV Cache 不直接传,而是写入一个共享的 KV Store,Decode 节点按需拉取,还能跨请求复用(Prefix Caching)。

vLLM 在 0.6+ 版本引入了

1
PD separator

和

1
TransferEngine

(基于 Mooncake RFC)来实现这种 KV Cache 传输。SGLang 也有类似的

1
TransferEngine

接入。下面是 vLLM 中开启 PD 分离的核心配置片段:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Prefill 节点(p-d 分离模式,role=prefill)
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Meta-Llama-3-70B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 1 \
  --kv-transfer-config '{"role": "prefill", "port": 29000}' \
  --max-num-batched-tokens 8192 \
  --gpu-memory-utilization 0.9 \
  --port 8000

# Decode 节点(role=decode,从 prefill 节点拉 KV Cache)
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Meta-Llama-3-70B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 1 \
  --kv-transfer-config '{"role": "decode", "host": "PREFILL_HOST", "port": 29000}' \
  --max-num-seqs 256 \
  --gpu-memory-utilization 0.85 \
  --port 8001

关键参数解读:

  • 1
    kv-transfer-config

    的

    1
    role

    字段决定本节点是 producer(prefill)还是 consumer(decode)。

  • Prefill 节点拉高
    1
    max-num-batched-tokens

    ,把算力榨干;Decode 节点拉高

    1
    max-num-seqs

    ,把并发顶上去。

  • 两边的
    1
    tensor-parallel-size

    必须一致,否则 KV Cache 形状对不上。

KV Cache 通过 RDMA/NVLink 在 Prefill 与 Decode 节点间传输

vLLM PD 分离部署实战

下面给出一个最小可运行的 vLLM PD 分离部署方案(基于 vLLM 0.6.4+,2 台 8xH100 机器,NVLink + IB 200G)。假设机器 A 跑 Prefill,机器 B 跑 Decode,前面挂一个 LLM 路由层。

1. 启动 Prefill 节点(机器 A)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 机器 A:8xH100,TP=8
export VLLM_KV_TRANSFER_BACKEND=mooncake  # 或 pyhttp
export NCCL_IB_DISABLE=0
export NCCL_IB_GID_INDEX=3
export NCCL_SOCKET_IFNAME=ib0  # 走 InfiniBand

python -m vllm.entrypoints.openai.api_server \
  --served-model-name llama3-70b \
  --model meta-llama/Meta-Llama-3-70B-Instruct \
  --tensor-parallel-size 8 \
  --kv-transfer-config \
    '{"role":"prefill","host":"0.0.0.0","port":29000,"backend":"mooncake"}' \
  --max-num-batched-tokens 16384 \
  --max-num-prefills 4 \
  --max-num-seqs 32 \
  --gpu-memory-utilization 0.92 \
  --enable-prefix-caching \
  --port 8000

2. 启动 Decode 节点(机器 B)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 机器 B:8xH100,TP=8
export VLLM_KV_TRANSFER_BACKEND=mooncake
export NCCL_IB_DISABLE=0
export NCCL_SOCKET_IFNAME=ib0

PREFILL_IP="10.0.0.10"  # 机器 A 的 IB IP

python -m vllm.entrypoints.openai.api_server \
  --served-model-name llama3-70b \
  --model meta-llama/Meta-Llama-3-70B-Instruct \
  --tensor-parallel-size 8 \
  --kv-transfer-config \
    "{"role":"decode","host":"${PREFILL_IP}","port":29000,"backend":"mooncake"}" \
  --max-num-seqs 512 \
  --max-num-batched-tokens 512 \
  --gpu-memory-utilization 0.88 \
  --port 8001

3. 路由层:把请求分发到 Prefill 池

实际生产中需要一个轻量路由把用户请求送到 Prefill 池,Prefill 完成后把 KV Cache 句柄传给 Decode 池继续生成。最简方案是用 vLLM 自带的

1
llmrouter

或自己写个 FastAPI 转发:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from fastapi import FastAPI, Request
import httpx, uuid

app = FastAPI()
PREFILL_URL = "http://10.0.0.10:8000"

@app.post("/v1/chat/completions")
async def route(req: Request):
    body = await req.json()
    # 给请求打上唯一 session_id,方便 prefill/decode 对齐 KV Cache
    body.setdefault("extra_body", {})["session_id"] = str(uuid.uuid4())
    # 当前实现:直接转给 prefill,prefill 内部把 KV 推给 decode
    # 更精细的做法是 prefill 返回 KV handle 后再调 decode
    async with httpx.AsyncClient(timeout=120) as c:
        r = await c.post(f"{PREFILL_URL}/v1/chat/completions", json=body)
        return r.json()

这套架构在 70B 模型、4K 输入、256 并发的典型场景下,相比单池部署:

  • TTFT 降低 35-50%(Prefill 池不被 Decode 抢算力)
  • TPOT 降低 20-30%(Decode 池 batch 堆得更满)
  • 整体吞吐(tokens/s/GPU)提升 1.5-2.2x

SGLang 的 PD 分离方案对比

SGLang 在 2025 年的 RDMA 版本里也实现了 PD 分离,思路和 Mooncake 类似但细节有差异。下面是两者的关键对比:

维度 vLLM (Mooncake TransferEngine) SGLang (RDMA KV Transfer)
KV 传输后端 Mooncake / PyNccl / HTTP RDMA (IB/RoCE) / TCP
KV Cache 池化 支持,可跨请求复用 支持,基于 RadixAttention 树
跨请求 KV 复用 需手动 enable-prefix-caching 原生 RadixAttention 自动复用
故障切换 Decode 节点宕机需重算 有 checkpoint 机制,部分可恢复
调度粒度 请求级 请求级 + 共享前缀感知
易用性 配置参数较多 开箱即用,参数更少

SGLang 的 PD 分离启动命令更简洁:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# SGLang Prefill 节点
python -m sglang.launch_server \
  --model-path meta-llama/Meta-Llama-3-70B-Instruct \
  --tp 8 \
  --dist-init-addr 10.0.0.10:29500 \
  --nnodes 2 --node-rank 0 \
  --pd-disagg \
  --prefill-port 29000 \
  --port 8000

# SGLang Decode 节点
python -m sglang.launch_server \
  --model-path meta-llama/Meta-Llama-3-70B-Instruct \
  --tp 8 \
  --dist-init-addr 10.0.0.10:29500 \
  --nnodes 2 --node-rank 1 \
  --pd-disagg \
  --prefill-host 10.0.0.10 --prefill-port 29000 \
  --port 8001

SGLang 的一大优势是共享前缀感知调度:当多个请求有相同的 system prompt(比如都是同一个 RAG 模板的问答),SGLang 的 RadixAttention 树会自动识别共享前缀,只算一次 Prefill,KV Cache 直接复用,在 RAG 场景下吞吐可以再翻一倍。vLLM 虽然也有 prefix caching,但调度层没有 RadixAttention 那么激进。

性能调优 checklist:让 PD 分离真正跑起来

PD 分离不是”配个参数就完事”,下面这些坑是实战中最容易踩的:

1. KV 传输带宽要匹配

如果 Prefill 算完 10.5 GB KV Cache 要 100ms,但传输需要 200ms,那 Prefill GPU 就在空等,算力利用率掉到一半。经验值:KV 传输带宽 ≥ Prefill 吞吐 × 单请求 KV 大小。比如 Prefill 池每秒处理 5 个 4K 请求,每个 KV 10.5 GB,那传输带宽至少 52.5 GB/s,必须走 NVLink 或 4x IB 200G。

2. Prefill / Decode 节点配比

不是 1:1 最优。经验上 70B 模型 4K 上下文,Prefill 与 Decode 的 GPU 比大约 1:2 到 1:3(Decode 吃显存多、并发多)。可以根据 TTFT 和 TPOT 的 SLO 反推:如果 TTFT SLO 是 500ms,Prefill 一个请求 300ms,则单 GPU Prefill 吞吐 ~3 req/s;如果总 QPS 是 30,则 Prefill 需要 10 GPU,Decode 按 1:2 配 20 GPU。实测发现这个比例对 MoE 模型(如 DeepSeek-V3)会更倾斜到 1:4,因为 MoE 的 Decode 更吃带宽。

3. Decode 池的 batch 堆叠

PD 分离的吞吐红利主要来自 Decode 池能堆更大 batch。但 batch 不是越大越好——超过 GPU 显存上限会 OOM,而且 attention 的复杂度是 O(n²)(虽然 FlashAttention 优化了常数)。建议用

1
max-num-seqs

逐步加压,监控 GPU 显存和 TPOT:


1
2
3
4
# 监控 Decode 节点
nvidia-smi dmon -s u -c 60   # 看 sm 利用率和显存
# 同时看 vLLM metrics
curl localhost:8001/metrics | grep -E "vllm:num_requests|vllm:time_per_output_token"

4. Prefix Caching 与 PD 分离的协同

PD 分离 + Prefix Caching 是组合拳。如果一个 RAG 服务的 system prompt 有 2K tokens 是固定的,那这 2K 的 KV Cache 在 Prefill 池算一次后可以缓存住,后续请求只算 user query 部分。vLLM 的

1
--enable-prefix-caching

和 SGLang 的 RadixAttention 都支持,但要注意缓存的 KV Cache 在哪台 Prefill 节点上——如果负载均衡把请求打到不同节点,缓存命中率会下降。建议用一致性哈希或基于 prompt hash 的路由。

5. 故障恢复

Decode 节点宕机时,正在生成的请求会丢。PD 分离系统一般要做 checkpoint:定期把 Decode 状态(KV Cache + 生成位置)写回 Prefill 池或 KV Store,故障时从 checkpoint 恢复。SGLang 在这方面做得更完善,vLLM 目前需要自己包一层。

什么时候该用 PD 分离,什么时候不该

PD 分离不是银弹,它的收益取决于负载特征。下表给出选型建议:

场景 是否推荐 PD 分离 理由
长上下文 RAG(4K-32K 输入) 强烈推荐 Prefill 重,分离后 TTFT 改善明显
高并发对话(短输入、多轮) 推荐 Decode 池堆 batch 收益大
单并发、低延迟(<200ms) 不推荐 KV 传输开销大于收益
纯短输入批处理(离线) 不推荐 单池 Continuous Batching 已足够
MoE 模型(DeepSeek-V3 等) 强烈推荐 MoE 的 Decode 带宽瓶颈更严重
多租户、不同 SLO 推荐 可按 SLO 分池调度

GPU 集群部署 PD 分离架构

总结:PD 分离是 2025 推理工程的分水岭

PD 分离架构从论文走向生产只用了不到两年,已经成为头部推理服务(DeepSeek、Moonshot、字节豆包)的标配。它的核心价值不在于”把一个 batch 拆成两个”,而在于让 Prefill 和 Decode 各自走到自己的 Roofline 最优点,同时通过 KV Cache 池化实现跨请求复用和故障恢复。

对于正在做推理服务化的团队,落地路径建议是:

  1. 先用单池 vLLM/SGLang + Continuous Batching + Prefix Caching 跑通基线。
  2. 当 QPS 上升到单池 TTFT 或 TPOT 触发 SLO 报警时,再引入 PD 分离。
  3. 从 2 节点(1 Prefill + 1 Decode)起步,验证 KV 传输链路和配比。
  4. 逐步扩到多节点池,引入路由层和 KV Cache 池化。
  5. 对 MoE 模型优先考虑 PD 分离,收益最大。

掌握了 PD 分离,你就理解了 2025 年大模型推理工程的核心范式。下一篇我们会继续深挖 NCCL 通信原语(AllReduce / AllGather / ReduceScatter)在多卡推理中的实现细节——那是 PD 分离之外另一个分布式推理的硬骨头。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » PD分离架构(Prefill-Decode Disaggregation)实战详解:原理拆解、vLLM/SGLang部署与多卡推理性能调优全攻略
分享到: 更多 (0)