Prhub

#47437 [Bugfix] Fix pooled Whisper encoder sliding-window kernel size

原始 PR 作者 njhill 合并时间 2026-07-03 01:19 文件变更 2 提交数 1 评论 1 代码增减 +27 / -13

执行摘要

修复 Whisper 编码器滑动窗口 kernel 大小错配

PR #47071 将 SlidingWindowSpec.sliding_window 缩小到池化单位以防止过度预留 KV 块,但注意力 kernel 同样读取该字段,导致 kernel 窗口也缩小了 block_pool_size 倍,截断了编码器的有效上下文,改变了 Voxtral Realtime 转录结果,导致 test_realtime_validation 测试失败。

值得精读,尤其是对涉及 KV 缓存管理或块池化机制的开发者。关键设计决策是将两个消费者的滑动窗口需求分离,并通过+1 余量解决块对齐驱逐的边界问题,是一个简洁而正确的修复。

讨论亮点

Claude Code 自动审查确认这是一个针对 CI 破坏性 bug 的定向修复,池化/非池化窗口解耦范围良好,+1 驱逐余量解释清晰。没有实质性的 review 讨论或争议。

实现拆解

  1. 修改 create_whisper_attention_backend_with_block_pooling 函数签名:增加 sliding_window: int | None = None 参数,用于传递模型的真实未池化窗口值。
  2. 在 WhisperCausalAttentionWithBlockPoolingBuilder.__init__ 中区分两类消费者:如果是 SlidingWindowSpec,则不直接修改 kv_cache_spec 的整体替换,而是用 spec_overrides 字典累积覆盖项,当 spec 是 SlidingWindowSpec 时将 sliding_window 替换为传入的未池化窗口值。
  3. 调整 WhisperCausalAttention.__init__ 调用:将 per_layer_sliding_window 作为新的参数传递给 create_whisper_attention_backend_with_block_pooling。
  4. 修改 get_kv_cache_spec 中池化窗口的计算:将 sliding_window 从 cdiv(sliding_window, block_pool_size) 改为 cdiv(sliding_window, block_pool_size) + 1,增加一个驱逐余量,确保在块对齐驱逐后 kernel 的完整窗口仍然驻留。
  5. 更新测试 assert_encoder_kv_cache_spec:将期望值从 188 调整为 189(即 cdiv(750, 4) + 1),并添加注释说明驱逐余量的由来。
文件 模块 状态 重要度
vllm/model_executor/models/whisper_causal.py 模型执行器 modified 6.8
tests/models/multimodal/generation/test_voxtral_realtime.py 测试套件 modified 4.06

关键符号

create_whisper_attention_backend_with_block_pooling WhisperCausalAttentionWithBlockPoolingBuilder.__init__ WhisperCausalAttention.get_kv_cache_spec assert_encoder_kv_cache_spec

关键源码片段

vllm/model_executor/models/whisper_causal.py data-contract

核心修复文件,修改了 block-pooling 构建器以区分管理器和 kernel 的窗口需求,并调整了池化窗口的计算。

# 关键变更:在 block-pooling 构建器中分离管理器和 kernel 的滑动窗口
​
@functools.lru_cache
def create_whisper_attention_backend_with_block_pooling(
    underlying_attn_backend: AttentionBackend,
    block_pool_size: int,
    sliding_window: int | None = None, # <-- 新增参数:模型的真实窗口大小(未池化)
) -> type[AttentionBackend]:
    # ...
    class WhisperCausalAttentionWithBlockPoolingBuilder(underlying_builder):
        def __init__(self, kv_cache_spec, layer_names, vllm_config, device):
            assert kv_cache_spec.num_kv_heads % block_pool_size == 0
            spec_overrides = dict(
                block_size=kv_cache_spec.block_size * block_pool_size,
                num_kv_heads=kv_cache_spec.num_kv_heads // block_pool_size,
            )
            if isinstance(kv_cache_spec, SlidingWindowSpec):
                # 管理器使用池化单位,kernel 运行在扩展序列上
                # 所以 kernel 应使用未池化的窗口值
                assert sliding_window is not None
                spec_overrides["sliding_window"] = sliding_window
            kv_cache_spec = replace(kv_cache_spec, **spec_overrides)
            super().__init__(kv_cache_spec, layer_names, vllm_config, device)
​
    def get_kv_cache_spec(self, vllm_config):
        # ...
        if isinstance(kv_cache_spec, SlidingWindowSpec):
            # 管理器使用池化窗口,+1 作为驱逐余量
            pooled = cdiv(kv_cache_spec.sliding_window, self.block_pool_size) + 1
            kv_cache_spec = replace(kv_cache_spec, sliding_window=pooled)
        return kv_cache_spec
tests/models/multimodal/generation/test_voxtral_realtime.py test-coverage

更新测试断言以匹配新的池化窗口计算(+1 余量),验证修复的正确性。

def assert_encoder_kv_cache_spec(engine: LLM) -> None:
    vllm_config = engine.llm_engine.vllm_config
    audio_config = vllm_config.model_config.hf_config.audio_config
    kv_cache_specs_per_rank = engine.llm_engine.model_executor.get_kv_cache_specs()
    assert len(kv_cache_specs_per_rank) == 1
    kv_cache_specs = kv_cache_specs_per_rank[0]
    assert AUDIO_LAYER_NAME in kv_cache_specs
    spec = kv_cache_specs[AUDIO_LAYER_NAME]
    assert audio_config.sliding_window == 750
    assert audio_config.block_pool_size == 4
    assert isinstance(spec, SlidingWindowSpec)
    assert spec.block_size == 16
    assert spec.num_kv_heads == 128
    # cdiv(750, 4) == 188 pooled tokens cover the model's window; the extra
    # +1 is an eviction margin (see whisper_causal.py get_kv_cache_spec).
    assert spec.sliding_window == cdiv(750, 4) + 1 == 189

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

低风险。变更范围小,仅涉及 Whisper 编码器的滑动窗口处理逻辑,对非滑动窗口模型无影响。+1 余量增加了极少的 KV 块预留,对显存影响微乎其微。测试断言已更新验证新行为。但需关注是否会影响其他使用相同 block-pooling 机制的模型(目前仅 Whisper 系列使用)。

直接影响 Voxtral Realtime 模型(因果 Whisper 编码器)的推理正确性,修复了剪裁准确转录结果的 bug。对其他音频模型无影响。KV 缓存预留略微增加(每个窗口多一个块),但可以忽略不计。CI 测试从失败恢复为通过。

核心路径变更 修复回归 bug

关联 Issue

#19 Use FP32 for log probabilities

完整报告

参与讨论