Repository navigation
perf(moe): default-on MLXCEL_FUSED_MOE, validated on M5 (#282) - #285
Merged
Merged
Conversation
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.
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.
This was referenced Jun 14, 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Flips
MLXCEL_FUSED_MOEto 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(alsofalse/off/no, case-insensitive) forces the provengather_qmm/SwitchGLUpath. This also fixes a latent bug. The previous gate wasstd::env::var("MLXCEL_FUSED_MOE").is_ok(), which is true whenever the variable is set, soMLXCEL_FUSED_MOE=0enabled the kernel instead of disabling it. A newswitch_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 truegather_qmmbaseline (env unset):fused_moe_forward) does not materialize in the newmoe_gateup/moe_down/ GeGLU kernels, which cast scores to the activation dtype and accumulate in f32.is_m5_neural_accelerator()gate is needed.Notable findings surfaced during validation
MLXCEL_FUSED_MOE=0never disabled the kernel (the.is_ok()bug above). Fixed here, which is exactly the off switch the issue asked to preserve.qwen2_moe, whoseSparseMoeBlockwas never wired with the fused dispatch, so it always falls back togather_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 reuseSwitchGLUbut 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: addfused_moe_enabled()(default-on;0/false/off/nodisables) plus afused_moe_enabled_fromunit test.src/models/{qwen3_moe,qwen3_next,dots1,gemma4}.rs: route the decode-MoE gate throughfused_moe_enabled()instead ofstd::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
MLXCEL_FUSED_MOE=0= 133.4 tok/s and produces thegather_qmmbaseline output; the two outputs differ as expected, confirming the flip and the fixed=0semantics.cargo test --release switch_layers,cargo fmt,cargo clippy --release --features metal,accelerate -- -D warnings.Closes #282.