Prhub

#29616 [Spec] Frozen-KV MTP: delay target KV binding to pool init + reset stale draft out_cache_loc

原始 PR 作者 kpham-sgl 合并时间 2026-06-30 02:57 文件变更 2 提交数 1 评论 7 代码增减 +21 / -28

执行摘要

延迟 Frozen-KV MTP 目标池绑定并重置 out_cache_loc

Frozen-KV MTP 在调度器中有一个特殊处理,即在构造 draft worker 之前分配目标 KV 池,这破坏了正常的初始化顺序。此外,draft 前向传播中携带了预填充阶段的 out_cache_loc,导致在长提示场景下崩溃。该 PR 合并了这两个修复,使 trtllm_mha + hybrid-SWA + NVFP4 路径可用。

该 PR 修复了关键 bug,设计选择清晰合理(延迟绑定模式已在其他地方验证),值得精读。重点关注 frozen_kv_mtp_worker_v2.py 中的延迟绑定模式和 out_cache_loc 重置的改动。

讨论亮点

审查者 ronhuafeng 指出该 PR 与 #29563 几乎完全重叠,两个 PR 都移除了调度器的 Frozen-KV MTP 预分配特殊处理并将绑定延迟到 alloc_memory_pool()。主要区别在于 #29563 还包含一个针对性回归测试和 memory_pool_config is None 守卫。维护者 kpham-sgl 决定合并此 PR 的更小表面。

实现拆解

  1. 延迟目标 KV 池绑定:在 frozen_kv_mtp_worker_v2.py__init__ 中,移除对 target_worker.get_memory_pool() 的调用,将 req_to_token_pooltoken_to_kv_pool_allocatordraft_pool_config 初始化为 None;不再将 req_to_token_pool 等参数传递给 TpModelWorker.__init__;移除 __init__ 中的 _bind_kv_context() 调用。
  2. 在 alloc_memory_pool 中绑定:将池绑定逻辑移动到 alloc_memory_pool() 方法中,接收调度器传入的 req_to_token_pooltoken_to_kv_pool_allocatormemory_pool_config,构建 draft_pool_config 并调用 _bind_kv_context()
  3. 移除调度器特殊处理:在 scheduler.pyinit_model_worker 中,移除 if self.spec_algorithm.is_frozen_kv_mtp(): self.init_target_memory_pool() 分支,使 Frozen-KV MTP 遵循正常的 init_tp_model_worker → maybe_init_draft_worker → init_memory_pools 路径。
  4. 重置 out_cache_loc:在 frozen_kv_mtp_worker_v2.pydraft() 方法中,在 topk 扩展后将 forward_batch.out_cache_loc 设置为 None,因为 frozen draft 从不写入 KV,设置 None 可让填充和 SWA 写入目标跳过该槽位。
文件 模块 状态 重要度
python/sglang/srt/speculative/frozen_kv_mtp_worker_v2.py 推测解码 modified 6.99
python/sglang/srt/managers/scheduler.py 调度器 modified 5.6

关键符号

__init__ alloc_memory_pool draft init_model_worker

关键源码片段

python/sglang/srt/speculative/frozen_kv_mtp_worker_v2.py core-logic

核心改动:延迟目标 KV 池绑定到 alloc_memory_pool,重置 draft 路径上的 out_cache_loc。

# frozen_kv_mtp_worker_v2.py 中的核心变更def __init__(self, ..., target_worker: TpModelWorker):
    # ... 其他初始化 ...
​
    # 原来的代码在这里调用了 target_worker.get_memory_pool(),
    # 但此时目标池尚未分配,导致构造必须提前分配目标池。
    # 现在的改动:将这些字段初始化为 None,推迟到 alloc_memory_pool 再绑定。
    self.req_to_token_pool = None
    self.token_to_kv_pool_allocator = None
    self.draft_pool_config: Optional[MemoryPoolConfig] = None
​
    # 调用 TpModelWorker.__init__ 时不再传递 pool 参数,
    # 因为此时池尚不存在。
    TpModelWorker.__init__(self, ..., is_draft_worker=True)
​
    # 注意:原来这里的 _bind_kv_context() 调用也被移除了,
    # 因为它依赖目标池,而目标池尚未分配。
    self.kv_context: Optional[FrozenKVMTPContext] = None
​
​
def alloc_memory_pool(self, memory_pool_config, req_to_token_pool, token_to_kv_pool_allocator):
    # 延迟绑定:在调度器分配了目标池后,通过此方法传入池对象。
    self.req_to_token_pool = req_to_token_pool
    self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
​
    # 构建一个虚拟的 draft_pool_config,max_total_num_tokens 固定为 64,
    # 因为 frozen draft 实际上不分配自己的 KV 池。
    self.draft_pool_config = MemoryPoolConfig(
        max_total_num_tokens=64,
        max_running_requests=memory_pool_config.max_running_requests,
    )
​
    # 调用父类的 alloc_memory_pool 完成池注册。
    TpModelWorker.alloc_memory_pool(
        self,
        memory_pool_config=self.draft_pool_config,
        req_to_token_pool=req_to_token_pool,
        token_to_kv_pool_allocator=token_to_kv_pool_allocator,
    )
​
    # 现在可以绑定 KV 上下文了,因为目标池已分配。
    if hasattr(self.draft_model_runner.model, "bind_frozen_kv_context"):
        self._bind_kv_context()
​
​
def draft(self, batch: ScheduleBatch):
    # ... 其他逻辑 ...
    self._expand_for_topk_draft(forward_batch)
​
    # 关键修复:frozen draft 从不写入 KV 缓存,因此将 out_cache_loc 设为 None。
    # 下游的 fill_from 和 SWA 写入目标在收到 None 时会跳过该槽位,
    # 从而避免将预填充的 out_cache_loc 错误地复制到解码大小的缓冲区中。
    forward_batch.out_cache_loc = None
​
    # ... 继续执行 draft forward ...
python/sglang/srt/managers/scheduler.py core-logic

移除 Frozen-KV MTP 的特殊提前池分配逻辑,简化初始化流程。

# scheduler.py 中 init_model_worker 的变更def init_model_worker(self):
    # 1. 加载模型权重
    self.init_tp_model_worker()
​
    # 删除的代码:原来这里针对 Frozen-KV MTP 提前分配了目标池,
    # 因为旧版的构造函数依赖目标池已存在。
    # if self.spec_algorithm.is_frozen_kv_mtp():
    # self.init_target_memory_pool()
​
    # 2. 初始化 draft worker(此时目标池尚未分配,但已不再依赖)
    self.maybe_init_draft_worker()
​
    # 3. 分配所有 worker 的 KV 缓存池(包括目标池和 draft 池)
    self.init_memory_pools()
​
    # 4. 后续初始化流程保持不变
    self.init_all_attention_backends()
    self.init_all_cuda_graphs()
    # ...

评论区精华

与 PR #29563 的重叠 设计

ronhuafeng 指出该 PR 与 #29563 几乎完全相同,都移除了调度器的特殊处理并延迟绑定。差异在于 #29563 包含回归测试和 memory_pool_config is None 守卫。

结论:维护者决定继续合并此 PR 的更小表面,并计划后续添加测试。 · 已解决

风险与影响

回归风险较低:延迟绑定模式已在 EAGLEWorkerV2 中验证,且该 PR 通过了现有的 Frozen-KV MTP 测试和 Gemma-4 31B 额外测试。但缺少针对 trtllm_mha + hybrid-SWA 路径的 CI 测试(仅手动验证),存在未来重构时回归的风险。重置 out_cache_locNone 依赖于下游算子正确处理 None,若后续引入新的算子路径可能遗漏此检查。

对用户:修复了在使用 Gemma-4 与 MTP 和 HiCache 时的崩溃问题。对系统:消除了调度器中的特殊处理,简化了初始化流程。对团队:为未来 trtllm_mha + SWA 配置的测试铺平了道路。

核心路径变更 缺少针对 trtllm_mha 的 CI 测试

关联 Issue

#28264 [Bug] SGLang + Gemma w/ MTP + 4L40s + HiCache Config throws Error: " output with shape [1] doesn't match the broadcast shape [276] "
#29021 Delay Frozen-KV MTP target KV binding until pool init

完整报告

参与讨论