Skip to content

Merge dev_backend_openvino into npu-support-mmBERT, resolve conflicts - #1

Draft
zhaixuejun1993 with Copilot wants to merge 16 commits into
npu-support-mmBERTfrom
copilot/npu-support-mmbert
Draft

Merge dev_backend_openvino into npu-support-mmBERT, resolve conflicts#1
zhaixuejun1993 with Copilot wants to merge 16 commits into
npu-support-mmBERTfrom
copilot/npu-support-mmbert

Conversation

Copilot AI commented Sep 8, 2026

Copy link
Copy Markdown

Overview

PR ravi9#306 (npu-support-mmBERT -> dev_backend_openvino) had drifted from its base and become unmergeable (mergeable_state=dirty). This updates the branch with the latest dev_backend_openvino and resolves the two resulting conflicts in the OpenVINO backend, keeping the mmBERT NPU support intact.

  • ggml-openvino-extra.cpp: one-sided conflict — upstream added a GPU quantization workaround (grouped 8-bit Q5_1/Q8_0 MoE experts -> 4-bit) that this branch never touched. Took upstream's version as-is.
  • ggml-openvino.cpp ROPE op-support gating: both branches modified the same check for different reasons. This branch relaxed it to accept packed-QKV RoPE views (needed for mmBERT); upstream replaced it with a check on whether the ROPE op's own result is an in-place view of a non-contiguous tensor. These conditions don't overlap, so both are now checked rather than picking one:
if (op->src[0]->op == GGML_OP_VIEW) {
    // ... existing same_shape / packed_qkv check (mmBERT)
}
if (op->view_src != nullptr && !ggml_is_contiguous(op->src[0])) {
    return {false, "ROPE on VIEW / non-contiguous input is not supported"};
}

Verified the merge preserves all mmBERT-specific functionality: cacheless-attention detection from the mask, single full-sequence prefill without NPUW/decode graph, static mask/output-index/mean-pooling shapes, opset6 MVN-based norm translation, and RoPE sin/cos caching keyed on RoPE params + optional frequency-factor input.

Additional information

OpenVINO SDK is not available in this environment, so a full compiled build could not be run here; conflict resolution was checked for structural/syntactic correctness (no leftover markers, balanced braces) and via diff against the base branch to confirm intended logic was preserved.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES - AI agent performed the branch merge and conflict resolution as directed.

As an AI agent, I remind the PR author that they remain fully responsible for reviewing and understanding every change in this PR before merging. Please review AGENTS.md and CONTRIBUTING.md for this project's policies on AI-assisted contributions.

mostafafaheem and others added 15 commits September 3, 2026 05:21
…state

The stateful path seeds its KV state from ggml's cache when the decode position
is ahead of what the state holds. That only works when ggml's cache is a plain
prefix, where cell i holds position i. A sliding-window layer keeps just the last
n_swa positions and drops the rest, so past the window cell i no longer holds
position i and the seeded state is wrong.

Slicing the state to the decode position also had no bounds check, so a position
past the end surfaced as a bare ov::Exception from the ROI constructor
(llama_decode ret = -3, with no reason given at default verbosity).

Refuse both cases with a clear message instead, and refuse on the compile path
too, where a new model starts with an empty state and so can only serve a
sequence from its beginning. Reproducible with llama-bench -d, which restores a
saved sequence state rather than recomputing the depth prefill.

Assisted-by: Claude Opus 5
The stateful path reinterprets ggml's KV buffer [1, 1, seq, n_heads_kv * head_size]
as [1, seq, n_heads_kv, head_size]. The head size is already taken from the
tensor's own combined dim, because gemma-4 varies it per layer type, but the head
count still came from a model-level scalar that compute_llm_params() overwrites
per attention node, so it ended up holding whatever the last layer said.

gemma-4 varies the head count per layer too: 12B has 8 x 256 sliding layers and
1 x 512 full layers, 31B has 16 x 256 and 4 x 512. So 40 of 12B's 48 layers were
split as 1 x 2048 instead of 8 x 256, and attention read the state with the wrong
head split - both models decoded garbage on CPU and GPU. E2B is unaffected, its
head count is 1 everywhere.

Record the count per layer instead and look it up by the cache_k_l<N> leaf name.
Key it by layer, not by layer type: the sliding/full classification comes from
cache extents, which tie at a small -c, while the head count does not.

The stateful state trim now derives its sequence axis per state for the same
reason, since pass::KVStateSeqAxis matches per state on the head count.

Assisted-by: Claude Opus 5
pass::KVStateSeqAxis was limited to states with a single KV head, where moving
the sequence axis from dim 1 to dim 2 is a pure metadata change. The limit was
also based on a measurement showing no gain for a multi-head model, but that was
taken at depth 0, which is the one depth where this change does nothing.

With several heads the pass does more than move metadata: it drops the reader
side transpose of the whole accumulated state, which the graph otherwise redoes
every token at a cost that grows with the context length, and replaces it with a
transpose of the single new row. Measured on GPU, tg128, alternating arms:
gemma-4-12B 6.27 -> 9.11 t/s at depth 8192 (stateless is 7.69, so stateful now
wins at depth instead of losing), Llama-3.2-1B 47.8 -> 59.6 t/s. Both are within
noise at depth 0, which is why the earlier check saw nothing.

The state refill needs the rows copied rather than reinterpreted now: ggml stores
[seq][n_heads_kv * head_size], and a relayout state with several heads is a
different element order. Without that, a refill would seed wrong data - it is
reachable today through llama-bench -d.

Assisted-by: Claude Opus 5
    Packed QKV views used by mmBERT were rejected by the ROPE support check. This split Q/K RoPE onto CPU, prevented cacheless attention detection, and sent fragmented encoder graphs through the decoder-oriented NPUW path.

    Accept packed QKV RoPE views, detect cacheless attention from its mask, and run these models as a single full-sequence prefill without NPUW or a decode graph. Also provide static mask, output index, and mean-pooling shapes and inputs.
    Replace the decomposed mean/variance normalization graph with an opset6 MVN operation. This preserves the GGML epsilon placement while allowing OpenVINO plugins to compile normalization as one operation with fewer intermediate tensors.

    Cache RoPE sine and cosine outputs in the graph-wide tensor map. Build the cache key from all RoPE parameters and the optional frequency-factor input so compatible Q/K and layer nodes share one subgraph without mixing different RoPE configurations.

    Expose NodeContext::put_shared() to publish translator-created outputs for graph-level reuse.
…ilot/npu-support-mmbert

# Conflicts:
#	ggml/src/ggml-openvino/ggml-openvino-extra.cpp
#	ggml/src/ggml-openvino/ggml-openvino.cpp

Co-authored-by: zhaixuejun1993 <52686861+zhaixuejun1993@users.noreply.github.com>
Copilot AI changed the title [WIP] Resolve merge conflicts for npu-support-mmBERT Merge dev_backend_openvino into npu-support-mmBERT, resolve conflicts Sep 8, 2026
Copilot AI requested a review from zhaixuejun1993 September 8, 2026 06:43
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.

4 participants