33This report compares Emily against two Nx backend baselines:
44
55* ** EXLA** : XLA host/CPU backend on Apple Silicon.
6- * ** EMLX** : older MLX-backed Nx backend on the Metal GPU.
6+ * ** EMLX** : MLX-backed Nx backend on the Metal GPU.
77* ** Emily** : local MLX/Metal backend, reported across eager, native, and fuse
88 lanes. For conclusions, use the best Emily lane for the workload.
99
@@ -21,7 +21,7 @@ Those lanes provide two complementary baselines:
2121| Baseline | Question answered |
2222| --- | --- |
2323| EXLA CPU | Is MLX/Metal GPU faster than XLA host CPU for this workload? |
24- | EMLX GPU | Is Emily's compiler/runtime faster than the older MLX-backed Nx stack? |
24+ | EMLX GPU | Is Emily's compiler/runtime faster than the other MLX-backed Nx stack? |
2525
2626The focused Qwen3-4B script includes an EXLA lane for explicit experiments, but
2727its default run is GPU-only. On this 24 GB M4 Pro, Qwen3-4B bf16 on EXLA-CPU was
@@ -36,33 +36,50 @@ Run on an Apple M4 Pro MacBook Pro with 24 GB RAM.
3636| --- | --- |
3737| Elixir / OTP | 1.19.5 / 28 |
3838| Nx | 0.12.1 |
39- | Emily | 0.7 .0 local checkout |
40- | EMLX | 0.3 .1, Metal GPU |
39+ | Emily | 1.0 .0 local checkout |
40+ | EMLX | 0.4 .1, Metal GPU |
4141| EXLA | 0.12.0, host CPU client |
4242
43+ ## Changes since the emlx 0.3.1 run (2026-06-13)
44+
45+ EMLX 0.4 is a major performance upgrade over 0.3.1, and it reshapes the
46+ Emily-vs-EMLX story. On the tiers EMLX completes, its times roughly halved
47+ (DistilBERT 19.19 ms -> 9.97 ms) or better (Qwen3-0.6B decode 11.42 -> 53.81
48+ tok/s, Qwen3-4B 7.33 -> 20.28 tok/s). The EXLA and Emily lanes moved only a few
49+ percent between runs, so the delta is the emlx upgrade itself, not machine
50+ drift.
51+
52+ Concretely, best-Emily-vs-EMLX went from 2.72x to 1.44x on DistilBERT, from
53+ 5.82x to 1.26x on Qwen3-0.6B decode, and from 3.20x to 1.17x on the Qwen3-4B
54+ addendum. EMLX 0.4.1 still does not complete the ViT-base or Whisper-tiny
55+ tiers in this harness.
56+
4357## Executive summary
4458
4559Emily's best lane wins the main model tiers that are GPU-friendly:
4660
4761| Tier | EXLA | EMLX | Best Emily | Best lane | vs EXLA | vs EMLX |
4862| --- | ---: | ---: | ---: | --- | ---: | ---: |
49- | DistilBERT QA | 8.99 ms | 19.19 ms | 7.06 ms | native | 1.27x faster | 2.72x faster |
50- | Qwen3-0.6B decode | 39.84 tok/s | 11.42 tok/s | 66.42 tok/s | fuse | 1.67x faster | 5.82x faster |
51- | ViT-base image classification | 56.19 ms | ERR | 23.93 ms | native | 2.35x faster | n/a |
52- | Whisper-tiny transcription | 88.49 ms | ERR | 961.68 ms | native | 10.9x slower | n/a |
63+ | DistilBERT QA | 8.89 ms | 9.97 ms | 6.94 ms | fuse | 1.28x faster | 1.44x faster |
64+ | Qwen3-0.6B decode | 40.15 tok/s | 53.81 tok/s | 67.57 tok/s | fuse | 1.68x faster | 1.26x faster |
65+ | ViT-base image classification | 55.88 ms | ERR | 21.79 ms | fuse | 2.56x faster | n/a |
66+ | Whisper-tiny transcription | 87.83 ms | ERR | 928.35 ms | native | 10.6x slower | n/a |
5367
54- The Qwen3-4B addendum shows the qualitative Emily-vs-EMLX difference clearly on
55- the largest practical Bumblebee model for this machine:
68+ The Qwen3-4B addendum on the largest practical Bumblebee model for this
69+ machine:
5670
5771| Lane | Qwen3-4B tok/s | vs EMLX |
5872| --- | ---: | ---: |
59- | EMLX | 7.33 | 1.00x |
60- | Emily eager | 8.03 | 1.10x |
61- | Emily native | 22.27 | 3.04x |
62- | Emily fuse | 23.46 | 3.20x |
73+ | EMLX | 20.28 | 1.00x |
74+ | Emily eager | 7.88 | 0.39x |
75+ | Emily native | 22.63 | 1.12x |
76+ | Emily fuse | 23.78 | 1.17x |
6377
64- So the headline is: ** Emily's compiler path is the differentiator.** Eager is
65- roughly EMLX-like on Qwen3-4B; native/fuse are about 3.2x faster.
78+ The headline is still: ** Emily's compiler path is the differentiator** — but
79+ the shape has changed with emlx 0.4. EMLX's own compiled lane now decisively
80+ beats op-by-op execution (including Emily's eager lane) on decode, so the
81+ contest is compiler-vs-compiler, and Emily native/fuse hold a consistent
82+ 1.2-1.4x lead on every tier both stacks complete.
6683
6784## Visual summary
6885
@@ -71,31 +88,31 @@ xychart-beta
7188 title "Qwen3-0.6B decode throughput"
7289 x-axis ["EXLA", "EMLX", "Eager", "Native", "Fuse"]
7390 y-axis "tokens/sec" 0 --> 70
74- bar [39.84, 11.42, 12.51, 59.96, 66.42 ]
91+ bar [40.15, 53.81, 11.96, 61.70, 67.57 ]
7592```
7693
7794``` mermaid
7895xychart-beta
7996 title "Qwen3-4B decode throughput"
8097 x-axis ["EMLX", "Eager", "Native", "Fuse"]
8198 y-axis "tokens/sec" 0 --> 25
82- bar [7.33, 8.03 , 22.27 , 23.46 ]
99+ bar [20.28, 7.88 , 22.63 , 23.78 ]
83100```
84101
85102``` mermaid
86103xychart-beta
87104 title "Best Emily lane vs EXLA"
88105 x-axis ["DistilBERT", "Qwen3", "ViT", "Whisper"]
89106 y-axis "speedup x" 0 --> 3
90- bar [1.27 , 1.67 , 2.35 , 0.09]
107+ bar [1.28 , 1.68 , 2.56 , 0.09]
91108```
92109
93110``` mermaid
94111xychart-beta
95112 title "Matmul crossover: best Emily / EXLA"
96113 x-axis ["128", "512", "1024", "2048"]
97114 y-axis "ratio (lower is faster)" 0 --> 2
98- line [1.80 , 1.12 , 0.31 , 0.20]
115+ line [1.87 , 1.20 , 0.32 , 0.20]
99116```
100117
101118## Tier 1 - op microbenchmarks
@@ -108,98 +125,106 @@ Examples from the fresh run:
108125
109126| Op | Size | Winner | Signal |
110127| --- | ---: | --- | --- |
111- | add | 256 | EXLA | best Emily is 2.28x slower than EXLA |
112- | add | 4096 | Emily | best Emily is 2.0x faster than EXLA |
113- | exp | 4096 | Emily | best Emily is 3.2x faster than EXLA |
114- | softmax | 4096 | Emily fuse | best Emily is 2.4x faster than EXLA and 1.29x faster than EMLX |
128+ | add | 256 | EXLA | best Emily is 2.08x slower than EXLA |
129+ | add | 4096 | Emily | best Emily is 2.2x faster than EXLA |
130+ | exp | 4096 | Emily | best Emily is 3.3x faster than EXLA |
131+ | softmax | 4096 | Emily/EMLX tie | both MLX lanes are ~ 2.6x faster than EXLA |
115132| matmul | 2048 | Emily/EMLX tie | both MLX lanes are about 5x faster than EXLA |
116133
117134Against EMLX, Emily's best op lane is usually close: sometimes a little faster,
118- sometimes a little slower. The bigger EMLX-vs-Emily separation appears in traced
119- model execution, especially Qwen decode, where Emily native/fuse avoid the
120- op-by-op execution shape .
135+ sometimes a little slower. Per-op parity is expected — both stacks dispatch the
136+ same MLX kernels — so the EMLX-vs- Emily separation appears in traced model
137+ execution, where compiler and dispatch strategy differ .
121138
122139## Tier 2 - DistilBERT QA
123140
124- DistilBERT is a clean three-way win for Emily native:
141+ DistilBERT is a three-way win for Emily native/fuse :
125142
126143| Lane | ms/call |
127144| --- | ---: |
128- | EXLA CPU | 8.99 |
129- | EMLX GPU | 19.19 |
130- | Emily eager | 16.45 |
131- | Emily native | 7.06 |
132- | Emily fuse | 8.50 |
145+ | EXLA CPU | 8.89 |
146+ | EMLX GPU | 9.97 |
147+ | Emily eager | 14.82 |
148+ | Emily native | 7.02 |
149+ | Emily fuse | 6.94 |
133150
134- Native is the right Emily option here. Fuse is not universally better; it helps
135- most when a compiled body is reused, such as decode loops.
151+ Native and fuse are within a few percent of each other here; either is the
152+ right Emily option for a single-forward workload. The EMLX lane roughly halved
153+ its 0.3.1 time but showed high run-to-run variance in this run (11.7 / 12.4 /
154+ 5.8 ms), so treat its mean with some caution.
136155
137156## Tier 3 - Qwen3-0.6B decode
138157
139158Qwen3-0.6B is the canonical completed three-way generation benchmark:
140159
141160| Lane | tok/s |
142161| --- | ---: |
143- | EXLA CPU | 39.84 |
144- | EMLX GPU | 11.42 |
145- | Emily eager | 12.51 |
146- | Emily native | 59.96 |
147- | Emily fuse | 66.42 |
162+ | EXLA CPU | 40.15 |
163+ | EMLX GPU | 53.81 |
164+ | Emily eager | 11.96 |
165+ | Emily native | 61.70 |
166+ | Emily fuse | 67.57 |
148167
149- This is the clearest main-suite result. Emily eager is near EMLX, while
150- native/fuse jump far ahead. Fuse is the best choice for decode loops.
168+ The 0.4 emlx lane now comfortably beats both EXLA-CPU and Emily's eager lane —
169+ op-by-op decode is simply not competitive from either stack. Emily native/fuse
170+ stay ahead at 1.15-1.26x over EMLX. Fuse is the best choice for decode loops.
151171
152172## Tier 4 - ViT-base image classification
153173
154- ViT-base strongly favors Emily native :
174+ ViT-base strongly favors Emily:
155175
156176| Lane | ms/call |
157177| --- | ---: |
158- | EXLA CPU | 56.19 |
178+ | EXLA CPU | 55.88 |
159179| EMLX GPU | ERR |
160- | Emily eager | 38.78 |
161- | Emily native | 23.93 |
162- | Emily fuse | 27.50 |
180+ | Emily eager | 35.58 |
181+ | Emily native | 23.96 |
182+ | Emily fuse | 21.79 |
163183
164184This tier is GPU-friendly: larger matrix multiplies and enough work per forward
165- for the GPU path to dominate. The EMLX lane did not complete in this harness, so
166- the meaningful comparison here is Emily vs EXLA.
185+ for the GPU path to dominate. The EMLX lane did not complete in this harness
186+ (on 0.3.1 or 0.4.1), so the meaningful comparison here is Emily vs EXLA.
167187
168188## Tier 5 - Whisper-tiny transcription
169189
170190Whisper-tiny remains Emily's bad case:
171191
172192| Lane | ms/call |
173193| --- | ---: |
174- | EXLA CPU | 88.49 |
194+ | EXLA CPU | 87.83 |
175195| EMLX GPU | ERR |
176- | Emily eager | 1815.80 |
177- | Emily native | 961.68 |
178- | Emily fuse | 981.89 |
196+ | Emily eager | 1920.57 |
197+ | Emily native | 928.35 |
198+ | Emily fuse | 955.36 |
179199
180200This is not a coverage win for EXLA; the Emily lanes reported zero fallbacks in
181201the live run. It is a workload-shape problem: Whisper-tiny is made of many small
182202kernels where CPU launch overhead and cache locality beat GPU dispatch. Native
183203cuts eager roughly in half, but still cannot remove the underlying small-kernel
184- cost. Fuse does not help this workload.
204+ cost. Fuse does not help this workload. The EMLX lane did not complete this
205+ tier on 0.3.1 or 0.4.1.
185206
186207## Qwen3-4B addendum
187208
188- The focused Qwen3-4B script was rerun after the three-way reorganization. Its
189- safe default remains GPU-only:
209+ The focused Qwen3-4B script's safe default remains GPU-only:
190210
191211``` sh
192212elixir bench/qwen3_4b_emily_vs_emlx.exs
193213```
194214
195- Fresh result:
215+ Fresh result (emlx 0.4.1) :
196216
197217| Lane | mean tok/s | min | max | vs EMLX |
198218| --- | ---: | ---: | ---: | ---: |
199- | EMLX | 7.33 | 7.31 | 7.37 | 1.00x |
200- | Emily eager | 8.03 | 7.98 | 8.08 | 1.10x |
201- | Emily native | 22.27 | 22.16 | 22.32 | 3.04x |
202- | Emily fuse | 23.46 | 23.35 | 23.58 | 3.20x |
219+ | EMLX | 20.28 | 20.07 | 20.44 | 1.00x |
220+ | Emily eager | 7.88 | 7.81 | 7.97 | 0.39x |
221+ | Emily native | 22.63 | 22.61 | 22.64 | 1.12x |
222+ | Emily fuse | 23.78 | 23.55 | 23.97 | 1.17x |
223+
224+ All four lanes produced the identical greedy completion, so the throughput
225+ comparison is on the same decode. Note the Emily lanes are essentially
226+ unchanged from the 0.3.1-era run (native 22.27 -> 22.63, fuse 23.46 -> 23.78)
227+ while EMLX jumped 7.33 -> 20.28 tok/s.
203228
204229An explicit EXLA smoke attempt on Qwen3-4B:
205230
@@ -218,9 +243,13 @@ used as the canonical three-way comparison on this 24 GB machine.
218243Use ` emily-fuse ` for autoregressive decode loops. It is best on Qwen3-0.6B and
219244Qwen3-4B because the loop body is reused.
220245
221- Use ` emily-native ` as the default best Emily lane for single-forward model
222- benchmarks. It wins DistilBERT and ViT here; fuse can be neutral or slower when
223- there is no repeated body to amortize.
246+ Use ` emily-native ` or ` emily-fuse ` for single-forward model workloads — they
247+ are within a few percent of each other on DistilBERT and ViT. Fuse's edge grows
248+ when a compiled body is reused, as in decode.
249+
250+ Avoid op-by-op execution for decode from either stack: Emily eager and
251+ pre-compiler EMLX both sit near 12 tok/s on Qwen3-0.6B where the compiled lanes
252+ reach 54-68 tok/s.
224253
225254Keep EXLA out of the default Qwen3-4B run on 24 GB machines. The EXLA 4B smoke
226255was killed by the OS, while Qwen3-0.6B gives a completed three-way generation
0 commit comments