Mooncake 和 HiCache 总览:KV 缓存如何变成系统资源

从 SGLang HiCache、Mooncake Store 和 Transfer Engine 的职责边界出发,说明一段 KV 缓存如何成为可跨节点复用的系统资源。

Code walkthroughsMooncake and HiCache internalsLLM servingKV cacheHiCacheMooncake

这组文章的目标不是介绍 Mooncake 有多快,而是回答一个工程问题:一段前缀 KV Cache 怎样从单个工作进程中的 GPU 张量,变成可以逐级备份、查询、传输和跨节点复用的系统资源。

这个问题值得单独拆开,因为远端 KV 缓存很容易被一句“接入 Mooncake”含糊带过。真正影响系统行为的不是一个后端名称,而是推理引擎中的缓存状态、Store 元数据可见性和 Transfer Engine 数据搬运之间的边界。

要把这条链路讲清楚,需要同时看三层:

text
SGLang HiCache
  负责前缀缓存、GPU/主机/存储层状态和调度时机

Mooncake Store
  负责对象、切片、副本、租约、元数据和可读性判断

Mooncake Transfer Engine
  负责 Segment、注册内存、批量传输和传输方式选择

layer view

KV 缓存从引擎状态变成分布式资源,需要经过三层语义

HiCache 决定缓存什么时候写、什么时候读;Store 决定对象是否存在、是否可读;Transfer Engine 只负责搬运已知地址上的字节。

SGLang HiCache

推理引擎中的缓存策略

基数树节点GPU KV 池主机 KV 池页哈希预取回载

Mooncake Store

对象元数据与可见性

对象键切片副本租约PROCESSINGCOMPLETE

Mooncake Transfer Engine

远端内存传输

Segment偏移长度注册内存BatchID传输方式

这三层经常被一句“把 KV 缓存放到 Mooncake”混在一起。实际阅读源码时必须拆开,否则很容易把 Store 当作普通 KV 服务,或者把 Transfer Engine 当成完整缓存系统。

源码版本与三层边界

本文是 Mooncake / HiCache 系列的入口篇,基于 SGLang v0.5.14 的 HiCache 相关源码路径,以及 kvcache-ai/Mooncake 稳定 release tag v0.3.11.post1(commit e9c61075720039bcfc5fffd19f847608402be3d0)的 Store / Transfer Engine 源码路径阅读。下文重点放在可复核的源码路径、接口职责和系统边界。

HiCache 是 SGLang 推理引擎内部的分层缓存机制。它知道请求、前缀、基数树节点、token 索引、GPU KV 池和主机 KV 池,也知道调度器什么时候应该预取或回载。它的核心边界在 SGLang 代码里,SGLang v0.5.14 主要入口包括:

text
python/sglang/srt/mem_cache/hicache_storage.py
python/sglang/srt/mem_cache/unified_radix_cache.py
python/sglang/srt/mem_cache/hiradix_cache.py
python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py

Mooncake Store 是对象和副本的语义层。它不知道 SGLang 的基数树,也不决定某个请求是否能跳过预填充。它看到的是对象键、切片、副本、Segment、状态和租约。Mooncake 侧主要入口是:

text
mooncake-store/include/client_service.h
mooncake-store/src/client_service.cpp
mooncake-store/src/master_service.cpp
mooncake-store/include/replica.h
mooncake-store/include/types.h

Mooncake Transfer Engine 是数据搬运层。它不理解 KV 缓存,也不理解对象是否可读。它处理的是本地地址、远端 Segment、偏移、长度、读写方向、BatchID 和传输方式。主要入口是:

text
mooncake-transfer-engine/include/transfer_engine.h
mooncake-transfer-engine/include/transport/transport.h
mooncake-transfer-engine/src/transfer_engine_impl.cpp
mooncake-transfer-engine/src/multi_transport.cpp

本文关注 KV 缓存从推理引擎状态进入 Store 和传输层的系统边界;不覆盖完整基准测试和模型计算内核细节,也不会把 Mooncake 的长期愿景写成当前已经具备的能力。

一段 KV 缓存的生命周期

从 SGLang 角度看,一段可复用前缀会经历几种状态:

text
GPU KV 池
  -> 主机 KV 池
  -> HiCacheStorage 后端
  -> Mooncake Store 对象
  -> Segment 上的 Mooncake 副本
  -> Transfer Engine 传输链路

反向读取时顺序也不是直接从 Mooncake 回 GPU:

text
Mooncake Store 对象
  -> 零拷贝读取到主机 KV 池
  -> HiCache 回载
  -> GPU KV 池
  -> 注意力内核使用设备 KV 槽位

call path

一页远端 KV 从写出到读回的完整路径

Mooncake 只把数据读回主机内存池;要让注意力计算真正使用这些数据,还要由 HiCache 回载到 GPU。

GPU KV 池HiCacheMooncake StoreTransfer Engine主机 KV 池
  1. 1

    GPU KV 池

    HiCache

    write_backup(node)

    先把设备 KV 槽位中的数据备份到主机端 KV Cache 页

  2. 2

    HiCache

    Mooncake Store

    batch_set(page key, buffer pointer)

    HiCacheStorage 后端把 page key 和主机端缓冲区信息传给 Store

  3. 3

    Mooncake Store

    Transfer Engine

    写入对象切片

    Store 根据副本的 Segment、偏移和长度生成传输请求

  4. 4

    Mooncake Store

    主机 KV 池

    batch_get_into(...)

    远端对象读回时先落到主机 KV 缓存页

  5. 5

    主机 KV 池

    GPU KV 池

    load_back(best_match_node)

    HiCache 分配设备槽位后,前缀才真正回到 L1

这条路径说明两个事实。

第一,HiCache 的 L3 后端不直接替代 GPU 缓存。它只是让已经离开 GPU 的 KV 缓存仍有机会被找回。真正参与注意力计算的仍然是 GPU KV 槽位。

第二,Mooncake Store 存的不是“一个请求”,而是 SGLang 按页组织出的缓存对象。MHA 通常会拆成 K/V 两类对象,MLA 则使用另一套键和缓冲区组织方式。

flow

资源生命周期:一段 KV 从计算结果变成系统资源

这篇总览更关心生命周期和所有权边界,而不是某个函数调用是否漂亮。

  1. 1

    生成

    GPU KV 池

    预填充产生的 KV 先属于推理引擎的设备槽位。

    设备内存基数树节点请求上下文
  2. 2

    主机内存备份

    主机 KV 池

    HiCache 把可恢复的前缀备份到主机内存,并用引用计数保护其生命周期。

    host_valuehost_ref_counterPoolTransfer
  3. 3

    发布

    Store 元数据

    Store 把对象、切片和副本状态推进到可读边界。

    对象键PROCESSINGCOMPLETE
  4. 4

    搬运字节

    Transfer Engine

    Transfer Engine 只搬运已知地址上的字节,不判断 KV 语义。

    Segment偏移长度传输方式
  5. 5

    恢复

    主机内存 -> GPU

    读回后还要由 HiCache 回载,才重新成为注意力计算可以使用的资源。

    batch_get_into回载设备侧提交

为什么不能只看 Store 或只看 Transfer Engine

Store 解决的是“这份数据是否存在、在哪里、能不能读”。它需要维护:

  • 对象键到元数据的映射
  • 对象被拆成哪些切片
  • 每个副本所在的 Segment、偏移量和长度
  • 副本处于 PROCESSINGCOMPLETE 还是已被移除
  • Client 拿到的元数据租约何时失效

Transfer Engine 解决的是“知道远端地址以后,字节怎么过去”。它需要处理:

  • 本地内存是否已经注册
  • 远端 Segment 是否已经打开
  • 这批传输是 READ 还是 WRITE
  • 应该使用 TCP、RDMA、EFA、NVLink 还是其他传输方式
  • BatchID 的生命周期和传输状态

如果只看 Store,会知道对象语义,却不知道大块 KV 数据如何跨节点搬运;如果只看 Transfer Engine,会知道数据如何搬运,却不知道这些字节为什么已经可读。要理解完整链路,必须把两边接起来。

HiCache 把缓存问题变成状态机

普通前缀缓存里,基数树节点的 value 指向 GPU KV 索引:命中就复用,没有命中就重新计算。

HiCache 多了一层状态:

text
value             GPU KV 索引
host_value        主机 KV 索引
hash_value        L3 存储使用的 page key
backuped          是否已经有主机内存备份
backuped_storage  是否已经写到 L3
lock_ref          GPU 侧引用保护
host_ref_counter  主机侧引用保护

因此前缀匹配不再只是“命中了多少 token”,还要区分:

text
哪些 token 已经在 GPU
哪些 token 只在主机内存
哪些 token 可能在存储层
哪些 token 需要重新执行 prefill

这就是 HiCache 与 Mooncake Store 接起来的原因。HiCache 负责把前缀缓存分页、计算哈希并纳入调度;Mooncake Store 则让这些页能在分布式环境中查询、传输和复用。

一个实用读法

后续章节可以按这个顺序读:

  1. HiCache 总览和写路径,先理解 SGLang 为什么需要 L1/L2/L3。
  2. HiCache 读取路径,理解预取和回载怎样进入调度。
  3. Mooncake Store 对象模型,理解对象、切片、副本和租约。
  4. Store Put/Get,理解元数据控制面和数据面如何分离。
  5. Transfer Engine,理解 Segment、内存注册和 BatchTransfer。
  6. SGLang 到 Mooncake Store 串联篇,说明 page key 如何映射为 Store 对象键并关联零拷贝批量 API。
  7. 共享 Transfer Engine 和部署边界,理解哪些是性能优化,哪些属于缓存语义。

读完以后,应该能画出一段前缀 KV Cache 从 GPU 写到远端、再从远端读回 GPU 的完整路径,并知道每个阶段该去哪个源码文件里验证。

职责与边界

system boundary

KV 缓存资源的所有权分散在引擎、元数据和传输三层

每一层都有自己的所有权和失败语义。把它们混成一个缓存服务,会让调试结论过度外推。

推理引擎决定是否复用

SGLang / HiCache

前缀匹配GPU 槽位主机缓存页回载

Status

基数树节点状态阈值与配额检查

Store 管理可见性

Mooncake Store

对象元数据副本状态租约可选分组语义

Status

PROCESSINGCOMPLETEGetReplicaList

传输层负责搬运字节

Transfer Engine

Segment注册内存读 / 写BatchID

Status

传输状态打开内存段传输错误

本文结论边界

不能据此推断的内容

不宣称性能结果不等同生产验证不覆盖完整故障模型

Status

需要日志证据需要固定源码快照

结论:远程 KV 缓存是三层状态机,不是一个远端字典

理解 Mooncake / HiCache 的核心价值,不是记住某个 API 名字,而是能回答这几个边界问题:

  • HiCache 为什么不能直接把 GPU KV 张量存进 Mooncake Store?
  • Mooncake Store 的 Master 为什么不负责搬运大块数据?
  • PROCESSING 副本和 COMPLETE 副本的读取语义有什么区别?
  • Transfer Engine 为什么不需要知道对象键?
  • 一次远端 KV 命中为什么仍然需要回载到 GPU?

这些问题背后遵循同一条原则:推理引擎负责判断 KV Cache 是否值得复用,Store 负责判断对象是否可读,Transfer Engine 只负责在确定的地址之间传输数据。 把这三层混在一起,调试时就会把元数据命中、传输成功和 GPU 可复用误认为同一件事。