Repository navigation
[GH-908] Lock contention in EpisodicMemoryManager - #944
Conversation
d732c65 to
1d176fd
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses lock contention issues in the EpisodicMemoryManager by replacing a coarse-grained instance-level lock with fine-grained read-write locks. The changes introduce an async read-write lock implementation and a lock manager to handle per-session locking, allowing concurrent read operations while maintaining exclusive write access when needed.
Changes:
- Implemented
AsyncRWLockandAsyncRWLockManagerfor fine-grained concurrency control - Renamed
putmethod torelease_reffor better clarity - Replaced generic
RuntimeErrorexceptions with specific error types (SessionInUseError,SessionNotFoundError,EpisodicMemoryManagerClosedError)
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/memmachine/common/rw_locks.py | New async read-write lock implementation and lock manager |
| src/memmachine/common/errors.py | Added specific error types for session management |
| src/memmachine/episodic_memory/instance_lru_cache.py | Converted synchronous methods to async with read-write locking |
| src/memmachine/episodic_memory/episodic_memory_manager.py | Replaced instance-level lock with per-session locks and updated error handling |
| tests/memmachine/common/test_rw_locks.py | Comprehensive test coverage for new lock implementation |
| tests/memmachine/episodic_memory/test_instance_lru_cache.py | Updated tests to use async methods and new release_ref naming |
| tests/memmachine/episodic_memory/test_episodic_memory_manager.py | Updated tests for new error messages and async method calls |
| examples/llamaindex/mem_machine_memory.py | Updated to use release_ref instead of put |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f3655b9 to
578fc84
Compare
| self._readers_lock = asyncio.Lock() | ||
| self._writer_lock = asyncio.Lock() | ||
|
|
||
| async def acquire_read(self) -> None: |
There was a problem hiding this comment.
Very likely, the aquire_write will be starved with this approach.
There was a problem hiding this comment.
Good point. Will introduce a gator to block the read lock when some one is trying to get a write lock.
| # Move accessed node to the front | ||
| ret: EpisodicMemory | None = None | ||
| task: asyncio.Task[None] | None = None | ||
| async with self._lock.read_lock(): |
There was a problem hiding this comment.
Would it be cheaper to acquire write lock without createing a new task?
| del self.cache[cast("str", lru_node.key)] | ||
| lru_node = self.tail.prev | ||
| lru_node = tmp | ||
| lru_node = tmp |
There was a problem hiding this comment.
If we really want to improve locking contention, this requires further optimization. The close() function is expensive in this function call. If it is done with lock, it potentially still can affect performance.
There was a problem hiding this comment.
Updated. Move the close to a queue and use an async task to do the close so that it does not block here.
578fc84 to
3ecc395
Compare
|
Update the |
The `EpisodicMemoryManager` has a instance level lock that serialize the operations in the manager. This change separate the instance level lock into better grained locks to reduce the contention. 1. introduce an async read-write lock to allow re-entrance for read operations. 2. introduce a lock pool to manage the read-write lock for each session. Use `_session_locks` to manage the sessions. 3. change the instance level lock to read-write `_close_lock` which only protect the status of `self._closed` 4. update the open/create/open_and_create functions to use read/write lock in different conditions. Some additional changes: * rename the put method to release_ref which makes more sense to the reader. * replace the runtime error with specific error defined in the project.
3ecc395 to
64fd09e
Compare
The `EpisodicMemoryManager` has a instance level lock that serialize the operations in the manager. This change separate the instance level lock into better grained locks to reduce the contention. 1. introduce an async read-write lock to allow re-entrance for read operations. 2. introduce a lock pool to manage the read-write lock for each session. Use `_session_locks` to manage the sessions. 3. change the instance level lock to read-write `_close_lock` which only protect the status of `self._closed` 4. update the open/create/open_and_create functions to use read/write lock in different conditions. Some additional changes: * rename the put method to release_ref which makes more sense to the reader. * replace the runtime error with specific error defined in the project.
…ne#944) The `EpisodicMemoryManager` has a instance level lock that serialize the operations in the manager. This change separate the instance level lock into better grained locks to reduce the contention. 1. introduce an async read-write lock to allow re-entrance for read operations. 2. introduce a lock pool to manage the read-write lock for each session. Use `_session_locks` to manage the sessions. 3. change the instance level lock to read-write `_close_lock` which only protect the status of `self._closed` 4. update the open/create/open_and_create functions to use read/write lock in different conditions. Some additional changes: * rename the put method to release_ref which makes more sense to the reader. * replace the runtime error with specific error defined in the project.
Purpose of the change
The
EpisodicMemoryManagerhas a instance level lock that serialize the operations in the manager. This change separate the instance level lock into better grained locks to reduce the contention.Description
_session_locksto manage the sessions._close_lockwhich only protect the status ofself._closedSome additional changes:
putmethod torelease_refwhich makes more sense to the reader.Fixes/Closes
Fixes #908
Type of change
How Has This Been Tested?
Checklist
Maintainer Checklist