Mooncake:以 KV Cache 为核心的大模型推理服务解耦架构

笔记日期: 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 的 LL 个 token 一次性并行处理,计算所有层的注意力 Key-Value 矩阵并缓存(即 KV Cache)。
  • Decode(解码/增量)阶段:每次前向传播只处理 1 个新 token,利用已缓存的 KV 执行注意力计算,生成下一个 token。

Prefill 阶段高度并行(所有 token 同时处理),计算密集;Decode 阶段完全串行(每步只生成一个 token),内存带宽密集。

2. KV Cache 的大小

在 Transformer 的每一层,每个 token 会产生一个 Key 向量 kRdkk \in \mathbb{R}^{d_k} 和一个 Value 向量 vRdvv \in \mathbb{R}^{d_v}。这些向量被缓存起来供后续 Decode 步骤复用。

对于每个 token,KV Cache 的存储量为:

KV每个 token=2×n层数×nKV 头数×d头维度×精度字节数\text{KV}_{\text{每个 token}} = 2 \times n_{\text{层数}} \times n_{\text{KV 头数}} \times d_{\text{头维度}} \times \text{精度字节数}

以 LLaMA2-70B 为例(n层数=80n_{\text{层数}} = 80,GQA 有 8 个 KV 头,d=128d_{\text{头}} = 128,FP16 存储):

KV每个 token=2×80×8×128×2=327,680 字节320 KB\text{KV}_{\text{每个 token}} = 2 \times 80 \times 8 \times 128 \times 2 = 327{,}680 \text{ 字节} \approx 320 \text{ KB}

128k token 的上下文需要约 40 GB KV Cache —— 占满一整块 A100-80GB 的一半显存。这就是为什么长文本场景下显存管理是核心挑战。

3. Prefill 阶段的计算复杂度

Prefill 阶段的自注意力计算复杂度随序列长度呈平方增长:

  • 注意力计算O(L2d)O(L^2 \cdot d)(与序列长度的平方成正比)
  • MLP/FFN 层O(Ldffn)O(L \cdot d_{\text{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 附带一个内容寻址哈希键:

hblock=Hash(hparent, tokens[start:end])h_{\text{block}} = \text{Hash}\left(h_{\text{parent}},\ \text{tokens}_{[start:end]}\right)

这种内容寻址方案实现了去重:如果两个请求的前缀完全相同,其 Block 哈希也完全相同,Conductor 无需比较原始张量就能识别它们为同一数据,直接复用缓存。哈希键还用于:

  1. 调度时的前缀匹配:找到哪个 Prefill 实例本地已缓存了最多的匹配 Block。
  2. 缓存淘汰与复制决策:跟踪热 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 传输时间的理论估算:

T传输=SKVBWRDMA+T延迟T_{\text{传输}} = \frac{S_{\text{KV}}}{BW_{\text{RDMA}}} + T_{\text{延迟}}

以 LLaMA2-70B 的 16k token 上下文为例:SKV=16,384×320KB5GBS_{\text{KV}} = 16{,}384 \times 320\,\text{KB} \approx 5\,\text{GB}BW=100GB/sBW = 100\,\text{GB/s}(800 Gbps),则 T传输50msT_{\text{传输}} \approx 50\,\text{ms}。在 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 的核心大脑,以独立服务运行,做所有路由决策。其完整调度算法如下。

输入与状态

  • P={p1,,pN}P = \{p_1, \ldots, p_N\}:Prefill 实例集合
  • D={d1,,dM}D = \{d_1, \ldots, d_M\}:Decoding 实例集合
  • RR:新到达请求,包含字段:
    • prompt_tokens:输入 token 序列
    • max_tokens:输出 token 数上限
    • TTFT_SLOTBT_SLO:延迟约束
  • BB: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)

一个微妙但工程上非常重要的细节是提前拒绝。没有它,以下场景会反复发生:

  1. Conductor 根据当前 Decoding 负载估算,把请求 RR 分配给某 Prefill 实例。
  2. RR 的 128k token Prompt 需要 20 秒才能完成 Prefill。
  3. 20 秒后 Prefill 完成,但此时 Decoding 实例已经因其他请求涌入而超载。
  4. Decoding 实例拒绝接收 RR(TBT SLO 无法满足)。
  5. 那 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,7719.9%
提前拒绝 + 负载预测3,58914.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,088229~0%(独立文档)[3P+1D][4M]+20%
L-Eval19,01972>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]。

指标MooncakevLLM
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. 输出长度难以预测。早期拒绝算法假设所有请求有均匀的解码时间 tdt_d。实际上输出长度高度可变,精确预测仍是开放问题(论文称其”代价高且准确率低”)。

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:512
  • TTFT_SLO:10 秒
  • TBT_SLO:0.1 秒/token

步骤 1 — 前缀哈希查找: Conductor 生成 2,048 个 Block 哈希(32k token / 16 token 每 Block),查询各 Prefill 实例的缓存状态:

实例匹配 Block 数cache_len(token 数)
P11,200 个 Block 命中19,200 token
P2600 个 Block9,600 token
P30 个 Block0 token
P4800 个 Block12,800 token

步骤 2 — 各实例的 TTFT 估算:

假设条件:

  • 所有实例排队等待时间均为 0.5 秒
  • Prefill 处理速率 = 2,000 token/秒(计算密集型,含二次方注意力)
  • 每个 Block 传输时间 = 16 token × 320 KB = 5 MB;100 GB/s → ~0.05 ms/Block

TTTFT(P1)=0.5+32768192002000=0.5+6.78=7.28T_{\text{TTFT}}(\text{P1}) = 0.5 + \frac{32768 - 19200}{2000} = 0.5 + 6.78 = 7.28\,\text{秒} ✓(< 10秒 SLO)

TTTFT(P2)=0.5+3276896002000=0.5+11.58=12.08T_{\text{TTFT}}(\text{P2}) = 0.5 + \frac{32768 - 9600}{2000} = 0.5 + 11.58 = 12.08\,\text{秒} ✗(> 10秒 SLO)

TTTFT(P3)=0.5+327682000=0.5+16.38=16.88T_{\text{TTFT}}(\text{P3}) = 0.5 + \frac{32768}{2000} = 0.5 + 16.38 = 16.88\,\text{秒} ✗(> 10秒 SLO)

TTTFT(P4)=0.5+32768128002000=0.5+9.98=10.48T_{\text{TTFT}}(\text{P4}) = 0.5 + \frac{32768 - 12800}{2000} = 0.5 + 9.98 = 10.48\,\text{秒} ✗(> 10秒 SLO)

步骤 3 — P4 向 P1 迁移远程缓存的可能性: 若 P4 从 P1 传输 P4 缺少的 6,400 token 对应 Block:

  • 传输量 = 640016×5MB=2,000MB\frac{6400}{16} \times 5\,\text{MB} = 2,000\,\text{MB}
  • 传输时间 = 2GB100GB/s=20ms\frac{2\,\text{GB}}{100\,\text{GB/s}} = 20\,\text{ms}

TTTFT(P4 + 从P1迁入缓存)=0.5+0.02+327681920020007.32T_{\text{TTFT}}(\text{P4 + 从P1迁入缓存}) = 0.5 + 0.02 + \frac{32768 - 19200}{2000} \approx 7.32\,\text{秒}

步骤 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 估算的完整公式:

TTTFT(p)=T等待(p)+Ttransfer(p)+Tprefill ⁣(Lcache_len(p)rprefill)T_{\text{TTFT}}(p) = T_{\text{等待}}(p) + T_{\text{transfer}}(p) + T_{\text{prefill}}\!\left(\frac{L - \text{cache\_len}(p)}{r_{\text{prefill}}}\right)

其中 rprefillr_{\text{prefill}} 是 Prefill 计算速率(token/秒),TtransferT_{\text{transfer}} 在使用本地缓存时为 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 VRAM80 GB/节点~1 TB/s微秒级当前正在执行的请求
CPU DRAM1-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 边缘缓存:当某内容变热门后,将其复制到靠近用户的边缘节点。阈值 KK 和窗口 WW 控制复制带宽成本与命中率改善之间的权衡。

有效吞吐(Goodput)的精确定义

Mooncake 的核心评价指标是 Goodput(有效吞吐),而非原始 throughput。只有在 SLO 约束范围内完成的请求才计入 Goodput:

Goodput={R已完成:TTFT(R)TTFT_SLO(R)t:TBTt(R)TBT_SLO(R)}T观测窗口\text{Goodput} = \frac{|\{R \in \text{已完成} : \text{TTFT}(R) \leq \text{TTFT\_SLO}(R) \wedge \forall t: \text{TBT}_t(R) \leq \text{TBT\_SLO}(R)\}|}{T_{\text{观测窗口}}}

分母是观测时间窗口;分子只统计完全满足 SLO 的已完成请求数。

为什么这个区分很重要? 举例说明:

系统原始吞吐(req/s)SLO 达标率Goodput(req/s)
系统 A(vLLM)10057% TBT 达标~57
系统 B(Mooncake)85100% 双指标达标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 的五个贡献单独来看都不是全新的思想,但组合起来形成了一个清晰、自洽的生产级架构:

  1. 以 KV Cache 为中心的解耦架构:不只是物理分离 Prefill 和 Decode,而是让 KV Cache 的放置和传输成本驱动一切调度决策。

  2. 跨集群分布式 KV Cache 池与哈希去重:将前缀缓存从单实例优化扩展为集群级资源,在高前缀复用负载下可消除 80%+ 的重复 Prefill 计算。

  3. CPP(分块流水线并行):首次将流水线并行应用于推理阶段的 Prefill,支持超长文本的多节点水平扩展。

  4. 逐层异步 KV 流式传输:计算与传输重叠,使 TTFT 中的传输开销最小化,并将 Prefill 的显存需求与上下文长度解耦。

  5. TTFT 驱动的提前拒绝 + 负载预测:在过载场景下防止无效 Prefill 计算浪费,同时保护 Goodput(有效吞吐)和尾延迟 SLO。

75% 的真实生产吞吐量提升证明这些架构选择不是学术练习,而是在 Kimi 实际规模下经过检验的有效改进。

前缀缓存的数学分析:何时缓存总比重算更快?

跨节点 KV Cache 传输的代价值得与重新计算的代价做精确对比,这个对比给出了缓存传输总是合算的数学保证。

定义前缀缓存命中率 ρ\rho 为已缓存 token 占总 prompt token 的比例:

ρ=已缓存 token 数总 prompt token 数\rho = \frac{\text{已缓存 token 数}}{\text{总 prompt token 数}}

通过缓存节省的 Prefill 计算量

节省的 Prefill FLOPs=ρCprefill(L)\text{节省的 Prefill FLOPs} = \rho \cdot C_{\text{prefill}}(L)

其中 Cprefill(L)C_{\text{prefill}}(L) 是长度为 LL 的完整 Prefill 计算量(注意力 + FFN):

Cprefill(L)=n层数(4L2d+8Ld2)C_{\text{prefill}}(L) = n_{\text{层数}} \cdot \left(4L^2 d + 8Ld^2\right)

4L2d4L^2 d 项是 QK 注意力计算;8Ld28Ld^2 项是 4 倍隐层宽度的 FFN。)

远程缓存传输的盈亏平衡条件:当传输代价 < 重算节省时,缓存传输有利可图:

KV_per_tokenBWRDMA<1rprefill\frac{\text{KV\_per\_token}}{BW_{\text{RDMA}}} < \frac{1}{r_{\text{prefill}}}

代入具体数值:KV_per_token=320KB\text{KV\_per\_token} = 320\,\text{KB}BWRDMA=100GB/sBW_{\text{RDMA}} = 100\,\text{GB/s}rprefill=2000token/sr_{\text{prefill}} = 2000\,\text{token/s}

  • 左边(传输成本):320×1031011=3.2μs/token\frac{320 \times 10^3}{10^{11}} = 3.2\,\mu\text{s/token}
  • 右边(重算成本):12000=500μs/token\frac{1}{2000} = 500\,\mu\text{s/token}

由于 3.2μs500μs3.2\,\mu\text{s} \ll 500\,\mu\text{s}在 Mooncake 的硬件环境下,传输一个已缓存的 KV Block 始终比重新计算快约 156 倍。这是 KV Cache 解耦在数学上合算的根本保证。

开放研究问题

论文留下了几个值得思考的开放问题:

  1. 最优 Prefill:Decode GPU 比例:[3P+1D] 配置是固定的,但最优比例取决于负载的输入/输出比、缓存命中率等动态因素,缺乏理论推导。

  2. 自适应阈值调优kvcache_balancing_threshold 目前手动设置,在负载分布日内动态变化的场景下,在线调整算法(如 Bandit 或控制论方法)会更鲁棒。

  3. 输出长度预测:精确的每请求输出长度预测可将早期拒绝从”当前负载高就拒”升级为”预测代价超容量就拒”,大幅提升 Goodput 精度。

  4. 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 权重)进行的实验评估。