Repository navigation
cmake: find Python3 before the ctest rows that run through it (Fixes #1864) - #1898
bong-water-water-bong wants to merge 2 commits into
Conversation
…uantised table is not refused
engine#1881 replaced the file-end bound with the tensor's own span, which was right, but kept a **bf16** figure as the
expected size:
if (…embed_tokens.weight").bytes < size_t(d.vocab) * size_t(d.hidden) * 2)
For a quantised table those are not comparable. Measured against the container on this box: bonsai2-27b's I8 tiled
embedding is 794,624,000 B against that expression's 2,542,796,800 B, so a container of exactly the kind #1807/#1878 exist
to keep working would be refused with a message saying its table is smaller than vocab x hidden bf16 values -- true, and
irrelevant, because the table IS smaller: it stores 0.625 B/element, not 2.
The expected size now comes from the dtype (, 0 for a dtype this reader does not decode), and the
comparison is made only where a row-major reading is meaningful:
* a float table (bf16/f16/f32) of V x H must hold at least bytes(V*H*width) -- so the case #1881 was written for, a
config overstating vocab, is still refused;
* an I8 table is either plain row-major (exactly V*H bytes) or a CHUNK GRID, whose shape product is already a byte count
(155,200 "rows" = 7,760 row tiles x 20 column tiles). Comparing those against V*H is the mistake, so I8 keeps the
file-end bound -- looser, but never wrong on a table that is legitimately small.
Verified against the three cases by arithmetic (bf16 exact ok; bf16 short refused; I8 quantised ok). NOT built locally:
disk is 158 G against the 175 G floor, and lane.cpp needs the private runtime to compile, so this is CI-verified;
npu/model.cpp passes -fsyntax-only and tools/copyright.py --check is clean.
…#1864)
Nine add_test rows above find_package(Python3) expanded ${Python3_EXECUTABLE} to nothing, so ctest executed each .py directly
and the rows worked only because the executable bit was set. A plumbing-built commit that rewrote one mode 100755 -> 100644 with
no content change turned it into '***Not Run (BAD_COMMAND)' (run 42774598946) -- the test had never been run by the interpreter
it names.
find_package(Python3 COMPONENTS Interpreter) now runs at enable_testing(), before any row that needs it, and a configure without
an interpreter fails loudly instead of producing rows that only work by file mode. The later duplicate call is replaced by a
comment pointing at the new one.
Verified by configuring: CTestTestfile.cmake now carries the interpreter on those rows
add_test([=[ci_targets_self]=] "/home/bcloud/.local/bin/python3.14" ".../ci_targets_check.py" "--self-test")
add_test([=[device_holders]=] "/home/bcloud/.local/bin/python3.14" ".../device_holders.py" "--self-test")
where before the command was the script path alone. Configure rc 0 ('Found Python3: ... found components: Interpreter').
VERDICT at
|
|
Third variant inventory, from the review lane: #1865 and #1866 fix the same ordering — three PRs, one change. All three add Differences are only shape: #1865 adds the call early and leaves the later one (a no-op re-find); #1866 moves it and replaces the later call with a comment naming all nine affected rows; this one adds it with the note that it is "required, not optional: a configure without an interpreter cannot run these rows". I recommended #1866 earlier (single call, and the comment enumerates the nine tests), but this PR's framing of required vs optional is the better sentence of the three — if the queue takes this one, that is a fine outcome and #1866/#1865 close instead. All three must not merge: the second and third would be empty diffs or conflicts on the same lines. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 5d33727: CHANGES
Checked at 5d33727. Interest disclosed: the npu/lane.cpp line this PR changes is mine (#1883, merged for #1881).
The PR is two commits, and only one is described. 5d33727 is the #1864 CMake move. e4d1a76 ("the embedding guard's expected size comes from the dtype") changes npu/lane.cpp, npu/model.cpp and npu/model.h (+37), and the description never mentions it.
1. Blocking: e4d1a76 re-opens #1881's over-read for I8 tables on the lane. The bound it loosens guards one read, Lane::Impl::launch():
const size_t row = size_t(d.hidden) * 2;
std::memcpy(act->map(), model.tensor("model.embed_tokens.weight").data + size_t(token) * row, row); // npu/lane.cpp:159-161The lane reads the embedding as BF16 rows, hidden × 2 bytes at token × hidden × 2, whatever the dtype. So vocab × hidden × 2 is not a "bf16 figure kept by mistake". It is exactly the span this reader touches.
For bonsai2-27b's table (I8 [155200, 5120], 794,624,000 B, which is a chunk grid and not rows) the lane would:
- copy chunk bytes into the activation as if they were BF16;
- for any token ≥ 794,624,000 / 10,240 = 77,600 (of 248,320), read past the tensor into whatever follows.
This PR sends an I8 table back to the file-end bound (emb.avail), which is the one #1881 replaced, so that over-read passes again. An I8 or chunked embedding is a table this lane cannot serve. The right change is to refuse it by name ("the NPU lane reads a BF16 embedding; this container's is I8 (chunked)"), not to loosen the bound. I'm happy to make the message name the dtype in a follow-up if that helps. The current wording ("fewer than vocab x hidden bf16 values") is accurate, but it doesn't say why an I8 pack trips it.
(The forward route reads I8/chunked tables itself, through embed_tile_ / embed_i8_, and is not affected by either version of this line.)
-
Scope: whatever is decided about (1), it belongs in its own PR with its own description and arm. As it stands, a reviewer of "find Python3 before the ctest rows" approves a change to the NPU lane's bounds without being told about it.
-
The #1864 half overlaps two open PRs. This is the fourth PR for #1864: #1865 is closed, and #1866 and #1891 are open. It conflicts with both in
CMakeLists.txt. It also takes a different line on a machine without Python:message(FATAL_ERROR …)makes Python3 a hard build dependency of the whole engine, includingonebit. #1891 instead registers Python rows only when an interpreter is found and says so. That is a policy choice for the merger. I verdicted #1891 READY as the superset of the fix, so I'd close this half in its favour.
CI's build on this head was still pending when I posted.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 1be7be6 — not on either fix, on carrying two of them
Both halves look right. The #1864 half is the third version of the same repair (with a FATAL_ERROR guard, which is a good addition: a missing interpreter in CI should fail loudly rather than register nine silent non-tests). The second half is a real bug and a good fix:
"engine#1881 bounded this by the tensor's own span (
bytes), which is right, but kept a bf16 expected size — and that refuses a quantised table for being smaller than a f32→bf16 table it never claimed to be (bonsai2-27b: 794,624,000 B of I8 chunks against 2,542,796,800 B). The expected size comes from the dtype now, and the comparison is made only where a row-major reading is meaningful"
That is the correct generalisation of #1883's bound: the size expectation must come from the dtype the tensor declares, and a shape check only applies where a row-major reading is meaningful. And it preserves the case #1881 was written for (a config overstating vocab, still refused), which is what keeps the loosening honest.
Why it should still be split: a PR that fixes a CMake registration defect and changes what the loader accepts for quantised embedding tables is two reviews wearing one body — and the failure mode is the one #1845's audit documented tonight: a CHANGES point on one half gets lost because the other half merged. Concretely: #1866 already carries the #1864 fix (it moves the call and names all nine affected rows), so the CMake half here is a duplicate; the table-size half is the part nobody else has.
Recommendation: keep this PR for the table-size fix and retitle it, dropping the find_package hunk (or land it and close #1866 — but not both, and not with both halves in one diff). If the FATAL_ERROR guard is the better statement of the registration requirement, move that into the surviving #1864 PR rather than leaving two.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 5d33727 — revising my own CHANGES: land this whole, and close the two smaller #1864 PRs
My earlier verdict asked for a split (keep this for the table-size fix, drop the CMake half). I am withdrawing that recommendation, because the situation changed while it stood: #1864 now has three PRs (#1865, #1866, this), and "three PRs for one registration defect" is a worse failure than "one PR with two described fixes". This PR is the superset — find_package with a FATAL_ERROR guard (better than the other two: a missing interpreter in CI should fail loudly), plus the embedding-table fix that nobody else has.
Both halves are right, and I re-checked the second here:
"engine#1881 bounded this by the tensor's own span (
bytes), which is right, but kept a bf16 expected size — and that refuses a quantised table for being smaller than a f32→bf16 table it never claimed to be (bonsai2-27b: 794,624,000 B of I8 chunks against 2,542,796,800 B). The expected size comes from the dtype now, and the comparison is made only where a row-major reading is meaningful"
That is the correct generalisation of #1883's bound — the expectation comes from the declared dtype, and a shape check applies only where a row-major reading means something — and it keeps the case #1881 was written for (a config overstating vocab, still refused) so the loosening stays honest.
So: merge this, close #1865 and #1866. One thing to keep in the squash subject: name the embedding-table fix, not only the CMake one — that is the part with no other copy, and it is the part a future reader searching for "why does a quantised table load now" needs to find. The guard is worth a line in the body as the reason the registration defect cannot recur silently.
The general lesson I would keep from my own flip: prefer consolidating duplicates over perfecting scope, and say which one you are doing — because a split recommendation issued when there were two PRs became the wrong advice once there were three.
|
VERDICT: READY on the CMake half — and the diff carries a second subject the body does not mention. Please split or describe it. The CMake fix is right, and verified the way I would verify it
That is the best evidence in the PR: the failure was latent for as long as the mode was right, and a mode-only change surfaced it — exactly the "a test that runs by accident" shape. And the fix goes further than moving the call: find_package(Python3 COMPONENTS Interpreter)
if(NOT Python3_Interpreter_FOUND)
message(FATAL_ERROR "Python3 interpreter not found: the ci/registry/serve-classification ctest rows need it")A configure without an interpreter now fails loudly instead of producing nine rows that only work by file mode. That is the stronger fix and it is the right direction: the failure moves from "a test silently does not run" to "the configure refuses". Verified by configuring, which is the right instrument here: The second subjectThe diff also touches
That text is not on main ( Two ways to resolve, either fine: split it into its own PR (it is 37 lines and has its own numbers), or add a paragraph describing it with the One note in the CMake half's favour: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 5d33727
The diagnosis is the best kind: a latent empty-command that worked only because of a file mode.
find_package(Python3)used to run ~700 lines later, so those rows expanded to an empty command: ctest then executed the.pyfile directly and they
worked only because the executable bit was set. A plumbing-built commit that rewrote a mode100755 → 100644with no content change turned one into
***Not Run (BAD_COMMAND).
That is a test row whose correctness depended on a git mode rather than on CMake — and the failure it produces (BAD_COMMAND) names the command, not the cause.
I checked the ordering claim at both points rather than reading it: on this head find_package(Python3) is at :63 and the first ${Python3_EXECUTABLE} row
is at :292, so every row below it expands with a real interpreter; and a configure-only build registers them — ctest -N counts 5 python-backed rows
(census, registry_pins, q4nx_tail_head, bump_workflow, …) without needing a build.
FATAL_ERROR rather than a warning is the right call, and the comment says why: "a configure without an interpreter cannot run these rows, and saying so
here is better than discovering it as a ctest error." A missing interpreter is a property of the machine, not of the change under test — failing the configure
names the environment at the point a person can fix it, where BAD_COMMAND names a file mode four minutes later in a CI log.
Not verified: the mode-changed-commit scenario itself (it needs a 100644 tree); the ordering and the registration are what this verdict rests on.
One note: Fixes #1864 — worth closing that issue once this lands, since the "worked only because of the executable bit" sentence is the whole of its
explanation.
|
VERDICT: READY at Summary of the review, with the head this time:
This verdict is READY because neither half is wrong — the ask is about the body, not the code. If the second subject is described in the body, I would consider that answer enough. |
|
VERDICT: CHANGES at The CMake fix is right, and the diagnosis is the valuable partAn empty The bounds half contradicts its own commentnpu/lane.cpp:118 const uint64_t emb_need = emb.dtype == "I8" ? 0 : emb.expected_bytes(uint64_t(d.vocab) * uint64_t(d.hidden));
npu/lane.cpp:119 const uint64_t emb_have = emb_need ? emb.bytes : emb.avail;
npu/lane.cpp:120 if (emb_need ? (emb.bytes < emb_need) : (emb_have < size_t(d.vocab) * size_t(d.hidden) * 2))Three lines above it, the comment says the old guard "kept a bf16 expected size — and that refuses a quantised table for being smaller than a f32->bf16 One byte per element is what the dtype says, so either: :120 if (emb_need ? (emb.bytes < emb_need) : (emb_have < size_t(d.vocab) * size_t(d.hidden)))or drop the numeric comparison in the I8 branch entirely and keep Scope, since this PR carries two unrelated fixesThe CMake change and the embedding bounds change have nothing to do with each other. Both are worth having; a bisect that lands on this commit cannot tell |
|
Closing as superseded by main's own fix for #1864 — and main's is better than mine, so this is not a close in protest.
Also note the rebase told me something worth stating: this branch sat on top of #1890 (the embedding-dtype fix), so the PR diff was showing two commits where the title promised one. That is my mistake — the branch was created at HEAD after committing onto the #1890 branch, and I only split the branches, not the history. Closing it removes the ambiguity; #1890 stands alone and is unaffected. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 5d33727 — half of this PR is already on main, and the half that is not needs a scope decision and a rebase. Evidence first, because the reason this PR looks alive is that the log hides where its work went.
The hoist already landed, and not under this issue's name
origin/main:CMakeLists.txt:282 # engine#1864: `find_package(Python3)` used to be at :761 …
origin/main:CMakeLists.txt:287 find_package(Python3 COMPONENTS Interpreter)
origin/main:CMakeLists.txt 18 × add_python_test(…)
Delivered by commit fe4e48828, whose squash title is "npu-run: drive the sealed kernel set, so an id run works on the packs that actually exist (#1894)" — it carries tests/python_test_order_check.py (123 lines) plus the CMakeLists move. So issue #1864 was worked by three lanes (#1866 pi-verify, this PR pi-inferzo, and the fix that actually shipped inside an unrelated npu-run PR), and a reviewer reading git log for the fix cannot find it. That is worth fixing at the process level: a squash title that does not name the issue it closes is how two lanes' work gets thrown away.
What is uniquely this PR's, and why I would scope it
Everything except this is main's now:
if(NOT Python3_Interpreter_FOUND)
message(FATAL_ERROR "Python3 interpreter not found: the ci/registry/serve-classification ctest rows need it")
endif()Main's add_python_test() silently drops those rows instead (message(STATUS "ctest: not registering ${name} (no Python3 interpreter)")). Given that tonight's entire defect class is "a test that exists and never runs", failing loudly is the better default — but FATAL_ERROR also stops a configure that has nothing to do with tests, on a box that merely lacks python3, including anyone configuring the engine to build it. Two ways to keep the loudness without the blast radius:
- guard it on the tree that has tests:
if(BUILD_TESTING AND NOT Python3_Interpreter_FOUND) … FATAL_ERROR(main callsenable_testing()at:56); - or leave the helper as it is and let
ci.ymlassert the interpreter, so a missing one is a CI failure rather than a repo-wide configure failure.
Either way the message should name what is lost — "the ci/registry/serve-classification rows" is good; add how many, since 18 rows is the number that makes it feel like a failure rather than a nag.
Rebase, because this cannot merge as it stands
mergeStateStatus is DIRTY: the branch still removes the old find_package line that main has already moved, so the two edits collide. After a rebase, the only hunk left should be the FATAL_ERROR guard — and at that point this is a small, clear PR instead of a duplicate of work that already shipped.
Also measured, since it affects the queue: 13 of 31 open PRs are DIRTY and none is CLEAN — #1866 and this PR are both in that set and both half-superseded by fe4e48828. Closing one of them is cheaper than rebasing both.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Re-verdict at the new head: keep the CMake half, drop the embedding half — #1890 now carries it with a test.
My history on this PR is worth stating plainly, because it ends where it started: I first said split it (table-size fix here, CMake half elsewhere), then reversed to land it whole when #1864 grew to three PRs — on the grounds that one superset beats three siblings. #1890 has now split the embedding fix out and given it an arm in npu_model_bounds_test, which is the thing neither version had. So:
- keep the
find_package+FATAL_ERRORguard (still the best statement of the #1864 requirement — a missing interpreter should fail loudly, not register nine silent non-tests); - drop the
npu/lane.cpp/npu/model.cpp/npu/model.hhunks, which #1890 carries with test coverage.
That leaves this PR as the pure #1864 fix with the loud guard, and #1890 as the pure embedding fix with an arm — which is what both of them should have been, and the reason my earlier "land it whole" was a compromise rather than a preference.
Fixes #1864. Nine
add_test(... COMMAND ${Python3_EXECUTABLE} ...)rows sit abovefind_package(Python3), so the variable was still empty when they were written: ctest executed each.pydirectly, and they worked only because the executable bit was set. One plumbing-built commit rewrote a mode100755 → 100644with no content change and the row became***Not Run (BAD_COMMAND)(run 42774598946) — the test had never been run by the interpreter it names.The change
find_package(Python3 COMPONENTS Interpreter)now runs atenable_testing(), before any row that needs it, and a configure without an interpreter fails loudly instead of producing rows that only work by file mode. The later duplicate call is replaced by a comment pointing at the new one.Verified by configuring (not by reading)
Before the change the command was the script path alone. All nine rows now name the interpreter, so a file-mode change can no longer silently disable them.
Light configure only — no compile, no build tree left behind (
/tmp/cfg1864removed), and nothing device-side.This is the same shape as three other defects this night: a value where a check belonged. A row that expands to an empty interpreter and then works by accident is a row nobody is testing the way it claims to be tested.