A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 22:30 UTC
[kv_offload+HMA][4/N]: Support sliding window lookup (#36645)
Signed-off-by: Or Ozeri <oro@il.ibm.com> Co-authored-by: Nicolò Lucchesi <nlucches@redhat.com>
Or Ozeri committed
Apr 20, 2026 at 12:53 UTC
f774ba028afe08fab1ca96ecf7c0b40c0fa45357
7 files changed
+269
-78
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py
+151
-9
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from collections.abc import Iterable
4
+from unittest.mock import MagicMock
5
6
import pytest
7
@@ -10,8 +11,16 @@ from tests.v1.kv_connector.unit.offloading_connector.utils import (
11
)
12
from tests.v1.kv_connector.unit.utils import EOS_TOKEN_ID
13
from vllm.distributed.kv_events import BlockRemoved, BlockStored
14
+from vllm.distributed.kv_transfer.kv_connector.v1.offloading.scheduler import (
15
+ OffloadingConnectorScheduler,
16
+)
17
from vllm.v1.core.kv_cache_utils import BlockHash
14
-from vllm.v1.kv_offload.abstract import OffloadingEvent
18
+from vllm.v1.kv_offload.abstract import (
19
+ OffloadingEvent,
20
+ OffloadingManager,
21
+ ReqContext,
22
+ get_offload_block_hash,
23
+)
24
from vllm.v1.request import RequestStatus
25
26
@@ -105,8 +114,7 @@ def test_offloading_connector(request_runner, async_scheduling: bool):
114
lambda keys, req_context: generate_store_output([])
115
)
116
runner.run(decoded_tokens=[EOS_TOKEN_ID])
108
- runner.manager.lookup.assert_called()
109
- assert len(list(runner.manager.lookup.call_args.args[0])) == 1
117
+ runner.manager.lookup.assert_called_once()
118
119
# single block lookup with a hit
120
runner.scheduler.reset_prefix_cache()
@@ -114,7 +122,7 @@ def test_offloading_connector(request_runner, async_scheduling: bool):
122
runner.manager.prepare_store.side_effect = (
123
lambda keys, req_context: generate_store_output([])
124
)
117
- runner.manager.lookup.return_value = 1
125
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
126
runner.run(
127
decoded_tokens=[EOS_TOKEN_ID], expected_loaded_gpu_block_indexes=(0, 1, 2)
128
)
@@ -126,7 +134,7 @@ def test_offloading_connector(request_runner, async_scheduling: bool):
134
runner.manager.prepare_store.side_effect = (
135
lambda keys, req_context: generate_store_output([])
136
)
129
- runner.manager.lookup.return_value = 1
137
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
138
runner.run(
139
decoded_tokens=[EOS_TOKEN_ID], expected_loaded_gpu_block_indexes=(3, 4, 5)
140
)
@@ -210,7 +218,7 @@ def test_request_preemption(request_runner, async_scheduling: bool):
218
219
# request should now return from preemption
220
# re-load [0, ..., 8] from the CPU and store [9, 10, 11]
213
- runner.manager.lookup.return_value = 3
221
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 3
222
runner.manager.prepare_store.side_effect = (
223
lambda keys, req_context: generate_store_output(keys)
224
)
@@ -251,7 +259,7 @@ def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling:
259
# start a request to load the first block, but don't complete
260
runner.scheduler.reset_prefix_cache()
261
runner.new_request(token_ids=[0] * offloaded_block_size)
254
- runner.manager.lookup.return_value = 1
262
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
263
runner.run(
264
decoded_tokens=[],
265
complete_transfers=False,
@@ -263,7 +271,7 @@ def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling:
271
272
# start a new request to load the same first block
273
runner.new_request(token_ids=[0] * offloaded_block_size)
266
- runner.manager.lookup.return_value = 1
274
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
275
runner.run(
276
decoded_tokens=[],
277
complete_transfers=False,
@@ -311,7 +319,7 @@ def test_abort_loading_requests(request_runner, async_scheduling: bool):
319
# start a request to load the first block, but don't complete
320
runner.scheduler.reset_prefix_cache()
321
runner.new_request(token_ids=[0] * offloaded_block_size)
314
- runner.manager.lookup.return_value = 1
322
+ runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
323
runner.run(
324
decoded_tokens=[],
325
complete_transfers=False,
@@ -336,3 +344,137 @@ def test_abort_loading_requests(request_runner, async_scheduling: bool):
344
345
# assert request is deleted
346
assert req_id not in runner.scheduler.requests
347
+
348
+
349
+# ---------------------------------------------------------------------------
350
+# Unit tests for _maximal_prefix_lookup / _sliding_window_lookup
351
+# ---------------------------------------------------------------------------
352
+
353
+
354
+def _make_scheduler_with_lookup(
355
+ lookup_results: dict[int, bool | None],
356
+) -> OffloadingConnectorScheduler:
357
+ """Create an OffloadingConnectorScheduler with a mocked manager.lookup."""
358
+ manager = MagicMock(spec=OffloadingManager)
359
+ manager.lookup.side_effect = lambda key, req_context: lookup_results.get(
360
+ int(get_offload_block_hash(key).decode()), False
361
+ )
362
+
363
+ scheduler = object.__new__(OffloadingConnectorScheduler)
364
+ scheduler.manager = manager
365
+ return scheduler
366
+
367
+
368
+_EMPTY_REQ_CTX = ReqContext()
369
+
370
+
371
+class TestMaximalPrefixLookup:
372
+ def test_all_hit(self):
373
+ sched = _make_scheduler_with_lookup({1: True, 2: True})
374
+ assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 2
375
+
376
+ def test_all_miss(self):
377
+ sched = _make_scheduler_with_lookup({})
378
+ assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
379
+
380
+ def test_partial_prefix(self):
381
+ sched = _make_scheduler_with_lookup({1: True, 2: True})
382
+ assert sched._maximal_prefix_lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) == 2
383
+
384
+ def test_miss_then_hit(self):
385
+ sched = _make_scheduler_with_lookup({2: True})
386
+ assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
387
+
388
+ def test_single_hit(self):
389
+ sched = _make_scheduler_with_lookup({1: True})
390
+ assert sched._maximal_prefix_lookup(to_keys([1]), _EMPTY_REQ_CTX) == 1
391
+
392
+ def test_empty(self):
393
+ sched = _make_scheduler_with_lookup({})
394
+ assert sched._maximal_prefix_lookup([], _EMPTY_REQ_CTX) == 0
395
+
396
+ def test_none_defers(self):
397
+ sched = _make_scheduler_with_lookup({1: None, 2: True})
398
+ assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) is None
399
+
400
+ def test_none_after_hit_defers(self):
401
+ sched = _make_scheduler_with_lookup({1: True, 2: None})
402
+ assert sched._maximal_prefix_lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) is None
403
+
404
+ def test_none_stops_at_miss(self):
405
+ """None is treated as hit for iteration, but miss stops the scan."""
406
+ sched = _make_scheduler_with_lookup({1: None, 2: False, 3: True})
407
+ assert sched._maximal_prefix_lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) is None
408
+ # lookup should have been called for blocks 1 and 2 (stops at miss)
409
+ assert sched.manager.lookup.call_count == 2
410
+
411
+
412
+class TestSlidingWindowLookup:
413
+ def test_all_hit_exact_window(self):
414
+ sched = _make_scheduler_with_lookup({1: True, 2: True})
415
+ assert sched._sliding_window_lookup(to_keys([1, 2]), 2, _EMPTY_REQ_CTX) == 2
416
+
417
+ def test_all_miss(self):
418
+ sched = _make_scheduler_with_lookup({})
419
+ assert sched._sliding_window_lookup(to_keys([1, 2, 3]), 1, _EMPTY_REQ_CTX) == 0
420
+
421
+ def test_window_at_end(self):
422
+ sched = _make_scheduler_with_lookup({2: True, 3: True})
423
+ assert sched._sliding_window_lookup(to_keys([1, 2, 3]), 2, _EMPTY_REQ_CTX) == 3
424
+
425
+ def test_window_in_middle(self):
426
+ sched = _make_scheduler_with_lookup({2: True, 3: True})
427
+ assert (
428
+ sched._sliding_window_lookup(to_keys([1, 2, 3, 4]), 2, _EMPTY_REQ_CTX) == 3
429
+ )
430
+
431
+ def test_no_full_window_falls_back_to_prefix(self):
432
+ sched = _make_scheduler_with_lookup({1: True, 2: True})
433
+ assert sched._sliding_window_lookup(to_keys([1, 2, 3]), 3, _EMPTY_REQ_CTX) == 2
434
+
435
+ def test_single_block_window(self):
436
+ sched = _make_scheduler_with_lookup({2: True, 3: True})
437
+ assert sched._sliding_window_lookup(to_keys([1, 2, 3]), 1, _EMPTY_REQ_CTX) == 3
438
+
439
+ def test_gap_resets_consecutive(self):
440
+ sched = _make_scheduler_with_lookup({2: True, 3: True, 4: True})
441
+ # [1, 2, 3, 0, 4] — gap at 0 resets, window of 2 found at [2,3]
442
+ assert (
443
+ sched._sliding_window_lookup(to_keys([1, 2, 3, 0, 4]), 2, _EMPTY_REQ_CTX)
444
+ == 3
445
+ )
446
+
447
+ def test_window_prefers_rightmost(self):
448
+ sched = _make_scheduler_with_lookup({1: True, 2: True, 4: True, 5: True})
449
+ # two valid windows: [1,2] at positions 0-1 and [4,5] at positions 3-4
450
+ # scans right-to-left, finds [4,5] first
451
+ assert (
452
+ sched._sliding_window_lookup(to_keys([1, 2, 3, 4, 5]), 2, _EMPTY_REQ_CTX)
453
+ == 5
454
+ )
455
+
456
+ def test_prefix_fallback_with_gap(self):
457
+ sched = _make_scheduler_with_lookup({2: True, 3: True, 4: True, 5: True})
458
+ # window of 4 not found contiguously (gap at 1)
459
+ assert (
460
+ sched._sliding_window_lookup(to_keys([2, 1, 3, 4, 5]), 4, _EMPTY_REQ_CTX)
461
+ == 1
462
+ )
463
+
464
+ def test_empty(self):
465
+ sched = _make_scheduler_with_lookup({})
466
+ assert sched._sliding_window_lookup([], 1, _EMPTY_REQ_CTX) == 0
467
+
468
+ def test_none_defers(self):
469
+ sched = _make_scheduler_with_lookup({1: True, 2: None})
470
+ assert sched._sliding_window_lookup(to_keys([1, 2]), 2, _EMPTY_REQ_CTX) is None
471
+
472
+ def test_none_with_full_window_still_defers(self):
473
+ """Even if a real window is found after a None, result is deferred."""
474
+ # Scan right-to-left: 4(True), 3(None) resets, 2(True), 1(True) = window
475
+ # but block 3 was None so defer_lookup is set
476
+ sched = _make_scheduler_with_lookup({1: True, 2: True, 3: None, 4: True})
477
+ assert (
478
+ sched._sliding_window_lookup(to_keys([1, 2, 3, 4]), 2, _EMPTY_REQ_CTX)
479
+ is None
480
+ )
tests/v1/kv_connector/unit/offloading_connector/utils.py
+12
-7
@@ -56,8 +56,12 @@ from vllm.v1.request import Request
56
from vllm.v1.structured_output import StructuredOutputManager
57
58
59
-def to_keys(int_ids: list[int]) -> list[OffloadKey]:
60
- return [make_offload_key(str(i).encode(), 0) for i in int_ids]
59
+def to_key(int_hash: int) -> OffloadKey:
60
+ return make_offload_key(str(int_hash).encode(), 0)
61
+
62
+
63
+def to_keys(int_hashes: list[int]) -> list[OffloadKey]:
64
+ return [to_key(i) for i in int_hashes]
65
66
67
class MockLoadStoreSpec(LoadStoreSpec):
@@ -116,6 +120,7 @@ class MockOffloadingSpec(OffloadingSpec):
120
self.manager = MagicMock(spec=OffloadingManager)
121
self.manager.lookup.return_value = 0
122
self.manager.prepare_load = lambda keys, req_context: MockLoadStoreSpec(keys)
123
+ self.manager.lookup.return_value = False
124
self.handler = MockOffloadingHandler()
125
126
def get_manager(self) -> OffloadingManager:
@@ -228,14 +233,14 @@ class RequestRunner:
233
self.scheduler_connector: OffloadingConnector = scheduler_connector
234
235
# extract mocked OffloadingManager of scheduler connector
231
- connector_scheduler = scheduler_connector.connector_scheduler
232
- assert connector_scheduler is not None
233
- manager = connector_scheduler.manager
236
+ self.connector_scheduler = scheduler_connector.connector_scheduler
237
+ assert self.connector_scheduler is not None
238
+ manager = self.connector_scheduler.manager
239
assert isinstance(manager, MagicMock)
240
self.manager: MagicMock = manager
241
237
- assert len(connector_scheduler.config.kv_group_configs) == 1
238
- kv_group_config = connector_scheduler.config.kv_group_configs[0]
242
+ assert len(self.connector_scheduler.config.kv_group_configs) == 1
243
+ kv_group_config = self.connector_scheduler.config.kv_group_configs[0]
244
assert kv_group_config.gpu_block_size == gpu_block_size
245
assert kv_group_config.offloaded_block_size == offloaded_block_size
246
tests/v1/kv_offload/test_cpu_manager.py
+36
-20
@@ -35,8 +35,12 @@ class ExpectedPrepareStoreOutput:
35
evicted_keys: list[int]
36
37
38
-def to_keys(int_ids: list[int]) -> list[OffloadKey]:
39
- return [make_offload_key(str(i).encode(), 0) for i in int_ids]
38
+def to_key(int_hash: int) -> OffloadKey:
39
+ return make_offload_key(str(int_hash).encode(), 0)
40
+
41
+
42
+def to_keys(int_hashes: list[int]) -> list[OffloadKey]:
43
+ return [to_key(i) for i in int_hashes]
44
45
46
def verify_store_output(
@@ -136,7 +140,7 @@ def test_already_stored_block_not_evicted_during_prepare_store(eviction_policy):
140
manager.complete_store(to_keys([2, 3, 4, 5]))
141
142
# block 2 must still be present in the cache
139
- assert manager.lookup(to_keys([2]), _EMPTY_REQ_CTX) == 1
143
+ assert manager.lookup(to_key(2), _EMPTY_REQ_CTX) is True
144
145
146
def test_cpu_manager():
@@ -160,7 +164,8 @@ def test_cpu_manager():
164
)
165
166
# lookup [1, 2] -> not ready
163
- assert cpu_manager.lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
167
+ assert cpu_manager.lookup(to_key(1), _EMPTY_REQ_CTX) is False
168
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is False
169
170
# no events so far
171
assert list(cpu_manager.take_events()) == []
@@ -170,9 +175,9 @@ def test_cpu_manager():
175
verify_events(cpu_manager.take_events(), expected_stores=({1, 2},))
176
177
# lookup [1, 2]
173
- assert cpu_manager.lookup(to_keys([1]), _EMPTY_REQ_CTX) == 1
174
- assert cpu_manager.lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 2
175
- assert cpu_manager.lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) == 2
178
+ assert cpu_manager.lookup(to_key(1), _EMPTY_REQ_CTX) is True
179
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is True
180
+ assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is False
181
182
# prepare store [2, 3, 4, 5] -> evicts [1]
183
prepare_store_output = cpu_manager.prepare_store(
@@ -196,6 +201,14 @@ def test_cpu_manager():
201
# complete store [2, 3, 4, 5]
202
cpu_manager.complete_store(to_keys([2, 3, 4, 5]))
203
204
+ # lookup (now that we have [2, 3, 4, 5])
205
+ assert cpu_manager.lookup(to_key(1), _EMPTY_REQ_CTX) is False
206
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is True
207
+ assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is True
208
+ assert cpu_manager.lookup(to_key(4), _EMPTY_REQ_CTX) is True
209
+ assert cpu_manager.lookup(to_key(5), _EMPTY_REQ_CTX) is True
210
+ assert cpu_manager.lookup(to_key(0), _EMPTY_REQ_CTX) is False
211
+
212
# prepare load [2, 3]
213
prepare_load_output = cpu_manager.prepare_load(to_keys([2, 3]), _EMPTY_REQ_CTX)
214
verify_load_output(prepare_load_output, [1, 2])
@@ -238,8 +251,8 @@ def test_cpu_manager():
251
cpu_manager.complete_store(to_keys([7, 9]), success=False)
252
253
# assert [7] is still stored, but [9] is not
241
- assert cpu_manager.lookup(to_keys([7]), _EMPTY_REQ_CTX) == 1
242
- assert cpu_manager.lookup(to_keys([9]), _EMPTY_REQ_CTX) == 0
254
+ assert cpu_manager.lookup(to_key(7), _EMPTY_REQ_CTX) is True
255
+ assert cpu_manager.lookup(to_key(9), _EMPTY_REQ_CTX) is False
256
257
verify_events(
258
cpu_manager.take_events(),
@@ -284,7 +297,8 @@ class TestARCPolicy:
297
)
298
299
# lookup [1, 2] -> not ready
287
- assert cpu_manager.lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
300
+ assert cpu_manager.lookup(to_key(1), _EMPTY_REQ_CTX) is False
301
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is False
302
303
# no events so far
304
assert list(cpu_manager.take_events()) == []
@@ -294,9 +308,9 @@ class TestARCPolicy:
308
verify_events(cpu_manager.take_events(), expected_stores=({1, 2},))
309
310
# lookup [1, 2]
297
- assert cpu_manager.lookup(to_keys([1]), _EMPTY_REQ_CTX) == 1
298
- assert cpu_manager.lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 2
299
- assert cpu_manager.lookup(to_keys([1, 2, 3]), _EMPTY_REQ_CTX) == 2
311
+ assert cpu_manager.lookup(to_key(1), _EMPTY_REQ_CTX) is True
312
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is True
313
+ assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is False
314
315
# blocks should be in T1 (recent)
316
assert len(arc_policy.t1) == 2
@@ -500,7 +514,7 @@ class TestARCPolicy:
514
cpu_manager.complete_store(to_keys([5]), success=False)
515
516
# block 5 should not be in cache
503
- assert cpu_manager.lookup(to_keys([5]), _EMPTY_REQ_CTX) == 0
517
+ assert cpu_manager.lookup(to_key(5), _EMPTY_REQ_CTX) is False
518
# block 5 should not be in T1 or T2
519
assert to_keys([5])[0] not in arc_policy.t1
520
assert to_keys([5])[0] not in arc_policy.t2
@@ -541,8 +555,8 @@ class TestARCPolicy:
555
cpu_manager.complete_store(to_keys([6]))
556
557
# verify blocks 2, 3 (in T2) are still present
544
- assert cpu_manager.lookup(to_keys([2]), _EMPTY_REQ_CTX) == 1
545
- assert cpu_manager.lookup(to_keys([3]), _EMPTY_REQ_CTX) == 1
558
+ assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is True
559
+ assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is True
560
561
# verify events
562
events = list(cpu_manager.take_events())
@@ -562,7 +576,8 @@ def test_filter_reused_manager():
576
)
577
578
# Lookup [1, 2] -> 1st time, added to tracker but not eligible for store yet
565
- assert manager.lookup(to_keys([1, 2]), _EMPTY_REQ_CTX) == 0
579
+ assert manager.lookup(to_key(1), _EMPTY_REQ_CTX) is False
580
+ assert manager.lookup(to_key(2), _EMPTY_REQ_CTX) is False
581
582
# prepare store [1, 2] -> should be filtered
583
prepare_store_output = manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -570,7 +585,7 @@ def test_filter_reused_manager():
585
assert prepare_store_output.keys_to_store == []
586
587
# Lookup [1] -> 2nd time, eligible now
573
- assert manager.lookup(to_keys([1]), _EMPTY_REQ_CTX) == 0
588
+ assert manager.lookup(to_key(1), _EMPTY_REQ_CTX) is False
589
590
# prepare store [1, 2] -> [1] should be eligible, [2] should be filtered
591
prepare_store_output = manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -579,12 +594,13 @@ def test_filter_reused_manager():
594
595
# Lookup [3, 4] -> 1st time
596
# (evicts [2] from tracker since max_size is 3 and tracker has [1])
582
- assert manager.lookup(to_keys([3, 4]), _EMPTY_REQ_CTX) == 0
597
+ assert manager.lookup(to_key(3), _EMPTY_REQ_CTX) is False
598
+ assert manager.lookup(to_key(4), _EMPTY_REQ_CTX) is False
599
# Verify [2] was evicted from the tracker (tracker now has: [1], [3], [4])
600
assert to_keys([2])[0] not in manager.counts
601
602
# Lookup [2] again -> (this adds [2] back to the tracker as 1st time)
587
- assert manager.lookup(to_keys([2]), _EMPTY_REQ_CTX) == 0
603
+ assert manager.lookup(to_key(2), _EMPTY_REQ_CTX) is False
604
# Verify [2] was re-added with count=1 (not eligible yet)
605
assert manager.counts.get(to_keys([2])[0]) == 1
606
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+48
-4
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from collections import defaultdict
4
-from collections.abc import Iterable
4
+from collections.abc import Iterable, Sequence
5
from dataclasses import dataclass, field
6
from itertools import islice
7
from typing import Any, NamedTuple
@@ -132,6 +132,49 @@ class OffloadingConnectorScheduler:
132
self._reqs_being_stored = defaultdict[ReqId, set[OffloadKey]](set)
133
self._reqs_being_loaded = defaultdict[ReqId, set[OffloadKey]](set)
134
135
+ def _maximal_prefix_lookup(
136
+ self, keys: Iterable[OffloadKey], req_context: ReqContext
137
+ ) -> int | None:
138
+ """Find the length of the maximal prefix of offloaded blocks."""
139
+ hit_count = 0
140
+ defer_lookup = False
141
+ for key in keys:
142
+ result = self.manager.lookup(key, req_context)
143
+ if result is None:
144
+ defer_lookup = True
145
+ # continue lookup to allow manager to kick-off async lookups
146
+ # for all blocks (until a miss is detected)
147
+ result = True
148
+ if not result:
149
+ break
150
+ hit_count += 1
151
+ return hit_count if not defer_lookup else None
152
+
153
+ def _sliding_window_lookup(
154
+ self,
155
+ keys: Sequence[OffloadKey],
156
+ sliding_window_size: int,
157
+ req_context: ReqContext,
158
+ ) -> int | None:
159
+ """Find the maximal ending position of consecutive offloaded blocks
160
+ within a sliding window."""
161
+ defer_lookup = False
162
+ consecutive_hits = 0
163
+ for idx in range(len(keys) - 1, -1, -1):
164
+ result = self.manager.lookup(keys[idx], req_context)
165
+ if result is None:
166
+ defer_lookup = True
167
+ # continue lookup to allow manager to kick-off async lookups
168
+ # for all blocks (until a hit is detected)
169
+ result = False
170
+ if not result:
171
+ consecutive_hits = 0
172
+ else:
173
+ consecutive_hits += 1
174
+ if consecutive_hits == sliding_window_size:
175
+ return idx + sliding_window_size if not defer_lookup else None
176
+ return consecutive_hits if not defer_lookup else None
177
+
178
def get_num_new_matched_tokens(
179
self, request: Request, num_computed_tokens: int
180
) -> tuple[int | None, bool]:
@@ -184,9 +227,10 @@ class OffloadingConnectorScheduler:
227
return 0, False
228
229
start_block_idx = num_computed_tokens // group_config.offloaded_block_size
187
- hits = self.manager.lookup(
188
- offload_keys[start_block_idx:],
189
- req_status.req_context,
230
+ # Full attention relays on all previous KV cache blocks.
231
+ # Thus, we search for a maximal prefix of KV cache which are all cached.
232
+ hits = self._maximal_prefix_lookup(
233
+ offload_keys[start_block_idx:], req_status.req_context
234
)
235
if hits is None:
236
# indicates a lookup that should be tried later
vllm/v1/kv_offload/abstract.py
+7
-13
@@ -7,8 +7,7 @@ This class runs in the scheduler, tracks which blocks are offloaded
7
and their address.
8
9
The class provides the following primitives:
10
- lookup() - find the length of the maximal series of blocks,
11
- starting from the first one, that are all offloaded.
10
+ lookup() - check whether a single block is offloaded and ready.
11
prepare_load() - prepare given blocks to be read.
12
The given blocks will be protected from eviction.
13
This function returns a LoadSpec which encapsulates
@@ -91,23 +90,18 @@ class OffloadingEvent:
90
91
class OffloadingManager(ABC):
92
@abstractmethod
94
- def lookup(
95
- self,
96
- keys: Iterable[OffloadKey],
97
- req_context: ReqContext,
98
- ) -> int | None:
93
+ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None:
94
"""
100
- Finds the length of the maximal series of blocks, starting from the
101
- first one, that are all offloaded.
95
+ Checks whether a single block is offloaded and ready to be read.
96
97
Args:
104
- keys: the keys identifying the blocks to lookup.
98
+ key: the key identifying the block to lookup.
99
req_context: per-request context (e.g. kv_transfer_params).
100
101
Returns:
108
- An integer representing the maximal number of blocks that
109
- are currently offloaded, or None if the lookup should be retried
110
- later. Returning None will delay the request handling by the vLLM
102
+ True if the block is offloaded and ready, False if not,
103
+ or None if the lookup should be retried later.
104
+ Returning None will delay the request handling by the vLLM
105
scheduler.
106
"""
107
pass
vllm/v1/kv_offload/cpu/manager.py
+3
-12
@@ -84,18 +84,9 @@ class CPUOffloadingManager(OffloadingManager):
84
85
# --- OffloadingManager interface ---
86
87
- def lookup(
88
- self,
89
- keys: Iterable[OffloadKey],
90
- req_context: ReqContext,
91
- ) -> int | None:
92
- hit_count = 0
93
- for key in keys:
94
- block = self._policy.get(key)
95
- if block is None or not block.is_ready:
96
- break
97
- hit_count += 1
98
- return hit_count
87
+ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None:
88
+ block = self._policy.get(key)
89
+ return block is not None and block.is_ready
90
91
def prepare_load(
92
self,
vllm/v1/kv_offload/reuse_manager.py
+12
-13
@@ -27,8 +27,9 @@ class FilterReusedOffloadingManager(OffloadingManager):
27
All methods are delegated to the *backing* manager. Two methods are
28
intercepted:
29
30
- * ``lookup`` — records each visited key in an internal LRU counter.
30
* ``prepare_store`` — filters out keys that have not yet
31
+ * ``lookup`` — records the visited key in an internal LRU
32
+ counter, then delegates to the backing manager.
33
crossed the threshold *before* calling the backing
34
``prepare_store``.
35
@@ -66,18 +67,16 @@ class FilterReusedOffloadingManager(OffloadingManager):
67
# Intercepted methods
68
# ------------------------------------------------------------------
69
69
- def lookup(self, keys: Iterable[OffloadKey], req_context: ReqContext) -> int | None:
70
- """Record each key, then delegate lookup to backing manager."""
71
- keys = list(keys)
72
- for key in keys:
73
- if key in self.counts:
74
- self.counts.move_to_end(key)
75
- self.counts[key] += 1
76
- else:
77
- if len(self.counts) >= self.max_tracker_size:
78
- self.counts.popitem(last=False) # evict LRU
79
- self.counts[key] = 1
80
- return self._backing.lookup(keys, req_context)
70
+ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None:
71
+ """Record the key, then delegate lookup to backing manager."""
72
+ if key in self.counts:
73
+ self.counts.move_to_end(key)
74
+ self.counts[key] += 1
75
+ else:
76
+ if len(self.counts) >= self.max_tracker_size:
77
+ self.counts.popitem(last=False) # evict LRU
78
+ self.counts[key] = 1
79
+ return self._backing.lookup(key, req_context)
80
81
def prepare_store(
82
self, keys: Iterable[OffloadKey], req_context: ReqContext