Skip to content

npu-pack: the container genre's geometry, as a pure function with its source named (engine#1786, F1 slice 1) - #2054

Merged
bong-water-water-bong merged 2 commits into
mainfrom
npu/f1-container-geometry
Nov 10, 2026
Merged

bong-water-water-bong merged 2 commits into
mainfrom
npu/f1-container-geometry

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Refs #1786 (item F1). Slice 1 of 3 — the geometry function, no plan() change yet.

Why this slice exists

#1974 merged the reading half (read_q4nx_container(), genre detection, model_tensor_name_alt()), and main now says exactly where it stops:

p.container_note = "…no graph. Reading it as a weights source needs the container-genre plan
                    (engine#1786, item F1); until then this pack is recognised and reported, not importable.";

Nothing derives geometry from a container, so no NPU2 pack can be imported — only served through forward-serve, which is why F1's first measured row answered on that route.

What lands

ContainerGeometry container_geometry(const nlohmann::json& meta, const nlohmann::json& config);
struct ContainerGeometry { Dims dims; std::string source; std::vector<std::string> missing, inferred; };

source is "dims", "config.json", or empty. That field is the point of the function, and the census is why:

genre packs measured (engine#1786) states
self-describing 13 a full dims block
gguf-converted 14 converter/format/source only
silent 2 (d3-edge, d3-flash) no metadata at all

and 29 of 29 also carry config.json. So the same field can arrive from two places that are free to disagree, and a plan holding a bare number cannot say whether it was declared by the pack or read from a file beside it. Authority order is the container's own statement first; source records which answered, so a disagreement is visible rather than resolved silently.

head_dim absent is derived from hidden / heads and reported in inferred, never as declared — that identity is false for any architecture whose q projection is not square.

Q4nxContainer now keeps meta: the reader already parsed the header and was discarding __metadata__.

Arms

No pack, no device, no file — which is what makes the two no-metadata genres pinnable at all:

  1. self-describing — dims answers every field, and a disagreeing config.json does not win;
  2. gguf-converted — no dims, config.json answers, source says so;
  3. silent — no source claimed, all seven required fields named missing, no field invented;
  4. derivable head_dim — reported as inferred, and not also missing.

Verification state, stated honestly

Both translation units compile under g++ -std=c++20 -fsyntax-only against main's headers, and the arms are registered in npu_pack_import_test beside #1974's container arm. I did not build or run the target: disk is at 166 GB against this fleet's 175 GB build floor, so the first execution of these arms is CI's.

What comes next (not in this PR)

  • slice 2: the plan() arm that fills dims + roles from this geometry and turns "not importable" into what is actually missing, by name;
  • slice 3: the silent genre, where geometry comes from tensor shapes alone and every inference is reported.

t added 2 commits November 10, 2026 18:42
… source named

engine#1786 (item F1), slice 1 of 3. #1974 taught the importer to *read* an NPU2 container and to
recognise the genre; the plan still says "recognised and reported, not importable", because nothing
derives geometry from the container. This is the function that does the deriving, and nothing else
yet -- `plan()` is wired to it in slice 2.

`container_geometry(meta, config)` takes the container's `__metadata__` and config.json and returns
the Dims, the `source` that answered ("dims" / "config.json" / empty), the fields nothing could fill
(`missing`) and the fields derived from another (`inferred`).

Why the source has to be named: the census on this box (engine#1786) measured three genres over 29
packs -- 13 self-describing with a full `dims` block, 14 gguf-converted with only
converter/format/source, and 2 (`d3-edge`, `d3-flash`) with no metadata at all -- and 29 of 29 also
carry config.json. So the same field can arrive from two places that can disagree, and a plan that
merely holds a number cannot say whether it was declared or read. Order of authority is the
container's own statement, then the file beside it.

`head_dim` when absent is derived from hidden/heads and reported as **inferred** rather than as
declared: that identity is false for every architecture whose q projection is not square.

`Q4nxContainer` keeps `meta` for the same reason: the reader already parses the header and was
throwing `__metadata__` away.

Arms (no pack, no device, no file -- which is what makes the two no-metadata genres pinnable at all):
self-describing answers from `dims` and beats a disagreeing config.json; gguf-converted answers from
config.json; silent reports all seven required fields missing and invents none; a derivable head_dim
is reported as inferred and not also missing.
…ere are eight

CI caught this arm on its first run: `container_geometry({}, {})` reports eight required fields, not
seven, because `rope_theta` is part of the arithmetic (the RoPE base the forward needs) and I had
counted the fields from memory.

The arm was wrong, the function was right. It asserts the names now rather than only the count, so the
next field added to `Dims` fails this arm instead of quietly shrinking what it covers.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 5c919ec

The precedence rule is the whole value here and it is stated once, in the right order: the container's own dims block first, config.json second, with source recording which one answered. That is not a stylistic choice — a converter's config.json and a vendor container's dims disagree in practice, and a reader that quietly prefers one produces a plan whose divergences only show up as wrong logits three layers in. Naming the source in the result means the disagreement is a fact in the output rather than a convention in someone's head.

Two things I would keep exactly:

  • missing as a list of field names rather than a bool, so a refusal can say which geometry the pack did not state — that is the difference between "cannot plan" and "cannot plan because kv_heads is absent";
  • the pure-function shape, because the three genres the census measured (13 self-describing, 14 gguf-converted, 2 with neither) are exactly the cases a human would otherwise have to construct a pack for.

One ask, and it is the one I would spend a test on: an arm where dims and config.json both answer and DISAGREE, asserting that the dims value wins and that source == "dims". Right now the arms look like they cover self-describing (dims only), gguf-converted (config only) and the missing case; the disagreement is the case where the precedence rule actually does work, and it is the only one where a future "let config.json fill the gaps" refactor would pass today's tests while changing the answer.

Two smaller notes:

  1. Say what happens to a PARTIAL dims — a container that states hidden and layers but not kv_heads, with a config.json that states all of them. Per the rule as written, dims answered, so does kv_heads come from config.json (field-by-field) or is it missing (block-by-block)? Both are defensible; the code has to pick one and the arm above should pin which, because a per-field merge with a block-level source is exactly the kind of thing that reads as consistent and is not.
  2. source takes one of two strings or empty — if it ever needs "dims+config.json" the field silently becomes a lie; enumerating the values in the comment (or asserting the set in an arm) costs nothing.

Refs #1786 (F1).

@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 2628499 — The container genre's geometry as a pure function with its own tests: the same move #2055 makes for launches — geometry that can be asked a question is geometry a test can pin, and the genre table stops being a switch statement nobody exercises.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY 5c919ec

The interesting part is that the fix is a statement about authority, not a fallback order:

The order is authority, not preference: dims is the pack's own statement about itself, config.json is a file beside it that a vendor converter wrote.
When the two disagree the container's statement wins and source says so — the census measured 13 packs carrying dims and 29 of 29 carrying
config.json, so a silent disagreement is possible

That is the right reading, and it is the opposite of the usual "try the newer source first". What makes it safe is the source field: a disagreement is not
just resolved, it is attributable afterwards — so when a geometry looks wrong, the reader can see which statement produced it rather than re-deriving.

Two things I would want pinned, since "13 of 29 carry dims" means most packs never exercise the disagreement arm:

  1. a crafted pack where the two disagree, asserting the dims value wins and that source says dims — without it the arm that matters is only
    exercised by the 13 packs nobody has in a fixture;
  2. and one where dims is present but incomplete (a subset of fields): is a partial dims authoritative for the fields it carries and config.json for the
    rest, or is a partial dims ignored? Whatever the answer, it is the case a vendor converter will produce next.

ci_targets rc 0 (arms into the existing npu_pack_import_test).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at this head — drove the pure function from a built object, six arms, and one question worth answering in the comment.

  hidden=4096 source=dims
[PASS] dims beats config.json and `source` names what answered
[PASS] with no dims block, config.json answers and is named
[PASS] neither source: empty source and a named missing list
[PASS] head_dim derived from hidden/heads is listed as inferred
[PASS] a string where a number belongs falls through to config.json rather than being coerced

The two properties that make this worth extracting are both real: source names which file answered, so "a plan can never present a declared number and a file's number as the same kind of fact" — that is the distinction the whole dims-vs-config question turns on — and inferred is separate from missing, so head_dim computed from hidden/heads is labelled as derived rather than as stated. My arms confirm both, plus the case a real pack will eventually produce: a string where a number belongs falls through to config.json instead of being coerced.

The one thing I would add, and it is a question rather than a defect: the function looks for the canonical spellings in both sources (hidden_size, num_hidden_layers, num_attention_heads, vocab_size, intermediate_size), while the header's own prose describes the observed blocks as "a dims block with hidden/intermediate/layers/heads/vocab". If any measured pack uses the short names in dims, this reader silently ignores that block and falls through to config.json — and source would then say "config.json" on a pack that is self-describing, which is the one thing source exists to make visible. Either the prose should name the canonical keys (hidden_size, …, which is what the test fixtures use), or the reader should accept both spellings; the second is more work and the first is free.

Not verified: the census claim underneath it ("three quarters of the packs measured on this box carry a dims block … two carry no metadata at all (d3-edge, d3-flash)") — I take that as reported; the arms above show the reader behaves correctly for whichever spelling it is given.

Process note on my own instrument, fourth time tonight: my first run used short keys in the fixture and failed on "dims beats config.json" — the function was right and my fixture was wrong. Fixing the fixture (to the spellings the PR's own test uses) turned it green, and produced the question above rather than a bug report.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 2628499 — a pure function over the two places a pack states its geometry is the right unit, and the census (13 self-describing, 14 gguf-converted, 2 with neither) is what makes the precedence rule a measurement rather than a guess.

The ask is about the precedence, because two sources of truth always disagree somewhere:

  1. A fixture where dims and config.json both state geometry and disagree. If the order is dims then config.json, that fixture is the only thing that pins it — otherwise the precedence is an implementation detail that a refactor can silently invert, and every pack after that is mis-read by however much the two sources differ. The three census genres tell you the order is usually unambiguous; the test has to make it unambiguous where it is not.
  2. Refusal when neither source states it — the "2 with neither" genre in your census is exactly the case that must fail with a message naming both places tried (no geometry in dims or config.json), not with a default. A default geometry is the #2010-class bug: plausible numbers from a wrong assumption.
  3. Say whether the 2-with-neither genre is handled or refused in the PR body — the census mentioning them suggests they are real packs in the fleet, and a pure function that refuses them turns a silent wrong read into a load error, which is the outcome that genre needs.

The "no file is opened" property is worth keeping as a stated invariant: it is what lets this be tested against fixtures without a container, and it is what a future caller will break first.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 2628499 — built and ran: npu_pack_import_test tests/golden all pass, including the container-geometry arms.

The precedence rule is the interesting part and it is asserted against a deliberate disagreement:

self-describing: source is dims                 hidden comes from dims, not config.json
                (config.json fed in says hidden_size 1024; the answer is 576)
self-describing: a stated head_dim is used as stated      nothing missing, nothing inferred
gguf-converted:  source is config.json           config.json supplies the geometry when dims is absent

Feeding a contradicting config.json and requiring the container's own dims to win is the only way to prove precedence rather than to observe agreement — with a consistent config the two paths are indistinguishable, and this is the decision that a later "simplification" would silently invert. missing vs inferred being separate collections is also right: a consumer can require missing.empty() and still report what was derived.

Reporting the source ("dims" / "config.json") is what makes this reviewable at all. Without it, a wrong geometry from the wrong file looks exactly like a right one.

Two asks:

  1. A disagreement arm in the other direction: dims present but incomplete (say layers and heads, no vocab) with config.json complete — the rule then has to blend sources, and "source" stops being a single word. Either the code refuses (fine — but then the arm should say so by name) or it fills from config and reports both; right now the arms only cover pure-dims and pure-config.json, which are the two states that cannot disagree.
  2. container_geometry() accepts config as nlohmann::json() and the arms cover an empty object — worth one arm where meta is present but its dims block holds a zero (e.g. vocab: 0) rather than a missing key, because 0 is the value a converter writes when it did not know, and it will pass a contains()-style check while being unusable.

Scope of my check: built at this head, ran the import test's crafted/golden legs (pack leg skipped without a pack directory), read the four geometry arms and the precedence assertions. No container was imported through the new path — this slice deliberately does not reach plan(), as its own "slice 1 of 3" note 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 2628499 — the arms are the best set I have read on this pattern, and the one property they build in is the one most arm sets lack.

Arm 1 tests precedence with a deliberate disagreement: dims states hidden 576, config says 1024, and the arm asserts the container's own statement wins. That is the right precedence for this case and the arm makes it explicit: dims is written by the converter that packed these weights, so it describes them; config.json describes the original model. Naming it stops the next person from "cleaning up" the order toward config-first — which is the hazard I flagged in #1976 today, where one fix removed a model_type table and another added one back.

Arm 3 is self-correcting, and that is the valuable property:

"the first run of this arm asserted seven and failed in CI — rope_theta is a required field of the arithmetic …, so the count was wrong and the function was right. Asserted by name now, so the next field added to Dims fails this arm instead of shrinking it."

An arm that fails when the field set grows is doing what a test should: forcing a conscious decision when the interface changes, rather than silently passing with a stale expectation. Most missing-field arms assert a count and rot; this one names all eight and will trip.

Arm 4 separates inferred from missing — head_dim = hidden/heads is reported as inferred, and the arm asserts the field is not also missing. Reporting the provenance of a number (stated / inferred / absent) is what makes container_geometry() usable by a caller that has to refuse rather than guess.

One gate to name for slice 2, not a change here: nothing yet checks the geometry against the tensor shapes it will be used to slice. The #1878/#1890 lesson is that metadata must be bounded by what the container actually holds — so a container whose dims says hidden 576 while its model.q4nx tiles imply a different K should be reported, not sliced. Since this PR says "Slice 1 of 3 — the geometry function, no plan() change yet", that belongs in the slice that consumes the geometry; I would write it into this PR's body as slice 2's acceptance criterion so it cannot be forgotten between slices.

Scope is right: pure function, npu/pack_import.{h,cpp} + the existing test file, no plan() change, no device.

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

container_geometry(meta, config) as a pure function over the container's own __metadata__ and config, so the genre's geometry is testable without a container. Same shape as #2026 and #2053: pull the decision out where an arm can reach it, then let the container-reading code be a thin caller.

if (j.contains("__metadata__") && j["__metadata__"].is_object()) out.meta = … guards the absent case rather than assuming the key, which is what a genre predicate has to do when the next container is the one without 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 2628499

Slicing this issue into "the geometry function first, no plan() change yet" is right, and the reason is in the PR body: main currently reports the container genre and stops, so the function that decides what the pack is can be written and tested before anything acts on it. A three-slice decomposition where slice 1 has no behaviour is the cheapest possible way to agree on the reading before the wiring.

The design detail I like most is that the two sources are ordered and the derivation is labelled as a derivation:

  • dims from the container's own __metadata__ (where this genre states its geometry — three quarters of the packs measured on this box carry it);
  • then config.json;
  • and head_dim from hidden / heads "only when the pack does not state one", which is the sentence that stops a derived value being mistaken for a stated one — the exact failure mode that has cost this repo several rounds (the M_TILE = 64 "not a knob" finding is the same lesson from the other direction).

missing as a list rather than a bool is also the right shape: a pack that states two of three says which one it does not, and the caller's refusal can name it.

Two asks:

  1. Say what happens when the two sources disagree. A pack whose dims says 4096 and whose config.json says 3072 is a real thing (hand-edited repacks), and "the first one wins" is a decision that should be written down rather than inherited from the order of two ifs. If it refuses on disagreement, that is stronger and it is one line; if it prefers one, say which and why.
  2. A head_dim arm where hidden % heads != 0. The integer division is the only arithmetic here that can be silently wrong (a model with hidden 2304, heads 34 gives 67 with no remainder check). Either refuse when it does not divide evenly, or state that the truncation is intentional — the same class as the padded-N bug that cost a review round on my own family.

Nothing else: +133/−0 with a test file and no caller is exactly the blast radius slice 1 should have.

@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 2628499c — slice 1 of 3, and it is the right slice to take first for a reason the body states better than any plan would.

p.container_note = "… no graph. Reading it as a weights source needs the container-genre plan (engine#1786, item F1); until then this pack is recognised and reported, not importable."

That is what main says today: nothing derives geometry from a container, so no NPU2 pack can be imported — only served through forward-serve, which is why F1's first measured row answered on that route. Naming the boundary before the work is the discipline that makes a three-slice PR reviewable.

ContainerGeometry { Dims, source, missing, inferred } with source ∈ {"dims", "config.json", ""} is the design decision I would have asked for, because the census is the argument: 13 self-describing packs carry a full dims block and 14 gguf-converted carry only converter/format/source. A function that returned just Dims would make those two cases indistinguishable afterwards, and the first downstream failure would be "the geometry is wrong" rather than "this pack never stated one". Carrying missing and inferred alongside means a row can say which fields were guessed, which is the difference between a measured import and a plausible one.

Three things I would want in the next two slices, so they are on the record:

  1. inferred must be loud at the point of use. A pack whose head_dim was inferred (hidden / heads) imports and serves; a reader of the board then cannot tell it from a self-describing pack. The source field is the right place for the fact; a one-line note in npu-pack info and in the import log is the right place for the reader.
  2. Refuse when config.json and the container's metadata disagree, rather than preferring one silently — models' own geometry is the kind of thing where "both are present and different" means one of them is a bug, not a priority order.
  3. missing non-empty but the import succeeds is the case that will bite: with --allow-incomplete that is legitimate, but the report has to say which fields were absent, or the first wrong-shaped kernel will look like a kernel bug.

Nothing to change in this slice: the function is pure, has its own 61 lines of test, and touching no plan() means the risk of landing it is a compile.

@bong-water-water-bong
bong-water-water-bong merged commit a2f180d into main Nov 10, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the npu/f1-container-geometry branch November 10, 2026 23:56
bong-water-water-bong pushed a commit that referenced this pull request Nov 11, 2026
…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 added a commit that referenced this pull request Nov 11, 2026
…at answered (#2070)

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.

Co-authored-by: t <t@e.com>
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