# PD 分离详解：Prefill 与 Decode 的瓶颈、拆分与代价


![为什么要 PD 分离](https://img.lixueduan.com/ai/cover/why-pd-disaggregation.jpg)

LLM 推理服务的性能瓶颈，不只在模型大小和 GPU 算力。副本增加后，请求落到哪个实例、长 Prompt 是否干扰流式输出、KV Cache 能否复用，都会直接影响延迟、吞吐和成本。

其中，Prefill/Decode 分离（PD 分离）是把两类截然不同的计算负载拆到独立资源池中的方案。本文从问题出发，说明它解决什么、代价是什么，以及何时不该使用它。

<!--more-->

## 推理服务真的无状态吗？

先问大家一个问题：***推理服务是无状态的吗***？

**是，也不是。**

只要客户端每次请求都带上完整的对话上下文，把请求调度到哪个后端服务，通常都不会影响推理的语义正确性。模型看到的上下文是完整的。
> 至于采样带来的输出差异，和请求落在哪个副本上是两回事。

但平台看到的是另一层状态。每个实例都维护着自己的请求队列、Prefix Cache 和 KV Cache。随机或者轮询不会破坏结果的正确性，却可能让请求反复落到没有对应缓存的实例上，造成 Cache Miss、重复 Prefill 和更多 GPU 计算，最终表现为更高的 TTFT、更低的吞吐和更高的资源成本。

同一段系统提示词、长文档或者多轮对话前缀，可能已经在实例 A 上完成 Prefill，并进入 Prefix Cache。下一次请求如果仍然落到实例 A，就有机会复用计算结果；如果被分给实例 B，实例 A 上的缓存没有损坏或失效，只是实例 B 无法使用，只能重新执行 Prefill。

即：**LLM 推理服务在用户层面是无状态的，但在运行时层面却不是。**
> 如果只关心接口是否可用、推理结果是否正确，可以把 LLM 推理服务看成无状态服务；但只要开始关注延迟、吞吐和 GPU 成本，就不能再忽略实例上的运行时状态。

普通负载均衡不懂 LLM 的运行时状态。它可能知道哪些实例 Ready，可以完成基础的流量分配，却不知道哪个实例的队列已经堆满、KV Cache 还剩多少，也不知道当前请求的前缀曾经在哪个实例上计算过。

所以，多副本 LLM 服务需要的不只是负载均衡，而是**能够感知负载和缓存局部性的智能路由**。

## 为什么要拆分 Prefill 和 Decode？

### Prefill 与 Decode 的计算特征不同

一次 LLM 推理请求可以粗略拆成 Prefill 和 Decode 两个阶段：

- Prefill 阶段处理输入 Prompt，并生成后续 Decode 所需的 KV Cache。Prompt 越长，需要完成的计算越多，因此这一阶段通常是 **compute-bound**，主要受 GPU 计算能力限制。

- Decode 利用已有 KV Cache，一次生成一个 Token。生成过程中，Attention 需要持续读取此前 Token 对应的 KV Cache；上下文越长，需要读取的历史 KV 越多，因此这一阶段通常是 **memory-bandwidth-bound**，更依赖显存带宽与 KV Cache 容量。

### 性能指标与资源差异

两个阶段对应的性能指标也不同：

- Prefill 侧主要关注 **TTFT（Time To First Token）**：也就是从请求发出到收到第一个 Token 的时间。它不仅包含 Prefill 计算，还包括此前的排队和路由等开销。

- Decode 侧主要关注 **TPOT（Time Per Output Token）** 和 **ITL（Inter-Token Latency）**：
    - TPOT 表示第一个 Token 之后，平均生成一个输出 Token 所需的时间；
    - ITL 表示相邻两个输出 Token 之间的时间，更能反映流式输出是否稳定、有没有明显抖动。

在 Long Context 场景下，Prefill 和 Decode 对计算、显存带宽与容量的需求更加不平衡。

如果把它们拆开，**Prefill Pool 可以偏向计算能力更强的 GPU，Decode Pool 则可以偏向显存带宽更高、容量更大的 GPU**。

两边的 Batch 策略也不同。Prefill 的 Batch 增大到一定程度后，计算单元会逐渐饱和，继续增加 Batch 对吞吐的提升趋缓，还可能推高 TTFT，因此通常需要控制 Batch Size。Decode 每一步只为每条活跃序列生成一个 Token，适当增大 Batch 可以提高 GPU 利用率与整体输出吞吐，但仍然要受 TPOT、ITL 和 KV Cache 容量约束。

![不同 Batch Size 和输入长度下 Prefill、Decode 两个阶段的吞吐变化](https://img.lixueduan.com/ai/kserve/distserve-figure-3.png)

> 图源：《[DistServe: Disaggregating Prefill and Decoding for Goodput-Optimized Large Language Model Serving](https://arxiv.org/abs/2401.09670)》，Figure 3。展示了模型在不同 Batch Size 与输入长度下的测试结果。具体数值取决于模型、硬件和推理引擎，这里主要观察两条趋势：Prefill 吞吐较早趋于平缓，而 Decode 吞吐可以随 Batch Size 增大继续提升。

在同一设备上同时运行 Prefill 和 Decode，意味着这颗芯片必须同时具备**强大的算力（满足 Prefill 需求）**和**巨大的显存容量/带宽（满足 Decode 的 KV Cache 需求）**。

然而，受限于芯片面积、功耗、良率和成本，芯片设计需要在"计算单元"和"显存接口/HBM 容量"之间做面积与功耗的 trade-off。因此，一颗"通吃"两颗阶段需求的芯片必然成本高昂，且在任一单一阶段都难以达到最优性价比。

PD 分离的本质，**就是承认这种硬件层面的 trade-off，让 Prefill 实例和 Decode 实例各自采用最适合自己工作负载特征的硬件配置（如 Prefill 用高算力卡，Decode 用高 HBM 容量卡），从而在系统层面实现总体拥有成本（TCO）的最优**。

### 共置部署为什么会互相干扰

即使每次都选对了实例，也只解决了路由问题。Prefill 和 Decode 如果继续放在同一个实例里，就会共享 GPU 和调度队列。长 Prompt 带来的 Prefill 峰值，可能抬高其他请求的 Token 间延迟。

这就是执行层的问题：怎样减少 Prefill 和 Decode 共享 GPU、共享调度队列带来的相互干扰。对应的解决思路，就是进一步把 Prefill 和 Decode 拆到不同实例，也就是 PD 分离。

## PD 分离到底拆了什么？

**PD 分离（Prefill/Decode Disaggregation）** 将 LLM 推理的两个阶段解耦，部署到独立的 Prefill 实例组和 Decode 实例组：

- Prefill 实例组接收完整输入 Prompt，构建本次请求所需的 KV Cache，并生成首个输出 Token；
- Decode 实例组通过高速网络获取 Prefill 产出的 KV Cache，以首 Token 为起点进行自回归解码，逐 Token 生成直至请求结束；

两个阶段通过高效的 KV Cache 传输机制衔接，并可以独立扩缩容，从而在合适的资源配比和传输网络下，同时优化吞吐量与延迟。

![聚合式推理与 PD 分离对比](https://img.lixueduan.com/ai/kserve/kserve-pd-disaggregation.jpg)

这里拆分的是**执行位置和资源池**，不是模型的计算顺序。一次请求仍然要先完成 Prefill，才能进入 Decode，只是两个阶段不再运行在同一个实例里。

因此，一次请求会像接力一样经过两组实例：

```text
请求进入
   │
   ▼
Prefill Pool：处理 Prompt，生成 KV Cache 和首 Token
   │
   ▼
Decode Pool：取得 KV Cache，继续生成后续 Token
   │
   ▼
返回响应
```

拆到这里，一个新的问题也随之出现：Prefill 和 Decode 不在同一个实例，KV Cache 怎么交接？

KV Cache 可以理解为模型处理 Prompt 后留下的中间状态。Decode 要从 Prefill 结束的位置继续生成，就必须获得本次请求对应的 KV Cache（否则就需要重新计算）。

聚合式推理中，两个阶段在同一个实例内运行，Decode 可以直接继续使用 Prefill 已经构建的 KV Cache。PD 分离后，两个阶段位于不同实例，各自管理本地显存，因此必须增加一次显式的交接：

1. Prefill 完成计算，将 KV Cache 保留在本地，同时向请求编排组件返回 KV Transfer 元数据。
2. 编排组件携带这些元数据发起 Decode 请求。
3. Decode 根据元数据，通过 NIXL 等 KV Connector 从 Prefill 实例获取 KV Cache，然后继续逐 Token 生成。

这里有两条不同的链路：编排组件传递的是**如何取得 KV Cache 的元数据**；真正的 **KV Cache 数据**不经过 Router / Proxy。至于数据是在 Prefill 和 Decode 之间 P2P 传输，还是经由共享存储或缓存服务交接，则取决于具体使用的 Connector。

这也不等于 Prefix Cache 命中。
- **Prefix Cache 复用**解决的是 Prefill 能否利用以前计算过的相同前缀，从而少做一部分计算；
- **KV Cache 传输**解决的是本次 Prefill 的结果如何交给 Decode。即使没有命中 Prefix Cache，Prefill 也可以完整处理 Prompt，再把新生成的 KV Cache 传给 Decode。

### KV Cache 共享方式有哪些？

在 vLLM 中，KV Cache 的交接由 **KV Connector** 抽象。它让作为 KV producer 的 Prefill 实例，将缓存交给作为 KV consumer 的 Decode 实例；调度器侧的 Connector 负责安排传输，Worker 侧的 Connector 负责实际读写数据。

不同 Connector 的差异，主要在于 KV Cache 放在哪里，以及 Prefill 和 Decode 之间通过什么方式传输：

- **P2P 传输**：`NixlConnector` 通过 NIXL 在实例之间传输 KV Cache，适合高速网络和异步收发场景。
- **共享存储或缓存服务**：`ExampleConnector` 使用本地共享目录，主要用于验证 Connector 接口和 PD 分离流程；`LMCacheConnectorV1` 由 LMCache 管理 KV Cache，可使用 NIXL 作为底层传输，也支持独立的 LMCache Server；`MooncakeConnector` 与 `FlexKVConnectorV1` 则分别使用 Mooncake 和 FlexKV 作为 KV Cache 的传输、管理或分布式存储层。
- **卸载、多级缓存与特定硬件**：`OffloadingConnector` 将 KV Cache 转移到 CPU 内存或文件系统等更低层级的存储中；`MultiConnector` 按顺序组合多个 Connector，形成多级 KV Cache 路径；`MoRIIOConnector` 面向 ROCm 环境。

vLLM 通过 `KVTransferConfig` 选择 Connector，并传递具体实现需要的参数。例如 NIXL Connector 的配置可以写成：

```bash
--kv-transfer-config \
'{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'
```

Connector 解决的是 KV Cache 如何被生产、保存和消费，不负责决定请求应该选择哪个 Prefill 或 Decode 实例。后者属于路由和请求编排问题，两者可以组合使用，但不是同一层能力。

## PD 分离的收益与代价

**PD 分离之后，最直接的收益是隔离两种计算负载**。
vLLM 官方文档特别强调了 tail ITL：
- 聚合模式会在 Decode 过程中插入 Prefill 工作，导致部分 Token 的生成间隔突然升高；
- PD 分离以后，长 Prompt 留在 Prefill Pool，不再直接占用 Decode Pool 的调度队列，逐 Token 输出会更稳定。

**Prefill 与 Decode 还可以独立设置副本数、GPU 类型和并行策略**：
- 文档问答通常输入很长、输出较短，可能需要更多 Prefill 能力；
- 聊天或推理场景输出很长，则可能更需要 Decode 能力。
通常可以用 `xP:yD` 表示两组实例的比例，具体取值要根据真实的输入长度、输出长度、并发和延迟目标压测出来。

*PD 分离也不是减少阶段干扰的唯一方案。*

vLLM 默认支持 **Chunked Prefill**，它会把较长的 Prefill 拆成较小的 Chunk，优先调度 Decode，再利用剩余的 Token Budget 插入 Prefill。这样可以在同一实例中平衡 compute-bound 的 Prefill 和 memory-bound 的 Decode，部署与调度也更简单；
但两个阶段仍然共享 GPU，`max_num_batched_tokens` 的取值也需要在 TTFT、ITL 和吞吐之间权衡。只有当这种共置优化仍无法同时满足两个阶段的 SLO，或者两边需要完全不同的资源配置和扩缩容策略时，PD 分离的价值才会更加明显。

**当然，PD 分离也不是银弹**。

- 模型权重要在 Prefill 和 Decode 两组 GPU 上分别加载
- 一次请求多了 Endpoint 选择和阶段编排
- KV Cache 还要跨实例传输
- 还需要额外考虑 NIXL、RDMA、故障恢复和两组工作负载的独立扩缩容。

**如果模型很小、Prompt 很短，或者网络只能走普通 TCP，这些额外开销可能超过隔离带来的收益。**

因此，单 GPU vLLM 不会因为换成 PD 架构就自动变快，vLLM 也明确提醒，Disaggregated Prefilling 本身并不保证提高吞吐。
**只有当 Prefill 对 Decode 的干扰已经影响 TTFT 或 ITL，或者两边的资源需求明显不对称时，PD 分离才值得引入**。

## 小结

PD 分离的核心原因，是 Prefill 和 Decode 的瓶颈并不相同：
* Prefill 通常是 **compute-bound**，更依赖 GPU 算力；
* Decode 则通常是 **memory-bandwidth-bound**，更依赖显存带宽和 KV Cache 容量。

把两个阶段放在同一个实例里，就要求同一套硬件同时满足两种不同的资源需求。

这不表示不存在同时拥有强算力和大 HBM 的高端芯片。问题在于，增加计算单元不一定能改善 Decode，堆叠更大的 HBM 和更宽的显存接口又会带来额外的面积、功耗和成本。**硬件本身就在算力与内存系统之间做取舍**，不存在能在所有输入输出比例、延迟目标和成本约束下都对两个阶段最优的芯片配置。

因此，PD 分离不是把一次推理变成两次推理，而是把同一次推理中的 Prefill 与 Decode 放到各自合适的资源池：Prefill 可以使用偏算力的硬件，Decode 可以使用偏显存容量和带宽的硬件，再通过 KV Cache 交接把两个阶段衔接起来。

当然，**PD 分离不是银弹**，这套架构也需要额外的调度、网络和容量规划。先用 Chunked Prefill 等共置优化解决问题；只有 TTFT、ITL 或资源配比已经成为明确瓶颈时，再引入 PD 分离。

---

> 作者: [意琦行](https://github.com/lixd)  
> URL: https://www.lixueduan.com/posts/ai/27-why-pd-disaggregation/  

