资讯动态

SGLang HiCache 分层缓存实战:解决多轮对话推理延迟抖动

发布时间:2026/10/2 10:23:38 来源:尧图企业网站定制
1. 从一次推理延迟抖动说起HiCache 到底在解决什么问题第一次注意到 HiCache 这个东西是在给一个多轮对话服务做压测的时候。场景很典型Qwen 系列模型SGLang 做推理后端前端挂了个客服机器人用户来回问十几轮每轮都带着完整历史。压测脚本跑起来之后我发现一个很别扭的现象——首 token 延迟TTFT忽高忽低短的时候两三百毫秒长的时候能飙到一秒多而且这个抖动跟请求长度不是线性关系反而跟“历史对话有没有被复用”强相关。顺着这个线索往下挖就绕不开 SGLang 的看家本领RadixAttention。它的核心思路是把 KV cache 组织成一棵前缀树radix tree多个请求如果共享相同的前缀就能直接命中树上的节点省掉重复的 prefill 计算。这在多轮对话、few-shot 提示、系统提示词固定的场景里收益巨大——系统提示词那几百上千 token 只需要算一次后面所有请求都能白嫖。但问题也随之而来这棵树是长在显存里的。显存就那么大模型权重占一大块剩下的留给 KV cache。当并发上来、对话轮次变多radix tree 会被撑爆SGLang 就得按 LRU 之类的策略把一些节点踢出去。被踢出去的 KV 一旦后面又需要只能重新 prefill于是延迟就抖了。这就是我压测时看到的现象的根源。HiCache就是冲着这个痛点来的。它做的事情说白了就是给 KV cache 加了一层分层存储hierarchical cache显存不够用的时候不直接把 KV 扔掉而是往下沉到更便宜的存储介质上比如主机内存甚至更远的存储层等需要的时候再捞回来。这样既缓解了显存压力又避免了“踢掉即丢失”的浪费。这个内容适合谁来参考如果你正在用 SGLang 部署模型、被长上下文或多轮对话的显存占用折磨、或者单纯想搞明白 KV cache 分层到底是怎么落地的那这篇东西应该对你有用。我会尽量把原理、参数、实操和踩过的坑都摊开讲不玩虚的。2. 拆开 HiCache分层缓存的设计逻辑与关键取舍2.1 为什么是“分层”而不是“扩容”或“压缩”面对显存不够最直觉的两个方案是加卡扩容或者做 KV 量化压缩。这两个方案我都试过各有各的坑。加卡扩容最直接但成本摆在那儿而且很多场景下显存是被“峰值”撑爆的平时利用率并不高为了那几次峰值去堆硬件不划算。KV 量化压缩比如 INT8、FP8能省显存但会引入精度损失长上下文下误差会累积有些对输出质量敏感的任务不敢用。分层缓存的思路不一样它承认“显存是稀缺的、贵的”但“主机内存是相对充裕的、便宜的”。把不活跃的 KV 沉到内存里活跃的留在显存本质上是在用存储层级换命中率。这个取舍的关键在于——被下沉的 KV 大概率还会被再次用到多轮对话的历史前缀就是典型所以“沉下去再捞回来”的成本要低于“直接扔掉重新算”的成本。提示分层缓存不是万能的。如果你的请求之间前缀共享度极低比如每个请求都是完全独立的随机文本那 radix tree 本身就命中不了HiCache 也救不了这时候该考虑的是别的优化方向。2.2 RadixAttention 与 HiCache 是怎么咬合的要理解 HiCache得先把 RadixAttention 的命中流程在脑子里过一遍。一个请求进来SGLang 会拿它的 token 序列去 radix tree 里做前缀匹配。匹配到的最长公共前缀对应的 KV 节点就是可以复用的部分剩下的新 token 才需要走 prefill。匹配、复用、插入新节点这一套是 RadixAttention 的日常。HiCache 介入的位置就在“节点被淘汰”和“节点被需要”这两个时刻淘汰时原本要被 LRU 踢出显存的节点如果 HiCache 开着会先被写到下一层存储host memory而不是直接销毁。需要时如果某个请求的前缀匹配发现节点不在显存里但 HiCache 记录显示它在 host memory 里就会触发一次“回捞”把 KV 从内存搬回显存然后继续复用。这里有个很关键的细节回捞本身是有成本的。从 host memory 搬 KV 到显存要走 PCIe带宽是有限的。如果回捞的数据量很大、频率很高那这个搬运开销可能反而拖慢整体。所以 HiCache 的收益取决于“回捞省下的 prefill 计算”和“搬运 KV 的开销”之间的平衡。这也是为什么它的 write policy写策略和淘汰策略这么重要——策略选得不好可能搬来搬去净亏。2.3 write policy什么时候把 KV 写下去write policy 是 HiCache 里最值得琢磨的一块。它决定了 KV 在什么时机从显存写到 host memory。常见的几种思路写穿write-through节点一产生就同步写到 host。好处是 host 里永远有最新副本回捞时不会缺数据坏处是写放大严重每个 KV 都要多走一次 PCIe显存和内存带宽都被吃掉。写回write-back节点只在被淘汰时才写。好处是写开销小只有真正要离开显存的才写坏处是淘汰那一刻会有一次集中的写操作可能造成延迟毛刺。按需写on-demand结合访问频率只把“冷但可能还会用”的节点写下去热的留在显存。实际部署里写回是比较常见的默认选择因为它把写开销摊到了淘汰时刻平时不干扰正常推理。但如果你的场景淘汰非常频繁写回的集中开销就会显现出来这时候可以考虑调低淘汰阈值、让淘汰更平滑或者干脆用写穿换稳定。注意write policy 不是孤立参数它和显存水位、淘汰阈值、回捞策略是联动的。单独调一个往往效果不好得一起看。3. 落地实操把 HiCache 跑起来的关键步骤3.1 环境与版本确认HiCache 是 SGLang 较新版本才引入的能力所以第一步是确认版本。我踩过的第一个坑就是版本太老参数根本不认。# 查看当前 sglang 版本 pip show sglang # 升级到较新版本具体版本号以官方发布为准 pip install -U sglang确认版本之后还要看你的部署形态。SGLang 有几种跑法单机单卡、单机多卡TP、多机。HiCache 在单机和多卡下都能用但多卡时要注意 KV 是分片的host memory 的分配和回捞逻辑会更复杂建议先在单卡上把流程跑通再上多卡。3.2 启动参数怎么配启动 SGLang 服务时和 HiCache 相关的参数主要围绕“开不开”“给多少内存”“怎么淘汰”这几件事。下面是我实测下来比较稳的一组配置思路python -m sglang.launch_server \ --model-path /path/to/your/model \ --host 0.0.0.0 \ --port 30000 \ --mem-fraction-static 0.85 \ --enable-hicache \ --hicache-host-memory-gb 64 \ --hicache-write-policy write_back逐个说下我的理解--mem-fraction-static 0.85这是给模型权重和显存内 KV 池预留的比例。0.85 是个经验值留 15% 给激活值和临时缓冲。如果你的模型特别大这个值要往下调否则容易 OOM。--enable-hicache总开关不开的话后面参数都没意义。--hicache-host-memory-gb 64给 HiCache 用的 host memory 上限。这个值要结合机器实际内存来定别把系统内存吃光了否则会触发 swap那性能就崩了。--hicache-write-policy write_back写回策略前面分析过适合大多数场景。提示参数名和默认值可能随版本变化启动前最好用--help确认一遍别照搬网上的老配置。3.3 参数选择的计算过程hicache-host-memory-gb到底给多少合适我一般这么估假设模型是 7B 级别FP16 权重约 14GB。单条 4K 上下文的 KV cache按经验大概几百 MB 量级具体跟层数、头数、head_dim 有关。如果显存里能放 100 条这样的 KV那 host memory 想放 4 倍冗余就是 400 条对应的量。粗算下来几十 GB 是合理的起点。更稳妥的做法是先小后大先给 16GB 跑起来观察命中率和回捞频率再逐步加。加的时候盯着两个指标——host memory 实际占用和回捞耗时。如果回捞耗时明显上升说明内存压力大了该停手了。3.4 验证 HiCache 是否真的生效配完参数不代表就生效了得验证。我的验证方法是构造一个前缀高度共享的请求序列对比开/关 HiCache 两种情况下的 TTFT 和显存占用。import requests import time # 构造一个长系统提示词 多轮追问的请求序列 system_prompt 你是一个专业的客服助手... * 50 # 拉长前缀 questions [问题一, 问题二, 问题三, 问题四, 问题五] for q in questions: payload { text: system_prompt q, sampling_params: {max_new_tokens: 64} } t0 time.time() r requests.post(http://localhost:30000/generate, jsonpayload) ttft time.time() - t0 print(f{q} TTFT: {ttft:.3f}s)如果 HiCache 生效你会看到第一轮因为要建缓存TTFT 偏高后面几轮因为前缀命中 可能的回捞TTFT 应该明显低于“完全重算”的情况。同时用nvidia-smi观察显存应该比不开 HiCache 时更平稳不容易顶到上限。4. 常见问题与排查技巧实录4.1 开了 HiCache 反而更慢这是最容易被劝退的情况。原因通常有几个回捞太频繁说明显存里的 KV 池太小节点刚沉下去就被需要来回搬。解法是调大mem-fraction-static里给 KV 的部分或者调小 host memory 让淘汰更保守。write policy 选错写穿在高频场景下写放大严重。换成写回试试。前缀共享度低请求之间没什么公共前缀radix tree 命中率本来就低HiCache 自然没收益。这种情况该优化的是提示词结构而不是缓存。4.2 显存没降下来有时候开了 HiCachenvidia-smi看显存还是满的。别慌这可能是正常的——HiCache 的作用是“淘汰时不丢”不是“主动清显存”。显存该用还是用只是淘汰的节点有了去处。真正要看的是命中率和重算比例而不是单纯的显存数字。4.3 多卡下的分片问题多卡 TP 部署时KV 是按头或按层分片的每个 rank 各管一部分。HiCache 的 host memory 也要对应分片管理。我遇到过一次因为各 rank 内存配额不一致导致某个 rank 回捞失败、请求卡住的情况。解法是确保各 rank 的hicache-host-memory-gb配置一致并且监控每个 rank 的缓存状态。下面这张表是我整理的常见现象和排查方向可以直接对照用现象可能原因排查方向TTFT 抖动大回捞频繁看回捞次数调大显存 KV 池开了更慢write policy 不当切换写回/写穿对比显存不降正常行为关注命中率而非显存数字请求卡住多卡配额不一致检查各 rank 配置内存吃满host memory 给太多下调配额留系统余量4.4 几个我踩过的坑第一个坑是没留系统内存余量。有次把 host memory 给到接近物理内存上限结果系统开始 swap整个服务卡成幻灯片。后来我固定留至少 20% 给系统。第二个坑是压测数据不真实。用随机文本压测前缀共享度极低HiCache 完全没收益差点误判它没用。换成真实的多轮对话日志后收益立刻显现。所以压测一定要用贴近生产的请求分布。第三个坑是忽略回捞延迟的尾部分布。平均回捞耗时可能不高但 P99 可能很难看。如果你的服务对尾延迟敏感得专门盯 P99必要时给回捞加限流或优先级。5. 和其他方案的横向对比HiCache 的边界在哪5.1 对比纯 RadixAttention纯 RadixAttention 只有显存一层命中就复用不命中就重算。HiCache 在它基础上加了第二层把“不命中”里的一部分变成了“回捞后命中”。所以 HiCache 的收益上限取决于有多少“本会被重算的请求”其实是可以回捞的。前缀共享度越高、对话越长这个比例越大。5.2 对比 KV 量化KV 量化是“把每个 KV 变小”HiCache 是“把不活跃的 KV 挪走”。两者不冲突理论上可以叠加量化省显存分层省淘汰损失。但叠加会引入更多变量调优复杂度上升建议先把一个调明白再上另一个。5.3 对比外部缓存服务有些方案会把 KV 放到更远的存储层比如本地 SSD 或分布式存储。HiCache 目前主要面向 host memory 这一层延迟比 SSD 低得多但容量受限于单机内存。如果你的场景需要跨实例共享缓存那可能得看更上层的方案HiCache 解决不了跨机的问题。注意选方案前先想清楚瓶颈在哪。是显存不够是重算太多还是跨实例共享不同瓶颈对应不同工具别拿着锤子找钉子。6. 我个人的调优心得调 HiCache 这段时间最大的体会是它不是一个“开了就快”的开关而是一个需要跟着业务负载一起调的动态系统。同样一组参数在客服对话场景下效果很好换到代码补全场景可能就一般因为后者的前缀共享模式完全不同。我现在习惯的做法是上线前先用真实流量采样跑一轮记录三个核心指标radix tree 命中率、回捞次数、回捞 P99 耗时。这三个数一出来参数该往哪调基本就清楚了。命中率低就优化提示词结构回捞次数高就调大显存 KV 池P99 难看就给回捞加限流。还有个小技巧把hicache-host-memory-gb设成可动态调整的如果版本支持在流量低谷时收紧、高峰时放宽比固定一个值更省资源。这个我还在摸索等有稳定结论再细说。最后提一句HiCache 这类分层缓存的思路其实在很多系统里都能见到影子——CPU 的 L1/L2/L3、数据库的 buffer pool、CDN 的边缘节点本质都是“用层级换命中”。理解了这层再看 HiCache 的参数就不会觉得是在背配置而是在做一次存储层级的权衡。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑