Skip to content

Detect cache thrashing in LRUCache - #2600

Merged
zcbenz merged 2 commits into
ml-explore:mainfrom
zcbenz:lru-cache-thrashing
Sep 19, 2025
Merged

zcbenz merged 2 commits into
ml-explore:mainfrom
zcbenz:lru-cache-thrashing

Conversation

@zcbenz

@zcbenz zcbenz commented Sep 17, 2025

Copy link
Copy Markdown
Member

Add a simple detection for cache thrashing when using LRUCache, by counting if cache misses have exceeded 2 * capacity. When that happens an exception would be thrown:

RuntimeError: Cache thrashing is happening, please set the environment variable MLX_CUDA_GRAPH_CACHE_SIZE to a larger value than 100 to fix degraded performance.

@awni awni 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.

Nice, thanks!!

@zcbenz
zcbenz merged commit 787c0d9 into ml-explore:main Sep 19, 2025
2 checks passed
@zcbenz
zcbenz deleted the lru-cache-thrashing branch September 19, 2025 00:12
faisalmemon pushed a commit to faisalmemon/mlx that referenced this pull request Oct 30, 2025
* Detect cache thrashing in LRUCache

* Do not check cache thrashing in tests
@BrewTestBot BrewTestBot mentioned this pull request Nov 20, 2025
1 task done
zcbenz pushed a commit to strayberry/mlx that referenced this pull request Aug 23, 2026
CommandEncoder::commit() returns early when an exception is thrown, for
example by the graph-cache thrashing check from ml-explore#2600, leaving from_nodes_,
to_nodes_, the graph keys, node_map_ and graph_ holding the nodes of the
failed graph. The next commit mixes them into a fresh graph and fails in
cudaGraphAddDependencies with cudaErrorInvalidValue, and every evaluation
after that keeps failing until the process is restarted.

Split the body into commit_impl() and restore the same state the successful
path restores when it throws, so a recoverable error only fails the
evaluation that caused it.

Two details the recovery depends on:

- check_cuda_error() never calls cudaGetLastError(), so the runtime error
  stays pending. ~CudaHandle() skips its destroy while an error is pending,
  which would leak every handle released afterwards and keep the graph from
  being recreated. Clear it first.
- CudaHandle::operator= can throw from both the destroy and the create. A
  graph left populated is worse than none: its nodes are still roots and run
  again on the next launch, while the fresh dependencies reference only the
  new nodes. Drop the handle even when destroying it fails.

The regression test shrinks the graph cache on a fresh stream and walks
distinct graph topologies until the check fires. The cache key is
topological, so varying shapes alone does not produce new keys. Before this
change the test sees one thrashing error followed by five
cudaGraphAddDependencies failures; after it, six independent thrashing
errors and no corruption.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants