Repository navigation
npu: the DFlash draft as mtp::run()'s drafter (DFlashHead, refs #1781) - #2255
Conversation
npu/dflash_head.h: a DraftHead over dflash_forward with needs_last_hidden() == false, so mtp::run() takes the one-pass round. It reads the forward's tap sink, fuses committed rows into its DraftKvCache at draft time, and after a verify batch trims the sink to the rows the round committed (the forward writes a tap row for every position, rejected drafts included). Embedding and pick rule (argmax or a DFlash 2 selector) are hooks; a draft at a position the sink does not end at is refused with a reason. tests/dflash_head_test.cpp (ctest dflash_head): three pick rules x k = 2, 7 on a toy base that writes taps the same way: text equals plain decode, the sink holds exactly the committed rows, every draft equals draft_forward() over the committed rows, bit for bit. Six mutants each fail it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at ee59a43 |
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 ee59a43: READY
Reviewed at ee59a4327d147f579972198c640041c948312f5c. It is on main (0 behind) and merges clean. The diff is additions only: 4 files, +406.
Checked
- Head and test.
npu/dflash_head.hadds about 150 lines.dflash_head_testandmtp_draft_testboth build in anONEBIT_NPUhost tree and pass.tools/copyright.py --checkreturns rc 0. - The toy base matches the real forward's sink.
Q4nxNpuForward::tap_layeropens each pass's rows at the sink's current size (tap_base_ = tap_sink_->size(), q4nx_forward.cpp:3781), and every pass writes them, rejected verify rows included. So trimming the sink between passes is safe, andcommit_rows()keepingmark_ + Mrows is the right trim. - Position rule. A sink of
rowsrows holds positions0..rows-1, and the last token runs atrows, sopos == rows + 1is correct. The cache only folds rows that were committed atdraft()time, and the sink is never trimmed belowmark_, socache_.rowscan never exceed the sink. - The one-pass path is actually taken. In the always-right arm every draft is accepted (k = 7: 26 of 26 in 4 rounds), so the comparison with plain decode is not vacuous.
Mutants
- My own: fold the new cache rows from the start of the sink instead of from
cache_.rows. It fails 6 checks ("1 of 10 drafts equal draft_forward over the committed rows"). - The body's "no trim after a verify batch": it fails 8 checks, as the table says.
Both were restored, and the test passes again.
Notes (not blocking)
reset()clears the cache but notmask_ormark_. That is harmless, becausedrafted_is cleared too, and nothing innpu/mtpcallsDraftHead::reset()on this head today.- The 27B run with the DFlash 2 selector, and the k sweep, are still owed on the device once the NPU is back on.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 262". dflash_head is test #193 there and passed.
Refs #1781 (follows #2253, which added the one-pass round this head needs).
What
npu/dflash_head.haddsDFlashHead, a DFlash draft as anmtp::DraftHeadoverdflash_forward. Itsneeds_last_hidden()returns false, somtp::run()takes the one-pass round:[t1, d1..dk]verified in one batch.Q4nxNpuForward::set_layer_taps, one row oftaps * hiddenper position). Atdraft()time it folds the rows not yet in itsDraftKvCacheinto it, then drafts the block[last token, mask, ...]withdraft_forward_cached.commit_rows(M)trims the sink back to its size atdraft()time plus the M rows the round committed. After a plain step (no draft that round) the one row is committed as written.tools/dflash2_probedid this trim by hand.error()says why ("drafting at position 6 over 3 committed rows").docs/npu.mddescribes it in "The draft loop, and whether to serve it".Proof
tests/dflash_head_test.cpp(ctestdflash_head, host only) runsDFlashHeadthroughmtp::run(). The base is a toy that writes one tap row per position it runs, rejected verify rows included, and leaves the trimming to the drafter, as the forward does. The draft is a small randomDraftWeights. The pick rule is always right, every third draft wrong, or always wrong, each at k = 2 and 7, block 8, 30 tokens:draft_forward()computed from scratch over thepos - 1committed rows, bit for bit;Six mutants each fail it:
The full ctest of an
ONEBIT_NPUbuild with the device tests excluded: 266 of 266 passed (before the rebase ontoc91988d; after it,dflash_headandmtp_draftpass again).Not in this PR: running it on the 27B with the DFlash 2 selector as the pick hook, and the k sweep asked for on #2253. Both need the NPU, which is off for now.
🤖 Generated with Claude Code