Prhub

#44455 [2/N][KV-Cache Layout Refactor] Pack K/V into the content dim across attention backends

原始 PR 作者 LucasWilkinson 合并时间 2026-07-11 23:11 文件变更 31 提交数 96 评论 49 代码增减 +664 / -604

执行摘要

将 K/V 打包到内容维度,统一 KV 缓存布局为 4D

参照RFC #42082,标准化KV缓存布局以消除后端间的差异,减少KV-connector中is_mamba/is_mla等标志的泛滥。当前布局导致代码紧耦合,且不利于异构TP和跨层传输。本PR集中解决K/V打包问题,是整体布局标准化系列的一部分。

建议所有v1用户关注此PR,特别是在使用PD disagg的场景下。设计决策值得学习:通过统一布局后端接口,减少连接器耦合。合并前需确保AMD测试修复通过。

讨论亮点

核心讨论包括:

  • AMD测试失败:NickLucche发现三个AMD侧测试失败(MLA backend、MoRIIO connector等),需要修复。
  • prefix_prefill stride映射错误:depthfirst-app指出在prefix_prefill.py中,新旧布局下stride索引交换,可能导致KV数据读写出界(高严重性)。LucasWilkinson后续修复。
  • 量化缓存兼容性:MatthewBonanni担心NVFP4 + 异构TP NIXL传输,LucasWilkinson回应NVFP4 + PD在主线上当前未测试/不工作,因此暂时break是允许的。
  • Intel异构硬件:NickLucche担忧去掉permute逻辑会影响Intel等异构部署,需要确保backend支持HND。
  • ROCm_ATTN支持:NickLucche疑问ROCM_ATTN是否支持blocks-first,LucasWilkinson确认在MI300x上测试无误。
  • 文档删除:NickLucche对Claude删除NIXL文档表示不满。

实现拆解

按以下步骤实现:

  1. 修改注意力后端形状定义:在所有注意力后端(FlashAttention、FlashInfer、Triton、ROCm等)的get_kv_cache_shape方法中,将返回的形状从(num_blocks, 2, block_size, num_kv_heads, head_size)改为(num_blocks, num_kv_heads, block_size, 2*head_size)。量化缓存类似,但使用padded_hs。
  2. 更新stride order:对应的get_kv_cache_stride_order方法适配4D形状,在NHD和HND布局下分别返回合适的permutation(例如NHD下为(0,2,1,3))。
  3. 改造K/V提取逻辑:在各个后端的forward/do_kv_cache_update中,用kv_cache.transpose(1,2).split(head_size, dim=-1)替换旧的kv_cache.unbind(1)PagedAttention.split_kv_cache调用,获得零成本view。
  4. 简化NVFP4量化缓存处理:移除nvfp4_kv_cache_split_views函数,改为调用nvfp4_split_data_scale对一侧处理。量化缓存使用separate head groups布局时,直接在dim=1上split。
  5. 调整KV-connector传输拓扑:在TransferTopology中判断形状是否为4D blocks-first并断言,删除is_kv_layout_blocks_firstsplit_k_and_v等属性,简化虚拟split逻辑。NIXL和MoRIIO连接器均适配新的4D布局,K/V不再拆分为独立区域。
  6. 更新测试和平台代码:测试文件(test_nixl_connector、test_mooncake_connector、test_minimax_m3等)更新以匹配新形状;ROCm平台代码调整以适应包装布局。
文件 模块 状态 重要度
vllm/utils/torch_utils.py 工具函数 modified 7.95
vllm/distributed/kv_transfer/kv_connector/utils.py KV 连接器 modified 7.74
vllm/v1/attention/backends/triton_attn.py 注意力后端 modified 7.61
vllm/v1/attention/backends/flashinfer.py 注意力后端 modified 7.18
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py NIXL 连接器 modified 7.12
vllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_layout.py MoRIIO 连接器 modified 6.99
tests/v1/kv_connector/unit/test_nixl_connector.py NIXL 测试 modified 6.23

关键符号

nvfp4_split_data_scale is_kv_layout_blocks_first split_k_and_v _content_packed_dim TritonAttentionBackend.get_kv_cache_shape TritonAttentionBackend.get_kv_cache_stride_order FlashInferBackend.get_kv_cache_shape TransferTopology.__post_init__

关键源码片段

vllm/utils/torch_utils.py core-logic

修改 NVFP4 KV 缓存的拆分函数,移除旧的分拆视图函数,精简为单侧处理,反映布局变化对量化缓存的影响。

def nvfp4_split_data_scale(kv_side: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """
    将NVFP4缓存的一侧(K或V)拆分为数据和缩放因子。
    输入形状为 (B, H, N, full_dim),其中 full_dim = data_dim + scale_dim。
    布局为 [K_data | K_scale | V_data | V_scale] 每页连续存储。
    调用方需先通过 split 或切片获得单侧。
    """
    num_pages, dim_1, dim_2, full_dim = kv_side.shape
    data_dim = full_dim * 8 // 9
    scale_dim = full_dim - data_dim
    data_per_kv = dim_1 * dim_2 * data_dim
    page_bytes = kv_side.stride(0)
​
    # 从原始 stride 推导 data 和 scale 的 stride,保持物理布局(NHD 或 HND)
    s1 = kv_side.stride(1) * data_dim // full_dim
    s2 = kv_side.stride(2) * data_dim // full_dim
    data_shape = (num_pages, dim_1, dim_2, data_dim)
    data_strides = (page_bytes, s1, s2, 1)
​
    s1_s = kv_side.stride(1) * scale_dim // full_dim
    s2_s = kv_side.stride(2) * scale_dim // full_dim
    scale_shape = (num_pages, dim_1, dim_2, scale_dim)
    scale_strides = (page_bytes, s1_s, s2_s, 1)
​
    base = kv_side.storage_offset()
    data = torch.as_strided(kv_side, data_shape, data_strides, storage_offset=base)
    scale = torch.as_strided(kv_side, scale_shape, scale_strides, storage_offset=base + data_per_kv).view(torch.float8_e4m3fn)
    return data, scale
vllm/distributed/kv_transfer/kv_connector/utils.py core-logic

核心修改:在 TransferTopology 中识别 4D packed 形状,简化 blocks-first 判断,移除 split_k_and_v 属性,直接使用统一布局。

# 在 TransferTopology.__post_init__ 中,识别标准 4D packed 布局
head_size = 1
# Mock 一个块形状来检查 layout
kv_cache_shape = attn_backend.get_kv_cache_shape(
    num_blocks=1, block_size=_MOCK_BLOCK_SIZE, num_kv_heads=1, head_size=head_size,
)
logger.debug("Test kv_cache_shape: %s", kv_cache_shape)# 新的注意力缓存形状应为 4D: [num_blocks, num_kv_heads, block_size, content_size]
assert kv_cache_shape[0] == 1, (
    "KV cache layout must be blocks-first; expected mocked "
    f"num_blocks=1 in leading dim, got shape {kv_cache_shape}."
)
if not self.is_mla:
    assert len(kv_cache_shape) == 4, (
        "Attention KV cache layout must be standardized as "
        "[num_blocks, num_kv_heads, block_size, content_size], "
        f"got shape {kv_cache_shape}."
    )# 旧属性 is_kv_layout_blocks_first 和 split_k_and_v 被移除,
# blocks-first 已成为唯一布局,K/V 不再需要拆分传输。
vllm/v1/attention/backends/triton_attn.py core-logic

展示了核心的 get_kv_cache_shape 和 get_kv_cache_stride_order 变化,反映 4D 打包布局的实现。

@staticmethod
def get_kv_cache_shape(num_blocks, block_size, num_kv_heads, head_size, cache_dtype_str="auto"):
    # K 和 V 打包到最后一维:逻辑形状为 (B, H, N, 2*C)
    if block_size % 16 != 0:
        raise ValueError("Block size must be a multiple of 16.")
    if kv_cache_uses_per_token_head_scales(cache_dtype_str):
        # 量化情况下额外 padding 用于存放缩放因子
        from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE, get_dtype_size
        cache_dtype = STR_DTYPE_TO_TORCH_DTYPE[cache_dtype_str]
        scale_pad = get_dtype_size(torch.float32) // get_dtype_size(cache_dtype)
        if get_kv_quant_mode(cache_dtype_str) == KVQuantMode.INT4_PER_TOKEN_HEAD:
            data_head_size = head_size // 2
        else:
            data_head_size = head_size
        padded_hs = data_head_size + scale_pad
        return (num_blocks, num_kv_heads, block_size, 2 * padded_hs)
    return (num_blocks, num_kv_heads, block_size, 2 * head_size)@staticmethod
def get_kv_cache_stride_order(cache_layout=None):
    """根据cache_layout返回从逻辑形状到物理内存的permutation。
    逻辑形状 (B, H, N, 2*C),物理排列由stride_order控制。
    NHD:物理为 (B, N, H, 2*C)
    HND:物理为 (B, H, N, 2*C)(恒等)
    """
    if cache_layout == "NHD":
        return (0, 2, 1, 3)
    elif cache_layout == "HND":
        return (0, 1, 2, 3)
    else:
        raise ValueError(f"Unknown cache layout: {cache_layout}")

评论区精华

AMD 测试失败 测试

NickLucche 报告三个 AMD 测试失败(MLA backend、MoRIIO connector 等),需要修复。

结论:需要在合并前修复,后续提交似乎已解决。 · 已解决

prefix_prefill stride 映射错误 正确性

depthfirst-app 指出在 prefix_prefill.py 的 `_fwd_kernel` 和 `_fwd_kernel_alibi` 中,新旧布局下 stride 索引交换,可能导致 KV 数据读写出界(高严重性)。

结论:LucasWilkinson 在后续提交中修复。 · 已解决

NVFP4 量化缓存兼容性 question

MatthewBonanni 担心 NVFP4 + 异构 TP NIXL 传输的兼容性。

结论:LucasWilkinson 回应 NVFP4 + PD 在主线上当前未测试 / 不工作,暂时 break 可以接受,后续再完善。 · 待处理

Intel 异构硬件支持 设计

NickLucche 担忧移除 NHD permute 逻辑会影响 Intel 等异构部署,需要确保 backend 支持 HND。

结论:暂无明确结论,需 Intel 方确认。 · 待处理

DOCS 文档被删除 question

NickLucche 对 Claude 自动删除 NIXL 文档表示不满。

结论:属于自动化 review 工具的副作用。 · 已解决

风险与影响

  1. stride计算错误(高):在prefix_prefill和scale cache中,深度优先分析发现stride映射错误,可能导致KV数据损坏或越界读取。已在后续提交中修复,但需警惕未覆盖的kernel路径。
  2. 量化缓存回归(中):NVFP4和FP8量化缓存布局特殊,新方案使用head-group布局,若未彻底测试可能引入精度或性能问题。当前NVFP4+PD未被测试,可能隐藏回归。
  3. 异构硬件兼容性(中):Intel GPU、AMD等平台需要额外验证新布局,特别是NIXL连接器移除permute逻辑后对非默认布局的影响。
  4. 性能退化(低):新增的transpose和split操作理论上为零成本view,但某些路径可能因stride不连续导致后续kernel变慢。复审评论中提到perf影响应极小。

影响范围广:所有使用v1 attention backends的模型都会受到影响,因为KV缓存形状改变。但经过E2E lm-eval PD disagg测试验证,覆盖了Qwen、DeepSeek、Nemotron等模型在多种backend和拓扑配置下,结果通过。用户升级到该PR后需确保模型兼容。团队需注意与外部PR(如Intel GPU、AMD ROCm)的冲突。测试配套更新完整,包括connector单元测试。

核心路径变更 多个 backend 同时修改 量化缓存路径未充分测试 AMD 测试失败待修复 prefix_prefill stride 隐患 异构硬件兼容性待确认

关联 Issue

#42082 [RFC]: Standardize KV-cache Layouts

完整报告

参与讨论