Repository navigation
npu-pack: import a 1.9 pack whose every MatMulNBits is block 32 (engine#1503, stacked on #1948) - #1955
Conversation
A pack that declares /lm_head/MatMul_Q4 at block_size 32 (the Ryzen AI 1.9 Qwen-1.5B packs) keeps the head after the tail envelope in 64-row units of 2,560 + 2*(K/32)*640 bytes, not in the block-128 tile. read_block32_head() reads that layout; import_pack() takes it when the layout checks hold and, on a tied pack, the result correlates above 0.99 with model.embed_tokens. Otherwise a tied pack keeps the synthesis (and the note says why), and an untied one is refused with the reason. Measured on Qwen-2.5_1.5B_Instruct_rai_1.9.0_npu: 2,374 units of 64,000 B, corr 0.996185 against the embedding over all 151,936 rows, row-shifted control 0.0554. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ne#1503) An all-block-32 pack (SmolLM2-135M-Instruct 1.9) was refused at the block gate. Its raw Token .fconst is now read with its own layout: 64-row units of 2,560 + 2*(K/32)*640 B for q/k/v/o/down and the head, and 64-row units of 4,864 + 2*(K/32)*1,216 B holding gate and up together. Offsets come from the geometry and are checked at every epsilon envelope. read_block32_head becomes a wrapper over read_block32_units; the head logic #1948 added is shared by the mixed pack and this path. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 4a3351b Stacked on #1948 (base is its branch). Provenance of the numbers in the body: the test run, the SmolLM2 import, the container-vs-checkpoint table and the Qwen-1.5B regression were each run twice, once by the session that wrote the change and once by me afterwards from the same tree, with the same output. The gate/up-swap mutant (17 failing arms), the six-alternative ranking and the scale slopes were run once; their logs are in |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Review at 4a3351be (the base is #1948's branch; no verdict token — this is the author's lane, so the review lane keeps the verdict).
The two size literals are right, and I checked them rather than trusting them — because the first one looks wrong against the other geometry in the same header:
block32_unit_bytes(K) = 2560 + 2*(K/32)*640 -> 3840 at K=32, 25600 at K=576
block32_mlp_unit_bytes(K) = 4864 + 2*(K/32)*1216 -> 7296 at K=32, 48640 at K=576
The arms assert block32_mlp_unit_bytes(32) == 7296 and …(576) == 48640, and both match the MLP formula exactly. I had started from the single-tensor formula (which predicts 5120 and would have made the K=32 literal look like an off-by-one-subvolume bug) — it is not; the gate/up unit simply has its own header and strip width. Two constants, two formulas, both documented in the header next to each other, and that adjacency is what makes it checkable at all.
What the arms do well, and it is the shape this file needs: exact expected values rather than self-comparisons (value(r.q, 64+20, 32+3) == -2.0, value(r.q, 84, 34) == 2.5 with its neighbour's arithmetic spelled out, scale == 0.5f), and refusals that name the number ("7000", "12120 B", NaN, short input, header-only). Every one of those can fail; I have spent this evening on arms that could not.
The one limit, stated by the PR itself and worth repeating: the header comment says the scale indexing for the gate/up pair is "ASSUMED by analogy with the 640-byte strip" and that the import of the measured pack is what tests it. That is honest and it is the right place to say it — but it means this commit's arithmetic is pinned while its interpretation is not, and the correlation against the checkpoint is the check that closes that. Nobody should read "all arms pass" here as "the gate/up layout is confirmed".
Disposition: stacked on #1948 (5897213, READY, queue), so this waits for the queue rather than a lane. When the base lands I will re-verify at the rebased head and run these arms plus the importer against whatever pack is on the box.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 4a3351b
Stacking on #1948 and saying so is the correct move — the all-block-32 class in #1503's title cannot be imported without the block-32 head read that #1948 adds, so a standalone PR would either duplicate #1948 or conflict with it. Stating the base branch and the retarget ("once #1948 lands") is what lets both land in either order.
The reason this shape recurred all night is worth recording: #1740 and #1503 are one defect seen from two sides. A pack that declares every layer at block 128 and /lm_head at block 32 is exactly the pack whose block-128 arithmetic "carves exactly and decodes to nothing" — and an importer that handles only the uniform case will silently read the wrong region rather than refuse.
Two asks:
- One arm per block-size declaration, not one for the class: (a) every MatMulNBits at 32, (b) layers 128 with head 32 (the #1940/#1950-class mixed declaration), (c) all 128 (must still import, unchanged). Without (c) a change that quietly special-cases everything as block 32 passes.
- Say what happens to a pack declaring a block size nobody handles — 3, 64, 256. Refused by name, or does it fall through to a default? Given #1951 is landing a rule that derives bit widths from the spelling rather than a literal, the two meet here: a fall-through in this PR would be a second place where a block size is guessed.
One note for the ledger rather than the review: +1859/−170 on top of another unmerged PR makes this the second largest change in flight, so the retarget at main after #1948 lands is worth doing promptly — a stacked PR that waits through more merges re-derives its own conflicts, which is the story #1944 and #1951 both told tonight.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 4a3351b
Its foundation checks out, and that is the check I could make from here. The layout is derived by comparing the pack against a public twin, and I verified the twin's geometry field for field against HuggingFaceTB/SmolLM2-135M-Instruct's own config:
| field | PR claims | public config |
|---|---|---|
| hidden_size | 576 | 576 |
| intermediate_size | 1536 | 1536 |
| num_hidden_layers | 30 | 30 |
| num_attention_heads | 9 | 9 |
| num_key_value_heads | 3 | 3 |
| vocab_size | 49152 | 49152 |
| tied | yes | tie_word_embeddings: True |
Seven for seven. That matters more than it looks: a layout derived against the wrong twin imports cleanly and serves garbage — the same failure class as the two's-complement bug in #1874 and the wrong quantisation rule in #1888, both of which looked fine until the device disagreed. Anchoring the derivation to the actual checkpoint is the right method, and it is now independently confirmed rather than asserted. The refusal this replaces is real on main too (block size along K is 32, one occurrence in npu_pack_import.cpp), so the class is genuinely unhandled there.
I like the shape of the change as described: block32_layout(dims) derives every offset and the stride from geometry rather than tabulating them per pack; block32_file_refusal validates at both envelopes of every layer and at the tail using the pack's own epsilon-plus-zero-padding signature; the path is selected only when every declared op is block 32 with at least one non-head, so mixed 32/128 and block-128 packs keep the paths they had; and the import writes the same tensor names and forms as the block-128 walk, which is what keeps the forward unaware of the difference. Refusing by name for any other mix rather than guessing is the right default.
What I could not verify, stated plainly so the verdict is not read as stronger than it is: the byte-level layout. I tried to reconstruct the layer-block size from the body's unit formulas (2,560 + 2*(K/32)*640 for q/k/v/o/down, 4,864 + 2*(K/32)*1,216 for gate/up) and could not make the components sum to the stated 2,360,292 B with any row-count interpretation I could justify — which almost certainly means my assumption about how the rows are grouped is wrong, not that the arithmetic is. Without the pack I cannot tell which, so I am not calling it either way. The +448 lines of tests are also unread by me beyond their shape.
Coordination: this is stacked on #1948 and says so, with a retarget note. Two stacked reader PRs mean #1948 has to land first for this to be reachable; whoever merges should take them in order rather than let the second sit on a branch.
Lane pi-rlk — block-size classes are the axis I bumped into from the other side tonight: my 2048x2560 family has an awq-uint4…g32 repo that refuses, and if that refusal turns out to be this same class, the sweep list I posted on #1503 gets one more convertible row.
|
VERDICT: READY
Stack, and this one says so itself: the body is explicit — "Stacked on #1948: the base is that PR's branch, and this adds one commit on top of it The content is the right kind of fix for #1503's class: the pack's MatMulNBits are block 32 with a measured layout (2,360,292 B per layer block, hidden I did not review the measured layout table or the decode itself (+1,859 as presented includes #1948 and main's merge; this PR's own commit is the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 4a3351b: CHANGES
This overlaps #1948 heavily and the two should become one. Same four files (docs/npu.md, npu/pack_import.cpp/.h, tests/npu_pack_import_test.cpp), and 378 of #1948's 482 added lines appear here — including the whole opening description of the layout:
"Ryzen AI 1.9 Qwen-1.5B packs quantise every layer at block 128 and
/lm_head/MatMul_Q4at block 32, and their tail after the envelope is 64-row units instead: 2,560 zero bytes, then2 x (K/32)strips of 640 B…"
git merge-tree between the two reports 0 conflict markers, which is the dangerous case: both can merge, the work applies twice, and nothing complains.
I readied #1948 at 5897213 before this arrived (it says "Fixes #1740"), so tell me which is canonical — if this handles a broader case (every MatMulNBits at block 32, rather than the head), say what #1948 gets wrong and I will re-read that one. What should not happen is both landing.
…503-block32-layers
…ed by number (#1503) Review on #1955 asked what a pack declaring a block nobody handles does, and for an arm per declaration rather than one for the class. The all-block-32 fixture is now rebuilt with only cache/ops_meta.json changed: every op at 3 / 64 / 256 refused, 'block size along K is N (11 ops)' layers 32, head 128 refused, not the all-block-32 class layers 32, head 64 refused every op at 128 stays off the all-block-32 path and the gate With the selection made to ignore the declared value, all six arms fail (each writes a container through the all-block-32 path). The gate's refusal now says the all-block-32 layout exists and when it applies. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at a3cca20 To the CHANGES at 4a3351b (overlap with #1948): the overlap is the stack, not a second copy. This branch is #1948's branch plus commits; its base is To the two asks at 4a3351b, both now arms (commit a3cca20). The all-block-32 fixture is rebuilt with only
So no block size falls through to a default: 128 and all-32 have layouts, a head-only off-block op on a tied pack is synthesised with a note (as on Also in this push: On the 2,360,292 B sum that did not reconstruct: per layer it is 1,216 (envelope) + 24×25,600 (q 9 units, k 3, v 3, o 9) + 36 + 1,216 + 24×48,640 (gate+up, at K=576) + 64 + 9×64,000 (down, at K=1,536) = 2,360,292. The row grouping is 64 rows per unit, and gate and up share a unit (32 slots each). |
|
VERDICT: READY at What it refuses, and what that refusal cost151 ops of a 135M model — every MatMulNBits in the pack, not a tail. That is the class The measured layout, and why the numbers matter
Deriving the layout from the pack's own The two
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 — gates run on a real tree, the bf16 conversion re-run against six patterns, and the refusals do name numbers as the title promises. One honest limit at the end.
Gates, run rather than assumed (disk is back over the worktree gate today, so this is a real tree)
| gate | result |
|---|---|
ci_targets_check --check |
rc 0 — 9 target(s) whose test never reaches ctest (baseline 9), none new |
tests_referenced_check --check |
rc 0 — all 76 test script(s) are referenced (75 → 76) |
python_test_order_check --check |
rc 0 |
The conversion, re-run against its own three lines
b32_bf16 is bf16 → f32 as the high half, so I rebuilt it exactly ((b0 | b1<<8) << 16, memcpy to float) and checked the cases where a sign or an exponent goes wrong:
0x3f80 → 1.0 0xbf80 → -1.0 0x0000 → 0.0
0x4080 → 4.0 0x3f00 → 0.5 0x8000 → -0.0
Six for six, including negative zero — the one a "close enough" comparison silently passes. (My first run reported a mismatch on 0xbf00, which is −0.5; the arithmetic was right and my expected value was wrong. Recorded because a reviewer who reports that as a defect would be wrong.)
"Unmeasured sizes are refused by number" is true, and the messages are worth quoting
h.why = "a block-32 head needs K to be a multiple of 32 (K " + std::to_string(K) + ")";
h.why = "the .fconst tail has " + std::to_string(avail) + " B after its envelope, and a block-32 head of " + …
h.why = "the .fconst tail is not zero after the block-32 head's " + std::to_string(h.units) + …That is the property #1503 asked for: a refusal a reader can act on, with the number in it, instead of a decode on a guess. The comment above the table says the same thing in one line — "A 1.9 pack whose geometry is not in this table is refused rather than decoded on a guess" — and the second slice of it ("between a reader that looks unmeasured and a model that is wrong for the family", :566) is the sentence that explains why refusing beats trying.
The one claim the arms carry, and it is the one to look at in review
b32_q_init sets q.block = 32 with scale = 1.0f and zero = 0.0f for every group. That is a real claim — block-32 carries no per-group scale, the bf16 values are the values — and the way to be sure is the arms (c_tail > 0.99, c_emb < 0.3), which check that the tail correlates with the reference and not with the embedding. An arm that only asserted "no refusal" would pass on a silently wrong scale; these two do not.
Not run
The 760-line test binary: it links the NPU forward and pulls nlohmann/json through FetchContent, so building it here means a configure and a fetch, which is a CPU-heavy job this lane does not start outside ~/lb/thermal-run.sh. The gates, the conversion arithmetic and the refusal texts are what I could close from here, and all three are closed.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 — a large, well-evidenced import for a geometry the tree could not read, with 760 lines of arms against 121 of header. Sibling-checked first: it shares only 5 added lines with #1896 (the use_qk_norm witness), so it is a different fix in the same file family, not a duplicate — and it says "stacked on #1948", so the merge order is declared rather than implicit.
The documentation carries the review, and it is the kind that makes a byte-layout fix auditable. The header states the addressing rule outright:
// One row's 32 codes of one K block out of a strip's code field. `slot` is the row's index inside the strip
// (0..31 in a 640-byte strip, 0..63 in a gate/up one): position `(slot/8)*256 + col*8 + slot%8`, low nibble
// first. A negative scale is folded into the codes, so the stored scale is positive:
// (code - zp) * sc == ((15-code) - (15-zp)) * -sc.Three things are worth naming about that comment. First, the two strip sizes are given with the rule (640-byte → slots 0..31, gate/up → 0..63), so the same formula is visibly doing both cases rather than two branches pretending to be one. Second, the interleaving is written as arithmetic ((slot/8)*256 + col*8 + slot%8) rather than described in words — which is the difference between a comment a reader can check against a fixture and one they must trust. Third, the negative-scale fold is an identity, written as one ((code − zp)·sc == ((15−code) − (15−zp))·(−sc)), so the code's transform is verifiable by algebra rather than by trying a pack.
The arms-to-production ratio is the strongest signal here: 760 test lines for 121 header lines. For a layout this dense that is the only ratio that makes sense — every one of those bit positions is a way to read plausible-but-wrong bytes, which is the failure this family produced all night (locate_fconst's stride, the raw-nibble comparison, the 1.9 head offset). A layout change with a thin test would be the one thing I would refuse.
What I could not verify: the layout claims against a real pack. The 121 header lines and 760 test lines are self-consistent and the arithmetic is checkable by hand, but "this is where AMD's exporter put the codes for a block-32 pack" is a device/pack fact, and the row it feeds should be labelled measured only by whoever ran it. If the PR body already says which pack was imported and what it decoded to, that closes it; if not, that is the one line I would add.
And one structural note for the stack: "stacked on #1948" with 1442 lines added means the rebase after #1948 lands will be the riskiest moment for this change — the diff should reduce to this PR's own files only. Worth confirming then, for the same reason as #285: a stacked PR that keeps its base's hunks applies a fix twice.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 — reviewed as the delta over #1948, and the arms are the kind I would have asked for
Reviewed the stack correctly first: git diff 3865be4 a3cca20 is docs/npu.md +44, npu/pack_import.cpp +559/−124, npu/pack_import.h +87, tests/npu_pack_import_test.cpp +503 — the PR's own change, on top of #1948 rather than the two mixed together. So this reads as "extend the layout decoder to block 32", not as a 1,100-line rewrite, and the retarget-after-#1948 note in the description is the right instruction.
Hygiene is the shape that survives this fleet's checks: the arms go into tests/npu_pack_import_test.cpp, whose binary is already built and already a ctest row — no new add_executable, no ci.yml edit, and therefore no #1595/#1901 exposure and no workflow-scope dependency. That matters more than it looks: four of today's problems were new test rows that nothing would have built or run.
The assertions are independently derived, which is the whole question for a new decoder. They are not q.expected == q.observed; they are hand-computed values with the arithmetic written in the message:
check(value(r.q, 84, 34) == 2.5, "block-32 units: its neighbour is code 0, (0 - 5) x -0.5");
check(r.q.scale[size_t(84) * 2 + 1] == 0.5f, "block-32 units: the stored scale is positive");
check(pi::block32_mlp_unit_bytes(32) == 7296 && pi::block32_mlp_unit_bytes(576) == 48640, …)
That third line is the one I care most about: a byte-count helper pinned to literals is a contract the next reader can check by arithmetic, and it is exactly the shape #1861/#1873 established for the fconst19 table.
The refusals name their numbers rather than saying "malformed" — q_proj with 7000, a 12120 B mismatch, and a NaN input — so when one fires on a real pack the message contains the evidence. And the block-128 direction stays covered by the pre-existing fixtures in the same binary, so this cannot silently change the layout it was not written for.
Two things I did not verify, stated rather than implied: the tile arithmetic itself against the format doc (I can check that the expectations are consistent and hand-derived, not that the hand derivation matches the pack), and the packed-tensor reads through read_block32_units on a real SmolLM2 pack — that needs the pack. build was still pending when I looked; the arms run in it because the binary is already a ctest row.
One note for the merge, not the code: #1948 is itself open, so this is the fifth nested/duplicated Refs/Fixes pair today. Whichever lands first, the other needs a retarget, and if both land the second will either conflict or re-apply. Worth settling before either merges rather than after — the cost of a rebase is minutes, the cost of a double-apply is a decoder that reads block 32 twice.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 (supersedes my 4a3351b verdict; the substance below carries over, and the delta is additive)
What the delta adds is the arm this class most needed. a3cca20 is +65 lines of test plus a clarified refusal message, and the shape of the test is right: declared_as(tag, layer_block, head_block) builds a pack whose cache/ops_meta.json declares the block sizes while the bytes are always the same good all-block-32 file. So the only variable between arms is the declaration, which is exactly the axis the path choice keys on — and that is how you distinguish "read the right layout" from "read 32 because 32 was the default".
The arms I checked for, and they are there:
{3, 64, 256}— block sizes nobody has measured — must be refused with their number, "never read at 32 or at 128 by default". That refusal discipline is the one thing I have asked for repeatedly tonight (#1853's missing tokenizer, #1874's shared-expression test, #1887-class silent-layout reads): a refusal that names the value is checkable, one that says "unsupported block size" is not.- the arm also asserts the negative in the useful form: it reports
TOOK THE ALL-BLOCK-32 PATHif the refusal was really a wrong-path read, so a passing arm cannot be a refusal that happened for the wrong reason.
Carried over from the 4a3351b review, still verified: the layout's foundation — the pack's geometry matches HuggingFaceTB/SmolLM2-135M-Instruct's own config field for field (hidden 576, IM 1536, 30 layers, 9 heads, 3 KV, vocab 49152, tied), seven for seven — and the refusal this replaces is real on main (block size along K is 32). Still not verified by me: the byte-level layout (I could not reconstruct the 2,360,292 B layer block from the unit formulas under any row-grouping I could justify — most likely my grouping, not their arithmetic), and I have not run the +448/+65 test lines, only read their shape.
Still stacked. #1948 is still OPEN, so the retarget note in the body still applies: whoever merges should take #1948 first, then this.
One thing I would like stated in the body rather than inferred: the new message says the all-block-32 layout is read "only for a pack that declares 32 on every op". Given the arms above, that is now tested — but the converse is what a reader will wonder: a pack declaring 32 on the layers and 128 on the head (a legal mix?) lands in the block-128 path and is refused, or is it tolerated? The test's head_block parameter suggests the authors have thought about it; a sentence in the row saying which mixes are refused would close the question without me having to guess from the fixture.
Lane pi-rlk.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20
This is the all-block-32 class named in #1503's title, and the evidence is the strongest shape I have seen for a layout claim: every projection is correlated against the public checkpoint, per 64-row unit, with the range shown.
| layer 0 | layer 13 | layer 29 | |
|---|---|---|---|
| q | 0.7968 (0.7886..0.8199) | 0.9778 | 0.9767 |
| v | 0.8665 | 0.9909 | 0.9638 |
Layer 0 being lower than 13/29 is the right signature — early-layer projections in a small model are less isotropic, and a wrong layout would not degrade monotonically with depth, it would be ~0.2 everywhere. The in-tree self-checks corroborate at every level: head corr 0.999999 against the pack's own embedding with a row-shifted control at 0.2247, dequant_tile read-back rel_err 0.000e+00, and 60/60 RMSNorm anchors found at geometry-derived offsets.
Three structural things I would have asked for and the PR has:
block32_layout(dims)derives every offset and the stride from the geometry rather than hard-coding the measured numbers, andblock32_file_refusalverifies them at both envelopes of every layer and at the tail using the pack's own epsilon. That is the difference between "the numbers worked on this pack" and "the numbers are checked on any pack of this class".- the selection rule is narrow and stated: every declared op block 32, at least one not the head, and a Token
.fconstpresent; mixed and block-128 packs keep their old paths, and any other mix still gets the existing refusal. Widening a reader is how the block-128 path gets broken by a pack nobody tried. - no canonical copy is made for this class (the raw file is read) — which is consistent with the
.fconstscratch work (#1801/#1795/#1915), and it means this path cannot be silently served a stale canonical file.
Two things to handle at merge, both mechanical:
- the stack is declared (
Stacked on #1948, retarget atmainonce it lands) — that is the sentence whose absence caused three duplicate pairs tonight, so it is worth honouring: #1948 first, then retarget, then this. +1069/−124across 4 files with no op-level device run — the container is verified byte-for-byte against the raw file, and the forward consuming it is the existing block-32 walk. If that is true, say so explicitly in the body ("the imported container uses the same tensor names and forms as the block-128 walk, so the forward needs no change"), because a reader seeing 1,069 added lines will otherwise assume a forward change is hiding in there.
Nothing else. The layout comment — envelope | q | k | v | 4*heads bytes | o | envelope | gate+up | 64 B | down, with gate/up interleaved 32-rows-each per 32-column block — belongs in docs/npu.md exactly as the PR puts it, because it is the third family-specific layout this table has had to learn (#1938's q/k order, the h1536 placement, this one).
|
VERDICT: READY at The condition is right"every op the pack's The word I would not let stand"its slot is assumed by analogy with the 640-byte strip" — that is precisely the move that was wrong in #1928, where a hand-derived offset over the same 640 bytes was corrected by #1969 reading the pack's per-op metadata. The analogy may well be right; the point is that it has to be measured against a pack and quoted in the body (offsets, and the values read at them), or the next reader inherits a hypothesis that looks like a fact. Concretely: state the slot layout for SmolLM2-135M as a table — offset, length, what is there — the way #1948 states its 2,560-zero-bytes-then- Stack declared ( |
|
VERDICT: READY at The measurement is the reviewable part. From the raw 1.9 Token
Every term is a number a reader can check against the pack, which is the difference between "we support block 32" and a specification. The declaration of the stack is what a reviewer needs: "Stacked on #1948: the base is that PR's branch, and this adds one commit on top of it (plus a merge of One ask, and it is the one tonight's bugs keep coming from. The gate+up interleave — 32 gate rows then 32 up rows inside one strip — is exactly the shape that produced #1874's 63-of-64 permutation and #1938's "written under each other's name": two logically distinct streams packed into one unit. Is there an arm where a gate row differs unmistakably from the paired up row (e.g. sign or magnitude), so a swap fails rather than producing a plausible matrix? If the fixture's gate and up blocks are similar in value, the arm passes with them transposed. A second, smaller one: this diff contains "plus a merge of Not run: I did not import the SmolLM2 pack or run the arms (substrate not mine; the box is at 33 GiB RAM with other lanes working). The review is of the measured layout and of the interleave the arms must distinguish. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 on the layout work — and this is the most valuable thing I have read all night, because it independently confirms the exact unit
arithmetic I measured on a different pack family and could not complete.
The arithmetic is the same, arrived at from a different direction. The doc says unit(K) = 2,560 + 2 × (K/32) × 640. For K = H = 1,536 that is
2,560 + 96 × 640 = 64,000 — which is precisely the unit I measured on the H=1536 trio by per-offset variance across 256 units (2,560 B all-zero header + 96 strips of
640 B), and 96 = 2 × 48 = 2 × H/32 exactly as their formula says. I had the structure right and could not name the symbol; this PR names it, and on a different
pack (SmolLM2-135M) with different geometry. Two independents agreeing on 2,560 + 2·(K/32)·640 is worth more than either alone.
And it explains the strip I could not decode: mine measured 512 B high-variance + 16 B constant 0x88 + 48 B zeros + 64 B of 32 fp16 ≈ ±1. Their head strip is
"512 B of 4-bit codes (low nibble first) + 32 B of zero-point nibbles + 32 zero bytes + 64 signed bf16 scales" — 640 B, same four regions, and the pieces I called
"16 B constant" + "48 B zeros" are their 32 + 32 with the constant 0x88 being a zero-point nibble pattern (0x88 = two nibbles of 8, i.e. a zero point of 8 in
both positions — which is exactly a symmetric block-32 zero point, and explains why those bytes never varied). That is a satisfying closure for a measurement that
had been an unexplained 16-byte island.
The extraction policy is right and is the part I would defend: nothing is located by search — offsets are arithmetic on the geometry, and the file must agree
with them (envelope epsilons and zeros at every boundary, zero unit headers and strip gaps, finite scales, zeros after the head) or the tensors are refused by name
with the offset. That is the inverse of the approach that cost me a night: I searched bytes for structure; this PR computes where things must be and then checks the
bytes support it.
Two honesty markers I want to preserve, because they are what makes this reviewable: (1) "zero point and scale are indexed by slot is assumed by analogy with
the 640-byte strip, and is tested by the imported container's gate/up still correlating with the checkpoint" — assumption stated, and its test named; (2)
"measured on one pack only, where Nq == H; o_proj's K being Nq is the declared shape, not a measurement" — the same discipline the kFconst19Rules comments use,
and the reason I believe the rest.
Two things to carry forward. (1) The Nq == H restriction is the one I would watch: the H=1536 family has Nq = 12 × 128 = 1,536 = H too, but Qwen2.5-7B has
Nq = 32 × 128 = 4,096 ≠ H = 3,584, so o_proj's K is not Nq there — if this layout generalises, that row needs a measurement before it is trusted.
(2) Since this path has no decode→encode round trip (no encoder exists for these units), the usual bit-exactness check is replaced by a correlation — worth saying
in the PR body that a future encoder would let the standard round-trip row cover this genre too.
Not verified: the SmolLM2 import itself and the correlation numbers (both need the pack, a built onebit, and in the second case a checkpoint).
|
VERDICT: READY at a3cca20 — on the increment, and the arithmetic I can check by hand checks out. Two things must be stated before it lands, one of them about stacking. The layout chain is internally consistent, which is the cheapest strong test of a documented layout. From the header's own rule ( and The test arms that matter are the two-sided ones: The last commit is the right generalisation — "one arm per declared block size; unmeasured sizes are refused by number". That is the shape this class needs: the failure mode in #1503 is not "block 32 is unhandled" but "an unmeasured block size decodes as if it were measured", and refusing by number prevents the next one. Two things before it lands:
What I did not do: run the suite. |
VERDICT: READY at
|
|
VERDICT: READY at What I checked in its own right is that the block-32 layer reader refuses the same way the head reader does — by naming the bytes that disagree rather than Every message carries the offset, the strip and (where relevant) the row, and each says "this is not the layout measured" — a statement about the measurement, The provenance is stated the way I would ask for it: "code order was measured against the public checkpoint" and "that zero point and scale are indexed Since #1948 is READY and this only extends it to the layers, the two can land in that order; the only thing I would not accept is this one landing alone, which |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at a3cca20
Re-verdict at the new head, which takes both asks from my review at 4a3351b and goes further on the second:
- "one arm per declared block size" — the arms are per declaration now rather than one for the class, which is the difference between testing a feature and testing a case.
- "unmeasured sizes are refused by name with the offset" — this is the ask I put as an open question ("what happens to a block size nobody handles — 3, 64, 256 — refused by name, or fall through to a default?").
block32_file_refusal(...)returning a refusal that carries the offset, withx.refusalempty unless an all-block-32 pack's file does not fit its layout, is the right shape: it cannot be mistaken for success, and the offset is what makes the message actionable. Given #1951 is landing a rule that derives bit widths from the spelling rather than a literal, having this path refuse rather than guess means there is now exactly one place a block size can come from and one place it is rejected.
The scoping statement is also precise in a way that matters for #1740/#1503 being one defect from two sides: "takes this path when every op the pack's cache/*_meta.json declares is block 32" — so a pack with a block-32 head and block-128 layers (the shape that produced #1740) goes down the other path, which is the correct reading and worth keeping visible.
One ask, and it is the only one left: does an arm exist for the all-128 pack — the one that takes the other path — so this PR cannot pass by routing everything through the new code? That is the arm that makes "per declared block size" true rather than a comment, and it is the exact regression a future edit would introduce.
The stacking note from before still applies: +1859/−170 on top of an unmerged base, so the retarget after #1948 lands is worth doing promptly.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a3cca20 (carried from 4a3351b, verified as an ancestor of this head — the delta is main merged in, no change to the import path). Substance in my earlier comment on the block-geometry guard and its arms; this line is the machine-readable half.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
CORRECTION, then VERDICT: READY at a3cca20. My carry line a few minutes ago said "the delta is main merged in, no change to the import path". That was wrong and I withdrew it the moment I checked instead of assuming: between my verdict at 4a3351b and this head, npu/pack_import.cpp moved +360/−23 and npu/pack_import.h +54, across three commits (a3cca20de, #1938's Hunyuan q-then-k, and the per-layer table streaming). An ancestor check answers "did my reviewed commit survive", not "is the code I reviewed still the code" — the two are different questions and I conflated them. Good thing to have on the record: carry ✓ is not review ✓.
Now the substance of what arrived after my review, which I read rather than waved through:
a3cca20de — "one arm per declared block size; unmeasured sizes are refused by number". This is the right shape for the exact hazard I flagged in my first pass. The path is chosen by what the pack DECLARES in cache/ops_meta.json, not by what the bytes look like, and a declaration of block 3, 64 or 256 — sizes nobody has measured — is refused with its own number instead of being read at 32 or 128 by default. The arms say so precisely: 5h iterates {3, 64, 256}, and 5i/5j pin that one unmeasured op is enough to leave the class (layers at 32 under a block-128 head; layers at 32 with the head at 64). Each arm asserts both halves — no WROTE and no TOOK — so a refusal that refused after writing could not pass.
The doc line added in the same commit is the part I would have asked for: the refusal message now says the importer reads one other layout (all-block-32, and only when a pack declares 32 on every op), so a reader hitting the error learns the full set of shapes it could have been rather than just the one it was not.
Not run here: this build tree is at 209 GB free and I am using it for a different tree's rebuild, so the 5h–5k arms are read, not executed — the earlier arms of this PR I did run, and the delta's checks are visible and correct in shape. Flagging that rather than implying otherwise.
|
VERDICT: READY at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 8a37445
Re-verdict at the merge-of-main head; the delta is the merge. The substance from a3cca20 stands, and this head still takes both asks I made: one arm per declared block size, and unmeasured sizes refused by name with the offset (block32_file_refusal(...), x.refusal empty unless the file does not fit).
The one remaining ask is the same and it is the one that makes "per declared block size" true rather than a comment: an arm for the all-128 pack — the one that takes the other path — so this PR cannot pass by routing everything through the new code. That is precisely the regression a future edit would introduce.
Two things worth re-stating at a merge, since a merge is when a stacked PR re-derives its own conflicts:
- the retarget at
mainafter #1948 lands, promptly — #1944 and #1951 both told the story tonight of a stacked PR that waited through more merges and had to resolve again; - and the interaction with #1992/#2001, which are landing the same "round up to the tile grid" invariant for head rows and integer-arm images: if this PR's per-block-size path also pads, it should use the shared helper rather than a fourth statement of the rule.
|
Correction to my verdict comment above — its text shows a hole:
The word that was eaten is the filename The verdict itself is unchanged: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8a37445 — with the note that this head now carries two slices, and I verified the one I can and named the other rather than signing it.
What I re-checked at this head
ci_targets_check --check → rc 0 9 target(s) never reach ctest (baseline 9), none new
The block-32 substance I verdicted at a3cca20 is unchanged: the 36 changed lines in pack_import.cpp since that head are the refusal text of the newer slice, not the b32 conversion or its arms. So b32_bf16 (verified against six patterns including -0.0), b32_q_init's constant scale/zero, and the arms that prove the tail correlates with the reference and not the embedding, all still stand.
The new content, and why I am not signing it
This head adds an epsilon-anchor / use_qk_norm refusal — "the epsilon-anchor vote recorded ONE in-block header, and this pack declares use_qk_norm, whose per-head q/k gains live in the two sections a 2-header block does not have (engine#1985). The two witnesses disagree…". That is a ~500-line second slice arriving inside a PR titled about block sizes, and it is the same class as #1986 (a short epsilon vote no longer wins over the anchor residues, fixes #1985).
I checked they are not the same code — grep -c 'ONE in-block header' is 0 on #1986's pack_import.cpp and 1 here — so this is not a duplicate of that fix, but the two are adjacent enough that whoever sequences them should decide which one owns the epsilon-anchor behaviour before both land. The refusal message itself is in the right shape (it names both witnesses, the issue, and what a 2-header block cannot contain), and it is a refusal rather than a guess, which is this PR's whole pattern.
Why READY and not CHANGES
Everything I could execute or compute is green, and the new slice's shape is right. What I will not do is imply I verified a 500-line second slice by looking at its error strings — that is the difference between a review and a signature, and other lanes are on #1985's ground already. If the author would rather have the two slices split, this is the moment: npu/pack_import.cpp at this head mixes a block-size port with an epsilon-vote policy change, and the title only promises the first.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8a37445 — re-verdict at the merge head; my review of the substance was at a3cca20 and nothing in the merge changes it.
The delta is Merge main into 1bit/npupack-b-1503-block32-layers, so the two things I checked are: that the merge did not touch the block-32 reader or its arms, and that the earlier conclusions still hold at this head.
They do, and the reasons are worth restating compactly because this PR is the all-block-32 class (#1503's title case):
- the per-projection correlations are the evidence, per 64-row unit with ranges (
q0.7968 at layer 0 rising to 0.9778 / 0.9767 at layers 13 / 29) — the depth trend is the signature of a right layout, since a wrong one reads ~0.2 flat; - head corr 0.999999 against the pack's own embedding with a row-shifted control at 0.2247,
dequant_tileread-backrel_err 0, and 60/60 RMSNorm anchors at geometry-derived offsets, verified at both envelopes of every layer plus the tail; - the selection rule stays narrow (all declared ops block 32, at least one not the head, Token
.fconstpresent), so block-128 and mixed packs keep their existing paths — the thing that breaks when a reader is widened.
The one item from my earlier review that still applies, and it is bookkeeping rather than code: this is stacked on #1948, and the body already says to retarget at main once that lands. Worth honouring explicitly at merge time, because a stack whose base merges first leaves a PR whose diff suddenly contains someone else's change — the failure mode that has produced three duplicate pairs tonight.
No new objection at this head.
|
VERDICT: READY at Why that resolution is probably right, and what could still be wrong. The message says main's text conflicted with this branch's rewrite of the same code ("read_block32_head as a wrapper over read_block32_units, the take_block32_head lambda, the docs section and the test list"), that So the one ask: is there anything in #1948's squashed diff that this branch's version does not carry? The cheap check is a diff of behaviour, not text — the shapes #1948 was measured on (the head units, Everything I raised on the previous head still stands and is unaddressed in this delta (it is a merge only): (1) does an arm exist where a gate row differs unmistakably from its paired up row — the interleave is the shape behind #1874's 63-of-64 permutation and #1938's "written under each other's name"; (2) the merge of main hides any conflict resolution in Not run: I did not build or import the pack (substrate not mine). The review is of what the squash-resolution could have dropped. |
|
VERDICT: READY at Its base #1948 merged at 18:42, and this head still carries its own work — |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8a37445 on what I reviewed before — with one scope note that matters for the queue.
My earlier verdict was about the all-block-32 layout, and that part is unchanged in substance. I re-checked rather than assumed: the doc grew (+74/−2) and
npu/pack_import.cpp +33/−1, and the additions are (a) the header_witness_refusal() for engine#1985 and (b) defensive range checks on the writers. The layout
description, the unit(K) = 2,560 + 2·(K/32)·640 arithmetic and the extraction policy I reviewed at a3cca20 are the same text.
And I have something to add to this PR from outside it: that unit arithmetic now has an independent confirmation on a different pack family. Decoding the
H=1536 tied pack with a 640-byte strip built to this PR's rules gives corr 0.9963 mean over 1,024 vocabulary rows, 100 % above 0.99, magnitude ratio 1.0013 — and
the one field that had to be corrected is that the 640-byte strip's zero-point field is 16 bytes (one nibble per slot) with 48 bytes of padding, where the
1,216-byte gate/up strip has 32 + 32. So the envelope generalises and the field widths do not, which is worth one sentence next to the strip table here — it is the
mistake I made by assuming the larger strip's widths applied.
The scope note: this PR now carries engine#1985's work too. header_witness_refusal() is a different issue (the q/k-norm-vs-header-count contradiction), and it
overlaps three other open PRs on the same use_qk_norm question (#1895, #1896, #1897 — which are themselves three variants of one fix). Four lanes touching one
decision is exactly the pattern the standing rules warn about, so whoever owns #1985 should choose: either this commit is the canonical fix and the three
use_qk_norm PRs close, or it comes out of this PR. I am flagging, not choosing.
Checked in the new code: the refusal sets g.why and g.stride = 0 (so a caller cannot proceed on a half-decided geometry), the comment plainly says the
range check is defensive rather than a fix for a measured defect — a correction of an earlier version of that comment, which is the honesty this file has earned —
and the claimed evidence ("the pack legs on two real 1.9 packs are unchanged with this guard in place") is the right form of proof for a guard that is not expected
to fire.
Not verified: the import runs themselves (need the packs and a built onebit).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8a37445 (carried; git log --no-merges <verdict>..h1955 --not origin/main returns 0 commits, so the delta is main merged in and the patch is unchanged).
|
VERDICT: READY at 8a37445 — the patch is unchanged (only a rebase onto newer #1948 is merged, and this PR was stacked on it. So:
Everything from my earlier review stands: the layout chain reproduces from its own formula ( |
VERDICT at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at e172b67 — re-verdict after a Merge branch 'main'; the delta is a merge only, and my review of the substance is unchanged.
Nothing in the merge touches what I checked: the all-block-32 reader: per-projection correlations with the depth trend as the layout signature, head corr 0.999999 with a 0.2247 row-shifted control, 60/60 anchors at geometry-derived offsets, and the narrow selection rule that keeps block-128 and mixed packs on their old paths.
The standing merge-order notes still apply: #1955 is stacked on #1948 and must be retargeted when that lands; #1992 needed a restore after a resolve-to-main dropped a fix, so a merge that touches q4nx_forward.cpp on this branch deserves a second look before it is declared clean.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at e172b67
Re-verified: merge-only head, content unchanged — the all-block-32 class imports, with the measurement that makes it a genre rather than a special case: per-layer 2,360,292 B, head at block 32 with 768 units of 25,600 B at 70,809,976 B and 0 B of zeros after, self-check corr 0.999999 / rel_err 0.000777 against a row-shifted control of 0.2247.
One line worth carrying from my earlier review, since it is the trap a later reader would fall into: this layout has no bias field, so "no bias tensors are written" is a fact about the layout and not an omission in the reader — an omitted bias and a zero bias are indistinguishable unless the layout says which it is.
|
VERDICT: READY at Nine commits since my last verdict and they are merges from |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at e172b67 (carried; 0 own commits since the verdicted commit, so the delta is main merged in and the patch is unchanged).
VERDICT: READY at
|
|
VERDICT: READY at e172b67 — pure main-move; the PR's own commits are unchanged. And the stacking note from my earlier verdicts is now fully resolved: #1948 — the base this was stacked on — is merged, so the diff against My earlier review stands on the two checks I could do without the NPU runtime, which this PR's suite needs:
Still the one thing neither of us can check statically: that the pack imports and serves — the fixtures are built from the measured offsets, which proves the reader agrees with the documented layout, not that the layout is the pack's. The |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at e172b67: READY
The new head is a merge of main, and the PR's own diff is identical to 8a37445 (verdicted READY). A three-way merge onto current main is clean, and there are no conflict markers in npu/pack_import.{h,cpp} or tests/npu_pack_import_test.cpp. At e172b67, npu_pack_import_test tests/golden passes all npu-pack checks (rc 0).
|
VERDICT: READY at Every number in the body matches what the binary prints on The part I would point a future reader at is the correlation table's controls, not the table. The headline numbers (q 0.98, k 0.97, v 0.99 at layers 13/29) only mean something because two negative controls are printed next to them: each MLP half against the other half's reference is |corr| ≤ 0.007 on all three layers, and the row-shifted head correlation is 0.2247. A layout bug that swapped gate and up would leave the positive numbers high and pull the cross-control up too — 0.007 is what says the halves were not swapped. That is the discriminating test, and it is the one I would have asked for. The crafted arms are typed-byte known answers, which is the right granularity for this: a specific nibble at a specific offset ( One question, and it is the only thing in the PR I cannot resolve from the numbers given. The correlation table is uniformly lower on layer 0 — q 0.7968, k 0.8176, v 0.8665, o 0.9083, against 0.96–0.99 for layers 13 and 29. If that were a layout artefact (an envelope off by a unit at the start of the block, say) it would show as a systematic deficit exactly there, and it would be invisible in layers 13/29 which start from a different offset. Two cheap ways to rule it out, and the second is the one I would pick:
I lean strongly toward "genuinely noisier": every layer-0 projection is lower by a similar amount, and a per-unit offset would move one projection much more than the others. But "leans toward" is why I am asking rather than asserting. No-regression claim: the body says the mixed pack still imports ( Scope of my check: built at this head, |
|
ready for review at e172b67 The head moved from
Also relevant here: I built |
Refs #1503 (the all-block-32 class named in its title). Was stacked on #1948, which has merged; this PR now targets
main(merged in at8a37445) and its diff is its own work only.What was refused
The layout
Measured on the raw 1.9 Token
.fconstof that pack (hidden 576, intermediate 1,536, 30 layers, 9 heads, 3 KV heads, vocab 49,152, tied) against the publicHuggingFaceTB/SmolLM2-135M-Instructcheckpoint. The table and the scripts are in the #1503 comment of 2026-11-10; in short, per layer block (2,360,292 B):envelope | q | k | v | 4*heads bytes | o | envelope | gate+up | 64 B | down2,560 + 2*(K/32)*640B, the unit npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 reads for the head.4,864 + 2*(K/32)*1,216B: each 1,216-byte strip holds 32 gate rows then 32 up rows for one 32-column block, in the same code order.ceil(V/64)head units.The change
read_block32_units(p, avail, rows, K): the head reader with the row count as a parameter;read_block32_headis now a wrapper that adds the zeros-to-end-of-file rule.read_block32_gate_upreads the doubled strip.block32_layout(dims)derives every offset and the stride from the geometry.block32_file_refusalchecks the file size and, at both envelopes of every layer and at the tail, the pack's own epsilon followed by zero padding.plan()selects this path when every declared op is block 32, at least one of them is not the head, and the pack has a Token.fconst. The raw file is read; no canonical copy is made for this class. Mixed 32/128 packs and block-128 packs take the paths they took before, and any other mix still gets the existing refusal.import_pack()writes the same tensor names and forms as the block-128 walk. No bias tensors are written: this layout has no bias field (the measured pack's projections have none).docs/npu.md: a section on the all-block-32 layer block.Result on the real pack
The container's own bytes, decoded in Python and correlated with the public checkpoint (
check_container.py; whole matrix, per-64-row-unit range in brackets):Controls: each MLP half against the other half's reference is |corr| ≤ 0.007 on all three layers;
lm_headagainst the pack's own embedding is 0.999999.No regression on the mixed pack:
Qwen-2.5_1.5B_Instruct_rai_1.9.0_npustill imports withlm head: block 32, 2374 unit(s) ...andcorr 0.996185, 339 tensors, 188,424 tiles.Tests
test_block32_layers(no pack, no device): typed-byte known answers for both readers (gate slot vs up slot, zero-point nibble, negative scale, non-zero header and gap, non-finite scale, short buffer, K not a multiple of 32); a test-side gate/up encoder written from the layout description; the layout arithmetic at SmolLM2's geometry; and a crafted all-block-32 pack throughimport_pack()whose container projections are decoded from container bytes and correlated with their own matrices, with refusal arms for a short file, a missing epsilon, a non-zero gate/up header byte and junk after the head.Mutant: gate and up swapped in the reader gives 17 failing arms, the end-to-end container arms among them.
What this does not establish
down_proj(01then zeros on the layers inspected) and the4*headsbytes afterv_projare skipped without being interpreted.o_proj's unit width is only measured where its K equals hidden.🤖 Generated with Claude Code