Prhub

#45181 [Spec Decode] Support mixed KV page sizes for DFlash

原始 PR 作者 pst2154 合并时间 2026-06-21 22:45 文件变更 8 提交数 10 评论 23 代码增减 +511 / -141

执行摘要

支持 DFlash 混合 KV 页面大小的填充与 reshape

DFlash draft 模型可能具有比目标模型更小的 KV head size(例如 MiMo 使用 192 维目标 KV heads 和 128 维 draft KV heads),导致页面大小比例为 3:2。现有 block-size scaling 路径无法安全统一,因此需要填充较小的物理页面,并通过正确的 stride 使注意力后端正确读取。详见 PR body。

推荐精读的设计决策:

  • 如何用 indexes_kv_by_block_stride 统一两种需要非连续 block stride 的场景(跨层统一布局和填充页面布局)。
  • 填充 vs 放大 block size 的权衡选择和实现细节。
  • _reshape_attention_kv_cache 的 strided view 计算方式,尤其是对 num-blocks-first 布局的适配。
  • 测试设计:覆盖 FlashAttention、HND、DiffKV 三种不同 stride order 的后端。
讨论亮点

核心讨论

  • 安全性讨论:benchislett 询问增加 block size 是否安全,pst2154 解释已有整除分支保持不变,新路径仅处理不可整除情况。
  • 后端兼容性:heheda12345 提出是否所有后端都支持 strided view,TheEpicDolphin 建议引入 supports_padded_kv_pages 标志保守启用。后 ivanium 指出可重用 use_uniform_kv_cache 的检测逻辑,最终演化为 indexes_kv_by_block_stride 属性。
  • 命名争议:ivanium 建议改为 block_stride_agnostic,TheEpicDolphin 坚持保留以与方法名一致,ivanium 最终同意。
  • merge 缺失字段:ivanium 发现 merge() 方法未传播 indexes_kv_by_block_stride,提供补丁修复。

实现拆解

  1. 添加后端能力检测:在 vllm/v1/attention/backend.pyAttentionBackend 中新增类方法 indexes_kv_by_block_stride,通过检查 get_kv_cache_stride_order 返回的 stride order 判断后端是否以 num-blocks-first 布局索引 KV 页面。该方法返回 bool,被 AttentionSpec 缓存为 indexes_kv_by_block_stride 字段(vllm/v1/kv_cache_interface.py)。

  2. 填充策略实现:在 vllm/v1/core/kv_cache_utils.pyunify_kv_cache_spec_page_size 中处理不可整除情况:当后端支持(indexes_kv_by_block_stride=True)时,设置 page_size_padded 为最大页面大小,保持原 block size;否则抛出 NotImplementedError

  3. 公共 reshape 函数提取:将 _reshape_kv_cache 中的注意力 reshape 逻辑抽取为独立函数 _reshape_attention_kv_cache,位于 vllm/v1/worker/gpu/attn_utils.py。该函数支持填充页面的 strided view,要求 kv_cache_shape[0] == num_blocks(num-blocks-first),并通过 torch.as_strided 修改 block stride 以跳过填充区域。

  4. 调用端适配:在 vllm/v1/worker/gpu_model_runner.py 中,_reshape_kv_cache_tensors 改用 _reshape_attention_kv_cache,删除内联的 padding 处理。在 vllm/v1/worker/kv_connector_model_runner_mixin.py 中,use_uniform_kv_cache 简化逻辑,直接检查 kv_cache_spec.indexes_kv_by_block_stride

  5. 测试覆盖:新增 tests/v1/worker/test_attn_utils.py,覆盖 FlashAttention、HND、DiffKV 三种后端在填充页面下的 stride 正确性;在 tests/v1/core/test_kv_cache_utils.py 中添加混合页面大小填充测试和后备降级测试。

文件 模块 状态 重要度
tests/v1/worker/test_attn_utils.py 测试 added 7.9
vllm/v1/worker/gpu/attn_utils.py 核心逻辑 modified 7.71
vllm/v1/attention/backend.py 基类 modified 7.12
vllm/v1/core/kv_cache_utils.py 工具 modified 6.8
vllm/v1/kv_cache_interface.py 接口 modified 5.89

关键符号

_reshape_attention_kv_cache indexes_kv_by_block_stride unify_kv_cache_spec_page_size use_uniform_kv_cache test_reshape_padded_flash_attention_kv_cache_strides_by_page

关键源码片段

tests/v1/worker/test_attn_utils.py test-coverage

新增测试文件,覆盖 FlashAttention、HND、DiffKV 后端在填充页面下的 stride 正确性,确保重塑逻辑正确。

def test_reshape_padded_flash_attention_kv_cache_strides_by_page():
    # 测试填充页面下 FlashAttention 后端的 KV 缓存 stride 和 offset
    num_blocks = 3
    # 构造一个填充后的 AttentionSpec:page_size_padded=384,实际 unpadded 大小 =256
    spec = FullAttentionSpec(
        block_size=16,
        num_kv_heads=1,
        head_size=2,
        dtype=torch.float32,
        page_size_padded=384,
    )
    assert spec.real_page_size_bytes == 256
​
    raw_tensors = {
        "layer": torch.zeros(spec.page_size_bytes * num_blocks, dtype=torch.int8)
    }
    attn_groups = [
        AttentionGroup(
            backend=FakeFlashAttentionBackend,
            layer_names=["layer"],
            kv_cache_spec=spec,
            kv_cache_group_id=0,
        )
    ]
​
    kv_cache = _reshape_kv_cache(attn_groups, raw_tensors, "auto", [spec.block_size], {})["layer"]
​
    # 验证 shape 为 (num_blocks, 2, block_size, num_kv_heads, head_size)
    assert kv_cache.shape == (num_blocks, 2, 16, 1, 2)
    # 验证 block stride 等于填充后页面大小(除以 dtype 大小)
    assert kv_cache.stride(0) == spec.page_size_bytes // 4
    # 验证 K/V 维度 stride 等于实际 unpadded 页面的一半
    assert kv_cache.stride(1) == spec.real_page_size_bytes // 2 // 4
    # 验证第二个 block 的 K 部分 offset 正确跳过了填充 page
    assert kv_cache[1, 0].storage_offset() == spec.page_size_bytes // 4
    assert (
        kv_cache[1, 1].storage_offset()
        == (spec.page_size_bytes + spec.real_page_size_bytes // 2) // 4
    )
vllm/v1/worker/gpu/attn_utils.py core-logic

核心逻辑变更,提取了 `_reshape_attention_kv_cache` 公共函数,支持填充页面的 strided reshape。

def _reshape_attention_kv_cache(
    kv_raw_tensor: torch.Tensor,
    kv_cache_spec: AttentionSpec,
    kv_cache_shape: tuple[int, ...],
    kv_cache_stride_order: tuple[int, ...],
    num_blocks: int,
    packing: tuple[int, int] | None,
) -> torch.Tensor:
    # 根据 stride order 获取排列后的 shape 和逆排列
    permuted_kv_cache_shape = tuple(
        kv_cache_shape[i] for i in kv_cache_stride_order
    )
    inv_order = [
        kv_cache_stride_order.index(i)
        for i in range(len(kv_cache_stride_order))
    ]
    dtype = kv_cache_spec.dtype
​
    if packing is not None:
        # 处理混合层打包(共享 KV 缓存 tensor 的情况)
        offset, block_stride = packing
        assert inv_order[0] == 0
        page_bytes = prod(kv_cache_shape[1:]) * get_dtype_size(dtype)
        kv_cache = (
            kv_raw_tensor.view(-1, block_stride)[:, offset : offset + page_bytes]
            .view(dtype)
            .view(kv_cache_shape)
        )
    elif kv_cache_spec.page_size_padded is not None:
        # 填充页面情况:需要 strided view 来跳过 padding
        # 仅支持 num-blocks-first 布局,即 kv_cache_shape[0] == num_blocks
        assert kv_cache_shape[0] == num_blocks, (
            "Padded KV pages require a num-blocks-first KV cache layout"
        )
        dtype_size = get_dtype_size(kv_cache_spec.dtype)
        page_stride = kv_cache_spec.page_size_bytes // dtype_size
​
        # 计算逻辑 shape 的自然 stride,然后覆盖 block stride
        strides = list(torch.empty(kv_cache_shape).stride())
        strides[inv_order[0]] = page_stride
        kv_cache = torch.as_strided(
            kv_raw_tensor.view(dtype),
            size=kv_cache_shape,
            stride=tuple(strides),
        )
    else:
        # 无填充,直接连续 view
        kv_cache = (
            kv_raw_tensor.view(dtype).view(kv_cache_shape)
        )
​
    # 按照 stride order 重排回到后端期望的物理布局
    return (
        kv_cache.permute(inv_order).contiguous()
        if packing is None
        else kv_cache
    )
vllm/v1/attention/backend.py core-logic

新增 `indexes_kv_by_block_stride` 类方法,作为后端能力检测的统一接口。

@classmethod
def indexes_kv_by_block_stride(cls) -> bool:
    """Whether the backend reads KV pages by the runtime block stride.    True when ``num_blocks`` is the outermost physical dimension of the KV
    cache, so the backend tolerates a non-contiguous block dim. This gates
    page size padding and cross-layer uniform KV layout.
    """
    try:
        kv_cache_stride_order = cls.get_kv_cache_stride_order(
            include_num_layers_dimension=False
        )
        layered_kv_cache_stride_order = cls.get_kv_cache_stride_order(
            include_num_layers_dimension=True
        )
    except (AttributeError, NotImplementedError):
        return False
​
    # 检查后端是否包含 layers 维度
    if len(layered_kv_cache_stride_order) != len(kv_cache_stride_order) + 1:
        return False
​
    # stride_order[0] != 0 表示 num_layers 不是第一维,即 block stride 是动态的
    return layered_kv_cache_stride_order[0] != 0

评论区精华

增加 block size 的安全性 正确性

benchislett 询问是否安全地增加 block size;pst2154 解释整除分支保持原行为,新填充分支只处理不可整除。

结论:确认整除分支不变,新填充分支仅用于不可整除且后端支持的情况。 · 已解决

后端能力检测设计:新标志 vs 重用 use_uniform_kv_cache 设计

heheda12345 质疑是否所有后端支持 strided view;TheEpicDolphin 建议增加 supports_padded_kv_pages 标志;ivanium 建议重用 use_uniform_kv_cache 的检测逻辑;最终采用 indexes_kv_by_block_stride 属性。

结论:采用 indexes_kv_by_block_stride 属性,从 get_kv_cache_stride_order 派生,既用于统一布局也用于填充门控。 · 已解决

merge 方法缺失 indexes_kv_by_block_stride 字段 正确性

ivanium 发现 merge() 未传播 indexes_kv_by_block_stride,提供补丁修复。

结论:ivanium 提交补丁修复,后续合并。 · 已解决

属性命名争议 style

ivanium 建议改为 block_stride_agnostic,TheEpicDolphin 坚持保持与方法名一致,最终 ivanium 同意。

结论:保留 indexes_kv_by_block_stride 名称。 · 已解决

所有后端是否都支持更大的 page_stride 性能

LucasWilkinson 询问是否所有后端支持 page_stride 大于页大小,ivanium 提到 MLA 后端需要单独修复(PR#45111)。

结论:目前仅 num-blocks-first 后端支持,其他后端会抛出错误,需要后续修复。 · 已解决

风险与影响

  1. 性能风险:填充页面引入的 strided view 可能导致缓存局部性下降,但 PR 作者验证 FlashInfer 输出 bit-identical,计算正确;实际性能影响需在真实场景测量。
  2. 后端兼容性:仅支持 num-blocks-first 布局的后端(FlashAttention、FlashInfer),对其他后端(如 MLA、ROCm)会抛出 NotImplementedError 或断言失败。未来需补充支持。
  3. 回归风险:已有整除放大 block size 的行为不变;填充影响的是非整除路径,且由 indexes_kv_by_block_stride 门控,不影响已有布局的后端。

对用户:支持不同 KV head size 的 draft 模型(如 MiMo-DFlash)进行推测解码,提升性能潜力。对系统:增加 KV 缓存分配和重塑的代码路径复杂度,但通过抽象保持了可维护性。对团队:该基础设施为后续更多混合页面大小场景提供参考。

核心路径变更 后端兼容性 性能潜在影响

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论