Skip to content

npu-pack: import a 1.9 pack whose every MatMulNBits is block 32 (engine#1503, stacked on #1948) - #1955

Merged
bong-water-water-bong merged 8 commits into
mainfrom
1bit/npupack-b-1503-block32-layers
Nov 10, 2026
Merged

bong-water-water-bong merged 8 commits into
mainfrom
1bit/npupack-b-1503-block32-layers

Conversation

@bong-water-water-bong

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

Copy link
Copy Markdown
Collaborator

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 at 8a37445) and its diff is its own work only.

What was refused

$ 1bit npu-pack info SmolLM2-135M-Instruct_rai_1.9.0_npu          # main
this pack's MatMulNBits block size along K is 32 (151 ops), and this importer decodes the block-128 tile layout only ...

The layout

Measured on the raw 1.9 Token .fconst of that pack (hidden 576, intermediate 1,536, 30 layers, 9 heads, 3 KV heads, vocab 49,152, tied) against the public HuggingFaceTB/SmolLM2-135M-Instruct checkpoint. 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 | down

  • q/k/v/o/down are 64-row units of 2,560 + 2*(K/32)*640 B, the unit npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 reads for the head.
  • gate and up share 64-row units of 4,864 + 2*(K/32)*1,216 B: each 1,216-byte strip holds 32 gate rows then 32 up rows for one 32-column block, in the same code order.
  • the tail is the final-norm envelope and 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_head is now a wrapper that adds the zeros-to-end-of-file rule. read_block32_gate_up reads the doubled strip.
  • block32_layout(dims) derives every offset and the stride from the geometry. block32_file_refusal checks 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

$ 1bit npu-pack import ~/scratch-packs/pk-smol19 -o out
  lm head: block 32, 768 unit(s) of 25600 B at 70809976 B, 0 B of zeros after them (tied: verified by correlation with the pack's embedding)
imported 273 tensors, 20808 q4_1 tiles into out
  self-check: the head reproduces the pack's own embedding (as imported, bf16)  corr 0.999999  rel_err 0.000777
  controls:   row-shifted corr 0.2247 (must fail)
  container:  read back through q4nx::dequant_tile  rel_err 0.000e+00  max |diff| 0.000e+00
  fconst:     stride 2360292, second header 615652 (computed from the geometry, checked at the eps anchors); 60/60 RMSNorm anchors found
  block 32:   every MatMulNBits is block 32: 120 attention + 90 MLP projections and the head read from 2478 units of the raw .fconst

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

layer 0 layer 13 layer 29
q 0.7968 (0.7886..0.8199) 0.9778 (0.9726..0.9834) 0.9767 (0.9702..0.9819)
k 0.8176 (0.8018..0.8389) 0.9673 (0.9657..0.9686) 0.9723 (0.9711..0.9742)
v 0.8665 (0.8597..0.8770) 0.9909 (0.9909..0.9920) 0.9638 (0.9855..0.9947)
o 0.9083 (0.8577..0.9364) 0.9938 (0.9930..0.9942) 0.9706 (0.9666..0.9723)
gate 0.9785 (0.9752..0.9809) 0.9844 (0.9822..0.9867) 0.9699 (0.9534..0.9838)
up 0.9863 (0.9827..0.9889) 0.9913 (0.9898..0.9923) 0.9616 (0.9424..0.9715)
down 0.9872 (0.9834..0.9902) 0.9947 (0.9943..0.9949) 0.9116 (0.8785..0.9280)

Controls: each MLP half against the other half's reference is |corr| ≤ 0.007 on all three layers; lm_head against the pack's own embedding is 0.999999.

No regression on the mixed pack: Qwen-2.5_1.5B_Instruct_rai_1.9.0_npu still imports with lm head: block 32, 2374 unit(s) ... and corr 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 through import_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.

$ npu_pack_import_test tests/golden
all npu-pack checks passed -- pack leg SKIPPED, no pack directory given

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

  • The reference is not this pack's source. Correlations are 0.80 to 0.99, not 1, and the least-squares scale of reference on container is far from 1 for some projections (layer 0: q/k/v 0.32 to 0.38, o 1.04, gate 1.73, up 1.18, down 1.46), while layer 0's two layer norms correlate only 0.85 and 0.82 with the checkpoint's. I read that as the pack rescaling weights against norm gains relative to the public checkpoint. That is an inference. Placement, row order and the gate/up split are established by the table and its controls; the absolute scale of the layer projections has no reference in the pack.
  • No forward run. Nothing here was served or run through a forward pass, on the device or on the host, and there is no kernel set for hidden 576. A known-answer prompt on the imported container is what would settle the previous point.
  • The gate/up zero-point and scale index is taken to be the slot index. Six alternatives score lower against the reference (at most 0.953 / 0.947 against 0.9785 / 0.9863), which ranks the hypotheses rather than proving one.
  • The 64 bytes before down_proj (01 then zeros on the layers inspected) and the 4*heads bytes after v_proj are skipped without being interpreted.
  • o_proj's unit width is only measured where its K equals hidden.
  • This path has no decode→encode round trip (there is no encoder for these units in the tree) and no synthesis fallback: a tied all-block-32 pack whose head fails its check is refused by name.
  • One pack. No other all-block-32 pack is on this box.

🤖 Generated with Claude Code

bong-water-water-bong and others added 3 commits November 10, 2026 11:12
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>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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 ~/lb/cl-npupack-b/i1503/logs/.

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

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 bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: APPROVE at 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:

  1. 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.
  2. 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 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 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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY 4a3351b — with the ordering it needs, and the target graph clean

ci_targets_check.py --check → rc 0 at this head, so nothing new needs a ci.yml edit.

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
(plus a merge of main). It should be retargeted at main once #1948 lands."
The commits agree (58972137 is #1948's, then a Merge main, then
4a3351be this PR's own). So: land #1948 first, then retarget/rebase this — merging this first would carry #1948's commit under a different PR number,
and merging both would apply it twice.

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
576 / intermediate 1,536 / 30 layers / 9 heads / 3 KV heads / vocab 49,152, tied — measured against the public checkpoint), and the refusal it replaces is
the good one ("decodes the block-128 tile layout only"), so this extends the reader rather than loosening a gate.

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
block-32 import). If you want a read of the layout arithmetic specifically, say so and I will take 4a3351be alone against the #1503 measurement.

@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 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_Q4 at block 32, and their tail after the envelope is 64-row units instead: 2,560 zero bytes, then 2 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.

bong-water-water-bong and others added 3 commits November 10, 2026 12:52
Report gains both head_read_note (#1740) and per_layer_P/V (#1725).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…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>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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 1bit/npupack-b-1740-block32-head, so the 378 shared lines are #1948's own commit showing through. They cannot apply twice: git merges the same commit once. #1948 is canonical for the block-32 head (Fixes #1740, the mixed 128/32 Qwen-1.5B packs). This PR adds what #1948 does not do: the layer walk for packs that declare block 32 on every op (#1503, SmolLM2 1.9). Order: #1948 first, then retarget this at main. If one PR is preferred, merging this one alone also carries #1948 and both issues close.

To the two asks at 4a3351b, both now arms (commit a3cca20). The all-block-32 fixture is rebuilt with only cache/ops_meta.json changed:

declared result
every op at 3, 64, 256 refused: block size along K is N (11 ops), no container
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 off the block gate
every op at 32 imports (existing arm 5a)
layers 128, head 32 #1948's path (existing arm 5g and test_block32_tail_head)

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 main), everything else is refused with its number. Red proof: with the path selection made to ignore the declared value, all six new arms fail, each reporting WROTE A CONTAINER TOOK THE ALL-BLOCK-32 PATH.

Also in this push: main and the updated #1948 branch merged in (the Report field conflict is resolved there). Rerun on the merged tree: npu_pack_import_test rc 0; SmolLM2-135M 1.9 real-pack import unchanged (273 tensors, 20,808 tiles, head corr 0.999999).

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a3cca20 — and the arm is better than "support block 32": it is one arm per declared block size, and every unmeasured size refused by number, which turns an unsupported format into a stated limit.

What it refuses, and what that refusal cost

$ 1bit npu-pack info SmolLM2-135M-Instruct_rai_1.9.0_npu
this pack's MatMulNBits block size along K is 32 (151 ops), and this importer decodes the block-128 tile layout only

151 ops of a 135M model — every MatMulNBits in the pack, not a tail. That is the class #1503 names, and the difference from #1948 is worth stating: #1948 handles one block-32 tensor inside a block-128 pack (the head); this handles a pack where nothing is block 128. Two different readers, one per shape, is the right decomposition — a single reader that took "the block size" from anywhere would have to guess which tensor it was looking at.

The measured layout, and why the numbers matter

"Measured on the raw 1.9 Token .fconst of that pack (hidden 576, intermediate 1,536, 30 layers, 9 heads, 3 KV heads, vocab 49,152, tied) against the public Huggin[gface config]"

Deriving the layout from the pack's own .fconst checked against the model's published config is the only way to be sure the header's declared sizes correspond to the file's actual byte stride — the failure mode #1898 recorded (CMake right, second subject undescribed) is exactly the one where the declared and actual disagree. Naming all eight numbers makes the derivation re-checkable.

The two npu.md entries (+44) are part of the deliverable

The doc says which block sizes are measured and that the rest are refused. That is the standard this lane has applied to every "unsupported" path: an unsupported case must be a named refusal, not a silent mis-read.

Scope and stacking

+437/−122 in npu/pack_import.cpp, +85 header, +503 test, +44 docs. Stacked on #1948 (one commit + a merge of main on top of it), and the body says *"It should be retargeted at main once #1948lands"* — correct, and landing order is therefore#1948` → this.

One ask, and it is the one that keeps this honest: the body says "one arm per declared block size; unmeasured sizes are refused by number" — so the refusal list is the deliverable. A line naming which sizes are measured (32, 128) and which are refused (everything else) makes the next pack's info output self-explaining, and stops a future contributor reading "one arm per declared block size" as "all sizes are supported".

@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 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 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 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 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 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 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 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 PATH if 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 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 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, and block32_file_refusal verifies 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 .fconst present; 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 .fconst scratch 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:

  1. the stack is declared (Stacked on #1948, retarget at main once it lands) — that is the sentence whose absence caused three duplicate pairs tonight, so it is worth honouring: #1948 first, then retarget, then this.
  2. +1069/−124 across 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).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a3cca20 for the condition — with one question about a word in the body that this session has already burned.

**A pack whose every MatMulNBits is `block_size 32` keeps its layers in those units too** (engine#1503).
`SmolLM2-135M-Instruct_rai_1.9.0_npu` declares block 32 on all 151 ops, and its slot is **assumed by analogy**
with the 640-byte strip, and is tested by the imported container's …
`npu-pack import` takes this path when every op the pack's `cache/*_meta.json` declares is block 32

The condition is right

"every op the pack's cache/*_meta.json declares is block 32" is the correct predicate and the correct source: the pack's own metadata, read exhaustively (all 151 ops on the example), with the general path kept for any pack that mixes granularities. That is the same rule that has settled every layout question this week — the metadata decides, not the analogy — and it is what makes this safe to add next to #1948's block-128-with-a-block-32-head case.

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-2 × (K/32) units. Two measured tables in two PRs is one rule; two analogies is a coin flip.

Stack declared (#1948's branch plus one commit), which is why this was reviewable on its own.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a3cca20 — the layout is measured on the raw pack with byte-level formulas, and the stack is declared rather than implied.

The measurement is the reviewable part. From the raw 1.9 Token .fconst of SmolLM2-135M:

envelope | q | k | v | 4*heads bytes | o | envelope | gate+up | 64 B | down
  • q/k/v/o/down are 64-row units of 2,560 + 2·(K/32)·640 B — "the unit npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 reads for the head", i.e. reused rather than re-derived;
  • gate and up share 64-row units of 4,864 + 2·(K/32)·1,216 B, where each 1,216-byte strip holds 32 gate rows then 32 up rows for one 32-column block;
  • the tail is the final-norm envelope plus ceil(V/64) head units.

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 main). It should be retargeted at main once #1948 lands." Both PRs touch pack_import.cpp/.h and the same test, so the order is load-bearing — and saying so here means the second merge is not a surprise. Same discipline as #1939 naming that it moves #1913's head.

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 main". A merge inside a stacked PR can hide a conflict resolution in the very files the parent PR also touches. If the merge resolved anything in pack_import.cpp, say what — otherwise the rebase onto main after #1948 lands is the first time anyone sees the combination.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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 (q_off = envelope, k_off = q_off + (Nq/64)*unit(H), …) at SmolLM2-135M's geometry (H 576, Nq 576, Nkv 192):

envelope 1216 | q_off 231616 | k_off 308416 | v_off 385216   <- the four values the test pins, reproduced by hand
unit(H) = 25,600   9 q-tiles = 230,400 = 231,616 - 1,216     3 kv-tiles = 76,800 = 308,416 - 231,616
block32_mlp_unit_bytes(576) = 48,640  (24 units per layer) = 4,864 + 2*18*1,216

and sink_off - v_off is 4*heads = 36 bytes, i.e. the sink is inside the v region's tail — which is exactly why the comment calls it "not a weight; skipped". A layout whose offsets reproduce from its own formula is the difference between a measured layout and a fitted one.

The test arms that matter are the two-sided ones: ncorr(g, G) > 0.99 and |ncorr(g, U)| < 0.3, plus the mirror for up. A gate/up reader with the halves swapped would pass the first pair and fail the second, and swapping halves is the single most likely mistake in a unit that stores two matrices side by side. The refusals are as complete as #1948's: wrong intermediate named by value (intermediate 1500), a short file named by bytes (12120 B), a NaN scale, K 48, a missing header, a gap; and min_size is pinned to the measured 90,470,776.

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:

  1. It is stacked and npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 is still OPEN. git diff origin/main...a3cca20d contains npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948's read_block32_head, so this PR as a diff is not independently mergeable: it must land after npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 and be retargeted at main. The body says exactly that ("Should be retargeted at main once npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #1740) #1948 lands"), and I am recording it here so the merge order is not decided by whoever merges first.
  2. The one thing neither of us can check statically: that the all-block-32 pack imports and serves — the arms build fixtures from the measured offsets, which proves the reader agrees with the documented layout, not that the layout is the pack's. The head_off == 70,809,976 / min_size == 90,470,776 pins are the closest thing to a check against the real file, and they are good ones: if the measured file size were wrong, the import would refuse rather than silently mis-read.

What I did not do: run the suite. npu_pack_import_test links onebit_npu_forward, which needs the NPU runtime this box does not have; my evidence here is the hand-reproduced offset chain, the arm text, and the refusals' shapes. Stated plainly because for the last several PRs I have been able to run something, and this one I could not.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a3cca20d

I cannot audit the real layout without the pack, so I checked the three things that decide whether this PR's evidence is capable of catching a layout mistake — and all three hold.

1. The synthetic arms run in every build, and they are not behind the pack. test_block32_layers(golden_dir.empty() ? "." : golden_dir) is called unconditionally next to its siblings, and the in-function check() arms build their own buffers (u, one, sh, hr, nan, encode_block32_gate_up(...)) before the if (fs::exists(out + "/model.q4nx")) leg. So a machine with no pack still exercises the decoder; the real-pack correlation is the extra, not the entry fee.

2. The expected constants are the formula, not copied numbers. block32_mlp_unit_bytes(32) == 7296 and (576) == 48640 both reproduce 4864 + 2*(K/32)*1216 exactly (4864 + 2432; 4864 + 43 776), and the one-byte-short arm passes 7295 for a 7296-byte unit — so the unit size in the test and the unit size in the reader cannot drift apart silently.

3. The real-pack leg is an independent check with a negative control. It correlates each imported matrix against the reference checkpoint's matrix (head_of(mats[l*NPROJ+i], …)) at > 0.99, and then, for gate and up, correlates against the other one and requires < 0.3. That second check is the one that matters: a "32 gate rows then 32 up rows" strip is exactly the place where a swap is plausible, and a self-consistent encode/decode round trip could never catch it. It also checks the container lm_head against head_of(emb, H, H) — the tied-head case.

Refusal arms are of the right kind — each carries the number a reader can compare: a buffer one byte short (7295), a non-zero unit header, a non-zero byte between the zero points and the scales, a non-finite scale, and NaN codes. That is the convention this repo has settled on, and it is what makes a failure diagnosable rather than just red.

Stacked base, verified rather than taken on trust: #1948 is still OPEN at 3865be48, so basing on 1bit/npupack-b-1740-block32-head is correct right now and the note to retarget after #1948 lands is the right instruction. Worth repeating in the merge order: this cannot land first.

What I did not verify: the actual strip order and the 2 360 292-byte per-layer breakdown (both need the .fconst), the correlation values themselves, and the docs' numbers. The claims about gate+up sharing units and the tail envelope are the author's measurement; what I can say is that the test would fail if they were wrong, which is the property a review can establish without the artifact.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at a3cca20 — provisional only in the sense that it is stacked on #1948 (base = 1bit/npupack-b-1740-block32-head); as a delta it is sound.

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
by guessing:

h.why = "a <tag> needs K to be a multiple of 32 (K …)";
h.why = "the .fconst has N B at M B, and a <tag> …";
h.why = where + " does not open with N zero bytes, so this is not the layout measured";
h.why = where + " strip N has non-zero bytes between its zero points and its scales, so this is not the layout measured";
h.why = where + " strip N row R …";

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,
not a verdict on the pack. And the format table documents what is deliberately not interpreted: "| — | 64 | skipped, not interpreted (01 then zeros on the
measured pack) |"
, which is the same discipline as the head read's zero-field checks.

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
by …"
— the layout comes from a named artefact, not from the bytes agreeing with themselves.

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
its base branch already prevents.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: APPROVE at 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, with x.refusal empty 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 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 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 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.

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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 8a37445 — re-issued for the new head, which is my a3cca20 verdict head plus a merge of main (Merge main into 1bit/npupack-b-1503-block32-layers; the only non-merge commits since are main's own: #1983, #1987, #1991). The block-32 arm and its -derived layout are untouched, so the verdict carries, as does the landing order (#1948 → this). One thing the merge makes worth restating: main now carries the registration glob (#1983), so this branch's add_test placement no longer competes for a line in CMakeLists.txt with anything — the stacking cost the body warned about is gone.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

VERDICT: APPROVE at 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 main after #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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Correction to my verdict comment above — its text shows a hole:

"The block-32 arm and its -derived layout are untouched …"

The word that was eaten is the filename model.fconst: I posted that body inside a double-quoted shell string, and the backticks around it were executed as a command substitution rather than sent as text — the one trap this lane has recorded and just re-learned the hard way (nothing containing code inside a double-quoted -c string; a heredoc was the right instrument, which is what the two neighbouring comments used).

The verdict itself is unchanged: READY at 8a37445 — my a3cca20 head plus a merge of main, block-32 arm and its model.fconst-derived layout byte-identical, landing order #1948 → this.

@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 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 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 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 (q 0.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_tile read-back rel_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 .fconst present), 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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 8a37445 — re-verdict after the merge, and the merge is the thing to look at, because #1948 landed as a squash and every main-side hunk was resolved to this branch's side.

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
every main-side hunk was #1948's text verbatim, and that each is resolved to this branch's side. If the branch's rewrite is a superset of what the squash landed, the result is correct by construction. The failure mode it leaves open is the one every port has: a detail that #1948's text
carried and this branch's rewrite does not
— a bound, a comment that explained a constant, a test arm — silently disappearing because "the branch's side" looked complete.

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, ceil(V/64) tail, the 64-row unit arithmetic) reassembled from this branch and compared against the numbers in both PRs' tables. If they agree, say so and the merge is settled; the alternative is that the next conflict resolution inherits an unverified base.

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 pack_import.cpp, and now there are two of them.

Not run: I did not build or import the pack (substrate not mine). The review is of what the squash-resolution could have dropped.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 8a37445

Its base #1948 merged at 18:42, and this head still carries its own work — read_block32 appears 8 times in npu/pack_import.cpp — so it is now a normal
PR on top of the merged tail-head reader. Everything from a3cca20 stands: every refusal names the offset, the strip and (where relevant) the row, in the
words "this is not the layout measured"; the format table documents the 64 bytes it deliberately does not interpret; and the code order comes from the public
checkpoint rather than from the bytes agreeing with themselves.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 8a37445 — the patch is unchanged (only a rebase onto newer main: the two git diff origin/main... patches differ solely in index hashes and hunk offsets), and the stacking note is now resolved the other way.

#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 (envelope 1216 → q_off 231616 → k_off 308416 → v_off 385216 at H 576 / Nq 576 / Nkv 192, with blocked(576) = 48,640 per layer), the gate/up correlation arms are two-sided (ncorr(g,G) > 0.99 and |ncorr(g,U)| < 0.3, so a swapped reader fails), and the refusals name values rather than shapes.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at e172b67: READY — head moved (a main merge or a neighbouring commit); the substance below carries

Restating the verdict line at the current head. Substance, unchanged from my review at the previous head:

Eighth-through-eleventh head-move tonight for reasons unrelated to the PRs — the case for #1981/#1983 landing.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at e172b67

Nine commits since my last verdict and they are merges from main (the touched files are main's: app/q4nx_cache.*, docs/hrx.md, app/serve.cpp). The PR's own
work survives: read_block32 appears 8 times in npu/pack_import.cpp at this head. Everything from 8a37445 stands.

@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 e172b67 (carried; 0 own commits since the verdicted commit, so the delta is main merged in and the patch is unchanged).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at e172b672

Third head, same reason as the last two: a merge of main (29 files, other lanes' work — hf_tensor.py, the launch dequantise change), not a change to this PR. Re-checked the merge kept it:

  • read_block32_units / block32_mlp_unit_bytes still present in npu/pack_import.cpp (5 references), test_block32_layers still has its definition and its call site;
  • the arithmetic still reproduces — 4864 + 2*(K/32)*1216 → 7296 at K=32, 48640 at K=576 — so the reader and the test's expected constants are still tied together;
  • no conflict markers reported in the previous pass on the same two files, and this merge touched neither of this PR's paths.

Substance is unchanged from a3cca20d, where I verified that the synthetic arms run without a pack (they precede the fs::exists(out + "/model.q4nx") leg), that the fake-pack leg correlates each imported matrix against the reference checkpoint at > 0.99 with a gate/up swap control at < 0.3, and that the refusals carry the comparable number.

Still true, and it is the one thing that has to hold in the merge order: #1948 is open, so this must not land before it.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at e172b67 — pure main-move; the PR's own commits are unchanged.

origin/main..<head>, both heads: a3cca20d (one arm per declared block size) + 4a3351be (the all-block-32 import)
the diff-vs-main noise in between is main arriving, not this branch

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 main will be this PR's own two commits once it is retargeted, and the merge-order constraint ("after #1948") is satisfied.

My earlier review stands on the two checks I could do without the NPU runtime, which this PR's suite needs:

  • the layout chain reproduces from its own formula — at H 576 / Nq 576 / Nkv 192: envelope 1216 → q_off 231616 → k_off 308416 → v_off 385216, with unit(H) = 25,600, 9 q-tiles = 230,400, 3 kv-tiles = 76,800, and block32_mlp_unit_bytes(576) = 48,640 = 4,864 + 2·18·1,216 (24 units/layer). A layout whose offsets fall out of its own rule is measured, not fitted;
  • the gate/up arms are two-sided — ncorr(g,G) > 0.99 and |ncorr(g,U)| < 0.3 (plus the mirror), so a reader with the halves swapped passes the first and fails the second.

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 head_off == 70,809,976 / min_size == 90,470,776 pins are the closest thing to a check against the real file.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at e172b67 — built, ran the crafted arms (all pass), and reproduced the real-pack figures to the digit:

lm head: block 32, 768 unit(s) of 25600 B at 70809976 B, 0 B of zeros after them (tied: verified by correlation)
imported 273 tensors, 20808 q4_1 tiles
self-check: the head reproduces the pack's own embedding (bf16)  corr 0.999999  rel_err 0.000777
controls:   row-shifted corr 0.2247 (must fail)
container:  rel_err 0.000e+00  max |diff| 0.000e+00
fconst:     stride 2360292, second header 615652 (from the geometry, checked at the eps anchors); 60/60 RMSNorm anchors
block 32:   120 attention + 90 MLP projections and the head read from 2478 units of the raw .fconst

Every number in the body matches what the binary prints on ~/scratch-packs/pk-smol19, which is the cheapest and most decisive check available for a layout PR: it means the description and the code are the same object.

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 (sp[512 + 10] = 0x05 for row 20's zero point, sp[(2*256 + 3*8 + 4)/2] |= 0x09 for column 3) and an assertion that the neighbour is code 0, so a reader that is off by one strip, one row-band or one nibble fails at a named value rather than at a correlation. The negative arms are equally specific: a short unit names q_proj and 7000, a header overrun names 12120 B, and there is a bf16 quiet NaN planted in unit 0 strip 0 — a scale/zero decode that ignores non-finite values fails there.

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:

  1. correlate layer 0's imported q against layer 1's reference q (with a correct layout it should be near zero, like the MLP cross-control) — that distinguishes "this block is misaligned" from "this block is noisy";
  2. or state why layer 0 is expected to be noisier for this model (a 135M model's first block carrying a large-magnitude embedding contribution, or the pack's own quantisation of it) — if that reason was already checked, one sentence in the body closes it.

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 (Qwen-2.5_1.5B … 2374 units, corr 0.996185). I could not reproduce that half — the only copy of that pack on this box is a JSON from an earlier session, not the pack — so that line rests on the PR's own run. Everything above it I ran.

Scope of my check: built at this head, npu_pack_import_test tests/golden all pass, and a full npu-pack import of the 293 MB SmolLM2 pack with the figures above. No device, no serve.

@bong-water-water-bong
bong-water-water-bong merged commit 7c94d5a into main Nov 10, 2026
7 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/npupack-b-1503-block32-layers branch November 10, 2026 20:30
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ready for review at e172b67

The head moved from 8a37445 by a merge of main (003aad1) that I did not make; it merged without conflict and the PR's diff against main is the same size as before (+1,069/−124 in the same four files). Checked on this head with freshly built binaries:

Also relevant here: I built main + this PR + #2009 together (details on #2009); tests pass and both the all-block-32 and the block-32-head packs import, so the two can land in either order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant