SGLang前缀缓存实战指南:RadixAttention如何让重复请求不再白算
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
本文面向初次接触 SGLang 推理框架的开发者,用大白话讲清楚前缀缓存(RadixAttention)为什么能省钱、内部怎么运作、如何开启与调优。读完你就能在自己的服务里把"相同开头只算一次"这件事落地,实测排队时间与显存占用双双下降。
先讲一个让人肉疼的真实场景
想象你维护着一个客服问答机器人:每个用户进来,系统都要拼一段固定指令(比如"你是XX平台的客服,请用中文、简洁地回答"),再加上之前几轮的历史对话,一起发给大模型。假设这段前缀有 800 个 token,100 个用户同时来问问题,模型就要把这 800 个 token 从头到尾算 100 遍。
问题就出在大模型的"自回归"特性上:每生成一个 token,它都要重新读取一遍全部历史 token 的注意力状态。生成前的那段预计算(业内叫prefill,预填充)非常昂贵,而如果大家的开头一模一样,这笔钱就纯粹是被重复烧掉的。
下面这张图来自 SGLang 项目文档(docs/images/dpa.png),展示了推理流水线里 prefill 与 decode 两个阶段的调度分工——prefill 是计算密集段,也正是前缀缓存发挥作用的主战场。
SGLang 给出的解法叫RadixAttention:把"已经算过的公共前缀"连同它的中间结果一起缓存下来,下一个请求如果开头相同,直接接着算,前面那段直接跳过。
先看收益再谈原理:这笔买卖到底划算不划算
缓存不是万能的,它的收益取决于你的请求有多少"公共开头"。拿三个常见场景举例:
| 业务场景 | 公共前缀占比 | 大致收益 |
|---|---|---|
| 客服会话(固定系统指令+历史对话) | 高,几乎每个请求都带 | 排队延迟大幅下降,显存更稳 |
| 批量代码补全(相同文件上下文) | 中高,同仓库内前缀相似 | 补全吞吐明显提升 |
| 独立的一次性提问(毫无公共前缀) | 几乎为零 | 收益可忽略 |
核心规律一句话:公共前缀越长、出现越频繁,缓存收益越大。多轮对话之所以受益明显,正是因为每一轮的新请求都带着之前全部轮次的完整历史,前缀一次比一次长,而复用一次就省一次 prefill。
把原理讲透:一棵会"记住"公共前缀的树
RadixAttention 的底层是一棵基数树(Radix Tree)。你可以把它想象成文件系统里的目录结构:/usr/bin和/usr/lib共享/usr这一段,磁盘上只存一份。对应到缓存里,每一段 token 序列就是"目录",整条请求就是"路径"。
整个工作机制可以拆成四步来看:
- 匹配:新请求从根节点出发,按 token 逐个往下比对,找到能复用的最长公共前缀,直接拿到对应的缓存索引,跳过这部分计算。核心逻辑在 python/sglang/srt/mem_cache/radix_cache.py 的
match_prefix里。 - 分裂与插入:如果新请求的前半段命中了、后半段是新内容,树会在公共前缀的末端"长"出一个新分支,把新内容挂上去,下次再有人用同样开头就能命中。
- 淘汰回收:显存不够时,树会按"最久没被用过"(LRU)的策略,从叶子节点开始剪枝,剪掉的节点把 token 空间还给缓存池,保证内存不炸。
- 引用保护:正在被当前请求使用的节点会打上引用标记(lock_ref),处于"保护"状态,淘汰阶段会绕开它们,避免出现"数据刚取出来就被回收"的尴尬。
顺带一提,对于超长 prompt,SGLang 还支持分块前缀缓存:把长序列切成若干块,按块粒度去匹配和复用,避免"整条差一个 token 就全部失效"的浪费。
三步开启前缀缓存,马上见效
SGLang 默认就开着 RadixAttention,你大概率已经在享受它了。想确认或手动控制,跟着这三步走:
第一步:确认没被关闭。启动服务时检查命令行参数里是否出现了--disable-radix-cache,这个参数存在且被设置时缓存才关。正常启动(python -m sglang.launch_server --model-path 你的模型)默认开启,无需额外操作。
第二步:按需调整页面大小。缓存按页管理,页面越小粒度越细、命中越灵活,但管理开销也越大。可以先用默认值跑通,再根据命中率微调--page-size这类参数。
第三步:为长序列开启分块。如果你的请求前缀普遍很长,找到分块前缀缓存相关的环境变量与阈值设置,把阈值调到合适位置,长 prompt 的复用率通常会有惊喜。
两个可以直接抄走的场景示例
场景一:客服会话的历史前缀复用
# 伪代码示意:把"系统指令+历史会话"作为公共前缀 system_instruction = "你是XX平台客服,请用中文简洁作答。" conversation_history = "用户:我想退款\n客服:请提供订单号\n用户:订单号是 8848" queries = ["现在退到哪一步了?", "大概几天到账?", "还能改成退货吗?"] for q in queries: # 三个请求共享同一段前缀,prefill 只算一次 response = ask(system_instruction + conversation_history + q)三个问题共享了同一段历史,第二个、第三个请求的前缀直接命中缓存,模型只计算真正不同的那部分,响应速度肉眼可见地变快。
场景二:批量代码补全
# 同一仓库内,文件头部上下文高度相似 file_context = "import torch\nimport torch.nn as nn\nfrom typing import List, Optional\n\n" snippets = ["def forward(self, x):", "def loss(self, y, pred):", "class Trainer:"] for snippet in snippets: # 每个补全请求都带着相同的 import 前缀,这部分只算一次 completion = complete(file_context + snippet)批量场景里,缓存命中率越高,整批任务的完成时间越接近"只算一遍"的理想值。
看懂这几个监控指标,调优才有依据
光开启不观察等于盲调。SGLang 暴露了与缓存直接相关的指标,重点关注这几个:
- 前缀缓存命中率:命中的请求占比,是衡量收益的第一指标,命中率上不去先怀疑场景是否真有公共前缀。
- 可淘汰缓存大小:当前能被 LRU 回收的空间,能帮你判断"缓存还富余多少"。
- 受保护缓存大小:被引用锁占住的空间,如果长期很大,说明并发密集,注意观察是否挤压了可回收空间。
- 总缓存大小:配合显存使用率一起看,确认缓存没把显存撑爆。
新手最容易踩的3个坑
坑一:以为缓存命中率必须 100%。命中率低不一定是配置问题,可能只是业务本身没有公共前缀。先检查请求分布,再怀疑参数。解决思路:用上面的指标确认公共前缀占比,别盲目改配置。
坑二:前缀共享了,但"差一个 token"导致全部失效。比如某个请求带了时间戳或随机 ID,前缀从此千奇百怪。解决思路:把动态内容从提示词里剥离出去,或交给分块前缀缓存按块复用。
坑三:缓存与并发互相"打架",偶发报错。淘汰和引用并发进行时,如果没有锁保护就可能出问题。解决思路:SGLang 已用引用计数机制处理这类冲突,遇到异常先检查是否关闭了缓存保护相关的默认行为、再检查显存是否过小导致淘汰过于激进。
小结
前缀缓存的本质就一句话:把重复的劳动变成一次劳动。RadixAttention 用一棵基数树把公共前缀的中间结果组织起来,配合 LRU 淘汰和引用保护,在显存安全的前提下把 prefill 的浪费压到最低。
上手路径也很简单:默认开启不用管 → 看命中率 → 有长前缀就开分块 → 遇到"差一个 token"就把动态内容挪出提示词。跑一轮观察下来,你会对"省下来的钱"有非常直观的感受。想深入源码,直接翻 python/sglang/srt/mem_cache/radix_cache.py 和 python/sglang/srt/mem_cache/base_prefix_cache.py,比任何教程都实在。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考