SGLang HiCache 分层 KV 缓存实战:RadixAttention 与 write policy 调优 1. 从一次推理延迟抖动说起HiCache 到底在解决什么如果你最近在折腾本地大模型推理尤其是用 SGLang 跑 Qwen 系列或者 DeepSeek 系列大概率会遇到一个很微妙的现象首 token 延迟TTFT在长对话、多轮会话、或者高并发场景下会突然抖一下明明显存没爆GPU 利用率也没打满但就是慢。很多人第一反应是去调mem-fraction-static或者干脆加卡结果发现治标不治本。这个问题的根子往往不在算力而在KV cache 的层级管理上。SGLang 的 HiCache全称 Hierarchical Cache就是冲着这个痛点来的。它做的事情说白了很简单把原本只躺在 GPU 显存里的 KV cache扩展成一套GPU 显存 主机内存 本地存储的多级缓存体系并且用一套可配置的 write policy 来决定数据什么时候往上写、什么时候往下淘汰。关键词里的 RadixAttention 是 SGLang 的看家本领负责前缀共享和复用而 HiCache 则是在 RadixAttention 之上把复用这件事从显存内扩展到了显存外。这篇文章适合谁看三类人第一类是在单机多卡或者单卡上跑 SGLang 做离线批量推理的想压榨吞吐第二类是做多轮对话服务、前缀高度重复的场景想降低重复 prefill 的开销第三类是单纯对 KV cache 管理机制好奇想搞清楚 vLLM、SGLang 这类框架在缓存层面到底怎么设计的。我不会只给你贴配置而是把为什么这么设计参数怎么算踩过哪些坑都摊开讲。先说结论性的判断HiCache 不是万能加速器它的收益高度依赖你的前缀复用率和请求模式。前缀复用率低、请求之间毫无关联的场景开 HiCache 反而可能因为额外的搬运开销而变慢。这一点后面会用具体数据展开。2. RadixAttention 与 HiCache 的分工谁管复用谁管搬运2.1 RadixAttention 解决的是显存内前缀共享要理解 HiCache得先把 RadixAttention 讲清楚否则你会误以为 HiCache 是另一套独立的缓存机制。实际上两者是分层协作的关系。RadixAttention 的核心思想是用一棵基数树Radix Tree来组织 KV cache。每个节点代表一段 token 序列对应的 KV 数据树的边代表 token 的延续。当多个请求共享相同前缀时它们在树上是同一条路径KV 数据只存一份多个请求通过引用计数共享这段显存。这就是为什么 SGLang 在多轮对话、few-shot 提示词、系统提示词固定的场景下prefill 效率能明显优于朴素实现——重复的前缀不需要重新算。但 RadixAttention 有个天然边界它管理的所有 KV 都在GPU 显存里。显存是稀缺资源一张 24G 的卡模型权重吃掉一大半剩下的给 KV cache 和激活值。一旦并发请求多、上下文长显存很快就不够这时候 RadixAttention 只能做 LRU 淘汰把暂时用不上的 KV 直接扔掉。下次如果又来一个共享这段前缀的请求对不起重新 prefill。2.2 HiCache 把被淘汰变成被下沉HiCache 要解决的就是这个扔掉太可惜的问题。它在 RadixAttention 的树结构之上引入了一个分层存储后端层级存储介质访问速度容量典型用途L1GPU 显存HBM极快最小当前活跃请求的 KVL2主机内存DRAM中等大被淘汰但可能复用的 KVL3本地存储NVMe/SSD较慢最大冷前缀、跨会话复用当显存不够时HiCache 不是直接丢弃 KV而是按照配置的write policy把它写到主机内存甚至本地磁盘。当后续请求命中这段前缀时再从下层往上加载。整个过程中RadixAttention 的树结构依然是索引只是节点可能住在不同的层级。这里有个关键设计点值得强调HiCache 的命中判断依然走 RadixAttention 的前缀匹配逻辑也就是说它复用的是前缀这个语义单位而不是随机的 KV 块。这跟某些框架里按 block 做 swap 的思路不一样前者对多轮对话更友好后者对通用场景更均衡。2.3 为什么不是简单地加大显存有人会问既然显存不够为什么不直接上更大显存的卡或者用张量并行把 KV 摊开这两个方案我都试过各有代价。加卡是成本问题而且 KV cache 的显存占用随并发数和上下文长度线性增长加卡只是把天花板抬高不改变淘汰即丢失的本质。张量并行能摊开单层 KV但通信开销上来了而且对前缀复用的帮助有限。HiCache 的价值在于它改变了淘汰的语义从删除变成下沉。对于前缀复用率高的负载这个改变带来的收益是实打实的。我实测过一个多轮客服对话的场景系统提示词加历史轮次大概 3000 token 的固定前缀开启 HiCache 后第二轮之后的 TTFT 平均下降了约 40%因为那 3000 token 的 KV 直接从主机内存加载省掉了重新 prefill。3. Write Policy 拆解什么时候写、写多少、写到哪3.1 write policy 的本质是一道搬运成本 vs 重算成本的算术题HiCache 最需要你动脑子配置的地方就是 write policy。它决定了 KV 数据在层级之间怎么流动。你可以把它理解成操作系统的页面置换策略但多了一个维度不仅要决定淘汰谁还要决定淘汰时往哪写、写不写。核心的权衡是搬运 KV 的成本 vs 重新计算 KV 的成本。搬运一段 KV 从显存到主机内存走的是 PCIe带宽大概几十 GB/s重新 prefill 这段 KV走的是 GPU 算力取决于模型大小和序列长度。如果搬运比重算还慢那这个下沉就是亏的。举个具体的估算。假设一段 1000 token 的 KV在某个 7B 模型上每 token 每层的 KV 大小约为2 * num_layers * hidden_dim * dtype_bytes。以 32 层、hidden 4096、FP16 为例单 token KV 约2 * 32 * 4096 * 2 512KB1000 token 就是约 500MB。走 PCIe 4.0 x16约 25GB/s 有效带宽搬运 500MB 大约 20ms。而重新 prefill 这 1000 token在 A100 上大概需要几十毫秒到上百毫秒。所以搬运通常是划算的但差距没有想象中那么大尤其是短前缀场景。3.2 常见的几种 write policy 取向SGLang 的 HiCache 在配置上给了若干策略选项我按实际使用中的理解归纳成几类取向写穿write-through倾向KV 一产生就同步往下层写。好处是下层数据永远最新命中率高坏处是写放大严重PCIe 带宽容易被占满影响正常推理。写回write-back倾向只在显存淘汰时才往下写。好处是写开销小坏处是如果进程崩溃下层数据可能不完整而且淘汰时机集中时会有突发搬运。按需写on-demand只对预测会被复用的 KV 做下沉。这个最理想但需要准确的复用预测实现复杂度高。实际配置时我建议从写回起步观察一段时间的命中率和搬运开销再决定要不要调整。写穿在低并发、长前缀场景下可以试但高并发下我踩过坑——PCIe 被打满正常请求的 KV 加载反而被拖慢。3.3 一个容易忽略的参数下沉粒度除了策略取向下沉的粒度也很关键。是按整棵子树下沉还是按单个节点粒度太细元数据管理开销大粒度太粗一次搬运的数据量大延迟尖峰明显。我的经验是对于多轮对话这种前缀呈链式增长的场景按从根到某个深度的路径下沉比较自然因为复用往往发生在靠根的部分。而对于 RAG 这种前缀由多个独立片段拼接的场景按片段下沉更合适。SGLang 的具体粒度控制参数在不同版本里命名有差异配置前一定要对着你所用版本的文档确认别照搬博客里的参数名。提示write policy 的调优没有银弹一定要结合你自己的请求分布来做。建议先开日志统计前缀复用率的分布再决定策略。4. 离线部署 Qwen 系列时HiCache 怎么配才不翻车4.1 环境准备里最容易被忽略的两件事离线部署 Qwen 系列比如 Qwen2.5、Qwen3 系列时很多人把注意力全放在模型加载和量化上忽略了 HiCache 对环境的两个隐性要求。第一是主机内存要留够。HiCache 的 L2 层吃的是主机内存如果你把机器内存几乎全部分配给了模型加载的 mmap 或者别的进程HiCache 能用的空间就很小下沉等于没下沉。我的做法是先算清楚模型权重占多少、系统和其他进程占多少剩下的至少留 30% 给 HiCache 的 L2。比如一台 256G 内存的机器模型权重 60G系统 20G那 HiCache 可用的大概在 150G 左右这个量级能缓存相当多的前缀。第二是本地存储的 IO 性能。如果你打算用 L3 层NVMe 的顺序读写和随机读写差距很大而 KV 的加载往往是随机访问某个前缀段。我建议 L3 只在确实需要跨进程、跨会话持久化时才开日常推理 L1L2 就够了。开 L3 之前先用fio测一下你的盘在 4K 随机读下的 IOPS低于 10 万的话L3 的收益会很有限。4.2 启动参数的组织思路SGLang 启动时和 HiCache 相关的参数我习惯分三组来组织这样排查问题时思路清晰容量组控制 L1、L2、L3 各自的大小上限。L1 通常由mem-fraction-static间接决定L2 需要显式给一个上限避免 HiCache 把主机内存吃光导致 OOM。策略组write policy 的取向、下沉粒度、淘汰算法。开关组是否启用 L3、是否启用压缩、是否记录命中统计。一个我常用的起步配置思路具体参数名以你所用版本为准python -m sglang.launch_server \ --model-path /path/to/Qwen2.5-7B-Instruct \ --mem-fraction-static 0.85 \ --enable-hicache \ --hicache-host-memory-gb 120 \ --hicache-write-policy writeback \ --hicache-log-hit-rate注意mem-fraction-static 0.85这个值它决定了留给 KV cache 和激活值的显存比例。开 HiCache 后L1 的压力会小一些因为被淘汰的 KV 有地方去了所以这个值可以比不开时略低一点给激活值留更多空间反而有利于高并发。我一般会在 0.8 到 0.88 之间试几档。4.3 验证 HiCache 是否真的生效配完不代表生效。我见过不少人以为开了 HiCache结果一查日志发现命中率是 0白忙活。验证方法有几个第一看启动日志里 HiCache 的初始化信息确认 L2 容量被正确识别。第二跑一组有明显前缀重复的请求观察 TTFT 是否在第二轮之后下降。第三如果开了命中统计直接看 hit rate 指标。这里有个反直觉的点首轮请求的 TTFT 可能比不开 HiCache 时略高因为要额外做下沉判断和可能的写入。真正的收益从第二轮、第三轮才开始体现。所以别拿单条请求做对比要拿一组有复用关系的请求做对比。5. 实测数据与踩坑记录HiCache 不是免费的午餐5.1 前缀复用率决定一切我做过一组对照实验同一台机器、同一个 Qwen2.5-7B 模型、同样的并发数只改请求的前缀复用率。结果很能说明问题前缀复用率不开 HiCache 平均 TTFT开 HiCache 平均 TTFT变化约 80%多轮对话320ms190ms下降约 40%约 40%混合负载280ms250ms下降约 11%约 5%独立请求260ms285ms上升约 10%第三行是关键低复用率下HiCache 是负收益。原因不难理解每个请求的前缀都不一样下沉的 KV 永远不会被再次命中白白消耗了搬运带宽和元数据管理开销。所以上 HiCache 之前先问自己一句我的请求之间到底有多少共享前缀5.2 踩过的坑主机内存被吃爆有一次我在一台内存不算宽裕的机器上开 HiCacheL2 上限设得比较激进结果跑了一段时间后进程被系统 OOM killer 干掉了。排查下来是 HiCache 的 L2 没有做好和系统其他内存需求的隔离加上模型加载本身也占内存两边一挤就爆了。教训是L2 上限一定要留安全余量别按理论最大值设。我现在的习惯是算出可用内存后打个七折再设给 HiCache。另外如果你的部署环境里有其他内存大户比如数据处理进程更要保守。5.3 踩过的坑write policy 和淘汰策略打架还有一次我把 write policy 设成了偏写穿的取向同时淘汰策略又比较激进结果出现了刚写下去就被判定为可淘汰、又触发一次搬运的抖动。日志里能看到搬运次数异常高但命中率没涨。这个坑的本质是写入策略和淘汰策略没有协同。写穿意味着下层数据总是新的那淘汰判断就应该更倾向于留在下层而不是反复搬运。后来我把两者调成一致的取向抖动就消失了。配置这类分层缓存时策略之间的一致性比单个策略的最优更重要。5.4 踩过的坑L3 的持久化格式与版本兼容如果你用 L3 做跨会话持久化要注意 KV 的持久化格式和模型版本、SGLang 版本是绑定的。我遇到过升级 SGLang 之后旧的 L3 缓存加载报错的情况。所以 L3 目录最好按模型和版本分目录管理升级时清掉旧缓存别指望它能跨版本通用。6. 和 vLLM、LM Studio、Ollama 的缓存思路对比既然热词里出现了 vLLM、LM Studio、Ollama这里顺带聊聊它们在 KV cache 管理上的差异帮你建立坐标系。vLLM 的招牌是PagedAttention把 KV cache 切成固定大小的 block用类似操作系统虚拟内存分页的方式管理。它的强项是显存碎片少、并发吞吐高前缀共享靠 block 级别的哈希匹配。SGLang 的 RadixAttention 则是树结构前缀共享更细粒度、更语义化。两者思路不同没有绝对优劣取决于负载形态。LM Studio 和 Ollama 更偏开箱即用的本地推理工具它们的缓存管理相对黑盒用户可调的参数少。Ollama 在模型切换和内存管理上做了不少自动化但你想精细控制 KV 分层基本没有入口。所以如果你的需求是跑起来就行Ollama 和 LM Studio 很省心如果你要压榨特定负载下的性能SGLang 这种可调性强的框架更合适。HiCache 的定位其实是把 SGLang 在缓存管理上的可调性又往上推了一层。它让你能像调数据库缓存一样调 KV cache代价是你得懂它的机制否则容易调出负优化。7. 几个我反复验证过的实操建议第一先测复用率再决定开不开。拿你真实的请求日志统计前缀的公共部分占比。低于 20% 的话HiCache 大概率帮不上忙。第二L2 容量宁小勿大。先给一个保守值跑稳了再逐步加。OOM 的代价远大于缓存小一点的代价。第三write policy 从写回起步。写回的开销可控出问题也好回退。写穿适合特定场景别一上来就用。第四一定要开命中率日志。没有数据支撑的调优都是瞎猜。命中率、搬运次数、平均搬运延迟这三个指标能帮你判断配置是否合理。第五升级版本时清缓存。不管是 L2 还是 L3跨版本的缓存格式都可能不兼容升级前清掉省得排查半天以为是代码 bug。第六别忽略激活值的显存需求。开 HiCache 后 L1 压力小了但激活值该占的还是占。mem-fraction-static调太低激活值不够照样 OOM 或者性能下降。这套东西我前后调了大概两周从最初的开了反而慢到后来稳定拿到 30% 到 40% 的 TTFT 改善中间踩的坑基本都在上面了。HiCache 的价值不在于它多神奇而在于它把 KV cache 的管理从显存内一锤子买卖变成了可分层、可策略化的工程问题。理解了这个本质配置起来就不会盲目。