Skip to content

vllm.v1.core.single_type_kv_cache_manager

Classes:

Functions:

ChunkedLocalAttentionManager

Bases: SingleTypeKVCacheManager

Methods:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
class ChunkedLocalAttentionManager(SingleTypeKVCacheManager):
    def __init__(self, kv_cache_spec: ChunkedLocalAttentionSpec, **kwargs) -> None:
        super().__init__(kv_cache_spec, **kwargs)
        self.attention_chunk_size = kv_cache_spec.attention_chunk_size

    @classmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        """
        For chunked local attention, we need to find the longest cache hit
        prefix of the blocks that is not longer than `max_length`. The prefix
        should be a common prefix hit for all the kv cache groups in
        `kv_cache_group_ids`. If no cache hit is found, return an empty list.
        note we mark as computed if the whole block is outside of the local
        window, and set the block as null. Examples:

        1. Attention chunk size of 8, block size of 4, max length of 15
        for next token at 15th (zero-indexed), 8th - 14th tokens are in
        the window(needs lookup), 0th - 7th are not in the window,
        so they are already marked as computed. We check the complete
        block3 (8th - 11th tokens), Assume block 3 is hit, we will return
        [null, null, block 3], otherwise, we return [null, null]

        2. Attention chunk size of 8, block size of 4, max length of 16
        for next token at 16th (zero-indexed), 0th - 15th tokens are not
        in the window, so they are already marked as computed.
        we return 4 blocks[null, null, null, null]

        Args:
            block_hashes: The block hashes of the request.
            max_length: The maximum length of the cache hit prefix.
            kv_cache_group_ids: The ids of the kv cache groups.
            block_pool: The block pool.
            kv_cache_spec: The kv cache spec.
            drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
            dcp_world_size: The world size of decode context parallelism.
            pcp_world_size: The world size of prefill context parallelism.
            alignment_tokens: The returned cache hit length (in tokens) should
                be a multiple of this value (in tokens).

        Returns:
            A list of cached blocks
        """
        assert isinstance(kv_cache_spec, ChunkedLocalAttentionSpec), (
            "ChunkedLocalAttentionManager can only be used for "
            "chunked local attention groups"
        )
        assert drop_eagle_block is False, (
            "Hybrid KV cache is not supported for " + "eagle + chunked local attention."
        )
        assert dcp_world_size == 1, "DCP not support chunked local attn now."
        assert pcp_world_size == 1, "PCP not support chunked local attn now."
        assert kv_cache_spec.block_size == alignment_tokens, (
            "KV cache groups with different block sizes are not compatible with "
            "chunked local attention now"
        )
        block_hashes = resolve_block_hashes(
            block_hashes,
            block_pool.hash_block_size,
            kv_cache_spec.block_size,
            supports_fine_grained_hash_lookup=cls.supports_fine_grained_hash_lookup,
            alignment_tokens=alignment_tokens,
        )
        max_num_blocks = max_length // kv_cache_spec.block_size
        if max_length > 0:
            local_attention_start_idx = (
                max_length
                // kv_cache_spec.attention_chunk_size
                * kv_cache_spec.attention_chunk_size
            )
        else:
            local_attention_start_idx = 0
        # we marked blocks out of window as computed
        # with null blocks, and blocks inside window based on cache lookup
        # result [null] [null] ... [null] [hit block 1 (1st block contain
        # last window)] [hit block 2] ... [hit block x]
        local_attention_start_block_idx = (
            local_attention_start_idx // kv_cache_spec.block_size
        )
        computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
            [block_pool.null_block] * local_attention_start_block_idx
            for _ in range(len(kv_cache_group_ids))
        )
        for i in range(local_attention_start_block_idx, max_num_blocks):
            block_hash = block_hashes[i]
            if cached_block := block_pool.get_cached_block(
                block_hash, kv_cache_group_ids
            ):
                for computed, cached in zip(computed_blocks, cached_block):
                    computed.append(cached)
            else:
                break
        hit_length = len(computed_blocks[0]) * kv_cache_spec.block_size
        return computed_blocks, hit_length

    def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
        """
        Get the number of tokens that will be skipped for attention computation.

        For chunked local attention, this corresponds to the tokens that are on
        the left side of the current chunk.

        Example 1:
        chunk size = 8, num_computed_tokens = 13
        Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
                 | ----- computed ---------------|
                                                  ^^ next token to be computed
                                   |----------------| <-- attention window for
                                                          next token
                 |--- skipped -----|
        Output: get_num_skipped_tokens(13) == 8

        Example 2:
        chunk size = 8, num_computed_tokens = 8
        Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
                 | --- computed ---|
                                     ^ next token to be computed
                                   |--| <-- attention window for next token
                 | --- skipped ----|
        Output: get_num_skipped_tokens(8) == 8

        Example 3:
        chunk size = 8, num_computed_tokens = 7
        Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
                 |---computed---|
                                 ^ next token to be computed
                 |-----------------| <-- attention window for next token
                 no token should be skipped.
        Output: get_num_skipped_tokens(7) == 0

        Args:
            num_computed_tokens: The number of tokens that have been computed.

        Returns:
            The number of tokens that will be skipped for attention computation.
        """
        num_skipped_tokens = (
            num_computed_tokens // self.attention_chunk_size
        ) * self.attention_chunk_size
        return num_skipped_tokens

    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        """
        cascade attention is not supported by chunked local attention.
        """
        return 0

find_longest_cache_hit(block_hashes, max_length, kv_cache_group_ids, block_pool, kv_cache_spec, drop_eagle_block, alignment_tokens, dcp_world_size=1, pcp_world_size=1) classmethod

For chunked local attention, we need to find the longest cache hit prefix of the blocks that is not longer than max_length. The prefix should be a common prefix hit for all the kv cache groups in kv_cache_group_ids. If no cache hit is found, return an empty list. note we mark as computed if the whole block is outside of the local window, and set the block as null. Examples:

  1. Attention chunk size of 8, block size of 4, max length of 15 for next token at 15th (zero-indexed), 8th - 14th tokens are in the window(needs lookup), 0th - 7th are not in the window, so they are already marked as computed. We check the complete block3 (8th - 11th tokens), Assume block 3 is hit, we will return [null, null, block 3], otherwise, we return [null, null]

  2. Attention chunk size of 8, block size of 4, max length of 16 for next token at 16th (zero-indexed), 0th - 15th tokens are not in the window, so they are already marked as computed. we return 4 blocks[null, null, null, null]

Parameters:

  • block_hashes

    (BlockHashList) –

    The block hashes of the request.

  • max_length

    (int) –

    The maximum length of the cache hit prefix.

  • kv_cache_group_ids

    (list[int]) –

    The ids of the kv cache groups.

  • block_pool

    (BlockPool) –

    The block pool.

  • kv_cache_spec

    (KVCacheSpec) –

    The kv cache spec.

  • drop_eagle_block

    (bool) –

    Whether to drop the last matched block for EAGLE/MTP.

  • dcp_world_size

    (int, default: 1 ) –

    The world size of decode context parallelism.

  • pcp_world_size

    (int, default: 1 ) –

    The world size of prefill context parallelism.

  • alignment_tokens

    (int) –

    The returned cache hit length (in tokens) should be a multiple of this value (in tokens).

Returns:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
@classmethod
def find_longest_cache_hit(
    cls,
    block_hashes: BlockHashList,
    max_length: int,
    kv_cache_group_ids: list[int],
    block_pool: BlockPool,
    kv_cache_spec: KVCacheSpec,
    drop_eagle_block: bool,
    alignment_tokens: int,
    dcp_world_size: int = 1,
    pcp_world_size: int = 1,
) -> tuple[tuple[list[KVCacheBlock], ...], int]:
    """
    For chunked local attention, we need to find the longest cache hit
    prefix of the blocks that is not longer than `max_length`. The prefix
    should be a common prefix hit for all the kv cache groups in
    `kv_cache_group_ids`. If no cache hit is found, return an empty list.
    note we mark as computed if the whole block is outside of the local
    window, and set the block as null. Examples:

    1. Attention chunk size of 8, block size of 4, max length of 15
    for next token at 15th (zero-indexed), 8th - 14th tokens are in
    the window(needs lookup), 0th - 7th are not in the window,
    so they are already marked as computed. We check the complete
    block3 (8th - 11th tokens), Assume block 3 is hit, we will return
    [null, null, block 3], otherwise, we return [null, null]

    2. Attention chunk size of 8, block size of 4, max length of 16
    for next token at 16th (zero-indexed), 0th - 15th tokens are not
    in the window, so they are already marked as computed.
    we return 4 blocks[null, null, null, null]

    Args:
        block_hashes: The block hashes of the request.
        max_length: The maximum length of the cache hit prefix.
        kv_cache_group_ids: The ids of the kv cache groups.
        block_pool: The block pool.
        kv_cache_spec: The kv cache spec.
        drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
        dcp_world_size: The world size of decode context parallelism.
        pcp_world_size: The world size of prefill context parallelism.
        alignment_tokens: The returned cache hit length (in tokens) should
            be a multiple of this value (in tokens).

    Returns:
        A list of cached blocks
    """
    assert isinstance(kv_cache_spec, ChunkedLocalAttentionSpec), (
        "ChunkedLocalAttentionManager can only be used for "
        "chunked local attention groups"
    )
    assert drop_eagle_block is False, (
        "Hybrid KV cache is not supported for " + "eagle + chunked local attention."
    )
    assert dcp_world_size == 1, "DCP not support chunked local attn now."
    assert pcp_world_size == 1, "PCP not support chunked local attn now."
    assert kv_cache_spec.block_size == alignment_tokens, (
        "KV cache groups with different block sizes are not compatible with "
        "chunked local attention now"
    )
    block_hashes = resolve_block_hashes(
        block_hashes,
        block_pool.hash_block_size,
        kv_cache_spec.block_size,
        supports_fine_grained_hash_lookup=cls.supports_fine_grained_hash_lookup,
        alignment_tokens=alignment_tokens,
    )
    max_num_blocks = max_length // kv_cache_spec.block_size
    if max_length > 0:
        local_attention_start_idx = (
            max_length
            // kv_cache_spec.attention_chunk_size
            * kv_cache_spec.attention_chunk_size
        )
    else:
        local_attention_start_idx = 0
    # we marked blocks out of window as computed
    # with null blocks, and blocks inside window based on cache lookup
    # result [null] [null] ... [null] [hit block 1 (1st block contain
    # last window)] [hit block 2] ... [hit block x]
    local_attention_start_block_idx = (
        local_attention_start_idx // kv_cache_spec.block_size
    )
    computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
        [block_pool.null_block] * local_attention_start_block_idx
        for _ in range(len(kv_cache_group_ids))
    )
    for i in range(local_attention_start_block_idx, max_num_blocks):
        block_hash = block_hashes[i]
        if cached_block := block_pool.get_cached_block(
            block_hash, kv_cache_group_ids
        ):
            for computed, cached in zip(computed_blocks, cached_block):
                computed.append(cached)
        else:
            break
    hit_length = len(computed_blocks[0]) * kv_cache_spec.block_size
    return computed_blocks, hit_length

get_num_common_prefix_blocks(running_request_id)

cascade attention is not supported by chunked local attention.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
    """
    cascade attention is not supported by chunked local attention.
    """
    return 0

get_num_skipped_tokens(num_computed_tokens)

Get the number of tokens that will be skipped for attention computation.

For chunked local attention, this corresponds to the tokens that are on the left side of the current chunk.

Example 1: chunk size = 8, num_computed_tokens = 13 Tokens: [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ... | ----- computed ---------------| ^^ next token to be computed |----------------| <-- attention window for next token |--- skipped -----| Output: get_num_skipped_tokens(13) == 8

Example 2: chunk size = 8, num_computed_tokens = 8 Tokens: [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ... | --- computed ---| ^ next token to be computed |--| <-- attention window for next token | --- skipped ----| Output: get_num_skipped_tokens(8) == 8

Example 3: chunk size = 8, num_computed_tokens = 7 Tokens: [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ... |---computed---| ^ next token to be computed |-----------------| <-- attention window for next token no token should be skipped. Output: get_num_skipped_tokens(7) == 0

Parameters:

  • num_computed_tokens

    (int) –

    The number of tokens that have been computed.

Returns:

  • int

    The number of tokens that will be skipped for attention computation.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
    """
    Get the number of tokens that will be skipped for attention computation.

    For chunked local attention, this corresponds to the tokens that are on
    the left side of the current chunk.

    Example 1:
    chunk size = 8, num_computed_tokens = 13
    Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
             | ----- computed ---------------|
                                              ^^ next token to be computed
                               |----------------| <-- attention window for
                                                      next token
             |--- skipped -----|
    Output: get_num_skipped_tokens(13) == 8

    Example 2:
    chunk size = 8, num_computed_tokens = 8
    Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
             | --- computed ---|
                                 ^ next token to be computed
                               |--| <-- attention window for next token
             | --- skipped ----|
    Output: get_num_skipped_tokens(8) == 8

    Example 3:
    chunk size = 8, num_computed_tokens = 7
    Tokens:  [ 0 1 2 3 4 5 6 7 | 8 9 10 11 12 13 14 15 ] ...
             |---computed---|
                             ^ next token to be computed
             |-----------------| <-- attention window for next token
             no token should be skipped.
    Output: get_num_skipped_tokens(7) == 0

    Args:
        num_computed_tokens: The number of tokens that have been computed.

    Returns:
        The number of tokens that will be skipped for attention computation.
    """
    num_skipped_tokens = (
        num_computed_tokens // self.attention_chunk_size
    ) * self.attention_chunk_size
    return num_skipped_tokens

CrossAttentionManager

Bases: SingleTypeKVCacheManager

Manager for cross-attention KV cache in encoder-decoder models.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
class CrossAttentionManager(SingleTypeKVCacheManager):
    """Manager for cross-attention KV cache in encoder-decoder models."""

    def add_local_computed_blocks(
        self,
        request_id: str,
        new_computed_blocks: Sequence[KVCacheBlock],
        num_local_computed_tokens: int,
        num_external_computed_tokens: int,
    ) -> None:
        # We do not cache blocks for cross-attention to be shared between
        # requests, so  `new_computed_blocks` should always be empty.
        assert len(new_computed_blocks) == 0

    def allocate_external_computed_blocks(
        self,
        request_id: str,
        num_local_computed_tokens: int,
        num_external_computed_tokens: int,
    ) -> None:
        # Cross-attention does not use prefix caching / external KV loads.
        return

    def cache_blocks(
        self,
        request: Request,
        num_tokens: int,
        retention_interval: int | None = None,
    ) -> None:
        # We do not cache blocks for cross-attention to be shared between
        # requests, so this method is not relevant.
        raise ValueError("Should not be called as prefix caching is disabled.")

    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        # Cross-attention blocks contain request-specific encoder states
        # and are not shared between different requests
        return 0

    @classmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        assert isinstance(kv_cache_spec, CrossAttentionSpec), (
            "CrossAttentionManager can only be used for cross-attention groups"
        )
        # Cross-attention does not benefit from prefix caching since:
        # 1. Encoder states are unique per request (different audio/image
        #    inputs)
        # 2. Encoder states are computed once per request, not incrementally
        # 3. No reusable prefix exists between different multimodal inputs
        # Return empty blocks to indicate no cache hits
        raise NotImplementedError("CrossAttentionManager does not support caching")

FullAttentionManager

Bases: SingleTypeKVCacheManager

Source code in vllm/v1/core/single_type_kv_cache_manager.py
class FullAttentionManager(SingleTypeKVCacheManager):
    supports_fine_grained_hash_lookup: ClassVar[bool] = True

    @classmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        assert isinstance(
            kv_cache_spec, FullAttentionSpec | ChunkedLocalAttentionSpec
        ), (
            "FullAttentionManager can only be used for full attention "
            "and chunked local attention groups"
        )
        block_size = kv_cache_spec.block_size
        if dcp_world_size > 1:
            # DCP shards each block's KV across ranks; hashes must be viewed at
            # the sharded block size.
            block_size *= dcp_world_size
        block_hashes = resolve_block_hashes(
            block_hashes,
            block_pool.hash_block_size,
            block_size,
            supports_fine_grained_hash_lookup=cls.supports_fine_grained_hash_lookup,
            alignment_tokens=alignment_tokens,
        )

        # Fine-grained mode (alignment_tokens == hash_block_size <
        # block_size): resolve_block_hashes kept the raw hash-granularity
        # list so interior boundaries can be probed.
        fine_grained = (
            alignment_tokens < block_size and block_size % alignment_tokens == 0
        )
        if fine_grained:
            # list or lazy BlobBlockHashes view
            assert isinstance(block_hashes, Sequence)
            full_block_hashes: BlockHashList = BlockHashListWithBlockSize(
                block_hashes, alignment_tokens, block_size
            )
        else:
            full_block_hashes = block_hashes

        computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
            [] for _ in range(len(kv_cache_group_ids))
        )
        # Phase 1: longest run of cached full blocks from the start. A missing
        # block implies every later block misses too (chained hashes).
        for block_hash in itertools.islice(full_block_hashes, max_length // block_size):
            cached_block = block_pool.get_cached_block(block_hash, kv_cache_group_ids)
            if not cached_block:
                break
            for computed, cached in zip(computed_blocks, cached_block):
                computed.append(cached)
        hit_length = len(computed_blocks[0]) * block_size

        # Phase 2 (fine-grained only): extend into the first non-full block by
        # probing its interior hash boundaries high-to-low (longest hit first).
        if fine_grained:
            # list or lazy BlobBlockHashes view
            assert isinstance(block_hashes, Sequence)
            scale_factor = block_size // alignment_tokens
            first_partial_idx = len(computed_blocks[0]) * scale_factor
            max_partial_idx = min(
                first_partial_idx + scale_factor - 1,
                max_length // alignment_tokens,
                len(block_hashes),
            )
            for fine_idx in range(max_partial_idx - 1, first_partial_idx - 1, -1):
                cached_tail = block_pool.get_cached_block(
                    block_hashes[fine_idx], kv_cache_group_ids
                )
                if not cached_tail:
                    continue
                for computed, cached in zip(computed_blocks, cached_tail):
                    computed.append(cached)
                hit_length = (fine_idx + 1) * alignment_tokens
                break

        # Eagle needs the tokens right before the generation point recomputed:
        # drop one hash unit when fine-grained (the tail block's KV is
        # append-only, so it still covers the reduced length), else one cache
        # block.
        if drop_eagle_block and hit_length > 0:
            hit_length -= min(alignment_tokens, block_size)
        # Round down to the alignment; a no-op when fine-grained (hits land on
        # hash boundaries by construction) and when alignment_tokens ==
        # block_size. Then trim blocks past the new tail.
        hit_length -= hit_length % alignment_tokens
        num_blocks = cdiv(hit_length, block_size)
        for computed in computed_blocks:
            del computed[num_blocks:]
        return computed_blocks, hit_length

    def cache_blocks(
        self,
        request: Request,
        num_tokens: int,
        retention_interval: int | None = None,
    ) -> None:
        super().cache_blocks(request, num_tokens, retention_interval=retention_interval)
        hash_block_size = self.block_pool.hash_block_size
        if self.block_size == hash_block_size:
            return
        self._cache_partial_tail_block(request, num_tokens)

    def _cache_partial_tail_block(
        self,
        request: Request,
        num_tokens: int,
    ) -> None:
        """Cache the prompt tail when it ends inside a cache block.

        Only the final prompt hash boundary is registered as a partial
        prefix-cache entry; intermediate hash boundaries inside the same cache
        block are intentionally skipped.
        """
        hash_block_size = self.block_pool.hash_block_size
        boundary_tokens = request.num_prompt_tokens // hash_block_size * hash_block_size
        if boundary_tokens == 0 or boundary_tokens > num_tokens:
            return
        if boundary_tokens % self.block_size == 0:
            return

        blocks = self.req_to_blocks[request.request_id]
        block_idx = boundary_tokens // self.block_size
        if block_idx >= len(blocks):
            return
        self.block_pool.cache_partial_block(
            request=request,
            block=blocks[block_idx],
            num_tokens=boundary_tokens,
            kv_cache_group_id=self.kv_cache_group_id,
            block_size=self.block_size,
        )

    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        blocks = self.req_to_blocks[running_request_id]
        num_common_blocks = 0
        for block in blocks:
            if block.ref_cnt == len(self.req_to_blocks):
                num_common_blocks += 1
            else:
                break
        return num_common_blocks

_cache_partial_tail_block(request, num_tokens)

Cache the prompt tail when it ends inside a cache block.

Only the final prompt hash boundary is registered as a partial prefix-cache entry; intermediate hash boundaries inside the same cache block are intentionally skipped.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def _cache_partial_tail_block(
    self,
    request: Request,
    num_tokens: int,
) -> None:
    """Cache the prompt tail when it ends inside a cache block.

    Only the final prompt hash boundary is registered as a partial
    prefix-cache entry; intermediate hash boundaries inside the same cache
    block are intentionally skipped.
    """
    hash_block_size = self.block_pool.hash_block_size
    boundary_tokens = request.num_prompt_tokens // hash_block_size * hash_block_size
    if boundary_tokens == 0 or boundary_tokens > num_tokens:
        return
    if boundary_tokens % self.block_size == 0:
        return

    blocks = self.req_to_blocks[request.request_id]
    block_idx = boundary_tokens // self.block_size
    if block_idx >= len(blocks):
        return
    self.block_pool.cache_partial_block(
        request=request,
        block=blocks[block_idx],
        num_tokens=boundary_tokens,
        kv_cache_group_id=self.kv_cache_group_id,
        block_size=self.block_size,
    )

MambaManager

Bases: SingleTypeKVCacheManager

Methods:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
class MambaManager(SingleTypeKVCacheManager):
    supports_fine_grained_hash_lookup: ClassVar[bool] = True

    def __init__(
        self, kv_cache_spec: MambaSpec, block_pool: BlockPool, **kwargs
    ) -> None:
        super().__init__(kv_cache_spec, block_pool, **kwargs)
        # Mamba layers use TP instead of DCP, so each rank holds the full
        # recurrent state. Undo the DCP/PCP block_size scaling that the base
        # class applies for attention groups whose KV cache is partitioned.
        self.block_size = kv_cache_spec.block_size
        self.mamba_cache_mode = kv_cache_spec.mamba_cache_mode
        self.num_speculative_blocks: int = kv_cache_spec.num_speculative_blocks
        self.cached_blocks_this_step: set[BlockHashWithGroupId] = set()
        if self.mamba_cache_mode == "align":
            # Mapping from request ID to the index of the block
            # allocated in the previous step
            self.last_state_block_idx: dict[str, int] = {}
            # The set of the requests that have been allocated blocks
            self._allocated_block_reqs: set[str] = set()
            # Requests that registered their own last-prompt-boundary partial
            # tail (producers). On the next step's CoW the boundary state moves
            # into a private cow_block; we record that block for connector
            # offload (see _pending_partial_tail_offloads).
            self._producer_partial_tail_reqs: dict[str, int] = {}

    @classmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        assert isinstance(kv_cache_spec, MambaSpec), (
            "MambaManager can only be used for mamba groups"
        )
        assert dcp_world_size == 1, "DCP not support mamba now."
        assert pcp_world_size == 1, "PCP not support mamba now."
        block_hashes = resolve_block_hashes(
            block_hashes,
            block_pool.hash_block_size,
            kv_cache_spec.block_size,
            supports_fine_grained_hash_lookup=cls.supports_fine_grained_hash_lookup,
            alignment_tokens=alignment_tokens,
        )
        computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
            [] for _ in range(len(kv_cache_group_ids))
        )
        hit_length = 0

        block_size = kv_cache_spec.block_size
        if alignment_tokens < block_size and block_size % alignment_tokens == 0:
            # list or lazy BlobBlockHashes view
            assert isinstance(block_hashes, Sequence)
            hash_block_size = alignment_tokens
            scale_factor = block_size // hash_block_size
            max_num_partial_units = min(
                max_length // hash_block_size, len(block_hashes)
            )
            for fine_idx in range(max_num_partial_units - 1, -1, -1):
                num_tokens = (fine_idx + 1) * hash_block_size
                block_hash = block_hashes[fine_idx]
                if cached_block := block_pool.get_cached_block(
                    block_hash, kv_cache_group_ids
                ):
                    block_idx = fine_idx // scale_factor
                    for computed, cached in zip(computed_blocks, cached_block):
                        computed.extend([block_pool.null_block] * block_idx)
                        computed.append(cached)
                    hit_length = num_tokens
                    break
            return computed_blocks, hit_length

        max_num_blocks = max_length // block_size
        # Search from right to left and early stop when a match is found.
        for i in range(max_num_blocks - 1, -1, -1):
            if cached_block := block_pool.get_cached_block(
                block_hashes[i], kv_cache_group_ids
            ):
                # When enable Mamba prefix caching, `block_size` will be aligned
                # across full attention layers and Mamba layers to ensure the
                # prefix hit length aligned at block
                if (
                    block_size != alignment_tokens  # Faster for common case.
                    and (i + 1) * block_size % alignment_tokens != 0
                ):
                    continue
                for computed, cached in zip(computed_blocks, cached_block):
                    # the hit length logic later assumes:
                    #  hit_length = len(hit_blocks_other_attn[0])
                    #               * self.other_block_size
                    # so we insert dummy blocks at the beginning:
                    computed.extend([block_pool.null_block] * i)
                    computed.append(cached)
                hit_length = (i + 1) * block_size
                break  # we just need the last match - early stopping

        return computed_blocks, hit_length

    @classmethod
    def reachable_block_mask(
        cls,
        start_block: int,
        end_block: int,
        alignment_tokens: int | None,
        kv_cache_spec: KVCacheSpec,
        use_eagle: bool,
        retention_interval: int | None = None,
        reachable_boundaries: Sequence[int] = (),
    ) -> list[bool] | None:
        """Sparse Mamba state-snapshot retention.

        ``retention_interval``:

          ``None`` -> dense (cache every block; default, unchanged behavior)
          ``0``    -> keep only the ``reachable_boundaries`` states
          ``> 0``  -> keep one state per ``retention_interval``-sized segment

        ``reachable_boundaries`` are proven reuse points (the replay boundary and
        any cross-request shared-prefix junction, Marconi-style APC); their
        boundary state is always kept so sparse retention does not defeat reuse.
        """
        if retention_interval is None or alignment_tokens is None:
            # Dense caching (default) or no alignment constraint imposed.
            return None
        assert isinstance(kv_cache_spec, MambaSpec)
        block_size = kv_cache_spec.block_size
        mask = [False] * (end_block - start_block)

        # (1) Segment-boundary states. A Mamba hit needs exactly the single
        # state block ending on the boundary (no window, and draft models have
        # no mamba layers, so no eagle shift). Block ``i`` ends at token
        # ``(i + 1) * block_size``.
        segment_tokens = None if retention_interval == 0 else retention_interval
        if segment_tokens is not None:
            per_segment = segment_tokens // block_size
            if per_segment <= 1:
                # Interval at/below the block size: every block is a boundary.
                return None
            first_boundary = (
                start_block + per_segment
            ) // per_segment * per_segment - 1
            for i in range(first_boundary - start_block, len(mask), per_segment):
                mask[i] = True

        # (2) Reachable-boundary states: the replay boundary (``num_prompt - 1``,
        # capped by ``get_computed_blocks``) and any shared-prefix junction, both
        # of which segments would otherwise skip under sparse retention. A Mamba
        # hit needs exactly the single state block ending on the boundary.
        for boundary_tokens in reachable_boundaries:
            aligned = boundary_tokens // alignment_tokens * alignment_tokens
            boundary_block = aligned // block_size - 1
            if start_block <= boundary_block < end_block:
                mask[boundary_block - start_block] = True

        return mask

    def remove_skipped_blocks(
        self,
        request_id: str,
        processed_computed_tokens: int,
        num_prompt_tokens: int | None = None,
    ) -> None:
        assert isinstance(self.kv_cache_spec, MambaSpec)

        super().remove_skipped_blocks(
            request_id, processed_computed_tokens, num_prompt_tokens
        )
        if self.mamba_cache_mode == "align":
            # `last_state_block_idx` refers to the block index allocated two steps ago.
            # The block allocated in the previous step is used to copy Mamba states
            # into the block allocated in the current step; the earlier block is
            # no longer needed and should be freed here.
            last_state_block_idx = self.last_state_block_idx.get(request_id)
            # Blocks allocated during prefill may be non-contiguous. Use
            # `last_state_block_idx` to free the appropriate block and replace it
            # with a null block.
            if (
                last_state_block_idx is not None
                and last_state_block_idx
                < cdiv(processed_computed_tokens, self.block_size) - 1
            ):
                blocks = self.req_to_blocks[request_id]
                if blocks[last_state_block_idx] != self._null_block:
                    self.block_pool.free_blocks([blocks[last_state_block_idx]])
                    blocks[last_state_block_idx] = self._null_block

    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        """
        cascade attention is not supported by mamba
        """
        return 0

    def get_num_blocks_to_allocate(
        self,
        request_id: str,
        num_tokens: int,
        new_computed_blocks: Sequence[KVCacheBlock],
        total_computed_tokens: int,
        num_local_computed_tokens: int,
        num_tokens_main_model: int,
        apply_admission_cap: bool = False,
    ) -> int:
        assert isinstance(self.kv_cache_spec, MambaSpec)
        if (
            len(new_computed_blocks) > 0
            and new_computed_blocks[-1].block_hash in self.cached_blocks_this_step
        ):
            # Mamba can't rely on blocks generated by other requests in the current step
            # To put it in the next step, we return num_gpu_blocks + 1 so
            # that kv_cache_manager will think there is no enough blocks to allocate now
            # and don't schedule it in the current step.
            return self.block_pool.num_gpu_blocks + 1
        if self.mamba_cache_mode != "align":
            # Allocate extra `num_speculative_blocks` blocks for
            # speculative decoding (MTP/EAGLE) with linear attention.
            if self.num_speculative_blocks > 0:
                num_tokens += (
                    self.kv_cache_spec.block_size * self.num_speculative_blocks
                )
            return super().get_num_blocks_to_allocate(
                request_id,
                num_tokens,
                new_computed_blocks,
                total_computed_tokens,
                num_local_computed_tokens,
                num_tokens_main_model,
                apply_admission_cap=apply_admission_cap,
            )
        else:
            # We don't allocate blocks for lookahead tokens in align mode, because if
            # x * block_size tokens are scheduled, num_tokens is
            # x * block_size + num_lookahead_tokens and breaks the alignment.
            # We can ignore lookahead tokens because current draft models don't have
            # mamba layers.
            num_tokens = num_tokens_main_model

            # NOTE(tdouble): this is an over-estimate of how many blocks we need because
            # num_tokens can include draft tokens that will later be rejected.
            num_required_blocks = (
                cdiv(num_tokens, self.block_size) + self.num_speculative_blocks
            )
            num_new_blocks = (
                num_required_blocks
                - len(new_computed_blocks)
                - len(self.req_to_blocks[request_id])
            )
            has_partial_hit = (
                self._has_partial_local_hit(
                    new_computed_blocks, num_local_computed_tokens
                )
                or request_id in self._partial_hit_reqs
            )
            if has_partial_hit:
                num_new_blocks = max(num_new_blocks, 0) + 1
            if num_new_blocks > 0:
                if request_id in self._allocated_block_reqs:
                    # Old request. Needs at most 1 more blocks as we can reuse the
                    # speculative blocks in previous step.
                    num_new_blocks = 1 + int(has_partial_hit)
                else:
                    # First prefill. Allocate 1 block for running state, the
                    # speculative blocks, and one extra block if a partial cache
                    # hit must be copy-on-written before the new tokens run.
                    num_new_blocks = (
                        1 + self.num_speculative_blocks + int(has_partial_hit)
                    )

            num_evictable_computed_blocks = self._get_num_evictable_blocks(
                new_computed_blocks
            )
            return num_new_blocks + num_evictable_computed_blocks

    def allocate_new_blocks(
        self, request_id: str, num_tokens: int, num_tokens_main_model: int
    ) -> list[KVCacheBlock]:
        assert isinstance(self.kv_cache_spec, MambaSpec)
        if self.mamba_cache_mode != "align":
            # Allocate extra `num_speculative_blocks` blocks for
            # speculative decoding (MTP/EAGLE) with linear attention.
            if self.num_speculative_blocks > 0:
                num_tokens += self.block_size * self.num_speculative_blocks
            return super().allocate_new_blocks(
                request_id, num_tokens, num_tokens_main_model
            )
        else:
            # We don't allocate blocks for lookahead tokens in align mode, because if
            # x * block_size tokens are scheduled, num_tokens is
            # x * block_size + num_lookahead_tokens and breaks the alignment.
            # We can ignore lookahead tokens because current draft models don't have
            # mamba layers.
            num_tokens = num_tokens_main_model
            req_blocks: list[KVCacheBlock] = self.req_to_blocks[request_id]
            # NOTE(tdouble): this is an over-estimate of how many blocks we need because
            # num_tokens can include draft tokens that will later be rejected.
            num_required_blocks = (
                cdiv(num_tokens, self.block_size) + self.num_speculative_blocks
            )
            partial_hit = self._partial_hit_reqs.get(request_id)
            has_partial_hit = partial_hit is not None
            # `num_required_blocks` might be less than `len(req_blocks)` if blocks are
            # over-allocated at last round.
            if num_required_blocks <= len(req_blocks) and not has_partial_hit:
                return []
            else:
                prev_block_len = len(req_blocks)
                blocks_allocated = request_id in self._allocated_block_reqs
                # Record the last state block
                if blocks_allocated:
                    # We always save the running state at the last
                    # (1 + num_speculative_blocks) block
                    self.last_state_block_idx[request_id] = (
                        prev_block_len - 1 - self.num_speculative_blocks
                    )
                elif prev_block_len > 0:
                    # When a new request hits the prefix cache, the last block
                    # saves the hit state.
                    self.last_state_block_idx[request_id] = prev_block_len - 1

                num_skipped_blocks = (
                    num_required_blocks - self.num_speculative_blocks - 1
                )
                # null blocks
                if prev_block_len < num_skipped_blocks:
                    req_blocks.extend(
                        [
                            self._null_block
                            for _ in range(prev_block_len, num_skipped_blocks)
                        ]
                    )

                if blocks_allocated:
                    # reuse previous speculative blocks in this step
                    for block_idx in range(
                        prev_block_len - self.num_speculative_blocks, prev_block_len
                    ):
                        if block_idx < num_skipped_blocks:
                            req_blocks.append(req_blocks[block_idx])
                            req_blocks[block_idx] = self._null_block
                        else:
                            break
                num_new_blocks = num_required_blocks - len(req_blocks)
                if has_partial_hit:
                    num_new_blocks = max(num_new_blocks, 0) + 1
                if blocks_allocated:
                    assert num_new_blocks <= 1 + int(has_partial_hit)
                else:
                    assert num_new_blocks <= self.num_speculative_blocks + 1 + int(
                        has_partial_hit
                    )
                new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
                returned_blocks = req_blocks[prev_block_len:]
                if partial_hit is not None:
                    block_idx, source_block = partial_hit
                    cow_block = new_blocks[0]
                    new_blocks = new_blocks[1:]
                    if blocks_allocated:
                        # The worker block table of a running request is
                        # append-only, so the request must stay on
                        # source_block. Move the cache entry to cow_block
                        # instead; the queued copy fills it before forward
                        # overwrites source_block.
                        assert req_blocks[block_idx] is source_block
                        self.block_pool.move_block_hashes(source_block, cow_block)
                        self._pending_cow_copies.append((source_block, cow_block))
                        source_block.ref_cnt += 1
                        boundary_tokens = self._producer_partial_tail_reqs.pop(
                            request_id, None
                        )
                        if boundary_tokens is not None:
                            # This CoW preserved a producer's own boundary
                            # state in cow_block; hand it to the connector for
                            # partial-tail offload once the copy has run.
                            self._pending_partial_tail_offloads.append(
                                (
                                    request_id,
                                    self.kv_cache_group_id,
                                    cow_block,
                                    boundary_tokens,
                                )
                            )
                        if cow_block.block_hash is not None:
                            # The moved entry is only filled by this step's
                            # copy, so defer same-step hits on it.
                            self.cached_blocks_this_step.add(cow_block.block_hash)
                    else:
                        self._apply_cow(request_id, block_idx, source_block, cow_block)
                        returned_blocks = [cow_block] + returned_blocks
                req_blocks.extend(new_blocks)
                self._allocated_block_reqs.add(request_id)
                self._partial_hit_reqs.pop(request_id, None)
                returned_blocks.extend(new_blocks)
                return returned_blocks

    def pop_blocks_for_free(self, request_id: str) -> list[KVCacheBlock]:
        if self.mamba_cache_mode == "align":
            self._allocated_block_reqs.discard(request_id)
            self.last_state_block_idx.pop(request_id, None)
            self._producer_partial_tail_reqs.pop(request_id, None)
            # A hand-off whose request died in this same scheduling pass must
            # not reach the connector: its unpin hook (free) has already run.
            self._pending_partial_tail_offloads = [
                entry
                for entry in self._pending_partial_tail_offloads
                if entry[0] != request_id
            ]
        return super().pop_blocks_for_free(request_id)

    def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
        """
        Get the number of tokens whose mamba state are not needed anymore. Mamba only
        need to keep the state of the last computed token, so we return
        num_computed_tokens - 1.
        """
        return num_computed_tokens - 1

    def cache_blocks(
        self,
        request: Request,
        num_tokens: int,
        retention_interval: int | None = None,
    ) -> None:
        num_cached_blocks_before = self.num_cached_block.get(request.request_id, 0)
        super().cache_blocks(request, num_tokens, retention_interval=retention_interval)
        num_cached_blocks_after = self.num_cached_block.get(request.request_id, 0)
        if self.mamba_cache_mode == "align":
            partial_hash = self._cache_partial_tail_block(request, num_tokens)
            if partial_hash is not None:
                self.cached_blocks_this_step.add(partial_hash)
        if num_cached_blocks_after > num_cached_blocks_before:
            for block in self.req_to_blocks[request.request_id][
                num_cached_blocks_before:num_cached_blocks_after
            ]:
                # Skip null blocks (align-mode skipped states) and blocks that
                # were not cached this step — with sparse retention
                # (reachable_block_mask) the intermediate state snapshots carry
                # no hash and must not be recorded as cached-this-step.
                if block.is_null or block.block_hash is None:
                    continue
                self.cached_blocks_this_step.add(block.block_hash)

    def new_step_starts(self) -> None:
        self.cached_blocks_this_step.clear()

    def _cache_partial_tail_block(
        self,
        request: Request,
        num_tokens: int,
    ) -> BlockHashWithGroupId | None:
        hash_block_size = self.block_pool.hash_block_size
        if self.block_size == hash_block_size:
            return None
        if num_tokens % self.block_size == 0:
            return None
        if num_tokens % hash_block_size != 0:
            return None
        latest_prompt_hash_boundary = (
            request.num_prompt_tokens // hash_block_size
        ) * hash_block_size
        if num_tokens != latest_prompt_hash_boundary:
            return None

        block_idx = num_tokens // self.block_size
        blocks = self.req_to_blocks[request.request_id]
        if block_idx >= len(blocks):
            return None
        source_block = blocks[block_idx]
        if source_block.is_null:
            return None

        partial_hash = self.block_pool.cache_partial_block(
            request=request,
            block=source_block,
            num_tokens=num_tokens,
            kv_cache_group_id=self.kv_cache_group_id,
            block_size=self.block_size,
        )
        if partial_hash is not None:
            self._partial_hit_reqs[request.request_id] = (block_idx, source_block)
            self.num_cached_block[request.request_id] = block_idx
            # Producer of this partial tail: the boundary state currently lives
            # in ``source_block`` but the next step's forward overwrites it. The
            # upcoming CoW copies it into a durable cow_block; record the req so
            # allocate_new_blocks hands that block to the connector for offload.
            self._producer_partial_tail_reqs[request.request_id] = num_tokens
        return partial_hash

get_num_common_prefix_blocks(running_request_id)

cascade attention is not supported by mamba

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
    """
    cascade attention is not supported by mamba
    """
    return 0

get_num_skipped_tokens(num_computed_tokens)

Get the number of tokens whose mamba state are not needed anymore. Mamba only need to keep the state of the last computed token, so we return num_computed_tokens - 1.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
    """
    Get the number of tokens whose mamba state are not needed anymore. Mamba only
    need to keep the state of the last computed token, so we return
    num_computed_tokens - 1.
    """
    return num_computed_tokens - 1

reachable_block_mask(start_block, end_block, alignment_tokens, kv_cache_spec, use_eagle, retention_interval=None, reachable_boundaries=()) classmethod

Sparse Mamba state-snapshot retention.

retention_interval:

None -> dense (cache every block; default, unchanged behavior) 0 -> keep only the reachable_boundaries states > 0 -> keep one state per retention_interval-sized segment

reachable_boundaries are proven reuse points (the replay boundary and any cross-request shared-prefix junction, Marconi-style APC); their boundary state is always kept so sparse retention does not defeat reuse.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
@classmethod
def reachable_block_mask(
    cls,
    start_block: int,
    end_block: int,
    alignment_tokens: int | None,
    kv_cache_spec: KVCacheSpec,
    use_eagle: bool,
    retention_interval: int | None = None,
    reachable_boundaries: Sequence[int] = (),
) -> list[bool] | None:
    """Sparse Mamba state-snapshot retention.

    ``retention_interval``:

      ``None`` -> dense (cache every block; default, unchanged behavior)
      ``0``    -> keep only the ``reachable_boundaries`` states
      ``> 0``  -> keep one state per ``retention_interval``-sized segment

    ``reachable_boundaries`` are proven reuse points (the replay boundary and
    any cross-request shared-prefix junction, Marconi-style APC); their
    boundary state is always kept so sparse retention does not defeat reuse.
    """
    if retention_interval is None or alignment_tokens is None:
        # Dense caching (default) or no alignment constraint imposed.
        return None
    assert isinstance(kv_cache_spec, MambaSpec)
    block_size = kv_cache_spec.block_size
    mask = [False] * (end_block - start_block)

    # (1) Segment-boundary states. A Mamba hit needs exactly the single
    # state block ending on the boundary (no window, and draft models have
    # no mamba layers, so no eagle shift). Block ``i`` ends at token
    # ``(i + 1) * block_size``.
    segment_tokens = None if retention_interval == 0 else retention_interval
    if segment_tokens is not None:
        per_segment = segment_tokens // block_size
        if per_segment <= 1:
            # Interval at/below the block size: every block is a boundary.
            return None
        first_boundary = (
            start_block + per_segment
        ) // per_segment * per_segment - 1
        for i in range(first_boundary - start_block, len(mask), per_segment):
            mask[i] = True

    # (2) Reachable-boundary states: the replay boundary (``num_prompt - 1``,
    # capped by ``get_computed_blocks``) and any shared-prefix junction, both
    # of which segments would otherwise skip under sparse retention. A Mamba
    # hit needs exactly the single state block ending on the boundary.
    for boundary_tokens in reachable_boundaries:
        aligned = boundary_tokens // alignment_tokens * alignment_tokens
        boundary_block = aligned // block_size - 1
        if start_block <= boundary_block < end_block:
            mask[boundary_block - start_block] = True

    return mask

RSWAManager

Bases: FullAttentionManager

KV cache manager for Reference Sliding Window Attention (R-SWA).

When num_prompt_tokens is supplied to remove_skipped_blocks, frees gap blocks between the prefill tail and the current decode window. This bounds per-request KV memory at O(prefix_len + rswa_window) instead of growing linearly with decode length.

Methods:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
class RSWAManager(FullAttentionManager):
    """KV cache manager for Reference Sliding Window Attention (R-SWA).

    When ``num_prompt_tokens`` is supplied to ``remove_skipped_blocks``, frees
    gap blocks between the prefill tail and the current decode window.  This
    bounds per-request KV memory at O(prefix_len + rswa_window) instead of
    growing linearly with decode length.
    """

    def __init__(self, kv_cache_spec: RSWASpec, **kwargs) -> None:
        super().__init__(kv_cache_spec, **kwargs)
        self.rswa_window: int = kv_cache_spec.rswa_window

    def remove_skipped_blocks(
        self,
        request_id: str,
        processed_computed_tokens: int,
        num_prompt_tokens: int | None = None,
    ) -> None:
        """Free gap blocks that are no longer needed for attention.

        Gap = blocks entirely within
            [ceil(prefix_len / block_size) * block_size,
             max(prefix_len, processed_computed_tokens - rswa_window))

        Freed blocks are replaced with null_block in req_to_blocks so the
        block_table passed to FA4 is valid (null_block KV is all-zero;
        rswa_mask_mod marks gap positions as non-visible so FA4 skips them).
        """
        if num_prompt_tokens is None:
            super().remove_skipped_blocks(
                request_id, processed_computed_tokens, num_prompt_tokens
            )
            return

        bs = self.block_size
        # First block fully after the prefill boundary.
        first_gap_block = cdiv(num_prompt_tokens, bs)
        # Decode window start position; blocks before this are evictable.
        window_start = max(
            num_prompt_tokens, processed_computed_tokens - self.rswa_window
        )
        last_gap_block = window_start // bs  # exclusive upper bound
        self._remove_blocks_in_range(request_id, first_gap_block, last_gap_block)

remove_skipped_blocks(request_id, processed_computed_tokens, num_prompt_tokens=None)

Free gap blocks that are no longer needed for attention.

Gap = blocks entirely within [ceil(prefix_len / block_size) * block_size, max(prefix_len, processed_computed_tokens - rswa_window))

Freed blocks are replaced with null_block in req_to_blocks so the block_table passed to FA4 is valid (null_block KV is all-zero; rswa_mask_mod marks gap positions as non-visible so FA4 skips them).

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def remove_skipped_blocks(
    self,
    request_id: str,
    processed_computed_tokens: int,
    num_prompt_tokens: int | None = None,
) -> None:
    """Free gap blocks that are no longer needed for attention.

    Gap = blocks entirely within
        [ceil(prefix_len / block_size) * block_size,
         max(prefix_len, processed_computed_tokens - rswa_window))

    Freed blocks are replaced with null_block in req_to_blocks so the
    block_table passed to FA4 is valid (null_block KV is all-zero;
    rswa_mask_mod marks gap positions as non-visible so FA4 skips them).
    """
    if num_prompt_tokens is None:
        super().remove_skipped_blocks(
            request_id, processed_computed_tokens, num_prompt_tokens
        )
        return

    bs = self.block_size
    # First block fully after the prefill boundary.
    first_gap_block = cdiv(num_prompt_tokens, bs)
    # Decode window start position; blocks before this are evictable.
    window_start = max(
        num_prompt_tokens, processed_computed_tokens - self.rswa_window
    )
    last_gap_block = window_start // bs  # exclusive upper bound
    self._remove_blocks_in_range(request_id, first_gap_block, last_gap_block)

SingleTypeKVCacheManager

Bases: ABC

An abstract base class for a manager that handle the kv cache management logic of one specific type of attention layer.

Methods:

Attributes:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
class SingleTypeKVCacheManager(ABC):
    """
    An abstract base class for a manager that handle the kv cache management
    logic of one specific type of attention layer.
    """

    supports_fine_grained_hash_lookup: ClassVar[bool] = False

    def __init__(
        self,
        kv_cache_spec: KVCacheSpec,
        block_pool: BlockPool,
        enable_caching: bool,
        kv_cache_group_id: int,
        scheduler_block_size: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
        needs_kv_cache_zeroing: bool = False,
        max_admission_blocks_per_request: int | None = None,
    ) -> None:
        """
        Initializes the SingleTypeKVCacheManager.
        Args:
            kv_cache_spec: The kv_cache_spec for this manager.
            block_pool: The block pool.
            kv_cache_group_id: The id of the kv cache group of this manager.
            scheduler_block_size: The scheduling granularity (LCM of all group
                block sizes); a multiple of this manager's ``block_size``.
            needs_kv_cache_zeroing: Whether worker-side KV cache zeroing needs
                newly allocated block IDs from this manager.
            max_admission_blocks_per_request: Recycling-aware per-request
                block cap used by `get_num_blocks_to_allocate`. Only set for
                spec types that recycle blocks across chunks (SWA,
                chunked-local); `None` (the default) means no cap, which is
                correct for full-attention-style specs that hold every
                block until the request finishes.
        """
        self.scheduler_block_size = scheduler_block_size
        # The block size for this manager; used for actual block allocation.
        self.block_size = kv_cache_spec.block_size
        self.dcp_world_size = dcp_world_size
        self.pcp_world_size = pcp_world_size
        if dcp_world_size > 1:
            self.block_size *= dcp_world_size
        self.kv_cache_spec = kv_cache_spec
        self.block_pool = block_pool
        self.enable_caching = enable_caching
        self._max_admission_blocks_per_request = max_admission_blocks_per_request
        # Record newly allocated block ids only when worker-side zeroing will
        # consume them and this manager holds a spec type that gets zeroed.
        self._record_new_block_ids = needs_kv_cache_zeroing and type(kv_cache_spec) in (
            FullAttentionSpec,
            TQFullAttentionSpec,
            MLAAttentionSpec,
            HiddenStateCacheSpec,
        )
        self.new_block_ids: list[int] = []

        # Mapping from request ID to blocks to track the blocks allocated
        # for each request, so that we can free the blocks when the request
        # is finished.
        self.req_to_blocks: defaultdict[str, list[KVCacheBlock]] = defaultdict(list)

        # {req_id: The number of cached blocks for this given request}
        # This is used to track the number of cached blocks for each request.
        # This is only used to track the RUNNING requests, we do not track the
        # data for preempted ones.
        self.num_cached_block: dict[str, int] = {}

        self.kv_cache_group_id = kv_cache_group_id
        self._null_block = block_pool.null_block

        # Whether this group's prefix-cache hits drop the EAGLE/MTP lookahead
        # block. Only consulted by managers whose hit logic is sparse within an
        # aligned segment (SWA). Initialized lazily by the coordinator after
        # determining the attention groups.
        self.use_eagle = False

        # Partial-hit copy-on-write bookkeeping. Populated only by fine-grained
        # managers (full attention, mamba "align"); harmlessly empty elsewhere.
        self._partial_hit_reqs: dict[str, tuple[int, KVCacheBlock]] = {}
        self._pending_cow_copies: list[tuple[KVCacheBlock, KVCacheBlock]] = []
        # Partial-tail offload hand-off for external KV connectors: when a
        # producer registers its last-prompt-boundary partial tail and the
        # durable boundary block is not on the append-only request block table
        # (mamba "align" CoW target), record the request, group, block, and
        # exact token boundary so a connector can offload it under the right
        # hash. Populated only by mamba "align".
        self._pending_partial_tail_offloads: list[
            tuple[str, int, KVCacheBlock, int]
        ] = []

    @classmethod
    def _get_num_evictable_blocks(cls, blocks: Sequence[KVCacheBlock]):
        return sum(blk.ref_cnt == 0 and not blk.is_null for blk in blocks)

    def _has_partial_local_hit(
        self,
        new_computed_blocks: Sequence[KVCacheBlock],
        num_local_computed_tokens: int,
    ) -> bool:
        # The local prefix-cache hit ends inside one of this manager's
        # blocks: the shared tail block needs CoW.
        return (
            len(new_computed_blocks) > 0
            and num_local_computed_tokens % self.block_size != 0
        )

    def get_num_blocks_to_allocate(
        self,
        request_id: str,
        num_tokens: int,
        new_computed_blocks: Sequence[KVCacheBlock],
        total_computed_tokens: int,
        num_local_computed_tokens: int,
        num_tokens_main_model: int,
        apply_admission_cap: bool = False,
    ) -> int:
        """
        Get the number of blocks needed to be allocated for the request.

        Args:
            request_id: The request ID.
            num_tokens: The total number of tokens that need a slot (including
                tokens that are already allocated).
            new_computed_blocks: The new computed blocks just hitting the
                prefix caching.
            total_computed_tokens: Include both local and external computed
                tokens.
            num_local_computed_tokens: The number of local prefix-cache computed
                tokens.
            num_tokens_main_model: The number of tokens for the main model (aka target
                model in spec decode). w/o spec decode, it is num_tokens;
                with spec decode, it is num_tokens - num_lookahead_tokens.
            apply_admission_cap: If True, clamp by `num_required_blocks` by
                `_max_admission_blocks_per_request`for recycling-aware specs
                (SWA, chunked-local).

        Returns:
            The number of blocks to allocate.
        """

        num_required_blocks = cdiv(num_tokens, self.block_size)
        if apply_admission_cap and self._max_admission_blocks_per_request is not None:
            # Recycling-aware specs (SWA, chunked-local) cap the per-request
            # reservation here so admission matches the startup pool sizer
            # (`SlidingWindowSpec.max_admission_blocks_per_request` / its
            # chunked-local counterpart). `remove_skipped_blocks` runs from
            # `allocate_slots` before each chunk's `get_num_blocks_to_allocate`,
            # so per-request peak real-held blocks <= this cap, which keeps
            # `sum(reservations) <= pool` <=> `sum(peak_real_held) <= pool`.
            # Drift between the two would re-introduce the deadlock from
            # issue #39734 or, worse, mid-prefill OOM.
            num_required_blocks = min(
                num_required_blocks, self._max_admission_blocks_per_request
            )
        num_req_blocks = len(self.req_to_blocks.get(request_id, ()))

        if request_id in self.num_cached_block:
            # Fast-path: a running request won't have any new prefix-cache hits.
            assert len(new_computed_blocks) == 0
            # NOTE: With speculative decoding, request's blocks may be allocated
            # for draft tokens which are later rejected. In this case,
            # num_required_blocks may be smaller than num_req_blocks.
            return max(num_required_blocks - num_req_blocks, 0)

        num_skipped_tokens = self.get_num_skipped_tokens(total_computed_tokens)
        num_local_computed_blocks = len(new_computed_blocks) + num_req_blocks
        # Number of whole blocks that are skipped by the attention window.
        # If nothing is skipped, this is 0.
        num_skipped_blocks = num_skipped_tokens // self.block_size
        # We need blocks for the non-skipped suffix. If there are still
        # local-computed blocks inside the window, they contribute to the
        # required capacity; otherwise, skipped blocks dominate.
        num_new_blocks = max(
            num_required_blocks - max(num_skipped_blocks, num_local_computed_blocks),
            0,
        )

        # Among the `new_computed_blocks`, the first `num_skipped_blocks` worth
        # of blocks are skipped; `num_req_blocks` of those may already be in
        # `req_to_blocks`, so only skip the remainder from `new_computed_blocks`.
        num_skipped_new_computed_blocks = max(0, num_skipped_blocks - num_req_blocks)

        # If a computed block is an eviction candidate (in the free queue and
        # ref_cnt == 0), it will be removed from the free queue when touched by
        # the allocated request, so we must count it in the free-capacity check.
        num_evictable_blocks = self._get_num_evictable_blocks(
            new_computed_blocks[num_skipped_new_computed_blocks:]
        )
        if self._has_partial_local_hit(new_computed_blocks, num_local_computed_tokens):
            # Reserve the extra block that allocate_new_blocks pulls for the
            # partial-hit CoW redirect.
            num_new_blocks += 1
        return num_new_blocks + num_evictable_blocks

    def add_local_computed_blocks(
        self,
        request_id: str,
        new_computed_blocks: Sequence[KVCacheBlock],
        num_local_computed_tokens: int,
        num_external_computed_tokens: int,
    ) -> None:
        """
        Add the locally cached (prefix-hit) blocks to the request:
        1. Touch the computed blocks (paired with adding them to `req_blocks`)
           so their ref_cnt exactly tracks the referencing requests.
        1.5. (Optional) For sliding window, skipped blocks are padded with nulls.
        2. Add the remaining computed blocks.

        Args:
            request_id: The request ID.
            new_computed_blocks: The new computed blocks just hitting the
                prefix cache.
            num_local_computed_tokens: The number of local computed tokens.
            num_external_computed_tokens: The number of external computed tokens.
        """
        # The coordinator only calls this for first-time allocations (running
        # requests are short-circuited there), so the request has no blocks yet.
        req_blocks = self.req_to_blocks[request_id]
        assert len(req_blocks) == 0
        num_total_computed_tokens = (
            num_local_computed_tokens + num_external_computed_tokens
        )
        num_skipped_tokens = self.get_num_skipped_tokens(num_total_computed_tokens)
        num_skipped_blocks = num_skipped_tokens // self.block_size
        if num_skipped_blocks > 0:
            # It is possible that all new computed blocks are skipped when
            # num_skipped_blocks > len(new_computed_blocks).
            new_computed_blocks = new_computed_blocks[num_skipped_blocks:]

        # Touch the computed blocks to make sure they won't be evicted.
        if self.enable_caching:
            self.block_pool.touch(new_computed_blocks)
        else:
            assert not any(new_computed_blocks), (
                "Computed blocks should be empty when prefix caching is disabled"
            )

        # Skip blocks are padded with null blocks.
        req_blocks.extend([self._null_block] * num_skipped_blocks)
        # Add the remaining computed blocks.
        req_blocks.extend(new_computed_blocks)
        # All cached hits (including skipped nulls) are already cached; mark
        # them so cache_blocks() will not try to re-cache blocks that already
        # have a block_hash set.
        self.num_cached_block[request_id] = len(req_blocks)
        if self._has_partial_local_hit(new_computed_blocks, num_local_computed_tokens):
            # Record the partial tail for the CoW redirect in
            # allocate_new_blocks; cap the cached count at the full blocks so
            # cache_blocks() re-caches the private copy once full.
            block_idx = num_local_computed_tokens // self.block_size
            self._partial_hit_reqs[request_id] = (block_idx, new_computed_blocks[-1])
            self.num_cached_block[request_id] = block_idx

    def allocate_external_computed_blocks(
        self,
        request_id: str,
        num_local_computed_tokens: int,
        num_external_computed_tokens: int,
    ) -> None:
        """
        Allocate new blocks for external (KV-connector) computed tokens.

        Must run only after every group's local blocks have been touched via
        `add_local_computed_blocks`, so this group's `get_new_blocks` cannot
        evict another group's cache-hit blocks (issue #33775).

        Args:
            request_id: The request ID.
            num_local_computed_tokens: The number of local computed tokens.
            num_external_computed_tokens: The number of external computed tokens.
        """
        num_total_computed_tokens = (
            num_local_computed_tokens + num_external_computed_tokens
        )
        num_skipped_tokens = self.get_num_skipped_tokens(num_total_computed_tokens)
        if num_skipped_tokens > 0:
            # Some external computed tokens may be skipped too.
            num_external_computed_tokens = min(
                num_total_computed_tokens - num_skipped_tokens,
                num_external_computed_tokens,
            )
        if num_external_computed_tokens <= 0:
            return

        req_blocks = self.req_to_blocks[request_id]
        allocated_blocks = self.block_pool.get_new_blocks(
            cdiv(num_total_computed_tokens, self.block_size) - len(req_blocks)
        )
        req_blocks.extend(allocated_blocks)
        if self._record_new_block_ids:
            self.new_block_ids.extend(b.block_id for b in allocated_blocks)

    def allocate_new_blocks(
        self, request_id: str, num_tokens: int, num_tokens_main_model: int
    ) -> list[KVCacheBlock]:
        """
        Allocate new blocks for the request to give it at least `num_tokens`
        token slots.

        Args:
            request_id: The request ID.
            num_tokens: The total number of tokens that need a slot (including
                tokens that are already allocated).
            num_tokens_main_model: The number of tokens for the main model (aka target
                model in spec decode). w/o spec decode, it is num_tokens;
                with spec decode, it is num_tokens - num_lookahead_tokens.
        Returns:
            The new allocated blocks.
        """
        cow_blocks: list[KVCacheBlock] = []
        if request_id in self._partial_hit_reqs:
            # Partial hit: redirect the shared tail to a private CoW block.
            # Replacing in place keeps the length-based allocation below
            # correct; the extra block was reserved by
            # get_num_blocks_to_allocate.
            block_idx, source_block = self._partial_hit_reqs.pop(request_id)
            cow_block = self.block_pool.get_new_blocks(1)[0]
            self._apply_cow(request_id, block_idx, source_block, cow_block)
            self.new_block_ids.append(cow_block.block_id)
            cow_blocks.append(cow_block)

        req_blocks = self.req_to_blocks[request_id]
        num_required_blocks = cdiv(num_tokens, self.block_size)
        num_new_blocks = num_required_blocks - len(req_blocks)
        if num_new_blocks <= 0:
            return cow_blocks
        else:
            new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
            req_blocks.extend(new_blocks)
            if self._record_new_block_ids:
                self.new_block_ids.extend(b.block_id for b in new_blocks)
            return cow_blocks + new_blocks

    @property
    def records_new_block_ids(self) -> bool:
        """Whether this manager's new blocks are zeroed by the worker."""
        return self._record_new_block_ids

    def take_new_block_ids(self) -> list[int]:
        """Drain and return block IDs allocated since the last call."""
        ids = self.new_block_ids
        self.new_block_ids = []
        return ids

    def take_pending_cow_copies(
        self,
    ) -> list[tuple[KVCacheBlock, KVCacheBlock]]:
        """Drain pending CoW source and destination block pairs."""
        pending_copies = self._pending_cow_copies
        self._pending_cow_copies = []
        return pending_copies

    def take_pending_partial_tail_offloads(
        self,
    ) -> list[tuple[str, int, KVCacheBlock, int]]:
        """Drain producer partial-tail hand-offs.

        Entries are ``(req_id, group_id, block, boundary_tokens)``.

        Only mamba "align" populates this. The block lives off the request
        block table, so the caller must pin it until the connector has read
        it — nothing else keeps it alive once the CoW retention is released.
        """
        pending = self._pending_partial_tail_offloads
        self._pending_partial_tail_offloads = []
        return pending

    def _apply_cow(
        self,
        request_id: str,
        block_idx: int,
        source_block: KVCacheBlock,
        cow_block: KVCacheBlock,
    ) -> None:
        """Redirect a partial prefix-cache hit to a private CoW block.

        Both copy endpoints stay retained until the copy has run on the worker,
        so a same-step free cannot recycle them: ``source_block`` keeps its
        hit-ref, ``cow_block`` takes an extra ref beyond the one handed to the
        request.
        """
        req_blocks = self.req_to_blocks[request_id]
        assert block_idx < len(req_blocks)
        assert req_blocks[block_idx] is source_block
        assert not source_block.is_null and source_block.ref_cnt > 0
        req_blocks[block_idx] = cow_block
        self._pending_cow_copies.append((source_block, cow_block))
        cow_block.ref_cnt += 1

    def cache_blocks(
        self,
        request: Request,
        num_tokens: int,
        retention_interval: int | None = None,
    ) -> None:
        """
        Cache the blocks for the request.

        Args:
            request: The request.
            num_tokens: The total number of tokens that need to be cached
                (including tokens that are already cached).
            retention_interval: Sparse local-checkpoint granularity. ``None``
                keeps dense checkpointing; ``0`` keeps only the latest replay
                boundary; a positive multiple of ``scheduler_block_size`` keeps
                a tail once per that-sized segment. Only SWA acts on it.
        """
        num_cached_blocks = self.num_cached_block.get(request.request_id, 0)
        num_full_blocks = num_tokens // self.block_size

        if num_cached_blocks >= num_full_blocks:
            return

        # Token boundaries whose reachable tail must be retained under sparse
        # retention: the replay boundary (``num_prompt - 1``, capped by
        # ``get_computed_blocks``) and any detected shared-prefix junction.
        reachable_boundaries = [request.num_prompt_tokens - 1]
        if request.shared_prefix_boundary:
            reachable_boundaries.append(request.shared_prefix_boundary)

        block_mask = self.reachable_block_mask(
            start_block=num_cached_blocks,
            end_block=num_full_blocks,
            alignment_tokens=self.scheduler_block_size,
            kv_cache_spec=self.kv_cache_spec,
            use_eagle=self.use_eagle,
            retention_interval=retention_interval,
            reachable_boundaries=reachable_boundaries,
        )
        self.block_pool.cache_full_blocks(
            request=request,
            blocks=self.req_to_blocks[request.request_id],
            num_cached_blocks=num_cached_blocks,
            num_full_blocks=num_full_blocks,
            block_size=self.block_size,
            kv_cache_group_id=self.kv_cache_group_id,
            block_mask=block_mask,
        )

        self.num_cached_block[request.request_id] = num_full_blocks

    @classmethod
    def reachable_block_mask(
        cls,
        start_block: int,
        end_block: int,
        alignment_tokens: int | None,
        kv_cache_spec: KVCacheSpec,
        use_eagle: bool,
        retention_interval: int | None = None,
        reachable_boundaries: Sequence[int] = (),
    ) -> list[bool] | None:
        """Per-block mask for ``cache_full_blocks``. ``None`` means cache
        every (non-null) block — the default for full attention.

        Subclasses with sparse hit semantics (SWA / Mamba) override this to skip
        blocks that can never serve a hit at any alignment-aligned prefix length.
        ``reachable_boundaries`` are token positions whose reachable tail must be
        retained; the base (dense) policy ignores them.
        """
        return None

    def pop_blocks_for_free(self, request_id: str) -> list[KVCacheBlock]:
        """
        Pop the request's bookkeeping and return its blocks without yet
        returning them to the block pool. The caller is responsible for
        eventually passing the returned blocks to `block_pool.free_blocks`,
        freeing them in reverse order (so that tail blocks are evicted first).

        Args:
            request_id: The request ID.

        Returns:
            The request's blocks in allocation order.
        """
        # Default to [] in case a request is freed (aborted) before alloc.
        req_blocks = self.req_to_blocks.pop(request_id, [])
        self.num_cached_block.pop(request_id, None)
        self._partial_hit_reqs.pop(request_id, None)
        return req_blocks

    def free(self, request_id: str) -> None:
        """
        Free the blocks for the request.

        Args:
            request_id: The request ID.
        """
        # Free blocks in reverse order so that the tail blocks are freed first.
        self.block_pool.free_blocks(reversed(self.pop_blocks_for_free(request_id)))

    @abstractmethod
    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        """
        Get the number of common prefix blocks for all requests with allocated
        KV cache.

        Args:
            running_request_id: The request ID.

        Returns:
            The number of common prefix blocks for all requests with allocated
            KV cache.
        """

        raise NotImplementedError

    @classmethod
    @abstractmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        """
        Get the longest cache hit prefix of the blocks that is not longer than
        `max_length`. The prefix should be a common prefix hit for all the
        kv cache groups in `kv_cache_group_ids`. If no cache hit is found,
        return an empty list.
        If eagle is enabled, drop the last matched block to force recompute the
        last block to get the required hidden states for eagle drafting head.
        Need to be customized for each attention type.

        Args:
            block_hashes: The block hashes of the request.
            max_length: The maximum length of the cache hit prefix.
            kv_cache_group_ids: The ids of the kv cache groups.
            block_pool: The block pool.
            kv_cache_spec: The kv cache spec.
            drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
                Always False for non-EAGLE/MTP groups, but can be False for EAGLE/MTP
                groups too if the last block is already dropped (e.g., in a
                convergence loop in `find_longest_cache_hit`).
            alignment_tokens: The returned cache hit length (in tokens) should
                be a multiple of this value (in tokens). By default, it should
                be set to the block_size.
            dcp_world_size: The world size of decode context parallelism.
            pcp_world_size: The world size of prefill context parallelism.

        Returns:
            A tuple containing cached blocks and the exact cache-hit length in
            tokens. The cached block tuple has skipped blocks replaced by null
            blocks for each kv cache group in `kv_cache_group_ids`.
            For example, sliding window manager should return a list like
            ([NULL, NULL, KVCacheBlock(7), KVCacheBlock(8)]) for block size 4
            and sliding window 8 and len(kv_cache_group_ids) = 1.
        """

        raise NotImplementedError

    def _remove_blocks_in_range(
        self,
        request_id: str,
        first_block: int,
        last_block: int,
    ) -> None:
        """Free blocks in ``[first_block, last_block)`` and replace with null_block.

        Iterates backward so newly-evictable tail blocks are reached even after
        earlier blocks in the range were nulled in a prior call.
        """
        if request_id not in self.req_to_blocks:
            return
        if first_block >= last_block:
            return
        blocks = self.req_to_blocks[request_id]
        last_block = min(last_block, len(blocks))

        freed: list[KVCacheBlock] = []
        for i in range(last_block - 1, first_block - 1, -1):
            if blocks[i] == self._null_block:
                break
            freed.append(blocks[i])
            blocks[i] = self._null_block
        if freed:
            self.block_pool.free_blocks(freed)

    def remove_skipped_blocks(
        self,
        request_id: str,
        processed_computed_tokens: int,
        num_prompt_tokens: int | None = None,
    ) -> None:
        """
        Remove and free the blocks that are no longer needed for attention computation.
        The removed blocks should be replaced by null_block.

        This function depends on `get_num_skipped_tokens`, which need to be implemented
        differently for each attention type.

        Args:
            request_id: The request ID.
            processed_computed_tokens: Computed-token prefix length covering
                fully processed and committed tokens only (safe to free).
            num_prompt_tokens: Optional prompt length for attention types (e.g.
                R-SWA) that evict a middle gap rather than a head prefix. Ignored
                by the default implementation.
        """
        del num_prompt_tokens
        # Remove the blocks that will be skipped during attention computation.
        num_skipped_tokens = self.get_num_skipped_tokens(processed_computed_tokens)
        if num_skipped_tokens <= 0:
            # This indicates that ALL tokens are inside attention window.
            # Thus we do not need to free any blocks outside attention window.
            # A typical case is full attention that we never free any token
            # before the request is finished.
            return
        blocks = self.req_to_blocks[request_id]
        num_skipped_blocks = num_skipped_tokens // self.block_size
        # `num_skipped_tokens` may include tokens that haven't been allocated yet
        # (e.g., when the attention window moves into the external computed tokens
        # range), so we must cap to the number of blocks that currently exist for
        # this request.
        num_skipped_blocks = min(num_skipped_blocks, len(blocks))
        self._remove_blocks_in_range(request_id, 0, num_skipped_blocks)

    def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
        """
        Get the number of tokens that will be skipped for attention computation.

        Args:
            num_computed_tokens: The number of tokens that have been computed.

        Returns:
            The number of tokens that will be skipped for attention computation.
        """
        # The default behavior is to not skip any tokens.
        return 0

    def new_step_starts(self) -> None:
        return None

records_new_block_ids property

Whether this manager's new blocks are zeroed by the worker.

__init__(kv_cache_spec, block_pool, enable_caching, kv_cache_group_id, scheduler_block_size, dcp_world_size=1, pcp_world_size=1, needs_kv_cache_zeroing=False, max_admission_blocks_per_request=None)

Initializes the SingleTypeKVCacheManager. Args: kv_cache_spec: The kv_cache_spec for this manager. block_pool: The block pool. kv_cache_group_id: The id of the kv cache group of this manager. scheduler_block_size: The scheduling granularity (LCM of all group block sizes); a multiple of this manager's block_size. needs_kv_cache_zeroing: Whether worker-side KV cache zeroing needs newly allocated block IDs from this manager. max_admission_blocks_per_request: Recycling-aware per-request block cap used by get_num_blocks_to_allocate. Only set for spec types that recycle blocks across chunks (SWA, chunked-local); None (the default) means no cap, which is correct for full-attention-style specs that hold every block until the request finishes.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def __init__(
    self,
    kv_cache_spec: KVCacheSpec,
    block_pool: BlockPool,
    enable_caching: bool,
    kv_cache_group_id: int,
    scheduler_block_size: int,
    dcp_world_size: int = 1,
    pcp_world_size: int = 1,
    needs_kv_cache_zeroing: bool = False,
    max_admission_blocks_per_request: int | None = None,
) -> None:
    """
    Initializes the SingleTypeKVCacheManager.
    Args:
        kv_cache_spec: The kv_cache_spec for this manager.
        block_pool: The block pool.
        kv_cache_group_id: The id of the kv cache group of this manager.
        scheduler_block_size: The scheduling granularity (LCM of all group
            block sizes); a multiple of this manager's ``block_size``.
        needs_kv_cache_zeroing: Whether worker-side KV cache zeroing needs
            newly allocated block IDs from this manager.
        max_admission_blocks_per_request: Recycling-aware per-request
            block cap used by `get_num_blocks_to_allocate`. Only set for
            spec types that recycle blocks across chunks (SWA,
            chunked-local); `None` (the default) means no cap, which is
            correct for full-attention-style specs that hold every
            block until the request finishes.
    """
    self.scheduler_block_size = scheduler_block_size
    # The block size for this manager; used for actual block allocation.
    self.block_size = kv_cache_spec.block_size
    self.dcp_world_size = dcp_world_size
    self.pcp_world_size = pcp_world_size
    if dcp_world_size > 1:
        self.block_size *= dcp_world_size
    self.kv_cache_spec = kv_cache_spec
    self.block_pool = block_pool
    self.enable_caching = enable_caching
    self._max_admission_blocks_per_request = max_admission_blocks_per_request
    # Record newly allocated block ids only when worker-side zeroing will
    # consume them and this manager holds a spec type that gets zeroed.
    self._record_new_block_ids = needs_kv_cache_zeroing and type(kv_cache_spec) in (
        FullAttentionSpec,
        TQFullAttentionSpec,
        MLAAttentionSpec,
        HiddenStateCacheSpec,
    )
    self.new_block_ids: list[int] = []

    # Mapping from request ID to blocks to track the blocks allocated
    # for each request, so that we can free the blocks when the request
    # is finished.
    self.req_to_blocks: defaultdict[str, list[KVCacheBlock]] = defaultdict(list)

    # {req_id: The number of cached blocks for this given request}
    # This is used to track the number of cached blocks for each request.
    # This is only used to track the RUNNING requests, we do not track the
    # data for preempted ones.
    self.num_cached_block: dict[str, int] = {}

    self.kv_cache_group_id = kv_cache_group_id
    self._null_block = block_pool.null_block

    # Whether this group's prefix-cache hits drop the EAGLE/MTP lookahead
    # block. Only consulted by managers whose hit logic is sparse within an
    # aligned segment (SWA). Initialized lazily by the coordinator after
    # determining the attention groups.
    self.use_eagle = False

    # Partial-hit copy-on-write bookkeeping. Populated only by fine-grained
    # managers (full attention, mamba "align"); harmlessly empty elsewhere.
    self._partial_hit_reqs: dict[str, tuple[int, KVCacheBlock]] = {}
    self._pending_cow_copies: list[tuple[KVCacheBlock, KVCacheBlock]] = []
    # Partial-tail offload hand-off for external KV connectors: when a
    # producer registers its last-prompt-boundary partial tail and the
    # durable boundary block is not on the append-only request block table
    # (mamba "align" CoW target), record the request, group, block, and
    # exact token boundary so a connector can offload it under the right
    # hash. Populated only by mamba "align".
    self._pending_partial_tail_offloads: list[
        tuple[str, int, KVCacheBlock, int]
    ] = []

_apply_cow(request_id, block_idx, source_block, cow_block)

Redirect a partial prefix-cache hit to a private CoW block.

Both copy endpoints stay retained until the copy has run on the worker, so a same-step free cannot recycle them: source_block keeps its hit-ref, cow_block takes an extra ref beyond the one handed to the request.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def _apply_cow(
    self,
    request_id: str,
    block_idx: int,
    source_block: KVCacheBlock,
    cow_block: KVCacheBlock,
) -> None:
    """Redirect a partial prefix-cache hit to a private CoW block.

    Both copy endpoints stay retained until the copy has run on the worker,
    so a same-step free cannot recycle them: ``source_block`` keeps its
    hit-ref, ``cow_block`` takes an extra ref beyond the one handed to the
    request.
    """
    req_blocks = self.req_to_blocks[request_id]
    assert block_idx < len(req_blocks)
    assert req_blocks[block_idx] is source_block
    assert not source_block.is_null and source_block.ref_cnt > 0
    req_blocks[block_idx] = cow_block
    self._pending_cow_copies.append((source_block, cow_block))
    cow_block.ref_cnt += 1

_remove_blocks_in_range(request_id, first_block, last_block)

Free blocks in [first_block, last_block) and replace with null_block.

Iterates backward so newly-evictable tail blocks are reached even after earlier blocks in the range were nulled in a prior call.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def _remove_blocks_in_range(
    self,
    request_id: str,
    first_block: int,
    last_block: int,
) -> None:
    """Free blocks in ``[first_block, last_block)`` and replace with null_block.

    Iterates backward so newly-evictable tail blocks are reached even after
    earlier blocks in the range were nulled in a prior call.
    """
    if request_id not in self.req_to_blocks:
        return
    if first_block >= last_block:
        return
    blocks = self.req_to_blocks[request_id]
    last_block = min(last_block, len(blocks))

    freed: list[KVCacheBlock] = []
    for i in range(last_block - 1, first_block - 1, -1):
        if blocks[i] == self._null_block:
            break
        freed.append(blocks[i])
        blocks[i] = self._null_block
    if freed:
        self.block_pool.free_blocks(freed)

add_local_computed_blocks(request_id, new_computed_blocks, num_local_computed_tokens, num_external_computed_tokens)

Add the locally cached (prefix-hit) blocks to the request: 1. Touch the computed blocks (paired with adding them to req_blocks) so their ref_cnt exactly tracks the referencing requests. 1.5. (Optional) For sliding window, skipped blocks are padded with nulls. 2. Add the remaining computed blocks.

Parameters:

  • request_id

    (str) –

    The request ID.

  • new_computed_blocks

    (Sequence[KVCacheBlock]) –

    The new computed blocks just hitting the prefix cache.

  • num_local_computed_tokens

    (int) –

    The number of local computed tokens.

  • num_external_computed_tokens

    (int) –

    The number of external computed tokens.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def add_local_computed_blocks(
    self,
    request_id: str,
    new_computed_blocks: Sequence[KVCacheBlock],
    num_local_computed_tokens: int,
    num_external_computed_tokens: int,
) -> None:
    """
    Add the locally cached (prefix-hit) blocks to the request:
    1. Touch the computed blocks (paired with adding them to `req_blocks`)
       so their ref_cnt exactly tracks the referencing requests.
    1.5. (Optional) For sliding window, skipped blocks are padded with nulls.
    2. Add the remaining computed blocks.

    Args:
        request_id: The request ID.
        new_computed_blocks: The new computed blocks just hitting the
            prefix cache.
        num_local_computed_tokens: The number of local computed tokens.
        num_external_computed_tokens: The number of external computed tokens.
    """
    # The coordinator only calls this for first-time allocations (running
    # requests are short-circuited there), so the request has no blocks yet.
    req_blocks = self.req_to_blocks[request_id]
    assert len(req_blocks) == 0
    num_total_computed_tokens = (
        num_local_computed_tokens + num_external_computed_tokens
    )
    num_skipped_tokens = self.get_num_skipped_tokens(num_total_computed_tokens)
    num_skipped_blocks = num_skipped_tokens // self.block_size
    if num_skipped_blocks > 0:
        # It is possible that all new computed blocks are skipped when
        # num_skipped_blocks > len(new_computed_blocks).
        new_computed_blocks = new_computed_blocks[num_skipped_blocks:]

    # Touch the computed blocks to make sure they won't be evicted.
    if self.enable_caching:
        self.block_pool.touch(new_computed_blocks)
    else:
        assert not any(new_computed_blocks), (
            "Computed blocks should be empty when prefix caching is disabled"
        )

    # Skip blocks are padded with null blocks.
    req_blocks.extend([self._null_block] * num_skipped_blocks)
    # Add the remaining computed blocks.
    req_blocks.extend(new_computed_blocks)
    # All cached hits (including skipped nulls) are already cached; mark
    # them so cache_blocks() will not try to re-cache blocks that already
    # have a block_hash set.
    self.num_cached_block[request_id] = len(req_blocks)
    if self._has_partial_local_hit(new_computed_blocks, num_local_computed_tokens):
        # Record the partial tail for the CoW redirect in
        # allocate_new_blocks; cap the cached count at the full blocks so
        # cache_blocks() re-caches the private copy once full.
        block_idx = num_local_computed_tokens // self.block_size
        self._partial_hit_reqs[request_id] = (block_idx, new_computed_blocks[-1])
        self.num_cached_block[request_id] = block_idx

allocate_external_computed_blocks(request_id, num_local_computed_tokens, num_external_computed_tokens)

Allocate new blocks for external (KV-connector) computed tokens.

Must run only after every group's local blocks have been touched via add_local_computed_blocks, so this group's get_new_blocks cannot evict another group's cache-hit blocks (issue #33775).

Parameters:

  • request_id

    (str) –

    The request ID.

  • num_local_computed_tokens

    (int) –

    The number of local computed tokens.

  • num_external_computed_tokens

    (int) –

    The number of external computed tokens.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def allocate_external_computed_blocks(
    self,
    request_id: str,
    num_local_computed_tokens: int,
    num_external_computed_tokens: int,
) -> None:
    """
    Allocate new blocks for external (KV-connector) computed tokens.

    Must run only after every group's local blocks have been touched via
    `add_local_computed_blocks`, so this group's `get_new_blocks` cannot
    evict another group's cache-hit blocks (issue #33775).

    Args:
        request_id: The request ID.
        num_local_computed_tokens: The number of local computed tokens.
        num_external_computed_tokens: The number of external computed tokens.
    """
    num_total_computed_tokens = (
        num_local_computed_tokens + num_external_computed_tokens
    )
    num_skipped_tokens = self.get_num_skipped_tokens(num_total_computed_tokens)
    if num_skipped_tokens > 0:
        # Some external computed tokens may be skipped too.
        num_external_computed_tokens = min(
            num_total_computed_tokens - num_skipped_tokens,
            num_external_computed_tokens,
        )
    if num_external_computed_tokens <= 0:
        return

    req_blocks = self.req_to_blocks[request_id]
    allocated_blocks = self.block_pool.get_new_blocks(
        cdiv(num_total_computed_tokens, self.block_size) - len(req_blocks)
    )
    req_blocks.extend(allocated_blocks)
    if self._record_new_block_ids:
        self.new_block_ids.extend(b.block_id for b in allocated_blocks)

allocate_new_blocks(request_id, num_tokens, num_tokens_main_model)

Allocate new blocks for the request to give it at least num_tokens token slots.

Parameters:

  • request_id

    (str) –

    The request ID.

  • num_tokens

    (int) –

    The total number of tokens that need a slot (including tokens that are already allocated).

  • num_tokens_main_model

    (int) –

    The number of tokens for the main model (aka target model in spec decode). w/o spec decode, it is num_tokens; with spec decode, it is num_tokens - num_lookahead_tokens.

Returns: The new allocated blocks.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def allocate_new_blocks(
    self, request_id: str, num_tokens: int, num_tokens_main_model: int
) -> list[KVCacheBlock]:
    """
    Allocate new blocks for the request to give it at least `num_tokens`
    token slots.

    Args:
        request_id: The request ID.
        num_tokens: The total number of tokens that need a slot (including
            tokens that are already allocated).
        num_tokens_main_model: The number of tokens for the main model (aka target
            model in spec decode). w/o spec decode, it is num_tokens;
            with spec decode, it is num_tokens - num_lookahead_tokens.
    Returns:
        The new allocated blocks.
    """
    cow_blocks: list[KVCacheBlock] = []
    if request_id in self._partial_hit_reqs:
        # Partial hit: redirect the shared tail to a private CoW block.
        # Replacing in place keeps the length-based allocation below
        # correct; the extra block was reserved by
        # get_num_blocks_to_allocate.
        block_idx, source_block = self._partial_hit_reqs.pop(request_id)
        cow_block = self.block_pool.get_new_blocks(1)[0]
        self._apply_cow(request_id, block_idx, source_block, cow_block)
        self.new_block_ids.append(cow_block.block_id)
        cow_blocks.append(cow_block)

    req_blocks = self.req_to_blocks[request_id]
    num_required_blocks = cdiv(num_tokens, self.block_size)
    num_new_blocks = num_required_blocks - len(req_blocks)
    if num_new_blocks <= 0:
        return cow_blocks
    else:
        new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
        req_blocks.extend(new_blocks)
        if self._record_new_block_ids:
            self.new_block_ids.extend(b.block_id for b in new_blocks)
        return cow_blocks + new_blocks

cache_blocks(request, num_tokens, retention_interval=None)

Cache the blocks for the request.

Parameters:

  • request

    (Request) –

    The request.

  • num_tokens

    (int) –

    The total number of tokens that need to be cached (including tokens that are already cached).

  • retention_interval

    (int | None, default: None ) –

    Sparse local-checkpoint granularity. None keeps dense checkpointing; 0 keeps only the latest replay boundary; a positive multiple of scheduler_block_size keeps a tail once per that-sized segment. Only SWA acts on it.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def cache_blocks(
    self,
    request: Request,
    num_tokens: int,
    retention_interval: int | None = None,
) -> None:
    """
    Cache the blocks for the request.

    Args:
        request: The request.
        num_tokens: The total number of tokens that need to be cached
            (including tokens that are already cached).
        retention_interval: Sparse local-checkpoint granularity. ``None``
            keeps dense checkpointing; ``0`` keeps only the latest replay
            boundary; a positive multiple of ``scheduler_block_size`` keeps
            a tail once per that-sized segment. Only SWA acts on it.
    """
    num_cached_blocks = self.num_cached_block.get(request.request_id, 0)
    num_full_blocks = num_tokens // self.block_size

    if num_cached_blocks >= num_full_blocks:
        return

    # Token boundaries whose reachable tail must be retained under sparse
    # retention: the replay boundary (``num_prompt - 1``, capped by
    # ``get_computed_blocks``) and any detected shared-prefix junction.
    reachable_boundaries = [request.num_prompt_tokens - 1]
    if request.shared_prefix_boundary:
        reachable_boundaries.append(request.shared_prefix_boundary)

    block_mask = self.reachable_block_mask(
        start_block=num_cached_blocks,
        end_block=num_full_blocks,
        alignment_tokens=self.scheduler_block_size,
        kv_cache_spec=self.kv_cache_spec,
        use_eagle=self.use_eagle,
        retention_interval=retention_interval,
        reachable_boundaries=reachable_boundaries,
    )
    self.block_pool.cache_full_blocks(
        request=request,
        blocks=self.req_to_blocks[request.request_id],
        num_cached_blocks=num_cached_blocks,
        num_full_blocks=num_full_blocks,
        block_size=self.block_size,
        kv_cache_group_id=self.kv_cache_group_id,
        block_mask=block_mask,
    )

    self.num_cached_block[request.request_id] = num_full_blocks

find_longest_cache_hit(block_hashes, max_length, kv_cache_group_ids, block_pool, kv_cache_spec, drop_eagle_block, alignment_tokens, dcp_world_size=1, pcp_world_size=1) abstractmethod classmethod

Get the longest cache hit prefix of the blocks that is not longer than max_length. The prefix should be a common prefix hit for all the kv cache groups in kv_cache_group_ids. If no cache hit is found, return an empty list. If eagle is enabled, drop the last matched block to force recompute the last block to get the required hidden states for eagle drafting head. Need to be customized for each attention type.

Parameters:

  • block_hashes

    (BlockHashList) –

    The block hashes of the request.

  • max_length

    (int) –

    The maximum length of the cache hit prefix.

  • kv_cache_group_ids

    (list[int]) –

    The ids of the kv cache groups.

  • block_pool

    (BlockPool) –

    The block pool.

  • kv_cache_spec

    (KVCacheSpec) –

    The kv cache spec.

  • drop_eagle_block

    (bool) –

    Whether to drop the last matched block for EAGLE/MTP. Always False for non-EAGLE/MTP groups, but can be False for EAGLE/MTP groups too if the last block is already dropped (e.g., in a convergence loop in find_longest_cache_hit).

  • alignment_tokens

    (int) –

    The returned cache hit length (in tokens) should be a multiple of this value (in tokens). By default, it should be set to the block_size.

  • dcp_world_size

    (int, default: 1 ) –

    The world size of decode context parallelism.

  • pcp_world_size

    (int, default: 1 ) –

    The world size of prefill context parallelism.

Returns:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
@classmethod
@abstractmethod
def find_longest_cache_hit(
    cls,
    block_hashes: BlockHashList,
    max_length: int,
    kv_cache_group_ids: list[int],
    block_pool: BlockPool,
    kv_cache_spec: KVCacheSpec,
    drop_eagle_block: bool,
    alignment_tokens: int,
    dcp_world_size: int = 1,
    pcp_world_size: int = 1,
) -> tuple[tuple[list[KVCacheBlock], ...], int]:
    """
    Get the longest cache hit prefix of the blocks that is not longer than
    `max_length`. The prefix should be a common prefix hit for all the
    kv cache groups in `kv_cache_group_ids`. If no cache hit is found,
    return an empty list.
    If eagle is enabled, drop the last matched block to force recompute the
    last block to get the required hidden states for eagle drafting head.
    Need to be customized for each attention type.

    Args:
        block_hashes: The block hashes of the request.
        max_length: The maximum length of the cache hit prefix.
        kv_cache_group_ids: The ids of the kv cache groups.
        block_pool: The block pool.
        kv_cache_spec: The kv cache spec.
        drop_eagle_block: Whether to drop the last matched block for EAGLE/MTP.
            Always False for non-EAGLE/MTP groups, but can be False for EAGLE/MTP
            groups too if the last block is already dropped (e.g., in a
            convergence loop in `find_longest_cache_hit`).
        alignment_tokens: The returned cache hit length (in tokens) should
            be a multiple of this value (in tokens). By default, it should
            be set to the block_size.
        dcp_world_size: The world size of decode context parallelism.
        pcp_world_size: The world size of prefill context parallelism.

    Returns:
        A tuple containing cached blocks and the exact cache-hit length in
        tokens. The cached block tuple has skipped blocks replaced by null
        blocks for each kv cache group in `kv_cache_group_ids`.
        For example, sliding window manager should return a list like
        ([NULL, NULL, KVCacheBlock(7), KVCacheBlock(8)]) for block size 4
        and sliding window 8 and len(kv_cache_group_ids) = 1.
    """

    raise NotImplementedError

free(request_id)

Free the blocks for the request.

Parameters:

  • request_id

    (str) –

    The request ID.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def free(self, request_id: str) -> None:
    """
    Free the blocks for the request.

    Args:
        request_id: The request ID.
    """
    # Free blocks in reverse order so that the tail blocks are freed first.
    self.block_pool.free_blocks(reversed(self.pop_blocks_for_free(request_id)))

get_num_blocks_to_allocate(request_id, num_tokens, new_computed_blocks, total_computed_tokens, num_local_computed_tokens, num_tokens_main_model, apply_admission_cap=False)

Get the number of blocks needed to be allocated for the request.

Parameters:

  • request_id

    (str) –

    The request ID.

  • num_tokens

    (int) –

    The total number of tokens that need a slot (including tokens that are already allocated).

  • new_computed_blocks

    (Sequence[KVCacheBlock]) –

    The new computed blocks just hitting the prefix caching.

  • total_computed_tokens

    (int) –

    Include both local and external computed tokens.

  • num_local_computed_tokens

    (int) –

    The number of local prefix-cache computed tokens.

  • num_tokens_main_model

    (int) –

    The number of tokens for the main model (aka target model in spec decode). w/o spec decode, it is num_tokens; with spec decode, it is num_tokens - num_lookahead_tokens.

  • apply_admission_cap

    (bool, default: False ) –

    If True, clamp by num_required_blocks by _max_admission_blocks_per_requestfor recycling-aware specs (SWA, chunked-local).

Returns:

  • int

    The number of blocks to allocate.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_blocks_to_allocate(
    self,
    request_id: str,
    num_tokens: int,
    new_computed_blocks: Sequence[KVCacheBlock],
    total_computed_tokens: int,
    num_local_computed_tokens: int,
    num_tokens_main_model: int,
    apply_admission_cap: bool = False,
) -> int:
    """
    Get the number of blocks needed to be allocated for the request.

    Args:
        request_id: The request ID.
        num_tokens: The total number of tokens that need a slot (including
            tokens that are already allocated).
        new_computed_blocks: The new computed blocks just hitting the
            prefix caching.
        total_computed_tokens: Include both local and external computed
            tokens.
        num_local_computed_tokens: The number of local prefix-cache computed
            tokens.
        num_tokens_main_model: The number of tokens for the main model (aka target
            model in spec decode). w/o spec decode, it is num_tokens;
            with spec decode, it is num_tokens - num_lookahead_tokens.
        apply_admission_cap: If True, clamp by `num_required_blocks` by
            `_max_admission_blocks_per_request`for recycling-aware specs
            (SWA, chunked-local).

    Returns:
        The number of blocks to allocate.
    """

    num_required_blocks = cdiv(num_tokens, self.block_size)
    if apply_admission_cap and self._max_admission_blocks_per_request is not None:
        # Recycling-aware specs (SWA, chunked-local) cap the per-request
        # reservation here so admission matches the startup pool sizer
        # (`SlidingWindowSpec.max_admission_blocks_per_request` / its
        # chunked-local counterpart). `remove_skipped_blocks` runs from
        # `allocate_slots` before each chunk's `get_num_blocks_to_allocate`,
        # so per-request peak real-held blocks <= this cap, which keeps
        # `sum(reservations) <= pool` <=> `sum(peak_real_held) <= pool`.
        # Drift between the two would re-introduce the deadlock from
        # issue #39734 or, worse, mid-prefill OOM.
        num_required_blocks = min(
            num_required_blocks, self._max_admission_blocks_per_request
        )
    num_req_blocks = len(self.req_to_blocks.get(request_id, ()))

    if request_id in self.num_cached_block:
        # Fast-path: a running request won't have any new prefix-cache hits.
        assert len(new_computed_blocks) == 0
        # NOTE: With speculative decoding, request's blocks may be allocated
        # for draft tokens which are later rejected. In this case,
        # num_required_blocks may be smaller than num_req_blocks.
        return max(num_required_blocks - num_req_blocks, 0)

    num_skipped_tokens = self.get_num_skipped_tokens(total_computed_tokens)
    num_local_computed_blocks = len(new_computed_blocks) + num_req_blocks
    # Number of whole blocks that are skipped by the attention window.
    # If nothing is skipped, this is 0.
    num_skipped_blocks = num_skipped_tokens // self.block_size
    # We need blocks for the non-skipped suffix. If there are still
    # local-computed blocks inside the window, they contribute to the
    # required capacity; otherwise, skipped blocks dominate.
    num_new_blocks = max(
        num_required_blocks - max(num_skipped_blocks, num_local_computed_blocks),
        0,
    )

    # Among the `new_computed_blocks`, the first `num_skipped_blocks` worth
    # of blocks are skipped; `num_req_blocks` of those may already be in
    # `req_to_blocks`, so only skip the remainder from `new_computed_blocks`.
    num_skipped_new_computed_blocks = max(0, num_skipped_blocks - num_req_blocks)

    # If a computed block is an eviction candidate (in the free queue and
    # ref_cnt == 0), it will be removed from the free queue when touched by
    # the allocated request, so we must count it in the free-capacity check.
    num_evictable_blocks = self._get_num_evictable_blocks(
        new_computed_blocks[num_skipped_new_computed_blocks:]
    )
    if self._has_partial_local_hit(new_computed_blocks, num_local_computed_tokens):
        # Reserve the extra block that allocate_new_blocks pulls for the
        # partial-hit CoW redirect.
        num_new_blocks += 1
    return num_new_blocks + num_evictable_blocks

get_num_common_prefix_blocks(running_request_id) abstractmethod

Get the number of common prefix blocks for all requests with allocated KV cache.

Parameters:

  • running_request_id

    (str) –

    The request ID.

Returns:

  • int

    The number of common prefix blocks for all requests with allocated

  • int

    KV cache.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
@abstractmethod
def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
    """
    Get the number of common prefix blocks for all requests with allocated
    KV cache.

    Args:
        running_request_id: The request ID.

    Returns:
        The number of common prefix blocks for all requests with allocated
        KV cache.
    """

    raise NotImplementedError

get_num_skipped_tokens(num_computed_tokens)

Get the number of tokens that will be skipped for attention computation.

Parameters:

  • num_computed_tokens

    (int) –

    The number of tokens that have been computed.

Returns:

  • int

    The number of tokens that will be skipped for attention computation.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
    """
    Get the number of tokens that will be skipped for attention computation.

    Args:
        num_computed_tokens: The number of tokens that have been computed.

    Returns:
        The number of tokens that will be skipped for attention computation.
    """
    # The default behavior is to not skip any tokens.
    return 0

pop_blocks_for_free(request_id)

Pop the request's bookkeeping and return its blocks without yet returning them to the block pool. The caller is responsible for eventually passing the returned blocks to block_pool.free_blocks, freeing them in reverse order (so that tail blocks are evicted first).

Parameters:

  • request_id

    (str) –

    The request ID.

Returns:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def pop_blocks_for_free(self, request_id: str) -> list[KVCacheBlock]:
    """
    Pop the request's bookkeeping and return its blocks without yet
    returning them to the block pool. The caller is responsible for
    eventually passing the returned blocks to `block_pool.free_blocks`,
    freeing them in reverse order (so that tail blocks are evicted first).

    Args:
        request_id: The request ID.

    Returns:
        The request's blocks in allocation order.
    """
    # Default to [] in case a request is freed (aborted) before alloc.
    req_blocks = self.req_to_blocks.pop(request_id, [])
    self.num_cached_block.pop(request_id, None)
    self._partial_hit_reqs.pop(request_id, None)
    return req_blocks

reachable_block_mask(start_block, end_block, alignment_tokens, kv_cache_spec, use_eagle, retention_interval=None, reachable_boundaries=()) classmethod

Per-block mask for cache_full_blocks. None means cache every (non-null) block — the default for full attention.

Subclasses with sparse hit semantics (SWA / Mamba) override this to skip blocks that can never serve a hit at any alignment-aligned prefix length. reachable_boundaries are token positions whose reachable tail must be retained; the base (dense) policy ignores them.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
@classmethod
def reachable_block_mask(
    cls,
    start_block: int,
    end_block: int,
    alignment_tokens: int | None,
    kv_cache_spec: KVCacheSpec,
    use_eagle: bool,
    retention_interval: int | None = None,
    reachable_boundaries: Sequence[int] = (),
) -> list[bool] | None:
    """Per-block mask for ``cache_full_blocks``. ``None`` means cache
    every (non-null) block — the default for full attention.

    Subclasses with sparse hit semantics (SWA / Mamba) override this to skip
    blocks that can never serve a hit at any alignment-aligned prefix length.
    ``reachable_boundaries`` are token positions whose reachable tail must be
    retained; the base (dense) policy ignores them.
    """
    return None

remove_skipped_blocks(request_id, processed_computed_tokens, num_prompt_tokens=None)

Remove and free the blocks that are no longer needed for attention computation. The removed blocks should be replaced by null_block.

This function depends on get_num_skipped_tokens, which need to be implemented differently for each attention type.

Parameters:

  • request_id

    (str) –

    The request ID.

  • processed_computed_tokens

    (int) –

    Computed-token prefix length covering fully processed and committed tokens only (safe to free).

  • num_prompt_tokens

    (int | None, default: None ) –

    Optional prompt length for attention types (e.g. R-SWA) that evict a middle gap rather than a head prefix. Ignored by the default implementation.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def remove_skipped_blocks(
    self,
    request_id: str,
    processed_computed_tokens: int,
    num_prompt_tokens: int | None = None,
) -> None:
    """
    Remove and free the blocks that are no longer needed for attention computation.
    The removed blocks should be replaced by null_block.

    This function depends on `get_num_skipped_tokens`, which need to be implemented
    differently for each attention type.

    Args:
        request_id: The request ID.
        processed_computed_tokens: Computed-token prefix length covering
            fully processed and committed tokens only (safe to free).
        num_prompt_tokens: Optional prompt length for attention types (e.g.
            R-SWA) that evict a middle gap rather than a head prefix. Ignored
            by the default implementation.
    """
    del num_prompt_tokens
    # Remove the blocks that will be skipped during attention computation.
    num_skipped_tokens = self.get_num_skipped_tokens(processed_computed_tokens)
    if num_skipped_tokens <= 0:
        # This indicates that ALL tokens are inside attention window.
        # Thus we do not need to free any blocks outside attention window.
        # A typical case is full attention that we never free any token
        # before the request is finished.
        return
    blocks = self.req_to_blocks[request_id]
    num_skipped_blocks = num_skipped_tokens // self.block_size
    # `num_skipped_tokens` may include tokens that haven't been allocated yet
    # (e.g., when the attention window moves into the external computed tokens
    # range), so we must cap to the number of blocks that currently exist for
    # this request.
    num_skipped_blocks = min(num_skipped_blocks, len(blocks))
    self._remove_blocks_in_range(request_id, 0, num_skipped_blocks)

take_new_block_ids()

Drain and return block IDs allocated since the last call.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def take_new_block_ids(self) -> list[int]:
    """Drain and return block IDs allocated since the last call."""
    ids = self.new_block_ids
    self.new_block_ids = []
    return ids

take_pending_cow_copies()

Drain pending CoW source and destination block pairs.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def take_pending_cow_copies(
    self,
) -> list[tuple[KVCacheBlock, KVCacheBlock]]:
    """Drain pending CoW source and destination block pairs."""
    pending_copies = self._pending_cow_copies
    self._pending_cow_copies = []
    return pending_copies

take_pending_partial_tail_offloads()

Drain producer partial-tail hand-offs.

Entries are (req_id, group_id, block, boundary_tokens).

Only mamba "align" populates this. The block lives off the request block table, so the caller must pin it until the connector has read it — nothing else keeps it alive once the CoW retention is released.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def take_pending_partial_tail_offloads(
    self,
) -> list[tuple[str, int, KVCacheBlock, int]]:
    """Drain producer partial-tail hand-offs.

    Entries are ``(req_id, group_id, block, boundary_tokens)``.

    Only mamba "align" populates this. The block lives off the request
    block table, so the caller must pin it until the connector has read
    it — nothing else keeps it alive once the CoW retention is released.
    """
    pending = self._pending_partial_tail_offloads
    self._pending_partial_tail_offloads = []
    return pending

SlidingWindowManager

Bases: SingleTypeKVCacheManager

Methods:

Source code in vllm/v1/core/single_type_kv_cache_manager.py
class SlidingWindowManager(SingleTypeKVCacheManager):
    def __init__(self, kv_cache_spec: SlidingWindowSpec, **kwargs) -> None:
        super().__init__(kv_cache_spec, **kwargs)
        self.sliding_window = kv_cache_spec.sliding_window

    @classmethod
    def _contiguous_blocks_for_hit(
        cls, window_size: int, block_size: int, use_eagle: bool
    ) -> int:
        blocks = cdiv(window_size - 1, block_size)
        if use_eagle:
            # Need to drop the last matched block if eagle is enabled. For
            # sliding window layer, we achieve this by increasing the number of
            # contiguous blocks needed for prefix cache hit by one and dropping
            # the last matched block.
            blocks += 1
        return blocks

    @classmethod
    def find_longest_cache_hit(
        cls,
        block_hashes: BlockHashList,
        max_length: int,
        kv_cache_group_ids: list[int],
        block_pool: BlockPool,
        kv_cache_spec: KVCacheSpec,
        drop_eagle_block: bool,
        alignment_tokens: int,
        dcp_world_size: int = 1,
        pcp_world_size: int = 1,
    ) -> tuple[tuple[list[KVCacheBlock], ...], int]:
        assert isinstance(kv_cache_spec, SlidingWindowSpec), (
            "SlidingWindowManager can only be used for sliding window groups"
        )
        assert dcp_world_size == 1, "DCP not support sliding window attn now."
        assert pcp_world_size == 1, "PCP not support sliding window attn now."
        # Fine-grained partial hits are not supported for sliding window now
        assert alignment_tokens % kv_cache_spec.block_size == 0, (
            "SlidingWindowManager does not support fine-grained (partial) cache hits"
        )
        block_hashes = resolve_block_hashes(
            block_hashes,
            block_pool.hash_block_size,
            kv_cache_spec.block_size,
            supports_fine_grained_hash_lookup=cls.supports_fine_grained_hash_lookup,
            alignment_tokens=alignment_tokens,
        )

        # The number of contiguous blocks needed for a prefix cache hit.
        sliding_window_contiguous_blocks = cls._contiguous_blocks_for_hit(
            kv_cache_spec.sliding_window, kv_cache_spec.block_size, drop_eagle_block
        )

        # TODO: reduce i by sliding_window_contiguous_blocks when cache miss, to
        # optimize the time complexity from O(max_num_blocks) to
        # O(max_num_blocks / sliding_window_contiguous_blocks +
        # sliding_window_contiguous_blocks),
        # which is good for low cache hit rate scenarios.
        max_num_blocks = max_length // kv_cache_spec.block_size
        computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
            [block_pool.null_block] * max_num_blocks
            for _ in range(len(kv_cache_group_ids))
        )
        block_size = kv_cache_spec.block_size
        num_contiguous_blocks = 0
        match_found = False
        # Search from right to left and early stop when a match is found.
        for i in range(max_num_blocks - 1, -1, -1):
            if cached_block := block_pool.get_cached_block(
                block_hashes[i], kv_cache_group_ids
            ):
                # Skip prefix matching check if the block is not aligned with
                # `alignment_tokens`.
                if num_contiguous_blocks == 0 and block_size != alignment_tokens:
                    post_pop_blocks = i if drop_eagle_block else i + 1
                    if (post_pop_blocks * block_size) % alignment_tokens != 0:
                        continue
                # Add the cached block to the computed blocks.
                for computed, cached in zip(computed_blocks, cached_block):
                    computed[i] = cached
                num_contiguous_blocks += 1
                if num_contiguous_blocks >= sliding_window_contiguous_blocks:
                    # Trim the trailing blocks.
                    # E.g., [NULL, NULL, 8, 3, NULL, 9] -> [NULL, NULL, 8, 3]
                    # when sliding_window_contiguous_blocks=2.
                    for computed in computed_blocks:
                        del computed[i + num_contiguous_blocks :]
                    match_found = True
                    break
            else:
                num_contiguous_blocks = 0
        if not match_found:
            # The first `num_contiguous_blocks` is a cache hit even if
            # `num_contiguous_blocks < sliding_window_contiguous_blocks`.
            for computed in computed_blocks:
                del computed[num_contiguous_blocks:]
            while (
                block_size != alignment_tokens  # Faster for common case.
                and len(computed_blocks[0]) * block_size % alignment_tokens != 0
            ):
                for computed in computed_blocks:
                    computed.pop()
        if drop_eagle_block and computed_blocks[0]:
            for computed in computed_blocks:
                computed.pop()
            # Re-align after eagle pop: the pop may break the alignment
            # when block_size != alignment_tokens (hybrid models with
            # different page sizes, e.g. Gemma4).
            while (
                block_size != alignment_tokens
                and len(computed_blocks[0]) * block_size % alignment_tokens != 0
            ):
                for computed in computed_blocks:
                    computed.pop()
        hit_length = len(computed_blocks[0]) * block_size
        return computed_blocks, hit_length

    @classmethod
    def reachable_block_mask(
        cls,
        start_block: int,
        end_block: int,
        alignment_tokens: int | None,
        kv_cache_spec: KVCacheSpec,
        use_eagle: bool,
        retention_interval: int | None = None,
        reachable_boundaries: Sequence[int] = (),
    ) -> list[bool] | None:
        assert isinstance(kv_cache_spec, SlidingWindowSpec)
        if alignment_tokens is None:
            # Fast path: when the coordinator imposes no alignment constraint.
            return None
        assert alignment_tokens % kv_cache_spec.block_size == 0

        block_size = kv_cache_spec.block_size
        # Contiguous blocks a hit needs at a boundary (incl. the EAGLE peek).
        need = cls._contiguous_blocks_for_hit(
            window_size=kv_cache_spec.sliding_window,
            block_size=block_size,
            use_eagle=use_eagle,
        )
        # The matched run's right edge sits on the aligned boundary block when
        # EAGLE peeks one block past it (shift=1), otherwise on the last block
        # before the boundary (shift=0).
        shift = 1 if use_eagle else 0

        mask = [False] * (end_block - start_block)

        # (1) Segment-boundary tails. ``retention_interval``:
        #   None -> dense (a tail at every ``alignment_tokens`` boundary);
        #   0    -> no dense tails (only the replay boundary below);
        #   >0   -> a tail once per ``retention_interval``-sized segment.
        segment_tokens = (
            alignment_tokens
            if retention_interval is None
            else (None if retention_interval == 0 else retention_interval)
        )
        if segment_tokens is not None:
            per_segment = segment_tokens // block_size
            if need >= per_segment:
                # Every block is reachable; cache them all.
                return None
            for i in range(start_block, end_block):
                if i >= shift and (i - shift) % per_segment >= per_segment - need:
                    mask[i - start_block] = True

        # (2) Reachable-boundary tails: the replay boundary (``num_prompt - 1``,
        # capped by ``get_computed_blocks``) and any shared-prefix junction. Both
        # land before segments would cover them under sparse retention, so keep
        # the ``need``-block tail ending on each boundary explicitly.
        if retention_interval is not None:
            for boundary_tokens in reachable_boundaries:
                aligned = boundary_tokens // alignment_tokens * alignment_tokens
                end = aligned // block_size + shift
                for j in range(max(start_block, end - need), min(end_block, end)):
                    mask[j - start_block] = True

        return mask

    def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
        """
        Get the number of tokens that will be skipped for attention computation.

        For sliding window, this corresponds to the tokens that are prior to
        the current sliding window.

        Example:
        sliding_window=4, num_computed_tokens=7

        Tokens:   [ 0  1  2  3  4  5  6  7 ]
                  | ---- computed -----|
                                         ^ next token to be computed
                               |-----------| sliding window for next token
                  |--skipped---|

        The current window contains tokens 4~7. Tokens 0~3 will be skipped for
        attention computation since they are outside the sliding window.
        Thus, get_num_skipped_tokens(7) == 4.

        Args:
            num_computed_tokens: The number of tokens that have been computed.

        Returns:
            The number of tokens that will be skipped for attention computation.
        """
        return max(0, num_computed_tokens - self.sliding_window + 1)

    def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
        """
        NOTE(Chen): The prefix blocks are null blocks for sliding window layers.
        So it's not correct to count ref_cnt like FullAttentionManager. Return
        0 here for correctness. Need to support cascade attention + sliding
        window in the future.
        """
        return 0

get_num_common_prefix_blocks(running_request_id)

NOTE(Chen): The prefix blocks are null blocks for sliding window layers. So it's not correct to count ref_cnt like FullAttentionManager. Return 0 here for correctness. Need to support cascade attention + sliding window in the future.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_common_prefix_blocks(self, running_request_id: str) -> int:
    """
    NOTE(Chen): The prefix blocks are null blocks for sliding window layers.
    So it's not correct to count ref_cnt like FullAttentionManager. Return
    0 here for correctness. Need to support cascade attention + sliding
    window in the future.
    """
    return 0

get_num_skipped_tokens(num_computed_tokens)

Get the number of tokens that will be skipped for attention computation.

For sliding window, this corresponds to the tokens that are prior to the current sliding window.

Example: sliding_window=4, num_computed_tokens=7

[ 0 1 2 3 4 5 6 7 ]

| ---- computed -----| ^ next token to be computed |-----------| sliding window for next token |--skipped---|

The current window contains tokens 4~7. Tokens 0~3 will be skipped for attention computation since they are outside the sliding window. Thus, get_num_skipped_tokens(7) == 4.

Parameters:

  • num_computed_tokens

    (int) –

    The number of tokens that have been computed.

Returns:

  • int

    The number of tokens that will be skipped for attention computation.

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_num_skipped_tokens(self, num_computed_tokens: int) -> int:
    """
    Get the number of tokens that will be skipped for attention computation.

    For sliding window, this corresponds to the tokens that are prior to
    the current sliding window.

    Example:
    sliding_window=4, num_computed_tokens=7

    Tokens:   [ 0  1  2  3  4  5  6  7 ]
              | ---- computed -----|
                                     ^ next token to be computed
                           |-----------| sliding window for next token
              |--skipped---|

    The current window contains tokens 4~7. Tokens 0~3 will be skipped for
    attention computation since they are outside the sliding window.
    Thus, get_num_skipped_tokens(7) == 4.

    Args:
        num_computed_tokens: The number of tokens that have been computed.

    Returns:
        The number of tokens that will be skipped for attention computation.
    """
    return max(0, num_computed_tokens - self.sliding_window + 1)

get_manager_for_kv_cache_spec(kv_cache_spec, max_in_flight_tokens, max_model_len, **kwargs)

Get the appropriate manager for a given KVCacheSpec.

Uses the KVCacheSpecRegistry to look up the manager class, supporting both built-in and custom specs registered via @register_kv_cache_spec and KVCacheSpecRegistry.register.

Parameters:

  • kv_cache_spec

    (KVCacheSpec) –

    The KVCacheSpec instance

  • max_in_flight_tokens

    (int) –

    The max tokens scheduled but not yet settled (one batch per concurrent step); see VllmConfig.max_in_flight_tokens

  • max_model_len

    (int) –

    The maximum context length the model could serve

Returns: An instance of the appropriate SingleTypeKVCacheManager subclass

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def get_manager_for_kv_cache_spec(
    kv_cache_spec: KVCacheSpec,
    max_in_flight_tokens: int,
    max_model_len: int,
    **kwargs,
) -> SingleTypeKVCacheManager:
    """
    Get the appropriate manager for a given KVCacheSpec.

    Uses the KVCacheSpecRegistry to look up the manager class, supporting
    both built-in and custom specs registered via @register_kv_cache_spec
    and KVCacheSpecRegistry.register.

    Args:
        kv_cache_spec: The KVCacheSpec instance
        max_in_flight_tokens: The max tokens scheduled but not yet settled
            (one batch per concurrent step); see `VllmConfig.max_in_flight_tokens`
        max_model_len: The maximum context length the model could serve
    Returns:
        An instance of the appropriate SingleTypeKVCacheManager subclass
    """
    manager_class = KVCacheSpecRegistry.get_manager_class(kv_cache_spec)
    assert manager_class is not None, (
        f"No manager registered for KVCacheSpec {type(kv_cache_spec)}"
    )
    # SlidingWindow / ChunkedLocalAttention managers recycle blocks;
    # the runtime admission cap must match the recycling-aware bound the
    # startup pool sizer uses (single source of truth: the spec method).
    # R-SWA also recycles gap blocks but peak physical KV still fits the
    # full-attention bound (prefix + window <= max_model_len), so it inherits
    # FullAttentionSpec sizing without a separate admission cap.
    if isinstance(
        kv_cache_spec,
        (SlidingWindowSpec, ChunkedLocalAttentionSpec),
    ):
        kwargs["max_admission_blocks_per_request"] = (
            kv_cache_spec.max_admission_blocks_per_request(
                max_in_flight_tokens=max_in_flight_tokens,
                max_model_len=max_model_len,
            )
        )
    manager = manager_class(kv_cache_spec, **kwargs)
    return manager

register_all_kvcache_specs(vllm_config)

Built-in spec registration

Source code in vllm/v1/core/single_type_kv_cache_manager.py
def register_all_kvcache_specs(vllm_config):
    """Built-in spec registration"""
    KVCacheSpecRegistry.register(
        FullAttentionSpec,
        FullAttentionManager,
        uniform_type_base_spec=FullAttentionSpec,
    )

    KVCacheSpecRegistry.register(
        SlidingWindowSpec,
        SlidingWindowManager,
        uniform_type_base_spec=SlidingWindowSpec,
    )
    KVCacheSpecRegistry.register(
        SlidingWindowMLASpec,
        SlidingWindowManager,
        uniform_type_base_spec=SlidingWindowMLASpec,
    )

    KVCacheSpecRegistry.register(
        MambaSpec, MambaManager, uniform_type_base_spec=MambaSpec
    )
    KVCacheSpecRegistry.register(
        ChunkedLocalAttentionSpec,
        ChunkedLocalAttentionManager,
        uniform_type_base_spec=ChunkedLocalAttentionSpec,
    )
    KVCacheSpecRegistry.register(
        CrossAttentionSpec,
        CrossAttentionManager,
        uniform_type_base_spec=CrossAttentionSpec,
    )

    # FullAttentionSpec subclasses — grouped with FullAttentionSpec
    KVCacheSpecRegistry.register(
        TQFullAttentionSpec,
        FullAttentionManager,
        uniform_type_base_spec=FullAttentionSpec,
    )
    KVCacheSpecRegistry.register(
        MLAAttentionSpec, FullAttentionManager, uniform_type_base_spec=FullAttentionSpec
    )
    KVCacheSpecRegistry.register(
        RSWASpec, RSWAManager, uniform_type_base_spec=FullAttentionSpec
    )
    # NOTE(Mengqing): HiddenStateCacheSpec won't take part in
    # grouping, thus the uniform_type_base_spec is just a
    # placeholder.
    KVCacheSpecRegistry.register(
        HiddenStateCacheSpec,
        FullAttentionManager,
        uniform_type_base_spec=FullAttentionSpec,
    )
    KVCacheSpecRegistry.register(
        SinkFullAttentionSpec,
        SinkFullAttentionManager,
        uniform_type_base_spec=FullAttentionSpec,
    )

    from vllm.platforms import current_platform

    current_platform.register_custom_kv_cache_specs(vllm_config)