Repository navigation
sc-12937: fix 'There is no Stream(gpu, N) in current thread' — share stream encoders across threads - #20
Merged
Conversation
…stream encoders across threads MLX registers each stream's CommandEncoder in a thread_local map (metal device.cpp / cpu encoder.cpp) and evals inline on the calling thread, so a stream is only usable from the OS thread that created it. Rust makes cross-thread graphs unavoidable: Array is Send, libtest runs every #[test] on its own thread, and MLX's global random KeySequence leaves unevaluated split nodes carrying the creating thread's stream. That produced the 20 order-dependent failures across value_and_grad, compile-with-state, optimizer convergence, and random tests (all 20 in the sc-12937 inventory; latent since the 0.31.2 bump — the thread_local map + throw predate 0.32.0, the suite just never ran to completion before sc-12940). Fix: new fork patch thread-shared-streams.patch (4th patch in the per-file apply_patches.sh flow, required=true) redirects gpu::new_stream / cpu::new_stream to the global-map registration that upstream's own new_thread_unsafe_stream (#3578, 0.32.0) uses, restoring cross-thread stream visibility for all streams including per-thread defaults. Sequential cross-thread eval (the failing pattern) is safe; concurrent eval on the SAME stream from two threads remains the caller's responsibility, unchanged from upstream's thread-unsafe API. Adds mlx-tests/tests/test_cross_thread_stream.rs pinning the fixed behavior deterministically (cross-thread lazy-graph eval, global random state crossing threads, Send'd evaluated array reused off-thread). Full suite (cargo test --all --no-fail-fast -- --test-threads=1): 964 passed / 0 failed on current main (pre-PR-#17 baseline was 939/20; PR #17's RNG eager-eval covers the RNG-mediated subset, this patch fixes the class — 2 of the new regression tests are red on main without it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-without-patch coverage - Serialize the four cross-thread regression tests behind a static Mutex: they intentionally move graphs across threads, so two of them evaluating concurrently under plain parallel `cargo test` raced the same stream's command encoder into a Metal assertion (SIGABRT). CI runs --test-threads=1 and is unaffected. - Add test_eval_graph_on_spawned_thread (main-thread graph evaluated on a spawned thread — red without the patch) and document that test_evaluated_array_used_from_other_thread is a guard, not a red/green pin. - Fix stale "ALL THREE patches" comment in mlx-sys/build.rs (now four). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
sc-12937: fix "There is no Stream(gpu, N) in current thread" at the root — share stream encoders across threads
Shortcut story sc-12937
Root cause (verified, corrects the story's hypothesis)
MLX registers each stream's
CommandEncoderin athread_localmap (metal::get_command_encoders()indevice.cpp, same shape for cpu inencoder.cpp) andgpu::eval()runs inline on the calling thread — so a stream is only usable from the OS thread that created it. Any graph node whose primitive stream was created on another thread throwsThere is no Stream(gpu, N) in current thread.Rust makes cross-thread graphs unavoidable:
ArrayisSend, libtest runs every#[test]on its own spawned thread, and pmetal moves work across tokio workers. This is not a 0.31.2→0.32.0 regression: thethread_localmap + throw already existed at v0.31.2; the full suite just never ran to completion before sc-12940, so all 20 failures were latent. What 0.32.0 added is the escape hatch this PR uses:new_thread_unsafe_stream(upstream #3578) registers encoders in a global fallback map thatget_command_encoderconsults from any thread.Relationship to sc-12787 (PR #17, merged while this was in flight)
PR #17's
random.rschange (eagerly evaluating the global RNG state) fixed the RNG-mediated subset of this class — which happens to cover all 20 of the story's original failures (they all crossed threads via the global random key). Verified on pristine current main: the originalnn::value_and_gradrepro passes, but the general class still fails — a lazy graph built on one thread and evaluated on another dies with the exact story error (2 of the 4 new regression tests here are red on current main without this patch). The Rust-side RNG workaround stays as belt-and-braces; this PR fixes the class at the MLX level.The fix
mlx-sys/patches/thread-shared-streams.patch— 4th fork patch in the per-fileapply_patches.shflow (sc-12746/sc-12780 conventions,required=true):gpu::new_streamandcpu::new_streamdelegate to the global-map registration used by upstream's ownnew_thread_unsafe_stream. Every stream (including per-thread defaults) becomes visible from every thread.Regression tests
mlx-tests/tests/test_cross_thread_stream.rs— 4 deterministic tests (the original 20 were test-order-dependent), serialized behind astatic Mutexso plain parallelcargo testcan't race MLX's non-thread-safe eval:test_eval_graph_built_on_other_threadStream(gpu, 0))test_eval_graph_on_spawned_threadStream(gpu, 2))test_global_random_state_across_threadstest_evaluated_array_used_from_other_threadVerification
cargo test -p pmetal-mlx-rs --lib nn::value_and_grad -- --test-threads=1): was 3/5 failing on 932beb4 → 5/5 pass.cargo test --all --no-fail-fast -- --test-threads=1: 964 passed / 0 failed (pre-PR-sc-12787: wire up CI on GitHub-hosted macos-15 (validate.yml never executed) + fix everything it catches #17 baseline was 939 passed / 20 failed).cargo fmt --all -- --checkclean;cargo clippy -- -D warningsclean.apply_patches.sh, idempotency guard intact).🤖 Generated with Claude Code