[KV Offloading] Per-request tier filtering with TierFilter/TierMatcher (#48123)
Signed-off-by: Ronen Schaffer <ronen.schaffer@ibm.com>
Ronen Schaffer committed
Jul 27, 2026 at 13:29 UTC
77cba0259f21dfc6f4f6298f54f3e43c9cc824c3
16 files changed
+301
-52
tests/v1/kv_connector/unit/offloading_connector/test_events.py
+11
-13
@@ -9,8 +9,6 @@ from tests.v1.kv_connector.unit.utils import create_vllm_config
9
from vllm.config import KVEventsConfig, KVTransferConfig
10
from vllm.distributed.kv_events import (
11
MEDIUM_CPU,
12
- MEDIUM_FS,
13
- MEDIUM_OBJ,
12
BlockRemoved,
13
BlockStored,
14
)
@@ -33,6 +31,7 @@ from vllm.v1.kv_cache_interface import (
31
)
32
from vllm.v1.kv_offload.base import (
33
Locality,
34
+ Medium,
35
OffloadingEvent,
36
OffloadingKVEventsConfig,
37
OffloadKey,
@@ -40,7 +39,7 @@ from vllm.v1.kv_offload.base import (
39
)
40
from vllm.v1.kv_offload.tiering.spec import TieringOffloadingSpec
41
43
-_CPU_MEDIUM = MEDIUM_CPU
42
+_CPU_MEDIUM = Medium.CPU
43
_FULL_ATTENTION_EVENT_SPEC = OffloadingEventGroupSpec(
44
kv_cache_spec_kind=KVCacheSpecKind.FULL_ATTENTION.value,
45
kv_cache_spec_sliding_window=None,
@@ -135,7 +134,7 @@ def _record_lookup_chunks(
134
135
def _stored_event(
136
keys: list[OffloadKey],
138
- medium: str = _CPU_MEDIUM,
137
+ medium: Medium = _CPU_MEDIUM,
138
locality: Locality | None = None,
139
) -> OffloadingEvent:
140
return OffloadingEvent(
@@ -148,7 +147,7 @@ def _stored_event(
147
148
def _removed_event(
149
keys: list[OffloadKey],
151
- medium: str = _CPU_MEDIUM,
150
+ medium: Medium = _CPU_MEDIUM,
151
locality: Locality | None = None,
152
) -> OffloadingEvent:
153
return OffloadingEvent(
@@ -181,7 +180,7 @@ def test_take_events_forwards_locality_to_rich_store():
180
181
events = list(
182
tracker.take_events(
184
- [_stored_event([key], locality=Locality.LOCAL, medium=MEDIUM_FS)]
183
+ [_stored_event([key], locality=Locality.LOCAL, medium=Medium.STORAGE)]
184
)
185
)
186
@@ -199,7 +198,7 @@ def test_take_events_forwards_locality_to_placeholder_store():
198
199
events = list(
200
tracker.take_events(
202
- [_stored_event([key], locality=Locality.REMOTE, medium=MEDIUM_FS)]
201
+ [_stored_event([key], locality=Locality.REMOTE, medium=Medium.STORAGE)]
202
)
203
)
204
@@ -216,7 +215,7 @@ def test_take_events_forwards_locality_to_remove():
215
216
events = list(
217
tracker.take_events(
219
- [_removed_event([key], locality=Locality.LOCAL, medium=MEDIUM_FS)]
218
+ [_removed_event([key], locality=Locality.LOCAL, medium=Medium.STORAGE)]
219
)
220
)
221
@@ -240,7 +239,7 @@ def test_take_events_publishes_routable_block_stored():
239
240
for i, event in enumerate(batch1):
241
assert isinstance(event, BlockStored)
243
- assert event.medium == _CPU_MEDIUM
242
+ assert event.medium == _CPU_MEDIUM.value
243
assert event.block_hashes == [_wire_hash(_hash(i))]
244
assert event.block_size == block_size
245
assert event.token_ids == list(
@@ -324,7 +323,7 @@ def test_lookup_promotion_factor_gt_1_store_and_remove():
323
assert len(removed) == 1
324
assert isinstance(removed[0], BlockRemoved)
325
assert removed[0].block_hashes == expected_hashes
327
- assert removed[0].medium == _CPU_MEDIUM
326
+ assert removed[0].medium == _CPU_MEDIUM.value
327
assert removed[0].group_idx == 0
328
assert not tracker._pending_event_metadata
329
@@ -444,12 +443,11 @@ def test_pending_cpu_removal_consumes_hit_backfill_until_next_hit():
443
]
444
445
447
-@pytest.mark.parametrize("medium", [MEDIUM_FS, MEDIUM_OBJ])
448
-def test_secondary_stored_event_does_not_mutate_cpu_metadata(medium: str):
446
+def test_secondary_stored_event_does_not_mutate_cpu_metadata():
447
tracker, _, _, key = _lookup_chunk()
448
expected_metadata = dict(tracker._pending_event_metadata)
449
452
- stored = list(tracker.take_events([_stored_event([key], medium)]))
450
+ stored = list(tracker.take_events([_stored_event([key], Medium.STORAGE)]))
451
assert stored[0].token_ids == [1, 2, 3, 4]
452
assert tracker._pending_event_metadata == expected_metadata
453
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py
+4
-3
@@ -33,6 +33,7 @@ from vllm.v1.kv_cache_interface import (
33
)
34
from vllm.v1.kv_offload.base import (
35
LookupResult,
36
+ Medium,
37
OffloadingEvent,
38
OffloadingManager,
39
OffloadPolicy,
@@ -299,7 +300,7 @@ def test_abort_before_hit_uses_placeholder_then_later_hit_heals_removal(
300
301
assert not tracker._pending_event_metadata
302
302
- raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=False))
303
+ raw_events.append(OffloadingEvent(keys=[key], medium=Medium.CPU, removed=False))
304
events = list(runner.connector_scheduler.take_events())
305
assert len(events) == 1
306
assert isinstance(events[0], BlockStored)
@@ -320,7 +321,7 @@ def test_abort_before_hit_uses_placeholder_then_later_hit_heals_removal(
321
)
322
assert key in tracker._pending_event_metadata
323
323
- raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=True))
324
+ raw_events.append(OffloadingEvent(keys=[key], medium=Medium.CPU, removed=True))
325
[event] = runner.connector_scheduler.take_events()
326
assert isinstance(event, BlockRemoved)
327
assert event.medium == MEDIUM_CPU
@@ -355,7 +356,7 @@ def test_promotion_hit_precedes_stored_event_translation(
356
raw_events: list[OffloadingEvent] = []
357
358
def lookup(key, req_context):
358
- raw_events.append(OffloadingEvent(keys=[key], medium=MEDIUM_CPU, removed=False))
359
+ raw_events.append(OffloadingEvent(keys=[key], medium=Medium.CPU, removed=False))
360
return LookupResult.HIT
361
362
def take_raw_events():
tests/v1/kv_offload/cpu/test_manager.py
+2
-2
@@ -6,10 +6,10 @@ from dataclasses import dataclass
6
import numpy as np
7
import pytest
8
9
-from vllm.distributed.kv_events import MEDIUM_CPU
9
from vllm.v1.kv_offload.base import (
10
LoadStoreSpec,
11
LookupResult,
12
+ Medium,
13
OffloadingEvent,
14
OffloadKey,
15
PrepareStoreOutput,
@@ -115,7 +115,7 @@ def verify_events(
115
stores: list[set[OffloadKey]] = []
116
evictions: list[set[OffloadKey]] = []
117
for event in events:
118
- assert event.medium == MEDIUM_CPU
118
+ assert event.medium == Medium.CPU
119
if event.removed:
120
evictions.append(set(event.keys))
121
else:
tests/v1/kv_offload/tiering/test_fs_tier.py
+3
-4
@@ -18,10 +18,10 @@ import numpy as np
18
import pytest
19
import torch
20
21
-from vllm.distributed.kv_events import MEDIUM_FS
21
from vllm.v1.kv_offload.base import (
22
Locality,
23
LookupResult,
24
+ Medium,
25
OffloadingEvent,
26
OffloadingKVEventsConfig,
27
OffloadKey,
@@ -514,8 +514,7 @@ def test_successful_store_emits_stored_event(fs_tier_with_events):
514
events = list(tier.take_events())
515
assert len(events) == 1
516
assert events[0].keys == keys
517
- # Literal medium pins the wire contract, not just the constant choice.
518
- assert events[0].medium == "FS"
517
+ assert events[0].medium == Medium.STORAGE
518
assert events[0].locality is Locality.LOCAL
519
assert not events[0].removed
520
# take_events drains the buffer.
@@ -685,7 +684,7 @@ def test_cascade_store_emits_fs_event_through_tiering_manager(tmp_path):
684
events.extend(manager.take_events())
685
time.sleep(0.01)
686
688
- fs_events = [e for e in events if e.medium == MEDIUM_FS]
687
+ fs_events = [e for e in events if e.medium == Medium.STORAGE]
688
assert len(fs_events) == 1
689
assert set(fs_events[0].keys) == set(keys)
690
assert not fs_events[0].removed
tests/v1/kv_offload/tiering/test_obj_tier.py
+2
-2
@@ -21,6 +21,7 @@ import torch
21
from vllm.v1.kv_offload.base import (
22
Locality,
23
LookupResult,
24
+ Medium,
25
OffloadingKVEventsConfig,
26
OffloadKey,
27
ReqContext,
@@ -473,8 +474,7 @@ class TestObjTierKVEvents:
474
events = list(self.tier.take_events())
475
assert len(events) == 1
476
assert events[0].keys == keys
476
- # Literal medium pins the wire contract, not just the constant choice.
477
- assert events[0].medium == "OBJ"
477
+ assert events[0].medium == Medium.STORAGE
478
assert events[0].locality is Locality.REMOTE
479
assert not events[0].removed
480
# take_events drains the buffer.
tests/v1/kv_offload/tiering/test_tiering_offloading.py
+136
-3
@@ -20,8 +20,13 @@ import torch
20
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
21
OffloadingConnectorStats,
22
)
23
+from vllm.distributed.kv_transfer.kv_connector.v1.offloading.scheduler import (
24
+ _parse_tier_filter,
25
+)
26
from vllm.v1.kv_offload.base import (
27
+ Locality,
28
LookupResult,
29
+ Medium,
30
OffloadingCounterMetadata,
31
OffloadingEvent,
32
OffloadKey,
@@ -29,6 +34,8 @@ from vllm.v1.kv_offload.base import (
34
ReqContext,
35
RequestOffloadingContext,
36
ScheduleEndContext,
37
+ TierFilter,
38
+ TierMatcher,
39
make_offload_key,
40
)
41
from vllm.v1.kv_offload.tiering.base import (
@@ -245,9 +252,9 @@ class TestTieringOffloadingManager:
252
self.manager.on_new_request(req_context)
253
254
def test_take_events_aggregates_tier_owned_events(self, manager_setup):
248
- primary_event = OffloadingEvent(to_keys([1]), "CPU", removed=False)
249
- secondary_event1 = OffloadingEvent(to_keys([2]), "tier-1", removed=False)
250
- secondary_event2 = OffloadingEvent(to_keys([3]), "tier-2", removed=True)
255
+ primary_event = OffloadingEvent(to_keys([1]), Medium.CPU, removed=False)
256
+ secondary_event1 = OffloadingEvent(to_keys([2]), Medium.STORAGE, removed=False)
257
+ secondary_event2 = OffloadingEvent(to_keys([3]), Medium.STORAGE, removed=True)
258
259
self.primary_tier.take_events = MagicMock(return_value=[primary_event])
260
self.secondary_tier1.take_events = MagicMock(return_value=[secondary_event1])
@@ -973,6 +980,56 @@ class TestTieringOffloadingManager:
980
self.secondary_tier2.drain_jobs.assert_called_once()
981
assert self.manager._transfer_jobs == {}
982
983
+ @pytest.mark.parametrize(
984
+ "load_tier_filter",
985
+ [
986
+ TierFilter(matchers=(TierMatcher(medium=Medium.STORAGE),)),
987
+ TierFilter(matchers=()),
988
+ ],
989
+ ids=["non_matching_medium", "empty_no_load"],
990
+ )
991
+ def test_tier_filter_skips_filtered_secondary(
992
+ self, manager_setup, load_tier_filter
993
+ ):
994
+ """Filter excluding secondary medium returns MISS from secondaries
995
+ even when they hold the block; primary is unaffected."""
996
+ blocks = to_keys(range(2))
997
+ # Put one block in primary, one only in secondary
998
+ self._start_request()
999
+ self.manager.prepare_store(blocks[:1], _CTX)
1000
+ self.manager.complete_store(blocks[:1], _CTX, success=True)
1001
+ self.secondary_tier1.blocks[blocks[1]] = True
1002
+
1003
+ # Secondaries have medium=CPU, so load_tier_filter skips them.
1004
+ self.secondary_tier1.lookup = MagicMock(wraps=self.secondary_tier1.lookup)
1005
+
1006
+ ctx = ReqContext(req_id="r1", load_tier_filter=load_tier_filter)
1007
+ assert self.manager.lookup(blocks[0], ctx) is LookupResult.HIT
1008
+ assert self.manager.lookup(blocks[1], ctx) is LookupResult.MISS
1009
+ self.secondary_tier1.lookup.assert_not_called()
1010
+
1011
+ @pytest.mark.parametrize(
1012
+ "load_tier_filter",
1013
+ [
1014
+ TierFilter.ALL,
1015
+ TierFilter(matchers=(TierMatcher(medium=Medium.CPU),)),
1016
+ TierFilter(matchers=(TierMatcher(),)),
1017
+ ],
1018
+ ids=["all", "explicit_cpu", "unconstrained_matcher"],
1019
+ )
1020
+ def test_tier_filter_allows_matching_secondary(
1021
+ self, manager_setup, load_tier_filter
1022
+ ):
1023
+ """Filter that matches the secondary's medium allows lookup."""
1024
+ blocks = to_keys(range(1))
1025
+ self.secondary_tier1.blocks[blocks[0]] = True
1026
+
1027
+ self.secondary_tier1.lookup = MagicMock(wraps=self.secondary_tier1.lookup)
1028
+
1029
+ ctx = ReqContext(req_id="r2", load_tier_filter=load_tier_filter)
1030
+ assert self.manager.lookup(blocks[0], ctx) is LookupResult.RETRY
1031
+ self.secondary_tier1.lookup.assert_called()
1032
+
1033
1034
class TestTieringOffloadingWithoutSecondaryTiers:
1035
"""Test TieringOffloadingManager with no secondary tiers (backward compat)."""
@@ -999,5 +1056,81 @@ class TestTieringOffloadingWithoutSecondaryTiers:
1056
assert count_hits(manager, blocks) == 3
1057
1058
1059
+@pytest.mark.parametrize(
1060
+ "raw,expected",
1061
+ [
1062
+ (
1063
+ [{"medium": "storage"}],
1064
+ TierFilter(matchers=(TierMatcher(medium=Medium.STORAGE),)),
1065
+ ),
1066
+ (
1067
+ [{"medium": "CPU"}],
1068
+ TierFilter(matchers=(TierMatcher(medium=Medium.CPU),)),
1069
+ ),
1070
+ (
1071
+ [{}],
1072
+ TierFilter(matchers=(TierMatcher(),)),
1073
+ ),
1074
+ (
1075
+ [{"medium": "storage", "locality": "local"}],
1076
+ TierFilter(
1077
+ matchers=(TierMatcher(medium=Medium.STORAGE, locality=Locality.LOCAL),)
1078
+ ),
1079
+ ),
1080
+ (
1081
+ [{"medium": "cpu"}, {"medium": "storage"}],
1082
+ TierFilter(
1083
+ matchers=(
1084
+ TierMatcher(medium=Medium.CPU),
1085
+ TierMatcher(medium=Medium.STORAGE),
1086
+ )
1087
+ ),
1088
+ ),
1089
+ (
1090
+ [],
1091
+ TierFilter(matchers=()),
1092
+ ),
1093
+ ],
1094
+ ids=[
1095
+ "medium_storage",
1096
+ "medium_cpu_uppercase",
1097
+ "unconstrained",
1098
+ "with_locality",
1099
+ "multiple_matchers",
1100
+ "empty_list_deny_all",
1101
+ ],
1102
+)
1103
+def test_parse_tier_filter_valid(raw, expected):
1104
+ assert _parse_tier_filter(raw) == expected
1105
+
1106
+
1107
+@pytest.mark.parametrize(
1108
+ "raw",
1109
+ [
1110
+ "not a list",
1111
+ [{"medium": "unknown"}],
1112
+ [{"locality": "nowhere"}],
1113
+ ],
1114
+ ids=["non_list", "invalid_medium", "invalid_locality"],
1115
+)
1116
+def test_parse_tier_filter_invalid_returns_all(raw):
1117
+ assert _parse_tier_filter(raw) is TierFilter.ALL
1118
+
1119
+
1120
+def test_parse_tier_filter_skips_bad_entries():
1121
+ result = _parse_tier_filter(
1122
+ [
1123
+ {"medium": "storage"},
1124
+ "not a dict",
1125
+ {"medium": "bogus"},
1126
+ {"medium": "cpu"},
1127
+ ]
1128
+ )
1129
+ assert result.matchers == (
1130
+ TierMatcher(medium=Medium.STORAGE),
1131
+ TierMatcher(medium=Medium.CPU),
1132
+ )
1133
+
1134
+
1135
if __name__ == "__main__":
1136
pytest.main([__file__, "-v"])
vllm/distributed/kv_events.py
+1
-2
@@ -44,8 +44,7 @@ class KVCacheEvent(
44
45
MEDIUM_GPU = "GPU"
46
MEDIUM_CPU = "CPU"
47
-MEDIUM_FS = "FS"
48
-MEDIUM_OBJ = "OBJ"
47
+MEDIUM_STORAGE = "STORAGE"
48
49
50
class BlockStored(KVCacheEvent):
vllm/distributed/kv_transfer/kv_connector/v1/offloading/events.py
+17
-5
@@ -19,7 +19,13 @@ from collections.abc import Iterable
19
from dataclasses import dataclass
20
from typing import TYPE_CHECKING, Any, NamedTuple
21
22
-from vllm.distributed.kv_events import BlockRemoved, BlockStored, KVCacheEvent
22
+from vllm.distributed.kv_events import (
23
+ MEDIUM_CPU,
24
+ MEDIUM_STORAGE,
25
+ BlockRemoved,
26
+ BlockStored,
27
+ KVCacheEvent,
28
+)
29
from vllm.logger import init_logger
30
from vllm.v1.core.kv_cache_utils import BlockHash, maybe_convert_block_hash
31
from vllm.v1.kv_cache_interface import (
@@ -28,6 +34,7 @@ from vllm.v1.kv_cache_interface import (
34
get_kv_cache_spec_sliding_window,
35
)
36
from vllm.v1.kv_offload.base import (
37
+ Medium,
38
OffloadingEvent,
39
OffloadingKVEventsConfig,
40
OffloadKey,
@@ -43,6 +50,11 @@ if TYPE_CHECKING:
50
51
logger = init_logger(__name__)
52
53
+_MEDIUM_TO_EVENT_STR: dict[Medium, str] = {
54
+ Medium.CPU: MEDIUM_CPU,
55
+ Medium.STORAGE: MEDIUM_STORAGE,
56
+}
57
+
58
59
class OffloadingEventGroupSpec(NamedTuple):
60
kv_cache_spec_kind: str | None
@@ -218,7 +230,7 @@ class OffloadingEventsTracker:
230
def _placeholder_stored(
231
self,
232
key: OffloadKey,
221
- medium: str,
233
+ medium: Medium,
234
locality: str | None,
235
) -> BlockStored:
236
return BlockStored(
@@ -229,7 +241,7 @@ class OffloadingEventsTracker:
241
token_ids=[],
242
lora_id=None,
243
block_size=0,
232
- medium=medium,
244
+ medium=_MEDIUM_TO_EVENT_STR[medium],
245
lora_name=None,
246
group_idx=get_offload_group_idx(key),
247
locality=locality,
@@ -267,7 +279,7 @@ class OffloadingEventsTracker:
279
token_ids=list(meta.token_ids),
280
block_size=meta.block_size,
281
lora_id=meta.lora_id,
270
- medium=event.medium,
282
+ medium=_MEDIUM_TO_EVENT_STR[event.medium],
283
lora_name=meta.lora_name,
284
extra_keys=(
285
list(meta.extra_keys) if meta.extra_keys is not None else None
@@ -308,7 +320,7 @@ class OffloadingEventsTracker:
320
for group_idx, hashes in by_group.items():
321
yield BlockRemoved(
322
block_hashes=hashes,
311
- medium=event.medium,
323
+ medium=_MEDIUM_TO_EVENT_STR[event.medium],
324
group_idx=group_idx,
325
locality=locality,
326
)
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+60
-1
@@ -39,7 +39,9 @@ from vllm.v1.kv_cache_interface import (
39
)
40
from vllm.v1.kv_offload.base import (
41
GPULoadStoreSpec,
42
+ Locality,
43
LookupResult,
44
+ Medium,
45
OffloadingManager,
46
OffloadingSpec,
47
OffloadKey,
@@ -47,6 +49,8 @@ from vllm.v1.kv_offload.base import (
49
ReqContext,
50
RequestOffloadingContext,
51
ScheduleEndContext,
52
+ TierFilter,
53
+ TierMatcher,
54
make_offload_key,
55
)
56
from vllm.v1.outputs import KVConnectorOutput
@@ -54,6 +58,10 @@ from vllm.v1.request import Request, RequestStatus
58
59
logger = init_logger(__name__)
60
61
+KV_LOAD_TIERS_KEY = "kv_load_tiers"
62
+MATCHER_MEDIUM_KEY = "medium"
63
+MATCHER_LOCALITY_KEY = "locality"
64
+
65
66
@dataclass(slots=True)
67
class TransferJobStatus:
@@ -376,10 +384,61 @@ class RequestOffloadState:
384
)
385
386
387
+def _parse_tier_filter(raw: Any) -> TierFilter:
388
+ """Parse raw kv_transfer_params tier matchers into a TierFilter."""
389
+ if not isinstance(raw, list):
390
+ logger.warning(
391
+ "_parse_tier_filter: expected list, got %s; ignoring",
392
+ type(raw).__name__,
393
+ )
394
+ return TierFilter.ALL
395
+ matchers: list[TierMatcher] = []
396
+ for entry in raw:
397
+ if not isinstance(entry, dict):
398
+ logger.warning("_parse_tier_filter: entry is not a dict; skipping")
399
+ continue
400
+ medium: Medium | None = None
401
+ locality: Locality | None = None
402
+ raw_medium = entry.get(MATCHER_MEDIUM_KEY)
403
+ if raw_medium is not None:
404
+ try:
405
+ medium = Medium(raw_medium.upper())
406
+ except (ValueError, AttributeError):
407
+ logger.warning(
408
+ "_parse_tier_filter: unknown medium %r; skipping entry",
409
+ raw_medium,
410
+ )
411
+ continue
412
+ raw_locality = entry.get(MATCHER_LOCALITY_KEY)
413
+ if raw_locality is not None:
414
+ try:
415
+ locality = Locality(raw_locality.upper())
416
+ except (ValueError, AttributeError):
417
+ logger.warning(
418
+ "_parse_tier_filter: unknown locality %r; skipping entry",
419
+ raw_locality,
420
+ )
421
+ continue
422
+ matchers.append(TierMatcher(medium=medium, locality=locality))
423
+ if not matchers:
424
+ if not raw: # input was [] — user explicitly wants nothing
425
+ return TierFilter(matchers=())
426
+ # all entries were invalid — fall back to ALL
427
+ return TierFilter.ALL
428
+ return TierFilter(matchers=tuple(matchers))
429
+
430
+
431
def _create_req_context(req: Request) -> ReqContext:
432
+ params = req.kv_transfer_params
433
+ load_filter = TierFilter.ALL
434
+ if params:
435
+ raw = params.get(KV_LOAD_TIERS_KEY)
436
+ if raw is not None:
437
+ load_filter = _parse_tier_filter(raw)
438
return ReqContext(
439
req_id=req.request_id,
382
- kv_transfer_params=req.kv_transfer_params,
440
+ kv_transfer_params=params,
441
+ load_tier_filter=load_filter,
442
)
443
444
vllm/v1/kv_offload/base.py
+46
-9
@@ -8,7 +8,7 @@ from abc import ABC, abstractmethod
8
from collections.abc import Collection, Iterable, Sequence
9
from dataclasses import dataclass, field
10
from enum import Enum, auto
11
-from typing import TYPE_CHECKING, Any, NamedTuple, NewType, TypeVar
11
+from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, NewType, TypeVar
12
13
import numpy as np
14
import torch
@@ -48,10 +48,54 @@ def get_offload_group_idx(key: OffloadKey) -> int:
48
_T = TypeVar("_T")
49
50
51
+class Medium(Enum):
52
+ """Storage medium of an offloading tier."""
53
+
54
+ CPU = "CPU"
55
+ STORAGE = "STORAGE"
56
+
57
+
58
+class Locality(Enum):
59
+ """Locality of a tier's storage relative to the publishing instance."""
60
+
61
+ LOCAL = "LOCAL"
62
+ REMOTE = "REMOTE"
63
+
64
+
65
+class TierMatcher(NamedTuple):
66
+ medium: Medium | None = None
67
+ locality: Locality | None = None
68
+
69
+ def matches(self, medium: Medium | None, locality: Locality | None) -> bool:
70
+ medium_matches = self.medium is None or medium is None or self.medium == medium
71
+ locality_matches = (
72
+ self.locality is None or locality is None or self.locality == locality
73
+ )
74
+ return medium_matches and locality_matches
75
+
76
+
77
+@dataclass(frozen=True)
78
+class TierFilter:
79
+ """Per-request filter controlling which tiers participate."""
80
+
81
+ matchers: tuple[TierMatcher, ...] = ()
82
+
83
+ ALL: ClassVar["TierFilter"]
84
+
85
+ def allows(self, medium: Medium | None, locality: Locality | None) -> bool:
86
+ if self is TierFilter.ALL:
87
+ return True
88
+ return any(m.matches(medium, locality) for m in self.matchers)
89
+
90
+
91
+TierFilter.ALL = TierFilter(matchers=(TierMatcher(),))
92
+
93
+
94
@dataclass
95
class ReqContext:
96
req_id: str
97
kv_transfer_params: dict[str, Any] | None = None
98
+ load_tier_filter: TierFilter = TierFilter.ALL
99
# Per-request scratch space keyed by value type, so a tier can parse
100
# kv_transfer_params once (in on_new_request) and read the result back
101
# on later calls for the same request.
@@ -110,17 +154,10 @@ class PrepareStoreOutput:
154
evicted_keys: list[OffloadKey]
155
156
113
-class Locality(Enum):
114
- """Locality of a tier's storage relative to the publishing instance."""
115
-
116
- LOCAL = "LOCAL"
117
- REMOTE = "REMOTE"
118
-
119
-
157
@dataclass
158
class OffloadingEvent:
159
keys: list[OffloadKey]
123
- medium: str
160
+ medium: Medium
161
# True if blocks are removed, False if stored
162
removed: bool
163
locality: Locality | None = None
vllm/v1/kv_offload/cpu/manager.py
+2
-2
@@ -6,13 +6,13 @@ from typing import Literal
6
7
from typing_extensions import override
8
9
-from vllm.distributed.kv_events import MEDIUM_CPU
9
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
10
OffloadingConnectorStats,
11
)
12
from vllm.v1.kv_offload.base import (
13
LoadStoreSpec,
14
LookupResult,
15
+ Medium,
16
OffloadingEvent,
17
OffloadingManager,
18
OffloadKey,
@@ -52,7 +52,7 @@ class CPUOffloadingManager(OffloadingManager):
52
store_threshold: int = 1,
53
max_tracker_size: int = 64_000,
54
):
55
- self.medium: str = MEDIUM_CPU
55
+ self.medium: Medium = Medium.CPU
56
self._num_blocks: int = num_blocks
57
self._num_allocated_blocks: int = 0
58
self._free_list: list[int] = []
vllm/v1/kv_offload/tiering/base.py
+7
-1
@@ -7,12 +7,14 @@ Abstract interfaces and data types for the secondary tiering layer.
7
from abc import ABC, abstractmethod
8
from collections.abc import Collection, Iterable
9
from dataclasses import dataclass
10
-from typing import TYPE_CHECKING, Any
10
+from typing import TYPE_CHECKING, Any, ClassVar
11
12
import numpy as np
13
14
from vllm.v1.kv_offload.base import (
15
+ Locality,
16
LookupResult,
17
+ Medium,
18
OffloadingEvent,
19
OffloadingMetricMetadata,
20
OffloadKey,
@@ -27,6 +29,7 @@ if TYPE_CHECKING:
29
)
30
from vllm.v1.kv_offload.base import OffloadingSpec
31
32
+
33
# Type alias for job IDs used in async transfer tracking
34
JobId = int
35
@@ -108,6 +111,8 @@ class SecondaryTierManager(ABC):
111
async jobs; get_finished_jobs() polls for completion.
112
"""
113
114
+ medium: ClassVar[Medium | None] = None
115
+
116
def __init__(
117
self,
118
offloading_spec: "OffloadingSpec",
@@ -124,6 +129,7 @@ class SecondaryTierManager(ABC):
129
self._offloading_spec = offloading_spec
130
self._primary_kv_view: memoryview = primary_kv_view
131
self.tier_type = tier_type
132
+ self.locality: Locality | None = None
133
134
@abstractmethod
135
def lookup(self, key: OffloadKey, req_context: ReqContext) -> LookupResult:
vllm/v1/kv_offload/tiering/example/manager.py
+4
-1
@@ -11,12 +11,13 @@ TieringOffloadingManager without requiring actual storage or network backends.
11
12
import logging
13
from collections.abc import Iterable
14
-from typing import TYPE_CHECKING
14
+from typing import TYPE_CHECKING, ClassVar
15
16
from typing_extensions import override
17
18
from vllm.v1.kv_offload.base import (
19
LookupResult,
20
+ Medium,
21
OffloadKey,
22
ReqContext,
23
RequestOffloadingContext,
@@ -42,6 +43,8 @@ class ExampleSecondaryTierManager(SecondaryTierManager):
43
- Completes transfers immediately (synchronous)
44
"""
45
46
+ medium: ClassVar[Medium] = Medium.CPU
47
+
48
def __init__(
49
self,
50
offloading_spec: "OffloadingSpec",
vllm/v1/kv_offload/tiering/fs/manager.py
+2
-2
@@ -30,11 +30,11 @@ except ImportError:
30
31
from typing_extensions import override
32
33
-from vllm.distributed.kv_events import MEDIUM_FS
33
from vllm.logger import init_logger
34
from vllm.v1.kv_offload.base import (
35
Locality,
36
LookupResult,
37
+ Medium,
38
OffloadingEvent,
39
OffloadKey,
40
ReqContext,
@@ -100,7 +100,7 @@ class FileSystemTierManager(SecondaryTierManager):
100
content.
101
"""
102
103
- medium: ClassVar[str] = MEDIUM_FS
103
+ medium: ClassVar[Medium] = Medium.STORAGE
104
105
def __init__(
106
self,
vllm/v1/kv_offload/tiering/manager.py
+2
@@ -324,6 +324,8 @@ class TieringOffloadingManager(OffloadingManager):
324
for tier in self.secondary_tiers:
325
if tier is exclude_tier:
326
continue
327
+ if not req_context.load_tier_filter.allows(tier.medium, tier.locality):
328
+ continue
329
result = tier.lookup(key, req_context)
330
if result is LookupResult.HIT:
331
promoted = self._initiate_promotion(tier, key, req_context)
vllm/v1/kv_offload/tiering/obj/manager.py
+2
-2
@@ -7,13 +7,13 @@ import time
7
from collections.abc import Iterable
8
from typing import TYPE_CHECKING, ClassVar, NamedTuple
9
10
-from vllm.distributed.kv_events import MEDIUM_OBJ
10
from vllm.distributed.nixl_utils import NixlWrapper as nixl_agent
11
from vllm.distributed.nixl_utils import nixl_agent_config
12
from vllm.logger import init_logger
13
from vllm.v1.kv_offload.base import (
14
Locality,
15
LookupResult,
16
+ Medium,
17
OffloadingEvent,
18
OffloadKey,
19
ReqContext,
@@ -98,7 +98,7 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
98
primary tier. Object keys are formed as ``{prefix}/{hash_shard}/{hash}.bin``.
99
"""
100
101
- medium: ClassVar[str] = MEDIUM_OBJ
101
+ medium: ClassVar[Medium] = Medium.STORAGE
102
103
def __init__(
104
self,