读 session_controller.py 与 SessionAwareCache#
这章解决什么问题#
前面的 request lifecycle 已经在 2.3 会话、超时与中止路径 里解释了 session 为什么会改变请求生命周期,调度与内存部分也在 4.5 Runtime checker、SessionAwareCache 与内存不变量 里解释了 SessionAwareCache 为什么要接管 streaming session 的 KV 生命周期。但如果你真正打开源码,仍然会遇到一个更具体的问题:
session 相关实现到底应该从哪几棵树开始读,怎样把它们接起来?
这章的目标,就是把:
session_controller.pysession_aware_cache.py
收成一条稳定的源码阅读路径。
为什么这两棵树必须配对读#
单读 session_controller.py,你会知道:
- session 怎样被打开 / 关闭
- append / replace / drop_previous_output 怎样改变请求链
- streaming session 有哪些限制
单读 session_aware_cache.py,你会知道:
SessionSlot怎样接管req_pool_idxmatch_prefix(...)怎样恢复会话态 KVrelease_session(...)怎样释放 slot 占用的资源
但只有把两者配在一起,你才会真正看清一件事:
session 不只是“请求之间共享逻辑上下文”,它还会改变 KV 所有权与缓存生命周期。
这就是为什么它不是一个轻量 API 特性,而是一条贯穿请求、缓存和回收的系统支线。
一张图:session 在源码里并不是单树问题#
这张图解决的理解障碍是:很多读者会把 session 理解成 SessionController 自己的一棵树,但真正的实现其实横跨请求链和缓存链。
flowchart LR
Open["open_session / close_session"] --> Ctrl["SessionController"]
Ctrl --> Req["Session / SessionReqNode / Req chain"]
Req --> Cache["SessionAwareCache"]
Cache --> Slot["SessionSlot"]
Slot --> KV["req_pool_idx / KV ownership"]图比纯文字多解释的一点是:session 的复杂度不只在“请求如何串起来”,还在“谁拥有 KV,什么时候转交和释放”。
第一层:先抓 SessionController 的三件事#
更稳的阅读顺序,不是从文件头一路扫,而是先抓这三件事:
Session自身怎样表示一条会话Session.create_req(...)怎样把新请求接到已有链上SessionController怎样打开 / 关闭 / 回收会话
只要先抓住这三点,后面 SessionAwareCache 的存在就会变得很自然。
Session 为什么不是普通的请求列表#
Session 最值得先记住的几个字段是:
session_idstreamingtimeoutreq_nodes
这说明它并不是“为每个 session 保存一串 request id”那么简单,而是维护一棵基于 SessionReqNode 的请求树。
也就是说,session 的抽象主语不是线性历史,而是:
- 某条请求接在哪个父请求之后
- 某次 replace / append 会删掉哪些子树
从技术书角度看,这一点特别重要,因为它解释了为什么 session 语义天然会改变请求生命周期,而不只是附加一个 id。
Session.create_req(...) 应该怎么读#
这是 session_controller.py 里最值得重点看的函数。更稳的阅读问题是:
- 当前请求是在 streaming session 还是普通 session
- 它是 append、replace,还是 drop previous output
- 它最后怎样构造成新的
Req
从实现上看,这个函数做的关键工作有:
- 检查 streaming session 是否非法使用 replace / offset / drop_previous_output
- 决定上一个 request node 是谁
- 必要时对旧请求树做
abort()或clear_children(...) - 按上一轮输出拼接新的
origin_input_ids - 最后构造新的
Req(...)
这说明 session 的本质不是“保存上下文”,而是“重新编译下一轮请求的输入与父子关系”。
为什么 streaming session 的限制这么强#
源码里对 streaming session 的限制非常明确:
- 不支持
replace - 不支持
drop_previous_output - 不支持非零
offset
这说明 streaming session 不是普通 session 的一个松散变体,而是一种更严格的运行人格。原因也很明确:
- 普通 session 还能在逻辑上重写请求链
- streaming session 已经开始把输出暴露给外部,再允许任意重写会让内外状态一致性变得很脆弱
这不是“上游偷懒”,而是很典型的 runtime consistency 约束。
SessionReqNode 为什么值得单独记住#
这不是简单的树节点实现,而是 session 对请求历史的真实建模方式。它负责:
- parent / children 关系
- 递归清子树
- 把未完成请求标记为
FINISH_ABORT
这说明 session 生命周期的很多动作不是“删除历史记录”,而是要把一整片请求子树转成 abort 或清理状态。
也就是说,session 本质上让 request lifecycle 从“单请求状态机”扩成了“请求树”。
第二层:SessionAwareCache 为什么是另一半实现#
如果 SessionController 只负责请求链,而不碰 cache,那 session 仍然只是逻辑层功能。但 SessionAwareCache 的存在说明:
session 还会改变 KV 所有权与缓存回收语义。
它是一个 wrapper / decorator,而不是重写全部 prefix cache。这个设计特别值得技术书强调,因为它体现了一种很稳的工程策略:
- 不去撕开 scheduler 主循环
- 先在 prefix cache 边界包一层会话态逻辑
这既保留了原有主线,又给 streaming session 插入了正式扩展点。
SessionSlot 才是 streaming session 的 KV 持有人#
SessionSlot 最值得记住的是:
req_pool_idxkv_committed_lenkv_allocated_lencache_protected_lenlast_node
以及 save_from_req(...) / restore_to_req(...) 两个方向相反的动作。
这说明会话态 KV 并不是仍然永久挂在某个 Req 上。更准确地说:
- request 完成后,slot 可以接管这组 KV
- 下一轮请求进入时,再从 slot 恢复到 request
这就是为什么 session 会真正改变缓存生命周期,而不是只改变输入拼接逻辑。
match_prefix(...) 为什么是 streaming session 最关键的 cache 钩子#
SessionAwareCache.match_prefix(...) 的语义非常清楚:
- 非 streaming 请求:完全 pass-through 给 inner cache
- streaming 请求:优先看当前
session_id是否已有 slot - 如果 slot 持有 KV,就
restore_to_req(req)并直接构造 prefix match 结果
这说明 streaming session 的 prefix 复用,并不是让树缓存自己神奇记住会话,而是通过 slot 主动恢复。
这样设计的好处是边界非常清晰:
- 普通请求不受影响
- streaming session 的特殊性被限定在 wrapper 层
为什么 save_from_req(...) 不会把 slot 立刻清空#
源码注释已经点得很清楚:在 chunked prefill + streaming session 场景下,请求可能因为 budget 不足被 scheduler 拒绝,然后下一轮重试。如果此时 slot 过早清空,就会丢掉幂等恢复能力。
这是一处特别值得写进书里的“反直觉点”:
- 看到 slot 没清,初看像泄漏
- 但它其实是在为 chunked 重试保留恢复点
这种设计只有放在“请求会重试、KV 需要跨轮保存”的背景下才会变得合理。
release_session(...) 真正在释放什么#
这段函数做的不是简单的 del session。它会:
- 释放
last_node上的 lock ref - 计算 slot 持有的 KV 区间
- 把相应 KV indices 还给 allocator
- 把
req_pool_idx放回 free slots
也就是说,会话关闭真正释放的是一整组缓存持有关系,而不只是控制器里的元数据。
这再次说明 session 是 request lifecycle 与 cache lifecycle 的交叉点。
这两棵树对排障有什么直接价值#
session 相关问题很容易被误判成普通请求问题,但其实常常出在这两棵树之一:
- append / replace 行为不符合预期:优先看
Session.create_req(...) - 请求树被过早清空或错误 abort:优先看
SessionReqNode - streaming session 下一轮复用错上下文:优先看
SessionAwareCache.match_prefix(...) - 会话结束后内存不回落:优先看
release_session(...)
换句话说,session 故障不是一类统一问题,而是逻辑链与缓存链两个层次的问题。
如果你要顺着源码读这条支线,推荐顺序是什么#
建议按下面顺序:
SessionSession.create_req(...)SessionController.open/close/_closeSessionSlotSessionAwareCache.match_prefix(...)SessionAwareCache.cache_finished_req(...)SessionAwareCache.release_session(...)
这样读,你会先建立“请求树”的理解,再建立“KV 所有权”的理解,两层会自然接上。
这一层最容易出现的误判#
1. 以为 session 只是给请求附一个 session_id#
实际上它会改变请求树与输入重组逻辑。
2. 以为 SessionAwareCache 只是优化 prefix match#
它真正改变的是 streaming session 的 KV 生命周期。
3. 以为 session 关闭只会删一条控制器记录#
它还会释放 lock、KV 和 req_pool_idx 所有权。
小结#
这一章真正要补齐的,是代码导读里 session 支线的正式源码入口:
SessionController负责把 session 建成一棵请求树SessionAwareCache负责把会话态 KV 生命周期包装进 prefix cache- 两者配在一起,才构成 session 的完整实现
到这里,请求生命周期、调度与内存两部分里反复出现的 session 概念,就终于在源码层真正汇合了。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。