Prhub

#32887 [Perf] Fast-path chain-style draft token organization in multi-layer EAGLE

原始 PR 作者 hnyls2002 合并时间 2026-07-30 17:55 文件变更 5 提交数 3 评论 3 代码增减 +71 / -66

执行摘要

为 chain-style draft 添加快速路径,避免每次 decode 启动 kernel

PR body 指出 chain-style drafts 下 topk 始终为 1,parent_list 和 topk_index 是常量,每次 decode 重复执行 slice/topk/sort/gather/cat 是无谓开销。返回预分配常量可节省 kernel 启动和计算资源。

建议精读,尤其是 _rebuild_topk1_chain_buffersdraft_forward 中快速路径的设计模式:将编译期可知的常量预分配并缓存,在推理热点处用简单的形状检查替换复杂内核。这一模式可推广到其他存在运行时不变量的推测解码路径。

讨论亮点

本 PR 没有 review 评论或实质性讨论;作者自行合并。提交历史显示设计演进:第一版在 multi-layer 类内实现预分配,第二版将缓冲区上提到基类并移除 per-bs 缓存,第三版清理注释和冗余初始化。

实现拆解

  1. 基类添加公共方法base_spec_worker.py):在 EagleDraftWorkerBase 中新增类属性 _topk1_parents_prealloc_topk1_score_indices_prealloc(均为 Optional[torch.Tensor]),以及 _rebuild_topk1_chain_buffers 方法。该方法在 topk==1 时基于 speculative_num_stepsmax_bs 预生成常量张量(parents_prealloc[-1,0,...,S-2] 重复到 [max_bs, steps]score_indices_prealloc[0,...,S-1] 重复);当 topk!=1 时直接返回。

  2. 子类删除重复实现eagle_worker_v2.pystandalone_worker_v2.py):移除子类中自行定义的 _rebuild_topk1_chain_buffers 方法和手动置 None 的初始化语句,统一调用基类方法。

  3. multi-layer 入口添加初始化和快速路径multi_layer_eagle_worker_v2.py):在 __init__ 末尾调用 self._rebuild_topk1_chain_buffers();重写 draft_forward 方法:检查 parents_prealloc is not None 且输入 topk_index 形状匹配,若满足则直接切出前 bs 行返回,否则调用 _draft_forward_organize 走完整组织逻辑。

  4. 提取组织逻辑multi_layer_eagle_worker_v2.py):将原有的 cat/topk/sort/gather 逻辑移动到 _draft_forward_organize 方法中,并在最后调用 organize_draft_results(从 eagle_utils 导入)。

  5. 辅助函数注释eagle_utils.py):为 organize_draft_results 添加维度说明注释,便于理解张量形状。

文件 模块 状态 重要度
python/sglang/srt/speculative/multi_layer_eagle_worker_v2.py 推测解码 modified 7.29
python/sglang/srt/speculative/base_spec_worker.py 推测解码 modified 7.13
python/sglang/srt/speculative/eagle_worker_v2.py 推测解码 modified 7.09
python/sglang/srt/speculative/standalone_worker_v2.py 推测解码 modified 4.98
python/sglang/srt/speculative/eagle_utils.py 推测解码 modified 4.54

关键符号

_rebuild_topk1_chain_buffers _draft_forward_organize organize_draft_results

关键源码片段

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

核心改动:添加快速路径入口和提取组织逻辑,关联符号 _draft_forward_organize

def draft_forward(self, forward_batch: ForwardBatch):
    # ... ( 前面计算 topk_p, topk_index, hidden_states)
​
    # Chain-style (topk=1, one token per draft step, all of them selected):
    # _draft_forward_organize's slice/cat/topk/sort/gather is the identity on
    # topk_index, and parent_list is the constant [-1, 0, .., S-2] per row.
    parents_prealloc = self._topk1_parents_prealloc
    if (
        parents_prealloc is not None
        and topk_index.shape[1] == self.speculative_num_steps
        and topk_index.shape[0] <= parents_prealloc.shape[0]
    ):
        bs = topk_index.shape[0]
        # Fast path: return cached constants directly
        return (
            parents_prealloc[:bs],
            self._topk1_score_indices_prealloc[:bs],
            topk_index, # unchanged
        )
​
    # Slow path: perform the general tree organization
    return self._draft_forward_organize(topk_p, topk_index, hidden_states)def _draft_forward_organize(
    self,
    topk_p: torch.Tensor,
    topk_index: torch.Tensor,
    hidden_states: torch.Tensor,
):
    # Original logic with cat/topk/sort/gather, now factored out
    # ... (score_list/token_list/parents_list building)
    return organize_draft_results(
        score_list, token_list, parents_list, self.speculative_num_draft_tokens
    )
python/sglang/srt/speculative/base_spec_worker.py core-logic

基类新增预分配缓冲区和重建方法,所有子类共享

def _rebuild_topk1_chain_buffers(self) -> None:
    # For topk=1 the draft tree degenerates to a chain, so parent_list and
    # top_scores_index are runtime-invariant. Must be rebuilt after any
    # change to speculative_num_steps / speculative_num_draft_tokens.
    if self.topk != 1:
        return
    # _override_worker_state can set both directly, bypassing the hook that
    # pins this relation; the fast path is only valid when it holds.
    assert self.speculative_num_draft_tokens == self.speculative_num_steps + 1, (
        "topk=1 requires speculative_num_draft_tokens == speculative_num_steps + 1, "
        f"got {self.speculative_num_draft_tokens} and {self.speculative_num_steps}"
    )
    num_steps = self.speculative_num_steps
    sa = self.server_args
    decode_max_bs = (
        sa.cuda_graph_config.decode.max_bs
        if sa.cuda_graph_config is not None
        else None
    )
    max_bs = max(
        decode_max_bs or 0,
        sa.max_running_requests or 0,
        1,
    )
    # A single-step chain has no parent entries (slow path drops the last
    # step). repeat (not expand): the kernel reads these as contiguous.
    parent_width = num_steps if num_steps > 1 else 0
    self._topk1_parents_prealloc = torch.arange(
        -1, parent_width - 1, dtype=torch.long, device=self.device
    ).repeat(max_bs, 1)
    self._topk1_score_indices_prealloc = torch.arange(
        num_steps, dtype=torch.long, device=self.device
    ).repeat(max_bs, 1)

评论区精华

快速路径条件假设 设计

PR body 中指定快速路径仅对 chain-style (topk=1, num_draft_tokens == num_steps+1) 生效;代码通过断言和 shape 检查保证安全,且当条件不满足时回退到一般组织逻辑。

结论:已实现为在条件满足时返回预分配常量,否则回退到完整组织逻辑。 · 已解决

风险与影响

  1. 预分配容量风险:缓冲区大小基于 cuda_graph_config.decode.max_bs 等配置,若实际 batch size 超过该值,代码通过 shape[0] <= parents_prealloc.shape[0] 检查后不会进入快速路径(安全回退),不会崩溃。
  2. 条件假设风险:快速路径依赖 topk==1num_draft_tokens == num_steps+1_rebuild_topk1_chain_buffers 内部有 assert 检查后者,但若运行时动态修改(如 _override_worker_state)导致条件被绕过,快速路径可能返回错误常量。但当前无运行时修改路径。
  3. 缺少测试覆盖:本 PR 没有添加或修改测试用例,快速路径的回归风险未被自动化验证。
  4. 性能退化可能:若缓存张量未被正确移至设备或形状不一致,快速路径会跳过(回退到慢路径),无性能收益但不会出错。

仅影响 multi-layer EAGLE 场景且 topk=1 的 draft_forward 调用。对一般 tree 模式(topk>1)无影响。每个 decode 步骤减少至少一次 topk、sort、gather 等 GPU kernel 启动,预期降低延迟。对公共基类的修改也影响 EagleDraftWorkerStandaloneDraftWorker,但它们同样受益于代码复用。

缺少测试覆盖 核心路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论