Prhub

#45111 [Attention] Re-enable cross-layer KV cache layout for MLA via stride-aware kernels

原始 PR 作者 ivanium 合并时间 2026-06-22 21:57 文件变更 13 提交数 4 评论 2 代码增减 +847 / -21

执行摘要

修复三个 MLA 内核 stride bug,重新启用跨层 KV 缓存布局

Issue #37032 报告 GLM-4.7-Flash 启用 KV offloading 时输出乱码。PR #37090 紧急禁止 MLA 模型使用跨层 KV 缓存布局,牺牲了该布局带来的内存和性能优势。本 PR 确定根因为少数内核未正确使用缓存的 block 维 stride,修复后可安全恢复跨层布局,同时保持对未验证后端的保守行为。

建议关注注意力系统和 MLA 模型的开发者精读。该 PR 展现了从 issue 分析到内核修复再到测试验证的完整工程实践,尤其是如何构造 torch.as_strided 跨层视图进行位精确等价测试的方法值得推广。

讨论亮点

仅有一条 review 讨论:LucasWilkinson 在 vllm/v1/attention/ops/triton_decode_attention.py 中对 _page_stride 的实现提出改良建议,推荐使用 unflatten 比手动 stride 计算更清晰。ivanium 接受建议并立即修正。该讨论无争议,门关闭之前已 resolve。

实现拆解

  1. 内核 stride 修复:修改 vllm/v1/attention/ops/triton_decode_attention.py 中两个 stage-1 解码 kernel,将 page_stride 参数化;修改 csrc/libtorch_stable/attention/mla/sm100_cutlass_mla_kernel.custride_C 从硬编码改为读取 kv_c_and_k_pe_cache.stride(0)/stride(1);修改 csrc/libtorch_stable/cache_kernels.cuindexer_k_quant_and_cache 的 block 基地址计算改用 kv_cache.stride(0)
  2. Per-backend opt-in:在 TritonMLABackendCutlassMLABackendFlashAttnMLABackendFlashMLABackendFlashInferMLABackend 五个后端中静态重写 get_kv_cache_stride_order(),在 include_num_layers_dimension=True 时返回 (1,0,2,3)(num_blocks 居前),表示支持跨层布局。MLACommonBackend 保持恒等排列 (0,1,2,3) 作为安全默认,未验证后端保持 opt-out。
  3. 新增测试:新增 tests/kernels/attention/test_mla_cross_layer_kernel_equivalence.py,包含 7 个测试函数,用 torch.as_strided 构造跨层视图,验证 concat_and_cache_mla 写入、FlashMLA 密集/稀疏解码、FlashInfer MLA 解码、FA3 解码、indexer_k_quant_and_cache 的位精确等价和无泄漏。在 test_triton_decode_attention.py 新增参数化测试 test_decode_attention_cross_layer_view,覆盖 MLA/GQA/MHA 路径。在 test_cutlass_mla_decode.py 新增 test_cutlass_mla_decode_cross_layer_view
  4. 更新布局测试test_kv_cache_layout.py 重命名旧测试为 test_mla_common_backend_rejects_cross_layer_kv_cache,新增参数化测试 test_verified_mla_backends_support_cross_layer_kv_cache 验证五个后端正返回 (1,0,2,3)
文件 模块 状态 重要度
tests/kernels/attention/test_mla_cross_layer_kernel_equivalence.py 跨层内核测试 added 8.14
tests/kernels/attention/test_triton_decode_attention.py Triton 解码测试 modified 6.65
tests/kernels/attention/test_cutlass_mla_decode.py Cutlass 解码测试 modified 6.48
tests/v1/kv_connector/unit/test_kv_cache_layout.py 缓存布局测试 modified 6.53
vllm/v1/attention/ops/triton_decode_attention.py Triton 解码 kernel modified 5.39
vllm/v1/attention/backends/mla/triton_mla.py MLA 后端 modified 6.35
csrc/libtorch_stable/cache_kernels.cu 缓存内核 modified 3.13

关键符号

_page_stride get_kv_cache_stride_order test_concat_and_cache_mla_into_unified_slot_view test_flashmla_dense_decode_unified_slot_view test_flashinfer_mla_dense_decode_unified_slot_view test_flashmla_fp8_sparse_decode_unified_slot_view test_indexer_k_quant_and_cache_into_unified_slot_view test_flashattn_mla_dense_decode_unified_slot_view test_decode_attention_cross_layer_view test_cutlass_mla_decode_cross_layer_view test_mla_common_backend_rejects_cross_layer_kv_cache test_verified_mla_backends_support_cross_layer_kv_cache

关键源码片段

tests/kernels/attention/test_mla_cross_layer_kernel_equivalence.py test-coverage

新增测试套件,验证所有 MLA 内核在跨层视图下与连续缓存位精确等价,是发现和验证 stride bug 的核心测试。

import pytest
import torchpytestmark = pytest.mark.skipif(
    not torch.cuda.is_available(), reason="MLA cache kernels require CUDA"
)def test_concat_and_cache_mla_into_unified_slot_view():
    """验证 concat_and_cache_mla 写入跨层缓存视图的正确性,确保无泄漏到相邻层。"""
    from vllm import _custom_ops as ops
​
    torch.manual_seed(0)
    dev = "cuda"
    kv_lora_rank = 512
    pe = 64
    entry = kv_lora_rank + pe
    page = 64
    num_blocks = 32
    ntok = 200
​
    # 随机输入数据
    kv_c = torch.randn(ntok, kv_lora_rank, device=dev, dtype=torch.bfloat16)
    k_pe = torch.randn(ntok, pe, device=dev, dtype=torch.bfloat16)
    slot = torch.randperm(num_blocks * page, device=dev, dtype=torch.int64)[:ntok]
    scale = torch.tensor(1.0, device=dev)
​
    def write(cache):
        ops.concat_and_cache_mla(kv_c, k_pe, cache, slot, "auto", scale)
​
    # 参考:连续单层缓存,形状 (num_blocks, page, entry)
    ref = torch.zeros(num_blocks, page, entry, device=dev, dtype=torch.bfloat16)
    write(ref)
​
    # 跨层缓存:每个 block 包含 3 层,取中间层的视图(stride(0) = 完整 slot 大小)
    layer_page_elems = page * entry
    n_layers = 3
    unified_slot_elems = n_layers * layer_page_elems
    big = torch.zeros(num_blocks, unified_slot_elems, device=dev, dtype=torch.bfloat16)
    flat = big.view(-1)
    offset = layer_page_elems # 中间层
    view = torch.as_strided(
        flat,
        size=(num_blocks, page, entry),
        stride=(unified_slot_elems, entry, 1),
        storage_offset=offset,
    )
    write(view)
​
    # 位精确等价检验
    max_diff = (ref.float() - view.float()).abs().max().item()
    assert max_diff == 0.0, f"max|Δ| = {max_diff}"
​
    # 验证相邻层未被污染
    neighbour_lo = torch.as_strided(
        flat, (num_blocks, layer_page_elems), (unified_slot_elems, 1), 0
    )
    neighbour_hi = torch.as_strided(
        flat,
        (num_blocks, layer_page_elems),
        (unified_slot_elems, 1),
        2 * layer_page_elems,
    )
    assert neighbour_lo.abs().max().item() == 0.0
    assert neighbour_hi.abs().max().item() == 0.0
tests/kernels/attention/test_triton_decode_attention.py test-coverage

新增 test_decode_attention_cross_layer_view 参数化测试,验证 Triton 解码内核处理跨层视图的正确性,覆盖 MLA/GQA/MHA 三种注意力模式。

@pytest.mark.parametrize(
    "H_Q,H_KV,D_QK,D_V,is_mla",
    [
        (16, 1, 576, 512, True), # MLA 路径 (grouped kernel, v = trans(k))
        (32, 8, 128, 128, False), # GQA 路径 (grouped kernel)
        (32, 32, 128, 128, False), # MHA 路径 (normal kernel)
    ],
)
@pytest.mark.parametrize("PAGE_SIZE", [16])
def test_decode_attention_cross_layer_view(H_Q, H_KV, D_QK, D_V, is_mla, PAGE_SIZE):
    """验证解码内核使用缓存的实际 page-dim stride,而非假设 pages 连续排列。
    跨层缓存中每层的视图 stride(0) 被 num_layers 放大,输出必须与连续缓存完全一致。"""
    B = 3
    seq_len = 1027
    CACHE_SIZE = 16384
    NUM_LAYERS = 3
    LAYER_IDX = 1
    dtype = torch.bfloat16
    sm_scale = 1.0 / (D_QK**0.5)
    num_kv_splits = 8
    num_pages = CACHE_SIZE // PAGE_SIZE
​
    # 构建连续缓存的参考输出
    k_ref = torch.randn(num_pages, PAGE_SIZE, H_KV, D_QK, dtype=dtype, device=DEVICE_TYPE)
    if is_mla:
        v_ref = k_ref[..., :D_V]
    else:
        v_ref = torch.randn(num_pages, PAGE_SIZE, H_KV, D_V, dtype=dtype, device=DEVICE_TYPE)
​
    # 跨层缓存:相邻层填充随机垃圾,确保 packed-pages 寻址会读到错误数据
    k_xl = torch.randn(num_pages, NUM_LAYERS, PAGE_SIZE, H_KV, D_QK, dtype=dtype, device=DEVICE_TYPE)
    k_view = k_xl[:, LAYER_IDX]
    k_view.copy_(k_ref)
    if is_mla:
        v_view = k_view[..., :D_V]
    else:
        v_xl = torch.randn(num_pages, NUM_LAYERS, PAGE_SIZE, H_KV, D_V, dtype=dtype, device=DEVICE_TYPE)
        v_view = v_xl[:, LAYER_IDX]
        v_view.copy_(v_ref)
​
    def run(k_buffer, v_buffer):
        o = torch.zeros(B, H_Q, D_V, dtype=dtype, device=DEVICE_TYPE)
        lse = torch.zeros(B, H_Q, dtype=dtype, device=DEVICE_TYPE)
        attn_logits = torch.empty((B, H_Q, num_kv_splits, D_V + 1), dtype=torch.float32, device=DEVICE_TYPE)
        decode_attention_fwd(q, k_buffer, v_buffer, o, lse, req_to_page, b_seq_len,
                             attn_logits, num_kv_splits, sm_scale, PAGE_SIZE, is_mla=is_mla)
        return o, lse
​
    o_ref, lse_ref = run(k_ref, v_ref)
    o_xl, lse_xl = run(k_view, v_view)
    assert torch.equal(o_ref, o_xl)
    assert torch.equal(lse_ref, lse_xl)
tests/kernels/attention/test_cutlass_mla_decode.py test-coverage

新增 test_cutlass_mla_decode_cross_layer_view,验证 sm100 Cutlass MLA decode 内核在跨层视图下的正确性,这是修复的 kernel 之一。

@pytest.mark.skipif(
    not current_platform.has_device_capability(100),
    reason=CUTLASS_MLA_UNSUPPORTED_REASON,
)
@torch.inference_mode()
def test_cutlass_mla_decode_cross_layer_view():
    """验证 Cutlass MLA decode 内核使用缓存的 page-dim stride,而非假设 pages 连续排列。
    跨层视图的 stride(0) 被 num_layers 放大,输出必须与连续缓存完全一致。"""
    device = torch.device("cuda:0")
    torch.set_default_dtype(torch.bfloat16)
    torch.set_default_device(device)
    torch.manual_seed(42)
​
    b, mean_sk, d, dv, block_size = 4, 512, 576, 512, 64
    num_layers, layer_idx = 3, 1
    scale = math.sqrt(d) ** (-1)
​
    num_pages = b * (mean_sk // block_size)
    cache_seqlens = torch.full((b,), mean_sk, dtype=torch.int32)
    block_table = torch.arange(num_pages, dtype=torch.int32).view(b, mean_sk // block_size)
​
    # 连续缓存参考
    kv_contig = torch.randn(num_pages, block_size, d)
    # 跨层缓存,相邻层填随机数据
    kv_cross_layer = torch.randn(num_pages, num_layers, block_size, d)
    kv_view = kv_cross_layer[:, layer_idx]
    kv_view.copy_(kv_contig)
    assert kv_view.stride(0) == num_layers * block_size * d
​
    q_nope = torch.randn(b, 128, dv)
    q_pe = torch.randn(b, 128, d - dv)
    sm_count = num_compute_units(device.index)
    workspace_size = ops.sm100_cutlass_mla_get_workspace_size(mean_sk, b, sm_count, num_kv_splits=1)
    workspace = torch.empty(workspace_size, dtype=torch.uint8)
​
    def run(cache):
        out = torch.empty(b, 128, dv)
        lse = torch.empty(b, 128, dtype=torch.float32)
        ops.sm100_cutlass_mla_decode(out, lse, q_nope, q_pe, cache, cache_seqlens,
                                     block_table, workspace, scale, 1)
        return out, lse
​
    out_contig, lse_contig = run(kv_contig)
    out_view, lse_view = run(kv_view)
​
    assert torch.equal(out_contig, out_view)
    assert torch.equal(lse_contig, lse_view)

评论区精华

_page_stride 实现方式 style

LucasWilkinson 在 review 中建议使用 `unflatten` 替代手动 stride 计算,认为更清晰。ivanium 感谢并立即修改。

结论:ivanium 修改了实现,改用了 unflatten 方案。 · 已解决

风险与影响

核心注意力路径变更,影响所有 MLA 模型(DeepSeek、GLM-4.7 等)。主要风险:虽全面测试覆盖已验证后端,但未验证后端(ROCm AITER、tokenspeed、XPU)保持 identity 排列,完全不受影响。Triton decode attention 也服务非 MLA 模型,但测试已覆盖 GQA/MHA 路径,且修复仅在 stride 使用方式上,GPU 计算逻辑不变,回归风险低。性能影响:新增 stride 读取开销可忽略。兼容性:get_kv_cache_stride_order 协议扩展为每个后端可显式 opt-in,向后兼容。

用户:MLA 模型(如 DeepSeek-V2/V3、GLM-4.7)在使用 KV offloading 时不再产生乱码,并可重新利用跨层缓存布局减少内存占用、提升吞吐。开发者:新增 MLA 后端时需覆写 get_kv_cache_stride_order 以选择是否支持跨层;测试套件为后续 kernel 的 stride 正确性提供了校验模板。系统:无外部接口变更,所有影响限于内部内核和注意力层。

注意力路径核心变更 回归风险低但影响面广 未验证后端自动 opt-out

关联 Issue

#34742 refactor(attention): add default get_kv_cache_stride_order implementation
#37032 [Bug]: GLM 4.7-flash returns gibberish when native KV cache offloading is on
#37090 [Bugfix] Disable cross-layer KV cache for MLA attention backends
#41093 [P/D][Mooncake] Add cross-layer KV cache support to MooncakeConnector
#42374 [Core][WIP][1/N] Standardize kv layout
#44455 [2/N][KV-Cache Layout Refactor] Pack K/V into the content dim across attention backends
#44458 [4/N][KV-Cache Layout Refactor] Standardize KV cache layout

完整报告

参与讨论