[KV Offload] Add KVCR secondary-tier adapter (#53624)
Signed-off-by: Moein Khazraee <moein@nvidia.com> Signed-off-by: Moein <moein@nvidia.com> Co-authored-by: Olga Andreeva <124622579+oandreeva-nv@users.noreply.github.com> Co-authored-by: Olga Andreeva <oandreeva@nvidia.com>
Moein Khazraee committed
Sep 14, 2026 at 23:55 UTC
000c7df9ffd3e470980fd4cd6b8ec1b0585500ff
7 files changed
+1211
-1
.buildkite/scripts/install-kv-offload.sh
new
+18
@@ -0,0 +1,18 @@
1
+#!/bin/bash
2
+# SPDX-License-Identifier: Apache-2.0
3
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4
+
5
+set -euo pipefail
6
+
7
+if [ -z "${CUDA_VERSION:-}" ]; then
8
+ exit 0
9
+fi
10
+
11
+# TODO: Use the PyPI wheel once it is published.
12
+uv pip install --system \
13
+ "kvcr @ git+https://github.com/ai-dynamo/kvcr.git@v0.1.0"
14
+
15
+# Keep only the NIXL wheel matching the CI image's CUDA runtime.
16
+NIXL_VERSION=$(uv pip show --system nixl | sed -n 's/^Version: //p')
17
+uv pip uninstall --system nixl-cu12 nixl-cu13
18
+uv pip install --system --no-deps "nixl-cu${CUDA_VERSION%%.*}==${NIXL_VERSION}"
.buildkite/test_areas/misc.yaml
+1
@@ -130,6 +130,7 @@ steps:
130
source_file_dependencies: *v1-core-kv-metrics-deps
131
commands:
132
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
133
+ - bash /vllm-workspace/.buildkite/scripts/install-kv-offload.sh
134
- pytest -v -s v1/kv_offload
135
- pytest -v -s v1/simple_kv_offload
136
mirror:
tests/v1/kv_offload/tiering/test_factory.py
+5
-1
@@ -10,6 +10,7 @@ These tests verify:
10
3. Error paths — missing tier_type, unknown tier_type, duplicate registration.
11
"""
12
13
+from importlib.util import find_spec
14
from unittest.mock import MagicMock
15
16
import pytest
@@ -42,8 +43,11 @@ def _make_mock_args():
43
44
45
def test_pre_registered_tiers_can_be_imported():
45
- """CI sentinel: example/fs/obj paths must import and yield SecondaryTierManager."""
46
+ """CI sentinel: registered tiers import and yield SecondaryTierManager."""
47
for tier_type in SecondaryTierFactory._registry:
48
+ # KVCR is an optional external dependency and may not be installed.
49
+ if tier_type == "kvcr" and find_spec("kvcr") is None:
50
+ continue
51
cls = SecondaryTierFactory._registry[tier_type]()
52
assert issubclass(cls, SecondaryTierManager)
53
tests/v1/kv_offload/tiering/test_kvcr_tier.py
new
+544
@@ -0,0 +1,544 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Collection, Iterable, Mapping
5
+from types import SimpleNamespace
6
+from typing import Any
7
+
8
+import numpy as np
9
+import pytest
10
+
11
+pytest.importorskip("kvcr")
12
+
13
+from kvcr import ROUTER_HINT_KEY, KVCRBindings
14
+from kvcr.config import G3Options, KVCRBackendConfigs, KVCRConfig, KVCRGuardConfig
15
+from kvcr.policy import FIFOPolicy, G3FIFOPolicy, G3LRUPolicy, LRUPolicy
16
+from kvcr.types import (
17
+ BlockKey,
18
+ CacheTier,
19
+ InventoryEvent,
20
+ MemDescriptor,
21
+ OpEntryResult,
22
+ OpEntryStatus,
23
+ OpHandle,
24
+ QueryStatus,
25
+)
26
+
27
+from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
28
+ OffloadingConnectorStats,
29
+)
30
+from vllm.v1.kv_offload.base import (
31
+ LookupResult,
32
+ Medium,
33
+ OffloadKey,
34
+ ReqContext,
35
+ make_offload_key,
36
+)
37
+from vllm.v1.kv_offload.tiering.base import JobResult, TransferJob
38
+from vllm.v1.kv_offload.tiering.kvcr import manager as kvcr_manager
39
+from vllm.v1.kv_offload.tiering.kvcr.manager import KVCRSecondaryTierManager
40
+
41
+
42
+def _op_entries(
43
+ entries: Mapping[BlockKey, bool],
44
+) -> dict[BlockKey, OpEntryResult]:
45
+ return {
46
+ key: OpEntryResult(OpEntryStatus.SUCCESS if success else OpEntryStatus.FAILED)
47
+ for key, success in entries.items()
48
+ }
49
+
50
+
51
+class _StubControlChannel:
52
+ def __init__(self, endpoint: str) -> None:
53
+ self.endpoint = endpoint
54
+
55
+ def send(self, endpoint: str, message: bytes) -> bool:
56
+ return True
57
+
58
+ def recv(self) -> list[bytes]:
59
+ return []
60
+
61
+ def close(self) -> None:
62
+ pass
63
+
64
+
65
+class RecordingKVCR:
66
+ def __init__(self) -> None:
67
+ self.config: KVCRConfig | None = None
68
+ self.guard_config: KVCRGuardConfig | None = None
69
+ self.backend_configs = KVCRBackendConfigs()
70
+ self.constructor_bindings: KVCRBindings | None = None
71
+ self.nixl_agent_name = "recording"
72
+ self.framework_control: _StubControlChannel | None = None
73
+ self.inventory_sink = None
74
+ self.query_status = QueryStatus.MISS
75
+ self.stats: OffloadingConnectorStats | None = None
76
+ self.submit_hint_calls: list[tuple[tuple[Any, ...], dict[str, Any]]] = []
77
+ self.discard_hint_calls: list[str] = []
78
+ self.deliver_calls: list[
79
+ tuple[OpHandle, dict[BlockKey, list[MemDescriptor]], str | None]
80
+ ] = []
81
+ self.deposit_calls: list[
82
+ tuple[OpHandle, dict[BlockKey, list[MemDescriptor]]]
83
+ ] = []
84
+ self.completed: list[tuple[OpHandle, dict[BlockKey, OpEntryResult]]] = []
85
+ self._next_op_handle = 1
86
+
87
+ def submit_hint(self, *args: Any, **kwargs: Any) -> None:
88
+ self.submit_hint_calls.append((args, kwargs))
89
+
90
+ def discard_hint(self, request_id: str) -> None:
91
+ self.discard_hint_calls.append(request_id)
92
+
93
+ def query(
94
+ self,
95
+ keys: Collection[BlockKey],
96
+ request_id: str | None = None,
97
+ ) -> list[tuple[QueryStatus, CacheTier | None]]:
98
+ tier = {
99
+ QueryStatus.HIT: CacheTier.LOCAL_G2,
100
+ QueryStatus.FETCHING: CacheTier.LOCAL_G2,
101
+ QueryStatus.FETCHABLE: CacheTier.REMOTE_G2,
102
+ QueryStatus.MISS: None,
103
+ }[self.query_status]
104
+ return [(self.query_status, tier) for _ in keys]
105
+
106
+ def deliver(
107
+ self,
108
+ blocks: Mapping[BlockKey, list[MemDescriptor]],
109
+ request_id: str | None = None,
110
+ ) -> OpHandle:
111
+ op_handle = self._next_op_handle
112
+ self._next_op_handle += 1
113
+ self.deliver_calls.append((op_handle, dict(blocks), request_id))
114
+ self.complete(op_handle, {key: True for key in blocks})
115
+ return op_handle
116
+
117
+ def deposit(
118
+ self,
119
+ blocks: Mapping[BlockKey, list[MemDescriptor]],
120
+ ) -> OpHandle:
121
+ op_handle = self._next_op_handle
122
+ self._next_op_handle += 1
123
+ self.deposit_calls.append((op_handle, dict(blocks)))
124
+ self.complete(op_handle, {key: True for key in blocks})
125
+ return op_handle
126
+
127
+ def complete(
128
+ self,
129
+ op_handle: OpHandle,
130
+ entries: Mapping[BlockKey, bool],
131
+ ) -> None:
132
+ self.completed.append((op_handle, _op_entries(entries)))
133
+
134
+ def poll_completed(
135
+ self,
136
+ ) -> Iterable[tuple[OpHandle, dict[BlockKey, OpEntryResult]]]:
137
+ completed = self.completed
138
+ self.completed = []
139
+ return completed
140
+
141
+ def get_stats(self) -> OffloadingConnectorStats | None:
142
+ stats = self.stats
143
+ self.stats = None
144
+ return stats
145
+
146
+ def close(self) -> None:
147
+ pass
148
+
149
+
150
+class _ExternalPolicy(FIFOPolicy):
151
+ pass
152
+
153
+
154
+def _job(
155
+ job_id: int,
156
+ req_context: ReqContext,
157
+ key: OffloadKey | None = None,
158
+ chunk_id: int = 0,
159
+) -> TransferJob:
160
+ return TransferJob(
161
+ job_id=job_id,
162
+ keys=[key if key is not None else OffloadKey(b"k0")],
163
+ chunk_ids=np.array([chunk_id], dtype=np.int64),
164
+ is_promotion=True,
165
+ req_context=req_context,
166
+ )
167
+
168
+
169
+def _make_tier(
170
+ monkeypatch,
171
+ kvcr: RecordingKVCR,
172
+ *,
173
+ enable_telemetry: bool = False,
174
+ secondary_g2_slots: int = 0,
175
+ kvcr_service_socket_path: str | None = None,
176
+ compatibility_digest: str | None = None,
177
+ enable_kv_cache_events: bool = False,
178
+ self_describing_kv_events: bool = False,
179
+ policy: str | None = None,
180
+ g3: dict[str, object] | None = None,
181
+ control_ports: list[int] | None = None,
182
+ data_parallel_rank_local: int | None = None,
183
+) -> KVCRSecondaryTierManager:
184
+ def make_control(_bind_host, bind_port, advertise_host):
185
+ return _StubControlChannel(f"tcp://{advertise_host}:{int(bind_port)}")
186
+
187
+ def make_kvcr(config, bindings, backend_configs, guard_config):
188
+ kvcr.config = config
189
+ kvcr.guard_config = guard_config
190
+ kvcr.nixl_agent_name = config.nixl_agent_name
191
+ kvcr.backend_configs = backend_configs
192
+ kvcr.constructor_bindings = bindings
193
+ kvcr.framework_control = bindings.framework_control
194
+ kvcr.inventory_sink = bindings.inventory_sink
195
+ return kvcr
196
+
197
+ monkeypatch.setattr(kvcr_manager, "KVCR", make_kvcr)
198
+ monkeypatch.setattr(kvcr_manager, "ZmqPeerControlChannel", make_control)
199
+ return KVCRSecondaryTierManager(
200
+ offloading_spec=SimpleNamespace(
201
+ config=SimpleNamespace(
202
+ parallel=SimpleNamespace(
203
+ data_parallel_rank_local=data_parallel_rank_local,
204
+ )
205
+ ),
206
+ kv_events_config=SimpleNamespace(
207
+ enable_kv_cache_events=enable_kv_cache_events,
208
+ self_describing_kv_events=self_describing_kv_events,
209
+ ),
210
+ ),
211
+ primary_kv_view=memoryview(np.zeros((4, 16), dtype=np.int8)),
212
+ tier_type="kvcr",
213
+ router_capabilities=["router_hint"],
214
+ control_host="127.0.0.1",
215
+ control_ports=control_ports if control_ports is not None else [7777],
216
+ control_advertise_host="127.0.0.1",
217
+ enable_telemetry=enable_telemetry,
218
+ secondary_g2_slots=secondary_g2_slots,
219
+ kvcr_service_socket_path=kvcr_service_socket_path,
220
+ compatibility_digest=compatibility_digest,
221
+ policy=policy,
222
+ g3=g3,
223
+ local_dram_backend="UCX",
224
+ remote_fw_dram_backend="UCX",
225
+ )
226
+
227
+
228
+def test_kvcr_tier_configures_service_for_local_dp_rank(monkeypatch):
229
+ """Keep the control endpoint and guard pool aligned to the local DP rank."""
230
+ kvcr = RecordingKVCR()
231
+ tier = _make_tier(
232
+ monkeypatch,
233
+ kvcr,
234
+ kvcr_service_socket_path="/tmp/kvcr.sock",
235
+ compatibility_digest="Opaque-Digest",
236
+ secondary_g2_slots=1,
237
+ control_ports=[7001, 7002],
238
+ data_parallel_rank_local=1,
239
+ )
240
+
241
+ assert kvcr.framework_control is not None
242
+ assert kvcr.framework_control.endpoint == "tcp://127.0.0.1:7002"
243
+ assert kvcr.guard_config == KVCRGuardConfig(
244
+ kvcr_service_socket_path="/tmp/kvcr.sock",
245
+ guard_index=1,
246
+ compatibility_digest="Opaque-Digest",
247
+ )
248
+ assert kvcr.config is not None
249
+ assert kvcr.config.pool_layouts == [("", tier._primary_row_stride)]
250
+ assert kvcr.backend_configs.local_dram is None
251
+
252
+
253
+def test_kvcr_tier_converts_g3_paths(monkeypatch, tmp_path):
254
+ """Convert user-provided G3 paths to KVCR's typed configuration."""
255
+ kvcr = RecordingKVCR()
256
+ path = tmp_path / "g3.data"
257
+
258
+ _make_tier(
259
+ monkeypatch,
260
+ kvcr,
261
+ g3={"paths": [str(path)], "capacity_bytes_per_file": 64},
262
+ )
263
+
264
+ assert kvcr.backend_configs.g3 == G3Options(
265
+ paths=(path,),
266
+ capacity_bytes_per_file=64,
267
+ )
268
+
269
+
270
+def test_kvcr_tier_adapts_request_and_load(monkeypatch):
271
+ """Check hint forwarding, key conversion, load descriptors, and cleanup."""
272
+ kvcr = RecordingKVCR()
273
+ tier = _make_tier(monkeypatch, kvcr)
274
+ router_hint = {"opaque": True}
275
+ ctx = ReqContext(
276
+ req_id="req",
277
+ kv_transfer_params={ROUTER_HINT_KEY: router_hint, "unrelated": object()},
278
+ )
279
+ key = make_offload_key((123).to_bytes(8, "big"), 0)
280
+ same_hash_other_group = make_offload_key((123).to_bytes(8, "big"), 7)
281
+ other_key = make_offload_key((124).to_bytes(8, "big"), 0)
282
+
283
+ tier.on_new_request(ctx)
284
+
285
+ assert kvcr.submit_hint_calls == [((), {"request_id": "req", "hints": router_hint})]
286
+
287
+ bindings = kvcr.constructor_bindings
288
+ assert bindings is not None
289
+ assert bindings.key_adapter is not None
290
+ monkeypatch.setenv("VLLM_KV_EVENTS_USE_INT_BLOCK_HASHES", "0")
291
+ decode = bindings.key_adapter.decode
292
+ hash_123 = (123).to_bytes(8, "big")
293
+ assert decode(BlockKey(bytes(key))) == hash_123
294
+ assert decode(BlockKey(bytes(same_hash_other_group))) == hash_123
295
+ assert decode(BlockKey(bytes(other_key))) == (124).to_bytes(8, "big")
296
+
297
+ tier.submit_load(_job(7, ctx, key=key, chunk_id=2))
298
+
299
+ # Check load destinations and job completion independently of hint parsing.
300
+ assert len(kvcr.submit_hint_calls) == 1
301
+ _, blocks, request_id = kvcr.deliver_calls[0]
302
+ assert request_id == "req"
303
+ assert list(blocks) == [key]
304
+ (descriptor,) = blocks[key]
305
+ assert descriptor.end_point_name == kvcr.nixl_agent_name
306
+ assert descriptor.addr == tier._primary_base_addr + 2 * 16
307
+ assert descriptor.size == 16
308
+ assert list(tier.get_finished_jobs()) == [JobResult(7, True)]
309
+
310
+ # Here we verify that request cleanup discards the request-scoped hint in
311
+ # KVCR.
312
+ tier.on_request_finished(ctx)
313
+ assert kvcr.discard_hint_calls == ["req"]
314
+
315
+
316
+def test_kvcr_tier_allows_request_without_router_hint(monkeypatch):
317
+ """Keep router hints optional for requests from non-hint-aware routers."""
318
+ kvcr = RecordingKVCR()
319
+ tier = _make_tier(monkeypatch, kvcr)
320
+
321
+ tier.on_new_request(ReqContext(req_id="req"))
322
+
323
+ assert kvcr.submit_hint_calls == []
324
+
325
+
326
+@pytest.mark.parametrize(
327
+ ("status", "expected"),
328
+ [
329
+ (QueryStatus.MISS, LookupResult.MISS),
330
+ (QueryStatus.HIT, LookupResult.HIT),
331
+ (QueryStatus.FETCHABLE, LookupResult.HIT),
332
+ (QueryStatus.FETCHING, LookupResult.RETRY),
333
+ ],
334
+)
335
+def test_kvcr_tier_maps_query_status(monkeypatch, status, expected):
336
+ """Map KVCR cache states to the scheduler's lookup contract."""
337
+ kvcr = RecordingKVCR()
338
+ kvcr.query_status = status
339
+ tier = _make_tier(monkeypatch, kvcr)
340
+
341
+ assert tier.lookup(OffloadKey(b"k0"), ReqContext(req_id="req")) is expected
342
+
343
+
344
+def test_kvcr_tier_serves_primary_pin_request(monkeypatch):
345
+ """Hold primary-tier hits until KVCR releases the corresponding pin."""
346
+ kvcr = RecordingKVCR()
347
+ tier = _make_tier(monkeypatch, kvcr)
348
+ keys = (BlockKey(b"k0"), BlockKey(b"k1"), BlockKey(b"k2"))
349
+ hit_keys = (keys[0], keys[2])
350
+ chunk_ids = {keys[0]: 1, keys[2]: 5}
351
+ lifecycle: list[str] = []
352
+
353
+ class Parent:
354
+ def on_new_request(self, req_context):
355
+ lifecycle.append("new")
356
+ return SimpleNamespace()
357
+
358
+ def lookup(self, key, req_context):
359
+ return LookupResult.HIT if key in hit_keys else LookupResult.MISS
360
+
361
+ def create_store_job(self, requested_keys, req_context):
362
+ return TransferJob(
363
+ job_id=11,
364
+ keys=requested_keys,
365
+ chunk_ids=np.array([chunk_ids[key] for key in requested_keys]),
366
+ is_promotion=True,
367
+ req_context=req_context,
368
+ )
369
+
370
+ def on_request_finished(self, req_context):
371
+ lifecycle.append("finished")
372
+
373
+ bindings = kvcr.constructor_bindings
374
+ assert bindings is not None
375
+ request = bindings.request_pin(keys)
376
+
377
+ tier.serve_external_requests(Parent())
378
+
379
+ [(queued_request, result)] = bindings.poll_pin_results()
380
+ assert queued_request == request
381
+ assert result is not None
382
+ pin_handle, descriptors = result
383
+ assert descriptors[keys[1]] is None
384
+ block_descriptors = descriptors[keys[2]]
385
+ assert block_descriptors is not None
386
+ (descriptor,) = block_descriptors
387
+ assert descriptor.addr == (tier._primary_base_addr + 5 * tier._primary_row_stride)
388
+ assert lifecycle == ["new", "finished"]
389
+
390
+ polls = 0
391
+
392
+ def release_pin():
393
+ nonlocal polls
394
+ polls += 1
395
+ bindings.release_pin(pin_handle)
396
+ return []
397
+
398
+ monkeypatch.setattr(kvcr, "poll_completed", release_pin)
399
+ tier.drain_jobs()
400
+
401
+ assert polls == 1
402
+ assert list(tier.get_finished_jobs()) == [JobResult(11, True)]
403
+
404
+
405
+def test_kvcr_telemetry_is_opt_in_and_namespaced_at_vllm_boundary(monkeypatch):
406
+ """Keep telemetry opt-in and namespace metrics only at the vLLM boundary."""
407
+ assert KVCRSecondaryTierManager.build_metric_definitions({}) == {}
408
+ definitions = KVCRSecondaryTierManager.build_metric_definitions(
409
+ {"enable_telemetry": True}
410
+ )
411
+ assert "vllm:kvcr_duration_seconds" in definitions
412
+ assert "vllm:kvcr_transfer_blocks" in definitions
413
+
414
+ kvcr = RecordingKVCR()
415
+ tier = _make_tier(monkeypatch, kvcr, enable_telemetry=True)
416
+ bindings = kvcr.constructor_bindings
417
+ assert bindings is not None
418
+ assert bindings.stats_factory is not None
419
+ stats = bindings.stats_factory()
420
+ stats.increase_counter(
421
+ "kvcr_transfer_blocks",
422
+ 2,
423
+ ("remote_deliver",),
424
+ )
425
+ kvcr.stats = stats
426
+
427
+ returned = tier.get_stats()
428
+
429
+ assert returned is stats
430
+ assert returned.reduce() == {"vllm:kvcr_transfer_blocks:('remote_deliver',)": 2}
431
+
432
+
433
+@pytest.mark.parametrize(
434
+ ("policy", "expected_type"),
435
+ [
436
+ ("fifo", FIFOPolicy),
437
+ ("lru", LRUPolicy),
438
+ ("g3_fifo", G3FIFOPolicy),
439
+ ("g3_lru", G3LRUPolicy),
440
+ (f"{__name__}._ExternalPolicy", _ExternalPolicy),
441
+ ],
442
+)
443
+def test_kvcr_tier_passes_selected_policy(monkeypatch, policy, expected_type):
444
+ """Resolve every built-in and fully qualified external policy."""
445
+ kvcr = RecordingKVCR()
446
+ _make_tier(monkeypatch, kvcr, policy=policy)
447
+
448
+ bindings = kvcr.constructor_bindings
449
+ assert bindings is not None
450
+ assert type(bindings.policy) is expected_type
451
+
452
+
453
+@pytest.mark.parametrize(
454
+ ("socket_path", "digest"),
455
+ [("/tmp/kvcr.sock", None), (None, "Opaque-Digest")],
456
+)
457
+def test_kvcr_tier_requires_complete_service_config(monkeypatch, socket_path, digest):
458
+ """Reject partial guard configuration before connecting to the service."""
459
+ with pytest.raises(ValueError, match="configured together"):
460
+ _make_tier(
461
+ monkeypatch,
462
+ RecordingKVCR(),
463
+ kvcr_service_socket_path=socket_path,
464
+ compatibility_digest=digest,
465
+ )
466
+
467
+
468
+def test_kvcr_tier_stores_and_emits_inventory(monkeypatch):
469
+ """Cover store descriptors and inventory translation at the KVCR boundary."""
470
+ kvcr = RecordingKVCR()
471
+ tier = _make_tier(
472
+ monkeypatch,
473
+ kvcr,
474
+ secondary_g2_slots=2,
475
+ enable_kv_cache_events=True,
476
+ self_describing_kv_events=True,
477
+ )
478
+ local_dram = kvcr.backend_configs.local_dram
479
+ assert local_dram is not None
480
+ assert [(name, size) for name, _, size in local_dram.pools] == [
481
+ ("", 2 * tier._primary_row_stride)
482
+ ]
483
+
484
+ key = OffloadKey(b"k0")
485
+ tier.submit_store(_job(11, ReqContext(req_id="req"), key=key, chunk_id=2))
486
+
487
+ _, blocks = kvcr.deposit_calls[0]
488
+ assert blocks[key][0].addr == tier._primary_base_addr + 2 * 16
489
+ assert list(tier.get_finished_jobs()) == [JobResult(11, True)]
490
+
491
+ assert kvcr.inventory_sink is not None
492
+ kvcr.inventory_sink(
493
+ InventoryEvent((BlockKey(bytes(key)),), CacheTier.LOCAL_G2, False)
494
+ )
495
+ kvcr.inventory_sink(InventoryEvent((BlockKey(bytes(key)),), CacheTier.G3, False))
496
+ events = list(tier.take_events())
497
+ assert [
498
+ (event.keys, event.medium, event.ownership, event.removed) for event in events
499
+ ] == [
500
+ ([key], Medium.CPU, "kvcr", False),
501
+ ([key], Medium.STORAGE, "kvcr", False),
502
+ ]
503
+ assert all(event.removal_expected for event in events)
504
+ tier.shutdown()
505
+
506
+
507
+def test_kvcr_tier_requires_self_describing_inventory_events(monkeypatch):
508
+ """Require tier-aware events whenever KVCR owns local cache inventory."""
509
+ with pytest.raises(ValueError, match="self_describing_kv_events"):
510
+ _make_tier(
511
+ monkeypatch,
512
+ RecordingKVCR(),
513
+ secondary_g2_slots=1,
514
+ enable_kv_cache_events=True,
515
+ )
516
+
517
+
518
+def test_kvcr_tier_accumulates_block_results(monkeypatch):
519
+ """Wait for every block result and preserve partial success while draining."""
520
+ kvcr = RecordingKVCR()
521
+ tier = _make_tier(monkeypatch, kvcr)
522
+ keys = [OffloadKey(b"k0"), OffloadKey(b"k1")]
523
+ tier.submit_load(
524
+ TransferJob(
525
+ job_id=13,
526
+ keys=keys,
527
+ chunk_ids=np.array([0, 1], dtype=np.int64),
528
+ is_promotion=True,
529
+ req_context=ReqContext(req_id="req"),
530
+ )
531
+ )
532
+
533
+ op_handle, _, _ = kvcr.deliver_calls[0]
534
+ kvcr.completed.clear()
535
+ kvcr.complete(op_handle, {keys[0]: True})
536
+ assert list(tier.get_finished_jobs()) == []
537
+
538
+ kvcr.complete(op_handle, {keys[1]: False})
539
+ tier.drain_jobs()
540
+
541
+ [result] = tier.get_finished_jobs()
542
+ assert result.job_id == 13
543
+ assert not result.success
544
+ assert result.successful_keys == {keys[0]}
vllm/v1/kv_offload/tiering/factory.py
+6
@@ -123,3 +123,9 @@ SecondaryTierFactory.register_tier(
123
"vllm.v1.kv_offload.tiering.obj.manager",
124
"ObjectStoreSecondaryTierManager",
125
)
126
+
127
+SecondaryTierFactory.register_tier(
128
+ "kvcr",
129
+ "vllm.v1.kv_offload.tiering.kvcr.manager",
130
+ "KVCRSecondaryTierManager",
131
+)
vllm/v1/kv_offload/tiering/kvcr/__init__.py
new
+2
@@ -0,0 +1,2 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/v1/kv_offload/tiering/kvcr/manager.py
new
+635
@@ -0,0 +1,635 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""vLLM secondary-tier adapter for KVCR."""
4
+
5
+import ctypes
6
+import mmap
7
+import socket
8
+import time
9
+import uuid
10
+from collections.abc import Collection, Iterable, Mapping
11
+from pathlib import Path
12
+from typing import TYPE_CHECKING, Any, cast
13
+
14
+from kvcr import (
15
+ DURATION_METRIC,
16
+ KVCR,
17
+ ROUTER_HINT_CAPABILITIES,
18
+ ROUTER_HINT_KEY,
19
+ STATE_METRIC,
20
+ TRANSFER_BLOCKS_METRIC,
21
+ TRANSFER_BYTES_METRIC,
22
+ KVCRBindings,
23
+)
24
+from kvcr.config import (
25
+ FrameworkDramInput,
26
+ G3Options,
27
+ KeyAdapter,
28
+ KVCRBackendConfigs,
29
+ KVCRConfig,
30
+ KVCRGuardConfig,
31
+ LocalDramOptions,
32
+ RemoteFWDramOptions,
33
+)
34
+from kvcr.control_channels import ZmqPeerControlChannel
35
+from kvcr.policy import (
36
+ FIFOPolicy,
37
+ G3FIFOPolicy,
38
+ G3LRUPolicy,
39
+ KVCachePolicy,
40
+ LRUPolicy,
41
+)
42
+from kvcr.types import (
43
+ BlockKey,
44
+ CacheTier,
45
+ InventoryEvent,
46
+ MemDescriptor,
47
+ OpHandle,
48
+ PinRequestId,
49
+ PinResult,
50
+ QueryStatus,
51
+)
52
+from typing_extensions import override
53
+
54
+from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import (
55
+ OffloadingConnectorStats,
56
+)
57
+from vllm.logger import init_logger
58
+from vllm.utils.import_utils import resolve_obj_by_qualname
59
+from vllm.v1.core.kv_cache_utils import (
60
+ BlockHash,
61
+ maybe_convert_block_hash,
62
+)
63
+from vllm.v1.kv_offload.base import (
64
+ LookupResult,
65
+ Medium,
66
+ OffloadingCounterMetadata,
67
+ OffloadingEvent,
68
+ OffloadingGaugeMetadata,
69
+ OffloadingHistogramMetadata,
70
+ OffloadingMetricMetadata,
71
+ OffloadKey,
72
+ ReqContext,
73
+ RequestOffloadingContext,
74
+ get_offload_block_hash,
75
+)
76
+from vllm.v1.kv_offload.tiering.base import (
77
+ JobResult,
78
+ ParentManager,
79
+ SecondaryTierManager,
80
+ TransferJob,
81
+)
82
+
83
+if TYPE_CHECKING:
84
+ from vllm.v1.kv_offload.base import OffloadingSpec
85
+
86
+
87
+_REQUIRED_ROUTER_CAPABILITIES = ROUTER_HINT_CAPABILITIES
88
+
89
+logger = init_logger(__name__)
90
+
91
+_BUILTIN_POLICIES: dict[str, type[KVCachePolicy]] = {
92
+ "fifo": FIFOPolicy,
93
+ "lru": LRUPolicy,
94
+ "g3_fifo": G3FIFOPolicy,
95
+ "g3_lru": G3LRUPolicy,
96
+}
97
+
98
+
99
+def _resolve_policy(name: str | None) -> KVCachePolicy | None:
100
+ if name is None:
101
+ return None
102
+ policy_type = _BUILTIN_POLICIES.get(name)
103
+ if policy_type is None:
104
+ if "." not in name:
105
+ raise ValueError(
106
+ f"Unknown KVCR policy: {name!r}. "
107
+ f"Supported: {list(_BUILTIN_POLICIES)}; external policies "
108
+ "must use a fully qualified module.Class path."
109
+ )
110
+ policy_type = resolve_obj_by_qualname(name)
111
+ if not isinstance(policy_type, type) or not issubclass(
112
+ policy_type, KVCachePolicy
113
+ ):
114
+ raise TypeError(f"{name} is not a KVCachePolicy")
115
+ return policy_type()
116
+
117
+
118
+def _vllm_metric_name(name: str) -> str:
119
+ return f"vllm:{name}"
120
+
121
+
122
+def _kvcr_metric_definitions() -> dict[str, OffloadingMetricMetadata]:
123
+ return {
124
+ _vllm_metric_name(DURATION_METRIC): OffloadingHistogramMetadata(
125
+ documentation="KVCR operation and stage duration in seconds.",
126
+ labelnames=("scope", "result"),
127
+ ),
128
+ _vllm_metric_name(TRANSFER_BYTES_METRIC): OffloadingCounterMetadata(
129
+ documentation="Bytes transferred by KVCR operations.",
130
+ labelnames=("operation",),
131
+ ),
132
+ _vllm_metric_name(TRANSFER_BLOCKS_METRIC): OffloadingCounterMetadata(
133
+ documentation="KV blocks transferred by successful KVCR operations.",
134
+ labelnames=("operation",),
135
+ ),
136
+ _vllm_metric_name(STATE_METRIC): OffloadingGaugeMetadata(
137
+ documentation="Current KVCR metadata and operation counts.",
138
+ labelnames=("resource",),
139
+ ),
140
+ }
141
+
142
+
143
+class _FrameworkPinAdapter:
144
+ """Adapt KVCR framework pin callbacks to primary-tier store jobs."""
145
+
146
+ def __init__(self, tier: "KVCRSecondaryTierManager") -> None:
147
+ self._tier = tier
148
+ self._next_request_id = 0
149
+ self._next_pin_request_id = 0
150
+ self._pending_pins: dict[PinRequestId, tuple[BlockKey, ...]] = {}
151
+ self._completed_pins: list[tuple[PinRequestId, PinResult]] = []
152
+ self._pin_jobs: dict[str, int] = {}
153
+ self._pin_job_results: list[JobResult] = []
154
+
155
+ def request_pin(self, keys: Collection[BlockKey]) -> PinRequestId:
156
+ # The parent callback is valid only from serve_external_requests().
157
+ request = PinRequestId(self._next_pin_request_id)
158
+ self._next_pin_request_id += 1
159
+ self._pending_pins[request] = tuple(keys)
160
+ return request
161
+
162
+ def process_pending(self, parent: ParentManager) -> None:
163
+ pending = self._pending_pins
164
+ self._pending_pins = {}
165
+ self._completed_pins.extend(
166
+ (request, self._resolve_pin(parent, keys))
167
+ for request, keys in pending.items()
168
+ )
169
+
170
+ def poll_pin_results(self) -> list[tuple[PinRequestId, PinResult]]:
171
+ completed = self._completed_pins
172
+ self._completed_pins = []
173
+ return completed
174
+
175
+ def cancel_pin_request(self, request: PinRequestId) -> None:
176
+ self._pending_pins.pop(request, None)
177
+
178
+ def _resolve_pin(
179
+ self, parent: ParentManager, keys: Collection[BlockKey]
180
+ ) -> tuple[str, dict[BlockKey, list[MemDescriptor] | None]] | None:
181
+ offload_keys = tuple(OffloadKey(bytes(key)) for key in keys)
182
+ request_id = f"kvcr-source:{self._next_request_id}"
183
+ self._next_request_id += 1
184
+ req_context = ReqContext(req_id=request_id)
185
+ started = False
186
+ job: TransferJob | None = None
187
+ pin_handle: str | None = None
188
+ try:
189
+ parent.on_new_request(req_context)
190
+ started = True
191
+ hit_keys = tuple(
192
+ key
193
+ for key in offload_keys
194
+ if parent.lookup(key, req_context) is LookupResult.HIT
195
+ )
196
+ if not hit_keys:
197
+ return None
198
+
199
+ job = parent.create_store_job(hit_keys, req_context)
200
+ job_keys = tuple(job.keys)
201
+ chunk_ids = tuple(int(chunk_id) for chunk_id in job.chunk_ids)
202
+ if len(job_keys) != len(chunk_ids) or set(job_keys) != set(hit_keys):
203
+ return None
204
+
205
+ descriptors: dict[BlockKey, list[MemDescriptor] | None] = {
206
+ BlockKey(bytes(key)): None for key in offload_keys
207
+ }
208
+ descriptors.update(
209
+ (
210
+ BlockKey(bytes(key)),
211
+ [self._tier._make_descriptor(chunk_id)],
212
+ )
213
+ for key, chunk_id in zip(job_keys, chunk_ids)
214
+ )
215
+ pin_handle = f"kvcr-job-{job.job_id}"
216
+ self._pin_jobs[pin_handle] = job.job_id
217
+ return pin_handle, descriptors
218
+ except Exception:
219
+ logger.warning("KVCR source acquisition failed", exc_info=True)
220
+ return None
221
+ finally:
222
+ if started:
223
+ try:
224
+ parent.on_request_finished(req_context)
225
+ except Exception:
226
+ logger.warning("KVCR source request cleanup failed", exc_info=True)
227
+ if job is not None and pin_handle is None:
228
+ self._pin_job_results.append(
229
+ JobResult(job_id=job.job_id, success=False)
230
+ )
231
+
232
+ def release_pin(self, pin_handle: str) -> bool:
233
+ job_id = self._pin_jobs.pop(pin_handle, None)
234
+ if job_id is not None:
235
+ self._pin_job_results.append(JobResult(job_id=job_id, success=True))
236
+ return True
237
+
238
+ def has_active_pins(self) -> bool:
239
+ return bool(self._pin_jobs)
240
+
241
+ def take_pin_job_results(self) -> list[JobResult]:
242
+ results = self._pin_job_results
243
+ self._pin_job_results = []
244
+ return results
245
+
246
+
247
+class _VllmKeyAdapter:
248
+ """Adapt vLLM's local key format to KVCR."""
249
+
250
+ def encode(self, framework_key: object) -> BlockKey:
251
+ if not isinstance(framework_key, bytes):
252
+ raise TypeError("vLLM offload keys must be bytes")
253
+ return BlockKey(framework_key)
254
+
255
+ def decode(self, key: BlockKey) -> int | bytes:
256
+ block_hash = BlockHash(get_offload_block_hash(OffloadKey(key)))
257
+ return maybe_convert_block_hash(block_hash)
258
+
259
+
260
+# Job ID, remaining blocks, aggregate success, and successful load keys.
261
+_JobState = tuple[int, int, bool, set[OffloadKey] | None]
262
+
263
+
264
+class KVCRSecondaryTierManager(SecondaryTierManager):
265
+ """Secondary tier wrapper around the KVCR KV P2P API."""
266
+
267
+ @classmethod
268
+ @override
269
+ def build_metric_definitions(
270
+ cls, extra_config: dict[str, Any]
271
+ ) -> dict[str, OffloadingMetricMetadata]:
272
+ if not extra_config.get("enable_telemetry", False):
273
+ return {}
274
+ return _kvcr_metric_definitions()
275
+
276
+ def __init__(
277
+ self,
278
+ offloading_spec: "OffloadingSpec",
279
+ primary_kv_view: memoryview,
280
+ tier_type: str,
281
+ router_capabilities: Iterable[str] | None = None,
282
+ control_host: str = "0.0.0.0",
283
+ control_ports: list[int] | None = None,
284
+ control_advertise_host: str | None = None,
285
+ eager_ctrl_connect: bool = True,
286
+ opportunistic_query: bool = True,
287
+ enable_telemetry: bool = False,
288
+ operation_timeout_ms: int = 1000,
289
+ abandon_timeout_ms: int = 5000,
290
+ metadata_retry_interval_ms: int = 100,
291
+ secondary_g2_slots: int = 0,
292
+ kvcr_service_socket_path: str | None = None,
293
+ compatibility_digest: str | None = None,
294
+ policy: str | None = None,
295
+ g3: dict[str, Any] | None = None,
296
+ local_dram_backend: str = "UCX",
297
+ remote_fw_dram_backend: str = "UCX",
298
+ ) -> None:
299
+ super().__init__(offloading_spec, primary_kv_view, tier_type)
300
+ selected_policy = _resolve_policy(policy)
301
+ if (kvcr_service_socket_path is None) != (compatibility_digest is None):
302
+ raise ValueError(
303
+ "kvcr_service_socket_path and compatibility_digest must be "
304
+ "configured together"
305
+ )
306
+ missing = _REQUIRED_ROUTER_CAPABILITIES - set(router_capabilities or ())
307
+ if missing:
308
+ raise ValueError(
309
+ f"KVCR secondary tier requires router capabilities: {sorted(missing)}"
310
+ )
311
+ events_enabled = offloading_spec.kv_events_config.enable_kv_cache_events
312
+ self_describing_events = (
313
+ offloading_spec.kv_events_config.self_describing_kv_events
314
+ )
315
+ if (
316
+ events_enabled
317
+ and (
318
+ secondary_g2_slots > 0
319
+ or kvcr_service_socket_path is not None
320
+ or g3 is not None
321
+ )
322
+ and not self_describing_events
323
+ ):
324
+ raise ValueError(
325
+ "KVCR local G2/G3 inventory requires self_describing_kv_events"
326
+ )
327
+ self._key_adapter: KeyAdapter = _VllmKeyAdapter()
328
+ advertise_host = control_advertise_host or socket.gethostname()
329
+ dp_local_rank = offloading_spec.config.parallel.data_parallel_rank_local
330
+ if dp_local_rank is None:
331
+ if control_ports is None or len(control_ports) != 1:
332
+ raise ValueError(
333
+ "control_ports must contain exactly one port when "
334
+ "data_parallel_rank_local is unset"
335
+ )
336
+ dp_local_rank = 0
337
+ if control_ports is None or not 0 <= dp_local_rank < len(control_ports):
338
+ raise ValueError(
339
+ "control_ports must contain a port for "
340
+ f"local data-parallel rank {dp_local_rank}"
341
+ )
342
+ if any(not 1 <= port <= 65535 for port in control_ports):
343
+ raise ValueError("control_ports must be between 1 and 65535")
344
+ control = ZmqPeerControlChannel(
345
+ control_host,
346
+ control_ports[dp_local_rank],
347
+ advertise_host,
348
+ )
349
+ # Give colocated workers distinct transport listen ports.
350
+ with socket.socket() as _s:
351
+ _s.bind(("", 0))
352
+ _nixl_listen_port = _s.getsockname()[1]
353
+ self._primary_base_addr = ctypes.addressof(
354
+ ctypes.c_char.from_buffer(primary_kv_view)
355
+ )
356
+ if primary_kv_view.strides is None:
357
+ raise ValueError("primary KV memoryview must expose strides")
358
+ self._primary_row_stride = int(primary_kv_view.strides[0])
359
+ if secondary_g2_slots < 0:
360
+ raise ValueError("secondary_g2_slots must be non-negative")
361
+ if (
362
+ secondary_g2_slots or kvcr_service_socket_path is not None
363
+ ) and not events_enabled:
364
+ logger.warning(
365
+ "KVCR local DRAM is enabled, but KV cache events are disabled; "
366
+ "local DRAM inventory will not be published"
367
+ )
368
+ local_mapping: mmap.mmap | None = None
369
+ local_dram: LocalDramOptions | None = None
370
+ if secondary_g2_slots:
371
+ if kvcr_service_socket_path is not None:
372
+ logger.warning(
373
+ "secondary_g2_slots is ignored when "
374
+ "kvcr_service_socket_path is configured"
375
+ )
376
+ else:
377
+ local_mapping = mmap.mmap(
378
+ -1, secondary_g2_slots * self._primary_row_stride
379
+ )
380
+ local_dram = LocalDramOptions(
381
+ pools=[
382
+ (
383
+ "",
384
+ ctypes.addressof(ctypes.c_char.from_buffer(local_mapping)),
385
+ len(local_mapping),
386
+ )
387
+ ],
388
+ backend=local_dram_backend,
389
+ )
390
+ guard_config = (
391
+ KVCRGuardConfig(
392
+ kvcr_service_socket_path=kvcr_service_socket_path,
393
+ guard_index=dp_local_rank,
394
+ compatibility_digest=compatibility_digest,
395
+ )
396
+ if kvcr_service_socket_path is not None and compatibility_digest is not None
397
+ else None
398
+ )
399
+ g3_config = None
400
+ if g3 is not None:
401
+ g3_values = dict(g3)
402
+ g3_values["paths"] = tuple(Path(path) for path in g3_values["paths"])
403
+ g3_config = G3Options(**g3_values)
404
+ nixl_agent_name = f"KVCR-{uuid.uuid4()}"
405
+ self._framework_pin_adapter = _FrameworkPinAdapter(self)
406
+ self._inventory_events: list[OffloadingEvent] = []
407
+
408
+ try:
409
+ self._kvcr = KVCR(
410
+ KVCRConfig(
411
+ pool_layouts=[("", self._primary_row_stride)],
412
+ enable_telemetry=enable_telemetry,
413
+ operation_timeout_ms=operation_timeout_ms,
414
+ abandon_timeout_ms=abandon_timeout_ms,
415
+ nixl_agent_name=nixl_agent_name,
416
+ nixl_listen_port=_nixl_listen_port,
417
+ ),
418
+ KVCRBindings(
419
+ request_pin=self._framework_pin_adapter.request_pin,
420
+ poll_pin_results=self._framework_pin_adapter.poll_pin_results,
421
+ release_pin=self._framework_pin_adapter.release_pin,
422
+ cancel_pin_request=(self._framework_pin_adapter.cancel_pin_request),
423
+ framework_control=control,
424
+ key_adapter=self._key_adapter,
425
+ inventory_sink=(self._record_inventory if events_enabled else None),
426
+ stats_factory=(
427
+ OffloadingConnectorStats if enable_telemetry else None
428
+ ),
429
+ policy=selected_policy,
430
+ ),
431
+ KVCRBackendConfigs(
432
+ framework_dram=FrameworkDramInput(
433
+ self._primary_base_addr, primary_kv_view.nbytes
434
+ ),
435
+ local_dram=local_dram,
436
+ g3=g3_config,
437
+ remote_fw_dram=RemoteFWDramOptions(
438
+ eager_ctrl_connect=eager_ctrl_connect,
439
+ opportunistic_query=opportunistic_query,
440
+ metadata_retry_interval_ms=metadata_retry_interval_ms,
441
+ backend=remote_fw_dram_backend,
442
+ ),
443
+ ),
444
+ guard_config,
445
+ )
446
+ except BaseException:
447
+ control.close()
448
+ if local_mapping is not None:
449
+ local_mapping.close()
450
+ raise
451
+ self._local_dram_mmap = local_mapping
452
+ self._finished_jobs: list[JobResult] = []
453
+ self._jobs_by_op: dict[OpHandle, _JobState] = {}
454
+
455
+ @override
456
+ def lookup(self, key: OffloadKey, req_context: ReqContext) -> LookupResult:
457
+ block_key = self._key_adapter.encode(key)
458
+ status, _ = self._kvcr.query((block_key,), req_context.req_id)[0]
459
+ if status is QueryStatus.FETCHING:
460
+ return LookupResult.RETRY
461
+ if status in (QueryStatus.HIT, QueryStatus.FETCHABLE):
462
+ return LookupResult.HIT
463
+ return LookupResult.MISS
464
+
465
+ @override
466
+ def submit_load(self, job_metadata: TransferJob) -> None:
467
+ blocks = {
468
+ self._key_adapter.encode(key): [self._make_descriptor(int(chunk_id))]
469
+ for key, chunk_id in zip(
470
+ job_metadata.keys, job_metadata.chunk_ids, strict=True
471
+ )
472
+ }
473
+ if not blocks:
474
+ self._finished_jobs.append(
475
+ JobResult(job_id=job_metadata.job_id, success=True)
476
+ )
477
+ return
478
+
479
+ op_handle = self._kvcr.deliver(
480
+ blocks, request_id=job_metadata.req_context.req_id
481
+ )
482
+ self._jobs_by_op[op_handle] = (
483
+ job_metadata.job_id,
484
+ len(blocks),
485
+ True,
486
+ set(),
487
+ )
488
+
489
+ @override
490
+ def submit_store(self, job_metadata: TransferJob) -> None:
491
+ blocks = {
492
+ self._key_adapter.encode(key): [self._make_descriptor(int(chunk_id))]
493
+ for key, chunk_id in zip(
494
+ job_metadata.keys, job_metadata.chunk_ids, strict=True
495
+ )
496
+ }
497
+ if not blocks:
498
+ self._finished_jobs.append(
499
+ JobResult(job_id=job_metadata.job_id, success=True)
500
+ )
501
+ return
502
+ op_handle = self._kvcr.deposit(blocks)
503
+ self._jobs_by_op[op_handle] = (
504
+ job_metadata.job_id,
505
+ len(blocks),
506
+ True,
507
+ None,
508
+ )
509
+
510
+ @override
511
+ def get_finished_jobs(self) -> Iterable[JobResult]:
512
+ results = self._finished_jobs
513
+ self._finished_jobs = []
514
+ results.extend(self._poll_finished_jobs())
515
+ return results
516
+
517
+ @override
518
+ def get_stats(self) -> OffloadingConnectorStats | None:
519
+ stats = cast(OffloadingConnectorStats | None, self._kvcr.get_stats())
520
+ if stats is not None:
521
+ # Add the framework namespace only at the vLLM boundary.
522
+ for metrics in stats.data.values():
523
+ for name in tuple(metrics):
524
+ metrics[_vllm_metric_name(name)] = metrics.pop(name)
525
+ return stats
526
+
527
+ @override
528
+ def serve_external_requests(self, parent: ParentManager) -> None:
529
+ self._framework_pin_adapter.process_pending(parent)
530
+
531
+ def _poll_finished_jobs(self) -> list[JobResult]:
532
+ results: list[JobResult] = []
533
+ for op_handle, entries in self._kvcr.poll_completed():
534
+ job_state = self._jobs_by_op.get(op_handle)
535
+ if job_state is None:
536
+ continue
537
+ job_id, remaining, success, successful_keys = job_state
538
+ remaining -= len(entries)
539
+ success = success and all(entry.success for entry in entries.values())
540
+ if successful_keys is not None:
541
+ successful_keys.update(
542
+ OffloadKey(bytes(key))
543
+ for key, entry in entries.items()
544
+ if entry.success
545
+ )
546
+ if remaining > 0:
547
+ self._jobs_by_op[op_handle] = (
548
+ job_id,
549
+ remaining,
550
+ success,
551
+ successful_keys,
552
+ )
553
+ continue
554
+ self._jobs_by_op.pop(op_handle, None)
555
+ results.append(
556
+ JobResult(
557
+ job_id=job_id,
558
+ success=success,
559
+ successful_keys=successful_keys if not success else None,
560
+ )
561
+ )
562
+ results.extend(self._framework_pin_adapter.take_pin_job_results())
563
+ return results
564
+
565
+ @override
566
+ def has_pending_work(self) -> bool:
567
+ # Keep scheduler polling: control messages may arrive without a load job.
568
+ return True
569
+
570
+ @override
571
+ def take_events(self) -> Iterable[OffloadingEvent]:
572
+ events = self._inventory_events
573
+ self._inventory_events = []
574
+ return events
575
+
576
+ @override
577
+ def on_new_request(self, req_context: ReqContext) -> RequestOffloadingContext:
578
+ params = getattr(req_context, "kv_transfer_params", None)
579
+ if isinstance(params, Mapping):
580
+ hint = params.get(ROUTER_HINT_KEY)
581
+ if hint is not None:
582
+ self._kvcr.submit_hint(
583
+ request_id=req_context.req_id,
584
+ hints=hint,
585
+ )
586
+ return RequestOffloadingContext()
587
+
588
+ @override
589
+ def on_request_finished(self, req_context: ReqContext) -> None:
590
+ self._kvcr.discard_hint(req_context.req_id)
591
+
592
+ @override
593
+ def drain_jobs(self) -> None:
594
+ while self._jobs_by_op or self._framework_pin_adapter.has_active_pins():
595
+ drained = self._poll_finished_jobs()
596
+ if drained:
597
+ self._finished_jobs.extend(drained)
598
+ else:
599
+ time.sleep(0.001)
600
+
601
+ @override
602
+ def shutdown(self) -> None:
603
+ self.drain_jobs()
604
+ # TODO: Keep registered buffers alive if KVCR.close() fails.
605
+ self._kvcr.close()
606
+ if self._local_dram_mmap is not None:
607
+ self._local_dram_mmap.close()
608
+ self._local_dram_mmap = None
609
+
610
+ def _record_inventory(self, event: InventoryEvent) -> None:
611
+ if event.tier is CacheTier.LOCAL_G2:
612
+ medium = Medium.CPU
613
+ elif event.tier is CacheTier.G3:
614
+ medium = Medium.STORAGE
615
+ else:
616
+ raise ValueError(f"unsupported KVCR inventory tier: {event.tier}")
617
+ self._inventory_events.append(
618
+ OffloadingEvent(
619
+ keys=[OffloadKey(bytes(key)) for key in event.keys],
620
+ medium=medium,
621
+ removed=event.removed,
622
+ ownership="kvcr",
623
+ removal_expected=True,
624
+ )
625
+ )
626
+
627
+ def _make_descriptor(self, chunk_id: int) -> MemDescriptor:
628
+ return MemDescriptor(
629
+ end_point_name=self._kvcr.config.nixl_agent_name,
630
+ mem_type="DRAM",
631
+ addr=self._primary_base_addr + chunk_id * self._primary_row_stride,
632
+ size=self._primary_row_stride,
633
+ device_Id=0,
634
+ info="",
635
+ )