Repository navigation
npu: Gemma4-12B-IT-NPU2 loads and answers: the 16x256 wide chunks, attention_k_eq_v, no PLE (Refs #1786) - #2265
Conversation
…tention_k_eq_v, no PLE (Refs #1786) Three things stood between FastFlowLM's dense Gemma4 and an answer: - init failed with an EMPTY error: load_norms read post_layernorm (the per-layer-embedding block's norm) for every Gemma4, and this one has no PLE. Read only when ple_dim_ > 0; the lookups now name what is missing. - its I8 tensors are declared [rows/16, K/256, bytes], 2304 (Q4W) or 4352 (Q8W, the head): 16 x 256 chunks, 128 bf16 scales at g*16 + r (symmetric), then 4-bit two's-complement codes at c*8 + r/2 or int8 at c*16 + r. A Q4W tensor has a 4608-byte Q4S grid's byte count and was read as one (non-finite at layer 0); the declared shape now routes it. Settled against google's QAT Q4_0 GGUF (corr 0.9986, 7 tensors, layers 0/5/47) and the pack's own bf16 embedding (Q8W head, 1.00000). - attention_k_eq_v: global layers have no v_proj; v is the k projection (our llama.cpp fork's gemma4.cpp: Vcur = wv ? wv x : Kcur), then Gemma4's scale-free v norm. A missing v_proj without the key is refused. Host: '<|channel>thought\n<channel|>Paris<turn|>' (the template's empty-thought prefix, then the answer); Gemma4-E2B (PLE) still 'Paris'. Tests: wide_chunks in npu_dequant_bf16_test (both kinds, refusals, the Q4S byte-count collision), Gemma4-12B keys in q4nx_gemma3_semantics_test. ctest 268/268. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at 16b9d8a |
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 16b9d8a: READY
Reviewed at 16b9d8ae8b4a93f0870eae229bea5304ce50280b. It is 3 behind main and merges clean. The diff is 8 files, +170/-4.
Checked
- The wide-chunk reader matches the layout it documents.
- Scales are at
(c/32)*16 + r. - Q4W codes are at
c*8 + r/2, even row in the low nibble, two's complement: 256 columns × 8 bytes = 2048 B. - Q8W codes are at
c*16 + r: 4096 B. q4nx_dequant_tensor_widerefuses a wrong byte count, rows off the 16-row grid, and any other chunk size, and it zeroes the pad columns.
- Scales are at
- Routing.
declared_row_bytesreturns the last dimension only for a 3-D tensor whose chunk is 2304 or 4352. Every 2-D tensor reads as before. attention_k_eq_v. The fill copies k's weight rows (nkv * hd * kpad_) intowv_, so v isWk·xbeforek_norm. Attention then applies Gemma4's scale-free v norm, which is the fork'sVcur = wv ? wv x : Kcur. A missingv_projwithout the key is refused by name.- PLE guard.
post_layernormis read only whenple_dim_ > 0. Therd()lambda now names the missing tensor instead of leavinginit failed:empty. - Tests. I built and ran them in an
ONEBIT_NPUhost tree:npu_dequant_bf16_test: all cases ok, including the newwidearms;q4nx_gemma3_semantics_test: ok;- every
batch_check_state_ci.pyarm (qwen2, qwen2vl, lfm3, lfm2, qwen3_5, default): rc 0; tools/copyright.py --check: rc 0.
My mutants on the wide reader (each restored, and the test passes again)
| mutant | npu_dequant_bf16_test |
|---|---|
| nibble order swapped (odd row in the low nibble) | FAIL, 2 checks |
scale slot row-major (r*8 + c/32) |
FAIL, 3 checks |
offset-8 codes (v - 8) |
FAIL, 2 checks |
These cover the alternative orders the body ruled out against Google's QAT GGUF. I did not re-run that correlation: neither the Gemma4-12B pack's weights nor the GGUF are on this box (only FLM's xclbin dirs). The 0.9986 figures and the Paris and sky answers are the author's.
Notes (not blocking)
- The forward-level rules have no CI test yet. If the
wv_copy is dropped, v is zero on every global layer. If the PLE guard is reverted, this model fails at init again. Only the real-pack run would show either. A fabricated dense-Gemma4 arm inbatch_check_state_ci.py(global layers withoutv_proj,attention_k_eq_v: true, no PLE), checked against a numpy reference as the lfm3, qwen2 and qwen2vl arms are, would pin both. - The key is detected with
strstron two spellings."attention_k_eq_v" : true(a space before the colon) or a pretty-printed nestedtext_configwith other whitespace would read as off, and the load would then refuse by name rather than misread, which is safe.get_numalready handlestrue. If the key can sit undertext_config, a nested lookup would be more robust than either. - Owed on the device, NPU off: a kernel set for the 16×256 wide shapes, and the device row.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 270".
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- the byte-count collision is handled the right way round, and it is asserted rather than described
Three changes, and the second is the one that would have been easy to get dangerously wrong.
The collision is real and the discriminator is the declaration, not the size. A Q4W tensor's byte count equals a 4608-byte Q4S grid's, so a size test cannot tell them apart -- the old code read the 12B's tensors as Q4S and layer 0 went non-finite at its first attention. The fix reads the declared shape:
if (d && d->ndim == 3 && (d->shape[2] == Q4NX_CHUNK_Q4W || d->shape[2] == Q4NX_CHUNK_Q8W)) return d->shape[2];and the geometry is then validated rather than trusted: if (nbytes != ntr * ntc * chunk_bytes) return -1;. A mis-declared pack is refused, not misread.
What makes this reviewable without the pack is the test, and it goes further than I would have asked:
- the synthetic arms check the mapping itself --
(code, two's complement, c*8 + r/2, even row low) * scale (g*16 + r), row-major grid -- against google's QAT Q4_0 GGUF, so the byte layout is anchored to something outside this tree; wide: another chunk size is not accepted(4608 ->-1) is the collision guard, pinned;wide: one byte short is refusedandwide: rows off the 16-row grid are refusedare the shape validations, pinned;wide: the columns past the container's K are written 0pins the padding rule;- and the last line of the test states the collision as an assertion --
(4096/16)*(3840/256)*Q4W == (4096/32)*(3840/256)*4608-- so the reason the discriminant must be the declared shape is in the tree, not just in this description.
The other two changes are the right kind of small. post_layernorm is read only when ple_dim_ > 0, so a dense Gemma4 with no PLE block stops looking for a tensor it does not have; and the bf16 lookups now name the missing tensor instead of returning false into an empty error message -- a load failure with no message is a defect of its own, independent of this model, and fixing it is what made the 12B diagnosable at all.
One observation, not an ask: the discriminant itself (ndim == 3 && shape[2] in {2304, 4352}) has no direct arm, because exercising it needs a real ggml_tensor declaration rather than the function's own arguments. The function-level refusal arms cover the same ground from below, so this is a note for whoever next touches the reader: the 3-D-with-4608 case (a legitimately declared Q4S grid) is the one input that would prove the branch is not over-eager, and it costs one tensor in an existing test.
What
FastFlowLM's
Gemma4-12B-IT-NPU2is a dense Gemma4, with no per-layer embedding. The per-op forward failed on it at init, with an empty error message. Three changes take its text tower to a correct answer on the host. Refs #1786; host-side only, the NPU is off.No per-layer embedding.
load_normsreadpost_layernorm, the PLE block's norm, for every Gemma4, and this model has none. It is now read only whenple_dim_ > 0. The bf16 lookups also name the missing tensor instead of returning false silently.The wide chunks. Every I8 tensor is declared
[rows/16, K/256, bytes]:bytesis 2304 (Q4NX_CHUNK_Q4W) or 4352 (Q4NX_CHUNK_Q8W, the tied head);g*16 + r(one per row and 32-column group, symmetric, no zero-point);c*8 + r/2(even row in the low nibble), or int8 codes atc*16 + r.A Q4W tensor has exactly the byte count of a 4608-byte Q4S grid, and that is how it was read. Layer 0 then went non-finite at its first attention.
declared_row_bytesnow reads a 3-D tensor's last dimension, anddequant_anyroutes by it.attention_k_eq_v. The global layers have nov_proj: their values are the k projection, taken beforek_normand then given Gemma4's scale-free v norm. This is our llama.cpp fork's rule (src/models/gemma4.cpp:Vcur = wv ? wv x : Kcur). The forward fills those v rows from k. A missingv_projwithout the key is refused by name.How the layout was settled
The reference is independent weights, not a guess: Google's own
gemma-4-12B-it-qat-q4_0-gguf.Evidence (host, 2026-11-12)
init failed:(empty)The model writes the template's own empty-thought prefix, then answers. Gemma4-E2B, the PLE model, still answers
Paris<turn|>on the host.Tests
npu_dequant_bf16_test→wide_chunks:q4nx_gemma3_semantics_test: Gemma4-12B's keys (attention_k_eq_v, no PLE, global head 512 × 1 kv head), and a Gemma4 without the key.batch_check_state_ci.pyarm (qwen3, lfm2, lfm3, qwen3_5, qwen2, qwen2vl) passes.tools/copyright.py --checkrc 0. Re-run at this head after the rebase onto npu: Qwen2.5-VL's contiguous "mrope" is a position layout, so its text tower loads and answers (Refs #1786) #2261. The Paris answer at this head gives identical tokens.Not in this PR: the vision and audio towers, a kernel set, and a device row.
🤖 Generated with Claude Code