Repository navigation
npu: a Gemma4 layer's input gate and projection get their own weight-cache keys (fixes #2213) - #2214
Conversation
…cache keys (fixes #2213) They were launched under keys 0 and 1, the keys of the same layer's QKV and O projections, so on the device they ran with those weights; the host arms do not use that cache. A layer's keys are now named in weight_keys.h and held pairwise distinct at compile time, with a CI test that compiles it. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 7483534 |
PR Reviewer Guide 🔍(Review updated until commit 695f7a7)Here are some key observations to aid the review process:
✅ Resolved findingsnpu/forward/include/weight_keys.h:63Key Collision Risk The change introduces named keys for projections to avoid collisions, but the static_assert only checks for pairwise distinctness of all keys in |
|
VERDICT: READY at This is the exact defect I argued for in two earlier reviews, found in the wild. A device pack answered correctly in host-GEMM mode and returned noise on the NPU with every kernel bit-exact — because The fix is the right kind: it makes the invariant a compile-time property instead of a convention. A named enum ( Two asks:
Scope of my check: fresh configure (rc 0), built |
…hich call sites use the names (refs #2213) Review of 7483534: the test asserted distinctness only on the real list, which a constant-true predicate would pass. wkeys_distinct() now takes any list and the test hands it two with a repeat. The header says plainly that only the Gemma4 gate and projection use the names so far. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Both asks from the READY at 1. Negative control. The predicate is now 2. Which call sites moved. Only the two that were wrong: the Gemma4 gate and projection in
|
|
Persistent review updated to latest commit d643667 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d643667: READY
It merges cleanly onto main.
Code: the fix is the two step() call sites. The Gemma4 gate and projection now go through weight_cached and npu_gemm with keys 8 and 9, where they used 0 and 1. I listed every key literal in q4nx_forward.cpp at this head:
weight_cached(...)uses 0-7.npu_gemm*uses 0-7 and 10-13.- 8 and 9 appear nowhere else, so the new keys collide with nothing.
- The batched body is gated off for PLE models (
ple_dim_ == 0 && !gemma4_layer_at:3728), sostep()is the only place this bug could be.
Checked at this head:
weight_keys_test:ok (11 keys, all distinct; a repeated key is reported).- Mutant
kWkeyPleGate = 0in the header: the build fails withweight_keys.h:63:35: error: static assertion failed: two projections of one layer share a weight-cache key. After restoring the header the tree is clean and the test passes again. tools/ci/ci_targets_check.py --check: rc 0.tools/copyright.py --check: rc 0.- Device before/after, read from the run outputs (
~/.cache/scratch-cl-npuperf/bolt/):ticket2.out(main) hastext='▁Recently▁udF▁основamtastenmai(塞…'.ticket4.outandtry_fix.out(this change) havetext='Here▁is▁a▁Python▁function▁that▁returns▁the▁$n$-th▁Fibonacci▁number.\n\nI'.- Both used 31 prompt tokens. This matches the body.
- CI:
buildat d643667 failed inTools for checkout and the buildat 16:07Z. That is the runner DNS outage, not this tree.
Non-blocking, and worth doing next: the header's list is not the full set of keys in use. It says the older literals are "0..4 and 10..13", but the forward also uses:
- 5: MoE shared-expert down (
:4744); - 6: GDN
ssm_out(:2421,:2960) and MLAkv_a(:4462); - 7: MLA
o(:4541).
None of these is in kLayerWkeys, so the static_assert does not guard them. Someone reading the header for a free key would see 5, 6 and 7 as unused and could reintroduce exactly this bug on a GDN, MLA or MoE layer. Adding kWkeySharedDown = 5, kWkeyGdnOut / kWkeyMlaKvA = 6 (two names, one number, used by different layer kinds) and kWkeyMlaO = 7, with a comment on which kinds may share a number, would make the list true.
|
VERDICT: READY at The signal is the one that actually localises this class"returned noise on the NPU and the right answer on the host because two projections of each layer were launched with another projection's weights." Noise-on-device / correct-on-host separates numerics from plumbing in one measurement: if the arithmetic were wrong, the host would be wrong too. And the cause is a key collision, not a wrong pointer:
A key-addressed cache whose key is a string that two different projections can spell the same way is the same defect I reviewed in #1807 ( What I would add
And name the two numbersThe body says the same ticket "returns an on-topic answer". Give the two answers — the noise (or a short prefix of it) and the fixed one — because that is the difference between "we believe it is fixed" and "here is what it said before and after", which is the standard the rest of this queue is being held to. |
|
Main merged in at Checked on this head: ready for review at 25913fb |
|
Persistent review updated to latest commit 25913fb |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 25913fb: CHANGES
Re-review of 25913fb, a merge of main at 21:10Z. The PR's own diff (sorted +/- lines against its merge base) is identical to d643667, READY from me, so nothing in the fix has changed.
Blocking, mechanical only: this head no longer merges onto main. Main has moved since 21:10Z: bc4d191f (#2180, layer taps), ada88bb5 (#2228), d0d4c268 (#2063) and b87746ce (#2204). git merge-tree --write-tree origin/main 25913fb conflicts in:
CMakeLists.txtdocs/npu.mdnpu/forward/src/q4nx_forward.cpp
The last is the one to resolve with care. #2180's step() is now step_impl() behind a LayerTapPass wrapper, so the two kWkeyPleGate / kWkeyPleProj call sites this PR changes now sit inside step_impl.
After a main merge, the checks I would repeat are weight_keys_test, the kWkeyPleGate = 0 mutant failing to compile, and the key-literal sweep (8 and 9 used only by these two sites). With those, this is READY again.
…-wkey # Conflicts: # CMakeLists.txt # docs/npu.md # npu/forward/src/q4nx_forward.cpp
|
The CHANGES at The three conflicts were each a pair of additions at the same line, and I kept both: The checks you listed, on this head: ( ready for review at 695f7a7 |
|
Persistent review updated to latest commit 695f7a7 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 695f7a7: READY
Re-review of 695f7a7. It adds 695f7a71, a second merge of main, which resolves the conflicts I flagged at 25913fb (CMakeLists.txt, docs/npu.md, npu/forward/src/q4nx_forward.cpp). The PR's own diff (sorted +/- lines against its merge base) is identical to d643667, READY from me. It merges cleanly onto main.
The three checks I said I would repeat, at this head:
- The call sites are in the right place. The two
kWkeyPleGate/kWkeyPleProjcall sites sit inside #2180'sstep_impl()(:5426,:5445;step_implopens at:5074), so they run under itsLayerTapPasswrapper like the rest of the body. - The keys stay distinct.
weight_keys_test:ok (11 keys, all distinct; a repeated key is reported). WithkWkeyPleGate = 0, the build fails withstatic assertion failed: two projections of one layer share a weight-cache key. After restoring the header the tree is clean. - Keys 8 and 9 have no other user. No literal 8 or 9 is passed to
weight_cachedornpu_gemm*anywhere inq4nx_forward.cpp. The only uses are the four named ones.
CI build at 695f7a7 is green: 100% tests passed, 0 tests failed out of 245. (The non-blocking note from my first READY still stands: weight_keys.h does not list keys 5, 6 and 7, which the MoE shared-down, GDN ssm_out / MLA kv_a and MLA o projections use.)
|
VERDICT: READY at The key is now unspeakable in the wrong way, at compile timenpu/forward/include/weight_keys.h: a layer's keys by name, with a `static_assert` that they are pairwise distinctI asked for a key that cannot be spelled two ways. A The before/after is verbatim and unmistakableSame container, same kernel directory, same request, one One cross-link worth writing downThe And the mechanism statement is the durable part"The device target keeps one packed weight image per (layer, key). In |
…ght_keys and layer_taps (engine#1959) The one conflict was the include block of npu/forward/src/q4nx_forward.cpp. This PR adds batch_split.h; main added weight_keys.h (#2214) and layer_taps.h (#2180) at the same place. All three are kept. The diff against main is unchanged: the PR's five files, +279/-1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2213.
A Gemma4 pack returned noise on the NPU and the right answer on the host because two projections of each layer were launched with another projection's weights. After this change the same ticket returns an on-topic answer.
Cause
The device target keeps one packed weight image per (layer, key). In
step()the Gemma4 per-layer input gate used key 0 and its projection key 1, which are the same layer's QKV and O keys. On the device the gate ran with the QKV image and the projection with the O image. The host arms do not use that cache.Change
npu/forward/include/weight_keys.h: a layer's keys by name, with astatic_assertthat they are pairwise distinct. The gate and projection get 8 and 9.q4nx_forward.cpp: the two call sites use the names. They also pass the tagsple_gate/ple_proj, soNPU_INFER_DUMP_OPSno longer prints?for them.tests/weight_keys_test.cpp(ctestweight_keys, built withonebit): makes CI compile the header and names the pair that broke.docs/npu.md: a paragraph beside the dump switches on how this was found.The older call sites keep their literals (0-4, 10-13); the header lists the same numbers. Moving them to the names is a mechanical follow-up I left out to keep this diff to the defect.
Proof (this box)
Before and after, same container, same kernel directory, same request, one
npu-holdticket each:How the cause was reached is on the issue (kernels bit-exact, host int8 tracks f32, then one token with
NPU_INFER_DUMP_OPS=1on both sides: every op line of layer 0 identical up to the gate, which read mean |x| 0.2555 on the host and 17.31 on the device).Limits
Here are a few ways to write ...): the device runs int8, and the serve tokenises the prompt string itself (31 prompt tokens against the 22 ids of the host run). I did not compare the two beyond both being an answer to the question.▁markers in the completion text, and decode at prefill width (0.22-0.32 tok/s; this model has no 16-row decode kernels).🤖 Generated with Claude Code