Prhub

#51840 [Bugfix][TieredOffloading] : Return HIT_PENDING when KV promotion is triggered

原始 PR 作者 varun-sundar-rabindranath 合并时间 2026-08-12 10:07 文件变更 2 提交数 2 评论 8 代码增减 +15 / -15

执行摘要

KV 提升触发时改返回 HIT_PENDING,避免多余前缀扫描

关联 RFC #51439 指出,tiered lookup 分两阶段:Phase1 检查 KV 是否存在于 tier(如 os.path.exists()),Phase2 提升到 CPU(如 os.read()),只有块真正物化到 CPU tier 才算 HIT。旧实现中二级 tier 命中并触发提升时返回 RETRY,而 offloading scheduler 对 RETRY 的处理是 # Block location uncertain — does not count as hit. # Don't break: keep scanning to let manager kick off async lookups.,于是会继续扫描 prefix 上所有前驱/后继 key,对滑动窗口和 Mamba(align="all")等最终丢弃的块也执行无谓的加载。RFC 实验给出量化证据:DeepSeek-V4-Flash 8192 token、blocks_per_chunk = 1 时 disk→CPU lookup 达 736 个 key(2.74GB),而 CPU→GPU 实际只消费 54 个;blocks_per_chunk = 4 时 64 个 vs 15 个。本 PR 是该 RFC 的第一个落地修复。

值得与 RFC #51439 一起精读。这是一个典型的“枚举语义收紧”修复:用 1 行改动厘清 RETRY(位置不确定)与 HIT_PENDING(位置确定、物化中)的边界,并给出误用所致的 IO 开销量化证据。可继续关注 vllm/v1/kv_offload/tiering/manager.pylookup() 扫描策略演进,以及 offloading scheduler 对 HIT_PENDING 消费逻辑是否有配套优化。

讨论亮点

本 PR 来自 fork,Claude bot 自动 review 被禁用("automated review is disabled"),没有逐行 review 评论;讨论集中在 CI 验证与语义结论上:

  • orozery 在首个 commit 的 CI(Buildkite #83385)后留言:"looks like tests needs fixing"——因为 lookup() 返回值语义变化后,test_tiering_offloading.py 中多处 assert result is LookupResult.RETRY 断言全部失效;
  • 作者以第二个 commit "fix tests" 一次性更新 8 处断言与注释为 HIT_PENDING
  • orozery 重跑 CI(#83417)并 retry 失败 job 后批准合入。
    设计层面的语义划分(RETRY = 位置不确定,不参与命中计数;HIT_PENDING = 位置确定但未物化)在关联 RFC #51439 中展开论证,本 PR 直接采纳,未在评论区再起争议。

实现拆解

  1. 定位语义混淆点(vllm/v1/kv_offload/tiering/manager.pyTieringOffloadingManager.lookup() 在 primary tier 未命中后遍历 secondary_tiers,逐个调用 tier.lookup(key, req_context);当二级 tier 返回 HIT 时,原逻辑 return LookupResult.MISS if not promoted else LookupResult.RETRY 把“提升已发起”与“位置不确定”归为同一个返回值,导致消费方误判。
  2. 核心修改(1 行):将该分支的 RETRY 改为 HIT_PENDING。关键洞察是:HIT_PENDING 原本就由 primary 槽位 in-flight 路径返回(_initiate_promotion 会立即置 ref_cnt = -1,同 step 内后续 lookup 即可看到该槽位),消费方 scheduler 对此值已有完整处理,因此无需改动 vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.pyRETRY 从此只保留给二级 tier 自身返回 RETRY(位置确实不确定)的场景,即循环内 any_retry = True 分支。
  3. 测试配套(tests/v1/kv_offload/tiering/test_tiering_offloading.py:将 8 处“lookup 触发提升”的期望从 RETRY 更新为 HIT_PENDINGtest_promotion_from_secondary(批量提升)、test_failed_promotion_keeps_only_successful_blocks(部分失败提升)、test_lookup_does_not_report_async_delay_for_promotion(提升不计入异步延迟)、test_lookup_reports_async_delay_when_deferred_lookup_resolves(第二次 lookup 由 RETRYHIT_PENDING,异步延迟统计不变)、test_lookup_batches_submit_load_per_request(按请求批量提交 submit_load)、test_lookup_shared_block_no_duplicate_promotion(两次 lookup 统一返回 HIT_PENDING 且只提交一次 submit_load)、test_reset_cache_clears_orchestrator_statetest_tier_filter_allows_matching_secondary
  4. CI 验证:首个 commit 后 Buildkite CI #83385 失败(orozery 指出 tests 需修复);第二个 commit "fix tests" 批量更新断言后 CI #83417 重跑通过,orozery approve 并合入。
文件 模块 状态 重要度
vllm/v1/kv_offload/tiering/manager.py 卸载管理 modified 4.89
tests/v1/kv_offload/tiering/test_tiering_offloading.py 卸载测试 modified 4.44

关键符号

lookup _initiate_promotion test_promotion_from_secondary test_failed_promotion_keeps_only_successful_blocks test_lookup_does_not_report_async_delay_for_promotion test_lookup_reports_async_delay_when_deferred_lookup_resolves test_lookup_batches_submit_load_per_request test_lookup_shared_block_no_duplicate_promotion test_reset_cache_clears_orchestrator_state test_tier_filter_allows_matching_secondary

关键源码片段

vllm/v1/kv_offload/tiering/manager.py core-logic

核心修复点:`lookup()` 在二级 tier 命中并发起 KV 提升时返回 `HIT_PENDING` 替代 `RETRY`,解决 offloading scheduler 对已命中块继续扫描 prefix 并触发无谓加载的问题。

# vllm/v1/kv_offload/tiering/manager.py —— lookup() 二级 tier 遍历分支(修复后)
​
        any_retry = False
        for i, tier in enumerate(self.secondary_tiers):
            if i == exclude_tier_idx:
                continue
            if not req_context.load_tier_filter.allows(tier.medium, tier.locality):
                continue
            labelvalues = self._metrics.tier_label(i)
            start_time = time.monotonic()
            result = tier.lookup(key, req_context)
            lookup_duration = time.monotonic() - start_time
            if result is LookupResult.HIT:
                self._metrics.on_lookup(
                    req_context,
                    key,
                    labelvalues,
                    result,
                    lookup_duration,
                )
                # 修复点:二级 tier 命中且提升已发起时返回 HIT_PENDING,
                # 表示“KV 落点已确定、只是尚未在 primary 物化”;
                # 旧代码返回 RETRY 会让 offloading scheduler 误判为
                # “位置不确定”,从而继续扫描整条 prefix 并对滑动窗口 /
                # Mamba 等最终被丢弃的块发起多余 tier -> CPU 加载。
                # 注意 _initiate_promotion 会立即在 primary 槽位置 ref_cnt = -1,
                # 同 step 内后续 lookup 会直接命中 in-flight 槽位。
                promoted = self._initiate_promotion(i, key, req_context)
                return LookupResult.MISS if not promoted else LookupResult.HIT_PENDING
            if result is LookupResult.RETRY:
                any_retry = True
            self._metrics.on_lookup(
                req_context,
                key,
                labelvalues,
                result,
                lookup_duration,
            )
​
        if any_retry:
            return LookupResult.RETRY
        return LookupResult.MISS
tests/v1/kv_offload/tiering/test_tiering_offloading.py test-coverage

8 处测试断言从 RETRY 批量更新为 HIT_PENDING,覆盖提升触发、部分失败、批量化 submit_load、共享块去重、reset cache、tier filter 等全部相关路径,是本次语义变更的回归保障。

# tests/v1/kv_offload/tiering/test_tiering_offloading.py —— 共享块去重提升测试(修复后)
​
    def test_lookup_shared_block_no_duplicate_promotion(self, manager_setup):
        """同一 step 内两个请求 lookup 同一块时,只触发一次提升。        修复前:第一个请求经二级 tier 命中返回 RETRY(调度器视作
        位置不确定),第二个请求经 primary in-flight 槽位(ref_cnt == -1)
        返回 HIT_PENDING。修复后:两者统一为 HIT_PENDING —— 块落点
        已确定、仅等待物化,避免调度器对 prefix 上其余键发起额外异步 lookup。
        """
        shared_block = to_keys([0])[0]
        self.secondary_tier1.blocks[shared_block] = True
        self.secondary_tier1.submit_load = MagicMock(
            wraps=self.secondary_tier1.submit_load
        )
​
        ctx_a = ReqContext(req_id="req_a")
        ctx_b = ReqContext(req_id="req_b")
​
        result_a = self.manager.lookup(shared_block, ctx_a)
        result_b = self.manager.lookup(shared_block, ctx_b)
​
        # 两次 lookup 都返回 HIT_PENDING:均指向同一个提升任务,
        # 不再区分“首次发起提升”与“in-flight 已存在”
        assert result_a is LookupResult.HIT_PENDING
        assert result_b is LookupResult.HIT_PENDING
​
        self._simulate_on_schedule_end()
​
        # 尽管有两次 lookup,submit_load 只被调用一次,且携带第一个请求的上下文
        self.secondary_tier1.submit_load.assert_called_once()
        job_metadata = self.secondary_tier1.submit_load.call_args.args[0]
        assert list(job_metadata.keys) == [shared_block]
        assert job_metadata.req_context is ctx_a

评论区精华

语义划分:RETRY 与 HIT_PENDING 的边界 设计

RFC #51439 指出旧实现把“二级 tier 命中并发起提升”与“位置不确定”统称为 RETRY,scheduler 对 RETRY 的处理是 `defer_lookup = True` 且不 break、继续扫描,导致对 SWA / Mamba 等最终丢弃的块发起无谓 tier → CPU 加载;本 PR 将该路径收敛到 HIT_PENDING(位置确定、物化中),与 primary 槽位 in-flight(ref_cnt = -1)语义对齐。

结论:采纳 HIT_PENDING 表达“提升已发起”,RETRY 仅保留给真正位置不确定的场景;消费方 offloading scheduler 无需改动(HIT_PENDING 已有处理路径)。 · 已解决

CI 测试断言失效与修复 测试

orozery 在首个 commit 的 CI(Buildkite #83385)后留言“looks like tests needs fixing”,因为 test_tiering_offloading.py 中多处 `assert result is LookupResult.RETRY` 断言随语义变更失效。

结论:作者以第二个 commit “fix tests” 将 8 处断言更新为 HIT_PENDING,CI #83417 重跑通过,orozery approve 合入。 · 已解决

风险与影响

  • 消费方调度器行为依赖HIT_PENDING 的消费方是 vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py。改动前该值只由 primary 槽位 in-flight 路径产生,改动后二级 tier 提升路径也返回它;本 PR 未改动 scheduler,依赖其既有 HIT_PENDING 处理逻辑,若扫描终止/命中计数对两种来源有不同预期,需在真实 SWA 场景验证。
  • 前缀命中覆盖范围变化:修复后 scheduler 不再因“提升已发起”继续扫描整条 prefix,对 Full-Attention 组是明确优化;但依赖 RETRY 链路触发相邻块异步查询的边界场景,命中覆盖范围会收窄,需观察 RFC 后续落地(如滑动窗口提前终止)是否补足。
  • 回归风险低:核心改动仅 1 行,8 处测试断言全部同步;风险集中在枚举语义契约而非代码实现。不涉及 CPU/GPU 缓存主链路、CUDA graph、投机解码等模块。
  • 对用户/系统:TieredOffloading 前缀缓存场景下,减少滑动窗口 / Mamba 等场景的无谓磁盘→CPU 块加载与异步 lookup(RFC 实验显示 DSV4 8192 token 场景 disk→CPU lookup 可从 736 次降至与实际消费量级一致),降低 offload 延迟与磁盘带宽消耗。
  • 对团队:为 RFC #51439 “Tiered Lookup Resolution” 系列定下第一个语义里程碑——HIT_PENDING 明确为“位置确定、物化中”,后续可基于此做两阶段查找优化(如提前终止扫描)。
  • 影响范围:仅 v1 TieredOffloading 路径(vllm/v1/kv_offload/ 与 offloading scheduler),对其它模块无直接影响。
核心路径变更 枚举语义契约调整 消费方调度器行为依赖 前缀命中覆盖范围变化

关联 Issue

#51439 [RFC]: [TieredOffloading][Performance] Tiered Lookup Resolution

完整报告

参与讨论