统一 Radix 缓存:为混合模型前缀缓存构建单一树结构
Blog Unified Radix Cache: One Tree for Hybrid Model Prefix Caching Prefix caching reuses KV when requests share the same token prefix. Under full attention, once the KV for a shared prefix is computed, it remains valid as more tokens are appended. A later request wit... Zhangheng Huang, Ke Bao, Yi Zhang, Jialin Ouyang, Sicheng Pan
LMSYS 团队提出 Unified Radix Cache,用单一 token 键控 radix 拓扑统一管理混合模型的 FULL、SWA 和 MAMBA 组件缓存,各组件独立执行路径、滑动窗口和检查点复用语义。
传统做法需为每种注意力组合实现独立缓存,本文以统一基树和组件钩子将复用规则解耦,SWE-bench上TTFT降低最高16.6%。
引言
前缀缓存会在多个请求共享相同 token 前缀时复用 KV。在 full attention 下,一旦共享前缀的 KV 被计算出来,随着更多 token 被追加,它仍然有效。之后带有相同前缀的请求可以复用这些 KV 条目,而不必在 prefill 期间重新计算它们。SGLang 通过一棵以 token 序列为键的基数树来跟踪这种映射。在 prefill 之前,这棵树会找到最长的可复用前缀,并将其 KV 位置返回给调度器。
混合模型打破了这一单一复用规则。一个请求可能同时组合 full attention KV、sliding window attention KV 和 recurrent states,而它们各自对复用有着不同的定义。Full attention KV 在整个匹配前缀范围内都可复用。Sliding window attention KV 只覆盖末尾窗口。Recurrent state 仅在精确的前缀检查点上有效。它们共享相同的 token 前缀,但 可复用边界并不相同。强行让它们全部采用单一边界,要么会丢弃有效的复用,要么会允许无效的复用。
这些复用规则在不同模型系列中以不同组合出现。将每一种组合都编码为一个专门的缓存类,会形成一个组合式的类矩阵,尤其是在加入诸如 HiCache 这类正交能力之后。早期的实现遵循了这种模式,在各个缓存变体之间重复了匹配、插入、锁定和淘汰逻辑。
统一基数缓存(Unified Radix Cache)通过将共享前缀身份与组件特定的复用有效性分离,解决了这一组合式设计难题。单一的以 token 为键的基数拓扑为每个前缀提供规范坐标,而全注意力 KV、滑动窗口注意力 KV 和 Mamba 检查点则作为组件挂载。HiCache 原生适配同一组件生命周期,将组件身份扩展至 GPU L1、Host L2 和外部 L3 层级。辅助池可以作为边车跟随组件,而无需定义新的复用边界。新的模型族可以组合这些能力,而无需引入另一棵缓存树。
要点
- 一棵树取代了缓存类矩阵。全注意力 KV、滑动窗口注意力 KV 和 Mamba 检查点共享同一棵基数拓扑,而各组件强制执行不同的复用语义。
- 钩子让树核心保持通用。组件控制匹配、拆分、插入、锁定和驱逐,因此新的混合组合无需新的树实现。
- HiCache 原生融入组件生命周期。组件与 sidecar 在跨越 GPU L1、Host L2 和外部 L3 层级时,保持相同的前缀标识。在多轮基准测试中,L3 在后续轮次将 DeepSeek-V4-Flash 的命中率维持在接近 98%,Inkling-Small 则维持在 96.8%。
- 会话活动引导淘汰。会话感知淘汰会优先保留活跃会话的缓存条目,但不会将其固定。在 SWE-bench 运行中,会话感知的 Unified Radix Cache 配置相比采用 LRU 的普通 HiRadixCache,TTFT 降低了 2.9% 至 16.6%。
- 实验性的 Rust 树核心降低了长前缀开销。在滑动窗口基准测试中,该原型在第 176 至 200 轮相比 Python 树,TTFT 最多降低了 42%。
一棵树,可组合的组件
混合模型将共享相同 token 前缀但遵循不同复用规则的可缓存值组合在一起。Unified Radix Cache 将共享前缀映射到一棵基数树拓扑,并将每条复用规则映射到一个 TreeComponent。UnifiedTreeCore 负责执行通用的匹配、拆分、插入、锁定和淘汰机制。UnifiedRadixCache 协调池操作,而每个组件只定义各自不同的语义。
FULL 组件始终存在。SGLang 为混合滑动窗口注意力添加 SWA 组件,为混合循环层添加 MAMBA 组件。例如,DeepSeek-V4 组合了 FULL 和 SWA,Kimi-K3 为其 KDA 循环状态组合了 FULL 和 MAMBA,而 Inkling 在同一棵树上组合了全部三个组件。新的模型家族可以复用已有的组件组合。如果它引入了一条当前集合无法表达的复用规则,SGLang 可以添加一个新的 TreeComponent,而无需再创建另一套树实现。
FULL 提供路径复用。它为匹配前缀中的每个 token 保留 KV,并保护对应的祖先路径。SWA 提供窗口复用。它要求一个连续的尾部窗口,而较旧的 SWA 槽位可能是空的墓碑,其基数节点仍保留在共享拓扑中。MAMBA 提供检查点复用。它要求在可复用前沿处有一个循环检查点,并在变更之前将共享状态复制到私有请求槽位中。这些组件对同一个候选边界应用不同的规则。
寻找安全的复用边界
在前缀匹配过程中,UnifiedTreeCore 沿着规范的 FULL 路径前进,并将每个访问到的节点视为候选边界。仅有 FULL 匹配是不够的。每个活跃组件都会创建一个验证器,只有当所有验证器都接受该候选时,可复用边界才会推进。拒绝并不会停止遍历,因为某个组件可能会接受更靠后的节点。在图 2 中,n1 和 n2 通过了所有验证器,而 n3 和 n4 至少未通过一个组件检查。遍历到达了 n4,但保留 n2 作为最深的安全结果。
遍历之后,核心会构建一个 MatchResult。随后组件终结器会准备所选值以供复用,包括当共享的 MAMBA checkpoint 变为某个请求私有时所需的复制。
贯穿树生命周期的组件钩子
同一套组件契约覆盖了树生命周期的其余部分:
| 生命周期 | 组件决定的内容 |
|---|---|
| 匹配 | create_match_validator 决定某个候选是否可复用。finalize_match_result_in_tree_core 和 finalize_match_result_in_cache 准备所选结果。 |
| 拆分 | redistribute_on_node_split 决定当 radix 节点被分割时组件数据如何移动。 |
| 插入 | update_component_on_insert_overlap 和 commit_insert_component_data 决定该组件拥有哪些池索引,以及新数据挂接到何处。 |
| 锁定 | acquire_component_lock 和 release_component_lock 保护一条路径、一个尾部窗口或一个检查点。 |
| 驱逐 | evict_device_start、evict_device_next_node 和 evict_device_end 选择设备候选。evict_component 移除组件数据,drive_host_eviction 回收主机资源。 |
这一契约让树核心保持通用,同时允许各组件保留不同的正确性规则。移除一个组件的载荷并不总是移除该基数节点。剩余的拓扑结构仍可锚定其他组件,而空的组件槽位可以作为墓碑保留,直到该组件被恢复或该节点变得不再必要。由于组件语义始终附着于同一个前缀身份,HiCache 可以将组件载荷跨内存层级扩展,而无需引入另一棵树。
跨内存层级的原生 HiCache
组件决定什么可以被复用。HiCache 决定可复用的载荷驻留在何处。统一基数缓存(Unified Radix Cache)在 GPU L1、主机 L2 和外部 L3 层级之间携带相同的组件身份,因此在层级之间移动数据不会改变其前缀身份或复用规则。组件描述所需的传输,HybridCacheController 执行物理 I/O。
组件、锚点与边车
并非每个物理池都需要自己的组件。锚点决定复用语义,或者提供其他池所遵循的页索引。边车存储单独的有效载荷,但复用其声明源池的索引。它随该源一起移动,而不对可复用边界进行投票,也不在基数拓扑中增加另一个槽位。
DeepSeek-V4让这一区分变得具体。FULL 覆盖逻辑前缀,而 SWA 仅覆盖其尾部窗口,因此两者都是组件。它们还使用独立的设备索引空间。在图 3 中归一化的六页示例里,分配器在运行时将 FULL 尾部槽位 F4, F5 映射到 SWA 槽位 S0, S1。C4 和 C128 压缩 KV 池、索引器缓冲区以及压缩器状态并不定义新的复用边界。它们注册为边车,其中三个池遵循 FULL,两个池遵循 SWA。
HiCache 多轮基准测试结果
多轮工作负载每一轮都会增长一个可复用的对话前缀。如果较低层级在 GPU 容量耗尽后仍保留该前缀,缓存命中率应保持较高,TTFT 的增长应更为缓慢。
我们在两个混合模型上比较了三种缓存配置:仅 GPU L1、GPU L1 加 Host L2,以及 GPU L1 加 Host L2 再加一个 500 GiB 的 Mooncake Store 分布式内存层作为 L3。DeepSeek-V4-Flash 在四块 H200 GPU 上使用 FULL 和 SWA,采用 TP4、48 个客户端、60 轮,每轮 4,096 个输入加 16 个输出 token。Inkling-Small 在八块 H200 GPU 上使用 FULL、SWA 和 MAMBA,采用 TP8、64 个客户端、30 轮,每轮 1,216 个输入加 64 个输出 token。
下面的命令概要中包含模型路径和 Mooncake 客户端配置的占位符。它们记录了此处使用的运行时和工作负载标志,但并非一个完整可复现的环境。
DeepSeek-V4 和 Inkling 的配置概要
独立运行每种缓存配置,并在启动下一个之前先停止其服务器。
DeepSeek-V4
export MODEL=/path/to/DeepSeek-V4-Flash-FP8
export SGLANG_ENABLE_UNIFIED_RADIX_TREE=1
COMMON="--trust-remote-code --model-path $MODEL --tp 4 --mem-fraction-static 0.9 \
--context-length 262144 --page-size 64 --max-running-requests 16 \
--host 0.0.0.0 --enable-cache-report --enable-metrics \
--enable-metrics-for-all-schedulers"
HICACHE="--enable-hierarchical-cache --hicache-ratio 2 --hicache-size 0 \
--hicache-mem-layout page_first --hicache-io-backend kernel \
--hicache-write-policy write_through \
--hicache-storage-prefetch-policy wait_complete"
sglang serve $COMMON --port 30001
sglang serve $COMMON $HICACHE --port 30000
sglang serve $COMMON $HICACHE --port 30000 \
--hicache-storage-backend mooncake \
--hicache-storage-backend-extra-config "$MOONCAKE_CLIENT_JSON"
PORT=30000
python3 benchmark/hicache/bench_multiturn.py \
--host 127.0.0.1 --port "$PORT" --model-path "$MODEL" \
--num-clients 48 --num-rounds 60 --request-length 4096 --output-length 16 \
--max-parallel 16 --request-rate 64 --disable-auto-run \
--disable-random-sample --enable-round-barrier --ready-queue-policy fifo \
--seed 20260626
Inkling
export MODEL=/path/to/inkling
export SGLANG_ENABLE_UNIFIED_RADIX_TREE=1
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
COMMON="--trust-remote-code --model-path $MODEL --tp 8 \
--mem-fraction-static 0.85 --context-length 262144 --page-size 64 \
--max-total-tokens 750080 --max-running-requests 16 --host 0.0.0.0 \
--enable-cache-report --enable-metrics --enable-metrics-for-all-schedulers \
--mamba-radix-cache-strategy extra_buffer --swa-full-tokens-ratio 0.1 \
--mamba-full-memory-ratio 0.1 --disable-prefill-cuda-graph"
HICACHE="--enable-hierarchical-cache --hicache-ratio 2 --hicache-size 0 \
--hicache-mem-layout page_first --hicache-io-backend kernel \
--hicache-write-policy write_through \
--hicache-storage-prefetch-policy wait_complete"
sglang serve $COMMON --port 30001
sglang serve $COMMON $HICACHE --port 30000
sglang serve $COMMON $HICACHE --port 30000 \
--hicache-storage-backend mooncake \
--hicache-storage-backend-extra-config "$MOONCAKE_CLIENT_JSON"
PORT=30000
python3 benchmark/hicache/bench_multiturn.py \
--host 127.0.0.1 --port "$PORT" --model-path "$MODEL" \
--num-clients 64 --num-rounds 30 --request-length 1216 --output-length 64 \
--max-parallel 16 --request-rate 64 --disable-auto-run \
--disable-random-sample --enable-round-barrier --ready-queue-policy fifo \
--seed 20260626 --log-file "inkling-${PORT}.jsonl" --tag inkling
图 4 按轮次报告了平均 TTFT 和提示词 token 缓存命中率。对于每一轮,命中率等于各请求中已缓存的 prefix token 之和除以这些请求完整提示词长度之和。在两种工作负载中,L1 最先丢失可复用的 prefix,L2 推迟了容量上限的到来,而 L3 在预热后仍保持高位,最终收于 96% 以上。
在 DeepSeek-V4-Flash 上,L3 将命中率维持在接近 98%,平均 TTFT 保持在 9 秒以下,并达到 145.5K 有效输入 token/s,相比之下 L1 为 9.4K,L1 加 L2 为 14.3K。在 Inkling-Small 上,L3 最终命中率为 96.8%、TTFT 为 1.23 秒,同时达到 67.1K 有效输入 token/s,相比之下 L1 为 15.5K,L1 加 L2 为 21.1K。
有效输入 token 吞吐量遵循 bench_multiturn.py:完整提示词长度之和除以实际耗时。它计入缓存命中的 prefix token,因此衡量的是 prefix 复用下的服务进展,而非原始 prefill 计算吞吐量。在这些运行中,L3 的增益主要来自在较小层级达到容量上限后仍能保持可复用 prefix 可用。
共享树上的会话感知驱逐
会话感知驱逐直接实现在 UnifiedRadixCache 中。该机制提供了一种普通 LRU 所不具备的复用信号。LRU 记录哪些缓存条目最近被访问过,但不记录哪些 prefix 属于活跃会话、并可能在其下一轮中被复用。在内存压力下,它可能驱逐某个活跃会话的 GPU KV,却保留无关条目。
应用程序为每个请求附加一个稳定的 session_id。请求成功完成后,Unified Radix Cache 会为该会话注册可复用区域。FULL 跟踪其前缀路径,SWA 跟踪其尾部窗口,MAMBA 跟踪其可复用前沿。所有会话仍然共享一个基数树拓扑,每一轮仍然提供其完整的提示词。
这些引用改变的是驱逐顺序,而非固定内存。FULL 根据候选是否被引用、其会话引用计数以及配置的基础驱逐优先级来排序候选。SWA 和 MAMBA 首先扫描各自可复用区域中未被引用的条目,然后在需要更多空间时回退到已被引用的条目。当前策略覆盖 GPU L1 和 Host L2,不覆盖外部 L3 层级。
当应用程序调用 /close_session 时,Unified Radix Cache 会移除该会话的引用,但不会立即删除其缓存条目。会话世代和有界的已关闭会话墓碑可防止在关闭或重新打开之后才完成的过期请求恢复已释放的引用。
SWE-bench 工作负载上的会话感知 HiCache
我们在 SWE-bench 智能体轨迹上,以 TP8 和 HiCache 对 DeepSeek-V4-Pro 与 Qwen3.5-397B-A17B 进行了评估。基线使用带 LRU 的普通 HiRadixCache。该对比启用了 Unified Radix Cache 和 --enable-session-radix-cache。由于这同时改变了缓存实现和淘汰策略,所观察到的差异不应被解读为对会话感知能力的孤立消融实验。基准记录提供了服务器标志和沙箱配置。
图 6 的顶行堆叠展示了设备缓存与主机缓存的命中率。在 batch size 128 时,DeepSeek-V4-Pro 的设备命中率从约 42% 提升至 51%。在 batch size 32 时,Qwen3.5-397B-A17B 从约 5% 提升至 34%。在 batch size 64 时,Qwen 的设备加主机总命中率从约 58% 提升至 67%。
底行报告了相应的 TTFT。相对于普通 HiRadixCache 基线,会话感知的 Unified Radix Cache 配置在 batch size 128 和 256 时,DeepSeek-V4-Pro 的 TTFT 分别降低了 11.0% 和 2.9%。Qwen3.5-397B-A17B 在 batch size 32 和 64 时,TTFT 分别降低了 13.5% 和 16.6%。
迈向 Rust 树核心
随着共享前缀不断增长,树遍历、锁记账、LRU 更新和驱逐扫描都会给调度器的关键路径增加工作量。UnifiedRadixCache 将这套树状态机与缓存编排分离,从而使树核心成为原生实现的天然目标。
实验性的 Rust 统一基数缓存是一个可选启用、仅限 L1 的原型。Rust 负责基数拓扑、按组件锁记账、侵入式 LRU 链表和驱逐遍历。Python 仍然是请求到 token 映射以及物理 KV 分配的唯一所有者。在修改树之后,Rust 返回延迟操作,由 Python 将其应用到池中。该原型支持 FULL、SWA 和 MAMBA,但不支持 HiCache。
我们在一个 200 轮的合成对话上,将该原型与 Python UnifiedRadixCache 进行比较。每一轮增加 100 个输入 token,并生成 100 个输出 token。两个后端使用相同的模型和服务器标志,在同一批 GPU 上顺序运行,并执行六次试验。工作负载涵盖 TP2 下 Qwen3-32B 的完整注意力、TP2 下 gpt-oss-20b 的 SWA,以及 TP4 下 Qwen3-Next-80B-A3B 的混合 SSM。复现脚本需要 Rust 扩展的 release 构建。
Rust 原型在 SWA 工作负载上录得最大幅度的降低。TTFT 在全部 200 轮中降低了 38%,在第 176 至 200 轮中降低了 42%。全注意力整体录得 TTFT 降低 10%,在最后 25 轮中降低 18%。混合 SSM 工作负载整体录得 TTFT 降低 5%,在最后 25 轮中降低 7%。
图 7 的底行从总 TTFT 中减去了由 CUDA event 计时的 GPU 预填充区间。该残差包括树簿记、调度、同步、采样、去分词、传输以及其他未插桩的工作。它不是直接的 CPU 计时器,完整的 Rust 与 Python 差异不能仅归因于基数树操作。混合 SSM 的结果说明了这一边界。其残差大幅下降,但更大的 GPU 前向限制了总 TTFT 中可见的变化。
这些测量仅适用于上述实验性原型。后续的 Rust UnifiedTreeCoreInterface RFC #32710 定义了目标归属边界。编排和池管理仍留在 Python 中,位于可替换的树核心之后。该 RFC 目前仅支持 FULL,且未公布性能结果。
未来工作
统一基数缓存建立了共享前缀标识,但系统中仍有三部分需要围绕它收敛:
- 完成可替换的 Rust 树核心。 路线图 #20415 跟踪这一迁移。剩余工作是将 Rust 核心扩展到 SWA、MAMBA 和 HiCache,同时将池分配和编排保留在 Python 中。
- 将 GPU L1 直接连接到外部 L3 层级。 直接 L3 模式将使 Host L2 成为可选的暂存层级,并将分布式内存暴露为更大的共享缓存。这需要协调的准入、预取、传输和驱逐,而不仅仅是另一个存储连接器。
- 在整个服务栈中协调智能体 KV 缓存。 智能体工作负载已经受益于每个引擎内的前缀缓存。路线图 #21846 将相同的缓存标识扩展到路由器、预填充和解码 worker 以及 HiCache,使这些层级能够为会话、子智能体和工具调用协调预取、降级和保留。
结论
混合模型没有单一通用的可复用边界,但它们不需要为每种注意力和循环状态组合都维护一棵单独的基数树。统一基数缓存保持一份规范化的 token 拓扑,而 FULL、SWA 和 MAMBA 组件各自执行自己的复用、锁定和驱逐语义。
这种共享标识的用途远不止前缀匹配。HiCache 将其保留跨内存层级,会话引用将其转化为保留信号,而实验性的 Rust 核心展示了树形簿记如何在不将池所有权移出 Python 的情况下演进。更长期的方向是让可复用的缓存状态对整个服务系统可见,而不仅仅是对本地分配器可见。
致谢
我们感谢阿里云 TairKVCache 团队共同主导了 Unified Radix Cache 与 HiCache 在各类混合模型上的集成,并验证了大规模生产部署。我们感谢 Thinking Machines Lab 团队在高负载下验证 Unified Radix Cache,并修复了各组件组合中的正确性问题。我们还感谢 Clank.world 团队在高并发生产部署中验证了 Gemma 4 SWA HiCache。
我们还感谢 Mingjun Zhang 在会话感知淘汰方面的工作及其验证。我们感谢蚂蚁集团 SCT 推理团队的 Tingwei Huang 帮助将 Hybrid HiCache 与 Mooncake 集成。我们感谢 Lianmin Zheng、Ishan Dhanani、Zhiqiang Xie、Chao Shi、Yanbo Yang、Shangming Cai、Hongjia Zhang 以及 SGLang 社区在架构评审、系统集成、基准测试和反馈方面的贡献。我们也感谢相关路线图 #20415 和 #21846 的所有贡献者。
来源:LMSYS:Blog(Chatbot Arena 团队) · lmsys.org