Skip to content

docs(benchmarks): Gemma3n decode profile on M5 Max - #358

Merged
inureyes merged 1 commit into
mainfrom
docs/gemma3n-m5max-decode-profile
Jun 18, 2026
Merged

inureyes merged 1 commit into
mainfrom
docs/gemma3n-m5max-decode-profile

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Companion to the M1 Ultra Gemma3n decode profile from #329 (PR #345),
measured on Apple M5 Max (40-core GPU, 128 GB, macOS 26.5.1), current
HEAD, MLX 0.31.2, mlx-vlm 0.4.4. M5 Max is detected as Neural-Accelerator
hardware, so use_fused_decode_path() is false and decode runs the
per-op split path, a different code path than M1 Ultra. That is why the
profile is worth repeating here rather than assuming the M1 Ultra result
carries over.

The #329 conclusion (no compiled fusion is justified for Gemma3n decode)
holds on M5 Max, by a partly different route.

Findings (Apple M5 Max)

  • M5 Max runs these 4-bit checkpoints at 1.8 to 1.9x M1 Ultra: e2b-4bit
    161.5, e4b-4bit 112.2 (text) / 107.0 (image) tok/s, e4b-bf16 40.4.
  • Decode is 92 to 96% GPU-bound. The Rust graph-build step (forward,
    0.47 to 0.66 ms/token), where an FFI-collapsing fusion would land,
    stays hidden behind async_eval.
  • MLX_MAX_OPS_PER_BUFFER is flat on M5 Max (at most +1.7% across the
    sweep, within best-of-3 noise). The same sweep gains +11 to 13% on M1
    Ultra, so that lever must stay hardware-gated and there is no free win
    for a fusion to displace.
  • 4-bit decode sits at 46 to 59% of the 478 GB/s GEMV bandwidth wall, bf16
    at 76%. The 4-bit idle is diffuse dispatch/structure overhead, not a
    fusable kernel hotspot.
  • mlxcel leads mlx-vlm 1.14 to 1.17x (1.20 to 1.23x on M1 Ultra); no
    parity gap pulling for a kernel.

Stale comment corrected

src/models/gemma3n.rs recorded the #60 fused decode path as a -6.3%
regression on M5-class hardware. Re-measured on this M5 Max with MLX
0.31.2 (forced on/off through a temporary override, reverted after) it is
neutral: e4b-bf16 +0.84%, e4b-4bit +0.34%, both within noise, with no
bf16 JIT crash. The gate stays off NA hardware because the fused path has
no upside on M5 and the per-op split path is the validated default; only
the figure was out of date. Gating logic is unchanged.

What changed

  • Add docs/benchmark_results/gemma3n-decode-profile-m5max.md with the
    full numbers, pipeline split, bandwidth floor, command-buffer sweep,
    fused-vs-split re-measurement, text-vs-VLM, mlx-vlm parity, and
    reproduce commands.
  • Link it from docs/benchmarks.md.
  • Update the two stale fused-path comments in src/models/gemma3n.rs.

Test plan

  • Decode best-of-3 (caffeinate, cool) on gemma3n-e2b/e4b-4bit and
    e4b-bf16 via mlxcel-bench-decode, text and image.
  • Pipeline split via MLXCEL_PROFILE_PIPELINE_DETAIL; op histograms
    via MLXCEL_EXPORT_DECODE_DOT.
  • MLX_MAX_OPS_PER_BUFFER sweep {default,100,200,1000,2000} on all
    three checkpoints.
  • Fused-vs-split via a temporary MLXCEL_FORCE_*_DECODE override,
    reverted; tree clean and binary rebuilt from HEAD afterward.
  • mlx-vlm wall-clock parity (chat template both sides).
  • GEMV peak bandwidth measured at 478 GB/s (f16/bf16).
  • cargo fmt --check passes. Docs and comments only; no inference
    code path changed.

Companion to the M1 Ultra profile from #329 (PR #345), measured on Apple
M5 Max (40-core GPU, 128 GB, macOS 26.5.1), current HEAD, MLX 0.31.2,
mlx-vlm 0.4.4.

The no-fusion conclusion holds on M5 Max, by a partly different route:

- M5 Max runs these 4-bit checkpoints at 1.8 to 1.9x M1 Ultra (e2b-4bit
  161.5, e4b-4bit 112.2 text / 107.0 image tok/s).
- Decode is 92 to 96% GPU-bound; the Rust graph-build step where an
  FFI-collapsing fusion would land stays hidden behind async_eval.
- The MLX_MAX_OPS_PER_BUFFER lever that recovers +11 to 13% on M1 Ultra
  is flat on M5 Max (at most +1.7%, within noise), so the knob must stay
  hardware-gated and there is no free win for a fusion to chase.
- mlxcel still leads mlx-vlm 1.14 to 1.17x (the only Python runtime that
  loads these multimodal checkpoints).

Also correct a stale note in src/models/gemma3n.rs. The comment recorded
the #60 fused decode path as a -6.3% regression on M5-class hardware.
Re-measured on this M5 Max with MLX 0.31.2 it is neutral (e4b-bf16
+0.84%, e4b-4bit +0.34%, both within noise, no bf16 JIT crash). The gate
stays off NA hardware because the fused path has no upside on M5 and the
per-op split path is the validated default; only the figure was out of
date. Gating logic is unchanged.

Docs and comments only; no inference code path changed.
@inureyes inureyes added type:docs Documentation improvements or additions area:models Model architectures, weights, loading, metadata labels Jun 18, 2026
@inureyes
inureyes merged commit 7b7271b into main Jun 18, 2026
5 checks passed
@inureyes
inureyes deleted the docs/gemma3n-m5max-decode-profile branch June 18, 2026 14:54
inureyes added a commit that referenced this pull request Jun 18, 2026
On pre-M5 Apple Silicon (M1 through M4, Metal GPU family 3) the default command-buffer op cap is small enough that high-op-density decode stalls the GPU at buffer boundaries. Raising MLX_MAX_OPS_PER_BUFFER to 1000 closes that dispatch-gap idle and recovers Gemma3n decode throughput (#329/#345).

apply_metal_ops_per_buffer_default (hardware.rs) sets the variable to 1000 at process start on M1-M4 when it is unset, wired into the CLI, server, and bench-decode main() before any MLX op. M5+ (Neural Accelerator) and non-Apple (CUDA) are left on MLX's default: the same sweep is flat on M5 Max (#358) and an earlier M5 study found larger buffers slower there. An operator-set MLX_MAX_OPS_PER_BUFFER always wins, and the knob is documented in the CLI help.

M1 Ultra re-bench (best-of-3): gemma3n-e2b 82.7 to 92.5 (+11.9%), gemma3n-e4b 63.0 to 68.4 (+8.6%) versus the recorded default. Dense qwen3-8b and MoE qwen3-30b-a3b also run faster at 1000 than at a starved cap, so the generation-wide gate does not regress them; the MoE decode-gap sweep (#268) was flat. GB10 is not benched in this environment and is left on MLX's default.

Closes #353.
@inureyes inureyes added this to the 0.3 milestone Jun 21, 2026
@inureyes inureyes self-assigned this Aug 31, 2026
@inureyes inureyes added the status:done Completed label Sep 6, 2026
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
Add M5 Max companion to the M1 Ultra Gemma3n decode profile (lablup#329/lablup#345) and correct the stale fused-path regression note in gemma3n.rs to neutral. Docs and comments only; gating logic unchanged.
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
…lup#360)

On pre-M5 Apple Silicon (M1 through M4, Metal GPU family 3) the default command-buffer op cap is small enough that high-op-density decode stalls the GPU at buffer boundaries. Raising MLX_MAX_OPS_PER_BUFFER to 1000 closes that dispatch-gap idle and recovers Gemma3n decode throughput (lablup#329/lablup#345).

apply_metal_ops_per_buffer_default (hardware.rs) sets the variable to 1000 at process start on M1-M4 when it is unset, wired into the CLI, server, and bench-decode main() before any MLX op. M5+ (Neural Accelerator) and non-Apple (CUDA) are left on MLX's default: the same sweep is flat on M5 Max (lablup#358) and an earlier M5 study found larger buffers slower there. An operator-set MLX_MAX_OPS_PER_BUFFER always wins, and the knob is documented in the CLI help.

M1 Ultra re-bench (best-of-3): gemma3n-e2b 82.7 to 92.5 (+11.9%), gemma3n-e4b 63.0 to 68.4 (+8.6%) versus the recorded default. Dense qwen3-8b and MoE qwen3-30b-a3b also run faster at 1000 than at a starved cap, so the generation-wide gate does not regress them; the MoE decode-gap sweep (lablup#268) was flat. GB10 is not benched in this environment and is left on MLX's default.

Closes lablup#353.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:models Model architectures, weights, loading, metadata status:done Completed type:docs Documentation improvements or additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant