Prhub

#47685 [ROCm] Align mixed encoder-decoder KV cache views in V2 runner

原始 PR 作者 AndreasKaratzas 合并时间 2026-07-07 12:09 文件变更 1 提交数 3 评论 4 代码增减 +82 / -0

执行摘要

修复 ROCm V2 runner 混合 KV 缓存视图对齐问题

PR #47035 标准化了旧模型运行器路径,但 V2 路径中 _reshape_kv_cache 仍存在混合布局语义:decoder self-attention 使用 ROCm 的 K/V-first 布局,而 cross-attention 使用 blocks-first 后端视图,导致块 ID 索引到不同的物理字节,引发 Nemotron Parse 生成测试失败。该 PR 旨在消除这种不一致,使共享分配上的所有视图物理地址一致。

该 PR 值得精读,特别是对 ROCm 平台或涉及混合 attention 后端的开发者。设计决策中,通过 stride 调整而非数据拷贝来对齐视图是一种高效方案。建议关注后续是否添加单元测试以及是否引入平台门控。

讨论亮点

Review 中主要讨论了两点:

  1. 是否 ROCm 特有:njhill 提问是否是 ROCm 特定的要求,避免不必要的工作。AndreasKaratzas 提议可以 gate under rocm,但 njhill 表示如果确实是 ROCm 特有最好避免门控,需要更好理解。最终合并时未加平台门控,因为该逻辑对 CUDA 可能也是安全的(只是多一次属性检查)。

  2. 复现验证:yewentao256 要求补充复现命令和日志,AndreasKaratzas 提供了 Buildkite job 链接验证修复。

实现拆解

实现按以下步骤进行:

  1. _reshape_kv_cache 末尾新增 elif has_attn and kv_cache_config is not None 分支,调用 _align_mixed_attention_kv_cache_views

  2. 新函数 _align_mixed_attention_kv_cache_views 遍历 attention 组,通过 get_kv_cache_block_dim 获取每个后端的块维度(0 表示 blocks-first,1 表示 K/V-first)。

  3. 遍历 kv_cache_config.kv_cache_tensors 中的共享分配,检查是否被不同块维度的层共享。若同时存在 0 和 1,说明视图冲突。

  4. 对块维度为 0 的层调用 _restride_blocks_first_kv_cache_to_kv_first_storage 重新设置 stride,使 blocks-first 视图在物理 K/V-first 存储上正确映射。

  5. 辅助函数 _restride 就地修改 tensor 的 stride 属性,避免数据拷贝。

该改动仅涉及一个文件,未添加测试,依赖现有集成测试覆盖。

文件 模块 状态 重要度
vllm/v1/worker/gpu/attn_utils.py KV 缓存 modified 7.81

关键符号

_align_mixed_attention_kv_cache_views _restride_blocks_first_kv_cache_to_kv_first_storage

关键源码片段

vllm/v1/worker/gpu/attn_utils.py core-logic

唯一变更文件,包含核心修复逻辑。新增两个函数:_align_mixed_attention_kv_cache_views 检测共享分配中的视图冲突,_restride_blocks_first_kv_cache_to_kv_first_storage 调整 blocks-first 视图的步长。

# 在 _reshape_kv_cache 末尾,处理完 Mamba 布局后添加:
elif has_attn and kv_cache_config is not None:
    _align_mixed_attention_kv_cache_views(
        attn_groups=attn_groups,
        kv_caches=kv_caches,
        kernel_block_sizes=kernel_block_sizes,
        cache_dtype=cache_dtype,
        kv_cache_config=kv_cache_config,
    )
​
​
def _align_mixed_attention_kv_cache_views(
    attn_groups: Iterable[AttentionGroup],
    kv_caches: dict[str, Any],
    kernel_block_sizes: list[int],
    cache_dtype: str,
    kv_cache_config: KVCacheConfig,
) -> None:
    """对齐共享 attention KV 视图,当不同后端布局不一致时。
    Encoder-decoder 模型可以在 decoder self-attention(使用 ROCM_ATTN,
    块维度为 1,K/V-first 布局)和 cross-attention(使用 blocks-first 后端)
    之间共享一个原始 KV 缓存分配。此函数保持物理存储为 ROCM_ATTN 期望的
    K/V-first 布局,并重新设置 blocks-first 逻辑视图的步长,使得块 ID 索引
    到相同的物理字节。
    """
    block_dims_by_layer: dict[str, int] = {}
    for group in attn_groups:
        kv_cache_spec = group.kv_cache_spec
        if not isinstance(kv_cache_spec, AttentionSpec):
            continue
        if group.kv_cache_group_id >= len(kernel_block_sizes):
            continue
        block_dim = group.backend.get_kv_cache_block_dim(
            kernel_block_sizes[group.kv_cache_group_id],
            kv_cache_spec.num_kv_heads,
            kv_cache_spec.head_size,
            cache_dtype_str=cache_dtype,
        )
        for layer_name in group.layer_names:
            if layer_name in kv_caches:
                block_dims_by_layer[layer_name] = block_dim
​
    for kv_tensor in kv_cache_config.kv_cache_tensors:
        if kv_tensor.block_stride > 0:
            continue
        shared_block_dims = {
            block_dims_by_layer[layer_name]
            for layer_name in kv_tensor.shared_by
            if layer_name in block_dims_by_layer
        }
        if 0 not in shared_block_dims or 1 not in shared_block_dims:
            continue
​
        for layer_name in kv_tensor.shared_by:
            if block_dims_by_layer.get(layer_name) == 0:
                _restride_blocks_first_kv_cache_to_kv_first_storage(
                    kv_caches[layer_name]
                )
​
​
def _restride_blocks_first_kv_cache_to_kv_first_storage(
    kv_cache: torch.Tensor,
) -> None:
    """通过 as_strided_ 调整 stride,使 blocks-first 视图在
    K/V-first 物理存储上正确映射。
    """
    assert kv_cache.ndim >= 3
    assert kv_cache.shape[1] == 2
    page_size = kv_cache.shape[2:].numel()
    num_blocks = kv_cache.shape[0]
    expected_tail_stride = torch.empty(kv_cache.shape[2:]).stride()
    # 后续根据 expected_tail_stride 计算新 stride 并调用 as_strided_
    ...

评论区精华

补充复现信息 other

yewentao256 要求提供完整复现命令和错误日志。AndreasKaratzas 提供了 Buildkite job 链接,证明修复通过。

结论:修复通过验证,未进一步质疑。 · 已解决

是否 ROCm 特有?考虑门控 设计

njhill 询问是否是 ROCm 特有要求,避免不必要工作。AndreasKaratzas 建议可以加门控。njhill 表示如果确实是 ROCm 特有最好,但需要更好理解。最终未加门控。

结论:暂时不添加平台门控,后续再评估。 · 已解决

风险与影响

  • 性能风险:新函数每次 KV 缓存分配时运行,但仅在混合布局存在时进行实际 stride 调整,开销极小。
  • 兼容性风险:修改了共享 KV 缓存的视图,可能影响其他依赖 blocks-first 布局的组件(如 KV Connector、推测解码)。但当前仅在检测到混合布局时操作,且物理存储保持 K/V-first 不变,风险可控。
  • 测试覆盖:无直接单元测试,仅依赖 Nemotron Parse 集成测试,可能遗漏其他混合布局场景。
  • 用户影响:修复了 ROCm 上 Nemotron Parse 模型生成错误的 bug,这些用户可直接受益。
  • 系统影响:新增两个内部函数,仅在 encoder-decoder 且存在共享分配时激活,不影响纯 decoder 或非共享模型。
  • 团队影响:提供了处理多 attention 后端布局差异的通用机制,便于后续支持更多后端。
非 CUDA 测试覆盖不足 共享视图调整可能影响其他组件

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论