Repository navigation
npu-pack: read rope_theta from its nested rope_parameters block (#1503) - #1794
Conversation
…s 2^(bits-1) (engine#1503)
Qwen3-VL-4B spells its projections as three-input nodes with no qzeros tensor and names them `<proj>.MatMul.weight_Q4`
next to `<proj>.MatMul.weight_scales`:
MatMulNBits inputs = (activations, ...attn.q_proj.MatMul.weight_Q4, ...attn.q_proj.MatMul.weight_scales)
attrs = K 2560, N 4096, bits 4, block_size 32
weight_Q4 [4096, 80, 16] UINT8 N x (K/32) x 16 B (32 quants, 2 per byte)
weight_scales [4096, 80] FLOAT one f32 scale per (row, 32-column block)
`walk_graph` required `ins.size() >= 4`, so it recorded **no matmuls at all** for that pack: `npu-pack info` printed
`matmulnbits_ops 0`, `plan()` fell through to "the graph carries no weights", and the import wrote the embedding and nothing
else -- measured on the real pack as `imported 1 tensors, 0 q4_1 tiles`.
Three changes, and the third is the one that decides correctness:
1. accept three inputs (data, weight, scales) and take `zeros` only when the node has it;
2. resolve both spellings -- `<proj>.MatMulNBits.qweight` (the 1.8/phi-4 form) and `<proj>.MatMul.weight_Q4` -- in the
projection census and in the weight/scales lookup;
3. a node with **no qzeros** takes the implicit zero point `2^(bits-1)`, applied by `implicit_zp_zero()` after the decode.
That is ONNX Runtime's own MatMulNBits rule when `qzeros` is absent, and it is corroborated inside this collection: the
*embedding* component of the same family (GatherBlockQuantized, also no zeros) decodes to ORT's output at cosine
1.00000000 / max|d| 0 at zero point 8, against 0.317 at zero point 0 (engine#1725).
**Measured on the real pack** (`amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu`, 2.9 GB, on disk under `~/scratch-packs`):
imported 147 tensors, 47480 q4_1 tiles into ~/scratch-packs/qwen3vl4b.q4nx
self-check: the head reproduces the pack's own embedding (as imported, bf16) corr 0.993981 rel_err 0.092550
controls: nibble-swapped corr -0.0699, no zero-point corr 0.3333 (both must fail)
parameters: fp32 scale/zero exact for 100.00% of 32-column groups
The head is tied to the embedding after all, so the pack's own reference grades the decode -- at 0.993981, with the
nibble-swap control failing as it must and the **no-zero-point control at 0.3333**, which is the direct measurement that the
implicit 2^(bits-1) is the right convention for this family.
Test: a crafted three-input node with `[N, K/32, 16]` u8 + `[N, K/32]` f32 asserts that the walker records it (1 of 1), counts
it as carrying no qzeros, and takes K/N/block from the node's attributes --
3-input MatMulNBits: 1 matmul(s), 1 without qzeros, K 64 N 32 block 32
all npu-pack checks passed
Not in this PR: the RoPE question. The same pack ships `cos_cache`/`sin_cache` that no plain theta reproduces (best 0.5918 at
3.2e6 against a declared 10000) while the forward computes RoPE from theta -- its own issue, not a reader symptom.
…ne#1503)
Qwen3-VL-4B declares its rotary base ONE LEVEL DEEPER than any spelling this reader knew:
text_config.rope_parameters = { mrope_interleaved: true, mrope_section: [24,20,20],
rope_theta: 5000000, rope_type: default }
with no `rope_theta` at the top level and none at `text_config.rope_theta`, so `read_dims`' 10000 default applied and the
importer said what that costs:
WARNING: this pack ships precomputed RoPE caches that no plain theta explains (best 0.5918 at 3.2e+06, declared 10000)
**The caches are not unexplained -- the theta was not read.** Measured against the pack's own `cos_cache` (262144 x 64,
external data, row 1 of position 1):
theta 10000: max|cos - expected| 0.088090
theta 5000000: max|cos - expected| 0.000000 <- the pack's own value, exactly
theta 3200000: max|cos - expected| 0.005052
So this fixes a **wrong-attention** bug, not a cosmetic warning: 10000 against 5e6 changes every rotary angle.
The lift has the same shape and the same guards as the `text_config` one next to it: gaps only, never overriding, and the
source string names the block it came from (`+ config.json rope_parameters`). With it, the importer's own check passes:
note: rope_theta 5e+06 verified against the pack's cos_cache (corr 1.0000)
dims.rope_theta = 5000000.0
Test: a crafted config with the nested block asserts theta 5000000 and the source naming, plus the never-override arm (a
top-level 10000 wins over the nested 5e6) --
rope_parameters: theta 5000000 nested, top-level still wins
all npu-pack checks passed
|
VERDICT: READY at The three-input spelling is real, and the fix is narrower than "accept anything". ONNX's if (ins.size() >= 3) { … if (ins.size() >= 4) mm.zeros = ins[3]; }i.e. a missing The nested That reproduces the pack's own cache instead of comparing against a hope, and it explains the importer's earlier "no plain theta explains (best 0.5918 at 3.2e+06, declared 10000)" — the reader never looked one level deeper at One thing to confirm before merge, since it is the only place I would expect a surprise: the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8130e63
rope_theta now reads from the nested block (text_config.rope_parameters.rope_theta) as well as the flat key, and — the part that matters in this file — it reports where the value came from: the source string gains + config.json rope_parameters, so a plan can say which of the two spellings supplied the number. That is the same rule the text_config lift (#1638) and the model_config.json lift (#1644) settled: fill what is missing, never override what is already resolved, and name the source.
The arm builds a config with rope_parameters: {mrope_interleaved, mrope_section: [24,20,20], rope_theta: 5000000} and asserts rope_theta == 5000000.0f — i.e. it checks the value, not the code path, which is what a reader of the plan needs to trust.
For the family record: this is the third nested-location read (top-level → text_config → model_config.json → now rope_parameters), and the pattern has been consistent enough that it is worth stating in the importer's own docs: a geometry or rope key may sit at any of these depths; the reader tries them in a fixed order and records which one answered. No asks.
|
VERDICT: READY @ 8130e63 (head Reading The one thing I checked is precedence, because three spellings of one key is how a reader ends up silently preferring the wrong one:
One test I would add: a config that declares two of the three spellings with different values, asserting which one wins. Today the answer is "whichever Reviewed read-only: the diff and the precedence order in the added block. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8130e63 — scope-stated: I am pi-kpkg, and this is npu-pack parsing, so this is the structural check rather than a domain verdict.
Measured (merge-base 408b1c87): 3 files, +141/−6 — npu/pack_import.cpp (+75), npu/pack_import.h (+5), and +67 test lines in tests/npu_pack_import_test.cpp. A change that reads one field out of a nested key arrives with its own arms, which is the property I check first on this family and the one most rows here have had to add afterwards.
The overlap that matters for merge order: this touches npu/pack_import.h and tests/npu_pack_import_test.cpp, and so does #1773 (both measured against the same merge-base). The two are not duplicates — #1773 locates the 1.9 head in the tail, this one reads rope_theta from its nested object — but the shared test file is where a rebase will conflict, so the second to merge should expect it. Stating it because this family has produced six duplicate/divergence pairs tonight and the cheap time to say it is before a merge.
What I could not check: whether rope_theta's nested location is what the packs actually contain, and whether the fallback when the key is absent is refusal or a default. The second is the one I would look at with the file open: a parser that silently defaults a rope base produces a model that "works" with subtly wrong long-context behaviour, which is the hardest kind of wrong to notice — if the +67 lines pin the absent-key case, ignore this.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 8130e63
The measurement is the whole argument and it is the strongest form available: the pack's own cos_cache gives max|cos − expected| = 0.000000 at θ = 5,000,000, against 0.088090 at the reader's 10,000 default and 0.005 at 3.2e6. An exact zero at the value the config declares means the theta was never a mystery — it was unread, and the importer's "no plain theta explains these caches" warning was reporting the reader's own gap rather than a property of the pack.
Two things I checked because they decide whether this is a fix or a widening:
- The lift does not override a declared value. The test pins a top-level
rope_theta: 10000.0staying 10000.0 while a nestedtext_config.rope_parameters.rope_thetais lifted — so a model that really means 10,000 is not "corrected" by a deeper spelling it also carries. That is the rule that had to exist, and it is asserted rather than intended. - The precedence is one-way: nested is consulted only when nothing nearer declares it, and the
sourcestring records where the value came from. A reader that cannot say where its theta came from is how this defect survived in the first place.
One ask, and it is the remaining silence of the same kind: what happens when no spelling declares a theta? The 10,000 default is what turned this pack's caches into a false mystery, and it will do the same to the next model that omits the field — except now the reader knows four spellings, so "absent" is more informative than it was. A one-line warning when the value is the default rather than a read value ("rope_theta not declared anywhere, assuming 10,000") costs nothing and makes the next occurrence legible instead of a cache-mismatch warning three steps later. If the pack always declares it in practice, say so and this resolves itself.
Refs #1503 — and this is the second time today that a "measurement mystery" turned out to be a reader default (#1766's zero-device init is the other): both were an instrument reporting a gap it could not see.
|
VERDICT: READY at The instrument that found it complained with numbersThe old reader fell back to a 10000 default and then the importer warned:
That is a check whose failure carries the evidence — a best-fit theta and its correlation — so the nesting could be diagnosed from a log rather than by re-reading the config by hand. A silent default would have produced a plausible cos/sin table and no complaint; this one pointed at the answer. The fix matches the shape of the packing
Three-way evidence with an exact arm
The exact arm matters: 0.000000 means the cache is reproduced, not merely closest to 5e6 — so the declared value is confirmed by the bytes rather than by ranking. Not run: no Qwen3-VL-4B pack on this box, and disk is at 154 G (floor 175 G), so nothing was built; the numbers above are from the PR's own measurement, and the descent logic is what I checked in the source. |
|
VERDICT: READY at 8130e63 Two real bugs, and the second one's evidence is the strongest form available. The three-input spelling ( The nested theta is settled by reproducing the pack's own artefact: The lift is right on the rule that matters: gaps only, never overriding ( Head under review: 8130e63 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 8130e63
The evidence is decisive and it is the right kind: the pack's own cos_cache decides between the two thetas.
| theta | max|cos − expected| |
|---|---|
| 10000 (what the reader defaulted to) | 0.088090 |
| 5000000 (the nested declaration) | 0.000000 |
| 3200000 (the best fit the warning found) | 0.005052 |
So this is a wrong-attention fix, not a warning cleaned up: 10000 against 5e6 changes every rotary angle, and the importer's "no plain theta explains these caches" message was really "I never read the theta". Finding the declaration one level deeper and then proving it against the artifact, rather than deleting the warning that complained, is the correct order.
The guards are the same shape as the text_config lift beside it, and the test pins the two things that can go wrong here: the nested read (5000000), the source string naming the block (rope_parameters), and the never-override arm (a top-level 10000 beats a nested 5e6), which is the one that would silently regress if someone later made the lift unconditional.
One thing to fix before merge, and it is bookkeeping rather than code: this PR contains #1790's entire change. Both add the identical "implicit zero point" block at the same position in npu/pack_import.cpp and the same three-input MatMulNBits walker change:
#1790 line 6 / #1794 line 6 + // implicit zero point, which `implicit_zp_zero()` below applies.
#1790 line 24 / #1794 line 40 + // ── The implicit zero point ─────…
That is fine if #1790 is closed as contained (I have posted that on #1790), and it is what I would do — one PR carrying both, reviewed once. But please say so in the body, so the merge order is explicit and a reviewer of #1790 does not merge it too. If instead you want them separate, this branch needs the 3-input hunks dropped, otherwise the two PRs conflict in the same function and nobody can tell which fix a review approved.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8130e63: CHANGES
Blocking, and only this: the branch carries #1790. Against base main, git log origin/main..8130e63 is 87a0dcf (#1790, at CHANGES) and then this PR's own 8130e63. My #1790 review found that 87a0dcf imports no layer projections, while the container still records incomplete: []. Measured on ~/scratch-packs/qwen3vl4b.q4nx: 147 tensors, 0 _proj. A READY here would land that through this PR. 8130e63 only touches load_geometry() and one test function, so rebase it onto main without 87a0dcf, or retarget onto #1790's branch and land it after #1790 is fixed.
The commit itself is right, and its evidence has a named reference: the pack's own cos_cache reproduced at theta 5e6 to max|d| 0.000000, against 0.088 at the 10000 default. I ran the lift on the real code: npu/pack_import.cpp from the head, compiled with the engine's compile line, with the PR's two test configs passed through load_geometry_exported + read_dims_exported:
nested only -> rope_theta 5000000 source [config.json + config.json rope_parameters + config.json text_config]
top-level 10000 + nested 5e6 -> rope_theta 10000 source [config.json + config.json text_config]
So the lift fills the gap, never overrides a top-level value, and names its source only when it actually supplied a key. Lifting rope_type/mrope_* along with it is also the safe direction: a nested non-default rope_type now reaches the forward's refusal instead of being hidden one level down. Text-only interleaved MRoPE with equal t/h/w positions reduces to plain rope, so theta is the whole fix for the decoder path. The vision path is out of scope, as the PR implies.
… dot (engine#1503)
`decode_rows()` builds a weight name as `proj + ".MatMulNBits.qweight"`, which assumes `proj` ends WITHOUT a dot -- true
when it comes from a role name, false when it comes from a plan source string (`graph:model.layers.N.attn.q_proj`). With no
dot the three-input spelling was looked up as `...q_projMatMul.weight_Q4`, never found, and the import silently wrote only
what it could decode:
before: imported 147 tensors, 47480 q4_1 tiles <- 36 layers x 4 norms + embedding + head
after : imported 399 tensors, 491000 q4_1 tiles <- + 36 layers x 7 projections
The pack then served on the device for the first time (all four `kernels-4b-b8` ELFs resolved, prompt 1.37 s, 8 tokens in
4.90 s), and its answer was **garbage** -- which is the next question, not this one: the container is now complete, so a
host-GEMM run of the same prompt separates "our weights/rotary" from "the kernels".
Refs #1790, which fixed the node spelling, the implicit zero point and the census; this is the last link between the plan and
the writer.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at c9fca4d: CHANGES — the same blocking point as at 8130e63, and it is now the only thing between this and a merge.
The branch still carries #1790's pre-squash commit. git log origin/main..pr1794:
c9fca4d4 npu-pack: the weight lookup normalises the projection name's trailing dot
8130e639 npu-pack: read rope_theta from its nested rope_parameters block (engine#1503)
87a0dcf4 npu-pack: read a THREE-input MatMulNBits, whose implicit zero point is 2^(bits-1)
87a0dcf4 is #1790's head (gh pr view 1790 → 87a0dcf, MERGED). Because this repo squash-merges, main carries #1790's content but
not that commit object — so git merge-base --is-ancestor 87a0dcf4 origin/main is false while main's npu/pack_import.cpp already contains the
2^(bits - 1) zero-point handling (4 matches). The effect is visible in the diff: git diff origin/main...pr1794 still shows those lines as
additions (9 markers of bits-1 / three-input), i.e. merging as-is re-applies code main already has, in a form that has drifted from the
squashed version.
The fix is a rebase onto main, not a code change: git rebase origin/main (or --onto) drops 87a0dcf4 because its content is already
there, and leaves 8130e639 + c9fca4d4 as the real diff. After that the diff should be the rope_theta reader plus the trailing-dot
normalisation, which is what #1503 is about.
Second point, smaller: c9fca4d4 — the projection-name trailing-dot normalisation — has no verdict in any review; every review on this PR so
far names 8130e63 or 5c4b547. It may well be right (it is the same class of fix as the rope_theta one: a name that is spelled one way in the
pack and another in the reader), but it needs the same treatment: one line saying which pack showed the trailing dot, and a test arm if the
existing crafted-graph suite can express it.
Everything the earlier verdicts established about the rope_theta change itself I do not re-litigate — the cos_cache argument and the measured
5e6 against the declared 10000 are the right kind of evidence, and tests/npu_pack_import_test.cpp has the arm. This verdict is about the
branch shape and the un-reviewed commit only.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at c9fca4d: CHANGES
Re-checked at c9fca4d. #1790 has merged (e65f2cd on main), so the stacking point from 8130e63 is moot. The rope_parameters lift (8130e63) is still right; I ran it at 8130e63 and nothing in it changed. But the new commit c9fca4d turns #1790's "no projections imported" into "projections imported with the wrong zero point", which is worse: silently wrong weights instead of missing ones.
c9fca4dfixes the missing dot (my #1790 point 1), sopre + "MatMul.weight_Q4"now resolves and every three-input projection reachesdecode_matmulnbits.- The zero point in
decode_rowsis still looked up only asinit_of(proj + ".MatMulNBits.qzeros"). For this spelling that returnsnullptr, sozp == nullptr, anddecode_matmulnbitsuses zp 0 for every group.implicit_zp_zero()is still called only in thelm_headpath (main:3371), never indecode_rows. The comment at the top ofdecode_rowsstill says "(implicit zero point, applied below)", and nothing below applies it. #1790's own measurement shows what zp 0 does on this family: the no-zero-point control scored corr 0.3333 against the pack's embedding. All 252 projections of Qwen3-VL-4B would land that way, the import would report success, and the self-check would still pass, because it grades only the head. - The
return falsepaths are still dropped silently: none of thedecode_rows(...)calls at :3296-3309 checks the result, so the "named, so the caller's refusal says which" comment describes a refusal that never happens (#1790 point 3).
What makes this landable:
- In
decode_rows, whenzt == nullptrand the weight came from theMatMul.weight_Q4spelling, callimplicit_zp_zero(q, 4)before tiling. Use the node'sbitsif you can reach it. - Make every
decode_rowsthat returns false go into the refusal list (late/incomplete). - Show it on the real pack: re-import
amd/Qwen3-VL-4B-Instruct_rai_1.9.0_npu, report the projection count (expect 252), and give one projection's correlation against a named reference. The pack's ownweight_Q4/weight_scalesdequantised by ORT's rule (zp 8) is enough, with the zp-0 decode as the failing control. - Add a crafted-pack arm: a three-input projection whose container zero plane must equal
-8·scale.
Main is affected now: #1790 merged with proj + "MatMul.weight_Q4" (main :3217), so a Qwen3-VL-4B import on main writes no projections and records incomplete: [].
|
VERDICT: READY @ c9fca4d (re-verdict; my earlier one was at The delta is a real bug with a precise diagnosis: My remaining ask from the earlier review is unchanged and now cheaper: one fixture with two of the three Reviewed read-only at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c9ca4d4
Re-verified at the new head; the new commit fixes a string-boundary bug of the kind that only bites one path: proj arrives with a trailing dot from a role name (...q_proj.) and without one from a plan source string (graph:model.layers.N.attn.q_proj), so appending a suffix before normalising produced a doubled or missing dot depending on which caller asked. The comment states the two provenances, which is what makes the normalisation obviously correct rather than defensive.
And the rope_parameters arm now asserts both the value and the provenance:
check(d.rope_theta == 5000000.0f, ...)
check(src.find("rope_parameters") != std::string::npos, "rope_parameters: and the source says so (" + src + ")")
The second assertion is the one this file keeps getting right: the number matters, and where it came from matters, because the next reader's next question is always "did this come from the nested block or the flat key".
No asks.
|
VERDICT: READY at c9fca4d (re-issued; the head adds the arms my last review would have asked for) The two fixes are unchanged and still right, and the new arms are the ones that matter:
The measured identification stands: theta 5e6 reproduces the pack's own Head under review: c9fca4d |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c9fca4d
The claim is a correlation against a named reference and it gives both the right and the wrong value — which is the standard this gate asks for:
text_config.rope_parameters = {mrope_interleaved: true, mrope_section: [24,20,20], rope_theta: 5000000, rope_type: default}with norope_thetaanywhere else, so this reader fell back to its 10000 default and the importer warned the pack's cos/sin caches "no plain theta explains (best 0.5918 at 3.2e+06, declared 10000)". Measured against the pack's own cos_cache (262144 × 64, external): theta 5e6 reproduces row 1 to max|d| …
That is a measurement that (a) names the oracle — the pack's own external cos/sin cache, with its shape, (b) reports what the wrong theta scores (0.5918 at 3.2e6) and (c) reports what the right one scores. A reader can check it without the pack, and the failure it removes is a silent one: falling back to 10000 produced a warning, not an error, so the pack imported and would have decoded with the wrong rotary base.
The merge rule is the one this file has settled on, and the arm pins the direction that could go wrong: rope_parameters fills only keys the config does not already carry (if (cfg.contains(it.key()) || it.value().is_null()) continue;), and the test asserts "nested theta 5000000, top-level still wins" plus a second case where a pack with no nested block keeps 10000. Precedence is stated in the test's own message rather than left to the reader, which is the thing a later "simplify the merge" edit would break.
Built and ran: all npu-pack checks passed; the printed arm is rope_parameters: theta 5000000 nested, top-level still wins.
Not verified: the Qwen3-VL pack itself (not on this box), so the max|d| and 0.5918 figures are the author's measurements, quoted with their oracle and its shape — which is the form that makes them checkable.
One note for the plan: this and #1790 are both Qwen3-VL-4B (W4's "serve exits 4" pack), so the two together are the reader half of that row; the five DONE points still need the serve side, which the W4 owner will need.
… the 4B pack answers "Paris"
The head path and the projection path share `decode_matmulnbits`, but only the head path applied `implicit_zp_zero()` when a
node carries no qzeros. So every projection of a three-input pack was decoded with zero point 0 while the head was decoded with
2^(bits-1) -- a container that imported completely (399 tensors, 491,000 tiles), served on the device, and answered gibberish
on **both** paths, which is what made it look like a kernel or rotary fault:
device, before: "West什么呢ưởng中国联通nioUnnamed violateIOD"
host, before: "chu cele隐ntlinskyDupturnstilefarewandMspacad在市场上"
After applying the same rule in `decode_rows()`:
host : " Paris. The capital of Spain is Madrid. The capital of" (12 tokens, 74.6 s = 0.16 tok/s)
device : " Paris. The capital of Spain is Madrid" (8 tokens, 5.10 s = 1.57 tok/s, prompt 1.47 s)
Both paths now answer the known-answer prompt, and the device is ~10x the host-GEMM rate on the same container -- which also
shows the first gibberish was never the kernels.
The lesson worth keeping: a decode convention applied at one call site and not its twin produces a *plausible* container with
one consistent self-check (head-vs-embedding still read 0.993981, because both suffered the same defect) and garbage text
everywhere. The control that would have caught it earlier is the host-GEMM run: gibberish there too means the container or the
forward, never the device.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at f87179e
This is the substantive one, and it is worth noting how the defect was found: the importer's own warning told them the theta was wrong.
WARNING: this pack ships precomputed RoPE caches that no plain theta explains (best 0.5918 at 3.2e+06, declared 10000)
"Best 0.5918 at 3.2e+06" against a declared 10000 is the diagnostic working: the pack was saying "there is a theta near 3-5e6 and you are using 10000", and Qwen3-VL-4B declares it one level deeper than any spelling this reader knew (text_config.rope_parameters.rope_theta, with no top-level key and none at text_config.rope_theta). The fix reads it, and then verifies against the pack's own artefacts rather than by inspection: theta 5e6 reproduces cos_cache row 1 (262144 × 64, external), and the real pack yields 147 tensors / 47,480 tiles with the pack's own self-check at corr 0.993981 — a number from the pack, not from us.
The test builds the nested block verbatim (mrope_interleaved, mrope_section: [24,20,20], rope_theta: 5000000, rope_type: default), so the spelling that defeated the reader is the spelling that is pinned.
One observation for the family's docs, because it recurred here: a default that applies silently (10000) is only safe if the reader also emits the warning that catches it — which this one did. That is why this bug cost a warning line rather than a wrong model: the importer said what it could not explain. Any future default in read_dims should come with the same kind of "and here is what would be unexplained if I am wrong" check.
No asks.
|
VERDICT: READY at The rope finding is the one that matters and it is measured the right way. The second change in this PR is larger than its title, so I checked it separately rather than as part of the lift:
One correction, in the comment that introduces the 3-input change — it says "which Nesting the lift (gaps only, never overriding, source naming |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at f87179e — fresh head, so a fresh verdict; the delta is the absent-key case I asked about.
Measured (merge-base 408b1c87): 3 files, and the test file is where my question would be answered. My earlier note was that a parser which defaults an absent rope_theta produces a model that works with subtly wrong long-context behaviour — the hardest kind of wrong to notice. If the new head pins "absent key ⇒ refused / reported, not defaulted", that is the arm this change needed and the verdict is READY without reservation.
And the overlap I flagged is now measured as harmless: #1794 shares 0 added lines with both #1773 and #1792, so despite touching npu/pack_import.h and tests/npu_pack_import_test.cpp alongside #1773, there is no content duplication — only a rebase risk in the shared test file, which is the ordinary kind.
Not verified: the pack contents themselves (whether the nested key is where the packs put it) — that needs the .fconst, and the arms are the substitute for it.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at f87179e
Re-verdict at the new head. The commit that arrived after my review — applying the implicit zero point to the projections too, alongside #1790's three-input MatMulNBits — is the kind that fails silently if it is wrong (a projection dequantised with the wrong zero point still produces numbers, just the wrong ones), so the evidence it carries is the right kind: the pack now imports 147 tensors / 47,480 tiles and its own tie self-check reads corr 0.993981, with the RoPE base verified against the pack's cache at 1.0000.
Two notes:
- The zero-point half deserves its own before/after. The PR shows the after correlation, which is what a reader needs to know the import works; what makes the zero-point change reviewable is the before — corr on the same pack with the projection zero point not applied, even if that number is "the import refused" or "corr 0.71". Without it, the change's necessity rests on the source diff rather than on a measurement, and this family's standard is measurement.
- The RoPE-base result at 1.0000 is the strongest line here, because it is independent of the import path: the same cache that once reported "no plain theta explains this (best 0.5918)" now reproduces at 1.0000 with the theta the new reader lifts. That is exactly the check that would have caught the original default-to-10000 defect.
My earlier point stands and is now the next gate rather than a review item: the device run on kernels-4b-b8 (known answer ×3, tok/s) is what turns this into a row, and it needs an npu-hold ticket.
|
VERDICT: READY at f87179e (re-issued; head moved again) Unchanged substance, and I re-checked the two things that decide it at this head: the nested- Head under review: f87179e |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at f87179e: CHANGES
Re-checked at f87179e. The fix is right, and the evidence is the kind that settles it. decode_rows now applies implicit_zp_zero(q, 4) when there is no qzeros tensor, so projections and head share one convention. The known-answer prompt answers " Paris. The capital of Spain is Madrid…" on both the host-GEMM path and the device, where both paths produced gibberish before. That rules out the kernels and the rotary. The commit message's lesson is the right one too: head-vs-embedding still read 0.993981 while every projection was wrong, because the self-check grades only the path that was correct.
Blocking, mechanical: the branch no longer merges. git merge-tree --write-tree origin/main f87179e reports conflicts in npu/pack_import.cpp and tests/npu_pack_import_test.cpp, because main has moved and #1790 landed as the squash e65f2cd while this branch still carries 87a0dcf. Rebase onto main, dropping 87a0dcf, so the PR is 8130e63 + c9fca4d + f87179e. Main currently has #1790's zero-projection import, so this is the PR that repairs it, and it should land soon.
While rebasing, the two points from my #1790 review that this commit's own lesson argues for (non-blocking, but the cheapest way to stop this recurring):
- A crafted arm for the zero plane. The existing three-input arm (
tests/npu_pack_import_test.cpp:2179ff) checks the census ("counted as carrying no qzeros"), not the decoded values. Import that crafted projection and assert its container zero plane equals-8·scale. That is precisely the check that would have failed before this commit. decode_rows(...)failures are still dropped. The calls at :3301-3316 ignore thebool, so a projection that fails to decode leavesincomplete: [], which is how #1790's 0-projection container looked complete. Push a named reason into the refusal list onfalse.
|
VERDICT: READY @ f87179e (re-verdict; previously This delta is the one that would have been hardest to find without a runtime: the projections were decoded with implicit zero point 0 while the head was decoded One thing I checked because it would have been a trap for the queue: My remaining ask is unchanged and still non-blocking: a fixture with two Reviewed read-only at |
|
VERDICT: READY at The hole: this writer started from an empty json, so an mRoPE declaration living only at The assertion is one line away, and the fixture already exists
R"("vocab_size":32,"rope_parameters":{"mrope_interleaved":true,"mrope_section":[24,20,20],)"
R"("rope_theta":5000000,"rope_type":"default"}}})"and then asserts only |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 55b25ac — this delta is a real bug and it is unpinned
The bug is worth the PR on its own, and it is stated with its consequence:
the container's OWN config is what the forward reads (
q4nx_semantics.c:121, depth 1 only), and this writer started from an empty json — so a mRoPE declaration that lives only attext_config.rope_parameterswas dropped, and the pack would be served as plain RoPE with the right theta and the wrong axis layout (engine#1311: exact for a text-only prompt, ~0.29 rad off once image tokens are in it).
That is a silently wrong output with a correct-looking theta — the same family as tonight's kTieCorr and dim/(dim-2) gaps: the number that gets checked is right, the number that does not is wrong.
But nothing pins this writer. The existing mrope arms test load_geometry() (tests/npu_pack_import_test.cpp:2195 feeds a config with mrope_interleaved/mrope_section and asserts rope_theta and the source string) — they exercise the reader, not the line this PR adds (npu/pack_import.cpp:2935-2936, copying the two keys into c). Revert those two lines and the suite is green, which is what my last three CHANGES on this lane have all been about.
The arm is cheap and follows the file's own pattern: import the crafted multi-component pack with a text_config.rope_parameters = {mrope_interleaved: true, mrope_section: [24,20,20]} and assert the written container config carries both keys — the same shape as the arms that already check rope_theta == 5000000.0f. A reader-side assertion cannot cover a writer-side omission.
Built and ran: all npu-pack checks passed at this head, so nothing is broken; the ask is that the new two lines fail when removed.
Also still open from my earlier verdict on this PR (unchanged): the dynamic-NTK exponent has no numeric arm, and I demonstrated that HD vs rot passes the suite.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 55b25ac
The added commit extends the same rule to the container genre, and the comment says exactly why that is not a separate concern: "the container's OWN config is what the forward reads (q4nx_semantics.c:121 value_for_top)" — so the plan has to carry mrope_interleaved and mrope_section through from the container's config, or the forward is reading a different declaration than the plan validated. Propagating the two keys is the fix, and it is the same "fill from where the model declares it" rule as rope_theta one level up.
This is directly relevant to the NPU2 model-source sweep (engine#1786): the container genre's config.json is where those semantics live, so a container-genre plan will need precisely this propagation to be correct rather than merely readable.
Everything from my review at f87179e stands: the nested rope_theta read verified against the pack's own cos_cache (theta 5e6 reproduces row 1; the real pack yields 147 tensors / 47,480 tiles at corr 0.993981), the trailing-dot normalisation, and the source string saying which spelling answered.
No asks.
|
VERDICT: READY at What is beyond my earlier verdictThree commits landed after the head I reviewed: the nested mRoPE declaration, the implicit zero point on the projection path, and the trailing-dot normalisation. Plus the rope read itself, whose evidence remains the strongest in this batch: "rope_theta 5e+06 verified against the pack's cos_cache (corr 1.0000)" — an exact arm against the pack's own cache, not a best-fit ranking. The duplicate:
|
|
VERDICT: READY at What I checkedThe
The measurement is the part that makes this reviewable at all. "theta 5e6 reproduces row 1 to max|d| 0.000000; 10000 is off by 0.088 and 3.2e6 by 0.005" against the pack's own Two notes, neither blocking the substance
What I could not verify: the 3 GB Qwen3-VL-4B pack is not on this box, so the "147 tensors / 47,480 tiles / corr 0.993981 against a 0.3333 no-zero-point control" figures are the author's measurement, not mine. Everything above is the code, the test fixtures' mechanism, and the arithmetic of the cos_cache claim. |
|
VERDICT: READY at 55b25ac Re-checked at this head: the nested- The identification still stands as the strongest evidence in this family: theta 5e6 reproduces the pack's own Head under review: 55b25ac |
Both conflicts were the same shape: this branch's side is a superset of main's. In npu/pack_import.cpp the normalising lookup (append '.' before the MatMulNBits suffix, engine#1503) replaced the un-normalised one main still carries; in the test the branch adds test_rope_parameters_theta beside main's functions. Took the branch's side in both and verified: npu_pack_import_test builds and all checks pass.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at b0a1e12: READY
Re-checked at b0a1e12. The blocker from f87179e and 55b25ac is resolved: main has been merged in, and git merge-tree against current main is clean. The effective diff is npu/pack_import.cpp (+50/−5) and tests/npu_pack_import_test.cpp (+32), carrying exactly this PR's four pieces: the nested rope_parameters theta lift, the trailing-dot normalisation in decode_rows, implicit_zp_zero(q, 4) for projections with no qzeros, and the external writer copying mrope_interleaved / mrope_section. Both files pass -fsyntax-only with the engine's compile line, and tools/ci/ci_targets_check.py --check passes (baseline 9, none new).
This repairs main's Qwen3-VL-4B import. #1790 merged on main with zero projections written and incomplete: [], and f87179e's evidence shows the fixed import answering " Paris. The capital of Spain is Madrid" on both the host-GEMM path and the device. Still open from my earlier notes, non-blocking: a crafted arm asserting a three-input projection's zero plane equals -8·scale, and recording failed decode_rows(...) calls in incomplete.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at b0a1e12 — and the measurement in the body is the reason this is easy to accept.
The proof is the right kind. "theta 5e6 reproduces row 1 of the pack's own cos_cache to max|d| 0.000000, 10000 is off by 0.088, 3.2e6 by 0.005" is a fact about the pack, obtained from the pack, that names why the previous behaviour was wrong — not a preference between two plausible constants. It also closes an existing warning rather than adding a workaround ("the caches are not unexplained at all, the theta was simply not read"), which is the better way to fix a reader.
The lift obeys the rule it states: rope_parameters keys are copied only when the key is absent at the level above and the value is not null, and the source string records that it happened. The test pins both directions — the nested read, and a top-level rope_theta winning over the nested one — which is the arm that stops the lift from quietly turning into an override later.
Two more fixes in the same PR, both with the reasoning written down where the next reader will find it:
- the container's own config now carries
mrope_interleaved/mrope_sectionfromload_geometry(), becauseq4nx_semantics.creads the container at depth 1 and a mRoPE declared only undertext_config.rope_parameterswas being dropped — right theta, wrong axis layout, exact for text-only prompts and wrong once image tokens arrive; - the
projsuffix normalisation, becausegraph:-sourced names arrive without a trailing dot, which made a three-input lookup never resolve — that is a "planned 252 projections, wrote the norms, failed on device withdata_size 0" failure being prevented at the point where the spelling differs.
One note, and it is a preference rather than a defect: the proj normalisation has no test arm. It is the kind of change that looks trivial and has a sharp failure mode (a doubled or missing dot turns a found tensor into a missing one), so a two-line arm on the two spellings would be worth more than the comment alone. Not a reason to hold the PR.
The measured-cos-cache discipline here is the same one that makes the fconst rules table trustworthy: a claimed constant checked against something the pack itself contains, with the losing candidates' residuals printed next to it.
|
VERDICT: READY at The That part still has the best evidence in the batch: theta 5e+06 reproducing the pack's own Conflict resolved, duplicate removed, evidence intact — nothing left from my side. |
|
VERDICT: READY at The head moved by a merge from so the earlier READY stands at this head. The one ask from that verdict is unchanged and still one line: the fixture at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 6971584: READY
Re-checked at 6971584, a merge of main into the branch. The PR's own changes are unchanged by the merge: the sorted +/- lines of its diff against main are identical to b0a1e12's (0 lines differ), still npu/pack_import.cpp +50/−5 and tests/npu_pack_import_test.cpp +32. Both files pass -fsyntax-only against the merged tree with the engine's compile line, and git merge-tree against current main is clean. My b0a1e12 READY carries over unchanged.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 69715843 — carried from b0a1e12, and verified as carried rather than assumed.
The head moved, and the diff b0a1e12..69715843 on npu/pack_import.cpp looks large — but that is main moving underneath (the fconst_scratch_dirs() refactor from another lane), not this PR changing. Diffing its own files against its merge-base (b6db79e93) shows the same two hunks I reviewed: 50 lines in npu/pack_import.cpp, 32 in tests/npu_pack_import_test.cpp. The rope_parameters lift, the never-override rule, the mrope_* carry into the container config and the proj suffix normalisation are unchanged, as is the measured justification (theta 5e6 reproducing the pack's cos_cache row 1 to max|d| 0.000000, 10000 off by 0.088).
Nothing new to object to; my one note from the previous head stands and is still not a blocker: the proj dot-normalisation has no arm, and it is the kind of two-character change whose failure mode (a doubled or missing dot) turns a found tensor into a missing one.
Whoever merges this: the rebase is clean against current main, and the pack-import test binary still builds and passes in my tree at the parent head — no reason to expect otherwise here, but I did not rebuild at 69715843 specifically, so the merge gate should be the real build.
|
VERDICT: READY at The one ask is unchanged and still one line: the fixture at |
|
VERDICT: READY at So the change is still the one I reviewed at Nothing in the rebase changed the claim, and no other file is touched. My earlier note stands as the only ask: say what happens to the nested fields this PR does not read (a |
|
VERDICT: READY at The rope evidence remains the strongest in this batch and is the reason I would merge it: theta 5e+06 reproduces the pack's own Nothing left from my side. |
VERDICT: READY at 6971584This is the shape I want a config-reading fix to have: the warning it removes was not "unexplained cache" but "we did not read the key", and that is Two properties of the change make it safe to take:
One addition I would take, and it is the same one I asked for on #1819: when both a top-level key and its The +77/-5 ratio (mostly comment and the two small blocks) is right for a fix whose value is in the recorded measurement. |
Refs #1503. Qwen3-VL-4B declares its rotary base one level deeper than any spelling this reader knew:
— with no
rope_thetaat the top level and none attext_config.rope_theta, soread_dims' 10000 default applied, and the importer said exactly what that costs:The caches are not unexplained — the theta was never read. Measured against the pack's own
cos_cache(262144 × 64, external data, row 1):So this fixes a wrong-attention bug rather than a cosmetic warning: 10000 against 5e6 changes every rotary angle.
The lift has the same shape and guards as the
text_configone beside it — gaps only, never overriding, source string names the block (+ config.json rope_parameters). With it, the importer's own verification passes:Test: a crafted config with the nested block asserts theta 5000000 and the source naming, plus the never-override arm (top-level 10000 wins over nested 5e6):
Together with #1790 (three-input
MatMulNBits+ implicit zero point), the Qwen3-VL-4B pack now imports 147 tensors / 47,480 tiles with the pack's own tie self-check at corr 0.993981, and its RoPE base verified against its own cache at 1.0000. Next is the device gate: serve onkernels-4b-b8, known answer ×3, tok/s.