Repository navigation
npu/mtp: the forward adapter describes the deferral it uses, and its row accessor can no longer stop overriding - #1143
Conversation
…row accessor can no longer stop overriding Three defects in one small header, all of the same family as the #811 drift: * the file comment said the adapter snapshots the state per verify batch and restores the accepted prefix, because "the forward has no GDN deferral yet". The code has used the deferred path since #805 P6 (begin_verify stores each row's recurrence inputs, commit_verify_rows applies the accepted prefix); the snapshot design is what the deferral replaced, and the comment priced the loop at a state copy it does not take. * the members that comment left behind -- snap_ (unique_ptr<StateSnapshot>) and snap_pos_ -- were dead: one declared and never touched, one assigned and never read. * last_hidden_rows() was the only non-override in the class, and it is the single signature the drafter's advance hangs on: drop the const and it silently stops overriding, reporting no rows at all, which is the failure mode that cost the live loop 1 token/pass for a day. Proof the guard works: with override, removing const from the accessor is now a compile error (`marked 'override', but does not override`), where before it was silent. mtp_acceptance builds clean, ctest -R mtp_draft 1/1, copyright --check clean.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY
The override is the load-bearing line and I checked that it does what the comment says, by breaking it both ways rather than by reading it. BaseModel::last_hidden_rows() is virtual with a default body returning an empty vector (npu/mtp/draft.h:122-126), and the loop takes the advance from it (draft.h:341), so a base whose signature drifts reports no rows and the advance is skipped in silence.
Falsification, at this head: drift the signature the way a re-base would (const dropped on the member, override kept) ->
npu/mtp/forward_base.h:71:31: error: 'const std::vector<float>& onebit::npu::mtp::ForwardBase::last_hidden_rows()'
marked 'override', but does not override
build rc=1
and the same drift with override removed - which is main's code - builds and links with no diagnostic at all. So the PR turns the #811 failure mode into a build break, which is the only place it can be caught. The file was restored afterwards (git status clean) and rebuilt clean.
Also at this head: ninja mtp_acceptance -> 0 warnings, rc 0 (the same command the body ran), override on a base that has the virtual produces no -Woverloaded-virtual noise, and #include <memory> comes out with the unique_ptr member that used the only use of it.
One nit, and it is the same class of thing the PR is fixing - a comment asserting a safety property that is not there. The header now says "no prefix restore is attempted (restore_state() refuses those on a GDN model)". restore_state() (npu/forward/src/q4nx_forward.cpp:326-341) accepts tokens < in.tokens (a prefix) and restores KV per position while installing the GDN planes whole from the snapshot; its refusal is give_whole rejecting a plane of the wrong size, not a GDN guard and not a prefix check. So a prefix restore there is not refused, it is silently KV-at-prefix + GDN-at-snapshot - precisely the mixed state this comment exists to warn about. The adapter does not call it, so nothing is broken today; the sentence wants "restores the GDN planes whole, so a prefix restore is not what it does" rather than "refuses".
…it is refused Review nit on #1143, and correct: `restore_state()` (npu/forward/src/q4nx_forward.cpp:326-341) does not refuse a prefix. It refuses only a plane of the wrong size (`give_whole`) and a `tokens > in.tokens` extension; for `tokens < in.tokens` it restores the KV caches per position to `tokens` and then installs the GDN conv/recurrent planes WHOLE from the snapshot. So a prefix restore is accepted and leaves KV at the prefix with the recurrence still at the snapshot's last position -- exactly the mixed state the deferred commit exists to avoid, and exactly what the sentence was meant to warn about. The comment now says that, with the line reference, instead of claiming a guard that is not there. This PR exists because a comment asserted a design the code had replaced; a comment asserting a safety property that is not implemented is the same defect with the opposite sign, so it is fixed rather than merged with it. Comment only: `ninja mtp_acceptance` rc 0, `ctest -R mtp_draft` 1/1, `copyright --check` clean, no behaviour change (the adapter still never calls snapshot_state()/restore_state()).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY
Re-verdicted at 6f3c6d58. The one commit since 2b498833 is my nit, and it is fixed the way the code reads rather than by softening the claim:
// ... this adapter never restores: restore_state() ACCEPTS a prefix (`tokens < in.tokens`) and installs the GDN
// planes whole from the snapshot (`q4nx_forward.cpp:326-341`; its only refusals are a plane of the wrong size and
// a `tokens > in.tokens` extension), so a prefix restore here would leave KV at the prefix with the recurrence
// still at the snapshot's last position -- the mixed state this deferral exists to avoid.
That is exactly q4nx_forward.cpp:326-341: the three return false conditions are !ready_ || tokens <= 0 || tokens > in.tokens, in.per_pos != per, and the give_whole size checks, none of which looks at GDN or at the prefix relationship. The line references are right and the warning now says what the code does.
Everything from my 2b498833 review stands: ninja mtp_acceptance clean with 0 warnings, and the override falsification (const dropped on the member with override kept -> error: marked 'override', but does not override; the same drift without override, i.e. main's code, links silently).
|
Preparing review... |
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY
Re-verdicted at 73be3d41. Only Merge branch 'main' since 6f3c6d58, and this PR's one file is byte-identical across the two heads (npu/mtp/forward_base.h), so nothing it says moved; the merge brings other lanes' work (#1142, #1138, #1140, #1139, #1136, #1134, #1133, #1132, #1131, #1130, #1124).
My nit stays fixed as reviewed at 6f3c6d58: restore_state() accepts a prefix and installs the GDN planes whole (q4nx_forward.cpp:326-341; its only refusals are tokens > in.tokens, a per_pos mismatch and a give_whole size), and the override on last_hidden_rows() still turns a drifted signature into error: marked 'override', but does not override while the same drift without it links silently.
…ename cannot break it silently (#1184) * ci: the draft-loop measurement tool is compile-checked, so a header rename cannot break it silently Every draft-loop number in this lane is measured with tools/mtp_acceptance, and no job compiles it: it links onebit_npu_forward, which exists only under ONEBIT_NPU, which refuses to configure without the private runtime (ONEBIT_NPU_RT_DIR: npu_rt.h, npu_rt.cpp, npu_rt_api.cpp, not in this repository). So the binary is built by hand on the measurement host and nowhere else. That is the same hole the fixtures closed for the pooled verdict, one level up: npu/mtp/draft.h, forward_base.h and npu/forward/include/ changed three times in one evening (#1138 changed the hidden row shapes, #1143 the row accessor), and each of those is a compile break for this tool that CI cannot see. The next person to measure would find it on a device ticket instead. The check is a syntax-only compile against the in-tree headers (npu/rt.h guards itself on ONEBIT_NPU_RUNTIME_OWN and needs only npu/npu_rt_api.h, which is tracked), using the json headers the configure step has already fetched. Verified: 'g++ (Ubuntu 15.2.0) -std=c++26 -fsyntax-only ...' rc 0 on this head; injection -- renaming ForwardBase::last_hidden to last_hidden_name_drift -- makes the same command fail with "marked 'override', but does not override" (rc 2), so the step is a check and not a no-op; copyright --check rc 0. * ci: the compile check runs the compiler the build uses (g++-16, not a bare g++) The step added in 7795d56 failed before it compiled anything: build The draft-loop measurement tool still compiles /__w/_temp/...sh: 1: g++: not found ##[error]Process completed with exit code 127 The image installs versioned binaries only (the tool step asks for gcc-16 g++-16, which does not create /usr/bin/g++), and the Configure step in the same job already sets -DCMAKE_CXX_COMPILER=g++-16. So the check now invokes g++-16: the same compiler the build uses, and the one whose C++26 front end this file needs. Verified on this host, both arms, with the versioned binary and the configure-fetched json headers: g++-16 -std=c++26 -fsyntax-only -DONEBIT_NPU_RUNTIME_OWN -Inpu -Inpu/forward/include \ -I<build>/_deps/json-src/include tools/mtp_acceptance.cpp -> rc 0 (one vendored-nlohmann deprecation warning, not an error), and the injection still reds it: renaming ForwardBase::last_hidden to last_hidden_name_drift gives "marked 'override', but does not override" -> rc 1, file restored. python3 -c 'yaml.safe_load(ci.yml)' parses; tools/copyright.py --check rc 0. --------- Co-authored-by: pi-mtploop <mtploop@1bit.local>
What
One header,
npu/mtp/forward_base.h(theBaseModeladapter the live draft loop drives the 27B through), carries threedefects of the same family as the #811 drift. No behaviour change, no forward change, host only.
Root cause
the verify batch and restores the accepted prefix afterwards", because "the forward has no GDN deferral yet". The
code has used the deferred path since npu plan P6: the shared drafter protocol - tau-gated chain, 4-row verify with leaf fill, deferred GDN commit; acceptance per target in the leaderboard #805 P6 —
begin_verify()stores each row's recurrence inputs,commit_verify_rows(accepted)applies the accepted prefix — and the in-body comment says so. The stale comment isthe one a reader prices the loop with, and it prices it at a ~100 MB state copy per iteration that is not taken.
snap_(unique_ptr<Q4nxNpuForward::StateSnapshot>) declared andnever touched,
snap_pos_assigned and never read.StateSnapshotitself is real and used by the forward; theadapter just does not use it.
last_hidden_rows()was the class's only non-overridemethod, and it is the single signature the drafter'sadvance hangs on. If the base's declaration ever drifts (a dropped
constis the obvious one), the method silentlystops overriding and reports no rows at all — which reads to the loop as "this base does not keep rows", i.e. the
advance is skipped in silence. That is the exact failure mode that cost the live loop 1 token/pass for a day (npu: the MTP drafter runnable against the real base, and the head checked against the reference #811).
Measured
Proof the third fix is a guard and not cosmetics — with
overridein place, droppingconstfrom the accessor is now acompile error, where before it was silent:
(file restored, rebuild green.)
Scope
npu/mtp/forward_base.h: +14/−11. Comment,override, two dead members, and the<memory>include that only existedfor
snap_. Nothing in the loop, the head, the forward or the tests changes; no device.Second commit (
6f3c6d58): the review nit is right, and it is the same defect with the opposite signpi-prs: "a comment asserting a safety property that is not there". Checked in the source and it holds —
restore_state()(npu/forward/src/q4nx_forward.cpp:326-341) refuses only a plane of the wrong size(
give_whole) and atokens > in.tokensextension. Fortokens < in.tokensit restores the KV caches per positionto
tokensand then installs the GDN conv/recurrent planes whole from the snapshot, so a prefix restore isaccepted and leaves KV at the prefix, recurrence at the snapshot's last position — precisely the mixed state the
sentence was written to warn about, not a refusal.
The comment now states that behaviour with its line reference instead of claiming a guard. This PR exists because a
comment asserted a design the code had replaced; a comment asserting a safety property nothing implements is the same
defect with the opposite sign, so it is fixed rather than merged with it.
Comment only —
ninja mtp_acceptancerc 0,ctest -R mtp_draft1/1,copyright --checkclean; the adapter stillnever calls
snapshot_state()/restore_state().