Fix concurrent ModelManager mutations and LRU cache synchronization - #2938
Fix concurrent ModelManager mutations and LRU cache synchronization#2938KushagraKanaujia wants to merge 1 commit into
Conversation
…oboflow#2819) This commit addresses critical concurrency bugs in ModelManager and WithFixedSizeCache that could lead to: - Split lock generations (multiple locks for same model_id) - Loss of mutual exclusion between concurrent mutations - Duplicate LRU queue entries - Queue/manager state desynchronization - Decorator bypass of in-progress mutations ## Root Causes ### 1. Lock Generation Splitting The base ModelManager's remove() called _dispose_model_lock() while still holding the per-model lock inside the context manager. This allowed a new add_model() call to create a new lock generation (L2) while the old lock (L1) was still held, breaking per-model mutual exclusion. ### 2. Decorator Mutation Bypass ModelManagerDecorator.add_model() checked existence with an unlocked `if model_id in self` and returned immediately if true, bypassing any in-progress remove() operation that already held the per-model lock. ### 3. LRU Rollback Failures WithFixedSizeCache removed queue entries BEFORE calling super().remove(), so if clear_cache() failed, the identifier remained in the manager but disappeared from the queue, causing permanent desynchronization. ### 4. Duplicate Queue Entries Concurrent cold adds could both pass the `if queue_id in self` check before either acquired the queue lock, then both append the same queue_id, creating duplicates. ## Changes ### inference/core/managers/base.py - Added _models_lifecycle_locks dict to track ongoing mutations - Added _get_lifecycle_lock() method for decorators to check mutation state - Modified remove() to dispose lock AFTER releasing it, not during - Updated _dispose_model_lock() to clean up both state and lifecycle locks - Added detailed docstrings explaining lock generation stability ### inference/core/managers/decorators/base.py - Modified add_model() to check for ongoing mutations via lifecycle lock - If mutation detected, defer to wrapped manager to preserve ordering - Added logging for when decorator detects concurrent mutations ### inference/core/managers/decorators/fixed_size_cache.py - Modified remove() to remove queue entry AFTER successful super().remove() - Added try/except in eviction loop to restore queue entry on failure - Added double-check after acquiring queue lock to prevent duplicate entries - Moved queue.append() inside lock acquisition to prevent race conditions - Added detailed error logging for eviction failures ### tests/inference/unit_tests/core/managers/test_concurrent_mutations.py - New comprehensive test suite covering all concurrent scenarios - Tests for lock generation stability - Tests for decorator mutation ordering - Tests for LRU queue consistency on failures - Tests for duplicate queue entry prevention - Tests for alias handling in concurrent scenarios ## Impact - Prevents duplicate model loading and memory/VRAM spikes - Maintains strict per-model mutual exclusion - Ensures LRU queue always reflects manager state - Preserves mutation ordering across decorator layers - No breaking changes to public API ## Testing All changes are covered by new unit tests in test_concurrent_mutations.py that use threading primitives to deterministically reproduce the race conditions without timing dependencies. Fixes roboflow#2819 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Kushagra Kanaujia seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
voropaevv
left a comment
There was a problem hiding this comment.
I’m the author of #2821, which addresses the same issue.
At 738cf26a, the constructor-error path in add_model() still calls _dispose_model_lock() before releasing the per-model lock. A targeted component reproduction with real locks and threads reaches this sequence:
- H holds L1 while constructing the model.
- W obtains a reference to L1 and waits.
- H's constructor fails and removes the registry entry.
- N creates L2 and starts loading.
- H releases L1; W acquires it and also starts loading.
Two constructors for the same resolved identifier are then active simultaneously. External dependencies were stubbed; this was not a full-suite or production-server test.
Moving disposal after remove() releases its lock also does not account for callers that already hold references to that generation. The required invariant is that an old generation cannot remain usable for mutations while new callers use a replacement. Reservation counting is one way to enforce it, but the implementation does not need to match #2821.
Two additional component cases expose bookkeeping/composition problems:
- In
decorators/base.py,_get_lifecycle_lock()can be inherited by a wrappedModelManagerDecorator, whose initializer does not create_state_lock.WithFixedSizeCache(ModelManagerDecorator(ModelManager(...)))then raisesAttributeError. - In
fixed_size_cache.py,newis appended before eviction. Ifold.clear_cache()raises, restoringoldand re-raising leaves[old, new]in the queue while onlyoldis registered; the later add-error cleanup is not reached.
Could we add the load-failure and remove/reload schedules, nested-decorator case, and failed-eviction postconditions as regressions before considering the split-generation and bookkeeping issues resolved?
PawelPeczek-Roboflow
left a comment
There was a problem hiding this comment.
TMP impediment - CLA
Summary
This PR fixes critical concurrency bugs in
ModelManagerandWithFixedSizeCacheidentified in issue #2819.Problem Statement
The current implementation has several race conditions that can cause:
model_id, losing mutual exclusionImpact
These bugs can lead to:
Root Causes
1. Lock Generation Splitting
ModelManager.remove()called_dispose_model_lock()while still holding the per-model lock:This allowed new
add_model()calls to create a new lock generation while the old one was still held.2. Decorator Bypass
ModelManagerDecorator.add_model()checked existence without synchronization:3. LRU Rollback Failures
WithFixedSizeCacheremoved queue entries before attempting removal:4. Duplicate Queue Entries
Concurrent adds could both pass the existence check before either acquired the queue lock.
Solution
Changes to
inference/core/managers/base.py_models_lifecycle_locksdict tracks ongoing mutationsremove()now disposes lock AFTER releasing it_get_lifecycle_lock()method: Allows decorators to detect in-progress mutationsChanges to
inference/core/managers/decorators/base.pyChanges to
inference/core/managers/decorators/fixed_size_cache.pyTesting
Added comprehensive test suite in
test_concurrent_mutations.py:All tests use threading primitives (Events, Threads) to deterministically reproduce race conditions without timing dependencies.
Backward Compatibility
✅ No breaking changes - All changes are internal implementation improvements. Public API remains unchanged.
Related Issues
Fixes #2819
Acknowledgments
Thanks to the issue author for the excellent bug report and detailed analysis!
🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com