Repository navigation
npu: the DFlash draft's forward as a host reference, with hand-computed cases (refs #1780) - #2174
Conversation
…ed cases (refs #1780) draft_forward() runs one draft pass in f32 on the host: the target's tapped hidden states through fc and hidden_norm as context, the masked block as queries, five non-causal layers, the final norm. decode_draft() turns a loaded package into the weights it needs. dflash_forward_test: two cases worked out by hand, structure properties on a small random model, seven refusals, and the real package when a path is given. Not here: the target-side taps, the pair's acceptance, anything on the device. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 939d7b1 |
PR Reviewer Guide 🔍(Review updated until commit c93f99c)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/dflash_forward.cpp:157-222Memory Allocation in Loop The draft_forward function allocates memory for intermediate results (c, x, kv_in, score, attn) inside the loop over layers. This could lead to performance issues or memory fragmentation, especially if the number of layers is large. Consider pre-allocating these buffers outside the loop. npu/dflash_forward.cpp:157-222Inefficient Memory Usage The function uses std::vector for intermediate computations, which may not be optimal for memory usage and cache performance. Consider using more efficient memory layouts or pre-allocated buffers for better performance on the target hardware. |
|
VERDICT: READY at Why this shape and not "compare against the device"
Two things the acceptance should state
On the tap
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 939d7b1: READY
A host reference for the DFlash draft's forward is the piece that makes every later claim about the draft measurable, and it is the right time to land it: the draft's arithmetic has been arriving in pieces (#1958's acceptance rule, #2053's acceptance arithmetic, #2059's seam), and each piece was tested against itself. A host reference is what lets the device be compared to something that is not the device.
Compiling dflash_forward.cpp and dflash_draft.cpp into the test rather than linking whatever onebit_npu built is the detail worth noting: the reference is built beside its subject from the same sources at the same commit, so it cannot silently be a different revision of the thing it judges.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 939d7b1: CHANGES
Reviewed head 939d7b1 (refs #1780). This is the second verdict on this head. The forward reads right, and the body's mutant table is thorough on the attention structure. One property the reference exists to pin is not covered.
Checked
git merge-treeonto main: no conflicts. CIbuild,hrx_buildand CodeQL green. CI runs the test:#181: dflash_forward … Passed.dflash_forward_test, built from this head: both hand cases match to six digits ("got 1.063206 0.932520, expected 1.063206 0.932520"; "got -1.344616 0.438187"), "dflash_forward: ok (20 checks)".- Mutants beyond the body's, each rebuilt and run:
mutant (npu/dflash_forward.cpp) |
result |
|---|---|
attention scale 1/D instead of 1/sqrt(D) (:175) |
rc 1: "one context row and one block row: the output is the hand-computed one" |
attention output overwrites x instead of adding (:209) |
rc 1: "one row, no context …" |
MLP output overwrites x instead of adding (:221) |
rc 1: "one row, no context …" |
SiLU applied to up, times gate (:218) |
rc 0: all 20 checks pass |
I restored the file (git status clean).
Blocking: the gate/up roles are not pinned. silu(gate) * up against silu(up) * gate is the kind of swap that gives a finite, plausible and wrong draft: the real-package arm checks finiteness and position shift, not values, so it passes too. In the hand cases the two give the same result, presumably because their gate and up weights coincide. A hand case with distinct gate and up weights (for example a gate row that drives silu negative where up is positive) makes the order observable. Since this forward is the host reference later device work will be checked against, it should pin the one MLP fact a converter most often gets backwards.
Not run by me: the real-package arm on ~/scratch-packs/dflash-bolt (finite, peak |x| 15.6, 4.29e-06 shift), which needs the 1.3 GB package.
…ne is activated is pinned (refs #1780) Review of 939d7b1 found that silu(up) * gate passed all 20 checks, because the tiny model's gate and up were both the identity. They now differ, with a gate that goes negative where up is positive; that mutant fails both hand cases. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
The CHANGES at The tiny model's gate and up were both the identity, so Only Separate evidence on the real weights, since the real-package arm checks only finiteness: with #2180's taps, this forward's proposals for the Bolt draft were scored against its target model on the host. One prompt, 16 greedy tokens: two passes accepted 4 and 5 of 7 proposals, and the first proposal was right in both. That is a small sample (a three-prompt run is in progress), but a swapped gate/up or a wrong tap point would not produce it. ready for review at cb4970d |
|
Persistent review updated to latest commit cb4970d |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at cb4970d: READY
Re-review of cb4970d (adds cb4970d, which answers my CHANGES at 939d7b1: silu(up) * gate passed all 20 checks because the tiny model's gate and up were both the identity).
Checked at this head (merges cleanly onto main; review build at the head):
dflash_forward_test: ok (20 checks).- With the Bolt draft (
~/scratch-packs/dflash-bolt): ok (22 checks), peak |x| 15.614, moved by at most 4.29e-06 at pos0 4096. This matches the body. It took 7.6 s with a 1.64 GB peak. ctest -R dflash: 4 of 4 passed.- CI
buildat cb4970d: success.
Mutants (one edit each to npu/dflash_forward.cpp, rebuilt, then restored; the tree is clean afterwards):
| mutant | result |
|---|---|
silu(up) * gate (the one that passed at 939d7b1) |
rc 1, 3 fail lines |
gate and up weights swapped in the two linear calls |
rc 1, 3 fail lines |
The new tiny-model gate drives its first output negative where up is positive, so the hand cases now pin which projection is activated.
Non-blocking: every tiny-model matrix is still diagonal, so a transposed projection (W^T x) would pass the hand cases. Only the real-package finiteness check would see it. Making one off-diagonal entry nonzero in, say, L.v or L.down would close that.
|
Persistent review updated to latest commit c93f99c |
|
Head moved to |
…lash_acceptance for a draft against its target (refs #1780) (#2236) * npu: a Gemma4 pack stops re-reading its layer weights every token once they are packed (refs #1780) step() skips the per-token Q4NX dequant for a dense model whose projections are on the device, but excluded per-layer-embedding models. Their gate and projection are read at init and packed like the rest, so the exclusion only cost time: 3.4 s per step against 1.9-2.5 s on an E4B, same reply. NPU_PLE_RELOAD=1 keeps the old behaviour for an A/B. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tools: dflash_acceptance, a DFlash draft scored against its target on the host; hand cases pin transposes (refs #1780) The target runs greedily with set_layer_taps() on the draft's target_layer_ids; the draft's reference forward proposes a block from those rows and each proposal is checked against the target's own next token. dflash_forward_test: v and down each get an off-diagonal entry, so a projection applied transposed fails the hand cases (review note on #2174). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Refs #1780 (item 2 of its "supported" list: the draft takes the target's tapped hidden states and a masked block and returns the block's hidden rows). This does not finish the issue.
The draft's forward now exists as a host reference, checked against two hand-computed cases. It has not been run against a target model yet, so nothing here says its proposals are accepted.
What is added
npu/dflash_forward.{h,cpp}:decode_draft(load_draft(dir))gives f32 weights (about 1.3 GB for the Bolt draft).draft_forward(weights, taps, ctx, noise, block, pos0, window)runs one pass. The header states the computation line by line.tests/dflash_forward_test.cpp, registered as ctestdflash_forwardand built withonebit.docs/amd-drafters.md: a section for the forward, including what is not established.The structure follows the interface the public DFlash reference states: the context is
hidden_norm(fc(taps))and is not passed through a layer'sinput_norm; keys and values are the context rows followed by the block rows; the block is not causal; the draft has no embedding table or head of its own. The code is written from that description, plain loops, one thread.Proof (this box, device-free)
The hand cases are plain arithmetic written out in the test: one block row with no context, and one context row plus one block row with a different gain on every norm that touches the attention.
Mutants (each is one edit to
dflash_forward.cpp, linked with the build's own link line)head % kv_headsinstead of its groupinput_normhidden_normon the contextk_norm/ noq_norminput_norm1/sqrt(head_dim)Two of these (context through
input_norm, nok_norm) survived the first version of the test, because every gain in the hand cases was 1. The hand cases now use distinct gains and both fail.Not done, and what comes next on #1780
Nemotron-3.5-Bolt-E4B-NPU2, which is not on this box. The next step is the target side: hidden states attarget_layer_idsout of our forward, then acceptance of this reference's proposals against the target's greedy tokens.sliding_windowfrom the package is not applied by default: the caller passeswindow. For a context under 1024 positions it makes no difference.🤖 Generated with Claude Code