Gemma4 moe pr v3#221
Conversation
ef866cb to
5f01724
Compare
640d290 to
9f60666
Compare
0886e0f to
0b789a5
Compare
0b789a5 to
5fcf9ed
Compare
7a9b001 to
ab6cd7e
Compare
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.
35bd501 to
6288d94
Compare
| auto zero_point_f16 = std::make_shared<ov::op::v0::Constant>(zp); | ||
| auto w_zp = | ||
| std::make_shared<ov::op::v1::Subtract>(weights_f16, zero_point_f16, ov::op::AutoBroadcastType::NUMPY); | ||
| result = std::make_shared<ov::op::v1::Multiply>(w_zp, scales_f16, ov::op::AutoBroadcastType::NUMPY); |
There was a problem hiding this comment.
Does the Compressed Matmul kernel/pattern support (w_f16 - zp_f16) * scale as well, similar to GatherCompressed here? If so, can we remove the two paths for zp_int and bias_f16 and use zp_f16 uniformly?
There was a problem hiding this comment.
It makes sense but it may impact a larger scope of models. We can make it another PR maybe?
Integer zp was causing round error for moe expert weights. For MatMul, int zp seems to be working fine but it makes sense to try fp16 and see how it impacts accuracy and memory usage.
|
If I understand it correctly, this PR enables mulmat_id as GatherCompressed + Dequantize selected experts + fp Matmul in runtime. I found in ov there is also CompressedGatherMatmul which does the above steps in one op. But it seems like this op can only be constructed inside a complete MOE pattern. Should we investigate into this op? |
Yes, it totally makes sense to investigate on this. But GatherMatmul seems to be an internal op. We need to either introduce internal ops into frontend (this could be beneficial for Rope as well) or translate this into a pattern that plugin matchers can capture and generate GatherMatmul which we can do with public ops like GroupedMatmul. For the first option, I created a branch which shows significant performance gains on GPU: https://github.com/cavusmustafa/llama.cpp/tree/gathermatmul-pr Need to investigate the second option more so we can achieve the same result without internal op access. |
…-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.
PR: Gemma-4 26B MoE support on the llama.cpp OpenVINO backend
Branch:
gemma4_moe_pr_v3(github.com/cavusmustafa/llama.cpp)Target:
ravi9/llama.cpp:dev_backend_openvinoBase commit:
c6311c391(Merge PR #249, build doc change)Change: 1 commit, 10 files, +569 / −75, all under
ggml/src/ggml-openvino/1. What this PR does
Adds support for the Gemma-4 26B-A4B model to the OpenVINO backend. This is a
hybrid-FFN Mixture-of-Experts model that previously did not run on the backend.
After this PR it runs end-to-end on the OpenVINO CPU device (greedy
"Paris is the capital of" → "France"), and the smaller dense Gemma-4 E2B variant
runs on CPU, GPU, and NPU.
The model (why it needs special handling)
ffn_gate_up_exps.weight→ Q4_K, shape[k=2816, m=1408, n_expert=128]ffn_down_exps.weight→ Q5_1, shape[k=704, m=2816, n_expert=128](only 8 experts run per token) but not memory (all 128 must be resident).
2. The changes, grouped by problem solved
All changes live in
ggml/src/ggml-openvino/. The single commit bundles the workbelow (originally developed as separate commits; squashed for the rebase onto the
current dev base).
2.1 Memory: keep 3-D expert weights compressed (avoid OOM)
Files:
ggml-quants.cpp/.h,ggml-openvino-extra.cpp/.h,ggml-openvino.cpp,ggml-decoder.cpp,openvino/op/mul_mat_id.cppThe naive path decompresses all 128 experts to f32 at compile time (~100 GB → OOM).
The fix builds the expert dequant as a rank-2
[n_expert, m*k]subgraph(
Constant(u4)→Convert→Subtract→Multiply→Reshape) and gathers experts directly onthat rank-2 node, so the plugin folds Gather + dequant into a single
GatherCompressedop — weights stay compressed, only the selected experts aredecompressed at runtime (
mul_mat_id.cpp).ggml-quants.cpp): Q4_K/Q5_1 store an fpminthat isn't amultiple of
scale. Using an integer zero-point corrupts every weight. The fix keepsthe fusable
Subtractform but makes the zero-point an exact f16 valuezp = −min/scale— algebraicallyw·scale + min, yet stillGatherCompressed-matchable.ggml-openvino-extra.cpp,ggml-openvino.cpp): the extractedu4/u8 + f16 scale/zp are written into the backend buffer (not a heap duplicate),
keeping the compile peak down.
2.2 Correctness: per-expert VIEW handling
Files:
openvino/op/view.cpp,ggml-decoder.cppThe MoE output is 8
ggml_view_2dexpert planes summed by a chain of ADDs.Fixed: slice the single indexed expert axis and reshape (
view.cpp).(A second bug — 8 same-named expert views colliding in the name-keyed
tensor_map—was fixed during development with a unique VIEW name, but the current dev base now
disambiguates all same-named tensors via
get_tensor_ov_name()(#<hash_pos>suffix),which subsumes it, so this PR no longer carries that change.)
2.3 Dynamic token dim (decode + GPU prefill)
Files:
ggml-decoder.cpp,openvino/op/get_rows.cpp,openvino/op/view.cpp,openvino/utils.cppThe backend baked the captured prefill token count into the MoE graph as a static
dimension, which broke multi-token decode (Broadcast mismatch) and corrupted the GPU
KV-cache (static RoPE concats tripped an in-place-concat bug). Fix keeps the token dim
dynamic through the routing-norm ops (
SUM_ROWS/DIV/CLAMPnow tracked incompute_node_dynamic_dims), the per-expert scale, and the expert-plane view — so all60 RoPE concats stay dynamic.
get_rows.cppbroadcasts the batched-gather index batchdim from the data's
ShapeOf.2.4 GPU full-MoE path
Files:
ggml-openvino.cpp,ggml-openvino-extra.cpp/.hgpu_full_moe_enabled()): keeps the whole MoE on a singleOV submodel instead of fragmenting at every MoE node (fragmentation caused cross-submodel
rank-5 / garbage-index bugs). Fully auto-enabled for quant-MoE models on GPU by
latching on the
MUL_MAT_IDop at placement time (device==GPU && saw MUL_MAT_ID). No envvar or manual flag — CPU/NPU take the unchanged path (the gates it controls are GPU-guarded).
(A related fix — dodging OpenVINO's
rms_fusionpass, whose fused GPU kernel collapsed theMoE router RMSNorm and flipped expert selection — was developed here as a gated
Multiply(x,x)square, but the current dev base now usesMultiply(x,x)unconditionally,so this PR no longer carries a
rms_norm.cppchange.)2.5 op gating (
is_op_unsupported_caseinggml-openvino.cpp)Decline ops the backend can't run correctly, so the scheduler falls back to CPU:
selected_expertsVIEW guard — the model usesARGSORT-based routing, not the fused op.
tolerance), and oversized non-expert MUL_MAT_ID (the large-tmp cap is bypassed only
for the real
ffn_moe_gate_up/ffn_moe_downexperts, not arbitrary MUL_MAT_ID).2.6 Null-guard (needed for gemma4 to run at all)
File:
ggml-decoder.cpp—compute_llm_paramscalledis_kvcache(node->view_src, …)on a
GGML_OP_SCALEop without checkingview_src != nullptr. gemma4'sinp_scaledSCALE has a null
view_src→ segfault. Added the guard (latent bug in the base;surfaced by gemma4).
3. Verification
Built against OpenVINO 2026.2.1. Compared against
ravi9/dev_backend_openvino.No regressions in dense models or existing tooling — the MoE-specific gates are all
conditioned on MoE op names / the auto-detected full-MoE path, so non-MoE models take the
unchanged code path. This PR is deliberately scoped to MoE only: an unrelated
token_embdextraction tweak (a general dense-model quality change, plus itsGGML_OPENVINO_EMBD_REQUANTenv var) was removed from this branch so the diff containsnothing outside the MoE feature — dense models keep the base behavior byte-for-byte.
Pre-existing CI failures on the base (NOT introduced by this PR)
These fail identically on plain
dev_backend_openvinowith no MoE commit. This PRonly rebases on top of the current tip and inherits them; it changes none of the code
involved. They will fail on that base's CI for any PR until fixed upstream.
test-thread-safety(CI test 33) — fails withGGML OpenVINO backend std::exception: map::at/ "One or more threads failed". Bisected to the base's owncommit
49852ce11("remove the unique name in llama.cpp; add new ov name in ov bk"),which introduced
get_tensor_ov_name()(aggml_hash_findon the cgraphvisited_hash_set). Verified:4102b17e1(before49852ce11) → ✅ "All threads finished without errors"c6311c391(current base HEAD) → ❌map::atc6311c391+ MoE commit) → ❌ identicalmap::atNot concurrency-specific (also fails at
-np 1 -t 1); it's the multi-model/multi-contextpath hitting a
map::atlookup miss in the new hash-based naming. Our MoE commit does nottouch that code (the base's
get_tensor_ov_namereplaced this PR's earlier unique-viewnaming during rebase). Fix belongs in the base (
49852ce11), not here.GATED_DELTA_NETop test stalls the fulltest-backend-opssweep on the currentdev base (a base/qwen3next op, untouched by this PR — all MoE ops run and pass before it).
Known non-issues (hardware/environment, not this PR)
(~14 GB shared budget). The 26B monolithic OV compile peaks ~30 GB and the ~14.4 GB of
resident experts exceed the iGPU budget, so 26B-on-GPU and 26B-perplexity OOM on this
box. These are capacity limits, not code defects — the GPU code path is validated by
E2B-on-GPU and test-backend-ops GPU 2597/2597, and perplexity is validated on the 1B.
They pass on larger-RAM machines (prior 64 GB box).
4. Files changed
ggml-quants.cpp/.hggml-openvino-extra.cpp/.hgpu_full_moe_enabled()helperggml-openvino.cppggml-decoder.cppcreate_weight_node, dynamic-dim tracking (SUM_ROWS/DIV/CLAMP), null-guardopenvino/op/mul_mat_id.cppopenvino/op/view.cppopenvino/op/get_rows.cppopenvino/utils.cppprocess_view_input_new5. How to build & run
Env vars added by this PR: none. The GPU full-MoE path is fully auto-detected
(
device==GPU && the model has 3D quantized MoE experts); there is no new flag to set.