Skip to content

Commit 3523cbb

Browse files
authored
[KV cache] support-past-present-share-buffer (#268)
## Support past present share buffer Add `supports_past_present_share_buffer=True` Benefit: Save the `memcpy` overhead.
1 parent a0b7ce1 commit 3523cbb

3 files changed

Lines changed: 86 additions & 19 deletions

File tree

src/mobius/_execution_providers.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,21 @@ class EpCapabilities:
7171
quantization (0 = highest accuracy, 4 = fastest).
7272
provider_options: Default ORT GenAI provider options dict for this EP.
7373
enable_graph_capture: Whether this EP defaults to GPU graph capture.
74-
supports_past_present_share_buffer: Whether this EP requires past and present
75-
KV-cache tensors to share the same pre-allocated backing buffer.
76-
``True`` for WebGPU, which allocates the full KV-cache at model
77-
load time and maps both past and present views into it. ``False``
78-
for all other EPs where the runtime manages KV-cache memory
79-
dynamically.
74+
supports_past_present_share_buffer: Whether past and present KV-cache
75+
tensors alias the same pre-allocated buffer. When ``True``, the
76+
ORT GenAI runtime allocates a single KV-cache buffer at model load
77+
and maps both past and present as views into it, avoiding a
78+
per-step copy. This is the recommended setting for every EP that
79+
supports ``GroupQueryAttention`` (CPU, CUDA, DML, WebGPU,
80+
TRT-RTX). Set to ``False`` only for EPs that do not support GQA
81+
or cannot handle aliased KV-cache buffers.
82+
cap_kv_buffer_max_length: When ``True`` **and**
83+
``supports_past_present_share_buffer`` is also ``True``, the
84+
generated ``max_length`` in genai_config is capped to avoid
85+
pre-allocating huge KV-cache buffers on memory-constrained
86+
devices. ``True`` only for WebGPU (consumer GPU); ``False`` for
87+
CUDA / CPU / DML / TRT-RTX where the runtime can handle large
88+
pre-allocations.
8089
"""
8190

8291
name: str
@@ -90,6 +99,7 @@ class EpCapabilities:
9099
provider_options: dict[str, str] = dataclasses.field(default_factory=dict)
91100
enable_graph_capture: bool = False
92101
supports_past_present_share_buffer: bool = False
102+
cap_kv_buffer_max_length: bool = False
93103

94104
def __post_init__(self) -> None:
95105
if not self.supports_fused_rope and self.qkv_pack_dtypes:
@@ -98,6 +108,13 @@ def __post_init__(self) -> None:
98108
f"supports_fused_rope=False — UnpackQKV lowering always fires for "
99109
f"this EP, so packing would be immediately undone."
100110
)
111+
if self.cap_kv_buffer_max_length and not self.supports_past_present_share_buffer:
112+
raise ValueError(
113+
f"EP '{self.name}': cap_kv_buffer_max_length=True requires "
114+
f"supports_past_present_share_buffer=True — the cap only matters "
115+
f"when the runtime pre-allocates the full KV-cache buffer at load "
116+
f"time, which is what buffer sharing enables."
117+
)
101118

102119

103120
class EpRegistry:
@@ -199,6 +216,7 @@ def _register_builtins() -> None:
199216
gqa_dtypes=frozenset({ir.DataType.FLOAT}),
200217
qkv_pack_dtypes=frozenset({ir.DataType.FLOAT}),
201218
default_int4_accuracy_level=4,
219+
supports_past_present_share_buffer=True,
202220
),
203221
EpCapabilities(
204222
name="cuda",
@@ -211,6 +229,7 @@ def _register_builtins() -> None:
211229
"enable_cuda_graph": "0",
212230
"enable_skip_layer_norm_strict_mode": "1",
213231
},
232+
supports_past_present_share_buffer=True,
214233
),
215234
EpCapabilities(
216235
name="dml",
@@ -221,6 +240,7 @@ def _register_builtins() -> None:
221240
qkv_pack_dtypes=frozenset(),
222241
supports_packed_multi_head_attention=True,
223242
supports_fused_rope=False,
243+
supports_past_present_share_buffer=True,
224244
),
225245
EpCapabilities(
226246
name="webgpu",
@@ -229,6 +249,7 @@ def _register_builtins() -> None:
229249
default_int4_accuracy_level=4,
230250
provider_options={"enableGraphCapture": "0", "validationMode": "basic"},
231251
supports_past_present_share_buffer=True,
252+
cap_kv_buffer_max_length=True,
232253
),
233254
EpCapabilities(
234255
name="trt-rtx",
@@ -239,12 +260,15 @@ def _register_builtins() -> None:
239260
supports_skip_layer_norm=False,
240261
enable_graph_capture=True,
241262
provider_options={"enable_cuda_graph": "1"},
263+
supports_past_present_share_buffer=True,
242264
),
243265
# onnx-standard: ONNX-only runtime — emits zero custom-domain ops.
244266
# All com.microsoft ops (SkipLayerNorm, PackedMHA) are expanded via
245267
# InlinePass to their standard-ONNX function bodies. No GQA or QKV
246268
# packing fusion is applied. Use this EP to produce models that run
247269
# on any conformant ONNX runtime without ORT extensions.
270+
# KV buffer sharing is unsupported here: GQA isn't emitted, so
271+
# standard Attention's concat-grow semantics handle the cache.
248272
EpCapabilities(
249273
name="onnx-standard",
250274
gqa_dtypes=frozenset(), # no GroupQueryAttention

src/mobius/integrations/ort_genai/genai_config.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,14 @@ def _default_search_params(*, ep: str, context_length: int) -> dict[str, Any]:
6161

6262
caps = ep_registry.get(ep)
6363
share_buffer = caps.supports_past_present_share_buffer if caps is not None else False
64-
if share_buffer:
65-
# EPs that pre-allocate KV-cache for the full max_length at load time
66-
# (e.g. WebGPU) need a capped default to avoid pre-allocating huge
67-
# buffers (~8 GB for 128K-token models) on consumer hardware. Users
68-
# can raise the limit in genai_config.json for their target device.
64+
cap_length = caps.cap_kv_buffer_max_length if caps is not None else False
65+
if share_buffer and cap_length:
66+
# Memory-constrained EPs (e.g. WebGPU on consumer GPUs) pre-allocate
67+
# KV-cache for the full max_length at load time. Cap the default to
68+
# avoid pre-allocating huge buffers (~8 GB for 128K-token models).
69+
# Users can raise the limit in genai_config.json for their device.
70+
# The cap only applies when buffer sharing is also active — without
71+
# sharing, the runtime grows the cache on demand and no cap is needed.
6972
max_length = min(context_length, _SHARE_BUFFER_MAX_LENGTH_CAP)
7073
else:
7174
max_length = context_length

src/mobius/integrations/ort_genai/genai_config_test.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def test_search_params_defaults(self):
136136
assert search["top_p"] == pytest.approx(1.0)
137137
# max_length tracks the model's context window
138138
assert search["max_length"] == 8192
139-
# CPU does not share past/present KV buffers
140-
assert search["past_present_share_buffer"] is False
139+
# CPU shares past/present KV buffers (all GQA-capable EPs do)
140+
assert search["past_present_share_buffer"] is True
141141

142142
def test_search_params_webgpu_sets_past_present_share_buffer(self):
143143
"""WebGPU EP sets supports_past_present_share_buffer=True via EpCapabilities and caps max_length."""
@@ -174,8 +174,13 @@ def test_search_params_webgpu_small_context_not_capped(self):
174174
assert config["search"]["past_present_share_buffer"] is True
175175
assert config["search"]["max_length"] == 2048
176176

177-
def test_search_params_cuda_does_not_share_buffer(self):
178-
"""CUDA EP does not set past_present_share_buffer by default."""
177+
def test_search_params_cuda_shares_buffer(self):
178+
"""CUDA EP sets past_present_share_buffer=True (all GQA-capable EPs do).
179+
180+
CUDA does NOT cap max_length — only memory-constrained EPs (WebGPU)
181+
set ``cap_kv_buffer_max_length=True``. Server-class GPUs handle
182+
large pre-allocations.
183+
"""
179184
gen = GenaiConfigGenerator(
180185
"llama",
181186
vocab_size=32000,
@@ -185,15 +190,19 @@ def test_search_params_cuda_does_not_share_buffer(self):
185190
num_key_value_heads=8,
186191
head_dim=128,
187192
ep="cuda",
193+
context_length=131072,
188194
)
189195
config = gen.generate()
190-
assert config["search"]["past_present_share_buffer"] is False
196+
assert config["search"]["past_present_share_buffer"] is True
197+
# CUDA: full context_length, NOT capped at 4096
198+
assert config["search"]["max_length"] == 131072
191199

192200
def test_search_params_custom_ep_with_share_buffer(self):
193201
"""A custom EP registered with supports_past_present_share_buffer=True gets the flag set.
194202
195203
This proves the value comes from EpCapabilities, not from a hardcoded
196-
'ep == webgpu' check.
204+
'ep == webgpu' check. The custom EP does NOT set
205+
``cap_kv_buffer_max_length``, so max_length is the full context.
197206
"""
198207
from mobius._execution_providers import EpCapabilities, ep_registry
199208

@@ -215,12 +224,43 @@ def test_search_params_custom_ep_with_share_buffer(self):
215224
)
216225
config = gen.generate()
217226
assert config["search"]["past_present_share_buffer"] is True
218-
# Buffer-sharing EP: max_length capped at 4096
219-
assert config["search"]["max_length"] == 4096
227+
# No cap_kv_buffer_max_length: max_length = full context_length
228+
assert config["search"]["max_length"] == 8192
220229
finally:
221230
# Clean up the test EP so it doesn't bleed into other tests
222231
ep_registry._entries.pop("test-custom-ep", None)
223232

233+
def test_search_params_custom_ep_with_max_length_cap(self):
234+
"""A custom EP with cap_kv_buffer_max_length=True caps max_length at 4096."""
235+
from mobius._execution_providers import EpCapabilities, ep_registry
236+
237+
ep_registry.register(
238+
EpCapabilities(
239+
name="test-capped-ep",
240+
supports_past_present_share_buffer=True,
241+
cap_kv_buffer_max_length=True,
242+
),
243+
overwrite=True,
244+
)
245+
try:
246+
gen = GenaiConfigGenerator(
247+
"llama",
248+
vocab_size=32000,
249+
hidden_size=4096,
250+
num_hidden_layers=32,
251+
num_attention_heads=32,
252+
num_key_value_heads=8,
253+
head_dim=128,
254+
ep="test-capped-ep",
255+
context_length=131072,
256+
)
257+
config = gen.generate()
258+
assert config["search"]["past_present_share_buffer"] is True
259+
# cap_kv_buffer_max_length=True: max_length capped at 4096
260+
assert config["search"]["max_length"] == 4096
261+
finally:
262+
ep_registry._entries.pop("test-capped-ep", None)
263+
224264
def test_session_options_present(self):
225265
"""Decoder has session_options with log_id."""
226266
gen = GenaiConfigGenerator(

0 commit comments

Comments
 (0)