Repository navigation
perf(moe): wire qwen2_moe to the fused decode-MoE kernel - #308
Merged
Merged
Conversation
Dispatch the fused single-token decode-MoE Metal kernel (#268) from qwen2_moe's SparseMoeBlock, mirroring qwen3_moe / dots1. For single-token decode with MLXCEL_FUSED_MOE on (default), the routed-expert sum runs through experts.forward_fused_kernel; the gather_qmm SwitchGLU + moe_weighted_sum path remains the fallback for prefill, when the flag is off, and for any config the kernel does not support. The shared-expert (sigmoid-gated) path is unchanged. This is a Group-B migration: qwen2_moe defined its own local SwitchGLU / SwitchLinear that exposed neither forward_fused_kernel nor quantized_parts, so the issue's assumption that the experts already used the shared SwitchGLU was wrong. SparseMoeBlock.experts now uses crate::models::switch_layers::SwitchGLU and the local SwitchGLU / SwitchLinear (and their loaders) are removed; nothing else referenced them. The shared SwitchLinear loader was stacked-only (`{prefix}.weight`), but Qwen1.5-MoE / Qwen2-MoE checkpoints ship experts unstacked under `experts.{idx}.{proj}` while qwen3-moe ships them pre-stacked under `switch_mlp`. To preserve exactly which weights load, from_weights_with_mode gains a per-expert stacking fallback (stack_individual_experts) that triggers only when the pre-stacked tensor is absent, so it never changes behavior for already-loadable checkpoints and now also serves the other unstacked MoE families. The per-tensor bit-width inference is shared across both paths; for qwen1.5-moe's uniform 4-bit it yields the same bits the local loader hardcoded. Docs: move qwen2_moe to the wired list in the fused-MoE design doc. Tests: cover the per-expert stacking fallback and the missing-weight error in switch_layers. Real-checkpoint validation (mlx-community/Qwen1.5-MoE-A2.7B-Chat-4bit: dispatch count, temp-0 parity vs MLXCEL_FUSED_MOE=0, decode tok/s) is pending the orchestrator's verification step.
This was referenced Jun 16, 2026
be-free-snowboy
pushed a commit
to freestylejs/snowbrain-mlxcel
that referenced
this pull request
Oct 27, 2026
Dispatch the fused single-token decode-MoE Metal kernel (lablup#268) from qwen2_moe's SparseMoeBlock, mirroring qwen3_moe / dots1. For single-token decode with MLXCEL_FUSED_MOE on (default), the routed-expert sum runs through experts.forward_fused_kernel; the gather_qmm SwitchGLU + moe_weighted_sum path remains the fallback for prefill, when the flag is off, and for any config the kernel does not support. The shared-expert (sigmoid-gated) path is unchanged. This is a Group-B migration: qwen2_moe defined its own local SwitchGLU / SwitchLinear that exposed neither forward_fused_kernel nor quantized_parts, so the issue's assumption that the experts already used the shared SwitchGLU was wrong. SparseMoeBlock.experts now uses crate::models::switch_layers::SwitchGLU and the local SwitchGLU / SwitchLinear (and their loaders) are removed; nothing else referenced them. The shared SwitchLinear loader was stacked-only (`{prefix}.weight`), but Qwen1.5-MoE / Qwen2-MoE checkpoints ship experts unstacked under `experts.{idx}.{proj}` while qwen3-moe ships them pre-stacked under `switch_mlp`. To preserve exactly which weights load, from_weights_with_mode gains a per-expert stacking fallback (stack_individual_experts) that triggers only when the pre-stacked tensor is absent, so it never changes behavior for already-loadable checkpoints and now also serves the other unstacked MoE families. The per-tensor bit-width inference is shared across both paths; for qwen1.5-moe's uniform 4-bit it yields the same bits the local loader hardcoded. Docs: move qwen2_moe to the wired list in the fused-MoE design doc. Tests: cover the per-expert stacking fallback and the missing-weight error in switch_layers. Real-checkpoint validation (mlx-community/Qwen1.5-MoE-A2.7B-Chat-4bit: dispatch count, temp-0 parity vs MLXCEL_FUSED_MOE=0, decode tok/s) is pending the orchestrator's verification step.
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
Wire qwen2_moe (Qwen1.5-MoE / Qwen2-MoE) into the fused single-token decode-MoE Metal kernel (#268), mirroring qwen3_moe / dots1. For single-token decode with
MLXCEL_FUSED_MOEon (the default), the routed-expert weighted sum runs throughexperts.forward_fused_kernel; the provengather_qmmSwitchGLU+moe_weighted_sumpath stays as the fallback for prefill, when the flag is off, and for any config the kernel does not support. The shared-expert (sigmoid-gated) addition is unchanged.Group-B correction to the issue
The issue assumed qwen2_moe's experts already used the shared
SwitchGLUexposingforward_fused_kernel. That was wrong: qwen2_moe defined its own localSwitchGLUandSwitchLinearthat exposed neitherforward_fused_kernelnorquantized_parts, and the localSwitchLinear::Quantizedhad nomodefield. So this is a Group-B migration (the same class as #301/#302/#303): migrate qwen2_moe tocrate::models::switch_layers::SwitchGLU, then add the one-branch fused dispatch. The localSwitchGLU/SwitchLinearand their loaders are removed; a repo-wide grep confirmed nothing else referenced them.Weight-loading parity
The shared
SwitchLinearloader was stacked-only ({prefix}.weightholding all experts), but the two checkpoint conventions differ: qwen3-moe ships experts pre-stacked underswitch_mlp, while Qwen1.5-MoE / Qwen2-MoE ship them unstacked underexperts.{idx}.{proj}(verified against the localmodels/qwen1.5-moe-a2.7b-4bitcheckpoint, which is fully per-expert). The local qwen2_moe loader handled the per-expert layout via a stacking fallback; the shared loader did not, so loading qwen2_moe through it unchanged would have errored.To preserve exactly which weights load,
SwitchLinear::from_weights_with_modegains a per-expert stacking fallback (stack_individual_experts) that converts a{root}.switch_mlp.{proj}prefix to{root}.experts.{idx}.{proj}and stacks contiguously from index 0. It runs only when the pre-stacked tensor is absent, so it never changes behavior for an already-loadable checkpoint, and it now also serves the other unstacked MoE families slated for migration. The per-tensor bit-width inference is shared across both paths; for qwen1.5-moe's uniform 4-bit it yields the same bits the local loader hardcoded (args.bits()), so there is no silent change to loaded weights, only a strictly more robust path for mixed-precision exports.What changed
src/models/qwen2_moe.rs:SparseMoeBlock.expertsnow typedcrate::models::switch_layers::SwitchGLU; construction calls the sharedSwitchGLU::from_weights(weights, "{moe}.switch_mlp", group_size, bits); the routed-expert sum inSparseMoeBlock::forwarduses the fused-gated branch with amoe_weighted_sumfallback; localSwitchGLU/SwitchLineardefinitions and loaders removed.src/models/switch_layers.rs:from_weights_with_moderefactored into a sharedfrom_stacked_partsbuilder plus a newstack_individual_expertsper-expert fallback; moduleUsed by:doc updated to include Qwen2Moe; two unit tests added (per-expert quantized stacking, missing-weight error).docs/benchmark_results/fused-moe-decode-kernel-design.md: qwen2_moe moved from the not-wired list to the wired list.Test plan
cargo check --lib --tests --features metal,acceleratecargo clippy --lib --tests --features metal,accelerate -- -D warnings(clean)mlx-community/Qwen1.5-MoE-A2.7B-Chat-4bit: confirm the kernel actually dispatches (not a silent fallback), temp-0 greedy parity vsMLXCEL_FUSED_MOE=0within the documented f16 jitter class (no NaN / no garbage), and decode tok/s fused-on vs off. Pending the orchestrator's verification step.Closes #286