Repository navigation
npu: the batch-check oracle compares from the same state it claims to, and names the positions (fixes #1905) - #1909
Conversation
…, and names the positions (fixes #1905) `NPU_BATCH_CHECK` ran a sequential replay over `start_pos .. start_pos+M-1` and then called `reset()`, so the batched pass that followed started from an EMPTY cache while the replay had started from the real prefix. For any chunk with `start_pos > 0` the oracle therefore compared "one token at position 1 from scratch" against "position 1 continuing the previous chunk" and reported MISMATCH — measured on `bonsai2-27b-NPU-b8`, width 1, four ids: [batchcheck] M=1 start=0 seq_argmax=198 batch_argmax=198 MATCH [batchcheck] M=1 start=1 seq_argmax=220 batch_argmax=653 MISMATCH The first row matching and every later row not is the signature of a state mismatch in the debug path, not of a row-dependence bug in the kernels -- and it sends the reader to the kernels. Latent until now because the packs on this box make a single pass (#1904); it goes live as soon as an M=128 prefill chunks a prompt, where every chunk after the first would "diverge". **The fix uses what the forward already has:** `snapshot_state(start_pos, snap)` before the replay and `restore_state(snap, start_pos)` after it -- the pair the prefix cache uses, whose plane-copy half (`q4nx_state_planes.h`) is device-free and unit-tested. `reset()` stays for the `start_pos == 0` case, where there is nothing to restore and it is exactly right. If the snapshot cannot be taken the oracle REFUSES with a message rather than comparing two different states again. **And the line names the positions compared:** `positions=%d..%d` instead of only `start=%d`, so the range is in the output rather than inferred from `start` and `M`.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 473fdf4: READY
Checked at 473fdf4 (q4nx_forward.cpp +27 −3). onebit_npu_forward_syntax builds at this head. The change is in the NPU_BATCH_CHECK debug path only, which no host test drives, so this is a read.
- The snapshot is taken before the replay (
snapshot_state(start_pos, …), then thestep()loop), so it holds the prefix the batched pass continues from. Restoring it instead ofreset()makes the two sides start from the same state. That is the cause of thestart=1 … MISMATCH#1904 measured. - A failed restore is refused by name (
"batch check: the sequential replay's prefix could not be restored …") instead of falling through. That matters, becausereset()here doesn't just mislead the oracle: it is the state the real batched pass then runs from. - The new
positions=a..blog line has no parser to break (git grep '\[batchcheck'outside the forward: nothing).
Non-blocking:
|
VERDICT: READY at I had the same fix open (#1914 at
Measurement I have that this PR does not, offered as support (device-free,
Only Nothing else from me: the snapshot pair is the same one the prefix cache uses, |
…#1905) (#1919) * tests: pin #1909's batch-check fix, which merged with no test (engine#1905) #1909 made the NPU_BATCH_CHECK oracle restore the prefix it replays from instead of reset()ing it. It changed q4nx_forward.cpp only, so nothing fails if the reset comes back. tests/batch_check_state_test.cpp runs the forward in host-GEMM mode (it skips with 77 unless ONEBIT_TEST_Q4NX names a container, like mtp_batch_all and gdn_batch_order). It splits six ids into chunks of 3, 2 and 1, the last being the serve path's split-off final token. After each chunk it checks that the forward holds exactly the positions run so far, then checks the last logits against plain step(). NPU_BATCH_CHECK is a per-process static, so it is set before the first call, and the reference is step(), which the oracle does not touch. On Qwen3-0.6B: main 4/4 pass (rel-L1 0). With q4nx_forward.cpp from before #1909: 3 FAIL. The forward holds only the last chunk, and argmax is 4605 vs 368. docs/npu.md gets the oracle's paragraph; it was documented nowhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tests: batch_check_state is bit-exact and says where it runs (review on #1919) On host GEMM the batched and sequential passes are the same arithmetic in the same order (rel-L1 measured 0.00e+00), so the logit check is now exact; a 5e-3 window could hide a one-row prefix leak. The CMake row and the test header now say the row exists only under ONEBIT_NPU, which ci.yml does not configure: a dev-box test, not CI coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ated container (fixes #1946) (#2039) * tests: batch_check_state runs in CI, on the runtime stub and a fabricated container (engine#1946) batch_check_state_test pins #1909 (the NPU_BATCH_CHECK oracle must restore the prefix it replays from), but it links the NPU forward, whose runtime is private, and it needs a Q4NX container. So it existed only under ONEBIT_NPU and a regression of #1909 could land green. - onebit_npu_forward_library(name runtime): the forward's one source list and link interface, used by onebit_npu_forward (private runtime) and by onebit_npu_forward_ci (onnx/ep/npu_runtime_ci_stub.cpp, whose device calls throw; the host-GEMM forward makes none). - In a configure without ONEBIT_NPU (CI's), batch_check_state_test links the stub-built forward and is built through add_dependencies(onebit ...). tests/batch_check_state_ci.py writes a seeded two-layer qwen3 (hidden 256), packs it with tools/safetensors_to_q4nx.py and runs the test on it: no device, no model, ~3 s. Skips (77) only without numpy. Measured: passes on main; with #1909 reverted it fails 3 of 4 (holds 2 and 1 positions instead of 5 and 6, rel-L1 0.78). The NPU-lane configure still builds its three container rows. ci_targets_check: the by-design bucket goes 5 -> 4 and nothing new is unbuilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tests: batch_check_state_ci's container goes under tmp_root(), not /tmp (engine#2029 rule, #2034) The fabricated container and its HF source are about 2 MB, but tests/tmp_root.h's rule now applies to every test: ONEBIT_TEST_TMP, else ~/.cache/scratch-tests/tmp, else the system temp directory. Re-run in the CI-style configure: Passed, directory removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
NPU_BATCH_CHECKran a sequential replay overstart_pos .. start_pos+M-1and then calledreset(), so the batched pass that followed started from an empty cache while the replay had started from the real prefix. For any chunk withstart_pos > 0the oracle compared "one token at position 1 from scratch" against "position 1 continuing the previous chunk" and reported MISMATCH. Measured by the issue onbonsai2-27b-NPU-b8, width 1, four ids:That pattern — first row matches, every later row not — is the signature of a state mismatch in the debug path, not of a row-dependence bug in the kernels, and it sends the reader to fix the wrong thing. It is latent today because the packs on this box make a single pass (#1904); it goes live the moment an M=128 prefill chunks a prompt, where every chunk after the first would "diverge".
The fix uses what the forward already has
snapshot_state/restore_stateare the pair the prefix cache already uses, and their plane-copy half (q4nx_state_planes.h,take_whole) is device-free and unit-tested.reset()stays forstart_pos == 0, where there is nothing to restore and it is exactly right — so the change is inert for the single-pass case that works today.If the snapshot cannot be taken, the oracle refuses with a message rather than repeating the old behaviour of comparing two different states and calling it a divergence.
And the line names the positions compared
[batchcheck] M=… positions=<start>..<start+M-1> …instead of onlystart=…, because the old line made the reader infer the range — and, worse, made a bad comparison look like a good one.Testing
Not built locally: the box is at 155 GB free, under the floor these rules set for a build tree, so CI's
build+ctestis the compile check. The device measurement that motivates the change is the issue's, taken onbonsai2-27b-NPU-b8under annpu-holdticket; the arm that would pin this end to end needs a pack whose pass width is > 1, which the issue's own note says is the same blocker as the k-row identity demonstration (the 3584 family is built-M 128).tools/copyright.py --checkrc 0.