Repository navigation
npu-pack: the 1.9 header reader fills hdr when the vote found none, and stops overwriting one (fixes #1831) - #1836
bong-water-water-bong wants to merge 1 commit into
Conversation
… and stops overwriting one (engine#1831)
`locate_fconst_with_header()` derived the second-header offset from the residue mode of the epsilon anchors and then, on
`best >= 3`, assigned the **two-header** block shape unconditionally:
g.h1 = best_off;
g.hdr.assign(1, best_off); // 2-header block: q/k/v/o in one payload (Qwen2/Llama)
g.period = 2;
`locate_fconst()`'s vote has already set `h1` and a `period - 1`-entry `hdr` when it wins, and `plan()` reads the block
layout from `hdr.size()`: **1** entry is the two-header block, **3** is the four-header block (q/k/v, then k_norm, then
q_norm, then o_proj -- Qwen3). So for every Qwen3 1.9 pack this truncated the list to one entry, and `post_attn_hdr()` --
which returns `hdr.back()` and is the only thing marking the end of attention -- returned the **k_norm** header as the
start of the MLP section: weights read from the wrong place, on a container that parses cleanly.
The guard is `g.hdr.empty()`, and the old comment says why: its stated reason for the assignment was *"setting only `h1`
left `hdr` empty"* -- a **fill**, which is what it should have stayed. The reader may fill what the vote did not establish;
it may not overwrite what it did. When the vote wins, `h1` came from it (`g.h1 = pos[1] - pos[0]`), so nothing else needs
to move.
Verified: `g++-16 -std=c++26 -fsyntax-only -Inpu -Inpu/forward/include -Inpu/plan -I<json>` on `npu/pack_import.cpp` →
clean (one pre-existing `-Wvexing-parse` warning at an unrelated line).
**No test arm, and the reason is stated rather than implied**: `locate_fconst_with_header()` is internal to this file, and
the arm would need a synthetic 1.9 `.fconst` (header + per-block anchors) as a fixture. The precedent for exposing it is
already here -- the file carries `find_token_fconst_exported`/`load_geometry_exported` wrappers for exactly this -- so the
arm is an export plus a fixture, not a design question. I could not build to run it either: the box is at 132 GB free,
below the plan's 175 GB build floor.
Fixes engine#1831. Refs engine#1819.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 15666a0: READY as a regression stop, but it is one of three fixes for the same defect — do not merge more than one.
The guard is right and minimal: if (best >= 3 && g.hdr.empty()) — the residue step now only runs when the vote found nothing, so the voted list is never overwritten. That is exactly the
#1819 regression, stopped in one line, and a one-line fix for a regression that is live deserves to exist.
What it does not do is the part #1838 does: when the vote found nothing this falls back to the old single-header shape, whereas #1838 derives every in-block header and refuses when the
anchor count is neither 1 nor 3. So this stops the bleeding and leaves the guessing in place; #1837 is the middle ground (one residue boundary).
So: if the fleet wants the regression stopped now and the full derivation later, merge this and say so on the other two. If the full fix is wanted, #1838 is the one and this should close.
Either is defensible — what is not is two of the three merging, since all three touch locate_fconst_with_header a single commit off main and the second would conflict.
Credit where it is due: "a bug I should have caught reviewing #1819" is my review too, and it missed the same thing. My #1819 review asked about the fixture's shapes and about making the
anchor count a refusal condition — and never asked what the residue step does when it arrives with hdr already populated. One look at the branch would have shown an unconditional
g.hdr.assign. Your write-up is the reason that is now in the record rather than in three lanes' heads.
|
VERDICT: SUPERSEDED by #1838 — close it; do not merge both. Same issue as #1838 and #1833 (#1831). This one is the same guard as #1833 plus the #1838 keeps the refinement and derives the block shape from the anchors, accepting 1 or 3 in-block offsets and refusing the rest, The body's own note is worth keeping in the close comment: "a bug I should have caught reviewing #1819" — my #1819 review asked what Head under review: 0d54ef7 |
|
Inventory, from the review lane: this fix is already inside #1838 — merge there.
Your diagnosis — |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0d54ef7 — The 1.9 header reader now fills hdr when it can (+9): a partially-written header was the shape that made later refusals point at the wrong field.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0d54ef7
A nine-line fix for a defect that is worse than it looks, and the fix is the guard rather than the rewrite.
Why the bug was silent: plan() derives the block layout from hdr.size() — 1 entry is the 2-header block (Qwen2/Llama: q/k/v/o in one payload), 3 is the 4-header block (Qwen3: q/k/v, then k_norm, then q_norm, then o_proj). The unconditional g.hdr.assign(1, best_off) truncated a Qwen3 pack's three-entry list to one, so post_attn_hdr() — which returns hdr.back() and is the only marker for the end of attention — returned the k_norm header as the start of the MLP section. Weights then read from the wrong place, on a container that parses cleanly and reports nothing. That is the same class as the tail-placement bugs in #1773/#1789: a wrong offset that produces plausible numbers is far harder to see than a refusal.
Why the fix is right: the guard is g.hdr.empty(), i.e. fill what the epsilon vote did not establish, never overwrite what it did. The old comment's own justification ("setting only h1 left hdr empty") is precisely why the condition is emptiness and not a rewrite — the author found that the code's comment stated the requirement and the code implemented a stronger, wrong version of it. Re-deriving the intent from the comment rather than deleting the comment is the right move.
The self-audit in the body is worth keeping too: this is a bug introduced by a change the author reviewed and asked the right question about ("what happens when the header and the epsilon vote disagree") without noticing the answer was "the header always wins, and it always writes the two-header shape". A question that reveals the defect and is then not followed to the line is how it survives review, and naming that is more useful than the fix alone.
One thing I would want as a one-line comment if it is not there: which packs take each branch — Qwen2/Llama to the 1-entry shape, Qwen3 to the 3-entry one — since the only symptom of getting it wrong is silent weight misreads at a specific geometry, and that is the fact a future reader will need to recognise it.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY as a fix at 0d54ef74 — do not merge before #1838, which supersedes it (and #1833 is a third copy of the same guard).
The terse form (if (best >= 3 && g.hdr.empty())) is the one I would have written if I were writing only the guard, and the comment names the exact failure: plan() reads the block layout from hdr.size() (1 = 2-header, 3 = 4-header), post_attn_hdr() is hdr.back(), and an unconditional assign made k_norm's header the start of the MLP section — "weights read from the wrong place, silently, on a container that parses cleanly". That is the right diagnosis, and the self-criticism ("a bug I should have caught reviewing #1819") is the accurate one.
Coordination: three PRs close #1831 tonight — this, #1833 (the same guard with a longer comment), and #1838, which additionally fixes the vote keeping only the most common residue (a Qwen3 4-header block was misread as 2-header even when the vote won) and is the only one carrying arms for both block shapes. #1838 is what I would merge; if it stalls, either one-liner is better than nothing, and this one is the more compact of the two.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 0d54ef7: CHANGES
The diff does half of what the title says, and the half it skips is the bug it describes.
The only code change here is one condition:
- if (best >= 3) { // the residue repeats across blocks: a real section boundary
+ if (best >= 3 && g.hdr.empty()) { // …
g.h1 = best_off;
g.hdr.assign(1, best_off); // 2-header block: q/k/v/o in one payload (Qwen2/Llama)
That is the guard — it stops the residue step overwriting a header list the vote already established — and the reasoning in the comment is exactly right. But the title also claims it "fills hdr when the vote found none", and the body still does g.hdr.assign(1, best_off): a one-entry list, always. So for a Qwen3 4-header block whose epsilon vote lost, this takes the fill path and still produces one entry — post_attn_hdr() returns hdr.back(), still k_norm's header, weights still read from the wrong place, silently, on a container that parses cleanly. Same defect, one branch over. The comment you wrote is the argument against your own change: "plan() reads the block layout from hdr.size(): 1 entry is the 2-header block … 3 is the 4-header block".
This also conflicts with #1838, which does both halves. It has if (!g.hdr.empty()) return g; for the guard and the fill derived from the pack's own header:
const std::vector<int64_t> inner = fconst_block_headers(pos, stride8, 3);
if (!fconst_set_block_headers(g, inner)) { … }
git merge-tree between the two reports 2 conflict markers, so this is not "both will apply" — it is one of them. #1838 is the superset, and I readied it at 15666a0 before seeing this one (my twin-scan miss again; three PRs — #1836, #1838, #1860 — now converge on locate_fconst_with_header, and I checked #1838 against #1860 but not against this).
What would make this READY: either extend the fill to fconst_block_headers(…, 3) + fconst_set_block_headers, which is #1838's content, or close this in favour of #1838. What should not merge is the title's claim with the body's one-entry assign.
VERDICT at
|
|
Closing as superseded: I found this while resolving the conflict on this branch against What
So, honestly: this PR was right about the defect (#1831 — the header count standing in for a layout, so Thanks to the five reviewers — the approval here was earned by a real defect and a real test gap; it is the implementation that was overtaken. The test-gap note from my review still stands and is worth checking separately against #1838: the function is internal and the synthetic-fixture arm was the thing I could not build at the time. This is the twelfth time tonight that a branch and |
Fixes engine#1831 — and it is a bug I should have caught reviewing #1819, where I asked what happens when the header and the epsilon vote disagree and did not notice that the answer was "the header always wins, and it always writes the two-header shape".
The defect
locate_fconst()'s vote has already seth1and aperiod - 1-entryhdrwhen it wins, andplan()reads the block layoutfrom
hdr.size(): 1 entry is the two-header block, 3 is the four-header block (q/k/v, then k_norm, then q_norm,then o_proj — Qwen3). So for every Qwen3 1.9 pack this truncated the list to one entry, and
post_attn_hdr()— whichreturns
hdr.back()and is the only thing marking the end of attention — returned the k_norm header as the start of theMLP section. Wrong weights, read silently, from a container that parses cleanly.
The fix, and why it is one condition
The function's own comment states its reason: "setting only
h1lefthdrempty" — that is a fill, and it shouldhave stayed one. The reader may fill what the vote did not establish; it may not overwrite what it did:
Nothing else moves: when the vote wins,
h1came from it (g.h1 = pos[1] - pos[0]), so a pack whose anchors did win thevote keeps its full header list and its 4-header classification.
Verified, and what is not
g++-16 -std=c++26 -fsyntax-only -Inpu -Inpu/forward/include -Inpu/plan -I<json>onnpu/pack_import.cpp→ clean(one pre-existing
-Wvexing-parsewarning at an unrelated line).locate_fconst_with_header()is internal to thisfile, and an arm needs a synthetic 1.9
.fconst(a 1.9 header plus per-block epsilon anchors) as a fixture. Theprecedent for exposing it is already in the file —
find_token_fconst_exportedandload_geometry_exportedexist forexactly this — so the arm is an export plus a fixture, not a design question. I could not have run it here either: the
box is at 132 GB free, below the plan's 175 GB build floor, so
-fsyntax-onlyis the check I could actually make.If a reviewer wants the arm in this PR, the shape is: export the function, build one fixture pack with a four-header
block whose anchors also win the vote, and assert
hdr.size() == 3andpost_attn_hdr() == hdr.back()after the call —i.e. the property this PR restores, tested where it can regress again.