vLLM Explained

v1/core/kv_cache_manager.py: KVCacheManager and KVCacheBlocks

AdvancedCode mapabout 7 min read + 3 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/v1/core/kv_cache_manager.py gives the scheduler one small API for KV blocks, and hides the coordinator, the per-group managers and the block pool behind it.

Filevllm/v1/core/kv_cache_manager.py, 922 lines of Python. Two classes.
Main classesKVCacheBlocks (vllm/v1/core/kv_cache_manager.py:35) and KVCacheManager (vllm/v1/core/kv_cache_manager.py:131).
Runs inThe EngineCore process, main thread, inside the scheduler. CPU only: it keeps Python objects, never GPU tensors.
Called byScheduler (vllm/v1/core/sched/scheduler.py) and AsyncScheduler; KV connectors through bind_kv_cache_manager.
CallsA KVCacheCoordinator (kv_cache_coordinator.py), and through it the SingleTypeKVCacheManagers and the BlockPool.
Not in this fileThe block hash and the free queue (kv_cache_utils.py), the hit search per attention type (single_type_kv_cache_manager.py), the pool (block_pool.py), the start-up sizing (kv_cache_planning.py).

The page The KV cache manager: blocks, the block pool and memory sizing explains the ideas with a figure. This page reads the file.

Where it sits#

Figure: the layers under KVCacheManager, one allocate_slots call, and free (the same figure as on The KV cache manager: blocks, the block pool and memory sizing).
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLineWhat it does
KVCacheBlocksvllm/v1/core/kv_cache_manager.py:34-54the result type: blocks[i][j] = group i, block j
KVCacheBlocks.get_block_idsvllm/v1/core/kv_cache_manager.py:81-107ids per group; None with allow_none when all groups are empty
KVCacheManager.__init__vllm/v1/core/kv_cache_manager.py:132-224coordinator, block pool, watermark, the shared empty result
usagevllm/v1/core/kv_cache_manager.py:226-234the share of blocks in use (0.0-1.0)
make_prefix_cache_stats, record_prefix_cache_statsvllm/v1/core/kv_cache_manager.py:236-262queries and hits for the prefix-cache metrics
get_computed_blocksvllm/v1/core/kv_cache_manager.py:264-321the longest prefix-cache hit of a new request
get_computed_blocks_for_connectorvllm/v1/core/kv_cache_manager.py:323-369the hit for hybrid Mamba models with a KV connector
allocate_slotsvllm/v1/core/kv_cache_manager.py:371-618blocks for the new tokens; None if they do not fit
freevllm/v1/core/kv_cache_manager.py:620-629all blocks of a request back to the pool
remove_skipped_blocks, pop_blocks_for_freevllm/v1/core/kv_cache_manager.py:631-663sliding-window frees; deferred frees
evict_blocks, reset_prefix_cachevllm/v1/core/kv_cache_manager.py:665-689drop hashes (for connectors, RLHF weight updates, benchmarks)
get_num_common_prefix_blocksvllm/v1/core/kv_cache_manager.py:691-694deprecated: always zeros (cascade attention was removed)
take_eventsvllm/v1/core/kv_cache_manager.py:696-721KV cache events for publishers
get_blocks, get_block_ids, get_block_ids_for_computed_tokensvllm/v1/core/kv_cache_manager.py:723-749the block table of a request
estimate_cached_tokensvllm/v1/core/kv_cache_manager.py:751-780cached tokens of a request, for prefill stats
cache_blocksvllm/v1/core/kv_cache_manager.py:782-792hash new full blocks, only with prefix caching
truncate_computed_blocksvllm/v1/core/kv_cache_manager.py:800-831cut a hit at a block boundary
take_new_block_ids, take_kv_cache_block_copies, ...vllm/v1/core/kv_cache_manager.py:833-922per-step lists for the worker: blocks to zero, copy-on-write copies, Mamba hand-offs

Walk through the main methods#

KVCacheBlocks: the result type#

@dataclass
class KVCacheBlocks:
    """The allocation result of KVCacheManager, work as the interface between
    Scheduler and KVCacheManager, to hide KVCacheManager's internal data
    structure from the Scheduler.
    """

    blocks: tuple[Sequence[KVCacheBlock], ...]
    """
    `blocks[i][j]` refers to the i-th kv_cache_group
    and the j-th block of tokens.We don't use block of
    tokens as the outer dimension because it assumes all
    kv_cache_groups have the same number of blocks, which is true for now but
    will be broken if we want to give different block_size to different
    kv_cache_groups in the future.

    Each single type KVCacheBlocks could be represented as:
    - list[KVCacheBlock] for more than one KVCacheBlock
    - an empty tuple for requests without KVCacheBlock
      (a precomputed KVCacheBlocks is in KVCacheManager to avoid GC overhead)
    """
  1. Line 41. A tuple with one sequence per KV cache group. A model with only full attention has one group.
  2. Lines 43-48. The group is the outer level, because groups may get different block sizes.
  3. Lines 50-53. An empty result is a tuple of empty tuples, made once and shared, to save garbage collection work.

get_computed_blocks: the prefix-cache hit#

        if not self.prefix_cache_lookup_enabled(request):
            return self.empty_kv_cache_blocks, 0, 0

        # NOTE: When all tokens hit the cache, we must recompute the last token
        # to obtain logits. Thus, set max_cache_hit_length to prompt_length - 1.
        # This can trigger recomputation of an entire block, rather than just
        # the single last token, because allocate_slots() requires
        # num_computed_tokens to be block-size aligned. Removing this limitation
        # could slightly improve performance in the future.
        max_cache_hit_length = request.num_tokens - 1
        computed_blocks, num_new_computed_tokens, num_uncached = (
            self.coordinator.find_longest_cache_hit(
                request.block_hashes, max_cache_hit_length
            )
        )
  1. Lines 286-287. No lookup when prefix caching is off, or when the request skips the cache read (for example a request with prompt logprobs) (vllm/v1/core/kv_cache_manager.py:249-251).
  2. Line 295. At most num_tokens - 1 tokens: the last prompt token must run to produce logits. The comment says that this can cost a whole block of recompute.
  3. Lines 296-300. The coordinator searches the hash chain. How the search works is on Prefix caching in the engine core: hashes, lookup, caching, eviction and KV events.

allocate_slots: count, then change#

The concept page reads this method in two parts (The KV cache manager: blocks, the block pool and memory sizing, Steps 3 and 4). The order of its stages, line by line:

  1. Lines 474-482. ValueError for a call with nothing to do.
  2. Lines 484-497. Take the new hit blocks, cut for groups that keep a longer hit.
  3. Lines 501-507. num_local_computed_tokens and total_computed_tokens.
  4. Lines 509-516. Watermark blocks, only for waiting or preempted requests while others run.
  5. Lines 522-539. full_sequence_must_fit: the whole request must fit, or None.
  6. Lines 541-544. num_tokens_need_slot, with the lookahead, capped at max_model_len.
  7. Lines 555-559. Free blocks outside the attention window, on the processed tokens only.
  8. Lines 561-579. Count the blocks, compare with the free blocks minus reservations, or None.
  9. Lines 581-593. Adopt the hit blocks and allocate blocks for connector tokens.
  10. Lines 595-600. Allocate the new blocks.
  11. Lines 602-616. Cache full blocks, unless a KV load is still running.
  12. Line 618. Return only the new blocks.
        num_blocks_to_allocate = self.coordinator.get_num_blocks_to_allocate(
            request_id=request.request_id,
            num_tokens=num_tokens_need_slot,
            new_computed_blocks=new_computed_block_list,
            num_encoder_tokens=num_encoder_tokens,
            total_computed_tokens=num_local_computed_tokens
            + num_external_computed_tokens,
            num_local_computed_tokens=num_local_computed_tokens,
            num_tokens_main_model=num_tokens_main_model,
            prefill_end=prefill_end,
        )

        # Keep `reserved_blocks` free for other in-flight sequences, and an
        # additional watermark of headroom for waiting/preempted admissions.
        available_blocks = self.block_pool.get_num_free_blocks() - reserved_blocks
        required_blocks = num_blocks_to_allocate + watermark_blocks
        if required_blocks > available_blocks:
            # Cannot allocate new blocks
            return None

The check uses reserved_blocks, which the scheduler sets only for asynchronous KV loads (vllm/v1/core/sched/scheduler.py:1305-1315).

free and usage#

    def free(self, request: Request) -> None:
        """Free the blocks allocated for the request.
        We free the blocks in reverse order so that the tail blocks are evicted
        first when caching is enabled.

        Args:
            request: The request to free the blocks.

        """
        self.coordinator.free(request.request_id)

The docstring describes the reverse order, but the reverse happens in the single-type manager (vllm/v1/core/single_type_kv_cache_manager.py:597-598). usage forwards to BlockPool.get_usage: 1 - free / (num_gpu_blocks - 1) (vllm/v1/core/block_pool.py:881-892).

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Data flow: types in and out

ValueTypeWhere
kv_cache_configKVCacheConfig: num_blocks, tensors, groupsargument of __init__ (vllm/v1/core/kv_cache_manager.py:134)
request.block_hasheslist[BlockHash], one per full block of hash_block_sizevllm/v1/request.py:225
return of get_computed_blocks(KVCacheBlocks, int, int): hit blocks, hit tokens, shared-prefix boundaryvllm/v1/core/kv_cache_manager.py:264
return of allocate_slotsKVCacheBlocks | None: only the new blocksvllm/v1/core/kv_cache_manager.py:385, vllm/v1/core/kv_cache_manager.py:618
get_block_ids()tuple[list[int], ...], one list per groupvllm/v1/core/kv_cache_manager.py:81-107
usagefloat in [0, 1]vllm/v1/core/kv_cache_manager.py:226-234
Deep dive

Worked example: one request from admission to free

Block size 16, one group, prefix caching on. The request of Tour 1: 61 prompt tokens, 48 of them hit in blocks 7, 3 and 12; the generation stops after 10 output tokens. (Block ids illustrative; derived from the code below.)

  1. Waiting pass. get_computed_blocks → 3 blocks, 48 tokens. allocate_slots(req, 13, 48, hit): need cdiv(61, 16) = 4, have 3, take 1: block 40. Cache up to 61: 3 full blocks, all cached already. Block table [7, 3, 12, 40].
  2. Decode steps 1-3. Tokens 62-64 fit in block 40 (slots 48-63). allocate_slots takes nothing. Step 3 computes token 64, so block 40 becomes full and gets its hash. The synchronous scheduler hashes it when it schedules step 3 (vllm/v1/core/kv_cache_manager.py:612-616). AsyncScheduler hashes it later, when the output of step 3 arrives (vllm/v1/core/sched/async_scheduler.py:75-79).
  3. Decode step 4. The request needs slot 65: cdiv(65, 16) = 5 blocks, 1 new. Block table [7, 3, 12, 40, 58].
  4. Stop. After 10 output tokens the request has 71 tokens, 70 of them computed. free returns [58, 40, 12, 3, 7] to the pool. Block 58 (6 tokens, no hash) goes to the front of the free queue. The others (hashed) go to the end, if no other request holds them.
Deep dive

The coordinator classes

ClassUsed whenNotes
KVCacheCoordinatorNoPrefixCacheprefix caching offany number of groups, even 0; the hit is always empty (vllm/v1/core/kv_cache_coordinator.py:476-521)
UnitaryKVCacheCoordinatorprefix caching on, one groupasserts hash_block_size == block_size (vllm/v1/core/kv_cache_coordinator.py:524-591)
HybridKVCacheCoordinatorprefix caching on, several groupsfinds a hit that every group agrees on (vllm/v1/core/kv_cache_coordinator.py:609); see The hybrid KV cache: specs, groups and one manager per layer type

All three share the base class, which makes the one BlockPool and the single-type managers (vllm/v1/core/kv_cache_coordinator.py:89-188).

Deep dive

Error paths

SituationResultWhere
allocate_slots with 0 new tokens and nothing to adoptValueErrorvllm/v1/core/kv_cache_manager.py:474-482
Not enough free blocksreturns None (no change)vllm/v1/core/kv_cache_manager.py:538-539, vllm/v1/core/kv_cache_manager.py:577-579
Negative watermarkAssertionErrorvllm/v1/core/kv_cache_manager.py:207
Prefix cache reset while blocks are in usereturns False (the pool logs a warning)vllm/v1/core/kv_cache_manager.py:684-685
KV event with an unknown group indexWARNING log, event kept without annotationvllm/v1/core/kv_cache_manager.py:709-715

The file reads no environment variable. Its inputs are KVCacheConfig and the arguments of the scheduler (prefix caching, watermark, speculative settings).

Check yourself#

  1. 1
    What does allocate_slots return when the blocks do not fit, and what has it changed then?
    easy
    Show answer

    None, and nothing has changed: both checks come before any touch or allocation (vllm/v1/core/kv_cache_manager.py:538-539, vllm/v1/core/kv_cache_manager.py:577-579, then vllm/v1/core/kv_cache_manager.py:581-600).

  2. 2
    Why does get_computed_blocks never return a hit for the whole prompt?
    medium
    Show answer

    The engine must run at least the last prompt token to get logits, so the hit is at most num_tokens - 1 tokens (vllm/v1/core/kv_cache_manager.py:289-295). Because hits are whole blocks, a prompt of exactly 64 tokens with all 4 blocks cached gets a hit of 48 tokens.

  3. 3
    A new request comes with a draft-token lookahead of 4 and a 30-token prompt; block size 16. How many slots and blocks does allocate_slots reserve, and how many full blocks does it cache?
    hard
    Show answer

    Slots: min(0 + 30 + 4, max_model_len) = 34, so cdiv(34, 16) = 3 blocks (vllm/v1/core/kv_cache_manager.py:541-544). Cached: up to min(30, request.num_tokens) = 30 tokens, so 1 full block (vllm/v1/core/kv_cache_manager.py:612-616). The lookahead slots are allocated but never cached.