Repository navigation
fix: avoid leaking every Router instance via _cached_get_model_group_info - #41582
Closed
kratos0718 wants to merge 1 commit into
Closed
kratos0718 wants to merge 1 commit into
kratos0718 wants to merge 1 commit into
Conversation
_cached_get_model_group_info is decorated with @lru_cache directly on the Router class. That keys the cache on self as well as model_group, so every Router that ever calls it stays reachable through the cache for the life of the process, even after discard() (the router's own documented cleanup method) and every other reference are dropped. set_response_headers calls it on every request, so this hits any process that constructs more than one Router over its lifetime, for example once per tenant or on every proxy config reload. The sibling method cached_deployment_model_info already wraps the bound method in __init__ instead of decorating it at class scope, which keeps the cache scoped to the instance. This does the same thing for _cached_get_model_group_info. As a side effect it also fixes cache_clear() on this cache: it currently clears every Router's entries process-wide instead of just the one being invalidated, since it is still the same shared class-level cache object. Added a regression test that constructs a Router, calls the cached method, calls discard(), drops the reference, and asserts the Router is collected. It fails against the current code and passes with this fix.
Contributor
Contributor
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
|
The Router leak is fixed on main by #49513, which gives each router its own cache. Reopen if it still happens or something is missing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
How it solves it:
User Flow
This is an SDK-level object lifecycle bug, not something visible in a single request/response pair, so it's written from the seat of a developer embedding
litellm.Routerin their own long-running process (a proxy wrapper, a multi-tenant service creating one Router per tenant, or a config-reload path that rebuilds the Router).Before: memory grows without bound as old Router instances pile up
set_response_headerscalls on every request)router.discard(), the method Router itself documents as its cleanup step, then drop every other reference to the Routergc.get_referrerson the Router object) and find it is still reachable, held by the cached helper's own internal cachediscard()was called correctlyAfter: a discarded Router with no other references is actually freed
discard(), drop the referencegc.get_referrersreturns nothingRelevant issues
Affected release
Linear ticket
Pre-Submission checklist
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
This is a memory lifecycle bug: there's no request or response that looks different before and after, since the leak only shows up in what stays reachable on the heap after a Router is discarded. A live-proxy curl session would show identical output either way. The honest proof here is a direct reproduction against the Router object itself, run at each commit with
uv run python3 -c "...", not a pytest suite.Before (4b368bf)
Router leaks after discard()
uv run python3 -c "import litellm, weakref, gc; r = litellm.Router(model_list=[{'model_name': 'gpt-4', 'litellm_params': {'model': 'gpt-4', 'api_key': 'fake'}}]); r._cached_get_model_group_info('gpt-4'); r.discard(); ref = weakref.ref(r); del r; gc.collect(); print(ref() is None)"False, the Router is still reachable after discard() and every other reference are droppedAfter (7f1296f)
Router leaks after discard()
True, the Router is collected once discard() has run and nothing else references itType
🐛 Bug Fix
✅ Test
Caveats (if any)
Low
Final Attestation