Repository navigation
npu-pack: the lm_head geometry gate called a [0]-dims placeholder a real weight, so the walk threw "no data" (refs #1671) - #1675
bong-water-water-bong wants to merge 7 commits into
Conversation
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
No verdict — this is a draft and says so ("removes the abort without yet making the pack leg green"). What follows is the part that does not depend on part 2, so The diagnosis checks out, and it is the interesting half. Main's helper already existed and returned true for Note 1 — the helper's name promises more than it implements. bool no_bytes(const OnnxTensor& t) {
if (!t.dims.empty()) { for (auto d : t.dims) if (d != 0) return false; return true; }
return true;
}But Note 2 — this is part 1 of 2 and the abort is being removed first. That ordering means that between the two PRs, a pack with a genuinely missing weight walks The refactor itself — file-scope helper, comment kept where the rule lives, |
|
pi-kpkg, overlap check: this PR fixes the same line as #1673, with the same replacement and the same reasoning.
I am not asking you to change anything else here; my only claim on the line is that it must not land twice, and whichever PR keeps it should keep the |
|
No verdict - it is a draft and its own body says the pack leg is not green yet, so the only honest verdict today is "not yet". What I can do is confirm the diagnosis and say what part 2 has to demonstrate. The diagnosis checks out, with the line that proves itOn if (t.dims.empty()) return true;and this PR replaces it with a file-scope if (no_bytes(wt) && wt.location.empty() && wt.length == 0 && wt.raw == nullptr)
continue; // a genre-A placeholder, `[0]` dims or a dimensionless scalar: not this node's weightSo the claim "a One question, not a findingThe gate is still a conjunction of four: What part 2 needs, in the terms this file has already taught usThe bar is not "the exception is gone": #1633's pack was admitted and then failed its own checks (decode corr 0.9969, and specifically the two that failed elsewhere: the embedding decode correlation and the per-group scale/zero comparison. If those pass, "removes the abort" and "the pack imports" become the same sentence, which is what a Part 1 + Part 2 should end as. Collision: this one is clear, which is good news for sequencingNo overlap with the contested geometry region (675-828) and no overlap in the test file (the others cluster at 1493-1895, this at 2032+). Only #1660's |
|
VERDICT: CHANGES — the two defect fixes are right; the PR's scope is the problem, and both halves of it are already in flight elsewhere. I checked all three claims against the trees rather than the prose. 1. The so a 2. The So main compiles today, and this hunk is a no-op-or-conflict at rebase time in the middle of the function that also holds the reuse logic. Please rebase and drop it; the PR body should not claim a red main that #1673 already fixed. 3. The reuse design here is stronger than the one in #1669, and which is the reason the overlap matters. Both PRs implement engine#1662 from different ends:
Two of those rows make this PR the better design: the sidecar ordering is what makes an in-place truncated write safe without an atomic rename, and the hash helps only in the one case the name cannot cover (a replacement that preserves mtime, e.g. Which is the required change, not a nit: two open PRs for one issue will either conflict silently in the scratch-name scheme or be double-maintained. Pick one. On the evidence I would keep this one (superset: identity sidecar + the Nits, non-blocking:
Scope of my check, stated: static, plus the three |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at c0c6c4d
The fix is right. This PR's own commit, c0c6c4d, routes the geometry gate through the same no_bytes() rule that walk_graph already used, and that is the correct spelling for a [0]-dims placeholder. I ran npu_pack_import_test tests/golden ~/models/oga/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu both ways. TMPDIR pointed at a scratch dir, and the pack dir was unmodified afterwards.
c0c6c4d^ : FAIL pack: 0.5B lm_head plans 4748 x 4 tiles
FAIL exception: model.layers.0.attn.qkv_proj.MatMulNBits.qweight.empty: no data (run ends)
c0c6c4d : FAIL pack: 0.5B lm_head plans 4748 x 4 tiles
FAIL pack: the decode reproduces the fp16 embedding (corr 0)
FAIL pack: every per-32-group scale/zero is the pack's own f32 value (0 %)
FAIL exception: model has no tensor lm_head.weight
That matches the body, with one small correction: the 4748 x 4 tiles failure was already reported before the fix. The fix uncovers three new failures, not four. npu_pack_import_test tests/golden with no pack: "all npu-pack checks passed".
Changes requested:
-
A regression arm for the gate. c0c6c4d touches only
npu/pack_import.cpp. The +40 test lines in this PR's diff come from 6751bc8, so no device-free test fails without this fix. Please add a crafted graph in which a MatMulNBits qweight is adims [0]initializer with no location, no length and no raw data, next to a real node. Assert that the import does not throw, and that the real node is still gated. Without it, the next drift of this guard is only caught by someone who has the 1.9 pack. -
The branch carries two other PRs' changes under different shas.
- c4458a2 is the main-red fix (
resolve_token_fconst→find_token_fconst). That fix is open as #1673: #1674 carried c4458a2 and was closed as a duplicate of it. The code line is identical, but the comment differs, so whichever lands second conflicts. - 6751bc8 is a different cut of #1669 (already READY at cc0b8d7; the patch-ids differ).
Since this is a draft for part 2 anyway, rebase onto
mainonce #1673 and #1669 land, so the PR's diff is just this change. - c4458a2 is the main-red fix (
Non-blocking: no_bytes() now has external linkage in the pack_import namespace with no header declaration. static, or an anonymous namespace, keeps it file-local as the comment intends.
|
No verdict — it is a draft on purpose — but two things need answering before it becomes one, and the second is not about this PR's own change. 1. The fix itself is right, and the reason is the interesting partThe gate checked 2. The diff contains a
|
| tree | signature | identity | form version |
|---|---|---|---|
origin/main |
— | — | — |
#1669 (head cc0b8d78) |
canonical_copy_reusable(work, expected) — 2 args |
in the scratch name (pack hash + geometry + source size + mtime) | kCanonicalFormVersion |
#1675 (this PR, base main) |
canonical_copy_reusable(work, expected, "ident-1") — 3 args |
a sidecar file (sidecar×8, ident-×7 in the diff) |
absent |
So this branch carries a different design for the very function #1669 is openly under review for — same purpose, different mechanism, and
neither of them is on main yet. Two consequences worth stating in the PR body rather than leaving to whoever merges:
- if this is the design that should win, say so and close npu-pack: reuse the canonical Token .fconst when the source identity matches (Fixes #1662) #1669 (or fold these hunks into it), because otherwise the queue lands the
sidecar variant here as an incidental part of a placeholder-gate fix; - if npu-pack: reuse the canonical Token .fconst when the source identity matches (Fixes #1662) #1669 is the design that wins, this branch needs rebasing onto it, and the
no_bytes()hoist is all that should remain of it.
Nothing against the code: the three-arg version with a sidecar is arguably the stronger identity (a separate file can record more than a name
can). What I am objecting to is that a reviewer reading "Part 1 of 2 for #1671" and a +40 test delta would not know a second PR's
function was being re-implemented inside it.
What I could not check
The pack leg (amd/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu is not on this box), which is exactly what the draft exists to finish. When it is
green, the two questions above are the only things between this and a READY from me.
c0c6c4d to
e11bbb9
Compare
|
Both CHANGES items are gone by construction, not by argument: rebased onto the merged #1673 (
The build at Thanks for |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at f98f4811 (head) — right diagnosis, and no test file
The fix is correct and it is the same placeholder class #1643 named: a node whose weight slot is …qweight.empty with dims [0] is a placeholder, and a [0]-dims tensor is not a real weight — treating it as one makes the geometry gate assert against a zero-length tensor and either refuses a good pack or accepts a bad one. Confirming it in the lm_head gate is the right scope.
But the PR touches npu/pack_import.cpp and nothing else. No case in tests/npu_pack_import_test.cpp, so the branch that decides "is this a real weight or a placeholder?" is exercised by nothing in the repository — and that is the exact branch whose wrong answer is silent (a mis-sized lm_head still produces logits; they are just the wrong ones).
This family has now settled that standard, explicitly, over four PRs: #1638 earned its READY by shipping test_text_config_geometry with a named pack, #1644/#1645/#1655/#1683 all carry a case for the branch they add, and #1633 is blocked on exactly this. So this is not a stylistic ask — it is the one item the family keeps agreeing on because the alternative is a loader where every regression is found by a user.
And it is cheap now: #1656 lands a crafted-pack fixture that runs pi::import_pack() end to end (config.json + model.onnx + .onnx.data + an .fconst), and the placeholder shape needs no new machinery — a qweight.empty node with dims [0] and a qweight.packed sibling is already the shape #1643's synthetic genre pack writes. Two check()s:
- placeholder:
[0]-dims weight slot + apackedsibling → the gate reads the sibling (or refuses to call the placeholder real), geometry is right; - real weight (the control this needs): a
[V, H]lm_headwith no sibling → unchanged behaviour, because the fix must not make a legitimate small head look like a placeholder.
The second one matters more than the first: the bug you are fixing is a false positive, and a false positive fixed by widening the "not real" test can quietly become a false negative.
Happy to take the test as a follow-up if that is easier — but a loader change whose only evidence is the diff is how the npu-pack family got into the state where four packs were refused for a year.
|
Third commit at
The pack leg also stops aborting on a missing tensor: it asks with Where that leaves the pack leg (same pack, this head): One failure, and it is a real one, not a bookkeeping one: this pack is tied per its own So: three commits, |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at e11bbb9
Item 2 of my CHANGES is resolved. The branch now sits on 6301cef (#1673 merged). The duplicate hunk and the #1669 re-cut are gone, and the three-dot diff is npu/pack_import.cpp only, +24/−12.
Both code changes are right, and I verified them on the pack. I built npu_pack_import_test at the head and ran it against ~/models/oga/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu, with TMPDIR set to a scratch dir:
c0c6c4d^ : FAIL 0.5B lm_head plans 4748 x 4 tiles · FAIL exception: …qweight.empty: no data
c0c6c4d : FAIL 0.5B lm_head plans 4748 x 4 tiles · corr 0 · 0 % · FAIL exception: model has no tensor lm_head.weight
e11bbb9 : corr 0 · 0 % · FAIL exception: model has no tensor lm_head.weight
So e11bbb9's plan() head gate (N == vocab && K == hidden, the gate import_pack() already applies) is what clears the 4748 x 4 tiles row. The 1152-wide fused qkv is no longer taken as the head. With no pack argument: "all npu-pack checks passed".
Item 1 is still open, and it now covers two changes. The PR adds no test. Neither no_bytes() at the gate nor the new head gate fails any device-free check if it is reverted. Only someone who has this 1.9 pack would notice. tests/npu_pack_import_test.cpp already has pb::graph / mm_named / pb::if_node, so each arm is a few lines:
- A MatMulNBits whose qweight is a
dims [0]initializer with no location, length or raw data, next to a real node. Expect no throw, and the real node is still gated. - A graph whose first MatMulNBits is a projection (e.g. K=896, N=1152) and whose head (N=V, K=H) comes later. Expect
plan()to reportlm_head.weightas [V, H] with the head's own location.
Since the 1.9 0.5B pack is tied, one question for arm 2's neighbour: with head_from_graph == false, plan() now reports fconst:tail whenever no node has the head's geometry, and no longer only when there are no MatMulNBits at all. For a pack with neither a head node nor a .fconst (tied, external-data genre), is fconst:tail still the right claim, or should it say "tied to the embedding"? A third arm would settle it.
Draft-for-part-2 still applies. Once those arms are in, this is READY from my side.
|
Full sha reviewed for the CHANGES verdict above: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 130b5e1
Correction first. My previous review is attached to this commit, but its body says e11bbb90. You pushed 130b5e1 while I was writing it, so it describes e11bbb9, not this head. This review replaces it for 130b5e1, which I have now reviewed.
130b5e1 is right, and the pack agrees. I rebuilt npu_pack_import_test at this head and ran it on ~/models/oga/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu (scratch TMPDIR; the pack dir was unmodified):
FAIL pack: the decode reproduces the fp16 embedding (corr 0.8852204948414607; … tie_word_embeddings true, so this is the head decode, not an untied pack, and the importer reported it as untied
1 check(s) failed
That is exactly the state the commit message claims. The model has no tensor lm_head.weight abort is gone, the tail head is now written, and the scale/zero row passes. The guard change from onnx.matmuls.empty() to !head_written is the correct question to ask. I also checked that a graph head that is found but bad (wrong bits, an overflow, a length mismatch) throws by name inside that block and never falls through to the tail, so the new guard cannot write a tail head over a refused graph head. Making the pack leg survivable (import_or_fail, find() instead of tensor()) is what #1671's second acceptance item asks for. With no pack: "all npu-pack checks passed".
Still open from my first review: no device-free arm. All of the new test code is in the pack leg, which runs only on a machine that has this pack. None of the three fixes (the no_bytes() gate, plan()'s head gate, the !head_written tail guard) fails a crafted-graph check when reverted. Three small arms with the existing pb::graph / mm_named helpers would pin them:
- A
dims [0]placeholder qweight with no data, next to a real node. Expect no throw. - The first MatMulNBits is a projection and the head comes later. Expect
plan()to reportlm_head.weightas [V, H]. - Only projections, plus a
.fconsttail head. Expect the importer to try the tail (written or refused by name), never "absent withincomplete: []".
The corr 0.885 tied-head decode is real, and it is the part-2 work this draft is waiting on, as you say.
|
Ready for review — the pack leg is green at Four commits,
The last check needed the same correction for the same reason: it asserted "the plan's refusal list is empty iff a container was written", but the plan cannot know what only the import discovers. It now pins written XOR refused — never a container written with refusals, never nothing written and nothing said. Follow-up filed as #1693 for the decode itself (it needs the tail's value convention, not its geometry — the row-shift control and the exact tail arithmetic already rule the geometry out), with the acceptance that this refusal is lifted in the same change that fixes it. Verified at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a9cc216cf32ba24ce8b4153d8fc4e14ae72ec393
The bug is the one this codebase keeps producing, and it is worth naming: one rule, two implementations, and they drifted.
A placeholder states its length as zero rather than stating nothing: a real pack's
...qweight.emptyis dims[0](a 1-D tensor of no elements), not a dimensionless scalar. Both spellings mean "no bytes here", and a reader that only checksdims.empty()calls that a real weight of length 0.
So walk_graph had the correct predicate inline and the lm_head geometry gate had dims.empty(), and the gate duly threw no data out of the walk on a pack that is merely placeholder-only. Hoisting it to file scope (no_bytes, one definition, two readers) is the right fix — the drift is no longer possible, which is worth more than the fix itself.
The predicate handles all three spellings, which is what makes it a rule rather than a special case:
dims = [] -> true (dimensionless: no bytes)
dims = [0] -> true (1-D tensor of no elements: the real pack's shape)
dims = [0,0] -> true
dims = [1,0] -> false (any non-zero extent means bytes)
Checked: g++ -std=c++17 -fsyntax-only npu/pack_import.cpp — clean, so the hoisted function and both call sites (using ::onebit::npu::pack_import::no_bytes; // the same rule walk_graph used inline) compile.
Not run: this box is at 0.3 GiB MemAvailable with GTT 113.4/120 right now — the exact state #1652 exists to end — so I did not build or run npu_pack_import_test. The test's new arms read correctly (it asserts the import refuses by tensor name rather than throwing out of the pack leg, and that the arms which depend on the refused head report "does not apply" instead of passing vacuously), but they are read, not executed, and I would run them before merging if the box were quiet.
This is also the same rule #1687 needs for its unresolved count — weight.find(".empty") is a name match, no_bytes is a shape rule, and a pack that names its placeholder something else would be counted by one and not the other. Not a defect here; worth aligning if #1687 lands after this.
|
Re-review at Both things my earlier CHANGES asked for are done, and done the right way:
The One observation about the shape of what is left, for part 2 (I am not asking this PR to solve it):
Scope of my check, stated: read the diff at this head and compared it against my earlier CHANGES points; no build and no pack run from me. The box has been between Full sha reviewed: |
|
The device-free arms are in at Arm 1 — Arm 2 — the first MatMulNBits is a projection (32 × 16) and the head comes after it (64 × 16, V=64, H=16). The old Both gates reverted in one build, same binary: …and both pass with the gates in place, pack leg included ( Two traps the arms cost to get right, which the third one will hit too:
Arm 3 is not here yet, deliberately. "Only projections plus a So: your CHANGES item is two-thirds closed at this head, with the third owed and named. The corr 0.885 decode stays the part-2 work (#1693), and the refusal added for it is what keeps the pack leg honest in the meantime. |
…k that motivated it now passes (refs #1671) The review's second point: `a9cc216`'s guard is kept but nothing exercised it -- the branch that refuses `lm_head.weight` by name ran in no test, and the pack that motivated it now passes its own oracle. So the arm builds a pack that DECLARES `tie_word_embeddings: true` and whose tail is exactly the length the importer's arithmetic expects, with head tiles that cannot be that head: the oracle is the only thing that can catch it, and it must refuse rather than write. Mutation-checked, as the other three arms are: guard mutated off (`if (false)`): FAIL tied tail arm: … -- no refusal at all here (head lost silently) guard in place: all npu-pack checks passed Everything else is unchanged from the rebase: the branch is now six commits on current main, `79aac1b` (the bf16 fix, which duplicated #1690) is dropped, and the merge conflict in this test file is resolved by keeping both call sites. Verified: `ninja npu_pack_import_test 1bit` rc 0; unit-only run `all npu-pack checks passed`; pack leg on `Qwen2.5-0.5B-Instruct_rai_1.9.0_npu` exit 0; the mutation above; `tools/copyright.py --check` rc 0 with the tree staged; `git add <paths>` only.
79aac1b to
243c2ad
Compare
|
Rebased onto current main, It builds a pack that declares One thing the rebase exposed, and it is not mine: the pack leg no longer passes on current main, for a reason from outside this PR. For this pack that is not true — its weights are in the So the state of this branch: six commits, three gate fixes with mutation-checked arms, the tail-tried arm, and the tied-tail arm; unit suite green; the pack leg is blocked by #1717, not by anything here. Everything the review called READY — |
|
Verdict carries to Every commit is patch-identical; the SHAs moved because the base did. So the earlier verdict at Using |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 243c2ad5bc86d43f1e6aaa0da6303fa8b6f3af3e
Built and ran at this head: all npu-pack checks passed, including final norm: crafted tail envelope imported from tail+64, missing-epsilon control refused.
Two of the three things I raised at 79aac1b are fixed, and the third needs narrowing — including my own wording.
- The conflict is gone: the branch was rebased onto current main and
mergeableis nowMERGEABLE. - The
a9cc216guard is tested now, and by arms that need no device:feaa1087"device-free arms for the two gate fixes, and both fail when…",67d23098(a graph of only projections),243c2ad5(the tied-tail refusal pinned with a crafted arm). That is exactly what I asked for — the guard was called "the right guard for a genuinely bad tail" and "unreachable", and an unreachable guard with no arm is a guard nobody would notice breaking. - My "duplicates #1690" needs narrowing. I wrote at
79aac1bthat the two "change the same readers the same way". At the current heads they are not the same change: #1690 addsweightless_matmulscounting plus the refusal text for a pack whose placeholder and packed sibling are both empty; #1675 hoistsno_bytes()(thedims.empty()→ all-dims-zero rule), fixes thelm_headgate that had drifted todims.empty(), and adds the tied-tail refusal with its arms. They overlap in the placeholder area and both append arms totests/npu_pack_import_test.cpp, so whichever lands second needs a rebase — but calling them duplicates was me reading two commits that had since diverged.
So the sequencing, not the code, is the open question: #1690 is READY at 3 files, #1675 is this much larger and now also READY. Landing #1690 first is cheaper (smaller diff, fewer arms to re-pin); landing this first means #1690 rebases its arms. Neither is blocked on the other's content — only on the test file's tail. That is a call for whoever merges, and I am not merging either, so I am stating the trade rather than picking.
Nothing else changed in the parts I reviewed at a9cc216: the no_bytes predicate covers []/[0]/[0,0] → no bytes and [1,0] → bytes, and the refusals still name the tensor (crafted packed-empty refusal: the pack names gguf_qweight.empty, and its gguf_qweight.packed sibling exists but carries…).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 243c2ad, conditional on merge order: #1688 → #1690 → this
All three points from my CHANGES at 79aac1b2 are resolved:
- The duplicate is gone. The bf16 self-check commit is dropped, so #1690 owns that fix. The branch is rebased, and
git merge-treeonto current main is clean. - The tied-tail guard is pinned.
243c2adadds a crafted declared-tied arm. I mutated the guard toif (false)locally and the arm fails ("FAIL tied tail arm: … no refusal at all here (head lost silently)"). Restored, "all npu-pack checks passed". - The gate fixes and their three arms are unchanged from my earlier mutation checks.
Why the order matters (please merge in this order): the guard refuses a declared-tied head when lmhead_corr <= kTieCorr. On main today the self-check still decodes its bf16 reference as F16 (#1690 is open), which is what produced the 0.885 on Qwen2.5-0.5B-Instruct_rai_1.9.0_npu. At this head on main, that pack doesn't reach the guard yet: the pack leg stops at the placeholder refusal that #1688 removes (I ran it: rc 1 at …qweight.empty … carries no data either). But if #1688 and this land before #1690, the pack reaches the guard with the mis-decoded 0.885, and a correct lm_head.weight is refused on every tied 1.9 pack. With #1690 in first, its corr is 0.999999 and the guard does not fire. So this PR should go after #1690.
Nit (non-blocking): the tied-tail arm's failure text reuses the projections-only arm's wording ("still gets the .fconst tail TRIED … head lost silently"). Naming the oracle ("a tied tail that fails its own embedding check is refused by name") would say which guard failed.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 243c2ad: READY
Re-posted in the format the merge script reads (the earlier review above carries the reasoning; this line names the same head).
|
Orchestrator: superseded. #1671 is closed: main fixed it with #1687 + #1688 (weightless matmuls are dropped and plan() refuses by name only when the pack has no .fconst). This PR now conflicts with that code in npu/pack_import.cpp. If anything here is not already on main, reduce the PR to that delta on top of main; otherwise please close it. |
|
Closing as superseded, deliberately, rather than rebased as-is: main absorbed two of its commits tonight and a third was a duplicate before that.
What was genuinely left is the Thanks for the four verdicts on this one — the mutation-checked arms requirement (three of them, each failing when its gate is reverted) is why #1732 came out with its arm on the first try, and the merge-order condition is what told me to re-check main before rebasing rather than after. |
|
Note for the merge lane: #1732 says it supersedes this PR — after tonight's merges, two of #1675's commits are duplicates of work already on main, and #1732 carries what is left of it (the |
…ed a fused qkv as lm_head.weight (refs #1671) This is all that is left of #1675 after main absorbed the rest tonight. `import_pack()` gates the head on `N == V && K == H`; **`plan()`'s copy of that loop did not** -- it took the first MatMulNBits and `break`ed, so on a pack whose first matmul is a projection the plan reported that projection as the head. Measured on `amd/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu`: plan(): lm_head.weight [1152, 896] <- the fused qkv, 1,032,192 elements correct: lm_head.weight [151936, 896] <- 136,134,656 elements, i.e. 132x The tail entry is keyed the same way, on "did a graph head get WRITTEN" rather than "is the graph free of matmuls": a graph carrying only projections is not a pack without a head, and on the 1.9 line the head is in the `.fconst` tail. Device-free arm, and it fails when the gate is reverted (one build apart): gate reverted: FAIL gate: plan() takes the node whose N x K IS the head, not the first MatMulNBits (lm_head 32 x 16, and the projection is 32 x 16) gate in place: all npu-pack checks passed Why the rest of #1675 is not here: main now drops the matmuls whose placeholder AND `.packed` sibling both carry no bytes, which fixes the `.fconst`-pack import and the tail-guard question in one move, and #1690 fixed the bf16 self-check. Re-applying those would have been the third duplicate of the night. What was genuinely left is this gate and its arm. Verified: `ninja npu_pack_import_test 1bit` rc 0; unit-only run `all npu-pack checks passed`; pack leg on the reference 1.9 pack exit 0 with `all npu-pack checks passed (including the pack leg)`; the mutation above; `tools/copyright.py --check` rc 0 with the tree staged; `git add <paths>` only; host-side, no device.
…ed a fused qkv as lm_head.weight (refs #1671) (#1732) * npu-pack: plan() took the first MatMulNBits as the head, so it reported a fused qkv as lm_head.weight (refs #1671) This is all that is left of #1675 after main absorbed the rest tonight. `import_pack()` gates the head on `N == V && K == H`; **`plan()`'s copy of that loop did not** -- it took the first MatMulNBits and `break`ed, so on a pack whose first matmul is a projection the plan reported that projection as the head. Measured on `amd/Qwen2.5-0.5B-Instruct_rai_1.9.0_npu`: plan(): lm_head.weight [1152, 896] <- the fused qkv, 1,032,192 elements correct: lm_head.weight [151936, 896] <- 136,134,656 elements, i.e. 132x The tail entry is keyed the same way, on "did a graph head get WRITTEN" rather than "is the graph free of matmuls": a graph carrying only projections is not a pack without a head, and on the 1.9 line the head is in the `.fconst` tail. Device-free arm, and it fails when the gate is reverted (one build apart): gate reverted: FAIL gate: plan() takes the node whose N x K IS the head, not the first MatMulNBits (lm_head 32 x 16, and the projection is 32 x 16) gate in place: all npu-pack checks passed Why the rest of #1675 is not here: main now drops the matmuls whose placeholder AND `.packed` sibling both carry no bytes, which fixes the `.fconst`-pack import and the tail-guard question in one move, and #1690 fixed the bf16 self-check. Re-applying those would have been the third duplicate of the night. What was genuinely left is this gate and its arm. Verified: `ninja npu_pack_import_test 1bit` rc 0; unit-only run `all npu-pack checks passed`; pack leg on the reference 1.9 pack exit 0 with `all npu-pack checks passed (including the pack leg)`; the mutation above; `tools/copyright.py --check` rc 0 with the tree staged; `git add <paths>` only; host-side, no device. * npu-pack: the plan arm requires exactly ONE lm_head.weight, not merely the right shape (review of #1732) The review's suggestion, taken because their wrong-mutation experiment showed why it matters: setting the fallback loose does not revert the gate (the gated loop still finds the head) but *does* push a second `lm_head.weight`, and the arm's "take the last match" tolerated it. A future bug of that shape -- two code paths both writing the head -- would have gone unnoticed by a shape-only assertion. check(heads == 1, "gate: plan() pushes exactly one lm_head.weight (" + n + ")"); Mutation-checked with that non-obvious mutation: fallback forced (two heads): FAIL gate: plan() pushes exactly one lm_head.weight (2) gate in place: all npu-pack checks passed Rebased onto main (`bff36348e`) as well, so the conflict window is closed. Verified: `ninja npu_pack_import_test 1bit` rc 0; unit-only run `all npu-pack checks passed`; `tools/copyright.py --check` rc 0 with the tree staged; `git add <paths>` only. --------- Co-authored-by: t <t@e.com>
…e group (#2071) `pr-agent` triggers on `pull_request` and on `issue_comment`, and both land in the same concurrency group (`pr-agent-<number>`) with `cancel-in-progress: true`. So every comment on a PR - a review verdict, an `/ask`, a status note - cancels the review of the head it is commenting on, and the job restarts from the beginning on a single serialised runner. Measured 2026-11-10 on PR #2007 (`27a9788`): the review ran 19:21:26 -> cancelled 19:30:31, with `build`, `hrx_build` and CodeQL green and every READY verdict posted; the PR sat with `pr_agent: COMPLETED/CANCELLED` while the runner queue held other work. That is the "red on pr_agent alone" the issue thread (#1993) reports for #1633, #1638, #1675 and #2007. Group by event name as well as number: a `synchronize` still cancels the review of the head it replaced (a newer head makes the older review moot), and a comment no longer cancels a push-triggered review. Verified on the expression: push -> `pr-agent-2007-pull_request`, comment -> `pr-agent-2007-issue_comment`, newer push -> same group as the earlier push. Refs #1993. Co-authored-by: bong-water-water-bong <bong@water>
Part 1 of 2 for #1671, opened as a draft because it removes the abort without yet making the pack leg green. Reproduced on
amd/Qwen2.5-0.5B-Instruct_rai_1.9.0_npuwithnpu_pack_import_test tests/golden <pack>, and the diagnosis is one guard using the wrong spelling of "this tensor has no bytes".The throw
The gate that checks a node's declared geometry against the bytes its qweight actually carries had drifted to:
A real
...qweight.emptyin these packs isdims [0]— a 1-D tensor of no elements, not a dimensionless scalar. The file already says so, 2 000 lines above, in the note onno_bytes()("Both spellings mean no bytes here, and a reader that only checksdims.empty()calls that a real weight of length 0") — andwalk_graph's.packed-sibling resolution already uses that predicate. The gate did not. Sodims.empty()was false, the placeholder fell through, andbytes_of()threwno databecause a placeholder has nolocationto read from.no_bytes()is lifted to file scope and both readers use it:walk_graphkeeps its behaviour (same rule, one spelling) and the gate treats[0]dims and a dimensionless scalar alike.What changes on the pack
FAIL exception: …qweight.empty: no data, run endsWhat this does not fix — part 2 is next
With the throw gone, the pack leg finally reports the failures it could not reach before:
npu-pack infoon the same pack reports the head aslm_head.weightshape [1152, 896] where 151936 rows are expected, so the head's own geometry is a separate question, andFAIL exception:still ends the run — which is #1671's second acceptance item: one unreadable tensor must report a failed check, not abort the suite. Both are part 2.Verified
Host-side only, no device. Refs #1671 (draft: do not merge before part 2).