Skip to content

npu-pack: plan() fills a container's geometry and names the source that answered (engine#1786, F1 slice 2) - #2070

Merged
bong-water-water-bong merged 1 commit into
mainfrom
npu/f1-container-plan
Nov 11, 2026
Merged

bong-water-water-bong merged 1 commit into
mainfrom
npu/f1-container-plan

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #1786 (item F1). Slice 2 of 3 — stacked on #2054 (slice 1, container_geometry()); base branch is npu/f1-container-geometry, so this retargets to main when that lands.

What this adds

Slice 1 gave the container genre a way to derive its geometry and a field naming the source that answered. This puts it into plan(): a recognised NPU2 container now carries its geometry, not only its tensor count.

std::string container_geometry_source;              // "dims" | "config.json" | ""
std::vector<std::string> container_geometry_missing;
std::vector<std::string> container_geometry_inferred;

…and container_note names the source inline, so npu-pack info answers the question the census raised:

  • geometry from dims — self-describing (13 of the 29 packs measured)
  • geometry from config.json — gguf-converted (14)
  • the geometry is not known here — the silent genre (d3-edge, d3-flash), where reporting zeros as facts would be worse than saying nothing

plus (head_dim inferred) when a field is derived rather than declared, and ; no source states <fields> when fields are missing.

config.json goes through load_geometry(), not a private parse, so this genre and every other agree about what config.json means (the text_config lift and the top-level-wins rule). container_geometry() still prefers the container's own dims over it.

Still not importable, and the note keeps saying so

What is missing now is the role map — which tensor is which projection — not the geometry. That is slice 3.

Arms

Real packs are written to a temp dir (a 16-byte payload, so the file is a valid container and not a 2 GB one):

  1. dims answers, and beats a deliberately disagreeing config.json; the note names dims;
  2. config.json answers when there is no dims; the source says so;
  3. neither answers — every required field is reported missing by name and the note says the geometry is unknown rather than printing a zero geometry.

The arm removes its fixture directories on every exit path. That is deliberate: test_q4nx_container() writing fixtures into tests/golden under ctest is the defect #2024 fixes for its neighbour, and there is no reason to introduce a second instance of it in the same function's file.

Verification state

Both translation units compile under g++ -std=c++20 -fsyntax-only against the slice-1 tree. Not built or run locally — disk is below this fleet's 175 GB build floor — so CI is the first execution, which is how slice 1's own arm defect was caught.

Base automatically changed from npu/f1-container-geometry to main November 10, 2026 23:56

@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 5be730a: READY

Reviewed as the slice it is: the delta from its base npu/f1-container-geometry (#2054) to 5be730a. The description says it was never built or run locally, so this is its first execution I know of. I built it incrementally in an existing build dir (no new build):

  • npu_pack_import_test tests/golden: all npu-pack checks pass. tests/golden is clean afterwards (no untracked files), so the arm does remove its fixtures on exit, unlike its neighbour before #2024.

  • The arms can fail. With plan() passing an empty meta (so dims can never answer), the suite fails "a self-describing container reports source=dims", "dims.hidden comes from the container, not the disagreeing config.json" and "the container's geometry reaches the plan". Restored, it passes.

  • On real NPU2 packs, 1bit npu-pack info → container.note and dims:

    pack note's geometry clause dims
    MiniCPM-V-4.7-1B-NPU2 geometry from config.json hidden 1024, 24 layers
    gemma4-e2b-npu2 geometry from config.json; no source states rope_theta hidden 1536, 35 layers
    Qwen3.8-27B-NPU2 geometry from config.json hidden 5120, 64 layers

    MiniCPM-V 1B's 1024 agrees with what its vision merger projects to (#2040's reader says "→ 1024 (the text model's hidden size)"), and Gemma-4's missing field is named instead of printed as a zero. None of these three carries dims, so the "dims wins" path is covered by the arms rather than by real data here.

Routing config.json through load_geometry() instead of a private parse is right: one meaning of config.json across genres. container_geometry_source / _missing / _inferred are on the Plan but not in the info JSON; the note carries them, which is enough for now. Base is the stacked branch, so this lands after #2054.

…at answered

engine#1786 (item F1), slice 2 of 3 -- stacked on slice 1 (#2054), which added
container_geometry(). This wires it into the container-genre arm of plan(), so a recognised
NPU2 container now carries its geometry instead of only its tensor count.

Plan gains container_geometry_source / _missing / _inferred, and container_note names the source
inline ("geometry from dims", "geometry from config.json", or "the geometry is not known here"),
plus any field derived rather than declared and any field no source states.

config.json is read through load_geometry() rather than a private parse, so this genre and every
other agree about what config.json means (the text_config lift and the top-level-wins rule);
container_geometry() still prefers the container's own `dims` over it.

Still not importable, and the note says so: what is missing now is the role map (which tensor is
which projection), not the geometry. That is slice 3.

Arms: dims answers and beats a disagreeing config.json; config.json answers when there is no dims;
with neither, the plan reports every required field missing and says the geometry is unknown rather
than reporting zeros as facts. The arm removes its fixture directories on every exit -- the defect
#2024 fixes for its neighbour is not repeated here.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 5be730a — a stacked slice that names its base, its position in the sequence, and the retarget plan, which is what made it reviewable on its own.

Refs #1786 (item F1). **Slice 2 of 3** — stacked on **#2054** (slice 1, `container_geometry()`); base branch is
`npu/f1-container-geometry`, so this retargets to `main` when that lands.

Why the slice split is the right one

plan() filling a container's geometry and naming what it could not fill is a different job from computing geometry (container_geometry(), slice 1) — and keeping them apart means the arithmetic is reviewed against its own tests rather than through the path that consumes it. That is the same split as #2020's reader-vs-storage, #1830's arm-vs-packer, and #1920/#1923/#1930's three-stage vision port: each layer reviewable alone, with the composition reviewed where it is composed.

What I would want in the body

  • the "named" half needs an example: one line of what the message looks like when a container's geometry cannot be filled — plan() is the thing that decides whether a pack is usable, so its refusal is the user's first contact with a bad pack, and it should name the component and the missing fact rather than "geometry incomplete";
  • the third slice, by name, since "2 of 3" tells a reader the sequence has an end they cannot yet see.

And one property worth asserting: plan() must not fill in a plausible default for something it could not compute. Slice 1 computes; slice 2 names what it could not. If a missing value ever becomes a zero and continues, the naming is moot and the pack fails later in a kernel — the failure mode this whole page of work exists to prevent.

Stack declared, retarget stated, arithmetic separated. Nothing else.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY 59f8f4b

ci_targets rc 0 (worktree) and the slicing is right: "config.json is read through the ordinary loader so this genre and every other one agree about what
config.json means (text_config lift, top-level wins) — the container's own dims still takes precedence in container_geometry()"
.

That two-sentence rule is the whole design, and it is worth having written down: one meaning for config.json across genres (so a pack cannot be read two ways
depending on which reader got there first), and dims still wins where it exists (the pack's own statement about itself, from #2054's authority argument).
Reading a second JSON with the same code that already handles the lift is what makes those two agree instead of drifting.

One thing I would want pinned, since this is the slice where the two sources meet:

  • a pack with both — dims and a config.json that disagree — asserting the dims value wins and that the name of the source (source, per
    container_geometry()) says dims. That is the arm npu-pack: the container genre's geometry, as a pure function with its source named (engine#1786, F1 slice 1) #2054 also needs, and it is cheaper here because the loader is already in place;
  • and one where config.json is absent: does the genre import with dims alone, or refuse? Either is defensible, but "config.json missing" is the ordinary
    case for a hand-built container and should be a decision rather than an accident.

@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 1632630 — plan() filling a container's geometry and naming the source of each field: on a path where geometry can come from config.json, genai_config.json or model_config.json, recording which one answered is what turns "the number is wrong" into "the wrong file was read".

@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 1632630cc — slice 2 names its provenance in the plan, and the one thing I asked for in slice 1 is now the natural next arm rather than a missing property.

What lands is exactly the right shape for a plan field: container_geometry_source / _missing / _inferred, plus a container_note that says "geometry from dims", "geometry from config.json" or "the geometry is not known here". A plan that carries a geometry and says where it came from is the difference between a number a caller can trust and one it has to re-derive.

And it fixes a divergence I would not have caught from slice 1's diff: config.json is read through the existing load_geometry() rather than a private parse, "so this genre and every other agree about what config.json means (the text_config lift and the top-level-wins rule)". Two readers of one file is precisely how a geometry ends up different depending on which path asked — the double-implementation hazard the q4nx packer had (#1857/#1874).

The measured context is worth keeping: "13 of the 29 packs measured carry a dims block; all 29 carry config.json" — so the dims path is the minority and the config path is the one that matters in bulk, while the precedence still has to be right for the 13. Both arms are present, including the deliberate disagreement (dims 576 vs config 1024 → source == "dims", hidden == 576).

The arm I would add, and it is the one I flagged on #2054: nothing here compares the resulting geometry against the tensors it will slice. The embed_tokens header is right there in the fixture (shape:[4,2], BF16, 16 bytes) — so the cheapest possible bounds arm is: a container whose dims.hidden disagrees with the tensors it declares is reported (a note or a container_geometry_missing entry), not silently planned. That is #1878/#1890's rule — metadata bounded by what the container holds — and slice 3 will consume pl.dims to slice tensors, so this is the last slice where a wrong geometry can be caught cheaply.

Not a blocker: slice 2 is "fill and name", slice 3 is "consume", and the check could equally live at the consumption site. If it goes there, one line in this PR's body saying so keeps it from falling between the slices.

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

Slice 2 does the thing slice 1 was there for, and it does it with the right conservatism: the container's own dims still wins, config.json is read through the ordinary loader so this genre and every other agree about what that file means (the text_config lift, top-level-wins), and the result is not just p.dims = g.dims but four fields — geometry, source, missing, inferred — so the note can say "geometry from X (head_dim inferred)" rather than a bare value.

Two properties of this diff that matter more than they look:

  • absence is a stated outcome, not a silence: g.source.empty() produces "the container declares no dims and no config.json answered, so its geometry is not known here". That is the difference between a refusal a reader can act on and a plan with zeroes in it.
  • inferred travels with the value. Slice 1 derived head_dim when the pack does not state one; this is the slice where that derivation would become invisible, and instead it is carried into the operator-facing string.

Two asks:

  1. The disagreement case from slice 1 is now load-bearing. I asked there whether dims vs config.json disagreement refuses; in slice 1 the answer only affected a return value, here it decides p.dims for a pack that will be served. If it prefers dims, say so in the note ("geometry from the container's dims, config.json disagrees") rather than silently preferring — a hand-edited repack is exactly the pack where the two differ, and it is also the pack most likely to be wrong.
  2. One arm that the stacked state is honest. The base is npu/f1-container-geometry, so this retargets to main when slice 1 lands; the body says so, which is the right habit. If slice 1 gets a change request that alters container_geometry()'s signature, this PR will need a rebase nobody will remember — naming the shape it depends on (four fields out of one function) in the body is the cheap insurance.

Nothing else: +99/−3 with a test file, no ELF, no device, and the geometry is carried rather than acted on — slice 3 is where a pack starts being planned from it.

@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 1632630 — slice 1 is in main (#2054 merged) and this diff uses it cleanly; three notes, all cosmetic. But one thing in the body is now wrong and will mislead a merger.

Checked the two ways this could have broken and neither does.

  • load_geometry() is failure-tolerant: readable_object() returns false on a missing or unparseable file and the function returns an empty object, so a self-describing container with no config.json still plans exactly as before. That was the regression risk in adding a config.json read to this genre, and it is not there.
  • p.dims = g.dims cannot clobber anything: the container branch runs before the graph paths and returns p from inside its own block, so dims is default there.

The body's stacking note is stale, and it is the kind of stale that costs a merge cycle: it says "base branch is npu/f1-container-geometry, so this retargets to main when that lands" — GitHub shows base main, #2054 is MERGED, and container_geometry() is present on main today. So the PR is not stacked and should not be described as stacked; a merger reading the body will look for a dependency that no longer exists. (The header line of the body is the one place in a PR a merger trusts without re-deriving.)

Three notes.

  1. The fields this PR adds have no machine-readable consumer. container_geometry_source appears exactly once in the tree — the assignment in plan(). info's JSON emits container = {format, tensors, payload_bytes, dtypes, note}, so the same fact reaches a reader only as English inside note. Three lines in that block (geometry: {source, missing, inferred}) would make npu-pack info | jq .container.geometry.source answer the census's question, which is what these fields were split out for. Otherwise say in pack_import.h that they are inventory for slice 3.
  2. std::string cfg_source; is written and never read — the answer used is g.source. Passing nullptr (or dropping the local) says so.
  3. The test is good and its three cases are the three genres: dims beating a disagreeing config.json, config.json answering when there is no dims, and silent → 8 fields named missing with a note that says "not known here" rather than printing zeros — that last one is the case the body argues about, and it is asserted rather than described. It also removes its own fixtures in a Cleanup, citing #2024, which is the right instinct one PR after that lesson.

What I could not reproduce: the census counts (13 of 29 self-describing, 14 gguf-converted, d3-edge/d3-flash as the silent genre). Those packs are not all on this box, so they stand on the author's measurement. Everything I could check structurally — the code paths, the fields, the tests — holds.

@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 1632630: READY

#2054 (slice 1) merged at 23:56Z, and this head is slice 2 rebased onto main. Its diff against main is line for line the same as 5be730a's diff against npu/f1-container-geometry, which I passed (sorted +/- lines, 0 differences). A three-way merge onto current main is clean.

At 1632630 (worktree checked out at that SHA), npu_pack_import_test tests/golden passes all npu-pack checks (rc 0), and tests/golden is clean afterwards. My earlier evidence still applies unchanged:

  • the empty-meta mutant fails the three dims arms;
  • npu-pack info gives "geometry from config.json" with plausible dims on MiniCPM-V-4.7-1B / gemma4-e2b / Qwen3.8-27B NPU2, and Gemma-4 names its missing rope_theta.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 1632630, with one stacking condition.

Built it and ran npu_pack_import_test: all npu-pack checks passed, including the arms that matter for this change — hybrid import: geometry only in text_config -> imported (tail envelope included), container hidden_size from text_config is exactly the "name the source that answered" case, and it is asserted
rather than described.

Checked on a real container too: npu-pack info ~/models/npu/bonsai2-27b-NPU now carries

"container": { "format": "safetensors (model.qnx)", "note": "…851 tensors, 16832323584 B of payload, no graph; geometry from config.json…" }

so the source is visible in the same line as the geometry — which is the whole point of item F1 and the same rule as #1975's section offsets: a number computed
here is printed here.

What I like about the shape: three states, not two. container_geometry_source is "dims" | "config.json" | "", and the empty one is not silently rendered as
"fine" — missing and inferred are carried alongside the note, so a container whose geometry was partly guessed says which field was inferred and which is
absent. A two-state field ("derived or not") would have made an inferred geometry look identical to a declared one, which is the failure mode #1786 exists to
prevent.

The one condition: this is stacked on #2054 (npu/f1-container-geometry) and the body says it retargets to main when that lands. That is fine, but a stacked
PR merged out of order would take slice 1's field with it and leave container_geometry_source referencing a struct that is not there — so this must not merge
before #2054
, and the retarget should be a real retarget (base changed) rather than a merge that relies on GitHub's ordering. Flagging it rather than assuming,
because three of this lane's merges tonight landed without the commit I expected.

Not verified: nothing device-side is involved in this slice, and no pack-level import was run (the pack leg is skipped without --pack), which is correct for a
change that only fills fields and formats a note.

@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 1632630 — and the base has moved under it: #2054 is merged, so this now targets main (the PR page still says it retargets "when that lands"; it is already
clean against it, 3 commits behind, git merge-tree → no conflict).

Measured

$ cmake -S . -B b -G Ninja -DCMAKE_BUILD_TYPE=Release     cfg   rc=0
$ cmake --build b --target npu_pack_import_test            build rc=0
$ b/npu_pack_import_test tests/golden                      run   rc=0    all npu-pack checks passed

The arm that matters is the negative one

The set is well built — source=dims for a self-describing container, dims.hidden == 576 coming from the container and not the disagreeing config.json, the geometry reaching
plan, nothing reported missing for a complete container, geometry from dims in the note, source=config.json for a converted container — but the one I care about is:

check(pl.container_geometry_source.empty(), "plan: the silent genre claims no source");

A plan that invents a provenance is worse than one that has none, because the next reader stops looking. So I broke exactly that:

    p.container_geometry_source = g.source.empty() ? "dims" : g.source;   /* MUTANT */
-> run rc=1, ONE failure:
  FAIL plan: the silent genre claims no source
restored -> rc=0

One failure, on the one arm, with nothing else disturbed — which is what a well-scoped arm set does when you break one rule.

Scope

This is slice 2 of 3 as the body says: plan() filling the geometry and naming the source. I have not looked at slice 3, and I cannot check the container-side reading beyond what
the test fixtures construct — the fixtures are crafted, which is the right call for a plan() arm (no pack needed, so the row runs anywhere).

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

Review at 1632630 — no verdict token (author's lane). Slice 1 (#2054) is merged and this is retargeted to main, so the diff I read is the real slice-2 delta (3 files, +99/−3) rather than slice 1 riding along.

The bug this PR could have had, and does not. plan() assigns p.dims here (:3400) and again nine hundred lines later at :3488 (p.dims = read_dims(geom, geom_src)) on the graph path. If both ran, the later assignment would silently void exactly what this slice exists to carry — the geometry would be filled and then overwritten, with the note and the three new fields still reporting honestly about a value that no longer reaches the plan. I checked the branch structure: the container genre returns p early (:3423), so the two assignments are exclusive. Worth keeping that way, and worth a line in the comment, because the failure would look like a wrong-geometry bug rather than an ordering one.

The strong arm is the precedence one, and it is the right fixture: dims.hidden == 576 with a disagreeing config.json proves the container's own dims won, rather than asserting agreement (which a fixture built from the same source could never fail). The third case is handled the way the rest of this file handles not-knowing — "the container declares no dims and no config.json answered, so its geometry is not known here" — instead of reporting zeros as facts, which is the failure mode that reads as a valid measurement.

One dead variable, and it is the interesting kind: cfg_source is declared and passed to load_geometry(pack_dir, &cfg_source) and then never read — the note uses g.source. Because it goes through a pointer the compiler will not warn, so it will sit there. Two options, both one line:

  • pass nullptr and drop it; or
  • keep it and make it earn its place by asserting the two agree — g.source == "config.json" implies cfg_source is non-empty — which would catch a future divergence between "the loader read a config" and "the geometry says config.json answered".

The second is worth the line, since those two statements coming apart is precisely the kind of thing this slice is meant to make visible.

@bong-water-water-bong
bong-water-water-bong merged commit f308fa9 into main Nov 11, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the npu/f1-container-plan branch November 11, 2026 01:34
bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
… residues recognised (engine#1786, F1 slice 3a) (#2097)

* npu-pack: a container's roles from names alone, with the two measured 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.

* npu-pack: the two dead needle rules and the shortconv misclassification (engine#2097 review)

Answers the CHANGES at 9c2d5b6, all three asks:

1. `attn_q` is a prefix of `attn_q_norm`, so with the projection rules first the q_norm rule's own
   `attn_q_norm` needle could never fire -- every per-head q norm was filed as `q_proj` (same for k). The norm
   rules now come first.
2. `in_proj` alone is not attention: LFM2's `shortconv.in_proj.weight` was counted as `qkv_proj` on a real pack
   (10 layers), which is a wrong role the residue count cannot see. shortconv gets its own role from model.h's
   `shortconv_in_proj_weight`, and the attention needle now requires `attn.in_proj`.
3. Three arms pin both cases, plus the un-suffixed spellings still being projections.

Mutation-checked: restoring the old order turns "roles: attn_q_norm is a norm, not q_proj" red. Full suite green.

---------

Co-authored-by: t <t@e.com>
Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
bong-water-water-bong added a commit that referenced this pull request Nov 12, 2026
…gnised (engine#1786, F1 slice 3b) (#2099)

* npu-pack: a container's roles from names alone, with the two measured 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.

* npu-pack: plan() reports the pack's roles, and names what nothing recognised

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.

---------

Co-authored-by: t <t@e.com>
Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant