# PR #47437 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Bugfix] Fix pooled Whisper encoder sliding-window kernel size
- 合并时间：2026-07-03 01:19
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/47437

---

# 执行摘要

- 一句话：修复 Whisper 编码器滑动窗口 kernel 大小错配
- 推荐动作：值得精读，尤其是对涉及 KV 缓存管理或块池化机制的开发者。关键设计决策是将两个消费者的滑动窗口需求分离，并通过 +1 余量解决块对齐驱逐的边界问题，是一个简洁而正确的修复。

# 功能与动机

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

# 实现拆解

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`（模块 模型执行器；类别 source；类型 data-contract；符号 create_whisper_attention_backend_with_block_pooling, WhisperCausalAttentionWithBlockPoolingBuilder.__init__, WhisperCausalAttention.__init__, WhisperCausalAttention.get_kv_cache_spec）: 核心修复文件，修改了 block-pooling 构建器以区分管理器和 kernel 的窗口需求，并调整了池化窗口的计算。
- `tests/models/multimodal/generation/test_voxtral_realtime.py`（模块 测试套件；类别 test；类型 test-coverage；符号 assert_encoder_kv_cache_spec）: 更新测试断言以匹配新的池化窗口计算（+1 余量），验证修复的正确性。

关键符号：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`

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

```python
# 关键变更：在 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`

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

```python
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

```

# 评论区精华

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

- 暂无高价值评论线程

# 风险与影响

- 风险：低风险。变更范围小，仅涉及 Whisper 编码器的滑动窗口处理逻辑，对非滑动窗口模型无影响。+1 余量增加了极少的 KV 块预留，对显存影响微乎其微。测试断言已更新验证新行为。但需关注是否会影响其他使用相同 block-pooling 机制的模型（目前仅 Whisper 系列使用）。
- 影响：直接影响 Voxtral Realtime 模型（因果 Whisper 编码器）的推理正确性，修复了剪裁准确转录结果的 bug。对其他音频模型无影响。KV 缓存预留略微增加（每个窗口多一个块），但可以忽略不计。CI 测试从失败恢复为通过。
- 风险标记：核心路径变更 , 修复回归 bug

# 关联脉络

- PR #47071 Scale sliding_window down into pooled units: 此 PR 修复了 #47071 引入的 bug，该 PR 将 sliding_window 缩小到池化单位导致 kernel 窗口也缩小。