Repository navigation
fix(models): port upstream numeric fixes for four model families - #2457
Conversation
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
Implementation Review SummaryIntent
Findings AddressedNone needed: no CRITICAL or HIGH findings. Reviewed at Remaining Items
Checked against upstream
Verification
|
Security and performance review (head 87dcfdf)No CRITICAL or HIGH findings, so no fix commits were made. Security. No attacker-controlled input reaches the new code. PhiMoE rejects Performance.
Verification (GB10, CUDA, test-fast, TF32 off): |
Gemma 3n: KV-shared layers read the RoPE offset from their owner's cache after the owner had already appended the current pass, so their queries were rotated L positions ahead of their keys (one position on every decode step). The offset is now read once per forward from the first owner cache and threaded through the decoder layer into the attention (ml-explore/mlx-lm#1886). PhiMoE: routing is HF sparsemixer instead of a softmax over the top-k logits (ml-explore/mlx-lm#1939), and the longrope block is applied through phi3::SuRope with short/long table selection and the configured mscale; a null or absent rope_scaling keeps plain RoPE. SuRope gains a pub(crate) from_scaling constructor and the rotary prefix scale moves onto SuRopeTable so both families share it. PhiMoE reports rope_table_regime and rejects num_experts_per_tok > num_local_experts at load. Gemma 4 audio: the local attention mask is built from the query-minus-key distance and no longer admits dist == max_past, one key further back than the reference (Blaizzy/mlx-vlm#2385). Phi-3-small: the vocabulary's 91 padding ids are returned from output_suppressed_token_ids so CLI and server generation never emit them; forward logits stay raw (ml-explore/mlx-lm#1938). logit_trace gains an opt-in audio arm (MLXCEL_TRACE_AUDIO) that traces a Gemma 4 transcript behind an audio clip. Refs #2379
The Gemma 3n MLP and AltUp input round activations to bf16, so a 12-row pass and a narrower one can differ by one bf16 ulp (2.7e-3 in the last-token logits) with the fix in place. Every split whose passes run at six rows or fewer is now compared at 1e-4 (1e-6 with the fix, 5e-3 to 4e-2 without it), and the single pass at 1e-2 (2.7e-3 fixed, 2.7e-2 to 6.7e-2 reverted). The test now uses upstream's rope bases so the offset error is well above that rounding. Document PhiMoE sparsemixer routing and LongRoPE, Phi-3-small's suppressed padding ids, and the logit_trace audio arm. Refs #2379
Multi-pass splits are compared at 1e-3 instead of 1e-4: they agree to about 1e-6 on CUDA with the fix and the tightest reverted pair is 5.6e-3, so the gate keeps a 5x margin while leaving room for backends that pick other kernels per width. Add the CHANGELOG entry for the four ports and use a range check in the old-mask reference helper. Refs #2379
EmbeddingGemma 2 builds the same crate::audio::AudioEncoder, so the local mask fix moves its audio embeddings too. Raised in the PR review. Refs #2379
sparsemixer runs one round per selected expert, so num_experts_per_tok of 0 would reach an empty concatenate at the first forward instead of failing at load. validate() now rejects it next to the existing upper bound, and the routing-config test covers it. The plain-RoPE test also notes that its trimmed fixture stands in for a checkpoint without rope_scaling. Refs #2379
5123f33 to
4a26662
Compare
…2513) * fix(xla): update Gemma 3n emitter to the shared-layer query offset ## Summary The mlxcel-xla Gemma 3n emitter still built shared-KV-layer query positions the way the eager path did before #2457 (`positions + real_len` on the prefill graph, `position + 1` on the decode graph), so MLX-vs-XLA Gemma 3n parity diverged. The maintained MLX path reads `rope_offset` once per forward before any layer runs (mlx-lm's `cache[0].offset`), so a shared-KV layer rotates its queries with the same positions as its owner layer; the emitter now mirrors that on both graphs and the rope table shrinks from `2 * context_capacity` to `context_capacity` rows. - The `#[ignore]`d pinned E2B prefix-decode top1 (236798) predates #2457 and is refreshed to 854, re-captured on main at 551039e (MLX pin bff10220) with the mlx-vlm 0.7.8 CPU oracle agreeing on the same fixture; the full-prefill and greedy pins are unchanged and the comments record why. - `tests/xla_gemma3n_prefill.rs` fixes the stale comment about the extended rope table. ## Verification (GB10, --profile test-fast, MLX_ENABLE_TF32=0) Fail-before proof on the pre-change emitter: token-prefill logits max_abs 1.9375 against the 1.5 envelope, and the one-row prefix-decode carrier top1 xla=236798 vs mlx=854, exactly the pre-#2457 MLX value the stale offset reproduces. After the fix the same stage comparison resolves the prefill divergence the issue targets: token-prefill logits max_abs 0.8203 (within 1.5) with matching top1, the shared-layer stages improve (layer_last_active_plane max_abs 9 to 3), and the greedy trajectory matches ([236798, 236789] both sides). Remaining pinned-envelope failures are pre-existing drift, not introduced here: scaled_embeddings (0.03125 vs envelope 0), layer0_ple_injected (2 vs 0.25) and the layer-mid planes (15 vs 3) measure byte-identical on the pre-change emitter, and the prefix-decode comparison stays outside the envelope both before (2.2617, cosine 0.9432) and after (2.7266, cosine 0.8710); these stages predate both #2457 and this PR (the pin was last captured 2026-07-24 and the MLX pin has since moved), and per the #932 decision recorded drift is not gating. cargo test --profile test-fast --features cuda,xla-diagnostics -p mlxcel --lib gemma3n -- --test-threads=1: 40 passed, 0 failed. cargo test --profile test-fast --features cuda,xla-diagnostics -p mlxcel --test xla_gemma3n_prefill -- --test-threads=1 --ignored (MLXCEL_XLA_DEVICE=cuda): 1 passed. cargo fmt --all -- --check: the four changed files are clean (main carries pre-existing fmt diffs in src/models/inkling/mlp.rs and src/models/switch_layers.rs, untouched here). Scoped clippy -p mlxcel -p mlxcel-xla --lib -D warnings: green. Closes #2466 * docs: add PR 2513 technical report
Summary
Ports four upstream numeric fixes in families mlxcel ships, as one PR per the issue.
DecoderLayerintoGemma3nAttention::forward, so KV-shared layers no longer rotate queries at their owner's already-advanced offset.sparsemixerrouting replaces softmax over the top-k logits; thelongropeblock goes throughphi3::SuRope(newpub(crate) SuRope::from_scaling, rotary-prefix scale moved ontoSuRopeTable), withrope_table_regimefor the prompt cache. Nullrope_scalingkeeps plain RoPE;num_experts_per_tok > num_local_expertsfails at load. The fused decode kernel (run_fused_moe_two_kernel) casts scores to f32 and multiplies; it does not renormalize (the normalizing code nearmlx_cxx_kernels.cpp:1491is the DeepSeekfused_moe_forward, a different entry point). A test pins fused == graph with scores summing to 1.5025.causal_valid_maskbuilds the mask fromdist = q + max_past - c; the old construction admitteddist == max_past.output_suppressed_token_idsreturns the 91 padding ids belowvocab_size; forward logits stay raw.logit_tracegains an audio arm (MLXCEL_TRACE_AUDIO,MLXCEL_TRACE_AUDIO_PROMPT); docs and CHANGELOG updated.Tests
New tests in
gemma3n_tests.rs(new),phimoe_tests.rs,encoder_tests.rs,phi3small_tests.rs. The Gemma 3n chunking test and the mask predicate test fail onmain; the old-mask helper is asserted to differ at(12,13,0). Deviation from the issue: mlxcel's Gemma 3n MLP and AltUp input round to bf16, so the single 12-row pass differs from narrower passes by one bf16 ulp (2.7e-3 fixed, 2.7e-2 to 6.7e-2 reverted). Multi-pass splits are gated at 1e-3 (1e-6 fixed, at least 5.6e-3 reverted) and the single pass at 1e-2. Measured on CUDA only, withMLX_ENABLE_TF32=0.Real-checkpoint traces (GB10, fixed vs reverted,
--decided 2)Gemma 3n at width 1 shows no decided disagreement: shared layers write no KV, so the only error is a one-position query shift in 10 of 30 layers; the effect grows with width. The audio clip is a deterministic 4 s synthetic chirp/tone/noise WAV (
tests/fixtures/audio/tone.wavis 0.25 s); the arms differ (max logit delta 0.875) but not at decided positions. A transformers float32 reference for Phi-3.5-MoE (42B) is not feasible on this host, and mlx-lm is not a valid short-regime oracle (long table only).Other checks:
mlxcel generateon Phi-3.5-MoE is fluent at 128 tokens. Onphi-3-small-8k-instruct-aq4_64(substitute for mlx-community Phi-3-small-8k-instruct-4bit), 256 sampled tokens through the suppression bias emitted no listed id.Follow-ups (not in this PR)
mlxcel-xlagemma3n_emit_ops.rsdeliberately mirrors the old shared-layer query offset; MLX-vs-XLA Gemma 3n parity will now diverge. Not buildable here.phimoe::ModelArgsreadslayer_norm_eps(default 1e-6); the checkpoint spells itrms_norm_eps: 1e-5.Gemma3nModelmay be this bug; flipping it needs the parity battery.gemma3n.rswere updated by hand and not compiled.Closes #2379