跳转至

Mooncake

导言

Mooncake 是 Moonshot AI 为 Kimi 长上下文服务设计的 KVCache-centric 推理架构。它不再把 KV Cache 视为某张 GPU 上随请求消失的临时副产物,而是把它提升为跨 Prefill、Decode、CPU DRAM、SSD 与高速网络统一调度的系统资源。本文先用直觉解释,再从 P/D 解耦、Mooncake Store 与 Transfer Engine、缓存感知调度、Chunked Pipeline Parallelism 四个机制展开,并明确论文、当前开源代码与教学重构之间的证据边界。

一句话结论:Mooncake 的关键不是“给 vLLM 外接一个缓存”,而是让计算、KV 容量、传输、排队与 SLO 围绕同一个请求共同做决定。

从长对话的浪费说起

用户和 Kimi 连续对话时,新请求往往包含很长的历史前缀,真正新增的只有末尾几句话。如果每次都把全部历史重新做一遍 Prefill,系统会反复计算已经计算过的内容;如果把历史 KV 全留在 GPU,又会很快耗尽昂贵的显存。

可以把它想象成查阅一份不断追加的档案:普通做法每次从第一页重读;Mooncake 把已经读过的页压成可检索的 KV 小块,新请求先取回最长连续前缀,只阅读新增部分,再把完整上下文交给后续生成。

把长前缀压成可复用 KV 小块,只计算新增内容

这幅图表达的是认知隐喻,不是物理部署图。真正困难之处有四个:Prefill 与 Decode 的资源特征不同;KV 比 GPU 显存能容纳的更多;远端命中未必比本地重算更快;超长 Prefill 本身仍可能阻塞首 Token。

系统全景

Mooncake 论文把系统分成三个主要部分:

  • Conductor:选择 Prefill/Decode 实例,查询 KV 命中,估算排队和传输成本,并执行 SLO 感知的准入与调度。
  • Prefill/Decode 集群:Prefill 负责读完整 Prompt 并生成各层 KV;Decode 读取这些状态,以连续批处理逐 Token 生成。
  • Disaggregated KVCache:通过 Mooncake Store 管理键、位置、副本和淘汰,通过 Transfer Engine 在注册内存之间搬运数据。

Mooncake 论文中的系统架构与请求数据路径

图:Mooncake 论文 Figure 2 的原始裁切。它证明论文公开的组件划分和主数据路径,不证明 2026 年开源仓库与论文时期 Kimi 生产系统逐模块、逐策略完全相同。来源:FAST'25 论文

一次典型请求可以概括为:

  1. Conductor 对完整 Block 的前缀哈希按顺序查询,找到最长连续命中。
  2. 调度器比较候选 Prefill/Decode 实例的命中、排队、传输、计算与 SLO 风险。
  3. Prefill 实例取回已有 KV,只计算未命中的后缀。
  4. 每层新 KV 可以边产生边传给 Decode,避免等全部层完成后再一次性搬运。
  5. Decode 实例把请求加入连续批处理,生成 Token;可复用 KV 则进入 Store 的生命周期。

先区分三个层次

概念层包括 P/D 解耦、远端 KV 复用和成本感知调度;论文机制层是 FAST'25 描述的 Mooncake 生产架构;框架实现层是当前开源 Store、Transfer Engine 与 Conductor 的可见接口。后两者相关,但不能用当前仓库的一段代码反推 Kimi 生产系统的全部策略。

KV Cache 是多大的对象

对标准注意力层,一个请求的逻辑 KV 载荷可以近似写成:

\[ M_{KV} \approx 2 \times L \times N \times H_{kv} \times D_h \times b \]

其中,2 表示 Key 和 Value,\(L\) 是层数,\(N\) 是缓存 Token 数,\(H_{kv}\) 是 KV Head 数,\(D_h\) 是 Head Dimension,\(b\) 是每个元素的字节数。它说明:上下文越长,KV 容量近似线性增长;模型权重不变并不代表长对话的显存需求不变。

但这不是物理显存的精确账单。Tensor Parallel 分片、Block 填充、对齐、副本、元数据、Allocator 碎片和固定缓冲区都会改变实际占用。设备峰值至少应按下式看待:

\[ M_{device,peak}=M_{weights}+M_{KV,live}+M_{workspace}+M_{transfer\ buffers}+M_{allocator\ margin} \]

Mooncake 能把可复用、暂不活跃的 KV 转移到更大的资源池,但 Decode 正在使用的 KV、模型权重、Workspace 与传输缓冲仍需占用设备内存。

机制一:Prefill/Decode 解耦

解决什么

Prefill 是对整段输入做大矩阵计算,通常更偏计算吞吐;Decode 每步只产生少量 Token,却反复读取权重和历史 KV,更受显存带宽、批处理和尾延迟约束。把二者混在同一批次、同一实例,会产生资源干扰:长 Prefill 可能推高 Decode 的 Time Between Tokens,Decode 又会切碎 Prefill 的计算形状。

直觉上,Prefill 像批量录入档案,Decode 像实时逐字答复。两者需要的柜台节奏不同。 P/D 解耦把它们放入独立资源池,并用 KV 传输连接两个阶段。

对象 生产者 消费者 逻辑形状 生命周期
Prompt Tokens Gateway Prefill [S] 单请求
隐状态 l l+1 [B,S,H] 层内
单层 KV Prefill 第 l Store / Decode 第 l [2,S,H_kv,D_h] 请求或缓存
完整 KV 全部 Prefill 层 Decode [L,2,S,H_kv,D_h] Decode 全程
新 Token Decode Step 客户端和下一 Step Token ID 单步
def serve_disaggregated(request, conductor, store, p_pool, d_pool):
    prefix = store.longest_contiguous_prefix(request.block_keys)
    p = conductor.select_prefill(request, prefix, p_pool)
    d = conductor.select_decode(request, prefix, d_pool)

    cached_kv = store.get(prefix.keys, destination=p)
    suffix_tokens = request.tokens[prefix.token_count:]
    layer_kv = []

    for layer_id in range(request.model.num_layers):
        hidden, new_kv = p.prefill_layer(
            layer_id=layer_id,
            cached_kv=cached_kv[layer_id],
            suffix_tokens=suffix_tokens,
        )
        conductor.transfer(new_kv, source=p, destination=d)
        layer_kv.append(new_kv)

    full_kv = concatenate_by_token(cached_kv, layer_kv)
    d.attach(request.id, full_kv)

    while not request.is_finished:
        token = d.decode_one_step(request.id)
        request.append(token)
        conductor.emit(token, request.client)

    store.put(request.complete_block_keys(), d.export_reusable_kv(request.id))
    d.release(request.id)

伪代码强调三件事:命中前缀和新增后缀是两个对象;层 KV 可以流式传输;完整 KV 在 Decode 生命周期内仍需保持。错误重试、背压和副本恢复在真实系统中必须实现,但不应从公开材料臆造细节。

P/D 解耦的物理、因果、流程、时序与数据流五视图

图:根据 FAST'25 机制和固定版本公开接口重构的教学图,不是逐周期执行图。

适用性:Prefill/Decode 资源特征差异明显、上下文较长、负载可形成独立池时更有价值。代价:新增跨池 KV 传输、容量规划、故障域和调度复杂度;小 Prompt、低并发或慢网络下,解耦未必占优。其工程归属跨越推理运行时、调度、网络与容量管理,不是单一 Kernel 优化。

机制二:Mooncake Store 与 Transfer Engine

从“显存对象”变成“分布式对象”

PagedAttention 解决的是设备内 KV Block 的碎片和映射问题;Mooncake Store 进一步要回答跨实例问题:一个 Block 的键是什么、有哪些副本、放在哪里、谁能读、什么时候淘汰、数据如何绕过元数据服务直接传输。

论文描述的 Store 通常把连续 Token 切成 Block,通过前缀哈希识别,按副本和 LRU 管理。Block 大小是实现和实验参数,不是固定常数;FAST'25 的公开 Trace 格式使用 512-Token Block,但论文讨论的系统选择范围更宽。

对象 生产者 消费者 标识 放置
Block Key Store Client Master/Index 模型身份、Prefix Hash、Block 位置 元数据面
Replica Metadata Master Store Client Key 到 Segment/Location Master 内存
Segment Store Client Store Client 注册地址和长度 DRAM、SSD 或设备内存
Transfer Batch Client NIC/DMA 操作列表和 Batch ID 数据面
Completion Transport Caller Batch/Operation 状态 客户端状态
def cache_block(store, transfer, key, source_region, replica_targets):
    replicas = []
    for target in replica_targets:
        segment = store.allocate(target, source_region.length)
        batch = transfer.allocate_batch(capacity=1)
        transfer.submit(
            batch=batch,
            source=source_region,
            destination=segment,
            length=source_region.length,
        )
        status = transfer.wait(batch)
        transfer.free_batch(batch)
        if status == "COMPLETED":
            replicas.append(segment)
        else:
            store.release(segment)

    if len(replicas) == 0:
        return {"stored": False, "key": key, "replicas": []}

    store.publish_metadata(key=key, replicas=replicas)
    return {"stored": True, "key": key, "replicas": replicas}


def read_block(store, transfer, key, destination_region):
    replicas = store.lookup_metadata(key)
    for source_region in store.rank_replicas(replicas):
        batch = transfer.allocate_batch(capacity=1)
        transfer.submit(
            batch=batch,
            source=source_region,
            destination=destination_region,
            length=destination_region.length,
        )
        status = transfer.wait(batch)
        transfer.free_batch(batch)
        if status == "COMPLETED":
            return {"hit": True, "source": source_region}
    return {"hit": False, "source": None}

Store 与 Transfer Engine 的元数据、数据面和生命周期五视图

图:固定开源版本语义的教学重构。Master 管位置和副本;正常载荷由 Client 到 Client 传输,不经 Master 中转。实际介质、拓扑、租约与恢复策略随部署变化。

当前开源代码可直接看到 Store 的 put/get/batch_put/batch_get 路径,以及 Transfer Engine 的内存注册、Batch 分配、传输提交和状态查询;设计文档还明确区分 Master 元数据路径与客户端数据路径。参见固定版本的 Store C APITransfer Engine C APIStore 设计

远端命中不等于免费

是否复用应比较 \(T_{reuse}=T_{get}+T_{incremental}\)\(T_{full}\)。远端命中很长,但链路拥塞、排队或尾部后缀很短时,重算可能反而更合适。缓存还会引入副本成本、元数据一致性、注册内存、淘汰抖动和故障恢复。

机制三:KVCache-centric 调度

命中只是输入,不是目标

传统负载均衡常看队列,Prefix Cache Router 常看最长命中。Mooncake 的调度问题是二者的合并:一个实例命中更多 KV,却可能排队更久;另一个命中少,但计算资源空闲;远端 KV 的位置还会改变传输成本。

论文将首 Token 的估算拆成:

\[ \widehat{TTFT}=T_{transfer}+T_{queue}+T_{prefill} \]

Decode 侧还需要单独检查 TBT 风险。若没有候选满足 SLO,系统可以提前拒绝请求,而不是先占用昂贵资源、最后才超时。这样改善的是 goodput,即满足 SLO 的有效吞吐,而不是凭空创造物理容量。

对象 来源 去向 生命周期
Prefix Block Hashes 请求 Token Store Index 本次调度
每实例命中长度 Index Cost Model 本次决策
队列与传输估计 Telemetry/Topology Cost Model 决策窗口
候选 (P,D) Router Admission 本次决策
SLO Verdict / HTTP 429 Admission Gateway 请求准入
def schedule(request, p_instances, d_instances, index, slo):
    block_keys = hash_complete_prefix_blocks(request.tokens)
    hit_map = index.longest_matches_until_first_miss(block_keys, p_instances)
    candidates = []

    for p in p_instances:
        hit_tokens = hit_map[p.id]
        transfer_time = estimate_transfer(request, hit_tokens, p)
        queue_time = estimate_queue(p)
        prefill_time = estimate_prefill(request.token_count - hit_tokens, p)
        ttft = transfer_time + queue_time + prefill_time

        for d in d_instances:
            tbt = estimate_decode_tbt(request, d)
            candidates.append({
                "p": p,
                "d": d,
                "hit_tokens": hit_tokens,
                "ttft": ttft,
                "tbt": tbt,
            })

    feasible = [
        item for item in candidates
        if item["ttft"] <= slo.ttft and item["tbt"] <= slo.tbt
    ]
    if len(feasible) == 0:
        return {"admitted": False, "status": 429, "placement": None}

    placement = min(feasible, key=lambda item: (item["ttft"], item["tbt"]))
    reserve(placement["p"], placement["d"], request)
    return {"admitted": True, "status": 200, "placement": placement}

KVCache-centric 调度的联合成本、准入、时序与数据流五视图

图:论文调度语义的教学重构。固定版本开源 Conductor 明确提供完整 Block 前缀哈希扫描、首个 Miss 停止、记录各实例最长命中并由 Router 选实例;这不足以证明 Kimi 全量生产成本模型已经开源。参见 Conductor 设计

这里的伪代码用于展示决策闭环,不声称复现生产权重、预测器或并发保留协议。真正上线还要校准 P50/P99 预测误差、陈旧遥测、请求取消、租约、重试和副本扩散。

机制四:Chunked Pipeline Parallelism

超长 Prefill 仍然太慢怎么办

即使 P/D 已解耦,一个超长 Prompt 仍可能独占一条 Prefill 计算路径。Mooncake 的 CPP 将输入切成多个 Chunk,让不同 Chunk 在不同 Pipeline Stage 上重叠推进。它不是把注意力依赖凭空删除,而是改变 Chunk 的计算位置与时间安排,并按 Token 顺序组装最终 KV。

对象 生产者 消费者 逻辑形状 生命周期
Chunks Splitter Prefill Stages N × [B,C,H] Prefill
Boundary State Stage i Stage i+1 Chunk Hidden State Pipeline Wave
Chunk KV Attention Layers KV Collector N × [L,2,C,H_kv,D_h] 请求
Full KV Ordered Concat Decode [L,2,S,H_kv,D_h] Decode
def chunked_pipeline_prefill(tokens, stages, chunk_size):
    chunks = split_in_order(tokens, chunk_size)
    states = {(0, chunk_id): embed(chunk) for chunk_id, chunk in enumerate(chunks)}
    chunk_kv = {chunk_id: [] for chunk_id in range(len(chunks))}

    for wave in range(len(chunks) + len(stages) - 1):
        for stage_id, stage in enumerate(stages):
            chunk_id = wave - stage_id
            if chunk_id < 0 or chunk_id >= len(chunks):
                continue
            input_state = states[(stage_id, chunk_id)]
            output_state, kv = stage.forward_chunk(
                chunk_id=chunk_id,
                state=input_state,
                prior_chunk_kv=chunk_kv,
            )
            chunk_kv[chunk_id].append(kv)
            if stage_id + 1 < len(stages):
                states[(stage_id + 1, chunk_id)] = output_state

    ordered_kv = [chunk_kv[chunk_id] for chunk_id in range(len(chunks))]
    return concatenate_by_token_position(ordered_kv)

CPP 的物理、因果、流程、时序与 KV 数据流五视图

图:根据 FAST'25 §3.3 重构的 CPP 教学图,不等同于未公开的生产执行计划。Chunk 是物理切片,完整 KV 是按 Token 位置组成的逻辑和物理缓存集合。

CPP 更适合超长输入、单节点 TTFT 已成为主瓶颈且跨 Stage 通信可控的场景。短输入仍可走普通 Prefill 路径;否则固定 Pipeline Group、气泡、边界通信和调度复杂度可能抵消收益。

论文结果该怎样读

Mooncake 与三种 vLLM 基线的有效请求容量曲线

图:Mooncake 论文 Figure 1 的原始曲线裁切。横轴是 Token 间隔,纵轴是 Effective Request Capacity Ratio;三条竖线对应论文选定的 TBT Threshold。来源:FAST'25 论文

这张图最有价值的不是“Mooncake 比 vLLM 快”这一句,而是它把容量定义为满足 TBT 门槛的有效请求容量。在论文的 Conversation 工作负载、16 个节点(每节点 8 × A800)和 vLLM 0.5.1 变体基线下,作者报告三档门槛分别提升 498%、157% 和 59%

这些数字能证明:在该实验包络内,把 KV 复用、P/D 资源与 SLO 联合调度,可以显著扩大有效容量。它们不能证明:

  • 单独启用 Prefix Cache 就会获得相同收益;
  • 任意模型、卡型、网络和上下文分布都按同一比例提升;
  • 曲线是单请求延迟、平均吞吐或物理 GPU 算力的直接提升;
  • 当前开源版本在未经调优的集群上可以自动复现实验。

论文还报告,相比此前系统,历史 Kimi 生产工作负载容量提高 115% 和 107%,并在当时运行于数千节点、日处理超过 1000 亿 Token。这是有价值的生产规模证据,但仍属于作者报告的历史比较,不是跨系统、跨版本的独立统一 Benchmark。

什么时候值得采用

Mooncake 类架构通常适合以下组合:

  • 高前缀复用:多轮对话、共享 System Prompt、Agent 工作流或长文档反复追问。
  • KV 容量成为约束:GPU 显存无法保留足够多的热前缀,而主机内存、SSD 或远端内存可利用。
  • P/D 特征差异明显:可分别扩缩容,并能用高速网络承接 KV 跨池传输。
  • SLO 比裸吞吐重要:系统按 TTFT/TBT 的有效请求而不是提交请求数评估容量。
  • 有能力维护控制面:具备遥测、预测、准入、注册内存、故障处理和一致性治理。

不应只因为“上下文很长”就直接采用。如果请求几乎没有共享前缀、网络慢、并发低、Prompt 很短,或者团队无法承担分布式缓存控制面的复杂度,设备内 Prefix Cache、Continuous Batching、PagedAttention 或更简单的 P/D 架构可能更合适。

落地前的最小验证

固定模型、Tokenizer、Block Size、硬件、拓扑和 SLO,采集真实请求的前缀复用分布;分别测量本地重算、DRAM/SSD/远端取回的 P50/P99;最后比较满足 TTFT/TBT 的 goodput,而不是只看 Cache Hit Ratio。命中率提高但 goodput 不提高,说明优化了代理指标而非用户目标。

常见误解

  1. “Mooncake 就是 P/D 解耦。” P/D 是骨架;分布式 KV Store、传输、缓存感知调度和长输入 CPP 才让骨架成为完整系统。
  2. “缓存命中越长越好。” 长命中仍可能因远端传输和排队失败。正确目标是满足 SLO 的联合成本。
  3. “KV 放到 CPU/SSD 后,GPU 就不需要 KV。” Decode 活跃 KV 仍要被高速访问;外部 Store 主要承接可复用或暂不活跃状态。
  4. “Master 是 KV 数据代理。” 公开 Store 设计中,Master 管理元数据,正常数据由客户端之间直接传输。
  5. “开源仓库等于 Kimi 生产调度器。” 开源代码提供可复用组件和明确接口,但不能据此补全未公开的生产预测模型、准入政策和运维系统。
  6. “论文提升可以直接套到我的集群。” 必须先对齐模型、负载、基线、卡型、网络、SLO 与指标定义。

总结

Mooncake 的工作可以归纳为四步:把异质的 Prefill/Decode 分开,把 KV Cache 从设备私有状态变成分布式可复用对象,让调度器比较复用、传输、排队、计算和 SLO 的总成本,再用 CPP 处理极长 Prefill 的关键路径。

它真正改变的是推理系统的优化单位:不再只优化某个 Kernel、某张卡或某个 Batch,而是优化一个请求携带的计算与状态在整个集群中的生命周期。理解这一点,比记住某个百分比更重要。

参考资料

  1. Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving,FAST 2025。
  2. Mooncake 开源仓库,本文源码语义固定于 bfca1ce2af8419c50dc8d464820a95d97d43c930
  3. Mooncake README:组件概览
  4. FAST'25 Trace 格式与请求字段
  5. DistServe,OSDI 2024。
  6. PagedAttention / vLLM,SOSP 2023。

评论