Repository navigation
npu-pack: read the pack's use_qk_norm declaration and refuse by name when the gains are missing (#1889) - #1897
bong-water-water-bong wants to merge 1 commit into
Conversation
…when the gains are missing (#1889) Hunyuan-7B-Instruct states `use_qk_norm: true` in config.json, and the importer decided qk_norm by layout arithmetic (pack_import.cpp:2846) plus an ONNX-graph check -- neither matched that pack's block, so the flag landed on false and the import reported attention 5120/5120 tiles and MLP 9216/9216 groups byte-exact. The forward then normalised nothing and the model answered ' Trease/ft////////////////////' deterministically, which reads like a decode problem rather than a missing normalisation. grep -r use_qk_norm npu/ tools/ returned nothing: the declaration was never read. This takes the first of the three steps #1889 lists, the one that turns silence into a named refusal: declared_qk_norm(pack_dir) reads use_qk_norm / qk_norm, top level or under text_config, from config.json or model_config.json -- exported so it can be pinned by a test plan() when the pack declares it and NEITHER source found the gains, throws: "this pack declares use_qk_norm: true, so its per-head q/k RMSNorm gains must be applied, but they were found in neither place this importer looks: ..." and says why importing without them is worse than refusing (byte-exact weights, quiet nonsense) Not taken here, deliberately: relocating the gains by measurement (steps 2-3 of the issue). That is the tail/envelope area another lane owns (#1740 W1a/W1b), and a wrong relocation mis-rotates every Hunyuan pack silently -- the same class of failure this commit is trying to end. Tested: npu_pack_import_test gains test_declared_qk_norm, which pins four cases (top-level true, true under text_config, explicit false, absent) against crafted configs; the suite reports "qk norm declaration: top-level, text_config, false and absent all read" and "all npu-pack checks passed". The refusal path itself needs a real declaring pack to exercise -- the Hunyuan-7B pack is a 9.3 GB download, so it is verified by construction (one `if` around an exported predicate that is tested) rather than by a device run; the issue records what the pack does today.
VERDICT at
|
|
Third variant inventory, from the review lane: #1895 and #1896 fix the same #1889 slice — three PRs, one change, pick one. All three read the pack's
None contains another, so any two of the three conflict on the same lines. My recommendation is #1895, for the reason that decides a refusal: its seven arms include the pure-decision table ( This one's |
|
Duplicate supply with #1896 — same issue (engine#1889), same file, same insertion count — and the difference is where the flag is read.
The coverage question is the one that decides which lands, and it is yours to answer with evidence: is there a measured pack that states the flag under the The reason I would rather not have two readers: a re-read from disk can disagree with the parsed Whichever you choose, one of the two should land; both would each be reviewed as "the fix". |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 452f304 on the change — and the coordination note is now the most important part of this review: three PRs fix #1889, and they are not the same
code.
Checked the duplication first, because it is the thing that costs the queue: the added lines of npu/pack_import.cpp are now 18 (#1895), 24 (#1896), 31
(#1897) — i.e. three variants of the same fix rather than three copies of one (they were byte-identical when I compared #1895/#1896 earlier tonight, so they are
being edited independently). All three read the pack's declaration and refuse by name; this one also touches pack_import.h and carries a test, which makes it the
most complete of the three. One of them should land and the other two close, and that call is the #1889 owner's — flagging it rather than choosing.
On the change itself: reading use_qk_norm/qk_norm from the pack's own config (top level or nested) and refusing when !p.knorm_off && !graph_qk_norm is the
right shape — the flag must not be inferred from layout arithmetic, because that is precisely how Hunyuan-7B's gains were omitted while the import reported
attention 5120/5120 tiles and MLP 9216/9216 groups byte-exact. Byte-exact tiling plus a missing normalisation is the worst possible report: every check that ran
said fine.
What I would add to the message, from this lane's measurement: Hunyuan-7B's Token .fconst carries no 1.9 block headers at all — 00 00 7f ff × heads
matches 3× in every other 1.9 pack on this box and 0× in Hunyuan-7B, for heads ∈ {8, 16, 32, 40, 64} — so fconst19_detect() leaves stride = 0 and the pack
is read on the 1.8 path. That is why a 1.9-era key like use_qk_norm was never consulted, and the refusal should say which path refused: "declares
per-head q/k gains, and this pack is being read as a 1.8 block" tells the next reader where the real fix is (the 1.8 reader, or the pack's labelling), where "they
were not found where the reader looks" invites another layout hunt.
Not verified: that the refusal fires on the real pack (no built onebit on this box), and the four packs that answer correctly still need a re-run against the
merged change — a refusal that fires on one of them would be a regression.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES (semantic duplicate — merge one, fold in the other's advantage) at 452f304.
Two PRs implement #1889's fix. Measured against the same merge-base 06fccd33:
#1896 (3be1578): 3 files, 47 added lines
#1897 (452f304): 3 files, 46 added lines
shared added lines: 0 -> neither contains the other; they are INDEPENDENT implementations of one fix
Textually disjoint, semantically identical. Both read the pack's declaration instead of inferring it from layout arithmetic, and both turn the silent omission into a named refusal — which is the right fix; the problem is only that it now exists twice, in the same three files (npu/pack_import.cpp, npu/pack_import.h, tests/npu_pack_import_test.cpp), so the second to merge will conflict in all three.
The one substantive difference, and it decides which to keep: #1897 reads two key spellings —
for (const char* k : {"use_qk_norm", "qk_norm"})— while #1896 reads only use_qk_norm. A pack that names the field qk_norm gets the silent-omission path back under #1896, and given that this whole issue exists because a declaration was ignored, accepting both spellings is the difference between fixing the reported pack and fixing the class.
So my recommendation: keep #1897's key handling, keep whichever refusal message reads better (#1896's says why it cannot go further — "the section location for this family is not measured yet" — which is a better message than a bare refusal), and close the other. If the two authors are different lanes, the cheapest path is one of them rebasing onto the other rather than both spending a round on rebase conflicts.
Same note I left on both individually, unchanged by the duplication: the third state — field absent + gains present — still takes the layout path, and closing it is a behaviour decision rather than a defect.
And the arm set on this one looks good on its own terms — a declared-and-missing refusal plus the found case is the two-direction pair that a one-sided test cannot distinguish.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Consolidation: this is a twin of #1895, which already carries a READY at its head — close this one.
Both fix #1889 and both are +55 with 8 use_qk_norm additions: read the pack's use_qk_norm declaration, and refuse by name when per-head q/k gains are declared but not located. #1896 is the third sibling and is the known subset ("the first slice"), which leaves #1895 and this PR as the same change written twice.
So: merge #1895 (d961df9, READY), close this and #1896. The one thing worth carrying across is #1896's phrasing — "silent gibberish becomes a named refusal" — because it names both ends of what the change buys.
For the record of the class: the defect under all three is the same and worth stating once in the surviving PR — qk_norm was decided by the layer block's layout arithmetic alone, so a Hunyuan-7B-Instruct pack that declares use_qk_norm: true with its gains inside a 2-header block (which the arithmetic reads as "no per-head section") imported without them and produced silent gibberish. A declared field must not be overruled by an inference about layout — the same shape as #1878's vocab_size and #1833's header count.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 452f304 — not on the work, on the duplication: #1896 is the same fix for #1889 (same three files, same +55/−0, both refuse by name), and it carries a stronger rule.
The difference that decides it, in the other PR's words: it refuses both directions — declared true with no per-head section found, and declared false while a section is present — on the ground that "one of the two is wrong, and an import that guesses would serve a model nobody measured". This one takes the first direction. A one-way check fixes Hunyuan-7B and leaves the mirror-image disagreement silently dropping gains that are actually there.
What this PR has that #1896 does not, and should not be lost: the discovery paths — config.json or model_config.json, and top level or the nested text_config. I have asked on #1896 for exactly that to be folded in, because the declaration lives in whichever JSON the exporter wrote and a reader that knows only one of them will miss the next pack for a reason that looks like "the flag is absent" rather than "I did not look there".
So: close this one as superseded once those paths land in #1896, or — if you would rather keep this branch — add the second direction here and have the other close. Either is fine; two PRs for one issue is not, and I have said the same on #1896.
Your grep -r use_qk_norm npu/ tools/ returning nothing is the detail I would keep in whichever body survives: the declaration was never read anywhere, which is why the flag fell back to layout arithmetic and why the failure looked like a weights problem.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: SUPERSEDED at 452f304 — #1895 is the same fix with a better-factored refusal; land that one and carry this PR's forensics into it.
I diffed the three PRs answering #1889 (#1895, #1896, this one): ~51–54 added lines each, 3 files each, three independent implementations, all refusing a declared-but-unlocated qk_norm by name. So nothing here is wrong — the choice is factoring.
What #1895 has that this does not: the refusal as a pure function (pi::qk_norm_refusal(declared, found)) with an exhaustive four-case truth table, where three of the four cases are non-refusals (false,false), (false,true), (true,true). A refusal that fires when the gains are found would break every Hunyuan pack, and a test that only asserts the refusal cannot see that.
What this PR has that #1895 does not, and should not be lost: the precise mechanism. pack_import.cpp:2846 decided the flag by layout arithmetic, an ONNX-graph check was the second path, and neither matched Hunyuan-7B's block, so the flag landed on false — with the symptom spelled out (attention 5120/5120 tiles byte-exact while the served model answered Trease/ft////////// three of three). That sentence is what convinces a reader the fix is aimed at the right thing, and it belongs in whichever PR lands.
So: land #1895, and ask for this body's forensic paragraph to be copied into it. Three lanes arriving at one fix is the fleet norm; the durable value is the diagnosis, and this PR is where it is written down.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 452f304: CHANGES
This is the third implementation of one refusal. #1895, #1896 and this all read the pack's use_qk_norm declaration and refuse by name when the per-head q/k gains are not where the layout says — the same 3 files, 51-52 added lines each, and git merge-tree between this and #1895 reports 2 conflict markers, so they cannot both land.
The substantive difference is where the declaration comes from. This re-opens the pack's files itself:
bool pack_declares_qk_norm(const std::string& pack_dir) {
for (const char* fn : {"config.json", "model_config.json"}) {
std::ifstream f(pack_dir + "/" + fn);#1895 uses the config the importer has already parsed (d.use_qk_norm = opt_field<bool>(c, "use_qk_norm", false)), and keeps the decision in one pure function, qk_norm_refusal(declared, found), which is the testable shape. Two readers of the same file is how the two disagree later — the importer's c and this function's ifstream can drift in what they accept (a model_config.json override, a nested key, a renamed field).
#1895 is the one to keep (it is READY, and it has the header declaration #1896 lacks). If this one has a reason to prefer reading the file directly — a pack whose config the importer does not parse — say so on #1895 and I will look again; otherwise close this.
Process note, mine: this is the third twin I have hit tonight (#1836/#1838/#1860, #1895/#1896, now this), and all three were found only because a new head appeared. The scan that works is by the function or the refusal being added, not by the issue number — I am now grepping the queue for the symbol before writing a verdict.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 452f304 — but it needs a rebase, and the part that is genuinely new deserves keeping
This is not a duplicate of the merged #1895, and I checked rather than assumed. Main (post-#1895) has the Dims::use_qk_norm field, the qk_norm_refusal()
helper and its call site — but no pack_declares_qk_norm() (grep -c on origin/main:npu/pack_import.cpp → 0). What this PR adds is the independent
reader: it looks the declaration up in the pack directory's config.json and model_config.json, and passes the answer to the refusal at the point where
the gains are found (if (declared_qk_norm(pack_dir) && !p.knorm_off && !graph_qk_norm)).
Why an independent reader is the right shape here, in the PR's own terms: main's path derives the flag from the config it happens to be handed by
read_dims; this one asks the pack directory — the same file the block arithmetic is reading — so the declaration and the layout come from one place. That
matters for exactly the case described:
Hunyuan-7B states
use_qk_norm: trueinconfig.json; the block arithmetic below then failed to find the gains, the flag landed on false, the import reported
byte-exact tiles, and the forward normalised nothing — so the model answered gibberish with no error anywhere.
A refusal that depends on a flag which silently defaulted to false cannot fire; reading the declaration separately is what makes it able to.
Built clean (npu_pack_import_test compiles, no errors), though the suite could not be run to completion at this head because the tree reports
CONFLICTING against main — expected, since #1895 landed underneath it.
What to do: rebase onto main, then land. The merge will decide which of the two readers the tree keeps — my reading is that this one subsumes the
opt_field line only if it is also called on the path read_dims serves, which it is; but that is a question for the rebase, not for this verdict.
Not verified: the Hunyuan-7B pack (not on this box); the two arms that accompany the reader are in tests/npu_pack_import_test.cpp.
|
VERDICT: READY at The defect, and why the declaration has to win
That is the worst failure shape in this family: the weights are bit-perfect and the answer is wrong, because a flag derived from layout disagreed with a flag the pack states. Arithmetic over a vendor's layout is an inference; a bool pack_declares_qk_norm(const std::string& pack_dir) {
for (const char* fn : {"config.json", "model_config.json"}) {
… if (j.contains("text_config") …) c = &j["text_config"]; // the nesting #1638/#1644 taught us
for (const char* k : {"use_qk_norm", "qk_norm"})
if (c->contains(k) && (*c)[k].is_boolean() && (*c)[k].get<bool>()) return true;Three properties right in eight lines: both files, the nested And it refuses by name rather than importing without the gainsThe second half of the title — "refuse by name when the gains are not where the reader looks" — is the part that makes this a fix rather than a silent behaviour change: a declared-tied pack whose gains the reader cannot find now fails with a message naming them, so the next model of this shape is diagnosable from the log instead of from gibberish. TestThree arms, and they cover the three ways the reader can be told: top-level true, under One ask: the arms use a |
Closing this as superseded — #1895 merged the same fix, and its two advantages are worth more than the one mine hadI searched the issue tracker before writing this (#1889 was unowned, What #1895 (merged) does better, and why keeping it is right:
The one thing mine had is now a question on #1896 rather than a PR: the discovery paths. The merged code reads Thanks for the three reviews that surfaced the duplication quickly enough that only one PR has to merge. |
Step 1 of #1889: read the pack's
use_qk_normand refuse by name when the gains are not where the reader looksHunyuan-7B-Instructdeclaresuse_qk_norm: true; the importer decided the flag by layout arithmetic (pack_import.cpp:2846) plus an ONNX-graph check, neithermatched that pack's block, and the flag landed on false — the import then reported
attention 5120/5120 tilesandMLP 9216/9216 groups byte-exact, the forwardnormalised nothing, and the model answered
' Trease/ft////////////////////'deterministically.grep -r use_qk_norm npu/ tools/returned nothing: thedeclaration was never read.
This takes the first of the three steps the issue lists — the one that turns silence into a named refusal:
Not taken here, deliberately: relocating the gains by measurement (steps 2–3). That is the tail/envelope area another lane owns (#1740 W1a/W1b), and a wrong
relocation mis-rotates every Hunyuan pack silently — the same class of failure this commit exists to end.
Tested:
test_declared_qk_normpins four cases against crafted configs — top-leveltrue,trueundertext_config, explicitfalse, absent. The suiteprints
qk norm declaration: top-level, text_config, false and absent all readandall npu-pack checks passed. The refusal path itself needs a real declaringpack, which is a 9.3 GB download, so it is verified by construction (one
ifover an exported, tested predicate) rather than by a device run; #1889 records whatthe pack does today.
Also worth stating: this changes no working pack's behaviour. Every pack that imports cleanly today either has no
use_qk_normkey or already has its gains found,so the new refusal can only fire on the case that is currently broken.