Skip to content

Gemma4 ov int4 and stateful fixes#260

Draft
cavusmustafa wants to merge 6 commits into
ravi9:dev_backend_openvinofrom
cavusmustafa:gemma4_ov_int4_and_stateful_fixes
Draft

Gemma4 ov int4 and stateful fixes#260
cavusmustafa wants to merge 6 commits into
ravi9:dev_backend_openvinofrom
cavusmustafa:gemma4_ov_int4_and_stateful_fixes

Conversation

@cavusmustafa

Copy link
Copy Markdown
Collaborator
  • enables gemma4 dense stateful support
  • optional int4 downscale for requant: sacrifice accuracy for performance (env to enable - GGML_OPENVINO_INT4_REQUANT)

cavusmustafa and others added 6 commits July 15, 2026 02:19
Adds support for the Gemma-4 26B-A4B hybrid-FFN MoE model on the OpenVINO
backend (dense shared FFN + 128-expert top-8 MoE per layer, interleaved
SWA/global attention, QK-norm, sandwich norms, logit soft-cap).

Backend changes (all under ggml/src/ggml-openvino/):
- 3D quantized expert weights: rank-2 GatherCompressed-matchable dequant
  (Q4_K gate/up, Q5_1 down), f16 zero-point (zp = -bias/scale) so the
  fusable Subtract form stays algebraically w*s + min without OOM; extracted
  in-place into the backend buffer (use_bias sizing).
- Per-expert VIEW slicing on the non-static path + unique VIEW output names
  so the 8 same-named expert views no longer collide in the tensor_map.
- MoE token dim kept dynamic through SUM_ROWS/DIV/CLAMP routing-norm ops and
  the per-expert scale, so all RoPE concats stay dynamic (fixes decode
  Broadcast mismatch and the GPU in-place-concat KV-cache corruption).
- GET_ROWS batched-gather index broadcast tied to the data batch dim.
- Full-MoE path: keep the whole MoE (routing gather/softmax/normalization +
  expert matmuls) on one OV submodel instead of fragmenting at every MoE
  node. Auto-enabled for MoE models on the dynamic-shape devices (CPU/GPU),
  latched on MUL_MAT_ID at placement; NPU keeps the fragmented static path.
  The un-fragmented graph is numerically correct on both CPU and GPU and
  avoids the cross-submodel index corruption the fragmented path hit.
- op gating: exclude fused TOPK_MOE (uses ARGSORT routing), i64 CONCAT,
  borderline q4_1/q5_1 n=256 GET_ROWS, and oversized non-expert MUL_MAT_ID.
- Guard is_kvcache() against a null view_src on SCALE ops (gemma4 inp_scaled),
  which otherwise segfaults compute_llm_params.

Verified against ravi9/dev_backend_openvino: gemma4 26B MoE CPU greedy
"Paris is the capital of France"; gemma4 E2B on CPU/GPU; dense Llama-3.2-1B
on CPU+GPU byte-identical to baseline; test-backend-ops OPENVINO CPU
2622/2622 and GPU 2576/2576.
…-matmul placement)

The GPU full-MoE large-tmp exemption in mul_mat_id_requires_large_tmp used op names
(ffn_moe_gate_up/ffn_moe_down or an empty reserve-pass name) to keep the expert matmuls
on OpenVINO. At ubatch>=32 the ggml scheduler's reserve/measurement pass queries the
expert down-proj MUL_MAT_ID under an auto-assigned name 'node_<N>' (ggml_graph_add_node),
which matched neither, so the worst-case ~2GB temporary exceeded the 1 GiB cap and the op
was placed on ggml-CPU. It then read OpenVINO-produced routing ids across the OV<->CPU
split boundary -> garbage indexing -> hard segfault / 'i02 >= 0 && i02 < n_as' assert
during prefill (llama-bench -ub 32/64).

Fix: exempt structurally instead of by name. A genuine MoE-model expert matmul routes
over a 3D quantized expert-weight tensor (as->ne[2] > 1 && quantized) whose 'as' lives in
a WEIGHTS buffer; the scheduler's earliest reserve query runs before weights are bound and
reports an ANY buffer under an empty name, so treat that empty-name case as an expert
matmul too. test-backend-ops MUL_MAT_ID/_FUSION cases use named, non-weights 'as' tensors,
so they still hit the cap and stay on CPU as before.

Verified: 26B MoE GPU llama-bench no longer crashes at -ub 32 (pp64 1.95) / -ub 64;
test-backend-ops OPENVINO GPU MUL_MAT_ID 532/532 and MUL_MAT_ID_FUSION 9/9; default build
compiles.
Gemma-4 uses different attention head dimensions per layer type
(sliding_attention head_dim=256, full_attention global_head_dim=512),
so the single scalar model_params.head_size cannot describe every
layer. The stateful KV-cache reshape applied that one value to all
layers, corrupting the SWA layers and failing SDPA shape inference.

- ggml-decoder.cpp: derive the KV head size from each tensor's own
  combined dim (n_heads_kv * head_size) instead of the global scalar.
- permute.cpp: the stateful path carries hidden-state tensors in a
  rank-3 layout; drop the leading batch axis from the rank-4 perm when
  the input is rank-3 (Gemma-4's per-layer-embedding permute), so the
  Transpose order matches the input rank.

Stateful execution (GGML_OPENVINO_STATEFUL_EXECUTION) now runs and
produces correct output for Gemma-4 on CPU and GPU.
Decode on the OpenVINO GPU backend is weight-bandwidth bound. By
default the backend faithfully preserves the GGUF precision, which
keeps some weights at 8-bit where the native OpenVINO export uses
int4. This optional env flag re-quantizes those weights to symmetric
int4 (off by default; small accuracy reduction):

- MoE down-projection experts (Q5_1/Q8_0 -> u8) to int4 group-64.
  group-64 is required for the GatherMatmul fusion: it needs
  (m*k)/group divisible by the per-expert output rows N; the down
  expert has k=704 (704/64=11; 704/128 would break the fold).
- dense attention/FFN weights (Q6_K/Q5_K -> per-channel int8) to
  int4 group-128.

The int4 dequant chain still folds to GatherMatmulCompressed, so the
experts stay compressed. Measured on Gemma-4 26B-A4B (Arc B390):
prefill +13%, decode +18%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants