Prhub

#44044 [Feature] Support DCP with FP8 KV cache in MLA decode path

原始 PR 作者 shivampr 合并时间 2026-06-25 03:28 文件变更 4 提交数 10 评论 16 代码增减 +359 / -15

执行摘要

支持 DCP + FP8 KV Cache 在 MLA 解码中协同

Issue #32010 报告 DCP 与 FP8 KV Cache 同时使用时报错 'DCP not support fp8 kvcache now.',用户需要这一组合来提升推理效率。

值得精读:展示了 DCP 与量化在 MLA 架构下的协同设计,特别是 head 计数修正对性能的影响。测试方法也值得借鉴。

讨论亮点

核心讨论围绕 FlashMLA 元数据 head 计数是否应始终乘以 dcp_world_size。审阅者 MatthewBonanni 起初怀疑仅 FP8 需要,作者测试后证明普通 BF16 也有性能提升,最终决定无条件乘算。另外,LucasWilkinson 建议简化条件分支,作者采纳。

实现拆解

实现分为四部分:

  1. MLAAttention.forward_impl 解码分支:移除 assert not fp8_attention,将 all-gather 逻辑移到量化拼接之后,区分 tuple 和 Tensor 两种状态,保证量化 query 可直接 all-gather。
  2. FlashMLA 元数据 head 计数修正:在 _build_decode 中将 num_q_heads 乘以 dcp_world_size(无论是否 FP8),确保 scheduler 和 dense FP8 元数据使用正确的 head 数。
  3. Chunked prefill 上下文路径修复:在 _context_parallel_compute_prefill_context 中使用去量化 gather(gather_and_maybe_dequant_cache)替代 cp_gather_cache,并新增 padded_local_token_to_seq 字段。
  4. 测试覆盖:新增 mock 测试验证 DCP+FP8 decode 下的 all-gather 调用,新增 kernel 测试验证 MLA gather 与 seq_starts 的交互。
文件 模块 状态 重要度
vllm/model_executor/layers/attention/mla_attention.py 注意力层 modified 7.59
vllm/v1/attention/backends/mla/flashmla.py FlashMLA modified 5.99
tests/v1/attention/test_mla_backends.py 测试 modified 7.19

关键符号

MLAAttention.forward_impl FlashMLABackend._build_decode _context_parallel_compute_prefill_context test_mock_mla_dcp_fp8_decode_gathers_quantized_query test_gather_and_maybe_dequant_cache_mla_with_seq_starts

关键源码片段

vllm/model_executor/layers/attention/mla_attention.py core-logic

核心修改:移除 DCP+FP8 的拒绝断言,统一 all-gather 逻辑。

# mla_attention.py (MLAAttention.forward_impl decode section)
​
            if fp8_attention and self.impl.supports_quant_query_input:
                # FP8 量化拼接:将 nope 与 pe 合并为量化 tensor
                mqa_q = self._decode_concat_quant_fp8_op(
                    mqa_ql_nope, mqa_q_pe, self._q_scale
                )
            else:
                # 非 FP8 时保持元组形式
                mqa_q = (mqa_ql_nope, mqa_q_pe)
​
            if self.impl.dcp_world_size > 1:
                if isinstance(mqa_q, tuple):
                    # 未量化时先拼接 nope 与 pe -> (B, N, L + P)
                    mqa_q = torch.cat(mqa_q, dim=-1)
                # 在 head 维度上进行 all-gather
                mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
​
            attn_out, lse = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self)
vllm/v1/attention/backends/mla/flashmla.py core-logic

FlashMLA 解码元数据 head 计数修正,确保 DCP 下元数据正确。

# flashmla.py (FlashMLABackend._build_decode)
​
            max_query_len = query_lens_cpu.max().item()
            num_q_heads = self.num_q_heads
            if self.dcp_world_size > 1:
                # DCP 下实际 head 数 = 单卡 head 数 * world_size
                num_q_heads *= self.dcp_world_size
            # 计算 q tokens per head 用于调度
            num_q_tokens_per_head_k = max_query_len * num_q_heads // 1
​
            scheduler_metadata, _ = get_mla_metadata(
                seq_lens_device,
                num_q_tokens_per_head_k,
                1, # MQA
                is_fp8_kvcache=self.is_fp8_kvcache,
            )
            if self.is_fp8_kvcache:
                tile_scheduler_metadata, num_splits = get_mla_metadata_dense_fp8(
                    seq_lens_device,
                    num_q_tokens_per_head_k,
                    1,
                )
tests/v1/attention/test_mla_backends.py test-coverage

新增 mock 测试,验证 DCP+FP8 decode 下的 all-gather 行为。

# test_mla_backends.py (part of test_mock_mla_dcp_fp8_decode_gathers_quantized_query)class _FakeDCPGroup:
    # 模拟 DCP 组,记录 all_gather 调用
    def __init__(self):
        self.calls = 0
        self.input_dtype = None
        self.input_shape = None
​
    def all_gather(self, x, dim=1):
        self.calls += 1
        self.input_dtype = x.dtype
        self.input_shape = tuple(x.shape)
        # 模拟聚集:将 x 沿 dim 复制拼接(dcp_world_size=2)
        return torch.cat([x, x], dim=dim)# 替换全局 get_dcp_group 为 fake 实例
fake_group = _FakeDCPGroup()
monkeypatch.setattr(mla_attention_module, "get_dcp_group", lambda: fake_group)# 运行前向传播后验证 all_gather 被调用且输入形状正确
assert fake_group.calls == 1
assert fake_group.input_shape[1] == num_heads * 2

评论区精华

FlashMLA 元数据 head 计数调整范围 设计

MatthewBonanni 质疑是否只应在 FP8 下调整 head 计数,作者测试显示始终乘以 dcp_world_size 可提升 BF16 性能,最终决定统一应用。

结论:无条件将 num_q_heads 乘以 dcp_world_size,无论是否 FP8。 · 已解决

简化条件分支 style

LucasWilkinson 建议简化 mla_attention.py 中的条件分支,使用 `isinstance(mqa_q, tuple)` 判断是否已量化拼接。

结论:作者采纳建议,修改了代码逻辑。 · 已解决

风险与影响

本 PR 修改了 BF16 DCP 路径的元数据,可能引入未知回归,但 GSM8K 测试显示正确性不变且性能略有提升。chunked prefill gather 的变更涉及 gather_and_maybe_dequant_cache 内核,新的 kernel 测试覆盖了带有 seq_starts 的 MLA gather。风险较低。

影响范围限于使用 MLA 注意力且同时启用 DCP 和 FP8 KV Cache 的用户(如 DeepSeek-V2),他们现在可以正常运行。非 DCP 或非 FP8 路径无影响。团队需注意后续 MLA 相关变更可能与本 PR 产生交互。

核心路径变更 元数据调整 测试新增覆盖

关联 Issue

#32010 [Feature]: Support DCP with FP8 KV Cache

完整报告

参与讨论