@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7c139ab23f
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 5, 12:15 UTC

[KV Offload] Clean up ARC/LRU refactoring leftovers: group ARC tests and fix stale comment (#38217)

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

Ronen Schaffer committed Apr 7, 2026 at 15:14 UTC 7c139ab23f6d2e9b4603b40814956100a1ccf569
2 files changed +297 -334
tests/v1/kv_offload/test_cpu_manager.py
+295 -331
@@ -15,6 +15,7 @@ from vllm.v1.kv_offload.abstract import (
15 from vllm.v1.kv_offload.cpu.manager import CPUOffloadingManager
16 from vllm.v1.kv_offload.cpu.policies.arc import ARCCachePolicy
17 from vllm.v1.kv_offload.mediums import CPULoadStoreSpec
18 +from vllm.v1.kv_offload.reuse_manager import FilterReusedOffloadingManager
19
20
21 @dataclass
@@ -243,335 +244,300 @@ def test_cpu_manager():
244 )
245
246
246 -def test_arc_manager_basic():
247 - """
248 - Tests CPUOffloadingManager with arc policy.
249 - Verifies that ARC handles store, load, and lookup operations correctly.
250 - """
251 - block_size = 256
252 - arc_manager = CPUOffloadingManager(
253 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=True
254 - )
255 - arc_policy = arc_manager._policy
256 - assert isinstance(arc_policy, ARCCachePolicy)
257 -
258 - # prepare store [1, 2]
259 - prepare_store_output = arc_manager.prepare_store(to_hashes([1, 2]))
260 - verify_store_output(
261 - prepare_store_output,
262 - ExpectedPrepareStoreOutput(
263 - block_hashes_to_store=[1, 2],
264 - store_block_ids=[0, 1],
265 - block_hashes_evicted=[],
266 - ),
267 - )
268 -
269 - # lookup [1, 2] -> not ready
270 - assert arc_manager.lookup(to_hashes([1, 2])) == 0
271 -
272 - # no events so far
273 - assert list(arc_manager.take_events()) == []
274 -
275 - # complete store [1, 2]
276 - arc_manager.complete_store(to_hashes([1, 2]))
277 - verify_events(
278 - arc_manager.take_events(), block_size=block_size, expected_stores=({1, 2},)
279 - )
280 -
281 - # lookup [1, 2]
282 - assert arc_manager.lookup(to_hashes([1])) == 1
283 - assert arc_manager.lookup(to_hashes([1, 2])) == 2
284 - assert arc_manager.lookup(to_hashes([1, 2, 3])) == 2
285 -
286 - # blocks should be in T1 (recent)
287 - assert len(arc_policy.t1) == 2
288 - assert len(arc_policy.t2) == 0
289 -
290 -
291 -def test_arc_manager_t1_to_t2_promotion():
292 - """
293 - Tests that accessing a block in T1 promotes it to T2 (frequent).
294 - This is a key feature of ARC's adaptive behavior.
295 - """
296 - block_size = 256
297 - arc_manager = CPUOffloadingManager(
298 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=False
299 - )
300 - arc_policy = arc_manager._policy
301 - assert isinstance(arc_policy, ARCCachePolicy)
302 -
303 - # store and complete block 1
304 - arc_manager.prepare_store(to_hashes([1]))
305 - arc_manager.complete_store(to_hashes([1]))
306 -
307 - # block 1 starts in T1 (recent)
308 - assert to_hashes([1])[0] in arc_policy.t1
309 - assert to_hashes([1])[0] not in arc_policy.t2
310 -
311 - # touch block 1 (simulate second access)
312 - arc_manager.touch(to_hashes([1]))
313 -
314 - # block 1 should now be in T2 (frequent)
315 - assert to_hashes([1])[0] not in arc_policy.t1
316 - assert to_hashes([1])[0] in arc_policy.t2
317 -
318 -
319 -def test_arc_manager_eviction_with_load():
320 - """
321 - Tests ARC eviction behavior similar to LRU test.
322 - Verifies that blocks being loaded (ref_cnt > 0) cannot be evicted.
323 - """
324 - block_size = 256
325 - arc_manager = CPUOffloadingManager(
326 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=True
327 - )
328 -
329 - # prepare and complete store [1, 2, 3, 4]
330 - prepare_store_output = arc_manager.prepare_store(to_hashes([1, 2, 3, 4]))
331 - verify_store_output(
332 - prepare_store_output,
333 - ExpectedPrepareStoreOutput(
334 - block_hashes_to_store=[1, 2, 3, 4],
335 - store_block_ids=[0, 1, 2, 3],
336 - block_hashes_evicted=[],
337 - ),
338 - )
339 - arc_manager.complete_store(to_hashes([1, 2, 3, 4]))
340 -
341 - # prepare load [2, 3] (increases ref_cnt)
342 - prepare_load_output = arc_manager.prepare_load(to_hashes([2, 3]))
343 - verify_load_output(prepare_load_output, [1, 2])
344 -
345 - # prepare store [5, 6, 7] with [2, 3] being loaded
346 - # should fail because [2, 3] have ref_cnt > 0
347 - assert arc_manager.prepare_store(to_hashes([5, 6, 7])) is None
348 -
349 - # complete load [2, 3]
350 - arc_manager.complete_load(to_hashes([2, 3]))
351 -
352 - # now prepare store [5, 6, 7] should succeed
353 - # ARC will evict blocks one at a time from T1 as needed
354 - prepare_store_output = arc_manager.prepare_store(to_hashes([5, 6, 7]))
355 - assert prepare_store_output is not None
356 - # Should successfully evict enough blocks to make room (at least 1)
357 - assert len(prepare_store_output.block_hashes_evicted) >= 1
358 -
359 -
360 -def test_arc_manager_adaptive_target():
361 - """
362 - Tests ARC's adaptive target adjustment via ghost lists.
363 - When a block in B1 (ghost list) is accessed, target_t1_size increases.
364 - When a block in B2 is accessed, target_t1_size decreases.
365 - """
366 - block_size = 256
367 - arc_manager = CPUOffloadingManager(
368 - block_size=block_size, num_blocks=2, cache_policy="arc", enable_events=False
369 - )
370 - arc_policy = arc_manager._policy
371 - assert isinstance(arc_policy, ARCCachePolicy)
372 -
373 - # store blocks 1, 2 (fills cache)
374 - arc_manager.prepare_store(to_hashes([1, 2]))
375 - arc_manager.complete_store(to_hashes([1, 2]))
376 -
377 - initial_target = arc_policy.target_t1_size
378 -
379 - # store block 3, evicting block 1 (moves to B1 ghost list)
380 - arc_manager.prepare_store(to_hashes([3]))
381 - arc_manager.complete_store(to_hashes([3]))
382 -
383 - # block 1 should be in B1 (ghost list)
384 - assert to_hashes([1])[0] in arc_policy.b1
385 -
386 - # touch block 1 (cache miss, but in B1)
387 - # this should increase target_t1_size (favor recency)
388 - arc_manager.touch(to_hashes([1]))
389 -
390 - # target should have increased
391 - assert arc_policy.target_t1_size > initial_target
392 -
393 -
394 -def test_arc_manager_t1_t2_eviction_policy():
395 - """
396 - Tests that ARC evicts from T1 or T2 based on target_t1_size.
397 - If |T1| >= target_t1_size, evict from T1, otherwise from T2.
398 - """
399 - block_size = 256
400 - arc_manager = CPUOffloadingManager(
401 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=False
402 - )
403 - arc_policy = arc_manager._policy
404 - assert isinstance(arc_policy, ARCCachePolicy)
405 -
406 - # store blocks 1, 2, 3, 4
407 - arc_manager.prepare_store(to_hashes([1, 2, 3, 4]))
408 - arc_manager.complete_store(to_hashes([1, 2, 3, 4]))
409 -
410 - # promote blocks 3, 4 to T2 by touching them
411 - arc_manager.touch(to_hashes([3, 4]))
412 -
413 - # now: T1 = {1, 2}, T2 = {3, 4}
414 - assert len(arc_policy.t1) == 2
415 - assert len(arc_policy.t2) == 2
416 -
417 - # set target_t1_size to prefer evicting from T1
418 - # (when |T1| >= target, evict from T1)
419 - arc_policy.target_t1_size = 1
420 -
421 - # store block 5, should evict from T1 (block 1, LRU in T1)
422 - output = arc_manager.prepare_store(to_hashes([5]))
423 - assert output is not None
424 - assert to_hashes([1]) == output.block_hashes_evicted
425 -
426 - arc_manager.complete_store(to_hashes([5]))
427 -
428 - # block 1 should be in B1 (ghost list)
429 - assert to_hashes([1])[0] in arc_policy.b1
430 - # block 5 should be in T1
431 - assert to_hashes([5])[0] in arc_policy.t1
432 -
433 -
434 -def test_arc_manager_ghost_list_bounds():
435 - """
436 - Tests that ghost lists (B1, B2) don't grow unbounded.
437 - They should be capped at cache_capacity.
438 - """
439 - block_size = 256
440 - arc_manager = CPUOffloadingManager(
441 - block_size=block_size, num_blocks=2, cache_policy="arc", enable_events=False
442 - )
443 - arc_policy = arc_manager._policy
444 - assert isinstance(arc_policy, ARCCachePolicy)
445 -
446 - # fill cache with blocks 1, 2
447 - arc_manager.prepare_store(to_hashes([1, 2]))
448 - arc_manager.complete_store(to_hashes([1, 2]))
449 -
450 - # store many blocks to fill ghost lists
451 - for i in range(3, 20):
452 - arc_manager.prepare_store(to_hashes([i]))
453 - arc_manager.complete_store(to_hashes([i]))
454 -
455 - # ghost lists should not exceed cache_capacity
456 - assert len(arc_policy.b1) <= arc_policy.cache_capacity
457 - assert len(arc_policy.b2) <= arc_policy.cache_capacity
458 -
459 -
460 -def test_arc_manager_touch_ordering():
461 - """
462 - Tests that touch() correctly updates access patterns.
463 - Similar to LRU test but verifies T1/T2 ordering.
464 - """
465 - block_size = 256
466 - arc_manager = CPUOffloadingManager(
467 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=True
468 - )
469 - arc_policy = arc_manager._policy
470 - assert isinstance(arc_policy, ARCCachePolicy)
471 -
472 - # store blocks 1, 2, 3, 4
473 - arc_manager.prepare_store(to_hashes([1, 2, 3, 4]))
474 - arc_manager.complete_store(to_hashes([1, 2, 3, 4]))
475 -
476 - # promote 3, 4 to T2
477 - arc_manager.touch(to_hashes([3, 4]))
478 -
479 - # T1 = {1, 2}, T2 = {3, 4}
480 - # touch [1, 3, 4] - should promote 1 to T2, and move 3,4 to end of T2
481 - arc_manager.touch(to_hashes([1, 3, 4]))
482 -
483 - # T1 = {2}, T2 = {1, 3, 4} (in that order, with 4 most recent)
484 - assert len(arc_policy.t1) == 1
485 - assert len(arc_policy.t2) == 3
486 -
487 - # store block 5, should evict from T1 (block 2, only one in T1)
488 - prepare_store_output = arc_manager.prepare_store(to_hashes([5]))
489 - verify_store_output(
490 - prepare_store_output,
491 - ExpectedPrepareStoreOutput(
492 - block_hashes_to_store=[5],
493 - store_block_ids=[1], # reuses block 2's storage
494 - block_hashes_evicted=[2],
495 - ),
496 - )
497 -
498 -
499 -def test_arc_manager_failed_store():
500 - """
501 - Tests that failed store operations clean up correctly.
502 - Similar to LRU test but for ARC.
503 - """
504 - block_size = 256
505 - arc_manager = CPUOffloadingManager(
506 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=True
507 - )
508 - arc_policy = arc_manager._policy
509 - assert isinstance(arc_policy, ARCCachePolicy)
510 -
511 - # store blocks 1, 2, 3, 4
512 - arc_manager.prepare_store(to_hashes([1, 2, 3, 4]))
513 - arc_manager.complete_store(to_hashes([1, 2, 3, 4]))
514 -
515 - # prepare store block 5 (will evict block 1)
516 - prepare_store_output = arc_manager.prepare_store(to_hashes([5]))
517 - assert prepare_store_output is not None
518 - assert len(prepare_store_output.block_hashes_evicted) == 1
519 -
520 - # complete store with failure
521 - arc_manager.complete_store(to_hashes([5]), success=False)
522 -
523 - # block 5 should not be in cache
524 - assert arc_manager.lookup(to_hashes([5])) == 0
525 - # block 5 should not be in T1 or T2
526 - assert to_hashes([5])[0] not in arc_policy.t1
527 - assert to_hashes([5])[0] not in arc_policy.t2
528 -
529 - # evicted block should still be gone (in B1 ghost list)
530 - evicted_hash = prepare_store_output.block_hashes_evicted[0]
531 - assert evicted_hash in arc_policy.b1
532 -
533 -
534 -def test_arc_manager_full_scenario():
535 - """
536 - Comprehensive test covering multiple ARC operations in sequence.
537 - Similar to the full LRU test but adapted for ARC behavior.
538 - """
539 - block_size = 256
540 - arc_manager = CPUOffloadingManager(
541 - block_size=block_size, num_blocks=4, cache_policy="arc", enable_events=True
542 - )
543 - arc_policy = arc_manager._policy
544 - assert isinstance(arc_policy, ARCCachePolicy)
545 -
546 - # store [1, 2]
547 - arc_manager.prepare_store(to_hashes([1, 2]))
548 - arc_manager.complete_store(to_hashes([1, 2]))
549 -
550 - # store [3, 4, 5] -> evicts [1]
551 - prepare_store_output = arc_manager.prepare_store(to_hashes([3, 4, 5]))
552 - assert prepare_store_output is not None
553 - assert len(prepare_store_output.block_hashes_evicted) == 1
554 - arc_manager.complete_store(to_hashes([3, 4, 5]))
555 -
556 - # promote some blocks to T2
557 - arc_manager.touch(to_hashes([2, 3]))
558 -
559 - # T1 has {4, 5}, T2 has {2, 3}
560 - assert len(arc_policy.t1) == 2
561 - assert len(arc_policy.t2) == 2
562 -
563 - # store [6] -> should evict from T1 (4 is oldest in T1)
564 - prepare_store_output = arc_manager.prepare_store(to_hashes([6]))
565 - assert prepare_store_output is not None
566 - arc_manager.complete_store(to_hashes([6]))
567 -
568 - # verify blocks 2, 3 (in T2) are still present
569 - assert arc_manager.lookup(to_hashes([2])) == 1
570 - assert arc_manager.lookup(to_hashes([3])) == 1
571 -
572 - # verify events
573 - events = list(arc_manager.take_events())
574 - assert len(events) > 0 # should have store and eviction events
247 +class TestARCPolicy:
248 + """Unit tests for CPUOffloadingManager with ARC eviction policy."""
249 +
250 + def _make_manager(
251 + self, num_blocks: int = 4, enable_events: bool = True
252 + ) -> tuple[CPUOffloadingManager, ARCCachePolicy]:
253 + manager = CPUOffloadingManager(
254 + block_size=256,
255 + num_blocks=num_blocks,
256 + cache_policy="arc",
257 + enable_events=enable_events,
258 + )
259 + policy = manager._policy
260 + assert isinstance(policy, ARCCachePolicy)
261 + return manager, policy
262 +
263 + def test_basic(self):
264 + """
265 + Tests CPUOffloadingManager with arc policy.
266 + Verifies that ARC handles store, load, and lookup operations correctly.
267 + """
268 + cpu_manager, arc_policy = self._make_manager()
269 +
270 + # prepare store [1, 2]
271 + prepare_store_output = cpu_manager.prepare_store(to_hashes([1, 2]))
272 + verify_store_output(
273 + prepare_store_output,
274 + ExpectedPrepareStoreOutput(
275 + block_hashes_to_store=[1, 2],
276 + store_block_ids=[0, 1],
277 + block_hashes_evicted=[],
278 + ),
279 + )
280 +
281 + # lookup [1, 2] -> not ready
282 + assert cpu_manager.lookup(to_hashes([1, 2])) == 0
283 +
284 + # no events so far
285 + assert list(cpu_manager.take_events()) == []
286 +
287 + # complete store [1, 2]
288 + cpu_manager.complete_store(to_hashes([1, 2]))
289 + verify_events(
290 + cpu_manager.take_events(), block_size=256, expected_stores=({1, 2},)
291 + )
292 +
293 + # lookup [1, 2]
294 + assert cpu_manager.lookup(to_hashes([1])) == 1
295 + assert cpu_manager.lookup(to_hashes([1, 2])) == 2
296 + assert cpu_manager.lookup(to_hashes([1, 2, 3])) == 2
297 +
298 + # blocks should be in T1 (recent)
299 + assert len(arc_policy.t1) == 2
300 + assert len(arc_policy.t2) == 0
301 +
302 + def test_t1_to_t2_promotion(self):
303 + """
304 + Tests that accessing a block in T1 promotes it to T2 (frequent).
305 + This is a key feature of ARC's adaptive behavior.
306 + """
307 + cpu_manager, arc_policy = self._make_manager(enable_events=False)
308 +
309 + # store and complete block 1
310 + cpu_manager.prepare_store(to_hashes([1]))
311 + cpu_manager.complete_store(to_hashes([1]))
312 +
313 + # block 1 starts in T1 (recent)
314 + assert to_hashes([1])[0] in arc_policy.t1
315 + assert to_hashes([1])[0] not in arc_policy.t2
316 +
317 + # touch block 1 (simulate second access)
318 + cpu_manager.touch(to_hashes([1]))
319 +
320 + # block 1 should now be in T2 (frequent)
321 + assert to_hashes([1])[0] not in arc_policy.t1
322 + assert to_hashes([1])[0] in arc_policy.t2
323 +
324 + def test_eviction_with_load(self):
325 + """
326 + Tests ARC eviction behavior similar to LRU test.
327 + Verifies that blocks being loaded (ref_cnt > 0) cannot be evicted.
328 + """
329 + cpu_manager, _ = self._make_manager()
330 +
331 + # prepare and complete store [1, 2, 3, 4]
332 + prepare_store_output = cpu_manager.prepare_store(to_hashes([1, 2, 3, 4]))
333 + verify_store_output(
334 + prepare_store_output,
335 + ExpectedPrepareStoreOutput(
336 + block_hashes_to_store=[1, 2, 3, 4],
337 + store_block_ids=[0, 1, 2, 3],
338 + block_hashes_evicted=[],
339 + ),
340 + )
341 + cpu_manager.complete_store(to_hashes([1, 2, 3, 4]))
342 +
343 + # prepare load [2, 3] (increases ref_cnt)
344 + prepare_load_output = cpu_manager.prepare_load(to_hashes([2, 3]))
345 + verify_load_output(prepare_load_output, [1, 2])
346 +
347 + # prepare store [5, 6, 7] with [2, 3] being loaded
348 + # should fail because [2, 3] have ref_cnt > 0
349 + assert cpu_manager.prepare_store(to_hashes([5, 6, 7])) is None
350 +
351 + # complete load [2, 3]
352 + cpu_manager.complete_load(to_hashes([2, 3]))
353 +
354 + # now prepare store [5, 6, 7] should succeed
355 + # ARC will evict blocks one at a time from T1 as needed
356 + prepare_store_output = cpu_manager.prepare_store(to_hashes([5, 6, 7]))
357 + assert prepare_store_output is not None
358 + # Should successfully evict enough blocks to make room (at least 1)
359 + assert len(prepare_store_output.block_hashes_evicted) >= 1
360 +
361 + def test_adaptive_target(self):
362 + """
363 + Tests ARC's adaptive target adjustment via ghost lists.
364 + When a block in B1 (ghost list) is accessed, target_t1_size increases.
365 + When a block in B2 is accessed, target_t1_size decreases.
366 + """
367 + cpu_manager, arc_policy = self._make_manager(num_blocks=2, enable_events=False)
368 +
369 + # store blocks 1, 2 (fills cache)
370 + cpu_manager.prepare_store(to_hashes([1, 2]))
371 + cpu_manager.complete_store(to_hashes([1, 2]))
372 +
373 + initial_target = arc_policy.target_t1_size
374 +
375 + # store block 3, evicting block 1 (moves to B1 ghost list)
376 + cpu_manager.prepare_store(to_hashes([3]))
377 + cpu_manager.complete_store(to_hashes([3]))
378 +
379 + # block 1 should be in B1 (ghost list)
380 + assert to_hashes([1])[0] in arc_policy.b1
381 +
382 + # touch block 1 (cache miss, but in B1)
383 + # this should increase target_t1_size (favor recency)
384 + cpu_manager.touch(to_hashes([1]))
385 +
386 + # target should have increased
387 + assert arc_policy.target_t1_size > initial_target
388 +
389 + def test_t1_t2_eviction_policy(self):
390 + """
391 + Tests that ARC evicts from T1 or T2 based on target_t1_size.
392 + If |T1| >= target_t1_size, evict from T1, otherwise from T2.
393 + """
394 + cpu_manager, arc_policy = self._make_manager(enable_events=False)
395 +
396 + # store blocks 1, 2, 3, 4
397 + cpu_manager.prepare_store(to_hashes([1, 2, 3, 4]))
398 + cpu_manager.complete_store(to_hashes([1, 2, 3, 4]))
399 +
400 + # promote blocks 3, 4 to T2 by touching them
401 + cpu_manager.touch(to_hashes([3, 4]))
402 +
403 + # now: T1 = {1, 2}, T2 = {3, 4}
404 + assert len(arc_policy.t1) == 2
405 + assert len(arc_policy.t2) == 2
406 +
407 + # set target_t1_size to prefer evicting from T1
408 + # (when |T1| >= target, evict from T1)
409 + arc_policy.target_t1_size = 1
410 +
411 + # store block 5, should evict from T1 (block 1, LRU in T1)
412 + output = cpu_manager.prepare_store(to_hashes([5]))
413 + assert output is not None
414 + assert to_hashes([1]) == output.block_hashes_evicted
415 +
416 + cpu_manager.complete_store(to_hashes([5]))
417 +
418 + # block 1 should be in B1 (ghost list)
419 + assert to_hashes([1])[0] in arc_policy.b1
420 + # block 5 should be in T1
421 + assert to_hashes([5])[0] in arc_policy.t1
422 +
423 + def test_ghost_list_bounds(self):
424 + """
425 + Tests that ghost lists (B1, B2) don't grow unbounded.
426 + They should be capped at cache_capacity.
427 + """
428 + cpu_manager, arc_policy = self._make_manager(num_blocks=2, enable_events=False)
429 +
430 + # fill cache with blocks 1, 2
431 + cpu_manager.prepare_store(to_hashes([1, 2]))
432 + cpu_manager.complete_store(to_hashes([1, 2]))
433 +
434 + # store many blocks to fill ghost lists
435 + for i in range(3, 20):
436 + cpu_manager.prepare_store(to_hashes([i]))
437 + cpu_manager.complete_store(to_hashes([i]))
438 +
439 + # ghost lists should not exceed cache_capacity
440 + assert len(arc_policy.b1) <= arc_policy.cache_capacity
441 + assert len(arc_policy.b2) <= arc_policy.cache_capacity
442 +
443 + def test_touch_ordering(self):
444 + """
445 + Tests that touch() correctly updates access patterns.
446 + Similar to LRU test but verifies T1/T2 ordering.
447 + """
448 + cpu_manager, arc_policy = self._make_manager()
449 +
450 + # store blocks 1, 2, 3, 4
451 + cpu_manager.prepare_store(to_hashes([1, 2, 3, 4]))
452 + cpu_manager.complete_store(to_hashes([1, 2, 3, 4]))
453 +
454 + # promote 3, 4 to T2
455 + cpu_manager.touch(to_hashes([3, 4]))
456 +
457 + # T1 = {1, 2}, T2 = {3, 4}
458 + # touch [1, 3, 4] - should promote 1 to T2, and move 3,4 to end of T2
459 + cpu_manager.touch(to_hashes([1, 3, 4]))
460 +
461 + # T1 = {2}, T2 = {1, 3, 4} (in that order, with 4 most recent)
462 + assert len(arc_policy.t1) == 1
463 + assert len(arc_policy.t2) == 3
464 +
465 + # store block 5, should evict from T1 (block 2, only one in T1)
466 + prepare_store_output = cpu_manager.prepare_store(to_hashes([5]))
467 + verify_store_output(
468 + prepare_store_output,
469 + ExpectedPrepareStoreOutput(
470 + block_hashes_to_store=[5],
471 + store_block_ids=[1], # reuses block 2's storage
472 + block_hashes_evicted=[2],
473 + ),
474 + )
475 +
476 + def test_failed_store(self):
477 + """
478 + Tests that failed store operations clean up correctly.
479 + Similar to LRU test but for ARC.
480 + """
481 + cpu_manager, arc_policy = self._make_manager()
482 +
483 + # store blocks 1, 2, 3, 4
484 + cpu_manager.prepare_store(to_hashes([1, 2, 3, 4]))
485 + cpu_manager.complete_store(to_hashes([1, 2, 3, 4]))
486 +
487 + # prepare store block 5 (will evict block 1)
488 + prepare_store_output = cpu_manager.prepare_store(to_hashes([5]))
489 + assert prepare_store_output is not None
490 + assert len(prepare_store_output.block_hashes_evicted) == 1
491 +
492 + # complete store with failure
493 + cpu_manager.complete_store(to_hashes([5]), success=False)
494 +
495 + # block 5 should not be in cache
496 + assert cpu_manager.lookup(to_hashes([5])) == 0
497 + # block 5 should not be in T1 or T2
498 + assert to_hashes([5])[0] not in arc_policy.t1
499 + assert to_hashes([5])[0] not in arc_policy.t2
500 +
501 + # evicted block should still be gone (in B1 ghost list)
502 + evicted_hash = prepare_store_output.block_hashes_evicted[0]
503 + assert evicted_hash in arc_policy.b1
504 +
505 + def test_full_scenario(self):
506 + """
507 + Comprehensive test covering multiple ARC operations in sequence.
508 + Similar to the full LRU test but adapted for ARC behavior.
509 + """
510 + cpu_manager, arc_policy = self._make_manager()
511 +
512 + # store [1, 2]
513 + cpu_manager.prepare_store(to_hashes([1, 2]))
514 + cpu_manager.complete_store(to_hashes([1, 2]))
515 +
516 + # store [3, 4, 5] -> evicts [1]
517 + prepare_store_output = cpu_manager.prepare_store(to_hashes([3, 4, 5]))
518 + assert prepare_store_output is not None
519 + assert len(prepare_store_output.block_hashes_evicted) == 1
520 + cpu_manager.complete_store(to_hashes([3, 4, 5]))
521 +
522 + # promote some blocks to T2
523 + cpu_manager.touch(to_hashes([2, 3]))
524 +
525 + # T1 has {4, 5}, T2 has {2, 3}
526 + assert len(arc_policy.t1) == 2
527 + assert len(arc_policy.t2) == 2
528 +
529 + # store [6] -> should evict from T1 (4 is oldest in T1)
530 + prepare_store_output = cpu_manager.prepare_store(to_hashes([6]))
531 + assert prepare_store_output is not None
532 + cpu_manager.complete_store(to_hashes([6]))
533 +
534 + # verify blocks 2, 3 (in T2) are still present
535 + assert cpu_manager.lookup(to_hashes([2])) == 1
536 + assert cpu_manager.lookup(to_hashes([3])) == 1
537 +
538 + # verify events
539 + events = list(cpu_manager.take_events())
540 + assert len(events) > 0 # should have store and eviction events
541
542
543 def test_filter_reused_manager():
@@ -583,8 +549,6 @@ def test_filter_reused_manager():
549 block_size=block_size, num_blocks=4, cache_policy="lru", enable_events=True
550 )
551
586 - from vllm.v1.kv_offload.reuse_manager import FilterReusedOffloadingManager
587 -
552 manager = FilterReusedOffloadingManager(
553 backing=lru_manager, store_threshold=2, max_tracker_size=3
554 )
vllm/v1/kv_offload/reuse_manager.py
+2 -3
@@ -93,9 +93,8 @@ class FilterReusedOffloadingManager(OffloadingManager):
93 ]
94
95 # Delegate to the backing manager with only the eligible hashes.
96 - # Passing an empty list is intentional and safe — both
97 - # LRUOffloadingManager and ARCOffloadingManager handle it correctly,
98 - # returning a PrepareStoreOutput with empty lists.
96 + # Passing an empty list is intentional and safe — CPUOffloadingManager
97 + # handles it correctly, returning a PrepareStoreOutput with empty lists.
98 return self._backing.prepare_store(eligible)
99
100 # ------------------------------------------------------------------