Fifth machine. Two additions: - M3 Air (10c) vs M5 (10c): same GPU width, 2 generations apart -> M5 is 7.7x at seq 4096. Isolates architecture from width (the Ultras can't). head_dim-56 fallback reproduces here too (padding = 2.6x), so it now holds on every Apple GPU generation shipping today. - Fanless sustained load: NO meaningful throttle over 5 min (1.01x at 300s). We predicted a decay curve and found none — reporting the negative, since it validates that the burst numbers aren't short-run artifacts.
11 KiB
| title | date | device | script |
|---|---|---|---|
| M-series fleet ablation: M1 / M3 / M4 / M5 across 5 machines (Ultra → fanless Air) | 2026-07-16 | M1 Ultra 128GB · M3 Ultra 256GB · M3 Air 16GB (fanless) · M4 Pro mini 24GB · M5 MacBook Pro 16GB | scripts/bench_optimizations.py (unmodified) + engine-level tiled×compile extension |
M-series Fleet Ablation Benchmarks
Same methodology as the 2026-03-09 wave2 doc (--sweep ablation --resolution 512 1024 2048,
checkpoint weights, 3 warmup + 10 bench runs), executed across four machines spanning the
Apple Silicon range — M1 Ultra (64c) · M3 Ultra (80c) · M4 Pro mini 24GB · M5 MacBook Pro
16GB (10c) — i.e. oldest→newest GPU generation and Studio→laptop. Contributed from a
multi-Mac render-farm setup; happy to run follow-ups on any of them.
Headline findings
-
sdpais a ~4.5× regression on M1-class GPUs at 2048 — and free on M3. Single-toggle isolation at 2048 (median of 6, all-off baseline):toggle M1 Ultra M3 Ultra all off 4139 ms 1743 ms bf16 only 4120 ms (neutral!) 1766 ms sdpa only 18592 ms (4.5× slower) 1763 ms bf16+sdpa 6388 ms 1767 ms The suspected M1 danger (bf16 emulation — M1 lacks hardware bf16) is innocent on this workload.
Root cause (follow-up, same day): it is not the SDPA kernel. Instrumenting a real 2048 forward shows the model's attention shapes are
head_dim=56(global blocks(1,8,16384,56)×15,(1,16,4096,56)×3; windowed(4096,·,·,56)). At head_dim 56,mx.fast.scaled_dot_product_attentionsilently falls back to the unfused path — fused == unfused within 1 % on every shape on both M1 and M3 Ultra. Souse_sdpa=Truecurrently buys zero kernel benefit on any machine, while its branch pays extra 5-D transpose/reshape choreography — which is what costs ~4.5× on M1 (layout-sensitive), ~free on M3.The actionable win: pad head_dim 56 → 64. The fast path then engages and beats unfused decisively — despite ~14 % more FLOPs (fp32, exact model shapes):
shape (hdim 64) M3 fused/unfused M1 fused/unfused (1, 8, 16384, ·) 28.6 / 63.1 ms → 2.2× 53.8 / 144.5 ms → 2.7× (1, 16, 4096, ·) 3.8 / 7.2 ms → 1.9× 7.5 / 11.1 ms → 1.5× (4096, 2, 64, ·) 1.3 / 2.5 ms → 1.9× 1.5 / 2.6 ms → 1.7× …and on M5 it's worth ~5×, i.e. this fix matters MOST for laptop users. Same experiment on an M5 MacBook Pro (10-core GPU, 16 GB) — hdim 56 vs 64, fp32:
shape hdim 56 (today) hdim 64 (padded) speedup from padding (1, 16, 4096) 44.00 ms (fallback: 1.05× vs unfused) 8.91 ms (fast path: 0.21×) 4.9× (1, 16, 2048) 11.44 ms (1.06×) 2.23 ms (0.22×) 5.1× (4096, 2, 64) 16.72 ms (0.97×) 5.29 ms (0.25×) 3.2× Why bigger on the laptop: the fused kernel's advantage over unfused scales inversely with GPU width. At seq 4096 fp32 (hdim 64) fused-vs-unfused is 7.1× on M5 vs ~1.9× on M3 Ultra — a 10-core GPU cannot brute-force the materialized N×N path the way an 80-core Ultra can. So the head_dim-56 fallback costs Studio owners ~2× and MacBook owners ~5×. Padding is not a big-iron optimization; it's a laptop one.
Suggested changes: (a) pad qkv projections to head_dim 64 (at minimum for the global blocks) so sdpa's fast kernel actually engages — M1 benefits more than M3; (b) until then, default
use_sdpa=False(it is currently overhead-only). For MLX upstream: a warning (or doc note) when sdpa silently falls back on unsupported head dims would have made this obvious much sooner. -
stage_gcis harmful on Ultra-class machines at every resolution — 0.53× at 512, 0.64× at 1024, 0.66× at 2048 on M3 Ultra, for a 1–5 % peak-memory saving. Wave2 measured a mild 0.79–0.99× on its reference hardware; on big-memory machines it's pure overhead. Suggestion: document as a low-memory-only flag. -
Tiled + compile is the best 2048 config on both Ultras — upstream
engine.pyforcedcompile=Falsein tiled mode; tiles are fixed-shape, so fused compilation applies (patched in this fork, output bit-identical, verified against ground-truth alpha):engine config (2048 input) M3 Ultra M1 Ultra peak alpha MAE* full-frame 2048 2788 ms 5373 ms 27.9 GB 0.00906 tiled 512/64 2760 ms 4576 ms 2.2 GB 0.00821 tiled 512/64 + compile 2478 ms 4222 ms 2.3 GB 0.00821 tiled 768/64 + compile 1949 ms 3275 ms 2.4 GB 0.00842 tiled 1024/64 + compile 3475 ms 5864 ms 3.7 GB 0.00915 *alpha MAE vs exact ground truth: synthetic 2048² green-screen plates (soft-alpha subject + motion-blur stripes + defocus disk composited over chroma green), hint = 8× downscaled truth. Tiled beats full-frame on accuracy as well as memory — the model runs at native tile scale over full-res input. Confirms wave2's tiled-768 pick and adds ~8–13 % from compiling the tile graph.
-
24 GB Macs must tile at 2048. On the M4 Pro mini (24 GB), every full-frame 2048 config lands at 20–24 s/run — the ~26 GB working set swaps; toggle choice becomes noise. At ≤1024 the M4 is healthy (218 ms @512, 1149 ms @1024). Tiled 2048 runs in ~2.3 GB.
Cross-machine baselines (all-off)
| res | M3 Ultra | M1 Ultra | M4 Pro 24GB | wave2 reference |
|---|---|---|---|---|
| 512 | 53.3 ms | 80.8 ms | 218.1 ms | 119.6 ms |
| 1024 | 247.0 ms | 350.6 ms | 1149.2 ms | 610.7 ms |
| 2048 | 1750.2 ms | 3527.3 ms | 23147 ms (swap) | 4984.7 ms |
Raw SDPA kernel scaling (pure MLX, no CorridorKey) — fused vs unfused, hdim 64, fp32
Useful context for why the head-dim fix matters differently per machine. Fused-vs-unfused ratio (lower = fused is winning by more):
| seq | M3 Ultra (80c) | M1 Ultra (64c) | M5 (10c laptop) |
|---|---|---|---|
| 1024 | 0.46× | 0.54× | 0.28× |
| 2048 | 0.53× | 0.71× | 0.21× |
| 4096 | 0.53× | 0.59× | 0.14× |
Absolute fused times at 4096/fp32: M3 3.86 ms · M1 7.19 ms · M5 7.22 ms — an M5 laptop matches an M1 Ultra on fused attention while being ~4× slower unfused (51.5 ms vs 12.2 ms). The narrower the GPU, the more the fast path is worth. (M5 numbers taken on a machine in active desktop use; treat as indicative, not lab-clean.)
Generational delta at matched GPU width — M3 Air vs M5, both 10-core
The Ultras confound generation with width. These two don't: both are 10-core laptop GPUs, two generations apart (fused SDPA, hdim 64, fp32):
| seq | M3 Air (10c) | M5 (10c) | M5 speedup |
|---|---|---|---|
| 1024 | 2.44 ms | 0.80 ms | 3.1× |
| 2048 | 17.51 ms | 2.32 ms | 7.5× |
| 4096 | 55.77 ms | 7.22 ms | 7.7× |
~7.7× at identical core count — i.e. the M5's gain is architectural, not width. (Apple's own published MLX figures claim ~3.8× M4→M5 on FLUX image-gen; this attention workload shows more.) The head_dim-56 fallback reproduces on the M3 Air too — padding to 64 gives 2.6× (138.91 → 54.26 ms at (1,16,4096)), so the finding now holds on M1, M3 Ultra, M3 Air, M4 Pro and M5 — every generation Apple currently ships.
Fanless sustained load: the M3 Air does not throttle on this workload (negative result)
Every number above is a burst measurement, so we checked whether a fanless chassis invalidates
them. Sustained SDPA (1,16,2048,64) fp32 on the M3 Air (MacBook Air, no fan), per-30s medians
over 5 minutes:
| elapsed | 30s | 60s | 150s | 240s | 300s |
|---|---|---|---|---|---|
| median | 13.85 ms | 14.91 ms | 14.55 ms | 14.43 ms | 14.04 ms |
| vs first | 1.00× | 1.08× | 1.05× | 1.04× | 1.01× |
No meaningful throttle — ~4 % wobble, ending where it started. We expected a decay curve and did not find one; the burst numbers in this report are therefore not flattered by short runs. Caveat: attention at this shape may not be power-dense enough to reach the thermal ceiling — a full multi-minute diffusion pipeline could still behave differently.
Full ablation tables
M3 Ultra 256GB
| Config | 512 | 1024 | 2048 | peak @2048 |
|---|---|---|---|---|
| baseline | 53.3 | 247.0 | 1750.2 | 26689 MB |
| slim+sdpa+bf16+fused_decode+gpu_preprocess | 53.5 | 248.2 | 1790.1 | 27245 MB |
| slim+stage_gc+bf16+fused_decode+gpu_preprocess | 95.4 | 382.7 | 2644.3 | 26689 MB |
| slim+stage_gc+sdpa+fused_decode+gpu_preprocess | 96.3 | 385.9 | 2676.9 | 26661 MB |
| slim+stage_gc+sdpa+bf16+gpu_preprocess | 99.4 | 372.1 | 2618.6 | 26661 MB |
| slim+stage_gc+sdpa+bf16+fused_decode | 99.0 | 386.5 | 2688.0 | 26661 MB |
| stage_gc+sdpa+bf16+fused_decode+gpu_preprocess | 100.1 | 391.4 | 2628.2 | 26661 MB |
| slim+stage_gc+sdpa+bf16+fused_decode+gpu_preprocess | 101.0 | 390.9 | 2617.9 | 26661 MB |
M1 Ultra 128GB
| Config | 512 | 1024 | 2048 | peak @2048 |
|---|---|---|---|---|
| baseline | 80.8 | 350.6 | 3527.3 | 26689 MB |
| slim+sdpa+bf16+fused_decode+gpu_preprocess | 79.1 | 353.3 | 33137.8 | 27245 MB |
| slim+stage_gc+bf16+fused_decode+gpu_preprocess | 189.5 | 705.5 | 4919.0 | 26689 MB |
| slim+stage_gc+sdpa+fused_decode+gpu_preprocess | 195.6 | 711.1 | 4939.2 | 26661 MB |
| slim+stage_gc+sdpa+bf16+gpu_preprocess | 198.2 | 681.6 | 4853.8 | 26661 MB |
| slim+stage_gc+sdpa+bf16+fused_decode | 197.7 | 714.1 | 4822.9 | 26661 MB |
| stage_gc+sdpa+bf16+fused_decode+gpu_preprocess | 194.2 | 733.2 | 6274.1 | 26661 MB |
| slim+stage_gc+sdpa+bf16+fused_decode+gpu_preprocess | 193.8 | 737.1 | 5128.2 | 26661 MB |
Note the 33.1 s outlier: the only 2048 config without stage_gc but with sdpa —
sdpa's slow path dominating once nothing throttles it (see headline 1 for the isolation).
The sdpa cliff appears only at 2048; 512/1024 are unaffected.
M4 Pro Mac mini 24GB
| Config | 512 | 1024 | 2048 (swap-bound) |
|---|---|---|---|
| baseline | 218.1 | 1149.2 | 23147.1 |
| slim+sdpa+bf16+fused_decode+gpu_preprocess | 221.4 | 1139.0 | 20994.5 |
| slim+stage_gc+… (all stage_gc combos) | 266–273 | 1268–1292 | 20233–24253 |
At 2048 the ~26 GB working set exceeds 24 GB unified memory; all configs swap and differences are not meaningful. ≤1024 full-frame or tiled-anything is the usable envelope.
Recommended settings by hardware
| Hardware | 2048 recommendation |
|---|---|
| M3-class (Max/Ultra) | full-frame or tiled 768/64 + compile; every toggle optional; avoid stage_gc |
| M1/M2-class | avoid sdpa; tiled 768/64 + compile; avoid stage_gc |
| ≤24–32 GB any gen | tiled required (2.3 GB vs 26 GB); tiled 768/64 + compile |
Environment: MLX (venv per uv sync --extra mlx), macOS 26.5, checkpoint v1.0.0. Quality
harness (ground-truth plates + scoring) available on request — it's ~150 lines and
reproduces the alpha-MAE column.