Skip to content

vllm.v1.core.kv_cache_utils

KV-Cache Utilities.

Classes:

Functions:

BlockHashListWithBlockSize

Convert block-hash granularity from hash_block_size to target_block_size. Used when KV cache groups have different block sizes: hash_block_size is the size used to compute the original block_hashes; target_block_size is the group's actual block size.

Currently, only scaling up by an integer factor is supported (i.e., target_block_size is a multiple of hash_block_size). Conversion is performed lazily on access for efficiency. Each hash_block_size hash is already chained over its entire prefix, so the hash at the last hash_block_size boundary of a target_block_size block uniquely fingerprints that block's prefix; we use it directly.

Example (hash_block_size = 16, target_block_size = 32): the second 16-size hash already covers tokens 0-31, so it is the 32-size hash:

Block hashes with block_size 16: | Token Range | 0-15 | 16-31 | 32-47 | 48-63 | |-------------|------|-------|-------|-------| | Hash | A | B | C | D |

Block hashes with block_size 32: | Token Range | 0-31 | 32-63 | |-------------|------|-------| | Hash | B | D |

Parameters:

  • block_hashes

    (list[BlockHash]) –

    Block hashes to convert, computed at hash_block_size.

  • hash_block_size

    (int) –

    Block size at which block_hashes were computed.

  • target_block_size

    (int) –

    Desired block size; must be a multiple of hash_block_size.

Source code in vllm/v1/core/kv_cache_utils.py
class BlockHashListWithBlockSize:
    """Convert block-hash granularity from `hash_block_size` to `target_block_size`.
    Used when KV cache groups have different block sizes: `hash_block_size`
    is the size used to compute the original `block_hashes`; `target_block_size`
    is the group's actual block size.

    Currently, only scaling up by an integer factor is supported (i.e.,
    `target_block_size` is a multiple of `hash_block_size`). Conversion is
    performed lazily on access for efficiency. Each `hash_block_size` hash is
    already chained over its entire prefix, so the hash at the last
    `hash_block_size` boundary of a `target_block_size` block uniquely
    fingerprints that block's prefix; we use it directly.

    Example (`hash_block_size` = 16, `target_block_size` = 32):
    the second 16-size hash already covers tokens 0-31, so it is the 32-size
    hash:

    Block hashes with block_size 16:
    | Token Range | 0-15 | 16-31 | 32-47 | 48-63 |
    |-------------|------|-------|-------|-------|
    | Hash        | A    | B     | C     | D     |

    Block hashes with block_size 32:
    | Token Range | 0-31 | 32-63 |
    |-------------|------|-------|
    | Hash        | B    | D     |

    Args:
        block_hashes: Block hashes to convert, computed at `hash_block_size`.
        hash_block_size: Block size at which `block_hashes` were computed.
        target_block_size: Desired block size; must be a multiple of `hash_block_size`.

    """

    def __init__(
        self,
        block_hashes: list[BlockHash],
        hash_block_size: int,
        target_block_size: int,
    ):
        self.block_hashes = block_hashes
        assert target_block_size % hash_block_size == 0
        self.scale_factor = target_block_size // hash_block_size

    def __len__(self) -> int:
        return len(self.block_hashes) // self.scale_factor

    @overload
    def __getitem__(self, idx: int) -> BlockHash: ...

    @overload
    def __getitem__(self, idx: slice) -> list[BlockHash]: ...

    def __getitem__(self, idx):
        if isinstance(idx, int):
            return self._get_value_at(idx)

        if isinstance(idx, slice):
            start, stop, step = idx.indices(len(self))
            return [self._get_value_at(i) for i in range(start, stop, step)]

        raise TypeError(f"Invalid index type: {type(idx)!r}")

    def __iter__(self) -> Iterator[BlockHash]:
        for i in range(len(self)):
            yield self._get_value_at(i)

    def _get_value_at(self, idx: int) -> BlockHash:
        # The last hash_block_size hash within the target block already chains
        # over the whole prefix, so it is the target block's hash.
        return self.block_hashes[(idx + 1) * self.scale_factor - 1]

FreeKVCacheBlockQueue

This class organizes a list of KVCacheBlock objects to a doubly linked list of free blocks. We implement this class instead of using Python builtin deque to support removing a block in the middle of the queue in O(1) time. To close the performance gap to the builtin deque which is implemented in C++, this class does not allocate any Python objects when manipulating the linked list. Instead, this class manipulates the prev_free_block and next_free_block attributes of the given blocks.

The queue is ordered by block ID in the beginning. When a block is allocated and then freed, it will be appended back with the eviction order: 1. The least recent used block is at the front (LRU). 2. If two blocks have the same last accessed time (allocated by the same sequence), the one with more hash tokens (the tail of a block chain) is at the front. Note that we maintain this order by reversing the block order when free blocks of a request. This operation is outside of this class.

Parameters:

Methods:

  • append –

    Put a block back into the free list and increase

  • append_n –

    Put a list of blocks back into the free list

  • get_all_free_blocks –

    Get all free blocks in the free list. Mainly used for testing.

  • iter_blocks_after –

    Iterate free blocks in eviction order after the cursor.

  • popleft –

    Pop the first free block and reduce num_free_blocks by 1.

  • popleft_n –

    Pop the first n free blocks and reduce num_free_blocks by n.

  • prepend_n –

    Put a list of blocks at the front of the free list.

  • remove –

    Remove a block in the free list and reduce num_free_blocks by 1.

Source code in vllm/v1/core/kv_cache_utils.py
class FreeKVCacheBlockQueue:
    """This class organizes a list of KVCacheBlock objects to a doubly linked
    list of free blocks. We implement this class instead of using Python
    builtin deque to support removing a block in the middle of the queue
    in O(1) time. To close the performance gap to the builtin deque which is
    implemented in C++, this class does not allocate any Python objects when
    manipulating the linked list. Instead, this class manipulates the
    prev_free_block and next_free_block attributes of the given blocks.

    The queue is ordered by block ID in the beginning. When a block is allocated
    and then freed, it will be appended back with the eviction order:
    1. The least recent used block is at the front (LRU).
    2. If two blocks have the same last accessed time (allocated by the
       same sequence), the one with more hash tokens (the tail of a block
       chain) is at the front.
    Note that we maintain this order by reversing the block order when free
    blocks of a request. This operation is outside of this class.

    Args:
        blocks: A list of KVCacheBlock objects.

    """

    def __init__(self, blocks: list[KVCacheBlock]) -> None:
        self.num_free_blocks = len(blocks)

        # Initialize doubly links of consecutive blocks
        for i in range(self.num_free_blocks):
            if i > 0:
                blocks[i].prev_free_block = blocks[i - 1]
            if i < self.num_free_blocks - 1:
                blocks[i].next_free_block = blocks[i + 1]

        # Create a fake head and a tail block for the doubly linked list to
        # reduce branching in the code
        #
        # The implementation guaranteed that the fake head and tail
        # are NEVER got popped, so we could safely assume each real blocks
        # in the queue has prev and next blocks.
        self.fake_free_list_head = KVCacheBlock(block_id=-1)
        self.fake_free_list_tail = KVCacheBlock(block_id=-1)
        if self.num_free_blocks > 0:
            # Connect fake_head and fake_tail to the first and last block
            # respectively.
            self.fake_free_list_head.next_free_block = blocks[0]
            blocks[0].prev_free_block = self.fake_free_list_head
            self.fake_free_list_tail.prev_free_block = blocks[-1]
            blocks[-1].next_free_block = self.fake_free_list_tail
        else:
            # For empty list, simply connect the fake head and tail.
            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

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

        Returns:
            The first free block.

        """
        head = self.fake_free_list_head
        first_block = head.next_free_block
        if first_block is self.fake_free_list_tail or first_block is None:
            assert self.num_free_blocks == 0, (
                f"num_free_blocks ({self.num_free_blocks}) is out of sync "
                "with the free list."
            )
            raise ValueError("No free blocks available")

        next_block = first_block.next_free_block
        if next_block is None:
            # This should not happen if the block is from the free list.
            # It indicates a bug in the caller's logic.
            raise RuntimeError(
                "Invalid block found in popleft() "
                "which doesn't have a valid next_free_block"
            )

        # Connect fake_head and the next block of first_block (i.e. second block
        # or fake tail).
        head.next_free_block = next_block
        next_block.prev_free_block = head

        # Remove the block from the linked list.
        first_block.prev_free_block = first_block.next_free_block = None

        self.num_free_blocks -= 1
        return first_block

    def popleft_n(self, n: int) -> list[KVCacheBlock]:
        """Pop the first n free blocks and reduce num_free_blocks by n.

        Args:
            n: The number of blocks to pop.

        Returns:
            A list of n free blocks.

        """
        if n == 0:
            return []
        assert self.num_free_blocks >= n
        self.num_free_blocks -= n

        curr_block = self.fake_free_list_head.next_free_block
        if n == 1:
            assert curr_block is not None
            next_block = curr_block.next_free_block
            curr_block.prev_free_block = None
            curr_block.next_free_block = None
            if next_block is not None:
                self.fake_free_list_head.next_free_block = next_block
                next_block.prev_free_block = self.fake_free_list_head
            return [curr_block]

        # Pop n blocks from the head of the list
        ret = []
        for _ in range(n):
            assert curr_block is not None
            ret.append(curr_block)
            last_block = curr_block
            curr_block = curr_block.next_free_block
            # Reset prev_free_block and next_free_block of all popped blocks
            last_block.prev_free_block = None
            last_block.next_free_block = None

        if curr_block is not None:
            # The queue is not empty, connect the fake head to
            # the new first block.
            self.fake_free_list_head.next_free_block = curr_block
            curr_block.prev_free_block = self.fake_free_list_head
        return ret

    def remove(self, block: KVCacheBlock) -> None:
        """Remove a block in the free list and reduce num_free_blocks by 1.

        Args:
            block: The block to remove.

        """
        prev_block = block.prev_free_block
        next_block = block.next_free_block
        if prev_block is None or next_block is 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}")

        # Link the previous block to the next block.
        prev_block.next_free_block = next_block
        # Link the next block to the previous block.
        next_block.prev_free_block = prev_block

        # Remove the block from the linked list.
        block.prev_free_block = block.next_free_block = 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.

        Args:
            block: The block to append.

        """
        tail = self.fake_free_list_tail
        last_block = tail.prev_free_block
        if last_block is None:
            raise RuntimeError(
                "prev_free_block of fake_free_list_tail should always exist"
            )
        # 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 = tail
        tail.prev_free_block = block

        self.num_free_blocks += 1

    def prepend_n(self, blocks: list[KVCacheBlock]) -> None:
        """Put a list of blocks at the front of the free list."""
        if len(blocks) == 0:
            return

        first_block = self.fake_free_list_head.next_free_block
        assert first_block is not None, (
            "next_free_block of fake_free_list_head should always exist"
        )

        prev_block = self.fake_free_list_head
        for block in blocks:
            block.prev_free_block = prev_block
            prev_block.next_free_block = block
            prev_block = block

        prev_block.next_free_block = first_block
        first_block.prev_free_block = prev_block

        self.num_free_blocks += len(blocks)

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

        Args:
            blocks: The blocks to append.

        """
        if not blocks:
            return

        tail = self.fake_free_list_tail
        last_block = tail.prev_free_block
        assert last_block is not None, (
            "prev_free_block of fake_free_list_tail should always exist"
        )
        # 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 = tail
        tail.prev_free_block = last_block

        self.num_free_blocks += len(blocks)

    def get_all_free_blocks(self) -> list[KVCacheBlock]:
        """Get all free blocks in the free list. Mainly used for testing.

        Returns:
            A list of free blocks.

        """
        ret = []
        if self.fake_free_list_head.next_free_block is None:
            raise RuntimeError(
                "next_free_block of fake_free_list_head should always exist"
            )
        # Start from the first block
        curr_block: KVCacheBlock = self.fake_free_list_head.next_free_block
        # As long as next_free_block is available, we haven't reached to
        # the fake tail yet.
        while curr_block.next_free_block is not None:
            ret.append(curr_block)
            curr_block = curr_block.next_free_block
        return ret

    def iter_blocks_after(
        self,
        cursor: KVCacheBlock | None,
    ) -> Iterator[KVCacheBlock]:
        """Iterate free blocks in eviction order after the cursor."""
        if cursor is None:
            curr_block = self.fake_free_list_head.next_free_block
        else:
            curr_block = cursor.next_free_block

        while curr_block is not None and curr_block is not self.fake_free_list_tail:
            yield curr_block
            curr_block = curr_block.next_free_block

append(block)

Put a block back into the free list and increase num_free_blocks by 1.

Parameters:

Source code in vllm/v1/core/kv_cache_utils.py
def append(self, block: KVCacheBlock) -> None:
    """Put a block back into the free list and increase
    num_free_blocks by 1.

    Args:
        block: The block to append.

    """
    tail = self.fake_free_list_tail
    last_block = tail.prev_free_block
    if last_block is None:
        raise RuntimeError(
            "prev_free_block of fake_free_list_tail should always exist"
        )
    # 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 = tail
    tail.prev_free_block = block

    self.num_free_blocks += 1

append_n(blocks)

Put a list of blocks back into the free list

Parameters:

Source code in vllm/v1/core/kv_cache_utils.py
def append_n(self, blocks: list[KVCacheBlock]) -> None:
    """Put a list of blocks back into the free list

    Args:
        blocks: The blocks to append.

    """
    if not blocks:
        return

    tail = self.fake_free_list_tail
    last_block = tail.prev_free_block
    assert last_block is not None, (
        "prev_free_block of fake_free_list_tail should always exist"
    )
    # 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 = tail
    tail.prev_free_block = last_block

    self.num_free_blocks += len(blocks)

get_all_free_blocks()

Get all free blocks in the free list. Mainly used for testing.

Returns:

Source code in vllm/v1/core/kv_cache_utils.py
def get_all_free_blocks(self) -> list[KVCacheBlock]:
    """Get all free blocks in the free list. Mainly used for testing.

    Returns:
        A list of free blocks.

    """
    ret = []
    if self.fake_free_list_head.next_free_block is None:
        raise RuntimeError(
            "next_free_block of fake_free_list_head should always exist"
        )
    # Start from the first block
    curr_block: KVCacheBlock = self.fake_free_list_head.next_free_block
    # As long as next_free_block is available, we haven't reached to
    # the fake tail yet.
    while curr_block.next_free_block is not None:
        ret.append(curr_block)
        curr_block = curr_block.next_free_block
    return ret

iter_blocks_after(cursor)

Iterate free blocks in eviction order after the cursor.

Source code in vllm/v1/core/kv_cache_utils.py
def iter_blocks_after(
    self,
    cursor: KVCacheBlock | None,
) -> Iterator[KVCacheBlock]:
    """Iterate free blocks in eviction order after the cursor."""
    if cursor is None:
        curr_block = self.fake_free_list_head.next_free_block
    else:
        curr_block = cursor.next_free_block

    while curr_block is not None and curr_block is not self.fake_free_list_tail:
        yield curr_block
        curr_block = curr_block.next_free_block

popleft()

Pop the first free block and reduce num_free_blocks by 1.

Returns:

Source code in vllm/v1/core/kv_cache_utils.py
def popleft(self) -> KVCacheBlock:
    """Pop the first free block and reduce num_free_blocks by 1.

    Returns:
        The first free block.

    """
    head = self.fake_free_list_head
    first_block = head.next_free_block
    if first_block is self.fake_free_list_tail or first_block is None:
        assert self.num_free_blocks == 0, (
            f"num_free_blocks ({self.num_free_blocks}) is out of sync "
            "with the free list."
        )
        raise ValueError("No free blocks available")

    next_block = first_block.next_free_block
    if next_block is None:
        # This should not happen if the block is from the free list.
        # It indicates a bug in the caller's logic.
        raise RuntimeError(
            "Invalid block found in popleft() "
            "which doesn't have a valid next_free_block"
        )

    # Connect fake_head and the next block of first_block (i.e. second block
    # or fake tail).
    head.next_free_block = next_block
    next_block.prev_free_block = head

    # Remove the block from the linked list.
    first_block.prev_free_block = first_block.next_free_block = None

    self.num_free_blocks -= 1
    return first_block

popleft_n(n)

Pop the first n free blocks and reduce num_free_blocks by n.

Parameters:

  • n

    (int) –

    The number of blocks to pop.

Returns:

Source code in vllm/v1/core/kv_cache_utils.py
def popleft_n(self, n: int) -> list[KVCacheBlock]:
    """Pop the first n free blocks and reduce num_free_blocks by n.

    Args:
        n: The number of blocks to pop.

    Returns:
        A list of n free blocks.

    """
    if n == 0:
        return []
    assert self.num_free_blocks >= n
    self.num_free_blocks -= n

    curr_block = self.fake_free_list_head.next_free_block
    if n == 1:
        assert curr_block is not None
        next_block = curr_block.next_free_block
        curr_block.prev_free_block = None
        curr_block.next_free_block = None
        if next_block is not None:
            self.fake_free_list_head.next_free_block = next_block
            next_block.prev_free_block = self.fake_free_list_head
        return [curr_block]

    # Pop n blocks from the head of the list
    ret = []
    for _ in range(n):
        assert curr_block is not None
        ret.append(curr_block)
        last_block = curr_block
        curr_block = curr_block.next_free_block
        # Reset prev_free_block and next_free_block of all popped blocks
        last_block.prev_free_block = None
        last_block.next_free_block = None

    if curr_block is not None:
        # The queue is not empty, connect the fake head to
        # the new first block.
        self.fake_free_list_head.next_free_block = curr_block
        curr_block.prev_free_block = self.fake_free_list_head
    return ret

prepend_n(blocks)

Put a list of blocks at the front of the free list.

Source code in vllm/v1/core/kv_cache_utils.py
def prepend_n(self, blocks: list[KVCacheBlock]) -> None:
    """Put a list of blocks at the front of the free list."""
    if len(blocks) == 0:
        return

    first_block = self.fake_free_list_head.next_free_block
    assert first_block is not None, (
        "next_free_block of fake_free_list_head should always exist"
    )

    prev_block = self.fake_free_list_head
    for block in blocks:
        block.prev_free_block = prev_block
        prev_block.next_free_block = block
        prev_block = block

    prev_block.next_free_block = first_block
    first_block.prev_free_block = prev_block

    self.num_free_blocks += len(blocks)

remove(block)

Remove a block in the free list and reduce num_free_blocks by 1.

Parameters:

Source code in vllm/v1/core/kv_cache_utils.py
def remove(self, block: KVCacheBlock) -> None:
    """Remove a block in the free list and reduce num_free_blocks by 1.

    Args:
        block: The block to remove.

    """
    prev_block = block.prev_free_block
    next_block = block.next_free_block
    if prev_block is None or next_block is 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}")

    # Link the previous block to the next block.
    prev_block.next_free_block = next_block
    # Link the next block to the previous block.
    next_block.prev_free_block = prev_block

    # Remove the block from the linked list.
    block.prev_free_block = block.next_free_block = None
    self.num_free_blocks -= 1

KVCacheBlock dataclass

KV-cache block metadata.

Methods:

  • reset_hash –

    Reset the block hash when the block is evicted.

Source code in vllm/v1/core/kv_cache_utils.py
@dataclass(slots=True)
class KVCacheBlock:
    """KV-cache block metadata."""

    # Block ID, ranging from 0 to num_gpu_blocks - 1.
    block_id: int
    # Reference count.
    ref_cnt: int = 0
    # Deferred frees may contain blocks from multiple pools.
    pool: "BlockPool | None" = field(
        default=None, repr=False, compare=False, kw_only=True
    )
    # The hash key (block hash + group id) of the block, only available
    # when the block is full and cached.
    _block_hash: BlockHashWithGroupId | None = None
    # Number of prefix tokens covered by _block_hash. For full blocks this is
    # the full block boundary; partial entries can end inside a cache block.
    _block_hash_num_tokens: int | None = None

    # Used to construct a doubly linked list for free blocks.
    # These two attributes should only be manipulated by FreeKVCacheBlockQueue.
    prev_free_block: "KVCacheBlock | None" = None
    next_free_block: "KVCacheBlock | None" = None

    # Whether the block is a null block that should never be cached.
    is_null: bool = False

    @property
    def block_hash(self) -> BlockHashWithGroupId | None:
        return self._block_hash

    @property
    def block_hash_num_tokens(self) -> int | None:
        return self._block_hash_num_tokens

    def set_block_hash(
        self,
        block_hash: BlockHashWithGroupId,
        num_tokens: int | None = None,
    ) -> None:
        assert self.block_hash is None and self._block_hash_num_tokens is None, (
            "The block already has a hash. This should not happen."
        )
        self._block_hash = block_hash
        self._block_hash_num_tokens = num_tokens

    def reset_hash(self):
        """Reset the block hash when the block is evicted."""
        self._block_hash = None
        self._block_hash_num_tokens = None

    def __repr__(self) -> str:
        # Use block_id instead of KVCacheBlock object to avoid calling __repr__
        # on KVCacheBlock object recursively.
        prev_block_id = self.prev_free_block.block_id if self.prev_free_block else None
        next_block_id = self.next_free_block.block_id if self.next_free_block else None
        return (
            f"KVCacheBlock(block_id={self.block_id}, "
            f"ref_cnt={self.ref_cnt}, "
            f"_block_hash={self._block_hash!r}, "
            f"_block_hash_num_tokens={self._block_hash_num_tokens}, "
            f"prev_free_block={prev_block_id}, "
            f"next_free_block={next_block_id})"
        )

reset_hash()

Reset the block hash when the block is evicted.

Source code in vllm/v1/core/kv_cache_utils.py
def reset_hash(self):
    """Reset the block hash when the block is evicted."""
    self._block_hash = None
    self._block_hash_num_tokens = None

_gen_lora_extra_hash_keys(request)

Generate extra keys related to LoRA for block hash computation.

The adapter path is included so that re-pointing a LoRA name at a different adapter does not reuse KV computed with the previous one.

Parameters:

  • request

    (Request) –

    The request object.

Returns:

Source code in vllm/v1/core/kv_cache_utils.py
def _gen_lora_extra_hash_keys(request: Request) -> list[tuple[str, str, str]]:
    """Generate extra keys related to LoRA for block hash computation.

    The adapter path is included so that re-pointing a LoRA name at a different
    adapter does not reuse KV computed with the previous one.

    Args:
        request: The request object.

    Returns:
        Return the LoRA name and path of the request if it is a LoRA request.
        Return empty list otherwise.

    """
    lora_request = request.lora_request
    if not lora_request:
        return []
    return [("lora", lora_request.lora_name, lora_request.lora_path)]

_gen_mm_extra_hash_keys(request, start_token_idx, end_token_idx, start_mm_idx)

Generate extra keys related to MultiModal request for block hash computation. For multi-modal inputs, the extra keys are ("mm", mm_hash, start_offset) tuples that indicate a mm input contained in the block and its starting offset in the block tokens.

Parameters:

  • request

    (Request) –

    The request object.

  • start_token_idx

    (int) –

    The start token index of the block.

  • end_token_idx

    (int) –

    The end token index of the block.

  • start_mm_idx

    (int) –

    The start multi-modal index of the block.

Returns:

  • tuple[list[Any], int] –

    A tuple of extra keys and the next multi-modal index.

Source code in vllm/v1/core/kv_cache_utils.py
def _gen_mm_extra_hash_keys(
    request: Request, start_token_idx: int, end_token_idx: int, start_mm_idx: int
) -> tuple[list[Any], int]:
    """Generate extra keys related to MultiModal request for block hash
    computation. For multi-modal inputs, the extra keys are
    ("mm", mm_hash, start_offset) tuples that indicate a mm input contained
    in the block and its starting offset in the block tokens.

    Args:
        request: The request object.
        start_token_idx: The start token index of the block.
        end_token_idx: The end token index of the block.
        start_mm_idx: The start multi-modal index of the block.

    Returns:
        A tuple of extra keys and the next multi-modal index.

    """
    extra_keys: list[Any] = []

    mm_features = request.mm_features
    if not mm_features:
        return extra_keys, start_mm_idx

    # Note that we assume mm_features are sorted by mm_position.offset.
    # We do not need to check all mm inputs if the start token index is out of
    # range. This usually happens in the late prefill phase and decoding phase.
    last_pos = mm_features[-1].mm_position
    if last_pos.offset + last_pos.length <= start_token_idx:
        return extra_keys, start_mm_idx

    # Support start_mm_idx == -1 to indicate the last mm input.
    if start_mm_idx < 0:
        assert -start_mm_idx <= len(mm_features)
        start_mm_idx = len(mm_features) + start_mm_idx

    curr_mm_idx = start_mm_idx
    while mm_features and curr_mm_idx < len(mm_features):
        mm_feature = mm_features[curr_mm_idx]
        assert mm_feature.identifier is not None
        offset = mm_feature.mm_position.offset
        length = mm_feature.mm_position.length
        if end_token_idx > offset:
            if start_token_idx >= offset + length:
                # This block has passed the current mm input.
                curr_mm_idx += 1
                continue

            # The block contains the current mm input. Include its offset
            # relative to the start of the block so prefix-cache keys stay
            # distinct when the same MM item appears at different positions
            # within otherwise-identical placeholder blocks.
            extra_keys.append(("mm", mm_feature.identifier, offset - start_token_idx))

            if end_token_idx >= offset + length:
                # If this block contains the end of the current mm input,
                # move to the next mm input as this block may also contain
                # the next mm input.
                curr_mm_idx += 1
            else:
                # Otherwise this block is done with mm inputs.
                break
        else:
            # This block has not reached the current mm input.
            break
    return extra_keys, curr_mm_idx

_gen_prompt_embeds_extra_hash_keys(request, start_token_idx, end_token_idx)

Generate extra keys related to prompt embeds for block hash computation.

Parameters:

  • request

    (Request) –

    The request object.

  • start_token_idx

    (int) –

    The start token index of the block.

  • end_token_idx

    (int) –

    The end token index of the block.

Returns:

Source code in vllm/v1/core/kv_cache_utils.py
def _gen_prompt_embeds_extra_hash_keys(
    request: Request, start_token_idx: int, end_token_idx: int
) -> list[tuple[str, bytes]]:
    """Generate extra keys related to prompt embeds for block hash computation.

    Args:
        request: The request object.
        start_token_idx: The start token index of the block.
        end_token_idx: The end token index of the block.

    Returns:
        Return a stable hash of the block prompt embeddings if prompt embeds
        are present. Return empty list otherwise.

    """
    if request.prompt_embeds is None:
        return []
    block_range = (start_token_idx, end_token_idx)
    embeds_hash = request._prompt_embeds_per_block_hashes.get(block_range)
    if embeds_hash is None:
        block_prompt_embeds = request.prompt_embeds[start_token_idx:end_token_idx]
        # Hash prompt embeds once per block and cache on request
        embeds_hash = hashlib.sha256(tensor_data(block_prompt_embeds)).digest()
        request._prompt_embeds_per_block_hashes[block_range] = embeds_hash
    return [("prompt_embeds", embeds_hash)]

eagle_proof_margin(block_size, hash_block_size, fine_grained_lookup)

Tokens an EAGLE group matches past a cache hit before dropping them.

Fine-grained lookups drop one hash unit; others drop one cache block.

Source code in vllm/v1/core/kv_cache_utils.py
def eagle_proof_margin(
    block_size: int, hash_block_size: int, fine_grained_lookup: bool
) -> int:
    """Tokens an EAGLE group matches past a cache hit before dropping them.

    Fine-grained lookups drop one hash unit; others drop one cache block.
    """
    if fine_grained_lookup and block_size > hash_block_size:
        return hash_block_size
    return block_size

generate_block_hash_extra_keys(request, start_token_idx, end_token_idx, start_mm_idx)

Generate extra keys for the block hash. The extra keys can come from the multi-modal inputs, request specific metadata (e.g., LoRA names), and hashed data from prompt embeddings.

Parameters:

  • request

    (Request) –

    The request object.

  • start_token_idx

    (int) –

    The start token index of the block.

  • end_token_idx

    (int) –

    The end token index of the block.

  • start_mm_idx

    (int) –

    The start multi-modal index of the block.

Returns:

  • tuple[tuple[Any, ...] | None, int] –

    A tuple of extra keys and the next multi-modal index.

Source code in vllm/v1/core/kv_cache_utils.py
def generate_block_hash_extra_keys(
    request: Request, start_token_idx: int, end_token_idx: int, start_mm_idx: int
) -> tuple[tuple[Any, ...] | None, int]:
    """Generate extra keys for the block hash. The extra keys can come from
    the multi-modal inputs, request specific metadata (e.g., LoRA names), and
    hashed data from prompt embeddings.

    Args:
        request: The request object.
        start_token_idx: The start token index of the block.
        end_token_idx: The end token index of the block.
        start_mm_idx: The start multi-modal index of the block.

    Returns:
        A tuple of extra keys and the next multi-modal index.

    """
    mm_extra_keys: list[Any]
    mm_extra_keys, new_start_mm_idx = _gen_mm_extra_hash_keys(
        request, start_token_idx, end_token_idx, start_mm_idx
    )
    lora_extra_keys = _gen_lora_extra_hash_keys(request)
    cache_salt_keys: list[tuple[str, str]] = (
        [("cache_salt", request.cache_salt)]
        if (start_token_idx == 0 and request.cache_salt)
        else []
    )
    prompt_embeds_keys = _gen_prompt_embeds_extra_hash_keys(
        request, start_token_idx, end_token_idx
    )

    extra_keys: list[Any] = (
        lora_extra_keys + mm_extra_keys + cache_salt_keys + prompt_embeds_keys
    )

    if not extra_keys:
        return None, new_start_mm_idx

    return tuple(extra_keys), new_start_mm_idx

generate_scheduler_kv_cache_config(kv_cache_configs)

Generate the KV cache configuration for the scheduler.

Source code in vllm/v1/core/kv_cache_utils.py
def generate_scheduler_kv_cache_config(
    kv_cache_configs: list[KVCacheConfig],
) -> KVCacheConfig:
    """Generate the KV cache configuration for the scheduler."""
    assert all(
        [cfg.num_blocks == kv_cache_configs[0].num_blocks for cfg in kv_cache_configs]
    )
    assert all(
        cfg.hisparse_host_num_blocks == kv_cache_configs[0].hisparse_host_num_blocks
        for cfg in kv_cache_configs
    )
    # All workers have the same kv_cache_config except layer names, so use
    # an arbitrary one to initialize the scheduler.
    cfg = copy.deepcopy(kv_cache_configs[0])
    for group in cfg.kv_cache_groups:
        if isinstance(group.kv_cache_spec, UniformTypeKVCacheSpecs):
            # All layers in the UniformTypeKVCacheSpecs have the same type,
            # so use an arbitrary one to initialize the scheduler.
            group.kv_cache_spec = next(
                iter(group.kv_cache_spec.kv_cache_specs.values())
            )
    return cfg

get_block_hash(key)

Extract the BlockHash from a BlockHashWithGroupId.

Source code in vllm/v1/core/kv_cache_utils.py
def get_block_hash(key: BlockHashWithGroupId) -> BlockHash:
    """Extract the `BlockHash` from a `BlockHashWithGroupId`."""
    return BlockHash(key[:-4])

get_group_id(key)

Extract the group id from a BlockHashWithGroupId.

Source code in vllm/v1/core/kv_cache_utils.py
def get_group_id(key: BlockHashWithGroupId) -> int:
    """Extract the group id from a `BlockHashWithGroupId`."""
    return int.from_bytes(key[-4:], "big", signed=False)

get_kv_cache_capacity(vllm_config, kv_cache_config)

Get the group-aware KV cache token capacity and max concurrency.

Source code in vllm/v1/core/kv_cache_utils.py
def get_kv_cache_capacity(
    vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
) -> tuple[int, float]:
    """Get the group-aware KV cache token capacity and max concurrency."""
    max_model_len = vllm_config.model_config.max_model_len
    max_concurrency = get_max_concurrency_for_kv_cache_config(
        vllm_config, kv_cache_config
    )
    return int(max_concurrency * max_model_len), max_concurrency

get_max_concurrency_for_kv_cache_config(vllm_config, kv_cache_config)

Get the maximum concurrency for the given KV cache configuration.

A request at max_model_len consumes whole blocks from each group's block table — cdiv(per-request bytes, page bytes) of the group's spec — and all device groups draw those block ids from one shared pool, so the per-request total is the sum over groups. The memory/page ratio is identical whether a group carries an aggregated UniformTypeKVCacheSpecs (worker config) or a representative per-layer spec (scheduler config), so both capacity call sites agree.

Host groups use a separate pool; the smaller concurrency limit applies.

Source code in vllm/v1/core/kv_cache_utils.py
def get_max_concurrency_for_kv_cache_config(
    vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
) -> float:
    """Get the maximum concurrency for the given KV cache configuration.

    A request at max_model_len consumes whole blocks from each group's block
    table — cdiv(per-request bytes, page bytes) of the group's spec — and all
    device groups draw those block ids from one shared pool, so the per-request
    total is the sum over groups. The memory/page ratio is identical whether
    a group carries an aggregated UniformTypeKVCacheSpecs (worker config) or
    a representative per-layer spec (scheduler config), so both capacity
    call sites agree.

    Host groups use a separate pool; the smaller concurrency limit applies.
    """
    num_blocks_per_request = 0
    host_blocks_per_request = 0
    for group in kv_cache_config.kv_cache_groups:
        required = cdiv(
            group.kv_cache_spec.max_memory_usage_bytes(vllm_config),
            group.kv_cache_spec.page_size_bytes,
        )
        if group.host_resident:
            host_blocks_per_request += required
        else:
            num_blocks_per_request += required
    limits = [kv_cache_config.num_blocks / num_blocks_per_request]
    if host_blocks_per_request:
        assert kv_cache_config.hisparse_host_num_blocks is not None
        limits.append(
            kv_cache_config.hisparse_host_num_blocks / host_blocks_per_request
        )
    return min(limits)

get_none_hash_seed()

Return the seed NONE_HASH was derived from.

Components that must agree on NONE_HASH across processes (the P2P tier advertises this during its connect handshake) read the resolved seed here instead of re-deriving it, so they observe the random seed too. Falls back to the deterministic seed before init_none_hash has run.

Source code in vllm/v1/core/kv_cache_utils.py
def get_none_hash_seed() -> str:
    """Return the seed NONE_HASH was derived from.

    Components that must agree on NONE_HASH across processes (the P2P tier
    advertises this during its connect handshake) read the resolved seed here
    instead of re-deriving it, so they observe the random seed too. Falls back
    to the deterministic seed before ``init_none_hash`` has run.
    """
    if _NONE_HASH_SEED is None:
        return DEFAULT_NONE_HASH_SEED
    return _NONE_HASH_SEED

get_request_block_hasher(hash_block_size, caching_hash_fn)

Returns a function which computes the list of un-computed block hashes of a request.

Hashes are computed at hash_block_size granularity and chained over the full prefix, so each hash uniquely fingerprints the prefix ending at its boundary. Coarser group block sizes and partial-cache boundaries reuse these hashes directly (see BlockHashListWithBlockSize).

Source code in vllm/v1/core/kv_cache_utils.py
def get_request_block_hasher(
    hash_block_size: int,
    caching_hash_fn: Callable[[Any], bytes],
) -> Callable[[Request], list[BlockHash]]:
    """Returns a function which computes the list of un-computed block hashes
    of a request.

    Hashes are computed at ``hash_block_size`` granularity and chained over the
    full prefix, so each hash uniquely fingerprints the prefix ending at its
    boundary. Coarser group block sizes and partial-cache boundaries reuse
    these hashes directly (see ``BlockHashListWithBlockSize``).
    """

    def request_block_hasher(request: Request) -> list[BlockHash]:
        start_token_idx = len(request.block_hashes) * hash_block_size
        num_tokens = request.num_tokens

        if start_token_idx + hash_block_size > num_tokens:
            # Early stop when there no new full blocks created.
            return []

        curr_mm_idx = 0
        mm_features = request.mm_features
        if start_token_idx > 0 and mm_features:
            last_mm_pos = mm_features[-1].mm_position
            if last_mm_pos.offset + last_mm_pos.length > start_token_idx:
                curr_mm_idx, _ = get_mm_features_in_window(
                    mm_features,
                    start_token_idx,
                    start_token_idx + hash_block_size,
                )

        prev_block_hash_value = (
            request.block_hashes[-1] if request.block_hashes else None
        )
        new_block_hashes: list[BlockHash] = []
        while True:
            end_token_idx = start_token_idx + hash_block_size
            if end_token_idx > num_tokens:
                # We only hash full blocks
                break

            # MM and LoRA requests need extra keys for block-hash computation.
            extra_keys, curr_mm_idx = generate_block_hash_extra_keys(
                request, start_token_idx, end_token_idx, curr_mm_idx
            )

            # Compute the hash of the current block
            block_tokens = request.all_token_ids[start_token_idx:end_token_idx]
            block_hash = hash_block_tokens(
                caching_hash_fn, prev_block_hash_value, block_tokens, extra_keys
            )

            new_block_hashes.append(block_hash)
            start_token_idx += hash_block_size
            prev_block_hash_value = block_hash

        return new_block_hashes

    return request_block_hasher

hash_block_tokens(hash_function, parent_block_hash, curr_block_token_ids, extra_keys=None)

Computes a hash value corresponding to the contents of a block and the contents of the preceding block(s). The hash value is used for prefix caching. We use LRU cache for this function to avoid recomputing hash values for the same block contents.

Parameters:

  • hash_function

    (Callable[[Any], bytes]) –

    The hash function used to compute block hash.

  • parent_block_hash

    (BlockHash | None) –

    The hash of the parent block. None if this is the first block.

  • curr_block_token_ids

    (Sequence[int]) –

    A list of token ids in the current block. The current block is assumed to be full.

  • extra_keys

    (tuple[Any, ...] | None, default: None ) –

    Extra keys for the block.

Returns:

  • BlockHash –

    The hash value of the block and the token ids in the block.

  • BlockHash –

    The entire tuple is used as the hash key of the block.

Source code in vllm/v1/core/kv_cache_utils.py
def hash_block_tokens(
    hash_function: Callable[[Any], bytes],
    parent_block_hash: BlockHash | None,
    curr_block_token_ids: Sequence[int],
    extra_keys: tuple[Any, ...] | None = None,
) -> BlockHash:
    """Computes a hash value corresponding to the contents of a block and
    the contents of the preceding block(s). The hash value is used for
    prefix caching. We use LRU cache for this function to avoid recomputing
    hash values for the same block contents.

    Args:
        hash_function: The hash function used to compute block hash.
        parent_block_hash: The hash of the parent block. None
            if this is the first block.
        curr_block_token_ids: A list of token ids in the current
            block. The current block is assumed to be full.
        extra_keys: Extra keys for the block.

    Returns:
        The hash value of the block and the token ids in the block.
        The entire tuple is used as the hash key of the block.

    """
    if not parent_block_hash:
        parent_block_hash = NONE_HASH

    curr_block_token_ids_tuple = tuple(curr_block_token_ids)
    return BlockHash(
        hash_function((parent_block_hash, curr_block_token_ids_tuple, extra_keys))
    )

make_block_hash_with_group_id(block_hash, group_id)

Pack a BlockHash and group id into a BlockHashWithGroupId.

The group id is encoded using 4 bytes in big-endian order and appended to the block hash bytes. This representation avoids creating tuples while still allowing us to recover both components when needed.

Source code in vllm/v1/core/kv_cache_utils.py
def make_block_hash_with_group_id(
    block_hash: BlockHash, group_id: int
) -> BlockHashWithGroupId:
    """Pack a `BlockHash` and group id into a `BlockHashWithGroupId`.

    The group id is encoded using 4 bytes in big-endian order and appended to
    the block hash bytes.  This representation avoids creating tuples while
    still allowing us to recover both components when needed.
    """
    return BlockHashWithGroupId(block_hash + group_id.to_bytes(4, "big", signed=False))

partial_hash_hits_enabled(kv_cache_groups, hash_block_size, dcp_world_size=1, manager_classes=None)

Whether aligned Mamba states support sub-block prefix-cache hits.

If manager_classes (one per group) is given, also require every other prefix-cacheable group to support fine-grained lookups.

Source code in vllm/v1/core/kv_cache_utils.py
def partial_hash_hits_enabled(
    kv_cache_groups: Sequence[KVCacheGroupSpec],
    hash_block_size: int,
    dcp_world_size: int = 1,
    manager_classes: Sequence[type["SingleTypeKVCacheManager"]] | None = None,
) -> bool:
    """Whether aligned Mamba states support sub-block prefix-cache hits.

    If ``manager_classes`` (one per group) is given, also require every other
    prefix-cacheable group to support fine-grained lookups.
    """
    if not any(
        isinstance(spec, MambaSpec)
        and spec.mamba_cache_mode == "align"
        and (
            (dcp_world_size == 1 and spec.block_size > hash_block_size)
            or (dcp_world_size > 1 and spec.block_size >= hash_block_size)
        )
        for group in kv_cache_groups
        for spec in iter_layer_specs(group.kv_cache_spec)
    ):
        return False
    if manager_classes is None:
        return True
    unsupported_managers = {
        manager_cls.__name__
        for group, manager_cls in zip(kv_cache_groups, manager_classes)
        if group.kv_cache_spec.prefix_cacheable
        and not manager_cls.supports_fine_grained_hash_lookup
        and resolve_dcp_kv_block_size(group.kv_cache_spec, dcp_world_size)
        != hash_block_size
    }
    if unsupported_managers:
        logger.warning_once(
            "Disabling fine-grained prefix-cache hits because these KV "
            "cache managers require block-aligned lookups: %s.",
            ", ".join(sorted(unsupported_managers)),
        )
        return False
    return True

resolve_block_hashes(block_hashes, hash_block_size, block_size, *, supports_fine_grained_hash_lookup=False, alignment_tokens=None)

Resolve the block-hash view at block_size.

When block_size equals hash_block_size, reuse the precomputed block hashes directly; otherwise view them at block_size granularity. Fine-grained lookup keeps the original hashes for partial cache hits.

Source code in vllm/v1/core/kv_cache_utils.py
def resolve_block_hashes(
    block_hashes: BlockHashList,
    hash_block_size: int,
    block_size: int,
    *,
    supports_fine_grained_hash_lookup: bool = False,
    alignment_tokens: int | None = None,
) -> BlockHashList:
    """Resolve the block-hash view at ``block_size``.

    When ``block_size`` equals ``hash_block_size``, reuse the precomputed block
    hashes directly; otherwise view them at ``block_size`` granularity.
    Fine-grained lookup keeps the original hashes for partial cache hits.
    """
    if block_size == hash_block_size:
        return block_hashes
    if isinstance(block_hashes, BlockHashListWithBlockSize):
        # Already a block-size view
        assert block_hashes.scale_factor == block_size // hash_block_size
        return block_hashes
    # Fine-grained partial hits keep the raw hashes. The caller passes
    # alignment_tokens = hash_block_size to enable them, else >= block_size.
    if (
        supports_fine_grained_hash_lookup
        and alignment_tokens is not None
        and alignment_tokens < block_size
        and block_size % alignment_tokens == 0
    ):
        return block_hashes
    assert block_size % hash_block_size == 0
    return BlockHashListWithBlockSize(block_hashes, hash_block_size, block_size)

resolve_cache_hit_alignment_tokens(kv_cache_config, vllm_config, scheduler_block_size, hash_block_size)

Token granularity at which prefix-cache hits land.

Mirrors the alignment the hybrid KV cache coordinator pushes to its managers: fine-grained hits land on hash_block_size, all others on scheduler_block_size.

Source code in vllm/v1/core/kv_cache_utils.py
def resolve_cache_hit_alignment_tokens(
    kv_cache_config: KVCacheConfig,
    vllm_config: VllmConfig,
    scheduler_block_size: int,
    hash_block_size: int,
) -> int:
    """Token granularity at which prefix-cache hits land.

    Mirrors the alignment the hybrid KV cache coordinator pushes to its
    managers: fine-grained hits land on ``hash_block_size``, all others on
    ``scheduler_block_size``.
    """
    groups = kv_cache_config.kv_cache_groups
    if not vllm_config.cache_config.enable_prefix_caching or len(groups) <= 1:
        return scheduler_block_size
    manager_classes = []
    for group in groups:
        manager_cls = KVCacheSpecRegistry.get_manager_class(
            group.kv_cache_spec, group.role
        )
        assert manager_cls is not None
        manager_classes.append(manager_cls)
    if partial_hash_hits_enabled(
        groups,
        hash_block_size,
        vllm_config.parallel_config.decode_context_parallel_size,
        manager_classes,
    ):
        return hash_block_size
    return scheduler_block_size

resolve_dcp_kv_block_size(spec, dcp_world_size)

Return the token span of a cache block under DCP.

Source code in vllm/v1/core/kv_cache_utils.py
def resolve_dcp_kv_block_size(spec: KVCacheSpec, dcp_world_size: int) -> int:
    """Return the token span of a cache block under DCP."""
    return spec.block_size * (dcp_world_size if spec.dcp_sharded else 1)

resolve_dcp_kv_cache_spec(spec, dcp_world_size)

Return a KV cache spec with block sizes adjusted for DCP.

Source code in vllm/v1/core/kv_cache_utils.py
def resolve_dcp_kv_cache_spec(spec: KVCacheSpec, dcp_world_size: int) -> KVCacheSpec:
    """Return a KV cache spec with block sizes adjusted for DCP."""
    block_size = resolve_dcp_kv_block_size(spec, dcp_world_size)
    if block_size == spec.block_size:
        return spec
    if isinstance(spec, UniformTypeKVCacheSpecs):
        return replace(
            spec,
            block_size=block_size,
            kv_cache_specs={
                name: resolve_dcp_kv_cache_spec(layer_spec, dcp_world_size)
                for name, layer_spec in spec.kv_cache_specs.items()
            },
        )
    return replace(spec, block_size=block_size)

resolve_kv_cache_block_sizes(kv_cache_config, vllm_config)

Resolve (scheduler_block_size, hash_block_size).

  • scheduler_block_size is the token-alignment invariant used by the scheduler (e.g. for num_computed_tokens rounding). Single group: cache_config.block_size * dcp. Multiple groups: LCM of every group's effective block size. Attention groups are scaled by DCP; Mamba groups keep their full per-rank state and are not scaled.
  • hash_block_size is the granularity at which Request.block_hashes is computed. Single group: equals scheduler block size, and any other cache_config.prefix_match_unit is rejected while block hashing is active. Multiple groups: cache_config.prefix_match_unit override if set, else the GCD of group block sizes; every group's block size must be divisible by it. Returns the scheduler block size (i.e. disables finer hashing) if block hashing is inactive or a mamba group is not using cache mode "align".
Source code in vllm/v1/core/kv_cache_utils.py
def resolve_kv_cache_block_sizes(
    kv_cache_config: KVCacheConfig,
    vllm_config: VllmConfig,
) -> tuple[int, int]:
    """Resolve (scheduler_block_size, hash_block_size).

    - ``scheduler_block_size`` is the token-alignment invariant used by the
      scheduler (e.g. for ``num_computed_tokens`` rounding). Single group:
      ``cache_config.block_size * dcp``. Multiple groups: LCM of every
      group's effective block size. Attention groups are scaled by DCP;
      Mamba groups keep their full per-rank state and are not scaled.
    - ``hash_block_size`` is the granularity at which ``Request.block_hashes``
      is computed. Single group: equals scheduler block size, and any other
      ``cache_config.prefix_match_unit`` is rejected while block hashing is
      active. Multiple groups:
      ``cache_config.prefix_match_unit`` override if set, else the GCD of
      group block sizes; every group's block size must be divisible by it.
      Returns the scheduler block size (i.e. disables finer hashing) if block
      hashing is inactive or a mamba group is not using cache mode "align".
    """
    cache_config = vllm_config.cache_config
    dcp = vllm_config.parallel_config.decode_context_parallel_size
    groups = kv_cache_config.kv_cache_groups

    if len(groups) <= 1:
        if groups and not groups[0].kv_cache_spec.dcp_sharded:
            dcp = 1
        bs = cache_config.block_size * dcp
        # Reject a prefix_match_unit that would otherwise be silently ignored.
        if (
            cache_config.prefix_match_unit not in (None, bs)
            and len(groups) == 1
            and (
                cache_config.enable_prefix_caching
                or vllm_config.kv_transfer_config is not None
            )
        ):
            raise ValueError(
                f"Invalid prefix_match_unit={cache_config.prefix_match_unit}; "
                "with a single KV cache group, prefix-cache hits land on the "
                f"block size ({bs}). Unset it or set it to {bs}."
            )
        return bs, bs

    group_block_sizes = [
        resolve_dcp_kv_block_size(g.kv_cache_spec, dcp) for g in groups
    ]
    scheduler_block_size = math.lcm(*group_block_sizes)

    logger.info_once("kv cache group sizes %s", tuple(group_block_sizes))
    logger.info_once("kv lcm block sizes %s", scheduler_block_size)

    # Block hashes are only consumed by prefix caching and KV connectors
    # (P/D, offloading); when neither is active, keep hash_block_size equal
    # to the scheduler block size.
    connector_enabled = vllm_config.kv_transfer_config is not None
    if not (cache_config.enable_prefix_caching or connector_enabled):
        return scheduler_block_size, scheduler_block_size

    # Mamba groups outside align mode break divisibility; back off to the
    # scheduler block size. Read the mode from the resolved group spec because
    # its block size may have been updated independently of cache_config.
    if any(
        isinstance(spec, MambaSpec) and spec.mamba_cache_mode != "align"
        for group in groups
        for spec in iter_layer_specs(group.kv_cache_spec)
    ):
        return scheduler_block_size, scheduler_block_size

    hashing_sizes = [
        block_size
        for group, block_size in zip(groups, group_block_sizes)
        if group.kv_cache_spec.prefix_cacheable
    ] or group_block_sizes
    requested = cache_config.prefix_match_unit
    hash_block_size = requested if requested is not None else math.gcd(*hashing_sizes)
    if any(bs % hash_block_size != 0 for bs in hashing_sizes):
        raise ValueError(
            f"Invalid prefix_match_unit={hash_block_size}; prefix-cacheable "
            "KV cache group block sizes must be divisible by prefix_match_unit. "
            f"Got group block sizes={group_block_sizes}, "
            f"prefix-cacheable={hashing_sizes}."
        )
    prefix_alignments = {
        spec.tokens_per_state
        for group in groups
        for spec in iter_layer_specs(group.kv_cache_spec)
        if spec.prefix_cacheable
        and isinstance(spec.tokens_per_state, int)
        and spec.tokens_per_state > 1
    }
    cache_hit_alignment = (
        hash_block_size
        if partial_hash_hits_enabled(groups, hash_block_size, dcp)
        else scheduler_block_size
    )
    if any(cache_hit_alignment % alignment for alignment in prefix_alignments):
        raise ValueError(
            f"Invalid prefix_match_unit={hash_block_size}; prefix-cache boundaries "
            "must align with each spec's per-state compression. "
            f"Got alignments={sorted(prefix_alignments)}."
        )
    return scheduler_block_size, hash_block_size

resolve_none_hash_seed(hash_fn)

Resolve the seed to derive NONE_HASH from.

PYTHONHASHSEED wins if set. Otherwise cryptographic algorithms get the fixed default (shareable across processes) and non-cryptographic ones get fresh random bytes, keeping the seed unpredictable where collision resistance depends on it.

Source code in vllm/v1/core/kv_cache_utils.py
def resolve_none_hash_seed(hash_fn: Callable[[Any], bytes]) -> str:
    """Resolve the seed to derive NONE_HASH from.

    PYTHONHASHSEED wins if set. Otherwise cryptographic algorithms get the
    fixed default (shareable across processes) and non-cryptographic ones get
    fresh random bytes, keeping the seed unpredictable where collision
    resistance depends on it.
    """
    hash_seed = os.getenv("PYTHONHASHSEED")
    if hash_seed is not None:
        return hash_seed
    if hash_fn in _NON_CRYPTO_HASH_FUNCTIONS:
        return os.urandom(32).hex()
    return DEFAULT_NONE_HASH_SEED

to_event_extra_keys(extra_keys)

Convert block-hash extra keys to the untagged per-block list published in KV events.

External KV event consumers parse the pre-tagging shapes: bare LoRA names and cache salts, (mm_identifier, offset) pairs and bare prompt-embeds digests. Events keep that format until consumers handle the tagged keys.

Parameters:

  • extra_keys

    (Iterable[tuple[Any, ...] | None] | None) –

    One entry per block, each as returned by generate_block_hash_extra_keys, or None.

Returns:

  • list[tuple[Any, ...] | None] | None –

    One untagged entry per block, or None if there are no entries.

Source code in vllm/v1/core/kv_cache_utils.py
def to_event_extra_keys(
    extra_keys: Iterable[tuple[Any, ...] | None] | None,
) -> list[tuple[Any, ...] | None] | None:
    """Convert block-hash extra keys to the untagged per-block list published
    in KV events.

    External KV event consumers parse the pre-tagging shapes: bare LoRA names
    and cache salts, ``(mm_identifier, offset)`` pairs and bare prompt-embeds
    digests. Events keep that format until consumers handle the tagged keys.

    Args:
        extra_keys: One entry per block, each as returned by
            `generate_block_hash_extra_keys`, or None.

    Returns:
        One untagged entry per block, or None if there are no entries.

    """
    if not extra_keys:
        return None
    event_keys = [
        None
        if keys is None
        else tuple(key[1:] if key[0] == "mm" else key[1] for key in keys)
        for keys in extra_keys
    ]
    return event_keys or None

update_kv_cache_capacity(vllm_config, kv_cache_config)

Store and log the resolved KV cache capacity.

Source code in vllm/v1/core/kv_cache_utils.py
def update_kv_cache_capacity(
    vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
) -> None:
    """Store and log the resolved KV cache capacity."""
    num_tokens, max_concurrency = get_kv_cache_capacity(vllm_config, kv_cache_config)
    vllm_config.cache_config.kv_cache_size_tokens = num_tokens
    vllm_config.cache_config.kv_cache_max_concurrency = max_concurrency
    max_model_len = vllm_config.model_config.max_model_len
    pools = _kv_cache_device_name(vllm_config)
    if vllm_config.attention_config.hisparse_config is not None:
        pools += " + host"
    logger.info_once(
        "%s KV cache size: %s tokens, "
        "Maximum concurrency for %s tokens per request: %.2fx",
        pools,
        f"{num_tokens:,}",
        f"{max_model_len:,}",
        max_concurrency,
    )