Skip to content

ggml-openvino: Drop leading pad entries for missing dimensions - #360

Merged
cavusmustafa merged 55 commits into
ravi9:dev_backend_openvinofrom
swathiananth:llamacpp_OV_pad_issue
Oct 27, 2026
Merged

cavusmustafa merged 55 commits into
ravi9:dev_backend_openvinofrom
swathiananth:llamacpp_OV_pad_issue

Conversation

@swathiananth

@swathiananth swathiananth commented Oct 23, 2026 •

Copy link
Copy Markdown

Overview

Stateful execution drops leading dims, so PAD inputs can have rank < 4 while pads were always built with 4 entries, failing OpenVINO shape validation ("length of pads_begin mismatches with rank of input").
Trim the dropped leading pads for both regular and circular PAD.

Additional information

Summary

With GGML_OPENVINO_STATEFUL_EXECUTION=1, Qwen3-VL models fail on the very first graph evaluation on the OpenVINO backend. OpenVINO shape inference rejects a Pad node because the pad vectors have 4 entries while the padded tensor has rank 3.

The fix is in ggml/src/ggml-openvino/openvino/op/pad.cpp. When the PAD input has rank < 4, the translator now drops the leading pad entries for the missing dimensions (after checking they are zero), so the pad vectors always match the input
rank. This applies to both regular (constant) PAD and circular PAD.

Symptom

Command (Windows, Intel GPU):

set GGML_OPENVINO_DEVICE=GPU
set GGML_OPENVINO_STATEFUL_EXECUTION=1
llama-completion -m Qwen3VL-4B-Instruct-Q4_K_M.gguf -f prompt_1024.txt -c 1382 -n 128 -st -ngl -1 -no-cnv --ignore-eos

Output:

E GGML OpenVINO backend ov::Exception: Check 'pads_begin_coord->size() == static_cast<size_t>(arg_rank_len)'
  failed at src\core\shape_inference\include\pad_shape_inference.hpp:62:
While validating node 'opset1::Pad Pad_2600 (opset8::Gather Gather_2596_node_0#n0[0]:f32[1,?,2560],
  opset1::Constant Constant_2597[0]:i64[4], opset1::Constant Constant_2598[0]:i64[4],
  opset1::Constant Constant_2599[0]:f32[]) -> (dynamic[...])' with friendly_name 'Pad_2600':
length of pads_begin mismatches with rank of input, expect 3, but got 4

E graph_compute: ggml_backend_sched_graph_compute_async failed with error -1
E process_ubatch: failed to compute graph, compute status: -1
E cmn  common_promp: failed to eval

The model loads, but prompt evaluation fails and no tokens are generated (exit code 1).

Root cause

Where the PAD comes from

Qwen3-VL uses deepstack layers, so its input embedding width (n_embd_inp) is larger than the model width (n_embd):

// src/llama-hparams.cpp
n_embd_inp = n_embd + n_embd * n_deepstack_layers;

When the graph is built from text tokens, the token embeddings (width n_embd =
2560) are zero-padded along ne0 up to n_embd_inp:

// src/llama-graph.cpp
if (n_embd_inp != n_embd) {
    cur = ggml_pad(ctx0, cur, hparams.n_embd_inp() - n_embd, 0, 0, 0);
}

So the PAD input is the GET_ROWS result on the token embedding table, which the OpenVINO frontend translates into a Gather.
That is the Gather_2596 ... f32[1,?,2560] in the error message.

Why the ranks disagree

GGML tensors are always 4-D. A GGML PAD carries 8 pad values in op_params (begin/end for ne0..ne3), and the translator mapped them to OpenVINO axis order (OpenVINO axis 0 = GGML ne3):

pads_begin = {pads[6], pads[4], pads[2], pads[0]};   // always 4 entries
pads_end   = {pads[7], pads[5], pads[3], pads[1]};

This silently assumed the OpenVINO input also had rank 4. In stateful execution
the frontend drops leading singleton dimensions, so the embedding Gather produces
a rank-3 tensor [1, n_tokens, 2560]. ov::op::v1::Pad requires
len(pads_begin) == len(pads_end) == rank(input), so validation fails with
"expect 3, but got 4".

Circular PAD (translate_circular_pad) had the same assumption: it indexed
pads_begin[axis] for axis in [0, rank). With a rank-3 input that would apply
GGML ne3's pads to OpenVINO axis 0, ne2's to axis 1, and so on, which is the
wrong axis rather than an error.

The fix

Dropped dimensions are always the leading ones (GGML ne3, then ne2, ...), so the pads for the axes the input still has are the trailing entries of the 4-element vectors. The fix aligns the pads to the trailing input axes.

Regular (constant) PAD: translate_pad

std::vector<int64_t> pads_begin = {pads[6], pads[4], pads[2], pads[0]};
std::vector<int64_t> pads_end = {pads[7], pads[5], pads[3], pads[1]};

// Stateful models drop leading dims, so the input can have rank < 4.
const auto input_rank = input.get_partial_shape().rank();
if (context.is_stateful() && input_rank.is_static() && input_rank.get_length() < 4) {
    const size_t dropped = 4 - static_cast<size_t>(input_rank.get_length());
    for (size_t i = 0; i < dropped; ++i) {
        FRONT_END_OP_CONVERSION_CHECK(pads_begin[i] == 0 && pads_end[i] == 0,
                                      "PAD: cannot pad a dimension dropped from the input rank");
    }
    pads_begin.erase(pads_begin.begin(), pads_begin.begin() + dropped);
    pads_end.erase(pads_end.begin(), pads_end.begin() + dropped);
}
  • The rank is read from the actual OpenVINO input (input.get_partial_shape()),
    not from the GGML shape, because that is what Pad validates against.
  • If the rank is 4, or dynamic, behavior is unchanged.
  • The vectors became non-const so they can be trimmed.

For the failing node: input rank 3, dropped = 1; pads
{0,0,0,P} / {0,0,0,0} (P = n_embd_inp - n_embd) become {0,0,P} / {0,0,0},
which pads the last axis (GGML ne0) as intended.

Circular PAD: translate_circular_pad

// input can have rank < 4 (leading dims dropped); align pads to the trailing input axes
const size_t dropped = stateful && input_shape.size() < pads_begin.size() ? pads_begin.size() - input_shape.size() : 0;

for (size_t i = 0; i < dropped; ++i) {
    FRONT_END_CHECK_IMPLEMENTED(pads_begin[i] == 0 && pads_end[i] == 0,
                                "Circular PAD cannot pad a dimension dropped from the input rank");
}

for (size_t axis = 0; axis < input_shape.size(); ++axis) {
    ...
    const int64_t pad_begin = pads_begin[dropped + axis];
    const int64_t pad_end = pads_end[dropped + axis];

Same idea: skip dropped leading entries so each OpenVINO axis gets the pads of
its matching GGML dimension.

Why the zero check is safe

A dimension can only be dropped if it is a singleton, and a PAD that grows that dimension would make the output rank-4 again, which the frontend would not have squeezed. So in valid graphs the dropped pads are always zero. If that ever stops being true, the check fails with a clear conversion error instead of silently padding the wrong axis.

##Testing

Qwen3-VL-4B-Instruct Q4_K_M: Testing Summary

These results are from Intel Arc Pro B70 (32 GB) on Windows 11 with OpenVINO 2026.4, running on the GPU. The base build is commit 52e2420.

1. The Pad bug (stateful execution)

llama-completion -f prompt_1024.txt -c 1382 -n 128 -st -ngl -1 -no-cnv --ignore-eos, with GGML_OPENVINO_STATEFUL_EXECUTION=1:

Metric Base 52e2420 With fix
Result FAILED: Pad rank mismatch (expect 3, got 4), exit code 1 PASSED, exit code 0
Prompt eval n/a 879 tokens, 889.94 ms (987.71 t/s)
Generation n/a 127 runs, 27,057.87 ms (213.05 ms/token, 4.69 t/s)
Load time n/a 11,892.47 ms
Output n/a Coherent English (Response.txt)

Perplexity of the generated response (-c 64 -b 64 -np 1, 2 chunks):

Mode Chunk 1 Chunk 2 Final
Stateful off 4.6273 3.3406 PPL 3.3406 ± 0.70649
Stateful on 4.6240 crashed (CL_OUT_OF_RESOURCES) n/a

2. Stateless execution

Completion (greedy, -fa on): exit 0. The output reads "The sky appears blue because of a phenomenon called Rayleigh scattering, which occurs when sunlight interacts wi…" (truncated in the log).

Decode (tg128, tokens/s, median ± half the spread):

Depth 0 512 1024 4096 8192
t/s 95.28 ±2% 93.69 ±4% 87.10 ±3% 60.58 ±2% 45.71 ±2%
Peak GPU memory 7,823 MB (depths 0 to 4096) 17,313 MB

Prefill (tokens/s):

pp512 pp1024 pp4096 pp8192
t/s 10,187.30 ±2% 8,160.02 ±5% 6,002.50 ±25% FAILED (OOM: Can not allocate 8388608 bytes for USM Device)
Peak GPU memory 25,013 MB (pp512 to pp4096) 32,272 MB

Accuracy (GPU against the CPU backend; ctx 1024; perplexity on wikitext-2 over 16 chunks):

Mean KLD Same top token Perplexity
0.056882 90.729% 9.4028

3. The pp8192 failure: base vs. fix (same session, back-to-back)

Build pp8192 Peak GPU memory
Base 52e2420 2,488.26 t/s ±57% 32,621 MB
With fix 2,730.04 t/s ±46% 32,477 MB

At 8K the model uses about 32 GB on both builds, the card's full capacity, so the test passes or fails by chance. When it does pass, throughput drops sharply, from about 6,000 t/s at pp4096 to about 2,500 at pp8192, with ±50% spread. That points to memory spilling past the GPU's dedicated memory.

Conclusions

  • The fix works: it fixes Qwen3-VL in stateful mode. The base build can't run at all; the fix build runs and produces coherent output with low perplexity.
  • No regressions: in default mode, Qwen3-VL accuracy (KLD 0.057, 90.7% same top token, PPL 9.40) is in line with the other models.
  • The 8K failure predates the fix: the pp8192 failure comes from excessive memory use, about 25 GB already at pp4096 for a 2.5 GB model, and it looks the same with and without the fix.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes. AI was used for analysis and testing.

@swathiananth swathiananth changed the title Llamacpp ov pad issue Llamacpp OpenVINO backend pad issue Oct 23, 2026
@swathiananth swathiananth changed the title Llamacpp OpenVINO backend pad issue ggml-openvino: pad issue Oct 23, 2026
@swathiananth swathiananth changed the title ggml-openvino: pad issue ggml-openvino: Drop leading pad entries for missing dimensions Oct 23, 2026
osabnis and others added 25 commits October 23, 2026 21:06
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.
zhaixuejun1993 and others added 19 commits October 23, 2026 21:06
Extend the GGML_OPENVINO_RELEASE_WEIGHTS safety check to the static graph cache-miss path. Once host weight buffers have been released, any later compile would read invalid/zeroed host pages and bake the wrong constants into a new OpenVINO model.

The dynamic path already rejected this situation. Static execution can still miss the decoder cache when graph shapes or model parameters change, so it needs the same guard before erasing cached infer requests and rebuilding prefill/decode models.

This is a correctness and diagnosability change rather than a throughput optimization. It preserves the memory-saving release-weights mode for stable graph shapes, but turns unsupported recompilation into an explicit abort instead of silently producing a corrupted compiled model.
Add GGML_OPENVINO_NPU_KEEP_Q4_0 as an opt-in escape hatch for NPU weight handling. When enabled for native Q4_0 tensors, the NPU requant path returns nullopt and keeps the original Q4_0 extraction instead of regrouping the tensor through the selected NPU requant policy.

The motivation is to avoid unnecessary work for weights that are already stored as 4-bit values. The default NPU policy maps quantized weights to layouts such as Q4_0_128, which can reduce scale metadata but requires round-tripping Q4_0 through the requantization path. Keeping Q4_0 native can save that conversion work and preserve the original group-32 packing.

Keep the option experimental and disabled by default. Current NPU driver/plugin stacks can compile the resulting group-32 graph, but inference may hang or fail with ZE_RESULT_ERROR_DEVICE_LOST. The default behavior therefore continues to use the safer NPU requant policy; this flag is mainly useful for debugging driver behavior, measuring the true cost of regrouping, and testing future NPU stacks.

Trade-off: native Q4_0 avoids regrouping overhead and keeps the original quantization blocks, but it gives up the more regular group-128 layout expected by the default NPU compressed-weight path and may be unstable on current hardware/software combinations.
Compile the static prefill and decode models with separate OpenVINO config maps so the decode graph can opt into NPUW_UNFOLD_IREQS without forcing the same lowering on prefill. The build_static_model helper now receives the config map explicitly, and the decode path gets a copy of the base config with NPUW_UNFOLD_IREQS=YES when the user has not already set it.

The performance target is NPU token generation. Decode executes a long stream of single-token infers, where folded NPUW function-call dispatch overhead becomes visible in every token. Unfolding the NPUW calls into separate infer requests removes that repeated dispatch cost and measured about +20% token-generation throughput on the tested stack, matching the behavior seen in OpenVINO GenAI.

Prefill keeps the original folded config. Prompt processing is a batched workload dominated by larger matmuls, and the folded form is faster there while also using less memory. Splitting the config lets prefill and decode use the lowering that matches their workload shape instead of treating NPUW_UNFOLD_IREQS as a single global compromise.

Trade-off: unfolding can increase memory and request-management overhead, so the automatic default is limited to the NPU decode model. If a user explicitly provides NPUW_UNFOLD_IREQS through the named env alias or GGML_OPENVINO_NPU_CONFIG, that value is respected because the decode override is only applied when the property is absent.
Collect the remaining small cleanups from the NPU tuning series. This reorders the NPU KV-slice documentation and environment-variable cache entry next to the other NPU opt-in toggles, adds a short header comment for the centralized NPU helper declarations, and makes the static prefill attention-size assignment in the decoder a little more explicit.

There is no intended behavior change in this commit. The purpose is to make the final source layout match the split commits that introduced the NPU tuning knobs, so future readers can find the related flags and helpers in one place without mixing that mechanical cleanup into the functional performance commits.
Move the default NPU_COMPILER_TYPE=DRIVER setting into the NPU compile_config initializer so the default NPU plugin options are declared in one place.

Route GGML_OPENVINO_NPU_COMPILE_CONFIG and GGML_OPENVINO_NPU_COMPILER_TYPE through the same set_compile_option_from_env helper used by the other named NPU compile option aliases. This keeps the legacy NPU_COMPILE_CONFIG mapping to NPU_COMPILATION_MODE_PARAMS intact while making all optional env-to-compile_config overrides follow the same non-empty-value rule.

GGML_OPENVINO_NPU_CONFIG is still parsed last, so explicit comma-separated KEY=VALUE overrides continue to have the highest precedence over both defaults and named aliases.
Default the streaming requantization path on for NPU so the compile-time F32 dequant transient (~1-2 GB for token_embd) is capped without an env flag. Output is byte-identical to full materialization; GGML_OPENVINO_REDUCE_COMPILE_MEM / MEMORY_OPTIMIZE still override (set to 0 to force off). Kept the g_nonov_weight_cache opt-in to avoid holding token_embd resident (which regressed decode steady memory). pp1024_d0: PrefillPeakPrivWS 5974->4664 MiB (-22%), decode steady and pp/tg throughput unchanged.
Add an opt-in embedded-weight cache mode for the static NPU backend. GGML_OPENVINO_SELF_CONTAINED_BLOB=1 exports separate prefill and decode compiled models with ENABLE_WEIGHTLESS=false, uses isolated NPUW weight banks, validates raw GGUF weight manifests, binds imported ports by deterministic frontend order, and serializes the large NPUW compile/export/import operations.

Preserve raw model bytes during cold cache construction by requantizing into separately owned OV tensors instead of rewriting the backend model buffer. This keeps later graph fingerprints and manifests stable when llama-bench creates independent prompt and generation contexts.

Add strict GGML_OPENVINO_SELF_CONTAINED_MMAP=1 warm-import mode. The backend exposes a non-owning buffer_from_host_ptr wrapper over the GGUF mmap, uses raw tensor allocation sizes, avoids allocating/copying the private OpenVINO model-weight buffer, and stages the few mmap-backed runtime constants into small importable NPU host tensors. Invalid or missing cache entries fail clearly instead of trying to requantize into read-only mapped storage.

The normal path and existing weightless MODEL_PTR cache remain unchanged when the new flags are unset. Verified with phi-4-mini: chunk512 pp3072/tg512 cold 1123.10/27.60 t/s and warm mmap 1124.72/27.16 t/s; warm mmap removes roughly 3.9 GiB peak commit in the measured prefill workload. Self-contained blobs consume about 8.34 GiB for the separate pp and tg contexts.
Add GGML_OPENVINO_SELF_CONTAINED_LAZY_IMPORT to make the low-working-set strict mmap path opt in instead of changing the default warm mmap behavior.

When the flag is set with SELF_CONTAINED_BLOB=1 and SELF_CONTAINED_MMAP=1, the static NPU path imports only the current phase blob: prefill imports the prefill blob, decode imports the decode blob later. This avoids the pair-shaped shared compiled-model cache for the lazy path and caches only the current phase infer request.

The regular warm mmap path remains eager and continues to prepare both prefill and decode compiled models. Non-mmap self-contained builds remain serial, and the regular non-self-contained path keeps the existing parallel build behavior.

Also register the new environment variable, document the memory/latency tradeoff, and add profiling-only import timing around model-cache imports.

Validated with ReleaseOV build: cmake --build build/ReleaseOV --target llama-bench llama-simple --config Release. Lazy warm mmap run measured PrefillPeakWS around 5759 MiB versus the eager warm mmap path around 7897 MiB.
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
ravi9 force-pushed the dev_backend_openvino branch from c5cd082 to 947be59 Compare October 24, 2026 06:11
@swathiananth
swathiananth force-pushed the llamacpp_OV_pad_issue branch from b02f359 to fe4eff7 Compare October 26, 2026 20:00
@swathiananth
swathiananth marked this pull request as ready for review October 26, 2026 20:00
@ravi9
ravi9 force-pushed the dev_backend_openvino branch from 674af4a to 9e01c2f Compare October 26, 2026 21:41
@cavusmustafa
cavusmustafa merged commit db3959b into ravi9:dev_backend_openvino Oct 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants