Repository navigation
ggml-openvino: Update PERMUTE and CONCAT dynamic dim axis selection, Keep fp32 precision for sum-of-squares - #358
Merged
cavusmustafa merged 59 commits intoNov 2, 2026
Conversation
swathiananth
marked this pull request as draft
October 21, 2026 21:34
ravi9
force-pushed
the
dev_backend_openvino
branch
from
October 24, 2026 06:11
c5cd082 to
947be59
Compare
When the SWA cache size matches the full cache size, identify SWA layers by the number of layers using each mask instead of cache size; the larger group is SWA, and a tie leaves the layers unclassified.
Give SWA KV cache index tensors unique input names (a _swa suffix, like the masks) to prevent them from being merged with the full-attention cache indices.
Two attention masks do not imply SWA: DeepSeek sparse attention uses two full-attention masks with the same name. When cache extents match, classify the larger mask group as SWA only once its mask has fewer keys per row than the other mask. Keep the classification per KV cache buffer so new sequences reuse the same graph, and include swa_layers when checking graph reuse.
Waiting for the window to appear in the mask caused a mid-sequence graph recompile (~9 s for Gemma-4 at -c 1024 after position 512). SWA classification already gives each mask group its own mask, KV write indices, and attention size, which is correct whether or not the mask is a sliding window. Stateful mask rebuild recovers the window from the mask, or the causal mask for full attention. Classify the larger of two mask groups from the first graph and drop the per-KV-buffer bookkeeping.
Add a TODO above the equal-extent fallback to document that the larger mask group is currently assumed to be the SWA group. Since stateless graphs only require the two groups to remain distinct, this heuristic may only be necessary for stateful execution and could potentially be restricted to that path.
Dynamic CPU/GPU graphs previously passed n_seq_active and attention_size(_swa) as separate inputs. Derive these values directly from the KQ mask shape using ShapeOf + Gather inside the model. This allows the GPU plugin to infer the Q reshape and KV slice shapes from shape inference rather than runtime input values. Q's head size also becomes static, enabling the fused SDPA kernel. GGML_OPENVINO_DISABLE_SHAPE_FROM_MASK=1 restores the previous separate-input behavior. The fused SDPA path also requires two supporting changes: - Clamp the GPU mask to -30000. A key block that is entirely -inf for a row produces NaNs in the fused kernel. - Restate the head count and head size after the KV slice. The plugin loses all slice dimensions when the end index is dynamic, causing SDPA to fall back to the reference kernel. Additional graph and quantization improvements: - Skip the identity reshape of rows in SET_ROWS. - Share a single Split for one-element view slices in dynamic GPU graphs (e.g. Gemma-4 per-layer embeddings), replacing one StridedSlice per layer. - Add GGML_OPENVINO_REQUANT_EMBD=native (off by default) to preserve the file's native quantization for token_embd/output instead of requantizing to Q8_0_C. - Include both environment settings in the graph and model cache keys and document them.
The dynamic GPU path reused a Split output for any single-element view by deriving the chunk index from offset / stride. This produced incorrect results for views that were not chunk-aligned or whose remaining dimensions did not preserve the source strides, as seen in test-backend-ops CONT with use_view_slice. Restrict Split reuse to cases where the view exactly matches a split chunk, meaning the offset is aligned to the split stride and the remaining non-unit dimensions preserve the original source strides. Gemma-4 per-layer embedding slices continue to use the shared Split path.
Initialize the newly added `shape_source` and `shape_axis` fields in the `ModelExtraInputInfo` brace initializer used by `add_extra_inputs()`. Without initializers for these fields, GCC emits `-Wmissing-field-initializers`, causing CI to fail with `LLAMA_FATAL_WARNINGS`.
The option identifies embedding and output weights by their tensor names, which are defined by core llama.cpp. Remove it from this PR; preserving the file quantization of these weights will be addressed in a separate change that identifies them by their roles in the graph.
The manual GQA attention path reshaped Q, K, and V to 5-D so that query heads sharing a KV head could broadcast against it. The GPU plugin cannot fuse this graph: it materializes the attention scores and copies the transposed K and V caches for every layer. Instead, stack the query heads for each KV head along the sequence axis (Q [B, H, S, D] -> [B, H_kv, factor * S, D]) and tile the mask accordingly. This keeps the graph 4-D from the MatMuls through the Softmax, allowing the GPU plugin to fuse the attention into its SDPA kernel and fold the K and V transposes into the fused operation. Clamp the mask to -30000, matching the SDPA path, because the fused kernel can return NaN when an entire block is masked with -inf. Models where the K or V head size differs from the Q head size now use the SDPA path, since the manual path assumes one head size.
In stateless GPU mode, bind the entire KV cache as a `[1, ctx, n_heads_kv, head_size]` tensor, with one token per row, instead of binding the first `n_kv` rows as a `[1, 1, ctx, n_heads_kv * head_size]` tensor. Write cache updates with `ScatterUpdate` along the token axis, and read the cache through `VariadicSplit` and `Transpose`. This allows the GPU plugin's `StatelessKVFusion` to replace cache reads and writes with in-place `StatelessKV` nodes, avoiding full-cache copies on every decode step and repeated memory pool allocations as the bound cache size grows. Set `GGML_OPENVINO_DISABLE_KV_4D=1` to retain the previous layout.
With -fa off, llama.cpp stores the V cache transposed, with value elements as rows and tokens along each row. The KV slicing code assumes one token per row and binds the wrong data when n_kv < kv_size. Detect transposed caches with GgmlOvDecoder::is_kvcache_token_rows() and skip slicing for them. For grouped-query attention, avoid repeating K and V for each query head. Stack the query heads for each KV head along the row dimension and let SOFT_MAX apply the scale, mask, and softmax so the GPU plugin can fuse the operation with the MatMuls. Read only the current layer's V-cache rows when layers use different cache sizes. Reject -fa off in stateful mode with a clear error and document that it requires stateless mode.
With `-fa off`, the graph writes the V cache in transposed form, which is not supported by the stateful KV cache. Detect this pattern in `compute_llm_params()` as a `SET_ROWS` into a KV cache through a `[1, n]` reshape. When stateful execution is enabled, log a warning and fall back to stateless execution instead of failing graph conversion. Update the `-fa off` note in `OPENVINO.md` to document this behavior. Co-authored-by: Suhas Somashekar <suhas.somashekar@intel.com>
With -fa off, the 4-D KV cache layout introduced in ravi9#344 breaks MUL_MAT attention for Phi-3.5 mini and Phi-4 mini, causing oneDNN to fail with "could not construct a memory descriptor using strides". Preserve the [1, 1, ctx, n_heads_kv * head_size] layout when the graph writes a transposed V cache.
Q5_0 weights are requantized to Q4_0, but test-backend-ops builds naive graphs, where requantization is skipped. Q5_0 then reached the normal weight extraction, which had no Q5_0 case, so every Q5_0 MUL_MAT and GET_ROWS op test failed with "Unsupported quantized type: q5_0". Extract Q5_0 as symmetric i8 weights (q - 16) with one f16 scale per 32 weights, the same layout as Q8_0. Models are unchanged: they still requantize Q5_0 to Q4_0.
The NPU GatedDeltaNet kernel writes its output in FP16 and flushes values below 6.1e-5 to zero.
1. Add envs desc into doc 2. Refine the code with removing duplicated env
Reduce host overhead outside `infer()` from 0.5–1.1 ms to 0.14–0.38 ms per token on Panther Lake. - Reuse KV cache tensors and skip redundant port bindings. - Use persistent USM host tensors for small GPU inputs and outputs. - Defer graph-wide consumer lookup until after shape checks. - Return model and compute params by const reference. - Preserve tensor extras when memory, shape and type are unchanged.
Add GGML_OPENVINO_SELF_CONTAINED_RELEASE_MMAP_PAGES as an explicit opt-in for self-contained compiled-model cache hits. Before importing a valid self-contained blob, recursively walk the graph to find unique external GGUF mmap-backed weight buffers and drop their resident pages while preserving the mapping address and tensor pointers. On Linux use page-aligned madvise(MADV_DONTNEED). On Windows, where there is no reliable per-range mapped-file equivalent, call VirtualUnlock for the identified range and trim the current process working set with SetProcessWorkingSetSize(-1, -1). Pages required later can fault back on demand. Keep the behavior disabled by default and document the physical-memory versus commit-memory tradeoff. Profiling reports the submitted GGUF range and model import duration. Validated with ReleaseOV llama-bench/llama-simple build and pp3072+tg512 warm mmap lazy import. With page release enabled, PrefillPeakWS dropped from about 5758 MiB to 3053 MiB while throughput remained comparable; PeakCommit stayed about 3331 MiB.
…eights bank When set, prefill and decode self-contained blobs use one shared NPUW_WEIGHTS_BANK name (ggml-sc-shared-<pfp>-<dfp>) instead of separate per-phase banks. Since both decoders are built from the same model_weights map, NPUW dedups them into a single in-process bank; on warm import the identical (uid,tensor) pairs are content-verified and reused (patched NPUW weights_bank). Cuts resident compiled-weight footprint ~2GB (pp3072 warm: In-Use 7954->5871 MiB, WS 7891->5765) with prefill throughput unchanged.
The shared weights bank (GGML_OPENVINO_NPU_SHARED_BANK) supersedes both: it deduplicates the prefill/decode compiled-weight copy directly, so lazy per-phase import and GGUF-page release no longer reduce steady memory. Remove the two env vars, the self-contained lazy-import branches (defaulting to the eager path that imports both blobs), the release_external_weight_pages helper, and the now-unused ggml_openvino_buffer_is_external / _release_external_pages buffer APIs.
In strict self-contained warm-mmap mode weights come via buffer_from_host_ptr, so every alloc_buffer is graph scratch/KV. The large prefill compute buffer is never touched on the host by the on-device fused OV model, yet the NPU L0 host tensor (create_host_tensor + memset) makes all ~2.4 GiB resident. For buffers >= 1 GiB in this mode, reserve demand-zero pages (no L0, no memset) so they stay non-resident; KV/small buffers keep the L0 host tensor for zero-copy import. pp3072 warm mmap + shared bank: prefill peak WS 5765 -> 3314 MiB (-43%), In-Use delta ~5940 -> ~3232 MiB; prefill 1256.7 -> 1252.9 t/s and decode 28.27 -> 27.96 t/s (both flat). Commit charge unchanged (demand-paged).
The static graph built the sliding-window KQ mask at the full context length, while the windowed layers bind their (smaller) window KV cache. With gemma-4 at -c 4096 -ub 1024 the window cache is 1536 cells, so SDPA shape inference failed ([1,1,1024,4096] mask vs [1,8,1536,256] K/V) and every prompt errored out. With GGML_OPENVINO_NPU_KV_SLICE the prefill mask was meant to use attention_size_swa, but the check matched "swa" in the ggml tensor name, and llama.cpp names both masks "attn_inp_kq_mask"; the windowed layers got the full-attention sliced length instead ([1,1,1024,256] vs [1,8,1024,256]). Identify the sliding-window mask by tensor identity (is_swa_mask) and size it to ctx_per_seq_swa, both in the graph input shape and in the runtime mask padding.
make_dirs() mkdir'ed every path prefix, including the drive root "D:", which fails with EACCES (not EEXIST) on Windows. Any absolute cache path therefore logged "could not create model cache dir ... caching disabled" and silently ran without GGML_OPENVINO_COMPILED_MODEL_CACHE_DIR. Skip the drive root.
…red code Mechanical refactor, no behavior change (step 1 of the NPU/MoE architecture plan): * openvino/pipelines/: TranslateSession::apply_transformations() now registers common passes, then the device's passes, then the execution-mode passes (stateful KV, static shapes), selected by a per-graph GraphTraits. The GPU MoE fusions and the GPU eltwise-rank workaround move to pipelines/gpu.cpp; CPU and NPU pipelines are empty placeholders for device lowerings. Pass order is unchanged. * npu/npu_config.*: the NPU (NPUW) compile config built at backend init, the NPUW strip for cacheless graphs and the per-graph static prefill/decode variants (HFA prefill, decode NPUW_UNFOLD_IREQS) move out of ggml-openvino-extra.cpp and utils.cpp. * npu/npu_quant_policy.*: the NPU requantization rules (requant policy, token_embd Q6_K, keep-Q4_0) move out of ggml_openvino_get_requant_type(). Verified identical output before/after on OV-CPU, GPU and NPU (gemma-4 E4B).
KV-cache slicing (bind the KV cache, and size the KQ masks, at the attended
length instead of the full context) existed twice: on by default for dynamic
graphs (GGML_OPENVINO_DISABLE_KV_SLICE to turn it off) and opt-in for static
prefill (GGML_OPENVINO_NPU_KV_SLICE). The static mode is not NPU-specific; it
applies to any static graph, including CPU/GPU with GGML_OPENVINO_FORCE_STATIC.
Describe it as one mechanism with two modes behind GGML_OPENVINO_KV_SLICE
(unset = each mode's default, 0 = off, 1 = on) and two predicates,
ggml_openvino_{dynamic,static}_kv_slice_enabled(), used by the KV binding,
the static graph shapes, the mask padding and the cache keys. The two old
variables remain as deprecated aliases.
…inputs dev_backend_openvino added shape_source/shape_axis to ModelExtraInputInfo; the HFA past-KV and mask initializers still listed four fields, which GCC rejects under -Werror=missing-field-initializers.
…ersion - Convert the static prefill and decode models one after the other, both before either is compiled. The decoders share the weight nodes; converting on two threads crashed intermittently inside libopenvino, and converting decode after the prefill compile gave wrong output on Bonsai-8B. - Compile prefill and decode in parallel only with GGML_OPENVINO_NPU_PARALLEL_COMPILE=1, since it raises the host memory peak. - Unfold NPUW function calls on the decode graph only with GGML_OPENVINO_NPU_DECODE_UNFOLD=1. The unfold default gave wrong output on gemma-4-E4B and slowed decode on several small models. - Stream NPU weight requantization only with GGML_OPENVINO_REDUCE_COMPILE_MEM or GGML_OPENVINO_MEMORY_OPTIMIZE, as on the other devices. - Fix a -Werror=missing-field-initializers build error in utils.cpp. - Declare ggml_openvino_get_npu_requant_type() only in npu/npu_quant_policy.h. - Docs: document the new switches, deprecate stateful execution, show stateless-only results in the validated models table, and update the validation system.
ravi9
force-pushed
the
dev_backend_openvino
branch
from
October 26, 2026 21:41
674af4a to
9e01c2f
Compare
swathiananth
force-pushed
the
Gemma_3n_E2B_E4B_Qwen3_VL_fixes
branch
from
October 26, 2026 22:48
865a7a8 to
9e01c2f
Compare
…nvert. Prevents perf degrade.
ravi9
force-pushed
the
dev_backend_openvino
branch
2 times, most recently
from
October 29, 2026 17:37
be796d8 to
25e3135
Compare
Author
|
Date: 2026-10-29 Setup
Command: llama.cpp rounds the context up to Test Result:
|
swathiananth
marked this pull request as ready for review
October 30, 2026 00:30
Collaborator
|
LGTM, needs rebase |
…_3n_E2B_E4B_Qwen3_VL_fixes To get my branch upto date.
Author
Have merged with the remote. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Fixes Gemma 3n (E2B and E4B) on OpenVINO GPU, which gave NaN, blank or looping output on prompts of about 700 tokens or more. There are two parts:
1. New pass: KeepSumSquareFP32
Files: keep_sum_square_fp32.h, keep_sum_square_fp32.cpp, registered in pipelines/gpu.cpp.
Problem:
The GPU plugin runs fp32 graphs at fp16 inference precision. Gemma 3n's gaussian_topk (first 10 layers) computes a standard deviation over the 8192-wide FFN gate: sqrt(sum((x - mean)^2) / (n-1)).
What the pass does:
2. ggml-decoder.cpp:
Dynamic-dimension tracking PERMUTE. In ggml, op_params[i] is the destination axis of source dim i.
Uses op_params[dynamic_dim_idx] directly with a bounds check.
CONCAT- First takes a source's dynamic dim. If both sources have one and they disagree, it keeps src[0]'s and logs a warning. It falls back to the axis that grew (concat axis) only if neither source is dynamic.
Requirements