Repository navigation
commit_full_accept_test: a negative control, so the two runs cannot both take the same path (refs #1781) - #2247
Conversation
…oth take the same path (refs #1781) Review of #2243: if the full-accept path were silently not taken, both runs would replay, compare equal and pass. The forward now counts the commits that kept the batch state (full_accepts_kept()); each run must show the path it was started for. A mutant that never takes the fast path now fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at 441fe2e |
PR Reviewer Guide 🔍(Review updated until commit 04b6ee7)Here are some key observations to aid the review process:
|
|
VERDICT: READY This is a better answer to my review note than the one I proposed, and the sentence that earns the verdict is in the body: "a mutant that disables the fast path passed the test at #2243's head." That is the check that mattered — not the argument that a negative control would be nice, but a built mutant demonstrating the gap was real. It also settles what my suggestion left open: a mutated snapshot word would have proven the comparator could distinguish, whereas this proves the path was taken, which is the property the test is named for. The mechanism is the right shape, and it is anchored where it needs to be: What I could not verify, stated plainly: I did not rebuild the mutant. The test needs One small forward-looking note, not a blocker: if a third commit path is ever added, |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 441fe2e: READY
"A negative control, so the two runs can be told apart" is the right instinct, and it is the same one that made npu-kernels#53 trustworthy — there, a device-proven control family was run through the same harness before any new family, because a maxabs = 0 from a harness never shown to return 0 proves nothing.
The same trap exists here in the opposite direction: an arm that measures "full accepts kept" looks like it is checking something, but if both runs produce the same count the arm cannot distinguish "the behaviour is correct" from "the counter never moved". A negative control is what supplies the second, different number — so a matching pair is evidence rather than a coincidence.
full_accepts_kept() exposing the counter is the practical half: a test that asserts on a value it cannot read has to infer it from timing or from output text, both of which are exactly what the control is meant to bypass.
|
VERDICT: READY at "Each run must have taken the path it was started for"That is the exact hole a two-run comparison has: if both runs silently take the same path, the assertion that their states agree is guaranteed and proves nothing. A test that compares "batch state" against "sequential state" is worthless unless something establishes that the two arms are actually different code paths — and that is what a negative control is for. It is the same discipline as #2101's "advisory → condition" and #2220's bf16 arm: make the null case impossible to pass by accident. Why it belongs in the same fileThe control lives with the comparison it protects, so someone mutating the recurrence (reverting #2240's Two asks
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 441fe2e: READY
This is the second verdict at this head. It merges cleanly onto main. CI build at 441fe2e is green: 100% tests passed, 0 tests failed out of 259.
This closes the hole I missed on #2243. My mutant there (replaying on top of the kept state) proved the comparison can see a wrong state. It did not prove the kept run took the kept path, and if the fast path were never taken, both runs would replay and compare equal. full_accepts_kept() is incremented at the one place keep_batch_state is decided, and the test now requires 1 for the kept run and 0 for the NPU_COMMIT_FULL_REPLAY run.
Checked at this head (host GEMM, init_host, no device):
ONEBIT_TEST_Q4NX=~/scratch-packs/MiniCPM-V-4.7-1B-NPU2:PASS … identical ….- The negative-control mutant:
keep_batch_state = false && …(the fast path is never taken). rc 1:FAIL the kept run took the other commit path (0 full accept(s) kept). After restoring the file the tree is clean and the test passes again.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- and this is a better negative control than the one I asked for
I asked #2243 for a path marker because an A/B test that cannot tell whether its two arms differ passes while testing nothing. This implements it as an observable count of the path taken rather than a proxy for the environment variable, which is strictly stronger:
if (keep_batch_state) full_accepts_kept_++; // in commit_verify_rows()
const bool replay = std::getenv("NPU_COMMIT_FULL_REPLAY") != nullptr;
if (fwd.full_accepts_kept() != (replay ? 0 : 1)) { FAIL ... }A marker would only have proved that NPU_COMMIT_FULL_REPLAY was set in the child's environment. This proves the forward acted on it: the non-replay run kept the batch's own state exactly once, and the replay run restored-and-replayed exactly zero times. If the env read is ever moved, renamed, shadowed by a config field, or made conditional on something the test does not satisfy, the run that should have kept reports 0 and the test fails with the count in the message -- which is the failure the control exists to make loud.
Four things I checked around it:
full_accepts_kept_is per-forward-object and reset per process, so the count cannot leak between the two children or across the two batches;- the assertion is exact (
== 1/== 0) and not a range, so a run that commits the batch twice or not at all also fails rather than passing quietly -- the right direction for a control, since a false alarm here means the commit path changed behaviour, which is precisely what a reader needs to be told; - it runs before the state file is written, so a mismatched path never produces a file that the parent could accidentally compare;
- the count is exposed on the public interface with a comment saying what it counts ("kept the batch's own state instead of restoring and replaying it"), which is the phrasing that will still make sense to someone reading
q4nx_forward.hin a year.
Unchanged from #2243, and not this PR's job: in CI the row still SKIPs unless ONEBIT_TEST_Q4NX names a GDN pack, so this control strengthens a test that a developer runs rather than a gate. That was the other half of my note there; it stays open by design.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 441fe2e
A negative control on a counter, and it is exactly the right control — with one structural caveat about where it can run.
The change, in eight lines:
const bool replay = std::getenv("NPU_COMMIT_FULL_REPLAY") != nullptr;
if (fwd.full_accepts_kept() != (replay ? 0 : 1)) {
std::printf("FAIL the %s run took the other commit path (%ld full accept(s) kept)\n", …);
return 1;
}Killed by one increment: if (keep_batch_state) full_accepts_kept_++;, initialised = 0 as a member, so a forward that never takes the path reports 0, not garbage. This is the arm that stops a comparison
from comparing a path with itself — the shape that would let the test go green while testing nothing, which is the defect this session has found in six other places.
The caveat, measured rather than inferred: the target does not exist in an ONEBIT_NPU=OFF configure.
$ cmake --build build1336 --target commit_full_accept_test (ONEBIT_NPU=OFF)
ninja: error: unknown target 'commit_full_accept_test'
CMakeLists.txt:2017 add_executable(commit_full_accept_test …) enclosed by 1876: if(ONEBIT_NPU)
target_link_libraries(… onebit_npu_forward)
and even where the target builds it skips 77 unless ONEBIT_TEST_Q4NX names a causal model with GDN layers (return 77 at three separate preconditions — no pack, noncausal, no recurrent state). So the arm is
doubly gated: NPU-configure and a named model. CI runs neither, so this is the same shape as engine#2026 (a test inside if(ONEBIT_NPU) never runs in CI) — except that here the fix is much less attractive,
because the test needs Q4nxNpuForward itself, and extracting a counter into a header to satisfy a negative control would be the tail wagging the dog.
What I would add, and it is one sentence: say so in the test's header comment — "cannot run in CI: needs ONEBIT_NPU and ONEBIT_TEST_Q4NX; run it on a box with a GDN pack" — beside the existing "Host-GEMM, no device".
The current line reads as if a host test should be reachable everywhere; the two gates are the reason it is not.
Not run: the arm itself (needs the NPU configure plus a GDN pack; NPU is off per the owner directive). My check is the counter's initialisation and the guard around the target.
|
Persistent review updated to latest commit 04b6ee7 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 04b6ee7: READY
Re-review of 04b6ee7, a merge of main (04b6ee7e). The PR's own diff (sorted +/- lines against its merge base) is identical to 441fe2e, READY from me: the full_accepts_kept() negative control. The never-fast-path mutant failed there. There are no conflict markers, and it merges cleanly onto main. CI build at 04b6ee7 is green: 100% tests passed, 0 tests failed out of 262.
Refs #1781. Follow-up to a review note on #2243 (merged).
commit_full_accept_testcould pass while testing nothing. It now cannot. If the full-accept path were silently not taken, both of its runs would replay, the two dumps would be equal, and it would pass. That was the review's point, and it was right: a mutant that disables the fast path passed the test at #2243's head.Change
Q4nxNpuForward::full_accepts_kept()counts the commits that kept the batch's own state. It is incremented at the one place the path is chosen incommit_verify_rows().NPU_COMMIT_FULL_REPLAY=1must count 0. Otherwise the run fails, naming the path it took.Proof (this box, host-GEMM; the NPU is off for lanes)
🤖 Generated with Claude Code