Prhub

#52281 [ROCm] Give EngineCore cleanup grace after request abort

原始 PR 作者 AndreasKaratzas 合并时间 2026-08-20 00:40 文件变更 5 提交数 4 评论 13 代码增减 +221 / -3

执行摘要

给 ROCm EngineCore 关闭增加 15 秒清理宽限

AMD CI build 12007 暴露了 shutdown race:请求已完成,但 shutdown_timeout=0 让父进程在发送 SIGTERM 后不预留任何清理时间就 force-kill EngineCore,ROCm 的 executor、scheduler、distributed 与 cache 清理没有机会执行,导致 GPU 内存停留在 11.72 GB,并持续占用整个 120 秒 release wait。PR body 明确指出 zero timeout 语义只是“立即中止在途请求”,父进程仍需要独立窗口让 EngineCore 释放设备资源。

建议关注该 PR 的双层超时解耦设计与 gc.freeze() 应用边界。它对理解 vLLM V1 EngineCore 进程生命周期、ROCm teardown 特殊性以及 Python 解释器最终化阶段的 GC 行为都有参考价值。若团队维护 ROCm 或需要自定义进程退出宽限逻辑,值得精读。

讨论亮点

该 PR 没有实质性的 review 评论(review comments 为 0),作者通过多次 /ci run 触发 Buildkite CI 验证,DarkLight1337 直接批准。核心设计权衡记录在 PR body 中:正数请求排空超时耗尽的零预算不应获得新的宽限期,因为 EngineCore 依赖该 deadline 强制排空请求;GC 冻结只允许出现在终止的子进程路径,失败、未完成工作、正数超时与非 ROCm 路径均不冻结 GC 图。

实现拆解

  1. 在 vllm/v1/engine/utils.py 中新增常量 ROCM_ENGINE_PROCESS_SHUTDOWN_TIMEOUT_S = 15.0 与函数 get_engine_process_shutdown_timeout(request_timeout, process_timeout):仅当 request_timeout 与 process_timeout 同时为 0 且 current_platform.is_rocm() 时返回 15.0,其余情况原样返回 process_timeout。这样既给 ROCm 零超时路径提供清理窗口,又不破坏正数请求排空超时预算。
  2. CoreEngineProcManager.init 保存 self._request_shutdown_timeout = vllm_config.shutdown_timeout,并在 shutdown() 中调用 get_engine_process_shutdown_timeout 解耦计算进程清理超时;当计算值与调用方传入值不一致时记录日志,再调用 shutdown(self.processes, timeout=process_timeout)。
  3. 在 vllm/v1/engine/core.py 的 EngineCoreProc.run_engine_core 中增加 clean_shutdown 判定:SystemExit 退出码为 None/0、shutdown_state 为 SHUTTING_DOWN、has_work() 为 False、shutdown_timeout 为 0。满足条件且平台为 ROCm 时,在 finally 中执行完 engine_core.shutdown() 后调用 gc.freeze(),把存活对象图冻结在即将终止的专用进程内,避免解释器 finalization 阶段的慢速循环 GC 扫描。
  4. vllm/entrypoints/openai/dp_supervisor.py 的 _shutdown_children 复用 get_engine_process_shutdown_timeout(self.args.shutdown_timeout, self.args.shutdown_timeout),再叠加 CHILD_EXIT_GRACE_S,使 DP 场景下 ROCm 零路径最多等待 20 秒。
  5. 测试配套:tests/v1/engine/test_startup_watch_processes.py 新增两个参数化测试,覆盖 5 种超时组合与 6 种 GC 冻结场景;tests/entrypoints/openai/test_dp_supervisor.py 新增 test_shutdown_children_uses_engine_process_timeout 验证 DP supervisor 的超时叠加逻辑。
文件 模块 状态 重要度
vllm/v1/engine/utils.py 进程管理 modified 6.94
vllm/v1/engine/core.py 引擎核心 modified 6.08
vllm/entrypoints/openai/dp_supervisor.py 数据并行 modified 5.0
tests/v1/engine/test_startup_watch_processes.py 进程测试 modified 6.63
tests/entrypoints/openai/test_dp_supervisor.py 监督测试 modified 5.28

关键符号

get_engine_process_shutdown_timeout CoreEngineProcManager.shutdown EngineCoreProc.run_engine_core DPSupervisor._shutdown_children test_engine_core_process_shutdown_timeout test_freeze_gc_after_clean_rocm_engine_core_shutdown test_shutdown_children_uses_engine_process_timeout

关键源码片段

vllm/v1/engine/utils.py core-logic

核心逻辑所在:新增 get_engine_process_shutdown_timeout,将请求排空超时与进程清理超时解耦,并在 CoreEngineProcManager.shutdown() 中接入 ROCm 15 秒宽限。

# vllm/v1/engine/utils.py
# 常量:ROCm 专用清理宽限窗口(秒)
ROCM_ENGINE_PROCESS_SHUTDOWN_TIMEOUT_S = 15.0
​
​
def get_engine_process_shutdown_timeout(
    request_timeout: float | None,
    process_timeout: float | None,
) -> float | None:
    # request_timeout 即 VllmConfig.shutdown_timeout,为 0 表示收到 SIGTERM 后立即
    # 中止在途请求;但父进程仍需独立窗口让 EngineCore 释放设备资源,ROCm teardown
    # 更慢,force-kill 可能残留 VRAM。
    # process_timeout 可能是外层进程管理器传下的剩余预算:除非两者同时为 0,否则
    # 保持原值——正数请求排空超时耗尽后的零预算不应获得新的宽限期。
    if request_timeout == 0 and process_timeout == 0 and current_platform.is_rocm():
        return ROCM_ENGINE_PROCESS_SHUTDOWN_TIMEOUT_S
    return process_timeout
​
​
class CoreEngineProcManager:
    # 关键集成点:shutdown() 将请求排空超时与进程清理超时解耦
    def shutdown(self, timeout: float | None = None) -> None:
        self.manager_stopped.set()
        if self._finalizer.detach() is not None:
            process_timeout = get_engine_process_shutdown_timeout(
                self._request_shutdown_timeout, timeout
            )
            if process_timeout != timeout:
                logger.info(
                    "[shutdown] EngineCore process manager: using %ss ROCm "
                    "cleanup grace after immediate request abort",
                    process_timeout,
                )
            shutdown(self.processes, timeout=process_timeout)
vllm/v1/engine/core.py dependency-wiring

EngineCore 子进程入口:新增 clean_shutdown 判定,并在干净退出且 ROCm 时调用 gc.freeze(),避免终止进程 finalization 阶段重复循环 GC。

# vllm/v1/engine/core.py EngineCoreProc.run_engine_core 中的关键分支
        except SystemExit as e:
            logger.info_once("[shutdown] EngineCore: exiting busy loop")
            # 仅对“干净退出”启用后续优化:退出码成功、已进入 SHUTTING_DOWN、
            # EngineCore 无剩余工作、且配置的 shutdown_timeout 为 0
            clean_shutdown = (
                e.code in (None, 0)
                and engine_core is not None
                and engine_core.shutdown_state == EngineShutdownState.SHUTTING_DOWN
                and not engine_core.has_work()
                and engine_core.vllm_config.shutdown_timeout == 0
            )
            raise
        # ... 中间为 run_busy_loop、信号处理等既有逻辑(未改动)...
        finally:
            signal.signal(signal.SIGTERM, signal.SIG_DFL)
            signal.signal(signal.SIGINT, signal.SIG_DFL)
            if signal_callback is not None:
                signal_callback.stop()
            if engine_core is not None:
                engine_core.shutdown()
            if clean_shutdown:
                from vllm.platforms import current_platform
                if current_platform.is_rocm():
                    # 上面的清理已解冻并回收堆对象;冻结存活对象图可跳过解释器
                    # finalization 阶段的慢速循环 GC 扫描;进程退出时统一回收
                    gc.freeze()

评论区精华

shutdown_timeout=0 下 ROCm teardown 竞态 设计

PR body 引用 AMD CI build 12007:请求已完成,但 zero timeout 没给父进程时间做 ROCm cleanup 就 force-kill EngineCore,GPU 内存 11.72 GB 残留整个 120 秒 release wait。

结论:引入 get_engine_process_shutdown_timeout,仅在 ROCm 且请求超时与外层预算同时为 0 时给予 15 秒清理宽限。 · 已解决

正数 timeout 预算耗尽不享宽限 正确性

PR body 明确:Positive and None process timeout budgets are preserved;已耗尽的预算若来自正数请求排空超时,不应获得新的 grace period,因为 EngineCore 依赖该 deadline 强制排空请求。

结论:保留原语义,仅在 0/0 的 ROCm 路径注入 15 秒宽限。 · 已解决

gc.freeze 的适用边界 性能

PR body 说明 GC 优化仅限于终止的子进程路径:退出码成功、shutdown 状态为 SHUTTING_DOWN、无剩余工作、配置超时为 0;失败、未完成工作、正数超时与非 ROCm 路径都不冻结 GC 图。gc.freeze() 不禁止引用计数销毁,循环引用仅限该专用进程。

结论:在干净退出且 ROCm 时冻结存活对象图,进程退出即回收,避免 finalization 阶段慢速循环 GC。 · 已解决

DP supervisor 是否沿用同一宽限 设计

PR body 说明 DP supervisor 使用相同 EngineCore process timeout 并叠加 5 秒子进程退出余量,ROCm 零路径最多等待 20 秒后 force-kill 整个进程树。

结论:复用 get_engine_process_shutdown_timeout 并叠加 CHILD_EXIT_GRACE_S。 · 已解决

风险与影响

  1. 行为变更面很窄:仅 ROCm + shutdown_timeout=0 + 外层 process_timeout=0 时生效,非 ROCm 路径完全不变,正数与 None 预算也保持不变。
  2. 进程退出延迟:ROCm 零路径下 EngineCore 最长多存活 15 秒,DP 场景最长 20 秒;对追求极速释放进程资源的场景可能引入可感知延迟,但换取的是避免 120 秒显存残留。
  3. 新增风险点:clean_shutdown 判定在 except SystemExit 分支内调用 has_work(),若 has_work() 抛出异常,会覆盖原 SystemExit(0) 并改变进程退出形态,可能被父进程误判为异常退出。
  4. gc.freeze() 是进程终止专用优化,不禁止引用计数销毁,循环引用仅存在于该专用进程内,进程退出即回收,外部无内存风险。
  5. DP supervisor 新增 assert process_timeout is not None,若 shutdown_timeout 配置为 None,错误形态由原 TypeError 变为 AssertionError,行为略有变化。

用户影响:ROCm 用户配置 shutdown_timeout=0 时,EngineCore 进程最长延迟 15 秒退出,但能保证 ROCm teardown 完成并释放 GPU 显存,避免 11.72 GB 显存残留持续 120 秒;非 ROCm 用户无感知。系统影响:EngineCore 进程生命周期管理中的请求排空与进程清理两阶段语义被显式分离,后续其他平台也可复用该模式。团队影响:涉及 v1 引擎核心进程管理与 DP supervisor 两条路径,测试覆盖较完整,便于后续演进。

ROCm 专属路径 进程退出延迟最多 15 秒 SystemExit handler 内新增 has_work() 调用 gc.freeze 为进程终止专用优化 DP supervisor 断言超时非 None

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论