Skip to content

npu/mtp: the forward adapter describes the deferral it uses, and its row accessor can no longer stop overriding - #1143

Merged
bong-water-water-bong merged 4 commits into
mainfrom
npu/mtp-adapter-contract
Nov 5, 2026
Merged

bong-water-water-bong merged 4 commits into
mainfrom
npu/mtp-adapter-contract

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

One header, npu/mtp/forward_base.h (the BaseModel adapter the live draft loop drives the 27B through), carries three
defects of the same family as the #811 drift. No behaviour change, no forward change, host only.

Root cause

  1. The file comment describes a mechanism the code does not use. It said the adapter "takes a state snapshot before
    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 is
    the one a reader prices the loop with, and it prices it at a ~100 MB state copy per iteration that is not taken.
  2. The members that comment left behind are dead: snap_ (unique_ptr<Q4nxNpuForward::StateSnapshot>) declared and
    never touched, snap_pos_ assigned and never read. StateSnapshot itself is real and used by the forward; the
    adapter just does not use it.
  3. last_hidden_rows() was the class's only non-override method, and it is the single signature the drafter's
    advance hangs on. If the base's declaration ever drifts (a dropped const is the obvious one), the method silently
    stops 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

$ ninja mtp_acceptance            # ONEBIT_NPU=ON, main 6211c2fe
[2/2] Linking CXX executable mtp_acceptance          # clean
$ ctest -R mtp_draft
1/1 Test #31: mtp_draft ... Passed
$ tools/copyright.py --check                          # clean

Proof the third fix is a guard and not cosmetics — with override in place, dropping const from the accessor is now a
compile error, where before it was silent:

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
FAILED: CMakeFiles/mtp_acceptance.dir/tools/mtp_acceptance.cpp.o

(file restored, rebuild green.)

Scope

npu/mtp/forward_base.h: +14/−11. Comment, override, two dead members, and the <memory> include that only existed
for 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 sign

pi-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 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, 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_acceptance rc 0, ctest -R mtp_draft 1/1, copyright --check clean; the adapter still
never calls snapshot_state()/restore_state().

…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 bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Preparing review...

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

811 - Partially compliant

Compliant requirements:

Non-compliant requirements:

  • The C++ head is measured against the Python reference with specific metrics (rel-L1, max|d|, cosine)

Requires further human verification:

  • Verification of the measured metrics (rel-L1 1.838e-04, max|d| 1.567e-02, cosine 0.999999985) requires hardware testing which cannot be assessed through code review alone

805 - Partially compliant

Compliant requirements:

  • The shared drafter protocol with tau-gated chain, 4-row verify with leaf fill, deferred GDN commit
  • Acceptance per target in the leaderboard with gate and lane per the plan table

Non-compliant requirements:

  • Greedy text identical with public bars (74% first-draft, 2.63 tokens/pass)

Requires further human verification:

  • Verification of the public bars metrics (74% first-draft, 2.63 tokens/pass) requires hardware testing which cannot be assessed through code review alone

31 - Partially compliant

Compliant requirements:

  • A clean, simple documentation site at https://1bit-monster.github.io/engine/
  • Layout with top bar, sidebar, 760px text column, previous/next, and source links
  • Style with system fonts, teal accent, styled tables and code, heading anchors, dark mode
  • Build with tools/site.py, pinned python-markdown, and .github/workflows/pages.yml
  • New files carry the full Apache notice

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Stale Documentation

The file comment still describes a mechanism that is no longer used by the code. The comment incorrectly states that the adapter "takes a state snapshot before the verify batch and restores the accepted prefix afterwards", which is outdated since the deferred path has been in use since #805 P6. This can mislead developers about the actual implementation and performance characteristics.

// The one non-obvious part is commit_verify(): a GDN layer's recurrence cannot be re-indexed, so the verify batch
// DEFERS it -- begin_verify() stores each row's recurrence inputs instead of applying them, and
// commit_verify_rows(accepted) applies exactly the accepted prefix and drops the rest (#805 P6).  Nothing is
// snapshotted per iteration and 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.  The
// forward's snapshot_state()/StateSnapshot API exists and this adapter deliberately does not use it.
Dead Code

The members snap_ and snap_pos_ are declared but never used. The snap_ member is a unique_ptr<Q4nxNpuForward::StateSnapshot> that is declared but never touched, and snap_pos_ is assigned but never read. These dead code elements should be removed to improve code clarity and maintainability.

Q4nxNpuForward& fwd_;
Missing Override

The last_hidden_rows() method was previously not marked as override, which could lead to silent failures if the base class signature changes. This has been corrected to ensure proper override behavior and prevent the failure mode that previously cost 1 token/pass.

const std::vector<float>& last_hidden_rows() const override { return fwd_.last_hidden_rows(); }

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bong-water-water-bong
bong-water-water-bong merged commit 14bda3c into main Nov 5, 2026
8 of 9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the npu/mtp-adapter-contract branch November 5, 2026 21:19
bong-water-water-bong added a commit that referenced this pull request Nov 6, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant