@@ -674,6 +674,31 @@ silent about it and ships a tested pattern.
674674** Exit:** both patterns documented; concurrency soak demonstrates
675675the streamed path is stable.
676676
677+ ** Post-M14 note — eval serialisation:**
678+ MLX is not thread-safe (ml-explore/mlx #2133 ). The Metal
679+ ` CommandEncoder ` is shared state — concurrent ` mx::eval ` calls from
680+ different OS threads crash with `"A command encoder is already
681+ encoding to this command buffer"` (SIGABRT) or SIGSEGV from corrupted
682+ encoder state. The M14 soak tests (8 concurrent workers via
683+ ` Task.async_stream ` ) exposed this by being the first code path to call
684+ ` mx::eval ` from multiple dirty-CPU scheduler threads simultaneously.
685+
686+ Fixed by:
687+ 1 . ** ` emily::safe_eval() ` ** : a mutex-serialised ` mx::eval ` wrapper in
688+ ` c_src/emily/tensor.hpp ` . All eval callsites route through it.
689+ Graph-building ops (regular scheduler) remain lock-free.
690+ 2 . ** Removed ` set_default_stream ` from ` with_stream/2 ` ** : the NIF
691+ mutated MLX thread-local state on BEAM scheduler threads, which
692+ is unreliable since BEAM processes migrate between OS threads.
693+ The process-dictionary-based stream routing was already correct.
694+ 3 . ** Hardened ` resolve_stream(-1) ` ** : the -1 fallback now reads the
695+ device default directly instead of the (potentially corrupted)
696+ thread-local default.
697+
698+ The mutex serialises Metal dispatch at the cost of true concurrent
699+ GPU execution. See M15.5 (MLX upgrade) for the plan to restore
700+ concurrency via MLX's native thread-local ` CommandEncoder ` support.
701+
677702### M15 — Native linalg
678703
679704` lu ` , ` svd ` , ` qr ` , ` cholesky ` , ` triangular_solve ` , ` eigh ` ,
@@ -698,6 +723,37 @@ BinaryBackend-slow. MLX exposes most natively under `mx::linalg::*`.
698723** Exit:** all ` mx::linalg::* ` -backed callbacks pass property suite;
699724remaining ` via_binary ` linalg paths documented with rationale.
700725
726+ ### M15.5 — MLX upgrade (build from source)
727+
728+ Emily pins MLX 0.25.1 via pre-built binaries from ` cocoa-xu/mlx-build ` .
729+ MLX gained native thread-safety on ` main ` in April 2026 (thread-local
730+ ` CommandEncoder ` ml-explore/mlx #3348 , ` ThreadLocalStream ` C++ API
731+ ml-explore/mlx #3405 ), but neither fix is in any release yet (latest:
732+ 0.31.1). Building from source unblocks true concurrent Metal dispatch
733+ and removes the ` safe_eval ` mutex introduced in the post-M14 fix.
734+
735+ - ** Build MLX from source** (from ` main ` or 0.32+ when released)
736+ instead of fetching the pre-built 0.25.1 tarball. Extend ` mix.exs `
737+ to support an ` MLX_SOURCE ` env var pointing at a local MLX build.
738+ - ** Audit API changes** between 0.25.1 and target version. Emily's
739+ C++ surface is narrow (core ops, linalg, streams, eval, allocator),
740+ but six months of MLX releases may rename or remove functions.
741+ - ** Adopt ` ThreadLocalStream ` C++ API** : each BEAM dirty-CPU thread
742+ gets its own MLX stream automatically, enabling true per-thread
743+ Metal command queues without the eval mutex.
744+ - ** Remove ` emily::safe_eval ` mutex** once native thread-safety is
745+ validated — revert to direct ` mx::eval ` calls.
746+
747+ ** Testing** :
748+ - Amplified stress test (16 workers, 100 iterations) passes without
749+ mutex under the new MLX build.
750+ - Full test suite 20x with zero crashes.
751+ - Benchmark ` to_binary ` latency under concurrent load: confirm
752+ throughput improves vs. the mutex-serialised path.
753+
754+ ** Exit:** concurrent soak tests pass without mutex; MLX build-from-source
755+ documented; stress test confirms concurrent Metal dispatch is stable.
756+
701757### M16 — Mixed-precision training
702758
703759bf16 activations + f32 master weights + loss scaling is the standard
0 commit comments