Skip to content

[ICS] ggml-openvino: hybrid-attention MoE models (qwen35moe, e.g. Ornith 1.0 35B) fail to compute #299

Description

@haarika-madaka

ggml-org#27206

ggml-openvino: split-boundary tensors break compute on hybrid MoE models (three defects)

Target repo: ggml-org/llama.cpp (OpenVINO backend, ggml/src/ggml-openvino/)
Status: DRAFT (2026-08-16, TASK-30). gh is NOT authenticated on this machine —
filing needs Sander's GitHub. Local fix verified in the kernel project's
patches/ (llamacpp-openvino-split-shape-fixes.patch, applied on top of
llamacpp-openvino-deterministic-tensor-names.patch at commit 885c5bb).

Related existing issues (searched 2026-08-16)

Title

ggml-openvino: hybrid-attention MoE models (qwen35moe, e.g. Ornith 1.0 35B) fail to compute — three defects in split-boundary tensor handling (reshape view-src extra sharing, boundary view/reshape outputs never emitted, shape-blind compiled-model cache)

Symptom chain (one model, three defects in the same split path)

Model: qwen35moe 35B (Ornith 1.0 35B, 20G GGUF, 40 layers, 256 experts,
GatedDeltaNet conv-state layers), -ngl 99 -dev OPENVINO0, build 10428
(commit 885c5bb), OpenVINO 2026.4.0-git, intel_gpu plugin, iGPU sharing
system DRAM. ornith:9b (same architecture) computes fine — the 35B graph gets
split across OV/CPU subgraphs (MoE experts run on CPU via q4_K_8x8 repack) and
only the split path is affected.

Without any fix, the first warmup compute fails at input binding:

[GPU] The tensor size is not equal to model, can't set input tensor with index: 7,
because model input (shape=[1,2,8,1]) and tensor (shape=[1,1,2,8]) are incompatible

After fixing that, the compute proceeds further and aborts in the CPU MoE
repack with garbage expert ids; after fixing that, the compiled-model cache
reuses a wrong-shape blob. Each defect is described below.

Defect 1 — RESHAPE results share the source tensor's ov::Tensor (wrong shape at split boundary)

ggml_backend_openvino_buffer_init_tensor (ggml-openvino.cpp) shares
view_src->extra for ANY tensor with view_src != nullptr, including RESHAPE
results (ggml_new_tensor_impl sets view_src for reshape results too).
When a RESHAPE result crosses a graph-split boundary and becomes a model
input, the input tensor's extra is the SOURCE tensor's ov::Tensor (source
shape), so binding rejects the model's baked (reshape) shape:

  • Model input ffn_moe_weights_norm-0 (reshaped): baked [1,2,8,1] (OV order,
    ggml ne=[1,8,2,1]); runtime tensor bound via the shared extra: [1,1,2,8]
    (ggml ne=[8,2,1,1] — the 2D div result [8,N]).

Fix: only share the extra when tensor->op == GGML_OP_VIEW; RESHAPE falls
through and gets a fresh extra with its own shape (data is a memory alias, so
no copy cost). One-line change, no behavior change for real views.

Defect 2 — split-boundary VIEW/RESHAPE results are never emitted as model outputs

GgmlOvDecoder::compute_model_outputs (ggml-decoder.cpp) skips VIEW/RESHAPE
nodes unconditionally, and suppresses parents whose only consumers are inside
the graph (input_use_count == use_count). For a scheduler split graph whose
boundary tensor IS a view/reshape (e.g. the MoE top-k view
ffn_moe_topk-N = view of the argsort output ffn_moe_argsort-N, consumed by
a CPU-backend MUL_MAT_ID), the data is therefore never written to the ggml
buffer. The consumer split copies uninitialized/stale buffer memory:

GGML_ASSERT(i02 >= 0 && i02 < n_as) failed   (ggml-cpu/repack.cpp, MUL_MAT_ID ids)
ids_name=CPU#ffn_moe_topk-1#0  src0_name=blk.1.ffn_gate_exps.weight
first_ids: -1073594368 -1129906176 ...   (stale F16 data read as int32)

Fix: for VIEW/RESHAPE nodes whose data is consumed outside the split
(use_count > uses inside the graph — use_counts are shared with the full
graph), emit the PARENT tensor (view_src, resolved through nested views) as a
model output. The plugin writes the parent's full data into the shared buffer;
consumers of the view read the same memory. Whole-model (non-split) graphs are
unaffected (no external consumers).

Defect 3 — compiled-model cache ignores input shapes; split models are compiled static

Split-graph boundary inputs are compiled with the concrete token count baked
in (get_graph_input_shape only keeps the tracked dynamic dim when
m_model_is_splitted, which is only set with the GGML_OPENVINO_ENABLE_FALLBACK
env). The frontend compiled-model cache key
(ggml_openvino_model_fingerprint, model-cache.cpp) hashes topology + weights

  • config but NOT shapes, so a blob compiled for n_tokens=1 is imported for an
    n_tokens=2 decode and the static shape rejects the bind:
BIND-FAIL input[1] name=attn_post_norm-2 runtime_shape=[1,1,2,2048] model_shape=[1,1,1,2048]

Fix: fold the shapes of the statically-compiled input leafs into the
fingerprint (tokens/positions/masks stay dynamic and are excluded; weights are
already hashed; caches/scalars have constant shapes). Whole-model graphs keep
a single reusable blob; split graphs get one blob per token count. Cache
format version bumped (v2 → v5 in the local patch: v3 added the shape scan,
v4 switched to a graph-input scan because split subgraphs have an empty leafs
array, v5 resolves VIEW chains like compute_model_inputs()) so stale blobs MISS.

Note: making the boundary inputs actually dynamic instead (removing the
m_model_is_splitted gate) was also tried — the intel_gpu plugin then
segfaults in the IGC kernel JIT (SIGSEGV in libopencl-clang.so.17 during
compile_model, first conv-state split). That may be a separate plugin bug
worth its own report.

Environment

  • llama.cpp build 10428 (885c5bb), OpenVINO 2026.4.0-git, intel_gpu plugin,
    X1 Carbon Gen 14 (PTL-H484), 32G RAM, iGPU shares system DRAM.
  • GatedDeltaNet conv-state warnings (cannot determine dynamic dim for RESHAPE node conv_states_reshaped-N etc.) appear for BOTH the working 9B and the
    broken 35B and are NOT the trigger — they are noise; the trigger is the
    split-boundary handling above.
  • Cache-independent: Defects 1 and 2 reproduce without the cache env.

Local fix

patches/llamacpp-openvino-split-shape-fixes.patch (kernel project, applied on
top of the TASK-28 deterministic-tensor-names patch):

  • ggml-openvino.cpp: extra sharing only for GGML_OP_VIEW.
  • ggml-decoder.cpp: boundary view/reshape parents emitted as model outputs.
  • model-cache.cpp: fingerprint includes static input leaf shapes.
  • utils.cpp: cache format version v5; input-binding debug/diagnostics guarded
    by env GGML_OPENVINO_DEBUG_INPUT (shape-only print, no tensor data access).

Verification — 2026-08-16, TASK-30 wrap-up (end-to-end, fixed binary)

Fixed binary built at 885c5bb + the two patches; scoped cgroup
(MemoryHigh=29G/MemoryMax=55G/SwapMax=16G, CPUQuota=400%), -lv 5,
-ngl 99 -dev OPENVINO0 --no-cache-prompt, env GGML_OPENVINO_DEVICE=GPU
GGML_OPENVINO_MEMORY_OPTIMIZE=1, model = Ornith 1.0 35B (20G GGUF), -c 16384.

  • Cold compile (21:21 run): completes — 574 blobs written — and the first
    warmup compute SUCCEEDS. Full 8-token completion: stopped by limit, n_gen = 8, n_predict = 8 (tokens 2, <think>, Here, 's, a,
    thinking...). No ov::Exception, no Compute error, no repack assert
    (grep count 0).
  • Warm load (evening acceptance run): /health ok in 37s; 89 blob HITs;
    direct curl /completion → HTTP 200
    {"content":"2\n\n<think>\nHere's a thinking","stop":true, "tokens_predicted":8,"tokens_evaluated":4} and the server log confirms
    stopped by limit, n_gen = 8, n_predict = 8.
  • Memory profile (common_memory_breakdown_print, OPENVINO0): model
    1452 MiB retained + context 571 + compute 60 on the 31586 MiB device pool;
    41/41 layers offloaded to GPU. No runaway.
  • Regression check: ornith:9b and qwen3.5:4b still compute fine.
  • Patch hygiene: both patches git apply --check cleanly (exit 0) on a
    fresh clone at 885c5bb; the combined diff is byte-identical to the
    verified build workdir.

Non-blocking observations for maintainers:

  • A minority of blob imports fail at plugin compile with could not create a primitive descriptor for the reduction primitive (intel_gpu plugin,
    plugin.cpp:80) where the fresh-compile path succeeds — backend falls back to
    recompile (deterministic per blob; warm loads still ~37s to /health).
  • Per-shape blob fan-out: split graphs compile static shapes, so each
    distinct token count exercised gets its own blob (by design of defect-3's
    fix).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions