SGLang HiCache 总览:把 GPU KV 缓存扩成三级缓存

从 GPU KV 池、主机内存池、存储后端和基数树节点的状态变化出发,理解 HiCache L1/L2/L3 的整体结构。

Code walkthroughsSGLang runtimeLLM servingKV cacheHiCacheSGLang

HiCache 要解决的问题不是“多加一个远端 KV 存储”,而是把 SGLang 运行时里的 KV Cache 变成三级资源:

text
L1:GPU KV 池
L2:主机 KV 池
L3:存储后端,例如 Mooncake、3FS、文件系统或 NIXL

本文基于 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/pool_host.py
python/sglang/srt/mem_cache/storage/backend_factory.py

layer view

HiCache 把前缀 KV 缓存拆成 L1/L2/L3 三层资源

L1 可直接供注意力计算使用;L2 是结构化的主机 KV 内存;L3 通过 HiCacheStorage 接入 Mooncake、3FS、NIXL 或文件后端。

L1:GPU KV 池

注意力计算直接使用

TokenToKVPoolnode.value设备索引淘汰 / lock_ref

L2:主机 KV 池

GPU 与存储后端之间的结构化缓冲层

host_value页布局host_ref_counterD->H / H->D

L3: HiCacheStorage

远端或本地持久化后端

hash_valuebatch_existsbatch_getbatch_setPoolTransfer

存储后端实现

只接入 L3,不接管调度器

Mooncake Store3FSNIXLfile

L1 不是请求表,而是 KV 池

SGLang 推理引擎中,和 KV 缓存相关的结构至少有两类。

ReqToTokenPool 记录请求到 token 槽位的映射。它回答的是:某个请求的第几个 token 对应哪个 token 索引。

TokenToKVPoolAllocator 和具体 KV 池记录真实 K/V 张量的位置。它回答的是:token 索引所对应的 K/V 位于 GPU 内存的哪一段。

基数树缓存中的前缀节点通常保存 GPU token 索引,也就是节点的 value。一次前缀命中,本质上是基数树找到一段 token 前缀,并返回可以直接交给注意力后端的 GPU KV 槽位。

HiCache 的第一步不是推翻这套机制,而是在它之上补充主机内存和存储层状态。

HiCache 为 Radix 树节点补充位置状态

在普通前缀缓存中,节点更像“前缀到 GPU 索引的映射”。在 HiCache 中,节点还要记录这段前缀 KV 是否有主机内存备份,以及是否已经写入存储层。

核心字段可以这样理解:

text
value             这段前缀在 GPU KV 池中的 token 索引
host_value        这段前缀在主机 KV 池中的 token 索引
hash_value        这段前缀对应的 page key
backuped          是否已经完成 GPU 到主机内存的备份
backuped_storage  是否已经完成主机内存到存储层的备份
lock_ref          GPU 侧引用保护,避免异步回载或驱逐时节点被删除
host_ref_counter  主机侧引用保护,避免存储预取或写入时主机槽位被回收

因此一次匹配不再只有“命中多少 token”,而是拆成:

text
GPU 命中:     已经在 GPU,可直接复用
主机内存命中:GPU 中没有,但可以从主机内存回载
存储层命中:  主机内存中没有,但可以尝试从 L3 预取
未命中:      需要重新做预填充

这也是 HiCache 难读的原因:它把前缀树从纯索引结构变成了一套多级缓存状态机。

主机内存池是真正的 L2 KV 存储

主机内存池不是 Python 字典,也不是普通字节缓存。它和 GPU KV 池一样,是一块结构化内存,有自己的页大小、布局、分配器和缓冲区元数据。

常见布局包括:

text
layer_first
page_first
page_first_direct
page_head

这些布局决定主机 KV 在内存中如何排列,也决定存储后端能否按页零拷贝写入。例如,Mooncake 后端会调用主机内存池的 get_page_buffer_meta(),取得每个缓存页对应的指针和长度,再交给 Mooncake Store 的零拷贝批量接口。

所以主机内存池是正式的 L2,而不是临时中转变量。它承担三项职责:

  • 接收 GPU KV 的 D->H 备份。
  • 作为从主机内存回载到设备的来源。
  • 作为 L3 存储读写使用的零拷贝缓冲区。

HiCacheStorage 是 L3 的统一接口

SGLang 没有在基数树缓存中直接调用 Mooncake API,而是通过 HiCacheStorage 抽象 L3 存储后端。

HiCacheStoragev0.5.14 里有两组接口:

text
v1:
  batch_exists(keys)
  batch_get_v1(keys, host_indices)
  batch_set_v1(keys, host_indices)

v2:
  batch_exists_v2(keys, pool_transfers)
  batch_get_v2(transfers)
  batch_set_v2(transfers)

v1 面向普通 KV 池:一组 page key 对应一组主机端 KV Cache 页索引。

v2 面向混合缓存:除了 KV 以外,还可能有 Mamba 状态、SWA 状态、DeepSeek V4 辅助内存池和草稿模型 KV 等额外数据。v2 用 PoolTransfer 描述每个内存池的主机与设备索引、键、命中策略和依赖关系。

text
PoolTransfer
  name
  host_indices
  device_indices
  keys
  hit_policy
  nodes_to_load
  indices_from_pool

这样 HiCache 不需要了解 Mooncake、NIXL、3FS 每个后端的内部实现,只需围绕 page key 和主机内存池索引组织读写。

写策略决定何时进入 L2/L3

HiCache 的写入不是“生成 KV 后立刻全量写远端”。常见路径有两类。

写穿路径更主动:命中次数达到阈值,或者策略要求写穿时,节点会先做 D->H 备份;如果启用了存储层,再继续写入 L3。

回写路径更像一种淘汰保护:GPU 空间不足时,先把尚未备份的节点写入主机内存,再从 GPU 中驱逐。这样,被驱逐的前缀不会直接消失,而只是从 L1 降到 L2。

核心入口在 unified_radix_cache.py

text
write_backup(node)
  -> cache_controller.write(...)
  -> commit_hicache_transfer(...)
  -> node.backuped = true

write_backup_storage(node)
  -> cache_controller.write_storage(...)
  -> HiCacheStorage.batch_set_v1/v2(...)

读策略决定何时等待缓存

请求进入调度器后,HiCache 会先在本地树中查找前缀。GPU 命中的部分可以直接复用;主机内存命中的部分需要回载;存储层可能命中的部分需要预取到主机内存。

读路径的关键是:HiCache 不是只要 L3 有数据就阻塞等待。它会考虑:

  • 前缀是否按页对齐
  • 预取长度是否超过阈值
  • 主机内存池是否有可用空间
  • 回载量是否超过 GPU 内存配额
  • 当前请求是否值得等待缓存
  • 额外内存池是否也命中

核心入口包括:

text
prefetch_from_storage(...)
  -> 分配主机内存索引
  -> cache_controller.prefetch(...)
  -> HiCacheStorage.batch_get_v1/v2(...)

load_back(...)
  -> 分配 GPU 索引
  -> cache_controller.load(...)
  -> 提交设备索引

因此,HiCache 是一层能够感知调度状态的缓存,而不是透明的 KV 存储。

和 Mooncake 的连接点

当参数指定:

text
--hicache-storage-backend mooncake

StorageBackendFactory 会创建 Mooncake 后端。该后端实现 HiCacheStorage,并把 SGLang 的主机 KV 缓冲区注册给 Mooncake Store。之后:

text
HiCache page key
  -> Mooncake 对象键

主机端 KV 缓冲区指针
  -> Mooncake 零拷贝缓冲区指针

batch_set_v1/v2
  -> batch_put_from / batch_put_from_multi_buffers

batch_get_v1/v2
  -> batch_get_into / batch_get_into_multi_buffers

Mooncake 只接住 L3 这一段。前缀树、请求调度和 GPU 槽位管理仍然由 SGLang 负责。

把 HiCache 看作推理系统中的状态机

HiCache 难读,是因为它不是一个独立 KV 存储,而是嵌在 SGLang 推理引擎中的状态机。基数树节点同时保存 GPU、主机内存和存储三层位置:value 指向 GPU token 索引,host_value 指向主机内存池索引,hash_value 保存 L3 使用的 page key

这也解释了为什么 L3 后端读写的是主机内存池,而不是 GPU 内存池。存储层负责把缓存页写入或读出主机缓冲区;从主机内存到 GPU 的回载仍由 HiCache 和缓存控制器处理。存储层命中只说明“远端可能有数据”,不代表“注意力计算已经能够复用”。

阅读代码时可以沿着一个缓存页追踪:先在 GPU 上生成 KV,必要时备份到主机内存,再通过 batch_set 写入存储层;下一次请求先查询基数树,再通过 batch_exists 和预取把数据读回主机内存,最后回载到 GPU。把这条路径连起来,Mooncake 后端就不再像一个凭空插入的黑盒。