Skip to content

Commit 35bd501

Browse files
committed
ggml-openvino: fix Gemma-4 26B MoE crashes on the GPU device
1 parent 5fcf9ed commit 35bd501

2 files changed

Lines changed: 33 additions & 8 deletions

File tree

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -961,13 +961,24 @@ static bool mul_mat_id_requires_large_tmp(const ggml_tensor * op) {
961961
}
962962
// On the full-MoE GPU path the real gemma4 expert matmuls (ffn_moe_gate_up /
963963
// ffn_moe_down) legitimately exceed this cap and are handled correctly, so
964-
// exempt only those named ops. Other MUL_MAT_ID ops (e.g. the large-n
965-
// MUL_MAT_ID_FUSION test cases) still hit the cap and stay on CPU, since the
966-
// GPU translation produces wrong results for those oversized temporaries.
967-
if (gpu_full_moe_enabled() &&
968-
(strncmp(op->name, "ffn_moe_gate_up", sizeof("ffn_moe_gate_up") - 1) == 0 ||
969-
strncmp(op->name, "ffn_moe_down", sizeof("ffn_moe_down") - 1) == 0)) {
970-
return false;
964+
// exempt those named ops. The scheduler also queries these same expert matmuls
965+
// during its reserve/measurement pass with an EMPTY name (and a worst-case
966+
// full-batch ids->ne[1] that blows past the tmp cap); if we send them to CPU there
967+
// the placement can stick for the real graph, and the expert matmul then runs on
968+
// CPU reading OpenVINO-produced routing ids across the split boundary — which are
969+
// garbage (crash in ggml-cpu mul_mat_id: "i02 >= 0 && i02 < n_as"). So for the
970+
// unnamed reserve-pass query, fall back to a STRUCTURAL match: a genuine gemma4
971+
// expert matmul routes over a 3D quantized expert-weight tensor
972+
// (as->ne[2] == n_expert > 1). Real graph ops always have a name, so the named
973+
// MUL_MAT_ID_FUSION test cases (op named "out") are unaffected and still hit the
974+
// cap / stay on CPU as before.
975+
if (gpu_full_moe_enabled()) {
976+
const bool name_match = strncmp(op->name, "ffn_moe_gate_up", sizeof("ffn_moe_gate_up") - 1) == 0 ||
977+
strncmp(op->name, "ffn_moe_down", sizeof("ffn_moe_down") - 1) == 0;
978+
const bool unnamed_expert_matmul = op->name[0] == '\0' && as->ne[2] > 1 && ggml_is_quantized(as->type);
979+
if (name_match || unnamed_expert_matmul) {
980+
return false;
981+
}
971982
}
972983

973984
size_t tmp_elems = 1;

ggml/src/ggml-openvino/openvino/utils.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,17 @@ ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int inp
302302
// re-slice/flatten the already-resolved view (a Reshape to the full flattened
303303
// n_expert_used*n_embd tail, which then conflicts with the single-plane input). Treat a
304304
// dynamic-vs-dynamic axis as matching so the already-resolved view is reused as-is.
305+
//
306+
// A third case matters for split-model MoE fragments: translate_view resolves the
307+
// expert-plane view against the fragment's INPUT parameter. When the graph is split
308+
// the token axis of that parameter may already be concrete (static n_tokens) even
309+
// though get_view_input_ov_shape() still reports it as dynamic (-1). The resolved
310+
// view is then static [1,1,n_tokens,n_embd] while `expected` is [1,1,?,n_embd].
311+
// An "expected dynamic, actual static" axis is a valid concretization of the SAME
312+
// resolved view, so treat it as matching too. Falling through to process_single_view
313+
// here would re-slice/re-flatten the already-resolved single-plane view against the
314+
// recorded (multi-plane) source strides and emit a constant-target Reshape whose baked
315+
// dims no longer divide the concretized input -> "dimensions do not evenly divide".
305316
auto expected_ov_shape = context.get_view_input_ov_shape(input_index, 0);
306317
auto actual_shape = input.get_partial_shape();
307318
if (expected_ov_shape.rank().is_static() && actual_shape.rank().is_static() &&
@@ -311,7 +322,10 @@ ov::Output<ov::Node> process_view_input_new(const NodeContext & context, int inp
311322
const bool both_dynamic = expected_ov_shape[i].is_dynamic() && actual_shape[i].is_dynamic();
312323
const bool both_static_equal = expected_ov_shape[i].is_static() && actual_shape[i].is_static() &&
313324
expected_ov_shape[i] == actual_shape[i];
314-
if (!both_dynamic && !both_static_equal) {
325+
// expected dynamic, actual static: the resolved view already carries the
326+
// concrete size for this fragment; reuse it rather than re-materializing.
327+
const bool expected_dyn_actual_static = expected_ov_shape[i].is_dynamic() && actual_shape[i].is_static();
328+
if (!both_dynamic && !both_static_equal && !expected_dyn_actual_static) {
315329
shapes_match = false;
316330
break;
317331
}

0 commit comments

Comments
 (0)