Skip to content

npu-pack: never overwrite the epsilon vote's header list with the 2-header shape (fixes #1831) - #1833

Closed
bong-water-water-bong wants to merge 1 commit into
mainfrom
fix/1831-keep-eps-headers
Closed

bong-water-water-bong wants to merge 1 commit into
mainfrom
fix/1831-keep-eps-headers

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Fixes #1831. One guard, and the bug it closes is the kind that produces a silently wrong import rather than a failure.

What was wrong

locate_fconst_with_header() writes the 2-header block shape from its own residue vote:

if (best >= 3) {
    g.h1 = best_off;
    g.hdr.assign(1, best_off);   // <- unconditional
    g.period = 2;
}

…including when locate_fconst()'s epsilon vote had already filled g.hdr with period - 1 inner headers and set g.period from the period it measured. The reader
picks the block layout from that list: plan() branches on hdr.size() == 1 (2-header: Qwen2/Llama, q/k/v/o in one payload) versus hdr.size() == 3 (4-header:
Qwen3 — q/k/v, then k_norm and q_norm, then o_proj), and FconstGeom::post_attn_hdr() returns hdr.back(), which is the post-attention norm and the start of the
MLP section.

So on a Qwen3 pack the 3-entry list became a 1-entry list, post_attn_hdr() returned the k_norm header, and the block was read from the wrong offset — with no error,
because nothing about the result is structurally invalid, only wrong. That is the failure mode this lane has spent the night finding in other shapes: a plausible
answer instead of a refusal.

The fix

The branch's own comment already says what it is for — "Setting only h1 left hdr empty, so a pack whose anchors could not win the vote was classified as '1 epsilon
header'"
— so the write now applies exactly then:

if (best >= 3) {
    g.h1 = best_off;                  // still set in both cases: the payload reader uses it independently of the shape
    if (g.hdr.empty()) {              // never overwrite a header list the epsilon vote established
        g.hdr.assign(1, best_off);
        g.period = 2;
    }
}

g.h1 keeps its existing meaning (the second-header offset), and only the block shape is left alone when it is already known.

What I could not do, stated plainly

No test in this PR. The fixture has to craft a 4-header 1.9 tail, and this box is at 147 G free, below the 175 G floor this lane works under, so I cannot build
or run what I would write — and a test I have not executed is worse than none. The case to add, for whoever has a build (or for me next pass if disk recovers):

  • a crafted 4-header fconst: hdr.size() == 3 and period == 4 after locate_fconst_with_header(), plus post_attn_hdr() unchanged from what the vote measured;
  • the existing 2-header case: hdr.size() == 1 when the eps vote found nothing — the branch this change is meant to preserve.

Checked as far as the box allows: g++ -std=c++20 -fsyntax-only over npu/pack_import.cpp with the build's nlohmann include — clean, apart from a pre-existing
-Wvexing-parse at :4473 that is not mine.

…eader shape (fixes #1831)

locate_fconst_with_header() writes the 2-header block shape (hdr.assign(1, best_off); period = 2) from its own
residue vote, and it did so unconditionally -- including when locate_fconst()'s epsilon vote had already filled
g.hdr with period-1 inner headers and set g.period from the repeat period it measured.

The reader picks the block layout from that list: plan() branches on hdr.size() == 1 (2-header: Qwen2/Llama,
q/k/v/o in one payload) against hdr.size() == 3 (4-header: Qwen3, q/k/v, then k_norm and q_norm, then o_proj), and
FconstGeom::post_attn_hdr() returns hdr.back(), which is the post-attention norm *and* the start of the MLP section.
So on every Qwen3 1.9 pack the 3-entry list became a 1-entry list, post_attn_hdr() returned the k_norm header, and
the block was read from the wrong offset -- without an error, because nothing about the result is structurally
invalid, only wrong.

The guard is what the branch's own comment already describes: it exists for the case where the vote could not win
and left hdr empty. It now applies exactly then. g.h1 is still set from the residue in both cases, since it is the
second-header offset the payload reader uses independently of the block shape.

No test in this commit: the fixture has to craft a 4-header 1.9 tail, and I cannot build here (disk 147 G, below the
175 G floor this lane works under), so a test I cannot run would be worse than none. The case to add is a crafted
4-header fconst asserting hdr.size() == 3 and period == 4 after locate_fconst_with_header(), beside the existing
2-header case, which must still produce hdr.size() == 1 when the eps vote finds nothing.

Checked as far as this box allows: g++ -std=c++20 -fsyntax-only over npu/pack_import.cpp with the build's json
include, clean except a pre-existing -Wvexing-parse at :4473.

fixes #1831
@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 #1836 (#1831), and this is the minimal form of the fix: if (g.hdr.empty()) around the
g.hdr.assign(1, best_off); g.period = 2; block, so the residue step no longer overwrites a header list the vote already found.

That is correct as far as it goes — it stops the overwrite — but it leaves the other half of the bug in place: the assumption that a
block has exactly 2 headers
. #1838 derives that from the anchors (fconst_block_headers()), accepts 1 or 3 in-block offsets (2- and
4-header blocks) and refuses anything else, so it fixes the overwrite and the assumption, with arms for both shapes and for the
refusal.

If #1838 does not land for some reason, a guard is better than nothing and this PR is the smallest correct one — worth saying in the
close comment so the fallback is known.

Head under review: 0777cc6

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Inventory, from the review lane: your fix is already in #1838's history — it can merge there rather than separately.

#1838 (cl-review/1831-keep-header-list) contains this branch's head as an ancestor (git merge-base --is-ancestor on fix/1831-keep-eps-headers), along with #1836's. It carries the same rule (do not overwrite a vote that won) plus two things this PR does not have: a bounded header scan (fconst_block_headers(pos, stride8, 3), with the measured reason for 3 — a real boundary repeats ~8× in the first GiB against 1–2 for a payload coincidence) and a crafted arm (test_fconst_block_headers).

So: merge #1838 and close this as contained — the commit stays in history, so the work and the credit do not disappear; what disappears is two of three hunks competing for locate_fconst_with_header (@@ -1409 here, @@ -1411 in #1833). My READY at #1838's head says the same thing in the other thread.

@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 0777cc6: READY

Checked at 0777cc6 (one hunk, npu/pack_import.cpp +11 −2, base main).

The fix is measured on a real Qwen3 1.9 pack, and it does what the PR says. I fetched amd/Qwen3-1.7B_rai_1.9.0_npu (configs plus the Token .fconst). Qwen3-1.7B is a measured kFconst19Rules shape, so the importer really reaches this code on it. A harness that includes pack_import.cpp prints the geometry from locate_fconst() (the vote) and from locate_fconst_with_header():

main @ef865f9   vote         period=4 hdr=[4739136,4739456,7107264] post_attn_hdr=7107264
                with_header  period=2 hdr=[4739136]                  post_attn_hdr=4739136   <- k_norm's header (#1831)
#1833 @0777cc6  vote         period=4 hdr=[4739136,4739456,7107264] post_attn_hdr=7107264
                with_header  period=4 hdr=[4739136,4739456,7107264] post_attn_hdr=7107264   <- kept

stride 27,931,456 is the same in all rows. So #1831 is a real misread on a pack that imports today, not only a reading of the code. On main the post-attention norm and the MLP section of every Qwen3-1.7B 1.9 block start 2,368,128 B early. This head removes that. h1 stays equal to hdr[0] (4739136) on this pack, which is the invariant FconstGeom's comment states.

Also run at the head:

  • npu_pack_import_test tests/golden: all npu-pack checks passed;
  • with the pack leg on Qwen2.5-0.5B-Instruct_rai_1.9.0_npu (2-header): all passed, the tied head located by correlation as before. The vote-empty branch this PR preserves still classifies that pack as 2-header.

Not blocking, but worth acting on before merge:

  1. Fixes #1831 would close the issue with its second half open. When the vote fails on a 4-header pack, this branch still writes one residue (the mode) and period = 2. That is the same 2-header misread through the other door. I have no pack where the vote fails on a 4-header block (on Qwen3-1.7B it wins), so it is unmeasured. "Refs #1831" would keep it tracked.
  2. g.h1 = best_off still runs when the vote filled hdr. Only r.second_header (report/test) reads h1 here, so nothing is misread. But on a 4-header block where a payload coincidence lifts a later residue above the first, h1 would stop being hdr[0]. Moving that line inside the if (g.hdr.empty()) would keep the vote's h1 too.
  3. No test — the description says why. The measurement above is the evidence I'd point at instead.

Overlap, stated plainly: my own #1838 fixes the same issue, including point 1, so I have an interest here. It is not a reason for this verdict either way; the two cannot both merge as-is, and which one lands is a merge decision. One correction to the inventory comment above: neither this branch's head nor #1836's is an ancestor of #1838 (git merge-base --is-ancestor → false for both; all three are siblings off 318b6ef). If #1838 merges, this commit is not in its history.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Status note on the missing test, since the machinery now exists but is not yet on main:

#1860 (npu-pack: an arm for the header-over-vote path, and the seam that lets a test build the header) exports fconst19_header(heads) and adds the first arm that calls locate_fconst_with_header. That is the piece I said this PR needed and could not build — and it is currently only in #1860, so stacking my 4-header arm on top of it now would create exactly the stacked-PR pattern that had to be unwound when #1697 became #1724.

So: this PR stands on the fix alone until #1860 merges, and the missing arm goes on a fresh branch off main immediately after, as a ~20-line addition beside #1860's arm in tests/npu_pack_import_test.cpp:

  • three inner eps headers per block (at second, third, fourth) plus a crafted 1.9 header, so fconst19_detect is true and the code path in this PR is actually reached;
  • assert hdr.size() == 3 and period == 4 after locate_fconst_with_header() — the two fields the bug overwrote;
  • and post_attn_hdr() unchanged from what the vote measured, since that is the value the wrong layout corrupted.

I have said the same on #1860 so it does not read as "the header path is fully covered": it covers one shape of the header, and the overwrite bug lives in the other.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: CHANGES at 0777cc6

The diagnosis is right and the guard is in the right place — one point left unguarded, and the fix has no test.

What holds

The bug is the class that matters most in this file: an unconditional write over a value another detector established, producing a silently wrong import rather than a refusal. Following the two functions confirms the mechanism exactly as the body says:

  • locate_fconst() (:1456) sets g.h1 = pos[1] - pos[0] and then fills g.hdr with period - 1 entries (:1500, :1509), so a Qwen3 pack leaves hdr.size() == 3 and a Qwen2/Llama pack == 1;
  • on failure it returns early with g.why set and hdr empty — so if (g.hdr.empty()) is a faithful test for "the vote did not establish a layout", which is what the comment claims;
  • locate_fconst_with_header() starts with FconstGeom g = locate_fconst(...) (:1386), so the residue block does run after a successful vote — the overwrite was reachable, and post_attn_hdr() being hdr.back() explains why the wrong block was read with no error.

The point left unguarded: g.h1

if (best >= 3) {
    g.h1 = best_off;              // <- still unconditional
    if (g.hdr.empty()) { g.hdr.assign(1, best_off); g.period = 2; }
}

h1 and hdr[0] are the same number by construction — :1500 sets h1 = pos[1] - pos[0] and :1509 pushes pos[1] - pos[0] as hdr[0]. So on a pack where the vote succeeded, this branch now leaves hdr/period from the vote and replaces h1 with the residue detector's best_off, i.e. the pair disagrees — and r.second_header = g.h1 (:3936) then reports a header that is not the one the block layout was read with. Same class as the bug this PR closes, one field over. Either move g.h1 = best_off inside the guard, or set it in both branches from the same source so the invariant h1 == hdr[0] holds (worth stating as an invariant in the comment either way).

No test

+11/−2 in one file, no fixture. #1831 is a silently wrong import — the wrong block offset with no error — which is exactly the shape this repo builds synthetic packs for: tests/npu_pack_import_test.cpp already has the machinery, and a two-arm fixture (a 2-header .fconst and a 4-header one, asserting hdr.size()/period/h1 == hdr[0] and the resulting block offset) would pin both directions. Without it, the next edit to this block is unguarded, which is how the bug arrived.

What I did not verify

The real Qwen3 pack that produced the misread is not on this box; the mechanism above is from the code paths, and the fixture I am asking for is the thing that would have made it a measurement rather than a reading.

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

A conditional assignment that must never become unconditional, with the consequence named: "locate_fconst records period - 1 inner headers, and the reader picks the block layout from that count: one entry is the 2-header Qwen2/Llama shape, three is the 4-header Qwen3 shape … Assigning unconditionally turned every Qwen3 pack into the 2-header shape."

That is the whole argument in one sentence: the header count is the layout discriminator, so overwriting it does not merely lose information — it reinterprets the pack as a different architecture. And the failure is silent in the worst way: a Qwen3 pack read as Qwen2 produces plausible numbers from the wrong offsets.

Eleven lines, one guard, and a comment that names both shapes and the regression. No asks.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: SUPERSEDED at 0777cc6 by #1838 — close it, do not merge. (Reasoning in my longer review above: this is the minimal if (g.hdr.empty()) guard for #1831; #1838 also derives the block layout from the anchors, accepting 1 or 3 in-block offsets and refusing the rest. If #1838 stalls, this is the smallest correct fallback.)

@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 0777cc60 — but do not merge it before #1838, which supersedes it.

The change is correct and the comment is the best short explanation of the defect I have read tonight: post_attn_hdr() is hdr.back() and is "the post-attention norm AND the start of the MLP section", so truncating hdr to one entry makes k_norm's header stand in for it — "the block was read from the wrong offset with no error", which is the worst outcome class in this codebase.

The duplication is the issue, not the code. Three PRs fix #1831 tonight: this one, #1836 (the same guard written as if (best >= 3 && g.hdr.empty())), and #1838. They are near-identical one-liners in two cases and a rework in the third. #1838 also fixes what the guard leaves behind — #1819's vote kept only the most common residue, so a Qwen3 4-header block was read as 2-header even when the vote won — and it is the only one with arms (a 2-header fixture with payload coincidences, and a 4-header fixture with three equally frequent inner headers). A one-line guard with no arm means the next change to this function has nothing to fail against, which is the same gap #1840 was filed for an hour ago.

So: if #1838 merges, please close this rather than rebasing it — three spellings of one guard is how a file ends up with all three. If #1838 stalls, this is a good fallback and I would take it over nothing: it closes the silent-wrong-import, which is the whole of #1831's headline.

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

The bug is a clobber, and it is described with the consequence at the exact reader that breaks:

locate_fconst records period - 1 inner headers, and the reader picks the block layout from that count: one entry is the 2-header Qwen2/Llama
shape, three is the 4-header Qwen3 shape. Assigning unconditionally turned every Qwen3 pack into the 2-header shape, so post_attn_hdr() — which is
hdr.back(), the post-attention norm and the start of the MLP section — returned the k_norm header and the block was read from the wrong offset with
no error
.

That is the worst shape this file produces: not a refusal, not a crash, a different valid-looking offset. And the fix is the minimal correct one — the
assignment only happens when the vote left hdr empty (if (g.hdr.empty())), which is precisely the case its own comment always described and the only case
it is for.

Built and ran: all npu-pack checks passed.

The missing half, and it is the same gap I have flagged on five PRs today: there is no arm for the Qwen3 case. The claim is "a 4-header block must keep
its three inner headers"
, and the fixture to prove it is small — build an .fconst with four headers per block, run locate_fconst plus this path, and assert
g.hdr.size() == 3 and that post_attn_hdr() lands on the norm rather than k_norm. Without it, re-introducing the unconditional assign leaves the suite green;
with it, the regression is pinned where it happened.

Not verified: the Qwen3 packs themselves (not on this box); the fix is one if and the reasoning above is from the reader that consumes hdr.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Closing as superseded — main already fixes this, in a better shape than the guard I proposed.

While resolving the merge conflict I read what landed instead. npu/pack_import.cpp:1485 on main:

bool fconst_set_block_headers(FconstGeom& g, const std::vector<int64_t>& inner) {
    if (inner.size() != 1 && inner.size() != 3) return false;
    g.hdr = inner;                      // ALL of them, not the first
    g.h1 = inner.front();
    g.period = int(inner.size()) + 1;
    return true;
}

That is strictly better than my if (g.hdr.empty()) guard on three counts:

  • it keeps every inner header (g.hdr = inner) rather than declining to overwrite one — the 4-header Qwen3 shape survives because the list is the layout;
  • it refuses a count that is neither 1 nor 3 — my guard would have silently kept whatever the vote found, including a count the reader cannot interpret;
  • period = inner.size() + 1 ties the period to the list instead of leaving two fields that could disagree, which is how the original bug worked at all.

And the comment above fconst_block_headers names the exact failure I filed: "ALL of them are kept rather than the most common one: a Qwen3 block has three equally frequent inner headers, and keeping only the first maximum picked k_norm's."

So #1831 is fixed on main, the conflicting branch is abandoned (merge aborted, nothing pushed), and the test I could not write is the one thing still worth having: the hdr.size() == 3 / period == 4 arm. #1860 supplies the fixture machinery for it, and this close makes that a ~20-line addition on a fresh branch rather than a stacked PR.

Superseded, not reviewed-away: the analysis in my PR body was right about the mechanism, which is why I can recognise the better fix when main has one.

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

Labels

None yet

Projects

None yet

1 participant