为什么大模型推理需要 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 分离的核心原理:两阶段资源画像
要理解 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 分离系统必须依赖:
- NVLink / NVSwitch:单机内 GPU 间 900 GB/s,10.5 GB 只需 ~12ms。
- InfiniBand / RoCE RDMA:跨机 200-400 Gb/s,配合 GPUDirect RDMA 可以 GPU 显存到显存零拷贝。
- 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
关键参数解读:
-
1kv-transfer-config
的
1role字段决定本节点是 producer(prefill)还是 consumer(decode)。
- Prefill 节点拉高
1max-num-batched-tokens
,把算力榨干;Decode 节点拉高
1max-num-seqs,把并发顶上去。
- 两边的
1tensor-parallel-size
必须一致,否则 KV Cache 形状对不上。

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 分池调度 |

总结:PD 分离是 2025 推理工程的分水岭
PD 分离架构从论文走向生产只用了不到两年,已经成为头部推理服务(DeepSeek、Moonshot、字节豆包)的标配。它的核心价值不在于”把一个 batch 拆成两个”,而在于让 Prefill 和 Decode 各自走到自己的 Roofline 最优点,同时通过 KV Cache 池化实现跨请求复用和故障恢复。
对于正在做推理服务化的团队,落地路径建议是:
- 先用单池 vLLM/SGLang + Continuous Batching + Prefix Caching 跑通基线。
- 当 QPS 上升到单池 TTFT 或 TPOT 触发 SLO 报警时,再引入 PD 分离。
- 从 2 节点(1 Prefill + 1 Decode)起步,验证 KV 传输链路和配比。
- 逐步扩到多节点池,引入路由层和 KV Cache 池化。
- 对 MoE 模型优先考虑 PD 分离,收益最大。
掌握了 PD 分离,你就理解了 2025 年大模型推理工程的核心范式。下一篇我们会继续深挖 NCCL 通信原语(AllReduce / AllGather / ReduceScatter)在多卡推理中的实现细节——那是 PD 分离之外另一个分布式推理的硬骨头。
汤不热吧