Prhub

#32219 [MTP] Cut spec-v2 host-seam overhead in hybrid-linear MTP decode

原始 PR 作者 yuan-luo 合并时间 2026-07-28 11:38 文件变更 7 提交数 7 评论 6 代码增减 +553 / -11

执行摘要

削减 MTP spec-v2 主机缝隙开销,GPU 利用率 67% 提升至 95%

spec-v2 overlap 调度下,每步解码由 draft / verify / extend 三个 CUDA graph 组成,graph 之间是 eager “seams”(accept sampling、cache-slot 分配、注意力元数据重建、下一步 draft 输入准备)。event_loop_overlap 依赖“host loop time < GPU step time”才能维持 run-ahead,但四项每步 host 开销把主机重新钉回与 GPU 锁步:KDA 继承的 needs_cpu_seq_lens = True 导致每步 fwd_prepare_d2h_stream.synchronize()(bs=1 MTP 实测中位约 0.5 ms/step);混合包装器丢失 verify 缓冲钩子导致每步新建 bs × max_context_len 的 mask;verify 预填在主机上物化 seq_lens + draft_token_num;mamba replay-prep 每步 5-7 次 aten dispatch。PR body 强调“The host seam is a serial chain — single-point savings do not add up”,必须打包消除;这些改动来自 4×B200 TP4 的 bs=1 MTP 优化战役,最终 GPU 利用率从 67% 到 95%。

值得精读。四个看点:① 对 event_loop_overlap / FutureMap 主机 run-ahead 的分析是理解 SGLang CUDA-graph 调度瓶颈的绝佳材料;② 每个改动独立可回退且附 byte-identical 论证,是性能优化可验收性的范本;③ review 中抓到的 plan-stream 竞态说明此类“搬移 / 融合”改动极易引入时序依赖,值得作为安全检查清单;④ fail-loud(NotImplementedError)与显式门控回退的取舍清晰。

讨论亮点

核心交锋有二。其一(正确性):JustinTong0323 指出 hybrid_linear_attn_backend.py 中 MambaAttnBackendBase.update_verify_buffers_to_fill_after_draft 最初为空实现(pass),而 _replay_metadata 会在 plan 流上把 draft 产出的 retrieve_next_token / retrieve_next_sibling 拷入 captured 缓冲,先于 draft 流完成——空钩子会 replay 过期链接,静默破坏 GDN / KDA 树验证;建议流 join 后重拷并补 plan-stream 开关验收测试。yuan-luo 接受并实现重拷(commit 4710e23),但将 plan-stream e2e 验收推迟到专门的加固轮次。其二(设计):BBuf 建议把内联的 triton kernel 移到 sglang/kernels,yuan-luo 在最终提交 b908e693 完成迁移(kernels/ops/mamba/mamba_state_indices_triton.py)。

实现拆解

变更入口是 spec-v2 调度下 hybrid-linear MTP 的 CUDA-graph 回放与 verify 预填两条热点路径,按“一次提交一个可独立回退项”组织为 4 项优化加 2 个新测试文件:

  1. 移除 KDA 后端的每步 seq_lens D2H 阻塞同步(python/sglang/srt/layers/attention/linear/kda_backend.py):为 KDAAttnBackend 声明类属性 needs_cpu_seq_lens = False。该标志被 FutureMap 的 decide_needs_cpu_seq_lens 读取,决定 resolve_seq_lens_cpu 是否走阻塞的 fwd_prepare_d2h_stream.synchronize()。安全性论证基于:共享 MambaAttnBackendBase 的 KDA 元数据从不读 spec-v2 的 seq_lens_cpu 镜像——replay padding 来自 forward_batch.num_padding,replayssm track-flush mask 读取被 not is_kda 门控;GDN / Mamba2 / ShortConv 兄弟后端早已是 False,故 spec-v2 后端集合的 OR 可保持 False。对其他配置该标志仅被条件读取,惰性无副作用。

  2. 打通 verify 缓冲钩子并修复 plan-stream 竞态(python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py):MambaAttnBackendBase 实现 update_verify_buffers_to_fill_after_draft——topk > 1 且 cuda_graph_bs 非空时,把 EagleVerifyInput 的 retrieve_next_token / retrieve_next_sibling 重拷进 per-bs captured 缓冲;topk <= 1(chain 模式)、eager 路径(cuda_graph_bs 为 None)、dummy / capture 运行(retrieve_next_token 为 None)均为 no-op。这是 review 中发现的正确性问题:_replay_metadata 在 plan 流上拷贝树链接会与 compute 流的 build_tree 竞争,空实现会 replay 过期链接。HybridLinearAttnBackend 新增 get_verify_buffers_to_fill_after_draft(委托 full-attn 子后端,线性侧不消费 mask)和 update(转发两个子后端,未实现者保留 fail-loud NotImplementedError)。效果:draft 阶段直接写 captured verify 缓冲,每步 bs × max_context_len 的 mask 分配消失。

  3. 融合 uniform verify-prep cache-locs 内核(python/sglang/kernels/ops/speculative/cache_locs.py 与 python/sglang/srt/speculative/eagle_utils.py):新增 assign_extend_cache_locs_uniform / assign_extend_cache_locs_uniform_func。verify 预填每行恰好扩展 draft_token_num 个 token,end offset 在 kernel 内算为 start + draft_token_num,输出偏移直接是 pid × draft_token_num,省去主机端 seq_lens + N 的临时张量和 O(bs) 跨行 prefix-sum 加载。eagle_prepare_for_verify 切换到 uniform 变体;NPU / CPU 回退到 end_offset 张量参考路径;非均匀调用点 move_accept_tokens 保持原内核。

  4. 融合 mamba replay-prep state-indices 链(新增 python/sglang/kernels/ops/mamba/mamba_state_indices_triton.py):fused_replay_state_indices 单次 triton launch 完成 mapping gather + -1 哨兵 + 写静态缓冲,并保持参考链“清零 req_pool_indices padding 行”的副作用。_replay_metadata 在 _fused_state_indices_ok(CUDA + 静态 hybrid 池且 v2p 翻译为恒等)且 replayssm 关闭时走快路径;unified 池 / replayssm / 非 CUDA 走原链,行为位级不变。每步主机 dispatch 从 5-7 次降到 1 次 launch。

  5. 测试与验证配套:新增 test_fused_replay_state_indices.py(guard-padded 缓冲上融合内核 vs 参考链位级对比,bs ∈ {1, 2, 7, 32, 33} × num_padding 扫描 × 3 seeds,覆盖非单射 mapping、-1 哨兵、清零副作用与越界保护)和 test_verify_buffer_fixup_hook.py(fixup 钩子刷新与各 no-op 分支)。e2e 在 H20-3e、tp=4 验证 Qwen3.5-35B-A3B + NEXTN MTP(GDN 混合,覆盖第 2-4 项)与 Kimi-Linear-48B-A3B(KDA,覆盖第 4 项 fused replay)输出 byte-identical,spec_verify_ct 完全一致;CI 注册 base-b-kernel-unit 与 base-b 阶段。

文件 模块 状态 重要度
python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py 注意力后端 modified 7.82
python/sglang/kernels/ops/mamba/mamba_state_indices_triton.py Mamba 内核 added 6.59
python/sglang/srt/layers/attention/linear/kda_backend.py KDA 后端 modified 5.63
python/sglang/kernels/ops/speculative/cache_locs.py 投机内核 modified 6.04
python/sglang/srt/speculative/eagle_utils.py 投机验证 modified 5.27
test/registered/kernels/ops/mamba/test_fused_replay_state_indices.py 内核测试 added 7.67
test/registered/attention/unittests/hybrid_linear/test_verify_buffer_fixup_hook.py 后端测试 added 7.25

关键符号

fused_replay_state_indices _fused_replay_state_indices_kernel assign_extend_cache_locs_uniform assign_extend_cache_locs_uniform_func update_verify_buffers_to_fill_after_draft get_verify_buffers_to_fill_after_draft eagle_prepare_for_verify

关键源码片段

python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py core-logic

本 PR 核心源码文件:MambaAttnBackendBase 新增 _fused_state_indices_ok 门控与 _replay_metadata 单 launch 快路径,实现 plan-stream 树链接 fixup 钩子;HybridLinearAttnBackend 新增 get / update_verify_buffers_to_fill_after_draft 转发,消除每步 mask 分配并修复竞态。

# python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py
# _replay_metadata:每次 CUDA-graph replay 前刷新 per-bs state_indices_list。
​
    if self._fused_state_indices_ok and self.replayssm_write_pos_list is None:
        # 单 launch 快路径:mapping gather + padding 哨兵 + 写入静态缓冲,
        # 与参考链位级一致(含清零 req_pool_indices padding 行的副作用)。
        mamba_indices = fused_replay_state_indices(
            req_pool_indices=req_pool_indices,
            mamba_index_mapping=(
                self.req_to_token_pool.req_index_to_mamba_index_mapping),
            out_state_indices=self.state_indices_list[bs - 1],
            valid_bs=bs - int(num_padding),
            total_bs=bs,
        )
    else:
        # 参考链:unified 池(v2p 非恒等)/ replayssm / 非 CUDA 保持原语义。
        req_pool_indices[bs - num_padding:] = 0
        mamba_indices = self.req_to_token_pool.get_mamba_indices(req_pool_indices)
        mamba_indices = self._translate_mamba_indices(mamba_indices)
        mamba_indices[bs - num_padding:] = -1
        self.state_indices_list[bs - 1][:len(mamba_indices)].copy_(mamba_indices)
​
​
# MambaAttnBackendBase.update_verify_buffers_to_fill_after_draft:
# plan-stream 竞态修复。_replay_metadata 在 plan 流上拷贝 draft 产出的树链接,
# 会与 compute 流上的 build_tree 竞争;流 join 后必须重拷,否则 replay 过期链接
# 会静默损坏 GDN / KDA 树验证(review 发现的正确性问题)。
​
    def update_verify_buffers_to_fill_after_draft(self, spec_info, cuda_graph_bs):
        if self.topk <= 1 or cuda_graph_bs is None: # chain 模式 / eager 路径
            return
        if (not isinstance(spec_info, EagleVerifyInput)
                or spec_info.retrieve_next_token is None): # dummy / capture 运行
            return
        bs_without_pad = spec_info.retrieve_next_token.shape[0]
        self.retrieve_next_token_list[cuda_graph_bs - 1][:bs_without_pad].copy_(
            spec_info.retrieve_next_token)
        self.retrieve_next_sibling_list[cuda_graph_bs - 1][:bs_without_pad].copy_(
            spec_info.retrieve_next_sibling)
python/sglang/kernels/ops/mamba/mamba_state_indices_triton.py core-logic

新增的融合内核:把 _replay_metadata 的 5-7 次 aten dispatch 链融合为单次 triton launch,并保持清零 padding 行的关键副作用,是第 4 项优化的载体。

# python/sglang/kernels/ops/mamba/mamba_state_indices_triton.py
# 把 _replay_metadata 里 5-7 次 aten dispatch 的参考链:
# 清零 req_pool_indices padding 行 -> mapping gather -> v2p 翻译(静态池恒等)
# -> -1 哨兵 -> copy_ 进静态缓冲
# 融合成单次 triton launch,消除每步主机分发开销。@triton.jit
def _fused_replay_state_indices_kernel(
    req_pool_indices_ptr, # (total_bs,) int64,静态 replay 缓冲
    mamba_map_ptr, # (req_pool_size,) int32,req 索引到 mamba 槽位映射
    out_ptr, # (total_bs,) int32,state_indices_list[bs - 1]
    valid_bs,
    total_bs,
    BS_UPPER: tl.constexpr,
):
    # BS_UPPER 是 next_power_of_2(total_bs),非 2 的幂尺寸靠 in_range 掩码保护
    offs = tl.arange(0, BS_UPPER)
    in_range = offs < total_bs
    valid = offs < valid_bs
    req = tl.load(req_pool_indices_ptr + offs, mask=valid, other=0)
    idx = tl.load(mamba_map_ptr + req, mask=valid, other=0)
    out_val = tl.where(valid, idx.to(tl.int32), -1) # padding 行写 -1 哨兵
    tl.store(out_ptr + offs, out_val, mask=in_range)
    # 保持参考链副作用:清零 req_pool_indices 的 padding 行。
    # captured 内核会拿该缓冲做 gather,漏清零是延迟出现的非法访存。
    zeros = tl.zeros([BS_UPPER], dtype=req.dtype)
    tl.store(req_pool_indices_ptr + offs, zeros, mask=in_range & (~valid))
​
​
def fused_replay_state_indices(
    *, req_pool_indices, mamba_index_mapping, out_state_indices,
    valid_bs, total_bs,
):
    # 调用方必须保证 v2p 翻译是恒等映射(静态 hybrid 池);
    # unified 池的分配器翻译不是平坦表 gather,必须走参考链。
    _fused_replay_state_indices_kernel[(1,)](
        req_pool_indices, mamba_index_mapping, out_state_indices,
        valid_bs, total_bs,
        BS_UPPER=triton.next_power_of_2(total_bs),
    )
    return out_state_indices[:total_bs]
python/sglang/kernels/ops/speculative/cache_locs.py core-logic

新增 assign_extend_cache_locs_uniform 内核:end offset 在 kernel 内计算、输出偏移直接为 pid × draft_token_num,配合 eagle_utils.py 调用点切换,消除 verify 预填的主机端偏移加法。

# python/sglang/kernels/ops/speculative/cache_locs.py
# uniform 变体:verify 预填路径每行恰好扩展 draft_token_num 个 token,
# 所以 end offset = start + draft_token_num 直接在 kernel 内算,
# 输出偏移 = pid * draft_token_num,省去主机端 seq_lens + N 临时张量
# 和跨行 prefix-sum 加载。@triton.jit
def assign_extend_cache_locs_uniform(
    req_pool_indices,
    req_to_token,
    start_offset,
    out_cache_loc,
    pool_len: tl.constexpr,
    draft_token_num: tl.constexpr,
):
    BLOCK_SIZE: tl.constexpr = 64
    pid = tl.program_id(axis=0)
    kv_start = tl.load(start_offset + pid)
    token_pool = req_to_token + tl.load(req_pool_indices + pid) * pool_len
    out_cache_ptr = out_cache_loc + pid * draft_token_num
    offs = tl.arange(0, BLOCK_SIZE)
    for i in range(tl.cdiv(draft_token_num, BLOCK_SIZE)):
        o = offs + i * BLOCK_SIZE
        mask = o < draft_token_num
        data = tl.load(token_pool + kv_start + o, mask=mask)
        tl.store(out_cache_ptr + o, data, mask=mask)# assign_extend_cache_locs_uniform_func 内判断平台:
# CUDA / HIP / MUSA / XPU 走上面的 triton 内核;
# NPU / CPU 回退到 end_offset 张量参考路径(assign_extend_cache_locs_func)。

评论区精华

plan-stream 树链接竞态:fixup 钩子不能为空实现 正确性

JustinTong0323 指出 MambaAttnBackendBase.update_verify_buffers_to_fill_after_draft 最初是 pass:_replay_metadata 在 plan 流上拷贝 draft 产出的 retrieve_next_token / retrieve_next_sibling,未等 draft 流完成,空钩子会 replay 过期链接并静默破坏 GDN / KDA 树验证;建议流 join 后把当前 spec_info 重拷进 cuda_graph_bs 缓冲,并补 plan-stream 开关验收测试。

结论:yuan-luo 同意并提交 4710e23 实现重拷修复,新增 test_verify_buffer_fixup_hook.py 覆盖刷新与各 no-op 分支;plan-stream e2e 验收因当前无跨流顺序保证,推迟到专门的 plan-stream 加固轮次。 · 已解决

triton 内核位置:建议移入 sglang/kernels 设计

BBuf 在 review 中建议把内联在 hybrid_linear_attn_backend.py 的 triton kernel 移到 sglang/kernels 目录,保持内核与调度层代码分离。

结论:yuan-luo 已处理,最终提交 b908e693 将内核放入 kernels/ops/mamba/mamba_state_indices_triton.py,并同步更新测试导入。 · 已解决

风险与影响

  1. plan-stream 竞态修复无 e2e 覆盖:fixup 钩子依赖“流 join 后重拷”的调用顺序,当前 CI 没有 plan-stream-on + tree-verify hybrid 配置,回归只能靠单元测试兜底。
  2. 融合内核的门控假设:_fused_state_indices_ok 依赖静态 hybrid 池且 translate_mamba_indices 未被覆写(type 比较),未来若子类覆写翻译逻辑会静默落入参考链(安全但失配);融合内核若漏清零 req_pool_indices padding 行,captured 内核会延迟越界——测试用 guard-padded 缓冲覆盖。
  3. needs_cpu_seq_lens = False 的前置条件:KDA 元数据当前不读 seq_lens_cpu 镜像,若后续新增读取路径会静默错误。
  4. uniform cache-locs 假设每行扩展长度相同,只用于 eagle_prepare_for_verify;NPU / CPU 走回退路径,存在双路径维护成本。
  5. 收益集中在低并发(bs=1 到小 batch)与 MTP 场景,高并发受益有限;数值不变性依赖 tp=4 H20 的 byte-identical 验证,Blackwell bundle-level A/B 尚未发布。

用户侧:hybrid-linear(GDN / KDA)模型配合 NEXTN / MTP 在低并发下 TPOT 显著下降,MTP 场景 GPU 利用率从 67% 提升到 95%;无 API / flag / 配置变更,非 hybrid、NPU / CPU、unified 池、replayssm 配置均走原路径且位级不变。系统侧:spec-v2 重叠调度的 run-ahead 深度恢复,verify CUDA-graph 可与下一个 draft_extend 重叠。团队侧:确立“串行缝隙必须打包消除”的优化方法论,位级对比测试成为此类改动的验收标准;本 PR 是 hybrid-linear 优化系列的一部分,Blackwell bundle-level TPOT A/B 后续报告。

核心路径变更 计划流竞态无 e2e 覆盖 新内核依赖静态池假设 多路径回退维护成本 数值不变依赖位级测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论