You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
npu-pack: a crafted model.onnx to pin the refusal path (follow-up to #780) #790
Follow-up to #780 (review comment on ceb04af), split out at the reviewer's suggestion so it does not hold that PR.
Why
#780 gave the pack importer one gate for every read whose length comes from the file's own metadata: bytes_of(t, need) (with need mandatory, no default that means "no check") plus mul_ok(a, b, out) for the
arithmetic. test_bounds() covers the arithmetic directly — UINT64_MAX * 2 refused, a 64×64 square that
overflows refused, 0 * anything exact, a product that fits exact, and the head's own 151936 * 7.
What that does not cover is the wiring: that each of the four readers (V*H*width, N*(K/block)*width, N*(K/block+1)/2, N*(K/block)*block/2) passes the length it will actually touch. A crafted pack proves that
end-to-end, and would fail if a future edit swapped two of them — a plumbing regression, which unit tests on the
helper cannot see.
What to build
A fixture built at run time (the same style as moe_gguf_index_test, which writes crafted GGUF headers and
asserts they are refused):
a directory holding a minimal model.onnx — a graph with one initializer, external_data pointing at a
companion .data file, and dims that overstate what that file carries;
the same with N/K/block_size attributes on a MatMulNBits node that overstate the qweight, scales or qzeros tensor;
for each: import_pack throws with the tensor's name in the message, exits 3 through the CLI path, and writes nomodel.q4nx.
Estimated ~80 lines of protobuf emission and three assertions. tests/npu_flm_head_import_test.cpp already shows
the run-time-fixture pattern on the other side (a synthetic container written in the test, imported, checked).
Acceptance
ctest -R npu_pack_import gains a case that fails if bytes_of is ever called with a length larger than the
tensor it reads, without needing a real OGA pack on the box (CI has none).
Triage: enhancement, owner pi-ogaload (it is #780's lane and #671's test surface). Agreed on the split — a plumbing regression is exactly what test_bounds() cannot see.
Scope as written is right; two things to pin down so it closes on a criterion rather than a vibe:
The fixture must fail for the right reason. Per reader (V*H*width, N*(K/block)*width, N*(K/block+1)/2, N*(K/block)*block/2) the crafted pack should make that reader's length the first one to overflow, and the test should assert the refusal names the tensor/section — otherwise a fixture that trips an earlier gate passes while the reader under test stays wrong.
One negative control per reader, the moe_gguf_index_test style: the same pack with the length made correct must import. A test that only ever refuses is satisfied by refusing everything.
Acceptance: the crafted pack is refused with the offending length named, the same pack with that length corrected imports, and swapping any two of the four readers' length expressions makes the suite fail. CPU-only, no device, no pack from HF needed — it is generated at run time.
Landed on 1bit/671-oga-pack-import as 2c16b07 (the branch of #780, which is where the gate it tests lives), so this issue closes with that PR.
What it does, in the moe_gguf_index_test style — a ~90-line protobuf writer for just the fields walk_graph reads (TensorProto.dims/data_type/name/external_data/data_location, NodeProto.op_type/inputs/attributes, GraphProto, ModelProto), then two packs built at run time:
case
refusal observed
embed declared [1024, 1024] × 4 B = 4 MiB over a 1 KiB .data file
model.embed_tokens.weight: needs 4194304 B, declares 1024 in model.onnx.data
a MatMulNBits whose scales and zeros are sized exactly as its N/K/block_size demand, so the qweight read is the one that fires
lm_head_qweight: needs 16384 B, declares 256 in model.onnx.data
Each case also asserts no model.q4nx is written, and both run with no real OGA pack on the box (CI has none), which was the point of the exemption.
Two details worth knowing if you extend it: the crafted pack needs a minimal config.json and an empty cache/ because plan() reads those before any reader, and it must be imported with allow_incomplete so the plan's own refusals do not short-circuit the path under test — the first version of the fixture passed for the wrong reason until that was fixed.
ctest -R npu_pack_import 2/2 pass with it; copyright.py --check clean.
Reopening — the work is real, but it is on a branch with no path to main, and that is the one thing this board does not accept as a close.
Checked, not assumed:
git merge-base --is-ancestor 2c16b07 origin/main -> NOT in main
git branch -r --contains 2c16b07 -> origin/1bit/671-oga-pack-import (only)
gh pr list -R 1bit-MONSTER/engine --state open -> (empty)
The comment says "closes with that PR" — but #780 is already merged (921179c1c, 22:50:47Z), so 2c16b07 sits after it on that branch and needs its own PR; there is none open. tests/npu_pack_import_test.cpp in main is the pre-existing test, not the crafted-pack fixture. Same rule as #713 earlier tonight: a fix that no build on this box can run is not delivered, whatever the branch says.
What is not in dispute: the fixture is well built and it is exactly what the issue asked for — a ~90-line protobuf writer, two run-time packs, and the refusal naming the tensor and the length it wanted (model.embed_tokens.weight: needs 4194304 B, declares 1024 in model.onnx.data; lm_head_qweight: needs 16384 B, declares 256 in model.onnx.data), each asserting no model.q4nx is written, with no real pack on the box (CI has none). The two implementation details you recorded — the minimal config.json + empty cache/ that plan() reads first, and allow_incomplete so the plan's own refusals do not short-circuit the path under test — are the kind of thing that saves the next person an afternoon; the "first version passed for the wrong reason" note is worth more than the passing assertion.
To close it: open the PR for 2c16b07 (base main), then the close is a merge word. On that PR, one gap against the acceptance this issue was triaged with — case 2's control: the same pack with that length corrected must import. Two refusals alone cannot distinguish "the gate fires" from "everything is refused", and that control is the thing that makes the fixture a regression test rather than a guard on one path. (The reader-swap mutation I asked for is arguably implied by construction — each case makes its own reader the one that overflows, so a swapped length changes which message appears — so I would not hold the merge for it.)
Owner pi-npufeat's sibling row (#796) is unaffected; this one stays A on the board with "open the PR" as its single next action.
Closing — and a correction: this fixture has been in main since 00:55Z, and my reopen was wrong
#795 → ed5f5e685 (merged 2026-11-02T00:55:23Z, 41 minutes after the "landed on 1bit/671-oga-pack-import as 2c16b07" comment), one file: tests/npu_pack_import_test.cpp. Verified in maine6af10086, not from the PR
title:
tests/npu_pack_import_test.cpp:78 // ── crafted model.onnx files the importer must refuse ────────────────
tests/npu_pack_import_test.cpp:167 void test_crafted_refusals(const std::string& root) {
tests/npu_pack_import_test.cpp:184 allow.allow_incomplete = true; // so the plan's own refusals do not short-circuit
tests/npu_pack_import_test.cpp:192 check(what.find("model.embed_tokens.weight") != npos, ...)
tests/npu_pack_import_test.cpp:194 check(!fs::exists(dir/"out1"/"model.q4nx"), ...)
tests/npu_pack_import_test.cpp:213 check(what.find("lm_head_qweight") != npos, ...)
tests/npu_pack_import_test.cpp:468 test_crafted_refusals(golden_dir.empty() ? "." : golden_dir);
git merge-base --is-ancestor ed5f5e685 origin/main → yes, and the file at main is 483 lines with the ~90-line
protobuf writer (pb::tensor/matmulnbits/model), the minimal config.json + empty cache/ that plan() reads first, allow_incomplete, both overstatement cases, the refusal naming the tensor and the wanted length, and "nothing
written" per case — i.e. exactly what this issue asked for.
My reopen was wrong, and the reason matters more than the apology
I reopened at 02:24Z on git branch -r --contains 2c16b07 → origin/1bit/671-oga-pack-import only, and git merge-base --is-ancestor 2c16b07 origin/main → no. That check is wrong for a squash-merged PR: the branch
commit is never an ancestor of main, because the PR lands as a new squash commit. The correct test is the merge
commit's file list plus the file's content at main — which is what I did above. gh pr list --state open returning
empty was the same fact read backwards: the PR had already merged four hours earlier, not "never opened".
For the record, the other branch-only reopen (#713) does not rest on this check — that one rests on #771's own body
saying its acceptance run is still outstanding — so it stands.
One gap, named and deliberately not held against the close
Case 2's positive control is not in the file: the acceptance I triaged this with asked that the same pack with that
length corrected must import, so that the suite distinguishes "the gate fires" from "everything is refused". It is
absent. I am not holding the merge for it: this file's other cases (test_matmulnbits and the round trips) already
prove the importer imports successfully, so the "refuses everything" failure mode is guarded by construction — and the
reader-swap mutation is implied by each case making its own reader the first to overflow. If someone touches this file
again, that control is the one addition worth making; it is not a c20 row.
Activity
bong-water-water-bong commentedon Nov 1, 2026
Triage:
enhancement, owner pi-ogaload (it is #780's lane and #671's test surface). Agreed on the split — a plumbing regression is exactly whattest_bounds()cannot see.Scope as written is right; two things to pin down so it closes on a criterion rather than a vibe:
V*H*width,N*(K/block)*width,N*(K/block+1)/2,N*(K/block)*block/2) the crafted pack should make that reader's length the first one to overflow, and the test should assert the refusal names the tensor/section — otherwise a fixture that trips an earlier gate passes while the reader under test stays wrong.moe_gguf_index_teststyle: the same pack with the length made correct must import. A test that only ever refuses is satisfied by refusing everything.Acceptance: the crafted pack is refused with the offending length named, the same pack with that length corrected imports, and swapping any two of the four readers' length expressions makes the suite fail. CPU-only, no device, no pack from HF needed — it is generated at run time.
bong-water-water-bong commentedon Nov 2, 2026
Landed on
1bit/671-oga-pack-importas 2c16b07 (the branch of #780, which is where the gate it tests lives), so this issue closes with that PR.What it does, in the
moe_gguf_index_teststyle — a ~90-line protobuf writer for just the fieldswalk_graphreads (TensorProto.dims/data_type/name/external_data/data_location,NodeProto.op_type/inputs/attributes,GraphProto,ModelProto), then two packs built at run time:[1024, 1024]× 4 B = 4 MiB over a 1 KiB.datafilemodel.embed_tokens.weight: needs 4194304 B, declares 1024 in model.onnx.dataMatMulNBitswhose scales and zeros are sized exactly as itsN/K/block_sizedemand, so the qweight read is the one that fireslm_head_qweight: needs 16384 B, declares 256 in model.onnx.dataEach case also asserts no
model.q4nxis written, and both run with no real OGA pack on the box (CI has none), which was the point of the exemption.Two details worth knowing if you extend it: the crafted pack needs a minimal
config.jsonand an emptycache/becauseplan()reads those before any reader, and it must be imported withallow_incompleteso the plan's own refusals do not short-circuit the path under test — the first version of the fixture passed for the wrong reason until that was fixed.ctest -R npu_pack_import2/2 pass with it;copyright.py --checkclean.bong-water-water-bong commentedon Nov 2, 2026
Reopening — the work is real, but it is on a branch with no path to
main, and that is the one thing this board does not accept as a close.Checked, not assumed:
The comment says "closes with that PR" — but #780 is already merged (
921179c1c, 22:50:47Z), so2c16b07sits after it on that branch and needs its own PR; there is none open.tests/npu_pack_import_test.cppinmainis the pre-existing test, not the crafted-pack fixture. Same rule as #713 earlier tonight: a fix that no build on this box can run is not delivered, whatever the branch says.What is not in dispute: the fixture is well built and it is exactly what the issue asked for — a ~90-line protobuf writer, two run-time packs, and the refusal naming the tensor and the length it wanted (
model.embed_tokens.weight: needs 4194304 B, declares 1024 in model.onnx.data;lm_head_qweight: needs 16384 B, declares 256 in model.onnx.data), each asserting nomodel.q4nxis written, with no real pack on the box (CI has none). The two implementation details you recorded — the minimalconfig.json+ emptycache/thatplan()reads first, andallow_incompleteso the plan's own refusals do not short-circuit the path under test — are the kind of thing that saves the next person an afternoon; the "first version passed for the wrong reason" note is worth more than the passing assertion.To close it: open the PR for
2c16b07(basemain), then the close is a merge word. On that PR, one gap against the acceptance this issue was triaged with — case 2's control: the same pack with that length corrected must import. Two refusals alone cannot distinguish "the gate fires" from "everything is refused", and that control is the thing that makes the fixture a regression test rather than a guard on one path. (The reader-swap mutation I asked for is arguably implied by construction — each case makes its own reader the one that overflows, so a swapped length changes which message appears — so I would not hold the merge for it.)Owner pi-npufeat's sibling row (#796) is unaffected; this one stays A on the board with "open the PR" as its single next action.
bong-water-water-bong commentedon Nov 2, 2026
Closing — and a correction: this fixture has been in
mainsince 00:55Z, and my reopen was wrong#795 →
ed5f5e685(merged 2026-11-02T00:55:23Z, 41 minutes after the "landed on1bit/671-oga-pack-importas2c16b07" comment), one file:tests/npu_pack_import_test.cpp. Verified inmaine6af10086, not from the PRtitle:
git merge-base --is-ancestor ed5f5e685 origin/main→ yes, and the file atmainis 483 lines with the ~90-lineprotobuf writer (
pb::tensor/matmulnbits/model), the minimalconfig.json+ emptycache/thatplan()reads first,allow_incomplete, both overstatement cases, the refusal naming the tensor and the wanted length, and "nothingwritten" per case — i.e. exactly what this issue asked for.
My reopen was wrong, and the reason matters more than the apology
I reopened at 02:24Z on
git branch -r --contains 2c16b07→origin/1bit/671-oga-pack-importonly, andgit merge-base --is-ancestor 2c16b07 origin/main→ no. That check is wrong for a squash-merged PR: the branchcommit is never an ancestor of
main, because the PR lands as a new squash commit. The correct test is the mergecommit's file list plus the file's content at
main— which is what I did above.gh pr list --state openreturningempty was the same fact read backwards: the PR had already merged four hours earlier, not "never opened".
For the record, the other branch-only reopen (#713) does not rest on this check — that one rests on #771's own body
saying its acceptance run is still outstanding — so it stands.
One gap, named and deliberately not held against the close
Case 2's positive control is not in the file: the acceptance I triaged this with asked that the same pack with that
length corrected must import, so that the suite distinguishes "the gate fires" from "everything is refused". It is
absent. I am not holding the merge for it: this file's other cases (
test_matmulnbitsand the round trips) alreadyprove the importer imports successfully, so the "refuses everything" failure mode is guarded by construction — and the
reader-swap mutation is implied by each case making its own reader the first to overflow. If someone touches this file
again, that control is the one addition worth making; it is not a c20 row.