Prhub

#27689 [Perf] FlashInfer MLA: remove blocking D2H in spec-decode plan

原始 PR 作者 elvischenv 合并时间 2026-08-13 03:29 文件变更 1 提交数 3 评论 14 代码增减 +127 / -5

执行摘要

移除 FlashInfer MLA 投机解码阻塞 D2H,吞吐提升 27%

PR body 指出 H200 上 DeepSeek-R1 使用 MTP/EAGLE 时"no faster than standard decoding",nsys 显示每次 attention plan() 有 3 次同步 D2H 拷贝,阻塞 host 线程、破坏 CUDA graph 重叠,"the stalls negate the speculation benefit"。根因是 BatchMLAPagedAttentionWrapper.plan() 在收到 GPU 张量时通过阻塞 .to("cpu") 拷贝 qo_indptr / kv_indptr / kv_len_arr,而 spec-decode 的 verify 和 draft-extend 路径仍走 stock plan。

值得精读。核心设计——为 CUDA graph 重放预置 host 数组、安装无同步 plan 函数——与既有 fast_mla_decode_plan 和上游 #28854 保持一致,是消除投机解码中 D2H 阻塞的典型实践。建议关注:

1) 未来移除 seq_lens_cpu 依赖的 GPU-side plan 演进;
2) 补充单元测试覆盖 verify/draft-extend 路径;
3) DCP 回退路径的潜在性能优化空间。

讨论亮点

核心 review 来自 kpham-sgl:

To fundamentally remove D2H sync, we need to remove the dependency of seq_lens_cpu Here are some examples PRs... #28854 #26824

作者在 PR body 的 "Note on the seq_lens_cpu dependency" 中回应:FlashInfer-MLA 的 plan() 是 host-side 的,需要 host 端 indptr/长度来构建 tile schedule,因此无法像 #26824 那样完全移除;合并的 #28854 也采用相同做法,残余的 seq_lens_cpu D2H 是 host-planned attention + 数据依赖投机固有的调度器级成本。

此外 b8zhong 提问“为什么不用 FA3 MLA?这是 FA2 吧”,nvpohanh 回答“我认为 FlashInfer 在 Hopper 上使用 FA3”,未深入展开。kpham-sgl 还要求删除 AI 生成的评论,只保留关键内容。

实现拆解

实现分五步完成:

  1. 分配 pinned host 缓冲区:在 FlashInferMLAForCudaGraphs.__init__ 中(仅 prefill-capable 后端)创建 fast_plan_qo_indptr_cpufast_plan_kv_indptr_cpufast_plan_kv_len_arr_cpu 三个 pinned CPU 张量,供后续零拷贝复用。
  2. 新增无同步 plan 函数:在文件底部新增 fast_mla_prefill_plan,与既有 fast_mla_decode_plan 和合并的 fast_prefill_plan(#28854)对齐,直接接收 host 端数组并传给 _cached_module.plan,同时用非阻塞 copy 刷新 CUDA graph 缓冲区。
  3. 安装到 target-verify wrapper:在 init_forward_metadata_out_graph 的 capture 阶段,当真实 plan() 已填充 _cached_module 后,用 partial(fast_mla_prefill_plan, prefill_wrapper) 替换 verify wrapper 的 plan 方法,之后每次图重放都走无同步路径。
  4. 构建 host 数组:在 _apply_cuda_graph_metadata 的 target-verify 分支,用 seq_lens_cpu[:bs] + ndt 构造 kv_len_arr,用 torch.cumsum 构造 kv_indptr,用等差序列构造 qo_indptr;在 init_forward_metadata 的 eager DRAFT_EXTEND_V2 分支做同样构造,使 plan().to("cpu") 变为 no-op。这些数组仅在 seq_lens_cpuspec_info 存在且非 ragged 时启用,并加 assert 快速失败。
  5. DCP 回退:在 call_begin_forward 中,若 attn_dcp_metadata.dcp_kv_indices 存在(DCP 模式),强制清空 host 数组参数,回退到原 GPU 路径,因为 DCP 的 kv 划分与完整 seq_lens 构建的数组不匹配。

配套测试:未新增单元测试;文档未更新。改动仅一个源码文件,FlashInferMLAIndicesUpdaterPrefill.updatecall_begin_forward 增加 qo_indptr_cpu/kv_indptr_cpu/kv_len_arr_cpu 可选参数,decode、普通 prefill、ragged 路径逻辑保持不变。

文件 模块 状态 重要度
python/sglang/srt/layers/attention/flashinfer_mla_backend.py 注意力层 modified 7.75

关键符号

fast_mla_prefill_plan init_forward_metadata_out_graph init_forward_metadata _apply_cuda_graph_metadata

关键源码片段

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

唯一修改文件,核心逻辑集中在新增无同步 plan 函数、pinned host buffer 分配和两个 forward metadata 路径的改造,是性能收益的直接来源。

# 新增的 fast_mla_prefill_plan:无同步的 BatchMLAPagedAttentionWrapper.plan 替代实现
# 用于 target-verify 的 CUDA graph 重放,与 fast_mla_decode_plan 对应。
# 它直接接收 host 端已知的 indptr/ 长度数组,不再每次重放时从 GPU 拷贝,
# 避免阻塞 host 线程、破坏 graph 重叠调度。
def fast_mla_prefill_plan(
    self,
    qo_indptr_cpu: torch.Tensor,
    kv_indptr_cpu: torch.Tensor,
    kv_indices: torch.Tensor,
    kv_len_arr_cpu: torch.Tensor,
    num_heads: int,
    head_dim_ckv: int,
    head_dim_kpe: int,
    page_size: int,
    causal: bool,
    sm_scale: float,
    q_data_type: torch.dtype,
    kv_data_type: torch.dtype,
) -> None:
    """Sync-free plan for target-verify CUDA graph replay.    Like fast_mla_decode_plan, it hands host-known qo/kv indptr + lengths
    straight to _cached_module.plan (no per-replay device-to-host copy).
    Decode's indices updater writes the cuda-graph buffers in place so its
    fast plan can skip them; verify metadata is freshly built each step, so
    refresh the bound buffers here exactly as stock plan()'s use_cuda_graph
    branch does (host->device / device->device, non-blocking).
    """
    # 更新 wrapper 缓存参数,与 stock plan 保持一致
    self._causal = causal
    self._page_size = page_size
    self._sm_scale = sm_scale
​
    # 非阻塞刷新 CUDA graph 绑定的缓冲区(host -> device 或 device -> device)
    self._qo_indptr_buf.copy_(qo_indptr_cpu, non_blocking=True)
    self._kv_indptr_buf.copy_(kv_indptr_cpu, non_blocking=True)
    self._kv_indices_buf[: len(kv_indices)].copy_(kv_indices, non_blocking=True)
    self._kv_len_arr_buf.copy_(kv_len_arr_cpu, non_blocking=True)
​
    try:
        # host-side plan:FlashInfer 需要 CPU 上的 indptr/ 长度构建 tile schedule
        self._cached_module.plan(
            self._float_workspace_buffer,
            self._int_workspace_buffer,
            self._pin_memory_int_workspace_buffer,
            qo_indptr_cpu,
            kv_indptr_cpu,
            kv_len_arr_cpu,
            num_heads,
            head_dim_ckv,
            causal,
        )
    except Exception as e:
        raise RuntimeError(f"Error in alternate MLA prefill plan: {e}")
# 在 capture 阶段安装无同步 plan(init_forward_metadata_out_graph 内)
# 必须在真实 plan() 填充 _cached_module 之后安装,否则 fast path 无回退能力
if forward_mode.is_target_verify():
    # use sync-free fast_mla_prefill_plan for replay
    prefill_wrapper.plan = partial(fast_mla_prefill_plan, prefill_wrapper)# 在 _apply_cuda_graph_metadata 的 target-verify 分支构建 host 数组
elif forward_mode.is_target_verify():
    # build host indptr/len arrays for target-verify fast plan path
    assert (
        seq_lens_cpu is not None and spec_info is not None
    ), "target-verify cuda-graph replay requires host-resident seq_lens_cpu"
    ndt = spec_info.draft_token_num
    self.fast_plan_qo_indptr_cpu[: bs + 1] = torch.arange(
        0, (bs + 1) * ndt, ndt, dtype=torch.int32
    )
    self.fast_plan_kv_len_arr_cpu[:bs] = seq_lens_cpu[:bs] + ndt
    self.fast_plan_kv_indptr_cpu[1 : bs + 1] = torch.cumsum(
        self.fast_plan_kv_len_arr_cpu[:bs], dim=0
    )
    self.indices_updater_prefill.update(
        req_pool_indices[:bs],
        seq_lens[:bs],
        seq_lens_sum,
        prefix_lens=None,
        prefill_wrapper_paged=self.prefill_cuda_graph_metadata[
            self._verify_graph_key(bs, spec_info)
        ],
        use_ragged=False,
        spec_info=spec_info,
        qo_indptr_cpu=self.fast_plan_qo_indptr_cpu[: bs + 1],
        kv_indptr_cpu=self.fast_plan_kv_indptr_cpu[: bs + 1],
        kv_len_arr_cpu=self.fast_plan_kv_len_arr_cpu[:bs],
    )

评论区精华

如何从根本移除 D2H sync 设计

kpham-sgl 指出要根本消除 D2H sync 必须移除 seq_lens_cpu 依赖,并附上 #28854 和 #26824 作为示例。

结论:作者回应 FlashInfer-MLA 的 plan() 是 host-side 的,无法完全移除 seq_lens_cpu;#28854 也采用相同方式,残余 sync 是调度器级固有成本,移除是独立的后端级改动。 · 已解决

移除 AI 生成的评论 style

kpham-sgl 要求删除 AI 生成的评论,只保留重要内容。

结论:作者后续清理了评论(PR 中未见 AI 评论残留)。 · 已解决

为什么不用 FA3 MLA? question

b8zhong 疑问为何不用 FA3 MLA,认为这是 FA2;nvpohanh 认为 FlashInfer 在 Hopper 上使用 FA3。

结论:讨论未深入,FlashInfer 后端可能自动选择 FA3,但当前实现不依赖具体 FA 版本。 · unresolved

风险与影响

  1. 缺少单元测试:PR 未新增任何测试,仅依赖 CI 和实测。fast_mla_prefill_plan 是无同步、无 device-readback fallback 的路径,一旦 host 数组构造错误(例如 seq_lens_cpuspec_info 不一致)会在重放时产生静默错误结果,虽有 assert 但覆盖不全。
  2. seq_lens_cpu 残余同步:每次迭代仍有 D2H 拷贝 seq_lens_cpu,虽然非阻塞(如 nsys 显示 160 次 sync),但仍是性能瓶颈,未完全消除。
  3. DCP 回退影响:DCP 场景强制回退到 GPU 方案,功能不变但性能未优化,若 DCP + speculation 组合被使用可能仍有 D2H 阻塞。
  4. 核心路径变更:改动位于 attention 后端核心逻辑,涉及 CUDA graph capture/replay 的 plan 安装时序,若 capture 与 replay 顺序变化可能触发断言。
  5. FA2/FA3 讨论未决fast_mla_prefill_plan 基于 FlashInfer 现有 plan API,若未来切换到 FA3 MLA,该函数可能失效或需要适配。

影响范围集中在使用 FlashInfer MLA 后端 + MTP/EAGLE 投机解码的 DeepSeek 等模型(尤其 H200 等 Hopper 平台),实测吞吐提升 26.7%,TPOT 下降 21%。对标准 decode、普通 prefill、ragged 路径完全无影响;对 DCP + speculation 场景功能不变但性能未优化。团队可从该模式中复用 pinned host buffer + 无同步 plan 的设计,消除类似投机路径的调度开销。

缺少单元测试覆盖 核心路径变更 依赖 seq_lens_cpu 残余同步 DCP 回退性能未优化

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论