SGLang 前缀缓存(RadixAttention)如何避免重复 Prefill:完整讲解
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
做客服机器人时你会发现,一天几万个请求几乎都从同一段 2000 token 的系统提示词开头。不做前缀复用,每个请求都要老老实实把这 2000 个 token 的注意力 prefill 跑一遍,算力消耗随 QPS 线性上涨。SGLang 的前缀缓存(RadixAttention)就是针对这种浪费:首个请求把计算结果写进缓存,后续同前缀请求直接复用现成的 KV,只补算自己多出来的尾巴。
一句话原理:给公共前缀建一份"预制品"
可以把它类比成中央厨房的半成品:第一单客人点菜时,厨师把前奏部分(系统提示词)做熟装进保鲜盒;后面凡是同款前奏的订单,直接开盒接着炒自己的配菜,不用再从生料做起。SGLang 里这份"保鲜盒"就是挂在基数树(radix tree,一种按前缀共享组织的树结构)上的 KV cache(键值缓存)索引,实现位于 python/sglang/srt/mem_cache/radix_cache.py。省下的时间不是来自更快的算子,而是来自"这部分根本不用算"。
SGLang 处理一次请求的流程:命中是怎么发生的
把视角放在"一个请求从进来到离开",整个过程分四步:
- 匹配(match):请求 tokenize 后,从树根开始逐段比对,找到当前序列已缓存的最长前缀。若匹配终点落在某个节点段中间,会自动执行节点分裂(split node),把边界切干净——这不复制数据,只是重排结构。
- 只算增量:模型前向只处理没命中的那部分 token。命中 80% 的请求,prefill 计算量就只剩 20%。
- 写回(insert):新算出的 KV 索引挂到树上对应位置,供下一个同前缀请求命中。
- 释放与回收:每个节点带
lock_ref引用计数,请求在飞期间前缀被"锁住"不受淘汰影响;请求结束后锁释放,节点回到可淘汰(evictable)集合。内存吃紧时,淘汰器用堆按策略(默认 LRU)弹出最老的叶子释放,被锁节点自动跳过。
匹配用页(page)做对齐:当page_size > 1时,命中长度会向下取整到页的整数倍,这同时是内存对齐和碎片控制的抓手。
启用 SGLang 前缀缓存的 3 个常用启动参数
前缀缓存默认开启,什么都不用改就已经在工作。下面 3 个参数覆盖了大多数调优场景:
python -m sglang.launch_server \ --model-path <模型路径> \ --page-size 16 \ --radix-eviction-policy lru # 对比实验时可显式关闭缓存 python -m sglang.launch_server --model-path <模型路径> --disable-radix-cache| 参数 | 默认值 | 作用 |
|---|---|---|
--disable-radix-cache | False | 布尔开关,关掉前缀缓存做 A/B 对比 |
--page-size | 视后端而定 | 页大小,匹配/插入/淘汰的对齐粒度,建议 16 这类 2 的幂 |
--radix-eviction-policy | lru | 淘汰策略,可选lru/lfu/slru/priority |
命中率不用翻日志:服务加--enable-metrics后,指标接口会暴露缓存命中率、evictable_size(可回收量)与protected_size(被锁保护量)等,直接接 Prometheus 即可,实现见 python/sglang/srt/observability/metrics_collector.py。
性能数据:前缀缓存能省多少
| 场景 | 基线(无复用) | RadixAttention | 变化幅度 |
|---|---|---|---|
| 多轮对话 | 1.0x | 3.2x | +220% |
| 批量提示工程 | 1.0x | 4.8x | +380% |
| 代码补全 | 1.0x | 2.7x | +170% |
| 文档摘要 | 1.0x | 5.1x | +410% |
数据口径说明:以上为参考文章复述的对比数据,以"无缓存 prefill 耗时"为基线做相对加速比,实际数值取决于模型、序列长度与前缀重叠率;规律是共享前缀占比越高,收益越大,四舍五入后量级约为 5 倍封顶。
进阶速览:分块前缀缓存与 HiCache 分层
分块前缀缓存(Chunked Prefix Cache):长序列请求被切成多个 chunk 分块 prefill,普通模式下整条前缀要等第一个请求算完才可共享。该特性让"边算边共享"成为可能,目前面向 DeepSeek 等长序列模型;短序列场景若觉得有额外开销,可用--disable-chunked-prefix-cache关闭,相关阈值由环境变量SGLANG_CHUNKED_PREFIX_CACHE_THRESHOLD控制。
HiCache 混合缓存:把 KV 从 GPU 显存扩展到主机内存,形成"设备端 + 主机端"两级。设备端未命中时会先去主机端捞,而不是直接重算。关键参数是--hicache-ratio(主机池/设备池比值,cache 模式默认 2.0)和--hicache-write-policy(可选write_back/write_through/write_through_selective)。对"前缀很长、复现间隔长到会被 LRU 淘汰"的负载,这一层是命中率兜底。
避坑与调优:命中率、锁和命名空间隔离
| 现象 | 原因 | 处理 |
|---|---|---|
| KV 池紧张但缓存迟迟不释放 | 在飞请求持有lock_ref,被锁节点计入protected_size,淘汰器会跳过 | 先查 protected 占比;缩短单请求上下文或降低并发;热前缀多时换slru/priority策略保活 |
| 长序列首个请求耗时内缓存迟迟不可复用 | chunked prefill 下 KV 分块写入,整条前缀写完才完整可共享 | 长序列场景依赖分块前缀缓存特性;短序列负载用--disable-chunked-prefix-cache省掉开销 |
| 换了 LoRA 或采样 salt 后命中率骤降 | RadixKey支持extra_key命名空间,不同命名空间的 token 序列故意不共享节点 | 这是设计特性而非 bug:想共享就保持命名空间一致,想隔离(不同 adapter、不同 RAG 上下文)则正该这样用 |
高频疑问
前缀缓存是默认开启的吗?
是。disable_radix_cache默认 False,起服务即生效。想关闭只需要加--disable-radix-cache。
命中率怎么查?
--enable-metrics启动后拉/metrics,关注前缀缓存命中率以及evictable_size、protected_size两个量。命中率长期偏低且 evictable 接近 0,通常说明缓存容量小于工作集,考虑扩显存预算或上 HiCache。
淘汰策略怎么选?
默认lru对多轮对话够用;请求模式高度重复(少数超热前缀)可试lfu;想给关键租户的前缀更高存活率,用priority按优先级淘汰。
节点分裂会不会造成 KV 重复拷贝?
不会。分裂只调整树上节点的边界与指针,KV 索引按段切分引用,不重复搬运显存数据。
写在最后
SGLang 的前缀缓存把"重复 prefill 同一前缀"从默认行为变成了可感知、可度量、可淘汰的一等公民:基数树负责找前缀,引用锁负责保安全,页对齐负责控碎片,策略化淘汰负责省显存。理解了这条链路,你就有了在真实负载下调命中率和内存水位的全部抓手。接下来值得关注的是跨实例的缓存共享与量化格式 KV 的落地,前缀复用正从单服务内优化走向集群级基础设施。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考