Prhub

#17260 [Feature] [Ngram spec] Support ngram spec v2

原始 PR 作者 SYChen123 合并时间 2026-06-10 17:46 文件变更 13 提交数 66 评论 44 代码增减 +323 / -502

执行摘要

支持 ngram 猜测解码 v2,启用 overlap 调度

支持 ngram 猜测解码的 overlap 调度(v2),以缩短连续解码 batch 之间的 CPU 气泡,提升吞吐和延迟。相关讨论见 issue #11762 和 #21052。

值得精读:此 PR 展示了如何将 ngram 猜测解码平滑融入现有的 spec v2 overlap 框架,设计决策值得学习,尤其是通过 Mixin 复用 EAGLE 的 pipeline、以及对非连续接受 token KV cache 的移动策略。关注点:move_accepted_tokens_to_target_kvcache 的语义、max_tree_depth 的动态推导。
合并后建议:跟进 return_logprobs 支持,并补充 NPU/AMD 测试。

讨论亮点
  1. Overlap pipeline 设计:hnyls2002 质疑 ngram 没有 draft extend 阶段如何消除 CPU 气泡,SYChen123 解释 ngram v2 将 batching 与 draft 重叠,并通过分离 GPU 和 CPU 操作实现。
  2. 准确性调试:SYChen123 发现 gsm8k 准确率下降,跟踪到 req.output_ids 在 overlap 调度下不完整,以及将 prev_tokens 插入 ngram trie 导致准确率骤降,最终定位根本问题是停止检查逻辑——v2 未正确触发 stop string,导致模型继续生成。
  3. accept-length 问题:Ratish1 发现 _prepare_draft_tokens 中传给 NgramCorpus.batch_get 的上下文长度仅基于 origin_input_ids + output_ids,未包含 pending 的 prev_tokens,导致 trie 查询上下文不完整。
  4. 性能测量:SYChen123 提供 nsys profile 显示 bubble gap 从 4.95ms(v1)降至 3.96ms(v2),并附 mock 压测结果(仅接受 1 token 时 TPOT 仍降低 4%)。
  5. 代码清理:gemini-code-assist[bot] 多次建议移除中文注释、调试 print、注释掉的代码块,并修复类型提示问题。

实现拆解

实现分为以下步骤:

  1. 继承 EAGLE v2 MixinNgramVerifyInput 改为继承 EagleDraftInputV2MixinEagleVerifyInputV2Mixin,从而复用 spec-v2filter_batchmerge_batchprepare_for_decode 等方法。移除原有的 prepare_for_verify_fill_requests 等 v1 专用方法,大幅减少代码量。
  2. 整合 NGRAMWorker:移除独立的 ngram_worker_v2,将 v2 逻辑(如 _prev_decode_rids 集合、enable_overlap 标志、move_accept_tokens_to_target_kvcache 调用)直接合并进 NGRAMWorker__init__ 中新设 self.enable_overlapself.req_to_token_pool 等属性。
  3. 修改草稿准备与验证完成回调_prepare_for_speculative_decoding 中根据是否 overlap 走不同分支;on_verify_complete_cpu 处理 logprob 计算和 KV cache 移动。move_accept_tokens_to_target_kvcache 用于将非连续接受的 token 的 KV cache 移动到正确 slot。
  4. 适配通用验证采样:在 eagle_info_v2.pysample() 中,根据 batch.spec_algorithm.is_ngram() 动态调整 max_tree_depth(ngram 无深度限制)和 topk(设为 -1 表示不启用 topk 约束)。
  5. 配置与内存分配调整arg_groups/speculative_hook.py_handle_ngram 移除 server_args.disable_overlap_schedule = True,并自动根据 draft_token_numtopk 计算 speculative_num_stepsmem_cache/common.pyget_alloc_len_per_decode 添加 is_ngram 判断,使其在 page_size > 1 时也能使用平坦分配公式。
  6. 测试与兼容性:保留 test_spec_ngram.pytest_spec_ngram_extra.py 的 v2 测试,并额外添加 no-overlap(同步 v2)变体。v1 路径完全移除,现有用户需升级配置(去掉手动 --disable-overlap-schedule 等)。
文件 模块 状态 重要度
python/sglang/srt/speculative/ngram_info.py 猜测解码 modified 8.84
python/sglang/srt/speculative/ngram_worker.py 猜测解码 modified 8.2
python/sglang/srt/speculative/eagle_info_v2.py 猜测解码 modified 6.12
python/sglang/srt/arg_groups/speculative_hook.py 参数配置 modified 5.84
python/sglang/srt/speculative/spec_info.py 猜测解码 modified 5.4
python/sglang/srt/mem_cache/common.py 内存缓存 modified 5.35
test/registered/spec/test_spec_ngram.py 测试 modified 4.46
test/registered/spec/test_spec_ngram_extra.py 测试 modified 4.8
python/sglang/srt/managers/overlap_utils.py 调度器 modified 4.79
python/sglang/srt/managers/schedule_batch.py 调度器 modified 4.35
python/sglang/srt/model_executor/model_runner.py 执行器 modified 4.3
python/sglang/srt/model_executor/runner/decode_cuda_graph_runner.py 执行器 modified 4.3

关键符号

NgramVerifyInput.__init__ NgramVerifyInput.get_spec_adjust_token_coefficient NgramVerifyInput.generate_attn_arg_prefill NgramVerifyInput.filter_batch NgramVerifyInput.merge_batch NGRAMWorker.__init__ NGRAMWorker._prepare_for_speculative_decoding NGRAMWorker._prepare_draft_tokens NGRAMWorker.on_verify_complete_cpu NGRAMWorker.forward_batch_generation EagleVerifyInputV2Mixin.sample _handle_ngram (in speculative_hook) get_alloc_len_per_decode (in common.py) supports_spec_v2 (in spec_info.py)

关键源码片段

python/sglang/srt/speculative/ngram_info.py dependency-wiring

核心数据结构 NgramVerifyInput 重构:移除 v1 方法,继承 EAGLE v2 Mixin,添加 V2 属性 (future_indices, new_seq_lens, accept_tokens, accept_lens),实现 filter_batch、merge_batch 等 v2 必备方法。

# file: python/sglang/srt/speculative/ngram_info.pyclass NgramVerifyInput(SpecInput, EagleDraftInputV2Mixin, EagleVerifyInputV2Mixin):
    """Ngram 验证输入,通过 Mixin 复用 EAGLE v2 的 overlap 调度基础设施。"""
​
    def __init__(
        self,
        draft_token: torch.Tensor = None,
        custom_mask: torch.Tensor = None,
        positions: torch.Tensor = None,
        retrieve_index: torch.Tensor = None,
        retrieve_next_token: torch.Tensor = None,
        retrieve_next_sibling: torch.Tensor = None,
        draft_token_num: int = None,
        grammar: BaseGrammarObject = None,
        # V2 overlap 专属字段
        future_indices: Optional[torch.Tensor] = None,
        new_seq_lens: Optional[torch.Tensor] = None,
        accept_tokens: Optional[torch.Tensor] = None,
        accept_lens: Optional[torch.Tensor] = None,
    ):
        super().__init__(SpecInputType.NGRAM_VERIFY)
        self.draft_token = draft_token
        self.custom_mask = custom_mask
        self.positions = positions
        self.retrieve_index = retrieve_index
        self.retrieve_next_token = retrieve_next_token
        self.retrieve_next_sibling = retrieve_next_sibling
        self.draft_token_num = draft_token_num
        self.grammar = grammar
​
        # V2 overlap 数据:存储上一轮结果供下一轮草稿使用
        self.future_indices = future_indices
        self.new_seq_lens = new_seq_lens
        self.accept_tokens = accept_tokens
        self.accept_lens = accept_lens
​
        # 推导设备,兼容 custom_mask 为 None(v2 构造时可能仅有 new_seq_lens)
        self.device = (
            custom_mask.device if custom_mask is not None else new_seq_lens.device
        )
​
    def filter_batch(self, new_indices: torch.Tensor, has_been_filtered: bool = True):
        """按调度器过滤后的索引剪裁 spec_info 张量。"""
        if self.future_indices is not None:
            self.future_indices = self.future_indices[new_indices]
        if self.new_seq_lens is not None:
            self.new_seq_lens = self.new_seq_lens[new_indices]
        # accept_tokens 是扁平的一维张量,需要先 reshape 再剪裁
        self.accept_tokens = self.accept_tokens.reshape(-1, self.draft_token_num)[
            new_indices, :
        ]
        self.accept_tokens = self.accept_tokens.flatten()
        self.accept_lens = self.accept_lens[new_indices]
​
    def merge_batch(self, spec_info: NgramVerifyInput):
        """合并两个 spec_info(用于调度器合并 batch)。"""
        if self.future_indices is not None:
            assert spec_info.future_indices is not None
            self.future_indices = torch.cat(
                (self.future_indices, spec_info.future_indices), dim=0
            )
        if self.new_seq_lens is not None:
            assert spec_info.new_seq_lens is not None
            self.new_seq_lens = torch.cat(
                (self.new_seq_lens, spec_info.new_seq_lens), dim=0
            )
        self.accept_tokens = torch.cat(
            (self.accept_tokens, spec_info.accept_tokens), dim=0
        )
        self.accept_lens = torch.cat(
            (self.accept_lens, spec_info.accept_lens), dim=0
        )
python/sglang/srt/speculative/ngram_worker.py core-logic

工作线程核心改动:集成 spec v2 逻辑,包括 enable_overlap、内存池获取、_prev_decode_rids 集合、草稿准备 v2 分支、验证完成回调中调用 move_accept_tokens_to_target_kvcache 和 compute_spec_v2_logprobs。

# file: python/sglang/srt/speculative/ngram_worker.pydef _prepare_for_speculative_decoding(self, batch: ScheduleBatch):
    """准备 ngram 猜测解码的草稿和验证输入。"""
    ...
    # 构造 ngram 树掩码、位置等(原有逻辑)
    ...
    # 是否走 overlap v2 路径:需使用 spec-v2 风格的 NgramVerifyInput
    # (带 accepted tokens 传递)
    if self.enable_overlap and batch.is_spec_v2:
        # 从上一轮 batch 中取出 accept_tokens / accept_lens
        # 存储到 spec_info 供下一轮草稿使用
        ...
        # 此处调用 move_accept_tokens_to_target_kvcache
        # 将非连续接受的 token 的 KV cache 移动到正确的 slot
        ...
    else:
        # 传统 v1 路径(已删除,目前全部走 v2)
        passdef on_verify_complete_cpu(
    self, batch: ScheduleBatch, result: GenerationBatchResult
) -> GenerationBatchResult:
    """验证完成后的 CPU 回调:计算 logprob 并准备下一轮草稿所需的 accept 信息。"""
    verify_input: NgramVerifyInput = batch.spec_info
    ...
    # 将 accept_tokens 信息存入 spec_info,供 _prepare_for_speculative_decoding 下一轮使用
    accept_tokens = ...
    accept_lens = ...
    verify_input.accept_tokens = accept_tokens
    verify_input.accept_lens = accept_lens
    ...
    # 调用 compute_spec_v2_logprobs 代替此前的 add_output_logprobs_for_spec_v1
    ...
    return result

评论区精华

Overlap pipeline 设计(CPU 气泡消除) 设计

hnyls2002 询问 ngram 没有 draft extend 阶段如何避免 CPU 气泡;SYChen123 解释 ngram v2 将 batching(同步点)与草稿生成重叠,并通过 GPU 与 CPU 操作分离实现。

结论:当前方案已可减少气泡,但完全消除仍需后续步骤;hnyls2002 建议先行合并当前改动,后续继续优化。 · 已解决

准确性调试:gsm8k 精度下降 正确性

SYChen123 发现 v2 的 gsm8k 准确率相比 v1 下降,debug 后定位到两个原因:1) overlap 调度下 `req.output_ids` 不完整(未包含上一轮 accepted tokens)导致草稿上下文缺失;2) 将 pending tokens 插入 ngram trie 导致准确率骤降。最终发现问题出在停止检查逻辑 v2 未正确触发 stop string。

结论:通过不在 ngram trie 中插入 pending tokens 并正确实现停止检查,精度恢复正常。 · 已解决

accept-length 上下文不足 正确性

Ratish1 指出 `_prepare_draft_tokens` 中传递给 `NgramCorpus.batch_get` 的上下文字符串长度仅基于 `origin_input_ids + output_ids`,未包含 `prev_tokens`,导致 trie 查询上下文不完整。建议修正。

结论:SYChen123 采纳建议,修正后 accept-length 恢复。 · 已解决

代码清理与类型安全 style

gemini-code-assist[bot] 多次建议移除中文注释、调试 print、注释掉的代码块,并修正类型提示(如 NgramVerifyInput 的 __init__ 中 device 推导可能因两个参数均为 None 而失败)。

结论:SYChen123 在后续提交中逐步清理,但仍有少部分中文注释残留;最终版本已移除。 · 已解决

logprobs 支持缺失 测试

在实现过程中,开发者指出 `return_logprobs` 仍未完全支持(使用 `compute_spec_v2_logprobs` 但部分场景不兼容)。

结论:标记为 TODO,当前版本不阻塞合并,后续修复。 · unresolved

风险与影响

  1. 回归风险:v1 路径被完全移除,所有 ngram 用户必须升级;若存在未覆盖的边界情况(如 page_size > 1topk > 1 的组合),可能触发 CUDA illegal memory access(虽已修复)。
  2. 性能退化:overlap 调度下 CPU 开销仍然存在(batching 同步),但 profile 显示改善。极端负载下可能因 move_accepted_tokens_to_target_kvcache 引入额外 kernel 启动开销。
  3. Logprobs 支持不完整:PR 使用 compute_spec_v2_logprobs 代替旧的 add_output_logprobs_for_spec_v1,但开发者在讨论中指出 return_logprobs 仍未完全支持(标记为 TODO)。
  4. 适配性风险:NPU/AMD 平台未充分测试,assign_extend_cache_locs_func 可能返回 int32 而非预期的 int64 类型。

对用户:使用 --speculative-algorithm NGRAM 的用户会自动启用 v2(overlap 调度),无需额外配置。如果之前手动指定了 --disable-overlap-schedule,需要移除该参数才能享受 v2 提升。建议进行回归验证。
对系统:KV cache 分配逻辑变更,get_alloc_len_per_decode 现在对 ngram 使用平坦分配,影响内存预分配。eagle_info_v2.py 的 sample 方法为 ngram 动态调整 max_tree_depthtopk,对非 ngram 路径无影响。
对团队:代码量大幅减少(由 +323/-502 变为净减少 179 行),维护性改善。测试覆盖了基本功能,但缺少对 return_logprobs 的测试。

核心路径变更 数据契约调整 KV cache 移动新增 kernel 回归风险(v1 移除) logprobs 待支持

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论