笔记日期: 2026-05-28 笔记作者: Zhongzhu Zhou 论文标题: Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving 作者: Ruoyu Qin, Zheming Li, Weiran He, Mingxing Zhang, Yongwei Wu, Weimin Zheng, Xinran Xu arXiv: 2407.00079 状态: FAST 2025 最佳论文奖;ACM Transactions on Storage
一句话总结
Mooncake 是 Kimi(Moonshot AI)的生产级大模型推理服务平台,核心思想是:KV Cache 才是大模型推理系统中最稀缺的资源,整个系统架构应以 KV Cache 的管理、复用和迁移为中心。通过将 Prefill 集群与 Decoding 集群彻底解耦,并在整个 GPU 集群上维护分布式 KV Cache 池,Mooncake 在实际 Kimi 负载下比 vLLM 基线多处理 75% 的请求,在模拟长文本场景下最高实现 525% 的吞吐量提升,同时满足 TTFT 和 TBT 双重 SLO 约束。
前置知识
在正式理解这篇论文的技术贡献之前,我们需要建立几个关键概念的基础。这一节会从最基础的角度解释 Transformer 推理的关键特性。
1. Transformer 的自回归生成
以 GPT 为代表的解码器-only 大语言模型生成文本时采用**自回归(Autoregressive)**方式:每次只生成一个 token,生成的 token 又作为下一步的输入。整个推理过程分为两个阶段:
- Prefill(预填充/初始化)阶段:将整个输入 prompt 的 个 token 一次性并行处理,计算所有层的注意力 Key-Value 矩阵并缓存(即 KV Cache)。
- Decode(解码/增量)阶段:每次前向传播只处理 1 个新 token,利用已缓存的 KV 执行注意力计算,生成下一个 token。
Prefill 阶段高度并行(所有 token 同时处理),计算密集;Decode 阶段完全串行(每步只生成一个 token),内存带宽密集。
2. KV Cache 的大小
在 Transformer 的每一层,每个 token 会产生一个 Key 向量 和一个 Value 向量 。这些向量被缓存起来供后续 Decode 步骤复用。
对于每个 token,KV Cache 的存储量为:
以 LLaMA2-70B 为例(,GQA 有 8 个 KV 头,,FP16 存储):
128k token 的上下文需要约 40 GB KV Cache —— 占满一整块 A100-80GB 的一半显存。这就是为什么长文本场景下显存管理是核心挑战。
3. Prefill 阶段的计算复杂度
Prefill 阶段的自注意力计算复杂度随序列长度呈平方增长:
- 注意力计算:(与序列长度的平方成正比)
- MLP/FFN 层:(与序列长度线性成正比)
这意味着当序列长度翻倍时,注意力计算量变为原来的 4 倍。对于 128k token 的长文本,仅 Prefill 阶段就可能占用几十秒的 GPU 时间,严重阻塞其他请求的解码。
4. 服务级别目标(SLO)
生产环境中的 LLM 服务通常有两个关键延迟指标:
- TTFT(Time-to-First-Token,首 token 延迟):从请求到达到输出第一个 token 的时间。主要由 Prefill 阶段决定。
- TBT(Time-Between-Tokens,逐 token 间隔延迟):解码阶段相邻两个 token 之间的生成时间。主要由内存带宽决定(每步都要读取全部 KV Cache)。
这两个目标天然存在冲突:将更多请求打包成大 batch 能提高吞吐,但会增加每个请求的等待时间,可能违反 SLO。Mooncake 的调度器核心任务就是在这两者之间取得平衡。
5. 连续批处理(Continuous Batching,Orca 2022)
Orca 论文(OSDI 2022)引入了迭代级调度(Iteration-level Scheduling):调度器不再等一整个 batch 的请求全部完成,而是在每个解码步骤结束后检查哪些请求已生成结束符(EOS),将其移除并立刻补入新请求。这大幅提升了 GPU 利用率,是现代 LLM 推理系统的基础。
但 Orca 将 Prefill 和 Decode 耦合在同一个 GPU 集群中——长 Prefill 会打断正在进行的 Decode,导致 TBT 延迟抖动。Mooncake 彻底解除了这种耦合。
6. PagedAttention(vLLM 2023)
vLLM 引入了 PagedAttention:将 KV Cache 存储在不连续的、固定大小的”页”(Block)中,而不要求连续显存。这消除了显存碎片,允许细粒度的显存共享。Mooncake 继承了这一思路,并将其扩展到**分布式、多层次(GPU VRAM → CPU DRAM → SSD)**的全集群 KV Cache 池。
7. RDMA 远程直接内存访问
RDMA(Remote Direct Memory Access)允许一台节点直接读写另一台节点的内存,无需目标 CPU 介入,延迟极低(微秒级),带宽可达数百 Gbps。结合 GPUDirect RDMA 技术,GPU 的 HBM 显存可以直接暴露给 RDMA 网络,KV 张量可以在不经过 CPU 内存拷贝的情况下跨节点传输。Mooncake 的 Transfer Engine 正是基于这一硬件能力构建的。
问题背景:现有系统的瓶颈
Prefill 与 Decode 的耦合干扰
gantt
title 耦合架构下的 Prefill-Decode 干扰(vLLM/Orca 风格)
dateFormat X
axisFormat %s
section 请求 A(128k 长文本)
Prefill(128k token) :active, a1, 0, 120
Decode 步骤 1 :a2, 120, 130
Decode 步骤 2 :a3, 130, 140
section 请求 B(中途到达)
等待 Prefill 结束 :crit, b1, 50, 120
Prefill(短文本) :b2, 120, 130
Decode 步骤 1 :b3, 130, 140
section 请求 C(纯 Decode)
Decode 被 Prefill 阻塞 :crit, c1, 50, 120
Decode 步骤 2 :c2, 120, 130
Decode 步骤 3 :c3, 130, 140
图 1 — 耦合干扰。当一个长文本请求(A)占用 GPU 做 Prefill 时,所有纯 Decode 请求(C)和新到达请求(B)都被阻塞。请求 C 的 TBT SLO 可能因此被违反,尽管它根本不需要做 Prefill。
在 Kimi 生产环境中,该论文报告显示:当长文本请求存在时,vLLM 的 TBT SLO 达标率仅为 57%,43% 的解码间隔超出延迟目标。
显存预留浪费
Orca 风格的调度器在请求接入时按照 max_tokens 预留 GPU 显存。如果一个请求被分配 2048 个输出 token 的预算,但实际只生成了 30 个,那么剩下 2018 个 token 的显存槽位在整个请求生命周期内都被白白占用。这种过度预留导致有效显存利用率低,限制了整体并发量。
被忽视的 KV Cache 复用机会
生产数据揭示:大量请求共享相同的前缀(系统提示、少样本示例、热门文档开头等)。如果这些公共前缀的 KV Cache 只需计算一次然后在集群内共享复用,对应的 Prefill 计算就可以完全省去。vLLM 的 RadixAttention 实现了单实例内的前缀缓存,而 Mooncake 把这个思路扩展到了整个集群范围。
Mooncake 系统架构总览
graph TB
subgraph 客户端
R[请求流]
end
subgraph Conductor["Conductor(全局调度器)"]
C[调度逻辑<br/>TTFT 估算<br/>SLO 验证<br/>提前拒绝]
end
subgraph PrefillPool["Prefill 集群(计算密集)"]
P1[Prefill 实例 1]
P2[Prefill 实例 2]
P3[Prefill 实例 N]
end
subgraph DecodingPool["Decoding 集群(内存带宽密集)"]
D1[Decoding 实例 1]
D2[Decoding 实例 2]
D3[Decoding 实例 M]
end
subgraph KVStore["分布式 KV Cache 池"]
GPU[GPU VRAM<br/>活跃请求]
DRAM[CPU DRAM<br/>已缓存 Block]
SSD[SSD<br/>冷数据]
end
subgraph Messenger["Transfer Engine(传输引擎)"]
RDMA[GPUDirect RDMA<br/>800 Gbps]
end
R -->|路由| C
C -->|分配 Prefill 任务| PrefillPool
C -->|分配 Decode 任务| DecodingPool
PrefillPool <-->|读写 KV Block| DRAM
DecodingPool <-->|读取 KV Block| DRAM
DRAM <-->|淘汰冷数据| SSD
PrefillPool <-->|KV 传输| RDMA
RDMA <-->|KV 传输| DecodingPool
PrefillPool <-->|本地 KV| GPU
DecodingPool <-->|本地 KV| GPU
图 2 — Mooncake 系统架构。Conductor 是全局大脑;Prefill 集群和 Decoding 集群物理隔离;分布式 KV Cache 池横跨 CPU DRAM 和 SSD;Transfer Engine(Messenger)通过 RDMA 在节点间高速传输 KV Block。
请求完整生命周期
每个请求在 Mooncake 中的处理路径如下:
sequenceDiagram
participant 客户端
participant Conductor
participant KVPool as KV Cache 池(CPU DRAM)
participant Prefill as Prefill 实例
participant Messenger as Transfer Engine
participant Decode as Decoding 实例
客户端->>Conductor: 新请求(prompt_tokens, max_tokens, SLO 要求)
Conductor->>KVPool: 前缀哈希查找(查找已缓存的 Block)
KVPool-->>Conductor: cache_hit_len(已缓存的 token 数)
Conductor->>Conductor: 估算 TTFT = T_等待 + T_prefill(L - cache_hit) + T_传输
Conductor->>Conductor: SLO 验证(TTFT 是否可行?TBT 是否可行?)
alt SLO 可满足
Conductor->>Prefill: 分配请求 + 缓存信息
Prefill->>KVPool: 异步加载前缀 KV Block 到 GPU 显存
Prefill->>Prefill: 仅对未缓存的 token 做增量 Prefill 计算
Prefill->>KVPool: 逐层存储新的 KV Block(带哈希标签)
Prefill->>Messenger: 流式推送 KV Block 到 Decoding 节点
Messenger->>Decode: 通过 RDMA 传输 KV Block
Decode->>Decode: 开始自回归解码
Decode->>客户端: 流式输出 token
else SLO 无法满足
Conductor->>客户端: 提前拒绝(Early Reject)
end
图 3 — Mooncake 请求完整处理流程。TTFT 的关键路径包括:前缀缓存查找、增量 Prefill(只处理未缓存的 token)、KV 传输、Decoding。
KV Cache 的存储与哈希方案
KV Block 以分页形式存储——每个 Block 包含固定数量(如 16 个)token 的 KV 张量。每个 Block 附带一个内容寻址哈希键:
这种内容寻址方案实现了去重:如果两个请求的前缀完全相同,其 Block 哈希也完全相同,Conductor 无需比较原始张量就能识别它们为同一数据,直接复用缓存。哈希键还用于:
- 调度时的前缀匹配:找到哪个 Prefill 实例本地已缓存了最多的匹配 Block。
- 缓存淘汰与复制决策:跟踪热 Block(高频命中)并主动复制,冷 Block 下沉到 SSD。
Transfer Engine:KV 的跨节点高速传输
Transfer Engine(论文中称为 “Messenger”)是使 KV Cache 解耦在工程上可行的核心组件。没有高速 KV 传输,从 Prefill 节点向 Decoding 节点搬运 KV 张量的成本会抵消缓存复用和解耦带来的收益。
硬件基础:GPUDirect RDMA
GPUDirect RDMA 允许 GPU 的 HBM 显存直接暴露给 RDMA 网络:发送端的 RDMA 网卡直接读取 GPU HBM,目标节点的 GPU 直接接收,完全绕过 CPU 内存和 PCIe 拷贝。Mooncake 实验集群的硬件参数:
- 节点间:800 Gbps RDMA(InfiniBand HDR 或 RoCE v2)
- 节点内:NVLink(8× A800-SXM4-80GB per node)
- 每节点总显存 8 × 80 GB = 640 GB HBM
KV Block 传输时间的理论估算:
以 LLaMA2-70B 的 16k token 上下文为例:,(800 Gbps),则 。在 TTFT SLO 为 30 秒的场景下,50 ms 的传输开销完全可以接受。
逐层流式传输与计算重叠
朴素的设计是:Prefill 全部完成 → 传输所有 KV → 开始 Decoding。这会造成明显的 TTFT 开销。Mooncake 的改进是逐层异步流式传输:
gantt
title 逐层 KV 传输与 Prefill 计算的重叠(以 80 层模型为例)
dateFormat X
axisFormat %s
section Prefill 计算
第 1-20 层计算 :a1, 0, 20
第 21-40 层计算 :a2, 20, 40
第 41-60 层计算 :a3, 40, 60
第 61-80 层计算 :a4, 60, 80
section KV 传输(异步)
传输第 1-20 层 KV :b1, 20, 35
传输第 21-40 层 KV :b2, 40, 55
传输第 41-60 层 KV :b3, 60, 75
传输第 61-80 层 KV :b4, 80, 95
section Decoding(等待所有层)
等待传输完成 :crit, c1, 0, 95
Decode 步骤 1 :c2, 95, 100
图 4 — 逐层 KV 传输与 Prefill 计算重叠。Prefill 计算完每组层后,立即将这些层的 KV 张量异步推送到 Decoding 节点。传输与后续层的 Prefill 计算并行进行,显著降低 TTFT。
逐层传输的另一个好处是显存解耦:因为 KV 张量在计算后立即推出,Prefill GPU 不需要在显存中同时保留所有 80 层的 KV 缓冲区,只需维持一个层组的临时缓冲。这使得 Prefill 能处理任意长度的上下文,只要显存能容纳单个层组即可。
论文实验表明,对于 16k-128k 的上下文,逐层传输相比朴素批量传输可将 KV 存储延迟降低 30-60%。
Conductor:KV Cache 中心化的全局调度器
Conductor 是 Mooncake 的核心大脑,以独立服务运行,做所有路由决策。其完整调度算法如下。
输入与状态
- :Prefill 实例集合
- :Decoding 实例集合
- :新到达请求,包含字段:
prompt_tokens:输入 token 序列max_tokens:输出 token 数上限TTFT_SLO、TBT_SLO:延迟约束
- :KV Block 大小(每块包含的 token 数)
逐步算法(展开版)
算法:KV Cache 中心化调度(Conductor)
输入:P(Prefill 池), D(Decode 池), R(请求), B(Block 大小)
输出:(p*, d*) = 选定的(Prefill 实例, Decoding 实例),或 REJECT
步骤 1:生成前缀 Block 哈希序列
block_hashes ← PrefixHash(R.prompt_tokens, B)
// block_hashes[i] 是第 i 个 KV Block 的哈希键
步骤 2:初始化搜索状态
best_TTFT ← ∞
best_p ← None
步骤 3:遍历所有 Prefill 实例,寻找最优分配
FOR 每个 Prefill 实例 p ∈ P:
步骤 3a:计算本地前缀缓存命中长度
cache_len[p] ← PrefixMatch(p.cached_blocks, block_hashes)
// 返回从头开始连续命中的 token 数
步骤 3b:估算本地执行的 TTFT
T_等待[p] ← 估算 p 的当前排队等待时间
T_prefill[p] ← 估算对 (len(R.prompt) - cache_len[p]) 个 token 做 Prefill 的时间
// 这里包含 Prefill 的 O(L²) 注意力计算成本
步骤 3c:判断是否考虑远程缓存迁移
IF cache_len[p] < kvcache_balancing_threshold × len(R.prompt):
// 本地缓存命中率不够高,考虑从其他实例迁入缓存
FOR 每个其他实例 p',满足 cache_len[p'] > cache_len[p]:
T_迁入 ← 估算从 p' 向 p 迁移缺失 Block 的传输时间
T_候选 ← T_等待[p] + T_prefill_with_remote_cache + T_迁入
IF T_候选 < best_TTFT:
best_TTFT ← T_候选
best_p ← p
best_cache_source ← p'
END FOR
ELSE:
// 本地缓存足够,直接使用
T_候选 ← T_等待[p] + T_prefill[p]
IF T_候选 < best_TTFT:
best_TTFT ← T_候选
best_p ← p
best_cache_source ← p // 本地缓存
END IF
END FOR
步骤 4:选择 Decoding 实例
d* ← SelectDecodingInstance(D) // 基于负载均衡
步骤 5:SLO 验证(提前拒绝门控)
IF best_TTFT > R.TTFT_SLO 或 EstimateTBT(d*) > R.TBT_SLO:
RETURN REJECT // 提前拒绝,不浪费 Prefill 算力
步骤 6:热点迁移(主动缓存复制)
IF best_cache_len / len(R.prompt) > hot_threshold:
TriggerCacheReplication(best_cache_source → best_p)
// 主动将热门 Block 从缓存源复制到目标实例,避免后续再传输
步骤 7:返回调度结果
RETURN (best_p, d*)
三个关键设计选择的分析
设计选择 1:为什么要在缓存命中率和负载均衡之间权衡?
一个纯缓存感知的调度器会总是选择缓存命中率最高的实例,这会导致热点集中:流行系统提示会让少数实例超载,其余实例空闲。kvcache_balancing_threshold 参数控制这个权衡:如果本地缓存命中省去的 Prefill 计算不超过阈值比例,调度器就考虑负载均衡因素。
如果完全不考虑负载均衡会怎样? 实验(论文图 6)显示,纯缓存感知调度的 TTFT P90 反而差于”缓存感知 + 负载均衡”的组合策略,因为热点实例排队时间过长抵消了缓存命中的收益。
设计选择 2:为什么估算 TTFT 而不是最大化吞吐?
纯吞吐最大化忽略尾延迟。一个贪婪地打包尽可能多 token 的调度器会频繁违反新到请求的 TTFT SLO。通过在调度时估算 TTFT 并与 SLO 比较,Conductor 可以在系统过载时提前拒绝请求(而非浪费 GPU 时间做无法满足 SLO 的 Prefill)。
设计选择 3:为什么维护一个独立的 Conductor 服务?
与每个实例自己做调度(类似于 vLLM 的本地调度)相比,全局 Conductor 能看到整个集群的 KV Cache 分布状况,做出全局最优路由决策,尤其是在需要远程缓存迁移时。代价是 Conductor 本身是一个中心化的瓶颈,需要高可用设计。
分块流水线并行(CPP):支持超长文本的多节点 Prefill
对于超长 prompt(如 128k token),单节点可能既没有足够显存也没有足够算力完成及时 Prefill。Mooncake 引入了 Chunked Pipeline Parallelism(CPP,分块流水线并行):
graph LR
subgraph 输入
Prompt["128k token Prompt<br/>拆分为 ≥1000 token 的 Chunk"]
end
subgraph 流水线组["流水线组(3 个 Prefill 节点)"]
N1["节点 1<br/>Transformer 第 1-26 层<br/>处理 Chunk 1"]
N2["节点 2<br/>Transformer 第 27-53 层<br/>处理 Chunk 1"]
N3["节点 3<br/>Transformer 第 54-80 层<br/>处理 Chunk 1"]
end
subgraph 流水线调度
T1["t=0: Chunk1 → 节点1<br/>t=1: Chunk1 → 节点2, Chunk2 → 节点1<br/>t=2: Chunk1 → 节点3, Chunk2 → 节点2, Chunk3 → 节点1<br/>...(各节点始终有任务在跑)"]
end
Prompt --> N1
N1 -->|"隐层状态(层间通信)"| N2
N2 -->|"隐层状态"| N3
Prompt --> T1
图 5 — Chunked Pipeline Parallelism(CPP)。Prompt 被切成多个 Chunk,每个 Chunk 依次流过一条由多节点组成的流水线(每个节点负责一部分 Transformer 层)。当 Chunk1 在节点 2 计算时,Chunk2 已在节点 1 开始处理,所有节点持续保持计算饱和。
CPP 与其他并行方案的对比
| 方案 | 通信模式 | 主要缺点 |
|---|---|---|
| 张量并行(All-Reduce) | 每层都要做全局 All-Reduce | 通信频率极高;延迟随节点数增加 |
| 序列并行(Ring All-Gather) | 每层需要环状 All-Gather + Reduce | 拓扑复杂,对 Chunk 划分变化敏感 |
| CPP(Mooncake) | 仅在流水线阶段边界传递隐层状态 | 通信频率低;可与 KV 传输重叠 |
CPP 的关键优势是:跨节点通信只在隐层状态跨越流水线边界时发生(每个 Chunk 每个 Stage 一次),不像张量并行那样在每个注意力头都要通信。这使得通信可以与计算重叠:节点 1 把 Chunk 1 的隐层状态流给节点 2 的同时,节点 1 已经在处理 Chunk 2 了。
论文指出这是流水线并行首次用于推理阶段(Prefill)的工作——此前流水线并行主要用于训练(GPipe、PipeDream 等)。
边界条件:CPP 何时不适用? 当 prompt 很短(<500 token)时,启动多节点流水线的通信开销超过了并行收益,此时单节点 Prefill 更合适。CPP 专为 token 数量达到数千到数十万的超长上下文设计。
提前拒绝策略(Early Rejection)
一个微妙但工程上非常重要的细节是提前拒绝。没有它,以下场景会反复发生:
- Conductor 根据当前 Decoding 负载估算,把请求 分配给某 Prefill 实例。
- 的 128k token Prompt 需要 20 秒才能完成 Prefill。
- 20 秒后 Prefill 完成,但此时 Decoding 实例已经因其他请求涌入而超载。
- Decoding 实例拒绝接收 (TBT SLO 无法满足)。
- 那 20 秒的 Prefill 算力完全浪费。
flowchart TD
A[新请求到达] --> B{前缀缓存查找}
B --> C[估算 TTFT_candidate]
C --> D{TTFT_candidate ≤ TTFT_SLO?}
D -- 否 --> E[提前拒绝:TTFT 不可行]
D -- 是 --> F{估算 Decoding TBT 可行性}
F -- 不可行 --> G[提前拒绝:TBT 不可行]
F -- 可行 --> H[调度到 Prefill 实例]
H --> I[执行 Prefill]
I --> J{Decoding 实例接受?}
J -- 拒绝(负载已变化)--> K[重试或拒绝]
J -- 接受 --> L[传输 KV + 开始 Decoding]
L --> M[返回 token 给客户端]
图 6 — 提前拒绝决策流程。在调度 Prefill 之前同时检查 TTFT 和 TBT 的可行性,避免无效 Prefill 算力浪费,保护 Decoding SLO。
基于预测的提前拒绝
基础版提前拒绝只检查当前 Decoding 实例的状态快照。一个更精细的预测版版本还会考虑负载波动。论文观察到:Prefill 和 Decoding 的负载往往是反相振荡的——当一批 Prefill 任务同时完成,会在短时间内向 Decoding 池涌入大量请求,引发 TBT 抖峰。
预测算法的核心逻辑:
算法:系统级解码负载预测
对于未来时间窗口 [now, now + T_horizon] 中的每个时刻 t:
active_at_t = {正在 Decode 的请求 r | r.start ≤ t ≤ r.start + t_d}
arriving_at_t = {正在 Prefill 的请求 r | r.expected_prefill_done ≈ t}
total_at_t = |active_at_t| + |arriving_at_t|
avg_TBT_ratio[t] = mean over 所有 Decoding 实例 d of:
(total_at_t / d.容量) × baseline_TBT
IF max(avg_TBT_ratio) > TBT_SLO:
REJECT
ELSE:
ACCEPT
实验结果(论文 Table 2):
| 策略 | 被浪费的 Prefill 请求数 | 改善 |
|---|---|---|
| 无提前拒绝(基线) | 4,183 | — |
| 提前拒绝(仅状态快照) | 3,771 | 9.9% |
| 提前拒绝 + 负载预测 | 3,589 | 14.2% |
预测版额外减少 4.3% 的无效 Prefill,因为它能捕捉到”多个 Prefill 同时完成导致 Decoding 突发涌入”的模式,而纯状态快照策略在 Prefill 完成时才看到这一变化,已为时过晚。
为什么彻底解耦,而不是 Chunked Prefill?
一个合理的质疑是:Sarathi-Serve(OSDI 2024)已经提出了”分块 Prefill(Chunked Prefill)“——把长 Prefill 切成小块,与 Decode 步骤交替执行,从而减少干扰——为什么 Mooncake 还要完全解耦?
graph LR
subgraph vLLM["耦合架构(vLLM)"]
G1[GPU 1<br/>Prefill + Decode]
G2[GPU 2<br/>Prefill + Decode]
G3[GPU 3<br/>Prefill + Decode]
G4[GPU 4<br/>Prefill + Decode]
end
subgraph Mooncake["解耦架构(Mooncake)"]
direction LR
P1[Prefill GPU 1]
P2[Prefill GPU 2]
D1[Decode GPU 3]
D2[Decode GPU 4]
KV[KV 池<br/>CPU DRAM]
P1 <-->|写 KV| KV
P2 <-->|写 KV| KV
D1 <-->|读 KV| KV
D2 <-->|读 KV| KV
end
图 7 — 耦合 vs 解耦架构对比。在 vLLM 中,每块 GPU 同时承担 Prefill 和 Decode;在 Mooncake 中,GPU 资源专用于单一阶段,KV 状态通过共享池流动。
Mooncake 作者的两个核心论点:
论点 1:超长文本需要跨节点 Prefill 并行。在耦合架构中,单次请求的 Prefill 并行度受限于单个模型副本的张量并行度(通常 8-16 GPU)。解耦后,Mooncake 可以将多个 Prefill 节点组成 CPP 流水线,让单次 128k token 的 Prefill 跨越更多 GPU 水平扩展,而完全不影响 Decoding 集群。
论点 2:显存与上下文长度解耦。在耦合架构中,Prefill 期间 KV 张量必须保留在 GPU 显存直到 Decode 结束。解耦后,Prefill 节点将 KV 逐层写出到 CPU DRAM,GPU 显存只需保留当前层组的临时缓冲,从而实现显存需求与上下文长度无关。
解耦的代价:KV 传输开销加到 TTFT 上,以及更高的系统复杂度。这在短文本、低缓存命中率的场景下不如耦合架构简洁。论文对此是诚实的:Mooncake 是为 Kimi 的长文本重度负载优化的。
实验评测
测试环境
- 硬件:每节点 8 × NVIDIA A800-SXM4-80GB GPU;节点间 800 Gbps RDMA;节点内 NVLink
- 模型:LLaMA2-70B 架构(使用 dummy 权重,保证模型结构真实,避免泄露实际参数)
- 基线:vLLM(连续批处理 + PagedAttention,代表耦合架构的最优水平)
公开数据集实验
| 数据集 | 平均输入 token | 平均输出 token | 前缀缓存命中率 | Mooncake 配置 | vLLM 配置 | 吞吐量提升 |
|---|---|---|---|---|---|---|
| ArXiv 摘要 | 8,088 | 229 | ~0%(独立文档) | [3P+1D] | [4M] | +20% |
| L-Eval | 19,019 | 72 | >80%(共享上下文) | [3P+1D] | [4M] | +40% |
[3P+1D] = 3 个 Prefill 实例 + 1 个 Decoding 实例;[4M] = 4 个耦合实例。两者 GPU 总量相同。
在 ArXiv(无缓存复用),Mooncake 仍获得 20% 提升,完全来自于 Prefill 与 Decode 的解耦隔离。在 L-Eval(>80% 缓存命中),前缀缓存节省了大量 Prefill 计算,带来额外 20% 的提升。
模拟长文本吞吐扩展
xychart-beta
title "Mooncake vs. vLLM 吞吐量提升(50% 缓存命中率,512 输出 token)"
x-axis ["16k", "32k", "64k", "128k"]
y-axis "吞吐量提升(%)" 0 --> 600
bar [50, 120, 280, 525]
图 8 — 吞吐量提升随上下文长度超线性增长。16k token 输入提升 50%;128k token 输入提升 525%。原因是 vLLM 在长文本场景下无法批处理(单个请求占满显存),而 Mooncake 的解耦架构允许持续批处理。
提升超线性增长的机制:
- vLLM 的吞吐随上下文变长而下降(大 KV 独占显存,阻塞其他请求)
- Mooncake 的吞吐保持稳定(KV 存在 CPU DRAM,不占 Decode 实例显存)
真实 Kimi 生产负载
最具说服力的实验:回放 23,000 条真实 Kimi 请求(保留原始时间戳)。配置:Mooncake [10P+10D] vs. vLLM [20M]。
| 指标 | Mooncake | vLLM |
|---|---|---|
| TTFT P90 SLO 达标率 | ~100% | ~100% |
| TBT P90 SLO 达标率 | 100% | 57% |
| 有效吞吐(Goodput) | 多处理 75% 请求 | 基线 |
TBT 达标率的差异最为关键:vLLM 有 43% 的解码间隔超出延迟目标,而 Mooncake 维持 100% 达标。根本原因是 vLLM 的长文本 Prefill 反复打断解码批次,而 Mooncake 的 Decoding 集群完全不受 Prefill 干扰。
局限性与边界条件
任何系统设计都有其适用范围和不足,Mooncake 也不例外:
1. 短文本、低缓存命中率场景。当 prompt 很短(<500 token)且没有前缀复用时,KV 传输带来的 TTFT 额外开销使解耦架构不如耦合架构简洁高效。Mooncake 是为 Kimi 的长文本重度负载优化的,不是通用解法。
2. 冷启动问题。系统刚启动时缓存为空,每个请求都需要完整 Prefill。系统性能会随着缓存填充逐渐提升,无法立即达到稳态性能,这在弹性扩缩容时是一个实际工程挑战。
3. 阈值参数手动调优。kvcache_balancing_threshold 等关键参数目前需要手动调整,论文承认这依赖运维经验。在负载分布动态变化的场景下,次优参数会导致要么热点集中(过于重视缓存命中),要么缓存浪费(过于重视负载均衡)。自适应算法是未来工作方向。
4. 输出长度难以预测。早期拒绝算法假设所有请求有均匀的解码时间 。实际上输出长度高度可变,精确预测仍是开放问题(论文称其”代价高且准确率低”)。
5. 容错性。论文未详细讨论 Prefill 实例或 Decoding 实例在请求处理过程中故障时的恢复机制。分布式 KV Cache 池的高可用设计也未展开。
Conductor 调度完整示例推演
为了让算法更具体,我们通过一个完整的调度决策示例来理解 Conductor 的工作过程。
系统设置: 4 个 Prefill 实例(P1-P4),2 个 Decoding 实例(D1-D2),LLaMA2-70B(每 token 320 KB KV),RDMA 带宽 = 100 GB/s。
到来的新请求 R:
prompt_tokens:32,768 个 token(32k 上下文)max_tokens:512TTFT_SLO:10 秒TBT_SLO:0.1 秒/token
步骤 1 — 前缀哈希查找: Conductor 生成 2,048 个 Block 哈希(32k token / 16 token 每 Block),查询各 Prefill 实例的缓存状态:
| 实例 | 匹配 Block 数 | cache_len(token 数) |
|---|---|---|
| P1 | 1,200 个 Block 命中 | 19,200 token |
| P2 | 600 个 Block | 9,600 token |
| P3 | 0 个 Block | 0 token |
| P4 | 800 个 Block | 12,800 token |
步骤 2 — 各实例的 TTFT 估算:
假设条件:
- 所有实例排队等待时间均为 0.5 秒
- Prefill 处理速率 = 2,000 token/秒(计算密集型,含二次方注意力)
- 每个 Block 传输时间 = 16 token × 320 KB = 5 MB;100 GB/s → ~0.05 ms/Block
✓(< 10秒 SLO)
✗(> 10秒 SLO)
✗(> 10秒 SLO)
✗(> 10秒 SLO)
步骤 3 — P4 向 P1 迁移远程缓存的可能性: 若 P4 从 P1 传输 P4 缺少的 6,400 token 对应 Block:
- 传输量 =
- 传输时间 =
✓
步骤 4 — 选择最优:
- P1:7.28 秒 ✓(本地缓存,最优)
- P4 + P1 缓存:7.32 秒 ✓(略差,有迁移开销)
Conductor 选择 P1(本地缓存最多,TTFT 7.28s 最优)。
步骤 5 — Decoding 实例选择: D1 负载 60%,D2 负载 30% → 选择 D2(负载轻,TBT SLO 可行)。
步骤 6 — SLO 验证通过。 Conductor 分配 (P1, D2) 给本次请求。
这个例子说明了为什么 Conductor 需要同时考虑缓存感知和负载感知:P1 的大缓存避免了 60% 的 Prefill 计算;即使 P4 通过迁入缓存也能勉强满足 SLO,但代价是额外 20ms 传输时间且结果更差。
关键公式小结(供参考):
TTFT 估算的完整公式:
其中 是 Prefill 计算速率(token/秒), 在使用本地缓存时为 0,否则为跨节点缓存迁移时间。
KV Cache 存储层次与淘汰策略的深入分析
分布式 KV Cache 池引入了经典的分布式系统问题:内存填满怎么办?如何避免热点集中?何时复制,何时淘汰?
三层存储层次详细设计
graph TD
subgraph GPU显存["GPU VRAM(80 GB / 节点,带宽 ~1 TB/s)"]
ActiveKV["活跃请求的 KV Block<br/>— 延迟最低,容量最小"]
end
subgraph CPU内存["CPU DRAM(1-2 TB / 节点,带宽 ~50 GB/s)"]
WarmKV["热 KV Block<br/>— 近期前缀缓存,频繁访问"]
end
subgraph SSD存储["SSD(多 TB / 节点,带宽 ~3 GB/s)"]
ColdKV["冷 KV Block<br/>— 低频访问,长期持久化"]
end
ActiveKV -- "淘汰(请求完成后)" --> WarmKV
WarmKV -- "淘汰(LRU/LFU)" --> ColdKV
ColdKV -- "按需加载" --> WarmKV
WarmKV -- "按需加载" --> ActiveKV
图 A1 — KV Cache 三层存储层次。每升一层容量减少 10-100×,但带宽成比例提升。Conductor 在估算 TTFT 时需将各层的读取延迟纳入考量。
三层存储的容量和延迟特征对比:
| 层次 | 典型容量 | 带宽 | 访问延迟 | 适用场景 |
|---|---|---|---|---|
| GPU VRAM | 80 GB/节点 | ~1 TB/s | 微秒级 | 当前正在执行的请求 |
| CPU DRAM | 1-2 TB/节点 | ~50 GB/s | 数十微秒 | 近期使用的前缀缓存 |
| SSD | 数 TB/节点 | ~3 GB/s | 毫秒级 | 历史前缀、冷数据 |
淘汰策略对比
论文评估了三种 CPU DRAM 层的淘汰策略:
LRU(最近最少使用):淘汰最久未被访问的 Block。对有时间局部性(重复对话、常用系统提示)的工作负载效果最佳。
LFU(最不频繁使用):淘汰访问频率最低的 Block。对于结构上固定热门(与时间无关)的前缀效果更好。
请求特征驱动的淘汰:利用每个请求的元数据(剩余输出 token 预算、SLO 等级、请求优先级)指导淘汰。优先保留高价值请求或接近完成的请求的 KV Block,以避免浪费已做的 Prefill 计算。
生产环境中,Mooncake 采用组合策略:对一般 Block 使用 LRU,对正在传输中或正在被活跃 Decode 实例使用的 Block 设置”固定(pinned)“标记,禁止其被淘汰。
热点 Block 主动复制算法
当 Conductor 检测到某个前缀 Block 集合被大量并发请求访问时,会触发主动复制:
过程:热点缓存主动复制
IF block_hashes[0..N] 在时间窗口 W 内被 ≥ K 个并发请求请求:
FOR 每个不持有这些 Block 的 Prefill 节点 p:
IF p 有足够的 DRAM 容量:
TransferBlocks(source_node → p, block_hashes[0..N])
p.cached_blocks.add(block_hashes[0..N])
END FOR
// 复制后,后续请求可以路由到本地副本,
// 避免反复跨节点传输,将缓存命中负载分散到多个节点。
这类似于 CDN 边缘缓存:当某内容变热门后,将其复制到靠近用户的边缘节点。阈值 和窗口 控制复制带宽成本与命中率改善之间的权衡。
有效吞吐(Goodput)的精确定义
Mooncake 的核心评价指标是 Goodput(有效吞吐),而非原始 throughput。只有在 SLO 约束范围内完成的请求才计入 Goodput:
分母是观测时间窗口;分子只统计完全满足 SLO 的已完成请求数。
为什么这个区分很重要? 举例说明:
| 系统 | 原始吞吐(req/s) | SLO 达标率 | Goodput(req/s) |
|---|---|---|---|
| 系统 A(vLLM) | 100 | 57% TBT 达标 | ~57 |
| 系统 B(Mooncake) | 85 | 100% 双指标达标 | 85 |
虽然系统 A 的原始吞吐更高,但系统 B 的有效吞吐更高 49%。在生产环境中,SLO 违规意味着用户体验下降和服务惩罚,因此有效吞吐才是真正有价值的指标。
Mooncake 在真实 Kimi 负载下 75% 的 Goodput 提升直接意味着:服务同等请求量,Mooncake 可以少用 43% 的 GPU;或用同等 GPU,服务 75% 更多用户。
解耦服务生态中的定位
解耦 Prefill 与 Decode 的想法并非 Mooncake 首创。Mooncake 的贡献在于如何组织解耦以及在其上构建什么系统。横向对比:
| 系统 | 核心贡献 | Prefill-Decode 架构 | KV 管理方式 |
|---|---|---|---|
| Orca(2022) | 迭代级调度 | 耦合 | 本地、按请求 |
| vLLM(2023) | PagedAttention | 耦合 | 本地分页 Block |
| DistServe(2024) | P/D 解耦 | 解耦 | 各集群本地 |
| Sarathi-Serve(2024) | 分块 Prefill | 耦合(交错) | 本地 |
| Mooncake(2024) | KV 中心调度 + CPP + 跨集群 KV 池 | 解耦 | 分布式、多层、全集群 |
| Llumnix(2024) | 请求实时迁移 | 解耦 | 分布式 |
Mooncake 的关键区分是把 KV Cache 池从本地实现细节提升为全局管理的第一类分布式资源,使调度器能将缓存传输成本作为路由决策的一阶变量——而此前的所有系统都没有做到这一点。
系统实现概要
论文对 Mooncake 的工程实现有简要描述:
- 核心引擎:C++ 实现,使用 CUDA 核函数处理注意力和 MLP 计算。
- KV 传输:Messenger 服务基于 ibverbs API(InfiniBand verbs)构建,使用 GPUDirect RDMA 内存注册,支持 GPU HBM 到 GPU HBM 的直接零拷贝传输。
- 控制面:gRPC 用于 Conductor 与实例之间的控制消息(请求分配、缓存元数据查询)。
- 数据面:节点内 GPU 间使用 NCCL 通信(如张量并行的 All-Reduce);跨节点使用定制 RDMA 库。
- 缓存元数据:分布式哈希表(按节点分片),存储
block_hash → (node_id, 内存地址, 引用计数)的映射关系。 - Block 大小:通常为 16 个 token / Block,与 vLLM PagedAttention 的粒度兼容。
开源的 KV 传输引擎已在 GitHub 上公开:kvcache-ai/Mooncake。
行业影响与后续工作
Mooncake 于 2024 年 6 月发布,并获得了 FAST 2025 最佳论文奖,表明其不仅影响了 ML 推理社区,也深刻影响了存储与分布式系统领域。其核心思想在业界已被广泛认可:
- Llumnix(OSDI 2024):在解耦服务的基础上进一步引入请求实时迁移(Live Migration),允许正在进行中的 Decode 请求在实例间无缝迁移,实现更细粒度的负载均衡和 SLO 优先级分离。
- KV-Fold(2024):对超长文本的 KV Cache 做循环压缩,与 Mooncake 的 KV 解耦架构互补,可进一步减少传输量。
- DistServe 后续工作:探索在不同负载特性下最优的 Prefill:Decode GPU 比例。
Mooncake 的核心洞见——KV Cache 应该是调度的第一类资源,而非次要的显存管理问题——已经成为生产 LLM 推理系统设计的共识。
核心贡献总结
Mooncake 的五个贡献单独来看都不是全新的思想,但组合起来形成了一个清晰、自洽的生产级架构:
-
以 KV Cache 为中心的解耦架构:不只是物理分离 Prefill 和 Decode,而是让 KV Cache 的放置和传输成本驱动一切调度决策。
-
跨集群分布式 KV Cache 池与哈希去重:将前缀缓存从单实例优化扩展为集群级资源,在高前缀复用负载下可消除 80%+ 的重复 Prefill 计算。
-
CPP(分块流水线并行):首次将流水线并行应用于推理阶段的 Prefill,支持超长文本的多节点水平扩展。
-
逐层异步 KV 流式传输:计算与传输重叠,使 TTFT 中的传输开销最小化,并将 Prefill 的显存需求与上下文长度解耦。
-
TTFT 驱动的提前拒绝 + 负载预测:在过载场景下防止无效 Prefill 计算浪费,同时保护 Goodput(有效吞吐)和尾延迟 SLO。
75% 的真实生产吞吐量提升证明这些架构选择不是学术练习,而是在 Kimi 实际规模下经过检验的有效改进。
前缀缓存的数学分析:何时缓存总比重算更快?
跨节点 KV Cache 传输的代价值得与重新计算的代价做精确对比,这个对比给出了缓存传输总是合算的数学保证。
定义前缀缓存命中率 为已缓存 token 占总 prompt token 的比例:
通过缓存节省的 Prefill 计算量:
其中 是长度为 的完整 Prefill 计算量(注意力 + FFN):
( 项是 QK 注意力计算; 项是 4 倍隐层宽度的 FFN。)
远程缓存传输的盈亏平衡条件:当传输代价 < 重算节省时,缓存传输有利可图:
代入具体数值:,,:
- 左边(传输成本):
- 右边(重算成本):
由于 ,在 Mooncake 的硬件环境下,传输一个已缓存的 KV Block 始终比重新计算快约 156 倍。这是 KV Cache 解耦在数学上合算的根本保证。
开放研究问题
论文留下了几个值得思考的开放问题:
-
最优 Prefill:Decode GPU 比例:[3P+1D] 配置是固定的,但最优比例取决于负载的输入/输出比、缓存命中率等动态因素,缺乏理论推导。
-
自适应阈值调优:
kvcache_balancing_threshold目前手动设置,在负载分布日内动态变化的场景下,在线调整算法(如 Bandit 或控制论方法)会更鲁棒。 -
输出长度预测:精确的每请求输出长度预测可将早期拒绝从”当前负载高就拒”升级为”预测代价超容量就拒”,大幅提升 Goodput 精度。
-
KV 压缩后传输:若在 RDMA 传输前对 KV Block 做量化或低秩压缩,有效带宽可成倍提升,与 KV-Fold 等工作形成互补。
延伸阅读
- Orca(OSDI 2022):迭代级调度,Mooncake 的连续批处理基础。
- vLLM / PagedAttention(SOSP 2023):Mooncake 采用并扩展的细粒度 KV 分页方案。
- DistServe(OSDI 2024):影响了 Mooncake Prefill-Decode 分离设计的同期解耦服务工作。
- Sarathi-Serve(OSDI 2024):分块 Prefill 方案——Mooncake 完全解耦路线的耦合架构替代方案。
- Llumnix(OSDI 2024):在解耦服务基础上进一步引入请求实时迁移的调度系统。
- SGLang(MLSys 2025):RadixAttention 与高效结构化生成,与 Mooncake 的 KV 缓存策略互补。
- Mooncake 开源仓库:KV 传输引擎(Messenger)已在
github.com/kvcache-ai/Mooncake开源。
本笔记基于 Mooncake arXiv:2407.00079(Qin et al., 2024),该论文荣获 FAST 2025 最佳论文奖。所有性能数据来自论文在 A800 GPU 集群上使用 LLaMA2-70B 架构(dummy 权重)进行的实验评估。