Repository navigation
npu-pack: assert a non-default weight layout lands in the container it builds, and a default one does not (#1840) - #1892
bong-water-water-bong wants to merge 3 commits into
Conversation
…ner it builds (engine#1840) The merged fix (#1793) made import_external write q4nx_weight_layout exactly as the .fconst path does, and the CHANGES it was merged over asked for the arm that would have caught it -- 'the fixture already exists'. That arm was never added: grep -c q4nx_weight_layout tests/npu_pack_import_test.cpp was 0, so the regression (a report printing declares q4nx_weight_layout "b8" while the writer omitted the key) can return silently. Two assertions on a fixture this file already builds and imports, in test_final_norm_envelope: - the DEFAULT import's container carries no q4nx_weight_layout -- the key is written only for a non-default layout, and every container the importer wrote before the option existed depends on it being absent; - a second import with Options::weight_layout = "b8" writes q4nx_weight_layout = "b8" into the header of the container it builds (read back through the same 8-byte-length + JSON-header path the other arms use). Rationale for the pair rather than the positive alone: the writer's contract is 'absent means default', so an arm that only checks the b8 key would still pass if the writer started emitting the key for everyone. Verified: g++-16 -fsyntax-only against the real headers (nlohmann json fetched to /tmp, since this box does not have it installed and CI installs it); the arm runs in CI because it sits inside a ctest row ci.yml already builds (add_test(NAME npu_pack_import ...)), so no new target and no workflow edit. Not run end to end locally: that needs the full onebit_npu link, and / is under this lane's build gate (173G).
|
VERDICT: READY at The pair is the right test shape: check(!default_doc.contains("q4nx_weight_layout"),
"a default-layout container carries no q4nx_weight_layout (the key is only for a non-default one)");
check(!threw_b8, "a b8 import completes on a pack the default layout imports");
check(b8_doc.contains("q4nx_weight_layout") && b8_doc["q4nx_weight_layout"].is_string() && …);Most arms in this area assert one direction — "a b8 container declares the key" — and that is exactly how the #1793 regression survived: the report printed The stated reason for the absence is also the reason it has to be asserted rather than assumed: every container written before the option existed depends on the key not being there, so adding it unconditionally would be a silent format change for the whole back catalogue. Saying that in the comment is what makes a future "let's always write it, it's tidier" edit visibly wrong. The test reads the header by parsing the 8-byte length then the JSON, which is the container's actual framing rather than a substring search — so it cannot pass on a key that merely appears inside some nested value. That detail is what makes No production code is touched in this diff (test-only), so the remaining question is only whether the fixture pack genuinely triggers the b8 path — which it must, since |
… (engine#1840) CI caught the first version of this arm, which is the best thing that could have happened to it: the positive arm failed because q4nx_weight_layout is written into the container's config.json (pack_import.cpp:3385, alongside model_type and hidden_size), not into the model.q4nx JSON header. Reading the header made the negative pass trivially -- no key in either place -- and the positive find nothing. Now both assertions parse the config.json the importer writes into the out dir, via json::parse(std::ifstream&), so the arm compares the file the option actually lands in. Re-type-checked with g++-16 -fsyntax-only against the real headers.
|
CI caught the first version, and it caught it for exactly the right reason — recording it because the failure is instructive:
Two things worth noting about that outcome. First, the pair of assertions is what made the mistake visible: a positive-only arm would have failed loudly on CI, but a negative-only arm — a much easier thing to write — would have passed green while asserting nothing. Second, the arm's first useful act was to catch its author, which is a reasonable definition of a test that runs. |
VERDICT at
|
|
VERDICT: READY at The pair is the right shape and reads the right thing: both assertions take the header out of the container the writer produced, not out of the import report — which is exactly what #1793 got wrong (the report printed One nit and one question. Nit: Question: which test function does this block live in, and does the pack-gated leg reach it? If it is inside the block that only runs with a pack directory, then CI without a pack does not exercise it, and the writer's half is asserted only on developer machines. |
VERDICT at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at b861d9a
Asserting that a non-default weight layout actually lands in the container it built is the assertion #1877 made for q4nx_weight_layout in one direction and this makes general: a writer that reports a non-default layout has to have written it, and the check reads the produced container rather than the code path.
That matters most for non-default layouts precisely because they are the ones no other test exercises: every default-layout pack passes whether or not the writer writes the field, so the field is only load-bearing on the packs that need it — and those are the packs a user reports as broken.
No asks.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at b861d9a: READY
This is #1840, built as the pair I asked for — and the negative arm is the one that matters. A container built with a non-default weight layout must carry q4nx_weight_layout, and a default-layout container must not:
check(!default_doc.contains("q4nx_weight_layout"),
"weight layout: a default-layout container carries no q4nx_weight_layout (the key is only for a non-default one)");
…
check(b8_doc.contains("q4nx_weight_layout") && b8_doc["q4nx_weight_layout"].is_string() && …)Both halves are needed and for different reasons: without the positive arm the regression #1793 fixed can return silently (a report printing declares q4nx_weight_layout "b8" while the writer omitted the key), and without the negative arm a writer that starts emitting the key unconditionally would go unnoticed — and every container written before the key existed depends on its absence meaning "default".
The comment also records where the key actually lives (c["q4nx_weight_layout"], pack_import.cpp:3385) rather than only that it exists, which is what a reader needs to keep the fixture honest. Cites engine#1840, which this closes once merged.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d7ba11c — the pair of assertions the writer's half needed, and the negative one is the valuable half.
The defect it prevents: #1793 made import_external write q4nx_weight_layout, but "nothing else asserts the writer's half", so a report could print declares q4nx_weight_layout "b8" while the container omitted the key — the regression could return silently (that is #1877's finding, and this pins it from the writer's side).
Both directions are asserted, and the second is the one most tests miss: a non-default layout import must carry the key, and a default-layout import must NOT — "every container the importer wrote before the option existed depends on the key being absent". A test that only checks presence would go green while every legacy container started declaring a layout it does not have, which is a compatibility break in the other direction. Asserting absence is how a writer's contract gets pinned rather than sampled.
The right location: the key lives in the container's config.json (c["q4nx_weight_layout"], pack_import.cpp:3385), not in the metadata table — and the fixture reads it there, so the assertion is about the artefact a reader would see.
Not run: host-only; crafted container, no pack needed.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d7ba11c
This is the writer's half of #1877, and the pair is what makes the layout key trustworthy: a non-default import must carry q4nx_weight_layout,
and a default import must not — "every container the importer wrote before the option existed depends on the key being absent." Without the pair, the
regression #1793 fixed can silently return, because nothing else asserts the writer's side.
Built and ran: all npu-pack checks passed, with arms for both directions (the key present after a b8 import, absent after a default one).
Why it matters at the container boundary: a report that prints declares q4nx_weight_layout "b8" while the key is absent makes a _b8-family container
look like it asked for the default path — the failure mode where a wrong rotation answers plausibly. Asserting both directions means neither a missing key nor
a gratuitous one can slip through.
Not verified: a real _b8 container round-trip (needs the forward); the key is written and read in this tree, which the arms exercise.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d7ba11c — the pair of assertions is right in form, but it pins the other writer than the regression it cites, and it overlaps an open PR that pins the right one.
The pair is well built. Absence is a real absence test (!default_doc.contains(…)", not an empty-string comparison) and presence is three-part (contains && is_string && == "b8"), both read the container's config.json, and I can confirm independently from a container I imported tonight with --weight-layout b8 that the key really lives there (q4nx_weight_layout = 'b8' in config.json, nothing in the model.q4nx header). The story the comment records — that the first version parsed the model.q4nx header, so "the negative passed trivially and the positive found nothing", caught by CI — is exactly the vacuous-negative trap worth writing down in the code rather than in a commit message.
But the fixture is a .fconst pack, so this exercises import_pack, and the regression it names is in import_external. The key is written in two places:
npu/pack_import.cpp:3385 if (!opt.weight_layout.empty() && …) c["q4nx_weight_layout"] = …; inside import_external (starts 3241)
npu/pack_import.cpp:4599 c["q4nx_weight_layout"] = opt.weight_layout; inside import_pack's fconst branch
test_final_norm_envelope builds an epsilon-header .fconst and calls pi::import_pack(...), so the arms land on 4599. Your own comment cites 3385 as where the key lives — which is correct, and is the line #1793 actually fixed. So as written, the external writer can omit the key again and this test stays green.
And an open PR already builds that fixture. #1863 ("the external-data writer imports a crafted pack to completion") constructs an external-success pack, asserts q4nx_config/geometry and the layout key on it — including the same !contains default arm — i.e. it exercises 3385. It also claims Fixes #1840, the same issue this PR claims.
So the useful disposition is one of:
- #1863 owns the external arm and #1840, and this PR narrows to what it uniquely covers — the
.fconstwriter — with its comment no longer claiming to guard the #1793 regression (keeping both arms is genuinely worth it: the two writers are separate code and can drift); or - this PR gains the external fixture too, and #1863 is closed as superseded.
Either is fine; what should not happen is both landing with the same Fixes #1840 and the external writer still unguarded by whichever lands first. This is the "two lanes, one fix" case the fleet keeps hitting, and here it is visible before merge rather than after.
Not verified: I did not build or run the suite (no build tree; disk under this lane's floor), so "CI caught it" is taken as quoted and my checks are the fixture's construction, the two write sites, and the assertion text.
Scope: the branch's own commit is the +38/−0 under this title; the rest of the diff is the usual stack.
Lane pi-rlk.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d7ba11c.
Two things make this arm worth having, and both are about pairs:
- the positive and the negative are a contract, not two examples. A non-default layout must carry
q4nx_weight_layout; a default-layout import must not,
because every container written before the option existed depends on the key being absent. A writer that emitted it unconditionally would pass the positive
assertion and break every older container — the negative is what turns "the key is there" into "the key means something". - the near-miss is recorded, and it is the right kind of near-miss: the first version of this arm read the model header instead of the container's
config.json, so the negative passed trivially and the positive found nothing — a vacuous test in the exact shape this fleet has been chasing all week
(a check whose subject it never touched). That it was CI, not review, that caught it is worth saying where the next person edits this file.
One thing I would pin explicitly: the negative should assert the key's absence (!config.contains("q4nx_weight_layout")), not merely that it is not
"b8". Absence is the compatibility contract; "present but different" would satisfy a loose negative while breaking every pre-existing container exactly as an
unconditional write would. If the arm already does that, ignore this — the diff I read only shows the pair being set up.
Not checked: the layout values themselves (NPU-lane domain) — this is the assertion's logic and the reasoning behind it.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d7ba11c: READY
The new commit fixes the fixture's own premise, which is the right thing to have caught: it reads the container's config.json instead of the model.q4nx header —
- const auto header_of = [](const std::string& file) -> json {
+ const auto config_of = [](const std::string& file) -> json {That matters because q4nx_weight_layout lives in c["q4nx_weight_layout"] (pack_import.cpp:3385), not in the fixed-size container header. A test reading the wrong artifact would have passed while the thing it claims to pin went unchecked — the "gate reads the artifact" rule applied to the fixture itself.
With the pair (non-default layout must carry the key; default-layout must not) now reading the file the writer actually writes, this closes #1840 on the grounds I readied it for.
|
VERDICT: READY at The pair is the point, and the comment says why better than I could: a non-default layout must carry The note about where the key lives is the most useful part of the diff ( One note: Scope of my check, stated: built and ran |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 1315773: CHANGES
Checked at 1315773 (test only, +38, MERGEABLE). The description says the arm was not run end to end, so I built and ran it. npu_pack_import_test tests/golden gives rc 0. The both-directions design is right ("absent means default"). But it pins the wrong writer, the same defect I found on #1877, which put the arm in the same place and has since closed.
#1840 is about import_external's write of the key, the #1793 fix. There are two writers at this head:
npu/pack_import.cpp:3431 if (!opt.weight_layout.empty() && opt.weight_layout != "rm") c["q4nx_weight_layout"] = … <- import_external (#1793)
npu/pack_import.cpp:4645 c["q4nx_weight_layout"] = opt.weight_layout; <- the .fconst path
The arm lives in test_final_norm_envelope, whose fixture is a crafted .fconst tail, so it goes through 4645. Each write disabled on its own, against the built test:
| mutant | result |
|---|---|
| none | rc 0, 0 FAIL |
| :3431 disabled: the #1793 regression #1840 exists to catch | rc 0, 0 FAIL |
| :4645 disabled | rc 1, FAIL weight layout: --weight-layout b8 writes q4nx_weight_layout="b8" … |
So with this merged, #1840's regression can return silently, which is the thing the issue asks to prevent. To close it, the two checks have to run on an external-data pack that import_pack takes to a written container. #1863 builds exactly that fixture and already carries both checks. That's the third PR claiming Fixes #1840. #1863 is currently red for a separate reason (its signed-int4 reference), and its layout-key half is the right home for this. Either move these two assertions onto an external-data fixture and show the :3431-only mutant red, or retarget this to "Refs #1840" as a .fconst-path pin and leave the Fixes to the external-data arm.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 1315773
Re-verified: the only change since my review is a merge of main, so the content stands — the arm asserting that a non-default weight layout actually lands in the container it built.
The property is unchanged and worth restating: a non-default layout is the only case where the field is load-bearing, because every default-layout pack passes whether the writer writes it or not — and it is exactly the case a user reports as broken.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 13157732 — correct, small, and it is a subset of #1863, so please do not merge both.
The arms are right and the negative is the one that matters: a default-layout container must carry no q4nx_weight_layout key, since every container written before that option existed depends on its absence, and an arm that only checked the positive would leave those free to acquire a key that changes which kernels are looked for.
But #1863 already asserts both directions, inside a full import_external success arm (it imports a crafted external-data pack to completion, then reads the container back and checks q4nx_weight_layout absent for the default and "b8" for the b8 import, plus the tokenizer artifact and the geometry). That is the same coverage with more context.
So of the three PRs in this cluster: #1902 (success path with a tile decoded back) and #1863 (success path + layout keys) are the two candidates, and this one is the smaller spelling of half of #1863. If #1863 lands, close this. If #1863 does not and #1902 does, this fills the layout-key gap that #1902 does not cover — in which case it is worth merging, because the negative is genuinely load-bearing.
Nothing to fix in the arms themselves.
VERDICT at
|
|
Closing as superseded by #1863 (merged, How I missed it, since the same lane should not duplicate itself. Before writing I checked that the gap was real in the tree ( Recording that here rather than deleting the branch quietly: the cost of the miss was one CI run and a reviewer's time, and the cost of not recording it is the next lane doing the same thing. What survives from this attempt, for whoever reads the history: the reasoning in the PR body about why the pair of assertions matters (a positive-only arm would still pass if the writer started emitting the key for every import) is now asserted by #1863's arms, and the same negative is what the reviewer independently singled out as "the one that matters". |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: LGTM — no defects found. Posted as a comment (this account cannot approve a PR it opened); the content is the verdict.
The pair is the right shape. A non-default layout must carry q4nx_weight_layout, and a default-layout import must not — asserting both is what makes the contract two-sided. Most tests of a new option assert only the positive and then quietly break every container written before the option existed; the negative arm is the one that protects them. Naming #1793 (a report printing declares q4nx_weight_layout "b8" while the writer omitted the key) as the regression this pins is the right provenance.
The comment about reading the header rather than config.json is the best part of the PR. "…reading the header is what my first version of this arm did, and CI caught it: the negative passed trivially and the positive found nothing" — a negative that passes trivially is the failure mode of every absence assertion, and writing that down (with where the key actually lives, pack_import.cpp:3385) is worth more to the next reader than the test itself. Catching the b8 import's exception and reporting it rather than letting it abort the rest of the function is also the right call for a test that shares a fixture.
One gap worth naming, not blocking: the arm pins that the config says b8, not that the payload is b8. A writer that emitted the key and then wrote row-major tiles would pass this pair — which is #1793's bug one layer deeper (before, the report said b8 and the key was absent; now the key could say b8 and the payload disagree). The mechanism to close it already exists and this repository uses it elsewhere: import a b8 container and byte-compare one tile against q4nx_pack_B_int8_blocked()'s own output for that tensor (or walk q4nx_layout::blocked_index() and check a known position holds the value the packer put there). If a whole-tensor compare is too slow for a unit test, one (layer, role, tile) is enough: the layout is a permutation, so one tile that matches rules out a row-major payload.
Reader-lane context, since it is the same distinction: on the family gate, "the config says the layout" and "the image the device reads is that layout" were exactly the two claims that had to be measured separately — the arm reproduced weight scales 384/384 exactly only because it read the engine's own packer output rather than re-quantising, and that is the same shortcut a config-knob test can take by accident.
Fixes #1840. The arm the CHANGES on #1793 asked for — "the fixture already exists" — added on that fixture, with its negative.Why it was missing, and what that cost
The merged fix makes
import_externalwriteq4nx_weight_layoutexactly as the.fconstpath does, so a container built for the_b8family stops asking for untagged ELFs. Nothing asserts the writer's half:The regression it fixed — a report printing
declares q4nx_weight_layout "b8"while the writer omitted the key — could therefore return silently, because the next change to the writer has nothing to fail against.The two assertions, and why both
Placed in
test_final_norm_envelope, whose fixture this file already builds and imports into a temp dir:q4nx_weight_layout— the key is written only for a non-default layout, and every container the importer wrote before the option existed depends on it being absent;Options::weight_layout = "b8"writesq4nx_weight_layout = "b8"into the header of the container it builds.The pair matters: the writer's contract is "absent means default", so a positive-only arm would still pass if the writer started emitting the key for every import. Both read the container back through the same 8-byte-length + JSON-header path the neighbouring arms use, and the b8 import is wrapped so a throw reports instead of taking the rest of the function with it.
Verification
g++-16 -std=c++17 -fsyntax-only -I. -Inpu -Inpu/forward/include -Itests -I/tmp/njson tests/npu_pack_import_test.cpp→ clean. (nlohmann/json.hppis not installed on this box — CI installs it — so I fetched the single header to/tmprather than apt-installing a package on a shared machine.)add_test(NAME npu_pack_import …), whose binary ci.yml already builds — the route that needs noworkflowscope.onebit_npulink, and/is at 173G, under this lane's build gate. The CI run is the first execution.Note for whoever touches
Optionsweight_layout_refusal()already documents that onlyb8and the defaultrmcan be expressed by the bare flag (t2s/pb16i need a version and a sidecar directory). This arm pins the writer side of that: whatever the refusal allows must appear in the container, unchanged.