Prhub

#54362 [Bugfix][KV Offload] Fix SWA store reachability during chunked prefill

原始 PR 作者 Whamp 合并时间 2026-09-06 18:50 文件变更 4 提交数 2 评论 8 代码增减 +385 / -15

执行摘要

修复分块预填充期间滑动窗口注意力存储可达性计算错误。

PR body 明确指出:'During chunked prefill, the offloading scheduler used the current step's SWA chunk count as the store-reachability horizon. This treated each intermediate frontier as the prompt's final partial segment and submitted chunks that the load path could never request.' 这导致存储的 SWA 块数量不匹配(例如 152 vs 预期 150),并在第一个不匹配的块 248 处出现。

该 PR 值得精读,因为它修复了 KV 卸载中的一个正确性 bug,涉及复杂的调度状态机逻辑。设计决策中如何区分预填充、完成和中止状态,以及如何通过可达性掩码确保块存储的精确性,具有学习价值。建议关注 _build_store_jobs 中状态判断的逻辑流。

讨论亮点

orozery:'Thanks @Whamp ! I'm planning to merge #51886 first, then let's see how we can work on top of it.'

Whamp:'Thanks @orozery. #51886 has landed, so I rebased this PR onto current main and adapted the fix to the new reachable_block_mask path.'

讨论围绕合并顺序展开,无技术争议。PR 作者在 #51886 合并后 rebase 并适配了新的 reachable_block_mask 路径,解决了依赖关系。

实现拆解

  1. 调度器核心逻辑修正:在 vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py 中新增 _reachable_store_block_mask 和 _final_swa_alignment_blocks 辅助方法,封装可达性掩码构建和最终 SWA 对齐块数计算。修改 _build_store_jobs 方法,引入 prompt_offloadable_tokens(提示最终可卸载令牌数)和 store_horizon_chunks 逻辑:预填充阶段使用提示水平,请求完成时使用当前水平,中止时重新考虑 SWA 尾块并避免重复提交。
  2. 可达性掩码扩展:在 vllm/v1/core/single_type_kv_cache_manager.py 中,为 SlidingWindowManager.reachable_block_mask 类方法添加 final_segment_end_block 参数,使其能处理非 EAGLE SWA 组的最终部分段尾块可达性计算,确保在预填充未完成时正确标记可达块。
  3. 测试覆盖增强:在 tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py 中新增 test_chunked_prefill_uses_final_swa_store_horizon(覆盖完成、同步中止、异步中止三种生命周期)、test_active_decode_does_not_advance_swa_final_horizon(验证活跃解码不推进最终水平)和 test_dcp_abort_reconsiders_final_swa_tail(测试 DCP 中止场景)。在 tests/v1/core/test_prefix_caching.py 中新增 test_swa_reachable_block_mask_final_partial_segment 验证可达性掩码对最终部分段的正确性。
文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py KV 卸载调度器 modified 8.38
vllm/v1/core/single_type_kv_cache_manager.py KV 缓存管理器 modified 6.6
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py 调度器测试 modified 7.97
tests/v1/core/test_prefix_caching.py 前缀缓存测试 modified 6.32

关键符号

_reachable_store_block_mask _final_swa_alignment_blocks reachable_block_mask

关键源码片段

vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py core-logic

核心逻辑修改,修复 SWA 存储可达性计算

def _reachable_store_block_mask(
    self,
    group_config: GroupOffloadConfig,
    start_chunk_idx: int,
    end_chunk_idx: int,
    final_segment_end_chunk_idx: int | None,
    reachable_boundaries: tuple[int, ...],
) -> list[bool] | None:
    """Build the block mask for a range of candidate offload chunks."""
    blocks_per_chunk = self.config.blocks_per_chunk
    # 转换块索引并委托给 manager_cls 的 reachable_block_mask
    return group_config.manager_cls.reachable_block_mask(
        start_block=start_chunk_idx * blocks_per_chunk,
        end_block=end_chunk_idx * blocks_per_chunk,
        alignment_tokens=self.config.alignment_tokens,
        kv_cache_spec=group_config.kv_cache_spec,
        use_eagle=group_config.is_eagle_group,
        retention_interval=self.config.retention_interval,
        reachable_boundaries=reachable_boundaries,
        dcp_world_size=self.config.dcp_world_size,
        # 传递最终段结束块索引,用于非 EAGLE SWA 组的尾块可达性计算
        final_segment_end_block=(
            final_segment_end_chunk_idx * blocks_per_chunk
            if final_segment_end_chunk_idx is not None
            else None
        ),
    )
​
def _final_swa_alignment_blocks(
    self, group_config: GroupOffloadConfig
) -> int | None:
    """Return representable final-tail alignment for a non-EAGLE SWA group."""
    alignment_tokens = self.config.alignment_tokens
    if (
        alignment_tokens is None
        or group_config.is_eagle_group
        or not isinstance(group_config.kv_cache_spec, SlidingWindowSpec)
        or alignment_tokens % group_config.tokens_per_block != 0
    ):
        return None
    return alignment_tokens // group_config.tokens_per_block

修改后的 _build_store_jobs 核心逻辑:

def _build_store_jobs(
    self,
    scheduler_output: SchedulerOutput,
) -> dict[int, TransferJob]:
    blocks_per_chunk = self.config.blocks_per_chunk
    store_jobs: dict[int, TransferJob] = {}
    for req_id in chain(
        scheduler_output.num_scheduled_tokens,
        scheduler_output.finished_req_ids or (),
    ):
        req_status = self._req_status.get(req_id)
        if req_status is None:
            continue
        req = req_status.req
​
        # 根据请求状态计算块数
        if req.status is RequestStatus.FINISHED_ABORTED:
            num_tokens_after_batch = req.num_computed_tokens
        elif req.is_finished():
            num_tokens_after_batch = req.num_tokens
        else:
            num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
            num_tokens_after_batch = req.num_computed_tokens + num_scheduled_tokens
​
        num_offloadable_tokens = self._calc_num_offloadable_tokens(
            req_status, num_tokens_after_batch
        )
        # 新增:计算提示的最终可卸载令牌数
        prompt_offloadable_tokens = self._calc_num_offloadable_tokens(
            req_status, req.num_prompt_tokens
        )
        # ... 后续循环中为每个 group_config 计算 store_horizon_chunks 和 reconsider_final_swa_tail ...

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

扩展 reachable_block_mask 以支持最终段

@classmethod
def reachable_block_mask(
    cls,
    start_block: int,
    end_block: int,
    alignment_tokens: int | None,
    kv_cache_spec: KVCacheSpec,
    use_eagle: bool,
    retention_interval: int | None = None,
    reachable_boundaries: Sequence[int] = (),
    dcp_world_size: int = 1,
    final_segment_end_block: int | None = None, # 新增参数
) -> list[bool] | None:
    assert isinstance(kv_cache_spec, SlidingWindowSpec)
    if alignment_tokens is None:
        return None
    block_size = kv_cache_spec.block_size * dcp_world_size
    if alignment_tokens % block_size != 0:
        return None
​
    need = cls._contiguous_blocks_for_hit(
        window_size=kv_cache_spec.sliding_window,
        block_size=block_size,
        use_eagle=use_eagle,
    )
    shift = 1 if use_eagle else 0
    mask = [False] * (end_block - start_block)
​
    # (1) Segment-boundary tails.
    segment_tokens = (
        alignment_tokens
        if retention_interval is None
        else (None if retention_interval == 0 else retention_interval)
    )
    if segment_tokens is not None:
        per_segment = segment_tokens // block_size
        if need >= per_segment:
            return None
        for i in range(start_block, end_block):
            if i >= shift and (i - shift) % per_segment >= per_segment - need:
                mask[i - start_block] = True
        # 新增:处理最终段尾块可达性
        if final_segment_end_block is not None and not use_eagle:
            final_segment_size = final_segment_end_block % per_segment
            if final_segment_size:
                final_segment_start = final_segment_end_block - final_segment_size
                final_tail_start = max(
                    final_segment_start, final_segment_end_block - need
                )
                final_tail_end = min(end_block, final_segment_end_block)
                for i in range(max(start_block, final_tail_start), final_tail_end):
                    mask[i - start_block] = True
​
    # (2) Reachable-boundary tails.
    if retention_interval is not None:
        for boundary_tokens in reachable_boundaries:
            aligned = boundary_tokens // alignment_tokens * alignment_tokens
            end = aligned // block_size + shift
            for j in range(max(start_block, end - need), min(end_block, end)):
                mask[j - start_block] = True
​
    return mask

评论区精华

合并顺序与依赖 设计

orozery 建议先合并 #51886(共享块池 DCP 缩放修复),再在此 PR 基础上工作,以确保正确的依赖顺序。

结论:PR 作者在 #51886 合并后 rebase 到 main,并适配了新的 reachable_block_mask 路径。 · 已解决

风险与影响

变更涉及 KV 卸载调度器的核心路径 _build_store_jobs,影响所有使用滑动窗口注意力和 KV 卸载的模型(如某些多模态模型)。主要风险包括:

  • 正确性回归:新引入的 store_horizon_chunks 和 reconsider_final_swa_tail 逻辑若存在边界错误,可能导致存储块集合不正确,影响 KV 卸载的完整性和加载效率。
  • 性能影响:额外计算 prompt_offloadable_tokens 和可达性掩码可能引入微小开销,但对整体推理性能影响有限。
  • 兼容性:reachable_block_mask 新增参数可能影响现有调用点,但代码显示默认值为 None,保持向后兼容。
  • 测试覆盖:新增测试覆盖关键场景,但未进行真实 GPU 服务评估,生产环境需观察。

用户影响:透明修复,用户无需调整代码,但 KV 卸载的可靠性提升,避免因存储不可达块导致的内存浪费或加载失败。
系统影响:改进 KV 卸载调度器的正确性,确保分块预填充期间 SWA 块存储与请求状态严格同步,降低系统级错误概率。
团队影响:提供了更健壮的测试框架,为后续 KV 卸载优化奠定基础,增强团队对复杂调度逻辑的信心。

核心路径变更 测试覆盖增加 模型兼容性风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论