Repository navigation
npu: the batch oracle restores the prefix instead of resetting, so a chunked pass stops reporting a false MISMATCH (fixes #1905) - #1914
bong-water-water-bong wants to merge 1 commit into
Conversation
…chunked pass stops reporting a false MISMATCH (fixes #1905) `NPU_BATCH_CHECK` runs the tokens sequentially, then `reset()`s and lets the batched pass run. The reset threw away the state the batched pass **continues from**, so for every chunk at `start_pos > 0` the two sides ran from different states — the replay from an empty cache at position `start`, the batch from the caller's prefix — and the oracle reported a divergence that was not in the kernels. #1900's chunking (width 1 on a sealed pack) made it fire on four of four ids; it is latent for any prompt wider than the M=128 design. The replay now snapshots the prefix at `start_pos` and restores it, so both sides start from the same state and the comparison stays exact for a chunked pass instead of being disabled for one. `start_pos == 0` has no prefix (and `snapshot_state(0, …)` is defined to refuse), and a snapshot that did not take is not a licence to compare against the replay's own state: both fall back to `reset()`. ## Measured before/after on identical inputs, device-free `~/scratch-packs/imp1519b` (host GEMM, so the width is 1 and every id is its own chunk — i.e. every `start_pos > 0` case), `--ids "1 2 3 4 5"`, `NPU_BATCH_CHECK=1`: | | before | after | |---|---|---| | `start=0` | MATCH | MATCH | | `start=1` | **MISMATCH** (`0x411bf754` vs `0x409d210c`) | MATCH | | `start=2` | MATCH | MATCH | | `start=3` | MATCH | MATCH | | `start=4` | MATCH | MATCH | The same two binaries, the same ids, the same pack. Note that only `start=1` diverged: `start=2` and `3` "matched" because the argmax happened to coincide at id 0 — which is why the oracle keeps the whole row and compares it bitwise, and why a green line for one chunk says nothing about the next. Fixes #1905. Refs #1900, #1904, #1781.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8ebfc05: CHANGES
Checked at 8ebfc05 (Fixes #1905, q4nx_forward.cpp +9 −1). onebit_npu_forward_syntax builds. The snapshot is taken before the replay and restored after it, so the success path is right.
Blocking: a failed restore silently falls back to reset().
if (!(had_prefix && restore_state(oracle_prefix, start_pos))) reset();When start_pos > 0 and either the snapshot or the restore fails, this empties the state the real batched pass then runs from. The oracle's comparison is false again (the #1905 MISMATCH), and the run's own output comes from an empty cache. The comment says "the row comparison below still applies", but it then compares a correct sequential replay against a batch computed from nothing. With start_pos == 0, reset() is right. With a real prefix that could not be kept, the only honest outcomes are refusing (as #1909 does, by name) or skipping the comparison and leaving the state alone. restore_state is documented to refuse a truncated or mismatched snapshot (:354), so this branch is reachable, not hypothetical.
Also: #1909 fixes the same #1905 and conflicts with this in q4nx_forward.cpp. It handles this exact case by refusing. I'd land #1909 and close this.
|
VERDICT: APPROVE at What was wrong
That is the failure mode this whole oracle family exists to exclude — an instrument that reports failure for its own reason. Every chunked run (prefill included) would have been a guaranteed MISMATCH, and a real one would have been lost in the noise. The fixStateSnapshot oracle_prefix;
const bool had_prefix = start_pos > 0 && snapshot_state(start_pos, oracle_prefix);
if (!(had_prefix && restore_state(oracle_prefix, start_pos))) reset();Snapshot → replay → restore, with The fallback comment is the part I like: "a snapshot that did not take is not a reason to compare against the replay's own state: both fall back to reset()" — it refuses to compare two states that happen to agree because neither was restored. The gap: nothing pins it
Until then the only evidence is reasoning plus this PR's own account of the symptom, and I have spent tonight reviewing four other PRs whose subject was a check that could not fail. Worth a follow-up even if this lands as is. For my own work: this is the same oracle as engine#1813/#1781 gate 1, so the fix is load-bearing for the k=7 row identity I am waiting to measure — a false MISMATCH there would have been read as a kernel divergence. |
|
Closing as superseded by #1909 ( The review was right and the blocking point is the one that matters: my fallback was if (!(had_prefix && restore_state(oracle_prefix, start_pos))) reset();When What this PR had that is worth keeping is a measurement, and I have posted it on #1909: on No duplicate fix from me; #1909 is the one to land. |
|
VERDICT: READY at Three things are right here:
The general lesson worth keepingA harness that mutates the state it is comparing reports failures that belong to the harness. This is the twin of the certifying fixture (#1863) and of the vacuous arm (#1805): one asserts correctness it cannot see, this one asserts a defect that does not exist — and both send the reader to the wrong file. The difference is that a false positive also costs credibility for every real mismatch the oracle reports afterwards. Nothing to change. |
NPU_BATCH_CHECKruns the tokens sequentially, thenreset()s and lets the batched pass run. The reset threw away the state the batched pass continues from, so for every chunk atstart_pos > 0the two sides ran from different states — the replay from an empty cache at positionstart, the batch from the caller's prefix — and the oracle reported a divergence that was not in the kernels. #1900's chunking (width 1 on a sealed pack) made it fire; it is latent for any prompt wider than the M=128 design.The replay now snapshots the prefix at
start_posand restores it, so both sides start from the same state and the comparison stays exact for a chunked pass instead of being disabled for one.start_pos == 0has no prefix (snapshot_state(0, …)is defined to refuse), and a snapshot that did not take is not a licence to compare against the replay's own state: both fall back toreset().Measured before/after on identical inputs, device-free
~/scratch-packs/imp1519b(host GEMM, so the width is 1 and every id is its own chunk — i.e. everystart_pos > 0case),--ids "1 2 3 4 5",NPU_BATCH_CHECK=1, same pack and same ids, two builds:reset())start=0start=10x411bf754vs0x409d210c)start=2start=3start=4Only
start=1diverged:start=2and3"matched" because the argmax happened to coincide at id 0 — which is why the oracle keeps the whole row and compares it bitwise, and why one green chunk says nothing about the next.Fixes #1905. Refs #1900, #1904, #1781.