SGLang 前缀缓存实战:RadixAttention 如何让重复前缀省下 3–5 倍计算
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
本文围绕 SGLang 的 RadixAttention 前缀缓存展开:它用一棵基数树(Radix Tree)记录「前缀 → 已算好的 KV 缓存位置」的映射,新请求进来先查树、命中多少就免算多少。全文从真实部署场景出发,讲清工作原理、最小可运行示例、加速来源、场景化用法、调参手册和常见坑。
一个真实场景:客服机器人的「每次都在重算」
先讲个具体画面。某团队用 SGLang 部署了一个客服机器人,每条请求都带着同一段约 2000 token 的知识库摘要和角色设定开头,后面才是用户的具体问题。高峰期一小时几千条请求,GPU 的算力几乎全花在这段「每次都一样的开头」上——同一个 2000 token 的 prefill 被反复算了几千遍。
SGLang 为此专门做了 RadixAttention:把已经算过的 KV 缓存按前缀持久化在一张基数树里(基数树可以理解成「压缩版前缀字典树」,相同前缀只存一份),并且默认开启。第二个请求带着相同开头进来时,调度器直接从树上找到已算好的 KV 张量索引,跳过这部分 prefill,只计算新增的 token。效果不是「快一点」,而是同一批请求里,后到请求的首 token 延迟(TTFT)可以砍掉一大截,GPU 的 prefill 吞吐同步放大,端到端常见 3–5 倍的加速区间,具体取决于前缀重合度。
前缀缓存到底解决什么问题:一段前缀,一份计算
用两个比喻把痛点说透。
快递驿站分拣。没有前缀缓存时,每个包裹(请求)都要从产地(模型第一层)开始走完全程。驿站早该建立分拣规则:同一条路(前缀)的包裹,到达同一站之后的路可以共用。RadixAttention 就是那张「按前缀分区」的分拣表——token 序列是地址,共同前缀越长,越晚到的包裹越不用重走老路。
图书馆按书架编号。每个「已经读过的前缀」占一个书架位(KV 缓存的物理位置),编号写进目录树。新读者报出书名开头,管理员顺着目录找到最远的那个共同前缀,直接从下一本接着读。
痛点拆开看是两条因果链:
- 成本链:前缀重复 → prefill 重复计算 → GPU 计费时间被浪费。因为 prefill 阶段是算力密集型的,一段 2000 token 的公共前缀每多算一遍,就多烧一遍等量的矩阵乘法。
- 延迟链:前缀重复 → TTFT 必须等完整 prefill 算完 → 用户等待时间里有一大段是「白等」。命中缓存后,这段等待直接消失,只剩新增 token 的计算时间。
所以前缀缓存的收益不是玄学,它能被直接量化:每命中 1 个 token,就省掉 1 个 token 的 prefill 计算。命中率是唯一的杠杆。
一张图看懂 RadixAttention 的工作方式
整个机制可以压缩成「进来查树 → 命中则复用 → 算完插树 → 空间不够再淘汰」四步:
对应到源码,两个核心文件就够了:
- 树本体:
[radix_cache.py](https://link.gitcode.com/i/f21b0ef418d88df98eb7f361ff5fd976)。match_prefix找最长已缓存前缀,insert把新算的 KV 挂回树上,evict负责腾空间。每个TreeNode上维护着lock_ref(引用计数,请求在用就保护该节点不被淘汰)和last_access_time(LRU 排序依据)。 - 开关与参数:
[server_args.py](https://link.gitcode.com/i/52d81e3484c60450db7c0a6f5664f701)。--disable-radix-cache可整体关闭,--page-size、--radix-eviction-policy、--enable-hierarchical-cache都在这张参数表里。
一句话总结数据流:请求的 token 序列是钥匙,树里存的是「钥匙 → KV 张量物理位置」的映射;命中即省算,算完再存回去。
5 分钟跑通:从启动服务到验证命中
你马上能拿到的东西:一条启动命令 + 三次请求,亲眼看到缓存命中。
第一步,启动 SGLang 服务(前缀缓存默认开启,无需额外参数):
# 前缀缓存默认启用;--page-size 控制每页 token 数,默认 1 python -m sglang.launch_server \ --model-path Qwen/Qwen2.5-7B-Instruct \ --port 30000第二步,发三次「同前缀、不同结尾」的请求:
PROMPT='以下是公司FAQ摘要:……(2000 token 的公共前缀)…… 问题:' # 第一条:缓存为空,全量 prefill,最慢 curl -s localhost:30000/generate -d "{\"text\": \"${PROMPT}退货政策是什么?\"}" # 第二、三条:前缀命中,只算最后几个 token,TTFT 明显下降 curl -s localhost:30000/generate -d "{\"text\": \"${PROMPT}运费怎么算?\"}" curl -s localhost:30000/generate -d "{\"text\": \"${PROMPT}几天能到?\"}"验证是否命中,有两个抓手:
# 1) 服务信息接口:cache_hit_rate 会随重复前缀请求上升 curl -s localhost:30000/get_server_info # 2) Prometheus 指标中也暴露 gpu_prefix_cache_hit_rate curl -s localhost:30000/metrics | grep prefix_cache_hit看到cache_hit_rate从第一条请求的低值逐步爬升,就说明树在正常工作。想彻底关掉做对照实验,加--disable-radix-cache即可。
它凭什么快 3–5 倍:把命中率翻译成省下的计算
先给结论:加速不来自单次请求变快,而来自「重复前缀」从计算图里消失了。
因果链是这样的:
- 一批请求共享同一段前缀,长度 P token;
- 只有第一个请求付出 P 次 prefill 的计算,后续请求的这段直接查表拿到 KV 索引;
- 省下的 prefill 时间线性叠加到 GPU 批处理吞吐上,TTFT 同步下降;
- 如果前缀长到超出 KV 池容量后被 LRU 淘汰,加速就打折——命中率是上限,淘汰策略决定你能吃到多少。
用「省下的 token 数」而不是百分比来表达收益,更直观。假设公共前缀 P = 4000 token,一批 100 条请求:
| 缓存策略 | 这批请求实际付出的 prefill 计算量 | 相比全量重算省掉的部分 |
|---|---|---|
| 每次请求独立重算(无缓存) | 100 × 4000 = 40 万 token | 0 |
| RadixAttention 前缀全命中 | 1 × 4000 + 各请求独立部分 | 约 39.6 万 token 的 prefill |
| 命中率 50%(部分被淘汰) | 约 20 万 token | 约 20 万 token |
换算成体感:prefill 占大头的工作负载里,前缀部分省掉九成计算,对应的就是 TTFT 从「等整段算完」变成「等最后几个 token 算完」,端到端 3–5 倍的加速区间就是这么来的。前缀越短、请求越离散、KV 池越紧张,收益越接近 1 倍——它不是魔法,是「别算第二遍」的算术。
这张饼图传达的重点是:缓存把最贵的部分从「算力」变成了「显存读取」,算力被腾出来给真正的新 token。
把前缀缓存用起来:四类高命中场景
前缀缓存的收益 = 前缀重合度 × 前缀长度 × 请求频次。下面四类业务天然凑齐这三个条件。
客服知识库问答。每条请求都带「角色设定 + 知识库文档 + 工具说明」三段固定内容,只有最后的用户问题变化。把这三段稳定放在 prompt 最前面,重合度接近 100%,是最典型的受益场景。
Agent 工具调用循环。Agent 每轮都会把「系统指令 + 工具 schema + 历史动作记录」重新拼进 prompt。历史只增不改,每一轮都在复用上一轮的前缀——轮数越多,省得越多。关键约束:历史部分不要每轮重排或改写,前缀一变,树就断在第一个分叉处。
RAG 长文档问答。对同一份 5 万 token 的文档提 20 个问题,文档 chunk 放在前缀位置,20 条请求只付 1 次文档的 prefill 成本。文档切换频率越高,命中率越低,这时就该考虑下一节的 HiCache。
评测批量跑分。benchmark 批量请求往往共享同一题面模板或 system prompt。把同模板请求排在一起提交,比打散提交命中率高一档——连续提交对树很友好,打散则反复制造分叉。
四类场景的共同操作原则一句话:固定内容前置、可变内容后置、别动已经发过的部分。
调优与进阶:一张调参手册
把分散的机制整理成这张表,参数名全部对应实际的--启动参数:
| 调什么 | 参数 / 机制 | 默认 | 什么时候动它 | 预期效果 |
|---|---|---|---|---|
| 命中率 | cache_hit_rate(/get_server_info与/metrics中gpu_prefix_cache_hit_rate) | — | 任何调优前先读它 | 长期低于 30% 时,先排查 prompt 结构,再谈调参 |
| 整体开关 | --disable-radix-cache | 不关闭 | 做 A/B 对照实验 | 关闭后 prefill 全量重算,可做收益归因 |
| 页粒度 | --page-size | 1 | 配合分页注意力 / 稀疏 KV 特性时按后端要求调整 | 页越大,对齐浪费越多,管理开销越小 |
| 淘汰策略 | --radix-eviction-policy:lru/lfu/slru/priority | lru | 访问模式不均衡、少数热前缀反复出现 →lfu或slru更稳 | 热前缀留存时间变长,命中率上限提高 |
| 长前缀跨请求留存 | --enable-hierarchical-cache+--hicache-ratio(cache 模式默认 2.0,即主机 KV 池 = 2 倍设备池) | 关闭 | 前缀总长超过 GPU 容量、主机内存充足 | GPU 淘汰的前缀下沉到主机内存,重命中时搬回,省一次完整 prefill |
| HiCache 写策略 | --hicache-write-policy:write_back/write_through/write_through_selective | — | 主机带宽紧张时选 selective,只写热数据 | 减少无谓的主机写入 |
| 分块前缀缓存 | --disable-chunked-prefix-cache(默认开启,面向 DeepSeek 系模型的分块前缀处理) | 开启 | 短序列为主的负载,分块簿记开销不划算时关闭 | 短请求省掉分块开销 |
| 命名空间隔离 | RadixKey的extra_key(区分 LoRA / 多版本 RAG 上下文) | 无 | 多 adapter、多版本知识库混跑 | 不同extra_key的条目互不串用,避免脏命中 |
三个经验值供参考:
- 命中率长期 < 30%:别急着调参,先检查 prompt 拼装顺序,是不是把可变内容放在了前面;
- 命中率 > 60% 且 TTFT 已经很低:显存可以挪给更大的 batch,并发上去了总成本反而降;
- 开了 HiCache 之后命中率仍掉:说明前缀总量超过「GPU + 主机」两层之和,该考虑第三层存储后端(如 mooncake、hf3fs、aibrix 这类,见
mem_cache/storage/目录)。
常见误区与踩坑实录
问:命中率很高,为什么 TTFT 没降多少?查请求顺序。前缀缓存是「按到达顺序」吃的——第一批全量算,后面才受益。如果你的压测是单条串行发,前几条请求延迟难看的;换成并发压测,整体 TTFT 和吞吐才体现真实收益。
问:system prompt 里加了一行当前时间戳,命中率直接腰斩?对,而且只保留到第一个不同 token 为止。前缀匹配严格从左到右,第一个不同的 token 之后全部重算。时间戳、request_id、每次变化的占位符这类字段,要么挪到 prompt 末尾,要么改成完全一致的静态文案。
问:显存没满,缓存为什么还会被淘汰?KV 池是独立分配的:模型权重、激活、batch 预留之外才轮到 KV 池。而且请求正在解码时,它占用的节点lock_ref > 0,不参与淘汰;请求结束才释放。所以「显存占用 70%」和「KV 池水位」是两回事,看/get_server_info的 token 用量比看 nvidia-smi 更准。
问:多个 TP rank,前缀树不会各算各的吧?不会错乱。KV 缓存按 TP 切分后各 rank 维护自己那份分片,匹配长度由调度器统一决策。监控里的gpu_prefix_cache_hit_rate是调度器视角的统一口径,不用担心多卡数据不一致。
问:前缀缓存会「污染」输出吗?A 请求的 KV 被 B 用错了?匹配键是完整 token 序列(可加extra_key命名空间),B 的前缀必须逐 token 等于树里已存前缀才会复用;不同 LoRA、不同 adapter 走不同的extra_key,物理上不会串用。
问:多轮对话历史越滚越长,缓存还划算吗?划算。每轮「历史 + 新问题」里,历史部分是严格的前缀扩展,天然高命中;真正开始吃缓存的是第 2 轮起的每一轮。第 1 轮没有前缀可复用,别把它算进收益预期。
问:开了 HiCache,延迟是变好还是变坏?分层看:命中 GPU 层的请求不受影响;命中主机层的请求多了「内存搬回 GPU」的传输,比纯 prefill 快、比 GPU 直取慢——总体仍是赚的,因为省掉的是完整的逐层计算。只有主机带宽被写策略打满时才会劣化,这时换write_through_selective。
写在最后
把整件事收回业务口径:前缀缓存把「重复前缀」从计算账单里划掉,换回来的是三样东西——更低的单次 TTFT、更高的单卡并发、以及按 token 计费的推理成本直接缩水。对短 prompt 的单条请求,它是锦上添花;对带长 system prompt、长文档、长 Agent 历史的推理服务,它是「同一张卡,服务两到三倍流量」的关键开关。
前缀缓存不改变模型,它改变的是模型该被计算几次——而「少算」,永远是推理系统里最便宜的性能。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考