Skip to content

[Bugfix] Format kernel-import errors eagerly so warning_once does not retain them - #54098

Merged
yewentao256 merged 7 commits into
vllm-project:mainfrom
zwang86:fix-warning-once-exception-leak
Sep 15, 2026
Merged

yewentao256 merged 7 commits into
vllm-project:mainfrom
zwang86:fix-warning-once-exception-leak

Conversation

@zwang86

@zwang86 zwang86 commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

FIX #54096

Purpose

logger.warning_once caches its arguments forever (lru_cache). The kernel import fallbacks passed the live ImportError as a lazy format argument, so the cache retained the exception — and through __traceback__ every frame and local on the raising path. import_kernels runs inside LLM.__init__, so on platforms without compiled vllm._C kernels the pinned locals included the LLM instance under construction: it could never be garbage collected, its EngineCore subprocess only exited with the interpreter, and on accelerator plugins the device stayed held by the leaked engine. Each exception instance was also a distinct cache key, defeating the once-deduplication.

Per review feedback, this fixes the specific call sites and nothing else: format the exception eagerly at the five kernel import sites (repr(e) with %s renders byte-identical output to the previous %r of the live exception). No logger changes, no new tests.

Test Plan

Block the vllm._C import with a meta-path finder, keep a weakref to the raised exception, call Platform.import_kernels() twice, then gc.collect():

import gc, sys, weakref
from vllm.platforms.interface import Platform

class TrackedError(ModuleNotFoundError):  # plain ModuleNotFoundError is not weakref-able
    pass

refs = []
class Blocker:
    def find_spec(self, name, path=None, target=None):
        if name in ("vllm._C", "vllm._moe_C_stable_libtorch"):
            e = TrackedError(f"No module named '{name}'")
            refs.append(weakref.ref(e))
            raise e
        return None

for m in ("vllm._C", "vllm._moe_C_stable_libtorch"):
    sys.modules.pop(m, None)
sys.meta_path.insert(0, Blocker())

Platform.import_kernels()
Platform.import_kernels()  # second call: shows dedup behavior
gc.collect()
print("exception retained after gc:", any(r() is not None for r in refs))

Test Result

Output before this fix (main):

WARNING 09-14 21:12:50 [interface.py:368] Failed to import from vllm._C: TrackedError("No module named 'vllm._C'")
WARNING 09-14 21:12:50 [interface.py:368] Failed to import from vllm._C: TrackedError("No module named 'vllm._C'")
exception retained after gc: True

The warning fires on every call ("once" is defeated because each exception instance is a new cache key), and the exception is retained forever with its traceback.

Output after this fix:

WARNING 09-14 21:10:57 [interface.py:368] Failed to import from vllm._C: TrackedError("No module named 'vllm._C'")
exception retained after gc: False

The warning text is byte-identical, fires once, and nothing is retained.

End-to-end: on a Neuron (OOT platform) stack with this fix applied, del llm shuts the EngineCore down within 2s; previously the engine survived until interpreter exit and held the device.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify mergify Bot added the bug Something isn't working label Aug 27, 2026

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reporting this!

When the exception is raised inside LLM.init — e.g. the vllm._C ImportError

I think in this case, we should fix the specific bug we have instead of doing updates to logs. I don't think it worth the complexity we introduce here. Something raised in LLM.__init__ is severe

… retain them

logger.warning_once caches its arguments forever (lru_cache). The kernel
import fallbacks passed the live ImportError as a lazy format argument,
so the cache retained the exception -- and through __traceback__ every
frame and local on the raising path. import_kernels runs inside
LLM.__init__, so on platforms without compiled vllm._C kernels the
pinned locals included the LLM instance under construction: it could
never be garbage collected, its EngineCore subprocess only exited with
the interpreter, and on accelerator plugins the device stayed held by
the leaked engine. Each exception instance was also a distinct cache
key, defeating the once-deduplication.

Format the exception eagerly (repr(e) with %s renders byte-identical
output to the previous %r of the live exception) at the five kernel
import call sites, and add a regression test that the ImportError is
collectable after Platform.import_kernels.

Signed-off-by: Zeyu Wang <zwang86@users.noreply.github.com>
@zwang86
zwang86 force-pushed the fix-warning-once-exception-leak branch from bc8b025 to 356cb47 Compare August 27, 2026 22:13
@mergify mergify Bot added the nvidia label Aug 27, 2026
@zwang86 zwang86 changed the title [Bugfix] Prevent logger *_once from retaining exception tracebacks [Bugfix] Format kernel-import errors eagerly so warning_once does not retain them Aug 27, 2026
@mergify mergify Bot added the cpu Related to CPU backends label Aug 27, 2026
@zwang86

zwang86 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for reporting this!

When the exception is raised inside LLM.init — e.g. the vllm._C ImportError

I think in this case, we should fix the specific bug we have instead of doing updates to logs. I don't think it worth the complexity we introduce here. Something raised in LLM.__init__ is severe

@yewentao256 Thanks for the feedback, reworked it.
The PR now only touches the five kernel-import call sites (format the exception eagerly with repr(e); log output is byte-identical) and leaves the logger untouched. Added a targeted regression test that the ImportError is collectable after import_kernels.

@zwang86
zwang86 requested a review from yewentao256 August 27, 2026 22:23

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work!

Comment thread tests/test_logger.py Outdated
Comment on lines +37 to +82
def test_import_kernels_does_not_retain_the_import_error(monkeypatch):
"""The vllm._C ImportError must be collectable after import_kernels.

``logger.warning_once`` caches its arguments forever (``lru_cache``). If
the live ImportError is passed as a format argument, the cache retains it
and — through ``__traceback__`` — every frame on the raising path.
``import_kernels`` runs inside ``LLM.__init__``, so on platforms without
compiled ``vllm._C`` kernels this pinned the ``LLM`` instance itself: it
could never be garbage collected and its EngineCore only exited with the
interpreter. See #54096.
"""
import gc
import weakref

from vllm.platforms.interface import Platform

blocked = ("vllm._C", "vllm._moe_C_stable_libtorch")
exc_refs = []

class _TrackedModuleNotFoundError(ModuleNotFoundError):
pass # python-level subclass; supports weak references

class _BlockKernelImports:
def find_spec(self, name, path=None, target=None):
if name in blocked:
exc = _TrackedModuleNotFoundError(f"No module named '{name}' (test)")
exc_refs.append(weakref.ref(exc))
raise exc
return None

for mod in blocked:
monkeypatch.delitem(sys.modules, mod, raising=False)
monkeypatch.setattr(sys, "meta_path", [_BlockKernelImports()] + sys.meta_path)

Platform.import_kernels()

assert exc_refs, "the blocked import was never attempted"
gc.collect()
leaked = [ref for ref in exc_refs if ref() is not None]
assert not leaked, (
"ImportError retained after import_kernels; a cached reference "
"(e.g. a live exception passed to logger.warning_once) pins its "
"traceback and every local on the raising path"
)


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_import_kernels_does_not_retain_the_import_error(monkeypatch):
"""The vllm._C ImportError must be collectable after import_kernels.
``logger.warning_once`` caches its arguments forever (``lru_cache``). If
the live ImportError is passed as a format argument, the cache retains it
and — through ``__traceback__`` — every frame on the raising path.
``import_kernels`` runs inside ``LLM.__init__``, so on platforms without
compiled ``vllm._C`` kernels this pinned the ``LLM`` instance itself: it
could never be garbage collected and its EngineCore only exited with the
interpreter. See #54096.
"""
import gc
import weakref
from vllm.platforms.interface import Platform
blocked = ("vllm._C", "vllm._moe_C_stable_libtorch")
exc_refs = []
class _TrackedModuleNotFoundError(ModuleNotFoundError):
pass # python-level subclass; supports weak references
class _BlockKernelImports:
def find_spec(self, name, path=None, target=None):
if name in blocked:
exc = _TrackedModuleNotFoundError(f"No module named '{name}' (test)")
exc_refs.append(weakref.ref(exc))
raise exc
return None
for mod in blocked:
monkeypatch.delitem(sys.modules, mod, raising=False)
monkeypatch.setattr(sys, "meta_path", [_BlockKernelImports()] + sys.meta_path)
Platform.import_kernels()
assert exc_refs, "the blocked import was never attempted"
gc.collect()
leaked = [ref for ref in exc_refs if ref() is not None]
assert not leaked, (
"ImportError retained after import_kernels; a cached reference "
"(e.g. a live exception passed to logger.warning_once) pins its "
"traceback and every local on the raising path"
)

We don't need a specific test for this small update

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the test

Comment thread vllm/platforms/interface.py Outdated
Comment on lines +368 to +371
# Format eagerly: warning_once caches its arguments forever, and a
# live exception would pin its traceback (and every local on the
# raising path, including the LLM under construction).
logger.warning_once("Failed to import from vllm._C: %s", repr(e))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Format eagerly: warning_once caches its arguments forever, and a
# live exception would pin its traceback (and every local on the
# raising path, including the LLM under construction).
logger.warning_once("Failed to import from vllm._C: %s", repr(e))
logger.warning_once("Failed to import from vllm._C: %s", repr(e))

Comment not needed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the comment

Signed-off-by: Zeyu Wang <zwang86@users.noreply.github.com>
Signed-off-by: Zeyu Wang <zwang86@users.noreply.github.com>

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also update the PR description, what's the output before/after this fix

@zwang-amazon

Copy link
Copy Markdown

Please also update the PR description, what's the output before/after this fix

Thank you @yewentao256 , updated the PR description as well.

@yewentao256 yewentao256 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the work!

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Sep 14, 2026
@yewentao256 yewentao256 added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 14, 2026
@github-actions

Copy link
Copy Markdown

✅ @zwang86, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Your branch must contain every commit currently on its upstream target branch. Merge or rebase onto the latest target branch, then rerun the command. Append --allow-stale to a run command to test an outdated branch at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@zwang86

zwang86 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #88947 for commit 8e656af14827.

@zwang86

zwang86 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

❌ This PR is 3 commits behind upstream main. Your branch must contain every commit currently on upstream main. No new CI build was started. Merge or rebase onto the latest main, then rerun /ci run. To test this branch at your own risk, use /ci run --allow-stale.

@zwang86

zwang86 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #89148 for commit f82ca61a48c7.

@yewentao256
yewentao256 merged commit b6ce714 into vllm-project:main Sep 15, 2026
119 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Sep 15, 2026
keneoneth pushed a commit to keneoneth/vllm that referenced this pull request Sep 16, 2026
… retain them (vllm-project#54098)

Signed-off-by: Zeyu Wang <zwang86@users.noreply.github.com>
Co-authored-by: Zeyu Wang <zwang86@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
… retain them (vllm-project#54098)

Signed-off-by: Zeyu Wang <zwang86@users.noreply.github.com>
Co-authored-by: Zeyu Wang <zwang86@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cpu Related to CPU backends nvidia ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: logger.warning_once with a live exception argument leaks the exception's entire traceback (pins the LLM instance on platforms without vllm._C)`

3 participants