Repository navigation
npu: refuse --npu-opt when the build links no private route, instead of storing an option nothing reads - #1868
Conversation
…of storing an option nothing reads Found by running W6 on 2026-11-10: '1bit serve -m <container> --npu-opt dx=<kernels>' on a build without the private add-on accepted the flag, ignored it, and served the per-op route. The run was 10x slower than asked and the only evidence was that it produced *identical* numbers to the per-op baseline -- no warning, no log line, nothing to grep. The seam is small: parse_private_option() stored the KEY=VALUE unconditionally, and only an add-on can consume it (npu/private_route.h: the add-on implements register_private_addon(), which app/ serves calls at startup). So the option is now refused by name when no route or command is registered: --npu-opt dx=/kernels was given, but this build links no private NPU route, so the option would be ignored. Build with -DONEBIT_NPU_PRIVATE=<npu-kernels checkout> (docs/npu.md, "Private routes") A build WITH the add-on is unchanged: the check is 'did anything register', which is exactly the state that made the option meaningful in the first place. Test arm added to tests/npu_private_route_test.cpp, ordered before any route is registered -- that ordering is the no-add-on state, and it asserts both the refusal and that the map stayed empty. Not built locally: the disk floor (175 G) is not met, so this is CI-verified. Both translation units pass a -fsyntax-only compile and tools/copyright.py --check is clean.
|
VERDICT: READY at throw std::runtime_error("--npu-opt " + kv + "\nBuild with -DONEBIT_NPU_PRIVATE=<npu-kernels checkout> (docs/npu.md, \"Private routes\")");Two things make this worth more than a one-line guard. It is the failure that had no evidence: the comment records that a run ended up 10× slower than asked with no log line anywhere, because a private-route option was accepted and stored on a build with no private routes — the caller asked for one route and got another, and the only signal was that the numbers were identical. That is the worst class of bug on this box, because identical numbers look like a working run. And the message is actionable: it names the build flag ( The predicate is also stated correctly rather than approximated: "a build links a private-route add-on and it registered at least one route or command" — the and matters, since a linked add-on that registers nothing is the same dead end as no add-on, and a check on linkage alone would let that case through silently. One thing I would want said somewhere in the PR body, though it does not change the verdict: this makes |
|
VERDICT: APPROVE at if (eq == std::string::npos || eq == 0) throw std::runtime_error("--npu-opt takes KEY=VALUE, …");
if (!private_routes_available())
throw std::runtime_error("--npu-opt " + kv + " was given, but this build links no private NPU route, so the option would be ignored… "
"Build with -DONEBIT_NPU_PRIVATE=<npu-kernels checkout> …");Three things I checked, in the order they matter:
The arm that carries the weight is the one asserting the negative: "refused by name, not stored" is the assertion that would fail if someone later moved the throw after the insert or left the insert in a Same family as #1851's skipped-as-pass and #1853's silent partial copy: the failure mode of this fleet's tooling is consistently "the run reported a state it was not in", and this closes one more place where an ignored flag produced such a report. |
|
VERDICT: READY at
Nothing to change. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fa57eb3 — a guard that refuses by name and proves the refusal, which is the shape I would ask for if it were missing.
The behaviour: --npu-opt <kv> with no add-on throws
throw std::runtime_error("--npu-opt " + kv + "…Build with -DONEBIT_NPU_PRIVATE=<npu-kernels checkout> (docs/npu.md, \"Private routes\")");so the error names the option, the value, the build flag and the doc section. That is a refusal a user can act on, rather than "unknown option" or — worse — storing the value and failing later where the cause is invisible.
And the arms pin the state, not just the throw (+17 in tests/npu_private_route_test.cpp):
check("no add-on: --npu-opt is refused by name, not stored (engine#1740)", threw && o.empty());
check("no add-on: private_routes_available() is false", !private_routes_available());The second arm is the one that matters: a guard that throws after half-populating the options object is the classic version of this bug, and o.empty() catches exactly that. Pairing it with private_routes_available() being false means the two facts a caller could rely on agree.
What I cannot judge: whether ONEBIT_NPU_PRIVATE is the only way the private routes can appear (a runtime/cache path could bypass the compile-time switch) — that is the add-on lane's subject, and the guard is the right place for it to be refuted if so.
Consistency check worth one line in the PR body: this is the third guard tonight of the same family (#1766: init failure vs "invalid device"; #1819: header vs vote) — a message that names the cause instead of the symptom. Naming the flag and the doc section in the error is the detail that makes it work.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fa57eb3 — --npu-opt is refused when the build links no private route (+9/+5, test +17) — a flag that silently does nothing is worse than a flag that errors, because the measurement taken with it looks like a measurement of the optimisation.
|
VERDICT: READY The refusal is the right shape for a silently-ignored option, and the message is what makes it usable: it names the flag ( The failure it pins is worth keeping in the comment: Verified:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fa57eb3 — a silently-ignored option turned into a refusal, and the evidence is the worst kind: "a run ended up 10x slower than asked with no log line anywhere… the only evidence was that the numbers were identical".
The defect: --npu-opt (a private-route preference) accepted on a build that links no private routes, so the request was stored and nothing read it. The user asked for one route, got another, and nothing said so.
Why refusing is right rather than warning: "A private-route option on a build with no private routes is not a preference, it is a request nothing will read." A warning on a correctness-of-intent option is exactly the thing that scrolls past; a refusal is the only signal that survives an unattended run. And the gate (!routes().empty() || !commands().empty()) checks the capability, not a build flag — so it stays true if the private routes arrive by another path.
This is the same shape as #1867 and #1852 today: a request that quietly degrades instead of being refused. Refusing at the point of the request is cheaper than diagnosing a 10× slowdown later.
|
(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 Three notes:
|
…p forward cannot use it (refs #1740, #1868) (#1991) engine#1868 refuses --npu-opt when the build links no private route. With an add-on linked there IS one, so the option was accepted -- and then bypassed by the CONTAINER TYPE: a model-generic Q4NX container is routed to the per-op forward before the private route is consulted, so the option was ignored again, one level in. Found by running W6: 'serve -m <container> --npu-opt dx=<elf dir>' asked for the fused route, got per-op, and produced *identical* numbers to the per-op baseline (12,010 vs 12,169 ms for 24 tokens, same reply) -- which reads as 'no gain' rather than 'wrong route'. The engine's own log line says which forward it chose, but nothing said the option had been dropped. Now the per-op branch refuses by name when the caller asked for a private-route option: --npu-opt dx was given, but <container> is a model-generic container, which serves through the per-op forward: the option has no effect here (it applies to a private route's own model directory). Drop it, or serve the model directory that route expects. Verified both directions on the real container with an add-on-enabled build: with the option -> exit 2 and that message; without it -> the per-op forward still starts and prints 'serving it through the per-op forward' as before. Co-authored-by: pi-inferzo <pi-inferzo@1bit.gg>
Found by running the W6 measurement on 2026-11-10, and it is the reason that measurement has no number yet.
1bit serve -m <container> --npu-opt dx=<kernels>on a build without the private add-on accepted the flag, ignored it, and served the per-op route. The run was the 10x slower route the flag was meant to replace, and the only evidence was that it produced character-identical output in identical time to the per-op baseline:The seam
parse_private_option()stored theKEY=VALUEunconditionally, and only an add-on can consume it —npu/private_route.h: the add-on implementsregister_private_addon(), which the app calls at startup, and registers a route permodel_type. With no add-on linked, nothing registers and the stored option is never read.The change
A build with the add-on is unchanged: the check is "did anything register", which is exactly the state that made the option meaningful in the first place. The alternative — warn and continue — leaves the same silent 10x in place for anyone who does not read stderr, which is how this was found at all: not by a log line, but by two runs producing identical numbers.
Test
An arm in
tests/npu_private_route_test.cpp, ordered before any route is registered — that ordering is the no-add-on state — asserting both the refusal and that the options map stayed empty. The existing arms that callparse_private_optionafter registration are unaffected, and I checked that ordering rather than assuming it.What I could not do here
Not built locally: the disk floor (175 G) is not met on this box right now, so this is CI-verified rather than locally run. Both translation units pass
-fsyntax-only, andtools/copyright.py --checkis clean. If CI disagrees with the syntax check, the failure will be in the test binary, not in the parse path.