公司动态

SGLang 前缀缓存实战避坑:RadixAttention 如何让 8000 个重复 token 变成瞬时命中

📅 2026/8/21 19:38:43
SGLang 前缀缓存实战避坑:RadixAttention 如何让 8000 个重复 token 变成瞬时命中
SGLang 前缀缓存实战避坑RadixAttention 如何让 8000 个重复 token 变成瞬时命中【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang凌晨 1 点 47 分小周盯着监控面板上跳动的红字手心发凉。他在 SGLang 上部署的客服机器人白天跑得好好的一到晚高峰GPU 利用率冲到 99%首 token 延迟却从 800ms 飙到 4 秒。更离谱的是日志显示每分钟有几百个请求前面 8000 个 token 一字不差——都是同一条系统提示词加公司知识库唯一的差异是最后一句用户问题。可他的 GPU 像第一次见到这些文字一样把同一段话从头到尾算了一遍又一遍。那晚他做了个实验把重复的前缀只算一次其余请求直接续接服务立刻松了下来。这就是 SGLang 前缀缓存RadixAttention在做的事也是这篇文章想带你亲手验证的东西。每次重算 8000 个 token钱和时间都烧在了哪传统 KV 缓存为什么不顶用因为它的粒度是整条请求——一条对话算完KV 缓存只属于这一条别的请求碰都碰不到。多轮对话里用户每次追问前面几轮历史原样重算批量测试里所有请求共享的 system prompt 被当成普通文本一视同仁地重算。这就像客人每次点菜后厨都从洗菜切菜开始哪怕 90% 的食材你早就备好了。往深里说KV 缓存本质是中间计算结果。LLM 生成第 100 个 token 时必须依赖前 99 个 token 的注意力结果。前缀一样中间结果就该一样。旧方案不是不懂这个道理而是没有一个高效的数据结构去做按前缀查找、按前缀复用——查得慢、存得乱、回收难索性不算了。我突然想起小区里的快递代收点解法其实藏在快递代收点里。一栋 100 户的楼快递员如果每单都从总仓单独跑一趟就是传统做法代收点把去同一栋楼的包裹拼成一车前半程共享到楼下再按门牌分开派送——前缀相同前段路径就共享只有最后的分叉各自负责。按门牌分拣用的就是一棵树1 号楼 → 3 单元 → 502 室路径重合得越多共享的路段越长。把门牌号换成 token 序列把共享路段换成 KV 缓存你就得到了 RadixAttention一棵以 token 序列为路径的树路径重合的部分只存一份 KV 缓存。从问题倒推设计这棵树是怎么长出来的与其背概念不如跟着四个问题把它推出来。问题一怎么按前缀找缓存用树。从根往下每个节点存一段 token 序列和对应的 KV 索引查找就是沿着树一路吃下与请求相同的 token。问题二前缀只重合一半怎么办节点分裂。假设树上已存了 Hello新请求是 Helicopter两者重合 Hel 三个 token。匹配停在节点中间时把节点劈成两段父节点存 Hel两个子节点分别挂 lo 和 icopter。这样共享边界被精确暴露出来后续请求的匹配效率才高。源码里这一步是_split_node在match_prefix匹配到节点中途时触发。问题三缓存会不会撑爆显存两把锁。一是淘汰规则只有没有子节点的叶子才允许被回收因为它不可能再是任何其他请求的前缀回收时按最近访问时间建堆从最久没碰的开始典型的 LRU 思路。二是引用计数每个节点有lock_ref正被某个请求引用的节点被锁住宁可暂时不回收也不能让请求中途读不到数据。两个机制配合inc_lock_ref/dec_lock_ref在请求进出时增减计数。问题四什么时候写进树请求结束那一刻。cache_finished_req把输入 输出的完整 token 序列连同 KV 索引一起挂到树上下一次同样的前缀就能直接命中。核心实现就在python/sglang/srt/mem_cache/radix_cache.py几百行代码值得一读。三步跑起来命令行里的第一个缓存命中RadixAttention 默认开启你不需要配置任何参数。第一步启动服务python -m sglang.launch_server \ --model-path meta-llama/Llama-3.1-8B-Instruct \ --port 30000第二步用同一段 system prompt 连发两个请求curl 或 OpenAI SDK 都行。第一个请求让服务把前缀算一遍并缓存。第三步看命中率curl -s localhost:30000/metrics | grep cache_hit_rate第二个请求发出后sglang:cache_hit_rate会明显抬升。这个指标定义在python/sglang/srt/observability/metrics_collector.py它就是你验证缓存生效的仪表盘。一个 99% 的数字背后是真金白银回到小周的客服场景8000 token 的系统提示加 50 token 的用户问题。前缀匹配能命中 8000/8050 ≈ 99% 的 prefill 计算。prefill 是首 token 延迟的大头这 99% 的重复劳动被省掉后首 token 延迟几乎只由那 50 个新 token 决定——从秒级掉到百毫秒级。这是纯算术不含任何营销水分你用自己的模型就能复现。需要说清前提倍数取决于前缀重合度和序列长度序列越长、重复越多收益越大。SGLang 论文在共享前缀的多轮对话场景中报告了数倍的吞吐提升与这个算术的走向一致。翻车现场命中率上不去先查这三处坑 1命中率恒为 0症状cache_hit_rate一直是 0。 原因prompt 里混入了每轮都变的 token——时间戳、随机数、请求 ID或者 chat template 里每轮都变的属性头。官方文档就记录过 Claude Code 的 attribution 头导致 GLM 每次都要整段重算的案例。 解法检查 prompt 里有没有每轮都变的字段去掉或挪到共享前缀之后。坑 2改了 --page-size 后命中率反而降症状显存省了命中率也跌了。 原因page_size影响匹配的对齐粒度某些后端如滑动窗口注意力路径要求page_size1此时树更碎、命中更难。 解法没有特殊需求别动page_size保持默认。坑 3误关了缓存症状服务正常但命中率为 0。 原因--disable-radix-cache在某些场景批量 OCR、每请求都换新图确实是正确选择但很多人把它用在了重复前缀很高的场景。另外上下文并行、HiSparse 等特性与缓存互斥启动时会直接报错逼你二选一。 解法每个请求前缀都不同才关重复前缀多必须开着。遇到互斥报错按提示决定取舍。缓存之外它解决的是重复劳动这类共性问题RadixAttention 的意义不止于前缀缓存。它的树结构、锁引用、LRU 驱逐本质是一套中间结果复用的通用基础设施LoRA 场景用extra_key隔离不同 adapter 的缓存避免串味HiCache 把缓存从显存扩展到主机内存跨节点共享缓存也在演进中。理解了这棵树SGLang 的缓存体系你就理解了一大半。想深入三份材料够用核心源码python/sglang/srt/mem_cache/radix_cache.py、指标定义python/sglang/srt/observability/metrics_collector.py以及docs/cookbook下各模型卡片里关于 radix cache 的调优笔记。今晚就花三分钟把服务跑起来连发两个同前缀请求看一眼命中率数字的变化。你不用改一行代码——SGLang 已经把这棵树的根埋好了你要做的只是确认它真的在为你干活。等数字动起来的那一刻你就明白小周那晚为什么松了一口气。【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考