Repository navigation
npu-run: drive the sealed kernel set, so an id run works on the packs that actually exist (fixes #1862) - #1894
Conversation
…, and pin the order (fixes #1864) `find_package(Python3 COMPONENTS Interpreter)` sat at :761 while **nine** registrations above it expanded `${Python3_EXECUTABLE}` to the empty string, so `CTestTestfile.cmake` held e.g. ``` add_test([=[hrx_build_scope]=] "<src>/tools/ci/hrx_build_scope.py" "--self-test") ``` -- the script as the program. Those tests ran only through their **exec bit**, which is why a mode-only rewrite of `tools/ci/hrx_build_scope.py` (100755 -> 100644, no content change) turned the row into `BAD_COMMAND`. The interpreter is now found once, above the first user, and the registrations go through one place: ```cmake find_package(Python3 COMPONENTS Interpreter) function(add_python_test name) if(Python3_Interpreter_FOUND) add_test(NAME ${name} COMMAND ${Python3_EXECUTABLE} ${ARGN}) else() message(STATUS "ctest: not registering ${name} (no Python3 interpreter)") endif() endfunction() ``` Nine registrations used the variable and now use the helper; the second `find_package` (and its now-redundant guard comment) is gone, since the later block is still guarded by `Python3_Interpreter_FOUND`. **Why a helper and not just moving the find_package:** moving it fixes today's nine and leaves the next `add_test` above it free to repeat the bug, and it does nothing for a machine without Python3, where an empty interpreter makes ctest hand a `.py` to the kernel as a program. The helper makes both impossible by construction. ## The rule is now checked, on the file, with no configure `tests/python_test_order_check.py` (`--check --root`, `--self-test`) requires every `${Python3_EXECUTABLE}` use to follow the first `find_package(Python3` in each `CMakeLists.txt`, and is registered twice like the other `*_check.py` pairs. Against `origin/main` it reports **exactly the nine the issue counted**: ``` main offenders: [282, 283, 287, 288, 295, 303, 705, 707, 712] ``` and against this head, none. Its own comment mentions the variable, so the checker strips comments first -- with a self-test case for that, because a checker that trips on the prose explaining it would be switched off in a week. ## Verified ``` cmake -S . -B <build> -> rc 0 CTestTestfile.cmake BEFORE: add_test([=[hrx_build_scope]=] "<src>/tools/ci/hrx_build_scope.py" "--self-test") CTestTestfile.cmake AFTER : add_test([=[hrx_build_scope]=] "/home/bcloud/.local/bin/python3.14" "<src>/... " "--self-test") chmod -x tools/ci/hrx_build_scope.py; <script> --self-test -> "Permission denied", rc 126 (the pre-fix form: what ctest reported as BAD_COMMAND) chmod -x tools/ci/hrx_build_scope.py; ctest -R hrx_build_scope -> 100% tests passed, 0 failed out of 1 (through the interpreter) ctest -R 'python_test_order|ci_targets|tests_referenced|device_holders|moe_placement|hrx_build_scope|copyright_check_untracked|fa_ratio' -> 100% tests passed, 0 failed out of 11 python3 tests/python_test_order_check.py --self-test -> self-test ok python3 tools/copyright.py --check (staged) -> rc 0 ``` Fixes #1864.
… that actually exist (fixes #1862) `npu-run` required the per-role lane set (`layer_ctx{1,2,17}.elf`, `lmhead.elf`, `layer.pdi`) and no pack on this host ships it -- `find ~/models ~/scratch-packs -name layer_ctx1.elf` = 0 -- while every pack since the sealed route carries `SEALED-*.elf` / `full_i8_*` driven by the in-tree forward from `plan.json`. #1872 added a refusal that *names* that generation; this makes the CLI do the thing the refusal pointed at. A sealed kernel directory now runs the ids through the **same forward `forward-serve` uses**: ```cpp fwd.begin_verify(m, done); fwd.step_batch_all(chunk, done, all); // M x NV: the model's choice at EVERY row, not only the last ``` `begin_verify` + `step_batch_all` is the speculative verifier's call, which is why it returns per-row logits: that is the per-row datum #1781 gate 1 reads, and `step_batch` (plain prefill) cannot answer it. An id whose list is wider than the kernel's row width is walked in passes of that width with the position carried across. Device discipline is forward-serve's, unchanged: `NPU_INFER_HOST_GEMM=1` runs host GEMMs and takes **no** lock (engine#1353 -- a host-GEMM run that held it 4 h 52 m stopped two lanes' device work), otherwise the exclusive device lock is taken and `xrt::device(0)` opened. ## Verified Sealed pack `~/scratch-packs/imp1519b` (8 sealed ELFs), device-free: ``` NPU_INFER_HOST_GEMM=1 1bit npu-run --model …/imp1519b --kernels …/imp1519b/npu --ids "1 2 3 4 5 6 7" 1bit npu-run: sealed kernel set (8 ELF(s)), one multi-row pass through the in-tree forward 330 16 2 3 6 25010 873 (7 rows in, 7 ids out; identical on a second run) ``` Device run, under an npu-hold ticket, on `models/npu/bonsai2-27b-t2s2` (sealed `kernels-m16`): ``` gpu-hold[321893] command exited 0 after 46s 2026-11-10T06:38:59-04:00 69970 99320 210852 101013 ``` ## One honest limit, and it is the pack's, not this code's Both runs print `this runs in passes of 1 rows (the kernel's width)`: the width comes from `max_batch_rows()` = `xd_->max_rows()`, and these two packs report 1, so **the k-row identity demonstration still needs a pack whose kernels declare a wider width** (the 3584 family is built `-M 128`). What this PR fixes is that the route exists and runs; the k-vs-k×1 measurement is a property of the kernel set it is pointed at. Fixes #1862. Refs #1781, #1872.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 6a2b417
This answers a question I raised on engine#1786 tonight, from the other side, with the same measurement: "npu-run required the per-role lane set (layer_ctx{1,2,17}.elf, lmhead.elf, layer.pdi) and no pack on this host ships it — find ~/models ~/scratch-packs -name layer_ctx1.elf = 0 — while every pack since the sealed route carries SEALED-*.elf / full_i8_*." I measured exactly that (0 matches for layer_ctx1.elf) while probing an FLM NPU2 pack and reported it as an unprovisioned route; this makes the CLI use the generation that actually exists.
Two properties make it the right fix rather than a second route:
- it uses the same forward as
forward-serve—Q4nxNpuForward fwd; fwd.init(…)thenbegin_verify+step_batch_all— so the two CLIs cannot diverge in how they drive a pack, which is exactly the drift that produced two incompatible kernel generations in the first place; - the verify path is
M × NV: the model's choice at EVERY row, not only the last — the same correction #1813 made to the batch-check oracle, applied to the run path.
And the add_python_test() helper is a structural fix worth more than the two PRs it supersedes. #1864's defect was add_test(COMMAND ${Python3_EXECUTABLE} script) rows written before find_package, leaving an empty command word so ctest ran the .py directly — passing only while the file kept mode 100755. #1865/#1866 fix that by moving find_package earlier; this fixes the class: one function that runs the script through the interpreter, and on a machine without one, drops the test with a status line "instead of a .py being handed to the kernel as a program." A per-test registration that cannot be written wrongly beats a house rule about ordering.
Sequencing note for #1866: it is still worth landing (the nine existing rows need the early find_package until they are converted), but once add_python_test() exists, converting them is the durable fix and the ordering requirement becomes secondary. Worth saying in both PRs so the two do not look like alternatives.
No asks on the substance.
|
VERDICT: READY
Two notes for whoever merges:
I did not review the sealed-kernel-set driving logic itself ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 6a2b417
This is the PR that makes #1872's message actionable, and it is the one I have been waiting for on the F6 side: npu-run demanded the per-role lane set (layer_ctx{1,2,17}.elf, lmhead.elf, layer.pdi) and find ~/models ~/scratch-packs -name layer_ctx1.elf returns 0, while every pack since the sealed route carries SEALED-*.elf / full_i8_* and a plan.json the in-tree forward already knows how to drive. So the CLI was refusing the packs that exist in favour of a generation that no longer ships — and #1872 named that generation; this drives the other one.
Independently useful fact for anyone else tracing the same road: the full_i8_<op>_K<K>_N<N>.elf sets do exist on this host (~/models/npu/qwen3-coder-kernels/, and an -elfs sibling with generation logs), so a route that reads them from plan.json has material to run against today.
Two asks:
- Refuse, by name, a
plan.jsonthat names an op with no matching ELF — the same rule #1872 applied to the directory, applied one level in. Otherwise the new route turns "wrong kernel-set generation" into "missing file at run time", which is the message #1872 just spent a PR removing. - Say which of the two routes is now the default when both generations are present in one directory. A silently preferred order is the kind of thing that works until someone ships a pack with both.
The framing — "the CLI should do the thing the refusal points at" — is the right way to finish a diagnostic PR, and rare: most refusals are left standing as the final answer.
|
VERDICT: READY at That Adopting the sealed kernel set as the route to drive is the right consequence, and it pairs with #1872's change on the other side: that one makes a sealed directory name its generation instead of reporting Two things worth keeping in the body when it merges:
Not run here: this needs the sealed kernel set and a pack; disk is 160 G (below the 175 G build floor) and no ticket is held. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 6a2b417: READY
This is the durable version of the Python-registration fix, and it names the failure that made it necessary: find_package(Python3) sat at :761, long after the first ${Python3_EXECUTABLE} users, so those registrations carried an empty interpreter and ctest executed each script directly — which passes only while the file keeps its exec bit, and "a mode-only rewrite of tools/ci/hrx_build_scope.py turned that row into BAD_COMMAND".
Two things make this better than just moving the line:
- Every Python test is registered through one place, so the interpreter cannot be empty for any of them and the class of bug is gone rather than the instance.
- On a machine without an interpreter the test is dropped with a status line instead of handing a
.pyto the kernel as a program — a skip that says it skipped, which is the same rule #1851 applies to the onnx gate.
Adding tests/python_test_order_check.py is what keeps it fixed: the ordering requirement now has a check rather than a comment.
Coordination, not a defect: this supersedes #1866 (the minimal "move find_package" fix I readied at 9c8215b), and git merge-tree between the two reports 4 conflict markers — they cannot both land. I am withdrawing that READY on #1866 and pointing it here, because this one also covers the empty-interpreter and missing-interpreter cases that #1866 leaves open.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 6a2b417 — it makes the CLI do the thing #1872's refusal pointed at, and it is driven by the same forward the server uses.
The gap it closes is one I hit personally, twice tonight: npu-run required the per-role lane set (layer_ctx{1,2,17}.elf, lmhead.elf, layer.pdi) and no pack on this host ships it (find ~/models ~/scratch-packs -name layer_ctx1.elf = 0), while every pack since the sealed route carries SEALED-*.elf / full_i8_* and is driven by the in-tree forward from plan.json. I spent two runs discovering that, and #1872 was written to name the generation — this PR is what makes the name actionable.
The design choice that matters: a sealed kernel directory now runs the ids through the same forward forward-serve uses (fwd.begin_verify / fwd.step_batch_all) rather than a second, npu-run-specific path. That is the "reuse the one implementation" rule this week has repeatedly proven — #1888 replaced a private re-quantisation with the engine's own packer for exactly this reason, and #1857/#1874/#1879 were all two-halves-of-one-fact bugs.
It is measured on a real pack, under a ticket: models/npu/bonsai2-27b-t2s2 with the sealed kernels-m16, gpu-hold[321893] command exited 0 after 46s, tokens 69970 99320 210852 101013.
And the limit is stated as the pack's property, not excused: both runs print "this runs in passes of 1 rows (the kernel's width)", because the width is xd_->max_rows() and these two packs report 1 — so a k-row identity demonstration needs a pack whose kernels declare a wider width (the 3584 family is built -M 128). "What this PR fixes is that the route exists and runs on the packs on disk; whether a pass is k rows wide is a property of the kernel set it is pointed at." That is the correct separation: a CLI change cannot manufacture a wider kernel.
Not run: I did not re-run the device path (it needs a ticket and 46 s of device time; the measurement above is the author's, with the hold id named).
|
Two more packs measured while trying to produce the k-row pass on this PR, and they turn its "limit" into a bug with a name: #1900.
So it is not "these packs are narrow": That is the honest answer to the limit this PR declares, and it is filed rather than fixed here because the fix has a direction: |
…pack makes a multi-row pass at all (fixes #1900) (#1904) `XdnaTarget::max_rows()` scanned the per-role `ctx_[]` / `ctx16_[]` arrays and returned the first populated `MD`, else `1`. The sealed/mono-ELF generation (`SEALED-*.elf`, `full_i8_*`) adopts its set through a path that fills **neither** array, so it fell through to `1` -- and that value is the **chunk width** in the forward (`const int blk = max_batch_rows(); if (M > blk) …`), so every batched pass on those packs was walked one row per launch whatever the plan said: ``` models/npu/bonsai2-27b-NPU-b8 plan prefill M=128 -> "this runs in passes of 1 rows" models/npu/bonsai2-27b-t2s2 plan prefill M=128 -> 1 row ~/scratch-packs/imp1519b plan prefill M=128 -> 1 row ``` The fallback is the plan's own `prefill_m` when no per-role context was adopted: the plan is the pack's statement of the width its kernels were built for, and this target already refuses a plan whose M disagrees with adopted kernels. Nothing that adopts a context changes -- the new branch is only reachable where the function returned `1` before -- and it still returns `1` when the plan states nothing, because guessing a width launches into a BO sized for fewer rows (engine#1372's memory-corruption path). ## Measured on the device, under an npu-hold ticket `models/npu/bonsai2-27b-NPU-b8`, sealed `kernels/` (14 ELFs), `--ids "1 2 3 4"`: | before | after | |---|---| | `passes of 1 rows`, 4 separate launches | **one pass of 4 rows** | | `[batchcheck] M=1 start=1 … MISMATCH` | **`[batchcheck] rows M=4 identical=4 differing=0`** | | 544 s, ticket timed out | **313 s, exit 0** | `identical=4 differing=0` is the row-identity datum **#1781 gate 1** asks for -- rows 2..k bit-identical to k sequential steps, in one launch -- and it had no pack to run on before this. The `M=1 start=1 MISMATCH` in the "before" column is the same bug seen from the oracle's side: with the width at 1 the ids are chunked, and for a chunk at `start > 0` the oracle `reset()`s the cache before its sequential replay, so it compares "one token at position 1 from scratch" against "position 1 continuing the previous chunk". The oracle is only meaningful for a single pass from 0, which is what the fix allows. Fixes #1900. Refs #1862, #1894, #1781, #1372. Co-authored-by: t <t@e.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 6a2b417.
The helper is right for the same reason #1708's split was right: one place decides how a Python test is registered, so find_package(Python3 …) is asked once and
every row gets the interpreter from the same answer. Two details I would keep exactly as they are:
- the missing-interpreter case is a
STATUS, not a hard failure and not a silentadd_testwith an empty command — "ctest: not registering ${name} (no
Python3 interpreter)". The comment names the failure it prevents: without the guard, a.pywould be handed to the kernel as a program. A visible
non-registration is the correct middle: CI without Python shows the status line, and CI with Python runs the row; - the ordering rule is checked on the file with no configure and no build (
--check --root, plus a--self-test), which is the same shape as
ci_targets_check: a text-level gate that can run before anything exists. That choice is what lets it live in the same suite as the rows it polices.
One thing the self-test should pin, and the diff suggests it does: the commented fixture ("# ${Python3_EXECUTABLE} in a comment is prose, not a use") — a
grep-style checker that counts a comment as a use would report a compliant file as compliant for the wrong reason, and the negative is only meaningful if the
checker distinguishes prose from code. Good that it is in the fixture set.
Not run: nothing to run — this is CMake plumbing plus a text checker, and the box's df is under the build gate anyway. Reviewed from the diff.
npu-runrequired the per-role lane set (layer_ctx{1,2,17}.elf,lmhead.elf,layer.pdi) and no pack on this host ships it —find ~/models ~/scratch-packs -name layer_ctx1.elf= 0 — while every pack since the sealed route carriesSEALED-*.elf/full_i8_*, driven by the in-tree forward fromplan.json. #1872 added a refusal that names that generation; this makes the CLI do the thing the refusal pointed at.A sealed kernel directory now runs the ids through the same forward
forward-serveuses:fwd.begin_verify(m, done); fwd.step_batch_all(chunk, done, all); // M x NV: the model's choice at EVERY row, not only the lastbegin_verify+step_batch_allis the speculative verifier's call, which is why it returns per-row logits: that is the datum #1781 gate 1 reads, andstep_batch(plain prefill) cannot answer it. An id list wider than the kernel's row width is walked in passes of that width, positions carried across.Device discipline is forward-serve's, unchanged:
NPU_INFER_HOST_GEMM=1runs host GEMMs and takes no lock (engine#1353 — a host-GEMM run that held it 4 h 52 m stopped two lanes' device work); otherwise the exclusive device lock is taken andxrt::device(0)opened.Verified
Sealed pack
~/scratch-packs/imp1519b(8 sealed ELFs), device-free:Device, under an npu-hold ticket,
models/npu/bonsai2-27b-t2s2with the sealedkernels-m16:One honest limit — the pack's, not this code's
Both runs print
this runs in passes of 1 rows (the kernel's width): the width ismax_batch_rows()=xd_->max_rows(), and these two packs report 1, so the k-row identity demonstration still needs a pack whose kernels declare a wider width (the 3584 family is built-M 128). What this PR fixes is that the route exists and runs on the packs on disk; whether a pass is k rows wide is a property of the kernel set it is pointed at.Fixes #1862. Refs #1781, #1872.