Promote `prefix_cache_retention_interval` to an argument and change the default to 0 (#52216)
Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Tyler Michael Smith committed
Aug 17, 2026 at 09:20 UTC
017e9f4448b700e85ee16023287b025693c72b9e
17 files changed
+160
-63
tests/config/test_config_utils.py
+5
@@ -216,6 +216,11 @@ def test_cache_config_hash_ignores_kv_cache_sizing_knobs():
216
assert CacheConfig(gpu_memory_utilization=0.5).compute_hash() == base_hash
217
218
219
+def test_cache_config_hash_ignores_prefix_cache_retention_interval():
220
+ base_hash = CacheConfig().compute_hash()
221
+ assert CacheConfig(prefix_cache_retention_interval=64).compute_hash() == base_hash
222
+
223
+
224
def test_envs_compile_factors_relocation_invariant(tmp_path):
225
"""Relocating HOME or the XDG roots must not change the compile-cache
226
env hash.
tests/engine/test_arg_utils.py
+23
@@ -476,6 +476,7 @@ def test_prefix_cache_default():
476
# should be None by default (depends on model).
477
engine_args = EngineArgs.from_cli_args(args=args)
478
assert engine_args.enable_prefix_caching is None
479
+ assert engine_args.prefix_cache_retention_interval == 0
480
481
# with flag to turn it on.
482
args = parser.parse_args(["--enable-prefix-caching"])
@@ -487,6 +488,28 @@ def test_prefix_cache_default():
488
engine_args = EngineArgs.from_cli_args(args=args)
489
assert not engine_args.enable_prefix_caching
490
491
+ args = parser.parse_args(["--prefix-cache-retention-interval", "64"])
492
+ engine_args = EngineArgs.from_cli_args(args=args)
493
+ assert engine_args.prefix_cache_retention_interval == 64
494
+
495
+
496
+def test_prefix_cache_retention_interval_from_deprecated_env(
497
+ monkeypatch, caplog, disable_log_dedup
498
+):
499
+ monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "64")
500
+
501
+ engine_args = EngineArgs()
502
+
503
+ assert engine_args.prefix_cache_retention_interval == 64
504
+ assert "VLLM_PREFIX_CACHE_RETENTION_INTERVAL" in caplog.text
505
+ assert "deprecated" in caplog.text
506
+ assert "prefix_cache_retention_interval" in caplog.text
507
+
508
+ parser = EngineArgs.add_cli_args(FlexibleArgumentParser())
509
+ args = parser.parse_args(["--prefix-cache-retention-interval", "32"])
510
+ engine_args = EngineArgs.from_cli_args(args)
511
+ assert engine_args.prefix_cache_retention_interval == 32
512
+
513
514
@pytest.mark.parametrize(
515
("arg", "expected", "option"),
tests/v1/core/test_contiguous_kv_packing.py
+2
@@ -88,6 +88,7 @@ def _make_groups(n_c4, n_c128, n_swa):
88
def _mock_vllm_config(kv_connector_extra_config: dict[str, str] | None = None):
89
config = MagicMock()
90
config.cache_config.num_gpu_blocks_override = None
91
+ config.cache_config.prefix_cache_retention_interval = 0
92
config.kv_transfer_config = None
93
if kv_connector_extra_config is not None:
94
config.kv_transfer_config = MagicMock()
@@ -322,6 +323,7 @@ class TestInterleavedPacking:
323
)
324
325
assert config.num_blocks == 32
326
+ assert config.prefix_cache_retention_interval == 0
327
assert sum(t.size for t in config.kv_cache_tensors) == page_size * 2 * 32
328
assert config.kv_cache_tensors == [
329
KVCacheTensor(size=page_size * 32, shared_by=["full.0", "sw.0", "sw.1"]),
tests/v1/core/test_kv_cache_utils.py
+4
@@ -816,6 +816,7 @@ def test_metrics_empty_stats():
816
def test_get_kv_cache_configs_multiple_workers():
817
model_config = ModelConfig(max_model_len=16)
818
vllm_config = VllmConfig(model_config=model_config)
819
+ vllm_config.cache_config.prefix_cache_retention_interval = None
820
821
ref_kv_cache_spec = new_kv_cache_spec()
822
same_kv_cache_specs = [
@@ -1173,6 +1174,7 @@ def test_get_kv_cache_configs_multiple_workers():
1174
def test_get_kv_cache_configs_pp_sharding(asymmetric_memory):
1175
model_config = ModelConfig(max_model_len=512)
1176
vllm_config = VllmConfig(model_config=model_config)
1177
+ vllm_config.cache_config.prefix_cache_retention_interval = None
1178
1179
ref_kv_cache_spec = new_kv_cache_spec()
1180
pp_kv_cache_specs = [
@@ -1702,6 +1704,7 @@ def test_get_kv_cache_config_one_worker():
1704
# pass max_model_len to pass check_enough_kv_cache_memory
1705
model_config = ModelConfig(max_model_len=16)
1706
vllm_config = VllmConfig(model_config=model_config)
1707
+ vllm_config.cache_config.prefix_cache_retention_interval = None
1708
1709
mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2
1710
# all layers are full attention -> single group
@@ -2015,6 +2018,7 @@ def test_get_kv_cache_config_one_worker():
2018
def test_get_kv_cache_configs_attention_free():
2019
kv_cache_specs: dict[str, KVCacheSpec] = {}
2020
vllm_config = VllmConfig(model_config=ModelConfig(max_model_len=16))
2021
+ vllm_config.cache_config.prefix_cache_retention_interval = None
2022
kv_cache_configs = get_kv_cache_configs(vllm_config, [kv_cache_specs], [0])
2023
assert kv_cache_configs == [
2024
KVCacheConfig(
tests/v1/core/test_prefix_caching.py
+66
-44
@@ -4,6 +4,7 @@
4
5
import copy
6
from collections.abc import Callable
7
+from dataclasses import replace
8
from math import lcm
9
from types import SimpleNamespace
10
@@ -110,6 +111,11 @@ def make_kv_cache_manager(kv_cache_config: KVCacheConfig, **kwargs) -> KVCacheMa
111
"scheduler_block_size",
112
lcm(*(g.kv_cache_spec.block_size for g in kv_cache_config.kv_cache_groups)),
113
)
114
+ if "retention_interval" in kwargs:
115
+ kv_cache_config = replace(
116
+ kv_cache_config,
117
+ prefix_cache_retention_interval=kwargs.pop("retention_interval"),
118
+ )
119
return KVCacheManager(kv_cache_config, **kwargs)
120
121
@@ -3119,9 +3125,8 @@ def test_hybrid_cache_blocks_clamped_to_lcm():
3125
)
3126
3127
3122
-def test_hybrid_local_kv_retention_interval_aligns_in_manager(monkeypatch):
3128
+def test_hybrid_local_kv_retention_interval_aligns_in_manager():
3129
"""Verify fixed intervals retain sparse tails plus the latest replay tail."""
3124
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "64")
3130
block_size = 8
3131
kv_cache_config = KVCacheConfig(
3132
num_blocks=100,
@@ -3153,6 +3158,7 @@ def test_hybrid_local_kv_retention_interval_aligns_in_manager(monkeypatch):
3158
max_model_len=8192,
3159
enable_caching=True,
3160
hash_block_size=block_size,
3161
+ retention_interval=64,
3162
)
3163
3164
# The SWA manager uses the configured 64-token interval (a multiple of the
@@ -3184,18 +3190,15 @@ def test_hybrid_local_kv_retention_interval_aligns_in_manager(monkeypatch):
3190
"interval, expected_match",
3191
[
3192
# scheduler_block_size is 32 (= lcm(4*8, 8)); 33 is not a multiple of it.
3187
- ("33", "multiple of scheduler_block_size"),
3193
+ (33, "multiple of scheduler_block_size"),
3194
# A negative multiple (-32 % 32 == 0) must still be rejected explicitly,
3195
# otherwise it would pass the modulo check and silently degrade to dense.
3190
- ("-32", "non-negative"),
3196
+ (-32, "non-negative"),
3197
],
3198
)
3193
-def test_hybrid_local_kv_retention_interval_rejects_invalid(
3194
- monkeypatch, interval, expected_match
3195
-):
3199
+def test_hybrid_local_kv_retention_interval_rejects_invalid(interval, expected_match):
3200
"""A retention interval that is negative or not a multiple of
3201
scheduler_block_size errors out at construction time."""
3198
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", interval)
3202
block_size = 8
3203
kv_cache_config = KVCacheConfig(
3204
num_blocks=100,
@@ -3228,12 +3231,36 @@ def test_hybrid_local_kv_retention_interval_rejects_invalid(
3231
max_model_len=8192,
3232
enable_caching=True,
3233
hash_block_size=block_size,
3234
+ retention_interval=interval,
3235
)
3236
3237
3234
-def test_hybrid_local_kv_retention_interval_survives_recycling(monkeypatch):
3238
+def test_zero_retention_is_ignored_for_full_attention():
3239
+ kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=10)
3240
+ manager = make_kv_cache_manager(
3241
+ kv_cache_config,
3242
+ max_model_len=8192,
3243
+ enable_caching=True,
3244
+ hash_block_size=16,
3245
+ retention_interval=0,
3246
+ )
3247
+ assert manager.coordinator.retention_interval == 0
3248
+
3249
+
3250
+def test_positive_retention_rejects_full_attention():
3251
+ kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=10)
3252
+ with pytest.raises(ValueError, match="no sliding-window or Mamba"):
3253
+ make_kv_cache_manager(
3254
+ kv_cache_config,
3255
+ max_model_len=8192,
3256
+ enable_caching=True,
3257
+ hash_block_size=16,
3258
+ retention_interval=16,
3259
+ )
3260
+
3261
+
3262
+def test_hybrid_local_kv_retention_interval_survives_recycling():
3263
"""Verify retained local checkpoints are reused after block recycling."""
3236
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "1024")
3264
hash_block_size = 4
3265
kv_cache_config = KVCacheConfig(
3266
num_blocks=800,
@@ -3286,6 +3313,7 @@ def test_hybrid_local_kv_retention_interval_survives_recycling(monkeypatch):
3313
max_model_len=4096,
3314
enable_caching=True,
3315
hash_block_size=hash_block_size,
3316
+ retention_interval=1024,
3317
)
3318
3319
def fill_request(request_id: str, token_offset: int) -> list[int]:
@@ -3314,9 +3342,8 @@ def test_hybrid_local_kv_retention_interval_survives_recycling(monkeypatch):
3342
assert [len(blocks) for blocks in computed_blocks.blocks] == [4, 16, 128, 256]
3343
3344
3317
-def test_hybrid_local_kv_retention_latest_only_reuses_replay_boundary(monkeypatch):
3345
+def test_hybrid_local_kv_retention_latest_only_reuses_replay_boundary():
3346
"""Verify latest-only retention reuses only the replayable prompt boundary."""
3319
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "0")
3347
block_size = 8
3348
kv_cache_config = KVCacheConfig(
3349
num_blocks=100,
@@ -3348,6 +3375,7 @@ def test_hybrid_local_kv_retention_latest_only_reuses_replay_boundary(monkeypatc
3375
max_model_len=8192,
3376
enable_caching=True,
3377
hash_block_size=block_size,
3378
+ retention_interval=0,
3379
)
3380
3381
token_ids = [i for i in range(16) for _ in range(block_size)]
@@ -3388,14 +3416,13 @@ def test_hybrid_local_kv_retention_latest_only_reuses_replay_boundary(monkeypatc
3416
assert len(computed_blocks.blocks[1]) == 0
3417
3418
3391
-def test_hybrid_local_kv_retention_mtp_reuses_latest_boundary(monkeypatch):
3419
+def test_hybrid_local_kv_retention_mtp_reuses_latest_boundary():
3420
"""Verify MTP/EAGLE SWA retention keeps the extra proof block.
3421
3422
EAGLE/MTP lookup matches one additional local block after the returned
3423
prefix and then drops it. Sparse retention must therefore cache the normal
3424
local tail at the latest replay boundary plus one extra SWA block.
3425
"""
3398
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "0")
3426
block_size = 8
3427
kv_cache_config = KVCacheConfig(
3428
num_blocks=100,
@@ -3428,6 +3455,7 @@ def test_hybrid_local_kv_retention_mtp_reuses_latest_boundary(monkeypatch):
3455
max_model_len=8192,
3456
enable_caching=True,
3457
hash_block_size=block_size,
3458
+ retention_interval=0,
3459
use_eagle=True,
3460
)
3461
@@ -3824,12 +3852,11 @@ def test_cache_hit_local_and_external_two_groups_preempt_and_reallocate():
3852
assert manager.get_blocks("test").get_block_ids() != ([], [])
3853
3854
3827
-def test_swa_free_split_keeps_cached_tail_ahead_of_scratch(monkeypatch):
3828
- """Default path (no retention): freeing an SWA request must place its
3855
+def test_swa_free_split_keeps_cached_tail_ahead_of_scratch():
3856
+ """Dense retention: freeing an SWA request must place its
3857
uncached scratch blocks at the front of the free queue (recycled first)
3858
and keep its cached checkpoint blocks at the back (retained for prefix
3859
hits). This split is always-on, independent of the retention interval."""
3832
- monkeypatch.delenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", raising=False)
3860
block_size = 8
3861
kv_cache_config = KVCacheConfig(
3862
num_blocks=100,
@@ -3937,13 +3964,14 @@ def _make_pure_swa_manager(block_size, sliding_window, num_blocks=100, **kwargs)
3964
)
3965
3966
3940
-def test_pure_swa_retention_interval_caches_sparse_tails(monkeypatch):
3967
+def test_pure_swa_retention_interval_caches_sparse_tails():
3968
"""Sparse retention must work for a pure-SWA single-group model, not just
3969
hybrid models: only the per-interval tails plus the latest replay tail are
3970
cached, and a replay still hits the latest replayable boundary."""
3944
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "64")
3971
block_size = 16
3946
- manager = _make_pure_swa_manager(block_size, sliding_window=block_size)
3972
+ manager = _make_pure_swa_manager(
3973
+ block_size, sliding_window=block_size, retention_interval=64
3974
+ )
3975
assert type(manager.coordinator).__name__ == "UnitaryKVCacheCoordinator"
3976
3977
token_ids = [i for i in range(16) for _ in range(block_size)]
@@ -3976,11 +4004,12 @@ def test_pure_swa_retention_interval_caches_sparse_tails(monkeypatch):
4004
assert num_computed == 240
4005
4006
3979
-def test_pure_swa_retention_latest_only(monkeypatch):
4007
+def test_pure_swa_retention_latest_only():
4008
"""`=0` on a pure-SWA model keeps only the latest replay tail."""
3981
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "0")
4009
block_size = 16
3983
- manager = _make_pure_swa_manager(block_size, sliding_window=block_size)
4010
+ manager = _make_pure_swa_manager(
4011
+ block_size, sliding_window=block_size, retention_interval=0
4012
+ )
4013
4014
token_ids = [i for i in range(16) for _ in range(block_size)]
4015
req = make_request("0", token_ids, block_size, sha256)
@@ -4008,10 +4037,9 @@ def test_pure_swa_retention_latest_only(monkeypatch):
4037
assert num_computed == 240
4038
4039
4011
-def test_pure_swa_retention_dense_default_caches_all(monkeypatch):
4012
- """With retention unset, a pure-SWA model must keep the dense behavior:
4040
+def test_pure_swa_dense_retention_caches_all():
4041
+ """With retention set to ``None``, a pure-SWA model keeps dense behavior:
4042
every block boundary is a potential hit, so all blocks are cached."""
4014
- monkeypatch.delenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", raising=False)
4043
block_size = 16
4044
manager = _make_pure_swa_manager(block_size, sliding_window=block_size)
4045
@@ -4037,7 +4065,7 @@ def test_pure_swa_retention_dense_default_caches_all(monkeypatch):
4065
4066
4067
def test_mamba_reachable_block_mask_sparsifies_retention():
4040
- """Mamba state-snapshot retention: with VLLM_PREFIX_CACHE_RETENTION_INTERVAL
4068
+ """Mamba state-snapshot retention: with a configured retention interval,
4069
the manager keeps one cached state per interval-sized segment (plus the
4070
latest replay boundary) instead of a snapshot per block, which is what
4071
lets a small attention block_size avoid Mamba dominating the KV pool."""
@@ -4063,7 +4091,7 @@ def test_mamba_reachable_block_mask_sparsifies_retention():
4091
)
4092
return None if m is None else {i for i, v in enumerate(m) if v}
4093
4066
- # Dense default (None) -> no mask, every block cached (unchanged behavior).
4094
+ # Dense retention (None) -> no mask, every block cached.
4095
assert retained(None) is None
4096
# interval == block_size -> every block is a boundary -> stays dense.
4097
assert retained(block_size) is None
@@ -4111,7 +4139,7 @@ def test_mamba_reachable_block_mask_pins_shared_prefix():
4139
assert retained(0, 100) == {5, 14}
4140
# Coexists with segment tails (interval 64 -> {3,7,11,15} + replay 14).
4141
assert retained(64, 96) == {3, 5, 7, 11, 14, 15}
4114
- # Dense default ignores the hint (nothing to sparsify).
4142
+ # Dense retention ignores the hint (nothing to sparsify).
4143
assert retained(None, 96) is None
4144
# Out-of-range boundary is a no-op (only replay 14 remains).
4145
assert retained(0, 16 * block_size * 2) == {14}
@@ -4120,14 +4148,13 @@ def test_mamba_reachable_block_mask_pins_shared_prefix():
4148
assert retained(0, None) == {14}
4149
4150
4123
-def test_mamba_shared_prefix_survives_zero_retention(monkeypatch):
4151
+def test_mamba_shared_prefix_survives_zero_retention():
4152
"""Manager-level check of the full wiring: a pinned shared-prefix boundary
4153
(``Request.shared_prefix_boundary``, set by the scheduler on Marconi-style
4154
detection) keeps its Mamba state block cached under
4127
- ``VLLM_PREFIX_CACHE_RETENTION_INTERVAL=0``, which otherwise retains only the
4155
+ ``prefix_cache_retention_interval=0``, which otherwise retains only the
4156
end-of-prompt replay boundary. Without this, a shared prefix (junction
4157
before ``num_prompt``) would be recomputed by every sharing request."""
4130
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", "0")
4158
block_size = 16
4159
4160
# 16-block (256-token) prompt; replay boundary is block 240 // 16 - 1 = 14.
@@ -4140,6 +4167,7 @@ def test_mamba_shared_prefix_survives_zero_retention(monkeypatch):
4167
max_model_len=8192,
4168
enable_caching=True,
4169
hash_block_size=block_size,
4170
+ retention_interval=0,
4171
)
4172
req = make_request("r", token_ids, block_size, sha256)
4173
req.shared_prefix_boundary = shared_prefix_boundary
@@ -4163,24 +4191,21 @@ def test_mamba_shared_prefix_survives_zero_retention(monkeypatch):
4191
assert cached_mamba_blocks(96) == {5, 14}
4192
4193
4166
-def test_mamba_shared_prefix_reuse_under_zero_retention(monkeypatch):
4194
+def test_mamba_shared_prefix_reuse_under_zero_retention():
4195
"""Full cross-request Marconi flow: a partial shared prefix cached by the
4196
detecting request must stay reusable by a later request under
4169
- ``VLLM_PREFIX_CACHE_RETENTION_INTERVAL=0``. Without the pin the junction is
4197
+ ``prefix_cache_retention_interval=0``. Without the pin the junction is
4198
masked out and the later request misses; with it (and under dense) the reuse
4199
is preserved."""
4200
block_size = 16
4201
4202
def last_req_hit(retention, pin):
4175
- if retention is None:
4176
- monkeypatch.delenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", raising=False)
4177
- else:
4178
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", str(retention))
4203
manager = make_kv_cache_manager(
4204
_make_hybrid_kv_cache_config(block_size, 200, ["full", "mamba_align"]),
4205
max_model_len=8192,
4206
enable_caching=True,
4207
hash_block_size=block_size,
4208
+ retention_interval=retention,
4209
)
4210
shared = [7 for _ in range(2 * block_size)] # 2-block shared prefix
4211
@@ -4261,23 +4286,20 @@ def test_swa_reachable_block_mask_pins_shared_prefix():
4286
assert retained(0, 0, block_size) == {14}
4287
4288
4264
-def test_swa_shared_prefix_reuse_under_zero_retention(monkeypatch):
4289
+def test_swa_shared_prefix_reuse_under_zero_retention():
4290
"""SWA cross-request analog: a partial shared prefix's sliding-window tail
4266
- must stay reusable under ``VLLM_PREFIX_CACHE_RETENTION_INTERVAL=0``. Without
4291
+ must stay reusable under ``prefix_cache_retention_interval=0``. Without
4292
the pin the junction window is masked out and a later request misses; with
4293
it (and under dense) reuse is preserved."""
4294
block_size = 16
4295
4296
def last_req_hit(retention, pin):
4272
- if retention is None:
4273
- monkeypatch.delenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", raising=False)
4274
- else:
4275
- monkeypatch.setenv("VLLM_PREFIX_CACHE_RETENTION_INTERVAL", str(retention))
4297
manager = make_kv_cache_manager(
4298
_make_hybrid_kv_cache_config(block_size, 200, ["full", "sliding_window"]),
4299
max_model_len=8192,
4300
enable_caching=True,
4301
hash_block_size=block_size,
4302
+ retention_interval=retention,
4303
)
4304
shared = [7 for _ in range(4 * block_size)] # 4-block shared prefix
4305
tests/v1/engine/test_engine_args.py
+5
@@ -18,6 +18,7 @@ def test_prefix_caching_from_cli():
18
assert vllm_config.cache_config.enable_prefix_caching, (
19
"V1 turns on prefix caching by default."
20
)
21
+ assert vllm_config.cache_config.prefix_cache_retention_interval == 0
22
23
# Turn it off possible with flag.
24
args = parser.parse_args(["--no-enable-prefix-caching"])
@@ -47,6 +48,10 @@ def test_prefix_caching_from_cli():
48
with pytest.raises(ArgumentError):
49
args = parser.parse_args(["--prefix-caching-hash-algo", "invalid"])
50
51
+ args = parser.parse_args(["--prefix-cache-retention-interval", "64"])
52
+ vllm_config = EngineArgs.from_cli_args(args=args).create_engine_config()
53
+ assert vllm_config.cache_config.prefix_cache_retention_interval == 64
54
+
55
56
@pytest.mark.skipif(_xxhash is None, reason="xxhash not installed")
57
def test_prefix_caching_xxhash_from_cli():
tests/v1/engine/test_engine_core_client.py
+1
@@ -1066,6 +1066,7 @@ def test_kv_cache_events(
1066
model=model_name,
1067
enforce_eager=True,
1068
enable_prefix_caching=True,
1069
+ prefix_cache_retention_interval=None,
1070
block_size=block_size,
1071
)
1072
engine_args.kv_events_config = publisher_config
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+5
-1
@@ -247,7 +247,9 @@ def _make_vllm_config(
247
)
248
249
250
-def _make_kv_cache_config(*, block_size: int = 16) -> object:
250
+def _make_kv_cache_config(
251
+ *, block_size: int = 16, prefix_cache_retention_interval: int | None = 0
252
+) -> object:
253
"""Minimal single-group KVCacheConfig for topology tests."""
254
from vllm.v1.kv_cache_interface import (
255
FullAttentionSpec,
@@ -262,6 +264,7 @@ def _make_kv_cache_config(*, block_size: int = 16) -> object:
264
num_blocks=10,
265
kv_cache_tensors=[],
266
kv_cache_groups=[KVCacheGroupSpec(["layer0"], spec)],
267
+ prefix_cache_retention_interval=prefix_cache_retention_interval,
268
)
269
270
@@ -1594,6 +1597,7 @@ def test_requester_worker_init_uses_positional_setup(tmp_path, monkeypatch):
1597
"mlx5_0",
1598
"10.0.0.7:50051",
1599
)
1600
+ assert w.coord.retention_interval == 0
1601
1602
1603
def test_requester_worker_init_prefers_local_hostname_override(
vllm/config/cache.py
+22
-1
@@ -7,7 +7,7 @@ from typing import Any, ClassVar, Literal
7
8
from pydantic import Field, field_validator, model_validator
9
10
-from vllm.config.utils import config
10
+from vllm.config.utils import config, get_from_deprecated_env_if_set
11
from vllm.logger import init_logger
12
from vllm.utils.torch_utils import (
13
is_quantized_kv_cache,
@@ -35,6 +35,17 @@ CacheDType = Literal[
35
"nvfp4",
36
"nvfp4_4over6",
37
]
38
+
39
+
40
+def _get_prefix_cache_retention_interval() -> int | None:
41
+ env_value = get_from_deprecated_env_if_set(
42
+ "VLLM_PREFIX_CACHE_RETENTION_INTERVAL",
43
+ "v0.29",
44
+ "prefix_cache_retention_interval",
45
+ )
46
+ return 0 if env_value is None else int(env_value)
47
+
48
+
49
MambaDType = Literal["auto", "float32", "float16", "bfloat16"]
50
MambaCacheMode = Literal["all", "align", "none"]
51
PrefixCachingHashAlgo = Literal["sha256", "sha256_cbor", "xxhash", "xxhash_cbor"]
@@ -111,6 +122,15 @@ class CacheConfig:
122
security risk tolerance against the performance benefits before turning this on.
123
- "xxhash_cbor" combines canonical CBOR serialization with xxHash for
124
reproducible hashing. Requires the optional ``xxhash`` package."""
125
+ prefix_cache_retention_interval: int | None = Field(
126
+ default_factory=_get_prefix_cache_retention_interval, ge=0
127
+ )
128
+ """Token interval between retained sliding-window and Mamba prefix-cache
129
+ checkpoints. ``0`` retains only semantic checkpoints, including the latest
130
+ replay boundary and shared-prefix junctions. Positive values additionally
131
+ retain periodic checkpoints at the specified interval, which must be a
132
+ multiple of the scheduler block size. ``None`` retains checkpoints densely.
133
+ Applies only to sliding-window and Mamba cache groups."""
134
kv_cache_dtype_skip_layers: list[str] = field(default_factory=list)
135
"""Layer patterns to skip KV cache quantization. Accepts layer indices
136
(e.g., '0', '2', '4') or attention type names (e.g., 'sliding_window')."""
@@ -218,6 +238,7 @@ class CacheConfig:
238
"num_gpu_blocks_override",
239
"enable_prefix_caching",
240
"prefix_caching_hash_algo",
241
+ "prefix_cache_retention_interval",
242
# Prefix-caching implementation detail (doesn't affect compiled graph).
243
"prefix_match_unit",
244
"mamba_page_size_padded",
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+1
-1
@@ -1433,7 +1433,7 @@ class MooncakeStoreWorker:
1433
scheduler_block_size=self.block_size,
1434
hash_block_size=self.hash_block_size,
1435
use_eagle=use_eagle,
1436
- retention_interval=envs.VLLM_PREFIX_CACHE_RETENTION_INTERVAL,
1436
+ retention_interval=kv_cache_config.prefix_cache_retention_interval,
1437
)
1438
# One ChunkedTokenDatabase per group; addresses populated in
1439
# register_kv_caches once the kv-cache layout is known. Each group's
vllm/engine/arg_utils.py
+8
@@ -522,6 +522,9 @@ class EngineArgs:
522
prefix_caching_hash_algo: PrefixCachingHashAlgo = (
523
CacheConfig.prefix_caching_hash_algo
524
)
525
+ prefix_cache_retention_interval: int | None = get_field(
526
+ CacheConfig, "prefix_cache_retention_interval"
527
+ )
528
disable_sliding_window: bool = ModelConfig.disable_sliding_window
529
disable_cascade_attn: bool = ModelConfig.disable_cascade_attn
530
offload_backend: str = OffloadConfig.offload_backend
@@ -1224,6 +1227,10 @@ class EngineArgs:
1227
cache_group.add_argument(
1228
"--prefix-caching-hash-algo", **cache_kwargs["prefix_caching_hash_algo"]
1229
)
1230
+ cache_group.add_argument(
1231
+ "--prefix-cache-retention-interval",
1232
+ **cache_kwargs["prefix_cache_retention_interval"],
1233
+ )
1234
cache_group.add_argument(
1235
"--kv-cache-dtype-skip-layers", **cache_kwargs["kv_cache_dtype_skip_layers"]
1236
)
@@ -2001,6 +2008,7 @@ class EngineArgs:
2008
sliding_window=sliding_window,
2009
enable_prefix_caching=self.enable_prefix_caching,
2010
prefix_caching_hash_algo=self.prefix_caching_hash_algo,
2011
+ prefix_cache_retention_interval=self.prefix_cache_retention_interval,
2012
kv_cache_dtype_skip_layers=self.kv_cache_dtype_skip_layers,
2013
kv_sharing_fast_prefill=self.kv_sharing_fast_prefill,
2014
mamba_cache_dtype=self.mamba_cache_dtype,
vllm/envs.py
-6
@@ -1148,12 +1148,6 @@ environment_variables: dict[str, Callable[[], Any]] = {
1148
if "VLLM_PLUGINS" not in os.environ
1149
else os.environ["VLLM_PLUGINS"].split(",")
1150
),
1151
- # Retain local sliding-window KV checkpoints for prefix caching.
1152
- # Unset (default) preserves the dense local checkpointing behavior. `0`
1153
- # retains only the latest completed prompt boundary. Positive values retain
1154
- # checkpoints at the specified interval boundaries (rounded up to the
1155
- # prefix-cache alignment).
1156
- # Applies to sliding-window attention for now but not yet Mamba/linear attention.
1151
"VLLM_PREFIX_CACHE_RETENTION_INTERVAL": lambda: (
1152
int(os.environ["VLLM_PREFIX_CACHE_RETENTION_INTERVAL"])
1153
if "VLLM_PREFIX_CACHE_RETENTION_INTERVAL" in os.environ
vllm/v1/core/kv_cache_coordinator.py
+6
-5
@@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
4
from collections.abc import Sequence
5
from typing import NamedTuple
6
7
-from vllm import envs
7
from vllm.logger import init_logger
8
from vllm.utils.math_utils import cdiv, round_down
9
from vllm.v1.core.block_pool import BlockPool
@@ -45,16 +44,18 @@ def _validate_prefix_cache_retention_interval(
44
isinstance(g.kv_cache_spec, (SlidingWindowSpec, MambaSpec))
45
for g in kv_cache_config.kv_cache_groups
46
):
47
+ if retention_interval == 0:
48
+ return
49
raise ValueError(
49
- "VLLM_PREFIX_CACHE_RETENTION_INTERVAL is set but this model has "
50
+ "prefix_cache_retention_interval is set but this model has "
51
"no sliding-window or Mamba KV cache group, so retention has no "
51
- "effect. Unset it (it only applies to sliding-window and Mamba "
52
+ "effect. Set it to 0 (it only applies to sliding-window and Mamba "
53
"attention)."
54
)
55
56
if retention_interval < 0 or retention_interval % scheduler_block_size != 0:
57
raise ValueError(
57
- f"VLLM_PREFIX_CACHE_RETENTION_INTERVAL ({retention_interval}) "
58
+ f"prefix_cache_retention_interval ({retention_interval}) "
59
"must be non-negative and a multiple of scheduler_block_size "
60
f"({scheduler_block_size})."
61
)
@@ -151,7 +152,7 @@ class KVCacheCoordinator(ABC):
152
# A positive retention interval must be a multiple of the base hit granularity
153
# (``scheduler_block_size``) to land on real cache-hit boundaries.
154
# 0 = keep only the latest replay boundary; None = dense;
154
- self.retention_interval = envs.VLLM_PREFIX_CACHE_RETENTION_INTERVAL
155
+ self.retention_interval = kv_cache_config.prefix_cache_retention_interval
156
_validate_prefix_cache_retention_interval(
157
self.retention_interval, self.scheduler_block_size, kv_cache_config
158
)
vllm/v1/core/kv_cache_manager.py
+1
-1
@@ -243,7 +243,7 @@ class KVCacheManager:
243
- ``shared_prefix_boundary``: the block-aligned token position of
244
a shared prefix that a sparse-retention group (Mamba / sliding
245
window) has not cached yet (Marconi-style APC), or 0 if none.
246
- Pinned so ``VLLM_PREFIX_CACHE_RETENTION_INTERVAL`` does not drop
246
+ Pinned so sparse prefix-cache retention does not drop
247
the junction and defeat cross-request reuse.
248
"""
249
# We skip finding the prefix cache hit when prefix caching is
vllm/v1/core/kv_cache_utils.py
+6
@@ -1347,6 +1347,9 @@ def get_kv_cache_config_from_groups(
1347
num_blocks=1,
1348
kv_cache_tensors=[],
1349
kv_cache_groups=kv_cache_groups,
1350
+ prefix_cache_retention_interval=(
1351
+ vllm_config.cache_config.prefix_cache_retention_interval
1352
+ ),
1353
)
1354
1355
# Determine how model runners should initialize the KV cache tensors.
@@ -1406,6 +1409,9 @@ def get_kv_cache_config_from_groups(
1409
num_blocks=num_blocks,
1410
kv_cache_tensors=kv_cache_tensors,
1411
kv_cache_groups=kv_cache_groups,
1412
+ prefix_cache_retention_interval=(
1413
+ vllm_config.cache_config.prefix_cache_retention_interval
1414
+ ),
1415
)
1416
1417
vllm/v1/kv_cache_interface.py
+2
@@ -970,6 +970,8 @@ class KVCacheConfig:
970
For models with multiple types of attention, there will be multiple groups,
971
see `_get_kv_cache_config_uniform_page_size` for more details.
972
"""
973
+ prefix_cache_retention_interval: int | None = None
974
+ """Resolved retention policy for local prefix-cache checkpoints."""
975
976
@property
977
def has_mamba_layers(self) -> bool:
vllm/v1/simple_kv_offload/manager.py
+3
-4
@@ -4,7 +4,7 @@
4
5
import contextlib
6
from collections.abc import Iterable
7
-from dataclasses import dataclass, field
7
+from dataclasses import dataclass, field, replace
8
from typing import TYPE_CHECKING, Any
9
10
from vllm.config import VllmConfig
@@ -190,7 +190,6 @@ class SimpleCPUOffloadScheduler:
190
"""Derive a CPU KVCacheConfig from the GPU config.
191
Same kv_cache_groups, num_blocks scaled by CPU/GPU memory ratio."""
192
# Import here to avoid potential circular imports
193
- from vllm.v1.kv_cache_interface import KVCacheConfig as KVCacheConfigCls
193
from vllm.v1.kv_cache_interface import KVCacheTensor
194
195
assert len(gpu_config.kv_cache_tensors) > 0
@@ -215,10 +214,10 @@ class SimpleCPUOffloadScheduler:
214
for t in gpu_config.kv_cache_tensors
215
]
216
218
- return KVCacheConfigCls(
217
+ return replace(
218
+ gpu_config,
219
num_blocks=num_cpu_blocks,
220
kv_cache_tensors=cpu_tensors,
221
- kv_cache_groups=gpu_config.kv_cache_groups,
221
)
222
223
@staticmethod