Prhub

#48577 [CPU][Spec Decode] Optimize GDN conv path for speculative decoding

原始 PR 作者 tianmu-li 合并时间 2026-07-27 14:20 文件变更 5 提交数 18 评论 20 代码增减 +767 / -122

执行摘要

AMX C++ 内核优化 CPU 推测解码 GDN 卷积路径,MTP 吞吐提升约 18%

Improve CPU serving performance for Qwen3.5 GDN speculative decoding with MTP by replacing fallback torch conv1d path with cpp implementation when AMX is available.

建议仔细审阅 gdn_attention.py 中 _spec_forward 从逐序列循环到批量内核调用的重构模式,以及状态缓冲区滚动策略。对于在 CPU 上开发推测解码或类似 Mamba 结构内核的工程师具有参考价值。同时注意 num_accepted_tokens 参数的引入如何使推理引擎能够处理不同序列已接受 draft token 数不同的情况。

讨论亮点

审查中主要讨论了以下几点:

  • 1D 连续性问题:bigPYJ1151 指出 non_spec 路径的 state_indices_tensor 来自 metadata.non_spec_query_start_loc,是原始 1D 连续 tensor,无需 .contiguous();tianmu-li 最初认为可能来自 block_table[:,0] 但最终确认并移除。
  • num_acc_cpu 转换简化:bigPYJ1151 建议将 num_acc_cpu 的 to(device,dtype) 转换移至 AMX 路径外避免冗余;tianmu-li 移除转换,只在原生路径转换为 int32。
  • mixed_qkv_spec 连续性:bigPYJ1151 指出 index_select 产出已连续,无需 reshape;tianmu-li 改用 view。
  • 复用现有 helper:fadara01 询问是否可使用 causal_conv1d.py 中的接口减少重复;tianmu-li 表示现有接口不支持 accepted-token/wide-state 语义,并创建 follow-up PR #49650 以扩展该模块,fadara01 认可。

实现拆解

  1. 新增批量卷积内核:在 csrc/cpu/sgl-kernels/conv.cpp 中新增 causal_conv1d_update_multi_kernel_impl 模板函数,接受 num_accepted_tokens 数组,允许每个序列具有不同的已接受 draft token 数量。函数使用 tinygemm_kernel 计算卷积,并通过 memmove/memcpy 高效滚动状态缓冲区。
  2. 更新 C++/Python 绑定:修改 csrc/cpu/torch_bindings.cpp 和 vllm/_custom_ops.py,将 causal_conv1d_update_cpu 的 cache_seqlens 参数替换为 num_accepted_tokens,使新内核可被 Python 层调用。
  3. 重构 Python 推理路径:在 vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py 中重写 _spec_forward 函数。根据 torch.cpu._is_amx_tile_supported() 判断是否可用本地内核:若可用则直接调用 ops.causal_conv1d_update_cpu 批量处理整个批次;否则回退到逐序列的 PyTorch F.conv1d。同时移除多处多余的 .contiguous() 调用。
  4. 消除冗余数据移动:在 _cpu_gdn_attention_spec_aware 中,将 mixed_qkv.contiguous()、b.contiguous()、a.contiguous() 延迟到非 spec 路径,避免过早拷贝;在 _cpu_gdn_attention_nonspec 中移除不必要的 state_indices_tensor.contiguous()。
  5. 扩充测试套件:在 tests/kernels/mamba/cpu/test_cpu_gdn_ops.py 中新增 test_spec_aware_mixed_routing_preserves_token_order、test_spec_aware_nonspec_materializes_state_indices 等多个测试用例,验证 spec 感知路由下 token 顺序、状态索引连续性,以及两阶段调用与单阶段调用的数值一致性。
文件 模块 状态 重要度
csrc/cpu/sgl-kernels/conv.cpp C++ 内核 modified 7.47
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py GDN 注意力 modified 6.75
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py 单元测试 modified 7.24
vllm/_custom_ops.py 自定义 op modified 4.59
csrc/cpu/torch_bindings.cpp 绑定注册 modified 4.56

关键符号

causal_conv1d_update_multi_kernel_impl causal_conv1d_update_cpu _spec_forward _cpu_gdn_attention_spec_aware _cpu_gdn_attention_nonspec test_spec_aware_mixed_routing_preserves_token_order test_spec_aware_nonspec_materializes_state_indices

关键源码片段

csrc/cpu/sgl-kernels/conv.cpp core-logic

新增批量卷积前向模板函数,支持多个 accepted token 同时计算,并优化状态滚动,是性能提升的核心。

// causal_conv1d_update_multi_kernel_impl – 批量处理多 draft token 的卷积前向和状态更新
template <typename scalar_t>
void causal_conv1d_update_multi_kernel_impl(
    scalar_t* __restrict__ out,
    const scalar_t* __restrict__ input,
    scalar_t* __restrict__ conv_states,
    const scalar_t* __restrict__ weight,
    const scalar_t* __restrict__ bias,
    const int32_t* __restrict__ num_accepted_tokens, // 每个序列已接受的 draft 数
    const int32_t* __restrict__ conv_indices,
    bool silu_activation,
    int64_t batch, int64_t dim, int64_t seqlen, int64_t width,
    int64_t state_len, int64_t conv_state_slot_stride) {
  constexpr int64_t BLOCK_N = block_size_n() * 2;
  const int64_t NB = div_up(dim, BLOCK_N);
  AT_DISPATCH_BOOL2(bias != nullptr, has_bias, silu_activation, has_silu, [&] {
    at::parallel_for(0, batch * NB, 0, [&](int64_t begin, int64_t end) {
      int64_t bs, nb;
      data_index_init(begin, bs, batch, nb, NB);
      for (int64_t i = begin; i < end; ++i) {
        const int64_t nb_start = nb * BLOCK_N;
        const int64_t nb_size = std::min(dim - nb_start, BLOCK_N);
        const int32_t conv_state_index = conv_indices[bs];
        const int32_t history_offset = num_accepted_tokens[bs] - 1;
        switch (width << 4 | nb_size >> 4) {
          case 0x42:
            tinygemm_kernel<scalar_t, 4, 32, has_bias, has_silu>::apply(
                input + bs * seqlen * dim + nb_start,
                weight + nb_start * width,
                out + bs * seqlen * dim + nb_start,
                has_bias ? bias + nb_start : nullptr,
                conv_states + conv_state_index * conv_state_slot_stride +
                    history_offset * dim + nb_start,
                true, seqlen, dim, true);
            break;
          case 0x44:
            tinygemm_kernel<scalar_t, 4, 64, has_bias, has_silu>::apply(
                input + bs * seqlen * dim + nb_start,
                weight + nb_start * width,
                out + bs * seqlen * dim + nb_start,
                has_bias ? bias + nb_start : nullptr,
                conv_states + conv_state_index * conv_state_slot_stride +
                    history_offset * dim + nb_start,
                true, seqlen, dim, true);
            break;
          default:
            TORCH_CHECK(false, "Unexpected block size, ", width, " x ", nb_size);
        }
        data_index_step(bs, batch, nb, NB);
      }
    });
  });
  at::parallel_for(0, batch, 0, [&](int64_t begin, int64_t end) {
    for (int64_t bs = begin; bs < end; ++bs) {
      const int32_t conv_state_index = conv_indices[bs];
      const int32_t num_accepted = num_accepted_tokens[bs];
      scalar_t* state = conv_states + conv_state_index * conv_state_slot_stride;
      std::memmove(state, state + num_accepted * dim,
                   (state_len - seqlen) * dim * sizeof(scalar_t));
      std::memcpy(state + (state_len - seqlen) * dim,
                  input + bs * seqlen * dim,
                  seqlen * dim * sizeof(scalar_t));
    }
  });
}

评论区精华

1D tensor 是否需要 contiguous() 正确性

bigPYJ1151 指出 non_spec 路径的 state_indices_tensor 来自 `attn_metadata_i.non_spec_query_start_loc`,是原始 1D 连续 tensor,无需 `.contiguous()`。tianmu-li 最初认为可能来自 `block_table[:,0]` 切片导致不连续,但最终确认该路径下 query_start_loc 是原始 tensor,没有切片操作。

结论:移除不必要的 contiguous() 调用。 · 已解决

num_acc_cpu 转换位置 性能

bigPYJ1151 建议将 `num_acc_cpu` 的 `.to(dtype)` 转换移到 AMX 路径外部,避免 AMX 路径也做转换。tianmu-li 采纳并移除转换,只在 PyTorch 回退路径中做转换。

结论:num_accepted 转换只在回退路径执行,AMX 路径直接使用原始 metadata 并转换为 int32。 · 已解决

mixed_qkv_spec 连续性 style

bigPYJ1151 指出 `mixed_qkv_spec` 由 `index_select` 生成,已经是连续 tensor,无需再次 reshape/reorder。tianmu-li 改为使用 `view`。

结论:改用 view 避免多余操作。 · 已解决

复用 causal_conv1d.py 接口 设计

fadara01 询问是否可以使用 `causal_conv1d.py` 中的现有 helper 减少重复。tianmu-li 表示现有 helper 不支持 accepted-token/wide-state 语义,并创建了 follow-up PR #49650 来扩展该模块。

结论:暂不修改此 PR,创建 follow-up 统一接口。 · resolved_with_followup

风险与影响

  • 性能风险:新 AMX 内核依赖 AMX/AVX512,非 AMX CPU 自动回退到 PyTorch 路径,且有单元测试覆盖回退正确性,风险可控。
  • 正确性风险:批量状态更新使用 memmove/memcpy,若 state_len 与 seqlen 关系计算有误可能损坏状态;但 test_causal_conv1d_fwd_cpu_two_call_split 等测试覆盖了拆分调用与单次调用的一致性。
  • 兼容性风险:op 签名参数名称从 cache_seqlens 改为 num_accepted_tokens,但该 op 仅通过 Python 包装调用,所有内部调用已更新,对外部用户无影响。
  • 安全风险:无。
  • 用户层:CPU 上使用推测解码(MTP)的用户将获得 17-18% 的吞吐提升;其他用户不受影响。
  • 系统层:对非 AMX CPU 无退化;AMX CPU 获得加速。
  • 团队层:引入 follow-up #49650 以进一步统一 conv 接口到 causal_conv1d.py,减少维护重复。
AMX 依赖 状态缓冲区边界敏感 op 签名兼容

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论