@@ -277,7 +277,7 @@ and back. Lift to native MLX:
277277- ` gather ` → ` mlx::core::gather `
278278
279279Window reductions stay on ` via_binary ` in M9 — pool-based conv
280- training is scoped to M10 .
280+ training is scoped to M17 .
281281
282282** Testing — Layers 4 (Grad) and 5 (Training):**
283283
@@ -341,47 +341,116 @@ user-visible value, not difficulty. Headline rationale:
341341 without hand-holding.
3423425 . ** 1.0 release** (M22) ships the result.
343343
344- ### M10 — Quantized inference
344+ ### M10 — Quantized inference primitives (partial)
345345
346346Quantization is the single largest gap between Emily and "actually run
347347Qwen3 on a 16 GB MacBook". MLX ships native int4/int8 affine
348348quantization (` mx::quantize ` , ` mx::dequantize ` ,
349- ` mx::quantized_matmul ` ) and Bumblebee can already load quantized
350- checkpoints — Emily just silently can't consume them today. Without
351- this, the library's headline value proposition only works for users
352- with enough RAM for fp16 weights.
353-
354- - ** Native bindings** : ` Native.quantize/3 ` , ` Native.dequantize/4 ` ,
355- ` Native.quantized_matmul/6 ` over the MLX C++ functions. Pass group
356- size and bit-width as integers; default to MLX's `group_size=64,
357- bits=4` but expose overrides.
358- - ** Backend routing** : detect quantized operand structs at the Nx
359- layer (Bumblebee tags them in the parameter map) and dispatch the
360- matmul callback to ` Native.quantized_matmul ` rather than
361- ` Native.matmul ` . Non-matmul ops (norms, residual adds, embeddings)
362- stay on the existing fp16/bf16 fast paths — only the linear
363- projection is quantized.
364- - ** Memory accounting** : quantized inference is allocator-pattern
365- different from fp16 — packed weights load once and never
366- re-quantize. Add a soak case asserting peak memory matches the
367- expected packed-weight footprint within ~ 10%.
368-
369- ** Testing** :
370- - Native unit tests with hand-computed packed-weight expected values
371- at small group sizes so the bit-packing is checkable.
372- - Backend property test: ` quantize → dequantize → matmul ` against
373- ` Nx.BinaryBackend ` matmul on the dequantized weights, asserting
374- agreement within the documented quantization-error bound.
375- - Conformance: add ` Qwen/Qwen3-0.6B-AWQ ` (or the MLX-community
376- quantized variant) as ` :qwen3_quant_full ` . Greedy-decode the same
377- prompt as ` qwen3_full ` and assert the completion matches a
378- checked-in reference produced by MLX's own Python bindings on the
379- same quantized weights — * not* by the f16 model, because the whole
380- point is to catch quantization-specific drift.
381-
382- ** Exit:** Qwen3-0.6B-AWQ greedy-decodes end-to-end on Emily; Backend
383- property tests green; quantized soak harness asserts allocator
384- invariants.
349+ ` mx::quantized_matmul ` ); M10 binds it at the Native and Elixir levels
350+ and ships a direct-call helper for eager use. The Bumblebee-integrated
351+ conformance path is split out to M10.5 — see ** Scope note** below.
352+
353+ ** Shipped** :
354+
355+ - ** Native bindings** : ` Native.quantize/3 ` , ` Native.dequantize/5 ` ,
356+ ` Native.quantized_matmul/7 ` over the MLX C++ functions. ` quantize `
357+ returns a 3-tuple ` {w_q, scales, biases} ` . ` quantized_matmul/7 ` takes
358+ ` transpose ` as an explicit boolean rather than PLAN's original ` /6 ` :
359+ AWQ packed layouts need ` transpose=false ` while fresh-from-dense
360+ weights use ` transpose=true ` , and MLX exposes it as a required
361+ parameter.
362+ - ** ` Emily.QuantizedWeight ` ** (` lib/emily/quantized_weight.ex ` ) —
363+ ` Nx.Container ` -derived struct with `{value, scales, biases,
364+ group_size, bits, transpose}`. Scalar metadata survives container
365+ traversal via ` Nx.Container ` 's ` keep: ` option. ` from_dense/2 `
366+ validates rank, last-axis divisibility, dtype, and bit count.
367+ - ** ` Emily.Quantization.quantized_matmul/2 ` ** — direct-call helper that
368+ extracts refs from an input tensor and a ` %QuantizedWeight{} ` and
369+ dispatches the fused kernel. Intended for eager/benchmark use and as
370+ the substrate for M10.5's defn-integration path.
371+ - ** Memory soak** (` test/soak/quantized_memory_test.exs ` ) — 1000-iter
372+ quantized-matmul loop asserts active memory returns within 4 MB of
373+ baseline after ` Native.clear_cache/0 ` . Kept separate from the fp16
374+ memory soak because quantized inference is allocator-pattern
375+ different: packed weights load once and never re-quantize.
376+ - ** Native unit tests + Backend property tests** — see
377+ ` test/emily/native_test.exs ` (+7 cases) and
378+ ` test/emily/quantization/ ` (two new files). Round-trip `quantize →
379+ dequantize` and ` quantized_matmul` vs. ` matmul(x, dequantize(…))`
380+ oracles for both ` transpose=true ` and ` transpose=false ` layouts.
381+
382+ ** Scope note — why no Backend routing / Axon integration / conformance
383+ test in M10** :
384+
385+ - ** ` Backend.dot/7 ` dispatch doesn't work.** PLAN'd approach was
386+ "detect quantized operand structs at the Nx layer (Bumblebee tags
387+ them in the parameter map) and dispatch the matmul callback to
388+ ` Native.quantized_matmul ` ". But ` Nx.dot/2 ` calls
389+ ` Nx.LazyContainer.traverse/3 ` expecting a single ` %T{} ` ; a
390+ three-tensor ` %QuantizedWeight{} ` container raises before reaching
391+ ` Backend.dot/7 ` .
392+ - ** Axon layer-op dispatch doesn't work either.** ` Axon.layer ` ops run
393+ at ` Nx.Defn.jit ` trace time with ` Nx.Defn.Expr ` inputs;
394+ ` Nx.Defn.Evaluator ` walks those expressions dispatching ` Nx.Backend `
395+ callbacks with materialized refs. There is no public hook to inject
396+ a custom op like ` Native.quantized_matmul ` that isn't already a
397+ ` Nx.Backend ` callback, and ` deftransform ` / ` hook ` / metadata all
398+ run at trace time (no refs available).
399+ - ** Bumblebee has no AWQ loader yet.** The exploration for M10
400+ confirmed ` deps/bumblebee ` has zero quantization-loading code (no
401+ AWQ, GPTQ, MLX-format paths). PLAN.md's "Bumblebee can already load
402+ quantized checkpoints" is aspirational.
403+
404+ All three of these are meaningful scope. M10 ships the substrate they
405+ all need; M10.5 picks the defn-integration strategy and ships the
406+ conformance test.
407+
408+ ** Exit** : Native NIFs green under unit + property tests; QuantizedWeight
409+ container property tests green; direct-call helper green under oracle
410+ comparison; quantized memory soak clean.
411+
412+ ### M10.5 — Bumblebee quantized inference integration
413+
414+ Closes the gap M10 left open: getting ` Native.quantized_matmul `
415+ reachable from ` Nx.Defn.jit ` -traced Axon forward passes so Bumblebee's
416+ AWQ-loading (when it lands) routes through the fused kernel.
417+
418+ Approach choices (pick before starting):
419+
420+ 1 . ** Defn-native dequantize** — implement MLX's int4/int8 affine
421+ dequantize using Nx bit primitives (right-shift + mask + multiply +
422+ add). ` Emily.Quantization.Layers.quantized_dense/3 ` becomes
423+ ` Nx.dot(x, dequantize_defn(qw)) ` . Correct and unblocks the full
424+ Axon/Bumblebee path, but uses two kernels (dequantize + matmul)
425+ instead of MLX's fused one — M11's fast-kernel work subsumes the
426+ perf gap.
427+ 2 . ** Emily.Compiler custom-op intercept** — fork ` Nx.Defn.Evaluator `
428+ under Emily to recognise a sentinel ` Expr ` node and route to
429+ ` Native.quantized_matmul ` . Full fused-kernel story but large
430+ surface; fragile against upstream Nx evolution.
431+ 3 . ** Upstream Nx extension** — add a custom-backend-op hook to
432+ ` Nx.Defn.Compiler ` / ` Nx.Defn.Evaluator ` . Cleanest long-term
433+ solution; slowest to land because it needs upstream review/merge.
434+
435+ Also in scope for M10.5:
436+
437+ - ** Test-only AWQ loader** (` test/support/awq_loader.ex ` ) — reads
438+ ` Qwen/Qwen3-0.6B-AWQ ` safetensors, extracts ` qweight ` , ` scales ` ,
439+ ` qzeros ` , maps to MLX's ` (w_q, scales, biases) ` layout. The
440+ trickiest bit is the AWQ zero-point → MLX bias conversion
441+ (` biases = -scales * zero_points ` ) and the AWQ ` [in, out/pack] ` vs.
442+ MLX ` [out, in] ` layout difference.
443+ - ** ` :qwen3_quant_full ` conformance test** — greedy-decode
444+ Qwen3-0.6B-AWQ on Emily, assert the completion matches a checked-in
445+ reference produced by MLX's Python bindings on the same quantized
446+ weights.
447+ - ** Bumblebee upstream contribution (optional, follow-up to M10.5)** —
448+ upstream the AWQ loader into ` deps/bumblebee ` so the test-only path
449+ becomes unnecessary.
450+
451+ ** Exit** : Qwen3-0.6B-AWQ greedy-decodes end-to-end on Emily under
452+ ` Nx.Defn.jit ` ; conformance test green; Axon-integrated quantization
453+ documented.
385454
386455### M11 — ` mlx::fast::* ` fused kernels
387456
0 commit comments