Repository navigation
npu: the speculative loop end to end on the served 27B, and a fully accepted verify keeps its own state (refs #1781) - #2240
Conversation
…ccepted verify keeps its own state (refs #1781) tools/dflash2_probe: a DFlash 2 draft against the served 27B, acceptance in stages and the loop end to end (draft -> one batched verify pass -> commit), the draft's projections optionally on the NPU, a fallback to plain decoding. commit_verify_rows(): when every row of a verify batch is accepted, the state the batch's own recurrence left is kept instead of restoring the snapshot and replaying the rows (NPU_REPLAY_CHECK holds the two equal; NPU_COMMIT_FULL_REPLAY=1 keeps the old path). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at ecc482a |
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 at ecc482a: READY
This is the end-to-end arm the speculative work has been converging on. The arithmetic has had its own units for a while (#1958's acceptance rule, #2053's acceptance arithmetic, #2174's host reference for the draft forward, #2236's dflash_acceptance); dflash2_probe links onebit_npu_forward + onebit_npu and puts the whole loop — draft proposes, target verifies, chain advances — on the served route.
That is the form a speculative-decode claim has to take: acceptance ratio is measurable from the arithmetic, but the number that matters is tokens per second end to end, and that needs the loop as one process. It is also the only shape in which a rejection that forgets to emit its replacement (the failure #1958's arm forbids) shows up as wrong text rather than as a failed assertion.
|
VERDICT: READY at The number, with its control on the same line2.0× against plain decoding of the same binary — not against a different build, not against a published figure — which is the only comparison that isolates speculation from everything else that changed recently.
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at ecc482a: READY
This is the second verdict at this head. It merges cleanly onto main. CI build at ecc482a is green: 100% tests passed, 0 tests failed out of 254. That also answers the body's "the whole suite is not re-run after the fix".
The engine change (commit_verify_rows(), 15 lines) is sound. On a full accept (accepted == verify_rows_ > 0):
- the KV cache is still trimmed to
verify_pos_ + accepted; last_hidden_rows_is still cut to the accepted prefix;- only the recurrent and conv state restore, and the
conv_apply_rows/gdn_apply_rowsreplay, are skipped.
That is exactly the state the replay would rebuild, since the batch has processed all verify_rows_ rows. NPU_REPLAY_CHECK and NPU_COMMIT_FULL_REPLAY both force the old path, so the invariant stays checkable and the A/B stays runnable. A partial accept is unchanged.
Checked at this head (ONEBIT_NPU=ON ONEBIT_NPU_HOST=ON build): onebit_npu_forward and dflash2_probe build. tools/ci/file_create_check.py --check (the check that failed in the body's ctest run) gives no fopen for writing without a stated reason, rc 0. tools/copyright.py --check and tools/ci/ci_targets_check.py --check: rc 0.
Non-blocking: the new full-accept branch is reached only on the device. mtp_draft_test exercises a toy forward, not Q4nxNpuForward::commit_verify_rows. The evidence is the body's device run (NPU_REPLAY_CHECK max diff 0 over 6 full accepts, 48 of 48 tokens equal to plain greedy), which I did not re-run. A host-GEMM arm on a hybrid container would hold the invariant in CI without the NPU: step_batch_all on k rows, then commit_verify_rows(k), then compare rec_state_ / conv_state_ with the NPU_COMMIT_FULL_REPLAY path. The "reference from the same build and kernel set" note in the docs is a good one.
|
VERDICT: READY at Three things about that table make it trustworthy, and I want them named:
The recorded trap is worth keeping verbatim: a speculative loop must be compared with plain decoding from the same build and the same decode kernel set. That is the same discipline as today's load coupling — a comparison is only a comparison when everything else is equal — and it explains the 3.88 above against 4.17-4.27 from earlier tickets (different decode set), which the body says explicitly rather than quietly quoting the more flattering baseline. Two asks:
Scope of my check: fresh configure rc 0, |
… build, held on the host (refs #1781) (#2243) commit_full_accept_test runs a prefill, one fully accepted 4-row verify batch and one step, on each commit path (NPU_COMMIT_FULL_REPLAY unset / set, one process each), and compares the whole state and the next logits bit for bit. Host-GEMM; skips without ONEBIT_TEST_Q4NX. Review note on #2240. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1781 (gate 4's second named pair, and the loop substitution). This does not finish the issue: the loop is a tool, not yet
forward-serve.The speculative loop runs end to end on the served 27B with the DFlash 2 draft: 7.9-8.0 tok/s on a code prompt against plain decoding's 3.9-4.27, with output identical to plain greedy decoding. A fully accepted verify round now keeps the batch's own state instead of replaying it, which saves about 50-60 ms per round.
What is added
tools/dflash2_probe(a tool liketools/mtp_acceptance.cpp: it needs the target pack, the draft package and the device).loopruns the pair: a batched prefill, then rounds of draft 7, verify[last, d1..d7]in onestep_batch_all()pass,commit_verify_rows(accepted + 1), and the target's own next token. Switches:DFLASH2_DEVICE_KERNELS=<dir>: the draft's projections on the NPU through npu: the DFlash draft loader and forward read DFlash 2 packages (refs #1781) #2237'sset_gemm_hook(), using the engine'sI8Ctxwith_b816-row kernels;DFLASH2_K: verify only the first K proposals;DFLASH2_FALLBACK: finish with plain greedy steps when the last 3 rounds were below break-even.capture,draftandscoreare the staged acceptance measurement used on the issue.commit_verify_rows()(q4nx_forward.cpp, 15 lines): on a full accept, the state the batch's own recurrence left is kept instead of restoring the snapshot and replaying every row.NPU_REPLAY_CHECK=1already holds the two equal;NPU_COMMIT_FULL_REPLAY=1keeps the old path.docs/amd-drafters.md: how to run the loop, the measured table, and the reference trap below.Proof, at this head
Two
npu-holdtickets, the servedbonsai2-27b-t2s2with its 8-row decode set, the code prompt, 48 tokens,OMP_WAIT_POLICY=PASSIVE:Plain decoding on this route measured 4.17-4.27 tok/s with the 4-row decode set in earlier tickets on #1781. The 3.88 above is with the loop's 8-row set.
A trap worth recording. A speculative loop must be compared with plain decoding from the same build and the same decode kernel set. A reference captured earlier with the 4-row set and an older main parts from this head at token 23 ("number (0-indexed)." against "number."). The loop and plain decoding move together within one build, so the comparison holds; across builds it does not.
Not in this PR
forward-serve.🤖 Generated with Claude Code