spec(MODEL-MM-QWEN4-EXP): Qwen3.8-Flash-Next is a new architecture vLLM does not implement, so the port splits its oracle - #1980
Merged
Conversation
…LM does not implement, so the port splits its oracle
`Qwen/Qwen3.8-Flash-Next` was released on 2026-08-24 and declares
`Qwen4ExpForConditionalGeneration` / `model_type: qwen4_exp`. Nothing in this tree
matched it. This lands the row's records and its spec; no product code.
The name is misleading and the misreading is the expensive one. `Qwen3.8-27B` is,
per `specs/qwen38-27b-bf16-gate.md`, the Qwen3.6-27B shape retrained with exactly one
config key changed. That precedent does not extend here: the card calls this "the
architecture that will underpin Qwen4", and it diverges from `qwen3_5` in four
load-bearing places.
**vLLM implements nothing.** Read live 2026-08-26 at `origin/main` = `6a5e8f5979`:
no `qwen4*` path, no `registry.py` entry, and a repository-wide GitHub search for
`qwen4` returns zero results. `vllm-omni` likewise. That is absence from vLLM `main`,
not staleness in `555967922`, so advancing the parity pin does not reach it. What
exists is transformers#48337 "Add Qwen4Exp model", merged 2026-08-26, and SGLang
#36497, still open and therefore inadmissible.
So the row runs a split oracle, on developer direction of 2026-08-26: transformers
for the algorithm, vLLM ops for the optimized path. That is not a compromise. The
transformers reference states its own limits in code — `Qwen4ExpTextQSAIndexer.forward`
loops in Python over `(batch_idx, query_idx)` and carries the comment "we only allow
eager and sdpa" — so porting it as written yields a correct model at an indefensible
speed, while AGENTS.md's mirror-vLLM polarity still binds every primitive vLLM does
implement. The spec resolves each component against exactly one of the two, and an
implementer who cannot name the oracle for the line they are writing has found a gap
in the spec rather than a licence to choose.
The op survey is the substance of this change, and it moved two conclusions.
**Most of the model is already here.** `Qwen4ExpTextModel` inherits from
`Qwen3_5MoeTextModel` and leaves rotary, MLP, experts, TopK router and the entire
vision tower unchanged — `class Qwen4ExpVisionModel(Qwen3_5MoeVisionModel): pass`.
GDN is an exact hit on our Triton-AOT gate: the config's `128 / 128 / 16 / 48` against
the specializations `cuda_gdn.cu` pins to `K=V=128, Hg=16, H in {48,32}`. Exactly two
components have no vLLM op at all, and they are the two we must author ourselves: the
PLE dilated depthwise conv (`git grep dilation` over vLLM's `layers/mamba/` returns
nothing) and the hashed n-gram embedding.
**QSA's twin in vLLM is MiniMax-M3, not DeepSeek-V4.** DSA is an MLA indexer; QSA is
plain GQA, 24 Q heads over 2 KV, `head_dim` 256. `models/minimax_m3/common/indexer.py`
is vLLM's non-MLA block-sparse case and its own docstring describes QSA's shape:
scores KV blocks with index heads, selects top-k blocks, owns a side cache of one
index-key vector per token. `common/ops/index_topk.py` supplies the block-score kernel
and a bitonic top-k, and the pooled-key build is
`deepseek_v4/nvidia/ops/sparse_attn_compress_cutedsl.py`'s
`SparseAttnCompressNormRopeStoreC4Kernel`, which already carries `compress_ratio`.
This tree has a working DSA indexer, which makes DSA the path an implementer reaches
for first, and it is the wrong one. Recording that is most of why this spec exists.
Two structural consequences beyond the module list, both of which touch code this row
does not otherwise go near. The residual stream is `hc_count * hidden_size` = 4 x 2560
= 10240 wide through the whole stack, with a low-rank read gate and per-branch scalar
write gate around both attention and MLP, so the per-layer loop and every residual
buffer change. And `number_of_conv_states` is 3 on a PLE layer, plus the indexer side
cache, which lands on the same KV-cache seam #1963 and #1966 are moving.
**Nothing published fits.** Against roughly 119 GB usable on GB10: BF16 is ~360 GB,
the official FP8 ~180 GB, `RadixArk/...-NVFP4` ~128 GB, and `unsloth/...-GGUF` is a
README with zero weight files. No GGUF exists and no tool can produce one, because
llama.cpp has no `qwen4_exp` either, so the standing k-quant requirement means
authoring the architecture here and stating plainly that those arms have no llama.cpp
oracle. The architecture supplies its own lever: the per-token n-gram cost is 16
lookups of 160 dims, so 51 GB of the 180 GB — 28% of the model — is a table touched 16
times per token, and making it non-resident is the intended design point rather than a
workaround. RadixArk reached the same split independently. `gateable` is therefore
`no`, on memory rather than software.
Two decisions were put to the developer as explicit accept-or-reject, and both are
settled and recorded in place rather than left open.
The first is the oracle. `oracles/transformers.md` pins transformers to 5.14.1, tied
to whatever the pinned vLLM environment resolves so that environment cannot hold two
`transformers` at once, and 5.14.1 does not contain `Qwen4Exp`. The lane-scoped second
pin is accepted, on the argument that the invariant guards a vLLM environment against
drifting from its transformers and here there is no vLLM implementation to drift from,
with expiry the moment vLLM registers `qwen4_exp`. It resolves to a real release rather
than a branch SHA, which was not the expected outcome: the merge landed at 12:03:40Z on
2026-08-26 and `v5.16.0` published at 12:35:15Z. That was bounded rather than assumed —
fetching `models/qwen4_exp/modeling_qwen4_exp.py` at each tag gives HTTP 200 at
`v5.16.0` and HTTP 404 at `v5.15.0`, so 5.16.0 is the first release carrying the
architecture and therefore the tightest pin available. The version string stays
UNMEASURED: it is the release proven to contain the model, not a
`transformers.__version__` read off a running oracle, and `gateable` stays `no`.
The second is the first runnable arm: a Q4_K_M backbone with the n-gram table
non-resident, about 76 GB. Q8_0 was raised and does not fit at ~191 GB, and no partial
split reaches 119 GB while keeping the backbone at 8 bits. Q4_K_M throughout fits on
paper at ~109 GB but leaves roughly 10 GB for KV and activations on a model with 262144
native context, which is not a margin. This promotes the non-resident table from a note
to a first-class W6 deliverable and splits that wave in two, because it is not free:
GB10 is unified memory, so the existing host-pinned offload seam does not by itself
solve it there and the mechanism has to be disk-backed or genuinely unloaded. W6b is
the unknown, and the spec says to spike it before scheduling rather than design around
an unmeasured cost.
The `MODEL` row ratchet moves 377 -> 378 with its justification appended in the
existing log format, re-derived off the matrix rather than carried forward. One row
and not two: the MTP head is a `mtp` block inside the same text config, not a
separately registered architecture, so this is not the IndexTTS-2.5 or dots3-note
shape that moved the pin by two. The at-the-pin static invariants (324/373/356/310/261)
are unchanged, because the Upstream cell carries no pinned module/class target.
Tracked by #1978.
Gates: `check-agent-record` ok, `check-model-checklist` ok,
`tests/scripts/test_agent_record.py` + `test_check_model_checklist.py` 130 passed,
`agent-preflight.sh --staged` ok.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]
…DeepSeek-V4 rather than MiniMax-M3 Folds five parallel component investigations into the row spec and corrects two claims the first draft got wrong. Every load-bearing finding below was re-verified against the source before it was written down; the agents' reports are inputs, not evidence. The headline correction. The spec claimed "QSA's structural twin in vLLM is MiniMax-M3, NOT DeepSeek-V4", reasoning that QSA is plain GQA rather than MLA and so had to map onto the non-MLA block-sparse case. That reasoning rested on reading `MLAAttentionSpec` as an MLA claim. It is not one: M3's own indexer cache uses it while M3 is plain GQA, and the comment beside it says why, "Key-only: MLAAttentionSpec budgets one vector/token (not 2x for K+V)". It is a budget shape. With that prop removed the argument collapses, and nine independent structural matches with DeepSeek-V4 remain, `compress_ratio == 4` literally the same number: MQA index with one key head at dim 128, `relu(q.k)` summed over index heads, `1/sqrt(head_dim)`, one score set per query token with no head axis, the `(position+1) % COMPRESS_RATIO == 0` boundary, RMSNorm on the pooled key, RoPE at the block-start position, candidates counted as `visible // compress_ratio`, and one stored state per four tokens through `MLAAttentionSpec(tokens_per_state=compress_ratio)`. M3 is a different algorithm, not a worse fit. Verified: its score is `tl.max(qk, axis=1)` over 128 raw token dots with no pooling, no relu and no head reduction; it asserts `num_idx_heads == num_kv_heads` with the comment "no topk index reduce", so it emits one block set per KV head where QSA emits one per token; and `SPARSE_BLOCK_SIZE = 128` is welded to the KV page size on both the score and the attend side. It contributes exactly one thing, a wiring precedent: a plain-GQA model can own a key-only side cache and a private indexer backend. The genuinely new work is the consumer, and nothing upstream supplies it. Every DSv4 sparse consumer attends to compressed KV; M3's attend to raw tokens only at page granularity; QSA attends to raw tokens selected at ratio-4 granularity. Two silent failures follow. Wiring QSA's top-k into a DSv4 sparse-MLA consumer attends a pooled key and value and still emits plausible tokens, and a short-prompt gate cannot catch it, because below `indexer_budget` every candidate is selected. `## Gates` now requires at least one prompt past 2048 tokens of context for exactly that reason. And `SparseAttnCompressNormRopeStoreC4Kernel` does not mean-pool despite the name: it is a learned softmax pool over an overlapping window of eight, driven by a score channel this checkpoint does not have. The second correction is smaller and would have cost an afternoon: vLLM's grouped RMSNorm is on `RMSNormGated`, not the plain `RMSNorm`, whose only related knob is `var_hidden_size`, a prefix reduction that cannot express per-group norms. The chosen arm does not load today, and the blocker is neither the offload nor the budget. This tree cannot keep a gather table quantized at all: `KeepQuantKDim` returns -1 for `kEmbeddingTable` and the qwen3_5 loader asserts it by name, so a quantized n-gram table expands to bf16 and 51.2B parameters become 102.4 GB of anonymous memory. The reason was already recorded in a header comment, "a gather, not a GEMM ... A quantized-gather op is a follow-up row", and no such row exists. Separately, `moe_intermediate_size = 640` and `hc_lowrank = 320` are Q4_K-illegal on their reduction dims, and our GGUF reader has no entry for ggml type ids 3, 6, 7 or 20, so a stock `llama-quantize -Q4_K_M` file would fail at header parse. We author the converter, so the fix is Q4_0. `ENG-WEIGHT-OFFLOAD` does not help and must not be budgeted for: it moves zero bytes today and is documented inert on unified memory. The tier that works already ships and the 2.4T model proves it. W6 is split into a/b/c accordingly. Sizing survives: backbone ~67.7 GiB, whole process ~73.5 GiB of 119.631 at 32K single stream. The design works because per-token demand is at most 64 KiB of reads against the 2.4T expert lane's 6.95 GB/token. The n-gram derivation was verified against the published checkpoint rather than trusted: reconstructing the splitmix64 chain at the default seed 1234 reproduces the `layer_multipliers` buffer read out of the safetensors payload, and the head vocab sizes and offsets match entry for entry. Three silent C++ divergence sites are now named, the worst being that the shard reassembly must be numeric, since a lexicographic key sort gives shard_0, shard_1, shard_10 and permutes a 95 GiB table. Confirmed from the index: 128 contiguous shards, and the PLE sits on decoder layer 1. Also recorded: vLLM has no dilated 1-D convolution anywhere, a confirmed negative with zero hits in mamba/, csrc/ and tests/; the PLE conv is strided history at lags 9/6/3/0; its signed-sqrt gate clamps before the square root, so the output floor is 1e-3 and tiny scores are amplified rather than squashed; and `Qwen4ExpTextModel` has no final RMSNorm, which the natural copy of our DeepSeek-V4 tail would wrongly add. Tracked by #1978. Gates: `check-agent-record` ok, `check-model-checklist` ok, `agent-preflight.sh --staged` ok. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [Claude Code]
…, and repair the anchors that naming it shifted The 377 -> 378 bump changed `scripts/check-agent-record.py` with no evidence in `tests/scripts/test_agent_record.py`, which `check-pr-size.py` refuses by name: a governance-checker change owes semantic mutation evidence. The gate was right. `test_qwen4_exp_row_is_inside_the_model_ratchet` follows the convention the dots3-note and recipe-backfill tests set, naming the row the bump was taken for so a count raised to silence a broken parse cannot look identical to a count raised because a row landed. It also pins that there is exactly ONE qwen4_exp row: the MTP head is an `mtp` block inside the same text config, not a separately registered architecture, so this bump is not the by-two shape that IndexTTS-2.5 and dots3-note each took. Adding it shifted `tests/scripts/test_agent_record.py` by 45 lines and staled the two anchors `ENG-RECORD-ANCHOR-RATCHET` keeps into that same file, taking the rot from 31 to 33 STALE. Repaired to the true lines, 1449 -> 1494 and 1517 -> 1562, rather than raising the budget, which the baseline file forbids in terms: "never to be raised to make a failing check pass". That row's own record already describes this exact failure - an edit to the very file the row cites - which is what the row exists to measure. Isolated rather than assumed: the anchor ratchet passes on pristine `origin/main`, so the two new stale entries were this branch's. Tracked by #1978. Gates: `tests/scripts/test_agent_record.py` 121 passed, rot back to the committed baseline 31 STALE + 6 BROKEN. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [Claude Code]
`origin/main` moved from a4f8ada to 993b62e while this row's spec was in review, so the gate was rerun on what will actually land rather than on the tree this branch was cut from. No conflict; the row touches records main did not. Rerun on the merged tree: pr-size ok, `tests/scripts/test_agent_record.py` 121 passed with the record-anchor rot back at its committed baseline. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [Claude Code]
This was referenced Aug 26, 2026
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.
Qwen/Qwen3.8-Flash-Nextwas released on 2026-08-24 and declaresQwen4ExpForConditionalGeneration/model_type: qwen4_exp. Nothing in this treematched it. This lands the row's records and its spec; no product code.
The name is misleading and the misreading is the expensive one.
Qwen3.8-27Bis,per
specs/qwen38-27b-bf16-gate.md, the Qwen3.6-27B shape retrained with exactly oneconfig key changed. That precedent does not extend here: the card calls this "the
architecture that will underpin Qwen4", and it diverges from
qwen3_5in fourload-bearing places.
vLLM implements nothing. Read live 2026-08-26 at
origin/main=6a5e8f5979:no
qwen4*path, noregistry.pyentry, and a repository-wide GitHub search forqwen4returns zero results.vllm-omnilikewise. That is absence from vLLMmain,not staleness in
555967922, so advancing the parity pin does not reach it. Whatexists is transformers#48337 "Add Qwen4Exp model", merged 2026-08-26, and SGLang
#36497, still open and therefore inadmissible.
So the row runs a split oracle, on developer direction of 2026-08-26: transformers
for the algorithm, vLLM ops for the optimized path. That is not a compromise. The
transformers reference states its own limits in code —
Qwen4ExpTextQSAIndexer.forwardloops in Python over
(batch_idx, query_idx)and carries the comment "we only alloweager and sdpa" — so porting it as written yields a correct model at an indefensible
speed, while AGENTS.md's mirror-vLLM polarity still binds every primitive vLLM does
implement. The spec resolves each component against exactly one of the two, and an
implementer who cannot name the oracle for the line they are writing has found a gap
in the spec rather than a licence to choose.
The op survey is the substance of this change, and it moved two conclusions.
Most of the model is already here.
Qwen4ExpTextModelinherits fromQwen3_5MoeTextModeland leaves rotary, MLP, experts, TopK router and the entirevision tower unchanged —
class Qwen4ExpVisionModel(Qwen3_5MoeVisionModel): pass.GDN is an exact hit on our Triton-AOT gate: the config's
128 / 128 / 16 / 48againstthe specializations
cuda_gdn.cupins toK=V=128, Hg=16, H in {48,32}. Exactly twocomponents have no vLLM op at all, and they are the two we must author ourselves: the
PLE dilated depthwise conv (
git grep dilationover vLLM'slayers/mamba/returnsnothing) and the hashed n-gram embedding.
QSA's twin in vLLM is MiniMax-M3, not DeepSeek-V4. DSA is an MLA indexer; QSA is
plain GQA, 24 Q heads over 2 KV,
head_dim256.models/minimax_m3/common/indexer.pyis vLLM's non-MLA block-sparse case and its own docstring describes QSA's shape:
scores KV blocks with index heads, selects top-k blocks, owns a side cache of one
index-key vector per token.
common/ops/index_topk.pysupplies the block-score kerneland a bitonic top-k, and the pooled-key build is
deepseek_v4/nvidia/ops/sparse_attn_compress_cutedsl.py'sSparseAttnCompressNormRopeStoreC4Kernel, which already carriescompress_ratio.This tree has a working DSA indexer, which makes DSA the path an implementer reaches
for first, and it is the wrong one. Recording that is most of why this spec exists.
Two structural consequences beyond the module list, both of which touch code this row
does not otherwise go near. The residual stream is
hc_count * hidden_size= 4 x 2560= 10240 wide through the whole stack, with a low-rank read gate and per-branch scalar
write gate around both attention and MLP, so the per-layer loop and every residual
buffer change. And
number_of_conv_statesis 3 on a PLE layer, plus the indexer sidecache, which lands on the same KV-cache seam #1963 and #1966 are moving.
Nothing published fits. Against roughly 119 GB usable on GB10: BF16 is ~360 GB,
the official FP8 ~180 GB,
RadixArk/...-NVFP4~128 GB, andunsloth/...-GGUFis aREADME with zero weight files. No GGUF exists and no tool can produce one, because
llama.cpp has no
qwen4_expeither, so the standing k-quant requirement meansauthoring the architecture here and stating plainly that those arms have no llama.cpp
oracle. The architecture supplies its own lever: the per-token n-gram cost is 16
lookups of 160 dims, so 51 GB of the 180 GB — 28% of the model — is a table touched 16
times per token, and making it non-resident is the intended design point rather than a
workaround. RadixArk reached the same split independently.
gateableis thereforeno, on memory rather than software.Two decisions were put to the developer as explicit accept-or-reject, and both are
settled and recorded in place rather than left open.
The first is the oracle.
oracles/transformers.mdpins transformers to 5.14.1, tiedto whatever the pinned vLLM environment resolves so that environment cannot hold two
transformersat once, and 5.14.1 does not containQwen4Exp. The lane-scoped secondpin is accepted, on the argument that the invariant guards a vLLM environment against
drifting from its transformers and here there is no vLLM implementation to drift from,
with expiry the moment vLLM registers
qwen4_exp. It resolves to a real release ratherthan a branch SHA, which was not the expected outcome: the merge landed at 12:03:40Z on
2026-08-26 and
v5.16.0published at 12:35:15Z. That was bounded rather than assumed —fetching
models/qwen4_exp/modeling_qwen4_exp.pyat each tag gives HTTP 200 atv5.16.0and HTTP 404 atv5.15.0, so 5.16.0 is the first release carrying thearchitecture and therefore the tightest pin available. The version string stays
UNMEASURED: it is the release proven to contain the model, not a
transformers.__version__read off a running oracle, andgateablestaysno.The second is the first runnable arm: a Q4_K_M backbone with the n-gram table
non-resident, about 76 GB. Q8_0 was raised and does not fit at ~191 GB, and no partial
split reaches 119 GB while keeping the backbone at 8 bits. Q4_K_M throughout fits on
paper at ~109 GB but leaves roughly 10 GB for KV and activations on a model with 262144
native context, which is not a margin. This promotes the non-resident table from a note
to a first-class W6 deliverable and splits that wave in two, because it is not free:
GB10 is unified memory, so the existing host-pinned offload seam does not by itself
solve it there and the mechanism has to be disk-backed or genuinely unloaded. W6b is
the unknown, and the spec says to spike it before scheduling rather than design around
an unmeasured cost.
The
MODELrow ratchet moves 377 -> 378 with its justification appended in theexisting log format, re-derived off the matrix rather than carried forward. One row
and not two: the MTP head is a
mtpblock inside the same text config, not aseparately registered architecture, so this is not the IndexTTS-2.5 or dots3-note
shape that moved the pin by two. The at-the-pin static invariants (324/373/356/310/261)
are unchanged, because the Upstream cell carries no pinned module/class target.
The component deep-dives, and two corrections to the above
Folds five parallel component investigations into the row spec and corrects two claims
the first draft got wrong. Every load-bearing finding below was re-verified against the
source before it was written down; the agents' reports are inputs, not evidence.
The headline correction. The spec claimed "QSA's structural twin in vLLM is MiniMax-M3,
NOT DeepSeek-V4", reasoning that QSA is plain GQA rather than MLA and so had to map onto
the non-MLA block-sparse case. That reasoning rested on reading
MLAAttentionSpecas anMLA claim. It is not one: M3's own indexer cache uses it while M3 is plain GQA, and the
comment beside it says why, "Key-only: MLAAttentionSpec budgets one vector/token (not 2x
for K+V)". It is a budget shape. With that prop removed the argument collapses, and nine
independent structural matches with DeepSeek-V4 remain,
compress_ratio == 4literallythe same number: MQA index with one key head at dim 128,
relu(q.k)summed over indexheads,
1/sqrt(head_dim), one score set per query token with no head axis, the(position+1) % COMPRESS_RATIO == 0boundary, RMSNorm on the pooled key, RoPE at theblock-start position, candidates counted as
visible // compress_ratio, and one storedstate per four tokens through
MLAAttentionSpec(tokens_per_state=compress_ratio).M3 is a different algorithm, not a worse fit. Verified: its score is
tl.max(qk, axis=1)over 128 raw token dots with no pooling, no relu and no head reduction; it asserts
num_idx_heads == num_kv_headswith the comment "no topk index reduce", so it emits oneblock set per KV head where QSA emits one per token; and
SPARSE_BLOCK_SIZE = 128iswelded to the KV page size on both the score and the attend side. It contributes exactly
one thing, a wiring precedent: a plain-GQA model can own a key-only side cache and a
private indexer backend.
The genuinely new work is the consumer, and nothing upstream supplies it. Every DSv4
sparse consumer attends to compressed KV; M3's attend to raw tokens only at page
granularity; QSA attends to raw tokens selected at ratio-4 granularity. Two silent
failures follow. Wiring QSA's top-k into a DSv4 sparse-MLA consumer attends a pooled key
and value and still emits plausible tokens, and a short-prompt gate cannot catch it,
because below
indexer_budgetevery candidate is selected.## Gatesnow requires atleast one prompt past 2048 tokens of context for exactly that reason. And
SparseAttnCompressNormRopeStoreC4Kerneldoes not mean-pool despite the name: it is alearned softmax pool over an overlapping window of eight, driven by a score channel this
checkpoint does not have.
The second correction is smaller and would have cost an afternoon: vLLM's grouped
RMSNorm is on
RMSNormGated, not the plainRMSNorm, whose only related knob isvar_hidden_size, a prefix reduction that cannot express per-group norms.The chosen arm does not load today, and the blocker is neither the offload nor the
budget. This tree cannot keep a gather table quantized at all:
KeepQuantKDimreturns-1 for
kEmbeddingTableand the qwen3_5 loader asserts it by name, so a quantizedn-gram table expands to bf16 and 51.2B parameters become 102.4 GB of anonymous memory.
The reason was already recorded in a header comment, "a gather, not a GEMM ... A
quantized-gather op is a follow-up row", and no such row exists. Separately,
moe_intermediate_size = 640andhc_lowrank = 320are Q4_K-illegal on their reductiondims, and our GGUF reader has no entry for ggml type ids 3, 6, 7 or 20, so a stock
llama-quantize -Q4_K_Mfile would fail at header parse. We author the converter, sothe fix is Q4_0.
ENG-WEIGHT-OFFLOADdoes not help and must not be budgeted for: itmoves zero bytes today and is documented inert on unified memory. The tier that works
already ships and the 2.4T model proves it. W6 is split into a/b/c accordingly.
Sizing survives: backbone ~67.7 GiB, whole process ~73.5 GiB of 119.631 at 32K single
stream. The design works because per-token demand is at most 64 KiB of reads against the
2.4T expert lane's 6.95 GB/token.
The n-gram derivation was verified against the published checkpoint rather than trusted:
reconstructing the splitmix64 chain at the default seed 1234 reproduces the
layer_multipliersbuffer read out of the safetensors payload, and the head vocab sizesand offsets match entry for entry. Three silent C++ divergence sites are now named, the
worst being that the shard reassembly must be numeric, since a lexicographic key sort
gives shard_0, shard_1, shard_10 and permutes a 95 GiB table. Confirmed from the index:
128 contiguous shards, and the PLE sits on decoder layer 1.
Also recorded: vLLM has no dilated 1-D convolution anywhere, a confirmed negative with
zero hits in mamba/, csrc/ and tests/; the PLE conv is strided history at lags 9/6/3/0;
its signed-sqrt gate clamps before the square root, so the output floor is 1e-3 and tiny
scores are amplified rather than squashed; and
Qwen4ExpTextModelhas no final RMSNorm,which the natural copy of our DeepSeek-V4 tail would wrongly add.
The ratchet test, and the anchors it shifted
The 377 -> 378 pin bump changed
scripts/check-agent-record.pywith no evidence intests/scripts/test_agent_record.py, andcheck-pr-size.pyrefused it by name: agovernance-checker change owes semantic mutation evidence. The gate was right and the
red was this branch's, not noise.
test_qwen4_exp_row_is_inside_the_model_ratchetfollows the convention the dots3-noteand recipe-backfill tests set, naming the row the bump was taken for so that a count
raised to silence a broken parse cannot look identical to a count raised because a row
landed. It additionally pins that there is exactly ONE qwen4_exp row, because the MTP
head is an
mtpblock inside the same text config rather than a separately registeredarchitecture — so this is not the by-two shape IndexTTS-2.5 and dots3-note each took.
Adding it shifted that file by 45 lines and staled the two anchors
ENG-RECORD-ANCHOR-RATCHETkeeps into it, taking the rot from 31 to 33 STALE. Repairedto the true lines rather than raising the budget, which the baseline file forbids in
terms. That row's own record already describes this exact failure — an edit to the very
file the row cites — which is what the row exists to measure. Isolated rather than
assumed: the anchor ratchet passes on pristine
origin/main, so both new entries werethis branch's.
origin/mainmoved from a4f8ada to 993b62e during review, so the gate was rerun onthe merged tree rather than on the tree this branch was cut from.
Tracked by #1978.
Gates:
check-agent-recordok,check-model-checklistok,tests/scripts/test_agent_record.py+test_check_model_checklist.py130 passed,agent-preflight.sh --stagedok.FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude:claude-opus-5 [Claude Code]