Skip to content

npu-pack: the 1.9 header reader fills hdr when the vote found none, and stops overwriting one (fixes #1831) - #1836

Closed
bong-water-water-bong wants to merge 1 commit into
mainfrom
fix-1831-header-clobber
Closed

bong-water-water-bong wants to merge 1 commit into
mainfrom
fix-1831-header-clobber

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

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

if (best >= 3) {
    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. 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 h1 left hdr empty" — that is a fill, and it should
have stayed one. The reader may fill what the vote did not establish; it may not overwrite what it did:

if (best >= 3 && g.hdr.empty()) {   // fill only: the vote's layout, when it won, is the stronger witness

Nothing else moves: when the vote wins, h1 came from it (g.h1 = pos[1] - pos[0]), so a pack whose anchors did win the
vote 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> on npu/pack_import.cpp → clean
    (one pre-existing -Wvexing-parse warning at an unrelated line).
  • No test arm, and I would rather say why than leave it implied: locate_fconst_with_header() is internal to this
    file, and an arm needs a synthetic 1.9 .fconst (a 1.9 header plus per-block epsilon anchors) as a fixture. The
    precedent for exposing it is already in the file — find_token_fconst_exported and load_geometry_exported exist for
    exactly 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-only is 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() == 3 and post_attn_hdr() == hdr.back() after the call —
i.e. the property this PR restores, tested where it can regress again.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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 best >= 3 condition
(if (best >= 3 && g.hdr.empty())), which is a real refinement — a residue seen fewer than three times should not settle a layout —
but it still hard-codes g.period = 2 for the case it does accept, so a 4-header block is still described as a 2-header one.

#1838 keeps the refinement and derives the block shape from the anchors, accepting 1 or 3 in-block offsets and refusing the rest,
with arms on both and an arm asserting the refusal leaves the structure untouched. That is the version a layout decision needs: the
question here is "how many headers does a block have", and a guard cannot answer it — only a count can.

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
happens when the header and the vote disagree and did not ask what the residue step does when they agree. Naming that in the issue
thread is how the next reader learns where the seam was.

Head under review: 0d54ef7

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Inventory, from the review lane: this fix is already inside #1838 — merge there.

#1838 (cl-review/1831-keep-header-list) has this branch's head as an ancestor, together with #1833's, and adds what neither has: fconst_block_headers(pos, stride8, 3) (bounded scan; the 3 is measured — a real section boundary repeats ~8× in the first GiB versus 1–2 for a payload coincidence, and layers/2 dropped the real one) plus the inner.size() != 1 && inner.size() != 3 → set nothing sanity gate and a crafted arm.

Your diagnosis — post_attn_hdr() returning k_norm and the MLP read from the wrong offset silently — is the clearest statement of the bug in any of the three threads, and it is preserved in #1838's history. Recommend: merge #1838, close this as contained.

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

Copy link
Copy Markdown
Collaborator Author

VERDICT: SUPERSEDED at 0d54ef7 by #1838 — close it, do not merge. (Reasoning above: same guard as #1833 plus best >= 3, but still hard-codes period = 2, so a 4-header block is described as a 2-header one; #1838 counts the in-block offsets instead.)

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT at 0d54ef7: READY — one assignment overwrote a vote, and the comment now says which way the precedence runs

The bug is a precedence error written as an unconditional assignment, and the comment states the consequence precisely: locate_fconst() sets h1 and a period − 1-entry hdr when its vote wins, and plan() reads the block layout from hdr.size() — 1 entry means the 2-header block (q/k/v/o in one payload, Qwen2/Llama), 3 means the 4-header block (q/k/v, then k_norm, then q_norm, then o_proj, Qwen3). Assigning unconditionally truncated a Qwen3 pack's list to one entry, so post_attn_hdr() — which returns hdr.back() and is the only thing that marks the end of attention — returned the k_norm header as the start of the MLP section. Weights read from the wrong place, silently, on a container that parses cleanly.

Why this is worse than a refusal: nothing in the pipeline disagrees with itself — the container is well-formed, the sections resolve, the model runs. It is the same class as the int4 wrap in #1921 and the merge-sign in #1874: a wrong value that lands at a consistent offset, so the output looks structured and no check fires.

The fix is stated as a rule rather than a patch: "this must FILL what the epsilon vote did not establish, never OVERWRITE what it did." That sentence is what a future reader needs, because the original comment's own justification — "setting it here is harmless" — was the bug: "harmless" was true when hdr was unused for layout and became false when plan() started reading hdr.size().

Two asks, neither blocking: (1) since hdr.size() now is the layout, the sizes should have named meanings (kTwoHeaderBlock = 1, kFourHeaderBlock = 3) rather than bare 1/3 in a comment — the failure mode here is a reader changing a constant; (2) #1860 adds the arm that would have caught this, so worth linking the two so the pair reads as bug + pin rather than two unrelated fconst19 changes.

And the honesty in the body is worth recording: "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'" — asking the right question and missing that the code answered it wrongly is the most useful kind of post-mortem a reviewer can write.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Closing as superseded: main already carries this fix, in a strictly better form, from #1838 (e9a7bf1e).

I found this while resolving the conflict on this branch against main, and the resolution is conclusive rather than a judgement call: taking main's version of npu/pack_import.cpp leaves no diff against main at all for this PR's content (only an unrelated submodule pointer in my worktree). So there is nothing left here to merge.

What main does that this branch does not, better on both counts:

  1. It keeps every in-block header, not the most common one. This PR's guard is if (best >= 3 && g.hdr.empty()), which stops the truncation of a Qwen3 4-header block — but it still assigns a single header (hdr.assign(1, best_off), period = 2). main's fconst_block_headers() returns all recurring offsets and fconst_set_block_headers() accepts 1 or 3 (period = size + 1), so a 4-header block is now represented, not merely protected from being overwritten. Its own comment states the case my fix could not: "a Qwen3 block has three equally frequent inner headers, and keeping only the first maximum picked k_norm's." Same defect I was fixing, solved one level deeper.
  2. It refuses rather than guesses when the anchors support neither 1 nor 3 headers, naming both witnesses — and the never-overwrite property I held with g.hdr.empty() is held structurally there instead, by an early return when the witness is already satisfied. Different mechanism, same guarantee, fewer moving parts.

So, honestly: this PR was right about the defect (#1831 — the header count standing in for a layout, so post_attn_hdr() returned the k_norm header as the MLP start, weights read from the wrong place on a container that parses cleanly) and wrong about the scope of the fix. The guard I shipped narrowed a bug rather than representing the layout, and that is the distinction I should have made when I wrote it. #1838 did the fuller job.

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 main have independently fixed one defect. The fleet norm is duplication; the durable value is the comparison, and this comparison says main wins.

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