[KV Offload] Add optional tier locality to FS/OBJ KV events (#48281)
Signed-off-by: Change72 <changg@nvidia.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Chang Guo committed
Jul 17, 2026 at 00:31 UTC
426d48bfa149582664d48f89df21ec9beae5c37b
11 files changed
+326
-10
docs/features/kv_offloading_usage.md
+5
-1
@@ -83,9 +83,11 @@ Each entry in `secondary_tiers` is a dict with a required `type` field plus tier
83
84
The filesystem and object-store tiers can publish hash-only `BlockStored` KV events for blocks they successfully store, tagged with a stable per-tier `medium` (`FS` for the filesystem tier, `OBJ` for the object-store tier). Set `enable_kv_events: true` in the tier's entry to opt in; events are published only when KV cache events are also enabled globally via `--kv-events-config`.
85
86
+Set the optional `locality` tier field to `LOCAL` or `REMOTE` to describe the tier's storage location relative to the publishing vLLM instance. `LOCAL` marks storage local to that instance, while `REMOTE` marks storage that is not local to it. When the setting is omitted, locality is unspecified. vLLM does not infer it from the tier type, so an OBJ tier is not implicitly `REMOTE`. A KV event includes `locality` only when the tier explicitly configures it. This metadata describes the tier property without implying that a consumer can already route requests to its blocks.
87
+
88
### Filesystem (FS)
89
88
-The filesystem tier (`type: "fs"`) writes blocks to a directory on local storage.
90
+The filesystem tier (`type: "fs"`) writes blocks to a filesystem directory.
91
92
| Key | Required | Default | Notes |
93
| --- | --- | --- | --- |
@@ -94,6 +96,7 @@ The filesystem tier (`type: "fs"`) writes blocks to a directory on local storage
96
| `n_read_threads` | no | `16` | Read-priority I/O threads (load path). |
97
| `n_write_threads` | no | `16` | Write-priority I/O threads (store path). |
98
| `enable_kv_events` | no | `false` | Publish `BlockStored` KV events (medium `FS`) for successfully stored blocks. Requires KV cache events to be enabled globally. |
99
+| `locality` | no | unspecified | `LOCAL` or `REMOTE` relative to the publishing vLLM instance. Included in the tier's KV events only when explicitly configured. |
100
101
Each thread group prefers its own queue but pulls from the other when its primary queue is empty, so a write-heavy or read-heavy burst won't leave the off-priority queue waiting. Size the totals to your storage's effective concurrency.
102
@@ -134,6 +137,7 @@ The object-store tier (`type: "obj"`) offloads blocks to an S3-compatible object
137
| `prefix` | no | `""` | Key prefix prepended to all object keys. |
138
| `io_threads` | no | `4` | Number of NIXL OBJ backend I/O threads. |
139
| `enable_kv_events` | no | `false` | Publish `BlockStored` KV events (medium `OBJ`) for successfully stored blocks. Requires KV cache events to be enabled globally. |
140
+| `locality` | no | unspecified | `LOCAL` or `REMOTE` relative to the publishing vLLM instance. Included in the tier's KV events only when explicitly configured; OBJ does not imply `REMOTE`. |
141
142
`store_config` fields:
143
examples/features/kv_events/kv_events_subscriber.py
+4
@@ -42,12 +42,16 @@ class BlockStored(KVCacheEvent):
42
"""
43
44
group_idx: int | None = None
45
+ kv_cache_spec_kind: str | None = None
46
+ kv_cache_spec_sliding_window: int | None = None
47
+ locality: str | None = None
48
49
50
class BlockRemoved(KVCacheEvent):
51
block_hashes: list[ExternalBlockHash]
52
medium: str | None
53
group_idx: int | None = None
54
+ locality: str | None = None
55
56
57
class AllBlocksCleared(KVCacheEvent):
tests/distributed/test_kv_cache_events.py
+99
@@ -1,6 +1,9 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from typing import Any
5
+
6
+import msgspec
7
import pytest
8
9
from vllm.distributed.kv_events import BlockRemoved, BlockStored
@@ -9,9 +12,44 @@ from vllm.distributed.kv_events import BlockRemoved, BlockStored
12
_FAKE_HASH: bytes = b"\xab" * 32
13
14
15
+class _LegacyBlockStored(
16
+ msgspec.Struct,
17
+ omit_defaults=True, # type: ignore[call-arg]
18
+ gc=False, # type: ignore[call-arg]
19
+ tag="BlockStored", # type: ignore[call-arg]
20
+):
21
+ """BlockStored wire schema before locality was added."""
22
+
23
+ block_hashes: list[bytes]
24
+ parent_block_hash: bytes | None
25
+ token_ids: list[int]
26
+ block_size: int
27
+ lora_id: int | None
28
+ medium: str | None
29
+ lora_name: str | None
30
+ extra_keys: list[tuple[Any, ...] | None] | None = None
31
+ group_idx: int | None = None
32
+ kv_cache_spec_kind: str | None = None
33
+ kv_cache_spec_sliding_window: int | None = None
34
+
35
+
36
+class _LegacyBlockRemoved(
37
+ msgspec.Struct,
38
+ omit_defaults=True, # type: ignore[call-arg]
39
+ gc=False, # type: ignore[call-arg]
40
+ tag="BlockRemoved", # type: ignore[call-arg]
41
+):
42
+ """BlockRemoved wire schema before locality was added."""
43
+
44
+ block_hashes: list[bytes]
45
+ medium: str | None
46
+ group_idx: int | None = None
47
+
48
+
49
def _make_block_stored(
50
group_idx: int | None = None,
51
kv_cache_spec_sliding_window: int | None = None,
52
+ locality: str | None = None,
53
) -> BlockStored:
54
return BlockStored(
55
block_hashes=[_FAKE_HASH],
@@ -23,16 +61,19 @@ def _make_block_stored(
61
lora_name=None,
62
group_idx=group_idx,
63
kv_cache_spec_sliding_window=kv_cache_spec_sliding_window,
64
+ locality=locality,
65
)
66
67
68
def _make_block_removed(
69
group_idx: int | None = None,
70
+ locality: str | None = None,
71
) -> BlockRemoved:
72
return BlockRemoved(
73
block_hashes=[_FAKE_HASH],
74
medium="GPU",
75
group_idx=group_idx,
76
+ locality=locality,
77
)
78
79
@@ -84,3 +125,61 @@ def test_block_stored_hash_differs_by_sliding_window():
125
event_a = _make_block_stored(group_idx=1, kv_cache_spec_sliding_window=128)
126
event_b = _make_block_stored(group_idx=1, kv_cache_spec_sliding_window=256)
127
assert hash(event_a) != hash(event_b)
128
+
129
+
130
+@pytest.mark.parametrize(
131
+ ("event_a", "event_b"),
132
+ [
133
+ (
134
+ _make_block_stored(locality="LOCAL"),
135
+ _make_block_stored(locality="REMOTE"),
136
+ ),
137
+ (
138
+ _make_block_removed(locality="LOCAL"),
139
+ _make_block_removed(locality="REMOTE"),
140
+ ),
141
+ ],
142
+)
143
+def test_event_hash_differs_by_locality(
144
+ event_a: BlockStored | BlockRemoved,
145
+ event_b: BlockStored | BlockRemoved,
146
+):
147
+ assert hash(event_a) != hash(event_b)
148
+
149
+
150
+def test_block_stored_locality_is_wire_compatible():
151
+ legacy = _LegacyBlockStored(
152
+ block_hashes=[_FAKE_HASH],
153
+ parent_block_hash=None,
154
+ token_ids=[1, 2, 3, 4],
155
+ block_size=4,
156
+ lora_id=None,
157
+ medium="GPU",
158
+ lora_name=None,
159
+ group_idx=2,
160
+ kv_cache_spec_sliding_window=128,
161
+ )
162
+ legacy_payload = msgspec.msgpack.encode(legacy)
163
+ assert (
164
+ msgspec.msgpack.encode(
165
+ _make_block_stored(
166
+ group_idx=2,
167
+ kv_cache_spec_sliding_window=128,
168
+ )
169
+ )
170
+ == legacy_payload
171
+ )
172
+ assert msgspec.msgpack.decode(legacy_payload, type=BlockStored).locality is None
173
+ new_payload = msgspec.msgpack.encode(_make_block_stored(locality="LOCAL"))
174
+ assert msgspec.msgpack.decode(new_payload)["locality"] == "LOCAL"
175
+ assert msgspec.msgpack.decode(new_payload, type=_LegacyBlockStored).medium == "GPU"
176
+
177
+
178
+def test_block_removed_locality_is_wire_compatible():
179
+ legacy = _LegacyBlockRemoved(block_hashes=[_FAKE_HASH], medium="GPU")
180
+ legacy_payload = msgspec.msgpack.encode(legacy)
181
+ assert msgspec.msgpack.encode(_make_block_removed()) == legacy_payload
182
+ assert msgspec.msgpack.decode(legacy_payload, type=BlockRemoved).locality is None
183
+ new_payload = msgspec.msgpack.encode(_make_block_removed(locality="REMOTE"))
184
+ assert msgspec.msgpack.decode(new_payload)["locality"] == "REMOTE"
185
+ assert msgspec.msgpack.decode(new_payload, type=_LegacyBlockRemoved).medium == "GPU"
tests/v1/kv_connector/unit/offloading_connector/test_events.py
+75
-5
@@ -7,7 +7,7 @@ import torch
7
8
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 MEDIUM_CPU, BlockRemoved, BlockStored
10
+from vllm.distributed.kv_events import MEDIUM_CPU, MEDIUM_FS, BlockRemoved, BlockStored
11
from vllm.distributed.kv_transfer.kv_connector.v1.offloading.config import (
12
build_offloading_config,
13
)
@@ -26,6 +26,7 @@ from vllm.v1.kv_cache_interface import (
26
KVCacheSpecKind,
27
)
28
from vllm.v1.kv_offload.base import (
29
+ Locality,
30
OffloadingEvent,
31
OffloadingKVEventsConfig,
32
OffloadKey,
@@ -103,12 +104,81 @@ def _record_chunks(
104
return keys
105
106
106
-def _stored_event(keys: list[OffloadKey]) -> OffloadingEvent:
107
- return OffloadingEvent(keys=keys, medium=_CPU_MEDIUM, removed=False)
107
+def _stored_event(
108
+ keys: list[OffloadKey],
109
+ locality: Locality | None = None,
110
+ medium: str = _CPU_MEDIUM,
111
+) -> OffloadingEvent:
112
+ return OffloadingEvent(
113
+ keys=keys,
114
+ medium=medium,
115
+ removed=False,
116
+ locality=locality,
117
+ )
118
+
119
+
120
+def _removed_event(
121
+ keys: list[OffloadKey],
122
+ locality: Locality | None = None,
123
+ medium: str = _CPU_MEDIUM,
124
+) -> OffloadingEvent:
125
+ return OffloadingEvent(
126
+ keys=keys,
127
+ medium=medium,
128
+ removed=True,
129
+ locality=locality,
130
+ )
131
+
132
+
133
+def test_take_events_forwards_locality_to_rich_store():
134
+ tracker = _tracker()
135
+ req = _request(block_hashes=[_hash(0)], token_count=4)
136
+ key = _record_chunks(tracker, req, _group_config(), num_chunks=1)[0]
137
+
138
+ events = list(
139
+ tracker.take_events(
140
+ [_stored_event([key], locality=Locality.LOCAL, medium=MEDIUM_FS)]
141
+ )
142
+ )
143
144
+ assert len(events) == 1
145
+ assert isinstance(events[0], BlockStored)
146
+ assert events[0].token_ids == [1, 2, 3, 4]
147
+ assert events[0].block_size == 4
148
+ assert events[0].locality == "LOCAL"
149
110
-def _removed_event(keys: list[OffloadKey]) -> OffloadingEvent:
111
- return OffloadingEvent(keys=keys, medium=_CPU_MEDIUM, removed=True)
150
+
151
+def test_take_events_forwards_locality_to_placeholder_store():
152
+ tracker = _tracker(self_describing_kv_events=False)
153
+ req = _request(block_hashes=[_hash(0)], token_count=4)
154
+ key = _record_chunks(tracker, req, _group_config(), num_chunks=1)[0]
155
+
156
+ events = list(
157
+ tracker.take_events(
158
+ [_stored_event([key], locality=Locality.REMOTE, medium=MEDIUM_FS)]
159
+ )
160
+ )
161
+
162
+ assert len(events) == 1
163
+ assert isinstance(events[0], BlockStored)
164
+ assert events[0].block_size == 0
165
+ assert events[0].locality == "REMOTE"
166
+
167
+
168
+def test_take_events_forwards_locality_to_remove():
169
+ tracker = _tracker()
170
+ req = _request(block_hashes=[_hash(0)], token_count=4)
171
+ key = _record_chunks(tracker, req, _group_config(), num_chunks=1)[0]
172
+
173
+ events = list(
174
+ tracker.take_events(
175
+ [_removed_event([key], locality=Locality.LOCAL, medium=MEDIUM_FS)]
176
+ )
177
+ )
178
+
179
+ assert len(events) == 1
180
+ assert isinstance(events[0], BlockRemoved)
181
+ assert events[0].locality == "LOCAL"
182
183
184
def test_take_events_publishes_routable_block_stored():
tests/v1/kv_offload/tiering/test_fs_tier.py
+64
@@ -20,6 +20,7 @@ import torch
20
21
from vllm.distributed.kv_events import MEDIUM_FS
22
from vllm.v1.kv_offload.base import (
23
+ Locality,
24
LookupResult,
25
OffloadingEvent,
26
OffloadingKVEventsConfig,
@@ -35,6 +36,7 @@ from vllm.v1.kv_offload.config import (
36
OffloadingParallelConfig,
37
)
38
from vllm.v1.kv_offload.tiering.base import JobMetadata
39
+from vllm.v1.kv_offload.tiering.factory import SecondaryTierFactory
40
from vllm.v1.kv_offload.tiering.fs.manager import (
41
FileSystemTierManager,
42
)
@@ -186,6 +188,7 @@ def fs_tier_with_events(tmp_path):
188
n_read_threads=4,
189
n_write_threads=4,
190
enable_kv_events=True,
191
+ locality="LOCAL",
192
)
193
yield tier
194
tier.shutdown()
@@ -251,6 +254,40 @@ def test_invalid_path_raises_at_construction():
254
)
255
256
257
+@pytest.mark.parametrize("locality", ["local", ""])
258
+def test_invalid_locality_raises_at_construction(tmp_path, locality):
259
+ tensor = _page_aligned_zero_tensor(4, _BLOCK_ELEMENTS)
260
+
261
+ with pytest.raises(ValueError, match="Locality"):
262
+ FileSystemTierManager(
263
+ offloading_spec=_MOCK_OFFLOADING_SPEC,
264
+ primary_kv_view=memoryview(tensor.numpy()),
265
+ tier_type="fs",
266
+ root_dir=str(tmp_path),
267
+ locality=locality,
268
+ )
269
+
270
+
271
+def test_factory_forwards_locality_to_fs_tier(tmp_path):
272
+ tensor = _page_aligned_zero_tensor(4, _BLOCK_ELEMENTS)
273
+ tier = SecondaryTierFactory.create_secondary_tier(
274
+ {
275
+ "type": "fs",
276
+ "root_dir": str(tmp_path),
277
+ "n_read_threads": 1,
278
+ "n_write_threads": 1,
279
+ "locality": "LOCAL",
280
+ },
281
+ memoryview(tensor.numpy()),
282
+ _MOCK_OFFLOADING_SPEC,
283
+ )
284
+ try:
285
+ assert isinstance(tier, FileSystemTierManager)
286
+ assert tier.locality is Locality.LOCAL
287
+ finally:
288
+ tier.shutdown()
289
+
290
+
291
def test_failed_load_missing_file(fs_tier):
292
"""Test that loading a block whose file does not exist results in a failed job."""
293
tier, _ = fs_tier
@@ -442,11 +479,38 @@ def test_successful_store_emits_stored_event(fs_tier_with_events):
479
assert events[0].keys == keys
480
# Literal medium pins the wire contract, not just the constant choice.
481
assert events[0].medium == "FS"
482
+ assert events[0].locality is Locality.LOCAL
483
assert not events[0].removed
484
# take_events drains the buffer.
485
assert list(tier.take_events()) == []
486
487
488
+@pytest.mark.parametrize(
489
+ ("locality", "expected"),
490
+ [(None, None), ("REMOTE", Locality.REMOTE)],
491
+)
492
+def test_store_event_uses_configured_locality(tmp_path, locality, expected):
493
+ tensor = _page_aligned_zero_tensor(4, _BLOCK_ELEMENTS)
494
+ locality_config = {} if locality is None else {"locality": locality}
495
+ tier = FileSystemTierManager(
496
+ offloading_spec=_make_offloading_spec(enable_kv_cache_events=True),
497
+ primary_kv_view=memoryview(tensor.numpy()),
498
+ tier_type="fs",
499
+ root_dir=str(tmp_path),
500
+ enable_kv_events=True,
501
+ **locality_config,
502
+ )
503
+ try:
504
+ tier.submit_store(make_job(1, [key(1)], [0]))
505
+ assert all(r.success for r in drain(tier))
506
+
507
+ events = list(tier.take_events())
508
+ assert len(events) == 1
509
+ assert events[0].locality is expected
510
+ finally:
511
+ tier.shutdown()
512
+
513
+
514
def test_load_job_emits_no_event(fs_tier_with_events):
515
tier = fs_tier_with_events
516
tier.submit_store(make_job(1, [key(1)], [0]))
tests/v1/kv_offload/tiering/test_obj_tier.py
+31
@@ -15,9 +15,11 @@ from types import SimpleNamespace
15
from unittest.mock import MagicMock, patch
16
17
import numpy as np
18
+import pytest
19
import torch
20
21
from vllm.v1.kv_offload.base import (
22
+ Locality,
23
LookupResult,
24
OffloadingKVEventsConfig,
25
OffloadKey,
@@ -265,6 +267,12 @@ def lookup_and_wait(
267
# ---------------------------------------------------------------------------
268
269
270
+@pytest.mark.parametrize("locality", ["local", ""])
271
+def test_invalid_locality_raises_at_construction(locality):
272
+ with pytest.raises(ValueError, match="Locality"):
273
+ _make_tier(locality=locality)
274
+
275
+
276
class TestMockObjTierBasic:
277
def setup_method(self):
278
self.tier, self.agent = _make_tier(num_blocks=4)
@@ -453,6 +461,7 @@ class TestObjTierKVEvents:
461
self.tier, self.agent = _make_tier(
462
offloading_spec=_make_events_spec(enable_kv_cache_events=True),
463
enable_kv_events=True,
464
+ locality="REMOTE",
465
)
466
467
def test_successful_store_emits_stored_event(self):
@@ -466,10 +475,32 @@ class TestObjTierKVEvents:
475
assert events[0].keys == keys
476
# Literal medium pins the wire contract, not just the constant choice.
477
assert events[0].medium == "OBJ"
478
+ assert events[0].locality is Locality.REMOTE
479
assert not events[0].removed
480
# take_events drains the buffer.
481
assert list(self.tier.take_events()) == []
482
483
+ @pytest.mark.parametrize(
484
+ ("locality", "expected"),
485
+ [(None, None), ("LOCAL", Locality.LOCAL)],
486
+ )
487
+ def test_store_event_uses_configured_locality(self, locality, expected):
488
+ locality_config = {} if locality is None else {"locality": locality}
489
+ tier, _ = _make_tier(
490
+ offloading_spec=_make_events_spec(enable_kv_cache_events=True),
491
+ enable_kv_events=True,
492
+ **locality_config,
493
+ )
494
+ try:
495
+ tier.submit_store(make_job(1, [key(1)], [0]))
496
+ assert all(r.success for r in drain(tier))
497
+
498
+ events = list(tier.take_events())
499
+ assert len(events) == 1
500
+ assert events[0].locality is expected
501
+ finally:
502
+ tier.shutdown()
503
+
504
def test_mixed_job_results_emit_event_only_for_successful_job(self):
505
"""With a failed and a successful store job resolving in the same
506
poll, exactly one event is emitted and its keys belong to the
vllm/distributed/kv_events.py
+6
@@ -74,6 +74,8 @@ class BlockStored(KVCacheEvent):
74
# filter groups as they are learned. Remove events only need group_idx+hash.
75
kv_cache_spec_kind: str | None = None
76
kv_cache_spec_sliding_window: int | None = None
77
+ locality: str | None = None
78
+ """LOCAL or REMOTE relative to the publisher; None means unspecified."""
79
80
def __hash__(self) -> int:
81
return hash(
@@ -88,6 +90,7 @@ class BlockStored(KVCacheEvent):
90
self.group_idx,
91
self.kv_cache_spec_kind,
92
self.kv_cache_spec_sliding_window,
93
+ self.locality,
94
)
95
)
96
@@ -96,6 +99,8 @@ class BlockRemoved(KVCacheEvent):
99
block_hashes: list[ExternalBlockHash]
100
medium: str | None
101
group_idx: int | None = None
102
+ locality: str | None = None
103
+ """LOCAL or REMOTE relative to the publisher; None means unspecified."""
104
105
def __hash__(self) -> int:
106
return hash(
@@ -103,6 +108,7 @@ class BlockRemoved(KVCacheEvent):
108
tuple(self.block_hashes),
109
self.medium,
110
self.group_idx,
111
+ self.locality,
112
)
113
)
114
vllm/distributed/kv_transfer/kv_connector/v1/offloading/events.py
+12
-2
@@ -198,7 +198,12 @@ class OffloadingEventsTracker:
198
kv_cache_spec=group_config.kv_event_group_spec,
199
)
200
201
- def _placeholder_stored(self, key: OffloadKey, medium: str) -> BlockStored:
201
+ def _placeholder_stored(
202
+ self,
203
+ key: OffloadKey,
204
+ medium: str,
205
+ locality: str | None,
206
+ ) -> BlockStored:
207
return BlockStored(
208
block_hashes=[
209
maybe_convert_block_hash(BlockHash(get_offload_block_hash(key)))
@@ -210,12 +215,14 @@ class OffloadingEventsTracker:
215
medium=medium,
216
lora_name=None,
217
group_idx=get_offload_group_idx(key),
218
+ locality=locality,
219
)
220
221
def _take_stored_event(self, event: OffloadingEvent) -> Iterable[KVCacheEvent]:
222
# Metadata is read, NOT popped: the entry must survive until the
223
# eviction event so BlockRemoved can fan out to the same hashes.
224
# Events are self-contained (own parent), so key order is free.
225
+ locality = event.locality.value if event.locality is not None else None
226
for key in event.keys:
227
meta = self._pending_event_metadata.get(key)
228
if meta is None:
@@ -227,7 +234,7 @@ class OffloadingEventsTracker:
234
"placeholder payload. Expected for non-full-attention "
235
"groups; otherwise indicates a missing populate path."
236
)
230
- yield self._placeholder_stored(key, event.medium)
237
+ yield self._placeholder_stored(key, event.medium, locality)
238
continue
239
240
yield BlockStored(
@@ -252,10 +259,12 @@ class OffloadingEventsTracker:
259
kv_cache_spec_sliding_window=(
260
meta.kv_cache_spec.kv_cache_spec_sliding_window
261
),
262
+ locality=locality,
263
)
264
265
def _take_removed_event(self, event: OffloadingEvent) -> Iterable[KVCacheEvent]:
266
# Keep group_idx unambiguous if a manager batch spans groups.
267
+ locality = event.locality.value if event.locality is not None else None
268
by_group: dict[int, list] = {}
269
for key in event.keys:
270
meta = self._pending_event_metadata.pop(key, None)
@@ -283,4 +292,5 @@ class OffloadingEventsTracker:
292
block_hashes=hashes,
293
medium=event.medium,
294
group_idx=group_idx,
295
+ locality=locality,
296
)
vllm/v1/kv_offload/base.py
+8
@@ -97,12 +97,20 @@ class PrepareStoreOutput:
97
evicted_keys: list[OffloadKey]
98
99
100
+class Locality(Enum):
101
+ """Locality of a tier's storage relative to the publishing instance."""
102
+
103
+ LOCAL = "LOCAL"
104
+ REMOTE = "REMOTE"
105
+
106
+
107
@dataclass
108
class OffloadingEvent:
109
keys: list[OffloadKey]
110
medium: str
111
# True if blocks are removed, False if stored
112
removed: bool
113
+ locality: Locality | None = None
114
115
116
"""
vllm/v1/kv_offload/tiering/fs/manager.py
+11
-1
@@ -33,6 +33,7 @@ from typing_extensions import override
33
from vllm.distributed.kv_events import MEDIUM_FS
34
from vllm.logger import init_logger
35
from vllm.v1.kv_offload.base import (
36
+ Locality,
37
LookupResult,
38
OffloadingEvent,
39
OffloadKey,
@@ -110,6 +111,7 @@ class FileSystemTierManager(SecondaryTierManager):
111
n_read_threads: int = 16,
112
n_write_threads: int = 16,
113
enable_kv_events: bool = False,
114
+ locality: str | None = None,
115
):
116
"""
117
Args:
@@ -123,8 +125,11 @@ class FileSystemTierManager(SecondaryTierManager):
125
enable_kv_events: Emit BlockStored KV events for blocks
126
successfully stored to this tier. Effective only when KV
127
cache events are enabled globally (kv_events_config).
128
+ locality: Whether this tier's storage is LOCAL or REMOTE relative
129
+ to the publishing vLLM instance.
130
"""
131
super().__init__(offloading_spec, primary_kv_view, tier_type)
132
+ self.locality = Locality(locality) if locality is not None else None
133
134
self.events: list[OffloadingEvent] | None = None
135
if enable_kv_events:
@@ -223,7 +228,12 @@ class FileSystemTierManager(SecondaryTierManager):
228
keys = self._store_job_keys.pop(job_id, None)
229
if success and keys:
230
self.events.append(
226
- OffloadingEvent(keys=keys, medium=self.medium, removed=False)
231
+ OffloadingEvent(
232
+ keys=keys,
233
+ medium=self.medium,
234
+ removed=False,
235
+ locality=self.locality,
236
+ )
237
)
238
results.append(JobResult(job_id=job_id, success=success))
239
return results
vllm/v1/kv_offload/tiering/obj/manager.py
+11
-1
@@ -12,6 +12,7 @@ from vllm.distributed.nixl_utils import NixlWrapper as nixl_agent
12
from vllm.distributed.nixl_utils import nixl_agent_config
13
from vllm.logger import init_logger
14
from vllm.v1.kv_offload.base import (
15
+ Locality,
16
LookupResult,
17
OffloadingEvent,
18
OffloadKey,
@@ -108,6 +109,7 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
109
prefix: str = "",
110
io_threads: int = 4,
111
enable_kv_events: bool = False,
112
+ locality: str | None = None,
113
):
114
"""
115
Args:
@@ -120,8 +122,11 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
122
enable_kv_events: Emit BlockStored KV events for blocks
123
successfully stored to this tier. Effective only when KV
124
cache events are enabled globally (kv_events_config).
125
+ locality: Whether this tier's storage is LOCAL or REMOTE relative
126
+ to the publishing vLLM instance.
127
"""
128
super().__init__(offloading_spec, primary_kv_view, tier_type)
129
+ self.locality = Locality(locality) if locality is not None else None
130
131
self.events: list[OffloadingEvent] | None = None
132
if enable_kv_events:
@@ -323,7 +328,12 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
328
keys = self._store_job_keys.pop(result.job_id, None)
329
if result.success and keys:
330
self.events.append(
326
- OffloadingEvent(keys=keys, medium=self.medium, removed=False)
331
+ OffloadingEvent(
332
+ keys=keys,
333
+ medium=self.medium,
334
+ removed=False,
335
+ locality=self.locality,
336
+ )
337
)
338
return results
339