Repository navigation
fix: align Gemma3n gelu_topk bf16 rounding across eager, XLA and mlx-lm - #2801
Merged
Merged
Conversation
Gemma3n sparse layers (0-9, activation_sparsity 0.95) rounded the bf16 gelu_topk three different ways: the eager compiled kernel promoted the whole tape to f32 (strong f32 multiplier and zero), which disagrees with mlx-lm on about 4% of bf16 elements, and the XLA tape left the multiplier, root and cubic constants unrounded with a two-rounding cube. The eager kernel now mirrors mlx-lm's gelu_topk graph op for op (#2795): the f32 multiplier is cast to the input dtype like mlx-lm's std_multiplier.astype, the zero and the nn.gelu_approx constants are built in the input dtype like Python's weak scalars, and the cube is one power op. The shared gelu_tanh_approx helper stays as-is because its x*x*x cube serves the dense GeGLU paths. The XLA emitter rounds the multiplier, root and cubic constants to bf16 at emit time (host-side round via bf16_scalar) and computes the cube as one f32 product, exact before a single rounding (36 to 34 converts). The pinned E2B probe's manual tape mirrors the same bf16 carrier and additionally pins the compiled production kernel against both the per-op tape and the FMA-site tape, since MLX's CUDA compiled kernels contract the two mul+add sites into single-rounded FMAs. A new ffi_gelu_topk_tests module pins the kernel bit for bit against the mlx-lm carrier and keeps an f32-carrier reference so the test can tell the carriers apart. Validated on the GB10 CUDA host: the mlx-lm compiled reference probe reports the bf16 carrier with fused sites at 0 mismatches of 8192 and 32768 (widths 2048 and 8192, MLX_ENABLE_TF32=0) while the old f32 carrier misses 370/1247; the pinned E2B IREE probe reports bit_mismatches=0 on every layer0_sparse_* stage and fma_sites_bit_mismatches=0 on layer0_sparse_activation_manual_vs_compiled; greedy smoke on gemma3n-e2b-4bit and gemma3n-e4b-bf16 through mlxcel generate and mlxcel-server reproduces mlx-lm's token sequences exactly. Refs #2795
inureyes
marked this pull request as ready for review
October 31, 2026 17:14
This was referenced Nov 1, 2026
Closed
inureyes
added a commit
that referenced
this pull request
Nov 1, 2026
…e stage could run on a real checkpoint (issue #2808, both failures recorded in the PR #2801 body). The weight-carrier check pinned MLX CPU dequant semantics while the probe executes MLX on GPU, so every 4-bit weight failed with about 51% 1-ulp mismatches and the probe never reached the stages; the AltUp chain, once reachable, drifted by f32 reduction noise that a strict bit comparison cannot tell from a formula error. The weight carrier is now built with the rounding the default device applies: dequantize_affine_bf16_single_round matches MLX's GPU mx.dequantize (q * scale + bias in f32, one round to BF16; verified bit-exact for the pinned E2B checkpoint, 4194304/4194304, and synthetic wide-magnitude weights), while the CPU path keeps dequantize_affine_bf16_sequential. The strict comparison itself is unchanged and still rejects any wrong scale, bias, group layout, or nibble order, which move carriers far beyond one ulp. The AltUp coefficient chain now compares under the #932 per-op rule with the measured noise recorded: coefficients at max_abs 1e-4 (measured 1.514e-5 prediction / 5.603e-6 correction), and the bf16 stages that consume them at max_abs 3.2e-3 plus two bf16 ulps at the operand's binade, the measured shapes of the compiled TF32 reduction noise (absolute 1.5e-4 on near-zero values, three ulps mid-binade, one ulp at [2, 8)). The layer0_sparse_* stages and manual_vs_compiled keep their bit-exact comparison and reported 0 mismatches in the final probe run. Closes #2808
inureyes
added a commit
that referenced
this pull request
Nov 1, 2026
…e stage could run on a real checkpoint (issue #2808, both failures recorded in the PR #2801 body). The weight-carrier check pinned MLX CPU dequant semantics while the probe executes MLX on GPU, so every 4-bit weight failed with about 51% 1-ulp mismatches and the probe never reached the stages; the AltUp chain, once reachable, drifted by f32 reduction noise that a strict bit comparison cannot tell from a formula error. (#2810) The weight carrier is now built with the rounding the default device applies: dequantize_affine_bf16_single_round matches MLX's GPU mx.dequantize (q * scale + bias in f32, one round to BF16; verified bit-exact for the pinned E2B checkpoint, 4194304/4194304, and synthetic wide-magnitude weights), while the CPU path keeps dequantize_affine_bf16_sequential. The strict comparison itself is unchanged and still rejects any wrong scale, bias, group layout, or nibble order, which move carriers far beyond one ulp. The AltUp coefficient chain now compares under the #932 per-op rule with the measured noise recorded: coefficients at max_abs 1e-4 (measured 1.514e-5 prediction / 5.603e-6 correction), and the bf16 stages that consume them at max_abs 3.2e-3 plus two bf16 ulps at the operand's binade, the measured shapes of the compiled TF32 reduction noise (absolute 1.5e-4 on near-zero values, three ulps mid-binade, one ulp at [2, 8)). The layer0_sparse_* stages and manual_vs_compiled keep their bit-exact comparison and reported 0 mismatches in the final probe run. Closes #2808
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
Gemma3n sparse layers (0-9, sparsity 0.95) rounded the bf16 gelu_topk three different ways: the eager compiled kernel promoted the whole tape to f32, and the XLA tape left three constants unrounded with a two-rounding cube. Both sides now move to mlx-lm's bf16 carrier, per the issue's settled Decision.
What changed
compiled_gelu_topkkernel (per-op and fused paths) mirrors mlx-lm's graph op for op: the f32 multiplier cast to the input dtype likestd_multiplier.astype, the zero andnn.gelu_approxconstants built in it like Python's weak scalars, the cube onepowerop. The sharedgelu_tanh_approxhelper stays as-is (itsx*x*xcube serves the dense GeGLU paths).sparse_gelu_stagestape rounds the multiplier, root and cubic constants to bf16 at emit time and computes the cube as one f32 product, exact before a single rounding (36 to 34 converts).ffi_gelu_topk_testsmodule pins the kernel bit for bit against mlx-lm's carrier, with an f32-carrier reference kept so the test can tell carriers apart.docs/gelu-call-sites.mdrows G15/G17 and thecompiled_gelu_topk/gemma3n_mlp_forwardrows describe the bf16 carrier and drop the known-gap note.Test plan
cargo test -p mlxcel-core --profile test-fast --features cuda -- --test-threads=1 gelu_topk: 3 passed. The bit-exact test reports FMA-site tape 0 mismatches, per-op 99/849, f32 carrier 212/883.mlxcel-xlasparse_gelu (34 converts), host-oracle andbf16_scalartests pass;cargo fmt --checkclean, clippy clean on the touched packages.gelu_topkas reference, widths 2048/8192): old f32 carrier 370/1247 mismatches, xxx cube 8/821, per-opnn.gelu_approx(z)0/816, bf16 carrier with fused FMA sites 0/0. On CUDA the implementation targets the FMA-contracted compiled kernel; the XLA tape reproduces the per-op tape (MLX CPU semantics).~/.cache/mlxcel/iree-cuda-3.12.0rc20260721works;IREE_CUDA_HOME+GEMMA3N_MODEL_DIR=models/gemma3n-e2b-4bit,--test-threads=1):bit_mismatches=0on everylayer0_sparse_*stage andfma_sites_bit_mismatches=0/24576onlayer0_sparse_activation_manual_vs_compiled(per-op 333/24576, the CUDA FMA residual). The probe overall still aborts at pre-existing, unrelated checks (below).gemma3n-e2b-4bitandgemma3n-e4b-bf16, throughmlxcel generateandmlxcel-server/v1/chat/completions, temperature 0, 64 tokens, two prompts: token sequences identical to mlx-lm greedy on the same rendered prompt (prompt_tokens 14/16 match). The only text difference is a trailing space delta the mlxcel SentencePiece detokenizer drops before a newline; display only.Pre-existing findings (not from this change)
probe_real_projection's weight-carrier check fails on CUDA for every 4-bit weight: the Rust diagnostic pins MLX CPU dequant semantics (bf16(bf16(q*scale)+bias), 0 mismatches vsmx.dequantizeon CPU) while the probe runs MLX on GPU; proven device-dependent via an oracle-venv GPU-vs-CPU check. This abortspinned_e2b_real_projection_qmv_probebefore the gelu stages; the stage results above were taken with that one assertion temporarily relaxed locally, and the edit is reverted.Closes #2795