Prhub

#49947 [Bugfix][KV Offload][OBJ] Preserve job completion during cleanup

原始 PR 作者 mindungil 合并时间 2026-07-28 12:10 文件变更 2 提交数 4 评论 3 代码增减 +133 / -7

执行摘要

修复 OBJ 次级 tier 清理时 release_xfer_handle 失败导致作业结果丢失

The OBJ secondary tier removes a transfer from _transfers before releasing its NIXL resources. If release_xfer_handle() raises, no JobResult is published and the OBJ tier has already discarded the only state needed to retry cleanup. The parent TieringOffloadingManager therefore cannot pop the corresponding _transfer_jobs entry or call primary.complete_read(). For a primary-to-OBJ store, the CPU cache block remains pinned and has_pending_work() remains true.

建议精读 manager.py 中的 _poll_active_transfers 方法,其异常处理模式(先处理关键资源,再尽力清理次要资源)值得参考。对应回归测试也展示了如何对异步重试逻辑进行精确注入测试。

讨论亮点

orozery 在 review 中指出原始实现引入了 TransferEntry.success 缓存字段,过于复杂。他建议:

1) 不要删除条目直到 release_xfer_handle 成功;
2) 如果 release_xfer_handle 抛出异常,继续轮询(条目保留,下次重试);
3) release_dlist_handle / deregister_memory 作为尽力而为的清理。mindungil 采纳建议,在 commit aa628aaf 中移除了 success 字段并调整逻辑。

实现拆解

  1. 调整 _poll_active_transfers 流程(vllm/v1/kv_offload/tiering/obj/manager.py):原有的清理顺序是先 del self._transfers[job_id],再依次调用 release_xfer_handle、release_dlist_handle、deregister_memory,最后发布结果。现在改为:先检查传输状态,若完成则尝试 release_xfer_handle,若该调用抛出异常,则保留传输条目,跳过本次轮询,等待下一次 get_finished_jobs 重试;若成功,则立即删除传输条目,接着尽力执行 release_dlist_handle 和 deregister_memory(捕获所有异常,不影响结果发布),最后发布 JobResult。
  2. 删除 TransferEntry.success 字段:review 中建议不再缓存成功/失败状态,因为状态判断已在 _poll_active_transfers 中完成,release 失败后的重试会重新检查状态。
  3. 新增回归测试(tests/v1/kv_offload/tiering/test_obj_tier.py):test_release_xfer_failure_retries_without_losing_result 验证 release_xfer_handle 首次失败后条目保留且结果不发布,第二次轮询时成功清理并发布;test_post_transfer_cleanup_failure_does_not_lose_result 验证 release_dlist_handle 或 deregister_memory 异常不会阻塞结果发布;test_xfer_cleanup_retry_finalizes_parent_job_and_primary_pin 使用真实 TieringOffloadingManager 和 CPUPrimaryTierOffloadingManager 验证整个栈在注入异常后最终完成作业并释放 pin。
  4. 导入与辅助函数变更:测试文件新增 TieringOffloadingManager 和 CPUPrimaryTierOffloadingManager 的导入;_make_tier 接受可选 primary_kv_view 参数,以适应集成测试场景。
文件 模块 状态 重要度
vllm/v1/kv_offload/tiering/obj/manager.py KV 卸载 modified 6.71
tests/v1/kv_offload/tiering/test_obj_tier.py 测试 modified 6.73

关键符号

_poll_active_transfers test_release_xfer_failure_retries_without_losing_result test_post_transfer_cleanup_failure_does_not_lose_result test_xfer_cleanup_retry_finalizes_parent_job_and_primary_pin

关键源码片段

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

核心修复文件,重构了 `_poll_active_transfers` 方法,引入重试和尽力而为清理

def _poll_active_transfers(self) -> None:
    """Poll all in-flight transfers once; move newly-completed (success or
    failure) into ``_pending_results`` and release their NIXL handles."""
    for job_id, entry in list(self._transfers.items()):
        # 1. 轮询传输状态
        try:
            state = self._agent.check_xfer_state(entry.xfer_handle)
        except Exception as exc:
            success = False
            logger.warning("check_xfer_state raised for job %d: %s", job_id, exc)
        else:
            if state == NIXL_PROC:
                continue # 还在处理中,跳过
            if state == NIXL_DONE:
                success = True
            else:
                success = False
                logger.warning("transfer failed job=%d state=%s", job_id, state)
​
        # 2. 尝试释放传输句柄(关键:失败则保留条目,下次重试)
        try:
            self._agent.release_xfer_handle(entry.xfer_handle)
        except Exception as exc:
            # Keep the entry until NIXL confirms that the transfer handle
            # can be released. The transfer may still access primary-tier
            # memory, so publishing its result would allow unsafe reuse.
            logger.warning("release_xfer_handle failed for job %d: %s", job_id, exc)
            continue
​
        # 3. 传输句柄已释放,后续清理为尽力而为,异常不阻塞结果发布
        try:
            self._agent.release_dlist_handle(entry.obj_handle)
        except Exception as exc:
            logger.warning("release_dlist_handle failed for job %d: %s", job_id, exc)
        try:
            self._agent.deregister_memory(entry.files_desc)
        except Exception as exc:
            logger.warning("deregister_memory failed for job %d: %s", job_id, exc)
​
        # 4. 删除传输条目并发布结果
        del self._transfers[job_id]
        self._pending_results.append(JobResult(job_id=job_id, success=success))
tests/v1/kv_offload/tiering/test_obj_tier.py test-coverage

新增三个回归测试,验证异常注入场景,确保行为正确

def test_release_xfer_failure_retries_without_losing_result(self, monkeypatch):
    tier, agent = _make_tier(num_blocks=4)
    # 模拟 check_xfer_state 始终抛异常,确保作业标记为失败
    agent.check_xfer_state = MagicMock(side_effect=RuntimeError("poll failed"))
    # 第一次 release_xfer_handle 失败,第二次成功
    release_xfer = MagicMock(
        side_effect=[RuntimeError("transfer is still active"), None]
    )
    monkeypatch.setattr(agent, "release_xfer_handle", release_xfer)
​
    tier.submit_store(make_job(1, [key(1)], [0]))
​
    # 第一次轮询:release 失败,条目保留,结果不发布
    assert list(tier.get_finished_jobs()) == []
    assert 1 in tier._transfers
​
    # 第二次轮询:release 成功,结果发布(success=False)
    results = list(tier.get_finished_jobs())
    assert len(results) == 1
    assert results[0].job_id == 1
    assert not results[0].success
    assert agent.check_xfer_state.call_count == 2
    assert release_xfer.call_count == 2
    assert not tier._transfers
    # 确认结果只发布一次
    assert list(tier.get_finished_jobs()) == []

评论区精华

避免引入 TransferEntry.success 字段,改用重试机制 设计

orozery 建议 : 1) 不删除条目直到 release_xfer_handle 成功 ; 2) 如果 release_xfer_handle 抛出则 continue; 3) release_dlist_handle/deregister_memory 作为尽力而为。

结论:mindungil 采纳建议,在 commit aa628aaf 中移除 success 字段并调整逻辑。 · 已解决

风险与影响

主要风险在于重试机制可能导致清理延迟,但每次 get_finished_jobs 都会重试,不会无限等待(NIXL 保证最终完成或失败)。release_dlist_handle 和 deregister_memory 的异常被捕获并日志记录,可能导致 NIXL 元数据泄漏,但不会影响数据正确性或阻塞后续操作。回归测试覆盖了异常注入,但无法完全模拟真实 NIXL 的传输取消场景,仍存在边缘情况未覆盖。

影响范围限定在 KV Offload 的 OBJ 次级 tier 模块。修复了在 NIXL 传输无法被取消时导致 CPU 缓存块永远 pinned 的资源泄漏问题。对于启用远程对象存储 KV Offload 的 vLLM 实例,此 PR 显著提高了稳定性。对未启用该功能的用户无影响。团队需要关注若未来 NIXL 行为变化,清理重试逻辑可能需要同步调整。

重试可能导致清理延迟 尽力而为清理可能泄漏 NIXL 元数据 异常注入未覆盖真实 NIXL 传输取消

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论