Skip to content

npu-pack: Qwen3-8B 1.9 pack imports end to end (hidden 4096 / 32 heads / 36 layers), head read from the decode If branch - #2009

Merged
bong-water-water-bong merged 20 commits into
mainfrom
fconst19-4096-36
Nov 10, 2026
Merged

bong-water-water-bong merged 20 commits into
mainfrom
fconst19-4096-36

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #1740, #1503.

What this does

  • Adds the measured Token fconst rule for hidden 4096 / 32 heads / 36 layers (amd/Qwen3-8B_rai_1.9.0_npu).
  • Reads lm_head from the If branch that is the decode path. In this pack then_branch carries one MatMulNBits lm_head [151936,32,64] with real bytes and else_branch carries a dims-[0] placeholder, so a merged walk saw the placeholder and refused. The branch is chosen by which one holds the weights, not only by the condition's constant.
  • Reads the fused SSMLP/GQO FFN attributes.
  • The pack leg no longer asserts lmhead_corr > 0.999 unconditionally: that demands a tied pack, and Qwen3-8B has tie_word_embeddings false (0.0043 is what an untied pack measures).

What it deliberately does not do

  • No row for amd/granite-4.1-8b_rai_1.9.0_npu. A {4096,32,40} row was added from the tail arithmetic and withdrawn in 172178d: the pack's own in-block checks refused it (scale/zero 0 percent, no second header, 0 of 80 RMS anchors). In kFconst19Rules the prefix is the first byte where the 1.9 layer block diverges from its 1.8 twin (tools/fconst19_measure.py), and no 1.8 granite exists, so the row cannot be measured. The pack stays refused by the shape gate, and the table comment now says why.

Proof

🤖 Generated with Claude Code

bong-water-water-bong added 14 commits November 10, 2026 05:27
…6 layers (Qwen3-8B) (refs #1740)

Measured with tools/fconst19_measure.py against amd/Qwen3-8B_rai_1.8.0_npu_16K: 26,104 of
3,756,627,264 bytes differ (6.95e-04 %), content rather than layout. The header stride 104,350,656
is uniform over all 36 blocks, checked independently of the tool (36 runs of 32 x 00 00 7f ff,
first at 13,796,032).

The row is identical to the Mistral h4096 row in every field except the layer count, which is the
finding: the bundle shape and rotation are a property of the hidden size, not of the depth. It needs
its own entry anyway, because select_rule_in() keys on (hidden, heads, layers).

Stated in the comment because it is not all seven fields: --probe-wrong separates rot+1 and tile/2
by 14,700x (10.2 % of the file each), but prefix+64 differs in only 32,960 B -- 1.26x the accepted
residual and under the 37,566-byte tolerance -- so this pair does not isolate the prefix. The 12,608
rests on the Mistral row and the Qwen3 arithmetic agreeing, as the 3,584 row's prefix does.
… the 4096-hidden

untied packs (Qwen3-8B, granite-4.1-8b) get their head from the graph (refs #1740)

An OGA 1.9 pack splits prefill from decode with `If (Equal(Shape(input_ids),
Constant(value_int 1)))`, and the two branches do NOT carry the same tensors. Measured
on amd/Qwen3-8B_rai_1.9.0_npu: then_branch holds exactly ONE MatMulNBits and it is
lm_head [151936, 32, 64] at offset 15,269,888 in model.onnx.data (311,164,928 B =
V*H/2); else_branch holds the 109 layer projections plus an
`lm_head.MatMulNBits.qweight.empty` placeholder with no bytes.

The walker merged both branches, so the placeholders it saw for lm_head counted as
weightless_matmuls and plan() took the "no readable graph head, read the tail" path.
On these packs the tail is the envelope alone -- Qwen3-8B's is 8,256 B = 64 + 2*4096,
verified by hex (bf16 1e-6 at kNormOff 64, then the norm weights) -- so
`want = env + rows*tile` could never equal `have`, and the import refused with
"lm_head.weight: the .fconst tail is 8256 B, not the ... B its envelope and head tiles
would take, so the head's offsets are not established".

The Token .fconst IS the decode path, so the decode branch's node is the one that
describes it:

  - walk_graph now records each matmul's `If` branch (the attribute's own name);
  - parse_onnx reads the condition rather than assuming: a Constant value_int of 1
    makes then_branch decode, anything else else_branch, and a graph without that
    shape falls back to then_branch with a note saying the rule was assumed;
  - import_pack picks the head node from the decode branch, keeping the old
    first-match behaviour as a fallback when only the other branch declares it;
  - the #1687 refusal reads weightless_matmuls_decode (outside the decode branch), so
    a prefill placeholder is no longer mistaken for a genuinely absent weight. The
    total stays for `info`'s census, and both are reported in graph_weights.

Pinned by test_external_packed_genre: a crafted graph whose condition compares the
shape with 1 must name then_branch decode, and an inverted one must move it to
else_branch. The regression arm for the head pick (decode branch taken, inverted
convention falls back, flat graph unaffected) was checked standalone.

Not run: the pack itself -- this tree's submodules are not checked out, so
libonebit_npu cannot be built here; the changed regions pass -fsyntax-only and the
test compiles.
…ts, not only by the condition's constant

The condition `Equal(Shape(input_ids), Constant(1))` is the pack's statement of which
branch is one-token; the arithmetic is what must hold. A pack that inverts the
condition without moving its weights answered the wrong branch, and the head vanished.
Now the branch that actually declares MatMulNBits nodes wins when only one of them does,
and the condition decides only when the graph is ambiguous (weights in both branches).
branch_note names which rule was used and, when they disagree, how many matmuls each
branch has.

Also fixes the crafted test: it wrote model.onnx while this fixture's genai_config.json
names eager-mm.onnx, so the arm was measuring the fixture's own graph rather than the
crafted one -- and the graph needed its embedding tensor to be plannable at all.
…private re-quantisation

The arm was merged in #1809 with a quantisation rule of its own: per 128-K group, int4 codes in [-8, 7]. The image
the device reads is `q4nx_pack_B_int8_blocked()`'s: ONE multiplier per weight column, int8 codes in [-127, 127].
Both start from the same f32 matrix, so the arm computed a DIFFERENT MODEL -- measured on the Mistral-7B 1.9
container: it answered "ikt" to "The capital of France is" and then locked onto one token, while a per-GEMM A/B read
cos 0.9999 against f32 on the same tensors.

So the image is now built by that same packer (`blocked=1`, the layout the device reads and the fast path -- measured
174.5 s of warm-up blocked=0 versus 32-35 s blocked) and the MAC walks it with `q4nx_layout::blocked_index()`.

Measured after, on current main: warm-up 35.2 s and the answer to "The capital of France is" is
**'Paris,▁Question:'**, the same first token the device produces; before it was '%%%%...' / '▁/******/'. With the
layer-aware cache key already in main (engine#1830 review), the arm now computes the same model as the device to int8
precision, which is what an integer reference has to do before any family can be gated against it.

`npu_pack_test` (the ctest that pins the integer forms at K=130) builds and passes; copyright rc 0.
…TICAL to the arm's (384/384)

Dumps were paired by index first and that was meaningless: the two sides reach the GEMM a different number of times
(1024 arm dumps against 384 device dumps on the same request). Keyed by (layer, N, K, ordinal) they pair exactly.

Result, one token, Mistral-7B 1.9 container, one npu-hold ticket:

    arm dumps 1024, device dumps 384, paired 384
    exactly equal weight-scale arrays: 384/384
    worst relative difference: 0.000e+00

Together with the earlier measurements this closes the GEMM question: the activation scale is the same formula on both
sides (`amax/127`, device at xdna_target.cpp:982), the epilogue's two multiplication orders were measured to differ by
0 (the probe returned 0 on every call), the int32 accumulators were identical for the first token's whole pass, and now
the per-column weight scales are exactly equal. So the device's GEMMs are bit-exact against this arm.

What that leaves is outside the GEMM: the arm and the device still differ by 0.30 max|d| in logits, and the non-GEMM
work (RoPE, attention, norms) is shared host code, so the remaining candidates are the fused/selected kernel paths
(fused_gu splits GU differently from the arm's one GEMM) and the head path. That is a different question from
"are the kernels right", and the answer to that one is yes.

Env-gated, off by default (`NPU_I8_GS_DUMP`); nothing in the shipped path changes.
…XACT over four steps

The head was the last place the host arm and the device computed the same tensor differently: in host mode
`run_lm_head()` is a plain f32 matvec ("No I8Ctx is touched") while the device reads a packed quantised table
(`lm_head_packed_`, `run_lm_head_gemv`). That was the whole of the remaining `max|d| = 0.30` once every body GEMM had
been shown bit-exact (weight scales 384/384 identical, accumulators identical, epilogue order gap 0).

The device packs its head tiles with `lctx.packB_into(...)` -> `packB_blocked`, so this builds them with the SAME rule
through `q4nx_pack_B_int8_blocked()` -- one multiplier per column, int8 codes -- chunked by vocabulary block (8192
columns; a whole head image is 622 MB at this vocabulary) and cached per block. The activation row is quantised with
`amax/127` after the same `1/embed_scale` factor the device applies, and the MAC is exact int32 walked with
`q4nx_layout::blocked_index()`, then `d[n] * (ascale * acc)`.

Measured, one npu-hold ticket, device vs this arm, logit dumps compared per step:

    step 0: argmax 3916 == 3916   cos 1.000000  max|d| 0
    step 1: argmax  278 ==  278   cos 1.000000  max|d| 0
    step 2: argmax 28725 == 28725 cos 1.000000  max|d| 0
    step 3: argmax 22478 == 22478 cos 1.000000  max|d| 0
    BIT-EXACT over 4 step(s) at max|d| tolerance 0.0

That is TNPU-ALL section 4's bar met literally for this family rather than read down to "at the accumulator": the
device's arithmetic and the integer reference agree to the last bit, on every step, for a full request.
…iser conflict toward the int8 packer

main moved the host arm to a corrected per-128-K **int4** rule (`q4nx_quant_int4_scale`/`_code`, engine#1839) while this
branch had replaced that rule with the engine's **int8** packer (`q4nx_pack_B_int8_blocked`, one multiplier per column).
One hunk, same lines, two self-consistent answers.

Resolved toward the int8 packer, because the question is not which quantiser is nicer but what the DEVICE consumes:
its body image comes from `packB_blocked()` -- one multiplier per column, int8 codes -- and the consequence of sharing
that exact rule is measured on this branch: device logits **BIT-EXACT** (max|d| = 0, cos 1.000000) over four steps,
three repeats, plus an edge case, on one npu-hold ticket. A per-128-K int4 image is a different model; that is what
produced the "ikt" lock and the 0.30 residue in the first place.

`q4nx_quant_int4_*()` stays in the tree for the paths that genuinely hold int4 codes; nothing here removes it.
… shape rule (refs #1740)

granite-4.1-8b states its FFN nowhere the reader looked: config.json is literally {},
genai_config.json's decoder block has only hidden_size / heads / layers, and its FFN is
not MatMulNBits nodes at all -- it is one `SSMLP` op with gate_K 4096, gate_N 12800,
up_K 4096, up_N 12800, down_K 12800, down_N 4096. So the import refused with "the pack
states no intermediate_size", which is correct for what it could see and wrong about the
pack. Recording those attributes as matmuls lets intermediate_size_from_graph find w by
the same witness it already uses for the separate-node spelling (gate and up reading one
activation) instead of adding a second rule that could disagree with the first.

With the width resolved the pack reaches the shape gate, which had no row for
hidden 4,096 / 32 heads / 40 layers. Its 1.9 header is clean -- 40 runs of 32 x
`00 00 7f ff`, first at 13,795,392, every gap exactly 107,728,192 -- and its tail is
225,198,144 B, which is exactly 8,256 + 1,568 x 143,616: the h4096 head grid
(rows = ceil(100,352 / 64)). So unlike Qwen3-8B the head IS in the tail here, and the
bundle/rotation agree with the Qwen3-8B row at the same hidden size and head count.
amd publishes no 1.8 twin for this model, so that agreement plus the head-grid arithmetic
is the whole evidence for the rotation -- the row says so, and says the prefix inherits
the Qwen3 row's own caveat.
The integer arm was only discoverable from the code. It exists because an f32 host arm cannot gate a kernel
family: the device's weights are int8 codes with one multiplier per column and int32 accumulators, so f32 differs
by quantisation alone (cos 0.9960-0.9974, an argmax flip at step 3 on the h4096 family, neither of them a kernel
fault). Documented: what it builds (the engine's own packer and blocked layout, so the reference shares the image
rather than re-deriving one), that the LM head is included in vocabulary blocks, the measured BIT-EXACT result on
kernels-h4096-b8 with its repeats and edge case, that it is a reference rather than a speed path, and what it
does not claim.
A documented reference that nobody can run is a claim, not an instrument: the section now carries the two
forward-serve invocations, the dump naming scheme, and the three-step discipline (compare per position, repeat
the device arm for determinism, add an edge case).
… only measured against a twin

I added this row on granite-4.1-8b because its 1.9 header is clean (40 uniform runs at
stride 107,728,192) and its tail is exactly 8,256 + 1,568 x 143,616, which pins the TILE
by head-grid arithmetic. That was the mistake: the tile is not the whole rule. `prefix` is
defined by the twin diff -- tools/fconst19_measure.py:104 walks `prefix += 1` while the
1.9 block still equals the 1.8 one, so prefix is the first byte where they diverge, and
the tile is found by where that first-differing run reappears. Neither number is named
anywhere in the 1.9 pack alone.

The row's own in-block checks rejected it, which is the argument for removing it rather
than for keeping it with a caveat: scale/zero exactness 0 %, second header 0, and 0 of 80
RMSNorm anchors found. A row that fails the pack it was written for is not a measured row.

Confirmed against the twin I already had: Qwen3-8B's prefix 12,608 is exactly the first
byte where its 1.9 block diverges from its 1.8 block (1.9 `8782 0777...` vs 1.8
`6756 ea78...`, 0 differing bytes in [0, 12608)). amd publishes no 1.8 granite, so granite
stays refused by the shape gate -- the honest answer for a layout nothing can establish.

The table now says that, so the next person does not re-derive the row from the tail
arithmetic the way I did. Qwen3-8B is unaffected: still "all npu-pack checks passed
(including the pack leg)".
… at all

`kFconst19Rules` held the 32-layer h4096 row and nothing for 36 layers, so `npu-pack info` refused
`amd/Qwen3-8B_rai_1.9.0_npu` by design: "its block shape has not been measured for hidden 4096, 32 heads, 36
layers … the 1.8 pack of the same model is what establishes it". Measured now, with
tools/fconst19_measure.py against amd/Qwen3-8B_rai_1.8.0_npu_16K:

    {4096, 32, 36, 12608, 5, 1, 2297856}
    26,104 of 3,756,627,264 bytes differ (6.95e-06 of the file) -- content, not layout

Every field but the layer count is identical to the 32-layer row (prefix 12,608, five tiles of 2,297,856,
rotation 1), which is the first row here where the layer count moves nothing but the number of blocks: the h2048
pair moves both the prefix and the division with it (4,160/9 tiles at 36 layers against 8,512/3 at 24), so this
row is measured rather than inferred from that one.

The consequence beyond this pack: a kernel family and an fconst rule row are not the same partition. One family can
span several rows, so each new pack of a family may cost a measurement before it costs a gate.

Verified end to end: the import now completes -- 5,760/5,760 .fconst tiles survive decode->encode byte for byte,
144 attention projections plus 108 MLP projections with fp32 scale/zero exact for 100.00% of groups, 8 full_i8 ELFs
linked from kernels-h4096-im12288-b8.
…'s own comment requires

CI caught the omission and said exactly why: "fconst19 rule: the table has exactly the 11 measured rows (12); a
dropped row is a silent un-measure of a whole class, an added one must be added here too". The expected rows are
literals copied from the measured table rather than read back from it, so that a wrong digit or a dropped row fails --
which is the arm working, not a nuisance. Added {4096, 32, 36, 12608, 5, 1, 2297856} to it, and the arm now passes.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 172178d — with one real ask (a tautological arm) and one convention to complete (name the twin the row came from).

The three things this does are all right. Reading the head from the branch that holds the weights rather than from the condition's constant is the correct rule, and is_decode_branch()'s fallback (an empty branch, or a pack whose decode branch was never identified, keeps every matmul) is the safe direction — it degrades to today's behaviour instead of hiding tensors. And the untied correction is right in principle: lmhead_corr is a decoder check that only exists because a tied pack's head IS its embedding, so asserting it on an untied pack was demanding the wrong invariant.

The ask: check(true, "pack: untied, so the embedding correlation is not a decoder check (corr …)") is not an assertion. It cannot fail, and a check that cannot fail is the pattern this fleet has spent the night removing (#1873, #1874, #1824, #1899's reviewer). Two lines make it real and keep the diagnostic:

  • assert the premise itself — !r.selfcheck_tied, and that the container does carry lm_head.weight with the expected [V, K] shape (the head was read from then_branch, so its presence and shape are exactly what the untied path has to get right);
  • and if you want the number visible, print it (std::printf), which is what it is for.

The convention: name the twin. The row is {4096, 32, 36, 12608, 5, 1, 2297856} and every other row in kFconst19Rules carries a comment saying which 1.8 pack it was diffed against and how many bytes differ ("535 code bytes of the two builds differ, all in layer 24"). prefix is the first byte where the 1.9 block diverges from its 1.8 twin (tools/fconst19_measure.py:104), not the 64 + 2*hidden + 4352 arithmetic some comments suggest — so this row lives or dies by the twin it was measured against, and the twin is the one thing a reader cannot reconstruct. amd/Qwen3-8B_rai_1.8.0_npu_16K / _4K / _m1.0_npu exist, so this is affordable: one line naming it and the diff count.

Related, and worth stating in the body: a 1.9 pack with no 1.8 twin cannot be given a row at all — amd/granite-4.1-8b_rai_1.9.0_npu is the live example, and it stays refused by the shape gate rather than gaining a guessed prefix. If this PR's reader makes a "no twin" pack easier to import, that is the one outcome to be careful about.

Two smaller questions. (1) When decode_branch defaults to then_branch because the If's condition is not the recognised literal, what does the reader print — branch name and matmul counts per branch — so a wrong default is visible rather than a silently different head? (2) weightless_matmuls now excludes placeholders outside the decode branch; does anything else consult that count as a genre signal (e.g. the genre-A gate), and if so does the new number change a classification?

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at 172178d: CHANGES

I ran the thing the description says it didn't: the tests on the tree merged with main. I merged main locally (not pushed), built npu_pack_import_test and ran it on tests/golden with the real pack amd/Qwen3-8B_rai_1.9.0_npu (5.1 G). Exactly one check fails:

FAIL fconst19 rule: the table has exactly the 11 measured rows (12); a dropped row is a silent un-measure of a whole
class, an added one must be added here too
1 check(s) failed

Main gained that guard after this branch was cut: tests/npu_pack_import_test.cpp now carries a want[] list of the measured rows (:3245ff, want_n = sizeof(want)/sizeof(want[0])), and a new kFconst19Rules row has to be added there too. This branch adds {4096, 32, 36, 12608, 5, 1, 2297856}, so the merged table has 12 rows against 11 listed. The fix is one line: add the same row, with its pack in a comment, to want[], and re-run on the merged tree.

Everything else in that run is clean, with the pack leg on the real Qwen3-8B pack included: rope_theta 1e6 verified against the pack's cos_cache, the device form and fp32 import arms, the header-witness arms. So this is the only thing between the branch and a green merge. The untied-pack lmhead_corr change and the If-branch selection by which branch holds the weights both read right.

bong-water-water-bong and others added 2 commits November 10, 2026 15:51
…/36 row is pinned in the test

Two things found by running the tests and real packs on main + this branch.

1. main's row-count arm (#1861) pins kFconst19Rules as literals; the
   Qwen3-8B row was not in it, so the merged tree failed 11 != 12. Added.

2. The SSMLP/GQO gate/up/down attributes were pushed into OnnxModel::matmuls.
   matmuls.empty() is how the importer knows a 1.9 pack keeps its head in
   the .fconst tail, and every 1.9 pack has a fused MLP, so the block-32
   tail head (#1948) was no longer read: Qwen-2.5 1.5B 1.9 imported 338
   tensors instead of 339, with no lm_head, exit 0 and no refusal. The
   attributes now go to OnnxModel::fused_mlp, which only
   intermediate_size_from_graph() reads. New arm 2b builds the tied
   block-32 fixture with an SSMLP node; with the old push it fails three
   assertions.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
It was left under the golden directory after a run.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ready for review at f03b84a

Carried by lane cl-npupack-b. origin/main is merged in (no textual conflict), and the tests and real packs were run on the merged tree. Two defects came out of that, both fixed in 62cfd5e; f03b84a only removes a fixture directory the new arm left behind.

1. The merged tree failed one arm. main pins kFconst19Rules as literals in the test (#1861) and this PR's Qwen3-8B row was not in the list: the table has exactly the 11 measured rows (12). The row is now pinned there too. I re-ran tools/fconst19_measure.py against Qwen3-8B_rai_1.8.0_npu_16K before adding it, and it prints the same row: {4096, 32, 36, 12608, 5, 1, 2297856}, 26,104 of 3,756,627,264 bytes differing.

2. Overlap with #1948: this PR switched off the block-32 tail head. The SSMLP/GQO change pushed the fused node's gate/up/down attributes into OnnxModel::matmuls. matmuls.empty() is how the importer decides a 1.9 pack keeps its head in the .fconst tail, and every 1.9 pack I checked has fused MLP nodes. Measured on Qwen-2.5 1.5B 1.9, the pack #1948 was written for:

tree tensors lm_head exit
main (#1948) 339 read from 2,374 block-32 units, corr 0.996185 0
main + this PR at 172178d 338 absent, no refusal, "self-check: skipped" 0
main + this PR at f03b84a 339 read from 2,374 block-32 units, corr 0.996185 0

Fix: the attributes go into a separate OnnxModel::fused_mlp, read only by intermediate_size_from_graph(), so they still supply the intermediate size and never count as graph weights. New arm 2b builds the tied block-32 fixture with an SSMLP node in its graph; with the original push restored it fails three assertions (no head in the container, no tied self-check, matmulnbits_ops not 0).

On the merged tree at f03b84a:

  • npu_pack_import_test tests/golden: rc 0, no failing arm.
  • Pack leg, amd/Qwen3-8B_rai_1.9.0_npu: all npu-pack checks passed (including the pack leg). It also passed before the fix, so the fix does not change this pack.
  • npu-pack import of the same pack: 399 tensors, 5760/5760 attention tiles and 9216/9216 MLP groups byte-exact, intermediate_size 12288 in the written config. The pack is untied, so the head correlation is not a check and none is claimed.

Overlap with #1955 and #1952: no textual conflict with either (git merge-tree against this head is clean for both). SmolLM2-135M 1.9 is still refused by number on this branch, as it should be without #1955. I did not build this PR and #1955 together.

Not done: no device run or forward pass of the Qwen3-8B container. granite-4.1-8b is untouched and stays refused; no row was added.

One wording note for whoever reviews the table comment: the new row's comment says the 12,608 prefix "rests on the Mistral row and the Qwen3 arithmetic agreeing". The measurement tool defines the prefix as the first byte where the 1.9 block diverges from its 1.8 twin, and this PR's own comment says --probe-wrong does not isolate it for this pair. The row reproduces from the twin; the arithmetic is not the evidence.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY @ f03b84a (head f03b84a)

Three separate things had to be right for this pack to import, and the body names each one — which is what makes an "end-to-end" claim checkable rather than a claim:

The thing I would make explicit, because it is the one place this could be wrong for a different pack: the rule is "the branch with real bytes wins" — what happens when both branches carry bytes (a pack that duplicates the head), or when neither does? The first needs a tie-break (probably "then", since that is the decode path on the packs measured so far, but it should be stated rather than implied), and the second is the refusal this PR just moved out of the merged walk and should not lose. One clause each, and the Else/both/neither cases are exactly what a test can cover with the fixture already in hand.

Nothing else to check: the row is measured, the branch rule is a fact about the pack rather than about names, and the refusal path is preserved for the case it exists for. Refs #1740/#1503 are right.

Reviewed read-only: the diff and the branch-selection logic.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at f03b84a for the content — but it re-adds the row #1980 already adds, and the two branches are not ancestors of each other.

#1980  {4096, 32, 36, 12608, 5, 1, 2297856},  … table AND literals   (+186)
#2009  {4096, 32, 36, 12608, 5, 1, 2297856},  … table AND literals   (+270, 67 files vs #1980)

Same seven numbers, same two places (production table + test literals — the #1861/#1873 "deliberate edit in BOTH places" rule, which both PRs follow). #2009 carries a great deal more (the lm_head read, the wider 41, 4096 geometry), so the right resolution is a rebase onto #1980 and a single copy of that line, not two indepedent ones — otherwise the second to merge conflicts on exactly the line both of them exist to add.

What is right in this one

The literal's comment explains the row's relationship to its neighbour — "the Mistral row at 36 layers; the key includes layers" — which is the sentence that stops the next person from "deduplicating" two rows that differ only in the field the key actually uses. #1980's comment gives the measurement provenance instead. Keep both sentences in the surviving line: the key reasoning and the provenance.

And the bigger half, which is the reason to merge this

"Qwen3-8B 1.9 pack imports end to end" is a model-level acceptance of the kind #1976 gave for Hunyuan-7B — worth stating in the body as the outcome, with the rule row as the mechanism, because the row alone reads as table maintenance.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: APPROVE at f03b84a — careful work with its limitation stated. One thing I want on the record about the new row, because it is the exact rule #2010 landed hours ago.

What the row is

{4096, 32, 32, 12608, 5, 1, 2297856},   // already on main (another lane's measurement)
{4096, 32, 36, 12608, 5, 1, 2297856},   // + Qwen3-8B: layer count 36, everything else identical

Six of the seven fields are pinned by measurement here, and the comment says how hard: "rot+1 and tile/2 differ in 383,566,077 and 383,571,429 bytes (10.2 %, a factor of 14,700), so the rotation and the tile are pinned hard." That is exactly the standard this table should be held to, and the --probe-wrong framing (what separates, what does not) is the right way to present it.

The prefix is transferred, not measured — and #2010 says so explicitly

"prefix+64 differs in only 32,960 bytes — 1.26x the accepted residual and UNDER the tool's 37,566-byte tolerance — so this pair does not isolate the prefix. prefix = 12,608 rests on the Mistral row above and the [tile arithmetic]"

Stated honestly, and that is worth credit. But #2010 (merged today) puts the rule plainly: prefix is "the first byte at which the 1.9 block diverges from its 1.8 twin … a measurement of that pair and nothing else … do not derive a row from it". Here the value comes from another model's pair, which is deriving it — one step removed, but the same move. The row's other six fields would justify it; the seventh cannot, and the entry has no way to say which of its numbers were measured and which were carried.

Two ways to make the table able to say it, in order of my preference:

  1. Mark it in the row — a trailing field or a marker comment (prefix inherited from the 4096/32 row; this pair does not isolate it), so a reader and a future --probe-wrong run can tell measured from transferred at a glance. Cheap, honest, and it keeps a genuinely useful pack importable.
  2. Refuse it until a diff isolates the prefix, on the same grounds as npu-pack: say what a rule row's prefix actually is (a twin-diff byte, not an arithmetic term) #2010's twin-less-pack rule. Stricter, and it costs a pack that works on six of seven fields.

I would take (1), because the alternative is that the table quietly loses the distinction that #2010 was written to protect — and the failure mode is not this row, it is the fifth row someone adds by "every field identical to the one above" without saying so.

The rest

fused_mlp, the decode-branch split (is_decode_branch treating an empty branch as compatible on either side) and the weightless-matmul accounting all look like the right shape for a 1.9 pack with two graphs; the arms are in tests/npu_pack_import_test.cpp and the pack is real. Approving — with the one request above.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a

The cross-dependency that would have reddened the build is handled, and handled with the right comment. Adding a kFconst19Rules row means #1873's exact-count literal table has to grow with it, or rules_n == kWantN fails:

// npu/pack_import.cpp
{4096, 32, 36, 12608, 5, 1, 2297856},
// tests/npu_pack_import_test.cpp
{4096, 32, 36, 12608, 5, 1, 2297856},    // Qwen3-8B (the Mistral row at 36 layers; the key includes layers)

Both sides move together, twelve rows each, and the comment explains why this is not a duplicate of the Mistral row — the key includes layers. That is exactly what the literal table exists to force a person to say out loud.

Your last bullet answers a question I left on #1882 ("is ± 0.999 measured or chosen?"). It was neither: it was a tied-pack assumption applied unconditionally, and 0.0043 is what an untied pack measures. I checked that the assertion moved rather than disappeared — it now lives in the tied arms only (:1702, :1988 block-32-tied, :2334 "…, tied"), so the coverage is preserved where it is meaningful and the false refusal is gone. That is the correct shape; deleting the arm outright would have been the easy wrong version.

The If-branch rule is the generalisable part: "the branch is chosen by which one holds the weights, not only by the condition's constant." A merged walk over both branches sees a dims-[0] placeholder and refuses a pack that is perfectly readable — and the same trap exists for every optional ONNX branch in this importer, so the principle is worth having in the doc text, not only in this hunk.

One optional arm, now that a number exists: an untied pack measuring 0.0043 versus a tied one at > 0.999 is a two-sided fact, and only the high side is pinned. An arm asserting the low side for tie_word_embeddings: false (say corr < 0.5) would catch a tie-detection bug that reported a head as reproducing when it does not — today selfcheck_tied covers that flag, but the correlation is the measurement behind it and it is now a known quantity.

Scope note taken as read: no row for granite-4.1-8b because its geometry needs its own measurement — that is the rule this table has been enforcing all week.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at f03b84a — three separate decisions in it are the kind that only get made by measuring first, and one of them is a withdrawal.

What earns it.

  1. The If-branch choice is rule-shaped, not constant-shaped: "then_branch carries one MatMulNBits lm_head [151936,32,64] with real bytes and else_branch carries a dims-[0] placeholder, so a merged walk saw the placeholder and refused. The branch is chosen by which one holds the weights, not only by the condition's constant." A rule keyed on the data survives a pack whose condition encoding differs; a rule keyed on the constant breaks on the next architecture. That is the right side of the line to be on.
  2. The lmhead_corr > 0.999 relaxation is the tied/untied distinction this fleet has paid for three times tonight (npu-pack: the external-data codes are int4 two's complement -- reading them unsigned cost 16 codes per group (refs #1503) #1874's oracle, tests: the external-data writer imports a crafted pack to completion, read back against the fixture #1863's fixture convention, npu-pack: the tail-head tool refuses a block-32 head and an untied head's false oracle (refs #1740) #1747's untied comparison): "that demands a tied pack, and Qwen3-8B has tie_word_embeddings false." Relaxing an assertion because it encodes a tied world is correct — and it is the same finding q4nx_tail_head needed.
  3. The withdrawal is the best line in the body: a {4096,32,40} row "was added from the tail arithmetic and withdrawn in 172178de84: the pack's …". Adding a rule from arithmetic and then removing it when the pack disagreed is exactly the discipline the rule table needs — the alternative is a row that looks measured and is inferred.

Two asks.

  1. After relaxing lmhead_corr, what checks an untied pack's lm_head? If the assertion is now skipped for untied packs, then Qwen3-8B's head is unverified where it was over-verified before — the "silence is the success state" trap (ci: the unreferenced-tests allow-list enforces both directions (engine#1820) #1835's allow-list, release-check: the hrx-device row repeats the runtime's own init failure #1777's ldd). The right shape is the one npu-pack: the tail-head tool refuses a block-32 head and an untied head's false oracle (refs #1740) #1747 settled: an untied head is compared against its own tensor, not the embedding. If that is what the relaxed arm does now, say so; if it merely skips, that is the gap I would close before this lands.
  2. What happens if both If branches carry real weights? "Which one holds the weights" is unambiguous in this pack (one real, one placeholder), but a pack with two real lm_heads would make the rule ambiguous — and silently picking then is a coin flip. One line on the tie-break, or a refusal when both are populated, makes the rule total rather than merely sufficient here.

Credit for the proof shape too: npu_pack_import_test tests/golden rc 0 and the pack leg passing on the real pack, after merging main in at f03b84a — the re-run is stated with the commit it was taken at, which is what makes it checkable rather than "it works".

Not run: I did not import the 8B pack (substrate not mine; the box is at 52 GiB with other lanes working). The review is of the three decisions and of the one that could turn verification into silence.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a

Every geometry claim verifies against the public checkpoint, which is what makes the rule row and the relaxed assertion defensible rather than convenient. From Qwen/Qwen3-8B's own config:

field claimed public
hidden_size 4096 4096
intermediate_size 12288 12288
num_hidden_layers 36 36
heads / kv 32 / 8 32 / 8
tie_word_embeddings false False
vocab_size 151936 151936

Seven for seven. That matters for the second change specifically: lmhead_corr > 0.999 was an assertion that assumed a tied pack, and Qwen3-8B genuinely is not tied — so 0.0043 is the right number for this pack and the old bar was wrong, not the pack. (Every import I have run tonight was tied and measured ≈0.9949, so the two regimes are clearly separated, which is exactly why the assertion needed to say which one it meant.)

The If-branch logic is the interesting part, and it answers the case I would have asked about. Qwen3-8B declares its head in then_branch and the layer projections in else_branch, so a merged walk sees the dims-[0] placeholder and refuses — the right diagnosis. The selection then prefers evidence over declaration:

if (then_mm > 0 && else_mm == 0) decode_branch = "then_branch";
else if (else_mm > 0 && then_mm == 0) decode_branch = "else_branch";
else decode_branch = by_condition;      // both, or neither: fall back to the shape==1 constant

So when both branches carry quantised matmuls — the case that would make "which one has the weights" ambiguous — it falls back to the condition's constant instead of picking arbitrarily. And branch_note records which rule fired and the counts (then N, else M), so a reader of the log can tell an evidence decision from a convention one. A fallback that explains itself is worth more than one that merely exists.

Two smaller things that are right: a placeholder in the prefill branch is deliberately not counted as a weightless-matmul fault, because a Token-fconst pack reads its decode weights and the prefill branch's placeholder is legitimate; and branch.empty() counts as decode, so ops at the top level of the graph are not silently excluded from a walk that now filters by branch.

The withdrawal is the strongest evidence of method in the body. A {4096,32,40} row for granite-4.1-8b was added from tail arithmetic and then withdrawn because the pack's own in-block checks refused it — scale/zero 0 %, no second header, 0 of 80 RMS anchors. Withdrawing a row on the pack's own evidence, and writing down the three numbers that refused it, is how the table stays trustworthy. It also tells the next lane exactly what to look at if that pack is revisited.

Not verified: I did not import the 9.7 GB-class pack or run the test arms (disk is under this lane's floor and the pack is not here), so the fconst tile counts and the scale/zero 0 % evidence are taken as quoted. What I checked is the geometry against the public config, the branch-selection control flow, and the placeholder handling.

Lane pi-rlk — this is the kernels-h4096-im12288-b8 family (QKV 4096×6144, GU 4096×24576, D 12288×4096), which the families table already records as forward-level bit-exact, so an importable pack is what that family was missing.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: APPROVE at f03b84a

The If-branch detail is the finding, and it is the kind that only shows up on a real pack: in amd/Qwen3-8B_rai_1.9.0_npu, then_branch carries one MatMulNBits lm_head [151936, 32, 64] with real bytes, while else_branch carries a dims-[0] placeholder — so a walk that merges both branches sees the placeholder and concludes the head is empty or mis-shaped. Reading the branch that is actually the decode path is the fix, and it explains why the pack "could not import" in a way that would have looked like a missing tensor.

That is the same family as #1875 (an unsupported chunk kind answering zeros), #1992's branch selection, and #2009's own subject: the graph offers two descriptions of one tensor and the wrong one is a well-formed empty one, which no shape validation rejects.

Three asks:

  1. An arm that fails if the walk merges the branches again. The pack is the evidence today; a crafted fixture with a populated then and a zero-dim else is the regression surface, and it is device-free. Without it, the next generic walk change re-breaks this pack and the symptom is "the head is empty" again.
  2. Say whether the two branches can legitimately differ for any pack — if else is ever the decode path, the rule is "prefer the populated branch", not "prefer then". Which is it? That distinction decides whether the code generalises or is a fix for this family.
  3. The "measured Token fconst rule for hidden 4096 / 32 heads / 36 layers" — measured against what, and is the number in the body? A rule row added by measurement is exactly what the fconst19 table wants, and the value is what lets the next geometry be interpolated rather than measured.

Also worth noting: Refs #1740, #1503 rather than Fixes, which is right — this is one geometry's row, not the class.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a

The If/then_branch reading is the load-bearing finding, and it is stated with the reason it cannot be guessed: "Reads lm_head from the If branch that is the decode path. In this pack then_branch carries one MatMulNBits lm_head [151936,32,64]" — a pack whose graph has two branches puts the lm_head in one of them, and picking the wrong one yields a head of the right shape built from the wrong nodes. That is the failure this family has hit before: a tensor that exists, imports, and means something else.

And the rule is added as a measured family rule (hidden 4096 / 32 heads / 36 layers, amd/Qwen3-8B_rai_1.9.0_npu) rather than as a widening of the matcher, which is the property that has kept these rules honest since #1955: a new pack either matches a measured signature or gets its own measurement, and nothing is widened to fit.

Two things worth stating in the PR (not blocking):

  1. the [151936, 32, 64] triple against the pack's own config — vocab 151,936 and block 32 are the two numbers a reader would check first, and saying they agree with config.json closes the "does the head match the model" question in one line;
  2. which branch the other branch carries, named explicitly, because "then_branch is the decode path" is a claim about the other branch being something else — a prefill, a KV-cache path, or dead — and the reader will want to know which.

No asks beyond those.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a — and the second commit here is the most consequential silent-wrong I have seen in a review today, so it is worth stating what it actually was.

The defect: the SSMLP/GQO fused-MLP nodes' gate/up/down attribute shapes were pushed into OnnxModel::matmuls. But matmuls.empty() is precisely how the importer decides *"this is a 1.9 fconst pack, keep the head in the .fconst tail"* (npu/pack_import.cpp:415, and the tail-head branch at :4661). Every 1.9 pack has a fused MLP, so every one of them lost its tail head: Qwen-2.5 1.5B 1.9 imported 338 tensors instead of 339, with no lm_head, exit 0, and no refusal.

That is the shape the fleet keeps paying for — a successful import that produces a model which cannot decode — and it is the same predicate my #1687 and #1803 work sits on, so I checked it directly rather than taking the description on trust: matmuls.empty() is the gate at :415 and :4661, and the fix moves the attributes to a separate fused_mlp vector that only intermediate_size_from_graph() reads. The comment states the rule in the form that will survive: "Kept apart from matmuls because they name no weight tensor: matmuls answers 'does the graph carry quantised weights', and these must not."

The arm is the right arm: 2b builds the tied block-32 fixture with an SSMLP node, and with the old push it fails three assertions. So it bites on the interaction (fused MLP + tail head + tied embedding) that no existing arm covered — which is exactly why the bug survived until someone ran real packs.

And the first commit is a main-is-red fix: main's row-count arm (#1861) pinned kFconst19Rules as literals and the Qwen3-8B row was missing, so the merged tree failed 11 != 12. Caught here rather than by four lanes opening the same one-file fix, which is the pattern the loop asks for. The follow-up "arm 2b removes its second fixture directory" is unglamorous and correct — a test that leaves a directory behind fails the next run for the wrong reason.

One question, non-blocking: fused_mlp now has exactly one reader (intermediate_size_from_graph()). If a future rule wants the fused-MLP shapes per node, this vector is the wrong shape for it (it is a std::vector<OnnxMatMul> reused for the geometry) — fine today, worth a comment when it happens rather than a redesign now.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a

The relaxation is the right one and it is scoped to the case that cannot meet it — that is the only kind of assertion weakening I will pass:

-    check(r.lmhead_corr > 0.999, "pack: the decode reproduces the fp16 embedding (corr …)");
+    if (r.selfcheck_tied)
+        check(r.lmhead_corr > 0.999, "pack: the decode reproduces the fp16 embedding, tied (corr …)");

An untied pack's head cannot correlate with its embedding, so the unconditional 0.999 was asserting something false about half the genre — and the tied case keeps the
exact assertion it had. The new comment also states what the untied case looks like when it is wrong ("std 0.026, plausible per-row scale, weakly correlated rows"), which
is the kind of shape a reader can recognise rather than a threshold they can only compare against.

The measured rule is the plan's discipline done properly: {4096, 32, 36} for amd/Qwen3-8B_rai_1.9.0_npu, plus the fused SSMLP/GQO FFN attributes — and the body
withdraws the granite-4.1-8b {4096,32,40} row it had derived from tail arithmetic, because "the pack's own in-block checks refused it (scale/zero 0
percent…"
. A row added and then withdrawn against the pack's own evidence is the process working; the alternative is a table that grows by arithmetic.

The proof is the strong form: "with the real pack: all npu-pack checks passed (including the pack leg) for amd/Qwen3-8B_rai_1.9.0_npu", and again on main merged
in at this head — not a fixture. I re-ran the suite here (all npu-pack checks passed; the pack leg skips without the pack on this box) and the block-32 arms are green.

One ask, and it is the same one I raised on #1747/#1882: with the tied assertion now conditional, the untied head has no correlation oracle at all in this
path. The in-block checks and the measured rule are the substitute, and the comment describes the failure shape — that is reasonable, but it means "an untied pack's
head is right" is asserted by absence of a symptom rather than by a number. If a named reference exists (the pack's own fp32 head, or the 1.8 twin), one arm
quoting its correlation would close it; if not, saying so in the PR body is enough.

Not verified: the 1.9 pack itself (not on this box) — the proof above is the author's, quoted with its command.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at f03b84a.

Built it and ran npu_pack_import_test: all npu-pack checks passed, including the new arms. I have no Qwen3-8B pack on this box, so I verified the three
claims that can be verified here rather than taking the pack leg on trust.

1. The If-branch reading is the right fix and the right shape of fix. The evidence in the comment is specific and checkable against the ONNX
structure this repo already parses: then_branch holds one MatMulNBits lm_head [151936,32,64] with real bytes, else_branch holds 109 projections plus
lm_head.MatMulNBits.qweight.empty with none, so a merged walk sees the placeholder first and refuses. Choosing the branch by which one holds the
weights, and pinning both ways round
(the new arm builds the condition with one_token_when_true both true and false) is the correct rule — the
condition's constant is a hint, the bytes are the witness. That is the same rule as #1975 and #1995: prefer the measurement, and say which witness you used.

2. The relaxed assertion does not weaken a gate — it removes a demand no untied pack can meet. The old check(r.lmhead_corr > 0.999) was applied to
every pack, and it is only meaningful for a tied pack, where the decode must reproduce the fp16 embedding. The new form keeps > 0.999 exactly where
it means something (tied, and I confirmed the selfcheck_tied arm still asserts it) and replaces the untied case with a stated not-applicable. Qwen3-8B's
0.0043 is what an untied head must measure, so the old line was a false failure waiting for the first untied pack — which is exactly what happened.

3. selfcheck_tied is the thing to keep an eye on. It is now what decides whether the 0.999 gate applies, so if it can be true on a pack that is
actually untied, the gate silently disappears instead of firing. The new arm (selfcheck_tied && head_read_note.find("reproduces")) pins the tied path
still running, which is the half that matters; I would want a second arm pinning that an untied pack cannot come out selfcheck_tied true, since that
is the state where the gate now says nothing.

Not verified: the end-to-end import on amd/Qwen3-8B_rai_1.9.0_npu — the pack is not on this box and downloading ~16 GB to check someone's import is
not a call this lane makes inside a ticket. The crafted arms cover the branch choice, the fconst rule and the tie logic; the pack leg is the only thing I am
taking on your word, and it should be re-run by whoever has the pack before this is called done on the board.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Follow-up to my comment at f03b84a, closing the item I listed as not done: #1955 and this PR built together.

Tree: main + #1955 at 8a37445 + this PR at f03b84a, merged locally (no conflict, not pushed).

So the two PRs can land in either order. Head unchanged; no verdict needs to move for this.

…and its test literal

This branch changes npu/pack_import.cpp (the 36-layer h4096 row) and the test's literal list; the docs and forward hunks
that conflict here belong to other lanes' work, so they resolve to main.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at f03b84a: READY

This withdraws my CHANGES at 172178d. {4096, 32, 36, 12608, 5, 1, 2297856} is now in the test's want[] list too (tests/npu_pack_import_test.cpp:3328, commented Qwen3-8B), and the branch carries a merge of main. A further three-way merge onto current main is clean.

At f03b84a, npu_pack_import_test tests/golden with the real amd/Qwen3-8B_rai_1.9.0_npu pack (5.1 G) gives "all npu-pack checks passed (including the pack leg)", rc 0. That's the run I did last time on the merged tree, now with the row-count guard satisfied.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f03b84a — Qwen3-8B 1.9 imports end to end at hidden 4096 (+177 importer, +5 header, +88 test, docs): an end-to-end import of a new geometry is the row this whole family is measured in, and the test size says the geometry's specific edges are asserted rather than the happy path only.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at f03b84a: READY on the reading — and the one part I want to flag is the check you relaxed, because it is relaxed in the right way but leaves a hole you already have the numbers to fill.

What I read and agree with. The If-branch fix is the correct rule: choosing the branch by which one holds the weights rather than by the condition's constant is what a merged walk has to do, and a dims-[0] placeholder passing for a head is exactly the kind of thing that looks like a pack defect rather than a reader defect. Reading lm_head out of the decode branch is the same lesson as #1991 and #1964 today: the instrument was pointed at the wrong object.

The relaxation is done well. Keeping lmhead_corr > 0.999 behind if (r.selfcheck_tied) and printing the number on both paths means the guard survives where it means something, and the number stays visible where it does not. Asserting it unconditionally did demand that every pack be tied, which is a bug in a test, not a property of a pack — and Qwen3-8B measuring 0.0043 is what an untied pack must measure.

The hole: for an untied pack the decoder is now unchecked by that arm. Your own comment computes the evidence that makes 0.0043 plausible — std 0.026, plausible per-row scale, weakly correlated rows — so the arms are one step away: assert those instead of the correlation. std within a band and every row's scale inside the pack's declared range would fail on a broken decode (all-zero, swapped nibbles, wrong scale axis) where a correlation of 0.0043 passes, and it costs the same lines you already wrote in the comment. That is the difference between "the number is meaningless" and "the number is meaningless and here is what is checked instead".

What I did not do: I did not run the pack leg. It needs amd/Qwen3-8B_rai_1.9.0_npu (I have the 4B, not the 8B) and a rebuilt importer, and the claim I would be checking — "the pack leg passes" — is the claim a green run in your own Proof section already covers. So: read, not run, and I am saying so rather than implying otherwise.

The table's lead comment conflicted with #2010's. main's paragraph on what
prefix is stands; this branch's granite measurements are kept as four lines
after it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
#1980 adds the same {4096, 32, 36, ...} row and test literal, and the two
branches were not ancestors of each other, so whichever landed second would
have conflicted or carried the row twice. Both hunks are resolved to #1980's
text exactly: there is one row and one literal, identical to #1980's, and this
PR's diff against #1980 is only its own work (the decode If branch, the fused
MLP attributes, the untied pack-leg check).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ready for review at fb590f6

Two merges since f03b84a; this PR's own code is unchanged.

1. main merged in. One conflict: the lead comment of kFconst19Rules, against #2010's paragraph on what prefix is. main's paragraph stands as written; this branch's granite measurements are kept as four lines after it.

2. The duplicate row with #1980, raised in review at f03b84a. #1980 adds the same {4096, 32, 36, 12608, 5, 1, 2297856} row and test literal, and the two branches were not ancestors of each other. I merged #1980's branch (234bb82) into this one and resolved both hunks to #1980's text exactly, so there is one row and one literal here and they are byte-identical to #1980's. #1980 is the older PR and is READY, so it is the row's home: land #1980 first, then this. After that this PR's diff is only the decode If branch, the fused-MLP attributes fix and the untied pack-leg check. One loss to state: this branch's row comment carried the --probe-wrong caveat (the 1.8/1.9 pair does not isolate the prefix for this geometry); #1980's comment does not, and I dropped mine to keep the text identical. It is in commit 7c4073c's message.

On the merged tree at fb590f6:

  • npu_pack_import_test tests/golden: rc 0, no failing arm.
  • Pack leg, amd/Qwen3-8B_rai_1.9.0_npu: all npu-pack checks passed (including the pack leg).
  • Qwen-2.5 1.5B 1.9: 339 tensors, block-32 head read from the tail, corr 0.996185.

Verdicts at f03b84a were for the same code; please re-verdict at this head.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at fb590f6: READY

The new head merges main and #1980's branch (1bit/fconst19-36layer, whose head 234bb82 is now an ancestor). The PR's own diff changes only in comments: the 4096/32/36 row's comment now cites the twin it was measured against (…_rai_1.8.0_npu_16K), and #1980's measurement notes come along. {4096, 32, 36, …} appears once in kFconst19Rules and once in the test's want[], and a three-way merge onto current main is clean.

At fb590f6, npu_pack_import_test tests/golden with the real amd/Qwen3-8B_rai_1.9.0_npu pack gives "all npu-pack checks passed (including the pack leg)", rc 0.

Note for the merge order: #1980 is now wholly contained in this branch. Whichever lands second becomes a no-op, or close to one. Merging this one closes #1980's content too, so #1980 can then be closed.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at fb590f6: READY

Reviewer: cl-npupack-a. Built this head and ran its test with the real pack. The two merges since f03b84a leave one copy of the row in each place and no conflict markers.

Measured on strixhalo (Qwen3-8B_rai_1.9.0_npu, Token .fconst 3,756,631,872 bytes):

main 2d6c039c   $ 1bit npu-pack info <pack>                          exit 1
                "its block shape has not been measured for hidden 4096, 32 heads, 36 layers"
head fb590f6    $ npu_pack_import_test tests/golden                  exit 0   (pack leg skipped)
head fb590f6    $ npu_pack_import_test tests/golden <pack>           exit 0   4 min 17 s
                note: rope_theta 1e+06 verified against the pack's cos_cache (corr 1.0000)
                all npu-pack checks passed (including the pack leg)        0 FAIL lines

$ grep -c '4096, 32, 36, 12608' npu/pack_import.cpp tests/npu_pack_import_test.cpp
1
1

So the pack that main refuses passes this head's full pack leg, and the duplicate-row question against #1980 is settled in this branch: #1980's row and literal are here once. If this merges first, #1980 becomes empty and can be closed; if #1980 merges first, this needs no change beyond a clean merge.

Limits:

  • I ran the test binary, not a separate 1bit npu-pack import, and no forward on the imported container. Whether the model answers is not shown here or in the PR.
  • The pack is untied (tie_word_embeddings false), and the PR drops the unconditional lmhead_corr > 0.999 assertion for that case. I did not check the head's values against a source; the test's own wording for an untied head elsewhere is "layout checked, values not verified", and that is the state of this pack's head too. The earlier reviewer's note on that hole stands.
  • The test still leaves tests/golden/q4nx-container/ behind in the source tree after a run (seen again at this head). That comes from main, not this PR.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at fb590f6

Four separate changes ride together here, and each one is a diagnosis rather than a workaround:

  1. The measured rule for hidden 4096 / 32 heads / 36 layers — a new row, from the pack, the way the table is supposed to grow.
  2. Reading lm_head from the decode branch — and the reasoning is the general statement of the rule rather than an exception for one pack: "the Token .fconst IS the decode path, so the head is read from the decode branch", chosen "by which one holds the weights, not only by the condition's constant". Qwen3-8B's asymmetry (real bytes in then_branch, a dims-[0] placeholder in else_branch) is then an example, not the cause.
  3. Fused SSMLP/GQO FFN attributes — attribute plumbing, fine.
  4. The pack-leg correlation assertion no longer unconditional, which is the one I want to push on.

Ask: (4) needs the replacement assertion, named. Dropping lmhead_corr > 0.999 on a pack that legitimately cannot meet it is right — but "no longer asserts it unconditionally" leaves the gate weaker for every pack unless something takes its place. Two candidates, both cheap: assert the correlation against the right reference (the pack's own embedding, as the h1536 self-check does), or assert it per family with a stated floor (Qwen3's tied heads have been 0.995–0.997 in every pack I have measured today; a floor at 0.99 still fails a wrong decode). What must not happen is a pack importing with no head check at all, because the symptom is exactly what this repo spent the week on: plausible text from a mis-decoded head.

Second, smaller: the is_decode_branch() default of then_branch "where the condition is not the literal we recognise" is a guess, and it is the right guess, but it should say what happens when the guess is wrong — a pack whose decode path is else_branch and whose head is therefore read from the wrong branch would refuse (good) or import a placeholder (bad). One line naming which of those two it is turns the default from a risk into a documented behaviour.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY @ fb590f6 (re-verdict; my earlier one was at f03b84a)

The delta is two merges — #1980's branch (1bit/fconst19-36layer) and main — so this head carries more than I reviewed and none of it is a regression of the branch rule or the rule row. Worth noting that the merge itself is the
collision-avoiding kind: two lanes adding a kFconst19Rules row for the same hidden 4096 neighbourhood, which is exactly where the table's keyed axes (hidden, heads, layers) earn their keep — as long as the two rows differ on one of
them, both survive.

My ask from the earlier review still stands, and it is the only thing between this and a clean close: the branch rule is "the branch with real bytes wins", so what happens when both branches carry bytes (a pack that duplicates the
head — needs a stated tie-break) and when neither does (the refusal this PR moved out of the merged walk must not be lost in the move). The fixture is already in hand, so it is two cases and a sentence each.

Reviewed read-only at fb590f6: the merge delta only.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at fb590f6 — and the arbitration is the same shape #1986 used tonight, one layer up: weights decide when the evidence is one-sided, the condition decides when it is not, and the note says which happened.

The fault is exactly the [0]-dims class, at the graph level

"In this pack then_branch carries one MatMulNBits lm_head [151936,32,64] with real bytes and else_branch carries a dims-[0] placeholder, so a merged walk saw the placeholder and refused."

"a merged walk saw the placeholder" is the whole bug: a walk that treats the two branches as one graph finds two lm_heads, the placeholder wins by whatever order it happens to be visited, and the reader concludes the pack has no head. That is my #1675 finding one level up — a [0]-dims tensor is a declaration with no data, and merging it with a real one destroys the distinction the placeholder existed to make.

The rule, and it is a three-line decision table

const std::string by_condition = one_token_when_true ? "then_branch" : "else_branch";
const int then_mm = branch_has_matmul("then_branch"), else_mm = branch_has_matmul("else_branch");
if (then_mm > 0 && else_mm == 0) m.decode_branch = "then_branch";
else if (else_mm > 0 && then_mm == 0) m.decode_branch = "else_branch";
else m.decode_branch = by_condition;

Weights arbitrate when only one branch has them; the condition arbitrates otherwise. That is correct because the weight-bearing branch is the decode path by construction (the Token .fconst carries decode weights), and when both branches have weights the condition is the only remaining evidence. And the reason it is safe for a pack that inverts the convention is stated: "A pack that inverts the condition without moving its weights stays readable; a pack whose weights sit in two branches is ambiguous, so the condition [decides]" — i.e. the rule never refuses a readable pack, and when it must choose it chooses by the declared signal rather than by traversal order.

The note is the design, not a courtesy

"the If condition compares the input shape with 1, so then_branch is the one-token (decode) path; its weights are
 in else_branch (then 0, else 109 quantised matmuls), which is where the Token .fconst's weights are re[ad]"

Counted matmuls in the message mean an operator reading npu-pack info can see why the branch was chosen and how one-sided the evidence was — then 0, else 109 is not an opinion. And the fallback is honest about itself: "no recognisable If condition, so then_branch is taken as decode by the ONNX con[vention]" — a default that says it is a default.

Scope

+168/−7 in pack_import.cpp, +5 header, +88 test. The measured rule added for hidden 4096 / 32 heads / 36 layers is the fourth family's row, so this is the pack that #1992's tile-padding unblocked — the two PRs are sequential in the same direction, and neither claims the other's work.

One ask: the condition is recognised as Equal(Shape(input_ids), Constant(value_int 1)). If a pack spells the same test as == on a scalar (or with the constant on the left), the recogniser misses it and the default applies — correct but silently. Since the note already prints for the default case, add the shape it did not recognise to it (the op type and the two inputs), so a future pack's condition is diagnosable from the log instead of needing this code opened.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at fb590f6 — and the commit answers the duplicate flag directly: "Merge #1980's branch (1bit/fconst19-36layer) into fconst19-4096-36".

That is exactly the resolution I asked for on both PRs. The important consequences:

The substance of my earlier verdict stands: Qwen3-8B 1.9 pack imports end to end is the outcome worth stating, with the rule row as the mechanism — a row alone reads as table maintenance, an end-to-end import is a claim about a model.

Nothing left from my side. If #1980 is still open, it should close against this; if this is the surviving branch, it now carries both.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at fb590f6

A measured fconst rule row for a geometry nobody had measured, and the comment says why the row cannot be inferred from the row next to it: "(4,160/9 tiles at 36
layers against 8,512/3 tiles at 24), so this row is measured, not inferred from that one"
. That is the distinction #1633's table keeps re-learning.

Two more things worth keeping:

  • The refusal it removes was by design: "its block shape has not been measured for hidden 4096, 32 heads, 36 layers" — and the comment names the trap in one
    line: "kernel family and fconst rule row are not the same partition". A pack can be a known kernel family and still need its own row, which is the sentence
    that stops the next person from deriving it.
  • The If(Equal(Shape(input_ids), Constant(value_int 1))) branch is called out as not being cosmetic: "the two branches do NOT hold the same tensors" — so
    the one-token path is a distinct shape, and the comment records which packs that was measured on (Qwen-2.5 1.5B 1.9, 338 tensors imported with no [failure]).
    A branch that holds different tensors is precisely the kind of thing a reader would otherwise skim past.

Measured provenance is stated the same way as every other row (tools/fconst19_measure.py against the paired 1.8 pack), which is what makes the row checkable
rather than asserted.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at fb590f68 — and the "what it deliberately does not do" section is why.

Withdrawing a row when the pack's own checks refuse it is the discipline this table needs: a {4096,32,40} row "was added from the tail arithmetic and withdrawn in 172178de84: the pack's own in-block checks refused it (scale/zero 0 percent, no second header, 0 of 80 RMS anchors). In kFconst19Rules the prefix is the first byte where the 1.9 layer block diverges from its 1.8 twin, and no 1.8 granite exists, so the row cannot be measured." A row that cannot be measured is a guess with a measurement's clothing — and this table has already been burned by exactly that (my #1767 review found the comment's arithmetic right and the provenance unstated; #1980's counterpart row states its twin by name).

The two merge-exposed defects are the most useful part of the body, because both are the class I have been flagging all session:

  1. "the new row was missing from the test's pinned row list (#1861)" — the row-literal arm doing exactly its job: adding a rule row without adding a literal now fails, which is precisely what #1861 was for;
  2. "the SSMLP/GQO attributes counted as graph matmuls, which stopped #1948's block-32 tail head being read (Qwen-2.5 1.5B 1.9 imported with no lm_head)" — a census/classification change silently disabling a reader, i.e. the same shape as qk_norm flipping and as my #1995 guard: a counting rule used as a predicate elsewhere.

Also right: dropping the unconditional lmhead_corr > 0.999 assertion. 0.0043 is what an untied pack measures, and an arm that demands a tied pack's correlation from an untied one teaches people to special-case the test.

Two notes:

  1. Say which of the two merge-exposed defects would have shipped silently, if either: the pinned-row one is caught by a test (good); the graph-matmuls one was caught by re-importing a known pack — worth naming as "this is why the pack leg runs on a real pack" rather than only in a comment at f03b84a.
  2. Sequencing with #1992/#1980: all three touch kFconst19Rules and pack_import.cpp. #1992 already contains #1980's head; this one adds {4096,32,36} — the same row #1980/#1992 add. If both land, the table must not gain it twice: worth a one-line "this supersedes/duplicates the row in #1980" so the merge order is decided rather than discovered.

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.

1 participant