Prhub

#50004 [DSv4 Perf] Adaptive topk width, 1.0% E2E throughput improvement

原始 PR 作者 yewentao256 合并时间 2026-07-28 03:56 文件变更 2 提交数 1 评论 0 代码增减 +56 / -7

执行摘要

C128A 自适应 topk 宽度,提升 1.0% 吞吐

在长序列场景下,C128A 解码时 kernel 循环次数与 max_compressed_tokens 成正比。原始固定为 8192,但实际 context 长度通常远小于该值,导致不必要计算。PR body 给出的例子:32k tokens 实际只需 256 宽度,kernel 循环减少 32 倍。

建议合入。该 PR 定位清晰、改动精炼、有基准数据和单元测试支撑。值得关注的是 triton.next_power_of_2 的使用方式和 _C128A_TOPK_ALIGNMENT 常量的定义位置,可在 code review 时确认其可见性。

讨论亮点

无 review 讨论或争议。PR 由 sfeng33 直接批准,claude[bot] 仅发送了自动提示。

实现拆解

  1. 计算自适应 topk 宽度:在 sparse_mla.py:_build_c128a_metadata 方法中新增 active_topk_width 变量,公式为 min(max(triton.next_power_of_2(max(cm.max_seq_len // compress_ratio, 1)), _C128A_TOPK_ALIGNMENT), c128a_max_compressed)。取 cm.max_seq_len // compress_ratio 并向上对齐到 _C128A_TOPK_ALIGNMENT,同时保证不超过缓冲区容量。
  2. 更改 buffer 视图为 packed 布局:在 build_c128a_topk_metadata 函数中,global_decode 和 prefill_local 的切片方式从直接取前 N 行(buffer[:num_decode_tokens])改为 buffer.view(-1)[:num_decode_tokens * max_compressed_tokens].view(num_decode_tokens, max_compressed_tokens)。这样使输出张量的第二维正好等于 active_topk_width,kernel 的 stride 也随之变化。
  3. 传递新 stride 给内核:_build_c128a_topk_metadata_kernel 调用时,原传递 buffer.stride(0) 改为传递 max_compressed_tokens 作为新的 stride,保证内核按 packed 视图正确索引。
  4. 新增单元测试:test_flashmla_sparse.py 添加 test_deepseek_v4_c128a_dynamic_topk_packed_buffers,构造 2 token 场景(capacity=256,active=128),验证输出的 shape、stride 和数值正确性。
文件 模块 状态 重要度
vllm/models/deepseek_v4/sparse_mla.py 模型实现 modified 6.26
tests/kernels/attention/test_flashmla_sparse.py 测试 modified 5.5

关键符号

_build_c128a_metadata build_c128a_topk_metadata

关键源码片段

vllm/models/deepseek_v4/sparse_mla.py data-contract

核心变更文件,实现自适应 topk 宽度计算并修改 buffer 视图为 packed 布局。

def _build_c128a_metadata(self, cm, req_id_per_token):
    # ... 省略前置计算 ...
    # 动态计算 topk_width:根据当前最大序列长度而不是固定 8192
    active_topk_width = min(
        max(
            triton.next_power_of_2(max(cm.max_seq_len // self.compress_ratio, 1)),
            _C128A_TOPK_ALIGNMENT, # 确保满足对齐要求
        ),
        self.c128a_max_compressed, # 上限为缓冲区容量
    )
    block_size = self.kv_cache_spec.block_size // self.compress_ratio
    global_decode, decode_lens, prefill_local = build_c128a_topk_metadata(
        cm.positions[:num_total],
        self.compress_ratio,
        num_decode_tokens,
        req_id_per_token,
        cm.block_table_tensor[:num_decodes],
        block_size,
        cm.slot_mapping,
        self.c128a_global_decode_buffer,
        self.c128a_decode_lens_buffer,
        self.c128a_prefill_buffer,
        max_compressed_tokens=active_topk_width, # 传入动态宽度
    )
    # ... 后续组装 result 字典 ...
​
​
def build_c128a_topk_metadata(...):
    num_tokens = positions.shape[0]
    num_prefill_tokens = num_tokens - num_decode_tokens
​
    # 使用 packed 视图:将 buffer 展平后取前 token*width 个元素,再 reshape
    # 这样输出的第二维精确等于 max_compressed_tokens,kernel 无需处理多余列
    global_decode = global_decode_buffer.view(-1)[
        : num_decode_tokens * max_compressed_tokens
    ].view(num_decode_tokens, max_compressed_tokens)
    decode_lens = decode_lens_buffer[:num_decode_tokens]
    prefill_local = prefill_buffer.view(-1)[
        : num_prefill_tokens * max_compressed_tokens
    ].view(num_prefill_tokens, max_compressed_tokens)
​
    if num_tokens == 0:
        return global_decode, decode_lens, prefill_local
​
    # 内核调用:stride 从原来的 buffer.stride(0) 改为 max_compressed_tokens
    _build_c128a_topk_metadata_kernel[(num_tokens,)](
        global_decode_buffer,
        max_compressed_tokens, # 新 stride
        decode_lens_buffer,
        prefill_buffer,
        max_compressed_tokens, # 新 stride
        positions,
        compress_ratio,
        max_compressed_tokens,
        num_decode_tokens,
        token_to_req_indices,
        block_table,
        block_table.stride(0),
        block_size,
        slot_mapping,
        BLOCK_SIZE=1024,
    )
    return global_decode, decode_lens, prefill_local
tests/kernels/attention/test_flashmla_sparse.py test-coverage

新增测试,验证 packed 视图下 shape、stride 和数值的正确性。

def test_deepseek_v4_c128a_dynamic_topk_packed_buffers():
    from vllm.models.deepseek_v4.sparse_mla import build_c128a_topk_metadata
    device = torch.device("cuda")
    capacity_width = 256 # 预分配缓冲区宽度
    active_width = 128 # 自适应后实际宽度(小于容量)
    global_decode_buffer = torch.empty(
        (2, capacity_width), dtype=torch.int32, device=device
    )
    decode_lens_buffer = torch.empty(2, dtype=torch.int32, device=device)
    prefill_buffer = torch.empty((2, capacity_width), dtype=torch.int32, device=device)
​
    # 传入 active_width 作为 max_compressed_tokens
    global_decode, decode_lens, prefill_local = build_c128a_topk_metadata(
        positions=torch.tensor([255, 511], dtype=torch.int64, device=device),
        compress_ratio=128,
        num_decode_tokens=1,
        token_to_req_indices=torch.tensor([0, 0], dtype=torch.int32, device=device),
        block_table=torch.tensor([[3]], dtype=torch.int32, device=device),
        block_size=capacity_width,
        slot_mapping=torch.tensor([0, 1], dtype=torch.int64, device=device),
        global_decode_buffer=global_decode_buffer,
        decode_lens_buffer=decode_lens_buffer,
        prefill_buffer=prefill_buffer,
        max_compressed_tokens=active_width,
    )
​
    # 验证输出 shape 和 stride 符合期望
    assert global_decode.shape == (1, active_width) # 第二维 = 128
    assert prefill_local.shape == (1, active_width)
    assert global_decode.stride() == (active_width, 1)
    assert prefill_local.stride() == (active_width, 1)
    # 验证 decode token 的 slot ids 正确
    assert global_decode[0, :2].cpu().tolist() == [768, 769]
    assert decode_lens.cpu().tolist() == [2]
    # 验证 prefill token 的本地索引正确
    assert prefill_local[0, :4].cpu().tolist() == list(range(4))
    # 验证剩余位置填充 -1
    assert torch.all(global_decode[0, 2:] == -1)
    assert torch.all(prefill_local[0, 4:] == -1)

评论区精华

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

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

风险与影响

  1. 对 CUDA Graph 兼容性:buffer 本身仍为固定大小预分配,视图变化不改变底层地址,CUDA graph 地址稳定性不受影响。
  2. 数值正确性:新切片方式在 max_compressed_tokens 比容量小时,仅取子视图,kernel 写入范围受限于 max_compressed_tokens,不会越界。测试覆盖了边界情况(active_width < capacity_width)。
  3. 回退路径:当 cm.max_seq_len 为 0 或异常值时,next_power_of_2 最小为 1,再经 _C128A_TOPK_ALIGNMENT 对齐,不会导致空张量。但若 _C128A_TOPK_ALIGNMENT 定义缺失(当前在文件中未显式定义),可能引发 NameError。建议确认该常量已在模块顶部或同文件定义。
  4. 性能回归风险低:算法仅减少计算量,不改变语义,且基准测试已确认收益。

用户: Deepseek V4 模型在短中 context 场景下首 token 时延降低约 1%,吞吐提升。长 context(接近 8192 压缩 token)无变化。
系统: 无新增依赖,代码变更集中在单一文件。
团队: 改动小(+56/-7),可读性好,容易维护。

依赖模块内常量 _C128A_TOPK_ALIGNMENT 需可见

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论