Prhub

#35412 [Fix] Land the decode mamba checkpoint depth on the tree page under DCP

原始 PR 作者 kpham-sgl 合并时间 2026-08-21 03:15 文件变更 9 提交数 6 评论 9 代码增减 +60 / -19

执行摘要

修复 DCP 下 decode mamba checkpoint 深度落空树页网格的静默错误

PR body 明确指出:捐赠的 mamba checkpoint 只有在 radix 树能命名的深度才可复用,而 cache_finished_req / cache_unfinished_req 会用 page_aligned(tree_page) 对插入键取整,因此落在树页网格外的 checkpoint 会被挂到前一个节点——它的 state 却已覆盖越过该节点的 token,后续请求匹配该节点时从中途恢复线性注意力递归并重新消费这些 token,表现为“Silent, no assert, wrong output”。#34808 修复了 extend 路径的 mamba_checkpoint_grid(tree_page),但明确声明 decode 仍使用自己的 --mamba-track-interval 网格,而该网格在 DCP 下并不落在树页上:守卫 assert mamba_track_interval % view.page_size == 0 只针对配置页,DCP 下树实际分配在 page_size × dcp_size,故 break condition 为 mamba_track_interval % (page_size × dcp_size) != 0。两个已发布的 Kimi-K3 B300 recipe(TestKimiK3B300Balanced 与 TestKimiK3B300MegaMoE)恰好命中坏行:树页 512 而 decode 每 256 打点,每个 256 的奇数倍命名的深度都没有节点承载。

值得精读。核心看点:用 lcm 派生网格同时满足 chunk、树页、用户间隔三个约束且不污染 ServerArgs 原始值;对检查点深度(checkpoint depth)与 flush 节奏(flush/write cadence)两类读者做刻意区分;以及“缓存复用结构完全不变、只改变挂载 state”的静态论证方法。建议与 #34808 一起阅读,理解同一不变量在 extend 与 decode 两侧的实现差异;同时关注 PR 遗留的解码半程精度验证缺失,后续应有跟进 PR 补齐。

讨论亮点

PR 无实质 review 评论(ispobock 直接 APPROVED),核心论述集中在 PR body 与提交信息中:

  • “A donated mamba checkpoint is only reusable at a depth the radix tree can name... Silent, no assert, wrong output.”——点明问题的严重性在于完全静默。
  • “The insert-time guard that would have caught this was deferred to #34760 and #34780 — both closed, never merged”——解释了为何 main 上没有任何检查能拦住这个错位。
  • “What changes is which state is attached: previously a state that had already consumed 256 more tokens than the node it hung on.”——静态论证缓存复用结构不变,只改变挂载的 state。
  • commit fa6bc760 记录了一个真实踩坑:“The batch handed to the result processor is the result-queue batch.copy(), whose field list is scoped to what process_batch_result needs and does not carry tree_cache — so batch.tree_cache.page_size was None there and raised on the...”,随后改为从 processor 自身的 self.tree_cache 读取。
  • 作者在 body 中明确留白:“Accuracy Tests Not yet measured — this is why the PR is a draft.”,解码半程的 teacher-forced logprob 对比未完成,合并时仍以此状态合入。

实现拆解

  1. 新增派生网格函数:在 python/sglang/srt/runtime_context.py 的 mamba_checkpoint_grid 旁新增 mamba_track_grid(tree_page),对 mamba_checkpoint_grid(tree_page)(即 mamba_cache_chunk_size() 与 tree_page 的 lcm)与 get_exec().mamba.mamba_track_interval 再取一次 lcm。这样 decode 捐赠深度同时满足三个约束:chunk 对齐(extend 前缀依赖 _force_track_h 的断言)、树页网格(DCP 下为 page_size × dcp_size)、用户配置的 track interval。函数为纯推导,不修改 server_args 中操作员填写的原始值。
  2. 调度与 spec 读取点替换:schedule_batch.prepare_for_decode、spec_utils 的 spec_prepare_for_decode / commit_mamba_states_after_verify / _verify_commit_step_indices、dspark_worker_v2._commit_target_mamba_states_after_verify、dflash_worker_v2._update_target_mamba_state_after_verify 全部从 get_exec().mamba.mamba_track_interval 改为 mamba_track_grid(batch.tree_cache.page_size),这些位置都持有 batch,可直接拿到树页句柄,与 extend 路径的取数方式一致。
  3. batch_result_processor 边界点改造:_mamba_prefix_cache_update(known_boundary 断言)、_mamba_check_track_boundary、_mamba_lazy_spec_update 三处改用 mamba_track_grid(self.tree_cache.page_size)。原因是递给 processor 的 batch 是 result-queue 的 copy(),字段列表只覆盖 process_batch_result 所需,不携带 tree_cache,直接读 batch.tree_cache.page_size 会得到 None 并抛异常(commit fa6bc760 专门修复此坑)。hybrid_linear_attn_backend 的 ReplaySSM force-flush 掩码与 Inkling sconv 保留原始 mamba_track_interval,因为它们是 flush/write 节奏而非 checkpoint 深度;由于 lcm 保证派生网格是原始间隔的倍数,每个快照步骤仍是 flush 步骤,但触发步骤从“同一组”变为“嵌套集合”。
  4. 测试配套:test_mamba_checkpoint_depth.py 新增 TestMambaTrackGrid 三个 CPU 用例(树页加宽向上取整、间隔已对齐保持不变、结果始终落在 chunk 上);test_batch_result_processor_mamba_boundary.py 为手工构造的 ScheduleBatch 与 processor double 补 tree_cache;test_ngram_mamba_verify_update.py 因 spec_utils 不再导入 get_exec,把 patch 目标从 mamba_track_interval 改为 mamba_track_grid。CI 上 unit 测试 rerun 通过,但 e2e test_kimi_k3_b300.py 最后一次 rerun 失败(8-gpu-b300 ❌),合并前未给出解释,作者预告的 cold-vs-warm 解码半程精度对比也未执行。
文件 模块 状态 重要度
python/sglang/srt/runtime_context.py 运行上下文 modified 6.04
python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py 投机解码 modified 5.91
python/sglang/srt/managers/scheduler_components/batch_result_processor.py 结果处理 modified 5.88
python/sglang/srt/speculative/spec_utils.py 投机解码 modified 5.47
python/sglang/srt/managers/schedule_batch.py 调度器 modified 5.07
python/sglang/srt/speculative/dflash_worker_v2.py 投机解码 modified 5.23
test/registered/unit/managers/test_mamba_checkpoint_depth.py 测试配套 modified 6.01
test/registered/unit/managers/test_batch_result_processor_mamba_boundary.py 测试配套 modified 4.3
test/registered/unit/spec/test_ngram_mamba_verify_update.py 测试配套 modified 3.79

关键符号

mamba_track_grid _commit_target_mamba_states_after_verify _update_target_mamba_state_after_verify _verify_commit_step_indices commit_mamba_states_after_verify spec_prepare_for_decode _mamba_prefix_cache_update _mamba_check_track_boundary _mamba_lazy_spec_update prepare_for_decode

关键源码片段

python/sglang/srt/runtime_context.py core-logic

新增 mamba_track_grid(tree_page),是整个修复的网格推导入口,对 checkpoint depth、tree page、track interval 三者取 lcm

# mamba_cache_chunk_size(): 系统实际使用的 mamba 状态缓存粒度,
# 取模型 mamba chunk size 与 page_size 的最大值,并缓存到 ServerArgs。
def mamba_cache_chunk_size() -> int:
    return get_server_args().mamba_cache_chunk_size
​
​
# mamba_checkpoint_grid(): extend 路径捐赠 checkpoint 的深度网格。
# DCP 会把树页加宽到 page_size * dcp_size,所以这里必须传入树实际
# 分配的页面,而不是配置里的 page_size,否则推导会重复一份已经
# 住在 cache builder 里的谓词。
def mamba_checkpoint_grid(tree_page: int) -> int:
    return math.lcm(mamba_cache_chunk_size(), tree_page)
​
​
# mamba_track_grid(): decode 路径(--mamba-track-interval)的等价网格。
# decode 捐赠的深度除了要落在 chunk 与树页网格上,还必须是用户配置
# 间隔的倍数——lcm 保证三者同时成立,且不修改 ServerArgs 里操作员
# 填写的原始值,后续 flush/write 节奏类读者仍可读原始间隔。
def mamba_track_grid(tree_page: int) -> int:
    return math.lcm(
        mamba_checkpoint_grid(tree_page),
        get_exec().mamba.mamba_track_interval,
    )
python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py dependency-wiring

DSPARK 验证路径在 verify 后提交 mamba 状态,是受影响 recipe TestKimiK3B300MegaMoE 的直接修复点

def _commit_target_mamba_states_after_verify(
    self,
    *,
    batch: ScheduleBatch,
    seq_lens_pre_verify: torch.Tensor,
    seq_lens_post_verify: torch.Tensor,
    commit_lens: torch.Tensor,
) -> None:
    # 只处理链式布局(topk == 1);树形布局需要共享 spec_utils 的索引映射
    assert get_spec().speculative_eagle_topk in (None, 1)
    attn_backend = self.target_worker.model_runner.attn_backend
​
    last_correct_step_indices = commit_lens.to(torch.int64) - 1
    mamba_steps_to_track = None
​
    if batch.mamba_track_indices is not None:
        # 关键修复点:DCP 下树页被加宽,直接使用 mamba_track_interval
        # 会让 checkpoint 深度落不到树节点上;改用派生的
        # mamba_track_grid,并从 batch 的 tree_cache 拿真实树页
        mamba_track_interval = mamba_track_grid(batch.tree_cache.page_size)
        to_track_mask = (
            seq_lens_pre_verify // mamba_track_interval
            != seq_lens_post_verify // mamba_track_interval
        )
        tracking_point = (
            seq_lens_post_verify // mamba_track_interval * mamba_track_interval
        )
        # 定位 verify 步序列中第一次跨过网格的那一步
        to_track_ith = torch.clamp(tracking_point - seq_lens_pre_verify - 1, min=0)
        can_track_mask = to_track_mask & (
            to_track_ith < commit_lens.to(to_track_ith.dtype)
        )
        mamba_steps_to_track = torch.where(
            can_track_mask,
            to_track_ith.to(torch.int64),
            torch.full_like(to_track_ith, -1, dtype=torch.int64),
        )
​
    # 把最后接受的 verify 步骤对应的 KDA/mamba 状态写入持久缓存
    attn_backend.update_mamba_state_after_mtp_verify(
        last_correct_step_indices=last_correct_step_indices,
        mamba_track_indices=batch.mamba_track_indices,
        mamba_steps_to_track=mamba_steps_to_track,
        model=self.target_worker.model_runner.model,
        req_pool_indices=batch.req_pool_indices,
    )
test/registered/unit/managers/test_mamba_checkpoint_depth.py test-coverage

新增 TestMambaTrackGrid 三个 CPU 用例,覆盖树页加宽、间隔不变、chunk 对齐三种关键行为,与 #34808 的 extend 用例并列

class TestMambaTrackGrid(unittest.TestCase):
    # 通过 override_server_args 临时注入配置,验证 mamba_track_grid
    # 的三种关键行为:树页加宽时向上取整、间隔已对齐时保持不变、
    # 推导结果始终落在 mamba cache chunk 网格上。
    def _grid(self, *, interval: int, tree_page: int, chunk: int = CHUNK) -> int:
        with get_context().override_server_args(
            mamba_track_interval=interval, _mamba_cache_chunk_size=chunk
        ):
            return mamba_track_grid(tree_page)
​
    def test_widened_tree_page_rounds_the_interval_up(self):
        # 树页 512 而间隔 256:lcm 会把它抬到 512,checkpoint 深度才能落树
        self.assertEqual(self._grid(interval=256, tree_page=512), 512)
​
    def test_interval_already_on_the_tree_page_is_untouched(self):
        # 非 DCP 或树页等于间隔时,两个网格原本重合,结果保持不变
        self.assertEqual(self._grid(interval=256, tree_page=256), 256)
        self.assertEqual(self._grid(interval=256, tree_page=128), 256)
        self.assertEqual(self._grid(interval=256, tree_page=64), 256)
​
    def test_grid_stays_on_the_chunk_size(self):
        # chunk 成为主导约束:192 与 64 对齐但 128 未对齐时,取 128 的倍数
        self.assertEqual(self._grid(interval=192, tree_page=64, chunk=128) % 128, 0)

评论区精华

解码半程准确度测试尚未测量 测试

作者在 PR body 中明确说明:“#34808 shipped a cold-vs-warm teacher-forced logprob comparison on a live Kimi-Linear dcp8 server for the prefill half; the decode half deserves the same evidence, and this PR currently argues from page alignment alone.” 并预告 8×B300 前后对比是下一步。

结论:合并时仍未提供解码半程的精度对比证据,仅以页面对齐的静态推导作为依据。 · 待处理

result-queue batch.copy() 不携带 tree_cache 导致 AttributeError 正确性

commit fa6bc760 记录:处理器收到的 batch 是 result-queue 的 batch.copy(),字段列表只覆盖 process_batch_result 所需,不携带 tree_cache,因此 batch.tree_cache.page_size 为 None 并抛异常。

结论:改为从 processor 自身的 self.tree_cache 读取 page_size,测试 double 同步补 tree_cache。 · 已解决

CI rerun:unit 测试最终通过,e2e test_kimi_k3_b300.py 失败 测试

issue 评论显示两次 /rerun-test 后 unit 测试(test_batch_result_processor_mamba_boundary、test_mamba_checkpoint_depth 等)通过,但最后一次 /rerun-test test/registered/models_e2e/test_kimi_k3_b300.py 在 8-gpu-b300 上返回 ❌(workflow 32325747992),合并前未见失败原因说明。

结论:e2e 失败在合并时未解决,与本 PR 解码半程精度验证缺失相互叠加,构成主要遗留风险。 · unresolved

风险与影响

  1. 解码半程正确性证据缺失:PR 自述未完成 #34808 同级别的 cold-vs-warm 精度对比,论证仅基于页面对齐的静态推导;合并前 CI 上 test_kimi_k3_b300.py 最后一次 rerun 失败(workflow 32325747992),两者叠加使修复效果缺乏端到端实证。
  2. flush 语义变化:hybrid_linear_attn_backend.py:403 注释原本保证 force-flush 掩码与 track 打点“fire on the same steps”,改动后变为嵌套步骤集合,ReplaySSM 快照写入频率减半;作者断言每个快照步骤仍是 flush 步骤,但这依赖 lcm 网格是原始间隔倍数这一性质,未来若有人改动 ReplaySSM 掩码的取数来源,该不变性会被破坏。
  3. checkpoint 稀疏化:若 mamba_track_interval 与实际树页、chunk 不存在约数关系,lcm 会显著放大网格,使 decode 打点变稀疏;本 PR 场景(64/512/256)仅翻倍到 512,但在更极端配置下可能有空间利用率下降。
  4. 测试补丁脆弱:test_ngram_mamba_verify_update.py 直接 patch mamba_track_grid 绑定,并依赖 spec_utils 不再导入 get_exec;后续重构若恢复该导入或改名,测试会以 AttributeError 形式失败。
  5. 上游守卫缺失仍在:#34760 / #34780 两个 insert-time mamba 深度检查均未合入,未来新的 page/dcp/interval 组合仍可能静默错位,本 PR 只是从源头让网格对齐,而非在插入点兜底。

用户侧:修复 Kimi-K3 B300 DCP8(HiCache 与 DSPARK 两条 recipe)下 decode 阶段 radix 缓存恢复导致的重放 token、错误输出的问题,受影响配置的 ping-pong 快照写入减半是附带收益;非 DCP 以及 page 64 × dcp 4 等原本对齐的配置完全无行为变化。系统侧:所有 mamba 混合架构的 decode checkpoint 深度统一由 mamba_track_grid 派生,后续新增读取点只需传入 tree_cache 即可获得一致性;batch_result_processor 对 self.tree_cache 的依赖成为新的隐式约束,测试 double 必须同步补齐。团队侧:暴露了 DCP 加宽树页后既有配置守卫失效的隐患,并提示“flush 节奏”与“checkpoint 深度”两类读者需区分对待。

核心调度与验证路径变更 解码半程准确度证据缺失 e2e 测试合并前失败 flush 触发步骤集合变化 上游 insert-time guard 仍缺失

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论