Prhub

#40996 DCP supports hybrid attention

原始 PR 作者 Yancey0623 合并时间 2026-07-10 12:34 文件变更 26 提交数 16 评论 53 代码增减 +785 / -127

执行摘要

DCP 支持混合注意力模型,非 DCP 层走本地缓存

PR body 明确说明:Hybrid-attention models such as Qwen/Qwen3.5-0.8B contain both DCP-capable full-attention layers and non-DCP layers. This PR enables DCP for supported attention groups without globally blocking hybrid-attention DCP, while keeping non-DCP groups on local cache/state handling. 此前 HybridKVCacheCoordinator 直接断言 dcp_world_size == 1,导致混合注意力模型无法享受 DCP 的长上下文 decode 加速。cjackal 在评论中提醒此前 #36480 的尝试在 397B MoE 上存在精度问题,作者补充了 Qwen3.5-397B-A17B-FP8 的 GSM8K 结果(TP4DCP1 0.9140625 / TP4DCP2 0.91015625)证明精度已无问题。

值得精读。该 PR 是 DCP 支持混合注意力的关键里程碑,其中三处设计最值得关注:①把“分片 vs 复制”语义下沉到 KVCacheSpec 子类(max_num_blocks_per_req),解决 CP 缩放公式散落各处的问题;②rank-invariant 的 collective 门控原则(任何决定是否发起 DCP 通信的判定都必须基于全局、rank 无关的输入),这是分布式正确性的通用教训;③FA2 混合批次拆分的兼容性 workaround 思路。建议结合后续 #50823 一起阅读,观察该契约的演进。

讨论亮点

主要交锋集中在三块:

  1. 死锁风险(阻塞项,已修复) — GirasoleY 指出 skip-context-attention 若按 rank 本地 context 长度判定,context 短于一个 interleave 轮次时会落在部分 rank 上,导致部分 rank 跳过 DCP collectives 而其它 rank 进入,形成死锁。最终由 GirasoleY 提交 e201de2 改为基于全局 context_kv_lens_cpu 判定,并新增 test_skip_gate_rank_invariant_with_divergent_local_context 回归测试。

  2. 设计整合(已落实) — GirasoleY 的 claude review 指出 block-size / CP-sharding 逻辑在 resolve_kv_cache_block_sizesSingleTypeKVCacheManagerKVCacheCoordinatorMambaManager_get_max_num_blocks_per_req 多处重复,建议 consolidate the KV-cache block-size / CP-sharding logic onto KVCacheSpec。最终通过 max_num_blocks_per_req 方法收口到 spec 子类。

  3. 正确性 bug(已修复) — gemini-code-assist[bot] 指出 q_descale/k_descale/v_descale 应按 token 数而非请求数切片,且非 FP8 模型下为 None 需判空;最终 run_split_fa2_dcp_context_attention 中已是 q_descale[:num_decode_reqs] if q_descale is not None else None 形式。

  4. 行为回退(已解决) — GirasoleY 担心 check_attention_cp_compatibility 去掉 interleave_size > 1 检查会破坏 #25049 验证的 FAv3 + MTP 支持。作者回复 this pr does support MTP, reverted this change,并同步回退 vllm/config/vllm.py 中 DCP 不支持 speculative decoding 的断言。

  5. 疑问与澄清 — ZJY0516 问为什么要拆 prefill/decode,作者说明是为了分离纯 prefill 与混合批次,最后演化为 FA2 专用 workaround;noooop 问 hybrid 模型为何默认关闭 prefix caching,作者解释 hybrid 支持 prefix caching 但保持 opt-in 等待成熟。

实现拆解

  1. KV cache 契约:按组区分“分片”与“复制”vllm/v1/kv_cache_interface.pyKVCacheSpec 新增 max_num_blocks_per_req(vllm_config, max_len) 方法;AttentionSpecdcp * pcp 缩放(每 rank 只存 max_len // (dcp * pcp) 个 token),MambaSpec 不缩放,并在 align 模式按整序列行宽 + num_speculative_blocks 计算。vllm/v1/core/kv_cache_utils.pyresolve_kv_cache_block_sizes 从“多 block size + CP 直接报错”改为 attention 组 block_size * dcp * pcp、Mamba 组保持原值再求 LCM。vllm/v1/core/kv_cache_coordinator.py 移除 dcp_world_size == 1 断言,改为显式只允许 FullAttentionSpecMambaSpec 两类组参与 DCP,并让 find_longest_cache_hit 基于每个 manager 的真实 block_size 对齐、对 full-attention 组传入 dcp_world_size

  2. BlockTable 与 slot mapping 分层vllm/v1/worker/block_table.py 新增 SlotMappingModeTOKEN_TO_KV_SLOT / NONE)。Mamba/GDN 组只把 block table 当作循环状态索引,不需要 token 级 slot mapping,NONE 模式下 compute_slot_mapping 直接返回,避免按 full-attention 的 DCP 布局算出错误 slot。Triton kernel 同时引入 KV_CACHE_BLOCK_SIZEBLOCKS_PER_KV_BLOCK 常量,支持分配块大于 kernel 块的混合布局;max_num_blocks 改为由调用方显式传入,不再隐式用 get_total_cp_world_size 推导。

  3. FlashAttention DCP forward 路径重构vllm/v1/attention/backends/flash_attn.py 的 metadata 扩展 num_decode_reqs / num_prefill_reqs / num_decode_tokens / num_prefill_tokens 拆分计数。builder 用全局(rank 无关的)seq_lens_cpu_upper_bound 计算 context_kv_lens_cpu:全 0 则走 skip_dcp_context_attention fast path(max_dcp_context_kv_len = 0,forward 直接退化为普通 varlen attention,不发任何 DCP collectives);否则用 split_dcp_context_queries 把重排后的 query 拆成 decode 与 extend 区段。对 FA2 混合批次(decode + 带 context 的 extend + 纯 prefill 同时出现),should_split_fa2_dcp_context_attention 返回 True,forward 改走 run_split_fa2_dcp_context_attention 分段计算并拼接 LSE 与输出。

  4. CUDA graph 捕获的 dummy 输入vllm/v1/worker/gpu_model_runner.py_dummy_run 调用 get_dcp_dummy_context_len 决定是否给 seq_lens 增加 dcp_world_size * cp_kv_cache_interleave_size 的假 context,随后 prepare_dcp_dummy_context_metadata 为每个 BlockTable 行填充合法 block id(取模分配已分配块)、修正 positions 与 slot mapping,保证 DCP context 路径能被图正常捕获。

  5. 通信与测试配套vllm/distributed/device_communicators/cuda_communicator.pyall_gather 支持非 0 维(DCP 沿 seq 维 gather query),通过 pynccl all-gather 后 reshape/movedim 实现;测试新增 tests/v1/worker/test_cp_utils.py(rank-invariant skip gate)、tests/distributed/test_pynccl.py(非零维 all-gather),并改 tests/models/language/generation/test_hybrid.py 覆盖 DCP 配置。

文件 模块 状态 重要度
vllm/v1/worker/cp_utils.py 上下文并行 modified 8.83
vllm/v1/attention/backends/flash_attn.py 注意力后端 modified 7.7
vllm/v1/core/kv_cache_coordinator.py 缓存协调 modified 7.33
vllm/v1/worker/block_table.py 块表 modified 6.99
vllm/v1/worker/gpu_model_runner.py 模型运行器 modified 6.97
vllm/v1/kv_cache_interface.py 缓存契约 modified 7.0
vllm/v1/core/kv_cache_utils.py 缓存工具 modified 6.04
vllm/distributed/device_communicators/cuda_communicator.py 通信器 modified 6.18
tests/v1/worker/test_cp_utils.py 单元测试 added 6.03

关键符号

get_dcp_dummy_context_len prepare_dcp_dummy_context_metadata should_skip_dcp_context_attention split_dcp_context_queries should_split_fa2_dcp_context_attention run_split_fa2_dcp_context_attention KVCacheSpec.max_num_blocks_per_req AttentionSpec.max_num_blocks_per_req MambaSpec.max_num_blocks_per_req BlockTable.compute_slot_mapping CudaCommunicator.all_gather HybridKVCacheCoordinator.find_longest_cache_hit

关键源码片段

vllm/v1/worker/cp_utils.py core-logic

DCP 混合注意力的核心新逻辑所在:dummy context 长度计算、dummy metadata 填充、rank-invariant skip gate、query 拆分以及 FA2 混合批次的分段 attention 执行,全部集中于此。

def should_skip_dcp_context_attention(context_kv_lens_cpu: torch.Tensor) -> bool:
    """是否可跳过 DCP context attention。    必须只基于 rank 无关的全局 context 长度计算(而不是本 rank 由
    get_dcp_local_seq_lens 得到的本地份额):非跳过路径会发起 DCP
    集体通信(query all-gather + LSE combine),因此所有 DCP rank
    必须走同一分支。某个 rank 可能持有 0 个本地 context token,
    而其它 rank 仍持有同一批请求的 context。
    """
    return int(context_kv_lens_cpu.max().item()) == 0
​
​
def split_dcp_context_queries(
    query_start_loc: torch.Tensor,
    seq_lens_cpu_upper_bound: torch.Tensor | None,
    max_query_len: int,
    num_actual_tokens: int,
) -> tuple[int, int, int, int]:
    """把重排后的 DCP context query 拆成 decode 与 extend 两个区段。"""
    num_reqs = query_start_loc.shape[0] - 1
    if max_query_len <= 1:
        # 纯 decode 批次:所有请求都是 decode,无需拆分
        return num_reqs, 0, num_actual_tokens, 0
    if seq_lens_cpu_upper_bound is None:
        # 没有 CPU 上界信息时保守处理:全部按 extend 处理
        return 0, num_reqs, 0, num_actual_tokens
    # 复用通用拆分工具,区分 decode 区段与 extend 区段的请求数和 token 数
    common_attn_metadata = cast(
        CommonAttentionMetadata,
        SimpleNamespace(
            max_query_len=max_query_len,
            num_reqs=num_reqs,
            num_actual_tokens=num_actual_tokens,
            query_start_loc_cpu=query_start_loc,
            seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
            is_prefilling=None,
        ),
    )
    (
        num_decodes,
        num_extends,
        _num_prefills,
        num_decode_tokens,
        num_extend_tokens,
        _num_prefill_tokens,
    ) = split_decodes_prefills_and_extends(common_attn_metadata)
    return num_decodes, num_extends, num_decode_tokens, num_extend_tokens
vllm/v1/kv_cache_interface.py data-contract

定义 spec 级 max_num_blocks_per_req 契约:AttentionSpec 按 DCP/PCP 分片缩放,MambaSpec 保持复制语义,是本次改动的基础抽象。

class KVCacheSpec:
    def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
        """每个请求需要的 block table 行宽(即 worker 侧每行列数)。"""
        return cdiv(max_len, self.block_size)
​
​
class AttentionSpec(KVCacheSpec):
    def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
        # Attention KV 按 token 在 DCP/PCP rank 间交错分片,每个 rank
        # 只保存 max_len // (dcp * pcp) 个 token,所以行宽要除以总 CP 大小。
        parallel_config = vllm_config.parallel_config
        total_cp_size = (
            parallel_config.decode_context_parallel_size
            * parallel_config.prefill_context_parallel_size
        )
        return cdiv(max_len, self.block_size * total_cp_size)
​
​
class MambaSpec(KVCacheSpec):
    def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
        # Mamba 状态在 DCP/PCP rank 间复制而非分片,因此不做 CP 缩放。
        if vllm_config.cache_config.mamba_cache_mode == "align":
            # align 模式下 block 行按整序列位置索引(旧状态会被
            # remove_skipped_blocks 置空),行宽仍需覆盖 max_len。
            return cdiv(max_len, self.block_size) + self.num_speculative_blocks
        return cdiv(self.max_memory_usage_bytes(vllm_config), self.page_size_bytes)

评论区精华

DCP context-attention skip gate 的 rank-invariant 死锁风险 正确性

GirasoleY 指出 `_forward_with_dcp` 中 `max_dcp_context_kv_len == 0` 的 early return 若由 rank 本地 context 长度触发,会让部分 rank 跳过 query all-gather / LSE combine collectives,而其它 rank 进入,导致 DCP group 死锁(短 prompt 后首个 decode 或短 chunked-prefill step 都会触发)。

结论:必须用全局 `context_kv_lens_cpu` 判定,而非 `get_dcp_local_seq_lens` 的本地结果。GirasoleY 提交 e201de2 修复,并新增测试 `test_skip_gate_rank_invariant_with_divergent_local_context`。 · 已解决

KV-cache block-size / CP-sharding 逻辑应整合进 KVCacheSpec 设计

GirasoleY 的 claude review 指出 block_size * dcp * pcp 缩放公式在 `resolve_kv_cache_block_sizes`、`SingleTypeKVCacheManager`、`KVCacheCoordinator`、`MambaManager`、`_get_max_num_blocks_per_req` 多处重复,建议将两类事实(分片 vs 复制)下沉到 spec 层。

结论:通过 `KVCacheSpec.max_num_blocks_per_req` 方法收口:`AttentionSpec` 缩放、`MambaSpec` 不缩放;`resolve_kv_cache_block_sizes` 与 `kv_cache_coordinator` 均改为读取 manager 实际 block_size。 · 已解决

FA2 混合批次中 descale 切片与 None 判空 正确性

gemini-code-assist[bot] 标记高优先级问题:`q_descale/k_descale/v_descale` 若按请求数(num_decodes)而非 token 数切片会越界;非 FP8 模型下这些张量为 None,直接切片会 TypeError。

结论:最终 `run_split_fa2_dcp_context_attention` 按 token 级切片,并在每个 descale 参数上使用 `if q_descale is not None` 条件。 · 已解决

保留 MTP / speculative decoding 与 DCP 的兼容性 设计

GirasoleY 提醒 `check_attention_cp_compatibility` 删除 `interleave_size > 1` 检查是行为变更,#25049 已验证 FAv3 + MTP;同时 vllm/config/vllm.py 新增的 `assert speculative_config is None` 也应回退。

结论:作者回退相关改动,确认该 PR 支持 MTP;`revert mtp` 提交落地。 · 已解决

为什么需要拆分 prefill 与 decode 批次 question

ZJY0516 询问 `num_dcp_context_reqs` 等 metadata 字段用途,认为 Qwen2.5 与 hybrid 的 DCP 差异不大,不明白为何需要拆批。作者解释用于分离纯 prefill 与混合批次,并说明最新提交已改为统一 DCP forward 路径,仅保留 FA2 兼容拆分。

结论:拆分为 FA2 paged-varlen context attention 在 decode + extend + 纯 prefill 混合提交时的 workaround;代码用 `should_split_fa2_dcp_context_attention` 条件化,带 TODO 注释说明 FA4 就绪后可移除。 · 已解决

hybrid 模型 prefix caching 默认策略 question

noooop 询问 `vllm/config/model.py` 中 hybrid 模型 `is_prefix_caching_supported` 改为 True 以及 arg_utils 中 hybrid 默认关闭 prefix caching 的理由。

结论:作者说明 hybrid 模型支持 prefix caching 但保持 opt-in 等待功能成熟,默认仍关闭。 · 已解决

合并前阻塞项与后续跟进 other

GirasoleY 最终评论:"Looks good! Thank you for iterating on this. I opened a draft PR to address the comments, feel free to take over as you see fits. Blocking for the potential hang issue in DCP path. The rest is just nit."

结论:hang 问题由后续 commit e201de2 修复,PR 通过并合并。 · 已解决

风险与影响

  1. 核心路径变更:KV cache 契约、BlockTable、注意力 forward 都是 decode 热路径,max_num_blocks_per_req 从隐式全局推导改为按 spec 计算,需警惕 block table 行宽不足或浪费。
  2. FA2 专用 workaroundrun_split_fa2_dcp_context_attention 只覆盖 fa_version == 2 且混合批次的场景,代码留有 TODO 等待 FA4 支持 Qwen3.5 head_size = 256 后移除;期间 FA3/FA4 走旧路径,两条路径行为需保持一致。
  3. 死锁风险:skip gate 若误用 rank 本地 context 长度会引发 DCP 集体通信死锁;本 PR 已用全局判定并补测试,但仍属高敏感逻辑,后续改动需特别小心。
  4. 兼容性限制:DCP + hybrid 下仅接受 FullAttentionSpecMambaSpec,sliding window 等其他 spec 的混合模型会被显式拒绝;PCP + hybrid 仍不支持。
  5. 性能:skip gate 需要一次 CPU 同步(context_kv_lens_cpu.max().item()),仅发生在存在 seq_lens_cpu_upper_bound 的批次;all_gather 非零维实现含 reshape/movedim,可能略逊于专用 kernel。

对用户:Qwen3.5 系列等混合注意力模型首次可在 DCP 下运行,长上下文 decode 可获得跨 rank 并行收益;此前该组合被整体禁止。对系统:v1 引擎的 KV cache 契约(spec 级 max_num_blocks_per_req)和各层初始化逻辑发生结构性变化,影响 BlockTable 行宽、slot mapping kernel 与 CUDA graph 捕获路径,所有 DCP 用户都会受影响。对团队:需要长期维护 FA2 混合批次拆分这一兼容路径,并在 FA4 就绪后清理;prepare_dcp_dummy_context_metadata 等函数增加了 CUDA graph 捕获的复杂度。

核心缓存契约变更 DCP 死锁风险 FA2 专用 workaround CUDA graph 捕获路径变更 兼容性限制

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论