feat(loader): support Gemma 4 E4B QAT q4_0 GGUF (#211) - #246
Conversation
…n_k_norm) (#211) Google's official gemma-4-E4B-it-qat-q4_0 GGUF failed to load: Error: Missing bias tensor: blk.24.attn_k_norm.weight Root cause: the E4B KV-share tail (18 layers, shared_kv_layers=18) reuses an earlier layer's already-normed K, so those layers carry no attn_k / attn_v / attn_k_norm. The QAT q4_0 repack omits all three; the Q8_0 ships dead, never-read copies — which is why Q8_0 loaded and q4_0 didn't. The CPU ForwardPass already skipped attn_k/attn_v for KV-share layers (`if (!kvShared)`) but loaded attn_k_norm unconditionally. The dense CUDA (CudaForwardPass) and hybrid loaders already guarded it; this aligns the CPU loader — which RunCommand always constructs as the base pass, so the throw fired even on `-g -1`. ApplyQkNormLayer passes k=null for KV-share layers, so the slot is never dereferenced — leaving it null is bit-identical to loading the dead Q8_0 tensor (no numeric change). Verified on the 4070 Ti: q4_0 loads + decodes coherently on CUDA and CPU ("The capital of France is **Paris**." on both), decode 100.4 vs Q8_0 70.5 t/s (1.42×), prefill on par (3283 vs 3444), frees ~3 GB VRAM. Adds a CPU regression test (the constructor that threw pre-fix) asserting the file genuinely omits the shared-layer K-norm and decodes with variety, plus a README row. Download entry (gemma4-e4b-qat) already present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the model loader to skip loading attn_k_norm for KV-share layers, which are omitted in Gemma 4 QAT q4_0 GGUF models, and adds a unit test to verify this behavior. The review feedback suggests using the per-layer KV head count instead of the global _numKvHeads when calculating kNormSize to avoid potential size calculation or loading errors on certain model variants.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // attn_k/attn_v guard above and the CUDA/hybrid loaders (#211). | ||
| if (!kvShared) | ||
| { | ||
| int kNormSize = _perChannelQkNorm ? _numKvHeads * layerHd : layerHd; |
There was a problem hiding this comment.
For models with per-layer KV head counts (such as some Gemma 4 variants), using the global _numKvHeads to calculate kNormSize when _perChannelQkNorm is enabled could lead to incorrect size calculations or loading failures. It is safer to use the per-layer KV head count from hp.LayerKvHeads if available.
int layerKv = hp.LayerKvHeads is { } lkv ? lkv[i] : _numKvHeads;
int kNormSize = _perChannelQkNorm ? layerKv * layerHd : layerHd;…review) pr-test-analyzer flagged that no automated test loaded the q4_0 file on the CUDA -g -1 path (the path the issue actually exercised) — the CudaForwardPass !kvShared guard pre-existed but had no coverage for an actually-absent attn_k_norm (the 12B q4_0 ships all k_norms + uses k_eq_v, a different branch). Adds Gemma4_E4B_Q4_0_CudaForward_LoadsAndMatchesCpuArgmax: asserts the shared-layer k_norm is genuinely absent, loads via CudaForwardPass, and matches the CPU reference at first-decode argmax. Refactored FindModelPath to take a filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review feedback: pr-test-analyzer (CUDA coverage gap): Added code-reviewer + silent-failure-hunter (Vulkan silent-failure-hunter (the unguarded |
|
gemini (ForwardPass.cs:389 — per-layer KV head count for |
Closes #211.
Problem
Google's official
gemma-4-E4B-it-qat-q4_0GGUF failed to load:The Q8_0 E4B loaded fine on the same path, so this was a tensor-layout gap, not a quant-decode issue.
Root cause (verified by tensor diff)
The E4B KV-share tail — 18 layers (
shared_kv_layers=18, layers 24–41) — reuses an earlier layer's already-normed K, so those layers carry noattn_k/attn_v/attn_k_norm. The diff confirms the q4_0 file omits exactly those three tensor types × 18 layers and nothing else; the Q8_0 ships dead, never-read copies (which is why it loaded).The CPU
ForwardPassalready skippedattn_k/attn_vfor KV-share layers (if (!kvShared)) but loadedattn_k_normunconditionally. The dense CUDA (CudaForwardPass) and hybrid loaders already guarded it — this aligns the CPU loader.RunCommandalways constructs the CPUForwardPassas the base pass (line 362), so the throw fired even on-g -1.Why it's numerically identical
ApplyQkNormLayerpassesk=nullfor KV-share layers (ForwardPass.cs:1492) and only dereferences_kNorm[layer]whenk != null. So the shared layers' K-norm slot was never read — loading the dead Q8_0 tensor vs leaving the slot null produces bit-identical output. Pure loader change.Verification (RTX 4070 Ti)
feedback_cross_backend_parity_test).Gemma4_E4B_Q4_0_CpuForward_LoadsWithAbsentSharedKvNorm— asserts the file genuinely omits the shared-layer K-norm (so it would have thrown pre-fix), constructs the pass, and decodes with variety. All 4 Gemma4 CPU tests green (no regression from Q8_0 no longer loading its dead K-norms).Notes
gemma4-e4b-qatdownload entry already exists.appsettings.Local.json(the "recommended model" convenience) is gitignored.🤖 Generated with Claude Code