Repository navigation
fix(speculative): Gemma 4 MTP breaks byte-identity on generation 15+, and its gate returns unconditionally true #1188
Description
Status: measured and confirmed on 2026-08-18
This issue was filed as "the mechanism should carry over, nobody has run the comparison". The comparison has now been run on an Apple M5 Max and the mechanism does carry over. Gemma 4 MTP is not byte-identical to classic decode on generation 15+ hardware under the default kernel selection, and disabling qmv_wide restores it exactly.
The gate change is no longer speculative work. What is new, and what makes it a real decision rather than a formality, is the price.
The measurement
Host: Apple M5 Max (Mac17,7), macOS 26.6.1, mlxcel at 07ab8612, [profile.test-fast], --features metal,accelerate. Target models/gemma-4-12b-it-4bit, drafter models/gemma-4-12b-it-assistant-4bit, --draft-kind mtp, temperature 0, 250 tokens, two prompts.
| comparison | prose prompt | code prompt |
|---|---|---|
MTP vs classic, qmv_wide on (default) |
differs at char 51 | differs at char 55 |
MTP vs classic, qmv_wide off |
byte-identical | byte-identical |
Block width does not change the answer: block 2 and block 5 both diverge under the default, at the same character. That is consistent with the mechanism, since use_qmv_wide splits at M >= 2 and a block of 2 is already on the wrong side of it.
Classic decode is deterministic run to run here (three runs, identical), so the divergence is systematic rather than the f16 jitter class.
One methodological trap, because it cost us a wrong answer first
Toggling MLXCEL_QMV_WIDE changes classic decode too, so the comparison has to hold the kernel fixed on both sides. Classic's multi-token prefill is itself an M >= 2 forward, so classic(wide on) and classic(wide off) differ from each other at char 51 on the prose prompt. Comparing MTP(wide off) against classic(wide on) conflates the prefill effect with the verify effect and reports a divergence that says nothing about the contract.
Second trap, for whoever reproduces this: the CLI echoes the prompt without a trailing newline and then appends Loading MTP drafter from "..." to that same line, so a naive line filter leaves the banner in the compared text and every MTP run "differs from classic" at the character where the banner starts. Extract the generated text by splitting on the prompt echo and dropping the two drafter banner lines.
What it costs
This is the number the decision turns on. Same host, code prompt, block 5, 300 tokens, three samples per arm alternated:
| kernel | decode | vs classic (43.5 tok/s) |
|---|---|---|
qmv_wide on (fast, not exact) |
121.50 tok/s | 2.79x |
qmv_wide off (exact) |
93.19 tok/s | 2.14x |
Spreads were 0.04 and 0.37 tok/s, so the gap is not measurement noise: exactness costs 23.3% on this pairing. For comparison, the Qwen 3.5 arms pay about 17 to 20% for the same guarantee, and that cost is already being paid in production today because their probe runs.
So the change is not free, and it is not a bug fix that only removes a wrong claim. It converts an advertised 2.80x into an honest 2.14x on this prompt, or it keeps 2.80x and qualifies the claim. Both are defensible; silently advertising byte-identity while not measuring it is not.
What to change
- Route the Gemma 4 arms through
mtp_exactness_gateinstead of returningtrue. Insrc/server/batch/speculative_burst.rs:
LoadedModel::Gemma4(_) | LoadedModel::Gemma4VLM(_) | LoadedModel::Gemma4Unified(_) => true,The probe machinery in src/models/speculative_exactness.rs is already family-agnostic: it compares a T = K verify block against the single-token chain, and on failure retries with qmv_wide disabled before declining. Nothing in it is Qwen-specific.
-
Decide what a failing probe should do for Gemma. Qwen's answer is "disable the kernel and keep the contract", which is why it pays 17 to 20%. Given the 23.3% measured here, the alternative worth putting on the table is to keep the fast kernel and state plainly that Gemma 4 MTP is not byte-identical on generation 15+, the way
MLXCEL_MTP_ALLOW_INEXACTalready allows. This issue should not pick that by default; it should make the choice explicit and measured. -
Qualify
docs/benchmarks.md. The MTP section overview still claims byte-identity with no qualification, and the Gemma 4 rows inherit it. The Qwen rows were qualified by fix(speculative): MTP temperature-0 byte-identity fails on Apple GPU generation 15+ and at block widths above the qmv batch limit #1186; these were not.
Not covered here
The 31B and Unified pairings, and any generation 13 or 14 host. The measurement above is one target, one drafter, one machine. It is enough to settle that the exposure is real; it is not enough to characterise the cost across the family.
Activity
inureyes commentedon Aug 16, 2026
Measured: Gemma 4 projection shapes on M5 Max
Filling the "not yet measured" cell, using the cheap first step this issue names. Op-level only; this is not a Gemma 4 model-level chain run.
Apple M5 Max 128 GB, macOS 26.6.1, architecture generation 17,
mainate3f322e5. Shapes read from local checkpoints:gemma-4-31b-it-4bit(hidden 5376, intermediate 21504),gemma-4-12b-it-4bit(3840, 15360), both affine 4-bit group 64, vocab 262144.Affine 4-bit, the shipped configuration
The mechanism carries to Gemma 4. Every Gemma 4 projection shape breaks at
T = 2, and the MTP default block width is 4, so the shipped configuration is inside the broken range. The unconditionaltruearm is wrong on generation 15 and newer.Both thresholds are visible on one machine
Sweeping the 31B attention shape and reporting the magnitude rather than just equal/diverges shows two steps, not one:
The
T = 2step isuse_qmv_wide. TheT = 10step isget_qmv_batch_limitcrossing into the matrix-matrix kernel. Note the limit here is 10, not the 12 quoted in #1186: that 12 is thearch_size == 'd'branch, which is the Ultra. A Max lands in thedefaultbranch, which returns 10 for both operands above 4096 on a generation that is not 13 or 14.The mxfp4 claim does not hold on generation 17
#1186 infers from
use_qmv_wide(mode, d) = mode != "affine" || arch_gen >= 15that non-affine modes diverge on every generation, and measured mxfp4 diverging on M1 Ultra (3, 4, 2 of 10240 bytes atT = 3). On M5 Max the opposite is true. Measured at the same 5120 -> 5120 shape #1186 used, so this is directly comparable:The affine control diverging in the same run is what makes this a real result rather than a broken harness.
So the two modes behave oppositely across the two generations: M1 Ultra is affine-clean and mxfp4-broken, M5 Max is affine-broken and mxfp4-clean (nvfp4 marginal, one byte at
T = 4). Whatever the full dispatch story is, "mxfp4 diverges everywhere" is not it, and #1186's Impact section should be narrowed accordingly.This also strengthens the case for #1189's approach over a hardware table. Two generations, two quantization modes, and the answer inverts.
What this does not establish
T = 12by the same mechanism, unmeasured.seq_id-keyed cache slots and needs a throwaway slot for the probe.Reproduction was a scratch harness at these shapes rather than a committed test, since #1189 already carries
tests/metal_block_vs_chain_op_parity.rsand adding Gemma 4 shapes to it belongs on that PR or on this issue's own change.inureyes commentedon Aug 16, 2026
M5 Max confirms the exposure: all four Gemma 4 rows diverge at the production block width
Ran the diagnostic from #1192 on an M5 Max 128 GB (macOS 26.6.1, Xcode 26.5.0), branch
test/issue-1188-gemma4-op-parity-shapesat0e1f9f10. Build 4 minutes, test 0.75 s, no checkpoint.T = 3Every Gemma 4 projection diverges, and the Qwen 3.8 affine rows diverge alongside them, which is the expected signature:
use_qmv_widekeys onarch_gen >= 15, not on the model family. The Gemma 4 arm ofmtp_capable_targetis admitting a host where its verify block is not byte-identical to the single-token chain. The model-level probe this issue asks for is justified.For contrast, the same commit on an M1 Ultra (generation 13) reports all four Gemma 4 rows
equalatT = 3.The 18/12 cliff does not appear here at all
On an M1 Ultra the same sweep shows Gemma 4's attention shapes holding to 17 and its MLP shapes breaking at 12, the two
get_qmv_batch_limitbuckets. Hereuse_qmv_widecatchesM >= 2first, so the batch limit is never reached and never observed. The per-shape cliff is a generation 13/14 phenomenon; from generation 15 there is a single threshold at 2.Amplitude varies by three orders of magnitude, and the gate must not care
The 8-bit MLP rows move 24 of 30720 bytes and 8 of 7680, while the 4-bit attention rows move about 38% of theirs. Byte-identity is broken in every one of them.
This is the argument for the gate deciding whether the arms differ rather than how much. A tolerance-based gate tuned on the loud rows would pass the quiet ones, and a quiet row still flips a near-tie argmax eventually: it is the same last-ulp mechanism, just rarer per position.
It also has a direct consequence for the probe design in #1186, filed separately below.
Follow-up on this issue
The measurement half of the acceptance criteria is now done for generation 17 and for the M1 Ultra control. What remains is the gate change: giving the Gemma 4 arms the same runtime probe, which needs a throwaway model-owned sequence slot because
forward_with_speculative_sinkskeys its cache onseq_idrather than taking a cache vector.test(speculative): extend the block-vs-chain diagnostic to Gemma 4 sh…
[-]fix(speculative): the Gemma 4 MTP gate returns unconditionally true and is exposed to the same generation 15+ byte-identity break[/-][+]fix(speculative): Gemma 4 MTP breaks byte-identity on generation 15+, and its gate returns unconditionally true[/+]inureyes commentedon Aug 18, 2026
Can the exactness cost be made conditional? Measured: no
Before wiring the gate, the obvious question is whether the 23.3% has to be paid unconditionally. The contract only breaks when a kernel difference flips an argmax, and a flip needs the top two logits to sit inside that difference. If near-ties were rare relative to the kernel error, the fast kernel could run every round and only ambiguous positions would be recomputed exactly.
Instrumented and measured. It does not work, and the numbers say why.
The two distributions
MLXCEL_MTP_GAP_LOG=1(added ine757aac1) printsposition, token, top1, top2, gapper verify position. Running a generation under each kernel selection and aligning the logs while the token streams still agree gives both halves. gemma-4-12b-it-4bit with the 4-bit assistant, block 5, 15 prompts, 444 aligned positions:The delta is exactly zero at only 145 of 444 positions. The top-two gap has median 5.875, so typically the gap is about forty times the error, which is why most tokens agree. The problem is the tails: the gap is under 3.0 at 30.6% of positions.
The break-even
A guard recomputes the block exactly whenever any position's gap falls under
t. Always-narrow costs 1.180x the fast verify, so a guard has to come in under that.Sound coverage costs nearly twice what simply running narrow costs. The one row that beats always-narrow,
t = 0.25, misses three potential flips in ten.Break-even needs the per-round ambiguity below 15.3%, which needs the per-position rate below 3.3%. Reaching that means guarding only
delta <= 0.125, and 29% of measured deltas exceed it.Why it fails
The kernel error and the top-two gap are the same order of magnitude. There is no threshold that is both cheap and safe, because the two distributions overlap where it matters rather than being separated by orders of magnitude.
A first pass with a thin sample looked promising, and that is worth flagging for anyone who repeats this: over a 16-position common prefix the max delta read 0.3125, which would have put the guard comfortably in the black. Across 444 positions the max is 3.0. The tail is the whole answer here, so a bound taken from a short run will mislead.
What this means for the decision
The choice in this issue stands as it was, without a third option: either pay the 23.3% and keep the contract, or keep the fast kernel and say plainly that Gemma 4 MTP is not byte-identical on generation 15+. There is no cheap conditional path between them.
Measured on Apple M5 Max (
Mac17,7), macOS 26.6.1, mlxcel ate757aac1.inureyes commentedon Aug 18, 2026
What the 23.3% actually buys, measured at the right shape
The decision here has been stuck between "pay 23.3% and keep byte-identity" and "keep the speed and admit it is not byte-identical", with nothing to say how much the second option costs a user. That is a gap in tooling rather than in judgement, so I built the missing measure:
examples/logit_traceplusscripts/compare_logit_traces.py(PR #1215 branch, commitbd97da0c). Teacher-forced, one trace per configuration, and it reports disagreement split by how decided the reference was.The answer
gemma-4-12b-it-4bit,
MLXCEL_QMV_WIDEoff as reference against on as candidate, forward width 5 behind 512 tokens of context, 300 positions:Split by decidedness, the disagreement is almost entirely where the model had no preference:
So the trade is 23.3% of throughput against a 0.585% disagreement rate on positions the model had decided, three quarters of which are it picking its own runner-up. That is a materially different-looking decision from "23.3% for byte-identity", and it is the number this issue was missing.
One methodological point, because it inverts the answer
The forward width selects which kernel is measured, so it selects the answer. The same comparison reads:
use_qmv_widesplits atM >= 2and the batch limit sends largerMto a matrix-matrix kernel both arms share, so anything traced at prefill width reports no difference at all and anything traced at a bare narrow width reports a catastrophe. Neither is the shape a verify runs at, which is a narrow forward behind a long context. The harness takes a prefill argument for exactly that reason.It also means the exposure is bounded in a useful way: prefill is unaffected, and the break lives only in the small-
Mwindow the verify block sits in.What I am not doing
Picking the default. The measurement narrows the question, it does not settle it: 0.585% on decided positions is small but it is not zero, and one of the twelve disagreements put the reference's choice outside the candidate's top eight. Whether that is worth 23.3% is a product call.
Measured on Apple M5 Max (
Mac17,7), macOS 26.6.1.docs: current speculative-decoding numbers, with the prompt they were…
feat(speculative): report the top-two logit gap at each verify position
docs: current speculative-decoding numbers, with the prompt they were…
docs(bench): record the M1 Ultra speculative decoding numbers
docs(bench): record the M1 Ultra speculative decoding numbers
feat(bench): measure speculative decoding properly, and record what i…
fix(speculative): probe Gemma 4 MTP exactness instead of assuming it (#…
docs(make): record the TF32 pin at the verify-test-cuda gate definiti…
fix(tests): seed synthetic model fixtures from a sorted key walk (#1266)