Skip to content

diag(MODEL-MM-QWEN4-EXP): the MoE expert selections DIFFER, and a third of the boundaries are exact bf16 ties (#2552) - #2556

Merged
mudler merged 6 commits into
mainfrom
row/MOEDIV-q4exp-router-2552
Sep 2, 2026
Merged

diag(MODEL-MM-QWEN4-EXP): the MoE expert selections DIFFER, and a third of the boundaries are exact bf16 ties (#2552)#2556
mudler merged 6 commits into
mainfrom
row/MOEDIV-q4exp-router-2552

Conversation

@localai-org-maint-bot

@localai-org-maint-bot localai-org-maint-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

The selections DIFFER, and it is NOT a defect. At num_experts = 512 and
num_experts_per_tok = 10 the router's top-k boundary is an EXACT bf16 tie at
79 of 240 prefill token-slots (32.9%)
on the CPU control, and inside ONE
representable bf16 step at 55.8%. At a tie the selection carries no information:
the lowest-index tie-break decides it, and any perturbation upstream reshuffles
it. The flip rate is the tie rate — 75 of 240 token-slots flip on the
VT_GDN_CHUNKED=0 arm, 78 on the production CUDA arm.

And #2552's own headline number is not a flip, which is the half the issue
got wrong. At decoder layer 0 with the Gated DeltaNet source removed — the arm
its table was taken on — both arms select the same experts for all five
tokens
, and the first flip is at layer 1. The 7.269e-05 residue there
decomposes onto the expert GEMM instead.

Why a value tap could never have said this

VT_Q4EXP_LAYER_FP reports rel(sum|x|). A discrete selection has bimodal
error rather than a tolerance: a token's top-k set either changes, and that
token's MoE output moves by an O(1) amount, or it does not and the residue is
rounding. Averaging both into one relative difference destroys exactly the bit
that decides which happened.

So this lands VT_MOE_SEL_FP=<calls>, a tap on the SELECTED EXPERT IDS on the
reference arm of the shared sparse-MoE block — the arm a stacked keep-quant
checkpoint takes and the only arm any qwen4_exp checkpoint reaches. Per token:
the ids sorted, since the assertion between two arms is set equality and
vt::MoeCombine is order-invariant over the k slots; the smallest selected and
largest rejected router logit with their raw bf16 bit patterns; and the margin
counted in bf16 ULPS under the sign-magnitude total order.

The margin is in logit space, not probability space, and that is the design.
The softmax denominator is itself a device-order f32 reduction and differs
between the arms, while the bf16 logits are what the selection is a function of.
A probability gap reads as "small" for one representable step and for fifty
alike; ulps = 0 says the two logits are the SAME bf16 value. Per call: an
FNV-1a hash of every token's sorted list, so comparing two arms at one layer is
one string comparison, plus four sum|x| axes decomposing the block.

The measurement

thor:gpu0 sm_110, rc job a5bf074b-0f1a-490d-ab51-5d561857ef9e, one binary
b3d5d97c…, three arms, the released unsloth/Qwen3.8-Flash-Next-GGUF UD-IQ1_S
(shard 1 88a14208…) verified inside the lease. CMAKE rc=0, BUILD rc=0 wall=921s. Full result in
the evidence file, with
the raw comparator output and all 384 digest lines per arm committed beside it so
every number is checkable rather than transcribed.

The instrument asserts it ran: every arm printed 48 prefill calls and a final
lines=576, the derivable total (48x5 prefill + 336x1 decode), and the
comparator re-derives it as COUNTED-PROPERTY … agree=True on both sides.

Two independent instruments agree bit for bit on the MoE block's input. This
tap's x axis reads the tensor VT_Q4EXP_LAYER_FP calls L00 mhc.mix, from a
different tree, and all three arms reproduce PREFILLDIV's values exactly
(3613.82031 / 3615.62777 / 3613.74301, and both rel values).

Layer 0, VT_GDN_CHUNKED=0, selections equal, rel against the CPU control:

axis what it is rel vs the input
x the block input 2.139e-05 1.0x
logit the bf16 router logits 2.378e-05 1.1x
exp the assembled per-slot expert outputs 1.421e-04 6.6x
shr the shared expert 4.310e-05 2.0x

The router GEMM is not the amplifier. The keep-quant grouped expert GEMM is.

Why it is not a defect

vLLM routes this model on bf16 logits at 512 experts too, so upstream sits on
the same knife edge. Read at cdefd9d499, a FORWARD REFERENCE 1566 commits past
the pin 5559679229, which carries no vllm/models/qwen4_exp/ at all:
Qwen4ExpSparseMoeBlock (vllm/models/qwen4_exp/nvidia/model.py:160) inherits
Qwen3Next's gate untouched; that gate is a plain ReplicatedLinear with no
params_dtype
(vllm/model_executor/models/qwen3_next.py:170-176); the runner
applies it as a plain F.linear
(fused_moe/runner/moe_runner.py:897-902); and the f32 widening happens inside
ops.topk_softmax (fused_moe/router/fused_topk_router.py:26-41). Same width,
same widening point, same lowest-index tie-break as ours.

vLLM does own a fp32-capable gate — GateLinear
(fused_moe/router/gate_linear.py:18-33), a PluggableLayer with three of five
tiers emitting fp32 logits — and Qwen3Next does not opt into it, so it is
unreachable for this architecture. That is recorded as a thing to mirror IF
upstream flips this model onto it. Widening ours first would move this model AWAY
from the oracle.

The expert GEMM's arm-to-arm difference is a documented re-association, and the
k-quant secondary oracle has the same one.
cuda_quant_dot.cu:2158-2170 states
it: the integer core is bit-identical and only the per-block float scale sum is
reassociated, warp tree against CPU sequential; the activation quantizer is a
bit-exact port either way. llama.cpp @ 237ad9b96 splits identically —
ggml-cuda/mmvq.cu:566-641 finishes with warp_reduce_sum while
ggml-cpu/arch/x86/quants.c:841-852 sums sequentially.

So neither arm is wrong, and #2552 closes as answered rather than fixed.

What it says about a gate

PREFILLDIV observed that token agreement is not monotone in numerical distance
and proposed no CPU-vs-CUDA token gate. This gives the mechanism: a third of the
routing decisions carry zero margin, so the emitted sequence is a function of
tie-break order rather than accuracy. No CPU-vs-CUDA token-exactness gate is
well posed for qwen4_exp.
The selection-set agreement rate and the tie-rate
histogram ARE well posed and are now measurable. A gate against vLLM stays the
right target and stays OWED: vLLM's GGUF support is an out-of-tree plugin and
every safetensors arm of this model exceeds the largest fleet box.

Gates, read literally

test_qwen4_exp_moe gates the printed ids against MoeReference, that suite's
independent double-precision reimplementation of the lane-pinned upstream, and
gates ulps against the reference's own bf16 logit bit patterns with the
sign-magnitude ordering re-derived test-side so a sign defect in the tap has
nothing agreeing with it. The case asserts its own capture through a sentinel
written into the redirected descriptor, because a redirect that caught nothing
would otherwise read as "the tap printed nothing" — the polarity the OFF arm
asserts.

The second ctest entry re-runs the same binary with the switch set, since the
budget is a read-once static. It is deliberately unfiltered: a -tc pattern
matching nothing exits 0 having run nothing and doctest reports
assertions: 0 … SUCCESS!, a skip wearing a pass this row has paid for. Running
the whole binary cannot fail that way and buys a second property free — every
value case re-runs with the tap on, so a tap that perturbed the block's output
would red them.

Three mutations, each with an applied-proof count 1 to 0 and each built at
rc=0
before it was run, because a mutation whose build failed reads as a
passing test:

mutation rc assertions
M1 delete the tap's call inside MoeBlock, keeping the symbol referenced so -Wunused-function cannot stand in for the test 1 120 of 121, 1 failed
M2 hash and print the SELECTION ORDER instead of the sorted set 1 167 of 172, 5 failed
M3 drop the sign-magnitude total order from the bf16 ulp count 1 158 of 172, 14 failed

Tree restored byte-for-byte after each. Green on the final merge result: build
rc=0, 121 | 121 passed switch unset, 172 | 172 passed switch set, both rc=0,
and the same two arms ran green inside the lease on thor (TAP GATE OFF rc=0,
TAP GATE ON rc=0). scripts/agent-preflight.sh rc=0.

What this is NOT

n=1: one prompt, one artifact, one box, one repetition, greedy, UD-IQ1_S,
num_reqs = 1. Not a token gate — no oracle decoded this prompt and the CPU
arm is a CONTROL, not an oracle, so nothing here says which arm is closer to
correct. No speed number. The "vLLM sits on the same knife edge" conclusion is
a SOURCE READ, not a measurement
: no vLLM process was run on this checkpoint.

The decode columns are VOID and are published only to say so. The arms were
measured on a99b9c69a, before #2550 landed, so the CUDA arm answered
11751 271 271 271 271 271 0 0 and from step 1 the two arms ran different token
sequences — the 336-of-336 decode flip count is a different-input artifact.
#2550 has since landed at bb78d1ee8 and this branch merges it; the prefill
result does not depend on it, because step 0 consumes the prompt rather than a
sampled token. A post-#2550 decode comparison is now well posed and is named as
the cheap next measurement rather than claimed.

docs/USAGE.md is deliberately untouched. Its checkpoint row still says no
qwen4_exp step reaches a CUDA queue, which is false — but #2554 already carries
that correction, and two pull requests writing the same enormous table cell is
the lock shape AGENTS.md warns about.

Refs #2547.
Refs #2550.
Closes #2552.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]

…ap the SELECTION

Wave PREFILLDIV left a second, independent source of CPU/CUDA prefill divergence
at decoder layer 0: with the Gated DeltaNet source removed the MoE block still
turns a 2.1e-05 input difference into a 7.269e-05 output difference. Nothing
measured so far can say whether that is an expert-selection flip or
re-association inside the expert GEMM, because `VT_Q4EXP_LAYER_FP` taps values
and a discrete selection has bimodal error rather than a tolerance.

This is the spec for the instrument that can tell them apart: a selected-expert-id
tap that prints the sorted top-k set per token, an FNV-1a hash of it so set
equality between two arms is one string comparison per layer, and the boundary
margin in bf16 ULPS rather than in probability space, because the softmax
denominator is itself a device-order reduction while the bf16 logits are what the
selection is a function of.

Refs #2552, #2547.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…e tap cannot see a routing flip

Wave PREFILLDIV left a second source of CPU/CUDA prefill divergence at decoder
layer 0 and could not classify it: the MoE block turns a 2.1e-05 input difference
into a 7.269e-05 output difference with the Gated DeltaNet source removed, and a
`rel(sum|x|)` cannot say whether an expert-selection flipped or the expert GEMM
re-associated. A discrete selection has bimodal error rather than a tolerance, so
averaging the two into one number destroys exactly the bit that decides.

`VT_MOE_SEL_FP=<calls>` prints the selection instead, on the reference arm of the
shared sparse-MoE block, which is the arm a stacked keep-quant checkpoint takes.
Per token: the selected ids SORTED, since the assertion between two arms is set
equality and `vt::MoeCombine` is order-invariant; the smallest selected and the
largest rejected router logit with their raw bf16 bit patterns; and the boundary
margin counted in bf16 ULPS under the sign-magnitude total order. Probability
space would have been the wrong space, because the softmax denominator is itself
a device-order f32 reduction while the bf16 logits are what the selection is a
function of, and a probability gap reads as "small" for one representable step
and for fifty alike. Per call: an FNV-1a hash of every token's sorted list, so
comparing two arms at one layer is one string comparison, plus four `sum|x|` axes
that decompose the block's own output.

The tap reads buffers the reference path had already downloaded, so it moves no
extra bytes except one guarded shared-expert readback, and it is inert with the
switch unset. The three fused CUDA arms keep their ids on device and are not
tapped: no `qwen4_exp` checkpoint reaches them, and a readback added to a
capturable path to instrument a model that cannot enter it would be dead code.

Gated in `test_qwen4_exp_moe` against `MoeReference`, that suite's independent
double-precision reimplementation of the lane-pinned oracle, with the case
asserting its own capture through a sentinel so a broken redirect cannot read as
"the tap printed nothing". A second ctest entry re-runs the same binary with the
switch set, because the budget is a read-once static and the default run can only
ever observe it unset; that entry is deliberately unfiltered, since a `-tc`
pattern matching nothing exits 0 having run nothing.

Refs #2552, #2547.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…oundaries are exact ties

The tap ran on `thor:gpu0` over the released UD-IQ1_S artifact, three arms on one
binary. At `E = 512` and `top_k = 10` the router's top-k boundary is an EXACT
bf16 tie at 79 of 240 prefill token-slots on the CPU control, 32.9%, and inside
one representable bf16 step at 55.8%. At a tie the selection carries no
information: the lowest-index tie-break decides it, and any perturbation upstream
reshuffles it. The flip rate is the tie rate, 75 of 240 slots on the
`VT_GDN_CHUNKED=0` arm against 79 tied.

#2552's own headline number is NOT a flip, which is the half the issue got wrong.
At layer 0 with the Gated DeltaNet source removed, the arm its table was taken
on, both arms select the same experts for all five tokens. The `7.269e-05`
residue decomposes onto the expert GEMM instead: `x` 2.139e-05 in, `logit`
2.378e-05 (the router GEMM does not amplify), `exp` 1.421e-04 at 6.6x, `shr`
4.310e-05.

It is not a defect. vLLM routes this model on bf16 logits at 512 experts too:
`Qwen4ExpSparseMoeBlock` inherits Qwen3Next's plain `ReplicatedLinear` gate with
no `params_dtype`, the runner applies it as a plain `F.linear`, and the f32
widening happens inside `topk_softmax` — our polarity, our widening point, our
lowest-index tie-break. The expert GEMM's arm-to-arm difference is the
re-association `cuda_quant_dot.cu` already documents, and llama.cpp splits its
own CPU and CUDA arms the same way. Read at a forward reference 1566 commits past
the pin, which carries no `qwen4_exp` at all, and recorded as a source read
rather than a measurement: no vLLM process was run on this checkpoint.

Two independent instruments agree bit for bit on the MoE block's input. This
tap's `x` axis reads the tensor `VT_Q4EXP_LAYER_FP` calls `L00 mhc.mix`, from a
different tree, and all three arms reproduce PREFILLDIV's values exactly.

The decode half is VOID and is published only to say so. On `origin/main`
`a99b9c69a` the CUDA arm answers `11751 271 271 271 271 271 0 0`, not the fluent
sequence PREFILLDIV recorded, because that wave measured a tree carrying #2550's
decode fix and #2550 has not landed. From step 1 the arms run different token
sequences, so the 336-of-336 decode flip count is a different-input artifact.
`docs/USAGE.md` is deliberately untouched: its checkpoint row still claims no
`qwen4_exp` step reaches a CUDA queue, which is false, and #2554 already carries
that correction.

Refs #2547.
Refs #2550.
Closes #2552.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…recting what the merge falsified

`main` moved from `a99b9c69a` to `bb78d1ee8` while this wave measured, and one of
the commits it brought is #2550 — the decode fix this wave's evidence file named
as NOT LANDED. That sentence was true of the tree three arms were measured on and
false of the merge result, which is the shape where a merge falsifies a branch's
own prose. Both the spec Outcome and the evidence file are corrected in this
commit rather than left to read as a claim about `main` today: the decode numbers
stay VOID because the arms ran different token sequences on the measured tree,
the prefill result is untouched because step 0 consumes the prompt and not a
sampled token, and a post-#2550 decode comparison is named as the cheap next
measurement instead of being claimed.

The spec conflict is a take-BOTH, not an automatic three-way merge: `main`'s
DECODEDIV section and this branch's MOEDIV section were added at the same anchor
above `## Now`, so the landed one is placed first and the new one after it, and
both are whole. `src/vllm/model_executor/models/qwen3_5.cpp`,
`tests/CMakeLists.txt` and `scripts/env-doc-allowlist.txt` auto-merged; the tap's
definition, its one production call site, its ctest entry and its allowlist row
were each re-verified present afterwards, because a clean auto-merge is not
evidence that a scoped edit survived.

Rebuilt and re-run on the merge result, read literally: build rc=0;
`test_qwen4_exp_moe` rc=0 with `assertions: 121 | 121 passed | 0 failed` switch
unset and rc=0 with `172 | 172 passed | 0 failed` switch set.

Refs #2547.
Refs #2550.
Refs #2552.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
…rather than only the merged hunks

`main` moved to `35116605b`. Everything it brought is ROCm — the expert-lane
guard, its spec, its reachability test and one `model_loader.cpp` change — and it
auto-merged with no conflict. A clean auto-merge is exactly the case that has
falsified prose on this row before, so the claims were re-checked rather than
assumed: nothing merged touches the sparse-MoE seam, the router, the keep-quant
grouped GEMM or `qwen4_exp`, and no sentence in this branch's spec section or
evidence file describes a file the merge moved.

The scoped edits were re-verified present by count instead of by eye, because a
conflict-free merge is not evidence that they survived: the tap's definition and
its one production call site (2), the switch read exactly once (1), the
allowlist row (1), the ctest entry (2), and both spec sections whole (1 each,
`## DECODEDIV` and `## Wave MOEDIV`), with zero conflict markers anywhere in the
tree.

Rebuilt and re-run on the merge result, read literally: build rc=0;
`test_qwen4_exp_moe` rc=0 at `assertions: 121 | 121 passed | 0 failed` with the
switch unset and rc=0 at `172 | 172 passed | 0 failed` with it set.

Refs #2547.
Refs #2552.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
@localai-org-maint-bot localai-org-maint-bot changed the title diag(MODEL-MM-QWEN4-EXP): tap the SELECTED EXPERT IDS, because a value tap cannot see a routing flip (#2552) diag(MODEL-MM-QWEN4-EXP): the MoE expert selections DIFFER, and a third of the boundaries are exact bf16 ties (#2552) Sep 2, 2026
… this wave's spec section onto the target

`build-test-cpu` was red on #2556 and the failure was not this wave's:
`test_qwen4_exp_layer_loop`, the rank-5 fixture break `main` was already carrying
after #2541 bounded `dense_attn::MakeTensor` at `kMaxRank = 4` and #2538 and
#2550 landed rank-5 paged-KV fixtures on top of it. Neither conflicted textually
with the bound, so both merged clean and both were red, and every `ci` run on
`main` is cancelled by the next push, so no verdict was ever rendered. #2554
repaired it as #2559. This branch pre-dated that repair; merging `main` is what
clears the red, and it does: `test_qwen4_exp_layer_loop` rc=0 at
`assertions: 426 | 426 passed | 0 failed`.

The spec conflicted exactly where this wave predicted it would. #2554 added
`## Wave PREFILLDIV` at the same anchor above `## Now` that this branch adds
`## Wave MOEDIV` to. It is resolved on the keyed-record rule rather than by
accepting a three-way merge: the complete target-branch file was taken and this
wave's one contiguous block re-applied to it, and the result is proven
numerically rather than eyeballed. Against the target, 0 lines removed and
exactly 167 added, which is the block's own line count; deleting precisely those
167 lines reproduces the target BYTE FOR BYTE
(`dd1d7ef61cedc14e61fa01f92e01a96797c4599a3e0c53d0ad9ad6396be68d3c` either side);
and the re-applied block is byte-identical to the one this branch already
carried. All three sibling sections survive whole and in landing order:
`## DECODEDIV` (#2496), `## Wave PREFILLDIV` (#2547), `## Wave MOEDIV` (#2552).

The conflict-free hunks were re-checked too, because a clean merge has falsified
this tree's prose repeatedly. `scripts/env-doc-allowlist.txt` carries BOTH keys
rather than one clobbering the other (`VT_MOE_SEL_FP` and #2554's
`VT_Q4EXP_LAYER_FP`); the tap's definition and its one production call site (2),
its switch read once (1) and its ctest entry (2) all survive; and #2554's
`docs/USAGE.md` correction — that `--device cuda` NOW SERVES this artifact — is
still on the merged tree rather than reverted by a branch that deliberately never
touched that file.

This wave's result is unchanged by the merge and is not re-derived here: it was
measured on `a99b9c69a` and nothing merged since touches the sparse-MoE seam, the
router or the keep-quant grouped GEMM.

Read literally on the merge result: CMAKE rc=0, BUILD rc=0,
`test_qwen4_exp_layer_loop` rc=0 (426/426), `test_qwen4_exp_moe` rc=0 at
`121 | 121 passed` switch unset and rc=0 at `172 | 172 passed` switch set.

Refs #2547.
Refs #2559.
Refs #2552.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5-1m [claude-code]
@mudler
mudler merged commit 4de001d into main Sep 2, 2026
26 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qwen4_exp: the MoE block is a SECOND source of CPU/CUDA prefill divergence, and nothing yet says whether it is an expert-selection flip

2 participants