[Refactor][kv_offload]: rename `block`→`chunk` (#52615)

Signed-off-by: Ronen Schaffer <ronen.schaffer@ibm.com>

Ronen Schaffer committed Sep 9, 2026 at 17:45 UTC 138d137b5b955a5ebee98dfc946ecdd65d7b87ce
34 files changed +933 -916
docs/features/kv_offloading_usage.md
+4
@@ -22,6 +22,10 @@ flowchart LR
22 CPU <--> SN["..."]
23 ```
24
25 +## Terminology: Chunks
26 +
27 +The unit of operation is a **chunk** — a fixed-size piece of KV data covering a group of tokens. By default, a chunk maps to a single accelerator block. A configurable `blocks_per_chunk` parameter allows larger chunks, yielding larger I/Os to the host and secondary tiers.
28 +
29 ## Single-Tier Setup (CPU Only)
30
31 ```bash
tests/v1/kv_connector/unit/offloading_connector/test_metrics.py
+1 -1
@@ -704,7 +704,7 @@ def test_prom_metrics_registers_tiering_metrics_from_spec():
704 )
705
706 metric = prom_metrics._offloading_metric_defs[
707 - TieringOffloadingMetrics.BLOCK_QUERIES
707 + TieringOffloadingMetrics.CHUNK_QUERIES
708 ]
709 assert metric.kwargs["labelnames"] == ["model_name", "engine", "tier"]
710
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py
+2 -2
@@ -96,7 +96,7 @@ def test_swa_offload_window_covers_unaligned_hit(boundary, eagle, left_state):
96 groups.append(
97 KVCacheGroupSpec([f"layer{i}"], kv_spec, is_eagle_group=eagle and i == 1)
98 )
99 - manager = CPUOffloadingManager(num_blocks=100)
99 + manager = CPUOffloadingManager(num_chunks=100)
100 spec = SimpleNamespace(
101 tokens_per_block=(256, 64, 8),
102 tokens_per_hash=8,
@@ -2888,7 +2888,7 @@ class TestEagle:
2888 req=req,
2889 req_context=ReqContext(req_id="test-req"),
2890 offloading_context=RequestOffloadingContext(
2891 - policy=OffloadPolicy.BLOCK_LEVEL
2891 + policy=OffloadPolicy.CHUNK_LEVEL
2892 ),
2893 num_locally_computed_tokens=num_computed_tokens,
2894 )
tests/v1/kv_offload/cpu/policies/test_factory.py
+6 -6
@@ -7,7 +7,7 @@ import pytest
7 from vllm.v1.kv_offload.base import OffloadKey, ReqContext
8 from vllm.v1.kv_offload.cpu.manager import CPUOffloadingManager
9 from vllm.v1.kv_offload.cpu.policies.arc import ARCCachePolicy
10 -from vllm.v1.kv_offload.cpu.policies.base import BlockStatus, CachePolicy
10 +from vllm.v1.kv_offload.cpu.policies.base import CachePolicy, ChunkStatus
11 from vllm.v1.kv_offload.cpu.policies.factory import CachePolicyFactory
12 from vllm.v1.kv_offload.cpu.policies.lru import LRUCachePolicy
13
@@ -20,10 +20,10 @@ class _DummyCachePolicy(CachePolicy):
20 def __init__(self, cache_capacity: int) -> None:
21 self.cache_capacity = cache_capacity
22
23 - def get(self, key: OffloadKey) -> BlockStatus | None:
23 + def get(self, key: OffloadKey) -> ChunkStatus | None:
24 return None
25
26 - def insert(self, key: OffloadKey, block: BlockStatus) -> None:
26 + def insert(self, key: OffloadKey, chunk: ChunkStatus) -> None:
27 pass
28
29 def remove(self, key: OffloadKey) -> None:
@@ -34,7 +34,7 @@ class _DummyCachePolicy(CachePolicy):
34
35 def evict(
36 self, n: int, protected: set[OffloadKey]
37 - ) -> list[tuple[OffloadKey, BlockStatus]] | None:
37 + ) -> list[tuple[OffloadKey, ChunkStatus]] | None:
38 return None
39
40 def clear(self) -> None:
@@ -72,7 +72,7 @@ class TestCachePolicyFactory:
72 policy_cls = CachePolicyFactory.get_cache_policy_cls("dummy")
73 assert policy_cls is _DummyCachePolicy
74
75 - manager = CPUOffloadingManager(num_blocks=4, cache_policy="dummy")
75 + manager = CPUOffloadingManager(num_chunks=4, cache_policy="dummy")
76 assert isinstance(manager._policy, _DummyCachePolicy)
77
78 def test_unregistered_policy_raises(self):
@@ -98,7 +98,7 @@ class TestCachePolicyFactory:
98 """End-to-end: CPUOffloadingManager resolves an unregistered policy
99 purely from cache_policy_module_path."""
100 manager = CPUOffloadingManager(
101 - num_blocks=4,
101 + num_chunks=4,
102 cache_policy="_DummyCachePolicy",
103 cache_policy_module_path="tests.v1.kv_offload.cpu.policies.test_factory",
104 )
tests/v1/kv_offload/cpu/test_canonical_layout.py
+2 -2
@@ -232,9 +232,9 @@ def test_cross_topology_roundtrip(writer_tp: int, reader_tp: int):
232 def canonical_view(rank: int, world_size: int) -> torch.Tensor:
233 region = SharedOffloadRegion(
234 engine_id=engine_id,
235 - num_blocks=num_blocks,
235 + num_chunks=num_blocks,
236 rank=rank,
237 - kv_bytes_per_block=row_stride,
237 + kv_bytes_per_chunk=row_stride,
238 cpu_page_size=row_stride // world_size,
239 )
240 regions.append(region)
tests/v1/kv_offload/cpu/test_gpu_worker.py
+46 -46
@@ -26,7 +26,7 @@ from vllm.v1.kv_offload.cpu.gpu_worker import CPUOffloadingWorker
26 from vllm.v1.kv_offload.cpu.shared_offload_region import SharedOffloadRegion
27
28 NUM_GPU_BLOCKS = [64]
29 -NUM_CPU_BLOCKS = [256]
29 +NUM_CPU_CHUNKS = [256]
30 GPU_PAGE_SIZES = [512, 1024]
31 BLOCKS_PER_CHUNK_VALUES = [1, 3]
32 NUM_TENSORS = [4]
@@ -225,7 +225,7 @@ def test_worker_syncs_before_cleanup_after_handler_failure(
225 @pytest.mark.parametrize("gpu_page_size_bytes", GPU_PAGE_SIZES)
226 @pytest.mark.parametrize("blocks_per_chunk", BLOCKS_PER_CHUNK_VALUES)
227 @pytest.mark.parametrize("num_gpu_blocks", NUM_GPU_BLOCKS)
228 -@pytest.mark.parametrize("num_cpu_blocks", NUM_CPU_BLOCKS)
228 +@pytest.mark.parametrize("num_cpu_chunks", NUM_CPU_CHUNKS)
229 @pytest.mark.parametrize("num_tensors", NUM_TENSORS)
230 @pytest.mark.parametrize("seed", SEEDS)
231 @pytest.mark.parametrize("device", DEVICES)
@@ -241,7 +241,7 @@ def test_transfer(
241 gpu_page_size_bytes: int,
242 blocks_per_chunk: int,
243 num_gpu_blocks: int,
244 - num_cpu_blocks: int,
244 + num_cpu_chunks: int,
245 num_tensors: int,
246 seed: int,
247 device: str,
@@ -288,41 +288,41 @@ def test_transfer(
288 SharedOffloadRegion.BLOCK_SIZE_ALIGNMENT,
289 )
290 simulated_world_size = 2
291 - kv_bytes_per_block = (
291 + kv_bytes_per_chunk = (
292 cpu_page_size if replicated_layout else cpu_page_size * simulated_world_size
293 )
294 mmap_region = SharedOffloadRegion(
295 engine_id=str(uuid.uuid4()),
296 - num_blocks=num_cpu_blocks,
296 + num_chunks=num_cpu_chunks,
297 rank=0,
298 - kv_bytes_per_block=kv_bytes_per_block,
298 + kv_bytes_per_chunk=kv_bytes_per_chunk,
299 cpu_page_size=cpu_page_size,
300 )
301
302 worker = CPUOffloadingWorker(
303 kv_caches=kv_caches,
304 blocks_per_chunk=blocks_per_chunk,
305 - num_cpu_blocks=num_cpu_blocks,
305 + num_cpu_chunks=num_cpu_chunks,
306 mmap_region=mmap_region,
307 )
308
309 # select block mappings
310 gpu_blocks = random.sample(range(num_gpu_blocks), num_mappings * blocks_per_chunk)
311 - cpu_blocks = random.sample(range(num_cpu_blocks), num_mappings)
311 + cpu_chunks = random.sample(range(num_cpu_chunks), num_mappings)
312
313 - # expand cpu blocks to gpu-page granularity for uniform comparison:
314 - # each cpu block maps to blocks_per_chunk consecutive sub-blocks
315 - cpu_blocks_expanded = [
316 - cpu_block * blocks_per_chunk + j
317 - for cpu_block in cpu_blocks
313 + # expand cpu chunks to gpu-page granularity for uniform comparison:
314 + # each cpu chunk maps to blocks_per_chunk consecutive sub-blocks
315 + cpu_chunks_expanded = [
316 + cpu_chunk * blocks_per_chunk + j
317 + for cpu_chunk in cpu_chunks
318 for j in range(blocks_per_chunk)
319 ]
320
321 - # maybe skip some GPU blocks to test reading/writing from the middle of a CPU block
321 + # maybe skip some GPU blocks to test reading/writing from the middle of a CPU chunk
322 blocks_to_skip = blocks_per_chunk - 1
323 if blocks_to_skip > 0:
324 gpu_blocks = gpu_blocks[blocks_to_skip:]
325 - cpu_blocks_expanded = cpu_blocks_expanded[blocks_to_skip:]
325 + cpu_chunks_expanded = cpu_chunks_expanded[blocks_to_skip:]
326
327 # set transfer direction
328 if gpu_to_cpu:
@@ -330,16 +330,16 @@ def test_transfer(
330 src_spec = GPULoadStoreSpec(
331 gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
332 )
333 - dst_spec = CPULoadStoreSpec(cpu_blocks)
334 - dst_to_src = dict(zip(cpu_blocks_expanded, gpu_blocks))
333 + dst_spec = CPULoadStoreSpec(cpu_chunks)
334 + dst_to_src = dict(zip(cpu_chunks_expanded, gpu_blocks))
335 num_dst_sub_blocks = num_gpu_blocks
336 else:
337 handler = worker._load_handler
338 - src_spec = CPULoadStoreSpec(cpu_blocks)
338 + src_spec = CPULoadStoreSpec(cpu_chunks)
339 dst_spec = GPULoadStoreSpec(
340 gpu_blocks, group_sizes=(len(gpu_blocks),), block_indices=(blocks_to_skip,)
341 )
342 - dst_to_src = dict(zip(gpu_blocks, cpu_blocks_expanded))
342 + dst_to_src = dict(zip(gpu_blocks, cpu_chunks_expanded))
343 num_dst_sub_blocks = num_gpu_blocks
344
345 # randomize src and dst tensors before transfer
@@ -410,7 +410,7 @@ def test_transfer(
410 @pytest.mark.parametrize("gpu_page_size_bytes", GPU_PAGE_SIZES)
411 @pytest.mark.parametrize("blocks_per_chunk", BLOCKS_PER_CHUNK_VALUES)
412 @pytest.mark.parametrize("num_gpu_blocks", NUM_GPU_BLOCKS)
413 -@pytest.mark.parametrize("num_cpu_blocks", NUM_CPU_BLOCKS)
413 +@pytest.mark.parametrize("num_cpu_chunks", NUM_CPU_CHUNKS)
414 @pytest.mark.parametrize("seed", SEEDS)
415 @pytest.mark.parametrize("device", DEVICES)
416 @torch.inference_mode()
@@ -421,7 +421,7 @@ def test_transfer_multi_group(
421 gpu_page_size_bytes: int,
422 blocks_per_chunk: int,
423 num_gpu_blocks: int,
424 - num_cpu_blocks: int,
424 + num_cpu_chunks: int,
425 seed: int,
426 device: str,
427 ) -> None:
@@ -470,37 +470,37 @@ def test_transfer_multi_group(
470 worker = CPUOffloadingWorker(
471 kv_caches=canonical_kv_caches,
472 blocks_per_chunk=blocks_per_chunk,
473 - num_cpu_blocks=num_cpu_blocks,
473 + num_cpu_chunks=num_cpu_chunks,
474 )
475
476 # group 0: aligned, group 1: empty, group 2: unaligned on CPU->GPU
477 - group_sizes_in_cpu_blocks = [num_mappings_per_group, 0, num_mappings_per_group]
477 + group_sizes_in_cpu_chunks = [num_mappings_per_group, 0, num_mappings_per_group]
478
479 - total_cpu_blocks = sum(group_sizes_in_cpu_blocks)
480 - total_gpu_blocks_needed = total_cpu_blocks * blocks_per_chunk
479 + total_cpu_chunks = sum(group_sizes_in_cpu_chunks)
480 + total_gpu_blocks_needed = total_cpu_chunks * blocks_per_chunk
481 gpu_blocks_all = random.sample(range(num_gpu_blocks), total_gpu_blocks_needed)
482 - cpu_blocks_all = random.sample(range(num_cpu_blocks), total_cpu_blocks)
482 + cpu_chunks_all = random.sample(range(num_cpu_chunks), total_cpu_chunks)
483
484 - # split gpu/cpu blocks per group
484 + # split gpu blocks / cpu chunks per group
485 gpu_blocks_per_group: list[list[int]] = []
486 - cpu_blocks_per_group: list[list[int]] = []
486 + cpu_chunks_per_group: list[list[int]] = []
487 gpu_offset = 0
488 cpu_offset = 0
489 - for size in group_sizes_in_cpu_blocks:
489 + for size in group_sizes_in_cpu_chunks:
490 gpu_count = size * blocks_per_chunk
491 gpu_blocks_per_group.append(gpu_blocks_all[gpu_offset : gpu_offset + gpu_count])
492 - cpu_blocks_per_group.append(cpu_blocks_all[cpu_offset : cpu_offset + size])
492 + cpu_chunks_per_group.append(cpu_chunks_all[cpu_offset : cpu_offset + size])
493 gpu_offset += gpu_count
494 cpu_offset += size
495
496 - # expand cpu blocks to gpu-page granularity
497 - cpu_blocks_expanded_per_group = [
496 + # expand cpu chunks to gpu-page granularity
497 + cpu_chunks_expanded_per_group = [
498 [
499 - cpu_block * blocks_per_chunk + j
500 - for cpu_block in cpu_blocks
499 + cpu_chunk * blocks_per_chunk + j
500 + for cpu_chunk in cpu_chunks
501 for j in range(blocks_per_chunk)
502 ]
503 - for cpu_blocks in cpu_blocks_per_group
503 + for cpu_chunks in cpu_chunks_per_group
504 ]
505
506 # skip sub-blocks from group 2 to test unaligned transfers.
@@ -509,7 +509,7 @@ def test_transfer_multi_group(
509 gpu_blocks_per_group[2] = gpu_blocks_per_group[2][
510 sub_blocks_to_skip:-sub_blocks_to_skip
511 ]
512 - cpu_blocks_expanded_per_group[2] = cpu_blocks_expanded_per_group[2][
512 + cpu_chunks_expanded_per_group[2] = cpu_chunks_expanded_per_group[2][
513 sub_blocks_to_skip:-sub_blocks_to_skip
514 ]
515
@@ -520,10 +520,10 @@ def test_transfer_multi_group(
520 gpu_blocks.extend(gpu_blks)
521 group_sizes.append(len(gpu_blks))
522
523 - # build flat cpu_blocks list
524 - cpu_blocks = []
525 - for cpu_blks in cpu_blocks_per_group:
526 - cpu_blocks.extend(cpu_blks)
523 + # build flat cpu_chunks list
524 + cpu_chunks = []
525 + for cpu_chnks in cpu_chunks_per_group:
526 + cpu_chunks.extend(cpu_chnks)
527
528 # block_indices: only relevant for unaligned transfers
529 block_indices: list[int] = [0, 0, sub_blocks_to_skip]
@@ -533,18 +533,18 @@ def test_transfer_multi_group(
533 src_spec = GPULoadStoreSpec(
534 gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
535 )
536 - dst_spec = CPULoadStoreSpec(cpu_blocks)
536 + dst_spec = CPULoadStoreSpec(cpu_chunks)
537 # per-group mapping: cpu sub-block -> gpu sub-block
538 dst_to_src_per_group = [
539 dict(zip(expanded, gpu_blks))
540 for expanded, gpu_blks in zip(
541 - cpu_blocks_expanded_per_group, gpu_blocks_per_group
541 + cpu_chunks_expanded_per_group, gpu_blocks_per_group
542 )
543 ]
544 - num_dst_sub_blocks = num_cpu_blocks * blocks_per_chunk
544 + num_dst_sub_blocks = num_cpu_chunks * blocks_per_chunk
545 else:
546 handler = worker._load_handler
547 - src_spec = CPULoadStoreSpec(cpu_blocks)
547 + src_spec = CPULoadStoreSpec(cpu_chunks)
548 dst_spec = GPULoadStoreSpec(
549 gpu_blocks, group_sizes=group_sizes, block_indices=block_indices
550 )
@@ -552,7 +552,7 @@ def test_transfer_multi_group(
552 dst_to_src_per_group = [
553 dict(zip(gpu_blks, expanded))
554 for gpu_blks, expanded in zip(
555 - gpu_blocks_per_group, cpu_blocks_expanded_per_group
555 + gpu_blocks_per_group, cpu_chunks_expanded_per_group
556 )
557 ]
558 num_dst_sub_blocks = num_gpu_blocks
@@ -644,7 +644,7 @@ def test_load_waits_for_pending_compute_stream_writes(default_vllm_config) -> No
644 ],
645 ),
646 blocks_per_chunk=1,
647 - num_cpu_blocks=num_blocks,
647 + num_cpu_chunks=num_blocks,
648 )
649 worker._load_handler.src_tensors[0].fill_(sentinel)
650 expected = torch.full((page_size_bytes,), sentinel, dtype=torch.int8)
tests/v1/kv_offload/cpu/test_manager.py
+144 -144
@@ -36,7 +36,7 @@ _EMPTY_REQ_CTX = make_req_context()
36
37
38 def make_cpu_manager(
39 - num_blocks: int = 4,
39 + num_chunks: int = 4,
40 cache_policy: str = "lru",
41 cache_policy_module_path: str | None = None,
42 enable_events: bool = False,
@@ -44,7 +44,7 @@ def make_cpu_manager(
44 max_tracker_size: int = 64_000,
45 ) -> CPUOffloadingManager:
46 return CPUOffloadingManager(
47 - num_blocks=num_blocks,
47 + num_chunks=num_chunks,
48 cache_policy=cache_policy,
49 cache_policy_module_path=cache_policy_module_path,
50 enable_events=enable_events,
@@ -56,7 +56,7 @@ def make_cpu_manager(
56 @dataclass
57 class ExpectedPrepareStoreOutput:
58 keys_to_store: list[int]
59 - store_block_ids: list[int]
59 + store_chunk_ids: list[int]
60 evicted_keys: list[int]
61
62
@@ -93,9 +93,9 @@ def verify_store_output(
93 store_spec = prepare_store_output.store_spec
94 assert isinstance(store_spec, CPULoadStoreSpec)
95 expected_array = np.array(
96 - expected_prepare_store_output.store_block_ids, dtype=np.int64
96 + expected_prepare_store_output.store_chunk_ids, dtype=np.int64
97 )
98 - assert np.array_equal(expected_array, store_spec.block_ids)
98 + assert np.array_equal(expected_array, store_spec.chunk_ids)
99
100
101 def verify_load_output(
@@ -103,7 +103,7 @@ def verify_load_output(
103 ):
104 assert isinstance(prepare_load_output, CPULoadStoreSpec)
105 expected_array = np.array(expected_prepare_load_output, dtype=np.int64)
106 - assert np.array_equal(expected_array, prepare_load_output.block_ids)
106 + assert np.array_equal(expected_array, prepare_load_output.chunk_ids)
107
108
109 def check_split_usage_stats(
@@ -146,7 +146,7 @@ def verify_events(
146
147 def test_cpu_eviction_removed_precedes_stored():
148 """An eviction is announced before the store that reuses its capacity."""
149 - manager = make_cpu_manager(num_blocks=2, enable_events=True)
149 + manager = make_cpu_manager(num_chunks=2, enable_events=True)
150
151 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
152 manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -164,23 +164,23 @@ def test_cpu_eviction_removed_precedes_stored():
164
165
166 @pytest.mark.parametrize("eviction_policy", ["lru", "arc"])
167 -def test_already_stored_block_not_evicted_during_prepare_store(eviction_policy):
167 +def test_already_stored_chunk_not_evicted_during_prepare_store(eviction_policy):
168 """
169 - Regression test: a block that is already stored must not be evicted
170 - by prepare_store() when it needs to make room for new blocks.
169 + Regression test: a chunk that is already stored must not be evicted
170 + by prepare_store() when it needs to make room for new chunks.
171 Applies to both lru and arc policies.
172
173 Scenario:
174 - - Store blocks [1, 2] and complete.
175 - - touch([1]) makes block 2 the LRU candidate.
174 + - Store chunks [1, 2] and complete.
175 + - touch([1]) makes chunk 2 the LRU candidate.
176 - prepare_store([2, 3, 4, 5]):
177 - * block 2 is filtered out as "already stored"
178 - * but without the fix, block 2 would be evicted as the LRU
177 + * chunk 2 is filtered out as "already stored"
178 + * but without the fix, chunk 2 would be evicted as the LRU
179 candidate to make room for [3, 4, 5]
180 - - After complete_store([2, 3, 4, 5]), block 2 must still be present.
180 + - After complete_store([2, 3, 4, 5]), chunk 2 must still be present.
181 """
182 manager = make_cpu_manager(
183 - num_blocks=4,
183 + num_chunks=4,
184 cache_policy=eviction_policy,
185 enable_events=True,
186 )
@@ -189,33 +189,33 @@ def test_already_stored_block_not_evicted_during_prepare_store(eviction_policy):
189 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
190 manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
191
192 - # touch [1] to make block 2 the LRU candidate
192 + # touch [1] to make chunk 2 the LRU candidate
193 manager.touch(to_keys([1]), _EMPTY_REQ_CTX)
194
195 # prepare_store([2, 3, 4, 5]):
196 - # - block 2 is already stored -> filtered out of keys_to_store
197 - # - block 2 must NOT be evicted even though it is the LRU candidate
198 - # - block 1 (ID 0) is evicted instead; new blocks [3,4,5] get IDs 2,3,0
196 + # - chunk 2 is already stored -> filtered out of keys_to_store
197 + # - chunk 2 must NOT be evicted even though it is the LRU candidate
198 + # - chunk 1 (ID 0) is evicted instead; new chunks [3,4,5] get IDs 2,3,0
199 prepare_store_output = manager.prepare_store(to_keys([2, 3, 4, 5]), _EMPTY_REQ_CTX)
200 verify_store_output(
201 prepare_store_output,
202 ExpectedPrepareStoreOutput(
203 keys_to_store=[3, 4, 5],
204 - store_block_ids=[2, 3, 0],
205 - evicted_keys=[1], # block 1 evicted, not block 2
204 + store_chunk_ids=[2, 3, 0],
205 + evicted_keys=[1], # chunk 1 evicted, not chunk 2
206 ),
207 )
208
209 - # complete_store must not silently drop block 2
209 + # complete_store must not silently drop chunk 2
210 manager.complete_store(to_keys([2, 3, 4, 5]), _EMPTY_REQ_CTX)
211
212 - # block 2 must still be present in the cache
212 + # chunk 2 must still be present in the cache
213 assert manager.lookup(to_key(2), _EMPTY_REQ_CTX) is LookupResult.HIT
214
215
216 def test_filter_reused_manager_reports_stores_skipped_counter():
217 manager = make_cpu_manager(
218 - num_blocks=4,
218 + num_chunks=4,
219 cache_policy="lru",
220 store_threshold=2,
221 )
@@ -226,7 +226,7 @@ def test_filter_reused_manager_reports_stores_skipped_counter():
226 prepare_store_output,
227 ExpectedPrepareStoreOutput(
228 keys_to_store=[],
229 - store_block_ids=[],
229 + store_chunk_ids=[],
230 evicted_keys=[],
231 ),
232 )
@@ -247,34 +247,34 @@ def test_cpu_manager_reports_cache_usage_gauge():
247 ] == pytest.approx(value)
248
249 # Zero-capacity manager always reports 0.0
250 - manager = make_cpu_manager(num_blocks=0)
250 + manager = make_cpu_manager(num_chunks=0)
251 check_usage_stats(manager, 0.0)
252
253 - # Empty manager (4 blocks, none allocated): usage = 0.0
254 - manager = make_cpu_manager(num_blocks=4)
253 + # Empty manager (4 chunks, none allocated): usage = 0.0
254 + manager = make_cpu_manager(num_chunks=4)
255 check_usage_stats(manager, 0.0)
256
257 - # After allocating 2 of 4 blocks: usage = 0.5
257 + # After allocating 2 of 4 chunks: usage = 0.5
258 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
259 check_usage_stats(manager, 0.5)
260
261 - # After filling all 4 blocks: usage = 1.0
261 + # After filling all 4 chunks: usage = 1.0
262 manager.prepare_store(to_keys([3, 4]), _EMPTY_REQ_CTX)
263 check_usage_stats(manager, 1.0)
264
265 - # After completing store, the blocks becomes evictable as it is not actively used
265 + # After completing store, the chunks become evictable as not actively used
266 # and usage drops.
267 manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
268 check_usage_stats(manager, 0.5)
269
270 - # After completing store, the blocks becomes evictable as it is not actively used
270 + # After completing store, the chunks become evictable as not actively used
271 # and usage drops.
272 manager.complete_store(to_keys([3, 4]), _EMPTY_REQ_CTX)
273 check_usage_stats(manager, 0.0)
274
275
276 def test_cpu_manager_reports_allocation_size_histogram():
277 - manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
277 + manager = make_cpu_manager(num_chunks=4, cache_policy="lru")
278
279 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
280 manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -297,12 +297,12 @@ def test_cpu_manager_reports_allocation_size_histogram():
297
298
299 def test_cpu_manager_reports_allocation_size_on_allocation_failure(monkeypatch):
300 - manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
300 + manager = make_cpu_manager(num_chunks=4, cache_policy="lru")
301
302 - def fail_allocate_blocks(keys):
302 + def fail_allocate_chunks(keys):
303 raise RuntimeError("allocation failed")
304
305 - monkeypatch.setattr(manager, "_allocate_blocks", fail_allocate_blocks)
305 + monkeypatch.setattr(manager, "_allocate_chunks", fail_allocate_chunks)
306
307 with pytest.raises(RuntimeError, match="allocation failed"):
308 manager.prepare_store(to_keys([1, 2, 3]), _EMPTY_REQ_CTX)
@@ -316,7 +316,7 @@ def test_cpu_manager_reports_allocation_size_on_allocation_failure(monkeypatch):
316
317
318 def test_cpu_manager_reports_allocation_size_on_eviction_failure():
319 - manager = make_cpu_manager(num_blocks=1, cache_policy="lru")
319 + manager = make_cpu_manager(num_chunks=1, cache_policy="lru")
320
321 manager.prepare_store(to_keys([1]), _EMPTY_REQ_CTX)
322 manager.get_stats()
@@ -332,7 +332,7 @@ def test_cpu_manager_reports_allocation_size_on_eviction_failure():
332
333
334 def test_cpu_manager_reports_cache_write_and_read_usage_gauges():
335 - manager = make_cpu_manager(num_blocks=4)
335 + manager = make_cpu_manager(num_chunks=4)
336
337 # Store path: pins write usage until complete_store.
338 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -356,7 +356,7 @@ def test_cpu_manager_reports_cache_write_and_read_usage_gauges():
356
357
358 def test_cpu_manager_clears_write_usage_after_failed_store():
359 - manager = make_cpu_manager(num_blocks=4)
359 + manager = make_cpu_manager(num_chunks=4)
360
361 manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
362 check_split_usage_stats(manager, write=0.5, read=0.0, total=0.5)
@@ -369,8 +369,8 @@ def test_cpu_manager():
369 """
370 Tests CPUOffloadingManager with lru policy.
371 """
372 - # initialize a CPU manager with a capacity of 4 blocks
373 - cpu_manager = make_cpu_manager(num_blocks=4, cache_policy="lru", enable_events=True)
372 + # initialize a CPU manager with a capacity of 4 chunks
373 + cpu_manager = make_cpu_manager(num_chunks=4, cache_policy="lru", enable_events=True)
374
375 # prepare store [1, 2]
376 prepare_store_output = cpu_manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
@@ -378,7 +378,7 @@ def test_cpu_manager():
378 prepare_store_output,
379 ExpectedPrepareStoreOutput(
380 keys_to_store=[1, 2],
381 - store_block_ids=[0, 1],
381 + store_chunk_ids=[0, 1],
382 evicted_keys=[],
383 ),
384 )
@@ -407,7 +407,7 @@ def test_cpu_manager():
407 prepare_store_output,
408 ExpectedPrepareStoreOutput(
409 keys_to_store=[3, 4, 5],
410 - store_block_ids=[2, 3, 0],
410 + store_chunk_ids=[2, 3, 0],
411 evicted_keys=[1],
412 ),
413 )
@@ -445,7 +445,7 @@ def test_cpu_manager():
445 prepare_store_output,
446 ExpectedPrepareStoreOutput(
447 keys_to_store=[6, 7, 8],
448 - store_block_ids=[1, 0, 3],
448 + store_chunk_ids=[1, 0, 3],
449 evicted_keys=[4, 5, 2],
450 ),
451 )
@@ -462,7 +462,7 @@ def test_cpu_manager():
462 prepare_store_output,
463 ExpectedPrepareStoreOutput(
464 keys_to_store=[9],
465 - store_block_ids=[3],
465 + store_chunk_ids=[3],
466 evicted_keys=[8],
467 ),
468 )
@@ -482,38 +482,38 @@ def test_cpu_manager():
482
483
484 def test_prepare_load_preserves_key_order():
485 - """block_ids[i] must correspond to keys[i] (co-indexed invariant)."""
486 - manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
485 + """chunk_ids[i] must correspond to keys[i] (co-indexed invariant)."""
486 + manager = make_cpu_manager(num_chunks=4, cache_policy="lru")
487
488 key_a, key_b, key_c = to_key(0), to_key(1), to_key(2)
489
490 - # Store all three keys and learn their block ID assignments
490 + # Store all three keys and learn their chunk ID assignments
491 store_output = manager.prepare_store([key_a, key_b, key_c], _EMPTY_REQ_CTX)
492 assert store_output is not None
493 assert isinstance(store_output.store_spec, CPULoadStoreSpec)
494 - key_to_block_id = {
495 - k: int(bid)
496 - for k, bid in zip(store_output.keys_to_store, store_output.store_spec.block_ids)
494 + key_to_chunk_id = {
495 + k: int(cid)
496 + for k, cid in zip(store_output.keys_to_store, store_output.store_spec.chunk_ids)
497 }
498 manager.complete_store([key_a, key_b, key_c], _EMPTY_REQ_CTX)
499
500 # Forward order: [a, b, c]
501 spec_fwd = manager.prepare_load([key_a, key_b, key_c], _EMPTY_REQ_CTX)
502 assert isinstance(spec_fwd, CPULoadStoreSpec)
503 - assert [int(x) for x in spec_fwd.block_ids] == [
504 - key_to_block_id[key_a],
505 - key_to_block_id[key_b],
506 - key_to_block_id[key_c],
503 + assert [int(x) for x in spec_fwd.chunk_ids] == [
504 + key_to_chunk_id[key_a],
505 + key_to_chunk_id[key_b],
506 + key_to_chunk_id[key_c],
507 ]
508 manager.complete_load([key_a, key_b, key_c], _EMPTY_REQ_CTX) # order irrelevant
509
510 # Arbitrary permutation: [b, c, a]
511 spec_perm = manager.prepare_load([key_b, key_c, key_a], _EMPTY_REQ_CTX)
512 assert isinstance(spec_perm, CPULoadStoreSpec)
513 - assert [int(x) for x in spec_perm.block_ids] == [
514 - key_to_block_id[key_b],
515 - key_to_block_id[key_c],
516 - key_to_block_id[key_a],
513 + assert [int(x) for x in spec_perm.chunk_ids] == [
514 + key_to_chunk_id[key_b],
515 + key_to_chunk_id[key_c],
516 + key_to_chunk_id[key_a],
517 ]
518 manager.complete_load([key_a, key_b, key_c], _EMPTY_REQ_CTX) # order irrelevant
519
@@ -522,10 +522,10 @@ class TestARCPolicy:
522 """Unit tests for CPUOffloadingManager with ARC eviction policy."""
523
524 def _make_manager(
525 - self, num_blocks: int = 4, enable_events: bool = True
525 + self, num_chunks: int = 4, enable_events: bool = True
526 ) -> tuple[CPUOffloadingManager, ARCCachePolicy]:
527 manager = make_cpu_manager(
528 - num_blocks=num_blocks,
528 + num_chunks=num_chunks,
529 cache_policy="arc",
530 enable_events=enable_events,
531 )
@@ -548,7 +548,7 @@ class TestARCPolicy:
548 prepare_store_output,
549 ExpectedPrepareStoreOutput(
550 keys_to_store=[1, 2],
551 - store_block_ids=[0, 1],
551 + store_chunk_ids=[0, 1],
552 evicted_keys=[],
553 ),
554 )
@@ -569,36 +569,36 @@ class TestARCPolicy:
569 assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is LookupResult.HIT
570 assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is LookupResult.MISS
571
572 - # blocks should be in T1 (recent)
572 + # chunks should be in T1 (recent)
573 assert len(arc_policy.t1) == 2
574 assert len(arc_policy.t2) == 0
575
576 def test_t1_to_t2_promotion(self):
577 """
578 - Tests that accessing a block in T1 promotes it to T2 (frequent).
578 + Tests that accessing a chunk in T1 promotes it to T2 (frequent).
579 This is a key feature of ARC's adaptive behavior.
580 """
581 cpu_manager, arc_policy = self._make_manager(enable_events=False)
582
583 - # store and complete block 1
583 + # store and complete chunk 1
584 cpu_manager.prepare_store(to_keys([1]), _EMPTY_REQ_CTX)
585 cpu_manager.complete_store(to_keys([1]), _EMPTY_REQ_CTX)
586
587 - # block 1 starts in T1 (recent)
587 + # chunk 1 starts in T1 (recent)
588 assert to_keys([1])[0] in arc_policy.t1
589 assert to_keys([1])[0] not in arc_policy.t2
590
591 - # touch block 1 (simulate second access)
591 + # touch chunk 1 (simulate second access)
592 cpu_manager.touch(to_keys([1]), _EMPTY_REQ_CTX)
593
594 - # block 1 should now be in T2 (frequent)
594 + # chunk 1 should now be in T2 (frequent)
595 assert to_keys([1])[0] not in arc_policy.t1
596 assert to_keys([1])[0] in arc_policy.t2
597
598 def test_eviction_with_load(self):
599 """
600 Tests ARC eviction behavior similar to LRU test.
601 - Verifies that blocks being loaded (ref_cnt > 0) cannot be evicted.
601 + Verifies that chunks being loaded (ref_cnt > 0) cannot be evicted.
602 """
603 cpu_manager, _ = self._make_manager()
604
@@ -610,7 +610,7 @@ class TestARCPolicy:
610 prepare_store_output,
611 ExpectedPrepareStoreOutput(
612 keys_to_store=[1, 2, 3, 4],
613 - store_block_ids=[0, 1, 2, 3],
613 + store_chunk_ids=[0, 1, 2, 3],
614 evicted_keys=[],
615 ),
616 )
@@ -628,36 +628,36 @@ class TestARCPolicy:
628 cpu_manager.complete_load(to_keys([2, 3]), _EMPTY_REQ_CTX)
629
630 # now prepare store [5, 6, 7] should succeed
631 - # ARC will evict blocks one at a time from T1 as needed
631 + # ARC will evict chunks one at a time from T1 as needed
632 prepare_store_output = cpu_manager.prepare_store(
633 to_keys([5, 6, 7]), _EMPTY_REQ_CTX
634 )
635 assert prepare_store_output is not None
636 - # Should successfully evict enough blocks to make room (at least 1)
636 + # Should successfully evict enough chunks to make room (at least 1)
637 assert len(prepare_store_output.evicted_keys) >= 1
638
639 def test_adaptive_target(self):
640 """
641 Tests ARC's adaptive target adjustment via ghost lists.
642 - When a block in B1 (ghost list) is accessed, target_t1_size increases.
643 - When a block in B2 is accessed, target_t1_size decreases.
642 + When a chunk in B1 (ghost list) is accessed, target_t1_size increases.
643 + When a chunk in B2 is accessed, target_t1_size decreases.
644 """
645 - cpu_manager, arc_policy = self._make_manager(num_blocks=2, enable_events=False)
645 + cpu_manager, arc_policy = self._make_manager(num_chunks=2, enable_events=False)
646
647 - # store blocks 1, 2 (fills cache)
647 + # store chunks 1, 2 (fills cache)
648 cpu_manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
649 cpu_manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
650
651 initial_target = arc_policy.target_t1_size
652
653 - # store block 3, evicting block 1 (moves to B1 ghost list)
653 + # store chunk 3, evicting chunk 1 (moves to B1 ghost list)
654 cpu_manager.prepare_store(to_keys([3]), _EMPTY_REQ_CTX)
655 cpu_manager.complete_store(to_keys([3]), _EMPTY_REQ_CTX)
656
657 - # block 1 should be in B1 (ghost list)
657 + # chunk 1 should be in B1 (ghost list)
658 assert to_keys([1])[0] in arc_policy.b1
659
660 - # touch block 1 (cache miss, but in B1)
660 + # touch chunk 1 (cache miss, but in B1)
661 # this should increase target_t1_size (favor recency)
662 cpu_manager.touch(to_keys([1]), _EMPTY_REQ_CTX)
663
@@ -671,11 +671,11 @@ class TestARCPolicy:
671 """
672 cpu_manager, arc_policy = self._make_manager(enable_events=False)
673
674 - # store blocks 1, 2, 3, 4
674 + # store chunks 1, 2, 3, 4
675 cpu_manager.prepare_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
676 cpu_manager.complete_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
677
678 - # promote blocks 3, 4 to T2 by touching them
678 + # promote chunks 3, 4 to T2 by touching them
679 cpu_manager.touch(to_keys([3, 4]), _EMPTY_REQ_CTX)
680
681 # now: T1 = {1, 2}, T2 = {3, 4}
@@ -686,22 +686,22 @@ class TestARCPolicy:
686 # (when |T1| >= target, evict from T1)
687 arc_policy.target_t1_size = 1
688
689 - # store block 5, should evict from T1 (block 1, LRU in T1)
689 + # store chunk 5, should evict from T1 (chunk 1, LRU in T1)
690 output = cpu_manager.prepare_store(to_keys([5]), _EMPTY_REQ_CTX)
691 assert output is not None
692 assert to_keys([1]) == output.evicted_keys
693
694 cpu_manager.complete_store(to_keys([5]), _EMPTY_REQ_CTX)
695
696 - # block 1 should be in B1 (ghost list)
696 + # chunk 1 should be in B1 (ghost list)
697 assert to_keys([1])[0] in arc_policy.b1
698 - # block 5 should be in T1
698 + # chunk 5 should be in T1
699 assert to_keys([5])[0] in arc_policy.t1
700
701 def test_batch_eviction_scans_t1_and_t2_once(self):
702 """ARC batch eviction must preserve order without restarting scans."""
703 cpu_manager, arc_policy = self._make_manager(
704 - num_blocks=256, enable_events=False
704 + num_chunks=256, enable_events=False
705 )
706 keys = to_keys(list(range(256)))
707 cpu_manager.prepare_store(keys, _EMPTY_REQ_CTX)
@@ -721,13 +721,13 @@ class TestARCPolicy:
721 t2_order = list(arc_policy.t2)
722 expected_t1 = [
723 key
724 - for key, block in arc_policy.t1.items()
725 - if block.ref_cnt == 0 and key not in protected
724 + for key, chunk in arc_policy.t1.items()
725 + if chunk.ref_cnt == 0 and key not in protected
726 ][:num_t1_evictions]
727 expected_t2 = [
728 key
729 - for key, block in arc_policy.t2.items()
730 - if block.ref_cnt == 0 and key not in protected
729 + for key, chunk in arc_policy.t2.items()
730 + if chunk.ref_cnt == 0 and key not in protected
731 ][:num_t2_evictions]
732 expected_t1_scans = t1_order.index(expected_t1[-1]) + 1
733 expected_t2_scans = t2_order.index(expected_t2[-1]) + 1
@@ -746,7 +746,7 @@ class TestARCPolicy:
746
747 def test_batch_eviction_falls_back_after_t1_iterator_exhausted(self):
748 """An exhausted T1 scan must keep falling back to T2."""
749 - cpu_manager, arc_policy = self._make_manager(num_blocks=8, enable_events=False)
749 + cpu_manager, arc_policy = self._make_manager(num_chunks=8, enable_events=False)
750 keys = to_keys(list(range(8)))
751 cpu_manager.prepare_store(keys, _EMPTY_REQ_CTX)
752 cpu_manager.complete_store(keys, _EMPTY_REQ_CTX)
@@ -764,8 +764,8 @@ class TestARCPolicy:
764 # the target, so each remaining selection must retry T1 then use T2.
765 eligible_t1 = [
766 key
767 - for key, block in arc_policy.t1.items()
768 - if block.ref_cnt == 0 and key not in protected
767 + for key, chunk in arc_policy.t1.items()
768 + if chunk.ref_cnt == 0 and key not in protected
769 ]
770 assert eligible_t1 == t1_order[:1]
771 assert len(t1_order) - 1 >= int(arc_policy.target_t1_size)
@@ -839,7 +839,7 @@ class TestARCPolicy:
839
840 def test_batch_eviction_failure_is_atomic(self):
841 """Finding only some candidates must not partially evict the cache."""
842 - cpu_manager, arc_policy = self._make_manager(num_blocks=4, enable_events=False)
842 + cpu_manager, arc_policy = self._make_manager(num_chunks=4, enable_events=False)
843 keys = to_keys(list(range(4)))
844 cpu_manager.prepare_store(keys, _EMPTY_REQ_CTX)
845 cpu_manager.complete_store(keys, _EMPTY_REQ_CTX)
@@ -858,13 +858,13 @@ class TestARCPolicy:
858 Tests that ghost lists (B1, B2) don't grow unbounded.
859 They should be capped at cache_capacity.
860 """
861 - cpu_manager, arc_policy = self._make_manager(num_blocks=2, enable_events=False)
861 + cpu_manager, arc_policy = self._make_manager(num_chunks=2, enable_events=False)
862
863 - # fill cache with blocks 1, 2
863 + # fill cache with chunks 1, 2
864 cpu_manager.prepare_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
865 cpu_manager.complete_store(to_keys([1, 2]), _EMPTY_REQ_CTX)
866
867 - # store many blocks to fill ghost lists
867 + # store many chunks to fill ghost lists
868 for i in range(3, 20):
869 cpu_manager.prepare_store(to_keys([i]), _EMPTY_REQ_CTX)
870 cpu_manager.complete_store(to_keys([i]), _EMPTY_REQ_CTX)
@@ -880,7 +880,7 @@ class TestARCPolicy:
880 """
881 cpu_manager, arc_policy = self._make_manager()
882
883 - # store blocks 1, 2, 3, 4
883 + # store chunks 1, 2, 3, 4
884 cpu_manager.prepare_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
885 cpu_manager.complete_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
886
@@ -895,13 +895,13 @@ class TestARCPolicy:
895 assert len(arc_policy.t1) == 1
896 assert len(arc_policy.t2) == 3
897
898 - # store block 5, should evict from T1 (block 2, only one in T1)
898 + # store chunk 5, should evict from T1 (chunk 2, only one in T1)
899 prepare_store_output = cpu_manager.prepare_store(to_keys([5]), _EMPTY_REQ_CTX)
900 verify_store_output(
901 prepare_store_output,
902 ExpectedPrepareStoreOutput(
903 keys_to_store=[5],
904 - store_block_ids=[1], # reuses block 2's storage
904 + store_chunk_ids=[1], # reuses chunk 2's storage
905 evicted_keys=[2],
906 ),
907 )
@@ -913,11 +913,11 @@ class TestARCPolicy:
913 """
914 cpu_manager, arc_policy = self._make_manager()
915
916 - # store blocks 1, 2, 3, 4
916 + # store chunks 1, 2, 3, 4
917 cpu_manager.prepare_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
918 cpu_manager.complete_store(to_keys([1, 2, 3, 4]), _EMPTY_REQ_CTX)
919
920 - # prepare store block 5 (will evict block 1)
920 + # prepare store chunk 5 (will evict chunk 1)
921 prepare_store_output = cpu_manager.prepare_store(to_keys([5]), _EMPTY_REQ_CTX)
922 assert prepare_store_output is not None
923 assert len(prepare_store_output.evicted_keys) == 1
@@ -925,13 +925,13 @@ class TestARCPolicy:
925 # complete store with failure
926 cpu_manager.complete_store(to_keys([5]), _EMPTY_REQ_CTX, success=False)
927
928 - # block 5 should not be in cache
928 + # chunk 5 should not be in cache
929 assert cpu_manager.lookup(to_key(5), _EMPTY_REQ_CTX) is LookupResult.MISS
930 - # block 5 should not be in T1 or T2
930 + # chunk 5 should not be in T1 or T2
931 assert to_keys([5])[0] not in arc_policy.t1
932 assert to_keys([5])[0] not in arc_policy.t2
933
934 - # evicted block should still be gone (in B1 ghost list)
934 + # evicted chunk should still be gone (in B1 ghost list)
935 evicted_hash = prepare_store_output.evicted_keys[0]
936 assert evicted_hash in arc_policy.b1
937
@@ -954,7 +954,7 @@ class TestARCPolicy:
954 assert len(prepare_store_output.evicted_keys) == 1
955 cpu_manager.complete_store(to_keys([3, 4, 5]), _EMPTY_REQ_CTX)
956
957 - # promote some blocks to T2
957 + # promote some chunks to T2
958 cpu_manager.touch(to_keys([2, 3]), _EMPTY_REQ_CTX)
959
960 # T1 has {4, 5}, T2 has {2, 3}
@@ -966,7 +966,7 @@ class TestARCPolicy:
966 assert prepare_store_output is not None
967 cpu_manager.complete_store(to_keys([6]), _EMPTY_REQ_CTX)
968
969 - # verify blocks 2, 3 (in T2) are still present
969 + # verify chunks 2, 3 (in T2) are still present
970 assert cpu_manager.lookup(to_key(2), _EMPTY_REQ_CTX) is LookupResult.HIT
971 assert cpu_manager.lookup(to_key(3), _EMPTY_REQ_CTX) is LookupResult.HIT
972
@@ -980,7 +980,7 @@ def test_filter_reused_manager():
980 Tests CPUOffloadingManager reuse filtering (store_threshold=2).
981 """
982 manager = make_cpu_manager(
983 - num_blocks=4,
983 + num_chunks=4,
984 cache_policy="lru",
985 enable_events=True,
986 store_threshold=2,
@@ -1023,7 +1023,7 @@ def test_filter_reused_manager():
1023 def test_filter_reused_manager_oversized_offer_makes_progress():
1024 """An offer larger than the tracker capacity must not churn forever."""
1025 manager = make_cpu_manager(
1026 - num_blocks=4,
1026 + num_chunks=4,
1027 cache_policy="lru",
1028 store_threshold=2,
1029 max_tracker_size=3,
@@ -1042,69 +1042,69 @@ def test_filter_reused_manager_oversized_offer_makes_progress():
1042 assert stored_keys == set(keys)
1043
1044
1045 -def test_evictable_cache_block_count():
1045 +def test_evictable_cache_chunk_count():
1046 """
1047 - Verifies _num_evictable_cache_blocks is maintained correctly through the
1047 + Verifies _num_evictable_cache_chunks is maintained correctly through the
1048 full store/load lifecycle, eviction, failed stores, concurrent loads,
1049 reset_cache, and the early-exit fast path in prepare_store.
1050 """
1051 - manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
1051 + manager = make_cpu_manager(num_chunks=4, cache_policy="lru")
1052
1053 - # Initially no blocks allocated.
1054 - assert manager._num_evictable_cache_blocks == 0
1053 + # Initially no chunks allocated.
1054 + assert manager._num_evictable_cache_chunks == 0
1055
1056 # Initial cache state [x, x, x, x]
1057
1058 - # We get 3 blocks from the cache.
1058 + # We get 3 chunks from the cache.
1059 manager.prepare_store(to_keys([1, 2, 3]), _EMPTY_REQ_CTX)
1060 # cache state [1', 2', 3', x] <- 1', 2', 3' are actively being used.
1061 - assert manager._num_evictable_cache_blocks == 0
1061 + assert manager._num_evictable_cache_chunks == 0
1062
1063 # Completing stores makes them idle.
1064 manager.complete_store(to_keys([1, 2, 3]), _EMPTY_REQ_CTX)
1065 - # cache state [1, 2, 3, x] <- 1, 2, 3 blocks are idle.
1066 - assert manager._num_evictable_cache_blocks == 3
1065 + # cache state [1, 2, 3, x] <- 1, 2, 3 chunks are idle.
1066 + assert manager._num_evictable_cache_chunks == 3
1067
1068 - # prepare_load pins a block: idle count decrements once even if the
1069 - # same block is loaded by two concurrent callers.
1068 + # prepare_load pins a chunk: idle count decrements once even if the
1069 + # same chunk is loaded by two concurrent callers.
1070 manager.prepare_load(to_keys([1]), _EMPTY_REQ_CTX)
1071 - # cache state [1', 2, 3, x] <- 2, 3 blocks are idle.
1072 - assert manager._num_evictable_cache_blocks == 2
1071 + # cache state [1', 2, 3, x] <- 2, 3 chunks are idle.
1072 + assert manager._num_evictable_cache_chunks == 2
1073 manager.prepare_load(to_keys([1]), _EMPTY_REQ_CTX) # 2nd concurrent load
1074 - # cache state [1', 2, 3, x] <- 2, 3 blocks are idle.
1075 - assert manager._num_evictable_cache_blocks == 2 # no double-decrement
1074 + # cache state [1', 2, 3, x] <- 2, 3 chunks are idle.
1075 + assert manager._num_evictable_cache_chunks == 2 # no double-decrement
1076
1077 # First complete_load does not restore idle (ref_cnt still 1).
1078 manager.complete_load(to_keys([1]), _EMPTY_REQ_CTX)
1079 - # cache state [1', 2, 3, x] <- 2, 3 blocks are idle.
1080 - assert manager._num_evictable_cache_blocks == 2
1081 - # Second complete_load drops ref_cnt to 0 -> block becomes idle again.
1079 + # cache state [1', 2, 3, x] <- 2, 3 chunks are idle.
1080 + assert manager._num_evictable_cache_chunks == 2
1081 + # Second complete_load drops ref_cnt to 0 -> chunk becomes idle again.
1082 manager.complete_load(to_keys([1]), _EMPTY_REQ_CTX)
1083 - # cache state [1, 2, 3, x] <- 1, 2, 3 blocks are idle.
1084 - assert manager._num_evictable_cache_blocks == 3
1083 + # cache state [1, 2, 3, x] <- 1, 2, 3 chunks are idle.
1084 + assert manager._num_evictable_cache_chunks == 3
1085
1086 # Eviction decrements idle count.
1087 - # Cache has 3 stored blocks and 1 free slot. Storing 3 new keys needs 2 eviction.
1087 + # Cache has 3 stored chunks and 1 free slot. Storing 3 new keys needs 2 eviction.
1088 manager.prepare_store(to_keys([4, 5, 6]), _EMPTY_REQ_CTX)
1089 - # cache state [1, 4', 5', 6'] <- block 1 is idle
1090 - assert manager._num_evictable_cache_blocks == 1
1089 + # cache state [1, 4', 5', 6'] <- chunk 1 is idle
1090 + assert manager._num_evictable_cache_chunks == 1
1091
1092 - # Failed store does not increment idle count (block discarded from cache).
1092 + # Failed store does not increment idle count (chunk discarded from cache).
1093 manager.complete_store(to_keys([4, 5, 6]), _EMPTY_REQ_CTX, success=False)
1094 - # cache state [1, x, x, x] <- block 1 is idle. Other returned to cache.
1095 - assert manager._num_evictable_cache_blocks == 1
1094 + # cache state [1, x, x, x] <- chunk 1 is idle. Other returned to cache.
1095 + assert manager._num_evictable_cache_chunks == 1
1096
1097 # reset_cache zeroes the count unconditionally.
1098 manager.reset_cache()
1099 # cache state [x, x, x, x]
1100 - assert manager._num_evictable_cache_blocks == 0
1100 + assert manager._num_evictable_cache_chunks == 0
1101
1102 - # setup 3 blocks with loads so idle count drops to 0.
1102 + # setup 3 chunks with loads so idle count drops to 0.
1103 manager.prepare_store(to_keys([10, 11, 12]), _EMPTY_REQ_CTX)
1104 manager.complete_store(to_keys([10, 11, 12]), _EMPTY_REQ_CTX)
1105 manager.prepare_load(to_keys([10, 11, 12]), _EMPTY_REQ_CTX)
1106 # cache state [10', 11', 12', x]
1107 - assert manager._num_evictable_cache_blocks == 0
1107 + assert manager._num_evictable_cache_chunks == 0
1108
1109 # prepare_store requiring eviction must return None immediately (fast exit).
1110 # Spy on policy.evict to confirm the fast path short-circuits before calling it.
@@ -1120,24 +1120,24 @@ def test_evictable_cache_block_count():
1120 # cache state [10', 11', 12', x] <- cannot evict anything
1121 assert manager.prepare_store(to_keys([14, 15]), _EMPTY_REQ_CTX) is None
1122 assert not evict_called, (
1123 - "_num_evictable_cache_blocks==0 should short-circuit before evict()"
1123 + "_num_evictable_cache_chunks==0 should short-circuit before evict()"
1124 )
1125
1126 # After releasing the loads, eviction becomes possible again.
1127 manager.complete_load(to_keys([10, 11, 12]), _EMPTY_REQ_CTX)
1128 # cache state [10, 11, 12, x] <- 10, 11, 12 are idle
1129 - assert manager._num_evictable_cache_blocks == 3
1129 + assert manager._num_evictable_cache_chunks == 3
1130 assert manager.prepare_store(to_keys([14, 15]), _EMPTY_REQ_CTX) is not None
1131 # cache state [10, 11, 14', 15'] <- 10, 11 are idle
1132 - assert manager._num_evictable_cache_blocks == 2
1132 + assert manager._num_evictable_cache_chunks == 2
1133 manager.complete_store(to_keys([14, 15]), _EMPTY_REQ_CTX)
1134 - # cache state [10, 11, 14, 15] <- all blocks idle
1135 - assert manager._num_evictable_cache_blocks == 4
1134 + # cache state [10, 11, 14, 15] <- all chunks idle
1135 + assert manager._num_evictable_cache_chunks == 4
1136
1137
1138 def test_touch_forwards_req_context_to_policy(monkeypatch):
1139 """Regression: CPUOffloadingManager.touch forwards ReqContext to policy."""
1140 - manager = make_cpu_manager(num_blocks=4, cache_policy="lru")
1140 + manager = make_cpu_manager(num_chunks=4, cache_policy="lru")
1141 received = []
1142
1143 def spy_touch(keys: Iterable[OffloadKey], req_context: ReqContext) -> None:
tests/v1/kv_offload/cpu/test_shared_offload_region.py
+55 -55
@@ -38,7 +38,7 @@ def _set_spawn_method(monkeypatch):
38
39 def _make_region(
40 engine_id: str,
41 - num_blocks: int = 4,
41 + num_chunks: int = 4,
42 cpu_page_size: int = PAGE_SIZE,
43 num_workers: int = 1,
44 rank: int = 0,
@@ -47,9 +47,9 @@ def _make_region(
47 assert cpu_page_size % PAGE_SIZE == 0
48 return SharedOffloadRegion(
49 engine_id=engine_id,
50 - num_blocks=num_blocks,
50 + num_chunks=num_chunks,
51 rank=rank,
52 - kv_bytes_per_block=num_workers * cpu_page_size,
52 + kv_bytes_per_chunk=num_workers * cpu_page_size,
53 cpu_page_size=cpu_page_size,
54 barrier=barrier,
55 )
@@ -99,16 +99,16 @@ def _region(engine_id: str, **kwargs):
99 def _multi_region(
100 engine_id: str,
101 num_workers: int,
102 - num_blocks: int = 4,
102 + num_chunks: int = 4,
103 cpu_page_size: int = PAGE_SIZE,
104 ):
105 """Context manager: create one SharedOffloadRegion per rank, clean up on exit."""
106 regions = [
107 SharedOffloadRegion(
108 engine_id=engine_id,
109 - num_blocks=num_blocks,
109 + num_chunks=num_chunks,
110 rank=rank,
111 - kv_bytes_per_block=num_workers * cpu_page_size,
111 + kv_bytes_per_chunk=num_workers * cpu_page_size,
112 cpu_page_size=cpu_page_size,
113 )
114 for rank in range(num_workers)
@@ -124,7 +124,7 @@ def _multi_region(
124 def _race_construct(
125 engine_id: str,
126 num_workers: int,
127 - num_blocks: int = 4,
127 + num_chunks: int = 4,
128 cpu_page_size: int = PAGE_SIZE,
129 ) -> tuple[list[SharedOffloadRegion], list[Exception]]:
130 """Spawn num_workers threads that all race to construct SharedOffloadRegion."""
@@ -137,9 +137,9 @@ def _race_construct(
137 try:
138 regions[rank] = SharedOffloadRegion(
139 engine_id=engine_id,
140 - num_blocks=num_blocks,
140 + num_chunks=num_chunks,
141 rank=rank,
142 - kv_bytes_per_block=num_workers * cpu_page_size,
142 + kv_bytes_per_chunk=num_workers * cpu_page_size,
143 cpu_page_size=cpu_page_size,
144 )
145 except Exception as e:
@@ -156,7 +156,7 @@ def _race_construct(
156
157 def _mp_race_construct_and_write(
158 engine_id: str,
159 - num_blocks: int,
159 + num_chunks: int,
160 rank: int,
161 num_workers: int,
162 cpu_page_size: int,
@@ -170,9 +170,9 @@ def _mp_race_construct_and_write(
170 try:
171 region = SharedOffloadRegion(
172 engine_id=engine_id,
173 - num_blocks=num_blocks,
173 + num_chunks=num_chunks,
174 rank=rank,
175 - kv_bytes_per_block=num_workers * cpu_page_size,
175 + kv_bytes_per_chunk=num_workers * cpu_page_size,
176 cpu_page_size=cpu_page_size,
177 )
178 t = region.create_next_worker_view(cpu_page_size)
@@ -198,9 +198,9 @@ def _mp_barrier_construct_and_hold(
198 try:
199 region = SharedOffloadRegion(
200 engine_id=engine_id,
201 - num_blocks=2,
201 + num_chunks=2,
202 rank=rank,
203 - kv_bytes_per_block=num_workers * PAGE_SIZE,
203 + kv_bytes_per_chunk=num_workers * PAGE_SIZE,
204 cpu_page_size=PAGE_SIZE,
205 barrier=lambda: barrier.wait(30),
206 )
@@ -233,9 +233,9 @@ def iid():
233
234
235 def test_create_next_worker_view_shape_and_stride(iid):
236 - """Returned tensor must have shape (num_blocks, tensor_page_size) and
236 + """Returned tensor must have shape (num_chunks, tensor_page_size) and
237 stride (row_stride, 1) where row_stride = cpu_page_size * num_workers."""
238 - with _region(iid, num_blocks=4, cpu_page_size=2 * PAGE_SIZE) as r:
238 + with _region(iid, num_chunks=4, cpu_page_size=2 * PAGE_SIZE) as r:
239 t = r.create_next_worker_view(PAGE_SIZE)
240 assert t.shape == (4, PAGE_SIZE)
241 # num_workers=1 → row_stride = cpu_page_size
@@ -253,7 +253,7 @@ def test_create_next_worker_view_storage_offset_rank0(iid):
253
254 def test_create_next_worker_view_storage_offset_rank1(iid):
255 """rank=1 worker's first tensor must start cpu_page_size bytes into the mmap."""
256 - with _multi_region(iid, num_workers=2, num_blocks=4) as (r0, r1):
256 + with _multi_region(iid, num_workers=2, num_chunks=4) as (r0, r1):
257 t1 = r1.create_next_worker_view(PAGE_SIZE)
258 assert t1.data_ptr() == r1._base.data_ptr() + PAGE_SIZE
259 del t1
@@ -261,7 +261,7 @@ def test_create_next_worker_view_storage_offset_rank1(iid):
261
262 def test_create_next_worker_view_row_stride_with_multiple_workers(iid):
263 """With num_workers=4, row_stride must be 4 * cpu_page_size."""
264 - with _region(iid, num_blocks=2, num_workers=4) as r:
264 + with _region(iid, num_chunks=2, num_workers=4) as r:
265 t = r.create_next_worker_view(PAGE_SIZE)
266 assert t.stride(0) == 4 * PAGE_SIZE
267 del t
@@ -332,12 +332,12 @@ def test_create_next_worker_view_overflow_does_not_mutate_cursor(iid):
332 def test_create_next_worker_view_write_visible_in_raw_mmap(iid):
333 """Writes into a create_next_worker_view view must appear at the correct
334 raw mmap offset"""
335 - with _region(iid, num_blocks=4) as r:
335 + with _region(iid, num_chunks=4) as r:
336 t = r.create_next_worker_view(PAGE_SIZE)
337 - t[2, :] = 42 # write to block row 2
337 + t[2, :] = 42 # write to chunk row 2
338
339 raw = memoryview(r.mmap_obj)
340 - # num_workers=1 → row_stride = PAGE_SIZE; block 2 starts at byte 2*PAGE_SIZE
340 + # num_workers=1 → row_stride = PAGE_SIZE; chunk 2 starts at byte 2*PAGE_SIZE
341 chunk = bytes(raw[2 * PAGE_SIZE : 3 * PAGE_SIZE])
342 assert all(b == 42 for b in chunk)
343 del raw, t
@@ -345,7 +345,7 @@ def test_create_next_worker_view_write_visible_in_raw_mmap(iid):
345
346 def test_create_next_worker_view_multi_tensor_layout(iid):
347 """Two tensors from the same worker land at consecutive byte offsets per row."""
348 - with _region(iid, num_blocks=2, cpu_page_size=2 * PAGE_SIZE) as r:
348 + with _region(iid, num_chunks=2, cpu_page_size=2 * PAGE_SIZE) as r:
349 ta = r.create_next_worker_view(PAGE_SIZE)
350 tb = r.create_next_worker_view(PAGE_SIZE)
351
@@ -353,8 +353,8 @@ def test_create_next_worker_view_multi_tensor_layout(iid):
353 tb[:, :] = 2
354
355 raw = memoryview(r.mmap_obj)
356 - for blk in range(2):
357 - row_offset = blk * 2 * PAGE_SIZE # num_workers=1
356 + for chunk in range(2):
357 + row_offset = chunk * 2 * PAGE_SIZE # num_workers=1
358 assert all(b == 1 for b in raw[row_offset : row_offset + PAGE_SIZE])
359 assert all(
360 b == 2 for b in raw[row_offset + PAGE_SIZE : row_offset + 2 * PAGE_SIZE]
@@ -366,7 +366,7 @@ def test_create_next_worker_view_multiprocess_slots(iid):
366 """Each worker process calls create_next_worker_view and writes distinct data;
367 the parent verifies each slot lands at the correct interleaved offset."""
368 num_workers = 2
369 - num_blocks = 4
369 + num_chunks = 4
370
371 ctx = get_mp_context()
372 done_queue = ctx.Queue()
@@ -375,9 +375,9 @@ def test_create_next_worker_view_multiprocess_slots(iid):
375 # Parent is rank 0 (creator); child is rank 1 (joiner).
376 region = SharedOffloadRegion(
377 engine_id=iid,
378 - num_blocks=num_blocks,
378 + num_chunks=num_chunks,
379 rank=0,
380 - kv_bytes_per_block=num_workers * PAGE_SIZE,
380 + kv_bytes_per_chunk=num_workers * PAGE_SIZE,
381 cpu_page_size=PAGE_SIZE,
382 )
383 try:
@@ -385,7 +385,7 @@ def test_create_next_worker_view_multiprocess_slots(iid):
385 target=_mp_race_construct_and_write,
386 args=(
387 iid,
388 - num_blocks,
388 + num_chunks,
389 1,
390 num_workers,
391 PAGE_SIZE,
@@ -403,12 +403,12 @@ def test_create_next_worker_view_multiprocess_slots(iid):
403 assert result["error"] is None, result["error"]
404
405 raw = memoryview(region.mmap_obj)
406 - for blk in range(num_blocks):
407 - row_start = blk * num_workers * PAGE_SIZE
406 + for chunk in range(num_chunks):
407 + row_start = chunk * num_workers * PAGE_SIZE
408 w0 = bytes(raw[row_start : row_start + PAGE_SIZE])
409 w1 = bytes(raw[row_start + PAGE_SIZE : row_start + 2 * PAGE_SIZE])
410 - assert all(b == 11 for b in w0), f"block {blk}: rank0 slot wrong"
411 - assert all(b == 22 for b in w1), f"block {blk}: rank1 slot wrong"
410 + assert all(b == 11 for b in w0), f"chunk {chunk}: rank0 slot wrong"
411 + assert all(b == 22 for b in w1), f"chunk {chunk}: rank1 slot wrong"
412
413 del raw, t0 # release before finally triggers cleanup
414 cleanup_queue.put(True)
@@ -422,8 +422,8 @@ def test_create_next_worker_view_multiprocess_slots(iid):
422 def test_create_next_worker_view_worker_isolation(iid):
423 """Writes by worker 0 must not affect worker 1's slot and vice versa."""
424 num_workers = 2
425 - num_blocks = 4
426 - with _multi_region(iid, num_workers=num_workers, num_blocks=num_blocks) as regions:
425 + num_chunks = 4
426 + with _multi_region(iid, num_workers=num_workers, num_chunks=num_chunks) as regions:
427 t0 = regions[0].create_next_worker_view(PAGE_SIZE)
428 t1 = regions[1].create_next_worker_view(PAGE_SIZE)
429
@@ -431,12 +431,12 @@ def test_create_next_worker_view_worker_isolation(iid):
431 t1[:, :] = 22
432
433 raw = memoryview(regions[0].mmap_obj)
434 - for blk in range(num_blocks):
435 - row_start = blk * num_workers * PAGE_SIZE
434 + for chunk in range(num_chunks):
435 + row_start = chunk * num_workers * PAGE_SIZE
436 w0 = bytes(raw[row_start : row_start + PAGE_SIZE])
437 w1 = bytes(raw[row_start + PAGE_SIZE : row_start + 2 * PAGE_SIZE])
438 - assert all(b == 11 for b in w0), f"block {blk}: worker0 slot corrupted"
439 - assert all(b == 22 for b in w1), f"block {blk}: worker1 slot corrupted"
438 + assert all(b == 11 for b in w0), f"chunk {chunk}: worker0 slot corrupted"
439 + assert all(b == 22 for b in w1), f"chunk {chunk}: worker1 slot corrupted"
440 del raw, t0, t1 # release before finally triggers cleanup
441
442
@@ -466,7 +466,7 @@ def test_file_exists_after_construction(iid):
466
467 def test_file_has_correct_size(iid):
468 """The mmap file size on disk must equal total_size_bytes."""
469 - with _region(iid, num_blocks=4) as r:
469 + with _region(iid, num_chunks=4) as r:
470 assert os.path.getsize(r.mmap_path) == 4 * PAGE_SIZE
471
472
@@ -491,11 +491,11 @@ def test_madvise_success_selects_madvise_population(iid, monkeypatch):
491 monkeypatch.setattr(sor, "_madvise_populate_write", _spy_madvise)
492 monkeypatch.setattr(sor, "_fallback_populate_write", _spy_fallback)
493
494 - num_blocks = 3
494 + num_chunks = 3
495 num_workers = 2
496 - with _region(iid, num_blocks=num_blocks, num_workers=num_workers, rank=1):
497 - # 1 probe (PAGESIZE) + N populate calls (one per block per worker column).
498 - expected_populate = num_blocks # ranked path: one call per block
496 + with _region(iid, num_chunks=num_chunks, num_workers=num_workers, rank=1):
497 + # 1 probe (PAGESIZE) + N populate calls (one per chunk per worker column).
498 + expected_populate = num_chunks # ranked path: one call per chunk
499 mmap_id = madvise_calls[0][2]
500 assert madvise_calls == [
501 (0, mmap.PAGESIZE, mmap_id),
@@ -510,7 +510,7 @@ def test_madvise_success_selects_madvise_population(iid, monkeypatch):
510
511
512 def test_madvise_einval_selects_fallback_for_ranked_region(iid, monkeypatch):
513 - """An EINVAL probe must select fallback for every ranked block."""
513 + """An EINVAL probe must select fallback for every ranked chunk."""
514 from vllm.v1.kv_offload.cpu import shared_offload_region as sor
515
516 fallback_calls: list[tuple[int, int]] = []
@@ -526,7 +526,7 @@ def test_madvise_einval_selects_fallback_for_ranked_region(iid, monkeypatch):
526 monkeypatch.setattr(sor, "_madvise_populate_write", _raise_einval)
527 monkeypatch.setattr(sor, "_fallback_populate_write", _spy_fallback)
528
529 - with _region(iid, num_blocks=3, num_workers=2, rank=1):
529 + with _region(iid, num_chunks=3, num_workers=2, rank=1):
530 assert fallback_calls == [
531 (mmap.PAGESIZE, mmap.PAGESIZE),
532 (3 * mmap.PAGESIZE, mmap.PAGESIZE),
@@ -551,7 +551,7 @@ def test_madvise_einval_selects_fallback_for_unranked_region(iid, monkeypatch):
551 monkeypatch.setattr(sor, "_madvise_populate_write", _raise_einval)
552 monkeypatch.setattr(sor, "_fallback_populate_write", _spy_fallback)
553
554 - with _region(iid, num_blocks=3, num_workers=2, rank=None):
554 + with _region(iid, num_chunks=3, num_workers=2, rank=None):
555 assert fallback_calls == [(0, 6 * mmap.PAGESIZE)]
556
557
@@ -651,7 +651,7 @@ def test_multiprocess_race_construct_and_write(iid):
651 """N processes race to construct the same SharedOffloadRegion, each writes
652 fill_value = rank+1 into their slot; parent verifies interleaved layout."""
653 num_workers = 4
654 - num_blocks = 3
654 + num_chunks = 3
655
656 ctx = get_mp_context()
657 done_queue = ctx.Queue()
@@ -662,7 +662,7 @@ def test_multiprocess_race_construct_and_write(iid):
662 target=_mp_race_construct_and_write,
663 args=(
664 iid,
665 - num_blocks,
665 + num_chunks,
666 rank,
667 num_workers,
668 PAGE_SIZE,
@@ -689,13 +689,13 @@ def test_multiprocess_race_construct_and_write(iid):
689 with open(mmap_path, "rb") as f:
690 raw = f.read()
691
692 - for blk in range(num_blocks):
692 + for chunk in range(num_chunks):
693 for w in range(num_workers):
694 - slot_start = (blk * num_workers + w) * PAGE_SIZE
694 + slot_start = (chunk * num_workers + w) * PAGE_SIZE
695 slot = raw[slot_start : slot_start + PAGE_SIZE]
696 expected = w + 1 # fill_value = rank + 1
697 assert all(b == expected for b in slot), (
698 - f"block {blk}, worker {w}: expected {expected} but got wrong bytes"
698 + f"chunk {chunk}, worker {w}: expected {expected} but got wrong bytes"
699 )
700
701 # Unblock all workers to clean up.
@@ -839,9 +839,9 @@ def test_insufficient_space_raises_clear_error(monkeypatch):
839 with pytest.raises(RuntimeError, match="Insufficient space"):
840 SharedOffloadRegion(
841 engine_id=engine_id,
842 - num_blocks=4,
842 + num_chunks=4,
843 rank=0,
844 - kv_bytes_per_block=PAGE_SIZE,
844 + kv_bytes_per_chunk=PAGE_SIZE,
845 cpu_page_size=PAGE_SIZE,
846 )
847
@@ -870,9 +870,9 @@ def test_ftruncate_failure_cleans_up_creator(monkeypatch):
870 with pytest.raises(OSError, match="ftruncate failed"):
871 SharedOffloadRegion(
872 engine_id=engine_id,
873 - num_blocks=4,
873 + num_chunks=4,
874 rank=0,
875 - kv_bytes_per_block=PAGE_SIZE,
875 + kv_bytes_per_chunk=PAGE_SIZE,
876 cpu_page_size=PAGE_SIZE,
877 )
878
tests/v1/kv_offload/test_factory.py
+11 -11
@@ -136,7 +136,7 @@ def test_get_spec_cls_defaults_to_cpu():
136 def test_create_cpu_offloading_spec():
137 spec = _create_spec()
138 assert isinstance(spec, CPUOffloadingSpec)
139 - assert spec.num_blocks > 0
139 + assert spec.num_chunks > 0
140
141
142 def test_cpu_spec_sizes_normalized_worker_layout():
@@ -156,7 +156,7 @@ def test_cpu_spec_sizes_normalized_worker_layout():
156 assert isinstance(spec, CPUOffloadingSpec)
157 assert spec.cpu_page_size_per_worker == 32
158 assert spec.kv_bytes_per_chunk == alignment
159 - assert spec.num_blocks == 3
159 + assert spec.num_chunks == 3
160
161
162 def test_cpu_spec_zero_worker_bytes_produces_empty_cache():
@@ -165,7 +165,7 @@ def test_cpu_spec_zero_worker_bytes_produces_empty_cache():
165 assert isinstance(spec, CPUOffloadingSpec)
166 assert spec.cpu_page_size_per_worker == 0
167 assert spec.kv_bytes_per_chunk == 0
168 - assert spec.num_blocks == 0
168 + assert spec.num_chunks == 0
169
170
171 def test_tiering_spec_aligns_row_size():
@@ -183,7 +183,7 @@ def test_tiering_spec_aligns_row_size():
183 assert isinstance(spec, TieringOffloadingSpec)
184 assert spec.cpu_page_size_per_worker == 32
185 assert spec.kv_bytes_per_chunk == alignment
186 - assert spec.num_blocks == 3
186 + assert spec.num_chunks == 3
187
188
189 @pytest.mark.parametrize("world_size", [2, 4, 8])
@@ -201,7 +201,7 @@ def test_tiering_spec_replicated_sizing_removes_world_factor(world_size: int):
201 assert spec.replicated_layout is True
202 assert spec.cpu_page_size_per_worker == worker_kv_bytes_per_block
203 assert spec.kv_bytes_per_chunk == worker_kv_bytes_per_block
204 - assert spec.num_blocks == 8
204 + assert spec.num_chunks == 8
205
206
207 def test_tiering_spec_create_worker_uses_single_slot_for_replicated_layout(monkeypatch):
@@ -239,7 +239,7 @@ def test_tiering_spec_create_worker_uses_single_slot_for_replicated_layout(monke
239 spec.create_worker(kv_caches)
240
241 assert region_calls[0]["rank"] == 0
242 - assert region_calls[0]["kv_bytes_per_block"] == worker_kv_bytes_per_block
242 + assert region_calls[0]["kv_bytes_per_chunk"] == worker_kv_bytes_per_block
243 assert worker_calls[0]["kv_caches"] is kv_caches
244 assert worker_calls[0]["mmap_region"] is region
245
@@ -292,7 +292,7 @@ def test_cpu_spec_replicated_sizing_on_shared_region(monkeypatch, world_size: in
292 assert spec.replicated_layout is True
293 assert spec.cpu_page_size_per_worker == worker_kv_bytes_per_block
294 assert spec.kv_bytes_per_chunk == worker_kv_bytes_per_block
295 - assert spec.num_blocks == 8
295 + assert spec.num_chunks == 8
296
297
298 @pytest.mark.parametrize("world_size", [2, 4, 8])
@@ -320,7 +320,7 @@ def test_cpu_spec_replicated_disabled_without_shared_region(
320 assert spec.replicated_layout is False
321 assert spec.cpu_page_size_per_worker == worker_kv_bytes_per_block
322 assert spec.kv_bytes_per_chunk == worker_kv_bytes_per_block * world_size
323 - assert spec.num_blocks == 2
323 + assert spec.num_chunks == 2
324
325
326 @pytest.mark.parametrize("config_replicated", [True, False])
@@ -381,7 +381,7 @@ def test_cpu_spec_create_worker_uses_mmap_on_cuda_alike(monkeypatch):
381 spec.create_worker(kv_caches)
382
383 assert region_calls[0]["engine_id"] == "test-engine"
384 - assert region_calls[0]["kv_bytes_per_block"] == worker_kv_bytes_per_block * 4
384 + assert region_calls[0]["kv_bytes_per_chunk"] == worker_kv_bytes_per_block * 4
385 assert worker_calls[0]["kv_caches"] is kv_caches
386 assert worker_calls[0]["mmap_region"] is region
387
@@ -419,11 +419,11 @@ def test_cpu_spec_create_worker_uses_tensor_path_off_cuda_alike(monkeypatch):
419 def test_cpu_spec_create_worker_skips_mmap_for_empty_cache(monkeypatch):
420 import vllm.v1.kv_offload.cpu.spec as cpu_spec_module
421
422 - # worker_kv_bytes_per_block=0 yields num_blocks=0; a zero-byte region cannot
422 + # worker_kv_bytes_per_block=0 yields num_chunks=0; a zero-byte region cannot
423 # be mmap'd, so even on CUDA-alike this must fall back to the tensor path.
424 spec = _create_spec(worker_kv_bytes_per_block=0, world_size=4)
425 assert isinstance(spec, CPUOffloadingSpec)
426 - assert spec.num_blocks == 0
426 + assert spec.num_chunks == 0
427
428 region_calls: list[dict[str, Any]] = []
429 worker_calls: list[dict[str, Any]] = []
tests/v1/kv_offload/tiering/p2p/test_manager.py
+17 -17
@@ -91,17 +91,17 @@ def _req_context(kv_params: dict | None = None) -> ReqContext:
91 def _job_metadata(
92 job_id: int,
93 keys: list[bytes] | None = None,
94 - block_ids: list[int] | None = None,
94 + chunk_ids: list[int] | None = None,
95 kv_params: dict | None = None,
96 ) -> TransferJob:
97 if keys is None:
98 keys = [b"key1"]
99 - if block_ids is None:
100 - block_ids = list(range(len(keys)))
99 + if chunk_ids is None:
100 + chunk_ids = list(range(len(keys)))
101 return TransferJob(
102 job_id=job_id,
103 keys=keys,
104 - block_ids=np.array(block_ids),
104 + chunk_ids=np.array(chunk_ids),
105 is_promotion=False,
106 req_context=_req_context(kv_params),
107 )
@@ -254,9 +254,9 @@ class TestLookup:
254 "kv_params,expected",
255 [
256 (_remote_decoder_kv_params(), OffloadPolicy.REQUEST_LEVEL),
257 - (None, OffloadPolicy.BLOCK_LEVEL),
258 - (_remote_prefiller_kv_params(), OffloadPolicy.BLOCK_LEVEL),
259 - ({"remote_decoder": {}}, OffloadPolicy.BLOCK_LEVEL),
257 + (None, OffloadPolicy.CHUNK_LEVEL),
258 + (_remote_prefiller_kv_params(), OffloadPolicy.CHUNK_LEVEL),
259 + ({"remote_decoder": {}}, OffloadPolicy.CHUNK_LEVEL),
260 ],
261 ids=["producer", "plain", "consumer", "producer_no_id"],
262 )
@@ -366,7 +366,7 @@ class TestSubmitStore:
366 job = _job_metadata(
367 job_id=1,
368 keys=[b"k1", b"k2"],
369 - block_ids=[3, 4],
369 + chunk_ids=[3, 4],
370 kv_params=_remote_decoder_kv_params(kv_request_id="req-1"),
371 )
372 mgr.submit_store(job)
@@ -397,7 +397,7 @@ class TestSubmitStore:
397 job = _job_metadata(
398 job_id=7,
399 keys=[b"k1", b"k2"],
400 - block_ids=[3, 4],
400 + chunk_ids=[3, 4],
401 kv_params=_remote_decoder_kv_params(kv_request_id="req-1"),
402 )
403 mgr.submit_store(job)
@@ -440,7 +440,7 @@ class TestSubmitLoad:
440 """Empty key list succeeds immediately."""
441 mgr = _make_manager()
442 job = _job_metadata(
443 - job_id=1, keys=[], block_ids=[], kv_params=_remote_prefiller_kv_params()
443 + job_id=1, keys=[], chunk_ids=[], kv_params=_remote_prefiller_kv_params()
444 )
445 mgr.submit_load(job)
446 assert mgr._finished_jobs == [JobResult(job_id=1, success=True)]
@@ -463,7 +463,7 @@ class TestSubmitLoad:
463 job = _job_metadata(
464 job_id=42,
465 keys=[b"k1", b"k2"],
466 - block_ids=[5, 6],
466 + chunk_ids=[5, 6],
467 kv_params=_remote_prefiller_kv_params(kv_request_id="req-42"),
468 )
469 mgr.submit_load(job)
@@ -1155,7 +1155,7 @@ class TestBidirectionalManager:
1155 _job_metadata(
1156 job_id=100,
1157 keys=[b"a-block"],
1158 - block_ids=[0],
1158 + chunk_ids=[0],
1159 kv_params=a_prefiller_params,
1160 )
1161 )
@@ -1163,7 +1163,7 @@ class TestBidirectionalManager:
1163 _job_metadata(
1164 job_id=200,
1165 keys=[b"b-block"],
1166 - block_ids=[0],
1166 + chunk_ids=[0],
1167 kv_params=b_prefiller_params,
1168 )
1169 )
@@ -1173,7 +1173,7 @@ class TestBidirectionalManager:
1173 _job_metadata(
1174 job_id=101,
1175 keys=[b"b-block"],
1176 - block_ids=[0],
1176 + chunk_ids=[0],
1177 kv_params=a_decoder_params,
1178 )
1179 )
@@ -1181,7 +1181,7 @@ class TestBidirectionalManager:
1181 _job_metadata(
1182 job_id=201,
1183 keys=[b"a-block"],
1184 - block_ids=[0],
1184 + chunk_ids=[0],
1185 kv_params=b_decoder_params,
1186 )
1187 )
@@ -1573,7 +1573,7 @@ class TestConnectionDeathMidTransfer:
1573 _job_metadata(
1574 job_id=900,
1575 keys=[b"a-block"],
1576 - block_ids=[0],
1576 + chunk_ids=[0],
1577 kv_params=a_prefiller_params,
1578 )
1579 )
@@ -1581,7 +1581,7 @@ class TestConnectionDeathMidTransfer:
1581 _job_metadata(
1582 job_id=901,
1583 keys=[b"b-block"],
1584 - block_ids=[0],
1584 + chunk_ids=[0],
1585 kv_params=a_decoder_params,
1586 )
1587 )
tests/v1/kv_offload/tiering/p2p/test_sessions.py
+2 -2
@@ -259,13 +259,13 @@ class FakeParent:
259 ) -> TransferJob:
260 keys_list = list(keys)
261 self.calls.append(("create_store_job", tuple(keys_list), ctx.req_id))
262 - block_ids = np.array([self.stored[k] for k in keys_list], dtype=np.int32)
262 + chunk_ids = np.array([self.stored[k] for k in keys_list], dtype=np.int32)
263 job_id = self._next_job_id
264 self._next_job_id += 1
265 return TransferJob(
266 job_id=job_id,
267 keys=keys_list,
268 - block_ids=block_ids,
268 + chunk_ids=chunk_ids,
269 is_promotion=False,
270 req_context=ctx,
271 )
tests/v1/kv_offload/tiering/test_fs_tier.py
+13 -13
@@ -105,15 +105,15 @@ def key(n: int) -> OffloadKey:
105 def make_job(
106 job_id: int,
107 keys: list[OffloadKey],
108 - block_ids: list[int] | None = None,
108 + chunk_ids: list[int] | None = None,
109 is_promotion: bool = False,
110 ) -> TransferJob:
111 - if block_ids is None:
112 - block_ids = list(range(len(keys)))
111 + if chunk_ids is None:
112 + chunk_ids = list(range(len(keys)))
113 return TransferJob(
114 job_id=job_id,
115 keys=keys,
116 - block_ids=np.array(block_ids, dtype=np.int64),
116 + chunk_ids=np.array(chunk_ids, dtype=np.int64),
117 is_promotion=is_promotion,
118 req_context=_CTX,
119 )
@@ -382,11 +382,11 @@ def test_store_load_data_integrity(fs_tier, monkeypatch, use_c_ext, batch_size):
382 tensor[:] = _page_aligned_rand_tensor(_NUM_BLOCKS, _BLOCK_ELEMENTS)
383
384 keys = [key(i) for i in range(batch_size)]
385 - store_block_ids = list(range(batch_size))
386 - load_block_ids = list(range(_NUM_BLOCKS - batch_size, _NUM_BLOCKS))
385 + store_chunk_ids = list(range(batch_size))
386 + load_chunk_ids = list(range(_NUM_BLOCKS - batch_size, _NUM_BLOCKS))
387 expected = tensor[:batch_size].clone()
388
389 - tier.submit_store(make_job(1, keys, store_block_ids))
389 + tier.submit_store(make_job(1, keys, store_chunk_ids))
390 store_results = drain(tier)
391 assert len(store_results) == 1
392 assert store_results[0].success
@@ -396,15 +396,15 @@ def test_store_load_data_integrity(fs_tier, monkeypatch, use_c_ext, batch_size):
396 tensor[:] = 0.0
397
398 # Load into a range disjoint by index from the store ids, to also
399 - # exercise loading a block into a different id than it was stored from.
400 - tier.submit_load(make_job(2, keys, load_block_ids, is_promotion=True))
399 + # exercise loading a chunk into a different id than it was stored from.
400 + tier.submit_load(make_job(2, keys, load_chunk_ids, is_promotion=True))
401 load_results = drain(tier)
402 assert len(load_results) == 1
403 assert load_results[0].success
404
405 - for i, bid in enumerate(load_block_ids):
406 - assert torch.allclose(tensor[bid], expected[i]), (
407 - f"Block {bid} data mismatch after store+load"
405 + for i, cid in enumerate(load_chunk_ids):
406 + assert torch.allclose(tensor[cid], expected[i]), (
407 + f"Chunk {cid} data mismatch after store+load"
408 )
409
410
@@ -895,7 +895,7 @@ def test_cascade_store_emits_fs_event_through_tiering_manager(tmp_path):
895 view = memoryview(tensor.numpy())
896 mock_region = MagicMock()
897 mock_region.create_kv_memoryview.return_value = view
898 - primary = CPUPrimaryTierOffloadingManager(num_blocks=4, mmap_region=mock_region)
898 + primary = CPUPrimaryTierOffloadingManager(num_chunks=4, mmap_region=mock_region)
899 tier = FileSystemTierManager(
900 offloading_spec=_make_offloading_spec(enable_kv_cache_events=True),
901 primary_kv_view=primary.get_kv_memoryview(),
tests/v1/kv_offload/tiering/test_metrics.py
+22 -22
@@ -25,8 +25,8 @@ def to_keys(int_ids: Iterable[int]):
25 def test_tiering_metrics_tracker_records_lookup_metrics():
26 tracker = TieringMetricsTracker(
27 tier_types=["fs", "p2p"],
28 - num_primary_blocks=5,
29 - primary_block_size=16,
28 + num_primary_chunks=5,
29 + primary_chunk_size=16,
30 )
31 tracker.on_new_request(_CTX)
32
@@ -45,18 +45,18 @@ def test_tiering_metrics_tracker_records_lookup_metrics():
45 stats = tracker.take_stats()
46 assert stats is not None
47 values = stats.data["data"]
48 - assert values[TieringOffloadingMetrics.BLOCK_QUERIES][("0:primary",)] == 1
49 - assert values[TieringOffloadingMetrics.BLOCK_QUERIES][("1:fs",)] == 1
50 - assert values[TieringOffloadingMetrics.BLOCK_QUERIES][("2:p2p",)] == 1
51 - assert values[TieringOffloadingMetrics.BLOCK_HITS][("2:p2p",)] == 1
52 - assert ("1:fs",) not in values[TieringOffloadingMetrics.BLOCK_HITS]
48 + assert values[TieringOffloadingMetrics.CHUNK_QUERIES][("0:primary",)] == 1
49 + assert values[TieringOffloadingMetrics.CHUNK_QUERIES][("1:fs",)] == 1
50 + assert values[TieringOffloadingMetrics.CHUNK_QUERIES][("2:p2p",)] == 1
51 + assert values[TieringOffloadingMetrics.CHUNK_HITS][("2:p2p",)] == 1
52 + assert ("1:fs",) not in values[TieringOffloadingMetrics.CHUNK_HITS]
53
54
55 def test_tiering_metrics_tracker_stops_lookup_metrics_after_allocation():
56 tracker = TieringMetricsTracker(
57 tier_types=["fs"],
58 - num_primary_blocks=5,
59 - primary_block_size=16,
58 + num_primary_chunks=5,
59 + primary_chunk_size=16,
60 )
61 tracker.on_new_request(_CTX)
62 key = to_keys([1])[0]
@@ -73,9 +73,9 @@ def test_tiering_metrics_tracker_stops_lookup_metrics_after_allocation():
73 stats = tracker.take_stats()
74 assert stats is not None
75 values = stats.data["data"]
76 - assert values[TieringOffloadingMetrics.BLOCK_QUERIES][("0:primary",)] == 1
77 - assert values[TieringOffloadingMetrics.BLOCK_QUERIES][("1:fs",)] == 1
78 - assert values[TieringOffloadingMetrics.BLOCK_HITS][("1:fs",)] == 1
76 + assert values[TieringOffloadingMetrics.CHUNK_QUERIES][("0:primary",)] == 1
77 + assert values[TieringOffloadingMetrics.CHUNK_QUERIES][("1:fs",)] == 1
78 + assert values[TieringOffloadingMetrics.CHUNK_HITS][("1:fs",)] == 1
79
80 tracker.on_request_allocated(_CTX)
81 tracker.on_lookup(
@@ -90,15 +90,15 @@ def test_tiering_metrics_tracker_stops_lookup_metrics_after_allocation():
90 stats = tracker.take_stats()
91 assert stats is not None
92 values = stats.data["data"]
93 - assert TieringOffloadingMetrics.BLOCK_QUERIES not in values
94 - assert TieringOffloadingMetrics.BLOCK_HITS not in values
93 + assert TieringOffloadingMetrics.CHUNK_QUERIES not in values
94 + assert TieringOffloadingMetrics.CHUNK_HITS not in values
95
96
97 def test_tiering_metrics_tracker_records_finished_job_metrics():
98 tracker = TieringMetricsTracker(
99 tier_types=["fs"],
100 - num_primary_blocks=5,
101 - primary_block_size=16,
100 + num_primary_chunks=5,
101 + primary_chunk_size=16,
102 )
103 cascade_key, promotion_key, failed_cascade_key, failed_promotion_key = to_keys(
104 range(4)
@@ -149,8 +149,8 @@ def test_tiering_metrics_tracker_records_finished_job_metrics():
149 def test_tiering_metrics_tracker_records_partial_promotion_success_bytes():
150 tracker = TieringMetricsTracker(
151 tier_types=["fs"],
152 - num_primary_blocks=5,
153 - primary_block_size=16,
152 + num_primary_chunks=5,
153 + primary_chunk_size=16,
154 )
155 keys = to_keys(range(3))
156 job = JobMetadata(
@@ -182,8 +182,8 @@ def test_tiering_metrics_tracker_records_partial_promotion_success_bytes():
182 def test_tiering_metrics_tracker_reports_active_job_and_primary_usage_gauges():
183 tracker = TieringMetricsTracker(
184 tier_types=["fs", "p2p"],
185 - num_primary_blocks=6,
186 - primary_block_size=16,
185 + num_primary_chunks=6,
186 + primary_chunk_size=16,
187 )
188 fs_job = JobMetadata(
189 TransferJob(0, to_keys([0, 1]), np.array([0, 1]), False, _CTX),
@@ -214,8 +214,8 @@ def test_tiering_metrics_tracker_reports_active_job_and_primary_usage_gauges():
214 def test_tiering_metrics_tracker_records_promotion_allocation_failures():
215 tracker = TieringMetricsTracker(
216 tier_types=["fs"],
217 - num_primary_blocks=1,
218 - primary_block_size=16,
217 + num_primary_chunks=1,
218 + primary_chunk_size=16,
219 )
220
221 tracker.on_promotion_allocation_failure()
tests/v1/kv_offload/tiering/test_obj_tier.py
+5 -5
@@ -106,14 +106,14 @@ def key(n: int) -> OffloadKey:
106 def make_job(
107 job_id: int,
108 keys: list[OffloadKey],
109 - block_ids: list[int] | None = None,
109 + chunk_ids: list[int] | None = None,
110 ) -> TransferJob:
111 - if block_ids is None:
112 - block_ids = list(range(len(keys)))
111 + if chunk_ids is None:
112 + chunk_ids = list(range(len(keys)))
113 return TransferJob(
114 job_id=job_id,
115 keys=keys,
116 - block_ids=np.array(block_ids, dtype=np.int32),
116 + chunk_ids=np.array(chunk_ids, dtype=np.int32),
117 is_promotion=False,
118 req_context=_CTX,
119 )
@@ -541,7 +541,7 @@ class TestMockObjTierFailures:
541 mmap_region = MagicMock()
542 mmap_region.create_kv_memoryview.return_value = primary_kv_view
543 primary_tier = CPUPrimaryTierOffloadingManager(
544 - num_blocks=num_blocks, mmap_region=mmap_region
544 + num_chunks=num_blocks, mmap_region=mmap_region
545 )
546 obj_tier, agent = _make_tier(
547 num_blocks=num_blocks, primary_kv_view=primary_kv_view
tests/v1/kv_offload/tiering/test_tiering_offloading.py
+204 -203
@@ -5,15 +5,16 @@ Unit tests for TieringOffloadingManager and ExampleSecondaryTierManager.
5
6 These tests verify:
7 1. Basic tiered offloading operations (store, load, lookup)
8 -2. Cascade behavior (blocks stored to all secondary tiers)
9 -3. Promotion behavior (blocks loaded from secondary to primary to GPU)
10 -4. ref_cnt management (blocks protected during async transfers)
8 +2. Cascade behavior (chunks stored to all secondary tiers)
9 +3. Promotion behavior (chunks loaded from secondary to primary to GPU)
10 +4. ref_cnt management (chunks protected during async transfers)
11 5. Eviction coordination between tiers
12 """
13
14 from collections.abc import Iterable
15 from unittest.mock import MagicMock
16
17 +import numpy as np
18 import pytest
19 import torch
20
@@ -57,10 +58,10 @@ _CTX = ReqContext(req_id="test")
58 _MOCK_OFFLOADING_SPEC = MagicMock()
59
60
60 -def _mock_mmap_region(num_blocks: int, row_bytes: int = 16):
61 +def _mock_mmap_region(num_chunks: int, row_bytes: int = 16):
62 """Create a mock SharedOffloadRegion for testing."""
63 mock = MagicMock()
63 - view = memoryview(torch.zeros((num_blocks, row_bytes), dtype=torch.int8).numpy())
64 + view = memoryview(torch.zeros((num_chunks, row_bytes), dtype=torch.int8).numpy())
65 mock.create_kv_memoryview.return_value = view
66 return mock
67
@@ -160,7 +161,7 @@ def test_tiering_spec_collects_secondary_metric_definitions(monkeypatch):
161 metadata = metrics[MetricsSecondaryTierManager.MY_TIER_METRIC]
162 assert metadata.documentation == "Number of bytes served by the test tier."
163 assert metadata.labelnames == ("tier",)
163 - assert metrics[TieringOffloadingMetrics.BLOCK_QUERIES].labelnames == ("tier",)
164 + assert metrics[TieringOffloadingMetrics.CHUNK_QUERIES].labelnames == ("tier",)
165 assert metrics[TieringOffloadingMetrics.LOOKUP_SYNC_DELAY].labelnames == ("tier",)
166 assert metrics[TieringOffloadingMetrics.LOOKUP_ASYNC_DELAY].labelnames == ("tier",)
167 assert metrics[TieringOffloadingMetrics.PROMOTION_JOB_FAILURES].labelnames == (
@@ -183,7 +184,7 @@ def test_tiering_spec_collects_secondary_metric_definitions(monkeypatch):
184 def test_tiering_manager_aggregates_secondary_stats():
185 mock_region = _mock_mmap_region(5)
186 primary_tier = CPUPrimaryTierOffloadingManager(
186 - num_blocks=5, mmap_region=mock_region
187 + num_chunks=5, mmap_region=mock_region
188 )
189 secondary_tier = MetricsSecondaryTierManager(
190 offloading_spec=_MOCK_OFFLOADING_SPEC,
@@ -232,19 +233,19 @@ class TestExampleSecondaryTierManager:
233 )
234
235 # Initially empty
235 - blocks = to_keys(range(3))
236 - assert tier.lookup(blocks[0], _CTX) is LookupResult.MISS
236 + chunks = to_keys(range(3))
237 + assert tier.lookup(chunks[0], _CTX) is LookupResult.MISS
238
238 - # Store blocks (simulate with direct insertion for testing)
239 - tier.blocks[blocks[0]] = True
240 - tier.blocks[blocks[1]] = True
239 + # Store chunks (simulate with direct insertion for testing)
240 + tier.chunks[chunks[0]] = True
241 + tier.chunks[chunks[1]] = True
242
242 - # Lookup should find first two blocks
243 - assert tier.lookup(blocks[0], _CTX) is LookupResult.HIT
244 - assert tier.lookup(blocks[1], _CTX) is LookupResult.HIT
243 + # Lookup should find first two chunks
244 + assert tier.lookup(chunks[0], _CTX) is LookupResult.HIT
245 + assert tier.lookup(chunks[1], _CTX) is LookupResult.HIT
246
246 - # Third block not present
247 - assert tier.lookup(blocks[2], _CTX) is LookupResult.MISS
247 + # Third chunk not present
248 + assert tier.lookup(chunks[2], _CTX) is LookupResult.MISS
249
250
251 # What a request-level cascade does with a key already present in the primary
@@ -260,7 +261,7 @@ _CASCADE_SUPPLY_BEHAVIOR = {
261 # The same test, before it lands.
262 LookupResult.HIT_PENDING: "defer",
263 # test_cascade_drops_deferred_keys_whose_primary_write_failed, where the
263 - # failed write frees the block.
264 + # failed write frees the chunk.
265 LookupResult.MISS: "drop",
266 # test_cascade_rejects_retry_from_primary. The primary tier resolves every
267 # key it holds, so RETRY cannot reach the cascade, and parking on it would
@@ -284,7 +285,7 @@ class TestTieringOffloadingManager:
285 # Create primary tier (CPU-based)
286 mock_region = _mock_mmap_region(5)
287 self.primary_tier = CPUPrimaryTierOffloadingManager(
287 - num_blocks=5, mmap_region=mock_region
288 + num_chunks=5, mmap_region=mock_region
289 )
290
291 mock_view = mock_region.create_kv_memoryview()
@@ -331,7 +332,7 @@ class TestTieringOffloadingManager:
332 TransferJob(
333 job_id=job_id,
334 keys=to_keys([1, 2]),
334 - block_ids=[0, 1],
335 + chunk_ids=np.array([0, 1], dtype=np.int64),
336 is_promotion=True,
337 req_context=_CTX,
338 ),
@@ -358,7 +359,7 @@ class TestTieringOffloadingManager:
359 TransferJob(
360 job_id=job_id,
361 keys=to_keys([1]),
361 - block_ids=[0],
362 + chunk_ids=np.array([0], dtype=np.int64),
363 is_promotion=True,
364 req_context=_CTX,
365 ),
@@ -392,23 +393,23 @@ class TestTieringOffloadingManager:
393
394 def test_basic_store_to_primary(self, manager_setup):
395 """Test basic store operation to primary tier."""
395 - blocks = to_keys(range(3))
396 + chunks = to_keys(range(3))
397
398 # Prepare store
399 self._start_request()
399 - result = self.manager.prepare_store(blocks, _CTX)
400 + result = self.manager.prepare_store(chunks, _CTX)
401 assert result is not None
402 assert len(result.keys_to_store) == 3
403
404 # Complete store
404 - self.manager.complete_store(blocks, _CTX, success=True)
405 + self.manager.complete_store(chunks, _CTX, success=True)
406
406 - # Blocks should be in primary tier
407 - assert count_hits(self.primary_tier, blocks) == 3
407 + # Chunks should be in primary tier
408 + assert count_hits(self.primary_tier, chunks) == 3
409
410 def test_cascade_to_all_secondary_tiers(self, manager_setup):
410 - """Test that blocks are cascaded to ALL secondary tiers."""
411 - blocks = to_keys(range(3))
411 + """Test that chunks are cascaded to ALL secondary tiers."""
412 + chunks = to_keys(range(3))
413
414 self.secondary_tier1.submit_store = MagicMock(
415 wraps=self.secondary_tier1.submit_store
@@ -419,44 +420,44 @@ class TestTieringOffloadingManager:
420
421 # Store to primary
422 self._start_request()
422 - result = self.manager.prepare_store(blocks, _CTX)
423 + result = self.manager.prepare_store(chunks, _CTX)
424 assert result is not None
425
426 # Complete store (triggers cascade via submit_store on each tier)
426 - self.manager.complete_store(blocks, _CTX, success=True)
427 + self.manager.complete_store(chunks, _CTX, success=True)
428
429 # submit_store was called once per secondary tier
430 self.secondary_tier1.submit_store.assert_called_once()
431 self.secondary_tier2.submit_store.assert_called_once()
432
432 - # Blocks should be in both secondary tiers
433 - assert self.secondary_tier1.get_num_blocks() == 3
434 - assert self.secondary_tier2.get_num_blocks() == 3
433 + # Chunks should be in both secondary tiers
434 + assert self.secondary_tier1.get_num_chunks() == 3
435 + assert self.secondary_tier2.get_num_chunks() == 3
436
436 - # Verify blocks are present
437 + # Verify chunks are present
438 assert all(
438 - self.secondary_tier1.lookup(b, _CTX) is LookupResult.HIT for b in blocks
439 + self.secondary_tier1.lookup(c, _CTX) is LookupResult.HIT for c in chunks
440 )
441 assert all(
441 - self.secondary_tier2.lookup(b, _CTX) is LookupResult.HIT for b in blocks
442 + self.secondary_tier2.lookup(c, _CTX) is LookupResult.HIT for c in chunks
443 )
444
445 def test_ref_cnt_protection_during_cascade(self, manager_setup):
445 - """Test that ref_cnt protects blocks during cascade."""
446 - blocks = to_keys(range(3))
446 + """Test that ref_cnt protects chunks during cascade."""
447 + chunks = to_keys(range(3))
448
449 # Store to primary
450 self._start_request()
450 - result = self.manager.prepare_store(blocks, _CTX)
451 + result = self.manager.prepare_store(chunks, _CTX)
452 assert result is not None
452 - self.manager.complete_store(blocks, _CTX, success=True)
453 + self.manager.complete_store(chunks, _CTX, success=True)
454
454 - # After complete_store, blocks should have ref_cnt > 0
455 + # After complete_store, chunks should have ref_cnt > 0
456 # (one for each secondary tier)
456 - for block_hash in blocks:
457 - block = self.primary_tier._policy.get(block_hash)
457 + for key in chunks:
458 + chunk = self.primary_tier._policy.get(key)
459 # ref_cnt should be 2 (one for each secondary tier)
459 - assert block.ref_cnt == 2
460 + assert chunk.ref_cnt == 2
461
462 # End of step 1: _maybe_process_finished_jobs() was already called by
463 # prepare_store() above (setting the per-step flag), so on_schedule_end()
@@ -466,9 +467,9 @@ class TestTieringOffloadingManager:
467
468 # ref_cnt still held: cascade jobs finished (sync tier) but haven't
469 # been polled yet because the per-step guard skipped the second call.
469 - for block_hash in blocks:
470 - block = self.primary_tier._policy.get(block_hash)
471 - assert block.ref_cnt == 2
470 + for key in chunks:
471 + chunk = self.primary_tier._policy.get(key)
472 + assert chunk.ref_cnt == 2
473
474 # Secondary tiers have completed jobs waiting to be drained
475 assert len(self.secondary_tier1.completed_jobs) > 0
@@ -479,37 +480,37 @@ class TestTieringOffloadingManager:
480 self._simulate_on_schedule_end()
481
482 # After cascade completes, ref_cnt should be 0
482 - for block_hash in blocks:
483 - block = self.primary_tier._policy.get(block_hash)
484 - assert block.ref_cnt == 0
483 + for key in chunks:
484 + chunk = self.primary_tier._policy.get(key)
485 + assert chunk.ref_cnt == 0
486
487 # All completed jobs have been drained
488 assert len(self.secondary_tier1.completed_jobs) == 0
489 assert len(self.secondary_tier2.completed_jobs) == 0
490
491 def test_lookup_from_primary(self, manager_setup):
491 - """Test lookup when blocks are in primary tier."""
492 - blocks = to_keys(range(3))
492 + """Test lookup when chunks are in primary tier."""
493 + chunks = to_keys(range(3))
494
494 - # Store blocks
495 + # Store chunks
496 self._start_request()
496 - self.manager.prepare_store(blocks, _CTX)
497 - self.manager.complete_store(blocks, _CTX, success=True)
497 + self.manager.prepare_store(chunks, _CTX)
498 + self.manager.complete_store(chunks, _CTX, success=True)
499
499 - # Lookup should find all blocks in primary
500 - assert count_hits(self.manager, blocks) == 3
500 + # Lookup should find all chunks in primary
501 + assert count_hits(self.manager, chunks) == 3
502
503 def test_promotion_from_secondary(self, manager_setup):
503 - """Test promotion of blocks from secondary to primary tier."""
504 - blocks = to_keys(range(3))
504 + """Test promotion of chunks from secondary to primary tier."""
505 + chunks = to_keys(range(3))
506
506 - # Manually add blocks to secondary tier (simulate previous cascade)
507 - for block in blocks:
508 - self.secondary_tier1.blocks[block] = True
507 + # Manually add chunks to secondary tier (simulate previous cascade)
508 + for chunk in chunks:
509 + self.secondary_tier1.chunks[chunk] = True
510
510 - # Lookup each block to initiate promotion for all of them
511 - for block in blocks:
512 - result = self.manager.lookup(block, _CTX)
511 + # Lookup each chunk to initiate promotion for all of them
512 + for chunk in chunks:
513 + result = self.manager.lookup(chunk, _CTX)
514 assert result is LookupResult.HIT_PENDING # promotion initiated
515
516 # End of step 1: flushes deferred submit_load() calls
@@ -518,11 +519,11 @@ class TestTieringOffloadingManager:
519 # End of step 2: processes the completed promotion jobs
520 self._simulate_on_schedule_end()
521
521 - # Now blocks should be in primary tier
522 - assert count_hits(self.primary_tier, blocks) == 3
522 + # Now chunks should be in primary tier
523 + assert count_hits(self.primary_tier, chunks) == 3
524
525 # Next lookup should succeed
525 - assert count_hits(self.manager, blocks) == 3
526 + assert count_hits(self.manager, chunks) == 3
527
528 @pytest.mark.parametrize(
529 ("successful_indices", "expected_results"),
@@ -538,18 +539,18 @@ class TestTieringOffloadingManager:
539 ],
540 ids=["partial", "legacy-full-failure"],
541 )
541 - def test_failed_promotion_keeps_only_successful_blocks(
542 + def test_failed_promotion_keeps_only_successful_chunks(
543 self, manager_setup, successful_indices, expected_results
544 ):
544 - blocks = to_keys(range(3))
545 - for block in blocks:
546 - self.secondary_tier1.blocks[block] = True
545 + chunks = to_keys(range(3))
546 + for chunk in chunks:
547 + self.secondary_tier1.chunks[chunk] = True
548
549 def submit_partial(job_metadata: TransferJob) -> None:
550 successful_keys = (
551 None
552 if successful_indices is None
552 - else tuple(blocks[i] for i in successful_indices)
553 + else tuple(chunks[i] for i in successful_indices)
554 )
555 self.secondary_tier1.completed_jobs.append(
556 JobResult(
@@ -561,24 +562,24 @@ class TestTieringOffloadingManager:
562
563 self.secondary_tier1.submit_load = submit_partial
564
564 - for block in blocks:
565 - assert self.manager.lookup(block, _CTX) is LookupResult.HIT_PENDING
565 + for chunk in chunks:
566 + assert self.manager.lookup(chunk, _CTX) is LookupResult.HIT_PENDING
567
568 self._simulate_on_schedule_end()
569 self._simulate_on_schedule_end()
570
571 assert [
571 - self.primary_tier.lookup(block, _CTX) for block in blocks
572 + self.primary_tier.lookup(chunk, _CTX) for chunk in chunks
573 ] == expected_results
574
575 def test_lookup_reports_sync_delay_for_resolved_lookups(self, manager_setup):
575 - """Resolved lookups report one sync delay sample per tier and block."""
576 + """Resolved lookups report one sync delay sample per tier and chunk."""
577 self._start_request()
577 - blocks = to_keys(range(2))
578 + chunks = to_keys(range(2))
579
579 - # No tier has these blocks: they resolve immediately as misses.
580 - for block in blocks:
581 - assert self.manager.lookup(block, _CTX) is LookupResult.MISS
580 + # No tier has these chunks: they resolve immediately as misses.
581 + for chunk in chunks:
582 + assert self.manager.lookup(chunk, _CTX) is LookupResult.MISS
583
584 stats = self.manager.get_stats()
585 assert stats is not None
@@ -614,11 +615,11 @@ class TestTieringOffloadingManager:
615 def test_lookup_does_not_report_async_delay_for_promotion(self, manager_setup):
616 """Promotion time is not included in lookup async delay."""
617 self._start_request()
617 - block = to_keys(range(1))[0]
618 - self.secondary_tier1.blocks[block] = True
618 + chunk = to_keys(range(1))[0]
619 + self.secondary_tier1.chunks[chunk] = True
620
620 - # First lookup finds the block in a secondary tier and defers.
621 - assert self.manager.lookup(block, _CTX) is LookupResult.HIT_PENDING
621 + # First lookup finds the chunk in a secondary tier and defers.
622 + assert self.manager.lookup(chunk, _CTX) is LookupResult.HIT_PENDING
623
624 # Promotion is not treated as unresolved secondary lookup time.
625 self._simulate_on_schedule_end(new_req_ids=[_CTX.req_id])
@@ -648,8 +649,8 @@ class TestTieringOffloadingManager:
649 # Promotion completes on the next scheduler step.
650 self._simulate_on_schedule_end()
651
651 - # Next lookup resolves via the now-promoted primary-tier block.
652 - assert self.manager.lookup(block, _CTX) is LookupResult.HIT
652 + # Next lookup resolves via the now-promoted primary-tier chunk.
653 + assert self.manager.lookup(chunk, _CTX) is LookupResult.HIT
654
655 stats = self.manager.get_stats()
656 if stats is not None:
@@ -663,13 +664,13 @@ class TestTieringOffloadingManager:
664 """Async delay is observed when a RETRY lookup later resolves."""
665 ctx = ReqContext(req_id="req_lookup_finish")
666 self._start_request(ctx)
666 - block = to_keys(range(1))[0]
667 + chunk = to_keys(range(1))[0]
668 self.secondary_tier1.lookup = MagicMock(
669 side_effect=[LookupResult.RETRY, LookupResult.HIT]
670 )
671
672 # First lookup is deferred by the secondary tier.
672 - assert self.manager.lookup(block, ctx) is LookupResult.RETRY
673 + assert self.manager.lookup(chunk, ctx) is LookupResult.RETRY
674
675 stats = self.manager.get_stats()
676 if stats is not None:
@@ -677,7 +678,7 @@ class TestTieringOffloadingManager:
678 stats.reduce(), TieringOffloadingMetrics.LOOKUP_ASYNC_DELAY
679 )
680
680 - assert self.manager.lookup(block, ctx) is LookupResult.HIT_PENDING
681 + assert self.manager.lookup(chunk, ctx) is LookupResult.HIT_PENDING
682
683 stats = self.manager.get_stats()
684 assert stats is not None
@@ -701,68 +702,68 @@ class TestTieringOffloadingManager:
702
703 def test_partial_lookup(self, manager_setup):
704 """Test lookup with partial hits."""
704 - blocks = to_keys(range(5))
705 + chunks = to_keys(range(5))
706
706 - # Store first 3 blocks to primary
707 + # Store first 3 chunks to primary
708 self._start_request()
708 - self.manager.prepare_store(blocks[:3], _CTX)
709 - self.manager.complete_store(blocks[:3], _CTX, success=True)
709 + self.manager.prepare_store(chunks[:3], _CTX)
710 + self.manager.complete_store(chunks[:3], _CTX, success=True)
711
711 - # Lookup all 5 blocks should return 3 (first 3 found)
712 - assert count_hits(self.manager, blocks) == 3
712 + # Lookup all 5 chunks should return 3 (first 3 found)
713 + assert count_hits(self.manager, chunks) == 3
714
715 def test_eviction_in_primary_tier(self, manager_setup):
716 """Test eviction in primary tier when capacity is exceeded."""
716 - # Primary tier has capacity of 5 blocks
717 + # Primary tier has capacity of 5 chunks
718 # First, fill the primary tier
718 - blocks = to_keys(range(5))
719 + chunks = to_keys(range(5))
720 self._start_request()
720 - result = self.manager.prepare_store(blocks, _CTX)
721 + result = self.manager.prepare_store(chunks, _CTX)
722 assert result is not None
723 assert len(result.keys_to_store) == 5
723 - self.manager.complete_store(blocks, _CTX, success=True)
724 + self.manager.complete_store(chunks, _CTX, success=True)
725
726 # End of step: release ref_cnt from cascade
727 self._simulate_on_schedule_end()
728
728 - # Now try to store 2 more blocks (should trigger eviction)
729 - more_blocks = to_keys(range(5, 7))
730 - result = self.manager.prepare_store(more_blocks, _CTX)
729 + # Now try to store 2 more chunks (should trigger eviction)
730 + more_chunks = to_keys(range(5, 7))
731 + result = self.manager.prepare_store(more_chunks, _CTX)
732
732 - # Should evict 2 blocks from primary tier
733 + # Should evict 2 chunks from primary tier
734 assert result is not None
735 assert len(result.evicted_keys) == 2
736 assert len(result.keys_to_store) == 2
737
738 def test_touch_propagates_to_all_tiers(self, manager_setup):
739 """Test that touch() propagates to all tiers."""
739 - blocks = to_keys(range(3))
740 + chunks = to_keys(range(3))
741
741 - # Store blocks
742 + # Store chunks
743 self._start_request()
743 - self.manager.prepare_store(blocks, _CTX)
744 - self.manager.complete_store(blocks, _CTX, success=True)
744 + self.manager.prepare_store(chunks, _CTX)
745 + self.manager.complete_store(chunks, _CTX, success=True)
746 self._simulate_on_schedule_end()
746 - # for secondary tiers to drain jobs, so primary tier's blocks are evictable.
747 + # for secondary tiers to drain jobs, so primary tier's chunks are evictable.
748 self._simulate_on_schedule_end()
749
750 self.secondary_tier1.touch = MagicMock(wraps=self.secondary_tier1.touch)
751 self.secondary_tier2.touch = MagicMock(wraps=self.secondary_tier2.touch)
752
752 - # Touch blocks
753 - self.manager.touch(blocks, _CTX)
753 + # Touch chunks
754 + self.manager.touch(chunks, _CTX)
755
756 # Verify touch was called on primary tier (check LRU order)
756 - primary_keys = list(self.primary_tier._policy.evictable_blocks.keys())
757 - assert primary_keys[-3:] == list(reversed(blocks))
757 + primary_keys = list(self.primary_tier._policy.evictable_chunks.keys())
758 + assert primary_keys[-3:] == list(reversed(chunks))
759
760 # Verify touch was propagated to all secondary tiers
760 - self.secondary_tier1.touch.assert_called_once_with(blocks, _CTX)
761 - self.secondary_tier2.touch.assert_called_once_with(blocks, _CTX)
761 + self.secondary_tier1.touch.assert_called_once_with(chunks, _CTX)
762 + self.secondary_tier2.touch.assert_called_once_with(chunks, _CTX)
763
764 def test_failed_store_no_cascade(self, manager_setup):
765 """Test that failed GPU→primary store doesn't cascade."""
765 - blocks = to_keys(range(3))
766 + chunks = to_keys(range(3))
767
768 self.secondary_tier1.submit_store = MagicMock(
769 wraps=self.secondary_tier1.submit_store
@@ -773,11 +774,11 @@ class TestTieringOffloadingManager:
774
775 # Prepare store
776 self._start_request()
776 - result = self.manager.prepare_store(blocks, _CTX)
777 + result = self.manager.prepare_store(chunks, _CTX)
778 assert result is not None
779
780 # Complete store with failure — cascade must not happen
780 - self.manager.complete_store(blocks, _CTX, success=False)
781 + self.manager.complete_store(chunks, _CTX, success=False)
782
783 # submit_store was never called on either secondary tier
784 self.secondary_tier1.submit_store.assert_not_called()
@@ -786,12 +787,12 @@ class TestTieringOffloadingManager:
787 def test_lookup_batches_submit_load_per_request(self, manager_setup):
788 """lookup() defers submit_load until on_schedule_end(), one per request.
789
789 - Blocks from different requests each get their own submit_load call, each
790 + Chunks from different requests each get their own submit_load call, each
791 carrying the correct req_context.
792 """
792 - blocks = to_keys(range(4))
793 - for block in blocks:
794 - self.secondary_tier1.blocks[block] = True
793 + chunks = to_keys(range(4))
794 + for chunk in chunks:
795 + self.secondary_tier1.chunks[chunk] = True
796
797 self.secondary_tier1.submit_load = MagicMock(
798 wraps=self.secondary_tier1.submit_load
@@ -801,10 +802,10 @@ class TestTieringOffloadingManager:
802 ctx_b = ReqContext(req_id="req_b")
803
804 # All lookups return HIT_PENDING: secondary hit triggers promotion
804 - assert self.manager.lookup(blocks[0], ctx_a) is LookupResult.HIT_PENDING
805 - assert self.manager.lookup(blocks[1], ctx_a) is LookupResult.HIT_PENDING
806 - assert self.manager.lookup(blocks[2], ctx_b) is LookupResult.HIT_PENDING
807 - assert self.manager.lookup(blocks[3], ctx_b) is LookupResult.HIT_PENDING
805 + assert self.manager.lookup(chunks[0], ctx_a) is LookupResult.HIT_PENDING
806 + assert self.manager.lookup(chunks[1], ctx_a) is LookupResult.HIT_PENDING
807 + assert self.manager.lookup(chunks[2], ctx_b) is LookupResult.HIT_PENDING
808 + assert self.manager.lookup(chunks[3], ctx_b) is LookupResult.HIT_PENDING
809
810 # submit_load must not fire during lookup - only at end of step
811 self.secondary_tier1.submit_load.assert_not_called()
@@ -816,20 +817,20 @@ class TestTieringOffloadingManager:
817 calls = self.secondary_tier1.submit_load.call_args_list
818 jm_a = calls[0].args[0]
819 jm_b = calls[1].args[0]
819 - assert set(jm_a.keys) == {blocks[0], blocks[1]}
820 + assert set(jm_a.keys) == {chunks[0], chunks[1]}
821 assert jm_a.req_context is ctx_a
821 - assert set(jm_b.keys) == {blocks[2], blocks[3]}
822 + assert set(jm_b.keys) == {chunks[2], chunks[3]}
823 assert jm_b.req_context is ctx_b
824
824 - def test_lookup_shared_block_no_duplicate_promotion(self, manager_setup):
825 - """A block looked up by two requests in the same step is promoted once.
825 + def test_lookup_shared_chunk_no_duplicate_promotion(self, manager_setup):
826 + """A chunk looked up by two requests in the same step is promoted once.
827
828 The first lookup initiates promotion (returns None via secondary hit).
829 The second lookup sees ref_cnt=-1 on the primary slot and returns None
830 via the primary in-flight path — without triggering a second promotion.
831 """
831 - shared_block = to_keys([0])[0]
832 - self.secondary_tier1.blocks[shared_block] = True
832 + shared_chunk = to_keys([0])[0]
833 + self.secondary_tier1.chunks[shared_chunk] = True
834
835 self.secondary_tier1.submit_load = MagicMock(
836 wraps=self.secondary_tier1.submit_load
@@ -838,10 +839,10 @@ class TestTieringOffloadingManager:
839 ctx_a = ReqContext(req_id="req_a")
840 ctx_b = ReqContext(req_id="req_b")
841
841 - result_a = self.manager.lookup(shared_block, ctx_a)
842 - result_b = self.manager.lookup(shared_block, ctx_b)
842 + result_a = self.manager.lookup(shared_chunk, ctx_a)
843 + result_b = self.manager.lookup(shared_chunk, ctx_b)
844
844 - # Both lookups find the shared_block and trigger promotion
845 + # Both lookups find the shared_chunk and trigger promotion
846 # returning HIT_PENDING.
847 assert result_a is LookupResult.HIT_PENDING
848 assert result_b is LookupResult.HIT_PENDING
@@ -851,12 +852,12 @@ class TestTieringOffloadingManager:
852 # Only one submit_load call despite two lookups
853 self.secondary_tier1.submit_load.assert_called_once()
854 job_metadata = self.secondary_tier1.submit_load.call_args.args[0]
854 - assert list(job_metadata.keys) == [shared_block]
855 + assert list(job_metadata.keys) == [shared_chunk]
856 assert job_metadata.req_context is ctx_a
857
858 def test_complete_store_forwards_req_context_to_submit_store(self, manager_setup):
859 """complete_store cascades to secondary tiers with the correct req_context."""
859 - blocks = to_keys(range(2))
860 + chunks = to_keys(range(2))
861
862 self.secondary_tier1.submit_store = MagicMock(
863 wraps=self.secondary_tier1.submit_store
@@ -865,8 +866,8 @@ class TestTieringOffloadingManager:
866 ctx = ReqContext(req_id="req_ctx", kv_transfer_params={"key": "value"})
867
868 self._start_request(ctx)
868 - self.manager.prepare_store(blocks, ctx)
869 - self.manager.complete_store(blocks, ctx, success=True)
869 + self.manager.prepare_store(chunks, ctx)
870 + self.manager.complete_store(chunks, ctx, success=True)
871
872 assert self.secondary_tier1.submit_store.call_count == 1
873 job_metadata = self.secondary_tier1.submit_store.call_args.args[0]
@@ -876,7 +877,7 @@ class TestTieringOffloadingManager:
877 self, manager_setup
878 ):
879 """Manager hook is eager; secondary hooks wait for cascade submission."""
879 - blocks = to_keys(range(2))
880 + chunks = to_keys(range(2))
881 ctx = ReqContext(req_id="req_delayed_secondary")
882 calls: list[tuple[str, str]] = []
883
@@ -911,14 +912,14 @@ class TestTieringOffloadingManager:
912 )
913
914 self._start_request(ctx)
914 - self.manager.prepare_store(blocks, ctx)
915 + self.manager.prepare_store(chunks, ctx)
916 self.manager.on_request_finished(ctx)
917
918 assert calls == [("primary_finish", ctx.req_id)]
919 self.secondary_tier1.on_request_finished.assert_not_called()
920 self.secondary_tier2.on_request_finished.assert_not_called()
921
921 - self.manager.complete_store(blocks, ctx, success=True)
922 + self.manager.complete_store(chunks, ctx, success=True)
923
924 assert calls == [
925 ("primary_finish", ctx.req_id),
@@ -930,7 +931,7 @@ class TestTieringOffloadingManager:
931
932 def test_failed_store_finalizes_finished_request(self, manager_setup):
933 """Failed primary stores still unblock secondary finalization."""
933 - blocks = to_keys(range(2))
934 + chunks = to_keys(range(2))
935 ctx = ReqContext(req_id="req_failed_store_finalize")
936
937 self.secondary_tier1.submit_store = MagicMock(
@@ -947,13 +948,13 @@ class TestTieringOffloadingManager:
948 )
949
950 self._start_request(ctx)
950 - self.manager.prepare_store(blocks, ctx)
951 + self.manager.prepare_store(chunks, ctx)
952 self.manager.on_request_finished(ctx)
953
954 self.secondary_tier1.on_request_finished.assert_not_called()
955 self.secondary_tier2.on_request_finished.assert_not_called()
956
956 - self.manager.complete_store(blocks, ctx, success=False)
957 + self.manager.complete_store(chunks, ctx, success=False)
958
959 self.secondary_tier1.submit_store.assert_not_called()
960 self.secondary_tier2.submit_store.assert_not_called()
@@ -981,7 +982,7 @@ class TestTieringOffloadingManager:
982
983 def test_reset_cache_finalizes_delayed_secondary_request(self, manager_setup):
984 """reset_cache abandons pending primary stores and finalizes secondaries."""
984 - blocks = to_keys(range(2))
985 + chunks = to_keys(range(2))
986 ctx = ReqContext(req_id="req_reset_finalize_secondary")
987
988 self.secondary_tier1.on_request_finished = MagicMock(
@@ -992,7 +993,7 @@ class TestTieringOffloadingManager:
993 )
994
995 self._start_request(ctx)
995 - self.manager.prepare_store(blocks, ctx)
996 + self.manager.prepare_store(chunks, ctx)
997 self.manager.on_request_finished(ctx)
998
999 self.secondary_tier1.on_request_finished.assert_not_called()
@@ -1008,8 +1009,8 @@ class TestTieringOffloadingManager:
1009 self, manager_setup
1010 ):
1011 """reset_cache drops active pending stores so resumed requests finalize."""
1011 - initial_blocks = to_keys(range(2))
1012 - resumed_blocks = to_keys(range(2, 4))
1012 + initial_chunks = to_keys(range(2))
1013 + resumed_chunks = to_keys(range(2, 4))
1014 ctx = ReqContext(req_id="req_reset_resume")
1015
1016 self.secondary_tier1.on_request_finished = MagicMock(
@@ -1020,7 +1021,7 @@ class TestTieringOffloadingManager:
1021 )
1022
1023 self._start_request(ctx)
1023 - self.manager.prepare_store(initial_blocks, ctx)
1024 + self.manager.prepare_store(initial_chunks, ctx)
1025 assert self.manager._req_state[ctx.req_id].pending_primary_stores == 1
1026
1027 self.manager.reset_cache()
@@ -1030,8 +1031,8 @@ class TestTieringOffloadingManager:
1031 self.secondary_tier1.on_request_finished.assert_not_called()
1032 self.secondary_tier2.on_request_finished.assert_not_called()
1033
1033 - self.manager.prepare_store(resumed_blocks, ctx)
1034 - self.manager.complete_store(resumed_blocks, ctx, success=True)
1034 + self.manager.prepare_store(resumed_chunks, ctx)
1035 + self.manager.complete_store(resumed_chunks, ctx, success=True)
1036 self.manager.on_request_finished(ctx)
1037
1038 self.secondary_tier1.on_request_finished.assert_called_once_with(ctx)
@@ -1039,12 +1040,12 @@ class TestTieringOffloadingManager:
1040 assert ctx.req_id not in self.manager._req_state
1041
1042 def test_on_new_request_lifecycle(self, manager_setup):
1042 - """Policy defaults to BLOCK_LEVEL, escalates when a tier requests it,
1043 + """Policy defaults to CHUNK_LEVEL, escalates when a tier requests it,
1044 and is cleaned up on on_request_finished."""
1044 - # Default: all tiers return BLOCK_LEVEL
1045 + # Default: all tiers return CHUNK_LEVEL
1046 ctx = ReqContext(req_id="req_policy_lifecycle")
1047 result = self.manager.on_new_request(ctx)
1047 - assert result.policy == OffloadPolicy.BLOCK_LEVEL
1048 + assert result.policy == OffloadPolicy.CHUNK_LEVEL
1049 assert self.manager._req_state[ctx.req_id].request_level_tiers is None
1050 self.manager.on_request_finished(ctx)
1051 assert ctx.req_id not in self.manager._req_state
@@ -1064,26 +1065,26 @@ class TestTieringOffloadingManager:
1065 assert ctx.req_id not in self.manager._req_state
1066
1067 @pytest.mark.parametrize("new_ids", [(), (3, 4)], ids=["fully_warm", "mixed"])
1067 - def test_prepare_store_cascades_existing_blocks_to_request_level_tiers(
1068 + def test_prepare_store_cascades_existing_chunks_to_request_level_tiers(
1069 self, manager_setup, new_ids
1070 ):
1070 - """prepare_store cascades hit blocks to request-level tiers only.
1071 + """prepare_store cascades hit chunks to request-level tiers only.
1072
1073 The fully-warm case is the producer shape from issue #52808: nothing is
1074 new, so primary returns an empty keys_to_store and no GPU->primary
1074 - transfer runs. A request-level tier must still be handed the blocks,
1075 + transfer runs. A request-level tier must still be handed the chunks,
1076 otherwise a peer waiting on them has nothing to fetch.
1077 """
1077 - # Store some blocks to primary first
1078 - existing_blocks = to_keys(range(3))
1078 + # Store some chunks to primary first
1079 + existing_chunks = to_keys(range(3))
1080 self._start_request()
1080 - result = self.manager.prepare_store(existing_blocks, _CTX)
1081 + result = self.manager.prepare_store(existing_chunks, _CTX)
1082 assert result is not None
1082 - self.manager.complete_store(existing_blocks, _CTX, success=True)
1083 + self.manager.complete_store(existing_chunks, _CTX, success=True)
1084 # Drain cascade completions
1085 self._simulate_on_schedule_end()
1086
1086 - # Make tier1 request-level, tier2 stays block-level
1087 + # Make tier1 request-level, tier2 stays chunk-level
1088 self.secondary_tier1.on_new_request = lambda req_context: (
1089 RequestOffloadingContext(policy=OffloadPolicy.REQUEST_LEVEL)
1090 )
@@ -1099,20 +1100,20 @@ class TestTieringOffloadingManager:
1100 wraps=self.secondary_tier2.submit_store
1101 )
1102
1102 - # Call prepare_store with existing + new blocks
1103 - new_blocks = to_keys(new_ids)
1104 - all_blocks = existing_blocks + new_blocks
1105 - result = self.manager.prepare_store(all_blocks, ctx)
1103 + # Call prepare_store with existing + new chunks
1104 + new_chunks = to_keys(new_ids)
1105 + all_chunks = existing_chunks + new_chunks
1106 + result = self.manager.prepare_store(all_chunks, ctx)
1107 assert result is not None
1107 - assert set(result.keys_to_store) == set(new_blocks)
1108 + assert set(result.keys_to_store) == set(new_chunks)
1109
1109 - # Only tier1 (request-level) should get existing blocks cascaded now.
1110 - # New blocks are cascaded to ALL tiers later via complete_store().
1110 + # Only tier1 (request-level) should get existing chunks cascaded now.
1111 + # New chunks are cascaded to ALL tiers later via complete_store().
1112 self.secondary_tier1.submit_store.assert_called_once()
1113 job_metadata = self.secondary_tier1.submit_store.call_args.args[0]
1113 - assert set(job_metadata.keys) == set(existing_blocks)
1114 + assert set(job_metadata.keys) == set(existing_chunks)
1115
1115 - # tier2 (block-level) does not get existing blocks here.
1116 + # tier2 (chunk-level) does not get existing chunks here.
1117 self.secondary_tier2.submit_store.assert_not_called()
1118
1119 def _make_request_level_request(self, req_id: str) -> ReqContext:
@@ -1150,7 +1151,7 @@ class TestTieringOffloadingManager:
1151 self.manager.primary_tier.lookup = lambda key, req_context: (LookupResult.RETRY)
1152
1153 with pytest.raises(AssertionError):
1153 - self.manager._cascade_existing_blocks_to_request_level_tiers(keys, ctx, {0})
1154 + self.manager._cascade_existing_chunks_to_request_level_tiers(keys, ctx, {0})
1155
1156 def _start_in_flight_primary_write(self, keys: list[OffloadKey]) -> ReqContext:
1157 """Leave a GPU->primary write for `keys` open, so they look present to
@@ -1202,7 +1203,7 @@ class TestTieringOffloadingManager:
1203 def test_cascade_drops_deferred_keys_whose_primary_write_failed(
1204 self, manager_setup
1205 ):
1205 - """A failed write frees the block, so the deferred key resolves to MISS
1206 + """A failed write frees the chunk, so the deferred key resolves to MISS
1207 and the request finalizes instead of parking forever."""
1208 keys = to_keys(range(3))
1209 writer_ctx = self._start_in_flight_primary_write(keys)
@@ -1224,21 +1225,21 @@ class TestTieringOffloadingManager:
1225 """reset_cache wipes every kind of orchestrator state and resets
1226 primary tier; pending submissions are dropped without being sent
1227 to the secondary tier. Active request state is retained."""
1227 - # Cascade — populates primary blocks and leaves cascade jobs
1228 + # Cascade — populates primary chunks and leaves cascade jobs
1229 # in _jobs (the synchronous example tier has already
1230 # queued completions); reset_cache's drain loop will pick them up.
1230 - blocks = to_keys(range(3))
1231 + chunks = to_keys(range(3))
1232 self._start_request()
1232 - self.manager.prepare_store(blocks, _CTX)
1233 - self.manager.complete_store(blocks, _CTX, success=True)
1233 + self.manager.prepare_store(chunks, _CTX)
1234 + self.manager.complete_store(chunks, _CTX, success=True)
1235 assert self.manager._jobs
1236
1237 # Pending promotion submission (deferred — no on_schedule_end after
1238 # the lookup that staged it).
1238 - promo_block = to_keys([99])[0]
1239 - self.secondary_tier1.blocks[promo_block] = True
1239 + promo_chunk = to_keys([99])[0]
1240 + self.secondary_tier1.chunks[promo_chunk] = True
1241 assert (
1241 - self.manager.lookup(promo_block, ReqContext(req_id="pending"))
1242 + self.manager.lookup(promo_chunk, ReqContext(req_id="pending"))
1243 is LookupResult.HIT_PENDING
1244 )
1245 assert self.manager._pending_load_submissions
@@ -1268,10 +1269,10 @@ class TestTieringOffloadingManager:
1269 assert self.manager._processed_jobs_this_step is False
1270
1271 # Primary tier reset to a fresh state.
1271 - assert self.primary_tier._num_allocated_blocks == 0
1272 + assert self.primary_tier._num_allocated_chunks == 0
1273 assert self.primary_tier._free_list == []
1273 - for block in blocks:
1274 - assert self.primary_tier.lookup(block, _CTX) is LookupResult.MISS
1274 + for chunk in chunks:
1275 + assert self.primary_tier.lookup(chunk, _CTX) is LookupResult.MISS
1276
1277 # Pending submission was dropped, not submitted.
1278 self.secondary_tier1.submit_load.assert_not_called()
@@ -1291,10 +1292,10 @@ class TestTieringOffloadingManager:
1292 )
1293
1294 # Drive a cascade so a job lands in _jobs.
1294 - blocks = to_keys(range(3))
1295 + chunks = to_keys(range(3))
1296 self._start_request()
1296 - self.manager.prepare_store(blocks, _CTX)
1297 - self.manager.complete_store(blocks, _CTX, success=True)
1297 + self.manager.prepare_store(chunks, _CTX)
1298 + self.manager.complete_store(chunks, _CTX, success=True)
1299 assert self.manager._jobs
1300
1301 self.manager.reset_cache()
@@ -1315,20 +1316,20 @@ class TestTieringOffloadingManager:
1316 self, manager_setup, load_tier_filter
1317 ):
1318 """Filter excluding secondary medium returns MISS from secondaries
1318 - even when they hold the block; primary is unaffected."""
1319 - blocks = to_keys(range(2))
1320 - # Put one block in primary, one only in secondary
1319 + even when they hold the chunk; primary is unaffected."""
1320 + chunks = to_keys(range(2))
1321 + # Put one chunk in primary, one only in secondary
1322 self._start_request()
1322 - self.manager.prepare_store(blocks[:1], _CTX)
1323 - self.manager.complete_store(blocks[:1], _CTX, success=True)
1324 - self.secondary_tier1.blocks[blocks[1]] = True
1323 + self.manager.prepare_store(chunks[:1], _CTX)
1324 + self.manager.complete_store(chunks[:1], _CTX, success=True)
1325 + self.secondary_tier1.chunks[chunks[1]] = True
1326
1327 # Secondaries have medium=CPU, so load_tier_filter skips them.
1328 self.secondary_tier1.lookup = MagicMock(wraps=self.secondary_tier1.lookup)
1329
1330 ctx = ReqContext(req_id="r1", load_tier_filter=load_tier_filter)
1330 - assert self.manager.lookup(blocks[0], ctx) is LookupResult.HIT
1331 - assert self.manager.lookup(blocks[1], ctx) is LookupResult.MISS
1331 + assert self.manager.lookup(chunks[0], ctx) is LookupResult.HIT
1332 + assert self.manager.lookup(chunks[1], ctx) is LookupResult.MISS
1333 self.secondary_tier1.lookup.assert_not_called()
1334
1335 @pytest.mark.parametrize(
@@ -1344,13 +1345,13 @@ class TestTieringOffloadingManager:
1345 self, manager_setup, load_tier_filter
1346 ):
1347 """Filter that matches the secondary's medium allows lookup."""
1347 - blocks = to_keys(range(1))
1348 - self.secondary_tier1.blocks[blocks[0]] = True
1348 + chunks = to_keys(range(1))
1349 + self.secondary_tier1.chunks[chunks[0]] = True
1350
1351 self.secondary_tier1.lookup = MagicMock(wraps=self.secondary_tier1.lookup)
1352
1353 ctx = ReqContext(req_id="r2", load_tier_filter=load_tier_filter)
1353 - assert self.manager.lookup(blocks[0], ctx) is LookupResult.HIT_PENDING
1354 + assert self.manager.lookup(chunks[0], ctx) is LookupResult.HIT_PENDING
1355 self.secondary_tier1.lookup.assert_called()
1356
1357
@@ -1360,7 +1361,7 @@ class TestTieringOffloadingWithoutSecondaryTiers:
1361 def test_works_without_secondary_tiers(self):
1362 """Test that manager works with empty secondary_tiers list."""
1363 primary_tier = CPUPrimaryTierOffloadingManager(
1363 - num_blocks=5, mmap_region=_mock_mmap_region(5)
1364 + num_chunks=5, mmap_region=_mock_mmap_region(5)
1365 )
1366
1367 # Create manager with no secondary tiers
@@ -1368,15 +1369,15 @@ class TestTieringOffloadingWithoutSecondaryTiers:
1369 primary_tier=primary_tier, secondary_tiers=[]
1370 )
1371
1371 - blocks = to_keys(range(3))
1372 + chunks = to_keys(range(3))
1373
1374 # Should work like a regular OffloadingManager
1375 manager.on_new_request(_CTX)
1375 - result = manager.prepare_store(blocks, _CTX)
1376 + result = manager.prepare_store(chunks, _CTX)
1377 assert result is not None
1377 - manager.complete_store(blocks, _CTX, success=True)
1378 + manager.complete_store(chunks, _CTX, success=True)
1379
1379 - assert count_hits(manager, blocks) == 3
1380 + assert count_hits(manager, chunks) == 3
1381
1382
1383 @pytest.mark.parametrize(
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
+1 -1
@@ -1131,7 +1131,7 @@ class OffloadingConnectorScheduler:
1131 # Skip prefix-hit chunks for block-level policy; for
1132 # request-level, next_stored_chunk_idx stays at 0 so all
1133 # chunks (including hits) are offloaded.
1134 - if req_status.offloading_context.policy == OffloadPolicy.BLOCK_LEVEL:
1134 + if req_status.offloading_context.policy == OffloadPolicy.CHUNK_LEVEL:
1135 group_state.next_stored_chunk_idx = num_chunks
1136
1137 src_spec = self.manager.prepare_load(keys_to_load, req_status.req_context)
vllm/v1/kv_offload/base.py
+8 -5
@@ -114,17 +114,17 @@ class LookupResult(Enum):
114
115
116 class OffloadPolicy(Enum):
117 - # Offload only newly-computed blocks as they arrive; prefix-hit
118 - # blocks (already offloaded by a prior request) are skipped.
119 - BLOCK_LEVEL = "block_level"
120 - # Offload all blocks for the request, including prefix hits.
117 + # Offload only newly-computed chunks as they arrive; prefix-hit
118 + # chunks (already offloaded by a prior request) are skipped.
119 + CHUNK_LEVEL = "chunk_level"
120 + # Offload all chunks for the request, including prefix hits.
121 # Used by tiers that need the complete KV context for a request.
122 REQUEST_LEVEL = "request_level"
123
124
125 @dataclass
126 class RequestOffloadingContext:
127 - policy: OffloadPolicy = OffloadPolicy.BLOCK_LEVEL
127 + policy: OffloadPolicy = OffloadPolicy.CHUNK_LEVEL
128
129
130 class ScheduleEndContext(NamedTuple):
@@ -400,6 +400,9 @@ class OffloadingManager(ABC):
400 class BlockIDsLoadStoreSpec(LoadStoreSpec, ABC):
401 """
402 Spec for loading/storing KV blocks from given block numbers.
403 +
404 + Subclass semantics differ: GPULoadStoreSpec.block_ids are GPU block
405 + indices; CPULoadStoreSpec.block_ids are CPU cache chunk indices.
406 """
407
408 def __init__(self, block_ids: list[int]):
vllm/v1/kv_offload/cpu/common.py
+11 -2
@@ -1,5 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import numpy as np
4 +
5 from vllm.v1.kv_offload.base import BlockIDsLoadStoreSpec
6
7
@@ -12,6 +14,13 @@ class CPUOffloadingMetrics:
14
15
16 class CPULoadStoreSpec(BlockIDsLoadStoreSpec):
17 + """Spec for loading/storing KV chunks to/from CPU memory.
18 +
19 + The inherited block_ids field holds chunk indices into the
20 + CPU cache (not GPU block IDs). The chunk_ids alias exposes
21 + the same array under the name used by the tiering layer.
22 """
16 - Spec for loading/storing a KV block to CPU memory.
17 - """
23 +
24 + @property
25 + def chunk_ids(self) -> np.ndarray:
26 + return self.block_ids
vllm/v1/kv_offload/cpu/gpu_worker.py
+10 -10
@@ -257,7 +257,7 @@ class SingleDirectionOffloadingHandler:
257 gpu_tensors: list of GPU KV cache tensors.
258 Each of shape (num_gpu_blocks, gpu_page_size_bytes) with dtype int8.
259 cpu_tensors: list of CPU KV cache tensors.
260 - Each of shape (num_cpu_blocks, cpu_page_size_bytes) with dtype int8.
260 + Each of shape (num_cpu_chunks, cpu_page_size_bytes) with dtype int8.
261 Order should match gpu_tensors.
262 layer_refs_per_group: list of CanonicalKVCacheRef per group.
263 gpu_to_cpu: if True, transfer from GPU to CPU; otherwise CPU to GPU.
@@ -527,20 +527,20 @@ class SingleDirectionOffloadingHandler:
527 # 1. GPU -> CPU
528 # 2. CPU -> GPU
529 #
530 - # transfers are also to CPU blocks, EXCEPT MAYBE for the first and last block.
531 - # i.e. the first and last CPU blocks in src_blocks can match against
530 + # transfers are also to CPU chunks, EXCEPT MAYBE for the first and last chunk.
531 + # i.e. the first and last CPU chunks in src_blocks can match against
532 # a smaller (byte-wise) set of GPU blocks in dst_blocks.
533 # In such cases, we may need to skip some gpu-sized sub-blocks,
534 - # and start reading/writing from the middle of the first CPU block.
534 + # and start reading/writing from the middle of the first CPU chunk.
535 # If we have multiple KV cache groups (when using HMA with hybrid models),
536 - # we may have a partial first/last CPU block per each group.
536 + # we may have a partial first/last CPU chunk per each group.
537 # The group_sizes parameter encodes the size of each group of blocks
538 # in the GPU dst_blocks.
539 # If group_sizes is None, we assume all blocks belong to a single group.
540 # The logical_offset parameter maps each group of blocks to its logical
541 # offset inside the request, counting in GPU blocks.
542 # This allows us to find the correct starting position
543 - # in the matching first CPU block.
543 + # in the matching first CPU chunk.
544
545 # extract group_sizes from the GPU spec
546 gpu_spec = src_spec if self.gpu_to_cpu else dst_spec
@@ -751,7 +751,7 @@ class CPUOffloadingWorker(OffloadingWorker):
751 self,
752 kv_caches: CanonicalKVCaches,
753 blocks_per_chunk: int,
754 - num_cpu_blocks: int,
754 + num_cpu_chunks: int,
755 mmap_region: SharedOffloadRegion | None = None,
756 canonical_layout: bool = False,
757 ):
@@ -790,16 +790,16 @@ class CPUOffloadingWorker(OffloadingWorker):
790 else:
791 t0 = time.monotonic()
792 cpu_tensor = torch.zeros(
793 - (num_cpu_blocks, cpu_page_size_bytes),
793 + (num_cpu_chunks, cpu_page_size_bytes),
794 dtype=torch.int8,
795 device="cpu",
796 pin_memory=pin_memory,
797 )
798 logger.debug(
799 "torch.zeros pinned tensor %d×%d (%.2f GB): %.3f s",
800 - num_cpu_blocks,
800 + num_cpu_chunks,
801 cpu_page_size_bytes,
802 - num_cpu_blocks * cpu_page_size_bytes / 1e9,
802 + num_cpu_chunks * cpu_page_size_bytes / 1e9,
803 time.monotonic() - t0,
804 )
805
vllm/v1/kv_offload/cpu/manager.py
+87 -87
@@ -23,7 +23,7 @@ from vllm.v1.kv_offload.cpu.common import (
23 CPULoadStoreSpec,
24 CPUOffloadingMetrics,
25 )
26 -from vllm.v1.kv_offload.cpu.policies.base import BlockStatus, CachePolicy
26 +from vllm.v1.kv_offload.cpu.policies.base import CachePolicy, ChunkStatus
27 from vllm.v1.kv_offload.cpu.policies.factory import CachePolicyFactory
28
29
@@ -34,14 +34,14 @@ class CPUOffloadingManager(OffloadingManager):
34 register their own or be loaded out-of-tree via cache_policy_module_path).
35
36 The manager owns all shared logic: ref-counting, event emission,
37 - block pool management, and the prepare_store/complete_store skeletons.
38 - Policy-specific block organization and eviction decisions are delegated
37 + chunk pool management, and the prepare_store/complete_store skeletons.
38 + Policy-specific chunk organization and eviction decisions are delegated
39 to the CachePolicy implementation.
40 """
41
42 def __init__(
43 self,
44 - num_blocks: int,
44 + num_chunks: int,
45 cache_policy: str = "lru",
46 cache_policy_module_path: str | None = None,
47 enable_events: bool = False,
@@ -49,59 +49,59 @@ class CPUOffloadingManager(OffloadingManager):
49 max_tracker_size: int = 64_000,
50 ):
51 self.medium: Medium = Medium.CPU
52 - self._num_blocks: int = num_blocks
53 - self._num_allocated_blocks: int = 0
52 + self._num_chunks: int = num_chunks
53 + self._num_allocated_chunks: int = 0
54 self._free_list: list[int] = []
55 self.events: list[OffloadingEvent] | None = [] if enable_events else None
56 policy_cls = CachePolicyFactory.get_cache_policy_cls(
57 cache_policy, cache_policy_module_path
58 )
59 - self._policy: CachePolicy = policy_cls(cache_capacity=num_blocks)
60 - # Track the number of blocks in the cache that are evictable. i.e. ref_cnt 0.
61 - self._num_evictable_cache_blocks: int = 0
62 - # Track blocks with an in-flight store (ref_cnt -1, not yet completed).
63 - self._num_write_pending_blocks: int = 0
59 + self._policy: CachePolicy = policy_cls(cache_capacity=num_chunks)
60 + # Track the number of chunks in the cache that are evictable. i.e. ref_cnt 0.
61 + self._num_evictable_cache_chunks: int = 0
62 + # Track chunks with an in-flight store (ref_cnt -1, not yet completed).
63 + self._num_write_pending_chunks: int = 0
64
65 self.store_threshold: int = store_threshold
66 self.max_tracker_size: int = max_tracker_size
67 self.stores_skipped_in_current_batch: int = 0
68 self.allocation_sizes_in_current_batch: list[int] = []
69
70 - # Number of block references. It is ordered so can evict the LRU entry in O(1).
70 + # Number of chunk references. It is ordered so can evict the LRU entry in O(1).
71 self.counts: OrderedDict[OffloadKey, int] | None = (
72 OrderedDict() if store_threshold >= 2 else None
73 )
74
75 - # --- block pool ---
75 + # --- chunk pool ---
76
77 - def _get_num_free_blocks(self) -> int:
78 - return len(self._free_list) + self._num_blocks - self._num_allocated_blocks
77 + def _get_num_free_chunks(self) -> int:
78 + return len(self._free_list) + self._num_chunks - self._num_allocated_chunks
79
80 - def _allocate_blocks(self, keys: list[OffloadKey]) -> list[BlockStatus]:
81 - num_fresh = min(len(keys), self._num_blocks - self._num_allocated_blocks)
80 + def _allocate_chunks(self, keys: list[OffloadKey]) -> list[ChunkStatus]:
81 + num_fresh = min(len(keys), self._num_chunks - self._num_allocated_chunks)
82 num_reused = len(keys) - num_fresh
83 assert len(self._free_list) >= num_reused
84
85 - # allocate fresh blocks
86 - blocks: list[BlockStatus] = []
85 + # allocate fresh chunks
86 + chunks: list[ChunkStatus] = []
87 for _ in range(num_fresh):
88 - blocks.append(BlockStatus(self._num_allocated_blocks))
89 - self._num_allocated_blocks += 1
88 + chunks.append(ChunkStatus(self._num_allocated_chunks))
89 + self._num_allocated_chunks += 1
90
91 - # allocate reused blocks
91 + # allocate reused chunks
92 for _ in range(num_reused):
93 - blocks.append(BlockStatus(self._free_list.pop()))
94 - return blocks
93 + chunks.append(ChunkStatus(self._free_list.pop()))
94 + return chunks
95
96 - def _free_block(self, block: BlockStatus) -> None:
97 - self._free_list.append(block.block_id)
96 + def _free_chunk(self, chunk: ChunkStatus) -> None:
97 + self._free_list.append(chunk.chunk_id)
98
99 def _get_load_store_spec(
100 self,
101 keys: Iterable[OffloadKey],
102 - blocks: Iterable[BlockStatus],
102 + chunks: Iterable[ChunkStatus],
103 ) -> CPULoadStoreSpec:
104 - return CPULoadStoreSpec([block.block_id for block in blocks])
104 + return CPULoadStoreSpec([chunk.chunk_id for chunk in chunks])
105
106 def _record_accesses(self, keys: Collection[OffloadKey]) -> None:
107 """Record an offer without evicting its tracked candidates."""
@@ -132,10 +132,10 @@ class CPUOffloadingManager(OffloadingManager):
132
133 @override
134 def lookup(self, key: OffloadKey, req_context: ReqContext) -> LookupResult:
135 - block = self._policy.get(key)
136 - if block is None:
135 + chunk = self._policy.get(key)
136 + if chunk is None:
137 return LookupResult.MISS
138 - if not block.is_ready:
138 + if not chunk.is_ready:
139 return LookupResult.HIT_PENDING
140 return LookupResult.HIT
141
@@ -145,18 +145,18 @@ class CPUOffloadingManager(OffloadingManager):
145 keys: Collection[OffloadKey],
146 req_context: ReqContext,
147 ) -> LoadStoreSpec:
148 - blocks = []
148 + chunks = []
149 for key in keys:
150 - block = self._policy.get(key)
151 - assert block is not None, f"Block {key!r} not found in cache"
152 - assert block.is_ready, f"Block {key!r} is not ready for reading"
153 - if block.ref_cnt == 0:
150 + chunk = self._policy.get(key)
151 + assert chunk is not None, f"Chunk {key!r} not found in cache"
152 + assert chunk.is_ready, f"Chunk {key!r} is not ready for reading"
153 + if chunk.ref_cnt == 0:
154 self._policy.mark_non_evictable(key)
155 - self._num_evictable_cache_blocks -= 1 # ref_cnt 0 -> 1
156 - assert self._num_evictable_cache_blocks >= 0
157 - block.ref_cnt += 1
158 - blocks.append(block)
159 - return self._get_load_store_spec(keys, blocks)
155 + self._num_evictable_cache_chunks -= 1 # ref_cnt 0 -> 1
156 + assert self._num_evictable_cache_chunks >= 0
157 + chunk.ref_cnt += 1
158 + chunks.append(chunk)
159 + return self._get_load_store_spec(keys, chunks)
160
161 @override
162 def touch(self, keys: Collection[OffloadKey], req_context: ReqContext) -> None:
@@ -167,12 +167,12 @@ class CPUOffloadingManager(OffloadingManager):
167 self, keys: Collection[OffloadKey], req_context: ReqContext
168 ) -> None:
169 for key in keys:
170 - block = self._policy.get(key)
171 - assert block is not None, f"Block {key!r} not found"
172 - assert block.ref_cnt > 0, f"Block {key!r} ref_cnt is already 0"
173 - block.ref_cnt -= 1
174 - if block.ref_cnt == 0:
175 - self._num_evictable_cache_blocks += 1 # ref_cnt 1 -> 0
170 + chunk = self._policy.get(key)
171 + assert chunk is not None, f"Chunk {key!r} not found"
172 + assert chunk.ref_cnt > 0, f"Chunk {key!r} ref_cnt is already 0"
173 + chunk.ref_cnt -= 1
174 + if chunk.ref_cnt == 0:
175 + self._num_evictable_cache_chunks += 1 # ref_cnt 1 -> 0
176 self._policy.mark_evictable(key)
177
178 @override
@@ -186,7 +186,7 @@ class CPUOffloadingManager(OffloadingManager):
186 self._record_accesses(keys)
187 keys = [k for k in keys if self.counts.get(k, 0) >= self.store_threshold]
188 self.stores_skipped_in_current_batch += num_keys - len(keys)
189 - # filter out blocks that are already stored
189 + # filter out chunks that are already stored
190 keys_to_store = [k for k in keys if self._policy.get(k) is None]
191
192 if not keys_to_store:
@@ -197,29 +197,29 @@ class CPUOffloadingManager(OffloadingManager):
197 )
198
199 self.allocation_sizes_in_current_batch.append(len(keys_to_store))
200 - num_blocks_to_evict = len(keys_to_store) - self._get_num_free_blocks()
200 + num_chunks_to_evict = len(keys_to_store) - self._get_num_free_chunks()
201
202 to_evict: list[OffloadKey] = []
203 - if num_blocks_to_evict > 0:
204 - if num_blocks_to_evict > self._num_evictable_cache_blocks:
203 + if num_chunks_to_evict > 0:
204 + if num_chunks_to_evict > self._num_evictable_cache_chunks:
205 # Eviction will fail.
206 return None
207 # There is a still a chance for eviction failure as some of the
208 - # idle blocks might be in the protected list.
208 + # idle chunks might be in the protected list.
209
210 - # Blocks from the original input are excluded from eviction candidates:
211 - # a block that was already stored must remain in the cache after this call.
210 + # Chunks from the original input are excluded from eviction candidates:
211 + # a chunk that was already stored must remain in the cache after this call.
212 protected = set(keys)
213 - evicted = self._policy.evict(num_blocks_to_evict, protected)
213 + evicted = self._policy.evict(num_chunks_to_evict, protected)
214 if evicted is None:
215 return None
216
217 - # cache-policy removes only idle blocks.
218 - self._num_evictable_cache_blocks -= len(evicted)
219 - assert self._num_evictable_cache_blocks >= 0
217 + # cache-policy removes only idle chunks.
218 + self._num_evictable_cache_chunks -= len(evicted)
219 + assert self._num_evictable_cache_chunks >= 0
220
221 - for key, block in evicted:
222 - self._free_block(block)
221 + for key, chunk in evicted:
222 + self._free_chunk(chunk)
223 to_evict.append(key)
224
225 if to_evict and self.events is not None:
@@ -231,17 +231,17 @@ class CPUOffloadingManager(OffloadingManager):
231 )
232 )
233
234 - blocks = self._allocate_blocks(keys_to_store)
235 - assert len(blocks) == len(keys_to_store), (
236 - "Block pool did not allocate the expected number of blocks"
234 + chunks = self._allocate_chunks(keys_to_store)
235 + assert len(chunks) == len(keys_to_store), (
236 + "Chunk pool did not allocate the expected number of chunks"
237 )
238
239 - for key, block in zip(keys_to_store, blocks):
240 - self._policy.insert(key, block)
241 - self._num_write_pending_blocks += len(keys_to_store)
239 + for key, chunk in zip(keys_to_store, chunks):
240 + self._policy.insert(key, chunk)
241 + self._num_write_pending_chunks += len(keys_to_store)
242
243 - # build store specs for allocated blocks
244 - store_spec = self._get_load_store_spec(keys_to_store, blocks)
243 + # build store specs for allocated chunks
244 + store_spec = self._get_load_store_spec(keys_to_store, chunks)
245
246 return PrepareStoreOutput(
247 keys_to_store=keys_to_store,
@@ -260,20 +260,20 @@ class CPUOffloadingManager(OffloadingManager):
260
261 if success:
262 for key in keys:
263 - block = self._policy.get(key)
264 - if block is not None and not block.is_ready:
265 - block.ref_cnt = 0
266 - self._num_write_pending_blocks -= 1
267 - self._num_evictable_cache_blocks += 1
263 + chunk = self._policy.get(key)
264 + if chunk is not None and not chunk.is_ready:
265 + chunk.ref_cnt = 0
266 + self._num_write_pending_chunks -= 1
267 + self._num_evictable_cache_chunks += 1
268 self._policy.mark_evictable(key)
269 stored_keys.append(key)
270 else:
271 for key in keys:
272 - block = self._policy.get(key)
273 - if block is not None and not block.is_ready:
274 - self._num_write_pending_blocks -= 1
272 + chunk = self._policy.get(key)
273 + if chunk is not None and not chunk.is_ready:
274 + self._num_write_pending_chunks -= 1
275 self._policy.remove(key)
276 - self._free_block(block)
276 + self._free_chunk(chunk)
277
278 if stored_keys and self.events is not None:
279 self.events.append(
@@ -286,17 +286,17 @@ class CPUOffloadingManager(OffloadingManager):
286
287 @override
288 def reset_cache(self) -> None:
289 - # Clear ALL blocks unconditionally. The scheduler's _stale_job_threshold
289 + # Clear ALL chunks unconditionally. The scheduler's _stale_job_threshold
290 # guarantees that complete_load / complete_store are never called for
291 # pre-reset jobs, so no lazy cleanup is needed. The scheduler also
292 # flushes in-flight load job IDs to the workers before any new stores
293 - # can begin, preventing a cross-direction data race on reused offload block IDs.
293 + # can begin, preventing a cross-direction data race on reused offload chunk IDs.
294 self._policy.clear()
295 - self._num_evictable_cache_blocks = 0
296 - self._num_write_pending_blocks = 0
295 + self._num_evictable_cache_chunks = 0
296 + self._num_write_pending_chunks = 0
297
298 self._free_list.clear()
299 - self._num_allocated_blocks = 0
299 + self._num_allocated_chunks = 0
300
301 @override
302 def take_events(self) -> Iterable[OffloadingEvent]:
@@ -309,11 +309,11 @@ class CPUOffloadingManager(OffloadingManager):
309
310 # Compute cache usage.
311 num_used = (
312 - self._num_allocated_blocks
312 + self._num_allocated_chunks
313 - len(self._free_list)
314 - - self._num_evictable_cache_blocks
314 + - self._num_evictable_cache_chunks
315 )
316 - usage = num_used / self._num_blocks if self._num_blocks > 0 else 0.0
316 + usage = num_used / self._num_chunks if self._num_chunks > 0 else 0.0
317 stats.set_gauge(CPUOffloadingMetrics.CPU_CACHE_USAGE_PERC, usage)
318
319 for allocation_size in self.allocation_sizes_in_current_batch:
@@ -323,8 +323,8 @@ class CPUOffloadingManager(OffloadingManager):
323 self.allocation_sizes_in_current_batch.clear()
324
325 write_usage = (
326 - self._num_write_pending_blocks / self._num_blocks
327 - if self._num_blocks > 0
326 + self._num_write_pending_chunks / self._num_chunks
327 + if self._num_chunks > 0
328 else 0.0
329 )
330 read_usage = max(usage - write_usage, 0.0)
vllm/v1/kv_offload/cpu/policies/arc.py
+33 -33
@@ -6,7 +6,7 @@ from collections.abc import Iterable, Iterator
6 from typing_extensions import override
7
8 from vllm.v1.kv_offload.base import OffloadKey, ReqContext
9 -from vllm.v1.kv_offload.cpu.policies.base import BlockStatus, CachePolicy
9 +from vllm.v1.kv_offload.cpu.policies.base import CachePolicy, ChunkStatus
10
11
12 class ARCCachePolicy(CachePolicy):
@@ -14,15 +14,15 @@ class ARCCachePolicy(CachePolicy):
14 ARC (Adaptive Replacement Cache) cache policy.
15
16 Data Structures:
17 - T1: Recent cache containing blocks accessed once.
18 - T2: Frequent cache containing blocks accessed multiple times.
19 - B1/B2: Ghost lists tracking recently evicted blocks from T1/T2.
17 + T1: Recent cache containing chunks accessed once.
18 + T2: Frequent cache containing chunks accessed multiple times.
19 + B1/B2: Ghost lists tracking recently evicted chunks from T1/T2.
20 target_t1_size: Adaptive target size for the T1 partition.
21
22 Algorithm Flow:
23 1. Cache lookup (lookup):
24 - Searches T1 and T2 for block hashes and counts consecutive hits
25 - until a miss or non-ready block is encountered.
24 + Searches T1 and T2 for chunk hashes and counts consecutive hits
25 + until a miss or non-ready chunk is encountered.
26
27 2. Cache touch (touch) - Adaptive Learning:
28 For each key (in reverse order):
@@ -31,15 +31,15 @@ class ARCCachePolicy(CachePolicy):
31 - If in B1 ghost list: Increase target_t1_size.
32 - If in B2 ghost list: Decrease target_t1_size.
33
34 - 3. Block eviction (evict) - Adaptive Replacement:
34 + 3. Chunk eviction (evict) - Adaptive Replacement:
35 Determines eviction source based on adaptive target:
36 - If T1 size >= target_t1_size: Evict from T1, add to B1.
37 - Otherwise: Evict from T2, add to B2.
38 Finally, bound each ghost list size.
39
40 - 4. Block insertion (insert):
41 - New blocks are always inserted into T1 and removed from B1/B2 if
42 - present. Blocks may later be promoted to T2 during touch operations.
40 + 4. Chunk insertion (insert):
41 + New chunks are always inserted into T1 and removed from B1/B2 if
42 + present. Chunks may later be promoted to T2 during touch operations.
43
44 Adaptive Behavior:
45 The algorithm self-tunes the recency vs. frequency trade-off:
@@ -50,19 +50,19 @@ class ARCCachePolicy(CachePolicy):
50 def __init__(self, cache_capacity: int):
51 super().__init__(cache_capacity)
52 self.target_t1_size: float = 0.0
53 - self.t1: OrderedDict[OffloadKey, BlockStatus] = OrderedDict()
54 - self.t2: OrderedDict[OffloadKey, BlockStatus] = OrderedDict()
53 + self.t1: OrderedDict[OffloadKey, ChunkStatus] = OrderedDict()
54 + self.t2: OrderedDict[OffloadKey, ChunkStatus] = OrderedDict()
55 # key -> None (only care about presence)
56 self.b1: OrderedDict[OffloadKey, None] = OrderedDict()
57 self.b2: OrderedDict[OffloadKey, None] = OrderedDict()
58
59 @override
60 - def get(self, key: OffloadKey) -> BlockStatus | None:
60 + def get(self, key: OffloadKey) -> ChunkStatus | None:
61 return self.t1.get(key) or self.t2.get(key)
62
63 @override
64 - def insert(self, key: OffloadKey, block: BlockStatus) -> None:
65 - self.t1[key] = block
64 + def insert(self, key: OffloadKey, chunk: ChunkStatus) -> None:
65 + self.t1[key] = chunk
66 self.b1.pop(key, None)
67 self.b2.pop(key, None)
68
@@ -75,13 +75,13 @@ class ARCCachePolicy(CachePolicy):
75 def touch(self, keys: Iterable[OffloadKey], req_context: ReqContext) -> None:
76 for key in reversed(list(keys)):
77 if key in self.t1:
78 - block = self.t1.pop(key)
79 - if not block.is_ready:
80 - # block was just prepared to be stored, not really touched
78 + chunk = self.t1.pop(key)
79 + if not chunk.is_ready:
80 + # chunk was just prepared to be stored, not really touched
81 # twice — keep it in T1 and mark as most recently used
82 - self.t1[key] = block
82 + self.t1[key] = chunk
83 else:
84 - self.t2[key] = block
84 + self.t2[key] = chunk
85
86 elif key in self.t2:
87 self.t2.move_to_end(key)
@@ -111,31 +111,31 @@ class ARCCachePolicy(CachePolicy):
111 @override
112 def evict(
113 self, n: int, protected: set[OffloadKey]
114 - ) -> list[tuple[OffloadKey, BlockStatus]] | None:
114 + ) -> list[tuple[OffloadKey, ChunkStatus]] | None:
115 if n == 0:
116 return []
117
118 # Collect candidates atomically: simulate T1 size changes as we select,
119 # but do not modify actual data structures until all n are found.
120 candidates: list[
121 - tuple[OffloadKey, BlockStatus, bool]
122 - ] = [] # (key, block, from_t1)
121 + tuple[OffloadKey, ChunkStatus, bool]
122 + ] = [] # (key, chunk, from_t1)
123 virtual_t1_size = len(self.t1)
124 # Keep the scans monotonic: restarting from the LRU end after every
125 - # selection makes a batch eviction quadratic in the number of blocks.
125 + # selection makes a batch eviction quadratic in the number of chunks.
126 t1_iter = iter(self.t1.items())
127 t2_iter = iter(self.t2.items())
128
129 def next_candidate(
130 - entries: Iterator[tuple[OffloadKey, BlockStatus]],
131 - ) -> tuple[OffloadKey, BlockStatus] | None:
132 - for key, block in entries:
133 - if block.ref_cnt == 0 and key not in protected:
134 - return key, block
130 + entries: Iterator[tuple[OffloadKey, ChunkStatus]],
131 + ) -> tuple[OffloadKey, ChunkStatus] | None:
132 + for key, chunk in entries:
133 + if chunk.ref_cnt == 0 and key not in protected:
134 + return key, chunk
135 return None
136
137 for _ in range(n):
138 - candidate: tuple[OffloadKey, BlockStatus, bool] | None = None
138 + candidate: tuple[OffloadKey, ChunkStatus, bool] | None = None
139
140 if virtual_t1_size >= int(self.target_t1_size):
141 entry = next_candidate(t1_iter)
@@ -158,15 +158,15 @@ class ARCCachePolicy(CachePolicy):
158 candidates.append(candidate)
159
160 # Apply all evictions now that we know n candidates exist.
161 - result: list[tuple[OffloadKey, BlockStatus]] = []
162 - for key, block, from_t1 in candidates:
161 + result: list[tuple[OffloadKey, ChunkStatus]] = []
162 + for key, chunk, from_t1 in candidates:
163 if from_t1:
164 del self.t1[key]
165 self.b1[key] = None
166 else:
167 del self.t2[key]
168 self.b2[key] = None
169 - result.append((key, block))
169 + result.append((key, chunk))
170
171 # Trim ghost lists to cache_capacity.
172 for ghost in (self.b1, self.b2):
vllm/v1/kv_offload/cpu/policies/base.py
+26 -26
@@ -7,36 +7,36 @@ from collections.abc import Iterable
7 from vllm.v1.kv_offload.base import OffloadKey, ReqContext
8
9
10 -class BlockStatus(ctypes.Structure):
10 +class ChunkStatus(ctypes.Structure):
11 """
12 - Offloading status for a single block of KV data.
12 + Offloading status for a single chunk of KV data.
13 Holds the following information:
14
15 - ref_cnt - the current number of transfers using this block as a source.
16 - A value of -1 indicates the block is not yet ready to be read.
17 - block_id - index of the physical CPU buffer slot.
15 + ref_cnt - the current number of transfers using this chunk as a source.
16 + A value of -1 indicates the chunk is not yet ready to be read.
17 + chunk_id - index of the physical CPU buffer slot.
18 """
19
20 - _fields_ = [("ref_cnt", ctypes.c_int32), ("block_id", ctypes.c_int64)]
20 + _fields_ = [("ref_cnt", ctypes.c_int32), ("chunk_id", ctypes.c_int64)]
21
22 - def __init__(self, block_id: int):
22 + def __init__(self, chunk_id: int):
23 super().__init__()
24 - # initialize block as "not ready" (ref_cnt = -1)
24 + # initialize chunk as "not ready" (ref_cnt = -1)
25 self.ref_cnt = -1
26 - self.block_id = block_id
26 + self.chunk_id = chunk_id
27
28 @property
29 def is_ready(self) -> bool:
30 """
31 - Returns whether the block is ready to be read.
31 + Returns whether the chunk is ready to be read.
32 """
33 return self.ref_cnt >= 0
34
35
36 class CachePolicy(ABC):
37 """
38 - Encapsulates both block organization (data structures) and replacement
39 - decisions (which block to evict). LRU and ARC differ in both dimensions —
38 + Encapsulates both chunk organization (data structures) and replacement
39 + decisions (which chunk to evict). LRU and ARC differ in both dimensions —
40 ARC's ghost lists and target_t1_size live at the intersection of storage
41 and eviction, so they cannot be separated cleanly.
42 """
@@ -45,35 +45,35 @@ class CachePolicy(ABC):
45 self.cache_capacity = cache_capacity
46
47 @abstractmethod
48 - def get(self, key: OffloadKey) -> BlockStatus | None:
49 - """Find block in data structures. Returns None if not present."""
48 + def get(self, key: OffloadKey) -> ChunkStatus | None:
49 + """Find chunk in data structures. Returns None if not present."""
50
51 @abstractmethod
52 - def insert(self, key: OffloadKey, block: BlockStatus) -> None:
53 - """Add a newly allocated block. For ARC: also removes from ghost lists."""
52 + def insert(self, key: OffloadKey, chunk: ChunkStatus) -> None:
53 + """Add a newly allocated chunk. For ARC: also removes from ghost lists."""
54
55 @abstractmethod
56 def remove(self, key: OffloadKey) -> None:
57 - """Remove a block (used to clean up after a failed store)."""
57 + """Remove a chunk (used to clean up after a failed store)."""
58
59 @abstractmethod
60 def touch(self, keys: Iterable[OffloadKey], req_context: ReqContext) -> None:
61 """
62 - Mark blocks as recently used.
62 + Mark chunks as recently used.
63
64 Args:
65 - keys: Blocks to mark as recently used.
66 - req_context: Per-request context for the request touching these blocks.
65 + keys: Chunks to mark as recently used.
66 + req_context: Per-request context for the request touching these chunks.
67 """
68
69 @abstractmethod
70 def evict(
71 self, n: int, protected: set[OffloadKey]
72 - ) -> list[tuple[OffloadKey, BlockStatus]] | None:
72 + ) -> list[tuple[OffloadKey, ChunkStatus]] | None:
73 """
74 - Evict exactly n blocks, skipping any in protected.
74 + Evict exactly n chunks, skipping any in protected.
75
76 - Returns a list of (key, block) for the evicted blocks,
76 + Returns a list of (key, chunk) for the evicted chunks,
77 or None if n evictions cannot be satisfied. The operation is atomic:
78 if None is returned, no state changes are made.
79
@@ -84,15 +84,15 @@ class CachePolicy(ABC):
84 @abstractmethod
85 def clear(self) -> None:
86 """
87 - Remove ALL blocks regardless of ref_cnt.
87 + Remove ALL chunks regardless of ref_cnt.
88
89 Ghost lists and adaptive state are also reset.
90 """
91
92 def mark_evictable(self, key: OffloadKey) -> None:
93 - """Called when a block's ref_cnt transitions to 0."""
93 + """Called when a chunk's ref_cnt transitions to 0."""
94 return
95
96 def mark_non_evictable(self, key: OffloadKey) -> None:
97 - """Called when a block's ref_cnt transitions from 0."""
97 + """Called when a chunk's ref_cnt transitions from 0."""
98 return
vllm/v1/kv_offload/cpu/policies/lru.py
+29 -29
@@ -6,7 +6,7 @@ from collections.abc import Iterable
6 from typing_extensions import override
7
8 from vllm.v1.kv_offload.base import OffloadKey, ReqContext
9 -from vllm.v1.kv_offload.cpu.policies.base import BlockStatus, CachePolicy
9 +from vllm.v1.kv_offload.cpu.policies.base import CachePolicy, ChunkStatus
10
11
12 class LRUCachePolicy(CachePolicy):
@@ -20,71 +20,71 @@ class LRUCachePolicy(CachePolicy):
20
21 def __init__(self, cache_capacity: int):
22 super().__init__(cache_capacity)
23 - # Blocks with ref_cnt 0 (not participating in any loads/stores) ordered in LRU
24 - self.evictable_blocks: OrderedDict[OffloadKey, None] = OrderedDict()
25 - self.blocks: dict[OffloadKey, BlockStatus] = {}
23 + # Chunks with ref_cnt 0 (not participating in any loads/stores) ordered in LRU
24 + self.evictable_chunks: OrderedDict[OffloadKey, None] = OrderedDict()
25 + self.chunks: dict[OffloadKey, ChunkStatus] = {}
26
27 @override
28 - def get(self, key: OffloadKey) -> BlockStatus | None:
29 - return self.blocks.get(key)
28 + def get(self, key: OffloadKey) -> ChunkStatus | None:
29 + return self.chunks.get(key)
30
31 @override
32 - def insert(self, key: OffloadKey, block: BlockStatus) -> None:
33 - self.blocks[key] = block
34 - if block.ref_cnt == 0:
35 - self.evictable_blocks[key] = None
32 + def insert(self, key: OffloadKey, chunk: ChunkStatus) -> None:
33 + self.chunks[key] = chunk
34 + if chunk.ref_cnt == 0:
35 + self.evictable_chunks[key] = None
36
37 @override
38 def remove(self, key: OffloadKey) -> None:
39 - del self.blocks[key]
40 - self.evictable_blocks.pop(key, None)
39 + del self.chunks[key]
40 + self.evictable_chunks.pop(key, None)
41
42 @override
43 def touch(self, keys: Iterable[OffloadKey], req_context: ReqContext) -> None:
44 for key in reversed(list(keys)):
45 - if key in self.evictable_blocks:
46 - self.evictable_blocks.move_to_end(key)
47 - # active blocks are untouched as they are non-evictable now. They
48 - # will eventually reach the end of evictable_blocks when they finish.
45 + if key in self.evictable_chunks:
46 + self.evictable_chunks.move_to_end(key)
47 + # active chunks are untouched as they are non-evictable now. They
48 + # will eventually reach the end of evictable_chunks when they finish.
49
50 @override
51 def clear(self) -> None:
52 - self.evictable_blocks.clear()
53 - self.blocks.clear()
52 + self.evictable_chunks.clear()
53 + self.chunks.clear()
54
55 @override
56 def evict(
57 self, n: int, protected: set[OffloadKey]
58 - ) -> list[tuple[OffloadKey, BlockStatus]] | None:
58 + ) -> list[tuple[OffloadKey, ChunkStatus]] | None:
59 if n == 0:
60 return []
61
62 - candidates: list[tuple[OffloadKey, BlockStatus]] = []
63 - for key, _ in self.evictable_blocks.items():
62 + candidates: list[tuple[OffloadKey, ChunkStatus]] = []
63 + for key, _ in self.evictable_chunks.items():
64 if key in protected:
65 continue
66
67 - block = self.blocks[key]
68 - assert block.ref_cnt == 0
69 - candidates.append((key, block))
67 + chunk = self.chunks[key]
68 + assert chunk.ref_cnt == 0
69 + candidates.append((key, chunk))
70 if len(candidates) == n:
71 break
72
73 if len(candidates) < n:
74 return None
75 for key, _ in candidates:
76 - del self.evictable_blocks[key]
77 - del self.blocks[key]
76 + del self.evictable_chunks[key]
77 + del self.chunks[key]
78 return candidates
79
80 @override
81 def mark_evictable(self, key: OffloadKey) -> None:
82 - # blocks can become evictable when,
82 + # chunks can become evictable when,
83 # store completes - i.e. ref_cnt -1 -> 0 # not in evictable list
84 # all loads complete - i.e ref_cnt 1 -> 0 # not in evictable list
85 - self.evictable_blocks[key] = None
85 + self.evictable_chunks[key] = None
86
87 @override
88 def mark_non_evictable(self, key: OffloadKey) -> None:
89 # key must have been in the evictable list.
90 - del self.evictable_blocks[key]
90 + del self.evictable_chunks[key]
vllm/v1/kv_offload/cpu/shared_offload_region.py
+26 -26
@@ -82,24 +82,24 @@ class SharedOffloadRegion:
82 def __init__(
83 self,
84 engine_id: str,
85 - num_blocks: int,
85 + num_chunks: int,
86 rank: int | None,
87 - kv_bytes_per_block: int,
87 + kv_bytes_per_chunk: int,
88 cpu_page_size: int,
89 barrier: Callable[[], None] | None = None,
90 ) -> None:
91 self.page_size = mmap.PAGESIZE
92 - assert kv_bytes_per_block % self.page_size == 0
92 + assert kv_bytes_per_chunk % self.page_size == 0
93
94 - self.num_blocks = num_blocks
95 - self._row_stride = kv_bytes_per_block
96 - self.total_size_bytes = self.num_blocks * self._row_stride
94 + self.num_chunks = num_chunks
95 + self._row_stride = kv_bytes_per_chunk
96 + self.total_size_bytes = self.num_chunks * self._row_stride
97
98 self.mmap_path = f"/dev/shm/vllm_offload_{engine_id}.mmap"
99 self._creator = False # set True only if this worker creates the file
100 self.rank = rank
101 if rank is not None:
102 - # byte offset to this worker's first slot within each block row
102 + # byte offset to this worker's first slot within each chunk row
103 self._worker_offset = rank * cpu_page_size
104 # exclusive upper bound for this worker's area within each row
105 self._worker_area_end = (rank + 1) * cpu_page_size
@@ -182,19 +182,19 @@ class SharedOffloadRegion:
182 populate_write_fn = _get_populate_write_fn(self.mmap_obj)
183
184 if rank is not None:
185 - # Populate only this worker's pages (one slot per block row).
185 + # Populate only this worker's pages (one slot per chunk row).
186 worker_offset = rank * cpu_page_size
187 _t0 = time.perf_counter()
188 page_size = self.page_size
189 - for block in range(num_blocks):
190 - raw_offset = block * self._row_stride + worker_offset
189 + for chunk in range(num_chunks):
190 + raw_offset = chunk * self._row_stride + worker_offset
191 aligned_offset = (raw_offset // page_size) * page_size
192 end = raw_offset + cpu_page_size
193 aligned_length = end - aligned_offset
194 populate_write_fn(self.mmap_obj, aligned_offset, aligned_length)
195 logger.debug(
196 - "MADV_POPULATE_WRITE loop: %d blocks in %.3f s",
197 - num_blocks,
196 + "MADV_POPULATE_WRITE loop: %d chunks in %.3f s",
197 + num_chunks,
198 time.perf_counter() - _t0,
199 )
200 else:
@@ -215,22 +215,22 @@ class SharedOffloadRegion:
215
216 Must be called once per canonical tensor. The full mmap layout is:
217
218 - worker0_block0 | worker1_block0 | ... | worker{M-1}_block0
219 - worker0_block1 | worker1_block1 | ... | worker{M-1}_block1
218 + worker0_chunk0 | worker1_chunk0 | ... | worker{M-1}_chunk0
219 + worker0_chunk1 | worker1_chunk1 | ... | worker{M-1}_chunk1
220 ...
221
222 - Each worker_block cell is cpu_page_size bytes and holds all canonical
223 - tensors for that worker and block concatenated:
222 + Each worker_chunk cell is cpu_page_size bytes and holds all canonical
223 + tensors for that worker and chunk concatenated:
224 [ tensor0_data | tensor1_data | ... | tensor{L-1}_data ]
225
226 Consecutive rows are separated by row_stride = cpu_page_size * M.
227
228 - Returns an int8 tensor of shape (num_blocks, tensor_page_size) with stride
228 + Returns an int8 tensor of shape (num_chunks, tensor_page_size) with stride
229 (row_stride, 1). Using int8 keeps stride == bytes, so swap_blocks
230 address arithmetic works without any dtype conversion.
231
232 Args:
233 - tensor_page_size: Bytes per block for this tensor.
233 + tensor_page_size: Bytes per chunk for this tensor.
234 """
235 assert self.rank is not None
236 new_offset = self._worker_offset + tensor_page_size
@@ -241,7 +241,7 @@ class SharedOffloadRegion:
241 )
242 worker_layer_view = torch.as_strided(
243 self._base,
244 - size=(self.num_blocks, tensor_page_size),
244 + size=(self.num_chunks, tensor_page_size),
245 stride=(self._row_stride, 1),
246 storage_offset=self._worker_offset,
247 )
@@ -266,8 +266,8 @@ class SharedOffloadRegion:
266 _canonical_offset=0, then advances by each tensor's size
267
268 Each canonical_t{i} cell is that tensor's canonical page for the
269 - block. Canonical areas are carved consecutively from the start of
270 - each block row; consecutive rows are separated by row_stride. Every
269 + chunk. Canonical areas are carved consecutively from the start of
270 + each chunk row; consecutive rows are separated by row_stride. Every
271 worker gets the identical byte ranges and writes only its disjoint
272 bytes within them, as described by its canonical mappings — unlike
273 create_next_worker_view, which gives each worker a private
@@ -279,13 +279,13 @@ class SharedOffloadRegion:
279 where one canonical copy replaces world_size worker copies.
280
281 Args:
282 - tensor_page_size: Canonical bytes per block for this tensor.
282 + tensor_page_size: Canonical bytes per chunk for this tensor.
283 """
284 new_offset = self._canonical_offset + tensor_page_size
285 assert new_offset <= self._row_stride
286 view = torch.as_strided(
287 self._base,
288 - size=(self.num_blocks, tensor_page_size),
288 + size=(self.num_chunks, tensor_page_size),
289 stride=(self._row_stride, 1),
290 storage_offset=self._canonical_offset,
291 )
@@ -296,10 +296,10 @@ class SharedOffloadRegion:
296 def create_kv_memoryview(self) -> memoryview:
297 """Return a zero-copy memoryview over the entire KV buffer.
298
299 - Shape: (num_blocks, row_stride_bytes). Secondary tiers address
300 - block *b* as ``view[b]``.
299 + Shape: (num_chunks, row_stride_bytes). Secondary tiers address
300 + chunk *b* as ``view[b]``.
301 """
302 - kv_tensor = self._base.view(self.num_blocks, self._row_stride)
302 + kv_tensor = self._base.view(self.num_chunks, self._row_stride)
303 np_arr = kv_tensor.numpy()
304 assert np_arr.ctypes.data == self._base.data_ptr(), (
305 "view()/numpy() created a copy instead of sharing the mmap buffer; "
vllm/v1/kv_offload/cpu/spec.py
+14 -14
@@ -73,7 +73,7 @@ class CPUOffloadingSpec(OffloadingSpec):
73 ),
74 CPUOffloadingMetrics.CPU_ALLOCATION_SIZE: OffloadingHistogramMetadata(
75 documentation=(
76 - "Histogram of the number of CPU blocks requested by each "
76 + "Histogram of the number of CPU chunks requested by each "
77 "KV offload prepare_store call."
78 ),
79 buckets=(1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144),
@@ -101,7 +101,7 @@ class CPUOffloadingSpec(OffloadingSpec):
101 )
102
103 world_size = config.parallel.world_size
104 - self.num_blocks = 0
104 + self.num_chunks = 0
105 self.kv_bytes_per_chunk = 0
106 self.cpu_page_size_per_worker = 0
107 self.replicated_layout = config.replicated_layout and self._uses_shared_region()
@@ -113,17 +113,17 @@ class CPUOffloadingSpec(OffloadingSpec):
113 # calculate cpu_page_size_per_worker
114 self.cpu_page_size_per_worker = kv_bytes_per_chunk // num_copies
115
116 - # calculate num_blocks
116 + # calculate num_chunks
117 aligned_kv_bytes_per_chunk = round_up(
118 kv_bytes_per_chunk, self.BLOCK_SIZE_ALIGNMENT
119 )
120 - self.num_blocks = int(cpu_bytes_to_use) // aligned_kv_bytes_per_chunk
120 + self.num_chunks = int(cpu_bytes_to_use) // aligned_kv_bytes_per_chunk
121
122 # Expose aligned_kv_bytes_per_chunk as
123 # kv_bytes_per_chunk. Note that this might contain
124 - # some padding. i.e. each offloaded block is of the form,
125 - # |--- W0-B0---|---- W1-B0---| ... |---- Wn-B0---| *** maybe-pad *** |
126 - # or |--- B0 (single copy) ---| *** maybe-pad *** |
124 + # some padding. i.e. each offloaded chunk is of the form,
125 + # |--- W0-C0---|---- W1-C0---| ... |---- Wn-C0---| *** maybe-pad *** |
126 + # or |--- C0 (single copy) ---| *** maybe-pad *** |
127 self.kv_bytes_per_chunk = aligned_kv_bytes_per_chunk
128
129 # scheduler-side
@@ -140,7 +140,7 @@ class CPUOffloadingSpec(OffloadingSpec):
140 @override
141 def get_manager(self) -> OffloadingManager:
142 if not self._manager:
143 - # store_threshold: how many times a block must be offered for
143 + # store_threshold: how many times a chunk must be offered for
144 # storage before it is eligible for CPU offloading. Values < 2
145 # disable filtering (a threshold of 1 equals no filter; 0 is the
146 # default).
@@ -150,7 +150,7 @@ class CPUOffloadingSpec(OffloadingSpec):
150 max_tracker_size = int(self.extra_config.get("max_tracker_size", 64_000))
151
152 self._manager = CPUOffloadingManager(
153 - num_blocks=self.num_blocks,
153 + num_chunks=self.num_chunks,
154 cache_policy=self.eviction_policy,
155 cache_policy_module_path=self.cache_policy_module_path,
156 enable_events=self.kv_events_config.enable_kv_cache_events,
@@ -166,9 +166,9 @@ class CPUOffloadingSpec(OffloadingSpec):
166
167 def create_worker(self, kv_caches: CanonicalKVCaches) -> CPUOffloadingWorker:
168 mmap_region: SharedOffloadRegion | None = None
169 - # num_blocks == 0 would size the region to zero bytes, which cannot be
169 + # num_chunks == 0 would size the region to zero bytes, which cannot be
170 # mmap'd; fall back to the tensor path (empty tensors) as before.
171 - if self._uses_shared_region() and self.num_blocks > 0:
171 + if self._uses_shared_region() and self.num_chunks > 0:
172 # Replicated layout puts all ranks on slot 0 (single MLA copy);
173 # otherwise each rank takes its own slot by physical device index.
174 if self.replicated_layout:
@@ -178,9 +178,9 @@ class CPUOffloadingSpec(OffloadingSpec):
178 rank = torch.accelerator.current_device_index() % world_size
179 mmap_region = SharedOffloadRegion(
180 engine_id=self.config.engine_id,
181 - num_blocks=self.num_blocks,
181 + num_chunks=self.num_chunks,
182 rank=rank,
183 - kv_bytes_per_block=self.kv_bytes_per_chunk,
183 + kv_bytes_per_chunk=self.kv_bytes_per_chunk,
184 cpu_page_size=self.cpu_page_size_per_worker,
185 barrier=_all_workers_barrier,
186 )
@@ -188,7 +188,7 @@ class CPUOffloadingSpec(OffloadingSpec):
188 return CPUOffloadingWorker(
189 kv_caches=kv_caches,
190 blocks_per_chunk=self.blocks_per_chunk,
191 - num_cpu_blocks=self.num_blocks,
191 + num_cpu_chunks=self.num_chunks,
192 mmap_region=mmap_region,
193 )
194 except Exception:
vllm/v1/kv_offload/tiering/base.py
+23 -23
@@ -45,8 +45,8 @@ class TieringOffloadingMetrics:
45 WRITE_TIME = "vllm:kv_offload_tiering_write_time"
46 PROMOTION_JOB_FAILURES = "vllm:kv_offload_tiering_promotion_job_failures"
47 CASCADE_JOB_FAILURES = "vllm:kv_offload_tiering_cascade_job_failures"
48 - BLOCK_QUERIES = "vllm:kv_offload_tiering_chunk_queries"
49 - BLOCK_HITS = "vllm:kv_offload_tiering_chunk_hits"
48 + CHUNK_QUERIES = "vllm:kv_offload_tiering_chunk_queries"
49 + CHUNK_HITS = "vllm:kv_offload_tiering_chunk_hits"
50 PRIMARY_WRITE_USAGE_PERC = "vllm:kv_offload_tiering_primary_write_usage_perc"
51 PRIMARY_READ_USAGE_PERC = "vllm:kv_offload_tiering_primary_read_usage_perc"
52 PROMOTION_ALLOCATION_FAILURES = (
@@ -62,7 +62,7 @@ class TransferJob:
62
63 job_id: JobId
64 keys: Collection[OffloadKey]
65 - block_ids: np.ndarray
65 + chunk_ids: np.ndarray
66 is_promotion: bool
67 req_context: ReqContext
68
@@ -90,14 +90,14 @@ class ParentManager(ABC):
90
91 Required call sequence for each remote request:
92 1. on_new_request(req_context) — set up per-request state
93 - 2. lookup(key, req_context) — check block availability
94 - (repeat per block)
95 - 3. create_store_job(keys, req_context) — pin blocks and get a
93 + 2. lookup(key, req_context) — check chunk availability
94 + (repeat per chunk)
95 + 3. create_store_job(keys, req_context) — pin chunks and get a
96 job handle
97 4. on_request_finished(req_context) — clean up per-request state
98
99 Steps 2-3 may be interleaved. Step 4 must be called even if no
100 - blocks were found, to avoid leaking async lookup state (e.g. in
100 + chunks were found, to avoid leaking async lookup state (e.g. in
101 the fs tier's AsyncLookupManager).
102 """
103
@@ -155,23 +155,23 @@ class SecondaryTierManager(ABC):
155 @abstractmethod
156 def lookup(self, key: OffloadKey, req_context: ReqContext) -> LookupResult:
157 """
158 - Check whether a block exists in this secondary tier.
158 + Check whether a chunk exists in this secondary tier.
159
160 Args:
161 key: Offload key to look up.
162 req_context: per-request context (e.g. kv_transfer_params).
163
164 Returns:
165 - HIT if the block is present and ready,
165 + HIT if the chunk is present and ready,
166 MISS if not found,
167 - or RETRY if the block is being transferred (retry later).
167 + or RETRY if the chunk is being transferred (retry later).
168 """
169 pass
170
171 @abstractmethod
172 def submit_store(self, job_metadata: TransferJob) -> None:
173 """
174 - Submit an async job to store blocks from the primary tier to this
174 + Submit an async job to store chunks from the primary tier to this
175 secondary tier.
176
177 This method must be lightweight and non-blocking: allocate metadata
@@ -179,19 +179,19 @@ class SecondaryTierManager(ABC):
179 calling thread.
180
181 Preconditions (guaranteed by the framework):
182 - - ``job_metadata.block_ids`` are valid primary-tier slots, pinned
182 + - ``job_metadata.chunk_ids`` are valid primary-tier slots, pinned
183 (ref-counted) for the duration of the transfer.
184
185 The implementation is responsible for:
186 - 1. Filtering out blocks already present in this tier
187 - 2. Evicting blocks if capacity is needed
186 + 1. Filtering out chunks already present in this tier
187 + 2. Evicting chunks if capacity is needed
188 3. Allocating space in this tier
189 - 4. Submitting the async transfer (read from primary via block_ids)
189 + 4. Submitting the async transfer (read from primary via chunk_ids)
190
191 Report completion via ``get_finished_jobs()``.
192
193 Args:
194 - job_metadata: Job metadata including job_id, keys, and block_ids
194 + job_metadata: Job metadata including job_id, keys, and chunk_ids
195 identifying the primary-tier slots to read from.
196 """
197 pass
@@ -199,24 +199,24 @@ class SecondaryTierManager(ABC):
199 @abstractmethod
200 def submit_load(self, job_metadata: TransferJob) -> None:
201 """
202 - Submit an async job to load blocks from this secondary tier to the
202 + Submit an async job to load chunks from this secondary tier to the
203 primary tier.
204
205 - This method must be lightweight and non-blocking: mark blocks as
205 + This method must be lightweight and non-blocking: mark chunks as
206 in-flight and submit the transfer, but do NOT perform the data copy
207 on the calling thread.
208
209 Preconditions (guaranteed by the framework):
210 - - ``job_metadata.block_ids`` are allocated primary-tier slots
210 + - ``job_metadata.chunk_ids`` are allocated primary-tier slots
211 ready to receive data.
212
213 The implementation must copy data from this tier into the
214 - primary-tier slots identified by ``block_ids``.
214 + primary-tier slots identified by ``chunk_ids``.
215
216 Report completion via ``get_finished_jobs()``.
217
218 Args:
219 - job_metadata: Job metadata including job_id, keys, and block_ids
219 + job_metadata: Job metadata including job_id, keys, and chunk_ids
220 identifying the primary-tier slots to write into.
221 """
222 pass
@@ -249,7 +249,7 @@ class SecondaryTierManager(ABC):
249
250 def touch(self, keys: Collection[OffloadKey], req_context: ReqContext):
251 """
252 - Mark blocks as recently used for eviction policy.
252 + Mark chunks as recently used for eviction policy.
253
254 Args:
255 keys: Offload keys to mark as recently used.
@@ -263,7 +263,7 @@ class SecondaryTierManager(ABC):
263 Called when a new request is first seen by the scheduler.
264
265 Returns a RequestOffloadingContext expressing this tier's preference
266 - for how blocks should be offloaded for this request.
266 + for how chunks should be offloaded for this request.
267
268 Args:
269 req_context: Per-request context.
vllm/v1/kv_offload/tiering/example/manager.py
+22 -22
@@ -3,7 +3,7 @@
3 """
4 ExampleSecondaryTierManager: A simple in-memory secondary tier.
5
6 -This implementation provides a minimal secondary tier that stores blocks
6 +This implementation provides a minimal secondary tier that stores chunks
7 in memory (using a dictionary) with immediate completion. It serves as a
8 reference for writing new tiers and is useful for testing the
9 TieringOffloadingManager without requiring actual storage or network backends.
@@ -39,7 +39,7 @@ class ExampleSecondaryTierManager(SecondaryTierManager):
39 A simple in-memory secondary tier.
40
41 This implementation:
42 - - Stores blocks in a dictionary (key -> True)
42 + - Stores chunks in a dictionary (key -> True)
43 - Completes transfers immediately (synchronous)
44 """
45
@@ -69,45 +69,45 @@ class ExampleSecondaryTierManager(SecondaryTierManager):
69 )
70
71 # key -> True (only care about presence)
72 - self.blocks: dict[OffloadKey, bool] = {}
72 + self.chunks: dict[OffloadKey, bool] = {}
73
74 # Completed jobs waiting to be retrieved by get_finished_jobs()
75 self.completed_jobs: list[JobResult] = []
76 assert primary_kv_view.strides is not None
77 - self._block_size = primary_kv_view.strides[0]
77 + self._chunk_size = primary_kv_view.strides[0]
78
79 @override
80 def lookup(self, key: OffloadKey, req_context: ReqContext) -> LookupResult:
81 """
82 - Check whether a block exists in this secondary tier.
82 + Check whether a chunk exists in this secondary tier.
83
84 Args:
85 key: Offload key to look up.
86 req_context: Per-request context.
87
88 Returns:
89 - HIT if the block is present, MISS if not found.
89 + HIT if the chunk is present, MISS if not found.
90 """
91 - return LookupResult.HIT if key in self.blocks else LookupResult.MISS
91 + return LookupResult.HIT if key in self.chunks else LookupResult.MISS
92
93 @override
94 def submit_store(self, job_metadata: TransferJob) -> None:
95 """
96 - Submit a job to store blocks from primary tier to this tier.
96 + Submit a job to store chunks from primary tier to this tier.
97
98 Args:
99 job_metadata: Job metadata including job_id, keys, and
100 - spec for reading blocks from the primary tier.
100 + spec for reading chunks from the primary tier.
101 """
102 keys = job_metadata.keys
103 - block_ids = job_metadata.block_ids
103 + chunk_ids = job_metadata.chunk_ids
104
105 - assert len(keys) == len(block_ids), (
106 - f"Length mismatch: {len(keys)} keys but {len(block_ids)} block_ids"
105 + assert len(keys) == len(chunk_ids), (
106 + f"Length mismatch: {len(keys)} keys but {len(chunk_ids)} chunk_ids"
107 )
108
109 for key in keys:
110 - self.blocks[key] = True
110 + self.chunks[key] = True
111 self.completed_jobs.append(
112 JobResult(
113 job_id=job_metadata.job_id,
@@ -119,21 +119,21 @@ class ExampleSecondaryTierManager(SecondaryTierManager):
119 @override
120 def submit_load(self, job_metadata: TransferJob) -> None:
121 """
122 - Submit a job to load blocks from this tier to primary tier.
122 + Submit a job to load chunks from this tier to primary tier.
123
124 Args:
125 job_metadata: Job metadata including job_id, keys, and
126 - spec for writing blocks into the primary tier.
126 + spec for writing chunks into the primary tier.
127 """
128 keys = job_metadata.keys
129 - block_ids = job_metadata.block_ids
129 + chunk_ids = job_metadata.chunk_ids
130
131 - assert len(keys) == len(block_ids), (
132 - f"Length mismatch: {len(keys)} keys but {len(block_ids)} block_ids"
131 + assert len(keys) == len(chunk_ids), (
132 + f"Length mismatch: {len(keys)} keys but {len(chunk_ids)} chunk_ids"
133 )
134
135 for key in keys:
136 - if key not in self.blocks:
136 + if key not in self.chunks:
137 self.completed_jobs.append(
138 JobResult(job_id=job_metadata.job_id, success=False)
139 )
@@ -170,6 +170,6 @@ class ExampleSecondaryTierManager(SecondaryTierManager):
170 completes, so there is nothing to wait for."""
171 return
172
173 - def get_num_blocks(self) -> int:
174 - """Get the number of blocks currently stored in this tier."""
175 - return len(self.blocks)
173 + def get_num_chunks(self) -> int:
174 + """Get the number of chunks currently stored in this tier."""
175 + return len(self.chunks)
vllm/v1/kv_offload/tiering/fs/manager.py
+2 -2
@@ -223,7 +223,7 @@ class FileSystemTierManager(SecondaryTierManager):
223 batch_store_block,
224 [self.file_mapper.get_file_name(key) for key in keys],
225 self._primary_kv_view,
226 - [int(bid) * self._block_size for bid in job_metadata.block_ids],
226 + [int(cid) * self._block_size for cid in job_metadata.chunk_ids],
227 self._block_size,
228 self._use_o_direct,
229 )
@@ -237,7 +237,7 @@ class FileSystemTierManager(SecondaryTierManager):
237 keys = list(job_metadata.keys)
238 self._load_job_keys[job_id] = keys
239 paths = [self.file_mapper.get_file_name(key) for key in keys]
240 - offsets = [int(bid) * self._block_size for bid in job_metadata.block_ids]
240 + offsets = [int(cid) * self._block_size for cid in job_metadata.chunk_ids]
241
242 def load_task() -> None:
243 try:
vllm/v1/kv_offload/tiering/manager.py
+64 -64
@@ -8,16 +8,16 @@ and zero or more secondary tiers (Storage, Network, etc.) to provide
8 hierarchical KV cache offloading.
9
10 Key Design Principles:
11 -1. Always offload to all tiers — When a block is stored to the primary tier,
11 +1. Always offload to all tiers — When a chunk is stored to the primary tier,
12 it is cascaded to ALL secondary tiers
13 2. Primary tier is the gateway — Secondary tiers cannot access GPU memory
14 directly; all data flows through the CPU primary tier
15 -3. Staged promotion — Blocks in secondary tiers must be promoted to the
15 +3. Staged promotion — Chunks in secondary tiers must be promoted to the
16 primary tier before GPU can access them
17 4. Transparent retry mechanism — Return None from lookup() to signal
18 "data is being promoted, try later"
19 5. ref_cnt as eviction protection — primary.prepare_read() increments ref_cnt,
20 - protecting blocks from eviction until complete_read() is called
20 + protecting chunks from eviction until complete_read() is called
21 """
22
23 import time
@@ -61,11 +61,11 @@ logger = init_logger(__name__)
61
62 @dataclass
63 class PendingPromotion:
64 - """Accumulator for blocks awaiting submit_load() for one (tier, request)."""
64 + """Accumulator for chunks awaiting submit_load() for one (tier, request)."""
65
66 req_context: ReqContext
67 keys: list[OffloadKey] = field(default_factory=list)
68 - block_ids: list[int] = field(default_factory=list)
68 + chunk_ids: list[int] = field(default_factory=list)
69
70
71 @dataclass(slots=True)
@@ -94,14 +94,14 @@ class CPUPrimaryTierOffloadingManager(CPUOffloadingManager):
94
95 def __init__(
96 self,
97 - num_blocks: int,
97 + num_chunks: int,
98 mmap_region: SharedOffloadRegion,
99 cache_policy: str = "lru",
100 cache_policy_module_path: str | None = None,
101 enable_events: bool = False,
102 ):
103 super().__init__(
104 - num_blocks=num_blocks,
104 + num_chunks=num_chunks,
105 cache_policy=cache_policy,
106 cache_policy_module_path=cache_policy_module_path,
107 enable_events=enable_events,
@@ -120,9 +120,9 @@ class CPUPrimaryTierOffloadingManager(CPUOffloadingManager):
120 def get_kv_memoryview(self) -> memoryview:
121 """Return the memoryview over the primary tier's KV cache buffer.
122
123 - The view has shape (num_blocks, row_stride_bytes) and is backed by the
124 - SharedOffloadRegion mmap. Secondary tiers address block *b* as
125 - ``view[b]``.
123 + The view has shape (num_chunks, row_stride_bytes) and is backed by the
124 + SharedOffloadRegion mmap. Secondary tiers address chunk *c* as
125 + ``view[c]``.
126 """
127 return self._kv_memoryview
128
@@ -206,14 +206,14 @@ class TieringOffloadingManager(OffloadingManager):
206 assert primary_view.strides is not None
207 self._metrics = TieringMetricsTracker(
208 tier_types=[tier.tier_type for tier in self.secondary_tiers],
209 - num_primary_blocks=self.primary_tier._num_blocks,
210 - primary_block_size=primary_view.strides[0],
209 + num_primary_chunks=self.primary_tier._num_chunks,
210 + primary_chunk_size=primary_view.strides[0],
211 )
212
213 # Pending promotion requests accumulated during lookup() calls; flushed
214 # as one batched submit_load() per (tier, request) in on_schedule_end().
215 # Outer key: tier index. Inner key: req_context.req_id — the same ReqContext
216 - # object is reused for all block lookups of a given request per engine step.
216 + # object is reused for all chunk lookups of a given request per engine step.
217 self._pending_load_submissions: dict[int, dict[str, PendingPromotion]] = {}
218
219 # Gate for once-per-step execution of _maybe_process_finished_jobs().
@@ -305,7 +305,7 @@ class TieringOffloadingManager(OffloadingManager):
305 2. For completed stores (primary→secondary): calls primary.complete_read()
306 to decrement ref_cnt
307 3. For completed loads (secondary→primary): calls primary.complete_write()
308 - to make blocks available
308 + to make chunks available
309 """
310 for i, tier in enumerate(self.secondary_tiers):
311 for completed_job in tier.get_finished_jobs():
@@ -324,11 +324,11 @@ class TieringOffloadingManager(OffloadingManager):
324
325 if transfer_job.is_promotion:
326 # secondary→primary transfer (promotion) completed.
327 - # Make blocks available in primary tier.
327 + # Make chunks available in primary tier.
328 self._complete_promotion(job_metadata, completed_job)
329 else:
330 # primary→secondary transfer completed.
331 - # Decrement ref_cnt on primary blocks.
331 + # Decrement ref_cnt on primary chunks.
332 self.primary_tier.complete_read(
333 transfer_job.keys, transfer_job.req_context
334 )
@@ -342,7 +342,7 @@ class TieringOffloadingManager(OffloadingManager):
342 exclude_tier_idx: int | None = None,
343 ) -> LookupResult:
344 """
345 - Check whether a single block is offloaded and ready.
345 + Check whether a single chunk is offloaded and ready.
346
347 Algorithm:
348 1. Process any completed async jobs first.
@@ -351,20 +351,20 @@ class TieringOffloadingManager(OffloadingManager):
351 hit and initiate promotion.
352
353 Args:
354 - key: Block hash to look up.
354 + key: Chunk hash to look up.
355 req_context: Per-request context.
356
357 Returns:
358 - HIT — block is ready in the primary tier.
359 - HIT_PENDING — block found but not yet readable (write
358 + HIT — chunk is ready in the primary tier.
359 + HIT_PENDING — chunk found but not yet readable (write
360 in-flight on the primary tier).
361 RETRY — promotion started or a secondary tier is busy.
362 - MISS — block not found in any tier, or primary is full
362 + MISS — chunk not found in any tier, or primary is full
363 and cannot accept a promotion.
364 """
365 # Poll first so a promotion that finished since the last call is
366 # already reflected as HIT (not stale HIT_PENDING/MISS) below, and
367 - # so blocks freed by cascade or promotion completions are evictable
367 + # so chunks freed by cascade or promotion completions are evictable
368 # in time for a promotion this lookup may initiate.
369 self._maybe_process_finished_jobs()
370
@@ -424,30 +424,30 @@ class TieringOffloadingManager(OffloadingManager):
424 req_context: ReqContext,
425 ) -> bool:
426 """
427 - Queue a block for promotion from a secondary tier to the primary tier.
427 + Queue a chunk for promotion from a secondary tier to the primary tier.
428
429 Allocates space in the primary tier immediately (sets ref_cnt=-1 so
430 subsequent lookups within the same step see the slot as in-flight),
431 then defers the actual submit_load() call to _flush_pending_promotions()
432 - so all blocks queued during one engine step are submitted as a single
432 + so all chunks queued during one engine step are submitted as a single
433 batched job.
434
435 Args:
436 tier_idx: The secondary tier index to promote from
437 - key: Block to promote
437 + key: Chunk to promote
438 req_context: Per-request context forwarded to primary.prepare_write().
439
440 Returns:
441 True if promotion was initiated, False if primary tier is full.
442 """
443 - # Allocate space in primary tier for promoted block.
443 + # Allocate space in primary tier for promoted chunk.
444 # Must happen immediately so primary.lookup() returns None (in-flight)
445 # for this key on any subsequent lookup() call within the same step,
446 # preventing duplicate promotion attempts.
447 primary_write_result = self.primary_tier.prepare_write([key], req_context)
448
449 if primary_write_result is None:
450 - # Primary tier is full; caller should treat the block as unavailable
450 + # Primary tier is full; caller should treat the chunk as unavailable
451 # rather than retrying indefinitely.
452 self._metrics.on_promotion_allocation_failure()
453 return False
@@ -455,16 +455,16 @@ class TieringOffloadingManager(OffloadingManager):
455 store_spec = primary_write_result.store_spec
456 assert isinstance(store_spec, CPULoadStoreSpec)
457 # Defer submit_load to on_schedule_end(). Group by (tier, request) so
458 - # each request's blocks are submitted as one batched job per tier.
458 + # each request's chunks are submitted as one batched job per tier.
459 tier_pending = self._pending_load_submissions.setdefault(tier_idx, {})
460 ctx_id = req_context.req_id
461 if ctx_id not in tier_pending:
462 tier_pending[ctx_id] = PendingPromotion(
463 - keys=[], block_ids=[], req_context=req_context
463 + keys=[], chunk_ids=[], req_context=req_context
464 )
465 entry = tier_pending[ctx_id]
466 entry.keys.extend(primary_write_result.keys_to_store)
467 - entry.block_ids.extend(store_spec.block_ids)
467 + entry.chunk_ids.extend(store_spec.chunk_ids)
468 return True
469
470 def _flush_pending_promotions(self) -> None:
@@ -483,7 +483,7 @@ class TieringOffloadingManager(OffloadingManager):
483 job_metadata = TransferJob(
484 job_id=job_id,
485 keys=entry.keys,
486 - block_ids=np.array(entry.block_ids, dtype=np.int32),
486 + chunk_ids=np.array(entry.chunk_ids, dtype=np.int32),
487 is_promotion=True,
488 req_context=entry.req_context,
489 )
@@ -497,16 +497,16 @@ class TieringOffloadingManager(OffloadingManager):
497 self, keys: Collection[OffloadKey], req_context: ReqContext
498 ) -> LoadStoreSpec:
499 """
500 - Prepare blocks to be loaded from primary tier to GPU.
500 + Prepare chunks to be loaded from primary tier to GPU.
501
502 Callers only pass keys already confirmed HIT by lookup() earlier this
503 step.
504
505 - This increments ref_cnt on the blocks in the primary tier, protecting
505 + This increments ref_cnt on the chunks in the primary tier, protecting
506 them from eviction during the transfer.
507
508 Args:
509 - keys: Blocks to prepare for loading.
509 + keys: Chunks to prepare for loading.
510 req_context: Per-request context.
511
512 Returns:
@@ -517,10 +517,10 @@ class TieringOffloadingManager(OffloadingManager):
517 @override
518 def touch(self, keys: Collection[OffloadKey], req_context: ReqContext):
519 """
520 - Mark blocks as recently used in all tiers.
520 + Mark chunks as recently used in all tiers.
521
522 Args:
523 - keys: Blocks to mark as recently used.
523 + keys: Chunks to mark as recently used.
524 req_context: Per-request context.
525 """
526 self.primary_tier.touch(keys, req_context)
@@ -530,13 +530,13 @@ class TieringOffloadingManager(OffloadingManager):
530 @override
531 def complete_load(self, keys: Collection[OffloadKey], req_context: ReqContext):
532 """
533 - Mark blocks as done loading from primary tier to GPU.
533 + Mark chunks as done loading from primary tier to GPU.
534
535 - This decrements ref_cnt on the blocks in the primary tier, allowing
535 + This decrements ref_cnt on the chunks in the primary tier, allowing
536 them to be evicted again.
537
538 Args:
539 - keys: Blocks that finished loading.
539 + keys: Chunks that finished loading.
540 req_context: Per-request context.
541 """
542 self.primary_tier.complete_load(keys, req_context)
@@ -546,37 +546,37 @@ class TieringOffloadingManager(OffloadingManager):
546 self, keys: Collection[OffloadKey], req_context: ReqContext
547 ) -> PrepareStoreOutput | None:
548 """
549 - Prepare blocks to be stored from GPU to primary tier.
549 + Prepare chunks to be stored from GPU to primary tier.
550
551 CRITICAL: This method calls _maybe_process_finished_jobs() FIRST to ensure
552 that any completed async transfers have their ref_cnt decremented
553 before the primary tier makes eviction decisions.
554
555 - For request-level tiers, blocks already present in the primary tier
555 + For request-level tiers, chunks already present in the primary tier
556 are immediately cascaded via submit_store().
557
558 Args:
559 - keys: Blocks to prepare for storing.
559 + keys: Chunks to prepare for storing.
560 req_context: Per-request context.
561
562 Returns:
563 - PrepareStoreOutput describing where to store blocks and what was
563 + PrepareStoreOutput describing where to store chunks and what was
564 evicted, or None if store cannot proceed.
565 """
566 # Step 1: Poll for completed async jobs FIRST
567 # _process_finished_jobs() handles two kinds of completions here:
568 # - Cascade completions (store to a secondary tier, either a local
569 # cascade or a store job created for a remote requester via
570 - # create_store_job()): decrements ref_cnt on the primary blocks
570 + # create_store_job()): decrements ref_cnt on the primary chunks
571 # that were read, making them evictable again once ref_cnt hits 0.
572 # - Promotion completions (secondary->primary loads): sets a
573 - # not-yet-ready block's ref_cnt from -1 to 0 via complete_write(),
573 + # not-yet-ready chunk's ref_cnt from -1 to 0 via complete_write(),
574 # making it evictable for the first time.
575 # Both must be accounted for before the eviction decision below.
576 self._maybe_process_finished_jobs()
577
578 - # Step 2: Store to primary tier (new blocks only).
579 - # Cascading of these newly-stored blocks to ALL secondary tiers
578 + # Step 2: Store to primary tier (new chunks only).
579 + # Cascading of these newly-stored chunks to ALL secondary tiers
580 # happens later in complete_store(), after the GPU→Primary transfer
581 # completes.
582 primary_result = self.primary_tier.prepare_store(keys, req_context)
@@ -588,7 +588,7 @@ class TieringOffloadingManager(OffloadingManager):
588 state = self._req_state[req_context.req_id]
589 state.pending_primary_stores += 1
590
591 - # Step 3: For request-level tiers, cascade blocks already in primary
591 + # Step 3: For request-level tiers, cascade chunks already in primary
592 request_level_tiers = self._req_state[req_context.req_id].request_level_tiers
593 if request_level_tiers:
594 keys_to_store_set = set(primary_result.keys_to_store)
@@ -596,13 +596,13 @@ class TieringOffloadingManager(OffloadingManager):
596 k for k in keys if k not in keys_to_store_set
597 )
598 if keys_already_in_primary:
599 - self._cascade_existing_blocks_to_request_level_tiers(
599 + self._cascade_existing_chunks_to_request_level_tiers(
600 keys_already_in_primary, req_context, request_level_tiers
601 )
602
603 return primary_result
604
605 - def _cascade_existing_blocks_to_request_level_tiers(
605 + def _cascade_existing_chunks_to_request_level_tiers(
606 self,
607 keys: Sequence[OffloadKey],
608 req_context: ReqContext,
@@ -610,7 +610,7 @@ class TieringOffloadingManager(OffloadingManager):
610 ) -> None:
611 """
612 For tiers that requested request-level policy, submit_store() for
613 - blocks that are already present in the primary tier.
613 + chunks that are already present in the primary tier.
614
615 A key whose primary write is still in flight (HIT_PENDING) cannot be
616 dropped: prepare_store already excluded it as present, and the
@@ -653,7 +653,7 @@ class TieringOffloadingManager(OffloadingManager):
653 continue
654 assert state.request_level_tiers
655 keys, state.pending_cascade_keys = state.pending_cascade_keys, []
656 - self._cascade_existing_blocks_to_request_level_tiers(
656 + self._cascade_existing_chunks_to_request_level_tiers(
657 keys, state.req_context, state.request_level_tiers
658 )
659 self._maybe_finalize_request(req_id)
@@ -666,30 +666,30 @@ class TieringOffloadingManager(OffloadingManager):
666 success: bool = True,
667 ) -> None:
668 """
669 - Mark blocks as done storing from GPU to primary tier.
669 + Mark chunks as done storing from GPU to primary tier.
670
671 - This is where secondary tier cascading happens — after blocks are
671 + This is where secondary tier cascading happens — after chunks are
672 confirmed to be in the primary tier, they are cascaded to ALL
673 secondary tiers.
674
675 For each secondary tier:
676 1. Call primary.prepare_read() to get LoadStoreSpec AND increment
677 - ref_cnt (protecting blocks during async transfer)
677 + ref_cnt (protecting chunks during async transfer)
678 2. Call tier.submit_store() to start async transfer: primary→secondary
679 3. Track the job in _store_jobs dictionary
680
681 Args:
682 - keys: Blocks that finished storing.
682 + keys: Chunks that finished storing.
683 success: Whether the GPU→primary transfer succeeded.
684 req_context: Per-request context forwarded to primary.prepare_read().
685 """
686 - # Step 1: Complete store in primary tier (makes blocks loadable)
686 + # Step 1: Complete store in primary tier (makes chunks loadable)
687 self.primary_tier.complete_store(keys, req_context, success)
688
689 if success:
690 # Step 2: Cascade to ALL secondary tiers
691 # For each secondary tier, call primary.prepare_read() to get the
692 - # LoadStoreSpec AND to increment ref_cnt (protecting blocks from
692 + # LoadStoreSpec AND to increment ref_cnt (protecting chunks from
693 # eviction during the async transfer). One prepare_read() call per
694 # secondary tier.
695 for tier_idx, tier in enumerate(self.secondary_tiers):
@@ -710,22 +710,22 @@ class TieringOffloadingManager(OffloadingManager):
710 req_context: ReqContext,
711 tier_idx: int = 0,
712 ) -> TransferJob:
713 - """Pin blocks in the primary tier and create a tracked store job.
713 + """Pin chunks in the primary tier and create a tracked store job.
714
715 - Calls prepare_read() to increment ref_cnt (protecting blocks
715 + Calls prepare_read() to increment ref_cnt (protecting chunks
716 from eviction during the async transfer), allocates a job ID,
717 and registers the job in _jobs.
718
719 The caller is responsible for the actual data transfer and
720 reporting completion via get_finished_jobs().
721 """
722 - primary_blocks_spec = self.primary_tier.prepare_read(keys, req_context)
723 - assert isinstance(primary_blocks_spec, CPULoadStoreSpec)
722 + primary_chunks_spec = self.primary_tier.prepare_read(keys, req_context)
723 + assert isinstance(primary_chunks_spec, CPULoadStoreSpec)
724 job_id = self._next_job_id()
725 job_metadata = TransferJob(
726 job_id=job_id,
727 keys=keys,
728 - block_ids=primary_blocks_spec.block_ids,
728 + chunk_ids=primary_chunks_spec.chunk_ids,
729 is_promotion=False,
730 req_context=req_context,
731 )
@@ -760,7 +760,7 @@ class TieringOffloadingManager(OffloadingManager):
760 policy = (
761 OffloadPolicy.REQUEST_LEVEL
762 if state.request_level_tiers
763 - else OffloadPolicy.BLOCK_LEVEL
763 + else OffloadPolicy.CHUNK_LEVEL
764 )
765 return RequestOffloadingContext(policy=policy)
766
vllm/v1/kv_offload/tiering/metrics.py
+23 -23
@@ -39,20 +39,20 @@ class _RequestMetricsState:
39 class _TierState:
40 active_promotion_count: int = 0
41 active_cascade_count: int = 0
42 - primary_write_block_count: int = 0
43 - primary_read_block_count: int = 0
42 + primary_write_chunk_count: int = 0
43 + primary_read_chunk_count: int = 0
44
45
46 class TieringMetricsTracker:
47 def __init__(
48 self,
49 tier_types: list[str],
50 - num_primary_blocks: int,
51 - primary_block_size: int,
50 + num_primary_chunks: int,
51 + primary_chunk_size: int,
52 ) -> None:
53 self._tier_types = tier_types
54 - self._num_primary_blocks = num_primary_blocks
55 - self._primary_block_size = primary_block_size
54 + self._num_primary_chunks = num_primary_chunks
55 + self._primary_chunk_size = primary_chunk_size
56 self._request_states: dict[str, _RequestMetricsState] = {}
57 self._tier_states = [_TierState() for _ in tier_types]
58 self._stats = OffloadingConnectorStats()
@@ -111,13 +111,13 @@ class TieringMetricsTracker:
111 def on_job_registered(self, job_metadata: _JobMetadataLike) -> None:
112 transfer_job = job_metadata.transfer_job
113 state = self._tier_states[job_metadata.tier_idx]
114 - block_count = len(transfer_job.block_ids)
114 + chunk_count = len(transfer_job.chunk_ids)
115 if transfer_job.is_promotion:
116 state.active_promotion_count += 1
117 - state.primary_write_block_count += block_count
117 + state.primary_write_chunk_count += chunk_count
118 else:
119 state.active_cascade_count += 1
120 - state.primary_read_block_count += block_count
120 + state.primary_read_chunk_count += chunk_count
121
122 def on_job_finished(
123 self, job_metadata: _JobMetadataLike, result: JobResult
@@ -149,25 +149,25 @@ class TieringMetricsTracker:
149 assert all(
150 state.active_promotion_count == 0
151 and state.active_cascade_count == 0
152 - and state.primary_write_block_count == 0
153 - and state.primary_read_block_count == 0
152 + and state.primary_write_chunk_count == 0
153 + and state.primary_read_chunk_count == 0
154 for state in self._tier_states
155 )
156
157 def _decrement_tier_state(self, job_metadata: _JobMetadataLike) -> None:
158 transfer_job = job_metadata.transfer_job
159 state = self._tier_states[job_metadata.tier_idx]
160 - block_count = len(transfer_job.block_ids)
160 + chunk_count = len(transfer_job.chunk_ids)
161 if transfer_job.is_promotion:
162 assert state.active_promotion_count > 0
163 state.active_promotion_count -= 1
164 - state.primary_write_block_count -= block_count
165 - assert state.primary_write_block_count >= 0
164 + state.primary_write_chunk_count -= chunk_count
165 + assert state.primary_write_chunk_count >= 0
166 else:
167 assert state.active_cascade_count > 0
168 state.active_cascade_count -= 1
169 - state.primary_read_block_count -= block_count
170 - assert state.primary_read_block_count >= 0
169 + state.primary_read_chunk_count -= chunk_count
170 + assert state.primary_read_chunk_count >= 0
171
172 def _observe_finished_job_stats(
173 self,
@@ -199,7 +199,7 @@ class TieringMetricsTracker:
199 if transfer_job.is_promotion
200 else TieringOffloadingMetrics.WRITE_TIME
201 )
202 - transfer_size = completed_key_count * self._primary_block_size
202 + transfer_size = completed_key_count * self._primary_chunk_size
203 self._stats.increase_counter(bytes_metric, transfer_size, labelvalues)
204 if completed_job.transfer_time is not None:
205 self._stats.increase_counter(
@@ -210,13 +210,13 @@ class TieringMetricsTracker:
210 for tier_idx, state in enumerate(self._tier_states):
211 labelvalues = self.tier_label(tier_idx)
212 write_usage = (
213 - state.primary_write_block_count / self._num_primary_blocks
214 - if self._num_primary_blocks > 0
213 + state.primary_write_chunk_count / self._num_primary_chunks
214 + if self._num_primary_chunks > 0
215 else 0.0
216 )
217 read_usage = (
218 - state.primary_read_block_count / self._num_primary_blocks
219 - if self._num_primary_blocks > 0
218 + state.primary_read_chunk_count / self._num_primary_chunks
219 + if self._num_primary_chunks > 0
220 else 0.0
221 )
222 stats.set_gauge(
@@ -248,12 +248,12 @@ class TieringMetricsTracker:
248 async_start_time: float | None,
249 ) -> None:
250 self._stats.increase_counter(
251 - TieringOffloadingMetrics.BLOCK_QUERIES,
251 + TieringOffloadingMetrics.CHUNK_QUERIES,
252 labelvalues=tier_label,
253 )
254 if result is LookupResult.HIT:
255 self._stats.increase_counter(
256 - TieringOffloadingMetrics.BLOCK_HITS,
256 + TieringOffloadingMetrics.CHUNK_HITS,
257 labelvalues=tier_label,
258 )
259 self._stats.observe_histogram(
vllm/v1/kv_offload/tiering/obj/manager.py
+2 -2
@@ -287,14 +287,14 @@ class ObjectStoreSecondaryTierManager(SecondaryTierManager):
287 self._store_job_keys[job_metadata.job_id] = list(job_metadata.keys)
288 obj_keys = (self._file_mapper.get_file_name(k) for k in job_metadata.keys)
289 self._submit_transfer(
290 - job_metadata.job_id, job_metadata.block_ids, obj_keys, NIXL_WRITE
290 + job_metadata.job_id, job_metadata.chunk_ids, obj_keys, NIXL_WRITE
291 )
292
293 def submit_load(self, job_metadata: TransferJob) -> None:
294 self._load_job_keys[job_metadata.job_id] = list(job_metadata.keys)
295 obj_keys = (self._file_mapper.get_file_name(k) for k in job_metadata.keys)
296 self._submit_transfer(
297 - job_metadata.job_id, job_metadata.block_ids, obj_keys, NIXL_READ
297 + job_metadata.job_id, job_metadata.chunk_ids, obj_keys, NIXL_READ
298 )
299
300 def on_request_finished(self, req_context: ReqContext) -> None:
vllm/v1/kv_offload/tiering/p2p/manager.py
+2 -2
@@ -417,7 +417,7 @@ class P2PSecondaryTierManager(SecondaryTierManager):
417 def submit_store(self, job_metadata: TransferJob) -> None:
418 job_id = job_metadata.job_id
419 keys = list(job_metadata.keys)
420 - block_ids = job_metadata.block_ids.tolist()
420 + block_ids = job_metadata.chunk_ids.tolist()
421
422 assert len(keys) == len(block_ids)
423
@@ -476,7 +476,7 @@ class P2PSecondaryTierManager(SecondaryTierManager):
476 def submit_load(self, job_metadata: TransferJob) -> None:
477 job_id = job_metadata.job_id
478 keys = list(job_metadata.keys)
479 - block_ids = job_metadata.block_ids
479 + block_ids = job_metadata.chunk_ids
480
481 source = job_metadata.req_context.get_state(P2PSourceInfo)
482 logger.debug(
vllm/v1/kv_offload/tiering/p2p/session/server.py
+1 -1
@@ -593,7 +593,7 @@ class ServerRole:
593 self.add_stored_blocks(
594 lookup.kv_request_id,
595 list(meta.keys),
596 - meta.block_ids.tolist(),
596 + meta.chunk_ids.tolist(),
597 meta.job_id,
598 round_seq=lookup.round_seq,
599 from_lookup=True,
vllm/v1/kv_offload/tiering/spec.py
+15 -15
@@ -8,7 +8,7 @@ and configurable secondary tiers (e.g., Storage, Network).
8
9 Configuration via kv_connector_extra_config:
10 - cpu_bytes_to_use: (required) Bytes to allocate for CPU primary tier
11 - - block_size: (optional) Block size for offloaded blocks (default: GPU block size)
11 + - block_size: (optional) Tokens per offloaded chunk (default: GPU block size)
12 - eviction_policy: (optional) Primary tier eviction policy: built-in "lru"/
13 "arc", or the name of a policy registered via CachePolicyFactory, or an
14 out-of-tree CachePolicy class name paired with cache_policy_module_path
@@ -105,7 +105,7 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
105 metrics[TieringOffloadingMetrics.LOOKUP_SYNC_DELAY] = (
106 OffloadingHistogramMetadata(
107 documentation=(
108 - "Histogram of blocking time spent in a per-block tier lookup "
108 + "Histogram of blocking time spent in a per-chunk tier lookup "
109 "that resolved as a hit or miss, labeled by tier, in seconds."
110 ),
111 labelnames=("tier",),
@@ -127,7 +127,7 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
127 metrics[TieringOffloadingMetrics.LOOKUP_ASYNC_DELAY] = (
128 OffloadingHistogramMetadata(
129 documentation=(
130 - "Histogram of wall-clock time from a per-block tier lookup "
130 + "Histogram of wall-clock time from a per-chunk tier lookup "
131 "first returning retry until that same tier lookup resolves "
132 "as a hit or miss, labeled by tier, in seconds."
133 ),
@@ -191,14 +191,14 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
191 labelnames=("tier",),
192 )
193 )
194 - metrics[TieringOffloadingMetrics.BLOCK_QUERIES] = OffloadingCounterMetadata(
194 + metrics[TieringOffloadingMetrics.CHUNK_QUERIES] = OffloadingCounterMetadata(
195 documentation=(
196 - "Number of block lookup queries sent to a tier, labeled by tier."
196 + "Number of chunk lookup queries sent to a tier, labeled by tier."
197 ),
198 labelnames=("tier",),
199 )
200 - metrics[TieringOffloadingMetrics.BLOCK_HITS] = OffloadingCounterMetadata(
201 - documentation="Number of block lookup hits in a tier, labeled by tier.",
200 + metrics[TieringOffloadingMetrics.CHUNK_HITS] = OffloadingCounterMetadata(
201 + documentation="Number of chunk lookup hits in a tier, labeled by tier.",
202 labelnames=("tier",),
203 )
204 metrics[TieringOffloadingMetrics.PROMOTION_ALLOCATION_FAILURES] = (
@@ -300,16 +300,16 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
300 # primary tier can eagerly create a memoryview over _base.
301 scheduler_mmap = SharedOffloadRegion(
302 engine_id=self._engine_id,
303 - num_blocks=self.num_blocks,
303 + num_chunks=self.num_chunks,
304 rank=None,
305 - kv_bytes_per_block=self.kv_bytes_per_chunk,
305 + kv_bytes_per_chunk=self.kv_bytes_per_chunk,
306 cpu_page_size=self.cpu_page_size_per_worker,
307 )
308 self._scheduler_mmap = scheduler_mmap
309
310 # Create primary tier (CPU-based)
311 primary_tier = CPUPrimaryTierOffloadingManager(
312 - num_blocks=self.num_blocks,
312 + num_chunks=self.num_chunks,
313 cache_policy=self.eviction_policy,
314 cache_policy_module_path=self.cache_policy_module_path,
315 enable_events=self.kv_events_config.enable_kv_cache_events,
@@ -367,9 +367,9 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
367
368 logger.info(
369 "Created TieringOffloadingManager with primary tier "
370 - "(%s, %s blocks) and %s secondary tier(s)",
370 + "(%s, %s chunks) and %s secondary tier(s)",
371 self.eviction_policy,
372 - self.num_blocks,
372 + self.num_chunks,
373 len(secondary_tiers),
374 )
375
@@ -393,9 +393,9 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
393 rank = torch.accelerator.current_device_index() % world_size
394 worker_mmap = SharedOffloadRegion(
395 engine_id=self._engine_id,
396 - num_blocks=self.num_blocks,
396 + num_chunks=self.num_chunks,
397 rank=rank,
398 - kv_bytes_per_block=self.kv_bytes_per_chunk,
398 + kv_bytes_per_chunk=self.kv_bytes_per_chunk,
399 cpu_page_size=self.cpu_page_size_per_worker,
400 )
401 try:
@@ -404,7 +404,7 @@ class TieringOffloadingSpec(CPUOffloadingSpec):
404 return CPUOffloadingWorker(
405 kv_caches=kv_caches,
406 blocks_per_chunk=self.blocks_per_chunk,
407 - num_cpu_blocks=self.num_blocks,
407 + num_cpu_chunks=self.num_chunks,
408 mmap_region=worker_mmap,
409 canonical_layout=self.config.canonical_layout,
410 )