Repository navigation
npu: the DFlash draft loader and forward read DFlash 2 packages (refs #1781) - #2237
Conversation
…1781) load_draft() takes DFlash2DraftModel as generation 2: nested dflash_config, Q4_K chunks for every projection, the grouped convolutions and the candidate selector. The forward gains draft_conv(), selector_pick(), a context key/value cache and an optional GEMM hook; its projections are threaded when the build has OpenMP. Hand-computed cases for the convolution and the selector, a stepwise-cache check, and a real-package arm in the loader test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at ac2b283 |
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 ac2b283: READY
It merges cleanly onto main. CI build at ac2b283 is green: 100% tests passed, 0 tests failed out of 248. tools/copyright.py --check and tools/ci/ci_targets_check.py --check: rc 0.
Checked at this head, from the review build:
dflash_forward_test:ok (27 checks).dflash_draft_test "" ~/scratch-packs/dflash2-27b:ok (60 checks), withDFlash 2 package: 81 tensors, 5 layers, hidden 5120, block 8, taps 5 19 33 47 61, conv 2 x groups of 16, selector rank 256 top 16, vocab 248320. That is the body's line.- DFlash 1 is unchanged (regression check):
dflash_draft_test ~/scratch-packs/dflash-boltgivesok (112 checks);dflash_forward_teston the Bolt package givesok (29 checks), withpeak |x| 15.614, moved by at most 4.29e-06 when the positions start at 4096, identical to #2174's figures. So routingdraft_forward()through the new cache did not move the generation-1 forward.
- A mutant not in the body's table:
cache_extend()skipsk_normon the cached context keys (dflash_forward.cpp:260). The test fails (rc 1):FAIL one context row and one block row: the output is the hand-computed one. This matters becausedraft_forward()now goes through the cache too, so the "cache grown 2+1+2 equals the full forward" arm could not see a fault common to both paths. The hand case does. After restoring the file the tree is clean and the test passes again.
Threading: the OpenMP loop splits output rows and keeps each double sum in one thread, so a build is deterministic. Only different builds (simd lanes) can differ in the last bits, which the code comment says.
|
VERDICT: READY at The scope is declared, which matters on a slice
Three kinds of assertion, and each answers a different questioncheck(max_diff(two, draft_forward(w, taps.data(), 2, noise.data(), block, 3)) == 0.0, …)
check(c.rows == ctx, "cache: rows add up");
check(max_diff(draft_forward_cached(w, c, noise.data(), block), draft_forward(w, …)) == 0.0, …)
check(same, "DFlash 2 convolution: three rows against the hand-computed ones …")
check(selector_pick(unary, 3, prev, h, succ, 2) == 2, …)
Two asks
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at ac2b283
Built both tests and ran them: dflash_draft: ok (54 checks), dflash_forward: ok (27 checks) — the counts are up from the 20 I measured on the previous head, and the new checks are the ones a
generation-2 loader needs.
The three selector arms are exactly the triple I would specify, and the middle one is the reason:
selector: the pairwise term can lift the lowest-logit candidate over the highest <- the term WORKS
selector: with no pairwise term it is the plain argmax <- the term is REQUIRED
selector: the first of equals is kept <- the tie rule
The second is the control that makes the first mean something: a selector that always returned argmax(unary + pairwise) passes arm 1 whether or not it reads unary; flat pairwise terms make arm 2
distinguish "reads unary" from "reads the pairwise matrix". Without arm 2 the feature could be silently absent.
The cache arm is the other one worth naming:
check(c.rows == ctx, "cache: rows add up")
check(max_diff(draft_forward_cached(w, c, noise, block), draft_forward(w, taps, ctx, noise, block)) == 0.0,
"…")
== 0.0, not a tolerance — a cache that returns the same rows in a different order, or recomputes instead of reusing, fails; a tolerance would let a one-row shift through. For a mechanism whose whole point is
"same numbers, less work", exact equality is the only assertion that is meaningful.
And max_diff(…, 3)) with block, 3: the convolution check is stated against three rows — "a row's own kernel, the row before, …" — which is the smallest window that distinguishes a real convolution
from a shifted copy. Two rows would pass for a kernel that only reads its immediate predecessor.
Scope discipline, again the right shape: "Refs #1781 (gate 4's second named pair) … This does not finish the issue." A PR that says which part it does not close is easier to merge than one that
implies the whole, and this is the second time tonight the same author has done that.
One note: both tests SKIP without a package (SKIP the DFlash 2 package: pass its directory second, or set ONEBIT_TEST_DFLASH2_DRAFT), so the 54/27 counts are the synthesised cases — the DFlash 2
package itself is unrun here, and the body should say which of its numbers come from a package and which from the synthetic path. The two named skip lines make that visible, which is the right compromise.
Not run: the DFlash 2 package (not on this box).
Refs #1781 (gate 4's second named pair:
qwen3.8-mtp:27b+ DFlash 2). This does not finish the issue.The draft loader and the draft's reference forward now read DFlash 2 packages, the draft behind the 1.8x code-prompt result on #1781. This PR is the library half with its tests; the loop that measured the result is not in it.
What is added
load_draft()readsDFlash2DraftModelas generation 2 (npu/dflash_draft.{h,cpp}):dflash_configand the rotation base fromrope_parameters;decode_row().As before, every tensor must be accounted for, and an architecture the loader does not know is refused by name.
The forward gains (
npu/dflash_forward.{h,cpp}):draft_conv(): one half of the grouped convolution;selector_pick(): the selector's choice at one block row;DraftKvCache/cache_extend()/draft_forward_cached(): the context's keys and values computed once per context row, for a caller that drafts many blocks over one growing context.draft_forward_fused()anddraft_forward()now go through it, so the existing hand cases cover it;set_gemm_hook(): an optional place for the projections to run. Unset, everything stays on the host;docs/amd-drafters.md: a DFlash 2 section.Proof (this box)
npu_dmabuf_importis left out because it opens the device.Mutants, one edit each to
npu/dflash_forward.cpp, linked with the build's own link line:Two test arms changed because behaviour changed:
DFlash2DraftModelis now read by the DFlash 2 rules and refused by the field it lacks;DFlash3DraftModel) is still refused by name.What these pieces measured on #1781 (not part of this PR's proof)
In a local loop against the served 27B, with the target on the NPU and the draft's projections on the NPU through the GEMM hook: on a code prompt 7.6-7.8 tok/s against 4.2-4.27 tok/s plain decoding, with output identical to plain greedy decoding, 48 of 48. With a fallback to plain decoding, the pair does not lose on prose or a list either. The tables are on #1781.
🤖 Generated with Claude Code