main
py 709 lines 26.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Callable, Iterable
4 from dataclasses import dataclass
5 from typing import Any
6 from unittest.mock import MagicMock
7
8 import pytest
9 import torch
10
11 from tests.v1.kv_connector.unit.utils import (
12 EOS_TOKEN_ID,
13 create_model_runner_output,
14 create_vllm_config,
15 )
16 from vllm import SamplingParams
17 from vllm.config import (
18 KVEventsConfig,
19 KVTransferConfig,
20 set_current_vllm_config,
21 )
22 from vllm.distributed.kv_transfer.kv_connector.v1 import KVConnectorRole
23 from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import (
24 OffloadingConnectorMetadata,
25 OffloadingWorkerMetadata,
26 )
27 from vllm.distributed.kv_transfer.kv_connector.v1.offloading_connector import (
28 OffloadingConnector,
29 )
30 from vllm.forward_context import ForwardContext
31 from vllm.utils.hashing import sha256
32 from vllm.v1.core.kv_cache_utils import (
33 get_request_block_hasher,
34 init_none_hash,
35 resolve_kv_cache_block_sizes,
36 )
37 from vllm.v1.core.sched.async_scheduler import AsyncScheduler
38 from vllm.v1.core.sched.scheduler import Scheduler
39 from vllm.v1.kv_cache_interface import (
40 FullAttentionSpec,
41 KVCacheConfig,
42 KVCacheGroupSpec,
43 KVCacheTensor,
44 )
45 from vllm.v1.kv_offload.base import (
46 CanonicalKVCaches,
47 GPULoadStoreSpec,
48 LoadStoreSpec,
49 LookupResult,
50 OffloadingManager,
51 OffloadingSpec,
52 OffloadingWorker,
53 OffloadKey,
54 PrepareStoreOutput,
55 RequestOffloadingContext,
56 TransferResult,
57 make_offload_key,
58 )
59 from vllm.v1.kv_offload.config import OffloadingConfig
60 from vllm.v1.request import Request
61 from vllm.v1.structured_output import StructuredOutputManager
62
63
64 def to_key(int_hash: int) -> OffloadKey:
65 return make_offload_key(str(int_hash).encode(), 0)
66
67
68 def to_keys(int_hashes: Iterable[int]) -> list[OffloadKey]:
69 return [to_key(i) for i in int_hashes]
70
71
72 class MockLoadStoreSpec(LoadStoreSpec):
73 def __init__(self, offload_keys: Iterable[OffloadKey]):
74 self.offload_keys: list[OffloadKey] = list(offload_keys)
75
76 def __repr__(self) -> str:
77 return repr(self.offload_keys)
78
79
80 class MockOffloadingWorker(OffloadingWorker):
81 def __init__(self):
82 self.transfer_specs: dict[int, tuple[LoadStoreSpec, LoadStoreSpec]] = {}
83 self.completed_transfers: list[TransferResult] = []
84 self.waiting_jobs: set[int] = set()
85 self.completed_jobs: list[int] = []
86 self.flushed_jobs: set[int] = set()
87
88 def get_finished(self) -> list[TransferResult]:
89 finished = self.completed_transfers
90 self.completed_transfers = []
91 return finished
92
93 def submit_store(
94 self, job_id: int, src_spec: LoadStoreSpec, dst_spec: LoadStoreSpec
95 ) -> bool: # type: ignore[override]
96 self.transfer_specs[job_id] = (src_spec, dst_spec)
97 self.waiting_jobs.add(job_id)
98 return True
99
100 def submit_load(
101 self, job_id: int, src_spec: LoadStoreSpec, dst_spec: LoadStoreSpec
102 ) -> bool: # type: ignore[override]
103 self.transfer_specs[job_id] = (src_spec, dst_spec)
104 self.waiting_jobs.add(job_id)
105 return True
106
107 def complete_jobs(self, job_ids: set[int]) -> None:
108 for job_id in job_ids:
109 if job_id in self.waiting_jobs:
110 self.waiting_jobs.remove(job_id)
111 self.completed_jobs.append(job_id)
112 result = TransferResult(
113 job_id=job_id,
114 success=True,
115 transfer_size=None,
116 transfer_time=None,
117 )
118 self.completed_transfers.append(result)
119
120 def wait(self, job_ids: set[int]) -> None:
121 self.flushed_jobs |= job_ids
122 self.complete_jobs(job_ids)
123
124
125 class MockOffloadingSpec(OffloadingSpec):
126 def __init__(self, config: OffloadingConfig):
127 super().__init__(config)
128
129 self.manager = MagicMock(spec=OffloadingManager)
130 self.manager.prepare_load = lambda keys, req_context: MockLoadStoreSpec(keys)
131 self.manager.lookup.return_value = LookupResult.MISS
132 self.manager.get_stats.return_value = None
133 self.manager.on_new_request.return_value = RequestOffloadingContext()
134 self.handler = MockOffloadingWorker()
135
136 def get_manager(self) -> OffloadingManager:
137 return self.manager
138
139 def get_worker(self, _: CanonicalKVCaches) -> OffloadingWorker:
140 return self.handler
141
142 def complete_transfers(self):
143 self.handler.complete_jobs(self.handler.waiting_jobs.copy())
144
145 def get_completed_transfers(self) -> list[tuple[LoadStoreSpec, LoadStoreSpec]]:
146 specs = [
147 self.handler.transfer_specs[job_id]
148 for job_id in self.handler.completed_jobs
149 ]
150 self.handler.completed_jobs.clear()
151 return specs
152
153 def get_flushed_transfers(self) -> list[tuple[LoadStoreSpec, LoadStoreSpec]]:
154 specs = [
155 self.handler.transfer_specs[job_id] for job_id in self.handler.flushed_jobs
156 ]
157 self.handler.flushed_jobs.clear()
158 return specs
159
160
161 @dataclass(frozen=True)
162 class GPUBlock:
163 group_idx: int
164 request_block_offset: int
165
166
167 @dataclass
168 class TransferSummary:
169 gpu_blocks: list[GPUBlock]
170 offload_addresses: list[Any]
171
172
173 class RequestRunner:
174 def __init__(
175 self,
176 block_size: int,
177 num_gpu_blocks: int,
178 blocks_per_chunk: int = 1,
179 async_scheduling: bool = True,
180 kv_cache_groups: list[KVCacheGroupSpec] | None = None,
181 extra_config_overrides: dict[str, Any] | None = None,
182 worker_count: int = 1,
183 retention_interval: int | None = None,
184 speculative_config: Any | None = None,
185 ):
186 assert blocks_per_chunk == 1 or kv_cache_groups is None, (
187 "blocks_per_chunk > 1 requires all groups to have the same "
188 "block size, so kv_cache_groups must be None (use default group)"
189 )
190
191 self.blocks_per_chunk: int = blocks_per_chunk
192 self.block_size: int = block_size
193 self.num_gpu_blocks: int = num_gpu_blocks
194 self.async_scheduling: bool = async_scheduling
195
196 self.req_id: int = -1
197
198 vllm_config = create_vllm_config(
199 block_size=block_size,
200 max_num_batched_tokens=1000,
201 disable_hybrid_kv_cache_manager=False,
202 )
203 vllm_config.scheduler_config.async_scheduling = async_scheduling
204 vllm_config.parallel_config.world_size = worker_count
205 vllm_config.cache_config.prefix_cache_retention_interval = retention_interval
206 if speculative_config is not None:
207 vllm_config.speculative_config = speculative_config
208
209 extra_config: dict[str, Any] = {
210 "spec_name": "MockOffloadingSpec",
211 "spec_module_path": "tests.v1.kv_connector.unit.offloading_connector.utils", # noqa: E501
212 # Preserve legacy behavior for tests; new opt-in tests override.
213 "offload_prompt_only": False,
214 # Exercise the self-describing KV events path by default;
215 # opt-out tests override this to cover the legacy placeholders.
216 "self_describing_kv_events": True,
217 }
218 if blocks_per_chunk > 1:
219 extra_config["block_size"] = block_size * blocks_per_chunk
220 if extra_config_overrides:
221 extra_config.update(extra_config_overrides)
222
223 vllm_config.kv_transfer_config = KVTransferConfig(
224 kv_connector="OffloadingConnector",
225 kv_role="kv_both",
226 kv_connector_extra_config=extra_config,
227 )
228 vllm_config.kv_events_config = KVEventsConfig(
229 # Enable so the offloading events tracker is active, but use the
230 # null publisher: these tests drain take_events directly and a
231 # real ZMQ publisher would bind a port per test.
232 enable_kv_cache_events=True,
233 publisher="null",
234 )
235
236 if kv_cache_groups is None:
237 kv_cache_groups = [
238 KVCacheGroupSpec(
239 ["layer"],
240 FullAttentionSpec(
241 block_size=block_size,
242 num_kv_heads=1,
243 head_size=1,
244 dtype=torch.float32,
245 ),
246 )
247 ]
248
249 # Groups overlay one allocation, sized by the largest group.
250 block_stride = max(
251 group.kv_cache_spec.page_size_bytes * len(group.layer_names)
252 for group in kv_cache_groups
253 )
254 kv_cache_tensors = [
255 KVCacheTensor(
256 size=block_stride * num_gpu_blocks,
257 layers=list(group.layer_names),
258 layer_stride=group.kv_cache_spec.page_size_bytes * num_gpu_blocks,
259 block_stride=group.kv_cache_spec.page_size_bytes,
260 )
261 for group in kv_cache_groups
262 ]
263
264 kv_cache_config = KVCacheConfig(
265 num_blocks=num_gpu_blocks,
266 kv_cache_tensors=kv_cache_tensors,
267 kv_cache_groups=kv_cache_groups,
268 )
269 vllm_config.cache_config.num_gpu_blocks = num_gpu_blocks
270 self.num_kv_groups = len(kv_cache_config.kv_cache_groups)
271
272 scheduler_block_size, hash_block_size = resolve_kv_cache_block_sizes(
273 kv_cache_config, vllm_config
274 )
275
276 scheduler_cls = AsyncScheduler if async_scheduling else Scheduler
277 self.scheduler = scheduler_cls(
278 vllm_config=vllm_config,
279 kv_cache_config=kv_cache_config,
280 log_stats=True,
281 structured_output_manager=StructuredOutputManager(vllm_config),
282 block_size=scheduler_block_size,
283 hash_block_size=hash_block_size,
284 )
285
286 self.worker_connector = OffloadingConnector(
287 vllm_config, KVConnectorRole.WORKER, kv_cache_config
288 )
289
290 # register worker kv_caches to enable OffloadingWorker creations
291 kv_caches: dict[str, torch.Tensor] = {}
292 for group in kv_cache_groups:
293 spec = group.kv_cache_spec
294 for layer_name in group.layer_names:
295 # Shape follows FlashAttention layout:
296 # Shape: (num_blocks, 2, block_size, num_kv_heads, head_size)
297 kv_caches[layer_name] = torch.empty(
298 num_gpu_blocks,
299 2,
300 spec.block_size,
301 spec.num_kv_heads,
302 spec.head_size,
303 dtype=spec.dtype,
304 )
305
306 with set_current_vllm_config(vllm_config):
307 self.worker_connector.register_kv_caches(kv_caches)
308
309 # extract connector of scheduler
310 scheduler_connector = self.scheduler.connector
311 assert scheduler_connector is not None
312 assert isinstance(scheduler_connector, OffloadingConnector)
313 self.scheduler_connector: OffloadingConnector = scheduler_connector
314
315 # extract mocked OffloadingManager of scheduler connector
316 self.connector_scheduler = scheduler_connector.connector_scheduler
317 assert self.connector_scheduler is not None
318 manager = self.connector_scheduler.manager
319 assert isinstance(manager, MagicMock)
320 self.manager: MagicMock = manager
321
322 num_kv_groups = len(kv_cache_config.kv_cache_groups)
323 assert len(self.connector_scheduler.config.kv_group_configs) == num_kv_groups
324 for group_config, kv_cache_group in zip(
325 self.connector_scheduler.config.kv_group_configs,
326 kv_cache_config.kv_cache_groups,
327 ):
328 tokens_per_block = kv_cache_group.kv_cache_spec.block_size
329 assert group_config.tokens_per_block == tokens_per_block
330 assert group_config.tokens_per_chunk == tokens_per_block * blocks_per_chunk
331
332 # extract OffloadingSpec of worker_connector
333 connector_worker = self.worker_connector.connector_worker
334 assert connector_worker is not None
335 offloading_spec = connector_worker.spec
336 assert isinstance(offloading_spec, MockOffloadingSpec)
337 self.offloading_spec: MockOffloadingSpec = offloading_spec
338
339 # mapping (offloading address) -> GPUBlock
340 self.offloaded: dict[Any, GPUBlock] = {}
341
342 self.completed_loads: list[TransferSummary] = []
343 self.completed_stores: list[TransferSummary] = []
344 self.flushed_gpu_blocks: set[GPUBlock] = set()
345 self.kv_connector_stats: list[Any] = []
346
347 # block_id -> GPUBlock
348 self.gpu_blocks: dict[int, GPUBlock] = {}
349
350 init_none_hash(sha256)
351 self._block_hasher = get_request_block_hasher(block_size, sha256)
352
353 self._dummy_ctx: ForwardContext = ForwardContext(
354 no_compile_layers={},
355 attn_metadata={},
356 slot_mapping={},
357 )
358
359 def _record_kv_connector_stats(self, engine_outputs: dict[int, Any]) -> None:
360 for output in engine_outputs.values():
361 scheduler_stats = output.scheduler_stats
362 if scheduler_stats is not None and scheduler_stats.kv_connector_stats:
363 self.kv_connector_stats.append(scheduler_stats.kv_connector_stats)
364
365 def new_request(
366 self,
367 token_ids: list[int],
368 kv_transfer_params: dict | None = None,
369 skip_reading_prefix_cache: bool = False,
370 ):
371 self.req_id += 1
372
373 sampling_params = SamplingParams(
374 max_tokens=1000,
375 skip_reading_prefix_cache=skip_reading_prefix_cache or None,
376 )
377 sampling_params.update_from_generation_config({}, EOS_TOKEN_ID)
378
379 req = Request(
380 request_id=str(self.req_id),
381 prompt_token_ids=token_ids,
382 sampling_params=sampling_params,
383 pooling_params=None,
384 block_hasher=self._block_hasher,
385 )
386 if kv_transfer_params is not None:
387 req.kv_transfer_params = kv_transfer_params
388
389 self.scheduler.add_request(req)
390
391 def _parse_transfers(self):
392 for src_spec, dst_spec in self.offloading_spec.get_flushed_transfers():
393 if isinstance(src_spec, GPULoadStoreSpec):
394 # store flush
395 for block_id in src_spec.block_ids:
396 self.flushed_gpu_blocks.add(self.gpu_blocks[block_id.item()])
397 else:
398 # load flush
399 for block_id in dst_spec.block_ids:
400 self.flushed_gpu_blocks.add(self.gpu_blocks[block_id.item()])
401
402 blocks_per_chunk = self.blocks_per_chunk
403
404 for src_spec, dst_spec in self.offloading_spec.get_completed_transfers():
405 if isinstance(src_spec, GPULoadStoreSpec):
406 store = True
407 gpu_spec = src_spec
408 offload_spec = dst_spec
409 else:
410 store = False
411 gpu_spec = dst_spec
412 offload_spec = src_spec
413
414 assert isinstance(offload_spec, MockLoadStoreSpec)
415 assert isinstance(gpu_spec, GPULoadStoreSpec)
416 assert len(gpu_spec.group_sizes) == self.num_kv_groups
417
418 gpu_blocks: list[GPUBlock] = []
419 for block_id in gpu_spec.block_ids:
420 gpu_blocks.append(self.gpu_blocks[block_id.item()])
421
422 # list of (offload_key, sub_block_offset)
423 offload_addresses: list[Any] = []
424 for offload_key in offload_spec.offload_keys:
425 for sub_block_idx in range(blocks_per_chunk):
426 offload_addresses.append((offload_key, sub_block_idx))
427
428 assert gpu_spec.block_indices is not None
429 assert len(gpu_spec.block_indices) == self.num_kv_groups
430
431 gpu_block_offset = 0
432 offload_address_offset = 0
433 for group_size, logical_offset in zip(
434 gpu_spec.group_sizes, gpu_spec.block_indices
435 ):
436 gpu_block_end_offset = gpu_block_offset + group_size
437 assert gpu_block_end_offset <= len(gpu_blocks)
438
439 offload_addresses_to_skip = logical_offset % blocks_per_chunk
440 offload_addresses_end_offset = (
441 offload_address_offset + offload_addresses_to_skip + group_size
442 )
443 assert offload_addresses_end_offset <= len(offload_addresses)
444
445 offload_addresses = (
446 offload_addresses[:offload_address_offset]
447 + offload_addresses[
448 offload_address_offset + offload_addresses_to_skip :
449 ]
450 )
451
452 gpu_block_offset += group_size
453 offload_address_offset += group_size
454
455 assert gpu_block_offset == len(gpu_blocks)
456 assert offload_address_offset == len(offload_addresses)
457
458 transfer_summary = TransferSummary(gpu_blocks, offload_addresses)
459 if store:
460 self.completed_stores.append(transfer_summary)
461 else:
462 self.completed_loads.append(transfer_summary)
463
464 def _update_gpu_blocks(self):
465 for group_idx, manager in enumerate(
466 self.scheduler.kv_cache_manager.coordinator.single_type_managers
467 ):
468 for blocks in manager.req_to_blocks.values():
469 for block_idx, block in enumerate(blocks):
470 self.gpu_blocks[block.block_id] = GPUBlock(group_idx, block_idx)
471
472 def _run(
473 self,
474 decoded_tokens: list[int],
475 complete_transfers: bool,
476 post_step_fn: Callable[[], None] | None = None,
477 ):
478 """Runs multiple engine (scheduler + worker) steps.
479 Assumes a single request is running.
480
481 Args:
482 decoded_tokens: the tokens to yield at each step.
483 complete_transfers: complete transfers immediately
484 post_step_fn: optional callback invoked after each step's
485 update_from_output(), before the next schedule().
486
487 """
488 tokens_iter = iter(decoded_tokens)
489 token_id = next(tokens_iter, None)
490 prev_scheduler_output = None
491 prev_model_runner_output = None
492 while True:
493 # Strict-always-False frees the request immediately on EOS, but
494 # the worker may still have a deferred store queued. In production
495 # the next request's step drains it; in single-request tests we
496 # must keep stepping until the scheduler sees no in-flight jobs
497 # and no pending finished_req_ids awaiting build_connector_meta.
498 if (
499 not self.scheduler.requests
500 and not self.connector_scheduler._jobs
501 and not self.scheduler.finished_req_ids
502 ):
503 break
504
505 scheduler_output = self.scheduler.schedule()
506 self._update_gpu_blocks()
507
508 kv_connector_metadata = scheduler_output.kv_connector_metadata
509 assert kv_connector_metadata is not None
510 assert isinstance(kv_connector_metadata, OffloadingConnectorMetadata)
511
512 self.worker_connector.handle_preemptions(kv_connector_metadata)
513
514 self.worker_connector.bind_connector_metadata(kv_connector_metadata)
515 self.worker_connector.start_load_kv(self._dummy_ctx)
516
517 if complete_transfers:
518 self.offloading_spec.complete_transfers()
519
520 self.worker_connector.wait_for_save()
521 finished_sending, finished_recving = self.worker_connector.get_finished(
522 scheduler_output.finished_req_ids
523 )
524 worker_meta = (
525 self.worker_connector.build_connector_worker_meta()
526 or OffloadingWorkerMetadata()
527 )
528
529 self.worker_connector.clear_connector_metadata()
530
531 model_runner_output = create_model_runner_output(
532 reqs=self.scheduler.running,
533 finished_sending=finished_sending,
534 finished_recving=finished_recving,
535 token_id=token_id or 0,
536 kv_connector_worker_meta=worker_meta,
537 )
538
539 prev_token_id = token_id
540 if self.scheduler.running:
541 token_id = next(tokens_iter, None)
542
543 if self.async_scheduling:
544 # in async scheduling we update the output of the previous step
545 if prev_model_runner_output is not None:
546 engine_outputs = self.scheduler.update_from_output(
547 prev_scheduler_output, prev_model_runner_output
548 )
549 self._record_kv_connector_stats(engine_outputs)
550 prev_scheduler_output = scheduler_output
551 prev_model_runner_output = model_runner_output
552 else:
553 engine_outputs = self.scheduler.update_from_output(
554 scheduler_output, model_runner_output
555 )
556 self._record_kv_connector_stats(engine_outputs)
557
558 if post_step_fn is not None:
559 post_step_fn()
560
561 if (
562 prev_token_id == EOS_TOKEN_ID
563 and prev_token_id != token_id
564 and (
565 self.scheduler.requests
566 or self.connector_scheduler._jobs
567 or self.scheduler.finished_req_ids
568 )
569 ):
570 # continue for one more step to allow offloading to kick off
571 continue
572
573 if token_id is None:
574 if self.async_scheduling:
575 # Flush the previous step's output.
576 engine_outputs = self.scheduler.update_from_output(
577 prev_scheduler_output, prev_model_runner_output
578 )
579 self._record_kv_connector_stats(engine_outputs)
580 prev_model_runner_output = None
581 if self.scheduler.requests:
582 # Request still running, just exhausted decoded_tokens.
583 break
584 if not self.scheduler.finished_req_ids and (
585 not complete_transfers or not self.connector_scheduler._jobs
586 ):
587 break
588
589 self._parse_transfers()
590
591 if EOS_TOKEN_ID in decoded_tokens:
592 assert not self.scheduler.running
593
594 def _to_gpu_blocks(
595 self, blocks: tuple[int | tuple[int, int], ...]
596 ) -> list[GPUBlock]:
597 gpu_blocks: list[GPUBlock] = []
598 for block in blocks:
599 if isinstance(block, int):
600 for group_idx in range(self.num_kv_groups):
601 gpu_blocks.append(
602 GPUBlock(group_idx=group_idx, request_block_offset=block)
603 )
604 else:
605 group_idx, offset = block
606 gpu_blocks.append(
607 GPUBlock(group_idx=group_idx, request_block_offset=offset)
608 )
609 return gpu_blocks
610
611 def run(
612 self,
613 decoded_tokens: list[int],
614 complete_transfers: bool = True,
615 expected_stored: tuple[int | tuple[int, int], ...] = (),
616 expected_loaded: tuple[int | tuple[int, int], ...] = (),
617 expected_flushed: tuple[int | tuple[int, int], ...] = (),
618 post_step_fn: Callable[[], None] | None = None,
619 ):
620 """Runs multiple engine (scheduler + worker) steps.
621 Assumes a single request is running.
622
623 Args:
624 decoded_tokens: the tokens to yield at each step.
625 complete_transfers: complete transfers immediately
626 expected_stored: GPU blocks
627 that are expected to be written during the run.
628 expected_loaded: GPU blocks
629 that are expected to be loaded during the run.
630 expected_flushed: GPU blocks
631 that are expected to be flushed during the run.
632
633 A GPU block is either a (group_idx: int, request_block_offset: int)
634 or just request_block_offset: int.
635 The latter case is a convenience for representing all groups.
636
637 """
638 expected_stored_gpu_blocks = self._to_gpu_blocks(expected_stored)
639 expected_loaded_gpu_blocks = self._to_gpu_blocks(expected_loaded)
640 expected_flushed_gpu_blocks = self._to_gpu_blocks(expected_flushed)
641
642 self.manager.reset_mock()
643 self._run(decoded_tokens, complete_transfers, post_step_fn=post_step_fn)
644
645 loaded_gpu_blocks: set[GPUBlock] = set()
646 for transfer in self.completed_loads:
647 for gpu_block, offloaded_address in zip(
648 transfer.gpu_blocks, transfer.offload_addresses
649 ):
650 loaded_gpu_blocks.add(gpu_block)
651 assert gpu_block == self.offloaded[offloaded_address]
652
653 assert set(expected_loaded_gpu_blocks) == loaded_gpu_blocks
654 self.completed_loads.clear()
655
656 stored_gpu_blocks: set[GPUBlock] = set()
657 for transfer in self.completed_stores:
658 for gpu_block, offloaded_address in zip(
659 transfer.gpu_blocks, transfer.offload_addresses
660 ):
661 stored_gpu_blocks.add(gpu_block)
662 self.offloaded[offloaded_address] = gpu_block
663
664 assert set(expected_stored_gpu_blocks) == stored_gpu_blocks
665 self.completed_stores.clear()
666
667 assert set(expected_flushed_gpu_blocks) == self.flushed_gpu_blocks
668 self.flushed_gpu_blocks.clear()
669
670
671 @pytest.fixture
672 def request_runner():
673 runners = []
674
675 def runner_factory(
676 block_size,
677 num_gpu_blocks,
678 async_scheduling,
679 blocks_per_chunk=1,
680 kv_cache_groups=None,
681 extra_config_overrides=None,
682 worker_count=1,
683 retention_interval=None,
684 speculative_config=None,
685 ):
686 runner = RequestRunner(
687 block_size=block_size,
688 num_gpu_blocks=num_gpu_blocks,
689 blocks_per_chunk=blocks_per_chunk,
690 async_scheduling=async_scheduling,
691 kv_cache_groups=kv_cache_groups,
692 extra_config_overrides=extra_config_overrides,
693 worker_count=worker_count,
694 retention_interval=retention_interval,
695 speculative_config=speculative_config,
696 )
697 runners.append(runner)
698 return runner
699
700 yield runner_factory # pass factory to the test
701
702
703 def generate_store_output(keys: Iterable[OffloadKey]):
704 keys = list(keys)
705 return PrepareStoreOutput(
706 keys_to_store=list(keys),
707 store_spec=MockLoadStoreSpec(keys),
708 evicted_keys=[],
709 )