Prhub

#51843 [Bugfix] Disable fine-grained prefix-cache hits for incompatible hybrid KV layouts

原始 PR 作者 mgoin 合并时间 2026-08-12 19:08 文件变更 3 提交数 4 评论 9 代码增减 +96 / -3

执行摘要

混合 KV 布局禁用细粒度前缀命中,修复断言崩溃

PR body 明确指出问题:"Fine-grained prefix-cache hits are enabled for hybrid models containing Mamba align groups. However other groups, such as a sliding-window DSpark drafter, may use KV cache managers that only support block-aligned lookups. This previously caused an assertion when prefix caching was enabled." 即细粒度命中按 hash block 边界返回长度,而 sliding-window drafter 的 KV cache manager 只能按更大的 block 对齐查找,两者冲突导致断言崩溃。修复目标是让混合 KV 布局在保证正确性的前提下尽可能保留 prefix-cache 能力。

值得精读。该 PR 是理解 vLLM v1 prefix cache 命中粒度体系(hash_block_size 与 scheduler_block_size 两级对齐)的很好入口,展示了一个"能力协商 + 统一开关降级 + 日志 + 回归测试"的完整 bugfix 模式。建议关注三个联动点:KVCacheCoordinator.enable_partial_hash_hits 的判定、_cache_hit_alignment_tokens 的对齐出口、Scheduler.mamba_partial_cache_hit 的调度守卫;后续若 SWA partial block hit 原型恢复,本 PR 的开关与日志将成为平滑升级的基础。

讨论亮点

njhill 审阅后总体认可("Thanks @mgoin lgtm just minor comments"),随后 APPROVED。两处高质量建议:其一,针对 scheduler 中 getattr(coordinator, "enable_partial_hash_hits", True) 的写法,建议把默认值直接定义在 KVCacheCoordinator 基类上(False 是更合理默认),mgoin 回复 "Yup I agree";其二,指出 unsupported_partial_hit_managers 集合在 dcp > 1 场景也会被构建、可能误告警,建议把构造与告警都收进 if 分支,mgoin 认可("Nice catch!")。此外 ivanium 在评论中提到团队已有 SWA partial block hit 原型但未合并,提示未来可以把细粒度命中恢复到 sliding-window 组上,否则当前降级逻辑让代码看起来更复杂。

实现拆解

  1. 能力判定重构(vllm/v1/core/kv_cache_coordinator.py):原判定只看"是否有 Mamba align 组 + 无 context parallelism";新逻辑先算出 has_partial_mamba_group,再枚举 self.single_type_managers,把 supports_fine_grained_hash_lookup 为 False 且 block_size 不等于 hash_block_size 的 manager 收进 unsupported_partial_hit_managers 集合。只要集合非空,enable_partial_hash_hits 置为 False,并用 logger.warning_once 告警一次。检测与告警都放在 enable_partial_hash_hits 为真的分支内,避免 dcp_world_size > 1 等场景误告警。
  2. 默认值上提(vllm/v1/core/kv_cache_coordinator.py):enable_partial_hash_hits 改为 KVCacheCoordinator 基类类属性,默认 False,保证任何未显式启用的路径都走最保守的 block 对齐行为,scheduler 侧不再需要 getattr 兜底。
  3. 调度器联动(vllm/v1/core/sched/scheduler.py):Scheduler.init 里 mamba_partial_cache_hit 追加 coordinator.enable_partial_hash_hits 条件。该标志控制 scheduler 是否在 prompt 末尾的 hash 边界处增加 partial-tail 停顿点;降级后不再创建这类记录,避免无法复用的缓存垃圾。
  4. 对齐粒度出口(vllm/v1/core/kv_cache_coordinator.py):_cache_hit_alignment_tokens property 依据 enable_partial_hash_hits 返回 hash_block_size 或 scheduler_block_size,保证所有 KV cache 组在查找时使用一致的命中对齐粒度。
  5. 测试配套(tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py):新增 test_hybrid_sliding_window_group_disables_partial_hash_hits,构造 full attention + Mamba align + sliding-window eagle drafter 三组 KV 布局,覆盖"禁用命中、按 mamba block 对齐回退、block 数严格对齐"三条断言;PR body 报告 23 个 partial 测试与 89 个 prefix 测试全部通过。
文件 模块 状态 重要度
vllm/v1/core/kv_cache_coordinator.py 缓存协调 modified 6.89
vllm/v1/core/sched/scheduler.py 调度器 modified 4.39
tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py 前缀缓存 modified 6.34

关键符号

KVCacheCoordinator.__init__ KVCacheCoordinator._cache_hit_alignment_tokens Scheduler.__init__ test_hybrid_sliding_window_group_disables_partial_hash_hits

关键源码片段

vllm/v1/core/kv_cache_coordinator.py core-logic

修复的核心:新增全组 KV cache manager 能力校验,决定是否启用细粒度 prefix-cache 命中;同时把 enable_partial_hash_hits 提升为基类默认 False,并新增降级告警日志。

# vllm/v1/core/kv_cache_coordinator.py 中的核心判定逻辑(head 版本整理)
class KVCacheCoordinator(ABC):
    """协调不同 KV cache group 的缓存布局、命中查找与分配。"""
​
    # 基类默认关闭细粒度前缀命中,子类按需打开;False 也是最保守的默认,
    # 保证未显式启用的路径一律走 block 对齐查找。
    enable_partial_hash_hits = False
​
    def __init__(self, kv_cache_config, max_model_len, ...):
        # 细粒度哈希命中需要同时满足三个前提:存在 Mamba "align" 组且其
        # block_size 大于 hash_block_size;未启用 context parallelism
        # (dcp_world_size == 1);每个 KV cache manager 都支持细粒度
        # 查找,否则 sliding-window drafter 这类组只能做 block 对齐查找,
        # 命中长度对不上会触发断言。
        has_partial_mamba_group = any(
            isinstance(g.kv_cache_spec, MambaSpec)
            and g.kv_cache_spec.mamba_cache_mode == "align"
            and g.kv_cache_spec.block_size > hash_block_size
            for g in kv_cache_config.kv_cache_groups
        )
        self.enable_partial_hash_hits = (
            dcp_world_size == 1 and has_partial_mamba_group
        )
        if self.enable_partial_hash_hits:
            # 收集所有要求 block 对齐查找的 manager。当 manager 的
            # block_size 大于 hash_block_size 时,hash 级细粒度查找无法
            # 满足,一旦存在就整体降级以保持各组命中长度一致。
            unsupported_partial_hit_managers = {
                type(manager).__name__
                for manager in self.single_type_managers
                if not manager.supports_fine_grained_hash_lookup
                and manager.block_size != hash_block_size
            }
            if unsupported_partial_hit_managers:
                self.enable_partial_hash_hits = False
                logger.warning_once( # 降级原因只告警一次,避免刷屏
                    "Disabling fine-grained prefix-cache hits because these KV "
                    "cache managers require block-aligned lookups: %s.",
                    ", ".join(sorted(unsupported_partial_hit_managers)),
                )
​
    @property
    def _cache_hit_alignment_tokens(self) -> int:
        # 细粒度命中时允许返回 hash 块对齐的长度;
        # 降级后必须保持 scheduler 块对齐,才能兼容所有组。
        return (
            self.hash_block_size
            if self.enable_partial_hash_hits
            else self.scheduler_block_size
        )
vllm/v1/core/sched/scheduler.py core-logic

一行关键守卫:mamba_partial_cache_hit 增加 coordinator.enable_partial_hash_hits 条件,确保降级后 scheduler 不再创建无法复用的 partial-tail 缓存记录。

# vllm/v1/core/sched/scheduler.py 中 Scheduler.__init__ 的联动守卫
# 仅当 coordinator 实际启用了细粒度命中时,才允许 scheduler 在 prompt
# 末尾的 hash 边界处追加 partial-tail 停顿点;一旦降级为 block 对齐
# 命中,partial 尾部记录无法被后续请求复用,反而浪费调度轮次。
self.mamba_partial_cache_hit = (
    self.need_mamba_block_aligned_split
    and self.hash_block_size < self.block_size
    and self.kv_cache_manager.coordinator.enable_partial_hash_hits
)
tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py test-coverage

新增 test_hybrid_sliding_window_group_disables_partial_hash_hits,端到端验证 full attention + Mamba align + sliding-window drafter 三组混合布局下的降级行为与 block 对齐命中。

# tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py
def test_hybrid_sliding_window_group_disables_partial_hash_hits():
    # 构造 full attention、Mamba "align" 和 sliding-window drafter
    # 三组混合 KV 布局:drafter 的 block size 是 hash 块的 2 倍,只能
    # 做 block 对齐查找,因此细粒度命中必须被整体禁用。
    hash_block_size = 2
    sliding_window_block_size = 2 * hash_block_size
    mamba_block_size = 2 * sliding_window_block_size
    kv_cache_config = KVCacheConfig(
        num_blocks=64,
        kv_cache_tensors=[],
        kv_cache_groups=[
            KVCacheGroupSpec(
                ["full"],
                FullAttentionSpec(
                    block_size=hash_block_size, num_kv_heads=1,
                    head_size=1, dtype=torch.float32)),
            KVCacheGroupSpec(
                ["mamba"],
                MambaSpec(
                    block_size=mamba_block_size, shapes=(1, 1),
                    dtypes=(torch.float32,), mamba_cache_mode="align")),
            KVCacheGroupSpec(
                ["swa_draft"],
                SlidingWindowSpec(
                    block_size=sliding_window_block_size, num_kv_heads=1,
                    head_size=1, dtype=torch.float32,
                    sliding_window=sliding_window_block_size),
                is_eagle_group=True),
        ],
    )
    manager = make_kv_cache_manager(
        kv_cache_config=kv_cache_config, max_model_len=8192,
        enable_caching=True, hash_block_size=hash_block_size,
        use_eagle=True)
​
    # 先用首个请求填充缓存:第一段按 mamba_block_size 分配,
    # 后续段按剩余 token 补齐,之后释放。
    tokens = list(range(3 * sliding_window_block_size))
    request = make_request("0", tokens, hash_block_size, sha256)
    computed_blocks, num_computed, _ = manager.get_computed_blocks(request)
    assert not manager.coordinator.enable_partial_hash_hits # 关键断言:已降级
    assert (manager.allocate_slots(
        request, mamba_block_size, num_computed, computed_blocks) is not None)
    request.num_computed_tokens = mamba_block_size
    manager.new_step_starts()
    assert manager.allocate_slots(
        request, len(tokens) - mamba_block_size) is not None
    request.num_computed_tokens = len(tokens)
    manager.free(request)
    manager.new_step_starts()
​
    # 复用相同前缀的更长请求:命中长度必须等于 mamba_block_size,
    # 且以 hash 块折算的 block 数严格对齐,不产生 partial-tail 记录。
    cached_request = make_request(
        "1", tokens + [len(tokens), len(tokens) + 1], hash_block_size, sha256)
    computed_blocks, num_computed, _ = manager.get_computed_blocks(cached_request)
    assert num_computed == mamba_block_size
    assert len(computed_blocks.blocks[0]) * hash_block_size == num_computed

评论区精华

基类默认值替代 getattr 兜底 设计

njhill 建议在 KVCacheCoordinator 基类直接定义 enable_partial_hash_hits = False,认为 False 是更合理的默认值;scheduler 侧应直接用属性而非 getattr(..., True) 兜底。

结论:mgoin 接受建议,最终实现为类属性默认 False,scheduler 直接读取 coordinator 属性。 · 已解决

避免 DCP 场景误告警与冗余计算 正确性

njhill 指出 unsupported_partial_hit_managers 集合在 dcp_world_size > 1 时也会被构建,可能导致误告警;建议把检测与告警都收进 enable_partial_hash_hits 为真的分支。

结论:mgoin 认可("Nice catch!"),最终实现将 unsupported_partial_hit_managers 计算与告警放入 if 分支内。 · 已解决

SWA partial block hit 原型是否恢复 question

ivanium 提到团队已有 SWA partial block hit 原型但未合并,建议后续恢复,否则降级逻辑让代码看起来更复杂。

结论:未在本次 PR 解决,作为后续演进方向(@ZJY0516 被提及)。 · 待处理

风险与影响

  1. 属性依赖风险:新逻辑要求每个 KV cache manager 正确实现 supports_fine_grained_hash_lookup;若某 manager 缺失该属性会产生 AttributeError,语义错误则会静默降级(性能损失)或错误启用(再次触发断言),建议核对所有 SingleTypeKVCacheManager 子类。
  2. 命中率回退:降级后短/中 prompt 的 prefix 命中必须落在 scheduler_block_size 边界,PR body 已明示该 trade-off;长 prompt(如 66k token)场景实测基本无损失。
  3. 行为影响面:所有含 Mamba align 组且带其他组的混合模型初始化都会走新判定;纯 full-attention 模型路径完全不变。
  4. 验证局限:Kimi K3 GSM8K 实测仅 300 requests,CI 覆盖为 23 个 partial + 89 个 prefix 测试,DCP 场景未直接覆盖。

用户/模型层:修复 Kimi K3 + DSpark SWA drafter 开启 prefix caching 时的断言崩溃,使这类混合模型可以安全使用前缀缓存,代价是短中 prompt 的复用可能下降。系统层:v1 引擎 KV cache 初始化与调度路径新增一次 manager 能力枚举,开销可忽略;scheduler 不再产生 partial-tail 记录,缓存布局更干净。团队层:确立了"按能力协商降级"的修复模式,为 ivanium 提到的 SWA partial block hit 原型恢复预留了清晰开关位与日志出口。

核心路径变更 行为降级 依赖 manager 能力属性 验证范围有限

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论