@@ -21,7 +21,9 @@ checklist so future-us understands the trade-offs.
2121
2222- Ahead-of-time compilation (IREE-style). Complementary, separate effort.
2323- Windows or non-Apple-Silicon Linux GPU. CPU-only Linux is a nice-to-have for CI.
24- - Training / gradients beyond what ` Nx.Defn ` gives for free. Inference is the priority.
24+ - Training framework features beyond ` Nx.Defn.grad ` : distributed training,
25+ mixed-precision master weights, a native optimizer library. (Autodiff +
26+ small-scale training loops: in scope from M9.)
2527- Drop-in replacement for EMLX. We borrow where it's clearly right, but
2628 we're not constrained by its API.
2729- ` Emily.Stream ` as a public API — MLX streams stay internal in v1.
@@ -174,7 +176,7 @@ throughput number.
174176
175177** Exit:** Axon MLPs forward with ` compiler: Emily.Compiler ` ; results
176178match ` Nx.Defn.Evaluator ` running on the same backend within float
177- tolerance. (Training is out of scope for v1 .)
179+ tolerance. (Training via ` Nx.Defn.grad ` lands in M9 .)
178180
179181### M6 — ` mlx::core::compile ` wrapping — ** dropped**
180182
@@ -254,7 +256,87 @@ exists; only the Backend callback still routes through the
254256BinaryBackend fallback). Gated on the M7 ViT and Whisper suites
255257staying green through the switchover.
256258
257- ### M9 — 1.0 release
259+ ### M9 — Gradient conformance and training primitives
260+
261+ Training on Emily has been technically possible since M2 —
262+ ` Nx.Defn.grad ` is pure symbolic differentiation in Elixir and lowers
263+ to the same ops the forward pass uses. M9 turns "possible" into
264+ "usable" by (a) lifting the training-hot indexing ops off the
265+ ` via_binary ` fallback and (b) building the test scaffolding needed
266+ to trust a gradient.
267+
268+ ** Primitives.** ` Nx.Defn.grad ` of indexing-shaped ops lands on
269+ ` indexed_add ` ; every such backward currently ships to BinaryBackend
270+ and back. Lift to native MLX:
271+
272+ - ` indexed_add ` → ` mlx::core::scatter_add `
273+ - ` indexed_put ` → ` mlx::core::scatter `
274+ - ` gather ` → ` mlx::core::gather `
275+
276+ Window reductions stay on ` via_binary ` in M9 — pool-based conv
277+ training is scoped to M10.
278+
279+ ** Testing — Layers 4 (Grad) and 5 (Training):**
280+
281+ 1 . ** Grad-equivalence property tests** — for a zoo of ` defn ` -expressible
282+ functions f, assert ` Nx.Defn.grad(f) ` on ` Emily.Backend ` matches the
283+ same grad on ` Nx.BinaryBackend ` within dtype-appropriate tolerance.
284+ Reuses M2's StreamData harness; the zoo excludes non-differentiable
285+ ops (` argmax ` , ` argmin ` , ` floor ` , ` sign ` , comparisons).
286+ 2 . ** Numerical finite-difference oracle** — for the differentiable
287+ subset, assert ` (f(x+ε) - f(x-ε)) / 2ε ≈ grad(f)(x) ` . Tolerance is
288+ per-op and documented; f32 central differences bottom out around
289+ 1e-3 relative, so symbolic-grad tolerance must be relaxed
290+ accordingly where this is the oracle. Pilot on 3–4 ops before
291+ scaling the harness.
292+ 3 . ** Training curve-matching** — handwritten MLP and handwritten
293+ transformer-block training step, fixed seed, 50–200 steps; assert
294+ per-step loss trajectory matches ` Nx.BinaryBackend ` within
295+ tolerance. No Axon dependency in this tier — fewer moving parts
296+ when a test goes red.
297+ 4 . ** Training memory soak** (` test/soak/training_test.exs ` ,
298+ ` @tag :soak ` ) — 1k training steps; MLX memory returns to baseline
299+ after ` clear_cache/0 ` . Training exercises a different allocator
300+ pattern than inference (param-grad pairs, optimizer state,
301+ long-lived activation caches).
302+ 5 . ** ` :training_full ` ** (opt-in via ` --only training_full ` , ** not**
303+ on default CI) — Axon MLP on MNIST → >97% test accuracy. Catches
304+ systemic numerical drift that curve-matching misses because both
305+ sides use ` Nx.BinaryBackend ` as the oracle.
306+
307+ Axon is added as a ** test-only** dependency, used only by the
308+ ` :training_full ` tier.
309+
310+ ** Risks specific to this milestone:**
311+
312+ - f32 tolerance calibration for oracle (2) is per-op; the harness
313+ must support per-op tolerance tables, not a single global epsilon.
314+ - Random-key flow through ` Emily.Compiler ` needs an explicit test:
315+ grad through ` dropout ` with threaded keys, two invocations of the
316+ same compiled function must advance the RNG correctly.
317+ - MLX scatter semantics (out-of-bounds handling, tie-breaking) may
318+ differ from Nx expectations. Document divergence; encode property
319+ exclusions if needed.
320+
321+ ** Exit:** oracles (1)–(3) green in default CI; (4) and (5) green in
322+ opt-in CI job.
323+
324+ ### M10 — Conv-pool training
325+
326+ Lift window reductions (` window_sum ` , ` window_max ` , ` window_min ` ,
327+ ` window_product ` , ` window_scatter_max ` , ` window_scatter_min ` ) off
328+ ` via_binary ` onto their native MLX counterparts. This closes the
329+ last gap in the training primitive set and unblocks pool-based conv
330+ models (small CNNs, ViT classifier heads trained from scratch).
331+
332+ Scope is narrow: the lifts are mechanical per-op changes. Test
333+ coverage extends the M9 grad-equivalence and curve-matching zoo to
334+ cover the new ops, plus a small-CNN MNIST run in ` :training_full ` .
335+
336+ ** Exit:** grad-equivalence on window ops green; small-CNN MNIST
337+ training converges in ` :training_full ` .
338+
339+ ### M11 — 1.0 release
258340
259341- API docs, HexDocs, README with a worked Bumblebee example
260342- Hex release (public), versioned per conventions (` @version ` in mix.exs)
@@ -267,6 +349,8 @@ staying green through the switchover.
267349| Native | Hand-computed expected values | ExUnit unit tests |
268350| Backend | ` Nx.BinaryBackend ` on the same inputs | StreamData property tests + Nx conformance |
269351| Compiler | ` Emily.Backend ` in non-defn mode | Equivalence tests (same function, two modes) |
352+ | Grad | ` Nx.BinaryBackend ` grad + finite differences | StreamData property tests + numerical oracle |
353+ | Training | ` Nx.BinaryBackend ` loss trajectory | Curve-matching; MNIST convergence (` :training_full ` , opt-in) |
270354| E2E | EXLA-produced golden outputs | Conformance tests with cached weights |
271355
272356A bug can only be introduced in the layer where its test fails — no
@@ -275,13 +359,19 @@ cross-layer mystery bugs.
275359Additional harnesses:
276360- ** Memory soak** (` test/soak/memory_test.exs ` , ` @tag :soak ` ): 10k
277361 iterations; MLX memory stats asserted to return to baseline.
362+ - ** Training memory soak** (` test/soak/training_test.exs ` ,
363+ ` @tag :soak ` , from M9): 1k training steps; baseline restored after
364+ ` clear_cache/0 ` .
278365- ** Concurrency** (` test/soak/concurrency_test.exs ` , ` @tag :soak ` ):
279366 parallel inference; determinism + no crashes.
280367- ** Benchmarks** (` bench/ ` ): Benchee scripts; results logged in
281368 ` RELEASE.md ` per version.
282369- ** Conformance vs EXLA** (CI matrix, Mac for Emily + Linux+CUDA for
283370 EXLA oracle): same model, same input; runs on every PR touching
284371 Backend.
372+ - ** Convergence** (` :training_full ` , from M9; opt-in CI job): Axon
373+ training loop on MNIST; catches numerical drift the curve-matching
374+ oracle can't see.
285375
286376## Risks and mitigations
287377
0 commit comments