Prhub

#51318 [Bugfix][DSv4] Revert adaptive C128A metadata packing

原始 PR 作者 tobymao 合并时间 2026-08-16 09:39 文件变更 2 提交数 3 评论 10 代码增减 +7 / -56

执行摘要

回退 DSV4 C128A 自适应打包,修复并发解码元数据错位

issue #52448 报告 DeepSeek-V4-Flash-0731 在并发 DSpark + breakable CUDA graphs(并发 ≥ 10)下,请求间歇性永远停留在 reasoning 通道,直到 max_tokens 耗尽返回 finish_reason=length、content 为空。PR body 明确根因:'The C128A metadata builder runs before FULL CUDA graph replay, but the sparse decode consumer is captured and reuses its capture-time row layout',即 #50004 引入的 batch 依赖 active_topk_width 与 capture 时固化的行 stride 不一致,Row 0 仍对齐但后续行读到 stale offsets。

值得精读:它清晰展示了 CUDA graph capture-time 布局与运行时动态 shape 之间的一致性约束,是理解 vLLM 图模式正确性的典型样例。重点看 build_c128a_topk_metadata 中 stride 从 packed width 改回 capacity stride 的取舍——这是‘为正确性放弃动态省带宽优化’的设计决策。建议后续在相关代码注释中明确 capture 约束,或考虑在 CUDA graph 重放中支持动态 stride 后再恢复自适应优化。

讨论亮点

PR 的 review 评论为空(fork 自动 review 被禁用,zyongye 直接 APPROVED),核心讨论集中在关联 issue #52448:

  • haydn-jones 反馈:'I believe this patch fixed the corruption I was seeing.' 在 B200 + FlashMLA + FP8 KV + DP=4 + 1M context 下应用后未再出现类似 #48089 的损坏。
  • dafeliton 确认:'Fixed ours too. Seems to happen primarily during concurrent requests at higher contexts.'
  • mertunsall 询问影响范围是否超出 DSV4,haydn-jones 与 dafeliton 均确认只在 DSV4 家族(V4-Pro/Flash)出现,Gemma/GLM 5.2 无此行为,回退也只影响 DSV4 C128A sparse MLA 路径。

实现拆解

  1. 移除动态宽度计算:在 vllm/models/deepseek_v4/sparse_mla.py_build_c128a_metadata 中删除 active_topk_widthmin/max/next_power_of_2/_C128A_TOPK_ALIGNMENT 的动态推导),将 max_compressed_tokens 固定为 self.c128a_max_compressed,使元数据宽度不再随 batch 的 max_seq_len 变化。
  2. 恢复容量 stride 布局build_c128a_topk_metadata 将原先 view(-1)[: n * width].view(n, width) 的 packed 切片改为 global_decode_buffer[:num_decode_tokens]prefill_buffer[:num_prefill_tokens] 的直接容量切片;同时把传给 Triton kernel 的行 stride 参数由 max_compressed_tokens 改为 global_decode_buffer.stride(0) / prefill_buffer.stride(0),保证运行时写入位置与 capture 时消费者固化的 stride 完全一致。
  3. 删除不匹配的测试tests/kernels/attention/test_flashmla_sparse.py 中删除 test_deepseek_v4_c128a_dynamic_topk_packed_buffers,因为该测试断言的 (active_width, 1) 紧凑 stride 布局已被回退;保留稀疏 FlashMLA metadata smoke test。
  4. 验证配套:pre-commit 全量检查通过;按 #52448 复现配置在 2× NVIDIA GB300 上(DSpark-7、FULL_AND_PIECEWISE、max_model_len=70000、并发 32,256 priming + 256 测量请求)得到 0 次问题命中、0 请求错误;issue 中多位用户(B200/FlashMLA 环境)报告修复有效。
文件 模块 状态 重要度
vllm/models/deepseek_v4/sparse_mla.py 稀疏注意力 modified 6.55
tests/kernels/attention/test_flashmla_sparse.py 内核测试 modified 5.1

关键符号

build_c128a_topk_metadata _build_c128a_metadata _build_c128a_topk_metadata_kernel

关键源码片段

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

核心源码:回退 adaptive C128A 打包,恢复 capacity-strided 布局以匹配 CUDA graph capture 时固化的行 stride,修复并发解码数据错位。

def build_c128a_topk_metadata(
    positions: torch.Tensor,
    compress_ratio: int,
    num_decode_tokens: int,
    token_to_req_indices: torch.Tensor,
    block_table: torch.Tensor,
    block_size: int,
    slot_mapping: torch.Tensor,
    global_decode_buffer: torch.Tensor,
    decode_lens_buffer: torch.Tensor,
    prefill_buffer: torch.Tensor,
    max_compressed_tokens: int = 8192,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    """Single kernel for all C128A tokens (decode + prefill).    Decode tokens: position → block_table lookup → global slot ids + topk_lens.
    Prefill tokens: position → local indices [0, ..., n-1, -1, ...].    Writes into pre-allocated buffers for CUDA graph address stability.
    Returns slices of the buffers.
    """
    num_tokens = positions.shape[0]
    num_prefill_tokens = num_tokens - num_decode_tokens
​
    # 关键:直接按容量切片,而不是 view(-1) 后压缩打包。
    # CUDA graph capture 时消费者 kernel 的行 stride 来自持久 buffer 的 stride(0)
    # 即 capacity width,运行时必须保持同样的行距,否则第 2 行起会读到陈旧偏移。
    global_decode = global_decode_buffer[:num_decode_tokens]
    decode_lens = decode_lens_buffer[:num_decode_tokens]
    prefill_local = prefill_buffer[:num_prefill_tokens]
​
    if num_tokens == 0:
        return global_decode, decode_lens, prefill_local
​
    _build_c128a_topk_metadata_kernel[(num_tokens,)](
        global_decode_buffer,
        global_decode_buffer.stride(0), # 行 stride 传 capacity stride,而非 packed width
        decode_lens_buffer,
        prefill_buffer,
        prefill_buffer.stride(0),
        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

评论区精华

修复验证与 corruption 消失 测试

haydn-jones 报告在 B200 + DeepSeek-V4-Flash-0731 + FlashMLA + FP8 KV + DP=4 + 1M context 下应用后未再出现类似 #48089 的损坏;dafeliton 也确认修复,主要发生在并发高上下文场景。

结论:多位用户在真实 GPU 环境独立验证修复有效,问题复现条件苛刻(DSpark + breakable CUDA graph + 并发)。 · 已解决

影响范围是否限于 DSV4 question

mertunsall 询问是否影响其他模型 / 部署;haydn-jones 说明只在 DSV4 家族(非 V3/3.2/Kimi K3/Minimax M3)出现;dafeliton 补充 Gemma/GLM 5.2 无此行为。

结论:确认仅影响 DSV4 C128A sparse MLA 路径,回退改动也只触及该路径。 · 已解决

风险与影响

  1. CUDA graph 布局耦合:修复依赖 capture 时 max_model_len 对应最大 stride 的前提;若未来重新引入自适应宽度,必须在 capture 侧同步处理,否则会再次出现同类错位。
  2. 性能回退:固定 c128a_max_compressed 后,元数据行宽不再按 batch 实际 max_seq_len 收缩,写回量可能增加(1M context 下宽达 8192),对吞吐/显存带宽有轻微影响,但这是确保正确性所付出的代价。
  3. 测试覆盖缺口:删除了直接断言布局的单元测试,后续对 build_c128a_topk_metadata 的改动缺少细粒度回归保护,需依赖 e2e 并发测试兜底。
  4. 复现条件苛刻:问题需要 DSpark + breakable CUDA graph + 并发 ≥ 10 同时满足,普通 CI 难以覆盖,依赖用户环境的独立验证。

修复 DeepSeek-V4-Flash-0731 在并发 DSpark + breakable CUDA graph 下的死循环、空回复与 finish_reason=length 烧满 max_tokens 的严重在线问题,直接影响所有使用 DSV4 家族 + C128A sparse MLA + CUDA graph 的生产部署;对 V3/3.2/Kimi K3/Mistral 等其他模型无影响。代码变更仅 2 个文件(+7/-56),合并成本低,但需要 NVIDIA GPU 环境复测以确认布局回归。

核心路径变更(CUDA graph 布局) 回退动态优化可能影响显存 / 性能 删除直接单元测试,回归依赖 e2e 需 DSpark+ 并发场景回归验证

关联 Issue

#52448 [Bug]: DeepSeek-V4-Flash think-until-cap / empty turn under concurrent DSpark + breakable CUDA graphs

完整报告

参与讨论