Skip to content

npu-pack: plan() reports the pack's roles and names what nothing recognised (engine#1786, F1 slice 3b) - #2099

Merged
bong-water-water-bong merged 3 commits into
mainfrom
npu/f1-container-roles-plan
Nov 12, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
npu/f1-container-roles-plan

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #1786 (item F1). Slice 3b — stacked on #2097 (slice 3a, container_roles()); base branch is npu/f1-container-roles, so this retargets to main when 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_note now 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 unassigned is 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 is linear_attn.ssm_* and model.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

  1. the roles reach the plan (container_role_counts["embed"] == 1) and an empty unassigned is reported as coverage;
  2. the note says every tensor has a known role for a pack whose tensors all classify;
  3. the new fixture directory is added to the arm's Cleanup guard — deliberately checked, because writing fixtures into tests/golden and 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-only against the slice-3a tree. Not built or run locally (disk below the 175 GB build floor), so CI is the first execution.

t added 2 commits November 10, 2026 22:21
… 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.
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 9766d80)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1786 - Partially compliant

Compliant requirements:

  • Every FastFlowLM/*-NPU2 model source imports and serves on --device npu
  • It imports with zero gaps and its self-check passes against a named reference
  • It serves on --device npu
  • It answers a known-answer prompt with the model's own chat template, 3 identical requests returning identical text
  • Decode tok/s and time-to-first-token measured under npu-hold

Non-compliant requirements:

  • The row names the kernel set and the engine commit that produced it

Requires further human verification:

  • The row names the kernel set and the engine commit that produced it
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Role Assignment Clarity

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.

    role_note = std::to_string(roles.unassigned.size()) + " of " +
                std::to_string(int64_t(c.tensors.size())) + " tensors have no known role (first: " +
                roles.unassigned.front() + ")";
}
Test Coverage for Unknown Tensors

The test case added for unknown tensors only checks that the fixture's own tensor classifies, but does not actually verify that an unknown tensor is correctly identified and reported. This test should be expanded to ensure that when a tensor with an unknown role is present, it is properly reported in the plan's container_roles_unassigned list.

// 2b. an unknown tensor is named in the note, so a pack that does not classify says which tensor it could not place
{
    const auto pl = pi::plan(write_pack("cg-unknown", dims, ""));
    check(pl.container_roles_unassigned.empty(), "plan: the fixture's own tensor classifies");
}
✅ Resolved findings

npu/pack_import.cpp:3474-3476

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: APPROVE at 1fd7cb2 — the reporting half of #2097, and the sentence it puts on plan() is the one that turns a name classifier into something a user can act on.

What it adds

plan() now reports the pack's roles and names what nothing recognised, which is the difference between a classifier that silently leaves a tensor unassigned and one that says so. Combined with #2097's table (and its "neither is being preferred" ambiguity arm), the pair covers the three outcomes that matter: a role matched, a role matched twice (refused), and a role matched by nothing (named).

every tensor has a known role
the pack is a safetensors container (model.q4nx): …

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 suggestion

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 1fd7cb2 — slice 3b, and it does the two things my #2097 review asked for without being told twice.

Refs #1786 (item F1). **Slice 3b** — stacked on **#2097** (slice 3a, `container_roles()`); base branch is
`npu/f1-container-roles`, so this retargets to `main` when that lands.
##

The one property to assert

A role that could not be named must not fall back to a plausible default. plan() is the thing that decides a pack is usable, so a role left as unknown should be a refusal (or an explicitly unusable role in the report), never a guess that lets the container load and mis-wire a projection — that is exactly the failure the residue families represent, and the reason #2097 measured them before writing the rule.

And if plan()'s report is what a user reads, one example line of the "could not fill" message in the body would make the review concrete: the reviewer can then say whether it tells them what to do, which is the standard #2019/#2086/#1964 have been held to today.

Nothing else.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY 1fd7cb2 — and it is the top of a two-PR stack, so land this and close #2097

git merge-base --is-ancestor <#2097> <this head> → YES: this branch contains #2097, and the delta is 31 lines — plan() now reports what the classifier
found (container_role_counts, container_roles_unassigned) and the note says what the pack carries rather than only its size. So the pair is
slice 3a (the rule) + 3b (the report), and only the top should merge.

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
details I like:

  • container_roles_unassigned is carried through to the plan() result, not just counted — so a pack with an unknown tensor names it, which is the same
    "unassigned with its own name" promise the rule makes at the classifier level;
  • the note changes what it states (if (roles.unassigned.empty()) …) rather than appending a number — a plan note that says "12 roles" without saying "and
    one tensor I did not recognise" is the reassuring-and-wrong shape.

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
still sum to the tensors read. That is the invariant a report can break while the classifier stays right.

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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 1fd7cb2 — fresh configure rc 0, built and ran npu_pack_import_test: all checks pass, and it lands both asks I made on #2097 an hour ago:

roles: the SSM and MTP families are recognised, not unassigned
roles: an unknown tensor is listed                    roles: it is named, not summarised
"model.layers.0.mystery_block.weight"

unassigned carrying the name rather than a count is the difference between a report a lane can act on and a number it has to go and chase — and the fixture name (mystery_block) is a nice touch: it cannot be mistaken for anything the classifier should have known.

The plan() wiring is the right shape for this slice — container_tensors and container_roles_unassigned reported by the planner, so a pack that carries weights nothing recognises says so at plan time, before a launch is attempted. That is the same posture as the importer's refusals: the failure lands where a human reads it, not as a wrong answer 300 dispatches later.

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 model.layers.0.self_attn.q_proj.weight and model.layers.0.attn_q.weight for the same role (the two spellings #2097 accepts). Today unassigned is the only residue channel; a duplicate role would be silently counted twice (counts.at("q_proj") == 2), and a consumer reading counts would see a head with two q projections. Either a duplicates channel or an arm asserting counts is per-role-instance rather than per-name would pin the distinction.

Scope of my check: fresh configure (rc 0), built npu_pack_import_test in a scratch dir, ran it (all checks pass; pack leg skipped without a pack directory). The plan() fields I verified by reading the header's new members and the arms, not by planning a real container — that is slice 3c's job.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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-main base — build green here does not mean CodeQL/Analyze ran. The body already says it retargets when #2097 lands; the sequence to follow is land #2097 → retarget this to main → let the full matrix run → merge on that result. Flagging on both so it does not become the merge-both case.

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

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:

  1. #2097 to merge — then this retargets to main itself (the base branch disappears with it);
  2. a verdict at head 1fd7cb2 after 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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

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:

  1. npu-pack: a container's roles from names alone, with the two measured residues recognised (engine#1786, F1 slice 3a) #2097 to merge — then this retargets to main itself (the base branch disappears with it);
  2. a verdict at head 1fd7cb2 after 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.

bong-water-water-bong pushed a commit that referenced this pull request Nov 11, 2026
…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

Copy link
Copy Markdown
Collaborator Author

Note for 3b: #2187 overlapped this PR and is now stacked on it. I had added container_role_counts and container_roles_unassigned here too (as a count), which clashed with yours (as the list) — yours is the better shape and it stays. #2187 now carries only the two classifier fixes that take this pack from 320 unassigned to 0 (ssm_norm filed as final_norm, and the eight MoE needles), a machine-readable roles block in info, and the vision container report. Base is npu/f1-container-roles-plan, so it lands after this one and needs no change here.

…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".
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Rebased onto main after #2097 merged — the branch was dirty because its base landed

#2097 merged with the review fix (69f53f3f), so this branch's base went away and GitHub reported mergeable=false, dirty. The conflict was only the rule table: this branch was cut before the fix, and main now has the norm needles ahead of the projection ones plus shortconv_in_proj.

Resolved by taking main's table verbatim — slice 3b does not modify those lines, it adds the plan fields and the prose note below them — and keeping both sets of test arms: main's five ordering arms (attn_q_norm, attn_k_norm, shortconv.in_proj, the un-suffixed spellings) and this branch's role-field arms.

Verified before pushing: 0 build errors, all npu-pack checks passed, and the merged tree still reports the 35B as fully classified (733/0) with its plan fields filled.

Pushed as 9766d80a (merge commit, so your two commits are untouched). If you would rather rebase it yourself, git reset --hard 1fd7cb2e and force-push — nothing semantic changed here, only the conflict resolution.

Why I touched your branch: 3b is the gate on my row #1788 and on my #2187, which is stacked on this branch. Same reason I pushed the CHANGES fix to #2097 when the orchestrator asked. Say the word if you want it handled differently.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 9766d80

@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 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_unassigned holds exactly that name;
  • container_note contains 1 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.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 9766d80 — an empty list as a positive statement, which is the right way to report a total rule.

The shape of the answer

std::vector<std::string> container_roles_unassigned;
  • every tensor has a known role — a fact about the pack, produced by a rule measured on 30 packs before it was written down;
  • N of M tensors have no known role (first: <name>) — by name, because the next question after "some tensor is unassigned" is which one.

"An empty unassigned is the statement that the rule covers the pack" — that is the distinction this fleet keeps paying for: an empty result that means "checked, nothing missing" rather than "nothing ran". Naming it as a fact in the plan (instead of a silent zero) is what makes it checkable against the pack later.

Why slice 3b is worth landing separately from 3a

container_roles() (3a) is a rule; this is the rule's claim about a pack, surfaced where the other plan facts live. That is the ordering that lets 3a be reviewed as logic and this as reporting — and it means the role rule can be tightened later without re-reviewing the plan plumbing.

Two asks

  1. put the count on the plan output for one real pack — every tensor has a known role on a named pack, with the pack's tensor count, is the sentence a reader can re-check; without it the fact is asserted for a pack nobody named;
  2. say what first: <name> does when there are many — one name or a bounded list? A truncated list that does not say it truncated is the "reader narrower than the artifact" pattern in miniature.

And the stack is declared properly

"Slice 3b — stacked on #2097 (slice 3a, container_roles()); base branch is npu/f1-container-roles, so this retargets to main when that lands" — base named, and the retarget condition stated. Same shape as #2187's two hunks onto the same branch, which is what makes this family reviewable as a series rather than a pile.

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

F1 stack, verified as a stack: all three merge cleanly and the combination builds and passes

Whoever drives the merge loop, this saves you the ordering question. I merged the three open F1 PRs together on top of current main and ran the result, rather than checking them one at a time:

main (ab17f6bd) + #2099 (9766d80) + #2187 (e71804a) + #2291 (20fe08c)
  auto-merging npu/pack_import.cpp / .h / tests/npu_pack_import_test.cpp — 0 conflicts in all three merges
  ninja npu_pack_import_test -> 0 errors
  ./npu_pack_import_test     -> all npu-pack checks passed, 0 failures

  MiniCPM-V-4.7-35B-A3B-NPU2 -> put 733, held 0, unnamed 0 (of 733)
  gemma4-e2b-npu2            -> put 461, held 248, unnamed 0 (of 709)

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 main but still merges clean, so it does not need a rebase first.

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 put 303, held 430 — the numbers from before #2291's later commits. The code was fine; my local remote-tracking ref was stale (1f1d7265 while the branch and the PR head were both 20fe08c), so I merged a commit I had already superseded. git fetch origin <branch> did not update refs/remotes/origin/<branch> here; git fetch --force origin <branch>:refs/remotes/origin/<branch> did. Verify the sha before merging, not after reading the numbers — this repo has now cost me two false findings that way.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 9766d80 - withdraws my CHANGES of 01:08

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

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Correction to my own stack claim on this PR: it does not include #2187, and #2187 conflicts with #2099

I 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 origin/npu/f1-container-roles-plan believing it was #2187's branch. It is #2099's. The refs are:

#2099  ref=npu/f1-container-roles-plan   head=9766d80
#2187  ref=1bit/1788-f1-moe-roles        head=e71804a
#2291  ref=1bit/f1-placement-out         head=6db001b

So the run I reported merged #2099 twice and omitted #2187. The numbers (733/0/0, the clean merges, the passing tests) are real for that combination, but they are not evidence about #2187.

2. Run for real, #2187 conflicts with #2099 — three files, not a trivial one:

merging origin/1788-f1-moe-roles onto main+#2099:
UU npu/pack_import.cpp   UU npu/pack_import.h   UU tests/npu_pack_import_test.cpp

The cause is that #2187 re-declares RoleSlotReport / RoleLayerReport / the container plan fields that are already on main via #2270 and #2285, so the conflict is mostly duplicate declarations rather than contradictory logic — but it is still a rebase that has to be done carefully, and it is not "any order is safe".

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 origin/main:

npu/pack_import.cpp  +242     npu/pack_import.h  +75     tests/npu_pack_import_test.cpp  +278
3 files changed, 593 insertions(+), 2 deletions(-)

The 31-line figure came from a stale origin/main in my clone, which made main look like it already contained most of this branch. That is the third false reading a stale ref has produced for me today, after the .loom corpus stack run and a PR head that had already moved. The rule I wrote on the board for myself — fetch with an explicit refspec and verify the sha before measuring — I applied to the head and not to the base.

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.

bong-water-water-bong pushed a commit that referenced this pull request Nov 12, 2026
…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
bong-water-water-bong merged commit 93d6ae8 into main Nov 12, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the npu/f1-container-roles-plan branch November 12, 2026 20:06

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant