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 前缀缓存是它最亮眼的杀手锏之一:通过基数树(Radix Tree)结构复用历史 KV 缓存,让多轮对话、批量提示工程、文档摘要等高频场景的推理吞吐提升数倍,最高可达约 5 倍加速。本文不讲空话,直接从"重复计算有多浪费"说起,带你走完"快速上手 → 原理拆解 → 淘汰机制 → 进阶玩法 → 指标调优 → 实战案例"的完整路径,读完就能在自己的项目里把前缀缓存用起来。
一、先算一笔账:你的 GPU 正在做大量"无用功"
想象一个深夜运营的智能客服机器人:所有请求都带着同一段冗长的系统提示词,比如"你是 XX 平台的客服,请遵循以下规则……",后面再接用户问题。
在没有前缀缓存的传统方案里,每一个请求都要从第一个 token 开始重新跑一遍注意力计算。系统提示词有 500 个 token,一晚上来 10 万个请求,这 500 个 token 就被白算 10 万次——它们的结果一模一样,却被反复生产、反复丢弃。
这正是大模型推理领域著名的"前缀重复计算"问题。KV 缓存(Key-Value Cache)技术虽然记住了生成过的内容,但传统实现里它只服务单个请求的生命周期:请求结束,缓存作废。于是:
| 典型场景 | 重复计算比例 | 直观感受 |
|---|---|---|
| 多轮对话(长历史) | 高 | 每轮都重算整段历史 |
| 批量提示工程 | 极高 | 相同系统提示被批量重算 |
| 代码补全 | 中 | 文件头被反复重算 |
| 文档问答 | 高 | 长文档每问一次算一次 |
SGLang 的做法是:把缓存从"请求级"升级为"全局级"。用一个数据结构记录所有请求算过的前缀,新请求来了先"查账",能复用的部分绝不重算。这个数据结构,就是基数树。
二、五分钟上手:三行代码开启缓存复用
先把门槛降到最低。SGLang 的 RadixAttention 在默认配置下就是开启状态,你甚至不需要写额外代码:
from sglang import function, gen, Engine # 1. 定义生成任务:框架会自动为相同前缀建立缓存 @function def chat_reply(question: str): result = gen("answer", max_tokens=128) return result # 2. 三个请求共享同一段系统提示词前缀 system_hint = "你是一位严谨的Python技术导师,请用中文简洁作答。" batch_questions = [ system_hint + "什么是装饰器?", system_hint + "什么是生成器?", system_hint + "什么是上下文管理器?", ] # 3. 批量执行:后两个请求会命中第一个请求留下的缓存 with Engine() as engine: for q in batch_questions: print(chat_reply.run(q, engine=engine))这段代码在做什么?它只是普通的批量推理调用,但 SGLang 会在内部记录第一个请求生成的前缀 KV,让后两个请求直接复用,只计算"装饰器/生成器/上下文管理器"这几个新增 token 的注意力。
如果你是自建服务、用命令行启动,也只需要关注几个启动参数:
python3 -m sglang.launch_server \ --model-path your-llm-path \ --enable-metrics # 打开监控,便于观察命中率想从源码跑通全流程,可以克隆仓库后按官方文档安装:
git clone https://gitcode.com/GitHub_Trending/sg/sglang三、拆开黑盒:基数树到底在管什么
光会用不够,我们来看它内部长什么样。基数树(Radix Tree)是字典树(Trie)的"压缩版":字典树每个字符一个节点,基数树则把没有分支的连续路径合并成一个节点,从而省内存、加快查找。
3.1 节点:缓存的最小管理单元
在python/sglang/srt/mem_cache/radix_cache.py中,每个节点记录四样关键信息:
class TreeNode: def __init__(self): self.children = {} # 子节点:按下一个 token 索引 self.parent = None # 父节点指针,向上回溯用 self.key = None # 本节点对应的 token 序列 self.value = None # 这些 token 的 KV 缓存索引 self.lock_ref = 0 # 引用锁:>0 表示正在被使用,不可驱逐 self.last_access_time = 0.0 # 最近访问时间,供 LRU 策略使用可以把它想象成文件系统里的目录树:key是目录名,value是目录下真正占空间的文件,lock_ref是"正在被打开的文件不可删除"的标记。
3.2 匹配:一条请求进来后发生了什么
新请求的前缀查找走match_prefix流程,核心逻辑可以用伪代码讲清楚:
def 查找最长缓存前缀(root, 请求token序列): 当前节点 = root 已命中KV = [] while 序列还没匹配完 and 当前节点有匹配的子节点: 子节点 = 找到对应子节点 if 子节点路径比剩余序列长: 把子节点从"匹配点"处切开 # 节点分裂,见下文 记录分裂后的前半段KV break else: 记录子节点的全部KV 继续往下一层走 return 已命中的KV, 停在哪节点分裂是这里最巧妙的设计:比如缓存里存了前缀[1,2,3,4,5],新请求只匹配到[1,2,3],树就会把原节点一分为二——[1,2,3]一段、[4,5]一段。分裂不复制数据(KV 索引切片共享),只是让树结构更精细,方便下次精确命中。这样"查前缀"从 O(全序列) 变成 O(匹配长度),配合页面对齐,性能开销极小。
3.3 插入:请求结束后的"归档"动作
请求算完,它的完整输入+输出 token 序列会被insert进树里。如果新序列与已有节点共享前缀,就只在分歧处长出新的分支节点,尽量复用旧节点,避免重复存储。
💡 小知识:SGLang 还支持
extra_key命名空间,可以让不同 LoRA 适配器、不同采样配置的缓存互不干扰。例如按 LoRA ID 隔离缓存行,避免张冠李戴。
四、内存告急时谁先走?淘汰机制的博弈论
GPU 显存永远是稀缺资源。基数树里存得越多,可用空间越少,所以必须有一套"驱逐(evict)"规则,决定缓存满了以后先丢掉谁。
4.1 只从"叶子"下手
SGLang 的驱逐策略有一个硬约束:只删除叶子节点(没有子节点的节点)。原因很直观:叶子删掉不影响任何其他前缀的完整性;而如果删中间节点,等于把它所有后代一起删掉,代价太大。
4.2 引用锁:正在用的缓存受保护
def 驱逐(num_tokens): 把当前所有可驱逐的叶子放进最小堆 # 按 LRU/优先级排序 while 还没驱逐够数量 and 堆不为空: 候选节点 = 堆顶弹出 if 候选节点.lock_ref > 0: # 被正在处理的请求引用 continue # 跳过,不能碰 释放候选节点的KV内存 把它从树里摘除lock_ref是关键的安全阀:当一个请求正在使用某段前缀时,该节点及其祖先节点的引用计数都会 +1,驱逐逻辑看到锁就绕道走。请求结束后引用计数 -1,节点重新变为"可驱逐"。
4.3 可选的淘汰策略
通过eviction_policy参数可以切换策略,SGLang 内置了几种:
| 策略 | 名称 | 适用场景 |
|---|---|---|
lru | 最近最少使用 | 通用场景,兼顾命中率与公平性 |
slru | 分段 LRU | 冷热数据区分明显的工作负载 |
priority | 优先级感知 | 高价值前缀(如核心系统提示)优先保留 |
选择思路:绝大多数场景直接用默认 LRU 即可;如果你的请求有明显的"热前缀",比如永远被引用的系统提示,可考虑 priority 策略,把热点路径的priority设高,让它不容易被挤出去。
五、进阶武器库:分块缓存、HiCache 与统一基数树
基础能力讲完,SGLang 前缀缓存还有几件"大杀器",值得在生产环境里重点考察。
5.1 分块前缀缓存(Chunked Prefix Cache)
长序列的缓存命中率往往被"最后一小段不匹配"拖累——比如两个长文档请求差一个 token,整条长前缀就全废了。分块缓存把前缀切成固定大小的块分别管理,例如阈值 256 token 一块:
# 环境变量方式启用(仅部分模型支持,如 DeepSeek 系列) os.environ["CHUNKED_PREFIX_CACHE_THRESHOLD"] = "256"块与块之间独立缓存、独立命中,长序列也能吃到高命中率,代价是管理开销略增。
5.2 HiCache:把缓存搬出 GPU
设备显存不够时,可以把低频访问的 KV 缓存"备份"到主机内存(CPU 侧),构成两级存储:
- 命中设备缓存:直接使用,零拷贝;
- 命中主机缓存:从主机加载回显存(HiCache 加载),换取显存空间;
- 完全未命中:重新计算,然后按策略写入设备或主机。
这相当于给前缀缓存加了一个"冷热分层",在显存受限的部署(如单卡 24GB 跑大模型)里尤其有价值。主机端缓存同样受host_ref_counter保护,不会在传输中被误删。
5.3 统一基数树与会话感知缓存
新版本引入的UnifiedRadixCache(通过SGLANG_ENABLE_UNIFIED_RADIX_TREE=1开启)把全注意力、滑动窗口注意力(SWA)、Mamba 等多种注意力形态的 KV 统一进同一棵缓存树,还支持会话感知驱逐:给长会话的缓存打上软引用标签,内存紧张时先淘汰无归属的缓存,再动活跃会话的缓存,避免"排队的用户把正在聊天的用户挤下线"。
启用会话感知只需两个动作:
# 启动时打开开关 SGLANG_ENABLE_UNIFIED_RADIX_TREE=1 python3 -m sglang.launch_server \ --model-path MODEL_PATH --enable-session-radix-cache# 请求时带上 session_id;会话结束调用 close_session curl http://localhost:30000/generate \ -H "Content-Type: application/json" \ -d '{"text": "完整提示词", "sampling_params": {"max_new_tokens": 128}, "session_id": "agent-42"}' curl -X POST http://localhost:30000/close_session \ -H "Content-Type: application/json" -d '{"session_id": "agent-42"}'⚠️ 注意:
session_id只负责给缓存打标签,不会自动拼接历史上下文,每轮请求仍需携带完整 prompt。
六、用数据说话:怎么观测缓存命中率
优化没有指标就是"盲人摸象"。SGLang 会暴露一组关键指标,最核心的是缓存命中率,它直接决定你能省多少算力:
cache_hit_rate:设备端前缀缓存命中率(越高越好,一般希望长期高于 0.5)evictable_size:当前可被驱逐的缓存大小protected_size:被引用锁保护的缓存大小total_size:缓存总规模
命中率和加速比的关系大致是:命中率越高,重复计算越少,延迟与吞吐改善越明显。生产建议:
- 先量化:启动时加
--enable-metrics,观察命中率基线; - 再定参:页面大小
page_size(如 16)影响对齐粒度,一般保持默认或按模型调整; - 后分层:显存吃紧时引入 HiCache,长序列任务开启分块缓存;
- 常态化:把命中率纳入服务监控大盘,出现异常下滑时优先排查是否误改了前缀结构(比如模板里混入了时间戳)。
七、两个实战场景对照
场景一:多轮对话——让"历史"不再是负担
history_prefix = "用户:你好,我想了解机器学习。\n助手:当然可以,请讲!\n用户:" follow_ups = ["什么是过拟合?", "怎么避免过拟合?", "举一个实际例子?"] for question in follow_ups: full_prompt = history_prefix + question answer = chat_reply.run(full_prompt) # 第一问计算完整前缀,后两问直接命中前两轮历史,只算新增部分对话越长、轮次越多,节省越明显——因为可复用的历史长度随轮次线性增长,而每次新增计算的只有本轮新内容。
场景二:批量提示工程——同一模板 N 次复用
template = "你是一名资深数据工程师。请解释以下概念:" tasks = ["数据仓库与数据湖的区别", "ETL 与 ELT 的区别", "列式存储的适用场景"] for task in tasks: prompt = template + task # 模板部分完全相同 print(chat_reply.run(prompt))三个请求共享全部模板 token,理论命中率接近"模板长度 / 全长度",批量越大、模板越长,收益越可观。
八、绕不开的难点与路线图
前缀缓存不是银弹,工程化落地时有几个真实挑战:
内存碎片化:频繁分裂节点可能让缓存碎片化。对策是页面对齐(固定 token 粒度分配)与智能合并节点,让内存分配更规整。
并发与一致性:多请求同时读写同一棵树,靠引用锁 + 写时复制保证安全;跨 TP worker 的缓存同步通过事件队列(如 BlockStored / BlockRemoved)完成,并辅以哈希校验防止数据错位。
未来方向:社区正在探索跨节点缓存共享、FP8/INT4 量化缓存的兼容、按序列特征自适应页面大小,以及基于请求模式预测的预加载。这些方向意味着"缓存命中率"这个指标仍有继续拉高的空间。
九、写在最后:现在就去试试
前缀缓存解决的是 LLM 推理里最"隐蔽"的性能浪费——同样的前缀被成千上万次重复计算。SGLang 用一棵基数树把这件事做到了优雅且高效:先查再算、能省则省、满了择优驱逐。
给你的行动清单:
- 克隆仓库
git clone https://gitcode.com/GitHub_Trending/sg/sglang,跑通一个带共享前缀的批量 demo; - 开启
--enable-metrics,观察命中率从 0 到逐步爬升的过程; - 换到真实的多轮对话负载,对比开启缓存前后的吞吐差异;
- 显存紧张时,把 HiCache 和分块缓存加进来,再看一轮指标。
下一步,可以深入研究 SGLang 的调度器如何与 RadixCache 协作、UnifiedRadixCache如何统一管理多种注意力形态,或者探索 HiCache 在长上下文场景下的完整设计与存储/运行时分离机制。从一棵树的节点开始,你正在进入 LLM 推理优化的核心地带。
后续预告:分布式推理篇——多 GPU 与多节点场景下,SGLang 如何继续压榨缓存与并行的双重红利。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考