Gemma 4 E4B support (gemma4 arch, MatFormer/PLE): inference + full-GPU LoRA training#138
Gemma 4 E4B support (gemma4 arch, MatFormer/PLE): inference + full-GPU LoRA training#1383ndetz wants to merge 8 commits into
Conversation
…ward Two fixes toward Gemma-4-E4B support: 1) Loader: GEMMA4 tensor-loading required attn_k / attn_k_norm for every layer, but E4B shares KV across the last shared_kv_layers layers (those layers have no own attn_k/attn_v/attn_k_norm in the GGUF). hparams already computes n_layer_kv_from_start and the KV-cache graph already has the reuse callback; only the loader was over-strict. Mark wk/attn_k_norm NOT_REQUIRED for shared-KV layers (i >= n_layer_kv_from_start). After this, E4B models LOAD and run inference (previously: 'missing tensor blk.N.attn_k.weight'). 2) Training: add backward pass for GGML_UNARY_OP_TANH (was unsupported, aborted in ggml_compute_backward). Gemma uses tanh for logit/attn soft-capping. d/dx tanh(x) = 1 - tanh(x)^2, expressed via existing ops (works on all backends, no new kernels).
GGML_UNARY_OP_GELU had no backward → ggml_compute_backward aborted on any training graph through a GELU (Gemma FFN / per-layer-embedding path). Implements the exact tanh-approx GELU derivative matching ggml_gelu_backward_f32 (ggml-cpu/vec.h), built purely from existing ops (sqr/scale/scale_bias/mul/tanh/ add) so it runs on every backend with no new kernels: dx = dy*0.5*(1 + t + x*c*(1+3a*x^2)*(1-t^2)), t=tanh(c*x*(1+a*x^2)) After this, E4B training advances through the full forward+backward graph construction into compute (next blocker is a CUDA out_prod shape issue on gemma4's variable per-layer KV dims, tracked separately).
CPU rms_norm_back asserts src0->nb[0]==sizeof(float) (contiguous), but gemma4's backward graph feeds a non-contiguous grad/src0 → GGML_ASSERT abort. Wrap both in ggml_cont before ggml_rms_norm_back (all-backend, faithful copy). After this, Gemma-4-E4B training runs end-to-end on the CPU backend and saves a valid GGUF LoRA adapter (168 tensors). Generic backward ops verified correct (Qwen3-0.6B loss converges with the same binary). NOTE: E4B loss still diverges — a remaining gemma4-specific backward numerical issue (soft-cap tanh / gemma (1+weight) RMSNorm / altup), tracked for follow-up.
The GELU backward computed (1+3a*x^2)*(1-tanh^2(g)). For Gemma's large pre-GELU activations, (1+3a*x^2)->Inf while (1-tanh^2)->0, giving Inf*0=NaN. This produced NaN gradients → NaN LoRA weights → training did not learn (val loss frozen) for Gemma-4-E4B specifically (Qwen uses SiLU, never hit this path). Clamp x to [-30,30] before the derivative: gelu'(x) saturates (->1 for x>>0, ->0 for x<<0), so this is numerically faithful while bounding the intermediates. After this fix, Gemma-4-E4B LoRA training converges: val loss drops monotonically across epochs (e.g. 3.24 -> 2.22 -> 1.89) where before it was frozen.
|
Update: Gemma-4-E4B LoRA training now converges end-to-end. The last blocker was a NaN in the GELU backward I added: for Gemma's large pre-GELU activations, Evidence on a Gemma-4-E4B Q4_K_M model:
Remaining for full GPU speed: the CUDA |
|
Pinpointed the CUDA The failing
Wrapping the generic MUL_MAT-backward out_prod args in Local workaround that makes gemma4 training work today: fall back |
ggml_cuda_out_prod only converted *quantized* src0/src1 to F32; F16 was explicitly excluded and fell through to being read as raw float*. That gave a wrong pointer interpretation AND lda = nb01/sizeof(float) = ne00/2 < m, so cuBLAS sgemm returned CUBLAS_STATUS_INVALID_VALUE. This is hit by Gemma-4-E4B's attention backward (F16 permuted operands). Fix: convert any non-F32 operand (F16 included) to F32 via the existing ggml_get_to_fp32_cuda path. Result: Gemma-4-E4B LoRA training now runs fully on CUDA (no CPU fallback for out_prod). 4 epochs in ~64s, val loss converges 3.18->2.28->1.93->1.62.
|
Resolved — out_prod now works on CUDA, Gemma-4-E4B trains fully on GPU. Root cause was simpler than the transposed-src0 theory: Fix (pushed): convert any non-F32 operand (F16 included) via the existing Result: 4 epochs in ~64s on an RTX 3090, val loss converges 3.18 → 2.28 → 1.93 → 1.62. Full chain (loader + dataset guard + TANH/GELU/rms backward + GELU clamp + this F16 out_prod fix) gives working full-GPU LoRA training for Gemma-4-E4B. |
…ining (zero model reload) Loads model once, serves /completion AND /train on the same resident model. Training context (training=true) on the shared model; trained adapter applied to inference ctx in-process. Gemma-4-E4B Q4 on RTX 3090: /train ~11s (vs ~180s with separate finetune binary + server restarts), inference ~1s. Demonstrates the zero-reload on-device personalization loop.
|
Added Training uses a context with On Gemma-4-E4B Q4_K_M (RTX 3090): in-process |
|
Hello @3ndetz , sorry for the late reply. We rebased our fork on llama.cpp v8828. is the issue still there? Also if you wanna add lora-finetuning support for a new model architecture Vulkan and Metal support is required |
|
Thanks for the review, @gianni-cor. Let me clear up the model identity first, since the PR title was ambiguous (my fault): It's gemma-4-E4B ( Validation (not just a smoke test): with these changes our build loads gemma-4-E4B (Q4_K_M) and runs full-GPU LoRA finetuning end-to-end. I trained a complete persona/belief LoRA on it over a ~25k-doc corpus (loss converges over thousands of steps), merged it with Re the v8828 rebase: fair question — our fork is not rebased onto v8828 yet, so I can't claim the loader fix is still required there. I'll rebase onto v8828, rebuild, and re-verify whether gemma-4-E4B still needs the shared-KV loader patch (newer llama.cpp may already cover part of it), then report back here with the concrete result. Re Vulkan/Metal: noted — the training-backward additions (TANH/GELU backward, out-prod fix) are CUDA-only at the moment; Vulkan/Metal kernels would be a separate follow-up. |
|
Followed up on the v8828 rebase question — checked The inference-loading issue is resolved upstream on v8828; my loader fix is redundant there. What may still be relevant from this PR is the training side — the unary backward ops (TANH/GELU backward), Thanks for the pointer — closing the inference part as obsoleted by the rebase. |
|
Followup — built I built the It loads with full GPU offload ( The remaining unique value of this PR is the gemma-4 LoRA training-backward (GELU/TANH backward + CUDA |
Summary
Adds support for Gemma-4-E4B (gemma3n-style MatFormer with KV-sharing). Upstream QVAC declares
LLM_ARCH_GEMMA4but cannot load E4B GGUFs (missing tensor 'blk.N.attn_k.weight'). This PR fixes inference loading and gets training to run end-to-end on the CPU backend.Status: draft / WIP.
What works after this PR
Inference loading of Gemma-4-E4B ✅
attn_k/attn_k_normfor every layer, but E4B shares KV across the lastattention.shared_kv_layerslayers (those have no ownattn_k/attn_v/attn_k_norm).hparams.n_layer_kv_from_startand the KV reuse callback already exist — only the loader was over-strict. Markwk/attn_k_normNOT_REQUIREDfor shared-KV layers.llama-cli(was a hard failure).TANH backward ✅ —
d/dx tanh = 1-tanh², via existing ops (all backends). Gemma uses tanh for soft-capping.GELU backward ✅ — exact tanh-approx GELU derivative matching
ggml_gelu_backward_f32, built from existing ops (scale_bias/sqr/mul/tanh/add), no new kernels.rms_norm_back contiguity ✅ — gemma4 feeds a non-contiguous grad into
rms_norm_back; CPU op asserts contiguous. Forceggml_cont.Verified
Remaining work
(1+weight)RMSNorm convention in the backward, or the altup / per-layer-embedding path. Needs per-op gradient checking on the gemma4 graph.out_prodraisesCUBLAS_STATUS_INVALID_VALUEon gemma4's variable per-layer KV dims (ggml-cuda/out-prod.cu:113). CPU backend is unaffected; GPU training needs an out_prod shape fix.ggml_opt_dataset_initwhen corpus < context window (ndatawraps to ~2^64). Workaround: corpus >-c. A clamp/error would be friendlier.Guidance welcome on the gemma4-specific backward (esp. whether soft-cap/altup have intended backward paths) and on out_prod for variable-KV shapes.