Repository navigation
npu-pack: the placement answered per tensor, with that tensor own layer type (engine#1788, 3c) - #2290
Conversation
…er type (engine#1788, 3c) An import iterates tensors, not roles, so the per-tensor decision is the one the writer needs: read the tensor own layer, take THAT layer declared type, and ask the table for a destination. The arm that matters is the pair -- the same role lands in two different places on two layers -- and the mutant that uses layer 0 type everywhere fails it: FAIL placement: the same role on the FULL-attention layer has no destination FAIL placement: and says why FAIL placement: unplaced counts the two decisions, not the vocabulary gap Three counts kept apart on purpose: placed, unplaced (a decision made deliberately, with its reason) and no_role (a vocabulary gap). A row per tensor, so nothing is silently skipped, and placed+unplaced+no_role == tensors is asserted -- the accounting that makes an import trustworthy.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
VERDICT: READY at "A writer does not iterate roles, it iterates tensors"That is the whole justification, and it is a fact about the loop rather than about the model: One PlacementRow per tensor: tensor, layer, layer_type, role, slot, whyCarrying The two refusal examples are the right two
Both produce an empty slot, and the parenthetical shows they are different kinds of empty — which matters because the repairs differ (measure a split vs extend the classifier). A single "unplaced" bucket would lose that. One asksay what the writer does with an empty slot — skip the tensor (leaving the destination unfilled, which the engine must then refuse), or abort the import? Those are different guarantees: abort is safe and blocks a whole pack on one unknown tensor; skip is permissive and defers the failure to load time, where it presents as a wrong answer rather than a missing field. One line, because "refuses to guess" is only as strong as what happens next. |
What this can and cannot be verified against at this layer, stated up frontTwo things a reviewer will reasonably ask, answered from the code rather than left to discover: 1. The pack leg cannot cover a container, so the synthetic arms are the whole evidence here. That means the five containers' 2,000-odd real tensors do not exercise 2. It is not wired into If you would rather it surface in |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- the per-tensor shapes are right, and the two spelling traps are handled; one uniqueness check is missing at the composite key
The two arms that matter are both here. "A row per tensor, so nothing is silently skipped" (rows.size() == v.size()) plus placed + unplaced + no_role == v.size() makes the report a partition of the container, which is the property that lets unplaced mean something: without it, an empty unplaced could mean "everything placed" or "the loop forgot half the tensors". And the pair is the arm this whole layered-key stack exists for:
model.layers.0.self_attn.qkv_proj.weight (linear_attention) -> linear_attn.qkv_proj.weight
model.layers.1.self_attn.qkv_proj.weight (full_attention) -> "" + why (>10 chars)
the same role landing in two different places on two layers, one destined and one refused with a reason. That is the second key demonstrated end to end rather than described.
Two traps I expected and found handled.
- Layer-index extraction accepts both spellings:
find(".layers.")with an offset of 8, falling back tofind(".layer.")with 7. That matters because #2277 exists partly because MiniCPM-V spells its blockslayer.N; a placement layer that assumedlayers.would file every MiniCPM tensor as layer-less and silently place it by role alone. - Layer-less tensors still get their slot and say so (
layer.empty() && layer_type.empty()forembed/final_norm/lm_head), rather than being treated as unplaceable because they have no type.
One check I would add, and it is at the composite key. row.slot is layer-relative (self_attn.q_proj.weight) and row.layer carries the index, so the identity of a destination is the pair (layer, slot) -- and nothing asserts that pair is unique across rows. Two tensors in the same layer that both elide to the same name would produce identical (layer, slot) and the import would write one over the other with no diagnostic; the spelling work in #2277 makes that reachable, since layers.1.x and layer.1.x are exactly the shapes a container mixes. One arm:
// two rows must not claim one destination
std::set<std::pair<std::string,std::string>> seen;
for (const auto & r : r.rows) if (!r.slot.empty() && !seen.insert({r.layer, r.slot}).second) { /* report both tensors */ }Either assert uniqueness or report it the way spans are reported in the writer (named, not resolved) -- but a silent last-writer-wins is the one outcome this stack has otherwise been careful to avoid.
Not blocking, and the PR is a clean second piece: #2285 mapped role -> slot, this answers per tensor with that tensor's own layer type, and unplaced + why keeps the refusals visible. The (layer, slot) check is the same invariant as #2285's no two roles share a destination, one level down -- where the key has two parts instead of one.
3c placement, second piece — the decision an import actually iterates: per tensor, with that tensor's own layer type
#2285 (merged) mapped a role to a slot. A writer does not iterate roles, it iterates tensors, and a container mixes layer types — so the destination depends on the tensor's own layer. That is what this adds:
One
PlacementRowper tensor:tensor,layer,layer_type,role,slot,why. A tensor with no layer (embed,final_norm,lm_head) reports empty layer and type and still gets its slot, because those roles do not need the second key.The arm that matters is the pair — the same role landing in two places on two layers:
Mutant — use
layer_types[0]for every tensor, i.e. forget whose layer it is — fails three arms:Three counts kept apart on purpose, because they mean three different things:
placedunplacedwhyno_roleplaced + unplaced + no_role == tensors.size()is asserted, and a row exists for every tensor. That accounting is the property that makes an import trustworthy: nothing is silently skipped, and "I could not decide" never renders as zero.Scope: this is the decision, not the write. It computes where each tensor goes; emitting the destination pack, and the eight role refusals that need shape measurements (
gate_up_proj's split, the shared-expert family), are the next slices. No device access, no container imported.npu_pack_import_testpasses;tools/copyright.py --checkclean.