[KV Offload] Replace OffloadingHandler with OffloadingWorker (#45053)
Signed-off-by: Martin Hickey <martin.hickey@ie.ibm.com>
Martin Hickey committed
Jun 24, 2026 at 12:44 UTC
f237e16b41bb444b3c9994260a36f9c2388bd019
13 files changed
+176
-465
tests/v1/kv_connector/unit/offloading_connector/test_worker.py
+3
-3
@@ -98,7 +98,7 @@ def _make_worker(kv_cache_config: KVCacheConfig):
98
spec = MagicMock(spec=OffloadingSpec)
99
spec.kv_cache_config = kv_cache_config
100
spec.vllm_config = MagicMock()
101
- spec.get_handlers.return_value = iter([])
101
+ spec.get_worker.return_value = MagicMock()
102
103
worker = OffloadingConnectorWorker(spec=spec)
104
worker.worker = MagicMock()
@@ -279,7 +279,7 @@ def test_register_kv_caches(backend):
279
worker, spec = _make_worker(kv_cache_config)
280
worker.register_kv_caches(kv_caches)
281
282
- canonical = spec.get_handlers.call_args[0][0]
282
+ canonical = spec.get_worker.call_args[0][0]
283
assert isinstance(canonical, CanonicalKVCaches)
284
285
# -- Expected block tensors ----------------------------------------------
@@ -422,7 +422,7 @@ def test_register_kv_caches_uniform_type(backend):
422
worker, spec = _make_worker(kv_cache_config)
423
worker.register_kv_caches(kv_caches)
424
425
- canonical = spec.get_handlers.call_args[0][0]
425
+ canonical = spec.get_worker.call_args[0][0]
426
assert isinstance(canonical, CanonicalKVCaches)
427
428
for block_tensor in canonical.tensors:
tests/v1/kv_connector/unit/offloading_connector/utils.py
+24
-24
@@ -1,6 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from collections.abc import Callable, Iterable, Iterator
3
+from collections.abc import Callable, Iterable
4
from dataclasses import dataclass
5
from typing import Any
6
from unittest.mock import MagicMock
@@ -43,19 +43,17 @@ from vllm.v1.kv_cache_interface import (
43
KVCacheGroupSpec,
44
)
45
from vllm.v1.kv_offload.base import (
46
+ CanonicalKVCaches,
47
GPULoadStoreSpec,
48
LoadStoreSpec,
49
OffloadingManager,
50
OffloadingSpec,
51
+ OffloadingWorker,
52
OffloadKey,
53
PrepareStoreOutput,
54
RequestOffloadingContext,
53
- make_offload_key,
54
-)
55
-from vllm.v1.kv_offload.worker.worker import (
56
- OffloadingHandler,
55
TransferResult,
58
- TransferSpec,
56
+ make_offload_key,
57
)
58
from vllm.v1.request import Request
59
from vllm.v1.structured_output import StructuredOutputManager
@@ -81,9 +79,9 @@ class MockLoadStoreSpec(LoadStoreSpec):
79
return repr(self.offload_keys)
80
81
84
-class MockOffloadingHandler(OffloadingHandler):
82
+class MockOffloadingWorker(OffloadingWorker):
83
def __init__(self):
86
- self.transfer_specs: dict[int, TransferSpec] = {}
84
+ self.transfer_specs: dict[int, tuple[LoadStoreSpec, LoadStoreSpec]] = {}
85
self.completed_transfers: list[TransferResult] = []
86
self.waiting_jobs: set[int] = set()
87
self.completed_jobs: list[int] = []
@@ -94,8 +92,17 @@ class MockOffloadingHandler(OffloadingHandler):
92
self.completed_transfers = []
93
return finished
94
97
- def transfer_async(self, job_id: int, spec: TransferSpec) -> bool:
98
- self.transfer_specs[job_id] = spec
95
+ def submit_store(
96
+ self, job_id: int, src_spec: LoadStoreSpec, dst_spec: LoadStoreSpec
97
+ ) -> bool: # type: ignore[override]
98
+ self.transfer_specs[job_id] = (src_spec, dst_spec)
99
+ self.waiting_jobs.add(job_id)
100
+ return True
101
+
102
+ def submit_load(
103
+ self, job_id: int, src_spec: LoadStoreSpec, dst_spec: LoadStoreSpec
104
+ ) -> bool: # type: ignore[override]
105
+ self.transfer_specs[job_id] = (src_spec, dst_spec)
106
self.waiting_jobs.add(job_id)
107
return True
108
@@ -109,7 +116,6 @@ class MockOffloadingHandler(OffloadingHandler):
116
success=True,
117
transfer_size=None,
118
transfer_time=None,
112
- transfer_type=None,
119
)
120
self.completed_transfers.append(result)
121
@@ -127,21 +133,18 @@ class MockOffloadingSpec(OffloadingSpec):
133
self.manager.prepare_load = lambda keys, req_context: MockLoadStoreSpec(keys)
134
self.manager.lookup.return_value = False
135
self.manager.on_new_request.return_value = RequestOffloadingContext()
130
- self.handler = MockOffloadingHandler()
136
+ self.handler = MockOffloadingWorker()
137
138
def get_manager(self) -> OffloadingManager:
139
return self.manager
140
135
- def get_handlers(
136
- self, _
137
- ) -> Iterator[tuple[type[LoadStoreSpec], type[LoadStoreSpec], OffloadingHandler]]:
138
- yield GPULoadStoreSpec, MockLoadStoreSpec, self.handler
139
- yield MockLoadStoreSpec, GPULoadStoreSpec, self.handler
141
+ def get_worker(self, _: CanonicalKVCaches) -> OffloadingWorker:
142
+ return self.handler
143
144
def complete_transfers(self):
145
self.handler.complete_jobs(self.handler.waiting_jobs.copy())
146
144
- def get_completed_transfers(self) -> list[TransferSpec]:
147
+ def get_completed_transfers(self) -> list[tuple[LoadStoreSpec, LoadStoreSpec]]:
148
specs = [
149
self.handler.transfer_specs[job_id]
150
for job_id in self.handler.completed_jobs
@@ -149,7 +152,7 @@ class MockOffloadingSpec(OffloadingSpec):
152
self.handler.completed_jobs.clear()
153
return specs
154
152
- def get_flushed_transfers(self):
155
+ def get_flushed_transfers(self) -> list[tuple[LoadStoreSpec, LoadStoreSpec]]:
156
specs = [
157
self.handler.transfer_specs[job_id] for job_id in self.handler.flushed_jobs
158
]
@@ -362,8 +365,7 @@ class RequestRunner:
365
self.scheduler.add_request(req)
366
367
def _parse_transfers(self):
365
- for transfer_spec in self.offloading_spec.get_flushed_transfers():
366
- src_spec, dst_spec = transfer_spec
368
+ for src_spec, dst_spec in self.offloading_spec.get_flushed_transfers():
369
if isinstance(src_spec, GPULoadStoreSpec):
370
# store flush
371
for block_id in src_spec.block_ids:
@@ -375,9 +377,7 @@ class RequestRunner:
377
378
block_size_factor = self.block_size_factor
379
378
- for transfer_spec in self.offloading_spec.get_completed_transfers():
379
- src_spec, dst_spec = transfer_spec
380
-
380
+ for src_spec, dst_spec in self.offloading_spec.get_completed_transfers():
381
if isinstance(src_spec, GPULoadStoreSpec):
382
store = True
383
gpu_spec = src_spec
tests/v1/kv_offload/cpu/test_gpu_worker.py
+20
-21
@@ -17,7 +17,7 @@ from vllm.v1.kv_offload.base import (
17
GPULoadStoreSpec,
18
)
19
from vllm.v1.kv_offload.cpu.common import CPULoadStoreSpec
20
-from vllm.v1.kv_offload.cpu.gpu_worker import CpuGpuOffloadingHandlers
20
+from vllm.v1.kv_offload.cpu.gpu_worker import CPUOffloadingWorker
21
from vllm.v1.kv_offload.cpu.shared_offload_region import SharedOffloadRegion
22
23
NUM_GPU_BLOCKS = [64]
@@ -103,7 +103,7 @@ def test_transfer(
103
cpu_page_size=cpu_page_size,
104
)
105
106
- handlers = CpuGpuOffloadingHandlers(
106
+ worker = CPUOffloadingWorker(
107
kv_caches=kv_caches,
108
block_size_factor=block_size_factor,
109
num_cpu_blocks=num_cpu_blocks,
@@ -130,7 +130,7 @@ def test_transfer(
130
131
# set transfer direction
132
if gpu_to_cpu:
133
- handler = handlers.gpu_to_cpu_handler
133
+ handler = worker._store_handler
134
src_spec = GPULoadStoreSpec(
135
gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
136
)
@@ -138,7 +138,7 @@ def test_transfer(
138
dst_to_src = dict(zip(cpu_blocks_expanded, gpu_blocks))
139
num_dst_sub_blocks = num_gpu_blocks
140
else:
141
- handler = handlers.cpu_to_gpu_handler
141
+ handler = worker._load_handler
142
src_spec = CPULoadStoreSpec(cpu_blocks)
143
dst_spec = GPULoadStoreSpec(
144
gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
@@ -156,23 +156,21 @@ def test_transfer(
156
orig_src_tensors = [x.clone() for x in handler.src_tensors]
157
orig_dst_tensors = [x.clone() for x in handler.dst_tensors]
158
159
- # call transfer function
159
+ # call transfer function via public API
160
start_time = time.time()
161
- assert handler.transfer_async(1, (src_spec, dst_spec))
161
+ if gpu_to_cpu:
162
+ assert worker.submit_store(1, src_spec, dst_spec)
163
+ else:
164
+ assert worker.submit_load(1, src_spec, dst_spec)
165
assert {x.job_id for x in handler._transfers} == {1}
166
167
# wait for transfer to complete
168
end_time = time.time() + 10
169
while time.time() < end_time:
167
- finished = handler.get_finished()
170
+ finished = worker.get_finished()
171
if finished:
172
assert finished[0].job_id == 1
173
assert finished[0].success
171
- assert (
172
- finished[0].transfer_type == ("GPU", "CPU")
173
- if gpu_to_cpu
174
- else ("CPU", "GPU")
175
- )
174
assert finished[0].transfer_size == (
175
len(gpu_blocks)
176
* sum([x.page_size_bytes for x in handler.kv_cache_groups_data_refs[0]])
@@ -208,8 +206,7 @@ def test_transfer(
206
del orig_tensor, tensor, src_tensor, dst_tensor, orig_dst_tensor
207
del src_view, dst_view, orig_dst_view, expected
208
211
- handlers.cpu_to_gpu_handler.shutdown()
212
- handlers.gpu_to_cpu_handler.shutdown()
209
+ worker.shutdown()
210
if mmap_region:
211
mmap_region.cleanup()
212
@@ -276,7 +273,7 @@ def test_transfer_multi_group(
273
tensors=kv_cache_tensors, group_data_refs=kv_cache_groups_data_refs
274
)
275
279
- handlers = CpuGpuOffloadingHandlers(
276
+ worker = CPUOffloadingWorker(
277
kv_caches=canonical_kv_caches,
278
block_size_factor=block_size_factor,
279
num_cpu_blocks=num_cpu_blocks,
@@ -338,7 +335,7 @@ def test_transfer_multi_group(
335
block_indices: list[int] = [0, 0, sub_blocks_to_skip]
336
337
if gpu_to_cpu:
341
- handler = handlers.gpu_to_cpu_handler
338
+ handler = worker._store_handler
339
src_spec = GPULoadStoreSpec(
340
gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
341
)
@@ -352,7 +349,7 @@ def test_transfer_multi_group(
349
]
350
num_dst_sub_blocks = num_cpu_blocks * block_size_factor
351
else:
355
- handler = handlers.cpu_to_gpu_handler
352
+ handler = worker._load_handler
353
src_spec = CPULoadStoreSpec(cpu_blocks)
354
dst_spec = GPULoadStoreSpec(
355
gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
@@ -375,12 +372,15 @@ def test_transfer_multi_group(
372
orig_src_tensors = [x.clone() for x in handler.src_tensors]
373
orig_dst_tensors = [x.clone() for x in handler.dst_tensors]
374
378
- assert handler.transfer_async(1, (src_spec, dst_spec))
375
+ if gpu_to_cpu:
376
+ assert worker.submit_store(1, src_spec, dst_spec)
377
+ else:
378
+ assert worker.submit_load(1, src_spec, dst_spec)
379
assert {x.job_id for x in handler._transfers} == {1}
380
381
end_time = time.time() + 10
382
while time.time() < end_time:
383
- finished = handler.get_finished()
383
+ finished = worker.get_finished()
384
if finished:
385
assert finished[0].job_id == 1
386
assert finished[0].success
@@ -418,5 +418,4 @@ def test_transfer_multi_group(
418
dst_view[dst_sub_block].cpu(), expected.cpu()
419
)
420
421
- handlers.cpu_to_gpu_handler.shutdown()
422
- handlers.gpu_to_cpu_handler.shutdown()
421
+ worker.shutdown()
tests/v1/kv_offload/test_worker.py
deleted
-165
@@ -1,165 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from vllm.v1.kv_offload.base import LoadStoreSpec
4
-from vllm.v1.kv_offload.worker.worker import (
5
- OffloadingHandler,
6
- OffloadingWorker,
7
- TransferResult,
8
- TransferSpec,
9
-)
10
-
11
-
12
-class LoadStoreSpec1(LoadStoreSpec):
13
- def __init__(
14
- self,
15
- submit_success: bool = True,
16
- async_success: bool = True,
17
- exception: bool = False,
18
- ):
19
- self.finished = False
20
- self.submit_success = submit_success
21
- self.async_success = async_success
22
- self.exception = exception
23
-
24
- @staticmethod
25
- def medium() -> str:
26
- return "1"
27
-
28
- def __repr__(self):
29
- return f"{self.medium()}: {id(self)}"
30
-
31
-
32
-class LoadStoreSpec2(LoadStoreSpec):
33
- @staticmethod
34
- def medium() -> str:
35
- return "2"
36
-
37
- def __repr__(self):
38
- return f"{self.medium()}: {id(self)}"
39
-
40
-
41
-class OffloadingHandler1To2(OffloadingHandler):
42
- def __init__(self):
43
- self.transfers: dict[int, LoadStoreSpec1] = {}
44
-
45
- def transfer_async(self, job_id: int, spec: TransferSpec) -> bool:
46
- src, dst = spec
47
- assert isinstance(src, LoadStoreSpec1)
48
- assert isinstance(dst, LoadStoreSpec2)
49
-
50
- if src.exception:
51
- raise Exception("An expected exception. Don't worry!")
52
- if not src.submit_success:
53
- return False
54
-
55
- self.transfers[job_id] = src
56
- return True
57
-
58
- def get_finished(self) -> list[TransferResult]:
59
- finished = []
60
- for job_id, spec in list(self.transfers.items()):
61
- if spec.finished:
62
- finished.append((job_id, spec.async_success))
63
- del self.transfers[job_id]
64
- return finished
65
-
66
- def wait(self, job_ids: set[int]) -> None:
67
- for job_id in job_ids:
68
- spec = self.transfers.get(job_id)
69
- if spec:
70
- assert spec.finished
71
-
72
-
73
-class OffloadingHandler2To1(OffloadingHandler):
74
- def __init__(self):
75
- self.transfers: dict[int, LoadStoreSpec1] = {}
76
-
77
- def transfer_async(self, job_id: int, spec: TransferSpec) -> bool:
78
- src, dst = spec
79
- assert isinstance(src, LoadStoreSpec2)
80
- assert isinstance(dst, LoadStoreSpec1)
81
-
82
- self.transfers[job_id] = dst
83
- return True
84
-
85
- def get_finished(self) -> list[TransferResult]:
86
- finished = []
87
- for job_id, spec in list(self.transfers.items()):
88
- if spec.finished:
89
- finished.append((job_id, spec.async_success))
90
- del self.transfers[job_id]
91
- return finished
92
-
93
- def wait(self, job_ids: set[int]) -> None:
94
- for job_id in job_ids:
95
- spec = self.transfers.get(job_id)
96
- if spec:
97
- assert spec.finished
98
-
99
-
100
-def test_offloading_worker():
101
- """
102
- Tests OffloadingWorker with 2 handlers.
103
- One handler performs 1->2 transfers, and the other handles 2->1.
104
- """
105
- worker = OffloadingWorker()
106
- handler1to2 = OffloadingHandler1To2()
107
- handler2to1 = OffloadingHandler2To1()
108
- worker.register_handler(LoadStoreSpec1, LoadStoreSpec2, handler1to2)
109
- worker.register_handler(LoadStoreSpec2, LoadStoreSpec1, handler2to1)
110
-
111
- # 1st transfer 1->2 (exception)
112
- src1 = LoadStoreSpec1(exception=True)
113
- dst1 = LoadStoreSpec2()
114
- assert not worker.transfer_async(1, (src1, dst1))
115
-
116
- # 2ed transfer 1->2 (failure to submit)
117
- src2 = LoadStoreSpec1(submit_success=False)
118
- dst2 = LoadStoreSpec2()
119
- assert not worker.transfer_async(2, (src2, dst2))
120
-
121
- # 3rd transfer 1->2 (failure)
122
- src3 = LoadStoreSpec1(async_success=False)
123
- dst3 = LoadStoreSpec2()
124
- assert worker.transfer_async(3, (src3, dst3))
125
-
126
- # 4th transfer 1->2 (success)
127
- src4 = LoadStoreSpec1()
128
- dst4 = LoadStoreSpec2()
129
- worker.transfer_async(4, (src4, dst4))
130
- assert set(handler1to2.transfers.keys()) == {3, 4}
131
-
132
- # 5th transfer 2->1
133
- src5 = LoadStoreSpec2()
134
- dst5 = LoadStoreSpec1()
135
- worker.transfer_async(5, (src5, dst5))
136
- assert set(handler2to1.transfers.keys()) == {5}
137
-
138
- # no transfer completed yet
139
- assert worker.get_finished() == []
140
-
141
- # complete 3rd, 4th
142
- src3.finished = True
143
- src4.finished = True
144
-
145
- # 6th transfer 1->2
146
- src6 = LoadStoreSpec1()
147
- dst6 = LoadStoreSpec2()
148
- worker.transfer_async(6, (src6, dst6))
149
-
150
- # 7th transfer 2->1
151
- src7 = LoadStoreSpec2()
152
- dst7 = LoadStoreSpec1()
153
- worker.transfer_async(7, (src7, dst7))
154
-
155
- # 6th and 7th transfers started
156
- assert 6 in handler1to2.transfers
157
- assert 7 in handler2to1.transfers
158
-
159
- # verify result of 3rd and 4th transfers
160
- assert sorted(worker.get_finished()) == [(3, False), (4, True)]
161
-
162
- # complete 6th and 7th transfers
163
- src6.finished = True
164
- dst7.finished = True
165
- assert sorted(worker.get_finished()) == [(6, True), (7, True)]
vllm/distributed/kv_transfer/kv_connector/v1/offloading/common.py
+3
-2
@@ -6,7 +6,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import (
6
KVConnectorMetadata,
7
KVConnectorWorkerMetadata,
8
)
9
-from vllm.v1.kv_offload.worker.worker import TransferSpec
9
+from vllm.v1.kv_offload.base import LoadStoreSpec
10
11
ReqId = str
12
@@ -60,7 +60,8 @@ class TransferJob:
60
"""
61
62
req_id: ReqId
63
- transfer_spec: TransferSpec
63
+ src_spec: LoadStoreSpec
64
+ dst_spec: LoadStoreSpec
65
66
67
@dataclass
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+3
-2
@@ -762,7 +762,8 @@ class OffloadingConnectorScheduler:
762
load_job_id = self._generate_job_id()
763
self._current_batch_load_jobs[load_job_id] = TransferJob(
764
req_id=request.request_id,
765
- transfer_spec=(src_spec, dst_spec),
765
+ src_spec=src_spec,
766
+ dst_spec=dst_spec,
767
)
768
# a load can only be issued when no other jobs are pending.
769
assert not req_status.transfer_jobs
@@ -998,7 +999,7 @@ class OffloadingConnectorScheduler:
999
)
1000
1001
store_jobs[job_id] = TransferJob(
1001
- req_id=req_id, transfer_spec=(src_spec, dst_spec)
1002
+ req_id=req_id, src_spec=src_spec, dst_spec=dst_spec
1003
)
1004
1005
logger.debug(
vllm/distributed/kv_transfer/kv_connector/v1/offloading/worker.py
+26
-18
@@ -21,11 +21,10 @@ from vllm.v1.kv_offload.base import (
21
CanonicalKVCacheRef,
22
CanonicalKVCaches,
23
CanonicalKVCacheTensor,
24
+ GPULoadStoreSpec,
25
+ LoadStoreSpec,
26
OffloadingSpec,
25
-)
26
-from vllm.v1.kv_offload.worker.worker import (
27
OffloadingWorker,
28
- TransferSpec,
28
)
29
30
logger = init_logger(__name__)
@@ -36,16 +35,17 @@ class OffloadingConnectorWorker:
35
36
def __init__(self, spec: OffloadingSpec):
37
self.spec = spec
39
- self.worker = OffloadingWorker()
38
+ self.worker: OffloadingWorker | None = None
39
40
# job_id -> req_id for in-flight loads.
41
self._load_jobs: dict[int, ReqId] = {}
43
- self._unsubmitted_store_jobs: list[tuple[int, TransferSpec]] = []
42
+ self._unsubmitted_store_jobs: list[
43
+ tuple[int, GPULoadStoreSpec, LoadStoreSpec]
44
+ ] = []
45
self._connector_worker_meta = OffloadingWorkerMetadata()
46
46
- def _register_handlers(self, kv_caches: CanonicalKVCaches):
47
- for src_cls, dst_cls, handler in self.spec.get_handlers(kv_caches):
48
- self.worker.register_handler(src_cls, dst_cls, handler)
47
+ def _init_worker(self, kv_caches: CanonicalKVCaches) -> None:
48
+ self.worker = self.spec.get_worker(kv_caches)
49
50
def register_kv_caches(
51
self, kv_caches: dict[str, torch.Tensor | list[torch.Tensor]]
@@ -138,7 +138,7 @@ class OffloadingConnectorWorker:
138
(block_stride, 1),
139
storage_offset=0,
140
)
141
- self._register_handlers(
141
+ self._init_worker(
142
CanonicalKVCaches(
143
[CanonicalKVCacheTensor(packed_tensor, block_stride)],
144
[
@@ -204,7 +204,7 @@ class OffloadingConnectorWorker:
204
group_data_refs=group_data_refs,
205
)
206
207
- self._register_handlers(canonical_kv_caches)
207
+ self._init_worker(canonical_kv_caches)
208
209
def register_cross_layers_kv_cache(
210
self, kv_cache: torch.Tensor, attn_backend: type[AttentionBackend]
@@ -251,11 +251,12 @@ class OffloadingConnectorWorker:
251
tensors=[kv_cache_tensor], group_data_refs=[[kv_cache_data_ref]]
252
)
253
254
- self._register_handlers(canonical_kv_caches)
254
+ self._init_worker(canonical_kv_caches)
255
256
def handle_preemptions(self, kv_connector_metadata: OffloadingConnectorMetadata):
257
- for job_id, transfer_spec in self._unsubmitted_store_jobs:
258
- success = self.worker.transfer_async(job_id, transfer_spec)
257
+ assert self.worker is not None
258
+ for job_id, src_spec, dst_spec in self._unsubmitted_store_jobs:
259
+ success = self.worker.submit_store(job_id, src_spec, dst_spec)
260
assert success
261
self._unsubmitted_store_jobs.clear()
262
@@ -263,14 +264,16 @@ class OffloadingConnectorWorker:
264
self.worker.wait(kv_connector_metadata.jobs_to_flush)
265
266
def start_kv_transfers(self, metadata: OffloadingConnectorMetadata):
266
- for job_id, transfer_spec in self._unsubmitted_store_jobs:
267
- success = self.worker.transfer_async(job_id, transfer_spec)
267
+ assert self.worker is not None
268
+ for job_id, src_spec, dst_spec in self._unsubmitted_store_jobs:
269
+ success = self.worker.submit_store(job_id, src_spec, dst_spec)
270
assert success
271
self._unsubmitted_store_jobs.clear()
272
273
for job_id, entry in metadata.load_jobs.items():
274
self._load_jobs[job_id] = entry.req_id
273
- success = self.worker.transfer_async(job_id, entry.transfer_spec)
275
+ assert isinstance(entry.dst_spec, GPULoadStoreSpec)
276
+ success = self.worker.submit_load(job_id, entry.src_spec, entry.dst_spec)
277
assert success
278
279
def prepare_store_kv(self, metadata: OffloadingConnectorMetadata):
@@ -278,7 +281,10 @@ class OffloadingConnectorWorker:
281
# NOTE(orozery): defer the store to the beginning of the next
282
# engine step, so that offloading starts AFTER transfers related
283
# to token sampling, thereby avoiding delays to token generation.
281
- self._unsubmitted_store_jobs.append((job_id, entry.transfer_spec))
284
+ assert isinstance(entry.src_spec, GPULoadStoreSpec)
285
+ self._unsubmitted_store_jobs.append(
286
+ (job_id, entry.src_spec, entry.dst_spec)
287
+ )
288
289
def get_finished(self, finished_req_ids: set[str]) -> tuple[set[str], set[str]]:
290
"""
@@ -290,6 +296,7 @@ class OffloadingConnectorWorker:
296
finished_recving so the base scheduler can resume requests
297
blocked on remote KV (and free aborted-during-load reqs).
298
"""
299
+ assert self.worker is not None
300
finished_recving: set[str] = set()
301
for transfer_result in self.worker.get_finished():
302
# we currently do not support job failures
@@ -328,4 +335,5 @@ class OffloadingConnectorWorker:
335
self._unsubmitted_store_jobs.clear()
336
self._load_jobs.clear()
337
self._connector_worker_meta = OffloadingWorkerMetadata()
331
- self.worker.shutdown()
338
+ if self.worker is not None:
339
+ self.worker.shutdown()
vllm/v1/kv_offload/base.py
+40
-8
@@ -5,7 +5,7 @@ Core abstractions for KV cache offloading in vLLM v1.
5
"""
6
7
from abc import ABC, abstractmethod
8
-from collections.abc import Collection, Iterable, Iterator, Sequence
8
+from collections.abc import Collection, Iterable, Sequence
9
from dataclasses import dataclass
10
from enum import Enum
11
from typing import TYPE_CHECKING, Any, NewType
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
23
OffloadingConnectorStats,
24
)
25
from vllm.v1.kv_cache_interface import KVCacheConfig
26
- from vllm.v1.kv_offload.worker.worker import OffloadingHandler
26
27
# `OffloadKey` identifies an offloaded block. It combines a block hash with
28
# its KV cache group index, encoded as raw bytes to avoid tuple GC overhead.
@@ -432,6 +431,41 @@ class CanonicalKVCaches:
431
group_data_refs: list[list[CanonicalKVCacheRef]]
432
433
434
+@dataclass
435
+class TransferResult:
436
+ job_id: int
437
+ success: bool
438
+ transfer_size: int | None = None
439
+ transfer_time: float | None = None
440
+
441
+
442
+class OffloadingWorker(ABC):
443
+ """Runs in the worker process. Performs async KV transfers for ONE
444
+ offloaded medium (e.g. CPU). Direction is explicit via submit_store /
445
+ submit_load, so there is no (src_medium, dst_medium) routing."""
446
+
447
+ @abstractmethod
448
+ def submit_store(
449
+ self, job_id: int, src_spec: GPULoadStoreSpec, dst_spec: LoadStoreSpec
450
+ ) -> bool:
451
+ """Async GPU -> offloaded medium."""
452
+
453
+ @abstractmethod
454
+ def submit_load(
455
+ self, job_id: int, src_spec: LoadStoreSpec, dst_spec: GPULoadStoreSpec
456
+ ) -> bool:
457
+ """Async offloaded medium -> GPU."""
458
+
459
+ @abstractmethod
460
+ def get_finished(self) -> list[TransferResult]: ...
461
+
462
+ @abstractmethod
463
+ def wait(self, job_ids: set[int]) -> None: ...
464
+
465
+ def shutdown(self) -> None:
466
+ return
467
+
468
+
469
class OffloadingSpec(ABC):
470
"""Spec for an offloading connector"""
471
@@ -524,16 +558,14 @@ class OffloadingSpec(ABC):
558
pass
559
560
@abstractmethod
527
- def get_handlers(
528
- self, kv_caches: CanonicalKVCaches
529
- ) -> Iterator[tuple[type[LoadStoreSpec], type[LoadStoreSpec], "OffloadingHandler"]]:
561
+ def get_worker(self, kv_caches: CanonicalKVCaches) -> OffloadingWorker:
562
"""
531
- Get offloading handlers along with their respective src and dst types.
563
+ Get an OffloadingWorker that handles async KV transfers for this spec.
564
565
Args:
566
kv_caches: Canonicalized KV caches.
567
536
- Yields:
537
- Tuples of (src_type, dst_type, offloading_handler).
568
+ Returns:
569
+ An OffloadingWorker instance for this medium.
570
"""
571
pass
vllm/v1/kv_offload/cpu/gpu_worker.py
+41
-21
@@ -7,7 +7,6 @@ from dataclasses import dataclass
7
8
import numpy as np
9
import torch
10
-from typing_extensions import override
10
11
from vllm import _custom_ops as ops
12
from vllm.logger import init_logger
@@ -20,17 +19,15 @@ from vllm.v1.kv_offload.base import (
19
CanonicalKVCacheRef,
20
CanonicalKVCaches,
21
GPULoadStoreSpec,
22
+ LoadStoreSpec,
23
+ OffloadingWorker,
24
+ TransferResult,
25
)
26
from vllm.v1.kv_offload.cpu.shared_offload_region import SharedOffloadRegion
27
from vllm.v1.kv_offload.cpu.swap_blocks_triton import (
28
THRESHOLD_BYTES,
29
swap_blocks_batch,
30
)
29
-from vllm.v1.kv_offload.worker.worker import (
30
- OffloadingHandler,
31
- TransferResult,
32
- TransferSpec,
33
-)
31
32
logger = init_logger(__name__)
33
@@ -166,10 +163,9 @@ def _new_descriptor_buffers(
163
)
164
165
169
-class SingleDirectionOffloadingHandler(OffloadingHandler):
166
+class SingleDirectionOffloadingHandler:
167
"""
171
- SingleDirectionOffloadingHandler handles transfers for a single direction,
172
- either CPU->GPU or GPU->CPU.
168
+ Handles transfers for a single direction, either CPU->GPU or GPU->CPU.
169
Transfers are guaranteed to be executed in order of their submission.
170
Each transfer uses a unique CUDA stream, and its stream will start
171
executing only after the streams of previous transfers have finished.
@@ -228,7 +224,6 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
224
self.src_block_size_factor = 1 if self.gpu_to_cpu else block_size_factor
225
self.dst_block_size_factor = block_size_factor if self.gpu_to_cpu else 1
226
231
- self.transfer_type = ("GPU", "CPU") if self.gpu_to_cpu else ("CPU", "GPU")
227
# mmap_region to clean up on shutdown (gpu_to_cpu handler owns it)
228
self._mmap_region = mmap_region
229
# job_id -> event
@@ -242,9 +237,9 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
237
# list of pinned descriptor buffer sets available for re-use
238
self._buffer_pool: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = []
239
245
- @override
246
- def transfer_async(self, job_id: int, transfer_spec: TransferSpec) -> bool:
247
- src_spec, dst_spec = transfer_spec
240
+ def transfer_async(
241
+ self, job_id: int, src_spec: LoadStoreSpec, dst_spec: LoadStoreSpec
242
+ ) -> bool:
243
assert isinstance(src_spec, BlockIDsLoadStoreSpec)
244
assert isinstance(dst_spec, BlockIDsLoadStoreSpec)
245
@@ -425,7 +420,6 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
420
# success
421
return True
422
428
- @override
423
def get_finished(self) -> list[TransferResult]:
424
results: list[TransferResult] = []
425
while self._transfers and self._transfers[0].end_event.query():
@@ -438,7 +432,6 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
432
success=True,
433
transfer_size=transfer.num_bytes,
434
transfer_time=transfer_time,
441
- transfer_type=self.transfer_type,
435
)
436
437
results.append(result)
@@ -451,14 +444,12 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
444
del self._transfer_events[transfer.job_id]
445
return results
446
454
- @override
447
def wait(self, job_ids: set[int]):
448
for job_id in job_ids:
449
event = self._transfer_events.get(job_id)
450
if event is not None:
451
event.synchronize()
452
461
- @override
453
def shutdown(self) -> None:
454
while self._transfers:
455
transfer = self._transfers.popleft()
@@ -474,7 +465,14 @@ class SingleDirectionOffloadingHandler(OffloadingHandler):
465
self._mmap_region = None
466
467
477
-class CpuGpuOffloadingHandlers:
468
+class CPUOffloadingWorker(OffloadingWorker):
469
+ """OffloadingWorker for CPU offloading.
470
+
471
+ Composes two SingleDirectionOffloadingHandler instances (one for each
472
+ direction) and exposes them through the explicit submit_store /
473
+ submit_load API.
474
+ """
475
+
476
def __init__(
477
self,
478
kv_caches: CanonicalKVCaches,
@@ -484,7 +482,6 @@ class CpuGpuOffloadingHandlers:
482
):
483
pin_memory = PIN_MEMORY
484
logger.info("Allocating %d CPU tensors...", len(kv_caches.tensors))
487
- self._mmap_region = mmap_region
485
if mmap_region is not None and pin_memory:
486
pin_mmap_region(mmap_region)
487
@@ -518,7 +515,7 @@ class CpuGpuOffloadingHandlers:
515
gpu_tensors.append(gpu_tensor)
516
cpu_tensors.append(cpu_tensor)
517
521
- self.gpu_to_cpu_handler = SingleDirectionOffloadingHandler(
518
+ self._store_handler = SingleDirectionOffloadingHandler(
519
gpu_tensors=gpu_tensors,
520
cpu_tensors=cpu_tensors,
521
block_size_factor=block_size_factor,
@@ -527,10 +524,33 @@ class CpuGpuOffloadingHandlers:
524
mmap_region=mmap_region,
525
)
526
530
- self.cpu_to_gpu_handler = SingleDirectionOffloadingHandler(
527
+ self._load_handler = SingleDirectionOffloadingHandler(
528
gpu_tensors=gpu_tensors,
529
cpu_tensors=cpu_tensors,
530
block_size_factor=block_size_factor,
531
kv_cache_groups_data_refs=kv_caches.group_data_refs,
532
gpu_to_cpu=False,
533
)
534
+
535
+ def submit_store(
536
+ self, job_id: int, src_spec: GPULoadStoreSpec, dst_spec: LoadStoreSpec
537
+ ) -> bool:
538
+ """Async GPU -> CPU."""
539
+ return self._store_handler.transfer_async(job_id, src_spec, dst_spec)
540
+
541
+ def submit_load(
542
+ self, job_id: int, src_spec: LoadStoreSpec, dst_spec: GPULoadStoreSpec
543
+ ) -> bool:
544
+ """Async CPU -> GPU."""
545
+ return self._load_handler.transfer_async(job_id, src_spec, dst_spec)
546
+
547
+ def get_finished(self) -> list[TransferResult]:
548
+ return self._store_handler.get_finished() + self._load_handler.get_finished()
549
+
550
+ def wait(self, job_ids: set[int]) -> None:
551
+ self._store_handler.wait(job_ids)
552
+ self._load_handler.wait(job_ids)
553
+
554
+ def shutdown(self) -> None:
555
+ self._store_handler.shutdown()
556
+ self._load_handler.shutdown()
vllm/v1/kv_offload/cpu/spec.py
+11
-20
@@ -1,6 +1,5 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from collections.abc import Iterator
3
from typing import Any
4
5
from typing_extensions import override
@@ -11,21 +10,16 @@ from vllm.utils.math_utils import round_up
10
from vllm.v1.kv_cache_interface import KVCacheConfig
11
from vllm.v1.kv_offload.base import (
12
CanonicalKVCaches,
14
- GPULoadStoreSpec,
15
- LoadStoreSpec,
13
OffloadingCounterMetadata,
14
OffloadingGaugeMetadata,
15
OffloadingManager,
16
OffloadingMetricMetadata,
17
OffloadingSpec,
18
+ OffloadingWorker,
19
)
22
-from vllm.v1.kv_offload.cpu.common import (
23
- CPULoadStoreSpec,
24
- CPUOffloadingMetrics,
25
-)
26
-from vllm.v1.kv_offload.cpu.gpu_worker import CpuGpuOffloadingHandlers
20
+from vllm.v1.kv_offload.cpu.common import CPUOffloadingMetrics
21
+from vllm.v1.kv_offload.cpu.gpu_worker import CPUOffloadingWorker
22
from vllm.v1.kv_offload.cpu.manager import CPUOffloadingManager
28
-from vllm.v1.kv_offload.worker.worker import OffloadingHandler
23
24
25
class CPUOffloadingSpec(OffloadingSpec):
@@ -107,7 +101,7 @@ class CPUOffloadingSpec(OffloadingSpec):
101
self._manager: OffloadingManager | None = None
102
103
# worker-side
110
- self._handlers: CpuGpuOffloadingHandlers | None = None
104
+ self._worker: CPUOffloadingWorker | None = None
105
106
self.eviction_policy: str = self.extra_config.get("eviction_policy", "lru")
107
@@ -131,25 +125,22 @@ class CPUOffloadingSpec(OffloadingSpec):
125
)
126
return self._manager
127
134
- def create_handlers(self, kv_caches: CanonicalKVCaches) -> CpuGpuOffloadingHandlers:
135
- return CpuGpuOffloadingHandlers(
128
+ def create_worker(self, kv_caches: CanonicalKVCaches) -> CPUOffloadingWorker:
129
+ return CPUOffloadingWorker(
130
kv_caches=kv_caches,
131
block_size_factor=self.block_size_factor,
132
num_cpu_blocks=self.num_blocks,
133
)
134
135
@override
142
- def get_handlers(
143
- self, kv_caches: CanonicalKVCaches
144
- ) -> Iterator[tuple[type[LoadStoreSpec], type[LoadStoreSpec], OffloadingHandler]]:
145
- if not self._handlers:
136
+ def get_worker(self, kv_caches: CanonicalKVCaches) -> OffloadingWorker:
137
+ if not self._worker:
138
if not (current_platform.is_cuda_alike() or current_platform.is_xpu()):
139
raise Exception(
140
"CPU Offloading is currently only supported on CUDA-alike "
141
"and XPU GPUs"
142
)
151
- self._handlers = self.create_handlers(kv_caches)
143
+ self._worker = self.create_worker(kv_caches)
144
153
- assert self._handlers is not None
154
- yield GPULoadStoreSpec, CPULoadStoreSpec, self._handlers.gpu_to_cpu_handler
155
- yield CPULoadStoreSpec, GPULoadStoreSpec, self._handlers.cpu_to_gpu_handler
145
+ assert self._worker is not None
146
+ return self._worker
vllm/v1/kv_offload/tiering/spec.py
+5
-5
@@ -44,7 +44,7 @@ from vllm.v1.kv_offload.base import (
44
OffloadingManager,
45
OffloadingMetricMetadata,
46
)
47
-from vllm.v1.kv_offload.cpu.gpu_worker import CpuGpuOffloadingHandlers
47
+from vllm.v1.kv_offload.cpu.gpu_worker import CPUOffloadingWorker
48
from vllm.v1.kv_offload.cpu.shared_offload_region import SharedOffloadRegion
49
from vllm.v1.kv_offload.cpu.spec import CPUOffloadingSpec
50
from vllm.v1.kv_offload.tiering.factory import SecondaryTierFactory
@@ -163,8 +163,8 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
163
raise
164
165
# Create TieringOffloadingManager. GPU↔CPU transfers use the inherited
166
- # get_handlers(); secondary tier transfers are handled by the
167
- # secondary tier managers and need no additional handlers here.
166
+ # get_worker(). Secondary tier transfers are handled by the
167
+ # secondary tier managers and need no additional workers here.
168
tiering_manager = TieringOffloadingManager(
169
primary_tier=primary_tier,
170
secondary_tiers=secondary_tiers,
@@ -187,7 +187,7 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
187
return self._manager
188
189
@override
190
- def create_handlers(self, kv_caches: CanonicalKVCaches) -> CpuGpuOffloadingHandlers:
190
+ def create_worker(self, kv_caches: CanonicalKVCaches) -> CPUOffloadingWorker:
191
rank = torch.accelerator.current_device_index()
192
worker_mmap = SharedOffloadRegion(
193
instance_id=self.vllm_config.instance_id,
@@ -196,7 +196,7 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
196
kv_bytes_per_block=self.kv_bytes_per_offloaded_block,
197
cpu_page_size=self.cpu_page_size_per_worker,
198
)
199
- return CpuGpuOffloadingHandlers(
199
+ return CPUOffloadingWorker(
200
kv_caches=kv_caches,
201
block_size_factor=self.block_size_factor,
202
num_cpu_blocks=self.num_blocks,
vllm/v1/kv_offload/worker/__init__.py
vllm/v1/kv_offload/worker/worker.py
deleted
-176
@@ -1,176 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from abc import ABC, abstractmethod
4
-from dataclasses import dataclass
5
-
6
-from vllm.logger import init_logger
7
-from vllm.v1.kv_offload.base import LoadStoreSpec
8
-
9
-# a single transfer spec (src_blocks_spec, dst_blocks_spec)
10
-TransferSpec = tuple[LoadStoreSpec, LoadStoreSpec]
11
-# transfers are forwarded to workers by (src_medium, dst_medium)
12
-TransferType = tuple[str, str]
13
-
14
-logger = init_logger(__name__)
15
-
16
-
17
-@dataclass
18
-class TransferResult:
19
- job_id: int
20
- success: bool
21
- transfer_size: int | None = None # Size in bytes
22
- transfer_time: float | None = None
23
- transfer_type: TransferType | None = None
24
-
25
-
26
-class OffloadingHandler(ABC):
27
- """
28
- OffloadingHandler class for managing asynchronous KV data transfers
29
-
30
- This class runs in the worker.
31
- It kicks off async KV data transfer requests, and allows
32
- collecting back completion statuses.
33
-
34
- The class provides the following primitives:
35
- transfer_async() - kicks off a new transfer job
36
- get_finished() - returns a list of newly finished job IDs.
37
- """
38
-
39
- @abstractmethod
40
- def transfer_async(self, job_id: int, spec: TransferSpec) -> bool:
41
- """
42
- Initiates an asynchronous transfer of KV data.
43
-
44
- Args:
45
- job_id: a unique ID that will be used when notifying back on
46
- transfer completion.
47
- spec: the (src, dst) spec of the KV data transfer.
48
-
49
- Returns:
50
- True if transfer was submitted successfully.
51
- """
52
- pass
53
-
54
- @abstractmethod
55
- def get_finished(self) -> list[TransferResult]:
56
- """
57
- Get transfers finished since last call.
58
-
59
- Returns:
60
- A list of (job_id, success) of transfers.
61
- """
62
- pass
63
-
64
- @abstractmethod
65
- def wait(self, job_ids: set[int]) -> None:
66
- """
67
- Wait for jobs to finish (blocking).
68
- Args:
69
- job_ids: The set of job IDs to wait for.
70
- """
71
-
72
- def shutdown(self) -> None:
73
- """Shutdown the handler and release any resources."""
74
- return
75
-
76
-
77
-class OffloadingWorker:
78
- """
79
- OffloadingWorker class for managing asynchronous KV data transfers
80
- using multiple OffloadingHandlers
81
-
82
- This class runs in the worker.
83
- It kicks off async KV data transfer requests, by delegating
84
- to one of its registered OffloadingHandlers, based on the transfer type.
85
-
86
- The class provides the following primitives:
87
- register_handler() - registers a new handler to handle
88
- a specific transfer type
89
- transfer_async() - kicks off a new transfer job
90
- using one of the registered handlers.
91
- get_finished() - returns a list of newly finished job IDs
92
- from all handlers.
93
- """
94
-
95
- def __init__(self):
96
- self.handlers: set[OffloadingHandler] = set()
97
- self.transfer_type_to_handler: dict[TransferType, OffloadingHandler] = {}
98
-
99
- def register_handler(
100
- self,
101
- src_cls: type[LoadStoreSpec],
102
- dst_cls: type[LoadStoreSpec],
103
- handler: OffloadingHandler,
104
- ) -> None:
105
- """
106
- Registers a new handler.
107
-
108
- Args:
109
- src_cls: the source type of transfers handled by this handler.
110
- dst_cls: the destination type of transfers handled by this handler.
111
- handler: the handler that will handle transfers.
112
- """
113
- transfer_type = (src_cls.medium(), dst_cls.medium())
114
- assert transfer_type not in self.transfer_type_to_handler
115
- self.handlers.add(handler)
116
- self.transfer_type_to_handler[transfer_type] = handler
117
-
118
- def transfer_async(self, job_id: int, spec: TransferSpec) -> bool:
119
- """
120
- Initiates an asynchronous transfer of KV data.
121
-
122
- Args:
123
- job_id: a unique ID that will be used when notifying back on
124
- transfer completion.
125
- spec: the (src, dst) spec of the KV data transfer.
126
-
127
- Returns:
128
- True if transfer was submitted successfully.
129
- """
130
- src, dst = spec
131
- transfer_type = (src.medium(), dst.medium())
132
- handler = self.transfer_type_to_handler.get(transfer_type)
133
- assert handler is not None
134
- try:
135
- success = handler.transfer_async(job_id, spec)
136
- except Exception as e:
137
- logger.warning(
138
- "Exception in %r transfer %d: %r",
139
- transfer_type,
140
- job_id,
141
- e,
142
- exc_info=True,
143
- )
144
- return False
145
-
146
- if not success:
147
- logger.warning("Failed to submit %r transfer %d", transfer_type, job_id)
148
- else:
149
- logger.debug("Submitted %r transfer %d: %r", transfer_type, job_id, spec)
150
- return success
151
-
152
- def get_finished(self) -> list[TransferResult]:
153
- """
154
- Get transfers finished since last call.
155
-
156
- Returns:
157
- A list of TransferResults
158
- """
159
- finished = []
160
- for handler in self.handlers:
161
- finished.extend(handler.get_finished())
162
- return finished
163
-
164
- def wait(self, job_ids: set[int]) -> None:
165
- """
166
- Wait for jobs to finish (blocking).
167
-
168
- Args:
169
- job_ids: The set of job IDs to wait for.
170
- """
171
- for handler in self.handlers:
172
- handler.wait(job_ids)
173
-
174
- def shutdown(self) -> None:
175
- for handler in self.handlers:
176
- handler.shutdown()