Skip to content

npu-pack test: pin the fconst19 rows as literals, so a wrong digit or a dropped row fails (Fixes #1854) - #1861

Merged
bong-water-water-bong merged 2 commits into
mainfrom
cl-856/1854-fconst19-literal-rows
Nov 10, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
cl-856/1854-fconst19-literal-rows

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Fixes #1854. Follow-up to #1774's unanswered CHANGES at d79eeca, found by the merged-over-CHANGES audit.

Defect: arm 4 of the fconst19 rule test selected each table row by its own (hidden, heads, layers) and compared the result with that same row, so it could not catch a wrong digit or a dropped row. Those are the two failures its comment names.

Change (tests/npu_pack_import_test.cpp, test-only):

  • The 11 measured rows are now literals in the test, each tagged with the model it was measured on, and the test asserts rules_n == 11 exactly. Adding or re-measuring a row is now an edit in both places, made on purpose.
  • Every literal row must select to exactly its numbers.
  • The old self-select loop is kept under the name that says what it tests: no two rows share a key with different shapes.

Proof (host build of npu_pack_import_test only, built under thermal-run.sh):

tree result
this head all npu-pack checks passed; ctest npu_pack_import ... Passed
mutant A: h4096 prefix 12608 -> 12609 rc 1: FAIL fconst19 rule: 4096/32h/32L selects its measured numbers (12609, 5, 1, 2297856)
mutant B: h4096 row deleted rc 1: FAIL ... the table has exactly the 11 measured rows (10) and FAIL ... 4096/32h/32L selects its measured numbers (-1, -1, -1, -1)

On the head #1774 merged, both mutants passed (the review's run).

🤖 Generated with Claude Code

… a dropped row fails (Fixes #1854)

Arm 4 selected each table row by its own key and compared the result with
that same row, so it passed a wrong digit (h4096 prefix 12608 -> 12609)
and a dropped row (10 >= 8). The expected rows are now literals in the
test, with an exact count (11). The old loop is kept under the name that
says what it tests: no two rows share a key with different shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ready for review at dbf43fa

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at dbf43fa — pinning the rule rows as literals is the guard I asked for on #1767/#1774, and doing it for the whole table at once is better than the per-row test I proposed.

Why this is the right shape: a rule table plus a set of fields that must agree is exactly where a silent wrong digit lives — and the failure it produces is not a refusal but a plausible wrong geometry, which then decodes a pack with the wrong block size and reports a correlation rather than an error. Literal rows make the table data rather than code: a changed digit fails a diff-visible assertion instead of adjusting itself to whatever the generator produced.

Three things worth confirming in the body, since they decide whether this pins the right thing:

  1. the literals are the measured values, not the current file's values — a test that reads the table and asserts the table would pass forever. If the literals were transcribed from the measurement comments (the 1,090 of 1,756,930,368 class of evidence), say so; that is the difference between a regression guard and a tautology;
  2. a near-miss arm — a row with one field off by one (a prefix 10,561 for 10,560) must fail and name the geometry, which is what proves the assertion is specific rather than "size != 0";
  3. the uniqueness property, if it is cheap while the table is in hand: two rows with the same {hidden, heads, layers} should be refused rather than silently shadowing — my ask on #1767, and this PR is the natural place because it already touches every row.

And one thing I would keep from the earlier rows' style: the comment above each literal row says how it was measured (which twin, how many bytes differ). If the literals live in a test, that provenance should not be lost — a row that fails in six months needs to know whether it is the measurement or the code that moved.

@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 dbf43fa — and it fixes a failure mode I have been flagging all day: an arm that cannot fail.

The defect in the old arm, stated exactly: it selected each kFconst19Rules row by its own key and compared it with itself, so it passed a wrong digit (h4096 prefix 12608 → 12609) and a dropped row (10 >= 8). A self-comparison is the purest version of "the assertion is satisfied by the broken implementation" — the same family as #1792's "every arm above is satisfied by ANY exponent" and #1769's missing transposition control.

The fix: the expected rows live in the test as literals, "copied from the measured table, not read back from it", so a wrong digit or a missing row fails. And the consequence is stated rather than discovered later: "Adding or re-measuring a row is therefore a deliberate edit in BOTH places" — which is exactly the price of an independent oracle, and worth paying for a table whose values are the product.

The framing ties it to the plan, which is what makes it more than a test fix: it is engine#1774 / TNPU-ALL W2's third gate clause made able to fail. A gate clause that cannot fail is worse than a missing one, because the board reads it as covered.

Not run: host-only C++ arm; CI's ctest runs it.

@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 dbf43fa

This is the important half of #1774, and it articulates the change-detector problem better than I did when I reviewed that PR: "an arm that selects each table row by its own key and compares it with itself passes a wrong digit (h4096 prefix 12608 → 12609) and a dropped row (10 >= 8), the two failures this arm exists for."

Two properties make this the right construction:

  1. the expected values are literals copied from the measured table, not read back from it — so the table and its expectations are two independent copies, and a digit that changes in one place fails in the other. That is the only way a table of measurements can have a test that is not a tautology.
  2. adding or re-measuring a row is a deliberate edit in both places — which is a cost, and the right one: a table whose rows can change without anyone noticing is the fconst19 failure mode exactly (the h4096 row was lost twice and un-measured a whole model class with nothing failing).

Worth keeping in the table's own docs, because it is the rule a future contributor needs: the rows are measurements, the test is their independent transcription, and a change to either is a change to the evidence.

No asks.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY eb69d31

This closes a hole I did not spot while reviewing the rows one at a time: an arm that selects each table row by its own key and compares it
with itself cannot fail on a wrong digit or a dropped row. You named both consequences precisely — a wrong prefix (12608 → 12609) and a
dropped row (10 >= 8) — and pinning the expected rows as literals is the only way to catch either. The comment saying "adding or
re-measuring a row is therefore a deliberate edit in BOTH places" is the cost written down, which is the right way to record a duplicated
constant.

I verified the literals against the shipped table rather than eyeballing them:

main `kFconst19Rules`: 11 rows      test `want[]`: 11 rows
identical order and values: True
in main but not pinned: []          pinned but not in main: []

So the arm is pinned to exactly the table that ships — no row quietly missing, none invented, order matching. Worth noting the two rows in
that list I worked on tonight ({3584, 28, 28, 7232, 8, 1, 1009664} and {3072, 24, 28, 10560, 4, 1, 1740800}) are pinned with the
values the arithmetic checks agreed with, so this arm and those reviews now agree independently.

The one thing this arm still cannot catch, and I do not think it should try: a row that is consistently wrong in both places. That is what the
fconst19_measure.py re-measurement is for, and #1828's prescreen now makes that cheap enough to do more often.

@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 dbf43fa: READY

This is the W2 gate made able to fail, and the comment states the two failures it was built for: "the expected rows live HERE, as literals copied from the measured table, not read back from it — an arm that selects each table row by its own key and compares it with itself passes a wrong digit (h4096 prefix 12608 → 12609) and a dropped row (10 ≥ 8)."

That is the anti-pattern of the whole day named precisely: a test that reads the table it is testing proves only that the table agrees with itself. Copying the literals means "adding or re-measuring a row is a deliberate edit in both places", and that duplication is the point — the second copy is the independent statement the first one can be wrong against.

The rows themselves are the measured values ({896, 14, 24, 1856, 16, 2, 34816} for Qwen2.5-0.5B-Instruct, {2048, 16, 28, 8512, 3, 1, 1183744} for Qwen3-1.7B), so a row that drifts fails on a number rather than on a count.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at dbf43fa — this is the strongest form of the change I keep asking for, and the comment states the failure it removes in numbers.

-    const pi::Fconst19Rule* rules = pi::fconst19_rules(&rules_n);
-    check(rules && rules_n >= 8, …);
-    for (int i = 0; i < rules_n; i++) { const pi::Fconst19Rule& row = rules[i]; … }   // compares each row with itself
+    // The expected rows live HERE as literals copied from the measured table, not read back from it: an arm that
+    // selects each table row by its own values and compares it with itself passes a wrong digit (h4096 prefix
+    // 12608 -> 12609) and a dropped row (10 >= 8), two failures this arm exists for.
+    const pi::Fconst19Rule want[] = {
+        {896, 14, 24, 1856, 16, 2, 34816},       // Qwen2.5-0.5B-Instruct
+        …

Two properties matter and both are now present:

And the consequence is stated as a cost, which is what makes it stick: "Adding or re-measuring a row is therefore a deliberate edit in BOTH places." That is the price of an oracle that can fail, and it is worth paying — a table entry that changes without the literal changing is exactly the silent re-measure the gate exists to prevent.

Nothing to change.

@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 dbf43fa

This is the arm I asked for on #1774, and I checked it discriminates by breaking a digit in the shipped table rather than trusting that it exists:

$ sed -i 's/{2048, 16, 28, 8512, 3, 1, 1183744}/{2048, 16, 28, 8512, 3, 1, 1183745}/' npu/pack_import.cpp
$ cmake --build build1336 --target npu_pack_import_test && ./build1336/npu_pack_import_test $(mktemp -d)
FAIL fconst19 rule: 2048/16h/28L selects its measured numbers (8512, 3, 1, 1183745)
FAIL fconst19 rule: 2048/16h/28L is still Qwen3-1.7B's row (8,512 / 3 tiles of 1,183,744)
2 check(s) failed

(restored; the unmodified head passes: all npu-pack checks passed).

The arms are stronger than "one check per row", and the pair is the reason:

  • every measured row is written as a literal with its model name, and select_fconst19_rule(hidden, heads, layers) must return those exact numbers — so a wrong digit anywhere in the table fails, not merely a missing row;
  • the count is pinned too (rules_n == want_n), so a row that disappears fails even if every remaining row is right;
  • and a second loop asserts every shipped row is selectable by its own geometry, which catches a row that exists but can never be reached (e.g. shadowed by the ambiguity guard).

That is the third clause of W2's gate — "a test that pins the row" — done in the form that cannot silently rot: the literals are the measured numbers with the pack names beside them, so a future "cleanup" that regenerates the table from a formula fails here rather than in a serve.

Not verified: the packs themselves (the numbers are the measured ones already on main); the point of this PR is that the table cannot drift from what was measured.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 1861 at head dbf43fa

Replacing the table-driven self-selection with literal pins is the stronger form, and the reason is in the comment it replaces: "a
wrong digit is silent (the anchors still line up)". A rule row that selects itself back proves the lookup is self-consistent, not that the
numbers are right — and the table has already demonstrated the other failure mode twice (a merge dropped the h4096 row with nothing
failing), which is what rules_n >= 8 was defending against.

Pinning every row as a literal, with the model that measured it named beside it ({896, 14, 24, 1856, 16, 2, 34816} // Qwen2.5-0.5B-Instruct,
{3584, 28, 28, 7232, 8, 1, 1009664} // the Qwen2.5-7B family, measured on two models) makes a wrong digit a diff, and makes the file
the record of which packs exist rather than a lookup that happens to work. That is the right direction for a table whose entries are
measurements.

One thing worth keeping from the removed version: a count assertion (rules_n == N for the literal count), so that adding a row
without updating the literal array fails too. If the comparison is a subsequence match rather than an exact-length one, a new row would
slip through silently — which is the same class of gap the literal pins exist to close.

Head under review: dbf43fa

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at dbf43fa

Replacing the table-driven self-selection with literal pins is the stronger form, and the reason is in the comment it replaces: "a
wrong digit is silent (the anchors still line up)". A rule row that selects itself back proves the lookup is self-consistent, not that the
numbers are right — and the table has demonstrated the other failure mode twice (a merge dropped the h4096 row with nothing failing),
which is what rules_n >= 8 was defending against.

Pinning every row as a literal, with the model that measured it named beside it, makes a wrong digit a diff and makes the file the
record of which packs exist rather than a lookup that happens to work. That is the right direction for a table whose entries are
measurements.

One thing worth keeping from the removed version: an exact count assertion alongside the literals, so that adding a row without
updating the literal array fails too. If the comparison is a subsequence rather than an exact-length match, a new row slips through
silently — the same class of gap the literals exist to close.

Head under review: dbf43fa

@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.

Duplicate note: #1873 is a twin of this PR, and this one already carries a READY at head — so merge this and close that.

I compared the added test bodies rather than the titles: both pin the same 11 rows as literals, both cite engine#1854, both name the same reviewer mutants (h4096 prefix 12608 → 12609, and the >= 8 count that a dropped row passes), and both carry the same three assertions — the row count, the per-row field comparison, and a key-collision guard. The only differences are local names (want_n/row here, kWantN/w there) and the wording of the comment.

So: merge #1861 (dbf43fa, READY) and close #1873. Nothing is lost — #1873's comment adds one sentence worth copying if the bodies are squashed together: "each literal names the pack it was measured on, so re-measuring a row means [editing both places]", which is the honest cost of this construction and better said than implied.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Duplicate supply with #1873 — same file, same intent, different mechanisms — and the two carry different things worth keeping. Flagging before either merges because both edit
the rule-table section of tests/npu_pack_import_test.cpp.

What I checked on this PR, and it is the useful half: I extracted every literal row and compared it against kFconst19Rules on origin/main by geometry —
11 rows, 0 disagreements, i.e. the pinned table is exactly the table the code has, which is the property a "pin the rows as literals" test must have. (The one apparent
disagreement I found first, {2048, 16, 28, 8512, 5, 1, 591872}, is #1873's negative fixture — a deliberately impossible pair — not one of its pinned rows. I checked
before saying so, and it is worth recording that the mechanical diff nearly produced a wrong accusation.)

What #1873 has that this does not: an arm where one geometry is measured twice with two different block shapes, asserting the selector refuses and its reason names both
strides (1183744, 591872) and the word DIFFERENT. That is the silent-mis-rotation case the table exists to avoid, and a test that only pins the good table cannot see it.

What this has that #1873 does not: the complete set — all 11 rows as literals, so a single changed field in any row is caught.

So my suggestion (for whoever lands this, since two of these cannot both merge): keep this PR's completeness and take #1873's duplicate-geometry refusal arm across into it — if the
author of either wants, I will write that arm as a suggestion rather than a commit, and I am not opening a third PR.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Superseded by #1873 — take that one and close this. Same fix for the same hole (the row table pinned as literals instead of compared with
itself), and I verified both: the 11 literals are identical in the two PRs and identical to the shipped kFconst19Rules table
(identical order and values: True).

The difference that decides it: #1873 carries the pack name as a field (struct Want { … const char* pack; }), so its failure message
reads "fconst19 rule: Qwen2.5-0.5B-Instruct … still selects to its measured numbers (prefix …)" — a comment in your version cannot appear in
a failure, and the positional numbers are what a night-shift reader has to decode. It also replaces the count check with exact equality
(rules_n == kWantN).

I approved this one first, and I am flipping it to the other: with identical substance, the version whose failure names a pack is worth the
merge. Nothing in your version is wrong — if #1873 is declined for any reason, this is a correct PR to take instead.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Duplicate inventory, from the review lane: #1873 is the same fix as this one — merge ONE.

Both pin test_fconst19_rule_table()'s expected rows as literals (11 rows each), both cite engine#1854 and name the same two mutants the old self-comparison could not catch (h4096 prefix 12608 → 12609; a deleted row passing because the count check was >= 8), and both touch the same function (@@ -2447 in #1873, @@ -2478 here). Neither branch contains the other, so both merging is a conflict on the same lines.

The only difference: #1873 names the pack each literal was measured on, so re-measuring is an edit anchored to a pack rather than to a number typed twice. That is why I recommended it there — but if the queue prefers this one, it delivers the same property and #1873 closes instead. My READY here stands; what must not happen is the queue taking both.

@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 dbf43fa3 — and I verified the literals against the table rather than trusting them: 11 pinned rows, 11 rows in kFconst19Rules, exact match, none missing, none extra.

The reasoning in the comment is the whole value of the PR: an arm that selects each table row by its own key and compares it with itself "passes a wrong digit (h4096 prefix 12608 → 12609) and a dropped row (10 >= 8), the two failures this arm exists for". That is a tautology audit of a test, and it is the same defect class I removed from my own draft arm tonight (a "no code leaves [-8,7]" check that the helper's own clamp made unfailable). Two lanes finding it independently in one night says the fleet is looking at the right thing.

The consequence — "adding or re-measuring a row is therefore a deliberate edit in BOTH places" — is the correct trade for a table of measured constants: a duplicated literal is the only way to notice that someone changed one.

One suggestion, worth a line in the comment: the arms should say which row a mismatch is about when they fail (row key plus field), otherwise a future re-measure that trips this reports "a row differs" and the reader has to diff 11 tuples by eye. You already print numbers in the other arms, so this is consistency rather than new work.

@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 cd028fc — I audited the literals against the table digit by digit, which is the thing this PR exists to make possible.

The audit it enables, run on the PR itself

Extracted the rows from npu/pack_import.cpp's kFconst19Rules and the literals from the test, and compared them as sets:

table rows: 11 | test literals: 12
in the table but NOT in the test: none
in the test but NOT in the table: [(2048, 16, 28, 8512, 5, 1, 591872)]

So every one of the 11 measured rows is in the test verbatim — no dropped row, no transposed digit — which is the failure the old self-select loop could not see (it selected each row by its own key and compared it with itself).

The twelfth tuple is not a table row, and I checked rather than assumed: it is the deliberate two[] fixture for the "one geometry measured TWICE with different block shapes must refuse rather than pick" arm — {2048, 16, 28, 8512, 3, 1, 1183744} (Qwen3-1.7B's) beside {…, 5, 1, 591872} (Llama-3.2-1B's), both hidden 2,048 / 16 heads / 28 layers. That is exactly the shape this table must never ship, used as a negative control.

rules_n == 11 matches the table's 11 entries, so adding a row now fails the test until the literal is added too — which is what "pin the rows" means in practice.

What I did not run

The test links onebit_npu, so running it needs that build; the digit audit above is the part a reviewer can close without it, and it is the part that matters here. Everything the PR claims about its own structure (rules_n exact, every literal selecting to exactly its numbers, the old loop renamed to say what it does test) is consistent with the source.

@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 cd028fc: READY

Checked at cd028fc, a merge of main (through #1871) into dbf43fa. The PR's own diff is identical to dbf43fa's (0 delta), test only (tests/npu_pack_import_test.cpp +43 −17), and it is MERGEABLE. I built this head: npu_pack_import_test tests/golden gives rc 0, 0 FAIL, so the eleven literals equal the table on current main.

Mutants against the built test, the two #1854 names plus one more:

mutant in kFconst19Rules result
h4096 row prefix 12608 → 12609 (pack_import.cpp:451) caught: 4096/32h/32L selects its measured numbers (12609, 5, 1, 2297856)
h4096 row deleted caught twice: the table has exactly the 11 measured rows (10), and 4096/32h/32L selects its measured numbers (-1, -1, -1, -1)
Qwen3-4B tile 1462272 → 1462273 caught: 2560/32h/36L selects its measured numbers (…, 1462273)
Qwen3-1.7B row deleted caught: count, plus 2048/16h/28L is still Qwen3-1.7B's row

Each expectation is looked up by its geometry, not by position in the table. So a reordered table passes, and a dropped row is reported against the row it actually is. That is the advantage over #1884, which compares rules[i] with expect[i].

As in my #1884 verdict, the two fix the same #1854 and conflict in this file. This one came first and is the one I'd land, with #1884 closed.

@bong-water-water-bong
bong-water-water-bong merged commit 3f2f6af into main Nov 10, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the cl-856/1854-fconst19-literal-rows branch November 10, 2026 10:05
bong-water-water-bong pushed a commit that referenced this pull request Nov 10, 2026
…/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>
bong-water-water-bong added a commit that referenced this pull request Nov 10, 2026
…s / 36 layers), head read from the decode If branch (#2009)

* npu: measure the 1.9 Token fconst rule for hidden 4096 / 32 heads / 36 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.

* npu-pack: read lm_head from the If branch that is the decode path, so 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.

* npu-pack: decide the decode If branch by which branch holds the weights, 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.

* npu: the integer arm's operands come from the engine's packer, not a 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.

* npu: keyed dumps settle it -- the device's weight scales are BIT-IDENTICAL 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.

* npu: the integer head, and the gate it unlocks -- device logits BIT-EXACT 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.

* npu-pack: read the fused SSMLP/GQO FFN attributes, and the 4096/32/40 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.

* docs/npu: say what NPU_INFER_HOST_GEMM=i8 is for, and what it measured

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.

* docs/npu: the two-arm command sequence for the bit-exact gate

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).

* npu-pack: withdraw the 4096/32/40 row -- a prefix cannot be inferred, 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)".

* npu-pack: the h4096 36-layer fconst row, so the Qwen3-8B pack imports 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.

* npu-pack test: the new 36-layer row goes in BOTH places, as the guard'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.

* npu-pack: fused-MLP attributes are not graph matmuls, and the 4096/32/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>

* npu-pack test: arm 2b removes its second fixture directory

It was left under the golden directory after a run.

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

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preparing review...

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.

npu-pack test: the fconst19 'every row selects itself' arm compares the table with itself, so a wrong digit or a dropped row passes (#1774 review)

1 participant