SGLang HiCache 总览:把 GPU KV 缓存扩成三级缓存
从 GPU KV 池、主机内存池、存储后端和基数树节点的状态变化出发,理解 HiCache L1/L2/L3 的整体结构。
HiCache 要解决的问题不是“多加一个远端 KV 存储”,而是把 SGLang 运行时里的 KV Cache 变成三级资源:
L1:GPU KV 池
L2:主机 KV 池
L3:存储后端,例如 Mooncake、3FS、文件系统或 NIXL
本文基于 SGLang v0.5.14。读源码时建议先看这几个文件:
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 池
注意力计算直接使用
L2:主机 KV 池
GPU 与存储后端之间的结构化缓冲层
L3: HiCacheStorage
远端或本地持久化后端
存储后端实现
只接入 L3,不接管调度器
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 是否有主机内存备份,以及是否已经写入存储层。
核心字段可以这样理解:
value 这段前缀在 GPU KV 池中的 token 索引
host_value 这段前缀在主机 KV 池中的 token 索引
hash_value 这段前缀对应的 page key
backuped 是否已经完成 GPU 到主机内存的备份
backuped_storage 是否已经完成主机内存到存储层的备份
lock_ref GPU 侧引用保护,避免异步回载或驱逐时节点被删除
host_ref_counter 主机侧引用保护,避免存储预取或写入时主机槽位被回收
因此一次匹配不再只有“命中多少 token”,而是拆成:
GPU 命中: 已经在 GPU,可直接复用
主机内存命中:GPU 中没有,但可以从主机内存回载
存储层命中: 主机内存中没有,但可以尝试从 L3 预取
未命中: 需要重新做预填充
这也是 HiCache 难读的原因:它把前缀树从纯索引结构变成了一套多级缓存状态机。
主机内存池是真正的 L2 KV 存储
主机内存池不是 Python 字典,也不是普通字节缓存。它和 GPU KV 池一样,是一块结构化内存,有自己的页大小、布局、分配器和缓冲区元数据。
常见布局包括:
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 存储后端。
HiCacheStorage 在 v0.5.14 里有两组接口:
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 描述每个内存池的主机与设备索引、键、命中策略和依赖关系。
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:
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 内存配额
- 当前请求是否值得等待缓存
- 额外内存池是否也命中
核心入口包括:
prefetch_from_storage(...)
-> 分配主机内存索引
-> cache_controller.prefetch(...)
-> HiCacheStorage.batch_get_v1/v2(...)
load_back(...)
-> 分配 GPU 索引
-> cache_controller.load(...)
-> 提交设备索引
因此,HiCache 是一层能够感知调度状态的缓存,而不是透明的 KV 存储。
和 Mooncake 的连接点
当参数指定:
--hicache-storage-backend mooncake
StorageBackendFactory 会创建 Mooncake 后端。该后端实现 HiCacheStorage,并把 SGLang 的主机 KV 缓冲区注册给 Mooncake Store。之后:
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 后端就不再像一个凭空插入的黑盒。