
1. 大模型推理的显存瓶颈与 HiCache 的破局思路做过大模型推理部署的朋友应该都有体会模型权重加载完之后真正卡脖子的往往不是算力而是 KV cache 的显存占用。尤其是长上下文、高并发场景下KV cache 能吃掉比模型权重还多的显存。SGLang 这套推理框架之所以在圈子里口碑不错很大程度上就是靠 RadixAttention 这套前缀缓存机制把多轮对话、few-shot 提示词里重复的 KV 复用起来省下大量重复计算。但问题来了RadixAttention 的缓存是放在显存里的容量有限。一旦请求量上来或者上下文拉得很长缓存就得频繁淘汰命中率断崖式下跌。HiCache 就是在这个背景下出现的——它把 KV cache 做成了一套分层存储体系显存放热数据内存甚至更下一级的存储放冷数据通过一套 write policy 决定什么时候把数据从显存刷到下层、什么时候再拉回来。说白了HiCache 解决的核心问题是让 KV cache 的容量不再被单张卡的显存死死锁住同时尽量不牺牲命中率和延迟。这套东西适合谁看如果你在用 SGLang 做离线批量推理、在线服务部署或者正在折腾 Qwen 系列模型的本地化落地那 HiCache 的配置和调优就是你绕不开的一环。哪怕你平时用的是 vLLM、Ollama、LM Studio 这类工具理解 HiCache 的分层缓存思路对你判断推理框架的缓存策略也有直接帮助。我下面会从设计思路、核心机制、实操配置、问题排查几个角度把 HiCache 拆开讲清楚。内容会结合 SGLang 的实际使用场景尽量给到能直接抄作业的参数和步骤。2. HiCache 的整体设计与分层缓存逻辑拆解2.1 为什么单层显存缓存撑不住长上下文场景先算一笔账。以 Qwen 系列 7B 级别的模型为例假设 FP16 精度KV cache 每个 token 的占用大致可以用这个公式估算每 token KV 占用 2 (K和V) × num_layers × num_kv_heads × head_dim × dtype_bytes拿一个 28 层、GQA 8 个 KV head、head_dim 128 的模型来算FP16 下每个 token 大约占 2 × 28 × 8 × 128 × 2 114688 字节差不多 112 KB。听起来不多但一个 32K 上下文的请求就是 3.5 GB 左右。如果并发 16 路光 KV cache 就要 56 GB这还没算模型权重和激活值。单张 80G 的卡模型权重占掉十几 G剩下的显存根本扛不住这种并发。RadixAttention 的前缀复用能缓解一部分但它的前提是请求之间有共享前缀。真实业务里多轮对话确实有共享但不同用户、不同任务之间的前缀差异很大缓存命中率不可能一直很高。一旦缓存满了SGLang 就得按 LRU 之类的策略淘汰被淘汰的 KV 直接丢掉下次用到还得重算。这就是单层显存缓存的死穴容量固定、淘汰即丢失、无法跨请求持久化。HiCache 的思路很直接既然显存贵且小那就把不常访问的 KV 挪到更便宜、更大的存储层去。显存当 L1主机内存当 L2需要的时候再按需换入。这样缓存的有效容量就从显存大小扩展到了内存大小命中率自然上去了。2.2 分层存储的层级划分与数据流向HiCache 的分层结构可以理解成三级层级存储介质访问速度容量典型用途L1GPU 显存极快受显存限制当前活跃请求的 KVL2主机内存较快受内存限制近期访问过的 KVL3可选本地磁盘/远端存储较慢很大冷数据、跨进程复用数据流向是这样的新计算的 KV 先落在 L1当 L1 压力大时按 write policy 把一部分 KV 下沉到 L2如果 L2 也满了再考虑下沉到 L3 或者直接淘汰。反过来当某个请求需要用到已经被下沉的 KV 时HiCache 会把它从 L2/L3 拉回 L1。这里有个关键点KV 的换入换出不是免费的。显存和内存之间的拷贝要走 PCIe带宽有限。所以 HiCache 的设计目标不是把所有 KV 都存下来而是用最小的搬运代价换取最高的命中率。这就引出了 write policy 的设计。2.3 write policy 的取舍什么时候把 KV 刷下去write policy 决定了 KV 从 L1 下沉到 L2 的时机和选择。常见的策略有几种写穿write-throughKV 一算出来就同时写 L1 和 L2。好处是 L2 永远有最新数据换入时不用回算坏处是每次计算都要多一次内存拷贝带宽压力大。写回write-back只在 L1 淘汰时才写到 L2。好处是平时没有额外开销坏处是淘汰瞬间可能产生拷贝峰值而且如果进程崩了L2 的数据可能不完整。按需下沉结合访问频率只把看起来不会再马上用到的 KV 下沉。这个需要预测实现复杂但搬运开销最小。SGLang 的 HiCache 在实际实现里更偏向写回加一定的预取策略。为什么因为推理场景下KV 一旦算完短时间内大概率还会被同一请求继续用到自回归生成这时候把它刷下去纯属浪费带宽。只有当请求结束或者缓存压力上来时才值得下沉。这个取舍逻辑和 CPU 缓存里的 write-back 是一个道理。提示write policy 的选择直接影响 PCIe 带宽占用和缓存命中率配置时不要只看命中率一个指标还要盯着显存到内存的拷贝量。3. HiCache 核心机制与关键参数实操解析3.1 RadixAttention 与 HiCache 的协同关系要理解 HiCache得先搞清楚它和 RadixAttention 是怎么配合的。RadixAttention 负责的是前缀匹配——把请求的 token 序列组织成一棵 radix tree相同前缀的请求共享同一段 KV。HiCache 负责的是这段 KV 存在哪一层。两者结合后radix tree 的每个节点除了记录 token 范围和 KV 的显存地址还要记录这份 KV 当前在 L1 还是 L2。当一个新的请求命中某个前缀节点时如果该节点的 KV 在 L1直接用如果在 L2就得先触发一次换入把 KV 从内存搬回显存再继续。这个协同带来的一个实际影响是前缀命中的收益取决于被命中 KV 所在的层级。命中 L1 的收益最高命中 L2 需要付出搬运成本命中 L3 成本更高。所以调优 HiCache 的时候不能只看命中率还要看命中层级分布。一个 90% 命中但大部分命中在 L2 的配置实际延迟可能比 70% 命中但大部分在 L1 的配置还差。3.2 关键参数与配置项说明在 SGLang 里启用和调优 HiCache涉及几个核心参数。下面这些是基于常见实践的整理具体名称以你使用的 SGLang 版本为准参数作用调优建议分层缓存开关是否启用 HiCache长上下文、高并发场景开启L1 显存配额分配给 KV cache 的显存上限留足激活值和临时缓冲一般不超过总显存的 70%L2 内存配额主机内存中用于 KV 的上限根据机器内存留出系统和其它进程余量write policy下沉策略优先写回配合预取换入阈值触发 L2 到 L1 换入的条件结合命中层级分布调整预取开关是否提前换入可能用到的 KV高并发下开启低延迟场景谨慎配置的时候有个经验L1 配额不要贪大。很多人觉得显存越大越好把 90% 显存都划给 KV cache结果激活值和临时张量没地方放反而触发 OOM 或者频繁的显存碎片整理。我一般建议 L1 配额控制在总显存的 60% 到 70%剩下的留给模型权重之外的运行时开销。3.3 换入换出的开销估算换入换出的开销主要来自 PCIe 拷贝。以 PCIe 4.0 x16 为例理论带宽约 32 GB/s实际有效带宽打个七折大概 22 GB/s。假设一次换入 1 GB 的 KV耗时约 45 毫秒。这个延迟对于在线服务来说不算小尤其是首 token 延迟敏感的场景。所以 HiCache 调优的一个核心原则是尽量减少换入换出的次数和单次搬运量。具体做法包括提高 L1 命中率让大部分请求在 L1 就解决预取要准避免预取了但没用上的无效搬运批量换入把多次小拷贝合并成一次大拷贝摊薄开销。这里可以给一个粗略的判断标准如果你的服务 P99 首 token 延迟要求在 200 毫秒以内那单次换入的数据量最好控制在 2 GB 以内否则光搬运就吃掉大半预算。4. 从零配置 HiCache 的完整实操流程4.1 环境准备与版本确认动手之前先把环境理清楚。SGLang 的 HiCache 相关功能在不同版本里成熟度不一样建议用较新的稳定版本。确认版本的方式python -c import sglang; print(sglang.__version__)同时确认你的 GPU 驱动、CUDA 版本和 PyTorch 版本匹配。HiCache 涉及显存和内存之间的数据搬运对驱动稳定性有一定要求驱动太老可能遇到拷贝异常。内存方面L2 的容量取决于主机内存。假设你要给 KV cache 留 128 GB 的内存空间那机器物理内存最好在 256 GB 以上留足余量给操作系统、模型加载和其它进程。别把内存压得太满否则触发 swap 之后性能会断崖式下跌比不用 HiCache 还惨。4.2 启动参数配置与分层缓存开启启动 SGLang 服务时通过命令行参数开启分层缓存。下面是一个参考配置python -m sglang.launch_server \ --model-path /path/to/your/model \ --host 0.0.0.0 \ --port 30000 \ --mem-fraction-static 0.7 \ --max-total-tokens 65536 \ --enable-hicache \ --hicache-mem-pool-size 128GB \ --hicache-write-policy write_back几个参数的解释--mem-fraction-static 0.7静态显存占比控制 L1 配额。0.7 是个比较稳的起点。--max-total-tokensKV cache 能容纳的 token 总数上限结合你的上下文长度和并发数估算。--enable-hicache开启分层缓存。--hicache-mem-pool-sizeL2 内存池大小。--hicache-write-policy下沉策略先用 write_back。启动之后观察日志里有没有 HiCache 初始化的相关信息确认内存池分配成功。如果日志报内存分配失败先把hicache-mem-pool-size调小再试。4.3 压测验证与命中率观测配置完不能拍脑袋说好了得压测。用 SGLang 自带的 benchmark 工具或者自己写脚本构造有共享前缀的请求序列模拟真实的多轮对话场景。python -m sglang.bench_serving \ --backend sglang \ --host 127.0.0.1 \ --port 30000 \ --num-prompts 500 \ --request-rate 20 \ --shared-prefix-len 2048压测过程中重点看几个指标缓存命中率整体命中率以及 L1/L2 各自的命中占比。换入换出量单位时间内从内存搬到显存的 KV 数据量。首 token 延迟TTFTP50 和 P99 都要看。吞吐每秒处理的 token 数或请求数。如果发现命中率很高但延迟没降下来大概率是命中集中在 L2搬运开销吃掉了收益。这时候可以考虑调大 L1 配额或者优化预取策略。4.4 离线批量推理场景的适配HiCache 在离线批量推理里其实更有价值。离线场景对单次延迟不敏感但对总吞吐和显存利用率敏感。批量任务里往往有大量相似的前缀比如同一套系统提示词、同一批 few-shot 示例HiCache 能把这些共享前缀的 KV 持久化在内存里跨批次复用。离线部署 Qwen 系列模型做批量任务时我的做法是把 L1 配额适当调小比如 0.5把 L2 内存池调大让更多 KV 沉在内存里。因为离线场景不怕搬运延迟怕的是重算。这样配置下来整体吞吐能比纯显存缓存高出一截尤其是任务之间有大量共享前缀的时候。注意离线场景下如果批次之间前缀差异极大HiCache 的收益会明显缩水这时候不如把内存留给别的用途。5. 常见问题排查与避坑经验实录5.1 命中率上不去怎么办命中率低是最常见的问题。排查顺序建议这样走先确认请求之间是否真的有共享前缀。如果每个请求的提示词都完全不同那 RadixAttention 本身就没什么可复用的HiCache 自然也帮不上忙。这种情况不是配置问题是场景不匹配。检查 L1 配额是否太小。L1 太小会导致 KV 频繁下沉刚沉下去又被换回来命中层级全在 L2。看 write policy 是否过于激进。如果用了写穿策略KV 一算完就下沉L1 里留不住热数据命中率反而低。确认 radix tree 的匹配粒度。有些实现里前缀匹配是按块block对齐的如果共享前缀长度不是块的整数倍可能匹配不上。5.2 换入换出导致的延迟抖动延迟抖动通常表现为 P99 突然飙高。原因往往是某次请求触发了一次大额换入把 PCIe 带宽占满了其它请求的拷贝被阻塞。解决办法限制单次换入的最大数据量超过就拆成多次小批量给换入换出设置优先级别让它和关键路径的计算抢资源开启预取把换入提前到请求真正需要之前用时间换平滑。我踩过的一个坑是预取开得太猛结果预取的数据大部分没用上白白占了带宽反而让真正需要的换入变慢。后来把预取窗口收窄只预取下一个大概率会命中的前缀节点抖动就明显改善了。5.3 内存池配置不当引发的连锁问题内存池配大了会挤占系统内存配小了又起不到分层效果。有个容易被忽略的点内存池的分配方式。如果是一次性大块分配启动时可能因为内存碎片分配失败如果是按需分配运行中可能出现延迟毛刺。我的建议是启动时预留略大于实际需要的内存池并且监控内存池的使用率。如果长期使用率低于 50%说明配大了可以回收一部分如果长期接近 100%说明配小了KV 下沉不进去等于白开。5.4 常见问题速查表现象可能原因排查方向命中率低无共享前缀 / L1 太小 / 策略激进检查请求特征、调大 L1、改 write policyP99 延迟抖动大额换入阻塞带宽限制单次换入量、开预取、设优先级启动 OOML1 配额过大降低 mem-fraction-static内存池分配失败内存不足或碎片调小内存池、检查系统内存吞吐不升反降搬运开销大于重算开销评估场景是否适合 HiCache缓存命中但延迟没降命中集中在 L2调大 L1、优化预取5.5 几个容易被忽视的实操细节第一监控要到位。HiCache 的效果高度依赖运行时状态没有监控就是盲调。至少要采集命中率、命中层级分布、换入换出量、内存池使用率这几个指标。第二版本升级要谨慎。HiCache 这类涉及底层存储管理的功能不同版本之间的行为和参数可能有变化。升级前先在测试环境验证别直接上生产。第三别迷信命中率。命中率是手段不是目的最终要看的是端到端延迟和吞吐。有时候降低一点命中率换来更平滑的延迟分布反而是更优的选择。第四结合具体模型调。不同模型的层数、KV head 数、head_dim 都不一样KV 的单 token 占用差异很大。调参前先把你用的模型算一遍单 token KV 占用心里有数再动手。6. 我个人在 HiCache 调优上的一些体会折腾 HiCache 这段时间最大的感受是它不是一个开了就变快的开关而是一套需要根据场景反复调校的机制。同样的配置在长上下文多轮对话场景里效果拔群换到前缀差异极大的批量任务里可能毫无收益甚至拖后腿。我现在的一个习惯是上 HiCache 之前先做两件事一是统计真实请求的前缀共享情况看看理论上限在哪二是算清楚单 token KV 占用和显存预算确定 L1 能放多少。这两件事做完配置基本就有方向了不用瞎试。另外分享一个小技巧调 write policy 的时候可以先用写回策略跑一轮观察 L1 的淘汰速率。如果淘汰很频繁说明 L1 不够或者请求的前缀复用度低如果淘汰很慢说明 L1 够用这时候再考虑要不要开预取进一步降延迟。这个顺序能帮你少走不少弯路。HiCache 这套分层思路本质上和操作系统里的虚拟内存管理是一脉相承的。理解了 page cache、swap 那套逻辑再看 HiCache 的 write policy 和换入换出会顺畅很多。如果你之前接触过这些底层概念上手 HiCache 会快得多。