# PR #50321 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[KV Offload] Support partial secondary-tier load results
- 合并时间：2026-08-05 11:48
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/50321

---

# 执行摘要

- 一句话：二级存储加载支持部分成功，避免整批 KV 块被丢弃
- 推荐动作：值得精读。虽然改动仅 3 个文件、88 行，但数据契约的边界语义设计（None vs 空集合、子集校验、assert fail-fast）很有借鉴价值；与 #49328 的互补关系也值得关注，二者构成 secondary-tier 加载鲁棒性的完整闭环。

# 功能与动机

PR body 明确指出当前设计的痛点："Keys are combined into asynchronous jobs only for transfer efficiency, but JobResult currently reduces the outcome back to one batch-level success value. If any fetch fails, the manager therefore discards the entire batch, including KV blocks fetched successfully." 即 key 合并成异步 job 只是为了传输效率，但结果被压缩回 batch 级布尔值，任何一个 fetch 失败都会把已成功获取的 KV 块一并丢弃。目标是把 secondary-tier fetch 变成 best-effort：丢失少量 key 只需重算这些 KV 值，fetch 时实际可用的值仍可复用；同时放宽对 lookup 快照准确性的依赖，tier 可基于较早查询提交候选 key、完成后上报真实可用子集。

# 实现拆解

1. 数据契约扩展（vllm/v1/kv_offload/tiering/base.py）：JobResult 新增 successful_keys: Collection[OffloadKey] | None = None 字段，并明确语义——仅适用于 promotion（二级→一级）任务；None 表示所有 key 与 success 同命运；空集合保留 legacy 全失败行为；上报 key 必须是原始 job keys 的子集。默认值选 None 而非空集合，是为了与 " 未提供 " 语义区分，避免 cascade 任务误填充。
2. 完成逻辑重构（vllm/v1/kv_offload/tiering/manager.py）：_process_finished_jobs 中原先单次调用 primary_tier.complete_write(keys, req_context, success) 的 promotion 分支被替换为提取出的 _complete_promotion(job_metadata, completed_job) 方法。该方法三分支归约：success=True 时全部 key 成功；successful_keys 非空时先 assert 其属于原始 job keys 的子集，再用集合差集求出失败 key，分别调用 complete_write(..., True) 与 complete_write(..., False)；空集合则全部失败。成功与失败 key 集合互斥，primary 侧按 job 粒度的回调语义不变，但结果粒度细化到 key。
3. 测试配套（tests/v1/kv_offload/tiering/test_tiering_offloading.py）：新增参数化测试 test_failed_promotion_keeps_only_successful_blocks，通过覆写 secondary_tier1.submit_load 注入 JobResult(success=False, successful_keys=...)，分别验证 partial（block 0、2 成功，block 1 失败，期望 HIT/MISS/HIT）与 legacy-full-failure（空集合，全部 MISS）。原有 test_promotion_from_secondary 继续覆盖全成功路径。

关键文件：
- `vllm/v1/kv_offload/tiering/manager.py`（模块 分层卸载；类别 source；类型 core-logic；符号 _complete_promotion, _process_finished_jobs）: 核心逻辑所在：提取 _complete_promotion 方法，将 promotion 完成从单个 batch 级 complete_write 拆分为按成功 / 失败 key 分组回调，是本 PR 的主要行为变化点。
- `vllm/v1/kv_offload/tiering/base.py`（模块 数据契约；类别 source；类型 data-contract；符号 JobResult）: 数据契约定义处：JobResult 新增 successful_keys 字段并明确 None/ 空集合的语义边界，是整个 PR 的前提。
- `tests/v1/kv_offload/tiering/test_tiering_offloading.py`（模块 分层卸载；类别 test；类型 test-coverage；符号 test_failed_promotion_keeps_only_successful_blocks, submit_partial）: 验证 partial 与 legacy-full-failure 两种失败模式的行为差异，确保重构后全成功路径不回归。

关键符号：_complete_promotion, _process_finished_jobs, test_failed_promotion_keeps_only_successful_blocks, submit_partial

## 关键源码片段

### `vllm/v1/kv_offload/tiering/manager.py`

核心逻辑所在：提取 _complete_promotion 方法，将 promotion 完成从单个 batch 级 complete_write 拆分为按成功 / 失败 key 分组回调，是本 PR 的主要行为变化点。

```python
def _complete_promotion(
    self, job_metadata: JobMetadata, completed_job: JobResult
) -> None:
    # 读取二级存储上报的成功 key 集合；success 为 False 时才可能携带
    # partial 语义，success 为 True 时下方直接覆盖为 job 全部 key。
    successful_keys = completed_job.successful_keys
    failed_keys: Collection[OffloadKey]
    if completed_job.success:
        # 全部成功：成功集合取 job 原始 key 列表，失败集合为空。
        successful_keys = job_metadata.keys
        failed_keys = ()
    elif successful_keys:
        # 部分成功：先断言上报 key 属于原始 job，再用集合差集算出
        # 失败 key，避免把未知 key 误判为成功。
        failed_keys_set = set(job_metadata.keys)
        assert failed_keys_set.issuperset(successful_keys), (
            f"Finished promotion job_id {completed_job.job_id} "
            "reported unknown successful keys"
        )
        failed_keys_set.difference_update(successful_keys)
        failed_keys = failed_keys_set
    else:
        # 空集合：保留 legacy 全失败行为（与 success 同命运）。
        successful_keys = ()
        failed_keys = job_metadata.keys

    # 成功与失败集合互斥，分别回调 complete_write，让成功块立即可用、
    # 失败块进入重算流程。
    if successful_keys:
        self.primary_tier.complete_write(
            successful_keys,
            job_metadata.req_context,
            True,
        )
    if failed_keys:
        self.primary_tier.complete_write(
            failed_keys,
            job_metadata.req_context,
            False,
        )

```

### `vllm/v1/kv_offload/tiering/base.py`

数据契约定义处：JobResult 新增 successful_keys 字段并明确 None/ 空集合的语义边界，是整个 PR 的前提。

```python
@dataclass
class JobResult:
    """Result of an async transfer job."""

    job_id: JobId
    # True 表示所有 key 都成功；False 表示全部或部分失败。
    success: bool
    # 仅适用于 promotion（二级到一级存储）任务：部分失败时标识哪些
    # key 已成功加载；None 表示所有 key 与 success 同命运。上报值
    # 必须为 job 原始 keys 的子集，manager 侧用 assert 校验。
    successful_keys: Collection[OffloadKey] | None = None

```

### `tests/v1/kv_offload/tiering/test_tiering_offloading.py`

验证 partial 与 legacy-full-failure 两种失败模式的行为差异，确保重构后全成功路径不回归。

```python
@pytest.mark.parametrize(
    ("successful_indices", "expected_results"),
    [
        # 部分成功：block 0、2 上报成功，block 1 失败
        ((0, 2), [LookupResult.HIT, LookupResult.MISS, LookupResult.HIT]),
        # 空集合：保留 legacy 全失败行为
        (None, [LookupResult.MISS, LookupResult.MISS, LookupResult.MISS]),
    ],
    ids=["partial", "legacy-full-failure"],
)
def test_failed_promotion_keeps_only_successful_blocks(
    self, manager_setup, successful_indices, expected_results
):
    blocks = to_keys(range(3))
    for block in blocks:
        self.secondary_tier1.blocks[block] = True

    # 覆写 submit_load，模拟二级存储上报部分成功的结果
    def submit_partial(job_metadata: JobMetadata) -> None:
        successful_keys = (
            None
            if successful_indices is None
            else tuple(blocks[i] for i in successful_indices)
        )
        self.secondary_tier1.completed_jobs.append(
            JobResult(
                job_id=job_metadata.job_id,
                success=False,
                successful_keys=successful_keys,
            )
        )

    self.secondary_tier1.submit_load = submit_partial

    for block in blocks:
        assert self.manager.lookup(block, _CTX) is LookupResult.RETRY

    self._simulate_on_schedule_end()
    self._simulate_on_schedule_end()

    assert [
        self.primary_tier.lookup(block, _CTX) for block in blocks
    ] == expected_results

```

# 评论区精华

orozery 提出两点设计建议并被采纳：一是把默认值从空集合改为 None，理由是 None 能明确区分 " 未提供 " 与 " 空集合 " 两种语义；二是建议注释该字段仅适用于 promotion job，避免 cascade 任务误填充。orozery 还建议将内联逻辑提取为 _complete_promotion 方法，最终代码据此重构。varun-sundar-rabindranath 提出风格简化方案（先归约 successful_keys 再求 failed_keys），并注明 "Just a style choice. Feel free to ignore"，作者采用了等价的 if/elif/else 三分支并保留 assert 子集校验。后续 4 个提交（Addressed reviewer comments、Apply clarification comment suggestions、Style clean up、Merge main）表明 review 反馈被完整消化，最终由 orozery APPROVE。

- JobResult.successful_keys 默认值应选 None 而非空集合 (design): 采纳建议，最终字段为 Collection[OffloadKey] | None = None，并写入详细注释。
- 将 promotion 完成逻辑提取为 _complete_promotion 方法 (design): 采纳，最终代码提取 _complete_promotion，_process_finished_jobs 的 promotion 分支缩为一行调用。
- _complete_promotion 分支风格与子集校验 (style): 作者采用等价的 if/elif/else 三分支写法，保留 assert 子集校验，风格建议部分采纳。

# 风险与影响

- 风险：
 1) 契约兼容性：JobResult 新增字段带默认值 None，现有 fs tier、Mooncake 等实现无需改动；但后续新 tier 若想利用 partial 语义，必须遵守 successful_keys 是原始 job keys 子集的契约，否则触发 assert（仅在非 -O 模式生效，优化模式下断言会被剥离）。
 2) 行为变化：部分成功时 primary_tier.complete_write 按成功 / 失败两组各调用一次，依赖两组 key 严格互斥；若 tier 上报集合与 job 原始 keys 不一致，assert 会 fail-fast 拦截。
 3) 测试盲区：当前仅 manager 层单测覆盖，没有真实二级存储后端（如 FS tier）对 partial 上报的端到端验证。
 - 影响：用户侧：长上下文推理与 KV offload 场景下，二级存储部分失败时不再需要整批重算，可显著降低尾部延迟和重算开销。系统侧：放宽了对 lookup 快照准确性的要求，允许更机会主义的 secondary tier 实现（先按过期查询提交候选，完成时报告实际可用子集），为后续优化铺路。团队侧：这是首次把异步传输结果从 batch 粒度细化到 key 粒度，后续所有 tier 实现都需理解该契约。
 - 风险标记：契约扩展需各 tier 实现理解 successful_keys 语义 , assert 子集校验仅在非 -O 模式生效 , 缺少真实二级存储后端的端到端测试

# 关联脉络

- PR #49328 Failed-load livelock fix for stale lookup verdicts (referenced in PR body): PR body 明确说明本 PR 与 #49328 互补："This is complementary to #49328, which prevents failed-load livelock by correcting stale lookup verdicts; it does not preserve partially successful load results." #49328 修正过期 lookup 判定防止 livelock，本 PR 补充部分成功加载结果的保留。注意：该 PR 标题未在本次材料中提供，此处为按 PR body 描述概括。