Repository navigation
npu-pack: classify the MoE families and the singular layer. spelling (engine#1788 acceptance) - #2187
bong-water-water-bong wants to merge 10 commits into
Conversation
PR Reviewer Guide 🔍(Review updated until commit 24ded5a)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/pack_import.cpp:3348-3355Incorrect MoE Role Assignment The order of checking in the npu/pack_import.cpp:3348-3355Incorrect MoE Role Assignment The order of checking in the npu/pack_import.cpp:3387-3389Final Norm Misclassification The condition added to identify npu/pack_import.cpp:3341-3346Incorrect MoE Role Assignment The change introduces new MoE tensor role mappings, but there's a potential issue with how the npu/pack_import.cpp:3380-3382Final Norm Misclassification The added condition for identifying |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at e499f34: READY
The role rule table extended to MoE families and the singular layernorm, in the same shape #2097 established: static const Rule kRules[] with {role, needle} pairs, and several needles mapping to one role (embed_tokens, tok_embeddings, word_embeddings all → embed).
That plurality is the part that makes a name-derived classifier safe: a role is recognised by any of the spellings the format uses, and the table is where a new spelling is added — one line, reviewable, rather than a chain of if statements in the importer. The MoE families are exactly where the spellings proliferate (model.layers.N.block_sparse_moe.*, mlp.experts.*, gate_up_proj fused), so extending the table now is cheaper than discovering each one as a misclassification.
Added:
|
| pack | before | after |
|---|---|---|
| MiniCPM-V-4.7-35B-A3B-NPU2 | 413 named / 320 unassigned | 733 named / 0 unassigned |
| MiniCPM-V-4.7-1B-NPU2 | — | 357 named / 0 unassigned (no regression: the dense sibling was already complete) |
Full suite unchanged and green (all npu-pack checks passed).
Why this belongs here rather than in 3b: it does not change what plan() does with the roles — it makes the classification observable through the shipped tool instead of through a test-only harness, which is the precondition for 3b being verifiable at all. If you would rather 3b own the output shape, drop this commit and keep the two classifier hunks; they are independent.
|
Persistent review updated to latest commit 5c8694c |
Added:
|
Correction to my own last comment: the vision container needs no role vocabulary — it already has a readerI wrote that "a plan that builds the vision tower needs The vision tower has its own pack reader: So the correct statement of the gap is narrower and better news:
I would rather correct this in the thread than leave a scope claim that would have sent someone building a vocabulary that already exists. |
|
Persistent review updated to latest commit 78a44da |
|
VERDICT: READY at The singular Naming all eight MoE families rather than mapping them to an existing role is right for this slice, because Two asks:
Scope of my check: fresh configure rc 0, built |
Added: the F1 acceptance is now an assertion, not a scratch harness
The fixture is a real safetensors container written by the test ( Both arms have teeth, verified by mutation: deleting the They also restate on the real packs what they assert on the fixture (35B 733/733, 1B 357/357) through the same code path that 3b will consume. This is the gate slice 3b should be judged by: when the container-genre plan starts consuming the roles, the acceptance is "this pack comes back 0 unassigned", and it is now checked in CI's |
|
Persistent review updated to latest commit 37139ab |
|
VERDICT: READY at Verified specifically, because each of these is a way the change could be subtly wrong rather than obviously broken:
I did not build or run this at the head (code review only, stated rather than implied); the prior verdict at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
READY at 37139ab. The measurement is the right kind — 733 real tensor names off the safetensors header, driven through container_roles() CPU-only — and taking it from 413/320 to 733/0 is the whole acceptance criterion for #1788's classifier.
Two notes, the first one an argument for generalising the fix you just made.
1. The final_norm rule is now a blocklist of spellings, and the test is built from the spelling it blocks. The change is:
if (!matched && n.find("norm.weight") != npos && n.find("layers.") == npos && n.find("layer.") == npos)
role = "final_norm";That is correct for layer. — and it is one more entry on a list that has to name every block spelling in the world. model.blk.0.norm.weight, h.0.norm.weight, decoder.block.0.norm.weight: each one is norm.weight with neither layers. nor layer. in it, so each one becomes a top-level final_norm and silently mis-classifies a layer norm as the model's final one — the exact defect this PR fixes, one pack family later. The test cannot catch it either, because every name in the new arm is written in the singular layer. spelling, i.e. the arm was built from the same assumption the code makes.
The robust rule here is positive and spelling-free: a name is final_norm only when it carries no numeric block index (\.\d+\.), which distinguishes model.norm.weight from model.<anything>.0.norm.weight for every prefix in every pack. If you adopt it, one arm using a spelling nobody has shipped yet (model.blk.0.norm.weight → not final_norm) is what keeps the rule honest; the current arms would pass either way. That is the difference between fixing this pack and closing the class.
2. The 733/733 claim lives only in the PR body. The unit arms use synthetic names (fairly), so a later change that un-names a hundred real tensors passes every row. Since the measurement is CPU-only and already scripted, either record the number in the test comment as the figure to re-derive, or add one arm over the pack's distinct spelling shapes (40 layers × 8 MoE roles). I would not block on it — but the sentence "733 → 733" is the acceptance criterion, and right now nothing in the tree can be re-run to check it.
Minor: check(r.counts.at("gate_exps") == 1 …) throws on a missing key rather than failing the check; r.counts.count("gate_exps") && r.counts.at(...) gives a readable FAIL instead of an abort — worth it in a test whose entire job is to say what went wrong.
Not checked: the vision-container block (I read it, but its 733-name measurement is a different pack) and anything device-side — the whole change is host-only by design, which is right for a classifier.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 37139ab — the measurement is complete (733 → 733 named, 0 unassigned) and both changes hold up, including one I set out to break and could not.
The measurement drives the change, which is the order I want
before: 733 real tensor names → 413 named, 320 unassigned (final_norm 31)
after: 733 → 733 named, 0 unassigned (final_norm 1, ssm 210, 8 MoE roles × 40)
Driven over the real container's 733 names, not a fixture — and 320 → 0 with the counts of what each group took. That is the number that says the classifier is finished rather than better: an unassigned count of zero with a role breakdown adding to 733 is checkable arithmetic.
Change 1 — I tried to break the guard and could not
// "never inside a layer" has to be spelled positively enough to survive the container's own naming:
// MiniCPM-V 4.7 spells its blocks `model.layer.N.` (singular), so a guard against "layers." alone filed
// 30 per-layer linear_attn.ssm_norm tensors as final_norm — ahead of the kSsm loop below (engine#1788).
if (!matched && n.find("norm.weight") != npos && n.find("layers.") == npos && n.find("layer.") == npos)My first read was that the layer. test subsumes layers. and is redundant. I checked the strings and it is false:
'layer.' in 'model.layers.3.norm.weight' → False (layers. has an "s" before the dot)
'layers.' in 'model.layer.3.norm.weight' → False
Neither spelling contains the other, so both tests are load-bearing, and the comment's diagnosis (a layers.-only guard misfiled 30 tensors, ahead of the SSM loop so they never reached it) is exactly the failure the two tests prevent. Good.
Change 2 — the eight MoE families map onto the forward's existing vocabulary
up/gate/down_exps_weight, share_{up,gate,down}_exps_weight, moe_router_weight, shared_expert_gate_weight — all present in npu/forward/include/model.h (verified: 2 hits each for the three, 1 each for the other three), so this is a translation into names the forward already speaks, not a new vocabulary. Same shape as #1897: read the declaration, don't invent one.
And the substring hazard is real and handled:
"The
share_*spellings come first becauseshare_gate_exps_projcontainsgate_exps_proj."
with an arm for it ("the shared spelling doesn't double-count"). That is the #1983 lesson (substring vs parser) caught before it shipped.
One ask, and it is about the durability of that fix rather than this change: ordering is a fragile way to prevent shadowing — it works until a ninth needle arrives whose spelling sits inside an earlier one, and nothing fails at that moment. Two cheap ways to make it structural: (a) an arm that iterates all eight needles and asserts each one's first matching role is its own (a decision table over the needle list, so a new needle with a shadowed name fails the test), or (b) match on the path boundary before _exps, so ordering stops mattering.
Scope: +176/−1 across pack_import.{cpp,h} and the test, stacked on #2097 (so that one first), with container_roles() not yet wired to an output (slice 3b) — correctly labelled as such.
37139ab to
82baf0c
Compare
Rebased onto #2099 (slice 3b) and reduced to what 3b does not already doChecking my row's critical path turned up an overlap I should have found before opening this: #2099 is slice 3b, and it adds the two fields this PR also added — So this branch is now stacked on
Dropped: the duplicate fields, my count-only version of Measured after the rebase, on this branch: The plan-level acceptance arms are rewritten against 3b's fields ( Also: the first rebuild after the rebase was SIGKILLed (exit 137) by the thermal guard at a hot die — the guard working as designed; no orphan was left (checked |
|
Persistent review updated to latest commit 82baf0c |
|
VERDICT: READY at 413 → 733 named, 320 → 0 unassigned, over real names
Driven over 733 real tensor names rather than a sample, which is what makes The etiquette is right and I want it said out loud
Two hunks, measured, offered on a branch rather than pushed onto a branch its author is working in. That is how a cross-lane fix should arrive, and it is the third PR tonight that named its stack explicitly. Three asks
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 82baf0c: READY
unnamed is the right thing to add to a name-derived classifier: counting the tensors that got no role and reporting it beside the named count
const int64_t unnamed = int64_t(container_roles_unassigned.size());
c["roles"] = {{"named", container_tensors - unnamed}, …turns "classification ran" into "classification covered N of M". That is the property this file has been missing all day in a different guise — #1841's projections that were silently skipped while the container reported complete, and #2187's own subject: a role table that matches what it knows and says nothing about the rest.
A classifier that reports its unassigned set is one that can be trusted at the boundary; one that reports only successes is a filter.
|
Persistent review updated to latest commit e08cb40 |
|
Persistent review updated to latest commit ae225c8 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at ae225c8: READY
Re-verified at the new head: the classifier still reports its unassigned count (unnamed) beside the named one, so "classification ran" reads as "classification covered N of M". That is the property that makes a name-derived classifier trustworthy at its boundary — one that reports only successes is a filter.
|
VERDICT: READY at
|
|
Persistent review updated to latest commit e71804a |
Split: the behaviour half is now #2277 on
|
…ain (engine#1788) (#2277) * npu-pack: the classifier fixes and the container import refusal, on main (engine#1788) The behaviour half of my #2187, ported to `main` because it does not need slice 3b: #2270 already landed on main from a main base, and this is the same move -- the parts that classify and refuse do not touch the plan fields 3b adds. - `final_norm`'s guard tested `layers.` and MiniCPM-V spells its blocks `layer.N`, so 30 per-layer linear_attn.ssm_norm tensors were filed as the model norm. - `self_attn.gate_proj` is an ATTENTION output gate, and it was filed as the dense `gate_proj` (name precedent: model.h's `self_attn_gate_proj_weight`). - the eight MoE families (share_* first), mapped to the fields model.h already reads. - gemma3/gemma4/bolt recognition: `pre_/post_feedforward_layernorm`, `inp_gate`, `per_layer_projection`, `layer_output_scale`, `post_layernorm`, `per_layer_model_proj` (role `gemma_local` -- no model.h field exists for them) and `rope_freqs`/`per_layer_token_embd` (role `container_extra`, known for a different reason). Measured: gemma3 36 -> 0 unassigned, gemma4 319 -> 0, bolt 382 -> 0, MiniCPM-V unchanged at 0. - `npu-pack import` on a container pack now refuses BY NAME ("733 tensor(s); importing it needs the container-genre writer (engine#1786, item F1)") instead of falling through to the ONNX importer's "cannot open model.onnx". Arms: the attention gate vs the FFN gate, the eight MoE families with the shared spelling not double-counting, the five gemma families recognised, and that a norm the forward reads is not swallowed by the recognisers. * tests: an arm for the attention gate, so removing the rule is red (engine#2277 review) The review's mutant was right: the body listed an attention-gate arm that the diff did not contain, so deleting {self_attn_gate_proj, "self_attn.gate_proj"} left the suite green while MiniCPM-V's 30 attention gates went back to being the dense FFN gate. Now: self_attn.gate_proj.weight -> self_attn_gate_proj, mlp.gate_proj.weight -> gate_proj, and counts.at("gate_proj") == 1; removing the rule fails two of those three. --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Verified against the new main: auto-merges clean, test passes, and the reporting is visible — not just setThe READY verdicts here were written when this PR was 3 files / +562. Since then #2277 landed the behaviour half on main (the classifier fixes and the container import refusal), so the unique diff is now 31 lines: Nobody had checked that residue against the merged main, so I did, on a local merge of That last line is the point of the PR and it is visible in the tool's output, which is what distinguishes this half from a field nothing reads. Two notes on getting there, because both bit me on this check and are the kind of thing that produces false review claims:
Nothing else holds: the fields reach the plan ( |
…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.
…pen model.onnx' A container-genre pack has no model.onnx by design, so import_pack fell through to the ONNX reader and reported a missing file about a pack that has no graph at all -- the exact diagnostic the plan() comment says the genre detection exists to avoid. Refused here the way the external-data genre already is: the message names the tensor count, how many names nothing knows, and the missing piece (the container-genre writer, engine#1786 item F1), and points at `npu-pack info` for what the container holds. Measured on MiniCPM-V-4.7-35B-A3B-NPU2: 733 tensor(s), 0 name(s) nothing knows -- so the reader half of that genre is finished and only the writer is missing.
MiniCPM-V 4.7 sets `attn_output_gate: true`, so every layer carries `self_attn.gate_proj.weight` -- and every one of them was filed under the dense-MLP role `gate_proj`, because that needle matches the substring. The forward already keeps them apart (`model.h`: `gate_proj_weight` vs `self_attn_gate_proj_weight`), so this uses its name and puts the rule ahead of the `gate_proj` needle. Measured on both packs on this box: MiniCPM-V-4.7-35B-A3B: gate_proj 30 -> 0, self_attn_gate_proj 30 (that pack has no dense gate at all) MiniCPM-V-4.7-1B: gate_proj 42 -> 24 real + 18 self_attn_gate_proj 733/733 and 357/357 still named, 0 unassigned; suite green.
…e same one (engine#1788, 3c step 2) The writer's second step is role -> destination tensor, and the role vocabulary is not a safe key on its own: the gate_proj defect fixed in the previous commit is exactly one needle serving two different tensors, and a container can carry both. So the mapping is a table with a refusal rather than a switch the writer may get wrong: - container_role_field(role) names the forward field the role fills (npu/forward/include/model.h), or "" when it is not a per-layer weight the loader reads -- fused pairs (qkv_proj, gate_up_proj), and ssm/mtp/vision, which are their own containers or their own code. - container_role_field_conflicts(counts) returns every destination claimed by more than one role, naming all of them. Arms: the two gate senses are different fields; a fused pair and an unknown role say ""; and a table-integrity arm runs every known role at once and requires no conflict -- mutation-checked by pointing up_exps at gate_exps_weight, which fails naming both roles and the field.
…t cannot be a payload (engine#1788, 3c)
The row footprint of this genre is `2*K + 640` bytes, which the writer needs and which decides copy-vs-re-tile. My
first version judged each row against the widths the geometry declares and was WRONG on both packs on this box:
both containers' `lm_head` implies K = 4032, a width neither model's config contains, and 37 of the 1B's rows with
it. So the check now reports the distribution as fact and refuses only the unambiguous case:
- `container_payload_rows` row_bytes -> count, in `info`: 35B {4736: 370, 8704: 1},
1B {4736: 150, 8704: 37} -- two widths per container, the second being the head.
- `container_payload_anomalies` only rows shorter than the 640-byte overhead or with an odd remainder, reported
by name with implied_k = -1.
Arms: the distribution, the malformed pair, and that a BF16 table is not an I8 row. Full suite green.
…'s input (engine#1788, 3c) After the plausible-K attempt produced false positives, this reports the fact the writer needs rather than judging it: for every role, the distinct (shape, dtype) forms it appears in, with counts. On MiniCPM-V 4.7 35B: q_proj [256,8,4736] I8 x10 qkv_proj [256,8,4736] I8 x30 gate_exps [4096,8,4736] I8 x40 down_exps [16384,2,4736] I8 x40 moe_router [2048,256] BF16 x40 embed [248144,2048] BF16 x1 lm_head [7755,8,8704] I8 x1 final_norm [2048] BF16 x1 Every role carries exactly ONE shape, which is what makes the writer a mapping rather than a per-shape decision -- and a role that ever appears in two shapes is reported as two, because that is where the writer must decide rather than copy. Arms: count-per-shape, the dtype in the shape text (a BF16 table is not an I8 row), a two-shape role, and empty input. Full suite green.
…e#1788, 3c step 5) For every role that names a forward field: which source tensor fills it, in what shape, and how many tensors carry that role. On MiniCPM-V 4.7 35B that is 19 fields, 0 ambiguous, and `no_source` listing only what a MoE model lacks by design (dense gate/up/down, shortconv) -- so "this model does not have it" and "something is missing" are different lines instead of one. Three refusals rather than guesses: - a role present in TWO shapes is `ambiguous` and is NOT mapped: that is where a writer must decide, and picking the first would have been the plausible-K mistake again; - a fused pair (`qkv_proj`, `gate_up_proj`) is not one destination field, so it is neither an item nor missing; - `source` is a REPRESENTATIVE with `of: N`, because a role carried per layer (k_proj of 10, moe_router of 40) must not read as a single tensor. Arms: per-layer count, two-shape ambiguity, fused-not-a-field, empty input. Full suite green.
… not read (engine#1788) Measured across every container on this box, three packs could not be finished and two of them shared the SAME six families: `pre_/post_feedforward_layernorm`, `inp_gate` (and its `_prefill` sibling), `per_layer_projection` (and its `_prefill`) and `layer_output_scale`. None has a field in npu/forward/include/model.h, so they are RECOGNISED under one role (`gemma_local`) rather than mapped -- `unassigned` has to keep meaning "nothing knows this", and these are known. gemma3-270m-npu2 36 -> 0 unassigned (36 gemma_local) gemma4-e2b-npu2 319 -> 74 (245 gemma_local) bolt-e4b 382 -> 88 (294 gemma_local) MiniCPM-V 35B 733 -> 733 / 0 (unchanged, no family of this kind) The recogniser runs AFTER the rule table, so a norm the forward does read still wins -- the second arm pins exactly that (`post_attention_layernorm` stays `post_norm`, `input_layernorm` stays `input_norm`). What is left on gemma4/bolt is NOT covered on purpose: `model.layers.N.post_layernorm.weight` (35) and `model.per_layer_model_proj.weight_layerN` (35) need a semantic decision -- is gemma4's `post_layernorm` this forward's post-attention norm? -- and guessing it would be the plausible-K mistake again.
… on the box now classifies completely `post_layernorm` and `per_layer_model_proj` are gemma-local for a MEASURED reason: gemma4-e2b carries BOTH `post_attention_layernorm` (35, classified post_norm) and `post_layernorm` (35), so the second cannot be the forward's post-attention norm; gemma3-270m has no `post_layernorm` at all, which is why only gemma4/bolt were left with it. `rope_freqs` and `per_layer_token_embd` (+ its `.scale`) get their own recognised role, `container_extra`, because they are known for a different reason: the forward only FLAGS a rope-freqs file (model_config.h has_rope_freqs_file) and the per-layer token embedding is read by our ONNX path as `embed_tokens_per_layer` (engine#1725) with no container-path field yet. Recognised, not mapped -- and not "unassigned", which would say nothing knows them. Result, every container on this box: gemma3-270m 237 / 0 gemma4-e2b 709 / 0 bolt-e4b 849 / 0 MiniCPM-V-35B 733 / 0 MiniCPM-V-1B 357 / 0 Suite green.
e71804a to
24ded5a
Compare
The merge order, measured pairwise — and a second correction to my own attributionAfter correcting the stack claim I re-ran the merges individually rather than in one pile, which is what I should have done first. As of #2099 So the collision is between #2187 and #2291, not with the gate — and my previous comment said #2187 conflicted with #2099, which was wrong in the other direction. Both are the two reporting halves of F1 slice 3b/3c and they edit the same regions of the same three files; the gate is innocent in both cases. Recommended order, now that it is measured:
What I got wrong twice today, since it is the useful part: first I claimed the stack was verified when my run had silently omitted #2187 (wrong branch ref), then I claimed #2187 conflicted with #2099 when the conflict was with #2291 (conflict found in a tree that already had #2291 applied). Both were attribution errors from a compound merge, and the fix in both cases is the same: merge pairwise, name each ref, and read which two commits the conflict is between before writing a sentence about it. |
|
Persistent review updated to latest commit 24ded5a |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 24ded5a — with one documentation ask that is now the only thing between this and the merge note it deserves: the body has not caught up with the last three commits.
What the head claims, and where the evidence lives
The last commit's subject is "the last three families on gemma4/bolt, and every container on the box now classifies completely", and the numbers are in the commit bodies:
aa7ed0afa gemma3-270m 36 -> 0 unassigned gemma4-e2b 319 -> 74 bolt-e4b 382 -> 88
24ded5a64 gemma3-270m 237 / 0 gemma4-e2b 709 / 0 bolt-e4b 849 / 0
PUT / TENSORS with zero unassigned on all three, after the previous commit still left 74 and 88. The second commit's reasoning is the part I would keep: "post_layernorm and per_layer_model_proj are gemma-local for a MEASURED reason: gemma4-e2b carries BOTH post_attention_layernorm (35, classified post_norm) and post_layernorm (35), so the second cannot be the forward's post-attention norm — they are different tensors in the same model. gemma3-270m has no post_layernorm at all." A name that looks like a synonym is shown to be a different tensor by the pack's own counts — that is the distinction this whole stack is built on, applied to itself.
But the PR body still opens with the 733-tensor MiniCPM measurement and says nothing about gemma3/gemma4/bolt (grep gemma|bolt|319|382 over the body: no matches). A reviewer reading the description sees a PR about the MoE families; the code now also recognises gemma-local families and three container extras, and the evidence for that is buried in commit messages. For a 30-hour PR whose value is precisely its measurements, that is worth fixing before it lands.
Ask: move the two tables above into the body under the existing measurement section, and name the roles the new commits add (gemma_local, container_extra) with their one-line reasons. Nothing else needs to change.
What I have already verified across this PR's heads, still true at this one
unassignedkept its meaning at every step: recognising a family the forward does not read is information about the forward, not a gap in the classifier, and the arms say so (the families this forward does not read are recognised, "a norm the forward reads is not swallowed by the recogniser").- The controls that make the numbers credible: MiniCPM's count is unchanged where it should be (the fourth row in the previous commit's table), and the 35B's
733 -> 733 / 0matches the container-role census from #2097, so two PRs' measurements agree. - The
plausible_kpost-mortem survives in the header, next to the functions it explains — the rejected design and the measurement that rejected it, where the next person will try it again.
And the ask I have made at every head, which matters more now that the PR is finishing
Preserve the commit bodies through the merge. Five commits, each one a self-contained step whose why is a measured failure of a tempting stronger version: the geometry-declared widths that were wrong on both packs, the two-shape role that must not be mapped, "this model does not have it" versus "something is missing", and now the synonym that is a different tensor. A squash subject keeps none of that, and it is exactly what a future author needs in order not to re-derive it the hard way.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 24ded5a: CHANGES
Reviewed at 24ded5a64b733204df5bafe36acb3e4a61c08ae6, base npu/f1-container-roles-plan (#2099, still open). This is a mergeability verdict, not one on the work.
The planned order no longer holds. The 19:53Z comment measured #2099 → #2187 → #2291, with #2291 to be rebased last. But #2291 merged to main at 19:42:40Z, ten minutes earlier. Against current main, this branch now conflicts in all three of its files:
git merge-tree --write-tree origin/main 24ded5a6
CONFLICT (content): npu/pack_import.cpp, npu/pack_import.h, tests/npu_pack_import_test.cpp
When #2099 merges and this PR retargets to main, it will hit exactly that.
Some of this branch is already on main, through #2277 and #2291:
- the attention-gate rule (
self_attn_gate_proj); - the container-genre import refusal ("importing it needs the container-genre writer");
- the gemma/bolt recognisers;
- the placement work.
What is still unique to #2187 is the reporting half: container_roles_unassigned, role_shapes, payload_rows and write_plan, which main does not have.
Ask: rebase onto current main (after #2099 if it lands first), keep only the reporting fields and their arms, and drop the hunks main now carries. Then the diff is the reporting half alone and can be reviewed on its own. As the 19:53Z comment itself recommends, merge pairwise and name each ref before claiming the stack is clean.
…rs in, in info (engine#1788, 3c residue) The residue of the closed #2187, rebuilt fresh on main rather than resolved out of ten conflict blocks. Two measurements the writer needs before it can decide whether a tensor is one the engine can consume: payload_rows : counts the 3-D I8 tensors by their payload width (the K the fconst tile is built for) role_shapes : which shapes each role appears in Both are measurements over the container, not mappings, so they cannot be wrong about a destination -- which is why they survive while #2187's placement table did not (main has that table from #2285/#2291). Measured on the 35B: payload_rows {4736: 370, 8704: 1} the one 8704 is lm_head, as its shape says role_shapes 22 roles; down_exps -> [16384,2,4736] I8 x 40 Carried on the Plan and emitted from manifest(), because manifest() is where the container report is rendered and it has no container in scope -- that is the whole reason the first attempt at this port produced two functions with no reader and was reverted. The trip through the plan is the fix, not a style choice. # Conflicts: # npu/pack_import.cpp (cherry picked from commit d7a423080781fd78c5c7ab88c4d8ddbef0036d4f)
…rs in, in info (engine#1788, 3c residue) The residue of the closed #2187, rebuilt fresh on main rather than resolved out of ten conflict blocks. Two measurements the writer needs before it can decide whether a tensor is one the engine can consume: payload_rows : counts the 3-D I8 tensors by their payload width (the K the fconst tile is built for) role_shapes : which shapes each role appears in Both are measurements over the container, not mappings, so they cannot be wrong about a destination -- which is why they survive while #2187's placement table did not (main has that table from #2285/#2291). Measured on the 35B: payload_rows {4736: 370, 8704: 1} the one 8704 is lm_head, as its shape says role_shapes 22 roles; down_exps -> [16384,2,4736] I8 x 40 Carried on the Plan and emitted from manifest(), because manifest() is where the container report is rendered and it has no container in scope -- that is the whole reason the first attempt at this port produced two functions with no reader and was reverted. The trip through the plan is the fix, not a style choice. # Conflicts: # npu/pack_import.cpp (cherry picked from commit d7a423080781fd78c5c7ab88c4d8ddbef0036d4f) (cherry picked from commit 3b866fe)
Closing this in favour of a fresh residue PR, and the reason is measured rather than notional.
What happened while it waited: this branch was opened when the writer's mapping and the per-tensor placement did not exist. Both have since merged by other routes — the role→slot table in #2285 and the per-tensor, layer-qualified placement in #2291 (merged 20:0xZ). This branch therefore carries an earlier version of the same table, which is why it now conflicts in ten blocks across the three files, and why the first conflict block is the placement table verbatim.
What is genuinely still unique here, checked against
mainjust now:So the reporting half — a container's I8 payload widths, the shapes each role appears in, and the writer's mapping as a plan it can be wrong about — is not superseded, and it is the part that has been ready twice. It deserves a branch that does not also contain a withdrawn placement table.
The plan: re-open that residue fresh on current
main, carrying only the reporting (the four fields above plus theinfoemission), with the placement table dropped becausemainhas the merged one. That is smaller than this branch's 593 lines and it will not conflict. I would rather spend one clean extraction than resolve ten blocks where six of them are deletions of superseded code — resolving a conflict by hand is exactly where a silent mistake gets in.I am CC-ing the two READY verdicts here into that PR's description rather than discarding them: both reviewed the reporting arms, and those arms are the content being carried forward.