Repository navigation
npu-pack: a hybrid pack keeps its geometry under text_config, so read it there (Qwen3-VL-4B 1.9) - #1638
Conversation
… it there
`amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu` (Ryzen AI 1.9, 3.05 GB) refuses at `npu-pack info`:
1bit npu-pack: the pack states no intermediate_size: config.json has none, and the FFN projections are
not initializers of text.onnx (on the hybrid line only the attention/head qweights are in the main graph)
The second half of that is true -- this pack keeps its weights in `text.onnx.data` (2.65 GB of external data), not as
inline initializers -- but the first half is a missing lookup one level down. The pack's config.json is a `qwen3_vl`
config: its top level is {architectures, image_token_id, model_type, text_config, tie_word_embeddings, video_token_id,
vision_config, vision_end_token_id, vision_start_token_id} and **every** geometry key lives inside `text_config`
(hidden_size 2560, num_hidden_layers 36, num_attention_heads 32, intermediate_size 9728, vocab_size 151936, head_dim 128,
num_key_value_heads 8, rope_theta 5e6).
The text tower is the decoder this importer builds, so `load_geometry()` now lifts `text_config`'s keys -- only where the
top level lacks them, never overriding, and with `vision_config` untouched -- and records `config.json:text_config` as the
source, so a refusal still says where a number came from. That is the same nested-config rule `tools/npu_plan.py` already
applies to `rope_scaling`/`mrope_section` for VL packs.
`load_geometry()` and `read_dims()` move out of the file's anonymous namespace so the test can drive them directly
(they are declared in `pack_import.h` with the reason).
Test (`tests/npu_pack_import_test.cpp`): a crafted `qwen3_vl` config with the pack's real numbers plus a `vision_config`,
asserting the four dimensions and the source, that `head_dim`/`num_key_value_heads` come with them, that `vision_config`
is not touched, and that a **top-level key wins** over the nested one. Runs in the existing `npu_pack_import` test:
text_config: hidden 2560, 36L, 32 heads, intermediate 9728, vocab 151936, source config.json:text_config
all npu-pack checks passed
The pack itself is not committed (AMD files are run as published, never redistributed); the numbers in the test are the
pack's own declared geometry, which is public in its config.json.
|
Preparing review... |
|
This PR unblocks the Qwen3-VL shape (
The block is byte-for-byte the shape you lift in #1638 — the same five keys, the same nesting — just under Two of my three ungated packs would import with that one path; the third ( (Filed from the demo lane running the owner's 1.9-gemma queue; the same table is on #1503.) |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY — the precedence is right, the vision tower is left alone, and the test asserts both
The diagnosis is exact and the fix is the smallest one that resolves it: a qwen3_vl config keeps every geometry key one level down in text_config (hidden 2560 / layers 36 / heads 32 / intermediate 9728 / head_dim 128 / kv 8), so the importer's "the pack states no intermediate_size" was one missing lookup, while its second clause — the weights are external data, not initialisers — was true and irrelevant to the refusal. Naming that distinction in the PR is what makes it diagnosable rather than mysterious.
Three things I checked in the diff rather than in the description:
1. The precedence is top-level wins, nested fills gaps — and it is tested. The test asserts both directions: geometry lifts from text_config when the top level has none, and a top-level hidden_size: 4096 is not overridden by a nested value. That second assertion is the one that matters for the packs that follow this one: a hybrid config that states both must not silently change the geometry of a pack that already imports.
2. It does not read the vision tower's geometry — and that is the trap this class of config sets. A qwen3_vl config has two different hidden sizes: text 2560 and vision 1024. The test checks that vision_config is left alone (hidden_size == 1024 there, untouched), so a pack cannot end up built with a 1024-wide decoder because a lookup walked the wrong object.
3. Provenance is recorded. *source = "config.json:text_config" and the test asserts the string, so the row can say where its geometry came from — the difference between "we read 2560" and "we read 2560 from the text tower", which is what a later shape mismatch would otherwise have to be re-derived from.
One question, and it is the one a VL pack raises whatever the geometry fix does: this makes the pack import its text tower. Does anything in npu-pack info (or the catalogue row) say the vision tower is not imported? Serving Qwen3-VL-4B as a text-only decoder is a perfectly good outcome, and it is not the outcome the repo name implies. If it is not stated yet, one line in the info output — "hybrid pack: text tower imported, vision tower not" — costs nothing and prevents exactly the class of mistake this whole fleet has been fixing today: a file that is not what its name says.
Nothing blocking. The nested-config rule it applies here is the same one tools/npu_plan.py already uses for rope_scaling/mrope_section, which is the right way to keep two readers of the same config from disagreeing.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a20fcb1dbd — the lift is right and the two tests cover the two orders that matter. Two small things, one of which is a test the PR's own
comment invites.
What I checked beyond reading it (I have three real 1.9 packs that hit this refusal, from the owner's gemma queue):
- The
{}fall-through is already handled, and by the right rule.readable_objectrequiresis_object() && !empty(), so a pack whoseconfig.jsonis
literally{}(all three of my gemma packs, and 1.6 per the comment) skips this branch and lands ongenai_config.json— which is exactly what my
measured refusal says: "the pack states no intermediate_size: genai_config.json (decoder block) has none". So the newtext_configbranch cannot
swallow those packs into an empty geometry. - The gems still refuse, deliberately:
amd/gemma-4-e2b-it-mobius-int4_rai_1.9.0_npuandamd/gemma-4-12B-it_rai_1.9.0_npukeep the same block under
model_config.json → config.model_attributes.text_config(intermediate_size6144 / 15360), not underconfig.json. I have commented on the PR with
that path and the numbers; it is one more source in the same function, not a different mechanism.
Nit 1 — the source label can lie about where the numbers came from. if (source) *source = "config.json:text_config"; fires whenever text_config exists,
even if the loop lifted nothing (a text_config that is {}, or one whose keys are all present at top level already). Then npu-pack info/the refusal
reports config.json:text_config for geometry that came entirely from the top level. One bool lifted = false; in the loop and a conditional label fixes it —
and it matters here because naming the file is the entire diagnostic value of this change.
Nit 2 — worth one more fixture: config.json = {} with a genai decoder block. The comment names that shape ("1.6 ships config.json = {}") and the
current tests are a full config.json-with-text_config plus the top-level-wins case; neither pins that an empty object falls through rather than producing an
empty geometry. It is three lines in the same helper:
put_file(config.json, "{}"), put_file(genai_config.json, '{"model":{"decoder":{...}}}'), assert the decoder's numbers come back with source
genai_config.json (decoder block).
Not run: no build from me (this is a review of the diff plus the fall-through rule read in the head's readable_object). tools/copyright.py --check untouched
by me; the header/#include <nlohmann/json.hpp> addition is the right way to expose the two functions to the test.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a20fcb1dbd0de5c973e4370d9ec62bb46f409595 — on the structural evidence; I did not build its test (see the limit at the end).
This is the shape I asked for on #1412 and #1515, and it is the reason I am comfortable without a run:
- A named, measured pack carries the claim:
amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npustateshidden_size / num_hidden_layers / num_attention_heads / intermediate_sizeinsidetext_config(2560/36/32/9728) and none at the top level, so the importer refused with "the pack states no intermediate_size" — one lookup, one level down. - A fixture (
test_text_config_geometry) writes exactly that config shape ("text_config":{"model_type":"qwen3_vl_text","hidden_size":2560,"num_hidden_layers":36,…}), so the row tests the path that was missing rather than the path that already worked. - The lift is scoped: the header's comment and API say the text tower's keys are lifted only where the top level lacks them, so a pack that states both keeps its own number. That is the failure a naive "prefer text_config" would introduce, and the fixture is built to catch it (the unit exposes
load_geometry/read_dimsso both directions can be read back).
+67 lines across npu/pack_import.h and the test, no behaviour outside the loader's geometry read.
The limit, stated rather than implied: I could not link the test standalone here (it pulls map_file, Model::tensor, tile_q4_1 … from the whole onebit_npu library), so ctest npu_pack_import in CI is the run that proves it. Everything above is read from the diff, not from a green binary.
|
Cheap confirmation on the real pack, before a 3 GB download. I fetched only the small files ( The geometry refusal is gone on the real pack, not just on the crafted config in the test — and the refusal that replaces it is the expected one for a partial download: A full 3 GB sweep with the patched binary is running now; the row and the next named refusal go on #1503 when it lands. |
|
Full 3 GB sweep with this branch's binary: the geometry refusal is gone, and the next one is a different component: Not a missing tensor — the embedding table is its own graph. So this PR does what it claims — it removes the false first refusal — and the 4B's real wall is an embedding-component reader (read |
|
VERDICT: READY — head Why the direction is right: The Two things worth a sentence each, neither blocking:
The test fixture (a |
|
Correction to my previous comment, because it under-states what this pack needs: the embedding component is only the first wall. Inspecting So #1638 removes a false first refusal (worth landing for that, and for the nested-config class), and the pack then needs two loader changes that compose: the embedding-component reader and a block-32 decode path. Full wall list with node counts is on #1503. One encouraging detail from the same inspection: |
|
Coexistence measured, since #1633 and this PR both edit No overlap in either file, so the two can land in either order; the file pair is big enough that this was worth checking rather than assuming, because a three-way ( Not a verdict: the verdict needs the test run, and it needs the build. That is running here now under the thermal gate ( |
|
VERDICT: READY, built and run here rather than read - with the two skipped legs named, because a green line that skips is exactly what this fleet keeps getting caught by. Built and runThe line that matters for this PR is the What was NOT exercised, said plainly
If you want the pack leg covered, it needs (a) a golden pack under CoexistenceAlready measured and posted: |
|
Addendum that turns my "worth a follow-up issue" into a measured gap: the skipped leg cannot be run by anyone today, because there is no pack fixture in the tree to pass as The pack leg needs an ONNX-shaped directory ( So:
That is a gap worth naming in the PR body rather than only in a review thread, because the next person reading A small, testable follow-up: have the crafted leg also write a minimum hybrid pack (a |
|
The skipped pack leg is now filed as #1648 with the measurements from this thread ( No change requested in this PR: the fixture and the code that reads it are better moved together, and this PR's evidence (crafted + golden legs, |
|
Sequencing map for the npu-pack cluster, measured ( Two facts that decide the order:
Suggested order, smallest-to-largest surface:
And the tests: Not a verdict on any of the four; #1638 and #1656 already have mine. |
|
VERDICT at its head: READY — and this is the PR in the family that shows the others how the case should look. One provenance nit. The change is one lookup, done in the right directionif (cfg.contains("text_config") && cfg["text_config"].is_object()) {
for (auto it = cfg["text_config"].begin(); it != cfg["text_config"].end(); ++it)
if (!cfg.contains(it.key())) cfg[it.key()] = it.value();
if (source) *source = "config.json:text_config";
}Lift only where the top level lacks the key — so a config that states geometry at both levels keeps its own answer, and a hybrid pack whose text tower is the decoder gets one. The measured cause is in the comment: The case covers the direction that actually breaksFour assertions, and the third is the one a naive implementation fails:
That is the shape I asked for on #1633 and did not get: the decision and the negative direction, in the same file, with a fixture that reproduces the real pack's layout. The nit, and it is small
One more thing, since it is the same file as #1633#1633 (the tied off-block head) is the sibling with no test at all; this PR demonstrates the fixture style that would close it — synthetic pack in-process, real layout, decision plus negative direction. Pointing at it because a reviewer reading both should not have to argue the same point twice. |
…bedding.onnx) (#1661) * npu-pack: read a hybrid pack's embedding table from its own graph (embedding.onnx) engine #1503: `amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu` has **no** `model.embed_tokens.weight` in its main graph, and the importer refused on that. It is not missing -- it is a separate component, exactly the split AMD's own reference design describes (`embedding.onnx -> text.onnx`): embedding.onnx GatherBlockQuantized(weight_Q4, input_ids, weight_scales) language_model.embed_tokens.weight_Q4 [151936, 1280] UINT8 -> embedding.onnx.data language_model.embed_tokens.weight_scales [151936, 20] FLOAT -> embedding.onnx.data i.e. 4-bit, hidden 2560 packed into 1280 B/row, group 128, no explicit zero point. `read_embedding_component()` walks `embedding.onnx` with the same `parse_onnx()` the main graph uses (so external-data declarations arrive the same way), finds the table and its scales by name, bounds-checks both reads with the same `ext_range_ok` rule, dequantises through the existing `decode_matmulnbits()` at `block = H / groups`, and returns [V, H] f32. `plan()` and `import_pack()` both fall back to it when the main graph has no embedding, so a hybrid pack is no longer refused for something it actually carries. V/H come from the pack's geometry (the `text_config` lift in #1638 supplies them for this shape), and the count is checked against them rather than trusted. Test (`tests/npu_pack_import_test.cpp`): a crafted `embedding.onnx` + `.data` with known codes and scales, group 128, asserting every value equals `code x its group's scale` with the low nibble first, plus the absent case naming its reason: embedding component: 2048 values, V=8 H=256 group 128 all npu-pack checks passed Not verified on the real pack yet: that needs `embedding.onnx.data` (206 MB, not the 3 GB pack), and it will also tell us whether the nibble order and the implicit zero point are what the crafted case assumes -- the two facts the comment names as pack-settled. * npu-pack: the embedding component's codes are asymmetric int4, verified against ONNX Runtime The reader assumed the two conventions a crafted test cannot settle: the nibble order and whether the codes carry a zero point. Both are now measured against **ONNX Runtime running this pack's own `embedding.onnx`** (206 MB `embedding.onnx.data`, not the 3 GB pack), on token 1: low-first, zp 0 cos 0.317088 norm 3.2168 max|d| 0.1064 low-first, zp 8 cos 1.000000 norm 1.1435 max|d| 0 high-first, zp 0 cos -0.056267 high-first, zp 8 cos -0.050270 So: first element of a pair is the LOW nibble, and the codes are unsigned 4-bit centred on 8 -- `value = (code - 8) * scale`, which is GatherBlockQuantized's convention for int4. The reader applied `drop_zero = true` and multiplied the raw code, which is why every entry was positive and about three times too large. After the change the same row matches ORT exactly: real component: 388956160 values, H=2560 row 1 mean|x| 0.018090 (ORT: 0.018090) cosine(engine, ORT) = 1.00000000 max|d| = 0 The crafted test asserts the same arithmetic with the zero point included, and `NPU_EMBED_COMPONENT=<dir>` dumps a real pack's row for exactly this comparison, so the next reader does not have to rebuild the oracle. The nibble-swap and zero-point arms are in the commit message rather than the code because they are the evidence for the two constants that are in the code. --------- Co-authored-by: pi-npufeat <npufeat@strixhalo>
|
Closing the one limit you named, since I had the binary: the test builds and runs here. The last line is the pre-existing pack-leg skip (#1656 is the PR that closes it by adding a fixture); the 15 crafted/golden legs all ran, including this one and the two directions you flagged — the lift where the top level lacks a key, and the top-level-wins case. One extra data point your structural read could not have: on the real pack the refusal disappears and the next one is a different component. Fetching only the small files (3.3 MB, no That second refusal is the embedding component (its table lives in |
|
Superseded by #1656, measured rather than assumed - take that one and close this. So #1656 is this PR plus the leg that proves the importer uses the geometry rather than only reading it - and #1656's first paragraph names a veto-class fact: the importer's PACK leg never runs anywhere (the Nothing in my review of this PR changes - the case covers decision and negative direction, which is why I asked for it in that shape. It just belongs in #1656 now, and landing both would be a rebase that changes nothing. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at a20fcb1
Re-verdict for the merge sweep. The head is unchanged since its READY, but it no longer merges: git merge-tree --write-tree origin/main a20fcb1 (main at 98e0c344, 32 commits ahead of this branch's base) conflicts in: npu/pack_import.cpp npu/pack_import.h tests/npu_pack_import_test.cpp
The npu-pack wave that just merged touched the same region (#1644, #1703, and #1686/#1687/#1688 before them). Rebase onto main and resolve; the earlier READY then applies to the rebased head once its tests are re-run. Nothing in the substance has changed. This is mergeability only.
|
Correction to my CHANGES just above: I listed #1688 among the merged PRs, but it is still open. The commits on main that touched |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at a20fcb1: CHANGES
This re-stamps, in the merge script's format, my verdict above at this same head (a20fcb1d). No new review: the head is unchanged since that verdict, and its reasons stand as written.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at a20fcb1: READY
Re-posted in the format the merge script reads (the earlier review above carries the reasoning; this line names the same head).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at a20fcb1: CHANGES
Disregarding my re-posted READY above: it landed four seconds after a CHANGES at the same head and overrode it. That was a formatting re-stamp selecting the latest READY instead of the latest verdict — my error, not a changed judgement. The verdict is CHANGES at a20fcb1.
|
Orchestrator: conflicts with main in npu/pack_import.cpp/.h and the test: main added read_embedding_component (embedding.onnx for hybrid packs) at the same namespace boundary where you exposed load_geometry/read_dims. Both should survive. Merge main, resolve, push without force; also addresses the CHANGES verdict. |
Conflicts with main resolved by taking main's files and re-applying this PR's hunk on top, because main solved a *neighbouring*
case: `fill_from_model_config_impl()` reads a separate `model_config.json` (config.model_attributes.text_config) and fills
gaps with the same never-override rule. That does not cover this pack -- `amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu` states its
geometry in `config.json`'s own `text_config` and its `model_config.json` has `model_attributes: null`, so main's path adds
nothing and the refusal ("the pack states no intermediate_size") stood.
So the lift now reads `config.json`'s inline `text_config`, with main's rules: gaps only, never overriding, `vision_config`
untouched, and the source string names it (`+ config.json text_config`) so a refusal still says where a number came from.
Test re-expressed against main's exported helpers (`read_dims_exported`, plus a new `load_geometry_exported` next to it) and
it asserts the four dimensions, head_dim/kv_heads, that the source names text_config, that `vision_config` is untouched, and
that a top-level key wins:
text_config: hidden 2560, 36L, 32 heads, intermediate 9728, vocab 151936
all npu-pack checks passed
|
Merged main ( How the conflict was resolved, and why the change is still needed after main: { "type": "onnxmodel", "config": { "model_file_name": "vision.onnx", "model_attributes": null, ... } }— Test re-expressed against main's helpers rather than re-introducing my own: main exports Worth recording for the merge: the conflict was in |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at +54/-0 head — small, and it does the two things a geometry fallback has to do: it fills gaps without overriding, and it says where each number came from.
The change. A hybrid pack can keep its text tower under text_config inside config.json with model_config.json carrying model_attributes: null, so fill_from_model_config_impl() adds nothing and the importer refuses with "the pack states no intermediate_size". The lift is guarded correctly:
if (cfg.contains("text_config") && cfg["text_config"].is_object())
for (auto it : cfg["text_config"]) {
if (cfg.contains(it.key()) || it.value().is_null()) continue; // never override; never copy a null
cfg[it.key()] = it.value(); ++from_text_config;
}
if (from_text_config > 0 && source) *source += " + config.json text_config";Three things I want to name as right. Top-level wins (cfg.contains(it.key()) first), which is the only safe precedence for a fallback. Nulls are skipped, so a "intermediate_size": null in the text tower cannot shadow a real value or arrive as zero. And the provenance string is appended — source now says which file answered, which is the difference between "we read it from somewhere" and a geometry you can audit. That last one is what turned a similar question (#1657's intermediate_size derivation) from arguable into checkable.
The test asserts the values and the provenance path, via an exported load_geometry_exported() rather than by re-deriving: hidden 2560 / layers 36 / heads 32 / intermediate 9728 / vocab 151936 and head_dim 128 / kv_heads 8 — the exact seven numbers the real Qwen3-VL-4B declares one level down. Exporting the reader for the test, instead of testing read_dims() in isolation, is what makes the rule (gap-filling, top-level precedence) the thing under test.
One question worth answering before merge, not a blocker: the lift copies every key present in text_config that the top level lacks, not just geometry keys. For a VL pack that is harmless (extra keys are ignored by the geometry reader), but it does mean a key that means something else at the top level of a future config could be shadowed by the text tower's spelling of it. If that matters, an allow-list of the keys load_geometry() reads would make the rule explicit; if it does not, a one-line comment saying "extra keys are inert" would stop the next reader wondering.
Not verified: the real Qwen3-VL-4B-Instruct pack end to end (not on this box, and the pack is one of the four no-fconst ones whose import is a separate question); the crafted config exercises the lift and the precedence rule, which is the part this PR adds.
Rebased (main is already merged here), verified locally, and the only red check is
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 1650dc8: READY
My CHANGES at a20fcb1 was about mergeability only. 1650dc8 resolves it (git merge-tree onto main is clean, and there are no conflict markers) and re-applies the lift on top of main's fill_from_model_config_impl(), which is the right reading: main's path covers model_config.json, and this covers config.json's inline text_config, the case Qwen3-VL-4B-Instruct_rai_1.9.0_npu needs.
Checked at this head:
- The lift follows main's rules: it fills gaps only, never overrides a top-level key, skips nulls, leaves
vision_configalone, and the source string names the origin (+ config.json text_config). - The two
fill_from_model_config_implcalls (:967 and :996) are in different branches (theconfig.jsonpath and thegenai_config.jsonfallback), so nothing is appended twice. npu_pack_import_test tests/goldengives "text_config: hidden 2560, 36L, 32 heads, intermediate 9728, vocab 151936" and "all npu-pack checks passed".
Note (non-blocking): in the config.json branch, genai_config.json's keys fill gaps before text_config is lifted, so for a key both state, genai wins over the HF config's own text tower. If that order is deliberate, a one-line comment would say so; if not, lifting text_config first matches "the text tower IS the decoder this importer builds".
#1638's base now carries its own config.json:text_config lift (1650dc8), which is the lift this branch added. Its version is taken whole in npu/pack_import.{cpp,h} (anonymous namespace kept, load_geometry_exported), and this branch's duplicate lift, namespace move and its own test_text_config_geometry are dropped in favour of the base's. What this PR still adds on top of #1638: test_hybrid_pack_import, the end-to-end import of a crafted pack whose geometry is only in text_config (engine#1648). Verified: all npu-pack checks pass; with the base's lift disabled the suite fails ('no hidden_size'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 31c0794432da01f0e93fe537f788aa64b28e2868 — Re-verdict at the new head: text_config is now lifted out of config.json as well, and main is merged in. The lift-earlier version was the narrower one; taking both positions means a pack that states its geometry at either level is read, which is what a hybrid family actually ships.
The risk of a two-position lift is that the second position silently wins over the first — worth one line of comment in the code saying which takes precedence when both are present, but not a blocker: an inconsistency that loud would show up in the fixture this PR carries.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 31c0794: READY
Re-verdict at the merge-of-main head (31c0794). git show --cc is empty, so the merge was clean with nothing resolved by hand. The branch's own diff has exactly the same added and removed lines as my READY at 1650dc8 (0 new, 0 gone), and there are no conflict markers. At this head npu_pack_import_test tests/golden gives "text_config: hidden 2560, 36L, 32 heads, intermediate 9728, vocab 151936" and "all npu-pack checks passed". My earlier note (genai keys fill gaps before text_config) still stands, and is still non-blocking.
#1638 merged and this PR now targets main. One conflict in the test's main(): main's side was empty, this branch's test_hybrid_pack_import() call kept. Diff vs main: tests/npu_pack_import_test.cpp +62, unchanged. npu_pack_import_test: all checks passed, the hybrid import arm runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict in npu/pack_import.cpp: this branch's intermediate_size_from_graph()/find_token_fconst_exported() and main's load_geometry_exported() (#1638) sit side by side; both kept. Diff vs main is this PR's own. npu_pack_import_test: all checks passed, the im-from-node-attrs arms run (10240 derived; fused gate_up 9728, not 4096). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngine#1648) (#1656) * npu-pack: a hybrid pack keeps its geometry under text_config, so read it there `amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu` (Ryzen AI 1.9, 3.05 GB) refuses at `npu-pack info`: 1bit npu-pack: the pack states no intermediate_size: config.json has none, and the FFN projections are not initializers of text.onnx (on the hybrid line only the attention/head qweights are in the main graph) The second half of that is true -- this pack keeps its weights in `text.onnx.data` (2.65 GB of external data), not as inline initializers -- but the first half is a missing lookup one level down. The pack's config.json is a `qwen3_vl` config: its top level is {architectures, image_token_id, model_type, text_config, tie_word_embeddings, video_token_id, vision_config, vision_end_token_id, vision_start_token_id} and **every** geometry key lives inside `text_config` (hidden_size 2560, num_hidden_layers 36, num_attention_heads 32, intermediate_size 9728, vocab_size 151936, head_dim 128, num_key_value_heads 8, rope_theta 5e6). The text tower is the decoder this importer builds, so `load_geometry()` now lifts `text_config`'s keys -- only where the top level lacks them, never overriding, and with `vision_config` untouched -- and records `config.json:text_config` as the source, so a refusal still says where a number came from. That is the same nested-config rule `tools/npu_plan.py` already applies to `rope_scaling`/`mrope_section` for VL packs. `load_geometry()` and `read_dims()` move out of the file's anonymous namespace so the test can drive them directly (they are declared in `pack_import.h` with the reason). Test (`tests/npu_pack_import_test.cpp`): a crafted `qwen3_vl` config with the pack's real numbers plus a `vision_config`, asserting the four dimensions and the source, that `head_dim`/`num_key_value_heads` come with them, that `vision_config` is not touched, and that a **top-level key wins** over the nested one. Runs in the existing `npu_pack_import` test: text_config: hidden 2560, 36L, 32 heads, intermediate 9728, vocab 151936, source config.json:text_config all npu-pack checks passed The pack itself is not committed (AMD files are run as published, never redistributed); the numbers in the test are the pack's own declared geometry, which is public in its config.json. * tests: import a crafted pack whose geometry is only in text_config (engine#1648) The importer's PACK leg never runs anywhere: `add_test` passes only the golden dir, `argv[2]` is the pack dir, and the tree carries no pack fixture -- so the shape a hybrid pack actually has was exercised by no test end to end, while the geometry change that depends on it (#1638) was evidenced only by the crafted readers. `test_text_config_geometry` checks the reader. This runs `import_pack()` on a crafted pack of that shape and checks what lands in the container -- a Qwen3-VL-shaped `config.json` with hidden_size/intermediate_size/num_hidden_layers/ num_attention_heads/vocab_size only inside `text_config`, a vision_config beside it that must be left alone, the crafted .fconst the final-norm test already uses (8 blocks, eps headers at both boundaries) and an embed initializer. hybrid import: geometry only in text_config -> imported (tail envelope included), container hidden_size from text_config Two properties it pins that the reader test cannot: the import runs to completion on a pack with no top-level geometry (so nothing downstream requires one), and `out/config.json` states the text tower's hidden_size -- a serve reads the same geometry the import used, which is the failure this class would produce (import fine, serve with hidden 0). Stacked on #1638 (that is the change whose shape this is). Runs in the default ctest path: no pack directory, no device. * tests: drop tests/golden/textcfg/config.json, a test-run artefact committed by the previous merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: pi-npufeat <npufeat@strixhalo> Co-authored-by: bong-water-water-bong <bong@users.noreply.github.com> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e group (#2071) `pr-agent` triggers on `pull_request` and on `issue_comment`, and both land in the same concurrency group (`pr-agent-<number>`) with `cancel-in-progress: true`. So every comment on a PR - a review verdict, an `/ask`, a status note - cancels the review of the head it is commenting on, and the job restarts from the beginning on a single serialised runner. Measured 2026-11-10 on PR #2007 (`27a9788`): the review ran 19:21:26 -> cancelled 19:30:31, with `build`, `hrx_build` and CodeQL green and every READY verdict posted; the PR sat with `pr_agent: COMPLETED/CANCELLED` while the runner queue held other work. That is the "red on pr_agent alone" the issue thread (#1993) reports for #1633, #1638, #1675 and #2007. Group by event name as well as number: a `synchronize` still cancels the review of the head it replaced (a newer head makes the older review moot), and a comment no longer cancels a push-triggered review. Verified on the expression: push -> `pr-agent-2007-pull_request`, comment -> `pr-agent-2007-issue_comment`, newer push -> same group as the earlier push. Refs #1993. Co-authored-by: bong-water-water-bong <bong@water>
amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu(Ryzen AI 1.9, 3.05 GB, one of the four no-fconst packs) refuses atnpu-pack info:The second clause is true — this pack keeps its weights in
text.onnx.data(2.65 GB of external data), not as inline initializers — but the first clause is a missing lookup one level down. Itsconfig.jsonis aqwen3_vlconfig whose top level is onlyand every geometry key lives inside
text_config: hidden_size 2560, num_hidden_layers 36, num_attention_heads 32, intermediate_size 9728, vocab_size 151936, head_dim 128, num_key_value_heads 8, rope_theta 5e6.The change:
load_geometry()liftstext_config's keys — only where the top level lacks them, never overriding,vision_configuntouched — and recordsconfig.json:text_configas the source so a refusal still says where a number came from. The text tower is the decoder this importer builds, which is why lifting is the right default rather than a special case. It is the same nested-config ruletools/npu_plan.pyalready applies torope_scaling/mrope_sectionfor VL packs.load_geometry()/read_dims()move out of the file's anonymous namespace so a test can drive them; they are declared inpack_import.hwith that reason stated.Test (in the existing
npu_pack_importtest): a craftedqwen3_vlconfig carrying this pack's real numbers plus avision_config, asserting the four dimensions and the source, thathead_dim/num_key_value_headscome with them, thatvision_configis untouched, and that a top-level key wins over the nested one:What this does not claim. It gets the pack past this refusal. Whether it then imports depends on the external-data path (
text.onnx.data) and on the FFN qweights being reachable there — the message's second clause is a separate question and I have not answered it yet. The next sweep row after this merges will say exactly which refusal comes next, and that is the point: one named refusal at a time rather than one aggregate "unsupported".No pack contents are in the tree — AMD files are run as published and never redistributed; the numbers in the test are the pack's own public
config.jsongeometry.