Prhub

#37078 [mem_cache] Move `kv_committed_len` into `ReqKvInfo`

原始 PR 作者 hnyls2002 合并时间 2026-08-30 13:42 文件变更 37 提交数 10 评论 0 代码增减 +117 / -116

执行摘要

将 kv_committed_len 收进 ReqKvInfo,统一 KV 行三段长度记录

PR body 明确说明动机:Move kv_committed_len from Req / SessionSlot into ReqKvInfo, next to cache_protected_len and kv_allocated_len, so the whole [protected, committed, allocated] ladder of a request's KV row is one record. 此前一个请求的 KV 生命周期由散落在多处、且 Req 与 SessionSlot 各持一份的字段描述,save/restore 需要逐字段手工同步,漏掉任何一个字段都会造成 committed/allocated 不一致。收拢后,整个 KV 行的状态可以随一条可整体拷贝的记录流转,这是后续 KV 所有权统一重构(req_pool_idx、Mamba 状态等陆续并入 ReqKvInfo)的基础。

值得快速浏览并作为理解 SGLang KV 所有权模型的起点:ReqKvInfo 的三段式长度阶梯是后续所有 KV 生命周期逻辑(分配、提交、保护、SWA 淘汰)的统一入口。对内存管理模块感兴趣的同学建议精读 schedule_batch.pyReqKvInfo 定义与 streaming_session.pysave_from_req / restore_to_req 模式,它展示了如何用『一条可整体拷贝的记录』替代『多处散落字段的手工同步』。

讨论亮点

该 PR 没有任何 GitHub review 评论(comments_count 与 review_comments_count 均为 0),设计约束主要通过 commit message 与代码注释沉淀:

  • 前序 commit "move cache_protected_len, swa_evict_floor into ReqKvInfo; add swa_dead_lo" 说明这是分步重构,先收内存保护与 SWA 字段,再收 committed 字段。
  • commit "assert session cp fixed after first turn" 把『流式会话首轮之后,受保护前缀不可再移动』固化为 save_from_req 中的断言,防止后续轮次破坏树锁契约。
  • commit "drop dead kv_committed_len attrs from test fakes" 说明迁移完成后,测试假对象上的死属性被一并清理,避免新旧两套字段长期共存造成测试失真。

实现拆解

  1. 字段定义收拢(schedule_batch.py):在 ReqKvInfo dataclass 中新增 kv_committed_len: int = 0,位置夹在 cache_protected_lenkv_allocated_len 之间,注释明确 committed <= allocated 的约束;同时删除 Req.__init__ 里的 self.kv_committed_len = 0,从此该状态只有一处定义。

  2. Req 生命周期读写点迁移(schedule_batch.py)effective_kv_committed_len(strip_thinking_cache 时用 min(kv.kv_committed_len, len(origin_input_ids)) 截断)、reset_for_retractprepare_for_extend(保留 TODO(th4) 不动)、prepare_for_decode(每个 decode 步骤提交 +1)、new_tokens_required_next_decode 及其 spec-v2 版本、mamba_lazy_spec_in_window 全部改为读写 req.kv.kv_committed_len

  3. SessionSlot 简化(streaming_session.py):删除 SessionSlot 上独立的 kv_committed_len 字段;save_from_req / restore_to_req 不再手工复制该字段,改由整份 self.kv = copy.copy(req.kv) 携带。NPU page 对齐分支、_free_tail_trim_overshoot 等同步改读写 slot.kv.kv_committed_len / req.kv.kv_committed_len。同时保留并强化了 cache_protected_len 在首轮后不可变的断言。

  4. 下游子系统同步迁移batch_result_processor.py 的 spec-v2 接受长度累加与 Mamba 边界检测(_resolve_spec_v2_tokens_mamba_prefix_cache_update_mamba_check_track_boundary_mamba_assert_committed_len_lookahead)、disaggregation/decode.py_pre_allocget_new_prebuilt_batchinvariant_checker.py_check_kv_page_invariants(守护 committed<=allocated 不变量)、beam_searchfork.pycoordinator.py(leader 行释放与幸存者同步)、NPU dsv4_allocator.py、flexkv/lmcache 后端、dflash_info_v2.pyallocation_sizing.py 等全部改为 kv.kv_committed_len

  5. 测试与不变量配套test_streaming_session_unit.py_FakeReq 把 committed 从平铺属性移入 kv 子对象,SessionSlot(...) 构造同步调整;test_unified_radix_cache_unittest.py 有 11 处 req.kv_committed_len = kv_len 改为 req.kv.kv_committed_len = kv_lentest_decode_radix_lock_ref.pytest_dllm_fdfo_kv_reuse.pytest_kv_page_invariants.pytest_fork.py 等也同步更新。PR 分支在过程中两次 merge main 解决与主线的冲突。

文件 模块 状态 重要度
python/sglang/srt/managers/schedule_batch.py 请求调度 modified 6.14
python/sglang/srt/session/streaming_session.py 流式会话 modified 5.55
python/sglang/srt/managers/scheduler_components/batch_result_processor.py 结果处理 modified 5.61
python/sglang/srt/disaggregation/decode.py 解码侧 modified 5.57
python/sglang/srt/managers/scheduler_components/invariant_checker.py 不变量检查 modified 4.71
test/registered/unit/mem_cache/test_streaming_session_unit.py 会话测试 modified 4.56
test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py 缓存测试 modified 4.53
python/sglang/srt/beam_search/fork.py 束搜索 modified 4.49

关键符号

effective_kv_committed_len reset_for_retract prepare_for_extend prepare_for_decode save_from_req restore_to_req try_match_prefix _resolve_spec_v2_tokens _mamba_check_track_boundary _pre_alloc _check_kv_page_invariants free_member_rows

关键源码片段

python/sglang/srt/managers/schedule_batch.py core-logic

本 PR 的核心定义处:在 ReqKvInfo 中新增 kv_committed_len 字段,删除 Req 上的平级属性,并迁移 effective_kv_committed_len、reset_for_retract、prepare_for_extend、prepare_for_decode、mamba 窗口判断与页数估算等调度主路径的读写。

@dataclasses.dataclass(slots=True, kw_only=True)
class ReqKvInfo:
    # 设备侧 KV 行状态:请求在前缀缓存之外持有的 KV。
    # 是否持有以 req.req_pool_idx is not None 为准(Req.is_holding_kv)。
​
    # 三段式长度阶梯,描述一行 KV 的生命周期:
    # [0, cache_protected_len) 树缓存拥有(匹配或插入),不可回收
    # [cache_protected_len, kv_committed_len) 内容已提交,可进入前缀缓存
    # [kv_committed_len, kv_allocated_len) 已分配但未提交,可回滚
    cache_protected_len: int = 0 # 树缓存拥有 [0, here)(匹配或插入)
    kv_committed_len: int = 0 # KV 内容提交到 here,满足 committed <= allocated
    kv_allocated_len: int = 0
​
    # SWA 窗口: [swa_dead_lo(page_size), swa_evicted_seqlen) 已经释放
    swa_evict_floor: int = 0 # [0, here) 永远不会被窗口淘汰(prefill 感知 SWA)
    swa_evicted_seqlen: int = 0 # SWA 淘汰游标
​
    def swa_dead_lo(self, page_size: int) -> int:
        # 请求可以自行释放的最低 SWA 位置:高于树拥有前缀与淘汰保护线,
        # 并向上按页对齐。
        lo = max(self.cache_protected_len, self.swa_evict_floor)
        if page_size > 1 and lo > self.cache_protected_len:
            lo = ceil_align(lo, page_size)
        return lo
​
    @property
    def is_released(self) -> bool:
        return self.kv_allocated_len == 0 and self.swa_evicted_seqlen == 0
​
    def mark_released(self) -> None:
        self.kv_allocated_len = 0
        self.swa_evicted_seqlen = 0
python/sglang/srt/session/streaming_session.py core-logic

SessionSlot 删除独立 kv_committed_len 字段,save_from_req / restore_to_req 不再逐字段复制而由整份 ReqKvInfo 拷贝携带;NPU page 对齐、_free_tail、_trim_overshoot 等同步迁移,是本次简化收益最明显的文件。

@dataclass
class SessionSlot:
    """Holds KV state between streaming session turns."""
​
    virtual_node: _VirtualNode = field(default_factory=_VirtualNode)
​
    # KV 池状态:整行 ReqKvInfo 随会话保存,不再单独维护 kv_committed_len
    req_pool_idx: Optional[int] = None
    kv: ReqKvInfo = field(default_factory=ReqKvInfo)
    # ... 其余 mamba 状态字段保持不变
​
    def save_from_req(self, req: Req, is_first: bool):
        """将一个结束请求的 KV 状态保存进 slot。"""
        self.req_pool_idx = req.req_pool_idx
​
        if is_first:
            self.last_node = req.last_node
            self.swa_uuid_for_lock = req.swa_uuid_for_lock
            self.skip_lock_node_ids = req.skip_lock_node_ids
        else:
            # 受保护前缀来自第一个请求的树锁;此后没有新 KV 交给树,
            # 所以后续轮次不得移动它,这里用断言固定该契约。
            assert req.kv.cache_protected_len == self.kv.cache_protected_len
​
        # 整行 KV 记录随 copy.copy 转移;kv_committed_len 不再单独复制,
        # 避免 Req 与 slot 各持一份导致的手工同步遗漏。
        self.kv = copy.copy(req.kv)
​
        self.mamba_pool_idx = req.mamba_pool_idx
        self.mamba_ping_pong_track_buffer = req.mamba_ping_pong_track_buffer
        self.mamba_next_track_idx = req.mamba_next_track_idx
        self.mamba_last_track_idx = req.mamba_last_track_idx
        self.mamba_last_track_seqlen = req.mamba_last_track_seqlen
        self.mamba_branching_seqlen = req.mamba_branching_seqlen
​
        # 所有权移交给 slot;清空 req 的引用,避免后续分配 / 回退路径
        # 把 slot 持有的 mamba 状态误认为自己的。
        req.req_pool_idx = None
        req.kv = ReqKvInfo()
        req.mamba_pool_idx = None
        req.mamba_ping_pong_track_buffer = None
        req.mamba_next_track_idx = None
        req.mamba_last_track_idx = None
        req.mamba_last_track_seqlen = None
        req.mamba_branching_seqlen = None
    # restore_to_req 同理:req.kv = copy.copy(self.kv) 一次带回全部字段

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 机械替换遗漏风险:37 个文件的批量替换依赖替换完整性与测试覆盖。任何遗漏的直接访问 req.kv_committed_len / slot.kv_committed_len 会立即触发 AttributeError(字段已删除),属于『快速失败』型错误;但若遗漏点只在特定硬件或特定路径(如 NPU、flexkv/lmcache 后端、beam search 分支)出现,则可能要到运行期才暴露。
  2. SessionSlot 字段删除影响面streaming_session.py 删除了 SessionSlot.kv_committed_len,所有流式会话外的代码如果直接构造或读取该字段会编译期不可见、运行期报错;save_from_req / restore_to_req 的行为正确性现在完全依赖 copy.copy(req.kv) 携带全部字段。
  3. 不变量守护依赖committed <= allocated 的约束仍由 invariant_checker.py_check_kv_page_invariants 守护,该检查同时覆盖 req 与 slot,迁移后两者都从 kv.kv_committed_len 读取,守护逻辑本身需要保持正确。
  4. 行为敏感点:NPU page 对齐分支(try_match_prefix 中对 req.kv_committed_lenslot.kv_committed_len 同时 min 截断)、mamba lookahead 断言(kv_committed_len - token_seq_len in (0, 1))以及 beam search fork.pyleader.kv_committed_len = startkv_allocated_len = start 的联合重置,都属于改动等价但语义敏感的位置。

对用户无任何可感知的功能变化;对系统而言,KV 行的三段长度(protected/committed/allocated)首次集中在同一条 ReqKvInfo 记录中,消除了 Req 与 SessionSlot 之间逐字段拷贝的一致性隐患。对团队而言,这是 KV 所有权统一重构的中间步骤,后续修改 KV 行状态只需关注 ReqKvInfo 一处定义;同时为 streaming session 的 slot/request 共享同一条记录(PR #37108)铺平了道路。影响面广但风险受控,属于『大范围、低风险、高一致性收益』的重构。

37 文件跨子系统机械替换 核心路径字段迁移 SessionSlot 字段删除影响面广 依赖测试覆盖验证无回归

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论