Skip to content

perf(moe): default-on MLXCEL_FUSED_MOE, validated on M5 (#282) - #285

Merged
inureyes merged 2 commits into
mainfrom
update/issue-282-fused-moe-default-on
Jun 14, 2026
Merged

inureyes merged 2 commits into
mainfrom
update/issue-282-fused-moe-default-on

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Flips MLXCEL_FUSED_MOE to on by default for single-token decode-MoE, after validating the fused kernels on M5 Max (Neural Accelerator) hardware for the first time. The #268 series (#276/#278/#279/#280/#281) shipped these kernels off-by-default and only ever ran them on M1 Ultra; this enables them by default once confirmed clean on M5. Closes #282.

The fused path stays an env override: MLXCEL_FUSED_MOE=0 (also false/off/no, case-insensitive) forces the proven gather_qmm / SwitchGLU path. This also fixes a latent bug. The previous gate was std::env::var("MLXCEL_FUSED_MOE").is_ok(), which is true whenever the variable is set, so MLXCEL_FUSED_MOE=0 enabled the kernel instead of disabling it. A new switch_layers::fused_moe_enabled() helper centralizes the decision (default-on; an explicit falsey value disables) and all four call sites route through it.

M5 validation (the core of this issue)

Apple M5 Max (Mac17,7, 128 GB, macOS 26.5.1; is_m5_neural_accelerator() returns true), release build with metal+accelerate, greedy temp-0, measured against the true gather_qmm baseline (env unset):

Model activation bits decode tok/s (off to on) gain greedy parity kernel dispatched
gemma-4-26b-a4b-it GeGLU 4 133.9 to 142.2 +6.2% within f16 jitter, coherent yes (30/tok)
qwen3.5-35b-a3b SwiGLU 4 153.6 to 161.3 +5.1% within f16 jitter, coherent yes (40/tok)
qwen3-30b-a3b SwiGLU 4 60.5 to 61.7 +1.9% byte-identical yes (48/tok)
dots.llm1 SwiGLU 4+6 14.1 to 14.0 ~par byte-identical yes (61/tok)
  • No NaN and no garbage on any model. The documented M5-Max mixed-dtype NaN risk (in the separate C++ fused_moe_forward) does not materialize in the new moe_gateup / moe_down / GeGLU kernels, which cast scores to the activation dtype and accumulate in f32.
  • No regression anywhere. Gains are smaller than M1 Ultra because the Neural Accelerator already accelerates the baseline expert matmul, leaving less inter-kernel idle for the fusion to recover.
  • Kernel execution on M5 was confirmed per layer with a temporary dispatch trace (reverted before this commit), not merely inferred from output, because byte-identical parity alone cannot distinguish a real run from a silent fallback.
  • M5 is clean with real gains, so the default is flipped for all hardware; no is_m5_neural_accelerator() gate is needed.

Notable findings surfaced during validation

  • MLXCEL_FUSED_MOE=0 never disabled the kernel (the .is_ok() bug above). Fixed here, which is exactly the off switch the issue asked to preserve.
  • qwen1.5-moe could not be validated: it loads as qwen2_moe, whose SparseMoeBlock was never wired with the fused dispatch, so it always falls back to gather_qmm (0 dispatches). Only four paths actually dispatch the kernel: qwen3_moe, qwen3_next, dots1, gemma4. mixtral, olmoe, minimax, phimoe, lfm2, and qwen3_vl_moe reuse SwitchGLU but are not wired. The doc's "Models covered" list overstated coverage and is corrected here. Wiring the remaining families is a separate follow-up.

Changes

  • src/models/switch_layers.rs: add fused_moe_enabled() (default-on; 0/false/off/no disables) plus a fused_moe_enabled_from unit test.
  • src/models/{qwen3_moe,qwen3_next,dots1,gemma4}.rs: route the decode-MoE gate through fused_moe_enabled() instead of std::env::var("MLXCEL_FUSED_MOE").is_ok().
  • docs/benchmark_results/fused-moe-decode-kernel-design.md: flags table now default-on, roadmap step 5 marked done, new M5 results section, corrected "Models covered".

Testing

  • M5 real-checkpoint validation on the four wired families (table above), all coherent with no NaN.
  • Behavioral flip check on gemma4: default (unset) = 142.5 tok/s and produces the fused output; MLXCEL_FUSED_MOE=0 = 133.4 tok/s and produces the gather_qmm baseline output; the two outputs differ as expected, confirming the flip and the fixed =0 semantics.
  • cargo test --release switch_layers, cargo fmt, cargo clippy --release --features metal,accelerate -- -D warnings.

Closes #282.

The #268 fused decode-MoE kernels shipped off-by-default and had only run
on M1 Ultra. Validated on M5 Max (Neural Accelerator, macOS 26.5) against
the true gather_qmm baseline: gemma4 +6.2%, qwen3.5 +5.1%, qwen3-30b +1.9%,
dots.llm1 ~parity; all coherent with no NaN and no regression. Per-layer
kernel execution on M5 confirmed via a dispatch trace. M5 is clean, so the
default flips for all hardware (no hardware gate).

Add switch_layers::fused_moe_enabled() (default-on; MLXCEL_FUSED_MOE=0, also
false/off/no, disables) and route the four wired call sites (qwen3_moe,
qwen3_next, dots1, gemma4) through it. This also fixes a latent bug: the old
std::env::var("MLXCEL_FUSED_MOE").is_ok() gate treated =0 as enabled.

Only those four paths dispatch the kernel; qwen1.5-moe (qwen2_moe), mixtral,
olmoe, minimax, phimoe, lfm2, and qwen3_vl_moe reuse SwitchGLU but are not
wired, so they stay on gather_qmm. Doc coverage list corrected. Wiring the
rest is a separate follow-up.

Closes #282.
@inureyes inureyes added type:performance Performance improvements priority:medium Medium priority area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) platform:macos macOS (Apple Silicon) specific labels Jun 14, 2026
Address PR #285 review (LOW items):

- Cache fused_moe_enabled() in a process-lifetime OnceLock<bool>, matching
  the codebase's hot-path env-gate convention (e.g. gemma4's
  mtp_divergent_fix_disabled). The pure fused_moe_enabled_from stays for
  tests. No behavior change for the CLI/server (each process reads the flag
  once at startup, as before).
- Refresh the four call-site comments (qwen3_moe, qwen3_next, dots1, gemma4):
  they said "when MLXCEL_FUSED_MOE is set" / "behind MLXCEL_FUSED_MOE", which
  read as opt-in now that the kernel is on by default.
- Document the MLXCEL_FUSED_MOE flag family (plus _SGY and _RELU2) in
  docs/environment-variables.md, which previously omitted them.

Refs #282.
@inureyes inureyes added the status:done Completed label Jun 14, 2026
@inureyes
inureyes merged commit b0bd58a into main Jun 14, 2026
5 checks passed
@inureyes
inureyes deleted the update/issue-282-fused-moe-default-on branch June 14, 2026 12:16
@inureyes inureyes added this to the 0.3 milestone Jun 21, 2026
inureyes added a commit that referenced this pull request Jun 21, 2026
* perf(moe): wire minimax to the fused decode-MoE kernel

Wire minimax's SparseMoeBlock to the shared forward_fused_kernel path
(#268 series, default-on since #285), following the one-branch pattern
from qwen2_moe / dots1. For single-token decode with MLXCEL_FUSED_MOE
enabled, the block now calls experts.forward_fused_kernel; for prefill
or when the env gate is off, it falls through to experts.forward +
moe_weighted_sum unchanged.

minimax experts are SwiGLU (SiLU), so the kernel runs with the default
swiglu act=0 template with no additional activation variant required.
Routing uses sigmoid with an e_score_correction_bias that biases expert
selection; the fused path receives the normalized unbiased scores, matching
exactly what moe_weighted_sum received on the non-fused path. The kernel
auto-falls-back for any unsupported quant config (non-affine, 3-bit, etc).

Also expose fused_moe_enabled_from as pub(crate) so that per-model test
files can assert the gating conditions without touching process-global env
state (the function is already documented as a testing seam).

Runtime validation is hardware-blocked: MiniMax-Text-01 at ~456B does not
fit 128 GB unified memory, so greedy temp-0 throughput and output-parity
measurements are deferred until a smaller variant or larger-memory machine
is available. The PR body documents this explicitly.

Changes:
- src/models/minimax.rs: add fused dispatch branch in SparseMoeBlock::forward
- src/models/switch_layers.rs: fused_moe_enabled_from fn -> pub(crate)
- src/models/minimax_tests.rs: new config + dispatch gate unit tests
- src/models/mod.rs: register minimax_tests module
- docs/benchmark_results/fused-moe-decode-kernel-design.md: update models covered

* docs(moe): record minimax-m2-3bit auto-fallback smoke on M1 Ultra

The bonus smoke loaded minimax-m2-3bit with the new dispatch wiring and
generated 16 coherent tokens at 17.6 tok/s on M1 Ultra. Because 3-bit quant
is not a supported fused-kernel input, forward_fused_kernel returned None and
decode fell back to gather_qmm cleanly (no crash, no OOM). This confirms the
wiring is non-breaking via the fallback path. Fused-path throughput and
output-parity validation remain blocked on a fitting 4-bit or 8-bit minimax
checkpoint.
@inureyes inureyes self-assigned this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata platform:macos macOS (Apple Silicon) specific priority:medium Medium priority status:done Completed type:performance Performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(moe): validate fused decode-MoE on M5, then enable MLXCEL_FUSED_MOE by default

1 participant