Skip to content

Commit 5fcf9ed

Browse files
committed
ggml-openvino: add Gemma-4 26B MoE support
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 GPU path auto-enabled for quant-MoE models (latched on MUL_MAT_ID at placement); keeps the whole MoE on one OV submodel and dodges the GPU rms_fusion bug (Multiply-square instead of Power(x,2)) for the router norm. - 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/NPU; dense Llama-3.2-1B (cli/bench/perplexity/server) on CPU+GPU byte-identical to baseline; test-backend-ops OPENVINO CPU 2622/2622 and GPU 2597/2597.
1 parent c6311c3 commit 5fcf9ed

10 files changed

Lines changed: 569 additions & 75 deletions

File tree

ggml/src/ggml-openvino/ggml-decoder.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
566566
if (node->op == GGML_OP_GATED_DELTA_NET) {
567567
model_params.state_size = node->src[0]->ne[0];
568568
}
569-
if (node->op == GGML_OP_SCALE && is_kvcache(node->view_src, nullptr)) {
569+
if (node->op == GGML_OP_SCALE && node->view_src != nullptr && is_kvcache(node->view_src, nullptr)) {
570570
compute_params.cache_rs_reset_len = ggml_nelements(node) / node->view_src->ne[0];
571571
compute_params.cache_rs_reset_idx = node->src[0]->view_offs / node->view_src->ne[0];
572572
}
@@ -979,6 +979,30 @@ std::shared_ptr<ov::Node> GgmlOvDecoder::create_weight_node(ggml_tensor * tensor
979979
return weight_node;
980980
}
981981

982+
// 3D quantized MoE expert weights [k, m, n_expert]: flatten to a rank-2
983+
// [n_expert, m*k] tensor and build the dequant subgraph with use_bias=true (the
984+
// exact f16 zero-point form). This is the path hit by test-backend-ops and the
985+
// host-buffer load; the backend-buffer path builds the same node in set_tensor.
986+
// translate_mul_mat_id gathers experts on axis 0 of this node and splits m*k.
987+
if (ggml_is_quantized(tensor->type) && tensor->ne[2] > 1) {
988+
GGML_ASSERT(tensor->ne[3] == 1 && "4D quantized expert weights are not supported");
989+
GGML_ASSERT(ggml_is_contiguous(tensor) && "expert weights must be contiguous to flatten");
990+
const int64_t n_expert = tensor->ne[2];
991+
const int64_t m = tensor->ne[1];
992+
const int64_t k = tensor->ne[0];
993+
ggml_tensor flat_tensor = *tensor;
994+
flat_tensor.ne[0] = m * k;
995+
flat_tensor.ne[1] = n_expert;
996+
flat_tensor.ne[2] = 1;
997+
flat_tensor.ne[3] = 1;
998+
flat_tensor.nb[1] = ggml_row_size(tensor->type, m * k);
999+
flat_tensor.nb[2] = ggml_nbytes(tensor);
1000+
flat_tensor.nb[3] = ggml_nbytes(tensor);
1001+
OvWeight flat_weight = process_weight_tensor(&flat_tensor, tensor->data, nullptr, /*use_bias=*/true);
1002+
flat_weight.weight_node->set_friendly_name(tensor->name);
1003+
return flat_weight.weight_node;
1004+
}
1005+
9821006
// There are three cases where we need to create a new weight node:
9831007
// 1. weights are in openvino_host_buffer. Weight loading to host buffer will not trigger backend_buffer_set_tensor
9841008
// 2. weights are in cpu/cpu_mapped buffer. On token_embd.weight goes to case 1 or 2, depending on whether mmap or direct_io is used
@@ -1694,8 +1718,22 @@ void GgmlOvDecoder::compute_node_dynamic_dims() {
16941718
case GGML_OP_DIAG:
16951719
case GGML_OP_TRI:
16961720
case GGML_OP_REPEAT:
1721+
// Shape-preserving elementwise ops: the dynamic dim is unchanged from src[0].
1722+
// DIV/CLAMP are used in the MoE routing-weight normalization
1723+
// (sum_rows -> clamp -> div). If they are left untracked here the dynamic
1724+
// (token) dim is lost there, the captured prefill token count gets baked into
1725+
// the downstream reshapes, and every decoder layer after layer 0 turns static
1726+
// (which then triggers the GPU in-place-concat KV-cache corruption).
1727+
case GGML_OP_DIV:
1728+
case GGML_OP_CLAMP:
16971729
m_node_dynamic_dims[node] = m_node_dynamic_dims[node->src[0]];
16981730
break;
1731+
case GGML_OP_SUM_ROWS:
1732+
// SUM_ROWS reduces ggml axis 0 to size 1 and preserves all other axes, so the
1733+
// dynamic dim is preserved unless it was axis 0 (then it is summed away).
1734+
m_node_dynamic_dims[node] =
1735+
(m_node_dynamic_dims[node->src[0]] == 0) ? -1 : m_node_dynamic_dims[node->src[0]];
1736+
break;
16991737
case GGML_OP_MUL_MAT_ID:
17001738
case GGML_OP_SOLVE_TRI:
17011739
m_node_dynamic_dims[node] = m_node_dynamic_dims[node->src[1]];

ggml/src/ggml-openvino/ggml-openvino-extra.cpp

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,28 @@ bool ggml_openvino_is_npu() {
173173
return ggml_openvino_get_device_config().is_npu;
174174
}
175175

176+
// Latched true once a MUL_MAT_ID op is seen during op placement; see header. Plain
177+
// non-atomic bool: placement runs single-threaded before the multi-threaded compute
178+
// that reads it, and the flag only ever transitions false->true (idempotent).
179+
static bool g_has_moe_expert_weights = false;
180+
181+
void ggml_openvino_note_moe_expert_weight() {
182+
g_has_moe_expert_weights = true;
183+
}
184+
185+
bool ggml_openvino_has_moe_expert_weights() {
186+
return g_has_moe_expert_weights;
187+
}
188+
189+
bool ggml_openvino_gpu_full_moe_enabled() {
190+
// Keep the whole MoE on one OV submodel when running a quant-MoE model on GPU.
191+
// Auto-detected: a MoE model is recognized when a 3D quantized expert weight has
192+
// been seen (see ggml_openvino_note_moe_expert_weight). On CPU/NPU the per-node
193+
// gates are no-ops anyway (they are GPU-guarded), so this stays OFF there and
194+
// preserves the existing behavior exactly.
195+
return ggml_openvino_get_device_name() == "GPU" && ggml_openvino_has_moe_expert_weights();
196+
}
197+
176198
// Get the remote context for the current device (returns empty optional for CPU)
177199
std::optional<ov::RemoteContext> ggml_openvino_get_remote_context() {
178200
return ggml_openvino_get_device_config().remote_context;
@@ -252,9 +274,12 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
252274
return layout;
253275
}
254276

255-
// Most quantized weights use the existing 2D extraction path. MXFP4 also
256-
// appears as 3D expert weights for MUL_MAT_ID, so allow that type through.
257-
if (tensor->type != GGML_TYPE_MXFP4 && (tensor->ne[2] != 1 || tensor->ne[3] != 1)) {
277+
// Handle 2D weight tensors, and 3D MoE expert weights [k, m, n_expert] which
278+
// are treated as a flattened 2D [n_expert*m, k] tensor (each row is quantized
279+
// independently along k, so the block layout is identical when flattened). This
280+
// covers both our quantized gemma4 experts (Q4_K/Q5_1) and MXFP4 3D experts,
281+
// which are handled by the dedicated block just below.
282+
if (tensor->ne[3] != 1) {
258283
return layout;
259284
}
260285

@@ -390,6 +415,10 @@ ggml_openvino_extracted_layout ggml_openvino_get_extracted_layout(const ggml_ten
390415
// For symmetric quantization, no zp needed (weights stored as signed)
391416
if (layout.is_symmetric) {
392417
layout.zp_size = 0;
418+
} else if (use_bias) {
419+
// use_bias stores the zero-point/bias as F16 (2 bytes/block), not a packed
420+
// integer. Must size the buffer accordingly so the extracted data fits in-place.
421+
layout.zp_size = n_blocks * sizeof(uint16_t);
393422
} else {
394423
layout.zp_size = layout.is_u4 ? ((n_blocks + 1) / 2) : n_blocks;
395424
}

ggml/src/ggml-openvino/ggml-openvino-extra.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ int ggml_openvino_getenv_int(const char * var, int default_value = 0);
9999
// Check if running on NPU
100100
bool ggml_openvino_is_npu();
101101

102+
// MoE detection. ggml_openvino_note_moe_expert_weight() latches a process-global flag
103+
// that ggml_openvino_has_moe_expert_weights() reports. It is called from supports_op()
104+
// the first time a GGML_OP_MUL_MAT_ID (the expert-routed matmul) is seen, which is the
105+
// defining op of a MoE model. The latch is set at op-placement time (not weight load):
106+
// the scheduler queries op placement before the expert weights are streamed in, and it
107+
// makes multiple placement passes, so the first pass that encounters MUL_MAT_ID sets the
108+
// flag and subsequent passes converge on the full-MoE layout. This lets the backend
109+
// recognize "this is a MoE model" without any architecture name.
110+
void ggml_openvino_note_moe_expert_weight();
111+
bool ggml_openvino_has_moe_expert_weights();
112+
113+
// Whether to keep the whole MoE on one OV submodel instead of fragmenting at every
114+
// MoE node (see the per-node "force to CPU on GPU" gates). Auto-detected: ON when
115+
// running on GPU and the model has 3D quantized expert weights (a quant-MoE model),
116+
// OFF otherwise. CPU/NPU behavior is unchanged (the gates it controls are GPU-guarded).
117+
bool ggml_openvino_gpu_full_moe_enabled();
118+
102119
// Get requantization type for a tensor type (returns nullopt if no requant needed)
103120
std::optional<ExtraQuantType> ggml_openvino_get_requant_type(const ggml_tensor * tensor, bool no_requant = false);
104121

0 commit comments

Comments
 (0)