fix(models): flatten mistral4 MoE tokens to 2D before SwitchGLU routing (#425)#426
Conversation
…ng (#425) Mistral4MoE::forward passed a 3D `[batch, seq, hidden]` tensor and 3D `[batch, seq, top_k]` routing indices to the shared SwitchGLU::forward, which expects 2D `[n_tokens, hidden]` and `[n_tokens, top_k]`. SwitchGLU reads `indices_shape[0]` as `n_tokens` and `indices_shape[1]` as `top_k`, then decides `do_sort = n_tokens * top_k >= 64`. With 3D `[1, seq, top_k]` it misread `n_tokens=1` and `top_k=seq`. For `seq < 64` the `do_sort=false` branch limped along; for `seq >= 64` it entered gather_sort with the wrong dims and aborted with `[reshape] Cannot reshape array of size seq*hidden into (1,1,1)`. This is the 3D-vs-2D SwitchGLU contract violation behind #425: it fired on every real mistral4 prompt, on a 512-token chunked-prefill continuation chunk, and on any image-bearing multimodal prefill (e.g. the 3047-token merged text+image sequence, 3047 x 4096 = 12480512). It stayed latent because mistral4 was only ever run on a single-token warmup before #424 made the family loadable; the warmup's `[1, 1, hidden]` MoE forward succeeds. Fix mirrors qwen3_moe's proven flatten/reshape-back pattern: capture `orig_shape`, flatten `x` to `[n_tokens, hidden]` (prefill `[1, l, hidden]` -> `[l, hidden]`, decode `[1, 1, hidden]` -> `[1, hidden]`), run the existing routing math on the flat tensor so indices and scores come out correctly shaped `[n_tokens, top_k]`, dispatch experts and shared experts on the flat tensor, then reshape the result back to the original rank. Ops and eps are unchanged, so per-token routing is numerically identical, just correctly shaped. SwitchGLU, moe_weighted_sum, and all other models are untouched. This also corrects the decode path, which the old 3D shape was misrouting. Decode output may differ slightly from the pre-fix buggy path; that is the intended correction, not a regression. Validation: cargo fmt --check, cargo check --lib, and cargo clippy --lib --tests -- -D warnings all pass. Real-model 119B validation (chunked-prefill text plus image+text on mlx-community/Mistral-Small-4-119B-2603-4bit) is performed by the orchestrator.
Real-model validation (Mistral-Small-4-119B-2603-4bit) — mistral4 now runs normallyBefore this fix, every mistral4 forward with >= 64 tokens aborted (
No reshape/broadcast/abort signals in any run. This completes the mistral4 bring-up chain (#423 load routing -> embed_tokens -> #425 MoE flatten): mistral4 (Mistral Small 4) now loads and runs text and image+text inference. |
Security and performance reviewReviewed Security / DoS (primary value): resolved, no new surface
Performance: no new hot-path cost
Correctness-adjacent: reshape-back is size-preserving
Verification
No CRITICAL/HIGH/MEDIUM/LOW findings to escalate. The PR removes a CRITICAL-severity process-abort DoS without introducing new risk. |
…pport (#425) Add a dedicated supported-models entry for the Mistral 4 (`mistral4`) MLA + MoE family now that it loads and runs end to end: the Mistral Small 4 119B checkpoint generates text and image-plus-text via the Mistral 3 VLM wrapper on the Mistral4 text backbone. Record the architecture values (q_lora_rank 1024, kv_lora_rank 256, split rope/nope head dims, 128 routed experts plus one shared with 4 active, Llama-4 attention scaling) and the validated checkpoint, and update the VLM wrapper note to reflect that image-plus-text on the Mistral4 backbone is now validated.
PR FinalizationChecks run
Code review
Docs
Tests
SummaryNo code or doc changes were required. The PR is ready to merge as-is. |
Summary
Mistral4MoE::forwardpassed 3D tensors to the sharedSwitchGLU::forward, which expects 2D inputs. This violated the SwitchGLU shape contract and aborted every real mistral4 prompt, every chunked-prefill continuation chunk, and any image-bearing multimodal prefill. The fix flattens tokens to[n_tokens, hidden]before routing and reshapes the result back, mirroring qwen3_moe's proven pattern.The bug: 3D-vs-2D SwitchGLU contract violation
SwitchGLU::forward(src/models/switch_layers.rs:324) expects 2D inputs:xis[n_tokens, hidden]and the routing indices are[n_tokens, top_k]. It readsindices_shape[0]asn_tokensandindices_shape[1]astop_k, then decides the sort path withdo_sort = n_tokens * top_k >= 64.Mistral4MoE::forwardinstead passed a 3Dx = [batch, seq, hidden]and 3Dinds = [batch, seq, top_k]. With[1, seq, top_k]indices, SwitchGLU misreadn_tokens = 1andtop_k = seq:seq < 64,do_sort = falselimped along on the wrong shapes.seq >= 64(any real prompt, a 512-token prefill chunk, or an image-bearing prefill),do_sort = trueenteredgather_sortwith the wrong dims and aborted with[reshape] Cannot reshape array of size seq*hidden into (1,1,1).This is the abort tracked in #425. It blocked:
mlx-community/Mistral-Small-4-119B-2603-4bit.--max-kv-size 1024 --prefill-chunk-size 512(reshape ... of size 2097152 = 512 x 4096 into (1,1,1)).reshape ... of size 12480512 = 3047 x 4096 into (1,1,1)).It stayed latent because mistral4 was only ever run on a single-token warmup before #424 made the family loadable; the warmup's
[1, 1, hidden]MoE forward (n_tokens=1, top_k=1, total < 64) happens to succeed.The fix: qwen3_moe-mirrored flatten / reshape-back
Following
SparseMoeBlock::forwardinsrc/models/qwen3_moe.rs:orig_shapeandhidden_dim.xto[n_tokens, hidden](n_tokens = product of leading dims): prefill[1, l, hidden] -> [l, hidden], decode[1, 1, hidden] -> [1, hidden]. The<= 2Dpath copies through unchanged.softmax,argpartition,slice_axis,take_along_axis, thenorm_topk_probblock with the1e-20eps, and therouted_scaling_factormultiply) on the flat tensor, so indices and scores come out correctly shaped[n_tokens, top_k]. Ops and eps are unchanged, so per-token routing is numerically identical, just correctly shaped.SwitchGLU,moe_weighted_sum, and all other models are untouched; onlyMistral4MoE::forwardchanges. No fused single-token kernel path was added (correctness-only; a fused mistral4 MoE optimization can be a separate change).This also corrects the decode path, which the old 3D shape was misrouting. Decode output may differ slightly from the pre-fix buggy path; that is the intended correction, not a regression.
What changed
src/models/mistral4.rs: rewroteMistral4MoE::forwardto flatten tokens to 2D before SwitchGLU routing and reshape the result back, with a comment documenting the 2D SwitchGLU contract and the>= 64-tokendo_sortcrash.Test plan
A
Mistral4MoE::forwardunit test needs real expert weights (a SwitchGLU built fromSwitchLinearper-expert weights), which is not CI-feasible, and there is no existing checkpoint-free MoE shape-test pattern in the tree to mirror, so no synthetic test was forced. Real-model 119B validation (chunked-prefill text plus image+text) is performed by the orchestrator.cargo fmt --checkcargo check --lib --features metal,acceleratecargo clippy --lib --tests --features metal,accelerate -- -D warningsCloses #425