NOTE 本文基于 vLLM v0.27.1(tag 6e448d0, 2026-08-11)源码深度剖析。文中所有文件路径、类名和行号均以该版本为准;vLLM 迭代很快,阅读时请以你手上的版本对照。

本章把前面讨论的四个问题从单机推向集群:当 Prefill 和 Decode 不再共享同一批 GPU,调度、KV Cache、路由和故障恢复都需要重新设计。

前八章主要讨论的是:

如何在一台机器或一组共享设备上,把请求高效地运行起来?

第九章进一步追问:

如果 Prefill 和 Decode 不再共享同一批 GPU,
它们如何协作完成同一个请求?

这就是 PD 分离,也就是 Prefill/Decode Disaggregation

PD 分离表面上是把 GPU 分成两个池子:

Prefill Pool  → 负责处理输入 Prompt
Decode Pool   → 负责逐 Token 生成

但它的本质并不是简单的服务拆分,而是:

把一次请求的执行过程拆成两个阶段,并把阶段之间的 KV Cache 作为跨节点状态进行传递。

因此,PD 分离真正改变的不是某个算子,而是 Serving 系统的状态边界:

单机 Serving:
    请求、调度、KV Cache 大多在本地完成

集群 Serving:
    请求需要跨节点路由
    KV Cache 需要跨节点传输
    状态需要被查询、缓存、恢复和淘汰

当前,在讨论怎么PD分离之前,我们要先搞清楚为什么要PD分离。

1. 为什么要分离 Prefill 和 Decode?

1.1 两种计算阶段,两种资源画像

Prefill 和 Decode 使用的是同一套模型,但它们的计算行为并不相同。

Prefill 需要一次性处理用户输入的全部 Prompt token:

Prompt:
[t₁, t₂, t₃, ..., tₙ]

一次前向:
    同时处理大量输入 token
    生成对应的 KV Cache

它通常具有以下特点:

  • 计算量大;
  • 矩阵乘法规模较大;
  • 更容易表现为计算密集型;
  • 长 Prompt 会显著增加处理时间;
  • 目标通常是降低 TTFT,即首 Token 延迟。

Decode 则是在已有 KV Cache 的基础上,每轮生成一个或少量 token:

已有 KV Cache
    +
当前生成 token
    ↓
生成下一个 token

它通常具有以下特点:

  • 每轮计算规模较小;
  • 需要反复读取模型权重和 KV Cache;
  • 更容易受到内存带宽、访存延迟和 batch 组织方式影响;
  • 生成阶段持续时间长;
  • 目标通常是稳定 TPOT,即每个输出 Token 的延迟。

这里需要强调:

Prefill 更偏计算密集,Decode 更容易受到带宽和访存影响,但这不是绝对规律。实际瓶颈还取决于模型结构、上下文长度、Batch 大小、量化方式和硬件型号。

同一批 GPU 同时承担两种 workload 时,资源画像会不断变化:

时间 →
┌──────────────┬──┬──┬──┬──────────────┬──┬──┐
│   Prefill    │D │D │D │   Prefill    │D │D │
└──────────────┴──┴──┴──┴──────────────┴──┴──┘
   计算密集        访存敏感       计算密集       访存敏感

混合部署下,一个长 Prompt 可能占用大量计算资源,使正在 Decode 的请求被延迟;而 Decode 请求又会持续占用 KV Cache 和显存,限制 Prefill 的 batch 组织。

因此,混合部署的主要问题并不是“Prefill 和 Decode 不能同时运行”,而是:

两种阶段的资源需求不同,却被迫共享相同的调度和硬件资源。

1.2 混合部署的主要代价

问题 直接后果
Prefill 计算量大 可能抢占 Decode 的执行机会
Decode 持续占用 KV Cache 限制 Prefill 的 batch 和并发
长 Prompt 进入队列 TTFT 和 TPOT 同时抖动
两类 workload 共用硬件 难以针对性选择设备和并行度
负载比例动态变化 资源利用率容易失衡
扩容只能整体扩容 Prefill 或 Decode 其中一侧可能出现资源浪费

例如,某一时刻请求主要是长文档输入,Prefill 池成为瓶颈;过一段时间后,输入变少,但大量请求进入长文本生成阶段,Decode 池又成为瓶颈。

如果 Prefill 和 Decode 共用同一批 GPU,只能整体增加机器,无法针对真正的瓶颈进行扩展。

1.3 PD 分离的基本架构

PD 分离将两类 workload 放到不同的资源池中:

graph LR
    R["Router<br/>请求路由与负载感知"]

    P["Prefill Pool<br/>处理 Prompt<br/>生成 KV Cache<br/>优化 TTFT"]

    T["KV Transfer<br/>跨节点传输 KV Cache<br/>RDMA / GPU Direct / 共享存储"]

    D["Decode Pool<br/>加载 KV Cache<br/>逐 Token 生成<br/>优化 TPOT"]

    R --> P
    P --> T
    T --> D

    style R fill:#e8f3ff,stroke:#4a90e2
    style P fill:#fff4d6,stroke:#d99a00
    style T fill:#f3e8ee,stroke:#c45a7a
    style D fill:#e8f8ee,stroke:#4a9c68

两类节点可以采用不同的资源配置:

Prefill Pool:
    更关注计算吞吐
    适合较高计算能力
    可以采用不同的 Tensor Parallel 配置
    重点优化长 Prompt 处理

Decode Pool:
    更关注内存带宽和稳定吞吐
    需要容纳更多并发请求的 KV Cache
    重点优化持续生成和 TPOT

PD 分离带来的核心收益包括:

  • Prefill 和 Decode 可以独立扩缩容;
  • 长 Prompt 不容易直接阻塞 Decode;
  • 两个池子可以使用不同的硬件配置;
  • 可以分别优化 TTFT 和 TPOT;
  • Decode 节点可以维持更稳定的 Continuous Batching;
  • 可以根据实际流量比例调整 Prefill/Decode 资源配比。

但它并不是无条件的性能升级。PD 分离会引入新的成本:

  • KV Cache 必须跨节点传输;
  • 请求路由变得更加复杂;
  • 节点间网络成为新的瓶颈;
  • KV Cache 的状态需要被追踪和恢复;
  • 故障处理从单进程问题变成分布式问题。

所以更准确的判断是:

PD 分离用跨节点协调和 KV Transfer 的成本,换取 Prefill 与 Decode 的资源独立性。

2. 一次请求如何经过 Prefill 和 Decode?

理解 PD 分离最直接的方式,是跟踪一个请求的完整生命周期。

sequenceDiagram
    participant U as 用户 / API Client
    participant R as Router
    participant P as Prefill Node
    participant T as KV Transfer
    participant D as Decode Node

    U->>R: 发送 Prompt
    R->>P: 选择 Prefill 节点
    P->>P: 执行 Prompt Prefill
    P->>P: 生成 KV Cache
    P->>T: 提交 KV 传输任务
    T->>D: 搬运并写入 Decode 节点
    D->>D: 加载 KV Cache
    D->>D: 执行首个 Decode Step
    D-->>U: 返回首个 Token

    loop 后续生成
        D->>D: 读取 KV Cache
        D->>D: 生成下一个 Token
        D-->>U: 流式返回 Token
    end

一次请求大致经历以下阶段。

2.1 第一步:Router 选择 Prefill 节点

Router 根据输入长度、Prefill 节点负载和可能的 Prefix Cache 命中情况,选择一个 Prefill 节点。

此时需要考虑的并不只是请求数,还包括:

  • Prompt token 数量;
  • 当前 Prefill 队列长度;
  • 节点剩余计算容量;
  • 已有 Prefix Cache 是否可复用;
  • 目标节点的设备和并行配置。

2.2 第二步:Prefill 节点处理 Prompt

Prefill 节点执行模型前向,处理输入 Prompt,并生成各层的 K/V 张量。

这些 KV 通常按照 vLLM 的 block 组织方式写入 KV Cache:

Prompt token
    ↓
Attention 层计算
    ↓
K/V 张量
    ↓
KV Cache Block

2.3 第三步:KV Transfer

Prefill 节点将请求需要的 KV Cache 传输给目标 Decode 节点。

传输内容可能包括:

  • KV Cache Tensor;
  • block table;
  • token 数量;
  • layer 信息;
  • 数据类型和布局;
  • 请求标识;
  • 传输状态和元数据。

2.4 第四步:Decode 节点接管请求

Decode 节点收到 KV Cache 后,将它放入本地 Cache,并建立请求与 KV block 的映射。

之后,Decode 节点不再重复处理原始 Prompt,而是直接基于已有 KV Cache 生成后续 token。

2.5 第五步:持续 Decode

后续生成通常持续在 Decode 池内完成:

读取已有 KV Cache
    ↓
处理当前 token
    ↓
生成下一个 token
    ↓
追加新的 KV Cache

这时请求已经从 Prefill 节点迁移到了 Decode 节点。

因此,PD 分离不是:

Prefill 服务返回结果
Decode 服务重新开始执行

而是:

Prefill 服务计算出中间状态
Decode 服务接管这个中间状态

这个中间状态就是 KV Cache。

3. KV Transfer:PD 分离真正的难点

把 GPU 分成两个池子很容易,真正困难的是中间这条连接线:

Prefill Node
    ──────────────── KV Cache ────────────────>
Decode Node

如果 KV 传输速度不够快,Prefill 阶段节省下来的计算时间,可能会被网络传输和同步等待重新吃掉。

因此:

PD 分离的核心不是“有没有两个池子”,而是 KV 能否高效、正确、及时地从一个池子交给另一个池子。

3.1 KV Cache 到底有多大?

KV Cache 的大小取决于模型结构和数据类型。一个简化的估算公式是:

KV bytes/token
≈
层数
× 2(K 和 V)
× KV heads
× head_dim
× 每元素字节数

整个请求的 KV 大小大致为:

KV 总大小
≈
Prompt token 数
× KV bytes/token

影响 KV 大小的主要因素包括:

  • Transformer 层数;
  • KV head 数;
  • head dimension;
  • KV Cache 数据类型;
  • GQA / MQA 配置;
  • block 对齐和元数据;
  • 是否使用压缩或量化 KV Cache。

例如,沿用前文的示例配置,假设:

每个 token 的 KV Cache ≈ 320 KB
Prompt 长度 = 2050 token

那么:

2050 × 320 KB
≈ 656,000 KB
≈ 641 MiB

因此,这个请求需要从 Prefill 节点搬运的 KV 数据大约是 641 MiB

这里的 320 KB/token 只是特定模型、层数、KV heads、head_dim 和数据类型下的示例,不是所有模型的固定值。

3.2 传输时间如何估算?

理想情况下:

传输时间
≈
KV 数据量 / 实际有效带宽
+
端到端固定延迟

如果 KV 大小为 641 MiB,实际有效带宽为 50 GB/s,则仅从带宽下限估算:

641 MiB / 50 GB/s
≈ 12.8 ms

但这只是理想值。真实端到端延迟还会受到以下因素影响:

  • KV block 的切分方式;
  • 元数据交换;
  • GPU 与 NIC 的拓扑;
  • PCIe 或 NVLink 路径;
  • RDMA 协议开销;
  • DMA 调度;
  • 接收端内存分配;
  • Tensor 布局转换;
  • 传输完成后的同步;
  • 多请求并发传输造成的带宽竞争。

因此,更准确的表述是:

12.8 ms 是理想带宽下限,不代表请求从 Prefill 到 Decode 的真实交接延迟。

PD 分离需要优化的是完整链路:

KV 生成
  + 元数据准备
  + 网络传输
  + 目标端写入
  + KV 状态确认
  + Decode 开始执行

3.3 KV Transfer 能否与计算重叠?

最理想的情况不是:

先完成全部 KV 传输
    ↓
再开始 Decode

而是让数据传输和模型执行尽可能形成流水:

Layer 0 KV 到达 → 可以开始处理 Layer 0
Layer 1 KV 到达 → 可以开始处理 Layer 1
Layer 2 KV 到达 → 可以开始处理 Layer 2

示意如下:

gantt
    title KV 传输与模型执行的流水
    dateFormat  s
    axisFormat  %S

    section KV Transfer
    Layer 0 KV 到达 :done, kv0, 0, 1
    Layer 1 KV 到达 :active, kv1, 1, 1
    Layer 2 KV 到达 :kv2, 2, 1
    Layer 3 KV 到达 :kv3, 3, 1

    section Attention / Decode
    执行 Layer 0 :a0, 1, 1
    执行 Layer 1 :a1, 2, 1
    执行 Layer 2 :a2, 3, 1
    执行 Layer 3 :a3, 4, 1

一些 KV Transfer 实现支持按层、按阶段或按块等待,使传输与模型前向存在重叠空间。

不过,需要注意:

是否能够真正实现按层流水,取决于 KV Connector、Worker、Attention Backend、Cache 布局和底层传输实现之间的协作。

如果传输必须等整个请求的 KV 全部到齐后才能开始 Decode,那么流程就会退化为:

Prefill
    ↓
整请求 KV 传输
    ↓
Decode

此时传输和计算完全串行,PD 分离的收益会显著下降。

4. vLLM 中的 KV Transfer 抽象

从功能职责上,可以把 KV Transfer 理解为三类组件:

Serving 语义层
    ↓
KV 索引与缓存层
    ↓
数据传输与存储层

不同实现的模块边界并不完全相同,但它们解决的问题大致可以归入这三类。

4.1 Serving 语义层:KV Connector

KV Connector 面向 vLLM 的调度和执行流程,负责把“远端 KV Cache”纳入请求生命周期。

它需要解决的问题包括:

  • 远端是否已经存在某个 Prompt 的 KV;
  • 当前请求有多少 token 可以复用;
  • Scheduler 是否应该为这些 token 分配计算资源;
  • Worker 何时开始加载 KV;
  • 某一层的 KV 是否已经可用;
  • 请求结束后如何清理相关状态;
  • 传输失败后是否回退到重新 Prefill。

从职责上可以分为两组。

4.1.1 Scheduler 侧

Scheduler 不应该直接搬运 Tensor,而是负责做决策:

是否存在远端 KV?
可以复用多少 token?
需要为哪些 token 分配本地 block?
当前请求是否可以进入下一阶段?

典型接口可能包括:

get_num_new_matched_tokens()
update_state_after_alloc()
request_finished()

4.1.2 Worker 侧

Worker 负责真正的数据面操作:

开始加载 KV
等待某一层 KV 到达
保存某一层 KV
将 KV 写入本地 Cache

典型接口可能包括:

start_load_kv()
wait_for_layer_load()
save_kv_layer()

因此,KV Connector 体现了前面章节反复出现的控制面与数据面分离:

graph TD
    S["Scheduler<br/>控制面"]
    C["KV Connector<br/>语义协调"]
    W["Worker<br/>数据面"]
    P["KV Pipe<br/>实际传输"]
    D["Decode KV Cache<br/>本地缓存"]

    S -->|"查询、决策、分配"| C
    C -->|"下发加载任务"| W
    W -->|"发送 / 接收 Tensor"| P
    P -->|"写入"| D

    style S fill:#e8f3ff,stroke:#4a90e2
    style C fill:#fff4d6,stroke:#d99a00
    style W fill:#e8f8ee,stroke:#4a9c68

Scheduler 只决定:

要不要使用远端 KV
要使用多少远端 KV
什么时候允许请求继续执行

Worker 才真正执行:

从哪里读取 KV
把 KV 搬到哪里
如何写入本地 Cache
什么时候确认完成

4.2 KV 索引与缓存层

跨节点传输并不意味着每次都要重新搬运全部 KV。

如果多个请求拥有相同前缀,系统可以尝试复用已有 KV:

请求 A:
[系统提示词][长文档 A]

请求 B:
[系统提示词][长文档 B]

如果前缀部分相同:

[系统提示词]

那么请求 B 可以复用请求 A 已经生成的部分 KV。

这需要一个 KV 索引与缓存层,负责维护:

Token 前缀 / KV Block
        ↓
远端位置、状态、引用关系和生命周期

它可能需要支持:

  • KV 查询;
  • Prefix Cache 命中;
  • block 元数据管理;
  • 引用计数;
  • 插入和驱逐;
  • 过期和失效;
  • 远端存储位置记录;
  • 传输状态追踪。

这层的关键作用是:

尽量减少重复计算和重复搬运。

但它也会带来新的问题:

  • 远端 KV 是否仍然有效;
  • 不同模型版本能否复用;
  • 不同数据类型能否复用;
  • 不同并行配置下布局是否兼容;
  • KV 被驱逐后如何通知使用方;
  • 多个请求同时写入同一个前缀时如何处理。

4.3 数据传输与存储层

最底层是数据传输与存储机制,负责真正搬运 KV Tensor。

可能涉及的路径包括:

GPU → GPU
GPU → CPU
CPU → GPU
GPU → NIC → 远端 GPU
GPU → 分布式内存池
GPU → 共享存储 → 远端 GPU

常见优化方向包括:

  • RDMA;
  • GPUDirect RDMA;
  • GPU IPC;
  • NVLink;
  • PCIe 拓扑优化;
  • 零拷贝或少拷贝;
  • 异步传输;
  • 多流并行;
  • 分块流水;
  • 传输与计算重叠。

可以把这三类职责总结为:

层次 主要问题
Serving 语义层 请求是否需要远端 KV,何时开始和结束
KV 索引与缓存层 哪些 KV 已经存在,能复用多少
传输与存储层 KV Tensor 实际如何搬运和保存

需要特别注意:

这三类职责是功能上的划分,不一定对应所有实现中固定的三个独立模块。

5. NIXL、LMCache、Mooncake 等生态组件

PD 分离涉及的组件很多,但它们并不处在完全相同的抽象层。

5.1 NIXL

NIXL 可以理解为面向 AI 推理场景的数据传输抽象,重点解决:

  • GPU、CPU 和内存之间的数据搬运;
  • 节点间高性能传输;
  • 异步传输;
  • 对不同硬件路径和网络能力进行抽象。

在 NVIDIA GPU 环境中,它可以结合 GPUDirect RDMA 等能力,减少 CPU 参与和不必要的数据拷贝。

更准确地说,NIXL 主要解决的是:

数据如何高效地从这里搬到那里

而不是单独负责完整的请求路由或 KV 生命周期管理。

5.2 LMCache

LMCache 更偏向 KV Cache 的缓存、复用和存储管理,关注的问题包括:

  • KV 是否已经存在;
  • KV 是否可以跨请求复用;
  • KV 应该放在 GPU、CPU 还是远端存储;
  • KV 如何在不同层级之间迁移;
  • KV 如何被淘汰和恢复。

它解决的不只是:

把 KV 发给另一个节点

还包括:

KV 放在哪里?
如何查找?
如何复用?
什么时候淘汰?

5.3 Mooncake

Mooncake 面向大规模推理服务场景,通常同时关注:

  • KV Cache 的分布式管理;
  • 高性能数据传输;
  • KV Cache 的调度;
  • 多节点资源协作;
  • Prefill 与 Decode 的协同。

它更接近一套面向大规模 Serving 的系统化方案,而不仅仅是单一传输 API。

5.4 其他传输和存储实现

不同生态中还可能出现面向特定场景的组件,例如:

  • KV 流式传输实现;
  • 基于 RDMA 的传输后端;
  • 分布式内存池;
  • 共享文件系统;
  • 面向元数据管理的存储系统;
  • GPU 到 GPU 的专用互联方案。

因此,不应把 NIXL、LMCache、Mooncake 简单看成完全等价的“KV Pipe”。更准确的关系是:

vLLM KV Transfer 抽象
        ↓
可组合不同的:
    传输实现
    KV 缓存
    分布式存储
    内存池
    调度与路由系统

它们可能在某些部署中组合使用,也可能由一个系统同时覆盖多个层次。

6. PD 分离下的请求路由

PD 分离之后,Router 不再只是一个普通的轮询负载均衡器。

它至少需要同时观察两类资源:

Prefill 资源:
    计算队列、Prompt 长度、Prefill 吞吐

Decode 资源:
    活跃请求数、KV Cache 容量、Decode Batch、TPOT

一个请求的路由过程可以抽象为:

graph TD
    R["Router"]
    R --> P1["Prefill Node 0<br/>短队列"]
    R --> P2["Prefill Node 1<br/>长队列"]

    P1 --> D1["Decode Node 0<br/>KV 容量充足"]
    P1 --> D2["Decode Node 1<br/>网络距离较近"]

    P2 --> D2
    P2 --> D3["Decode Node 2<br/>TPOT 压力较大"]

    style R fill:#e8f3ff,stroke:#4a90e2
    style P1 fill:#fff4d6,stroke:#d99a00
    style P2 fill:#fff4d6,stroke:#d99a00
    style D1 fill:#e8f8ee,stroke:#4a9c68
    style D2 fill:#e8f8ee,stroke:#4a9c68
    style D3 fill:#e8f8ee,stroke:#4a9c68

6.1 Prefill 节点选择

可以考虑:

  • Prompt 长度;
  • 当前排队请求的总 token 数;
  • Prefill batch 的预计完成时间;
  • 当前设备利用率;
  • Prefix Cache 是否命中;
  • 节点间网络位置。

6.2 Decode 节点选择

可以考虑:

  • 当前活跃请求数;
  • KV Cache 使用率;
  • 剩余 KV 容量;
  • 当前 Decode batch 大小;
  • 预计 TPOT;
  • 到 Prefill 节点的网络距离;
  • 是否已有相同前缀的 KV。

因此,路由目标不是简单的“请求数平均”,而是:

让 Prompt 计算排队可控
让 KV 传输代价可控
让 Decode Batch 稳定
让 KV Cache 容量不倾斜

7. KV Affinity:路由中的状态亲和性

如果同一前缀的请求尽量被路由到相同的 Decode 节点,就有机会复用该节点上已有的 Prefix Cache。

例如:

请求 A → Decode Node 0
请求 B → Decode Node 0

如果请求 A 和请求 B 共享长前缀,那么请求 B 可能直接复用 Node 0 上的部分 KV。

这就是 KV Affinity。

graph LR
    P["相同 Prompt 前缀"]
    P --> R["Router"]
    R --> D["固定或优先选择同一 Decode 节点"]
    D --> C["复用已有 Prefix KV"]

    style P fill:#fff4d6,stroke:#d99a00
    style D fill:#e8f8ee,stroke:#4a9c68

但 KV Affinity 不能无限强化,否则会产生新的热点:

所有相同前缀请求
    ↓
集中到同一个 Decode 节点
    ↓
Prefix Cache 命中率提高
但节点负载和 KV 容量失衡

因此,实际路由需要在几件事之间做平衡:

  • Prefix Cache 命中率;
  • Decode 节点负载;
  • KV Cache 容量;
  • 网络传输代价;
  • 请求的延迟目标。

可以将路由目标写成:

路由代价
=
Prefill 排队时间
+ KV 传输时间
+ Decode 排队时间
+ KV 容量压力
- Prefix Cache 命中收益

这已经不是传统的无状态服务路由,而是带有执行状态和缓存状态感知的路由。

8. 两个池子可以独立扩缩容

PD 分离最直接的收益之一,是 Prefill 和 Decode 可以独立扩缩容。

graph TD
    TRAFFIC["业务流量"]

    TRAFFIC --> PPOOL["Prefill Pool"]
    TRAFFIC --> DPOOL["Decode Pool"]

    PPOOL --> P1["Prefill 0"]
    PPOOL --> P2["Prefill 1"]
    PPOOL --> P3["Prefill N"]

    DPOOL --> D1["Decode 0"]
    DPOOL --> D2["Decode 1"]
    DPOOL --> D3["Decode N"]

    PPOOL -.->|"按输入 token / TTFT 扩容"| PA["Prefill 独立扩缩容"]
    DPOOL -.->|"按活跃请求 / TPOT / KV 容量扩容"| DA["Decode 独立扩缩容"]

可以根据不同指标进行扩容。

8.1 Prefill 池的扩容指标

  • 输入 token 速率;
  • Prefill 队列长度;
  • Prompt 平均长度;
  • TTFT;
  • Prefill GPU 利用率;
  • Prefill 阶段的排队时间。

8.2 Decode 池的扩容指标

  • 活跃生成请求数;
  • 输出 token 速率;
  • TPOT;
  • Decode batch 大小;
  • KV Cache 使用率;
  • 每个节点可容纳的最大请求数。

例如:

长文档请求增加
    → 优先扩容 Prefill Pool

对话生成请求增加
    → 优先扩容 Decode Pool

输出长度普遍变长
    → Decode Pool 的压力增加

输入长度普遍变长
    → Prefill Pool 的压力增加

但独立扩缩容也会带来资源配比问题。

如果 Prefill 节点过多而 Decode 节点不足:

Prompt 很快处理完
但大量 KV 堵在传输和 Decode 阶段

如果 Decode 节点过多而 Prefill 节点不足:

Decode 资源空闲
但请求迟迟无法完成 Prefill

因此,扩缩容系统需要同时观察两个池子的端到端吞吐,而不能只看单侧 GPU 利用率。

9. 故障恢复:KV Cache 变成分布式状态

在单机部署中,一个进程失效通常意味着本地请求和 KV Cache 一起丢失。

在 PD 分离架构中,状态分布在多个节点之间:

请求元数据
    ↓
Router / Engine

Prompt 计算状态
    ↓
Prefill Node

KV Cache
    ↓
Decode Node / KV Storage

输出生成状态
    ↓
Decode Node

因此,Decode 节点宕机时,系统需要处理:

Decode 节点故障
    ↓
本地 KV Cache 丢失
    ↓
请求无法从原位置继续生成
    ↓
重新选择 Decode 节点
    ↓
重新 Prefill 或恢复远端 KV

9.1 策略一:重新 Prefill

最简单的方式是重新处理原始 Prompt。

原始请求仍然保留
    ↓
重新选择 Prefill 节点
    ↓
重新计算 KV Cache
    ↓
发送给新的 Decode 节点

优点:

  • 实现简单;
  • 不需要额外的 KV 持久化;
  • 状态一致性问题较少。

缺点:

  • 长 Prompt 重新计算成本高;
  • 恢复期间 TTFT 增加;
  • 大量请求同时恢复时可能形成流量尖峰。

9.2 策略二:从远端 KV 存储恢复

如果 KV 已经写入远端缓存或分布式存储,可以直接恢复:

新 Decode 节点
    ↓
查询远端 KV
    ↓
加载对应 block
    ↓
继续 Decode

优点:

  • 避免重复 Prefill;
  • 适合长上下文和高价值请求;
  • 恢复时间更稳定。

缺点:

  • 需要维护远端 KV;
  • 增加存储和网络成本;
  • 需要处理版本、布局、数据类型和有效期;
  • 远端存储也可能成为新的故障点。

9.3 策略三:复制 KV Cache

可以在多个 Decode 节点或远端存储中保留副本。

优点:

  • 故障恢复速度快;
  • 降低单节点故障影响;
  • 可以减少重新 Prefill 的概率。

缺点:

  • 增加网络流量;
  • 增加显存或存储开销;
  • KV 写入路径更复杂;
  • 需要处理副本淘汰和一致性。

KV Cache 与数据库数据还有一个重要区别:

KV Cache 通常是可重建状态,而不是必须永久保存的业务事实。

因此,很多系统不会追求像数据库那样的强一致持久化,而是在以下目标之间进行权衡:

恢复速度
可靠性
网络成本
存储成本
重新计算成本

10. PD 分离不是无条件划算

PD 分离的收益取决于业务流量、模型结构和网络条件。

可以将它简化为:

PD 分离收益
≈
避免的 Prefill / Decode 干扰
+ 独立扩缩容收益
+ 硬件配置收益
- KV 传输成本
- 路由与协调成本
- 故障恢复成本

10.1 更适合 PD 分离的场景

  • Prompt 较长;
  • Prefill 和 Decode 的负载比例变化明显;
  • Decode 请求持续时间长;
  • 对 TPOT 稳定性要求高;
  • Prefill 和 Decode 需要不同的硬件配置;
  • 节点间具备高速互联;
  • KV Cache 复用率较高;
  • Prefill 和 Decode 可以分别扩缩容。

10.2 可能不适合 PD 分离的场景

  • 请求很短;
  • 并发量很低;
  • 节点间网络带宽有限;
  • KV Cache 传输无法与计算重叠;
  • Prefill 和 Decode 的资源需求差异不明显;
  • 路由和运维复杂度超过了性能收益。

可以用一个简单的判断标准:

如果:

KV 传输时间 + 协调时间
    <
混合部署中的排队与调度干扰

那么 PD 分离更可能有收益。

但还要分别观察两个指标:

  • TTFT:Time To First Token,首 Token 延迟;
  • TPOT:Time Per Output Token,每个输出 Token 的延迟。

PD 分离通常希望:

Prefill Pool → 优化 TTFT
Decode Pool  → 优化 TPOT

然而,KV Transfer 本身会增加首 Token 前的等待时间。因此,不能只看 Prefill 节点的计算时间,也不能只看 Decode 节点的吞吐,必须观察完整端到端链路:

请求进入
  → Prefill 排队
  → Prompt 计算
  → KV 传输
  → Decode 排队
  → 首 Token 返回

11. 从单机 KV Cache 到集群 KV Cache

前面几章里,KV Cache 主要是单机内部的执行状态:

请求
  ↓
Scheduler
  ↓
KV Cache Manager
  ↓
本地 GPU Cache

到了 PD 分离之后,KV Cache 的关系变成:

请求
  ↓
Router
  ↓
Prefill 节点
  ↓
KV Transfer
  ↓
Decode 节点
  ↓
本地或远端 KV Cache

KV Cache 现在需要具备更多属性:

  • 可定位;
  • 可传输;
  • 可查询;
  • 可复用;
  • 可迁移;
  • 可淘汰;
  • 可恢复;
  • 可监控。

这意味着 KV Cache 已经不再只是“显存中的一块 Tensor”,而是集群中的一种可管理资源。

graph TD
    LOCAL["本地 KV Cache<br/>GPU HBM"]
    HOST["主机级 KV Cache<br/>CPU Memory"]
    REMOTE["远端 KV Cache<br/>其他节点 GPU / 内存池"]
    STORAGE["共享存储<br/>分布式文件系统或对象存储"]

    LOCAL <--> HOST
    HOST <--> REMOTE
    REMOTE <--> STORAGE

    style LOCAL fill:#e8f3ff,stroke:#4a90e2
    style HOST fill:#fff4d6,stroke:#d99a00
    style REMOTE fill:#e8f8ee,stroke:#4a9c68
    style STORAGE fill:#f3e8ee,stroke:#c45a7a

不同层级可以承担不同目标:

GPU HBM:
    最低访问延迟,容量有限

CPU Memory:
    容量更大,访问延迟更高

远端 GPU / 内存池:
    支持跨节点复用,需要网络传输

共享存储:
    容量大,适合恢复和长期缓存,但延迟最高

这也使得未来的 Serving 系统更像一个“分层 KV Cache 系统”:

热 KV:
    当前 Decode 请求正在使用

温 KV:
    近期可能复用,放在本地或邻近节点

冷 KV:
    长时间未访问,放到远端存储

失效 KV:
    被淘汰或等待回收

12. 小结:PD 分离的本质是状态转移

PD 分离不是简单地把 GPU 分成两组。

它真正做的是:

把请求执行拆成 Prefill 和 Decode 两个阶段
        ↓
让两个阶段使用不同的资源池
        ↓
通过 KV Transfer 传递中间状态
        ↓
由 Router 和 KV 管理系统协调整个生命周期

本章的核心结论可以概括为三句话。

第一:

Prefill 和 Decode 的资源需求不同,混合部署会让两者互相干扰。

第二:

PD 分离的真正难点不在“分两个池”,而在 KV Cache 的跨节点传输、查询、复用和恢复。

第三:

当 KV Cache 从本地状态变成分布式状态后,Serving 系统就从单机调度问题演进成了集群状态管理问题。

完整的架构演进可以这样概括:

模型适配
    ↓
硬件解耦
    ↓
单机调度
    ↓
本地 KV Cache 管理
    ↓
Prefill / Decode 分离
    ↓
跨节点 KV Transfer
    ↓
集群级路由、缓存、扩缩容与故障恢复

因此,PD 分离真正带来的变化是:

让 Prefill 和 Decode 各自更接近最优资源配置,同时要求系统具备管理分布式 KV 状态的能力。

它不是单机 Serving 的简单放大,而是 Serving 架构从“本地执行系统”走向“分布式状态系统”的关键一步。

📂 本章源码导航

PD 分离与 KV 传输

想看什么 从哪开始
KV Connector 抽象(本章核心) vllm/distributed/kv_transfer/kv_connector/v1/base.py
各类 connector 实现 vllm/distributed/kv_transfer/
等待远端 KV 的请求状态 vllm/v1/request.pyWAITING_FOR_REMOTE_KVS