02. Disaggregated Prefill / Decode
谁该读这一篇? 想理解"为什么生产团队愿意花两倍机器把 prefill / decode 拆开"、"vLLM 通过哪个接口接 NIXL / LMCache / Mooncake"、"什么规模才划算"的读者。
前置阅读:
01-tp-pp-ep.md(先掌握同机的多卡并行才能讲跨集群);最好读过02-core-concepts/05-chunked-prefill.md(理解同卡上 prefill/decode 互相干扰的根源)。耗时: 约 12 分钟。
学完能: 1. 解释为什么 prefill 和 decode 适合不同硬件配置(算术强度差)。 2. 画出 disaggregated 的数据流(含 KV 转移触发点)。 3. 列出 KV connector 接口的 3 个核心方法,知道 NIXL / LMCache / Mooncake 在该接口下怎么接入。 4. 判断"我这场景值不值得搞 disaggregated"(并发、SLO、网络成本)。
1. 为什么要拆?
prefill 和 decode 的硬件特性完全不同:
| 特性 | Prefill | Decode |
|---|---|---|
| 算法复杂度 | O(seq_len²) | O(seq_len) |
| 算术强度(FLOP/Byte) | 高(compute-bound) | 极低(memory-bound) |
| 单请求 token 量 | 几百到几万 | 1 |
| 适合的 batch | 小 batch 也能打满 GPU | 大 batch 才能打满带宽 |
| 适合的硬件 | 算力强的卡(H100、B200) | 带宽强的卡(同上 + 大 KV) |
把它们混在同一卡:
- 长 prefill 阻塞 decode(被 chunked prefill 缓解但没根治)
- decode 的 sampling/调度开销在 prefill 时浪费
- 难以同时优化 TTFT 和 TPOT
2. 解决方案
flowchart LR
P["Prefill 节点群<br/>· 高并发短任务<br/>· 重算力 · 少 KV<br/>· 优化 TTFT"]
D["Decode 节点群<br/>· 大 batch 长上下文<br/>· 重 KV · 省算力<br/>· 优化 TPOT"]
P -->|"KV via RDMA (NIXL)"| D
classDef prefill fill:#fef3c7,stroke:#b45309,color:#1a1f29;
classDef decode fill:#eff5ff,stroke:#2563eb,color:#1a1f29;
class P prefill;
class D decode;
流程:
- 请求到达 Router
- Router 派给某个 Prefill 节点跑 prefill
- Prefill 完成,把 KV 写到该请求的 block,通过 RDMA 传到 Decode 节点
- Decode 节点接 KV,把请求放入自己的 batch,开始 decode
- Decode 节点生成 token 流式返回
3. vLLM 的实现:KV Connector
vllm/distributed/kv_transfer/ 提供 KV connector 抽象,每种传输后端实现接口:
| Backend | 协议 | 适用 |
|---|---|---|
| NIXL | NVIDIA 的 GPU-Direct RDMA | InfiniBand / RoCE 集群 |
| LMCache | 缓存 + 转发 | 已有 LMCache 部署 |
| Mooncake | 月之暗面的方案 | 大规模分布式 KV |
| Custom | 自己写 | 实验/特殊场景 |
接口要点:
send_kv(req_id, blocks):prefill 节点调用recv_kv(req_id) -> blocks:decode 节点调用- 异步:传输与计算 overlap
4. 数据流详解
sequenceDiagram
participant PS as Prefill Scheduler
participant PW as Prefill Worker
participant NIXL as NIXL / RDMA
participant DW as Decode Worker
participant DS as Decode Scheduler
PS->>PW: schedule prefill
PW->>PW: 完成 prefill,KV 写入本地 paged cache
PW->>NIXL: send_kv(req_id, block_ids)
NIXL->>DW: 异步 GPU-Direct DMA 到对端显存
PW->>PS: 完成 prefill,可释放 block
DW->>DS: recv_kv 完成,req 加入 running
DS->>DW: schedule decode step
DW->>DW: block_table[req] = local blocks,开始 decode
关键点:
- 传输的是 KV cache 内容(block 数据),不是模型权重
- 不是 SwapOut/SwapIn 那种"换出再换入",而是 source 节点的 KV 永久转移
- 用 RDMA 才有意义(PCIe 慢得多)
5. 为什么 NIXL 重要
NIXL 是 NVIDIA 开发的库,封装 GPUDirect RDMA:
- 从一卡的显存直接 DMA 到另一台机器上的卡显存
- 不经 CPU、不经主机内存(不像 vanilla MPI)
- 延迟 < 10 μs,带宽接近 NIC 上限
vLLM 通过 NIXL 让 disaggregated 在 ms 级 KV 转移上可行。
6. Scheduler 协调
两端都有自己的 Scheduler。挑战:
- Prefill 端要知道"KV 传过去了"才能 free 自己的 block
- Decode 端要知道"KV 收到了"才能开始 decode
vLLM 通过 KV connector 的回调 + Scheduler 的 metadata 字段(kv_connector_metadata)协调。这部分是 V1 时代的新设计。
7. 实际部署案例
- Moonshot (Kimi):声称用 disaggregated 后 TTFT/TPOT 分别比同卡部署优 30%/40%
- DeepSeek:开源了 EPLB + disaggregated 工具链
- NVIDIA Dynamo:基于 vLLM/TRT-LLM 的 Disaggregated Serving Framework
8. 局限与挑战
- 小并发不划算:KV 传输有最小开销,请求少时不如同卡
- 运维复杂:两个集群、路由、故障切换
- prefix caching 难做:缓存散布在多个 prefill 节点
- 长上下文:100k+ token 的 KV 转移本身可能 100ms+
9. 工程自检问答
Q: 为什么 prefill 和 decode 适合的卡不同? A: prefill 是 compute-bound,要 FLOPs;decode 是 memory-bound,要 HBM 带宽。理论上 prefill 可用算力强的小显存卡,decode 用大显存高带宽卡。实际多用同型号卡但跑不同配置。
Q: KV 转移是不是会被 prefill 节点的 free 阻塞? A: 是异步的,prefill 节点提交完转移就可以 free 本地 block,转移由 NIC + 接收端 buffer 接住。
Q: 一个请求生成中途想接更多 token 怎么办?还是只在 prefill 节点? A: 不会回到 prefill。请求一旦进 decode 节点就一直留在那。如果生成中插入新 token(如 tool calling 返回),是新一轮的 prefill 续接,可以在原节点或新 prefill 节点跑。
Q: vs chunked prefill? A: 互补。chunked prefill 是同卡上把长 prefill 切片,缓解阻塞但不消除。disaggregated 是物理隔离,根治。
10. 什么场景值得搞 disaggregated?
不是所有场景都值得搞。下面这张决策表给你判据:
| 场景 | 建议 | 理由 |
|---|---|---|
| QPS < 50 / 单机即可承载 | 不搞 | KV 传输固定开销摊不掉,运维成本 >> 收益 |
| TTFT 与 TPOT 都已达 SLO | 不搞 | 没需求 |
| TTFT 严重不达标但 TPOT OK | 可搞(多 prefill 节点) | 但先试 chunked prefill / prefix caching 是否能解决 |
| 同机一开 chunked 就 TPOT 抖 | 强烈考虑 | 这就是 disaggregated 解决的核心矛盾 |
| 长上下文(≥ 100K token) | 看情况 | KV 转移本身可能 100ms+,得算清楚 |
| RDMA 网络不具备(只有以太网) | 不搞 | 走 PCIe / TCP 的 KV 转移会让 TPOT 雪崩 |
| 多模型 / LoRA 切换频繁 | 不搞 | prefill 节点重启/换模型很贵 |
| 高 prefix cache 命中率(70%+) | 谨慎 | cache 散布到多 prefill 节点,命中率会下降,需要 cache-aware routing 抵消 |
业界报告的收益(参考量级,不要当真实数字):
- Moonshot Kimi:TTFT -30% / TPOT -40%(混合长短 prompt workload)
- llm-d 论文:高 QPS 长上下文下吞吐 +38.9%
- 大部分公开数据来自高并发 + 异构 workload。低并发 / 同构 workload 收益不明显或为负。
小结
- Disaggregated 通过物理隔离 prefill / decode,根治"长 prefill 阻塞 decode"和"硬件特性不匹配"两个矛盾。
- vLLM 用 KV connector 抽象屏蔽传输细节,NIXL / LMCache / Mooncake 都通过它接入。
- 数据流核心是异步 KV 转移 + 双端 Scheduler 协调;prefill 完成可立即 free,decode 端按
kv_connector_metadata知道 KV 何时就绪。 - 不是银弹:小并发、无 RDMA、模型频繁切换的场景反而吃亏。
自检
不用照着原文复述,重点是把现象、机制、源码入口和取舍讲顺。
1. KV connector 的 3 个核心方法 + 接入新 transport 要实现什么?
源码:vllm/distributed/kv_transfer/ 的 KVConnectorBase 抽象接口。
class KVConnectorBase(ABC):
@abstractmethod
def send_kv_caches_and_hidden_states(
self,
request_id: str,
kv_blocks: list[KVCacheBlock],
hidden_states: torch.Tensor,
) -> None:
"""prefill 节点:把这个请求的 KV + 最后 hidden state 发出去"""
@abstractmethod
def recv_kv_caches_and_hidden_states(
self,
request_id: str,
) -> tuple[list[KVCacheBlock], torch.Tensor]:
"""decode 节点:等收到对应请求的 KV + hidden state"""
@abstractmethod
def close(self) -> None:
"""清理资源(关闭 RDMA queue pair、释放 buffer 等)"""
接入新 transport 要做的事:
- 继承
KVConnectorBase,实现上面 3 个方法 - 把传输细节封装在内部(建立连接、序列化 KV、流控)
- 通常需要异步(
send返回 Future)—— scheduler 不能阻塞等 - 在
vllm/distributed/kv_transfer/factory.py或类似注册表里加"my_transport": MyConnector - 配套测试:单元测(mock 一对发收)+ 集成测(两个 vLLM 实例对发)
关键挑战:metadata 一致性。两端要约定 KV 块的 token 边界、layer 顺序、dtype 等——通常通过元数据 header(msgpack)+ 内容 buffer 分开传,header 走 ZMQ,内容走 RDMA。
2. prefill 节点跑到 80% 时崩了,请求会怎样?
默认 vLLM 行为:
- 检测:decode 节点等 KV 超时(默认 30-60s timeout)→ 标该请求 failed
- 响应:API Server 收到 abort,返回 5xx 错误给客户端
- 重试:取决于客户端策略,vLLM 自己不重试
进阶(部分平台如 llm-d 实现):
- 重新调度:把请求送回 prefill 节点池(新选一个健康节点),从头开始 prefill
- 代价:80% 的 prefill 算力全部白费
- 复杂度:需要 router 层记得请求原始 prompt(API Server 端的状态),prefill 输出尚未到 decode 时算丢失
生产建议:
- prefill 集群冗余度高于 decode(prefill 重做代价更高)
- 设置合理的 prefill 超时(不能太短让正常长 prompt 误杀,也不能太长拖累恢复)
- 监控
prefill_node_failure_totalmetric,超阈值告警
3. 为什么用 InfiniBand / RoCE 而不是以太网?100K token KV 转多少 MB?
Llama-70B KV 计算(GQA:8 KV head × 128 head_dim × 80 layer):
- 单 token KV = 2 (K+V) × 8 × 128 × 80 × 2 (BF16) = 327,680 字节 ≈ 320 KB
- 100K token = 320 KB × 100,000 = 32 GB(确实大)
网络选择:
| 网络 | 带宽 | 转 32GB 时长 |
|---|---|---|
| 1Gbps 以太网 | 125 MB/s | 256 秒 —— 不可能 |
| 10Gbps 以太网 | 1.25 GB/s | 25 秒 —— 不可接受 |
| 100Gbps 以太网(RoCE) | 12.5 GB/s | 2.5 秒 —— 还是太长 |
| 400G InfiniBand HDR | 50 GB/s | 640 ms —— 接近可用 |
| NVLink 跨机(NVL72 等) | 900 GB/s | 35 ms —— 理想 |
→ 对 100K token 这种极端长上下文,连 IB 都吃力,需要做层级 streaming(不等整请求 KV 全到,按 layer 分批发送,decode 节点收到第 N 层就开始算第 N 层),或 prefix-aware routing(让相似 prefix 的请求路由到同一组节点,复用 cache 减少传输)。
常见上下文长度(4-16K token): 1-5 GB,10Gbps 以太网 800 ms 可接受,IB 100 ms 流畅。这是 disaggregated 的主战场。
4. prefix cache 在 disaggregated 下命中率怎么变?怎么补救?
变差。原因:
- prefill 集群分片:原本同 prompt 命中同一 vLLM 实例的 cache,disaggregated 后分散到多个 prefill 节点,每个节点的 cache 独立 → 命中率 ÷ prefill_node_count
- decode 节点 cache 失效:decode 节点收到的 KV 是"传过来的",不算 cache 命中(每次都重传等价于重算)
补救:
| 方案 | 怎么做 | 代价 |
|---|---|---|
| Cache-aware routing | router 维护 "请求 prompt hash → 之前打到的 prefill 节点" 映射,让同 prefix 请求落同一节点 | router 层复杂度 + 节点负载可能不均 |
| L2 共享 cache(CPU DRAM) | prefill 节点把 cache spill 到共享 CPU 内存池(LMCache 等),所有 prefill 节点可查 | L2 命中比 L1 慢 10×,需 RDMA |
| L3 共享 cache(远端) | 把 cache 放到 NVMe / 对象存储,跨集群 cluster 共享 | L3 慢 100×,仅适合超长冷 prompt |
| prefix-pinned 部署 | 已知热门 system prompt 在所有 prefill 节点 pre-warm | 需要业务知识,pin 列表手工维护 |
| 不做 disaggregated | 如果 prefix cache 命中率是核心 KPI,重新考虑架构 | 见 §10 决策表 |
实战:80% 的 disaggregated 部署都配 cache-aware routing,否则 disaggregated 的收益(latency / TPOT 改善)会被 prefix cache 命中率下降抵消,得不偿失。详见 08-production-deployment/02-smart-routing-and-load-balancing.md。
下一步
- 想看 KV connector 源码:
vllm/distributed/kv_transfer/与vllm/v1/kv_offload/。 - 想看智能路由怎么帮 disaggregated 降低 KV 传输:
08-production-deployment/02-smart-routing-and-load-balancing.md。 - 想理解部署平台怎么管两个集群(llm-d / Dynamo / AIBrix):
08-production-deployment/01-deployment-architectures.md。 - 想做生产化故障预案:
08-production-deployment/06-reliability-and-failure-modes.md。 - 规模化实战:
05-large-scale-cluster-inference.md(P/D 分离在千卡万卡上的 KV 路由、比例动态调整、故障墙)。