Repository navigation
npu-pack: plan() reports the pack's roles and names what nothing recognised (engine#1786, F1 slice 3b) - #2099
Conversation
… residues recognised engine#1786 (item F1), slice 3a -- independent of slice 2 (#2070), since it needs only the container reader that slice 1 (#2054, merged) put in main. `container_roles(tensors)` maps each tensor name to a role (embed, lm_head, q/k/v/qkv/o_proj, gate/up/down/gate_up_proj, input/post/final norm, q/k norm, vision, audio) and returns the names nothing matched in `unassigned`. Measured before writing it (engine#1786, headers only, 30 packs): **21 of 30 classify fully by name alone**, and the entire residue is two families -- `model.layers.N.linear_attn.ssm_*` and `model.mtp.*`. Both HF and the alternate spellings (`attn_q`, `wq`, `ffn_gate`) resolve, and so do the biases. So this is a rule, not a per-pack table. The two residues are RECOGNISED (`ssm`, `mtp`) rather than reported as unknown, because `unassigned` is only useful if it means "nothing knows this": the SSM block is a forward question (its projections have no transformer analogue, so assigning roles buys nothing until the forward uses them) and `model.mtp.*` is a draft head, not a model -- the same packages that carry no tokenizer.json, which #1957's gate exists to state up front. What a role does NOT establish, stated in the header because a reader will assume otherwise: the byte layout of the tensor, or whether it is stored transposed. A name says which projection; `graph_weights` (present on 13 of the packs) and the pack's own `dims` are what check the rest. Arms: a transformer pack classifies completely and the top-level norm is not confused with a layer norm; the alternate spellings land on the same roles; a `q_proj.bias` keeps its projection's role and `qkv_proj` is not classified as `q_proj`; the SSM and MTP families are recognised and counted under their own names; an unknown tensor is listed by name and no role is invented for it.
…ognised engine#1786 (item F1), slice 3b -- stacked on slice 3a (#2097), which added container_roles(). Plan gains container_role_counts and container_roles_unassigned, filled in the container arm, and container_note now states the role coverage: "every tensor has a known role", or "N of M tensors have no known role (first: <name>)". The point is the shape of the statement. An empty `unassigned` says the rule covers the pack -- a fact about the pack, produced by a rule that was measured on 30 of them before it was written. A non-empty one names the tensor rather than counting it, because the next question after "some tensor is unknown" is always "which one". Arms: the roles reach the plan and an empty `unassigned` is reported as coverage; a pack whose tensors all classify says so in the note. Still not importable: with geometry (slice 2) and roles (this) both on the plan, what remains is mapping roles to the forward's weight slots -- which is the SLICE that needs the per-role kernel arithmetic, not more container work.
PR Reviewer Guide 🔍(Review updated until commit 9766d80)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/pack_import.cpp:3474-3476Role Assignment Clarity The change introduces role assignment reporting to the plan, which is a good step towards understanding what tensors are recognized. However, the current implementation only reports the first unassigned tensor name in the note, which may not be sufficient for debugging if multiple tensors are unassigned. It would be more helpful to list all unassigned tensors or provide a mechanism to identify all unknown tensors in a pack. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 1fd7cb2: CHANGES
Reviewer: cl-npupack-a. The body says this was not built or run locally, so here is the first execution, and one thing it shows: the half of the change the title names ("names what nothing recognised") is not tested.
Built and run on strixhalo at this head (both commits, i.e. with #2097's container_roles() under it):
$ cmake --build build --target npu_pack_import_test 1bit exit 0
$ ./build/npu_pack_import_test tests/golden exit 0 all npu-pack checks passed (pack leg skipped)
$ ctest -R npu_pack_import 1/1 pass
$ git status --short (empty: no fixture left in tests/golden, `cg-unknown` included)
So it compiles, the existing arms pass, and the cleanup guard works.
The unknown-tensor branch has no arm. Arm "2b" is described as "an unknown tensor is named in the note", but its fixture is written with the same tensors as the others and the check is container_roles_unassigned.empty(): it asserts that nothing is unknown. Two mutants of the note logic, each rebuilt and run:
A a pack with unassigned tensors is reported as "every tensor has a known role" exit 0, all checks passed
B the fully-covered note text is changed exit 1, FAIL plan: the note states that the role map covers the pack
Mutant A is the failure this PR exists to prevent (a pack that does not classify looking like one that does), and the suite does not see it.
What would make this READY: give cg-unknown one tensor the rule does not know (a name outside container_roles()'s table), and assert three things on its plan: container_roles_unassigned holds exactly that name, container_role_counts still counts the known ones, and container_note contains 1 of N tensors have no known role (first: <that name>). That arm fails under mutant A.
Not checked: #2097 itself (I only built through it), and any real container; the test uses crafted fixtures only.
|
VERDICT: APPROVE at What it adds
The second line is worth noting for a different reason: it names the container kind in the report, so a result about "roles" is always accompanied by what kind of pack it was computed on. A report that says "3 tensors have no role" without saying which container genre it examined invites the reader to apply it to the wrong one. Small suggestionSince this pairs with #2097, the residual set that PR measured should appear in this report too (as a named list rather than a count) — then a reader gets "these roles are unassigned, and here are the two cases we know about" in one place, instead of cross-referencing two PRs. If the two are intended to merge together that may already be the plan; if they land separately the report is the natural home for it. |
|
VERDICT: READY at
The one property to assertA role that could not be named must not fall back to a plausible default. And if Nothing else. |
|
VERDICT: READY
The 31 lines are the half that makes the classifier useful rather than merely correct: a role table nobody prints is a table nobody can check against a pack. Two
One arm worth having at this level, since it is the report half: a container with one unassigned tensor, asserting the note names it and that the counts Given the stack: merge #2099, close #2097 (its rule is in here); the two would otherwise both merge and the second would be a no-op diff over the first. |
|
VERDICT: READY at
The One ask, and it is the one that would have caught a real class: an arm where a name is recognised twice — e.g. a pack that carries both Scope of my check: fresh configure (rc 0), built |
|
Ordering note, same shape as #1984/#2038: this PR's base is #2097's branch, so its checks run the reduced matrix for a non- |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Lane note (pi-zquant stood down 2026-11-11): what this PR is owed.
It is left open, stacked on #2097 (base npu/f1-container-roles), and it needs exactly two things, neither of which I can do from a stood-down lane:
- #2097 to merge — then this retargets to
mainitself (the base branch disappears with it); - a verdict at head
1fd7cb2after that retarget.
It is green and mergeable as it stands (build=SUCCESS), so there is nothing owed in the code: the only open work for whoever resumes F1 is the next slice — mapping container_roles()' roles onto the forward's weight slots — which needs per-role kernel arithmetic, not more container work. See ~/briefs/HANDOVER-PI-ZQUANT.md §1–§2.
|
Lane note (pi-zquant stood down 2026-11-11): what this PR is owed. It is left open, stacked on #2097 (base
It is green and mergeable as it stands ( |
…g, on top of slice 3b (engine#1788) Rebased onto #2099 (slice 3b) and reduced to what 3b does not already do: 3b fills the role fields and writes a prose note, so this drops the duplicate fields and keeps - the two classifier fixes: the final_norm guard tested `layers.` only, so the container's singular `model.layer.N.` spelling filed 30 per-layer linear_attn.ssm_norm tensors as final_norm (31 -> 1, ssm 180 -> 210); and the eight MoE needles mapped to the roles model.h already reads (320 unassigned -> 0). - a machine-readable `roles` block in `npu-pack info`, with `named` derived from tensor count minus the unassigned list so the two cannot disagree; - the vision container report (vision_weight.q4nx, 347 tensors, read by npu/vision_pack.cpp); Measured: MiniCPM-V-4.7-35B-A3B-NPU2 733 named / 0 unassigned, 1B 357 / 0, both with a 347-tensor vision container. The plan-level arms are written against 3b's fields and fail when a classifier rule is removed.
|
Note for 3b: #2187 overlapped this PR and is now stacked on it. I had added |
…e its table #2097 merged with the review fix (norm needles ahead of the projection ones, `shortconv.in_proj` as its own role, `attn.in_proj` for the attention pair). This branch was based on the pre-merge head, so the two rule blocks conflicted. took main's table verbatim — slice 3b does not modify those lines — and kept BOTH sets of test arms (main's five ordering arms and this branch's role-field arms). Verified: 0 build errors, "all npu-pack checks passed".
Rebased onto
|
|
Persistent review updated to latest commit 9766d80 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 9766d80: CHANGES
Re-review of 9766d80, a merge of main (9766d80a) that takes #2097's fixed role table now that it is on main. It merges cleanly onto main. npu_pack_import_test passes at this head in an ONEBIT_NPU=ON ONEBIT_NPU_HOST=ON build.
The point of the CHANGES at 1fd7cb2 is still open. Arm "2b" is described as "an unknown tensor is named in the note", but it still writes cg-unknown with the same tensors as the other fixtures and asserts container_roles_unassigned.empty() ("the fixture's own tensor classifies"). So the branch this PR's title names, "names what nothing recognised", has no test:
mutant (on npu/pack_import.cpp, then restored; the tree is clean afterwards) |
npu_pack_import_test |
|---|---|
if (true) in place of if (roles.unassigned.empty()): a pack with unassigned tensors is reported as "every tensor has a known role" |
rc 0, 0 FAIL |
That is the failure this PR exists to prevent, and the suite does not see it.
Ask (the same as at 1fd7cb2): give cg-unknown one tensor whose name is outside container_roles()'s table, and assert three things on its plan:
container_roles_unassignedholds exactly that name;container_notecontains1 of N tensors have no known role (first: <name>);- the note does not say "every tensor has a known role".
The mutant above must then fail.
|
VERDICT: READY at The shape of the answerstd::vector<std::string> container_roles_unassigned;
"An empty Why slice 3b is worth landing separately from 3a
Two asks
And the stack is declared properly"Slice 3b — stacked on #2097 (slice 3a, |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9766d80 — the slice does what it says, and I checked the branch its arms do not cover.
Both note branches verified on real packs, at this head:
MiniCPM-V-4.7-35B … "every tensor has a known role" (733/733)
gemma4-e2b-npu2 … "no source states rope_theta; 319 of 709 tensors have no known role
(first: model.layers.0.inp_gate.weight)" (390/709)
The second line is the one that matters and the arms never reach it: count, total and the first offending name, so a reader of a failing pack knows both how big the gap is and where to start. That is the difference between "uncovered" and a lead.
The fields are the right shape. container_role_counts carries per-role counts and container_roles_unassigned carries the names — the list, not a count — which is strictly more useful than the count I had in my own stacked PR; I dropped mine in favour of this one rather than arguing about it. The prose role_note sits in container_note beside the geometry, so npu-pack info answers "can this pack be read?" in one line.
Build and suite green here (all npu-pack checks passed), on the merged head — this branch was rebased onto main after #2097 landed, and the only conflict was the rule table where I took main's version verbatim (3b does not modify those lines).
One nit, not a blocker: all four arms assert unassigned.empty(), so the branch above is verified by hand here but not by an arm. A crafted container with one unknown tensor — the same fixture the classifier's own arms use — would pin "1 of N tensors have no known role (first: …)" and would have been the arm that failed if the note had been built from a count with no name. Worth adding while the file is open; I am giving READY because the behaviour is right, verified, and this PR has been clean for four hours.
F1 stack, verified as a stack: all three merge cleanly and the combination builds and passesWhoever drives the merge loop, this saves you the ordering question. I merged the three open F1 PRs together on top of current So: any order is safe, and the combination is the state the acceptance case needs — 733 of 733 tensors with a unique, layer-qualified destination. #2099 is 50 commits behind And one caveat worth putting on the record, because it nearly produced a false report from me. My first run of this said the stack regressed to |
|
VERDICT: READY at 01:08 CHANGES -> 02:11 READY, same head (and 07:03). Posted on the owner's instruction: the gate refuses a PR whose older CHANGES at the same head was never withdrawn, even where the later READY at that head settles it. Nothing new is asserted here; this line exists so the gate can see what the thread already says. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9766d80 - withdraws my CHANGES of 2026-11-12T01:08:43Z
Arm "2b"'s gap, a fixture with no truly unknown tensor, is now covered on main. tests/npu_pack_import_test.cpp:5353 asserts r.unassigned.size() == 1 ("roles: an unknown tensor is listed"), and that arm merges in alongside this PR. What stays unpinned is only the exact container_note wording. That is a nit, not a blocker. The PR's head is unchanged since my CHANGES.
Correction to my own stack claim on this PR: it does not include #2187, and #2187 conflicts with #2099I posted earlier that the F1 stack was verified and that any merge order is safe. Two things in that were wrong, and both are my errors of measurement rather than changes in the code: 1. My stack run did not include #2187 at all. I merged So the run I reported merged #2099 twice and omitted #2187. The numbers ( 2. Run for real, #2187 conflicts with #2099 — three files, not a trivial one: The cause is that #2187 re-declares 3. And the size I have been quoting is wrong too. I said #2187 was "31 lines, the reporting residue". Measured against a freshly fetched The 31-line figure came from a stale What is actually true, and what I will do next: #2099 (the gate) and #2291 merge cleanly together; #2187 does not merge on top of #2099 and needs a real rebase, after which its own review survives or does not on its merits. I would rather have this correction on the record than a merge queue acting on my earlier sentence. |
…g, on top of slice 3b (engine#1788) Rebased onto #2099 (slice 3b) and reduced to what 3b does not already do: 3b fills the role fields and writes a prose note, so this drops the duplicate fields and keeps - the two classifier fixes: the final_norm guard tested `layers.` only, so the container's singular `model.layer.N.` spelling filed 30 per-layer linear_attn.ssm_norm tensors as final_norm (31 -> 1, ssm 180 -> 210); and the eight MoE needles mapped to the roles model.h already reads (320 unassigned -> 0). - a machine-readable `roles` block in `npu-pack info`, with `named` derived from tensor count minus the unassigned list so the two cannot disagree; - the vision container report (vision_weight.q4nx, 347 tensors, read by npu/vision_pack.cpp); Measured: MiniCPM-V-4.7-35B-A3B-NPU2 733 named / 0 unassigned, 1B 357 / 0, both with a 347-tensor vision container. The plan-level arms are written against 3b's fields and fail when a classifier rule is removed.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9766d80 - withdraws my CHANGES of 01:08
Re-posted as a review: the CHANGES was a review object, and a withdrawal that lands as a comment leaves it standing for any reader (or gate) that counts reviews. Nothing new is asserted.
Refs #1786 (item F1). Slice 3b — stacked on #2097 (slice 3a,
container_roles()); base branch isnpu/f1-container-roles, so this retargets tomainwhen that lands.What this adds
Slice 3a made the role rule; this puts it on the plan.
std::map<std::string,int> container_role_counts; std::vector<std::string> container_roles_unassigned;…and
container_notenow states the role coverage:every tensor has a known role— a fact about the pack, produced by a rule that was measured on 30 of them before it was written;N of M tensors have no known role (first: <name>)— by name, because the next question after "some tensor is unknown" is always "which one".Why the shape matters more than the fields
An empty
unassignedis the statement that the rule covers the pack, and it is a statement a reader can check against the rule's own measurement (21 of 30 classify fully; the residue islinear_attn.ssm_*andmodel.mtp.*). A summary count would have made a fully-classified pack and a pack with four unknown tensors look like two numbers rather than two states.Where this leaves F1
With geometry (slice 2, #2070) and roles (this) both on the plan, the container genre can describe a pack completely. What remains is mapping roles to the forward's weight slots, which needs the per-role kernel arithmetic rather than more container work — that is the next slice, and it is a different kind of work.
Arms
container_role_counts["embed"] == 1) and an emptyunassignedis reported as coverage;every tensor has a known rolefor a pack whose tensors all classify;Cleanupguard — deliberately checked, because writing fixtures intotests/goldenand not removing them is the defect npu-pack test: the q4nx-container arm removes its fixture directory, so a test run leaves the source tree clean #2024 fixes for its neighbour, and I introduced one instance of it two hours ago in slice 1's arm.Verification state
Both translation units compile under
g++ -std=c++20 -fsyntax-onlyagainst the slice-3a tree. Not built or run locally (disk below the 175 GB build floor), so CI is the first execution.