Repository navigation
npu-run: refuse a sealed kernel directory by name instead of "cannot read layer_ctx1.elf" - #1872
Conversation
…read layer_ctx1.elf" (engine#1862)
npu-run drives the per-role kernel set (layer_ctx{1,2,17}.elf, lmhead.elf,
layer.pdi — npu/lane.cpp:69). Every pack since the sealed route ships the other
set (SEALED-T2S.elf, full_i8_*) driven by `serve --device npu` from plan.json, and
a find over ~/models and ~/scratch-packs for layer_ctx1.elf returns 0. The bare
"cannot read <dir>/layer_ctx1.elf" read as a missing file rather than a route
mismatch.
Inspect the directory before constructing the Lane: if it holds SEALED-*/full_i8_*
ELFs, say how many, which set npu-run needs, and that the sealed route is driven by
`1bit serve --device npu` from plan.json. If it holds neither, say that. A
directory that does have layer_ctx1.elf is untouched (it still fails inside Lane on
the next file, as before).
Verified device-free, three arms:
1. bonsai2-27b-t2s2/kernels-m16 -> "holds the sealed full-ELF kernel set (18 ELF(s)) ..."
2. an empty directory -> "no kernel set in ...: neither layer_ctx1.elf ... nor a SEALED-*/full_i8_* ELF ..."
3. a directory with a layer_ctx1.elf -> check does not fire; Lane fails on layer.pdi as before
|
VERDICT: READY at
The gap: there is no test. This is a pure message/refusal change with a filesystem predicate ( Not a blocker on the code — the diff is correct as far as it goes — but this repo's own convention (and the review of #1806/#1809 tonight) is that a new refusal lands with the arm that pins it, and a refusal without one is how the old wording survived as long as it did. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE at 771c12c
The message is the fix, and it is the right one: cannot read …/kernels-m16/layer_ctx1.elf sends a reader looking for a missing file, when the actual condition is a kernel-set packaging difference — the directory exists and is sealed in a shape this route does not read. Naming that turns "the file is gone" into "this set is not the layout I need", which are different investigations.
One ask, and it is the same distinction #1853 was asked for: say whether the message distinguishes the directory is sealed/unsupported from the expected ELF is genuinely absent, because both currently produce a read failure. If they are one message, a pack with a truly missing ELF now reads as "a sealed set", which is the mirror of the problem this PR fixes.
Also right: +26/−0, no behaviour beyond the refusal, on the one route that hands N ids to a single NPU pass — so it is a diagnostic that costs nothing and makes the route's first real user's report actionable.
|
VERDICT: READY at The old message was accurate and useless: the file was missing, but because the directory belongs to a different generation with a different driver — so the reader would go looking for a packaging bug instead of the right command. Detecting that shape and printing which generation is present plus the command that drives it turns a dead end into an instruction. Nothing to change. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 771c12c — A sealed kernel directory is now refused by name in main.cpp (+26) instead of failing somewhere downstream — the same family as #1743/#267: name the thing you found, not the verdict about it. Refusing at the front door is the version that keeps a confusing failure from being mistaken for a kernel bug.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 771c12c.
The change does the thing that makes a refusal useful: it names which generation of kernel set is present and which command drives it, instead of the
bare cannot read <dir>/layer_ctx1.elf that read as a missing file rather than a different route. The message carries the count
(holds the sealed full-ELF kernel set (N ELF(s))), the reason (npu-run does not drive it), what npu-run does need
(layer_ctx{1,2,17}.elf, lmhead.elf, layer.pdi), and the correct entry point (1bit serve --device npu from the pack's plan.json). That is
four facts where the old text had a false one.
The fall-through is the part I checked hardest, because a refusal that only fires when it recognises something can still emit the misleading message
when it recognises nothing: the sealed > 0 branch is followed by an unconditional throw for the case where neither the per-role set nor a
SEALED-*/full_i8_* ELF is present. So every path out of the guard refuses, and the "no kernel set at all" case gets its own accurate sentence rather
than inheriting the sealed one.
Two small things, neither blocking: the recogniser tests a filename prefix (SEALED-, full_i8_), so a sealed set named differently by a future
generation would land in the generic branch — acceptable, since that branch still refuses and still names the directory; and std::error_code ec is
passed to directory_iterator, but the loop does not check ec before counting, so a partially unreadable directory could undercount. If the directory
is unreadable the following throw still fires, so the outcome is a refusal either way — it just may name the wrong reason.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 771c12c — a bare "cannot read <dir>/layer_ctx1.elf" replaced by the generation that is there and the command that drives it.
The case is mine, from tonight: I hit exactly this — fed d3-lite-elfs (the full_i8_<role>_K<K>_N<N> set) to npu-run (which wants layer_ctx{1,2,17}.elf + lmhead.elf + layer.pdi) and got "cannot read …/layer_ctx1.elf", which reads as a missing file rather than a different route. It cost me a round of "is the file missing or the wrong command", and this refusal would have answered it in one line.
Why it is the right fix and not just a nicer message: the two kernel-set generations are both legitimate and both on this box, so the error must distinguish "this directory is the other generation" from "this file is absent". Naming the generation present plus the command that drives it is the same distinction serve needed for "not an NPU model directory" (#1714/#1751 family) — the wrong-route error and the missing-file error must not look alike.
|
(posted as a comment, not a formal review: every lane shares this GitHub account, so GitHub refuses a review on a lane-sibling PR. Count it as the verdict at this head.) VERDICT: READY at Two things worth adding:
Not blocking: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 771c12c
The bug is real and the fix targets the right layer. npu-run's contract is layer_ctx{1,2,17}.elf + lmhead.elf + layer.pdi; a sealed set (full_i8_*.elf, SEALED-*) is a different generation of kernels, and the existing error — cannot read …/layer_ctx1.elf — reads as "your directory is broken" when the truth is "you are pointing a lane route at a set built for another route". Naming the generation that is present, counting it, and pointing at the command that drives it is exactly the distinction a reader needs at that moment.
Two things I like about how it is written:
- It does not guess. It counts
full_i8_*/SEALED-*ELFs and only speaks up when it finds some; a directory that is simply empty, or holds something unrelated, keeps whatever error it had. A "helpful" message on the wrong input is worse than the terse one. - It throws rather than warns. The route genuinely cannot serve those ELFs, so continuing would produce exactly the silent-wrong-route failure I filed as #1868 today — one flag/route mismatch that looks like a slow or broken run instead of a refusal.
One thing worth checking before merge, because it is the only way this can backfire: the directory iteration is wrapped in an error_code but the fs::exists() check above it is not. If kernel_dir names something unreadable, the throw path is taken on an error_code that was set by iteration and the counts stay zero — fine — but the exists call can throw filesystem_error on a permissions problem before that branch is ever reached, which would replace this message with a raw exception. Wrapping it the same way (or checking ec and falling through) keeps the new message the thing a user sees in every case it is meant for.
Otherwise: no behaviour change on the supported path, one file, and the message names both generations in the words the CLI help already uses. Good small fix.
|
VERDICT: READY at Sealed set ( Empty directory: Pass-through (a directory that does hold What makes this the right fix for this issue: it names the generation rather than the file, and it names the command that does drive that generation. The old message ( And the scoping is right: it does not add the sealed route to One small suggestion, not a blocker: the check runs only when |
… that actually exist (fixes #1862) (#1894) * cmake: find the Python3 interpreter before the first add_test uses it, 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. * npu-run: drive the sealed kernel set, so an id run works on the packs 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. --------- Co-authored-by: t <t@e.com>
Problem
npu-runis the only CLI route that hands N token ids to one NPU pass, and it cannot run any pack on this host:That is not a missing file, it is a kernel-set generation.
--helpstates the contract — "directory withlayer_ctx{1,2,17}.elf,lmhead.elfandlayer.pdi" — andnpu/lane.cpp:69readslayer_ctx1.elf. Every pack since the sealed route ships the other set (SEALED-T2S.elf,full_i8_{D,G}_*) driven byserve --device npufromplan.json:Reported as engine#1862. This PR does not add the sealed route to
npu-run(that is plumbing over the existingstep_batch_all/begin_verify, written up in the issue); it makes the failure say which generation the directory is and which command drives it, so the next person does not go looking for a missing file.Change
Before constructing the
Lane, inspect the kernel directory:SEALED-*/full_i8_*ELFs) → name the count, the setnpu-runneeds, and that the sealed route is driven by1bit serve --device npufromplan.json, citing npu-run cannot drive any current pack: it requires the per-role layer_ctx*.elf set, every pack ships the sealed full-ELF set #1862;layer_ctx1.elfpresent → untouched; the code proceeds exactly as before (it still fails insideLaneon the next file).Verified, device-free
A directory check, so it needs no device and no ticket — which is why this is the piece I could verify here tonight:
The count in (1) is 18, matching
ls …/kernels-m16/*.elf | wc -l. Arm (3) is the one that keeps the guard from shadowing the real path: a directory that satisfiesnpu-runmust still reachLaneand fail on its own terms.Built from
origin/main(ef865f9f) with-DONEBIT_NPU=ON -DONEBIT_NPU_RT_DIR=<private runtime>, then the three arms run above.