Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions tests/v1/core/test_kv_cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,31 @@ def test_free_kv_cache_block_queue_append_n():
)


def test_free_kv_cache_block_queue_last_resort_stays_behind():
"""Later appends stay ahead of the last-resort segment even after the
segment's first block leaves the queue by removal or popping."""
blocks = [KVCacheBlock(block_id=i) for i in range(7)]
queue = FreeKVCacheBlockQueue(blocks[0:1])

def order() -> list[int]:
return [block.block_id for block in queue.get_all_free_blocks()]

queue.append_last_resort_n(blocks[1:3])
queue.append(blocks[3])
assert order() == [0, 3, 1, 2]

queue.remove(blocks[1])
queue.append_n(blocks[4:5])
assert order() == [0, 3, 4, 2]

assert [block.block_id for block in queue.popleft_n(4)] == [0, 3, 4, 2]
queue.append_n(blocks[5:6])
queue.append_last_resort_n(blocks[6:7])
queue.append(blocks[1])
assert order() == [5, 1, 6]
assert queue.num_free_blocks == 3


def test_free_kv_cache_block_queue_prepend_n():
# Seed the queue with one block so prepend has an existing head to splice
# in front of (fake_head->b0->fake_tail).
Expand Down
6 changes: 3 additions & 3 deletions tests/v1/core/test_prefix_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,7 @@ def test_hisparse_prefix_hit_adopts_gpu_shadow_pages():
)


@pytest.mark.parametrize("free_blocks,adopted_pages", [(7, []), (10, [0, 1])])
@pytest.mark.parametrize("free_blocks,adopted_pages", [(7, []), (10, [0, 1, 2])])
def test_hisparse_prefix_hit_under_pressure_adopts_surviving_copies(
free_blocks, adopted_pages
):
Expand All @@ -1148,8 +1148,8 @@ def test_hisparse_prefix_hit_under_pressure_adopts_surviving_copies(
Admission counts free GPU copies as free, but adopting a copy pins it. When
adoption ran before the hit's allocation, it took blocks the allocation was
promised and the pool ran dry (``Cannot get N free blocks``). The allocation
now goes first, evicting a prefix's copies tail first; the copies it leaves
are adopted.
now goes first, evicting copies only after every other free block and a
prefix's tail before its head; the copies it leaves are adopted.
"""
manager = make_hisparse_kv_cache_manager(32, 16, enable_caching=True)
tokens = list(range(4 * HISPARSE_BLOCK_SIZE))
Expand Down
4 changes: 2 additions & 2 deletions vllm/v1/core/block_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ def unpin_blocks(
"""Release references to blocks that stay readable until reused.

The blocks become last-resort eviction candidates regardless of prefix
caching: they join the tail of the free queue and count as free.
caching: they stay behind every later-freed block and count as free.
``on_reuse`` fires when ``get_new_blocks`` hands a block out (or the
cache is reset), so the caller can stop reading it.
"""
Expand All @@ -727,7 +727,7 @@ def unpin_blocks(
self._reuse_watchers[block.block_id] = on_reuse
if block.ref_cnt == 0:
released.append(block)
self.free_block_queue.append_n(released)
self.free_block_queue.append_last_resort_n(released)

def _maybe_evict_cached_block(self, block: KVCacheBlock) -> bool:
"""If a block is cached in `cached_block_hash_to_block`, we reset its hash
Expand Down
69 changes: 44 additions & 25 deletions vllm/v1/core/kv_cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ class FreeKVCacheBlockQueue:
Note that we maintain this order by reversing the block order when free
blocks of a request. This operation is outside of this class.

Blocks added with ``append_last_resort_n`` form a segment at the tail that
later ``append``/``append_n`` calls stay in front of, so they are evicted
only after every other free block.

Args:
blocks: A list of KVCacheBlock objects.

Expand Down Expand Up @@ -297,6 +301,17 @@ def __init__(self, blocks: list[KVCacheBlock]) -> None:
self.fake_free_list_head.next_free_block = self.fake_free_list_tail
self.fake_free_list_tail.prev_free_block = self.fake_free_list_head

# First block of the last-resort tail segment, if any.
self.last_resort_head: KVCacheBlock | None = None

def _unlink_last_resort_head(self, block: KVCacheBlock) -> None:
"""Advance the last-resort segment past a block leaving the list."""
if block is self.last_resort_head:
next_block = block.next_free_block
self.last_resort_head = (
None if next_block is self.fake_free_list_tail else next_block
)

def popleft(self) -> KVCacheBlock:
"""Pop the first free block and reduce num_free_blocks by 1.

Expand All @@ -323,6 +338,7 @@ def popleft(self) -> KVCacheBlock:
"Invalid block found in popleft() "
"which doesn't have a valid next_free_block"
)
self._unlink_last_resort_head(first_block)

# Connect fake_head and the next block of first_block (i.e. second block
# or fake tail).
Expand Down Expand Up @@ -355,6 +371,7 @@ def popleft_n(self, n: int) -> list[KVCacheBlock]:
ret = []
for _ in range(n):
assert curr_block is not None
self._unlink_last_resort_head(curr_block)
ret.append(curr_block)
last_block = curr_block
curr_block = curr_block.next_free_block
Expand All @@ -380,6 +397,7 @@ def remove(self, block: KVCacheBlock) -> None:
# This should not happen if the block is from the free list.
# It indicates a bug in the caller's logic.
raise RuntimeError(f"remove() called on an invalid block: {block}")
self._unlink_last_resort_head(block)

# Link the previous block to the next block.
block.prev_free_block.next_free_block = block.next_free_block
Expand All @@ -391,28 +409,14 @@ def remove(self, block: KVCacheBlock) -> None:
self.num_free_blocks -= 1

def append(self, block: KVCacheBlock) -> None:
"""Put a block back into the free list and increase
num_free_blocks by 1.
"""Put a block back into the free list, ahead of any last-resort
blocks, and increase num_free_blocks by 1.

Args:
block: The block to append.

"""
if self.fake_free_list_tail.prev_free_block is None:
raise RuntimeError(
"prev_free_block of fake_free_list_tail should always exist"
)
last_block: KVCacheBlock = self.fake_free_list_tail.prev_free_block

# Connect the new block after the last block.
last_block.next_free_block = block
block.prev_free_block = last_block

# Connect the fake tail after the new block.
block.next_free_block = self.fake_free_list_tail
self.fake_free_list_tail.prev_free_block = block

self.num_free_blocks += 1
self.append_n([block])

def prepend_n(self, blocks: list[KVCacheBlock]) -> None:
"""Put a list of blocks at the front of the free list."""
Expand All @@ -436,28 +440,43 @@ def prepend_n(self, blocks: list[KVCacheBlock]) -> None:
self.num_free_blocks += len(blocks)

def append_n(self, blocks: list[KVCacheBlock]) -> None:
"""Put a list of blocks back into the free list
"""Put a list of blocks back into the free list, ahead of any
last-resort blocks.

Args:
blocks: The blocks to append.

"""
self._insert_before(self.last_resort_head or self.fake_free_list_tail, blocks)

def append_last_resort_n(self, blocks: list[KVCacheBlock]) -> None:
"""Put a list of blocks at the tail, behind every block appended later.

Args:
blocks: The blocks to append, in eviction order.

"""
if len(blocks) == 0:
return
self._insert_before(self.fake_free_list_tail, blocks)
if self.last_resort_head is None:
self.last_resort_head = blocks[0]

last_block = self.fake_free_list_tail.prev_free_block
assert last_block is not None, (
"prev_free_block of fake_free_list_tail should always exist"
)
def _insert_before(self, anchor: KVCacheBlock, blocks: list[KVCacheBlock]) -> None:
if len(blocks) == 0:
return

last_block = anchor.prev_free_block
assert last_block is not None, "free list anchors always have a predecessor"
# Add inter-connections between consecutive blocks
for block in blocks:
block.prev_free_block = last_block
last_block.next_free_block = block
last_block = block

# Connect the last block of <blocks> to the fake tail
last_block.next_free_block = self.fake_free_list_tail
self.fake_free_list_tail.prev_free_block = last_block
# Connect the last block of <blocks> to the anchor
last_block.next_free_block = anchor
anchor.prev_free_block = last_block

self.num_free_blocks += len(blocks)

Expand Down
Loading