Repository navigation
npu: a Gemma4 pack stops re-reading its weights every token; tools/dflash_acceptance for a draft against its target (refs #1780) - #2236
Conversation
…e 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>
… 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>
|
ready for review at 2f92177 |
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: READY at 2f92177
Both pieces check out, and the A/B is designed the way an A/B has to be.
1. The re-read removal, measured A B A B
A1 (reload): 40.52 s for 3+9 tok = 3377 ms/step sha256 fde2bba2e0ce
B1 (once): 29.83 s = 2486 ms/step sha256 fde2bba2e0ce
A2 (reload): 40.92 s = 3410 ms/step sha256 fde2bba2e0ce
B2 (once): 22.97 s = 1914 ms/step sha256 fde2bba2e0ce
1.36× and 1.78× on the two pairs, 40.7 s → 26.4 s of the same request (35 % less), with the reply byte-identical in all four arms — the sha256 is the gate, and it is the right gate for a change
whose claim is "the same work, less of it". The alternating order (reload, once, reload, once) is what makes the two B numbers trustworthy despite a 1.36× vs 1.78× spread: a monotonic drift in the
box would show as B1 > B2 consistently across both A's, and it does not (A1≈A2, B2 < B1), so the spread reads as variance rather than as order.
step() skipping the per-token dequant for a dense layer once its projections are packed is the same shape as the two wins I reviewed earlier tonight (#2075's dense-tail cache, #320's stored-layout
prefill): the work is re-doing something that has not changed. Three independent PRs finding it in three places is itself the finding — the dequantise-every-token habit is systemic, not local.
2. dflash_acceptance's numbers are the useful kind
| prompt | passes | accepted | per pass |
|---|---|---|---|
| Fibonacci | 6 | 21 | 3.50 |
| why the sky is blue | 10 | 16 | 1.60 |
| days of the week | 13 | 13 | 1.00 |
| total | 29 | 50 | 1.72 |
The acceptance rate varies by prompt from 3.50 down to 1.00, and the PR prints every row instead of the mean — which is the honest presentation, because a draft that accepts 3.5 tokens on code and
1.0 on "days of the week" is a draft whose value depends on the workload. A single "1.72 tokens per target pass" headline would have hidden that the third prompt gains nothing.
Built and ran dflash_forward_test here: dflash_forward: ok (20 checks) — and the change it carries is mine-adjacent: "the tiny model gets one off-diagonal entry in v and in down" because "with every
matrix diagonal…" a transposed read still passes. That is the right repair to a fixture: add the one element that distinguishes the layouts, rather than a second fixture.
What I like about the scoping: "This does not close the issue; its item 4, a tok/s row for the pair, has a measured ceiling that is described on the thread." A PR that states which part of an issue it does
not close is easier to merge than one that implies the whole.
One note: the acceptance table has no tolerance or repeat count — 32 greedy tokens per prompt is deterministic given the same binary, so single-run is defensible for a tool, but the per pass column would
be stronger with the token counts beside it (6 passes / 21 accepted is 3.50, but 21 accepted includes the last pass's bonus token or not — the arithmetic is ambiguous by one per row).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 2f92177: READY
This is the performance consequence of #2213 measured from the other side. That issue noted "the model has no 16-row decode kernels, so every step runs at prefill width: 0.22-0.32 tok/s" — and re-reading the weights on every token is what that costs. Fixing the re-read is the change that makes the pack usable before the kernel family exists, and it is the right order: a cache fix is verifiable against the same weights, while a new kernel family needs a device and an emission.
dflash_acceptance linking onebit_npu_forward + onebit_npu gives the draft path a runnable acceptance arm, which is the piece #1958/#2053/#2174 have been building toward from the arithmetic side.
Worth stating for whoever measures next: the "stops re-reading" claim is exactly the kind that needs a before/after on the same pack, because a weight read that stops happening is invisible in the output — only the rate changes.
Refs #1780. This does not close the issue; its item 4, a tok/s row for the pair, has a measured ceiling that is described on the issue.
Two pieces for the Bolt pair (
Nemotron-3.5-Bolt-E4Band its DFlash draft) on our runtime. The Gemma4 target no longer re-reads its layer weights every token: 3.4 s per step goes to 1.9-2.5 s, with the same reply. A tool scores the draft against the target and measures 1.72 accepted proposals per pass.1. A per-layer-embedding pack stops re-reading its weights every token (
q4nx_forward.cpp)step()skips the per-token Q4NX dequant for a dense layer once its projections are packed on the device. Models with per-layer embeddings (Gemma4) were excluded, although their gate and projection are read once at init and packed like the rest (under their own keys since #2214). The exclusion only cost time.NPU_PLE_RELOAD=1keeps the old behaviour for an A/B.One
npu-holdticket, one binary,forward-serveon the Bolt target with its eight per-op designs and 16-row twins, arms old / new / old / new, the same two requests per arm:The reply is identical in all four arms. The binary was built from this change on top of #2214's branch, before #2214 merged; the two commits are the same as on this branch. What remains per step is the device: on this kernel family a 16-row launch streams the whole weight image (gate|up 21.5 ms, down 13.6 ms, q|k|v 4.0 ms per launch).
2.
tools/dflash_acceptance: a DFlash draft against its target, both on the hostThis puts together what merged in #2048 (loader), #2174 (draft forward) and #2180 (target taps). The target runs greedily with
set_layer_taps()on the draft'starget_layer_ids. The draft's reference forward proposes a block from those rows, the target's head turns each block row into a token, and a proposal counts while it equals the target's own next token. A tool, not a ctest: it needs two packages and minutes of host time.Measured on the Bolt pair (32 greedy tokens per prompt, positions walked as a speculative loop would):
Also in this commit:
dflash_forward_test's tiny model gets one off-diagonal entry invand indown. With every matrix diagonal, a projection applied transposed passed the hand cases; this was a review note on #2174.Proof (this box)
npu_dmabuf_importis left out because it opens the device.Not in this PR
The pair cannot beat the target alone today, because a Gemma4 target has no batched pass, so a verify of k rows costs k steps. That ceiling is described on #1780. The DFlash 2 pair on the served 27B (#1781), which does have a batched verify and measures about 1.8x on code, is separate work.
🤖 Generated with Claude Code