Prhub

#50321 [KV Offload] Support partial secondary-tier load results

原始 PR 作者 mkhazraee 合并时间 2026-08-05 11:48 文件变更 3 提交数 5 评论 7 代码增减 +88 / -6

执行摘要

二级存储加载支持部分成功,避免整批 KV 块被丢弃

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、完成后上报真实可用子集。

值得精读。虽然改动仅 3 个文件、88 行,但数据契约的边界语义设计(None vs 空集合、子集校验、assert fail-fast)很有借鉴价值;与 #49328 的互补关系也值得关注,二者构成 secondary-tier 加载鲁棒性的完整闭环。

讨论亮点

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。

实现拆解

  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 分层卸载 modified 7.05
vllm/v1/kv_offload/tiering/base.py 数据契约 modified 5.4
tests/v1/kv_offload/tiering/test_tiering_offloading.py 分层卸载 modified 5.81

关键符号

_complete_promotion _process_finished_jobs test_failed_promotion_keeps_only_successful_blocks submit_partial

关键源码片段

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

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

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 data-contract

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

@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 test-coverage

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

@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

评论区精华

JobResult.successful_keys 默认值应选 None 而非空集合 设计

orozery 建议把字段类型改为 Collection[OffloadKey] | None = None,并强调需注释该字段仅适用于 promotion job,避免 cascade 任务误填充;同时补充注释说明 None 与空集合的语义差异。

结论:采纳建议,最终字段为 Collection[OffloadKey] | None = None,并写入详细注释。 · 已解决

将 promotion 完成逻辑提取为 _complete_promotion 方法 设计

orozery 在 manager.py 的 diff 上建议:"Can we extract this to a _complete_promotion function?",避免 _process_finished_jobs 内联逻辑膨胀。

结论:采纳,最终代码提取 _complete_promotion,_process_finished_jobs 的 promotion 分支缩为一行调用。 · 已解决

_complete_promotion 分支风格与子集校验 style

varun-sundar-rabindranath 提出更紧凑的风格(先归约 successful_keys 再求 failed_keys),并注明 "Just a style choice. Feel free to ignore"。

结论:作者采用等价的 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 模式生效 缺少真实二级存储后端的端到端测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论