PD 分离
Prefill-Decode Disaggregation(PD 分离)把大模型推理的 Prefill 和 Decode 阶段放到不同的 Worker 池中:Prefill Worker 处理 Prompt 并生成 KV Cache,Decode Worker 接收 KV Cache 后持续生成 token。
它解决的核心问题是阶段干扰。Prefill 和 Decode 虽然执行同一模型,但负载形态、延迟目标和最佳资源配置并不相同;让两者长期共享同一批 GPU,会迫使调度器在 TTFT 与 TPOT 之间反复折中。
本文以 SGLang 的实现为例,说明 PD 分离的通用架构和执行过程。SGLang 的 Router、Bootstrap Room 和 P/D 队列是具体实现,背后的阶段隔离、KV Handoff 与状态接管则是 PD 分离的共性。
Prefill 与 Decode 为什么需要隔离
| 对比项 | Prefill | Decode |
|---|---|---|
| 输入形态 | 一次处理多个 Prompt token | 每轮处理一个或少量新 token |
| 计算特征 | 大矩阵计算,通常偏计算密集 | 反复读取权重和 KV,通常偏访存密集 |
| 主要延迟指标 | TTFT | TPOT / ITL |
| 资源压力 | Prompt 长度、Prefill Batch | 并发序列数、输出长度、KV 容量 |
在聚合式引擎中,Prefill 与 Decode 共用 Scheduler、Batch 和 GPU。一个长 Prompt 加入执行时,可能让正在生成的请求长时间得不到下一轮 Decode,直接抬高尾部 TPOT;如果通过缩小 Prefill Chunk 保护 Decode,又会增加 Prefill 调度轮数,恶化 TTFT。
SGLang 还指出了 DP Attention 下的同步问题:不同 DP Rank 同时处理 Prefill 和 Decode 时,阶段耗时不一致会让 Decode Rank 等待较慢的 Prefill Rank。PD 分离后,同一个资源池内的请求处于相近阶段,Batch 形态更加稳定。
PD 分离改变了什么
PD 分离不是减少模型计算量,而是重新组织请求和状态:
- 资源分离:Prefill 和 Decode 使用独立 Worker 池,可以分别扩缩容和选择并行策略。
- 请求拆分:Router 为一次生成选择一个 P Worker 和一个 D Worker,并把两侧请求关联起来。
- 状态迁移:Prompt 产生的 KV Cache 必须从 P 移交给 D。
- 执行接管:D 收到 KV 后,需要恢复请求状态并继续生成,Client 只看到一条连续响应。
其中,KV Handoff 是最关键的新增路径。忽略 Page Padding 和额外状态,标准 MHA/GQA 模型的 KV Cache 大小约为:
因此 PD 分离后的 TTFT 中会增加 KV 传输和 D 侧接入开销:
PD 的收益来自隔离和独立调度,代价来自 KV 迁移。只有前者大于后者时,分离才有实际价值。
整体架构:以 SGLang 为例
一套完整的 PD 系统通常包含 Router、P Worker、D Worker、协调通道和 KV 数据通道。SGLang 对这些角色的实现如下:
| 通用角色 | SGLang 实现 | 职责 |
|---|---|---|
| Router | SGLang Model Gateway | 分别选择 P/D Worker,并把一次请求同时交给两侧 |
| 请求关联 | bootstrap_room | 标识同一次 P→D 状态传输 |
| 协调通道 | P 侧 Bootstrap Server | 交换并行拓扑、Rank、目标 KV Page 和传输状态 |
| Prefill 状态机 | PrefillBootstrapQueue | 等待 D 准备完成,执行 Prefill 并发送 KV |
| Decode 状态机 | DecodePreallocQueue、DecodeTransferQueue | 预留空间、接收 KV,并把请求交给 Decode Batch |
| KV 数据通道 | Mooncake / NIXL | 在 P/D 显存之间搬运 KV Cache |
这里需要区分两条通路:
- 控制面传递 Room ID、并行拓扑、KV Page 索引、内存地址和完成状态,数据量很小。
- 数据面传输所有层的 KV Cache,数据量大,应直接走 GPU P2P、RDMA 等高速路径,而不是经过 HTTP Router。
PD 分离并不规定唯一的握手顺序。SGLang 采用的是 D 侧先准备目标空间、P 侧随后 Push KV 的方式,下面以这条链路展开。
一次请求的执行过程
一次 Client 请求在 Gateway 中拆成关联的 P/D 请求,按“D 预留空间 → P 生成状态 → D 接管生成”执行。
1. Gateway 选择 P/D Worker
- Gateway 接收 Client 请求,保持 Client 侧 HTTP 连接。
- 根据模型从 Worker Registry 中取出 Prefill Pool 和 Decode Pool,排除不健康或 Circuit Breaker 已打开的 Worker。
- 分别运行 Prefill Policy 和 Decode Policy。P 侧可按 Prompt 负载或 Prefix Cache 选择,D 侧可按活跃序列数或 token 负载选择。
- 将选中的 P Worker 和 D Worker 组成仅对本次请求有效的逻辑 Pair。
2. Gateway 并发发送 P/D 请求
- Gateway 生成唯一的
bootstrap_room,用于关联同一次 P→D 状态传输。 - Gateway 生成 P/D 两份上游请求,将 Room ID、P 的 Bootstrap 地址和生成参数写入请求。
- Gateway 并发请求 P 和 D,使 D 侧握手和内存预留可以提前开始。
- P 侧响应用于报告 Prefill 和传输结果;D 侧 HTTP 请求保持等待,后续返回生成流。
3. D 准备目标空间,P 执行 Prefill
- D 将请求放入 Prealloc Queue,初始化 KV Receiver。
- D 通过 P 侧 Bootstrap Server 交换 TP、CP、DP、PP 拓扑、Rank 映射和传输端点。
- D 执行容量准入,分配请求槽位、Metadata Buffer、Prompt KV Page 以及 Decode 预留空间。若启用 D 侧 Prefix Cache,则先匹配本地 Prefix,只为缺失部分分配 Page。
- D 将 Prefix 命中长度、目标 Page 索引和传输地址发给 P,然后进入 Transfer Queue。
- P 在 Bootstrap Queue 确认 D 就绪后进入 Prefill Waiting Queue,与其他 Prompt 组成 Prefill Batch。
- P 复用本地 Prefix Cache,计算未命中的 Prompt,生成各层 KV 和第一个输出 token。
4. P 向 D 移交 KV 与元数据
- P 根据 D 提供的 Page 索引,确定待发送 KV 与目标 Page 的对应关系。
- P 通过 Mooncake 或 NIXL 将 KV 直接写入 D 的显存,数据不经过 Gateway。P/D Attention TP 不同时,该过程还包含 Gather、Layout Remap 或 Staging Buffer。
- P 将首 token、Prefix 命中长度、Logprob 和
bootstrap_room等小型状态写入 D 预留的 Metadata Buffer。 - 启用 Chunked Prefill 时,P 按 Chunk 增量发送 KV,最后一个 Chunk 提交完整元数据。
- P 将请求放入 Inflight Queue,轮询传输状态;确认 D 接收完成后,再释放本地 KV 和 Metadata Buffer。
5. D 校验状态并接管 Decode
- D 在 Transfer Queue 轮询 KV Receiver,确认所有相关 Rank 都已完成 KV 和元数据传输。
- D 校验 Metadata Buffer 中的
bootstrap_room,确认收到的状态属于当前请求。 - D 将首 token、Prefix 命中长度和 Logprob 提交到本地请求对象,建立 Prompt token 到 KV Page 的索引。
- D 构造 Prebuilt Extend Batch,只补齐 Scheduler 所需的 Batch 和索引元数据,不重复执行 Prompt Forward。
- 请求进入 Waiting Queue,再合并到 Running Batch;D 复用已接收的 KV,从 P 产生的首 token 继续逐 token Decode。
6. Gateway 代理响应并回收资源
- D 将首 token 和后续 token 写入与 Gateway 建立的上游 Response Stream。
- Gateway 将 D 的响应流代理到 Client 原有的 SSE 或 Chunked HTTP 连接。
- 正常结束时,D 释放请求槽位和不再保留的 KV Page,Gateway 结束 Client 响应和本次 P/D 关联。
- P 失败时,Gateway 取消正在等待 KV 的 D;Client 断开或传输超时时,对应组件取消请求并回收 Page、Buffer 和 Room 状态。
三个组件的状态流可以概括为:
Gateway: Client Request -> Select P/D -> Dual Dispatch -> Proxy D Response
Prefill: BootstrapQueue -> WaitingQueue -> Prefill Forward -> Inflight Transfer -> Finish
Decode : PreallocQueue -> TransferQueue -> Prebuilt Batch -> Running Batch -> Stream收益与代价
| 维度 | 收益 | 代价 |
|---|---|---|
| 调度 | Prefill 不再直接打断 Decode | Router 需要同时管理 P/D 两个请求 |
| 延迟 | TTFT、TPOT 可以分别优化 | TTFT 增加 KV 传输与接入时间 |
| 资源 | P/D 可以独立扩缩容、采用不同并行度 | 两侧都要加载模型权重 |
| 稳定性 | Decode Batch 更稳定,尾部 TPOT 更可控 | P、D、传输任一环节失败都要联合清理 |
| 缓存 | P 池可专注处理 Prompt 和 Prefix | P 侧命中的 Prefix KV 仍可能需要传到 D |
PD 分离更适合长 Prompt、高并发、Prefill 经常破坏尾部 TPOT,并且 P/D 最佳资源配比差异明显的场景。对于短 Prompt、低并发或网络带宽不足的服务,KV Handoff 可能比阶段干扰更昂贵,聚合部署反而更简单。
源码入口
SGLang 的实现把 Router、调度状态机和传输 Backend 分开,关键入口如下:
| 模块 | 源码 |
|---|---|
| P/D 请求选择与并发转发 | sgl-model-gateway/src/routers/http/pd_router.rs |
| Bootstrap 服务与 Rank 映射 | srt/managers/disagg_service.py、disaggregation/common/conn.py |
| Prefill 握手与 KV 发送 | disaggregation/prefill.py |
| Decode 预分配、接收与接管 | disaggregation/decode.py |
| Mooncake / NIXL Backend 映射 | disaggregation/utils.py |
这些实现共同体现了 PD 分离的核心边界:Router 关联请求,Scheduler 管理阶段状态,Transfer Backend 移动 KV,Decode 在数据完整后接管生成。
总结
PD 分离本质上是阶段专用调度 + KV 状态迁移。它通过独立的 P/D 资源池消除阶段干扰,让 TTFT 和 TPOT 可以分别优化;同时也引入了 KV 传输、目标内存预分配、请求状态接管和跨组件故障处理。
以 SGLang 为例,一次请求由 bootstrap_room 关联,Router 并发发给 P/D,D 先预留 KV Page,P 再将 KV 直接写入 D,最后 D 进入正常 Decode 调度。这条链路是理解 PD 分离机制最直接的方式。