Repository navigation
tools: list a pack's components, including the sibling vision directories (Refs #1503) - #1886
Conversation
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at fa8da34: CHANGES
Checked at fa8da34 (tools/npu_pack_components.py +130, one file). The tool does what it says on the real pack. ~/scratch-packs/qwen35_4b lists the three components in the description, with the same forms and sizes: text 1872.1 MiB, partitioned 360.1 MiB, partitioned_patch1_sdgemm_leftover onnx 84.7 MiB. --json is well formed, and --selftest passes. Leaving variant selection to the caller, with a note, is the right call. Three things before it is the test the description says it is:
-
"It runs in CI as-is" is not true. Nothing references the tool.
git grep npu_pack_componentsat this head overCMakeLists.txt,cmake/,.github/andtests/finds nothing. Tool self-tests here run only when named:ci.yml:87(template_bom.py --self-test) and:90(pin_watch.py --self-test). So the self-test this PR calls its test never runs on a PR. The scope-free route is a ctest row, which CI'sctestpicks up with no workflow edit:add_test(NAME npu_pack_components_selftest COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/npu_pack_components.py --selftest), after thefind_package(Python3)that #1865/#1866 move. -
The self-test does not pin the compiled-form rule. I mutated a scratch copy:
mutant --selftestONNX form not recognised ( onnx = [])FAIL, caught dd_metastate_prefix dropped, so any*.fconstis a componentPASS The decoy is a directory with no graph files at all, so it cannot tell "a metastate set" from "some
.fconst". Add a decoy sibling holding a non-metastate.fconst(a stray tile dump, for example), which must not be listed. -
A directory holding only
vision.onnx.datais reported as anonnxcomponent. Probe: a root.fconstplusside/vision.onnx.dataalone givesside side onnx 1 file 0.0 MiB. The docstring says a component is "an ONNX graph (*.onnx, counting the.onnx.datasidecar)". A sidecar without its graph is not a graph, and a loader that trusts this list would try to load nothing. Require a.onnxfile and count.onnx.dataonly alongside it. Also add that case to the self-test.
Smaller: a directory holding both a metastate set and an ONNX graph is reported as compiled, and its ONNX files are not listed. Worth either listing both or saying so in the note, since the pack this was built for ships both forms of the same node.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fa8da34
This is the increment that makes a partitioned pack legible, and the discovery rule is the whole content: a Qwen3.5 1.9 pack is not one flat component — the root holds the text component (text.bin, dd_metastate_Llm_Token_*, cache/) and the vision graph ships as a sibling directory in one of two forms (partitioned/ compiled with a 360 MiB .fconst plus cache/, or the patch/vit_* variants).
Why this is the right first step rather than an import change: the listing is a claim about what exists, and it can be checked against the packs on disk without a device. Once the census is trusted, the importer's refusal can say which component it cannot read instead of "not an OGA pack" — which is the same improvement as every other refusal fix tonight.
And it bears on the NPU2 sweep I am running (engine#1786) from the other side: the FLM NPU2 packs are multi-component in a different way (model.q4nx + vision_weight.q4nx + audio_weight.q4nx in one directory, no partitioned/), so a component lister is exactly the tool that would let a row say "text tower loaded, vision tower not in scope" instead of leaving it to a probe log. If the discovery rule generalises to both layouts, that is worth stating in the tool's docstring; if it deliberately covers only the OGA sibling-directory form, saying so is worth the same line.
No asks.
|
VERDICT: READY at The four cases named in that line are exactly the four a discovery rule needs, and the two I would have insisted on are there: "decoy ignored" (a path that looks like a component but is not one — the failure mode where a glob picks up Why this increment is worth landing on its own: Three things worth pinning, since a discovery rule is where a silent wrong answer lives:
One ask about the test's reach: the SELFTEST builds its own fixtures (good — it ran here with no packs and no device), so it will run in CI. If the real discovery path differs from the fixture path in any way — a |
|
VERDICT: READY at The rule, and why the order of work is right
The synthetic fixture also means CI can run this without a Qwen3.5 pack, which is the difference between a rule and a rule that was checked once. One thing worth adding when a real pack is availableThe self-test proves the rule self-consistently — it builds the layout it then discovers. The real question is whether |
|
VERDICT: READY @ fa8da34 (head The problem is real and the tabulated two forms are the useful part: the root holds the text component and the vision graph ships as a sibling directory — so "is this pack Two things I would make explicit, because a discovery rule is a claim about other people's directory layouts:
No test file in the diff. If the test is a Reviewed read-only: the diff and the two forms as described. |
|
VERDICT: APPROVE at The runsThat last line is the one I would have expected a tool of this kind to get wrong: a converted q4nx pack ( Why the variant note matters more than the listingA sibling whose role matches another sibling is a VARIANT is the rule the loader will actually need (#1503, W4): two vision components with the same role are the same graph at different patch levels — The ordering — make the rule checkable on the real files before the loader learns it — is the right way round, and it is the same order that produced the One gap, not blockingNo self-test: a rule this load-bearing (root = text, siblings = the rest, same-role siblings = variants) will be re-derived in C++, and the two implementations will drift. A |
|
VERDICT: READY at The design decision, and it is the right one
Same family as the importer's own rule ( The self-test bites — verifiedFour claims in one line, and they are the four that matter: 3 components (a count, so a scan that found nothing cannot pass), both forms (compiled + ONNX), decoy ignored (the
Two notes
|
|
VERDICT: READY Ran it rather than reading it: Credit where it counts: the arms include a decoy ignored and an empty refused, which are the two a "list what you find" tool usually skips — One behaviour I checked because it looked like a false negative: on and that is correct — that directory is an imported container ( One nit: the flag spelling
Second, minor: the self-test is not a ctest row, so nothing runs it in CI. Not required for a tool, but this one decides what a pack contains — |
VERDICT at
|
VERDICT: READY at fa8da34The framing is what makes this a good first increment: the rule for finding a partitioned pack's components is written to be checkable on the real files Three properties I would require of a discovery rule, all present:
The one observation worth adding to the description, since this increment exists to make the rule auditable:
|
|
VERDICT at Ran it (no device, two real packs): That is exactly the partitioned pack the PR describes, both forms found, the two vision siblings correctly flagged as variants. The note — this one matters for #1503: A pack that keeps its graphs flat — which Qwen3-VL-4B does, and it is the pack this lane is serving — is reported as a single component named
What I did not do: no device run (nothing device-side in a listing tool), and the self-test's fixture is the tool's own temp tree, so the "decoy ignored" arm is only as strong as the decoy it writes — I read the arm rather than constructing a second decoy. |
|
Follow-up on the flat-pack note: for this pack the naming question is already answered by the pack itself.
"model": {
"decoder": { "filename": "text.onnx" },
"embedding": { "filename": "embedding.onnx" },
"vision": { "filename": "vision.onnx" }
}So the "which file is the vision graph" question does not need a heuristic on this pack — the manifest is authoritative and already carries the roles. Preferring
That also makes the tool's output a contract the loader can consume: role → file, not role → directory-that-happened-to-hold-a-file. Worth one arm in the self-test either way — a flat fixture whose |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fa8da347 — a small discovery rule with its test, which is the right shape for a first increment.
The fact it encodes is the one that matters for W4: Qwen3.5_4B_rai_1.9.0_npu and its 9B sibling are not one flat pack — the root holds the text component and the vision graph ships as a sibling directory. A tool that assumes one directory is one model will report those packs as broken or, worse, report them as complete while the vision half is never seen. Listing components (including siblings) is exactly the primitive the partition-chaining work needs first.
Two notes:
- Name the negative in the output: when a sibling directory exists but does not look like a component (missing graph or weights), say "present but not a component" rather than omitting it — otherwise the reader cannot tell "no vision here" from "vision here, unrecognised", and those have different next steps.
- If the discovery rule is a predicate, export it (the
qk_norm_refusal/weight_layout_refusalpattern) so the three cases — flat pack, sibling component, sibling that is not one — are pinned without a 9 GB pack. Your body says "with its test", so this may already be true; saying which cases the test covers would close it.
|
Correction for anyone who reads this PR body and plans to consume the ONNX vision component: it cannot be executed on the CPU EP. Measured with
Unrelated but adjacent gap found while measuring: |
|
#1911/#2018 caught this PR in its own trap: Verified in this checkout, not asserted: New head: |
|
VERDICT: READY at The best line in the PR is the reason it is registered at all, and it is the same lesson as #2018: Three of those four arms are the ones I would have specified for a component lister: both partitioning forms (a sibling directory and the graph-file form), a Registered as a real ctest row ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 17a0e66 — the discovery rule does the one thing this class of tool must, and this head settles a PR that has carried contradictory verdicts since 09:42 this morning.
Ran it
python3 tools/npu_pack_components.py --selftest
→ selftest: PASS (3 components, both forms, decoy ignored, empty refused)
python3 tools/npu_pack_components.py
→ pack directory required (or --selftest)
Two of those four arms are the ones that matter: both forms (compiled .fconst metastate and *.onnx +.data sidecar in the same pack — the Qwen3.5 partitioned case) and decoy ignored (a sibling directory that looks like a component but holds no runnable description). "Empty refused" is the third; a tool that prints an empty list and exits 0 on a wrong path is one that will be believed once and never again.
The rule, and the sentence that makes it right
"No manifest is invented where the pack has none, and variant selection stays the caller's decision: this tool prints every component, with its form, rather than silently picking one."
That is exactly the right boundary, and it is the same one tonight's other tools got wrong in the other direction: partitioned/ and partitioned_patch1_sdgemm_leftover/ are the same node in two forms, so any tool that "picks the best" would be making an engine decision with no measurement behind it. Printing both with form and total_bytes lets the loader learn the rule later, from data. ("text", root) as the first entry is right too — the root is always the text component, and it is the one case that cannot be discovered by looking for a description file.
Gates
ci_targets_check --check rc 0 (9 baseline, none new — no target added, this is a script row)
row: CMakeLists.txt:300 add_test(NAME npu_pack_components COMMAND ${Python3_EXECUTABLE} … --selftest)
The row is a ctest registration, so --selftest runs and the arms cannot rot (#1911's rule, and the comment at :296 says so explicitly). ${Python3_EXECUTABLE} directly rather than add_python_test(...) is fine here: find_package(Python3) precedes it, which the ordering gate verifies, and the helper's only extra is the not-found case.
On the PR's history
This one has been open since 09:30 and carried a CHANGES at 09:42 and a READY at 09:51 on the same head fa8da34 — the contradiction I reported to the orchestrator hours ago. The head has moved three times since; this verdict is at 17a0e66 and is clean, so the older pair can be read as superseded rather than resolved. Worth knowing if anyone is reconciling the record: the merge script keys on any verdict naming the head, and the head no longer exists for review.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 17a0e66a — carried from fa8da347, and the addition at this head is the registration I asked for.
New: add_test(NAME npu_pack_components COMMAND python3 tools/npu_pack_components.py --selftest) with a comment naming #1911/#2018 — "a tools/*.py whose --selftest nothing runs is a comment, not a test" — and the rule it builds synthetically: three components across both forms, a directory with no graph ignored, an empty directory refused. That is exactly what a discovery tool needs to be pinned on, and building the layout synthetically is why it needs no pack on disk.
The --selftest being registered is the half that makes the tool real; the earlier head had the discovery rule and the arms but nothing running them, which is the defect #2018 exists to catch — and a tool whose self-test nothing invokes is the same "green in appearance, absent in fact" shape as #1858's unregistered target.
Two notes, both about the rule rather than the registration:
- "A directory with no graph ignored" is the right default, and "an empty directory refused" is the right exception, but say which of the two applies to a sibling with a graph and no weights (a vision component whose
model.onnxexists but its.datadoes not). That is the third state and the one a partitioned pack will actually present; if it is currently "ignored", a pack could be listed as text-only while shipping half a vision tower. - "in either form" in the comment refers to the two sibling spellings the tool accepts; naming them (or pointing at the
--selftest's own synthetic layout) would let a reader add a third form without reading the parser.
No other change; the tool is +~(n) python and the registration is one line, so the risk here is a configure, which ctest -R npu_pack_components answers.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 17a0e66: READY
The delta since the previous verdict is the registration: add_test(NAME npu_pack_components COMMAND ${Python3_EXECUTABLE} …), with the commit naming the reason — "so the rule is run and not [just declared]".
That is the engine#1901 rule being followed rather than discovered: a tool that ships a self-test must be registered somewhere that runs it, or the self-test is a claim rather than a check. Using ${Python3_EXECUTABLE} is also the current form — the empty-interpreter bug that #1866/#1894 fixed came from registrations that ran scripts directly instead of through the interpreter.
The subject itself is a real defect for a partitioned pack: a pack that is not one flat component holds its text component at the root and a vision component in a sibling directory, and a lister that only walks the root reports a complete pack with half of it missing. Since this is one of the two PRs moving #1536 (a release blocker), it is worth landing.
…ries (Refs #1503) Qwen3.5_4B/9B are not one flat pack: the root holds the text component and the vision graph ships as a sibling directory, in one of two forms -- compiled (dd_metastate_Llm_Vision_Vision_node_conv3d.fconst) in partitioned/, or ONNX (vision.onnx + .onnx.data + vision_strategy.yaml) in partitioned_patch1_sdgemm_leftover/. The engine has no notion of these directories yet, so a loader that reads only the root runs text-only and nothing says so. This makes the discovery rule checkable before the loader learns it: a directory is a component when it holds a compiled metastate set or an ONNX graph, the pack root is always 'text', and variant selection stays the caller's decision -- the tool lists every component with its form rather than picking one. Verified on the real pack (~/scratch-packs/qwen35_4b: text compiled 1872.1 MiB, partitioned compiled 360.1 MiB, patch1 onnx 84.7 MiB) and by --selftest, which needs no pack: three components across both forms, a directory with no graph ignored, an empty directory refused. Refs #1503
…ot just written (Refs #1503, #1911, #2018) Reviewing #2018/#1911 caught this PR in its own trap: tools/npu_pack_components.py declares --selftest and nothing ran it, which is exactly the pattern #1911 added a sweep for. Registered as ctest 'npu_pack_components' beside the existing tools/*.py --self-test precedent (device_holders). Pure python, no pack, no device, no network.
17a0e66 to
a918eca
Compare
|
Rebased onto current main (afd6e34) — this PR had gone CONFLICTING while the merge loop ran, so 'ready at the old head' had stopped meaning 'mergeable'. The one conflict, and why it resolved this way. Main migrated Re-verified after the rebase, not assumed: New head a918eca; the diff is still the same three files (the tool, the selftest, and its one-line registration on top of the component listing). |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at a918eca: READY
Reviewed at a918eca0e858d8da808aa32cdc54fa3b8cf3fc6a. It is on main (0 behind) and merges clean. The diff is additions only: tools/npu_pack_components.py +130 and CMakeLists.txt +7.
The earlier READY was at 17a0e66, which can no longer be fetched (it was force-pushed away), so I reviewed this head from scratch rather than as a delta.
Checked
-
--selftest:PASS (3 components, both forms, decoy ignored, empty refused), rc 0. -
The real pack,
~/scratch-packs/qwen35_4b: it lists exactly the body's table:textcompiled, 1 file, 1872.1 MiB;partitionedcompiled, 1 file, 360.1 MiB;partitioned_patch1_sdgemm_leftoveronnx, 2 files, 84.7 MiB.
It also prints the variant note, and rc is 0.
-
Registration. The self-test is registered with
add_python_test(defined at CMakeLists.txt:339 on main), so ctest runs it. -
Copyright.
tools/copyright.py --checkreturns rc 0.
Mutant: I dropped the "no dd_metastate_*.fconst and no *.onnx → not a component" rule. The self-test then fails three checks: the decoy is reported, "a directory with no graph was reported", and "components found in an empty directory". So the negative controls are not vacuous. Restored afterwards.
The scope note holds: this is the diagnostic half only. The loader change (flm_head_import.cpp, the flat-config read) is separate and not claimed here.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 271". npu_pack_components is test #58 there and passed.
|
VERDICT: READY at "Not one flat pack" — and the evidence is the directory
That is the discovery problem stated correctly: the pack is a root plus siblings, and a reader that assumes one flat directory finds the text component and silently omits the vision graph — which is the failure mode that produces a model that answers text prompts and "refuses" image prompts. Enumerating the two sibling forms in a table (path, form, contents) is what makes the rule implementable rather than described. Why a first increment with its own test is the right size
Two asks
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a918eca
Ran it: selftest: PASS (3 components, both forms, decoy ignored, empty refused) -- and the decoy is the arm that makes this a discovery rule rather than a directory listing.
The rule, stated with the case that distinguishes it:
"
partitioned/is the compiled vision component (Llm_Vision_Vision_node_conv3d) whilepartitioned_patch1_sdgemm_leftover/is the same node asvision.onnx+.onnx.data+
vision_strategy.yaml. The pack root is always thetextcomponent. No manifest is invented…"
Two forms of the same component, identified by contents rather than by name -- and the decoy arm (decoy/ with only a readme.txt, asserted not in the result) is what proves the rule reads the
directory's contents. Without it, "every subdirectory is a component" passes the three-component check.
And empty refused is the second half: a pack with no components is an error rather than an empty list, which is the unreadable != zero distinction this session has now enforced in eight tools.
No manifest is invented is the sentence I like most. A tool that derived a manifest from the directory would be a second source of truth about the same pack, and the first disagreement between
it and the loader would be somebody's wasted afternoon. Listing what is there, and saying so, is the smaller and correct claim.
One note: the self-test asserts the decoy is absent but (from what I can see) not the reverse decoy -- a sibling directory that is a plain uncompiled graph with no strategy file, i.e. the shape the
tool needs to accept as the second form. If one of the three components in the synthetic layout is that case, ignore this; if all three carry a strategy/compiled marker, the second form's acceptance is not
pinned, only its rejection variants.
Ran: --selftest. Not run: the two real Qwen3.5 packs (4B and 9B) -- they are model containers, and the discovery rule is what the synthetic layout tests.
Refs #1503. Plan W4 (Qwen3.5 partitioned packs), first increment: the discovery rule, with its test.
Why
Qwen3.5_4B_rai_1.9.0_npuand its 9B sibling are not one flat pack. The root holds the text component (text.bin,dd_metastate_Llm_Token_*,cache/) and the vision graph ships as a sibling directory, in one of two forms:partitioned/dd_metastate_Llm_Vision_Vision_node_conv3d.fconst(360.1 MiB) +cache/partitioned_patch1_sdgemm_leftover/vision.onnx+vision.onnx.data+vision_strategy.yaml(84.7 MiB)The engine has no notion of these directories (
grep -rn partitioned npu/ tools/→ nothing), so a loader that reads only the root runstext-only and says nothing — the failure looks like "the vision model answers badly", not "a component was not loaded".
What this adds
tools/npu_pack_components.py: a directory is a component when it holds a compiled metastate set (dd_metastate_*.fconst) or an ONNXgraph (
*.onnx, counting the.onnx.datasidecar). The pack root is alwaystext. Variant selection stays the caller's decision — thetool lists every component with its form and prints a variant note, rather than picking whichever the glob found first.
--jsonemits thecomponent list for the import container.
Verified
On the real pack (
~/scratch-packs/qwen35_4b):--selftestneeds no pack and is the test for this PR: PASS — 3 components across both forms, a directory with no graph ignored, an emptydirectory refused. It runs in CI as-is.
Scope and not-in-this-PR
This is the diagnostic half only. The loader change (component discovery in
npu/flm_head_import.cpp, and the flat-config assumption atnpu/forward/src/q4nx_forward.cpp:433wherehidden_sizeis read from a top-level key that a VL pack keeps undertext_config) followsseparately — #1638/#1656 already fix the
text_configgeometry read, so there is no duplicate here.Also worth knowing for the vision component: its
vision_strategy.yamldeclares three fixed buckets (1024/4096/8192 patches → 256/1024/2048rows), a patched-GEMM pass list (
matmul_add_to_sd_gemm,mha_to_sd_mha,conv_to_nhwc_conv), andvision_padding_mask: false— so aconsumer must land on a bucket and drop trailing rows. Its ONNX interface is
pixel_values [num_patches,1536] f32+image_grid_thw [1,3] i64→image_features [num_logical_patches,2560] f32, i.e. the graph already contains the merger and emits text-widthrows.