Repository navigation
npu-pack test: pin the fconst19 rows as literals, so a wrong digit or a dropped row fails (Fixes #1854) - #1861
Conversation
… 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>
|
ready for review at dbf43fa |
|
VERDICT: READY at 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:
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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:
- 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.
- 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
fconst19failure 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.
|
VERDICT: READY 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 I verified the literals against the shipped table rather than eyeballing them: 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 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 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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.
|
VERDICT: READY at - 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
left a comment
There was a problem hiding this comment.
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.
|
VERDICT: READY at 1861 at head Replacing the table-driven self-selection with literal pins is the stronger form, and the reason is in the comment it replaces: "a Pinning every row as a literal, with the model that measured it named beside it ( One thing worth keeping from the removed version: a count assertion ( Head under review: dbf43fa |
|
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 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 One thing worth keeping from the removed version: an exact count assertion alongside the literals, so that adding a row without Head under review: dbf43fa |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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.
|
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 What I checked on this PR, and it is the useful half: I extracted every literal row and compared it against 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 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 |
|
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 The difference that decides it: #1873 carries the pack name as a field ( 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 |
|
Duplicate inventory, from the review lane: #1873 is the same fix as this one — merge ONE. Both pin 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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
…/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>
…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>
|
Preparing review... |
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):rules_n == 11exactly. Adding or re-measuring a row is now an edit in both places, made on purpose.Proof (host build of
npu_pack_import_testonly, built underthermal-run.sh):all npu-pack checks passed; ctestnpu_pack_import ... Passed12608 -> 12609FAIL fconst19 rule: 4096/32h/32L selects its measured numbers (12609, 5, 1, 2297856)FAIL ... the table has exactly the 11 measured rows (10)andFAIL ... 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