读 BasePrefixCache 与 radix_cache.py#
这章解决什么问题#
前面的调度与内存章节已经把 prefix match、cache_protected_len、host hit 和 load-back 讲得比较深了,但如果你真正打开源码,仍然会遇到一个很具体的问题:
prefix cache 这条主线到底该从哪两个文件开始读,怎样把“抽象 trait”和“真正的 radix tree 实现”接起来?
这条线最核心的两个入口其实是:
base_prefix_cache.pyradix_cache.py
这章的目标,就是把这两棵树收成一条稳定阅读路径。
为什么这两棵树必须配对读#
单读 base_prefix_cache.py,你会看到:
MatchPrefixParamsMatchResultInsertParamsEvictParamsinc_lock_ref(...)/dec_lock_ref(...)
但你还不知道这些抽象在真实树结构里怎样落地。
单读 radix_cache.py,你会看到:
TreeNodeRadixKeymatch_prefix(...)_match_prefix_helper(...)cache_unfinished_req(...)cache_finished_req(...)
但你又容易在具体实现细节里迷路,看不清哪些是“prefix cache 家族通用接口”,哪些才是 radix tree 的特化行为。
所以更稳的方式是:
- 先用
BasePrefixCache建立抽象边界 - 再用
RadixCache看这套抽象怎样真正落地
一张图:prefix cache 这棵树为什么分成“抽象边界”和“具体实现”两层#
这张图解决的理解障碍是:很多读者会直接钻进 RadixCache.match_prefix(...),结果一开始就被树节点和 split 细节淹没。
flowchart TD
Base["BasePrefixCache\ntrait + params + results"] --> Ops["match / insert / evict / lock"]
Ops --> Radix["RadixCache"]
Radix --> Node["TreeNode / RadixKey"]
Radix --> Reuse["cache_unfinished_req / cache_finished_req"]
Radix --> Evict["evictable / protected / eviction policy"]图比纯文字多解释的一点是:radix_cache.py 不是孤立大文件,它是在实现 BasePrefixCache 这一层统一语义。
第一层:为什么应该先读 BasePrefixCache#
base_prefix_cache.py 最值得先抓住的不是抽象方法列表,而是那几个 NamedTuple / dataclass:
MatchPrefixParamsMatchResultInsertParamsEvictParamsIncLockRefResultDecLockRefParams
这组类型非常有价值,因为它们本质上定义了 prefix cache 家族的问题边界:
- 输入是什么
- 输出是什么
- cache match 需要回哪些信息
- lock 与 eviction 语义怎样被统一表达
也就是说,它在替整本系统定义一套“prefix cache 的公共语言”。
MatchResult 为什么是这条线最重要的抽象#
如果你只记一个对象,最该记的其实是 MatchResult。因为它直接告诉你上游把“prefix 命中”理解成什么:
device_indiceslast_device_nodelast_host_nodehost_hit_lengthmamba_branching_seqlencache_protected_len
这组字段本身已经说明,prefix match 在 SGLang 里不是一个布尔查询,而是一份:
- 当前可用前缀视图
- 节点边界
- 分层命中信息
- 保护边界
的复合结果。
换句话说,MatchResult 本身就已经体现了整本书前两轮补过的那些关键观点。
第二层:RadixKey 为什么比看起来重要#
RadixKey 不只是 token ids 包一层。它还明确携带了:
extra_keyis_bigram
这说明 prefix cache 的逻辑命名空间并不只有“token 前缀”。在实际实现里,命名空间还会被:
- LoRA / adapter id
- cache salt
- 其他不希望共享状态的额外维度
一起决定。
这是非常重要的系统设计点,因为它告诉你:
- 相同 token 前缀不一定天然应该共享
- prefix cache 的命名空间本身就是可扩展的
TreeNode 应该怎么理解#
技术书不必把 TreeNode 每个字段都讲成数据结构课,但有几个字段必须读清楚:
childrenkeyvaluelock_refhost_ref_counterpriority
这几个字段说明节点同时承担三层语义:
- 树结构本身
- 与 KV 索引值的绑定
- 可驱逐 / 被保护 / 主机侧引用的生命周期状态
也就是说,这不是一棵“纯索引树”,而是一棵带缓存所有权语义的树。
match_prefix(...) 为什么会主动 split node#
这是 radix_cache.py 里最值得读者停下来理解的一点。
当匹配结束在某个已存 segment 中间时,系统不会勉强复用模糊边界,而是:
- 主动 split node
- 把边界精确暴露出来
这说明 RadixCache 不是一个只查询不改树的结构。prefix match 自己也会让树更适合后续匹配。
这是一种很成熟的设计:
- 当前多做一次结构细化
- 后面换来更稳定的边界和更便宜的匹配
cache_unfinished_req(...) 为什么最该结合 ScheduleBatch 读#
这段逻辑和单纯的数据结构课差别非常大,因为它直接在服务运行时里改写:
req.prefix_indicesreq.last_nodereq.cache_protected_len
也就是说,RadixCache 不是只维护自己的树,它还会回写请求对象,让 scheduler 之后的预算和 extend 决策建立在新的前缀视图上。
这正是为什么本书前面反复说:
- prefix cache 不是旁路优化
- 它直接参与 batch 成形与请求推进
cache_finished_req(...) 为什么像“最终交账”#
unfinished 路径更像中途更新保护边界;finished 路径则更像最终结清:
- 根据 committed KV 最终插树
- free 重复部分
- free 未对齐尾部
dec_lock_ref(req.last_node)
这说明 finished insertion 的重点并不只是“再写一次树”,而是把:
- 请求
- 树
- allocator
三者之间的所有权关系最终结清。
这是一种很值得技术书专门说透的运行时语义。
inc_lock_ref(...) / dec_lock_ref(...) 为什么要回到 RadixCache 再看一遍#
前面的调度章节已经讲过它们会改:
evictable_size_protected_size_
放回 radix_cache.py 再看一次,你会更清楚一点:
- 这不是抽象 cache trait 的附属操作
- 而是 radix tree 节点沿 parent 链逐层传播的真实保护逻辑
也就是说,lock 保护的并不只是当前叶子,而是整条被请求持有的前缀路径。
这能帮助你真正理解“为什么 eviction 不是简单地删一段 value”。
eviction policy 为什么值得在这章里提一句#
RadixCache 初始化时会根据配置选择:
- LRU
- LFU
- FIFO
- MRU
- FILO
- Priority
- SLRU
这说明 radix tree 本身并不等于某种固定驱逐语义。树结构负责:
- 表达前缀共享
而 eviction policy 决定:
- 哪些叶子先被逐出
从系统阅读角度看,这有一个很重要的启发:
- 不要把“是 radix tree”误读成“驱逐策略天然已经定了”
这棵树对排障有什么直接价值#
如果你遇到:
- prefix reuse 看起来经常失效
protected_size_与evictable_size_变化不合理- 节点似乎命中了,但后面还是多做了不少前向
那么 BasePrefixCache + RadixCache 这组文件就是最稳的第一落点。
因为你在这里能同时确认:
- 抽象输出里到底承诺了什么
- 具体树实现里到底做了什么
这比直接一头扎进 scheduler 或 allocator 更容易先把问题层级分清。
如果你要顺着源码读这条 cache 树,推荐顺序是什么#
建议按下面顺序:
base_prefix_cache.py里的MatchResult/InsertParams/EvictParamsradix_cache.py里的RadixKeyTreeNodematch_prefix(...)cache_unfinished_req(...)cache_finished_req(...)inc_lock_ref(...)/dec_lock_ref(...)- 最后再回到 scheduler 或
ScheduleBatch看这些结果如何被消费
这样读,你先有抽象边界,再进具体实现,最后再回主链,最不容易迷路。
这一层最容易出现的误判#
1. 以为 RadixCache 只是纯数据结构实现#
它同时承担了前缀所有权、保护和驱逐状态语义。
2. 以为 prefix match 的结果就是一段 token 索引#
真正重要的还包括节点边界、host hit、受保护长度等状态。
3. 以为 eviction 策略是树结构天然决定的#
在实现上,树结构和驱逐策略是分开的。
小结#
这一章真正要补齐的,是代码导读里 prefix cache 这棵树的正式源码入口:
BasePrefixCache给了你一套公共语言RadixCache把这套语言真正落成树、节点、锁和驱逐逻辑- 两者配起来读,prefix reuse 这条主线才会从“概念理解”升级成“源码可读”
到这里,调度与内存章节里那条最难的 cache 主线,就终于有了一棵可以真正顺着去读的源码树。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。