@@ -7,6 +7,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77
88<!-- %% CHANGELOG_ENTRIES %% -->
99
10+ ## 0.7.0 - 2026-06-13
11+
12+ ### Added
13+
14+ - ** Native Expr compiler — on by default under
15+ ` compiler: Emily.Compiler ` .** Lowers a traced ` Nx.Defn.Expr ` to a
16+ flat IR once and replays the whole forward graph in a ** single NIF
17+ call per invocation** , collapsing the per-op BEAM↔worker round-trips
18+ a step-evaluated decode loop would otherwise pay. Weights cross the
19+ NIF boundary once (captured by the compiled program) and are never
20+ re-serialised per call. It is the default, so a bare
21+ ` compiler: Emily.Compiler ` compiles native:
22+
23+ Nx.Defn.jit(&forward/1, compiler: Emily.Compiler).(input)
24+
25+ Coverage is the full Nx primitive set (with ` Emily.Backend ` 's
26+ dtype-coercion and op-composition semantics ported into the
27+ lowering), the fused ` Emily.Fast.* ` kernels (RMSNorm, LayerNorm,
28+ RoPE, scaled dot-product attention and its mask / sink / mask+sink
29+ variants), ` Nx.Block.* ` including the full ` LinAlg ` family
30+ (` cholesky ` / ` solve ` / ` qr ` / ` eigh ` / ` lu ` / ` svd ` /
31+ ` determinant ` ), ` Nx.Random ` , and the control flow ` cond ` /
32+ ` defn while ` (with the host loop driven entirely from the worker
33+ thread). Anything the IR can't lower yet routes through
34+ ` Nx.Defn.Evaluator ` under the default ` native_fallback: :eval ` (with
35+ a one-shot ` [:emily, :compiler, :fallback] ` telemetry event), so the
36+ native lane is safe as the default on any model. The default is read
37+ from ` config :emily, :native ` (defaulting to ` true ` ), so
38+ ` config :emily, native: false ` opts every defn out of the native lane
39+ application-wide — e.g. on a memory-constrained host where the
40+ one-shot compile peak is too large; a per-call ` native: ` option
41+ always wins over the app-env default.
42+
43+ ` native_fallback: :raise ` fails instead — the conformance suites use
44+ this to prove a model lowers fully native.
45+
46+ End-to-end: DistilBERT (question answering with ` Nx.Serving ` ), ViT,
47+ Whisper (` speech_to_text ` end-to-end including the featurizer STFT,
48+ encoder/decoder, and autoregressive decode loop), and Bumblebee
49+ ` Text.generation ` (greedy * and* multinomial sampling) all compile
50+ fully native under ` native_fallback: :raise ` . Bumblebee generation
51+ on Qwen3-0.6B measures ** ~ 5× the evaluator's decode throughput**
52+ (~ 61 vs ~ 12 tok/s on an M-series Mac), with byte-identical
53+ completions. Native training drives Axon end-to-end — a LeNet CNN
54+ and a dense MLP train on real MNIST entirely through the single-NIF
55+ path (forward, categorical-cross-entropy, backward, Adam) to the
56+ same >97% / >96% accuracy as the evaluator.
57+
58+ - ** ` Emily.Compiler ` — ` :fuse ` opt-in.** Adds ` mx::compile ` fusion on
59+ top of the replay, fusing elementwise runs (RMSNorm, softmax, SiLU
60+ gating, residual adds) the plain replay leaves as separate kernels.
61+ For a ` defn while ` , the loop body is fused under ` mx::compile ` and
62+ cached per stream so it cache-hits across iterations rather than
63+ recompiling per step. Enable on top of the native generation path:
64+
65+ Nx.Defn.jit(&forward/1,
66+ compiler: Emily.Compiler, native: true, fuse: true)
67+
68+ On Qwen3-0.6B this lifts greedy decode to ** ~ 5.4× the evaluator
69+ (~ 1.1× over the plain native lane)** , ~ 68 vs ~ 62 tok/s; in
70+ isolation on a decode-shaped transformer block, fusion measures
71+ ~ 1.5–1.6× over the plain replay. Trade-off: ` mx::compile `
72+ reassociates f32 to within a few ULP, so output is ** not**
73+ bit-identical to the evaluator. Greedy argmax is robust to that
74+ empirically (Qwen3-0.6B token ids matched the evaluator exactly in
75+ our run), but the match is empirical, not guaranteed — a near-tie
76+ top-2 logit can flip a token. ** Sampling strategies will diverge
77+ from the evaluator under fusion** even with a fixed seed.
78+
79+ - ** ` Emily.Generation ` — a model-agnostic decode-loop driver.**
80+ JIT-compiles a caller-supplied shape-stable per-token forward
81+ (` fn token, offset, cache, params -> {logits, cache} end ` ) with the
82+ native single-NIF compiler and drives the autoregressive loop from
83+ Elixir — offset bookkeeping, KV-cache threading, stop conditions,
84+ next-token selection (greedy by default), and per-token streaming
85+ via ` :on_token ` . The forward runs fully native; the loop stays in
86+ Elixir, so token streaming and host-side control are preserved.
87+ Emily supplies only the mechanism — the model (forward + cache) is
88+ the caller's.
89+
90+ - ` Emily.async_eval/1 ` (and ` Emily.Native.async_eval/2 ` ) schedule
91+ evaluation of one or more lazy graphs ** without blocking on the
92+ GPU** , wrapping ` mlx::core::async_eval ` . The work is handed to the
93+ device's command queue and the call returns as soon as it is
94+ enqueued — not when it finishes. Lets a caller keep dispatching the
95+ next step's ops while the device computes the current one (e.g. an
96+ autoregressive decode loop), blocking only when a value is actually
97+ read back on the host via ` to_binary/1 ` / ` eval/1 ` . Pass every
98+ output of a step (logits plus all KV-cache buffers) in one call.
99+
100+ - ` Emily.Native.fast_rope_int/8 ` — RoPE with an ** integer**
101+ absolute-position ` offset ` (routing to MLX's int-offset ` rope `
102+ overload), for incremental decode where the caller tracks position
103+ host-side. Complements the existing tensor-offset ` fast_rope/8 ` .
104+ Note: feed the kernel the 4-D ` {batch, heads, seq, head_dim} `
105+ layout — in 3-D, MLX 0.31 mis-rotates single-token (` seq == 1 ` )
106+ inputs.
107+
108+ ### Fixed
109+
110+ - ** Dilated window reductions (` window_dilations > 1 ` ) returned wrong
111+ values.** ` window_sum ` /` window_max ` /` window_min ` /` window_product `
112+ with a dilated kernel silently produced garbage for windows past the
113+ first stride positions, on both the eager backend and the native
114+ compiler (they share the window-reduce core). A dilated kernel axis
115+ gets an ` as_strided ` stride > 1, so the sliding-window view aliases
116+ fewer physical elements than its logical size; MLX's strided-reduce
117+ fast path then read past the aliased buffer. The view is now
118+ materialised contiguously before the reduce when any dilation > 1
119+ (the common non-dilated pooling path is unchanged and stays
120+ copy-free).
121+
10122## 0.6.1 - 2026-05-31
11123
12124### Changed
0 commit comments