You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
In NpuModel + npu/mtp: chain continues while draft prob >= tau (0.6), verify rows = current + up to 3 drafts, spare rows filled with the drafts 2nd/3rd candidates, GDN commits deferred to the next pass (no per-token state banks). Gate: acceptance length, hit rate, passes/token on 8 prompts x 64 tokens per target; greedy text identical. Public bars: 74 % first-draft, 2.63 tokens/pass (Zhouyi build). Lanes: pi-bonsai then pi-hexagon. Depends on P4.
Part of #799 (the NPU plan layer). Gate and lane per the plan table in #799. Not a c20 blocker (bucket C: sequenced after the release freeze unless marked additive).
Acceptance protocol measured against the real base (8 prompts x 64 tokens, k=4, hidden variants compared). This is the
bar P6 has to keep, and one engine finding that changes what P6 can build.
Pre-norm residual hidden: first draft 70.4 % (marginally better at depth 1) but the chain collapses — d2 15.7 %, d3 10 %,
d4 5 %, i.e. 2.1 tokens/pass. The head feeding itself its own output hidden is only consistent when that hidden is in
the post-norm basis, so post-norm is the variant; that settles the plan's open item C.
Versus the published class (74 % first draft, 2.63 tokens/pass): we are a little behind at depth 1 and ahead on tokens
per pass (3.19), because our deeper drafts hold up (41 % at depth 4).
Engine finding — the deferred commit is required, not an optimisation.restore_state() refuses a prefix restore on
this model: it compares the requested per-position KV length against the conv/rec arrays, which for GDN layers are
fixed-size windows rather than per-position state, so put() returns false (restore_state(10 of 10) refused). With a
snapshot-and-replay adapter a live draft-verify loop is therefore either refused (what happens today) or would advance the
recurrence over rejected rows and silently diverge. So the protocol above is deliberately offline: the head is run over
the base's own greedy stream with its KV primed position by position — which is how the MTP layer is trained — and each
draft is compared with the token the model actually produced. That reports the same quantity the published bars report
without any base-state surgery.
Conclusion for P6: the loop (#785) and its commit interface (#798, commit_verify(accepted)) are in main, but the engine
side must defer GDN updates across a verify batch (store the rows' update inputs, apply the accepted prefix) before a
live speculative loop can be measured. Until then the offline protocol above is the honest number, and it is good enough
to justify the work.
Disposition: parked on the phases it depends on — the protocol half is landed and evidenced; the remaining rows are not measurable yet.
On origin/main (19486dcc): the P6 protocol lives in npu/mtp/draft.h (tau=0.6 default, reached_at vs accepted_at, deferred GDN commit) and is exercised by tests/mtp_draft_test.cpp; the leaderboard carries the live-loop acceptance rows from #1206 — depth1 75.14 %, hit rate 46.69 %, 2.87 tokens/pass, 8 prompts x 64 tokens, k=4, tau=0, greedy text 8/8 identical. (PR #1229, the leaderboard records guard, is merged and is not this protocol's PR.)
Remains: the tau=0.6 row, the Hexagon rows (P5 is parked: #804, blocked on private kernels/hardware), and the NpuModel side of the protocol, which depends on P4 (#803, PR #808, waiting on the release freeze).
Unpark: P4 lands so the protocol has its model-side target, and the P5 hardware path exists; the tau=0.6 and Hexagon rows then become measurable and this phase's acceptance is complete.
Scoped PR for this plan: #1277 (737dad46, refs #805) — the measurement window now carries tau.
Why that is the shape of the remaining work.Options::tau defaults to 0.6, but every published acceptance row is tau = 0: the live path printed no window identity and tools/mtp_depth_pool.py never read tau, so a tau=0.6 ticket could be pooled with a tau=0 one and the deeper rows (offered only by tau=0 chains) would read as a complete pass. #1277 prints == N prompts x M tokens, k=K tau=T, hidden=H at init on the live path too, has the tool name a mixed pool and report tau unstated for older logs, and keys the leaderboard-records guard by window.
What remains on P6, and why each row is blocked rather than skipped
row
state
tau-gated chain
landed (default 0.6, npu/mtp/draft.h), exercised by tests/mtp_draft_test.cpp
4-row verify, leaf fill
not built. The verify batch is flat: row m is position start_pos+m and is scored against row m-1's logits, so a draft's 2nd/3rd candidates cannot be placed at the same position without per-row position ids and an attention mask in the forward — a kernel-side change (private repos), and it needs P4's NpuModel to target.
deferred GDN commit
landed (restore_state() refuses a prefix restore; the verify kernel stores per-token update inputs)
acceptance / hit rate / tokens per pass, 8×64
landed at tau = 0 (#1206: depth 1 75.14 %, hit 46.69 %, 2.87 tokens/pass, text 8/8 identical)
the same at the default tau = 0.6
unmeasured. Needs one 8×64 --live --match --tau 0.6 window (~4 npu-hold tickets of ≤10 min). Blocked right now: /tmp/1bit-npu-device.lock is held by an orphaned host-GEMM serve (pid 2231177, ~/lb/1035-row) that pi-bonsai has asked its owner to stop, and c31's gate chain is running (TISSUES-ALL rule 1). My 15:17 probe ticket refused with device lock busy rather than running beside it — which is the harness working as designed.
The tau=0.6 row is measured and published: PR #1300 (on 5c496d32 → b0224e5d), 8 prompts x 64 tokens, k=4, one prompt per npu-hold ticket, text identical 8/8, 0 stream mismatches, amdxdna 0 in every arm.
tau = 0
tau = 0.6 (the default)
hit rate
46.69 % (338/724)
78.77 % (230/292)
depth 1 acceptance (all chains)
75.14 % (136/181)
78.34 % (217/277)
depth 2
47.51 % (86/181)
3.97 % (11/277)
depth 3
34.81 % (63/181)
0.72 % (2/277)
depth 4
29.28 % (53/181)
never offered
tokens per pass
2.87
1.83
The gate buys confidence per draft and pays for it with the chain: ~1.05 drafts per chain against 4.0, so 1.83 tokens/pass sits below the measured 3.2-4.1 break-even and the default is a 0.81x slowdown. Depth 1 is the only depth with a usable denominator at this tau (depth 2 has 13 chains, depth 3 has 2, depth 4 none).
So the gate rows of this issue are now closed at both taus. What remains is unchanged and honestly blocked: leaf fill (the verify batch is flat — row m is start_pos+m and is scored against row m-1's logits, so a draft's 2nd/3rd candidates need per-row positions and an attention mask in the forward, on top of P4/#803), and the Hexagon rows (P5, #804).
P6 coordination: what P4 gives the drafter today, and the one row that still waits on A3
P4/A1 merged as 0a2cbb97 (npu/target/target.h + npu/target/xdna/ + q4nx_lh, the launch layer behind NpuTarget). For P6 that means, precisely:
Available now: the launch layer is no longer inside Q4nxNpuForward, so a draft/verify pass reaches the device through NpuTarget::gemv(GemvArgs{role, layer, M, K, N, x, Bmat, wkey, tag, rows, decode_twin}) — note rows and decode_twin are already in that struct, which is the pair a 4-row verify pass needs (the M=16 twin streams its weights once for the batch). npu/mtp/draft.h is untouched by the move, as the interface was designed to leave it.
Also waiting on P5: the per-target acceptance row. --npu-target <name> exists end to end (compiled list in app/forward_serve.cpp, refusal by name before the device opens), so the Hexagon arm of the acceptance table can be selected by that flag the moment pi-hexagon's target links in — no new plumbing on your side.
Your tau=0.6 row (#1300: 78.77 % hit rate against 46.69 % at tau=0, depth-1 acceptance 78.34 %, 1.83 tokens/pass) is the number the umbrella needed for P6 and is already reflected in my #799 checklist. What I would keep out of P6 until A3 lands: any row whose claim is about where the deferred commit lives, as opposed to what the acceptance is.
P6 is complete — closing. Every part of this issue's protocol is landed and measured, and the one item never built (leaf fill) is now rejected on evidence, not implemented: a test in tests/mtp_draft_test.cpp [16] shows it cannot buy acceptance in this verifier. All evidence below is re-checked at origin/main0f45221e, not quoted from memory.
The protocol, part by part
part
state
evidence
tau-gated chain (default 0.6)
landed
npu/mtp/draft.h (Options::tau, reached_at vs accepted_at), tests/mtp_draft_test.cpp [6]/[9]
deferred GDN commit, no per-token state banks
landed
restore_state() refuses a prefix restore; the verify batch stores per-token update inputs and commit_verify(accepted) applies only the accepted prefix
4-row verify, greedy text identical
landed
flat batch step_batch_all, one M=k+1 batch per iteration; text identical to plain decode on 8/8 prompts at both gates
acceptance / hit rate / tokens-per-pass, 8 prompts × 64, per target
landed
13 27b-mtp-* leaderboard records, every one re-derived from in-tree fixtures by tests/mtp_depth_pool_test.py
leaf fill ("spare rows with 2nd/3rd candidates")
rejected with a test
see below
Measured (8 prompts × 64 tokens, k=4, one prompt per npu-hold ticket, text identical 8/8, 0 stream mismatches, amdxdna 0 in every arm)
tau = 0
tau = 0.6 (the default)
hit rate
46.69 % (338/724)
78.77 % (230/292)
depth 1 acceptance (all chains)
75.14 % (136/181)
78.34 % (217/277)
depth 2 / 3 / 4
47.51 / 34.81 / 29.28 %
3.97 / 0.72 % / never offered
tokens per pass (live)
2.87
1.83
depth-1 live vs offline, same positions
+0.00 pp PASS
+0.00 pp PASS
Reproduce: python3 tools/mtp_depth_pool.py tests/fixtures/mtp_clause55/*.txt and … tests/fixtures/mtp_tau06/*.txt (each prints the all-chains rates, hit rate and both tokens-per-pass figures; --selftest checks the recorded counts).
The finding worth keeping: at its default the gate raises confidence per draft (46.7 % → 78.8 % hit rate) and collapses the chain (~1.05 drafts per chain against 4.0), so 1.83 tokens/pass sits below the 3.2–4.1 break-even and the draft route is a 0.81× slowdown as configured. Serve it at tau = 0 or not at all; docs/npu.md says so.
Leaf fill — why it is not built
Acceptance is argmax(logits[i-1]) == draft[i] (npu/mtp/draft.h:317): a draft is accepted iff it is the base's own argmax, and the argmax grading depth i comes from row i-1 of the same batch. Measured in [16] against a toy base with a real two-level ranking:
rank-2 candidate at every depth ................. accepted 0/92
same head, argmax moved onto the runner-up ...... accepted 20/20 (control: the zero is the rule, not the harness)
right at depths 2..k, wrong at depth 1 .......... accepted 0, reached_at[1] == 0
the same head at k=8 (M=16 rows) ................ accepted 0 of 184 rows
So a non-argmax candidate at the same position is refused with certainty, a correct candidate under a refused parent is never scored, and widening the batch buys nothing — the rows are the chain, not candidate tests. A real tree verifier would need per-row positions + an attention mask in the forward, and with a row-by-row GDN state a state per branch: the "no per-token state banks" constraint this issue itself imposes. If that constraint is ever lifted, leaf fill becomes a new design and should get its own issue.
Merged in main for this issue
#820 (the drafter's stream cursor), #838 (the live-vs-offline comparison), #968, #1022 (the replay is 16× cheaper), #1042, #1206 (the leaderboard rows), #1213 (the page regeneration guard), #1229 (the records guard covers the directory), #1277 (the window carries tau), #1300 (the default tau=0.6 measured), #1305 (this determination).
Not in this issue: the Hexagon per-target rows belong to P5/#804, and the NpuModel side landed with P4 (#808) (0a2cbb97).
Activity
bong-water-water-bong commentedon Nov 2, 2026
Acceptance protocol measured against the real base (8 prompts x 64 tokens, k=4, hidden variants compared). This is the
bar P6 has to keep, and one engine finding that changes what P6 can build.
Post-final-norm hidden wins; 568 drafted positions:
Pre-norm residual hidden: first draft 70.4 % (marginally better at depth 1) but the chain collapses — d2 15.7 %, d3 10 %,
d4 5 %, i.e. 2.1 tokens/pass. The head feeding itself its own output hidden is only consistent when that hidden is in
the post-norm basis, so post-norm is the variant; that settles the plan's open item C.
Versus the published class (74 % first draft, 2.63 tokens/pass): we are a little behind at depth 1 and ahead on tokens
per pass (3.19), because our deeper drafts hold up (41 % at depth 4).
Engine finding — the deferred commit is required, not an optimisation.
restore_state()refuses a prefix restore onthis model: it compares the requested per-position KV length against the conv/rec arrays, which for GDN layers are
fixed-size windows rather than per-position state, so
put()returns false (restore_state(10 of 10) refused). With asnapshot-and-replay adapter a live draft-verify loop is therefore either refused (what happens today) or would advance the
recurrence over rejected rows and silently diverge. So the protocol above is deliberately offline: the head is run over
the base's own greedy stream with its KV primed position by position — which is how the MTP layer is trained — and each
draft is compared with the token the model actually produced. That reports the same quantity the published bars report
without any base-state surgery.
Conclusion for P6: the loop (#785) and its commit interface (#798,
commit_verify(accepted)) are in main, but the engineside must defer GDN updates across a verify batch (store the rows' update inputs, apply the accepted prefix) before a
live speculative loop can be measured. Until then the offline protocol above is the honest number, and it is good enough
to justify the work.
mtp tool: measure the two costs the cost model guessed -- one lm-head…
mtp: block the head's matvec over n so a code byte shares its cache l…
mtp tool: measure the two costs the cost model guessed -- one lm-head…
mtp: block the head's matvec over n so a code byte shares its cache l…
tests: the sweep's toy base carries a recurrent state, so a wrong com…
tests: the sweep's toy base carries a recurrent state, so a wrong com…
npu: the draft loop advances over either documented hidden-row shape,…
npu/mtp: the forward adapter describes the deferral it uses, and its …
mtp: the leaderboard records' numbers are re-derived from the fixture…
docs: the leaderboard carries the MTP draft loop's measured acceptanc…
mtp: a new MTP leaderboard record cannot arrive outside the records g…
bong-water-water-bong commentedon Nov 6, 2026
Disposition: parked on the phases it depends on — the protocol half is landed and evidenced; the remaining rows are not measurable yet.
On
origin/main(19486dcc): the P6 protocol lives innpu/mtp/draft.h(tau=0.6 default,reached_atvsaccepted_at, deferred GDN commit) and is exercised bytests/mtp_draft_test.cpp; the leaderboard carries the live-loop acceptance rows from #1206 — depth1 75.14 %, hit rate 46.69 %, 2.87 tokens/pass, 8 prompts x 64 tokens, k=4, tau=0, greedy text 8/8 identical. (PR #1229, the leaderboard records guard, is merged and is not this protocol's PR.)Remains: the tau=0.6 row, the Hexagon rows (P5 is parked: #804, blocked on private kernels/hardware), and the NpuModel side of the protocol, which depends on P4 (#803, PR #808, waiting on the release freeze).
Unpark: P4 lands so the protocol has its model-side target, and the P5 hardware path exists; the tau=0.6 and Hexagon rows then become measurable and this phase's acceptance is complete.
bong-water-water-bong commentedon Nov 6, 2026
Scoped PR for this plan: #1277 (
737dad46, refs #805) — the measurement window now carries tau.Why that is the shape of the remaining work.
Options::taudefaults to 0.6, but every published acceptance row istau = 0: the live path printed no window identity andtools/mtp_depth_pool.pynever read tau, so atau=0.6ticket could be pooled with atau=0one and the deeper rows (offered only by tau=0 chains) would read as a complete pass. #1277 prints== N prompts x M tokens, k=K tau=T, hidden=Hat init on the live path too, has the tool name a mixed pool and reporttau unstatedfor older logs, and keys the leaderboard-records guard by window.What remains on P6, and why each row is blocked rather than skipped
npu/mtp/draft.h), exercised bytests/mtp_draft_test.cppstart_pos+mand is scored against row m-1's logits, so a draft's 2nd/3rd candidates cannot be placed at the same position without per-row position ids and an attention mask in the forward — a kernel-side change (private repos), and it needs P4'sNpuModelto target.restore_state()refuses a prefix restore; the verify kernel stores per-token update inputs)tau = 0(#1206: depth 1 75.14 %, hit 46.69 %, 2.87 tokens/pass, text 8/8 identical)tau = 0.6--live --match --tau 0.6window (~4npu-holdtickets of ≤10 min). Blocked right now:/tmp/1bit-npu-device.lockis held by an orphaned host-GEMM serve (pid 2231177,~/lb/1035-row) that pi-bonsai has asked its owner to stop, and c31's gate chain is running (TISSUES-ALL rule 1). My 15:17 probe ticket refused withdevice lock busyrather than running beside it — which is the harness working as designed.Nothing here is closed by hand; #1277 stays
refsrather thanFixesbecause thetau=0.6and Hexagon rows are still open.mtp: the measurement window carries tau, so two chain gates cannot be…
bong-water-water-bong commentedon Nov 6, 2026
The tau=0.6 row is measured and published: PR #1300 (on
5c496d32→b0224e5d), 8 prompts x 64 tokens, k=4, one prompt pernpu-holdticket, text identical 8/8, 0 stream mismatches,amdxdna0 in every arm.tau = 0tau = 0.6(the default)The gate buys confidence per draft and pays for it with the chain: ~1.05 drafts per chain against 4.0, so 1.83 tokens/pass sits below the measured 3.2-4.1 break-even and the default is a 0.81x slowdown. Depth 1 is the only depth with a usable denominator at this tau (depth 2 has 13 chains, depth 3 has 2, depth 4 none).
So the gate rows of this issue are now closed at both taus. What remains is unchanged and honestly blocked: leaf fill (the verify batch is flat — row m is
start_pos+mand is scored against row m-1's logits, so a draft's 2nd/3rd candidates need per-row positions and an attention mask in the forward, on top of P4/#803), and the Hexagon rows (P5, #804).copyright: a fixture ticket under tests/fixtures/ is exempt by KIND, …
mtp: the draft route's DEFAULT tau=0.6 is measured — 78.8 % hit rate …
bong-water-water-bong commentedon Nov 7, 2026
P6 coordination: what P4 gives the drafter today, and the one row that still waits on A3
P4/A1 merged as
0a2cbb97(npu/target/target.h+npu/target/xdna/+q4nx_lh, the launch layer behindNpuTarget). For P6 that means, precisely:Q4nxNpuForward, so a draft/verify pass reaches the device throughNpuTarget::gemv(GemvArgs{role, layer, M, K, N, x, Bmat, wkey, tag, rows, decode_twin})— noterowsanddecode_twinare already in that struct, which is the pair a 4-row verify pass needs (the M=16 twin streams its weights once for the batch).npu/mtp/draft.his untouched by the move, as the interface was designed to leave it.NpuModel— the model maths behindNpuTarget&with the forward as an adapter — is not in the tree. Today the maths still lives inQ4nxNpuForward, so the deferred-commit ownership has not moved and a P6 row that asserts it would be asserting A1.--npu-target <name>exists end to end (compiled list inapp/forward_serve.cpp, refusal by name before the device opens), so the Hexagon arm of the acceptance table can be selected by that flag the moment pi-hexagon's target links in — no new plumbing on your side.Your tau=0.6 row (#1300: 78.77 % hit rate against 46.69 % at tau=0, depth-1 acceptance 78.34 %, 1.83 tokens/pass) is the number the umbrella needed for P6 and is already reflected in my #799 checklist. What I would keep out of P6 until A3 lands: any row whose claim is about where the deferred commit lives, as opposed to what the acceptance is.
mtp: leaf fill cannot buy acceptance in this verifier — P6's last pla…
bong-water-water-bong commentedon Nov 7, 2026
P6 is complete — closing. Every part of this issue's protocol is landed and measured, and the one item never built (leaf fill) is now rejected on evidence, not implemented: a test in
tests/mtp_draft_test.cpp[16] shows it cannot buy acceptance in this verifier. All evidence below is re-checked atorigin/main0f45221e, not quoted from memory.The protocol, part by part
npu/mtp/draft.h(Options::tau,reached_atvsaccepted_at),tests/mtp_draft_test.cpp[6]/[9]restore_state()refuses a prefix restore; the verify batch stores per-token update inputs andcommit_verify(accepted)applies only the accepted prefixstep_batch_all, one M=k+1 batch per iteration; text identical to plain decode on 8/8 prompts at both gates27b-mtp-*leaderboard records, every one re-derived from in-tree fixtures bytests/mtp_depth_pool_test.pyMeasured (8 prompts × 64 tokens, k=4, one prompt per
npu-holdticket, text identical 8/8, 0 stream mismatches,amdxdna0 in every arm)Reproduce:
python3 tools/mtp_depth_pool.py tests/fixtures/mtp_clause55/*.txtand… tests/fixtures/mtp_tau06/*.txt(each prints the all-chains rates, hit rate and both tokens-per-pass figures;--selftestchecks the recorded counts).The finding worth keeping: at its default the gate raises confidence per draft (46.7 % → 78.8 % hit rate) and collapses the chain (~1.05 drafts per chain against 4.0), so 1.83 tokens/pass sits below the 3.2–4.1 break-even and the draft route is a 0.81× slowdown as configured. Serve it at
tau = 0or not at all;docs/npu.mdsays so.Leaf fill — why it is not built
Acceptance is
argmax(logits[i-1]) == draft[i](npu/mtp/draft.h:317): a draft is accepted iff it is the base's own argmax, and the argmax grading depth i comes from row i-1 of the same batch. Measured in [16] against a toy base with a real two-level ranking:So a non-argmax candidate at the same position is refused with certainty, a correct candidate under a refused parent is never scored, and widening the batch buys nothing — the rows are the chain, not candidate tests. A real tree verifier would need per-row positions + an attention mask in the forward, and with a row-by-row GDN state a state per branch: the "no per-token state banks" constraint this issue itself imposes. If that constraint is ever lifted, leaf fill becomes a new design and should get its own issue.
Merged in main for this issue
#820 (the drafter's stream cursor), #838 (the live-vs-offline comparison), #968, #1022 (the replay is 16× cheaper), #1042, #1206 (the leaderboard rows), #1213 (the page regeneration guard), #1229 (the records guard covers the directory), #1277 (the window carries tau), #1300 (the default tau=0.6 measured), #1305 (this determination).
Not in this issue: the Hexagon per-target rows belong to P5/#804, and the NpuModel side landed with P4 (#808) (
0a2cbb97).mtp: a depth too thin for the 2 pp bar says so, instead of printing a…
mtp: the draft route at main — acceptance re-confirmed digit for digi…
npu docs: the round's cost, split — base passes are 69-82 % of it, an…
npu docs: the drafter's break-even quoted with its loop — 1.17 tokens…