perf(router): cache classifier model instead of reloading it on every request#319
Merged
codelion merged 2 commits intoJul 12, 2026
Conversation
router_plugin.run() called load_optillm_model() on every request, which deserializes the ~400M-param ModernBERT-large base model, does a HuggingFace Hub round-trip for the fine-tuned safetensors, and rebuilds the tokenizer — all just to run a single forward pass for the routing decision. That turns an O(1 forward pass) routing step into O(load a transformer) per call. Memoize the loaded (model, tokenizer, device) bundle behind a module-level singleton guarded by a threading.Lock (double-checked locking), the same idiom already used by CacheManager in optillm/inference.py. The classifier is stateless at inference (predict_approach runs under model.eval() + torch.no_grad), so reusing the loaded objects across requests is correctness-safe. A failed load is not cached (the global is only assigned on success), so run()'s existing except-fallback still retries on the next call. Add tests/test_router_plugin_caching.py: repeated calls load the base model exactly once, the cached value is the expected (model, tokenizer, device) triple, and concurrent first-time access loads exactly once. All tests mock the heavy loaders, so they need no network or weights.
The dedicated tests/test_router_plugin_caching.py was not in the unit-tests job's fixed file list in .github/workflows/test.yml, so CI never executed it. Fold the three caching tests (single-load-on-repeat, bundle shape, concurrent single-load) into tests/test_plugins.py — which the unit-tests job already runs via `pytest tests/test_plugins.py` — and drop the standalone file, so the regression is actually guarded in CI.
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.
Problem
router_plugin.run()callsload_optillm_model()on every request (router_plugin.py):load_optillm_model()is not cheap. Each call:AutoModel.from_pretrained("answerdotai/ModernBERT-large")— deserializes a ~400M-param base model,hf_hub_download("codelion/optillm-modernbert-large", "model.safetensors")— a HuggingFace Hub round-trip (ETag check + potential download),load_model(...)— loads the fine-tuned state dict,AutoTokenizer.from_pretrained(...)— rebuilds the tokenizer.The actual routing decision is a single forward pass (tens of ms), but it is preceded by a full transformer load on each request. So a step that should be
O(1 forward pass)isO(load a transformer)every time therouterapproach is used.Fix
Memoize the loaded
(model, tokenizer, device)bundle behind a module-level singleton guarded by athreading.Lock(double-checked locking):The heavy loading logic is unchanged — it just moves into
_load_optillm_model()and now runs once.Why this is safe
predict_approach()runs the model undermodel.eval()+torch.no_grad()and never mutates it, so reusing one instance across requests is correctness-safe.CacheManagerinoptillm/inference.py, which caches models/tokenizers with a double-checkedthreading.Lock).run()'s existingexceptfallback and the next request retries the load.The one intentional trade-off is that the model now stays resident for the process lifetime instead of being re-created and garbage-collected each request — which is the desired behavior for a long-running inference proxy that routes repeatedly.
Testing
Adds
tests/test_router_plugin_caching.py(mocks the heavy loaders, so no network/weights required — CI-friendly):test_repeated_calls_load_model_once— 6 calls toload_optillm_model()load the base model exactly once and return the same cached bundle.test_returns_expected_bundle_shape— the cached value is the(OptILMClassifier, tokenizer, device)triple callers expect.test_concurrent_calls_load_model_once— 8 threads hitting the first load concurrently still load exactly once (double-checked lock holds).The repeated-call test is red on
main(base model loads 6×) and green with this change. Runs both underpytestand standalone, matching the repo's existing test convention.