如果说 KV Cache 是大模型推理的”记忆加速器”,那么 PD 分离架构(Prefill-Decode Disaggregation) 就是把这个加速器搬上了高速公路——但同时也带来了一个新问题:记忆本身要运输了。
今天我们就来聊聊:在 PD 分离架构下,KV Cache 面临哪些挑战,又有哪些优化手段?
先回顾:LLM 推理的两个阶段
大模型推理分为两个特征截然不同的阶段:
Prefill 阶段(预填充)
- 一次性处理用户输入的所有 token
- 并行计算,生成全部输入 token 的 KV Cache
- 特点:计算密集型(Compute-bound),GPU 算力拉满
Decode 阶段(解码)
- 逐个生成输出 token,每步只算 1 个新 token
- 串行执行,每步都要读取完整的 KV Cache
- 特点:访存密集型(Memory-bound),瓶颈在显存带宽
flowchart LR
subgraph Prefill["Prefill 阶段"]
A1[用户输入 N 个 token] --> B1["并行计算
生成 N 个位置的 KV"]
B1 --> C1["计算密集
GPU 利用率高"]
end
subgraph Decode["Decode 阶段"]
A2[逐个生成 token] --> B2["每步读取完整 KV Cache
只算 1 个新 token"]
B2 --> C2["访存密集
GPU 利用率低"]
end
Prefill -->|传递 KV Cache| Decode
style Prefill fill:#fff3e0
style Decode fill:#e3f2fd
style C1 fill:#ffa726
style C2 fill:#42a5f5,color:#fff
混合部署的尴尬
如果把两个阶段混在同一批 GPU 上跑,会出现典型的互相干扰问题:
- Prefill 的大 batch 计算一来,Decode 的延迟瞬间飙升(首 token 和出字速度卡顿)
- Decode 占着 GPU 却只干活一点点,Prefill 排队等资源
flowchart TD
A[混合部署] --> B1["Prefill 抢占 GPU
Decode 延迟抖动"]
A --> B2["Decode 占着 GPU
算力大量空闲"]
A --> B3["两种负载特征相反
怎么调参都别扭"]
B1 --> C["TTFT 和 TPOT
无法同时优化"]
B2 --> C
B3 --> C
style A fill:#ffebee
style C fill:#ef9a9a
于是业界的答案是:拆开。
PD 分离架构:让专业的卡干专业的事
PD 分离(也叫 Prefill-Decode Disaggregation)的核心思想:用不同的 GPU 节点分别承担 Prefill 和 Decode。
flowchart TD
U[用户请求] --> R[路由层 / Scheduler]
subgraph P["Prefill 节点池"]
P1["P 节点 1
高算力配置"]
P2["P 节点 2
高算力配置"]
end
subgraph D["Decode 节点池"]
D1["D 节点 1
高带宽配置"]
D2["D 节点 2
高带宽配置"]
end
R -->|输入 token| P
P -->|"传输 KV Cache
(RDMA)"| D
D -->|流式输出| U
style P fill:#fff3e0
style D fill:#e3f2fd
style P1 fill:#ffa726
style P2 fill:#ffa726
style D1 fill:#42a5f5,color:#fff
style D2 fill:#42a5f5,color:#fff
代表项目:
| 项目 | 来源 | 特点 |
|---|---|---|
| DistServe | 学界(2024) | PD 分离的奠基性工作 |
| Splitwise | Microsoft | 异构机器混合部署 P/D |
| Mooncake | 月之暗面 | 以 KV Cache 为中心的分离架构,KVCache 池化 |
| vLLM / SGLang | 开源社区 | 均已支持 PD disaggregation |
收益很明确:
- ✅ Prefill 和 Decode 各自独立扩缩容
- ✅ TTFT(首 token 延迟)和 TPOT(每 token 延迟)可以分别优化
- ✅ 整体吞吐提升 1.5-2 倍以上
但是:KV Cache 成了”烫手山芋”
PD 分离带来收益的同时,引入了一个新瓶颈:Prefill 算出来的 KV Cache,必须通过网络搬到 Decode 节点上。
KV Cache 到底有多大?
一个请求的 KV Cache 大小:
$$\text{KV Size} = 2 \times L \times H_{kv} \times d \times S \times b$$
其中:
- $L$:模型层数
- $H_{kv}$:KV 头数(GQA 场景下远小于注意力头数)
- $d$:每个头的维度
- $S$:序列长度
- $b$:每个元素的字节数(FP16 为 2)
- 开头的 $2$:K 和 V 各一份
以 LLaMA-70B 为例(80 层、8 个 KV 头、头维度 128、FP16):
$$\text{KV Size per token} = 2 \times 80 \times 8 \times 128 \times 2 = 327680 \text{ B} \approx 320 \text{ KB/token}$$
一个 32K 上下文的请求,KV Cache 高达 约 10 GB!
传输时间估算
$$T_{\text{transfer}} = \frac{\text{KV Size}}{BW_{\text{network}}}$$
| 网络带宽 | 传输 10GB 耗时 |
|---|---|
| 25 Gbps(普通以太网) | ~3.3 秒 ❌ 不可接受 |
| 100 Gbps | ~0.8 秒 😐 勉强 |
| 200 Gbps RDMA | ~0.4 秒 🙂 |
| 400 Gbps RDMA | ~0.2 秒 ✅ |
结论:KV Cache 的存储与传输,是 PD 架构的头号优化对象。
PD 架构下的 KV Cache 优化全景
flowchart TD
A[PD 架构 KV Cache 优化] --> B1["① Prefix Cache
能不算就不算"]
A --> B2["② 多级缓存分层
能放近就放近"]
A --> B3["③ 传输优化
能快搬就快搬"]
A --> B4["④ 缓存感知调度
能命中就命中"]
A --> B5["⑤ KV 压缩
能变小就变小"]
B1 --> C["减少重复计算
降低传输量"]
B2 --> C
B3 --> C
B4 --> C
B5 --> C
style A fill:#ff7043,color:#fff
style B1 fill:#66bb6a,color:#fff
style B2 fill:#42a5f5,color:#fff
style B3 fill:#ffa726
style B4 fill:#ab47bc,color:#fff
style B5 fill:#26c6da
style C fill:#e8f5e9
下面逐个展开,重点是 Prefix Cache。
优化一:Prefix Cache(前缀缓存)
核心洞察:大量请求的前缀是重复的
真实业务中,请求的输入前缀重合率高得惊人:
flowchart TD
A["典型请求结构"] --> B1["System Prompt
几千 token,所有请求一样"]
A --> B2["Few-shot 示例
一批请求一样"]
A --> B3["RAG 检索文档
热门文档反复出现"]
A --> B4["多轮对话历史
本轮包含上一轮全部"]
A --> B5["用户新问题
真正独特的部分"]
B1 --> C["前缀部分可复用
只有尾部需要计算"]
B2 --> C
B3 --> C
B4 --> C
style A fill:#e3f2fd
style B5 fill:#ef9a9a
style C fill:#66bb6a,color:#fff
Prefix Cache 的思想:把已经算过的前缀 KV Cache 存下来,下次相同前缀直接复用,Prefill 只需要计算没命中的尾部。
工作原理:按块哈希
KV Cache 不能按”整句文本”缓存(差一个字就全废),而是按 固定大小的 token 块(block) 缓存,典型 block size 为 16:
1 | 输入 token: [t₁ t₂ ... t₁₆ | t₁₇ ... t₃₂ | t₃₃ ... t₄₀] |
每个 block 的哈希由其所有前缀 token + 本块 token 共同决定:
$$\text{hash}_i = H(\text{hash}_{i-1},\ t_{(i-1)B+1}, …, t_{iB})$$
这样保证:两个请求只要前缀 token 逐个相同,对应 block 的哈希一定相同,KV Cache 完全可复用(注意:是 token 级精确匹配,语义相近没用)。
进阶:RadixAttention(基数树索引)
SGLang 提出的 RadixAttention 用基数树(Radix Tree)管理所有请求的前缀,天然支持:
- 任意长度前缀的共享(不必对齐到整个 prompt)
- 自动化的 LRU 淘汰
- 分叉与合并(多轮对话、思维树搜索等场景)
flowchart TD
R["Radix Tree 根节点"] --> N1["System Prompt
共享 KV"]
N1 --> N2["用户A: 对话历史
轮次1"]
N1 --> N3["用户B: RAG文档X"]
N2 --> N4["用户A: 轮次2
复用轮次1 KV"]
N3 --> N5["其他请求引用文档X
直接命中"]
style R fill:#ff7043,color:#fff
style N1 fill:#66bb6a,color:#fff
style N2 fill:#42a5f5,color:#fff
style N3 fill:#42a5f5,color:#fff
style N4 fill:#a5d6a7
style N5 fill:#a5d6a7
效果有多猛?
| 场景 | 前缀命中率 | Prefill 计算节省 |
|---|---|---|
| 统一 System Prompt 的 Agent | 90%+ | TTFT 降低 60-80% |
| 多轮对话 | 70-90% | 随轮次递增 |
| RAG 热门文档问答 | 50-80% | 视文档热度 |
| Few-shot 批量评测 | 95%+ | 几乎只剩尾部计算 |
Prefix Cache 在 PD 架构下的特殊价值
在单体架构里,Prefix Cache 只是”省点计算”;但在 PD 分离架构下,它的价值被翻倍放大:
flowchart LR
A[请求命中 Prefix Cache] --> B1["① Prefill 少算
只算尾部 token"]
A --> B2["② 需要传输的 KV 变少
只传新增部分"]
A --> B3["③ Decode 节点可本地命中
干脆不用传"]
B1 --> C["算力省了"]
B2 --> C
B3 --> C
style A fill:#66bb6a,color:#fff
style C fill:#ffa726
命中前缀的 KV 根本不需要从 P 节点搬到 D 节点——既省算力,又省带宽,还降延迟,一箭三雕。
优化二:多级缓存分层存储
PD 架构下 KV Cache 的”居住地”不再只有 GPU 显存,而是形成层级体系:
flowchart TD
L1["L1: GPU HBM
最快最贵,容量小
存放当前请求 + 热点前缀"] --> L2["L2: 主机 DRAM
便宜大碗,百GB级
Prefix Cache 主战场"]
L2 --> L3["L3: 本地 SSD / 分布式存储
TB级,跨节点共享
全局 KVCache 池"]
L3 -->|预取/加载| L2
L2 -->|加载| L1
style L1 fill:#ef9a9a
style L2 fill:#ffcc80
style L3 fill:#a5d6a7
Mooncake 的做法最具代表性:把整个集群的空闲内存组成一个 KVCache 池(Pool):
- P 节点算完的 KV,可以直接写入池子,D 节点就近读取
- 同一个前缀的 KV 在池中全局可见,任何节点都能命中
- 配合 RDMA,DRAM 池的读取速度足以覆盖大部分 Decode 启动需求
优化三:KV 传输优化
就算没命中缓存,传输本身也能优化:
1. RDMA 高速传输
绕过 CPU 和内核协议栈,GPU/DMA 直接读写远端内存,延迟微秒级。
2. 传输与计算重叠(Overlap)
1 | 传统串行: [Prefill 计算] → [KV 传输] → [Decode 开始] |
Prefill 每算完一层的 KV 就立刻异步发送,Decode 端先收到先就位,把传输时间藏进计算时间里。
3. 传输量化
传输前把 KV 压成 FP8/INT8,落地后再解压(或直接用低精度参与计算),带宽需求直接减半甚至四分之一:
$$T_{\text{transfer}}^{\text{FP8}} = \frac{1}{2} T_{\text{transfer}}^{\text{FP16}}$$
4. 按需传输
Decode 阶段第一步其实只需要最后几层的 KV 先就位(因为逐层计算),可以做细粒度、分优先级的传输调度。
优化四:缓存感知调度(Cache-Aware Scheduling)
Prefix Cache 能不能命中,调度器说了算。如果同一个前缀的请求被随机打散到不同节点,缓存命中率会惨不忍睹。
策略:把请求路由到”最可能命中前缀”的节点。
flowchart TD
A[新请求到达] --> B["计算前缀哈希"]
B --> C{查全局缓存索引}
C -->|节点X有该前缀KV| D["路由到节点X
直接命中"]
C -->|都没有| E["按负载均衡选节点
并注册新前缀"]
D --> F["命中率最大化"]
E --> F
style A fill:#bbdefb
style C fill:#ce93d8
style D fill:#66bb6a,color:#fff
style F fill:#ffa726
SGLang 的 cache-aware routing、Mooncake 的全局 KVCache 索引,本质都是在做这件事:让缓存跟着请求走,让请求奔着缓存去。
优化五:KV 本体压缩
从源头减小 KV Cache 体积,传输和存储压力同步下降:
| 技术 | 原理 | 压缩效果 |
|---|---|---|
| GQA / MQA | 多个 Q 头共享 KV 头 | KV 减少 4-8 倍 |
| MLA | 低秩联合压缩 KV | KV 减少一个数量级 |
| KV 量化 | FP16 → FP8/INT8/INT4 | 2-4 倍 |
| 稀疏/驱逐 | 只保留重要 token 的 KV | 视策略而定 |
这些是模型层面的优化,与 PD 架构正交但收益叠加——KV 越小,PD 之间的搬运成本越低,分离架构越划算。
组合拳:一次请求的完整旅程
把上面的优化串起来,看一个命中 Prefix Cache 的请求在 PD 架构中如何流转:
flowchart TD
A[用户请求] --> B[调度器计算前缀哈希]
B --> C{全局索引查找}
C -->|"命中: P节点2有前缀KV"| D[路由到 P 节点 2]
D --> E["Prefill 只计算尾部新 token"]
E --> F{D 节点本地池
是否已有该前缀?}
F -->|是| G["零传输
D 节点直接读池"]
F -->|否| H["RDMA 只传增量 KV
FP8 压缩 + 逐层重叠"]
G --> I[Decode 流式输出]
H --> I
I --> J["新生成的 KV 写回缓存池
供下一轮对话复用"]
style B fill:#ce93d8
style E fill:#ffa726
style G fill:#66bb6a,color:#fff
style H fill:#42a5f5,color:#fff
style J fill:#a5d6a7
实际收益参考
以 Mooncake 公开数据与业界实践为参考:
| 优化组合 | TTFT 改善 | 吞吐改善 |
|---|---|---|
| 仅 PD 分离 | -20~30% | +50~100% |
| PD 分离 + Prefix Cache | -50~80% | +100~200% |
| + 多级缓存池 + RDMA | -70%+ | +200%+ |
| + 传输量化/重叠 | 进一步抹平传输开销 | 视带宽而定 |
注:具体数值与模型规模、请求前缀重合度、网络配置强相关,以上为典型区间。
总结
PD 分离架构把 Prefill 和 Decode 拆开,换来了独立扩展和极致优化,但代价是 KV Cache 必须流动起来。围绕这个核心矛盾,业界的优化思路可以浓缩成五句话:
flowchart TD
A[PD 架构 KV Cache 优化] --> B1["能不算就不算
Prefix Cache"]
A --> B2["能放近就放近
多级缓存分层"]
A --> B3["能快搬就快搬
RDMA + 重叠 + 量化"]
A --> B4["能命中就命中
缓存感知调度"]
A --> B5["能变小就变小
MLA / 量化 / GQA"]
B1 --> C["KV Cache 从'副产物'
变成'核心资产'"]
B2 --> C
B3 --> C
B4 --> C
B5 --> C
style A fill:#ff7043,color:#fff
style C fill:#66bb6a,color:#fff
核心要点回顾:
| 概念 | 作用 | 一句话理解 |
|---|---|---|
| PD 分离 | P/D 独立部署扩展 | 让专业的卡干专业的事 |
| Prefix Cache | 复用重复前缀的 KV | 背过的课文不再重背 |
| RadixAttention | 基数树管理前缀 | 前缀共享的”目录索引” |
| 多级缓存池 | HBM→DRAM→SSD 分层 | KV 的”物流仓储网络” |
| 缓存感知调度 | 请求奔着缓存去 | 货不动,让订单找货 |
一句话总结:在 PD 架构时代,KV Cache 不再是推理的”副产物”,而是整个系统围绕其设计的核心资产——谁能把 KV Cache 存得好、搬得快、命中得多,谁的推理成本就低得多。
参考资料
- Zhong, Y., et al. (2024). “DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving”
- Patel, P., et al. (2024). “Splitwise: Efficient Generative LLM Inference Using Phase Splitting”
- Qin, R., et al. (2024). “Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving”
- Zheng, L., et al. (2024). “SGLang: Efficient Execution of Structured Language Model Programs” (RadixAttention)
- vLLM Documentation: Automatic Prefix Caching
- DeepSeek-V2 Technical Report: Multi-head Latent Attention