Prhub

#45195 [Bugfix][V1] Clean up compiled-model bytecode hooks on VllmRunner exit

原始 PR 作者 Sunt-ing 合并时间 2026-06-17 11:31 文件变更 4 提交数 8 评论 2 代码增减 +61 / -2

执行摘要

修复 V1 单进程模式下编译模型 Bytecode Hook 残留导致内存泄漏

Issue #21073 报告在单进程模式下多次创建 LLM 会因内存不释放而 OOM。根因是注册的字节码钩子通过绑定方法持有了模型引用,导致 GC 无法回收。

建议阅读该 PR,特别是 weakref.finalizeTorchDynamo 钩子生命周期的管理方式。它展示了如何在不引入侵入式接口的前提下,实现资源的可靠释放,是 vLLM 编译模块清理机制的参考实现。

讨论亮点

在原始 PR #35676 的讨论中,确认了使用 weakref.finalize 而非 __del__ 的原因:避免终结器在对象已部分销毁时访问无效属性。此外,本 PR 也声明了其局限性——不解决所有内存清理场景(如裸 del llm),专注于单进程测试场景。

实现拆解

  1. 存储钩子句柄:在 TorchCompileWithNoGuardsWrapper.__init__ 中将 register_bytecode_hook 返回的 RemovableHandle 保存为 self._bytecode_hook_handle
  2. 添加 cleanup 方法:新增 cleanup() 方法,调用 handle.remove() 移除钩子。
  3. 注册终结器:在 LLMEngine.__init__(非多进程分支)中通过 _get_driver_model_for_cleanup 获取模型,并利用 weakref.finalize 注册回调,在引擎回收时执行 _cleanup_instance_caches
  4. 重置 Dynamo:在 VllmRunner.__exit__ 中添加 torch._dynamo.reset()
  5. 新增测试:添加 test_llm_delete_inprocess 测试用例,验证 GPU 内存释放。
文件 模块 状态 重要度
vllm/v1/engine/llm_engine.py 引擎层 modified 7.59
vllm/compilation/wrapper.py 编译层 modified 6.59
tests/v1/shutdown/test_delete.py 测试 modified 5.41
tests/conftest.py 测试 modified 3.11

关键符号

cleanup _get_driver_model_for_cleanup _cleanup_instance_caches test_llm_delete_inprocess

关键源码片段

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

核心引擎,新增驱模型获取和终结器注册。

# LLMEngine 初始化时(非多进程模式)注册终结器
# 捕获模型引用,以便在对象回收时清理字节码钩子
model = self._get_driver_model_for_cleanup()
if model is not None:
    # weakref.finalize 确保终结器在对象被 GC 时执行,
    # 避免 __del__ 可能访问已销毁模块的问题
    self._finalizer = weakref.finalize(
        self, LLMEngine._cleanup_instance_caches, model
    )# 静态方法,遍历模型所有模块,移除 TorchCompileWithNoGuardsWrapper 的钩子
@staticmethod
def _cleanup_instance_caches(model) -> None:
    from vllm.compilation.wrapper import TorchCompileWithNoGuardsWrapper
    for module in model.modules():
        if isinstance(module, TorchCompileWithNoGuardsWrapper):
            module.cleanup()
vllm/compilation/wrapper.py core-logic

编译包装器,存储钩子句柄并新增清理方法。

# __init__ 中:存储钩子句柄以便后续移除
if envs.VLLM_USE_BYTECODE_HOOK and mode != CompilationMode.STOCK_TORCH_COMPILE:
    self._bytecode_hook_handle = (
        torch._dynamo.convert_frame.register_bytecode_hook(self.bytecode_hook)
    )
    self._compiled_bytecode: CodeType | None = None# 新增 cleanup 方法,移除本实例注册的钩子
def cleanup(self) -> None:
    handle = getattr(self, "_bytecode_hook_handle", None)
    if handle is not None:
        handle.remove()

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

风险点包括:1)weakref.finalize 可能因引用循环或不执行而不触发,但本 PR 在 VllmRunner.__exit__ 中已手动调用 torch._dynamo.reset() 作为冗余保障;2)torch._dynamo.reset() 会全局重置 Dynamo,可能影响同一进程中其他未完成的编译任务,但单进程模式下通常只有一个引擎实例;3)变更仅在非多进程分支生效,不影响默认的多进程模式。

影响用户:使用 VLLM_ENABLE_V1_MULTIPROCESSING=0 进行单进程推理的用户,在删除 LLM 模型后 GPU 内存可正常释放,避免后续创建模型时 OOM。影响系统:新增的终结器和 dynamo.reset() 调用在正常路径下开销极低。影响团队:提供了明确的清理模式,可作为其他资源清理的参考设计。

核心路径变更 弱引用终结器可能不触发 全局 torch._dynamo.reset() 影响

关联 Issue

#21073 [Bug]: vLLM doesn't release memory on deletion of the LLM runner with VLLM_ENABLE_V1_MULTIPROCESSING=0

完整报告

参与讨论