|
2 | 2 |
|
3 | 3 | ## Fixed |
4 | 4 |
|
5 | | -- **Fix SIGABRT/SIGSEGV from concurrent `mx::eval` dispatch.** MLX's |
6 | | - Metal `CommandEncoder` is not thread-safe (ml-explore/mlx#2133). |
7 | | - Concurrent `mx::eval` calls from BEAM dirty-CPU scheduler threads |
8 | | - triggered `"A command encoder is already encoding"` assertions or |
9 | | - SIGSEGV from corrupted encoder state. Fixed by serialising all |
10 | | - `mx::eval` calls through `emily::safe_eval()` (mutex in |
11 | | - `c_src/emily/tensor.hpp`). Also removed `set_default_stream` calls |
12 | | - from `with_stream/2` — the NIF mutated MLX thread-local state which |
13 | | - is unreliable under BEAM process migration. Hardened |
14 | | - `resolve_stream(-1)` to avoid reading the thread-local default. |
15 | 5 | - Relax MNIST convergence canary threshold from 97% to 96% to eliminate |
16 | 6 | stochastic flaps (observed 96.99% on occasional runs). The test is a |
17 | 7 | sanity gate, not a performance benchmark. |
18 | 8 |
|
19 | 9 | ## Added |
20 | 10 |
|
21 | | -- M14 — Serving concurrency: stream-per-process. `Emily.Stream` lets |
22 | | - each BEAM process use its own Metal command queue for concurrent |
23 | | - inference. `Emily.Stream.new/1` creates a stream, |
24 | | - `Emily.Stream.with_stream/2` scopes all ops in a block to that |
25 | | - stream, and `Emily.Stream.synchronize/1` waits for completion. |
26 | | - The stream index is passed explicitly to every op NIF (no |
27 | | - thread-local race) via a `-1` sentinel for "use default stream" |
28 | | - (backwards-compatible). `Emily.Compiler.__partitions_options__/1` |
29 | | - error message now points to `Emily.Stream`. |
30 | | - - **New files**: `c_src/stream.cpp` (4 stream management NIFs), |
31 | | - `lib/emily/stream.ex` (`Emily.Stream` struct + API), |
32 | | - `test/emily/stream_test.exs`, `test/soak/stream_concurrency_test.exs`. |
33 | | - - **Modified**: every op NIF gained a trailing `int64_t s` stream |
34 | | - parameter; `Emily.Native` stubs, `Emily.Backend`, and all test |
35 | | - files updated accordingly. |
36 | | - - README now documents both concurrency patterns (stream-per-process |
37 | | - and pooled servings). |
| 11 | +- M14.5 — Worker-thread dispatch for vendored MLX. Replaces the |
| 12 | + stream-index NIF convention (M14) and the `safe_eval` mutex with a |
| 13 | + proper per-stream dedicated OS thread. Each `WorkerThread` (C++ class |
| 14 | + in `c_src/emily/worker.hpp`) owns an MLX stream and its Metal |
| 15 | + `CommandEncoder` on a single thread; NIFs dispatch work via |
| 16 | + `run_sync` (promise/future, blocks caller for ~1-10 µs). This |
| 17 | + eliminates the thread-local `CommandEncoder` mismatch that caused |
| 18 | + SIGABRT/SIGSEGV under BEAM process migration, and removes the global |
| 19 | + eval mutex that serialised all GPU work. |
| 20 | + - **MLX built from source.** MLX is vendored as a git submodule |
| 21 | + (`vendor/mlx`, pinned to commit `8e649be4`). The Makefile builds |
| 22 | + `libmlx.a` via cmake and statically links it into the NIF. The |
| 23 | + Metal shader library (`mlx.metallib`) is staged into `priv/` at |
| 24 | + compile time. No prebuilt download step. |
| 25 | + - **`Emily.MlxStream`** GenServer owns the default `WorkerThread` |
| 26 | + resource under the application supervisor. `default_worker/0` |
| 27 | + caches the worker ref in the process dictionary to avoid per-op |
| 28 | + GenServer calls. User-created streams (`Emily.Stream.new/1`) |
| 29 | + allocate their own worker; `enif_monitor_process` stops the thread |
| 30 | + when the creating process exits. |
| 31 | + - **NIF signature change**: every op NIF takes a worker ref as its |
| 32 | + first parameter (after env) instead of a stream index as its last. |
| 33 | + `Emily.Native`, `Emily.Backend`, and all test files updated |
| 34 | + accordingly. `c_src/stream.cpp` (old stream management NIFs) |
| 35 | + deleted. |
| 36 | + - **Concurrent Metal dispatch.** Multiple `WorkerThread`s = |
| 37 | + multiple MLX streams = concurrent Metal command queues. Model |
| 38 | + weights (`mx::array`) are shared across threads (refcounted, |
| 39 | + thread-safe reads). Shared data must be materialised before |
| 40 | + passing across workers (lazy tensors are bound to their creating |
| 41 | + worker's stream). |
| 42 | + - **New files**: `c_src/emily/worker.hpp` (WorkerThread class), |
| 43 | + `c_src/worker_nif.cpp` (resource registration with `down` |
| 44 | + callback), `lib/emily/mlx_stream.ex` (GenServer), |
| 45 | + `test/soak/eval_concurrency_test.exs`. |
| 46 | + - **`Emily.Stream`** rewritten: struct holds a `worker` ref instead |
| 47 | + of an integer index; `with_stream/2` stores it in the process |
| 48 | + dictionary under `:emily_worker`; `synchronize/1` removed. |
38 | 49 |
|
39 | 50 | - M13 — EXLA gradient conformance. Adds a third gradient oracle — |
40 | 51 | EXLA (XLA CPU backend) — to catch bugs where Emily and BinaryBackend |
|
0 commit comments