v1/core/kv_cache_manager.py: KVCacheManager and KVCacheBlocks
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.
| File | vllm/v1/core/kv_cache_manager.py, 922 lines of Python. Two classes. |
|---|---|
| Main classes | KVCacheBlocks (vllm/v1/core/kv_cache_manager.py:35) and KVCacheManager (vllm/v1/core/kv_cache_manager.py:131). |
| Runs in | The EngineCore process, main thread, inside the scheduler. CPU only: it keeps Python objects, never GPU tensors. |
| Called by | Scheduler (vllm/v1/core/sched/scheduler.py) and AsyncScheduler; KV connectors through bind_kv_cache_manager. |
| Calls | A KVCacheCoordinator (kv_cache_coordinator.py), and through it the SingleTypeKVCacheManagers and the BlockPool. |
| Not in this file | The 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#
Who calls it#
Scheduler.__init__builds it (vllm/v1/core/sched/scheduler.py:329-347).- The waiting pass calls
get_computed_blocksthrough_get_local_prefix_cache_hit(vllm/v1/core/sched/scheduler.py:589-599), andallocate_slotswith the hit (vllm/v1/core/sched/scheduler.py:1320-1337). - The running pass calls
allocate_slotswith new tokens only (vllm/v1/core/sched/scheduler.py:834-838). _free_request_blockscallsfreeorpop_blocks_for_free(vllm/v1/core/sched/scheduler.py:2866-2875).AsyncSchedulercallscache_blockswhen sampled tokens arrive (vllm/v1/core/sched/async_scheduler.py:75-79).make_statsandget_kv_cache_usagereadusageandmake_prefix_cache_stats(vllm/v1/core/sched/scheduler.py:2709-2711,vllm/v1/core/sched/scheduler.py:3025).- A KV connector gets the manager after construction (
vllm/v1/core/sched/scheduler.py:359-361).
What it calls#
get_kv_cache_coordinator(vllm/v1/core/kv_cache_manager.py:167-180), which picksKVCacheCoordinatorNoPrefixCache,UnitaryKVCacheCoordinatororHybridKVCacheCoordinator(vllm/v1/core/kv_cache_coordinator.py:995-1038).- The coordinator methods
find_longest_cache_hit,get_num_blocks_to_allocate,allocate_new_computed_blocks,allocate_new_blocks,cache_blocks,remove_skipped_blocks,free(vllm/v1/core/kv_cache_coordinator.py:190-473). BlockPooldirectly forusage, events and evictions (vllm/v1/core/kv_cache_manager.py:197,vllm/v1/core/kv_cache_manager.py:234).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
KVCacheBlocks | vllm/v1/core/kv_cache_manager.py:34-54 | the result type: blocks[i][j] = group i, block j |
KVCacheBlocks.get_block_ids | vllm/v1/core/kv_cache_manager.py:81-107 | ids per group; None with allow_none when all groups are empty |
KVCacheManager.__init__ | vllm/v1/core/kv_cache_manager.py:132-224 | coordinator, block pool, watermark, the shared empty result |
usage | vllm/v1/core/kv_cache_manager.py:226-234 | the share of blocks in use (0.0-1.0) |
make_prefix_cache_stats, record_prefix_cache_stats | vllm/v1/core/kv_cache_manager.py:236-262 | queries and hits for the prefix-cache metrics |
get_computed_blocks | vllm/v1/core/kv_cache_manager.py:264-321 | the longest prefix-cache hit of a new request |
get_computed_blocks_for_connector | vllm/v1/core/kv_cache_manager.py:323-369 | the hit for hybrid Mamba models with a KV connector |
allocate_slots | vllm/v1/core/kv_cache_manager.py:371-618 | blocks for the new tokens; None if they do not fit |
free | vllm/v1/core/kv_cache_manager.py:620-629 | all blocks of a request back to the pool |
remove_skipped_blocks, pop_blocks_for_free | vllm/v1/core/kv_cache_manager.py:631-663 | sliding-window frees; deferred frees |
evict_blocks, reset_prefix_cache | vllm/v1/core/kv_cache_manager.py:665-689 | drop hashes (for connectors, RLHF weight updates, benchmarks) |
get_num_common_prefix_blocks | vllm/v1/core/kv_cache_manager.py:691-694 | deprecated: always zeros (cascade attention was removed) |
take_events | vllm/v1/core/kv_cache_manager.py:696-721 | KV cache events for publishers |
get_blocks, get_block_ids, get_block_ids_for_computed_tokens | vllm/v1/core/kv_cache_manager.py:723-749 | the block table of a request |
estimate_cached_tokens | vllm/v1/core/kv_cache_manager.py:751-780 | cached tokens of a request, for prefill stats |
cache_blocks | vllm/v1/core/kv_cache_manager.py:782-792 | hash new full blocks, only with prefix caching |
truncate_computed_blocks | vllm/v1/core/kv_cache_manager.py:800-831 | cut a hit at a block boundary |
take_new_block_ids, take_kv_cache_block_copies, ... | vllm/v1/core/kv_cache_manager.py:833-922 | per-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)
"""- Line 41. A tuple with one sequence per KV cache group. A model with only full attention has one group.
- Lines 43-48. The group is the outer level, because groups may get different block sizes.
- 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
)
)- 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). - Line 295. At most
num_tokens - 1tokens: the last prompt token must run to produce logits. The comment says that this can cost a whole block of recompute. - 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:
- Lines 474-482.
ValueErrorfor a call with nothing to do. - Lines 484-497. Take the new hit blocks, cut for groups that keep a longer hit.
- Lines 501-507.
num_local_computed_tokensandtotal_computed_tokens. - Lines 509-516. Watermark blocks, only for waiting or preempted requests while others run.
- Lines 522-539.
full_sequence_must_fit: the whole request must fit, orNone. - Lines 541-544.
num_tokens_need_slot, with the lookahead, capped atmax_model_len. - Lines 555-559. Free blocks outside the attention window, on the processed tokens only.
- Lines 561-579. Count the blocks, compare with the free blocks minus reservations, or
None. - Lines 581-593. Adopt the hit blocks and allocate blocks for connector tokens.
- Lines 595-600. Allocate the new blocks.
- Lines 602-616. Cache full blocks, unless a KV load is still running.
- 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 NoneThe 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 diveData flow: types in and out
| Value | Type | Where |
|---|---|---|
kv_cache_config | KVCacheConfig: num_blocks, tensors, groups | argument of __init__ (vllm/v1/core/kv_cache_manager.py:134) |
request.block_hashes | list[BlockHash], one per full block of hash_block_size | vllm/v1/request.py:225 |
return of get_computed_blocks | (KVCacheBlocks, int, int): hit blocks, hit tokens, shared-prefix boundary | vllm/v1/core/kv_cache_manager.py:264 |
return of allocate_slots | KVCacheBlocks | None: only the new blocks | vllm/v1/core/kv_cache_manager.py:385, vllm/v1/core/kv_cache_manager.py:618 |
get_block_ids() | tuple[list[int], ...], one list per group | vllm/v1/core/kv_cache_manager.py:81-107 |
usage | float in [0, 1] | vllm/v1/core/kv_cache_manager.py:226-234 |
Deep diveWorked 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.)
- Waiting pass.
get_computed_blocks→ 3 blocks, 48 tokens.allocate_slots(req, 13, 48, hit): needcdiv(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]. - Decode steps 1-3. Tokens 62-64 fit in block 40 (slots 48-63).
allocate_slotstakes 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).AsyncSchedulerhashes it later, when the output of step 3 arrives (vllm/v1/core/sched/async_scheduler.py:75-79). - Decode step 4. The request needs slot 65:
cdiv(65, 16) = 5blocks, 1 new. Block table[7, 3, 12, 40, 58]. - Stop. After 10 output tokens the request has 71 tokens, 70 of them computed.
freereturns[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 diveThe coordinator classes
| Class | Used when | Notes |
|---|---|---|
KVCacheCoordinatorNoPrefixCache | prefix caching off | any number of groups, even 0; the hit is always empty (vllm/v1/core/kv_cache_coordinator.py:476-521) |
UnitaryKVCacheCoordinator | prefix caching on, one group | asserts hash_block_size == block_size (vllm/v1/core/kv_cache_coordinator.py:524-591) |
HybridKVCacheCoordinator | prefix caching on, several groups | finds 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 diveError paths
| Situation | Result | Where |
|---|---|---|
allocate_slots with 0 new tokens and nothing to adopt | ValueError | vllm/v1/core/kv_cache_manager.py:474-482 |
| Not enough free blocks | returns None (no change) | vllm/v1/core/kv_cache_manager.py:538-539, vllm/v1/core/kv_cache_manager.py:577-579 |
| Negative watermark | AssertionError | vllm/v1/core/kv_cache_manager.py:207 |
| Prefix cache reset while blocks are in use | returns False (the pool logs a warning) | vllm/v1/core/kv_cache_manager.py:684-685 |
| KV event with an unknown group index | WARNING log, event kept without annotation | vllm/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).
Related concepts#
- The KV cache manager: blocks, the block pool and memory sizing: the ideas, memory sizing and the usage metric.
- Prefix caching in the engine core: hashes, lookup, caching, eviction and KV events: the hash chain and the hit search.
- The hybrid KV cache: specs, groups and one manager per layer type: several KV cache groups.
- v1/core/block_pool.py: BlockPool and the prefix-cache map: the pool under this manager.
- v1/core/sched/scheduler.py: the Scheduler: the caller.
Check yourself#
- 1What doeseasy
allocate_slotsreturn when the blocks do not fit, and what has it changed then?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, thenvllm/v1/core/kv_cache_manager.py:581-600). - 2Why doesmedium
get_computed_blocksnever return a hit for the whole prompt?Show answer
The engine must run at least the last prompt token to get logits, so the hit is at most
num_tokens - 1tokens (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. - 3A new request comes with a draft-token lookahead of 4 and a 30-token prompt; block size 16. How many slots and blocks doeshard
allocate_slotsreserve, and how many full blocks does it cache?Show answer
Slots:
min(0 + 30 + 4, max_model_len) = 34, socdiv(34, 16) = 3blocks (vllm/v1/core/kv_cache_manager.py:541-544). Cached: up tomin(30, request.num_tokens) = 30tokens, so 1 full block (vllm/v1/core/kv_cache_manager.py:612-616). The lookahead slots are allocated but never cached.