Prhub

#50298 [DSv4 Perf] Remove redundant full kernel for dsv4, 1.88x kernel performance improvement

原始 PR 作者 yewentao256 合并时间 2026-07-30 23:39 文件变更 3 提交数 2 评论 0 代码增减 +44 / -23

执行摘要

移除冗余 torch.full 内核调用,提升 1.88x

该 PR 是 DeepSeek V4 性能优化 (Issue #45861) 的一部分。PR body 指出,在 combine_topk_swa_indices 中每次调用都通过 torch.full 创建索引张量,会产生额外的 CUDA 内核启动开销。通过传递 out 张量来重用预分配缓冲区,可以消除这个冗余调用。

建议精读该 PR。它展示了在 CUDA kernel 层面消除冗余启动的典型模式:通过预留工作空间和 out 参数复用缓冲区。设计决策(如 round_up 对齐、warmup 阶段的 workspace 预留)值得学习。

讨论亮点

该 PR 没有 review 评论或讨论线程。唯一的 review 来自 claude[bot] 的自动评论和 sfeng33 的批准。

实现拆解

  1. combine_topk_swa_indices 添加 out 参数:在 vllm/models/deepseek_v4/common/ops/cache_utils.py 中,函数签名新增 out: tuple[torch.Tensor, torch.Tensor] | None = None。当 outNone 时保留原有行为(内部调用 torch.full 分配张量),否则直接使用传入的 combined_indicescombined_lens 张量,跳过额外分配。

  2. 在 warmup 阶段预留工作空间:在 vllm/models/deepseek_v4/nvidia/flashmla.pyforward_mqa 中,当 attn_metadataNone(warmup 阶段)时,除了原有的 bf16 张量外,额外通过 workspace_manager.get_simultaneous 预分配 (max_num_batched_tokens, combined_topk) 的 int32 索引张量和 (max_num_batched_tokens,) 的长度张量。这里使用 round_up(top_k + window_size, 128) 计算 combined_topk,128 是稀疏 prefill 的对齐要求。

  3. 修改 _forward_prefill 使用预分配工作空间:在 vllm/models/deepseek_v4/nvidia/flashmla.py_forward_prefill 方法中,从工作空间获取的三个张量(kv, combined_indices_out, combined_lens_out)中,将后两者作为 out 参数传递给 combine_topk_swa_indices。同时根据当前 chunk 的 token 范围裁剪 combined_indices_outcombined_lens_out 的视图。

  4. 加强测试覆盖:在 tests/kernels/attention/test_flashmla_sparse.py 中,将 test_sparse_flashmla_prefill_smoke 参数化为 h_q 为 64 和 128 两种情况,使用随机输入和随机索引,并增加 topk_length 参数。测试验证:当索引张量中除第一个外全部置为 -1(无效)时,其输出应与使用完整随机索引的输出一致,从而验证稀疏注意力正确性。

文件 模块 状态 重要度
vllm/models/deepseek_v4/nvidia/flashmla.py 模型层 modified 6.75
vllm/models/deepseek_v4/common/ops/cache_utils.py 模型层 modified 5.24
tests/kernels/attention/test_flashmla_sparse.py 注意力 modified 5.4

关键符号

combine_topk_swa_indices forward_mqa _forward_prefill test_sparse_flashmla_prefill_smoke

关键源码片段

vllm/models/deepseek_v4/nvidia/flashmla.py core-logic

主变更文件,修改了 warmup 阶段工作空间预留和 prefill 路径中 combine_topk_swa_indices 的调用方式

# 摘自 vllm/models/deepseek_v4/nvidia/flashmla.py
# forward_mqa 中 warmup 阶段:预留 combined_indices 和 combined_lens 工作空间
if attn_metadata is None:
    # ... 原有代码 ...
    assert self.topk_indices_buffer is not None
    top_k = 0 if swa_only else self.topk_indices_buffer.shape[-1]
    # 计算对齐后的 combined_topk 宽度
    combined_topk = round_up(top_k + self.window_size, 128)
    current_workspace_manager().get_simultaneous(
        ((self.PREFILL_CHUNK_SIZE, M, q.shape[-1]), torch.bfloat16),
        ((self.max_num_batched_tokens, combined_topk), torch.int32),
        ((self.max_num_batched_tokens,), torch.int32),
    )
    output.zero_()
    return# _forward_prefill 中:从工作空间获取预分配张量并传递给 combine_topk_swa_indices
workspace = workspace_manager.get_simultaneous(
    ((chunk_size, chunk_M, q.shape[-1]), torch.bfloat16),
    ((self.max_num_batched_tokens, combined_topk), torch.int32),
    ((self.max_num_batched_tokens,), torch.int32),
)
kv, combined_indices_out, combined_lens_out = workspace
# ... 计算 query_start, query_end ...
# 根据当前 chunk 的 token 范围裁剪视图
combined_indices_out = combined_indices_out[: query_end - query_start]
combined_lens_out = combined_lens_out[: query_end - query_start]
# 调用 combine_topk_swa_indices,传入 out 参数以重用预分配缓冲区
combined_indices, combined_lens = combine_topk_swa_indices(
    topk_indices[query_start:query_end],
    query_start_loc[query_start:query_end + 1],
    seq_lens[query_start:query_end],
    gather_lens[query_start:query_end],
    self.window_size,
    self.compress_ratio,
    top_k,
    chunk_M,
    chunk_N,
    out=(combined_indices_out, combined_lens_out),
)
vllm/models/deepseek_v4/common/ops/cache_utils.py data-contract

核心工具函数,新增 out 参数允许外部传入预分配张量以避免 torch.full

# 摘自 vllm/models/deepseek_v4/common/ops/cache_utils.py
def combine_topk_swa_indices(
    topk_indices: torch.Tensor,
    query_start_loc: torch.Tensor,
    seq_lens: torch.Tensor,
    gather_lens: torch.Tensor,
    window_size: int,
    compress_ratio: int,
    topk: int,
    M: int,
    N: int,
    out: tuple[torch.Tensor, torch.Tensor] | None = None, # 新增参数
) -> tuple[torch.Tensor, torch.Tensor]:
    num_tokens = topk_indices.shape[0]
    combined_topk = (
        (topk + window_size + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
        // _SPARSE_PREFILL_TOPK_ALIGNMENT
        * _SPARSE_PREFILL_TOPK_ALIGNMENT
    )
    # 只在外部未提供张量时才分配
    if out is None:
        combined_indices = torch.full(
            (num_tokens, combined_topk),
            fill_value=-1,
            dtype=torch.int32,
            device=topk_indices.device,
        )
        combined_lens = torch.empty(
            num_tokens, dtype=torch.int32, device=topk_indices.device
        )
    else:
        combined_indices, combined_lens = out
    # ... 后续 kernel 调用使用 combined_indices 和 combined_lens ...
    _COMBINE_TOPK_SWA_INDICES_KERNEL(combined_indices, ...)
    return combined_indices, combined_lens
tests/kernels/attention/test_flashmla_sparse.py test-coverage

测试文件,增强了稀疏 prefill 测试的覆盖范围,使用随机输入并参数化 head 维度

# 摘自 tests/kernels/attention/test_flashmla_sparse.py
@pytest.mark.parametrize("h_q", [64, 128])
def test_sparse_flashmla_prefill_smoke(h_q: int):
    import vllm.v1.attention.ops.flashmla as fm
    ok, reason = fm.is_flashmla_sparse_supported()
    if not ok:
        pytest.skip(reason)
    device = torch.device("cuda")
    torch.manual_seed(0)
    s_q = 1
    s_kv = 8
    h_kv = 1
    d_qk = 576
    d_v = 512
    topk = 128
    # 使用随机张量代替全零张量
    q = torch.randn((s_q, h_q, d_qk), dtype=torch.bfloat16, device=device)
    kv = torch.randn((s_kv, h_kv, d_qk), dtype=torch.bfloat16, device=device)
    indices = torch.randint(s_kv, (s_q, h_kv, topk), dtype=torch.int32, device=device)
    # 构造参考输入:仅保留第一个有效索引
    reference_indices = indices.clone()
    reference_indices[..., 1:] = -1
    kwargs = {"topk_length": torch.ones(1, dtype=torch.int32, device=device)}
    # 使用 topk_length 参数传递有效长度
    reference = fm.flash_mla_sparse_fwd(q, kv, reference_indices, 1.0, d_v, **kwargs)
    actual = fm.flash_mla_sparse_fwd(q, kv, indices, 1.0, d_v, **kwargs)
    # 验证两个结果完全一致(索引无关的测试)
    for actual_tensor, reference_tensor in zip(actual, reference):
        torch.testing.assert_close(actual_tensor, reference_tensor, rtol=0, atol=0)
    assert actual[0].shape == (s_q, h_q, d_v)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 回归风险:修改了检查点改变的关键路径,但通过测试覆盖(参数化测试)和预热阶段的断言(assert self.topk_indices_buffer is not None)降低了风险。
  2. 工作空间管理:预热阶段预留了额外的 int32 张量,如果工作空间管理器在并发场景下处理不当,可能导致内存占用增加或竞争条件。但目前工作空间是每个模型实例独立的,风险较低。
  3. 兼容性combine_topk_swa_indices 的接口向后兼容,现有调用方不受影响。
  1. 用户影响:对使用 DeepSeek V4 模型的用户,prefill 阶段的延迟将显著降低(1.88 倍微操作加速),整体推理吞吐提升。
  2. 系统影响:减少了 CUDA 内核启动次数,降低了 GPU 工作负载。预热阶段额外预留的 int32 张量占内存很小(例如 8192 tokens × 1024 宽度 × 4 字节 ≈ 32 MB)。
  3. 团队影响:代码改动集中,可读性好,易于维护。
核心路径变更 工作空间管理

关联 Issue

#45861 [Feature]: Performance Optimization for Deepseek V4

完整报告

参与讨论