Repository navigation
fix(mlx): run all of a runner's GPU work on one stream (fixes tensor-parallel stalls with fast synch) - #2391
Open
AlexCheema wants to merge 1 commit into
Open
AlexCheema wants to merge 1 commit into
AlexCheema wants to merge 1 commit into
Conversation
With MLX_METAL_FAST_SYNCH on (the default for tensor parallel over RDMA), a GPU op that needs the result of one on another GPU stream spins on the GPU until it is there. mlx-lm runs the model on its generation stream, while exo's own ops (prefix cache copies, trims, snapshot restores) ran on the default stream. When a graph went from one stream to the other and back into a tensor-parallel forward, whose GPU work also spins waiting for the communication stream, the eval deadlocked: on one rank the communication stream spun in Fence::wait for a GPU that never got there, while the other rank waited in all_reduce. Under concurrent load the runner made no progress until the stuck-runner watchdog restarted it. Make mlx-lm's generation stream the runner thread's default GPU stream, and use it for exo's own generation code too, so there is one GPU stream. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4 tasks done
Contributor
Author
|
Follow-up: #2393 fixes the second cause of the tensor-parallel fast-synch stalls. A prompt eval that started while the previous decode step's collectives were still running could deadlock, mostly when another model's runner shared one of the Macs. With both PRs, RDMA tensor-parallel chaos testing (3 runs × 3 h, Qwen3.8-27B over 2 of 4 Mac minis, fast synch on, node crashes, freezes and partitions) had 0 stuck runners. |
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
Tensor parallel over RDMA (
MlxJaccl) stalls under concurrent load with fast synch on, which is exo's default and what JACCL needs for low latency. Within minutes of a few clients sending requests to Qwen3.8-27B on two Mac minis, a runner stops making progress. The stuck-runner watchdog then kills it after 90 s, and every request in flight fails. With fast synch off, the same load runs clean.Stack samples of both ranks during a stall:
mlx::core::eval_impl;mlx::core::Fence::waitfor its own GPU;jaccl::MeshImpl::all_reducefor the first rank's contribution.mx.eval([c.state for c in prompt_cache])in mlx-lm'sgenerate_step).Cause
With
MLX_METAL_FAST_SYNCH, a GPU op that needs the result of an op on another GPU stream spins on the GPU until it is there. A tensor-parallel forward's GPU work also spins, waiting for eachall_sumon the communication stream.mlx-lm runs the model on its own generation stream. exo's own ops ran on the default stream: prefix cache copies and trims, snapshot restores, snapshots. So graphs went from the generation stream to the default stream and back into the next tensor-parallel eval. Two GPU streams then wait on each other inside an eval that is also waiting on the communication stream, and nothing moves.
Fix
Run all of a runner's GPU work on one stream:
MlxBuildermakes mlx-lm's generation stream the runner thread's default GPU stream;Collectives still run on the CPU (communication) stream, as before.
Evidence
Reproduction without exo (two Mac minis, M4 Pro 64 GB, Thunderbolt 5 RDMA, JACCL, MLX main at 0e3ff364). A 64-layer tensor-parallel prefill (matmul +
all_sumper layer) whose caches are built by an op on the default stream from work on the generation stream:Regression test:
test_tensor_parallel_prefill_from_exo_built_caches_does_not_deadlock(marked slow, Metal only). It runs the same shape as two ranks on one Mac (ring backend). With this PR it passes 3/3 in about 7 s; without it, it hangs 3/3.exo under load (Qwen3.8-27B tensor parallel over RDMA on 2 Mac minis, 6 concurrent clients, fast synch on):
With the old build, even sequential single requests stalled once in four rounds of the speed test below.
Speed. Single requests (731-token prompt, 256 tokens out, no prefix cache, medians of 20, two rounds, fast synch on):
(The other "before" Qwen round stalled.)
For reference, on this PR's build with fast synch off:
So fast synch is worth keeping on for JACCL, and with this PR it no longer stalls.
Test plan
uv run basedpyright(0 errors),uv run ruff check,ruff format --checkon the changed filesuv run pytest src -m "not slow"(CI's command): 471 passed, 3 skipped🤖 Generated with Claude Code