☰
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", json=payload) 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 显存没降下来

有时候开了 HiCache,nvidia-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 的参数,就不会觉得是在背配置,而是在做一次存储层级的权衡。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询