Skip to content

Commit 1de2864

Browse files
committed
ggml-openvino: one KV-slice switch for dynamic and static graphs
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.
1 parent f95c358 commit 1de2864

6 files changed

Lines changed: 51 additions & 20 deletions

File tree

‎docs/backend/OPENVINO.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,10 +728,11 @@ Boolean flags follow a uniform convention: set to a **positive integer** (e.g. `
728728
| `GGML_OPENVINO_NPU_REQUANT_POLICY`| String | `group-128` | NPU-only weight requantization layout. Use `group-128` for `Q4_0_128` or experimental `channel-wise` for `Q4_0_C`. Any other value fails during model loading. |
729729
| `GGML_OPENVINO_NPU_FAST_MASK` | Boolean | `0` | NPU-only. Build the static prefill attention mask directly into the input tensor, skipping an intermediate host allocation + full-tensor copy per chunk. Numerically identical to the default path. |
730730
| `GGML_OPENVINO_NPU_L0_HOST_TENSORS`| Boolean | `0` | NPU-only. Back the KV-cache / compute buffers with Level Zero host memory so the plugin imports them without an extra host-to-device copy each infer, cutting prefill KV I/O time. Falls back to plain host memory if the L0 allocation is unavailable. |
731-
| `GGML_OPENVINO_NPU_KV_SLICE` | Boolean | `0` | NPU-only. Compile static prefill graphs and bind prefill KV-cache tensors at the active attention length instead of the full context capacity. Decode retains its fixed full-context graph. The prefill model is recompiled when the aligned attention length changes. |
731+
| `GGML_OPENVINO_KV_SLICE` | Boolean | per mode | Bind the KV cache, and size the KQ masks, at the attended length instead of the full context. Dynamic graphs (CPU/GPU): a view over the cache, no recompilation; on by default. Static graphs (NPU, or `GGML_OPENVINO_FORCE_STATIC`): the prefill graph is compiled at the aligned attended length and recompiled when it changes, decode keeps its full-context graph; off by default. Unset = each mode's default, `0` = off in both, `1` = on in both. |
732+
| `GGML_OPENVINO_NPU_KV_SLICE` | Boolean | `0` | Deprecated alias: `1` turns static KV slicing on (`GGML_OPENVINO_KV_SLICE` wins when set). Not NPU-specific; applies to any static graph. |
732733
| `GGML_OPENVINO_STATEFUL_EXECUTION`| Boolean | `0` | Keep KV and supported recurrent caches inside the model. Single-slot CPU/GPU execution only. |
733734
| `GGML_OPENVINO_DISABLE_CACHE` | Boolean | `0` | Disable the in-process compiled-model / decoder cache (cache is on by default). Set to `1` to disable. |
734-
| `GGML_OPENVINO_DISABLE_KV_SLICE` | Boolean | `0` | Disable the KV-cache input-tensor slicing optimization (slicing is on by default on CPU/GPU). Set to `1` to disable. |
735+
| `GGML_OPENVINO_DISABLE_KV_SLICE` | Boolean | `0` | Deprecated alias: `1` turns dynamic KV slicing off (`GGML_OPENVINO_KV_SLICE` wins when set). |
735736
| `GGML_OPENVINO_ENABLE_KV_4D` | Boolean | `0` | GPU, stateless: bind the KV cache as `[1, ctx, n_heads_kv, head_size]` instead of `[1, 1, ctx, n_heads_kv * head_size]`, so the GPU plugin's `StatelessKVFusion` can update and read the cache in place. **Off by default**: it needs an OpenVINO build with openvino#37569 (2026.5 or a recent nightly) and is much slower on older releases (e.g. 2026.4.1). Set to a positive integer to enable. |
736737
| `GGML_OPENVINO_KV_SCATTER_ELEMENTS` | Boolean | `0` | Use `ScatterElementsUpdate` instead of `ScatterUpdate` for non-stateful single-row KV writes. Intended for NPU decode paths where `ScatterUpdate` copies the full destination tensor to update one row. |
737738
| `GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT` | Boolean | `0` | Disable the stateful KV-state sequence-axis relayout (relayout is on by default). It moves the KV state sequence axis from dim 1 to dim 2, so the GPU plugin can append new tokens in place instead of copying the whole state every token, and the reader side no longer transposes the whole accumulated state. Set to `1` to disable. |

‎ggml/src/ggml-openvino/ggml-decoder.cpp‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
10251025
compute_params.mask_name = mask_name;
10261026
}
10271027
if (is_static) {
1028-
const bool slice_prefill = ggml_openvino_npu_kv_slice_enabled() && compute_params.input_len > 1;
1028+
const bool slice_prefill = ggml_openvino_static_kv_slice_enabled() && compute_params.input_len > 1;
10291029
if (!slice_prefill) {
10301030
compute_params.attention_size = model_params.ctx_per_seq;
10311031
}
@@ -1048,7 +1048,7 @@ std::pair<ModelParams, ComputeParams> GgmlOvDecoder::compute_llm_params(ggml_cgr
10481048
node->src[0]->src[0]->op == GGML_OP_VIEW) {
10491049
compute_params.attention_size = node->ne[0];
10501050
compute_params.mask_name.clear();
1051-
if (is_static && !(ggml_openvino_npu_kv_slice_enabled() && compute_params.input_len > 1)) {
1051+
if (is_static && !(ggml_openvino_static_kv_slice_enabled() && compute_params.input_len > 1)) {
10521052
compute_params.attention_size = model_params.ctx_per_seq;
10531053
}
10541054
}
@@ -1204,7 +1204,7 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
12041204
// Both masks are named "attn_inp_kq_mask", so tell them apart by tensor identity.
12051205
const bool swa_mask = is_swa_mask(input);
12061206
int attention_size = swa_mask ? m_model_params.ctx_per_seq_swa * m_model_params.n_seq : m_model_params.ctx;
1207-
if (m_is_prefill && ggml_openvino_npu_kv_slice_enabled()) {
1207+
if (m_is_prefill && ggml_openvino_static_kv_slice_enabled()) {
12081208
attention_size = swa_mask ? m_compute_params.attention_size_swa : m_compute_params.attention_size;
12091209
}
12101210
input_shape = ov::PartialShape{1, 1, m_is_prefill ? m_prefill_chunk_size : 1, attention_size};
@@ -1230,7 +1230,7 @@ ov::PartialShape GgmlOvDecoder::get_graph_input_shape(const ggml_tensor * op,
12301230
if (!m_is_static && !is_flat_kv) {
12311231
// do not fix ctx size to make llama-bench work across test params
12321232
input_shape[2] = -1;
1233-
} else if (m_is_prefill && ggml_openvino_npu_kv_slice_enabled()) {
1233+
} else if (m_is_prefill && ggml_openvino_static_kv_slice_enabled()) {
12341234
const auto layer = extract_layer_from_name(name);
12351235
if (layer.has_value() && !is_swa_layer(*layer) && m_compute_params.attention_size > 0 &&
12361236
m_compute_params.attention_size < input_shape[2].get_length()) {

‎ggml/src/ggml-openvino/ggml-openvino-extra.cpp‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ void ggml_openvino_device_config::init() {
160160
"GGML_OPENVINO_GDN_Q_SCALE",
161161
"GGML_OPENVINO_NPU_L0_HOST_TENSORS",
162162
"GGML_OPENVINO_NPU_KV_SLICE",
163+
"GGML_OPENVINO_KV_SLICE",
163164
"GGML_OPENVINO_NPU_HFA_PREFILL",
164165
"GGML_OPENVINO_SELF_CONTAINED_BLOB",
165166
"GGML_OPENVINO_SELF_CONTAINED_MMAP",
@@ -341,7 +342,28 @@ bool ggml_openvino_is_gpu() {
341342
return has_prefix(ggml_openvino_get_device_name(), "GPU");
342343
}
343344

344-
bool ggml_openvino_npu_kv_slice_enabled() {
345+
// -1: GGML_OPENVINO_KV_SLICE unset, use the mode's default (or its deprecated alias)
346+
static int kv_slice_setting() {
347+
const char * value = ggml_openvino_getenv_str("GGML_OPENVINO_KV_SLICE");
348+
if (value == nullptr || *value == '\0') {
349+
return -1;
350+
}
351+
return ggml_openvino_getenv_int("GGML_OPENVINO_KV_SLICE") != 0 ? 1 : 0;
352+
}
353+
354+
bool ggml_openvino_dynamic_kv_slice_enabled() {
355+
const int setting = kv_slice_setting();
356+
if (setting >= 0) {
357+
return setting != 0;
358+
}
359+
return ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_SLICE") == 0;
360+
}
361+
362+
bool ggml_openvino_static_kv_slice_enabled() {
363+
const int setting = kv_slice_setting();
364+
if (setting >= 0) {
365+
return setting != 0;
366+
}
345367
return ggml_openvino_getenv_int("GGML_OPENVINO_NPU_KV_SLICE") != 0;
346368
}
347369

‎ggml/src/ggml-openvino/ggml-openvino-extra.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,15 @@ bool ggml_openvino_is_npu();
119119

120120
// NPU-only opt-in performance toggles (all default off). Centralized here so each
121121
// env-var name lives in one place; see docs/backend/OPENVINO.md for semantics.
122-
bool ggml_openvino_npu_kv_slice_enabled();
122+
// KV-cache slicing: bind the KV cache, and size the KQ masks, at the attended length instead of the full context.
123+
// One mechanism, two modes:
124+
// dynamic graphs (CPU/GPU): the KV input is a view over the cache; the graph is not recompiled. On by default.
125+
// static graphs (NPU, or any device with GGML_OPENVINO_FORCE_STATIC): the prefill graph is compiled at the aligned
126+
// attended length and recompiled when it changes; decode keeps the full context. Off by default.
127+
// GGML_OPENVINO_KV_SLICE: unset = the default of each mode, 0 = off in both, 1 = on in both.
128+
// Deprecated aliases: GGML_OPENVINO_DISABLE_KV_SLICE=1 (dynamic off), GGML_OPENVINO_NPU_KV_SLICE=1 (static on).
129+
bool ggml_openvino_dynamic_kv_slice_enabled();
130+
bool ggml_openvino_static_kv_slice_enabled();
123131
bool ggml_openvino_npu_fast_mask_enabled();
124132
bool ggml_openvino_npu_l0_host_tensors_enabled();
125133
bool ggml_openvino_npu_hfa_prefill_enabled();

‎ggml/src/ggml-openvino/model-cache.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ static const char * cache_settings[] = {
174174
"GGML_OPENVINO_REQUANT_KQUANT",
175175
"GGML_OPENVINO_NATIVE_SOFTPLUS",
176176
"GGML_OPENVINO_DISABLE_KV_SLICE",
177+
"GGML_OPENVINO_KV_SLICE",
177178
"GGML_OPENVINO_MANUAL_GQA_ATTN",
178179
"GGML_OPENVINO_STATEFUL_EXECUTION",
179180
"GGML_OPENVINO_DISABLE_KV_STATE_RELAYOUT",

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,12 @@ namespace {
5656
std::optional<ov::Tensor> try_make_kv_sliced_tensor(const std::shared_ptr<GgmlOvDecoder> & ggml_decoder,
5757
const std::string & name,
5858
const ggml_tensor * ggml_tensor) {
59-
static const bool kv_slice_disabled = ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_SLICE");
60-
if (kv_slice_disabled) {
61-
return std::nullopt;
62-
}
63-
const bool static_npu_slice =
64-
ggml_decoder->is_static() && ggml_decoder->m_is_prefill && ggml_openvino_npu_kv_slice_enabled();
65-
if ((ggml_decoder->is_static() && !static_npu_slice) || ggml_decoder->is_stateful()) {
59+
// Static graphs bake the sliced length into the prefill graph (see ggml_openvino_static_kv_slice_enabled);
60+
// decode keeps its full-context graph.
61+
const bool slice = ggml_decoder->is_static() ?
62+
ggml_decoder->m_is_prefill && ggml_openvino_static_kv_slice_enabled() :
63+
ggml_openvino_dynamic_kv_slice_enabled();
64+
if (!slice || ggml_decoder->is_stateful()) {
6665
return std::nullopt;
6766
}
6867
if (ggml_tensor->op != GGML_OP_NONE || ggml_tensor->view_src != nullptr) {
@@ -142,7 +141,7 @@ std::optional<ov::Tensor> try_make_kv_4d_tensor(const std::shared_ptr<GgmlOvDeco
142141
// Column count of a static KQ mask input; must match GgmlOvDecoder::get_graph_input_shape().
143142
static size_t get_static_attention_size(std::shared_ptr<GgmlOvDecoder> ggml_decoder, const std::string & name) {
144143
const bool is_swa = ggml_decoder->is_swa_mask(ggml_decoder->get_input_ggml_tensor(name));
145-
if (ggml_decoder->m_is_prefill && ggml_openvino_npu_kv_slice_enabled()) {
144+
if (ggml_decoder->m_is_prefill && ggml_openvino_static_kv_slice_enabled()) {
146145
const auto params = ggml_decoder->get_compute_params();
147146
const int attention_size = is_swa ? params.attention_size_swa : params.attention_size;
148147
if (attention_size > 0) {
@@ -165,7 +164,7 @@ static uint64_t ggml_openvino_model_cache_extra_cfg(bool stateful, bool recurren
165164
uint64_t extra_cfg = 1; // Graph-ordinal port names (invalidate older disk-cache blobs).
166165
extra_cfg = extra_cfg * 131 + (stateful ? (recurrent ? 2u : 1u) : 0u);
167166
extra_cfg = extra_cfg * 131 + (ggml_openvino_reduce_compile_mem_enabled() ? 1u : 0u);
168-
extra_cfg = extra_cfg * 131 + (ggml_openvino_getenv_int("GGML_OPENVINO_DISABLE_KV_SLICE") ? 1u : 0u);
167+
extra_cfg = extra_cfg * 131 + (ggml_openvino_dynamic_kv_slice_enabled() ? 0u : 1u);
169168
extra_cfg = extra_cfg * 131 + (ggml_openvino_getenv_int("GGML_OPENVINO_ENABLE_KV_4D") > 0 ? 1u : 0u);
170169
extra_cfg = extra_cfg * 131 + (manual_gqa_enabled ? 1u : 0u);
171170
extra_cfg = extra_cfg * 131 + (ggml_openvino_getenv_int("GGML_OPENVINO_PROFILING") >= 2 ? 1u : 0u);
@@ -1736,7 +1735,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, const std::shared
17361735
ggml_decoder = entry->ptr;
17371736
old_m_params = ggml_decoder->get_model_params();
17381737
cache_hit = old_m_params.can_reuse_statically(m_params);
1739-
if (cache_hit && is_prefill && ggml_openvino_npu_kv_slice_enabled()) {
1738+
if (cache_hit && is_prefill && ggml_openvino_static_kv_slice_enabled()) {
17401739
const auto old_c_params = ggml_decoder->get_compute_params();
17411740
cache_hit = old_c_params.attention_size == c_params.attention_size &&
17421741
old_c_params.attention_size_swa == c_params.attention_size_swa;
@@ -1814,7 +1813,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, const std::shared
18141813
GGML_LOG_DEBUG("ggml-openvino: shared compiled model HIT (static)\n");
18151814
} else {
18161815
auto decode_c_params = c_params;
1817-
if (ggml_openvino_npu_kv_slice_enabled()) {
1816+
if (ggml_openvino_static_kv_slice_enabled()) {
18181817
decode_c_params.attention_size = m_params.ctx_per_seq;
18191818
decode_c_params.attention_size_swa = m_params.ctx_per_seq_swa;
18201819
}
@@ -1841,7 +1840,7 @@ enum ggml_status ov_graph_compute_static(ggml_cgraph * cgraph, const std::shared
18411840
base_extra = base_extra * 131 + static_cast<uint64_t>(ggml_openvino_getenv_int("GGML_OPENVINO_TOKEN_EMBD_I8"));
18421841
base_extra = base_extra * 131 + static_cast<uint64_t>(ggml_openvino_getenv_int("GGML_OPENVINO_TOKEN_EMBD_I4"));
18431842
base_extra = base_extra * 131 + static_cast<uint64_t>(ggml_openvino_getenv_int("GGML_OPENVINO_NPU_KEEP_Q4_0"));
1844-
base_extra = base_extra * 131 + static_cast<uint64_t>(ggml_openvino_getenv_int("GGML_OPENVINO_NPU_KV_SLICE"));
1843+
base_extra = base_extra * 131 + (ggml_openvino_static_kv_slice_enabled() ? 1u : 0u);
18451844
base_extra = ggml_openvino_model_cache_mix_string(
18461845
base_extra, ggml_openvino_getenv_str("GGML_OPENVINO_NPU_CONFIG"));
18471846
base_extra = ggml_openvino_model_cache_mix_string(

0 commit comments

Comments
 (0)