Prhub

#46090 [CPU][Spec Decode] Support DFlash speculative decoding for GDN models on CPU

原始 PR 作者 guybd 合并时间 2026-07-13 12:16 文件变更 5 提交数 14 评论 15 代码增减 +755 / -10

执行摘要

CPU 后端 GDN 模型 DFlash 投机解码支持

该 PR 基于 #44029(CPU 上 DFlash 投机解码),进一步将 DFlash 支持扩展到 GDN 混合模型。之前 CPU 后端无法运行 DFlash 投机解码:DFlash 输入扩展依赖 GPU/Triton,CPU GDN 的 conv/SSM kernel 拒绝投机解码形状,CPU attention backend 缺少预计算/存储 DFlash 上下文 KV cache 的钩子。因此需要改造 GDN attention 路径并新增 spec kernel。

该 PR 的设计决策值得学习,尤其是通过宽 conv 缓存实现状态回滚、SSM 多槽索引以及 C++ kernel 的并行/串行拆分。建议阅读 cpu_gdn_attention_core 的分支逻辑和 spec kernel 的 clamp 处理。推荐精读。

讨论亮点

在 review 中,depthfirst-app[bot] 通过静态分析发现 kernel 中 num_accepted_ptr[bi] 为 0 时 acc-1 下溢导致 OOB 读取(高严重度),开发者随后添加了 std::max(acc - 1, 0) 进行 clamp,与 GPU 端 guard 一致。另外,bigPYJ1151 建议调整函数命名(_cpu_gdn_attention_nonspec_legacy_cpu_gdn_attention_nonspec)并保留原始注释,以及指出 rearrange_mixed_qkv 的结果已经连续,新增的 contiguous() 多余,开发者均接受并修改。Depthfirst-app 还指出了 initial_state_sourcespec_state_indices 缺乏形状验证,其中 initial_state_source 在后续提交中补充了检查,但 spec_state_indices 仍缺少校验。

实现拆解

  1. 投机解码模式检测:在 cpu_gdn_attention_core 入口,通过 conv 缓存的时间维度是否大于 width-1 判断是否启用了投机解码。如果未启用,则调度到原有非投机路径 _cpu_gdn_attention_nonspec;否则进入感知投机解码的 _cpu_gdn_attention_spec_aware 路径。

  2. Conv 状态处理:在 spec-aware 路径中,conv 状态使用宽滚动缓冲区。_conv_buffer_view 将每个 token 的切片索引映射到缓冲区视图,_unpacked_conv_weight 引用提前保存的未打包卷积权重(见步骤 4),通过 PyTorch 原生卷积计算,并将结果写回缓存。

  3. SSM 状态多槽回滚_ssm_state_view 根据 spec_state_indices 将 SSM 状态映射到多个槽位,然后对每个 draft token 顺序执行 gated delta 规则更新。核心 kernel 为新增的 fused_sigmoid_gating_delta_rule_update_spec_cpu,它接受 num_accepted_tokensspec_state_indices,内部并行化到序列后串行迭代 draft token,每个 token 的状态更新后写入对应槽位,实现 GPU 等价的多槽回滚语义。

  4. 权重预打包调整:在 vllm/model_executor/layers/utils.pydispatch_cpu_unquantized_gemm 中,当遭遇卷积权重(非 2D)时,在 AMP 打包前保存一份未打包的权重副本到 layer._cpu_unpacked_conv_weight,供 spec-aware 路径使用(因为 AMX kernel 无法处理宽缓存形状,需退化为原生卷积)。

  5. C++ kernel 绑定与注册:在 csrc/cpu/sgl-kernels/fla.cpp 中实现 fused_sigmoid_gating_delta_rule_update_spec_kernel_impl,在 csrc/cpu/torch_bindings.cpp 中注册为自定义 op,在 vllm/_custom_ops.py 中添加 Python 包装函数。同时修复了 review 中发现的 num_accepted=0 时 OOB 读取问题。

文件 模块 状态 重要度
vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py GDN 注意力 modified 7.72
csrc/cpu/sgl-kernels/fla.cpp CPU 内核 modified 7.28
vllm/_custom_ops.py 内核绑定 modified 6.14
csrc/cpu/torch_bindings.cpp 算子注册 modified 5.12
vllm/model_executor/layers/utils.py 模型工具 modified 5.02

关键符号

cpu_gdn_attention_core _cpu_gdn_attention_nonspec _cpu_gdn_attention_spec_aware _spec_forward fused_sigmoid_gating_delta_rule_update_spec_cpu fused_sigmoid_gating_delta_rule_update_spec_kernel_impl _conv_buffer_view _ssm_state_view

关键源码片段

vllm/model_executor/layers/mamba/ops/cpu/gdn_attention.py infrastructure

核心逻辑变更:添加投机解码感知的分支路径,实现 conv 状态回滚和 SSM 多槽更新,是 PR 的主设计文件。

def cpu_gdn_attention_core(layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out):
    # GDN attention 核心入口
    # 根据 conv 层缓存的时间维度判断是否启用了投机解码
    width = layer.conv1d.weight.size(-1)
    conv_cache = layer.kv_cache[0]
    # 判断缓存宽度是否超过非投机时的宽度 (width-1)
    if is_conv_state_dim_first():
        state_len = conv_cache.size(-1)
    else:
        state_len = conv_cache.size(-2)
    spec_decode_cache = state_len > (width - 1)
​
    if not spec_decode_cache:
        # 非投机模式:使用原始 AMX / torch 实现
        _cpu_gdn_attention_nonspec(layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out)
        return
​
    # 投机解码模式:调用感知投机解码的实现
    _cpu_gdn_attention_spec_aware(layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out, width, state_len)
​
​
def _cpu_gdn_attention_spec_aware(layer, attn_metadata_i, mixed_qkv, b, a, core_attn_out, width, state_len):
    # 投机解码感知的 GDN attention 实现
    # conv 状态使用宽缓冲区视图,SSM 状态通过多槽索引实现回滚
    conv_weight = layer._cpu_unpacked_conv_weight # 提前保存的未打包权重
    spec_indices = attn_metadata_i.spec_conv_indices
    for t in range(q_len):
        # 从宽缓存中切片当前 token 的 conv 状态
        conv_view = _conv_buffer_view(conv_cache, spec_indices, t, width)
        # 执行卷积
        conv_out = F.conv1d(conv_view, conv_weight, ...)
        # 写回缓存
        conv_cache[..., t] = conv_out
    # 调用 SSM 更新 kernel
    fused_sigmoid_gating_delta_rule_update_spec_cpu(...)
csrc/cpu/sgl-kernels/fla.cpp core-logic

新增投机解码变体的 C++ kernel,实现多序列并行和序列内串行的 SSM 更新,与 GPU kernel 语义对齐。

// 投机解码变体 kernel:处理可变长度批次,每个序列有 q_len 个 draft token
// 内部串行迭代,读取初始状态从 num_accepted-1 槽,写入每个 token 后的状态到对应槽
template <typename scalar_t, typename param_t>
void fused_sigmoid_gating_delta_rule_update_spec_kernel_impl(
    const scalar_t* __restrict__ q_ptr,
    const scalar_t* __restrict__ k_ptr,
    const scalar_t* __restrict__ v_ptr,
    const param_t* __restrict__ A_log_ptr,
    const scalar_t* __restrict__ a_ptr,
    const scalar_t* __restrict__ dt_bias_ptr,
    const scalar_t* __restrict__ b_ptr,
    const int32_t* __restrict__ spec_indices_ptr,
    const int32_t* __restrict__ num_accepted_ptr,
    const int32_t* __restrict__ cu_seqlens_ptr,
    float* __restrict__ state_ptr,
    scalar_t* __restrict__ o_ptr,
    float* __restrict__ qk_scale_buf,
    int64_t total_tokens,
    int64_t batch_size,
    int64_t spec_stride,
    int64_t num_heads,
    int64_t head_dim,
    int64_t v_num_heads,
    int64_t v_head_dim,
    int64_t q_strideT,
    int64_t q_strideH,
    int64_t k_strideT,
    int64_t k_strideH,
    int64_t v_strideT,
    int64_t v_strideH,
    int64_t state_slot_stride,
    bool use_qk_l2norm_in_kernel,
    double softplus_threshold) {  // 可选:在 kernel 内计算 QK L2 归一化尺度
  if (use_qk_l2norm_in_kernel) {
    // norm 计算略
  }  // 主并行循环:在 (batch, v_head) 维度上并行
  at::parallel_for(0, batch_size * v_num_heads, 0, [&](int64_t begin, int64_t end) {
    for (int64_t idx = begin; idx < end; ++idx) {
      int64_t bi = idx / v_num_heads;
      int64_t ni = idx % v_num_heads;
      int64_t group_size = v_num_heads / num_heads;
      int64_t kh = ni / group_size;
      int64_t q_start = cu_seqlens_ptr[bi];
      int64_t q_len = cu_seqlens_ptr[bi + 1] - q_start;
      if (q_len <= 0) continue;      int64_t acc = (int64_t)num_accepted_ptr[bi];
      // 将 acc-1 clamp 到 0,防止 num_accepted=0 时索引到 -1
      int64_t prev_slot =
          (int64_t)spec_indices_ptr[bi * spec_stride + std::max(acc - 1, (int64_t)0)];
      // 从 prev_slot 加载初始状态,然后对每个 draft token 迭代
      for (int64_t t = 0; t < q_len; ++t) {
        // 加载当前 token 的 Q, K, V, A, B
        // 更新状态并写出 output
        // 将更新后的状态存储到当前 token 对应的槽
        // 内核主逻辑重复使用非 spec kernel 的计算模式
      }
    }
  });
}

评论区精华

num_accepted=0 时 OOB 读取 正确性

depthfirst-app[bot] 指出 kernel 中 num_accepted_ptr[bi] 为 0 时 acc-1 下溢导致读取 spec_indices_ptr 越界。

结论:开发者添加 std::max(acc-1, 0) 进行保护,与 GPU 端 tl.maximum 一致。 · 已解决

initial_state_source 和 spec_state_indices 缺乏验证 安全

depthfirst-app[bot] 指出 wrapper 缺少对 initial_state_source 的容量检查和 spec_state_indices 的形状验证。

结论:initial_state_source 在后续提交中补充了检查;spec_state_indices 仍缺少验证,但被认为风险较低。 · 待处理

函数命名调整 style

bigPYJ1151 建议将 _cpu_gdn_attention_nonspec_legacy 重命名为 _cpu_gdn_attention_nonspec。

结论:开发者遵从并重命名。 · 已解决

移除冗余 contiguous 调用 性能

bigPYJ1151 指出 rearrange_mixed_qkv 的结果已是连续的,添加 contiguous() 多余。

结论:开发者同意并移除。 · 已解决

保留原始注释 documentation

bigPYJ1151 要求保留被误删的注释行。

结论:开发者恢复注释。 · 已解决

风险与影响

  • 回归风险:非投机路径保持未变,但投机路径覆盖了新的 conv/SSM 状态形状,可能遗漏 GDN 其他变体或 CPU 平台的兼容性问题。
  • 性能风险:spec 模式下 conv 状态视图分配和额外的状态写回操作可能增加延迟;新 kernel 内部对每个 draft token 串行迭代,限制了并行度。
  • 安全风险:虽然 num_accepted=0 的 OOB 已修复,但 spec_state_indices 未做形状验证低风险仍然存在。
  • 测试覆盖:没有新增单元测试,仅依赖 benchmark 和集成测试,无法充分覆盖边界情况(如所有 draft 被拒绝、最大 spec tokens 等)。
  • 用户:Qwen3.5/Qwen3.6 模型在 CPU 后端使用 --speculative-config '{"method": "dflash", ...}' 后,可启用 DFlash 投机解码。benchmark 显示输出吞吐 37.51 tok/s,接受率 31.6%,TTFT 248ms。
  • 系统:对非投机用户透明。新增的 C++ kernel 和 Python 包装不破坏现有接口。
  • 团队:需要维护 spec kernel 变体,与 GPU kernel 语义保持同步。
OOB 边界已修复 缺少状态索引校验 无单元测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论