main
py 962 lines 40.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 from collections.abc import Sequence
6 from dataclasses import dataclass
7 from typing import Literal, overload
8
9 from vllm.distributed.kv_events import BlockStored, KVCacheEvent
10 from vllm.logger import init_logger
11 from vllm.utils.math_utils import cdiv
12 from vllm.v1.core.kv_cache_coordinator import (
13 HybridKVCacheCoordinator,
14 get_kv_cache_coordinator,
15 )
16 from vllm.v1.core.kv_cache_metrics import KVCacheMetricsCollector
17 from vllm.v1.core.kv_cache_utils import KVCacheBlock, KVCacheBlockCopy
18 from vllm.v1.core.single_type_kv_cache_manager import MambaManager
19 from vllm.v1.kv_cache_interface import (
20 AttentionSpec,
21 CrossAttentionSpec,
22 EncoderOnlyAttentionSpec,
23 KVCacheConfig,
24 MambaSpec,
25 get_kv_cache_spec_kind,
26 get_kv_cache_spec_sliding_window,
27 )
28 from vllm.v1.metrics.stats import PrefixCacheStats
29 from vllm.v1.request import Request, RequestStatus
30
31 logger = init_logger(__name__)
32
33
34 @dataclass
35 class KVCacheBlocks:
36 """The allocation result of KVCacheManager, work as the interface between
37 Scheduler and KVCacheManager, to hide KVCacheManager's internal data
38 structure from the Scheduler.
39 """
40
41 blocks: tuple[Sequence[KVCacheBlock], ...]
42 """
43 `blocks[i][j]` refers to the i-th kv_cache_group
44 and the j-th block of tokens.We don't use block of
45 tokens as the outer dimension because it assumes all
46 kv_cache_groups have the same number of blocks, which is true for now but
47 will be broken if we want to give different block_size to different
48 kv_cache_groups in the future.
49
50 Each single type KVCacheBlocks could be represented as:
51 - list[KVCacheBlock] for more than one KVCacheBlock
52 - an empty tuple for requests without KVCacheBlock
53 (a precomputed KVCacheBlocks is in KVCacheManager to avoid GC overhead)
54 """
55
56 def __add__(self, other: "KVCacheBlocks") -> "KVCacheBlocks":
57 """Adds two KVCacheBlocks instances."""
58 return KVCacheBlocks(
59 tuple(
60 list(itertools.chain(blk1, blk2))
61 for blk1, blk2 in zip(self.blocks, other.blocks)
62 )
63 )
64
65 @overload
66 def get_block_ids(
67 self,
68 allow_none: Literal[False] = False,
69 *,
70 group_ids: Sequence[int] | None = None,
71 ) -> tuple[list[int], ...]: ...
72
73 @overload
74 def get_block_ids(
75 self,
76 allow_none: Literal[True] = True,
77 *,
78 group_ids: Sequence[int] | None = None,
79 ) -> tuple[list[int], ...] | None: ...
80
81 def get_block_ids(
82 self,
83 allow_none: bool = False,
84 *,
85 group_ids: Sequence[int] | None = None,
86 ) -> tuple[list[int], ...] | None:
87 """Converts the KVCacheBlocks instance to block_ids.
88
89 Args:
90 allow_none: Return None when every selected group is empty.
91 group_ids: KV cache groups to include. Includes all groups by default.
92
93 Returns:
94 tuple[list[int], ...]: A tuple of lists where:
95 - the outer tuple corresponds to KV cache groups
96 - each inner list contains the block_ids of the blocks in that
97 group
98
99 """
100 groups = (
101 self.blocks
102 if group_ids is None
103 else tuple(self.blocks[group_id] for group_id in group_ids)
104 )
105 if allow_none and all(len(group) == 0 for group in groups):
106 return None
107 return tuple([blk.block_id for blk in group] for group in groups)
108
109 def get_unhashed_block_ids(self) -> list[int]:
110 """Get block_ids of unhashed blocks from KVCacheBlocks instance."""
111 assert len(self.blocks) == 1, "Only one group is supported"
112 return [block.block_id for block in self.blocks[0] if block.block_hash is None]
113
114 def get_unhashed_block_ids_all_groups(self) -> list[list[int]]:
115 """Get block_ids of unhashed blocks from KVCacheBlocks instance."""
116 # Skip padding blocks.
117 return [
118 [
119 block.block_id
120 for block in group
121 if block.block_hash is None and not block.is_null
122 ]
123 for group in self.blocks
124 ]
125
126 def new_empty(self) -> "KVCacheBlocks":
127 """Creates a new KVCacheBlocks instance with no blocks."""
128 return KVCacheBlocks(tuple(() for _ in range(len(self.blocks))))
129
130
131 class KVCacheManager:
132 def __init__(
133 self,
134 kv_cache_config: KVCacheConfig,
135 max_model_len: int,
136 scheduler_block_size: int,
137 hash_block_size: int,
138 max_in_flight_tokens: int | None = None,
139 enable_caching: bool = True,
140 use_eagle: bool = False,
141 num_prefill_lookahead: int = 0,
142 log_stats: bool = False,
143 enable_kv_cache_events: bool = False,
144 dcp_world_size: int = 1,
145 pcp_world_size: int = 1,
146 metrics_collector: KVCacheMetricsCollector | None = None,
147 watermark: float = 0.0,
148 enable_mamba_shared_prefix_checkpoint: bool = False,
149 ) -> None:
150 self.max_model_len = max_model_len
151 # When unset, fall back to `max_model_len` so the recycling-aware cap
152 # collapses to the prior (uncapped) admission behavior. The scheduler
153 # always supplies the real value at runtime.
154 if max_in_flight_tokens is None:
155 max_in_flight_tokens = max_model_len
156
157 self.enable_caching = enable_caching
158 self.enable_kv_cache_events = enable_kv_cache_events
159 self.use_eagle = use_eagle
160 self.log_stats = log_stats
161 self.metrics_collector = metrics_collector
162 # FIXME: make prefix cache stats conditional on log_stats. We still need
163 # this comment because when the log stats is enabled there are still
164 # potential configs we could expose in the future.
165 self.prefix_cache_stats = PrefixCacheStats() if log_stats else None
166
167 self.coordinator = get_kv_cache_coordinator(
168 kv_cache_config=kv_cache_config,
169 max_model_len=self.max_model_len,
170 max_in_flight_tokens=max_in_flight_tokens,
171 use_eagle=self.use_eagle,
172 enable_caching=self.enable_caching,
173 enable_kv_cache_events=enable_kv_cache_events,
174 dcp_world_size=dcp_world_size,
175 pcp_world_size=pcp_world_size,
176 scheduler_block_size=scheduler_block_size,
177 hash_block_size=hash_block_size,
178 metrics_collector=self.metrics_collector,
179 num_prefill_lookahead=num_prefill_lookahead,
180 )
181 # One predicate, read by both sides of the feature, so the scheduler
182 # cannot end a chunk at a junction the manager would refuse -- a refused
183 # junction costs a forward pass and displaces the block-boundary stop.
184 # Multi-module MTP is excluded because ``cache_blocks`` then hands the
185 # manager ``num_computed - num_reprefillable`` rather than the chunk end.
186 self.mamba_shared_prefix_checkpoint = (
187 enable_mamba_shared_prefix_checkpoint
188 and bool(self.coordinator.eagle_group_ids)
189 and self.coordinator.enable_partial_hash_hits
190 and self.coordinator.num_reprefillable_tokens == 0
191 )
192 if self.mamba_shared_prefix_checkpoint:
193 for manager in self.coordinator.single_type_managers:
194 if isinstance(manager, MambaManager):
195 manager.shared_prefix_checkpoint = True
196 self.num_kv_cache_groups = len(kv_cache_config.kv_cache_groups)
197 self.block_pool = self.coordinator.block_pool
198 self.retained_hit_group_ids = tuple(
199 manager.kv_cache_group_id
200 for manager in self.coordinator.single_type_managers
201 if manager.retains_longer_hit
202 )
203 self.kv_cache_config = kv_cache_config
204
205 # Watermark: minimum number of KV cache blocks to keep free when
206 # admitting waiting/preempted requests, to avoid frequent preemptions.
207 assert watermark >= 0.0, "watermark must be non-negative"
208 self.watermark_blocks = int(watermark * kv_cache_config.num_blocks)
209 self.kv_cache_event_metadata = tuple(
210 (
211 get_kv_cache_spec_kind(group.kv_cache_spec).value,
212 get_kv_cache_spec_sliding_window(group.kv_cache_spec),
213 )
214 for group in kv_cache_config.kv_cache_groups
215 )
216
217 # Pre-constructed KVCacheBlocks with no blocks, callers should use this
218 # via create_kv_cache_blocks instead of creating new ones to avoid GC
219 # overhead.
220 #
221 # We use nested tuples to ensure the empty KVCacheBlocks is immutable.
222 self.empty_kv_cache_blocks = KVCacheBlocks(
223 tuple(() for _ in range(self.num_kv_cache_groups))
224 )
225
226 @property
227 def usage(self) -> float:
228 """Get the KV cache usage.
229
230 Returns:
231 The KV cache usage (between 0.0 and 1.0).
232
233 """
234 return self.block_pool.get_usage()
235
236 def make_prefix_cache_stats(self) -> PrefixCacheStats | None:
237 """Get (and reset) the prefix cache stats.
238
239 Returns:
240 The current prefix caching stats, or None if logging is disabled.
241
242 """
243 if not self.log_stats:
244 return None
245 stats = self.prefix_cache_stats
246 self.prefix_cache_stats = PrefixCacheStats()
247 return stats
248
249 def prefix_cache_lookup_enabled(self, request: Request) -> bool:
250 """Whether a local prefix cache lookup may be run for this request."""
251 return self.enable_caching and not request.skip_reading_prefix_cache
252
253 def record_prefix_cache_stats(self, request: Request, num_hits: int) -> None:
254 # Don't count a request that skipped the cache lookup.
255 if not self.log_stats or not self.prefix_cache_lookup_enabled(request):
256 return
257 assert self.prefix_cache_stats is not None
258 self.prefix_cache_stats.record(
259 num_tokens=request.num_tokens,
260 num_hits=num_hits,
261 preempted=request.num_preemptions > 0,
262 )
263
264 def get_computed_blocks(self, request: Request) -> tuple[KVCacheBlocks, int, int]:
265 """Get the computed (cached) blocks for the request.
266 Note that the computed blocks must be full.
267
268 Args:
269 request: The request to get the computed blocks.
270
271 Returns:
272 A tuple containing:
273 - A list of blocks that are computed for the request.
274 - The number of computed tokens.
275 - ``shared_prefix_boundary``: the block-aligned token position of
276 a shared prefix that a sparse-retention group (Mamba / sliding
277 window) has not cached yet (Marconi-style APC), or 0 if none.
278 Pinned so sparse prefix-cache retention does not drop
279 the junction and defeat cross-request reuse.
280
281 """
282 # We skip finding the prefix cache hit when prefix caching is
283 # disabled or the request is marked as skipping kv cache read
284 # (which happens when the request requires prompt logprobs
285 # or calls a pooling model with all pooling).
286 if not self.prefix_cache_lookup_enabled(request):
287 return self.empty_kv_cache_blocks, 0, 0
288
289 # NOTE: When all tokens hit the cache, we must recompute the last token
290 # to obtain logits. Thus, set max_cache_hit_length to prompt_length - 1.
291 # This can trigger recomputation of an entire block, rather than just
292 # the single last token, because allocate_slots() requires
293 # num_computed_tokens to be block-size aligned. Removing this limitation
294 # could slightly improve performance in the future.
295 max_cache_hit_length = request.num_tokens - 1
296 computed_blocks, num_new_computed_tokens, num_uncached = (
297 self.coordinator.find_longest_cache_hit(
298 request.block_hashes, max_cache_hit_length
299 )
300 )
301
302 # When kv_cache_report_mode is "full", emit BlockStored events
303 # for the reused prefix cache blocks so that external consumers
304 # (e.g. gateway) can learn about them.
305 if (
306 num_new_computed_tokens > 0
307 and self.enable_kv_cache_events
308 and getattr(request, "kv_cache_report_mode", "incremental") == "full"
309 ):
310 self.coordinator.emit_cached_block_events(request, computed_blocks)
311
312 # The junction to pin is where the lagging sparse-retention group stops
313 # (``num_new_computed_tokens``) plus the uncached shared prefix -- i.e.
314 # the longest single-group hit. Sub-block gaps are left to the mask,
315 # which floors to the alignment boundary (a no-op there).
316 shared_prefix_boundary = (
317 num_new_computed_tokens + num_uncached if num_uncached else 0
318 )
319
320 blocks = self.create_kv_cache_blocks(computed_blocks)
321 return blocks, num_new_computed_tokens, shared_prefix_boundary
322
323 def get_computed_blocks_for_connector(
324 self, request: Request
325 ) -> tuple[KVCacheBlocks, int, int, bool]:
326 """Local prefix-cache lookup for a request scheduled with a KV connector.
327
328 Hybrid (Mamba + full-attention) models can have per-group prefix hits
329 diverge under block pressure: the full-attention tail may be evicted
330 while a deeper Mamba state survives, or vice versa. Report the
331 full-attention hit as the local prefix - the connector transfers the
332 remaining suffix and the Mamba state is transferred unconditionally by
333 nixl's ``_apply_prefix_caching`` - and flag when that hit ran deeper
334 than a lagging group. Such a hit only has a valid Mamba state at its
335 boundary if the connector supplies it, so the caller must fall back to
336 ``get_computed_blocks`` to reconcile when no external tokens are found.
337
338 Non-hybrid models and already-convergent hits use ``get_computed_blocks``.
339
340 Returns:
341 The ``get_computed_blocks`` triple (blocks, number of local computed
342 tokens, shared-prefix boundary) plus ``hit_diverged``.
343
344 """
345 coordinator = self.coordinator
346 if not (
347 self.kv_cache_config.has_mamba_layers
348 and isinstance(coordinator, HybridKVCacheCoordinator)
349 and coordinator.full_attention_group_id is not None
350 ):
351 return *self.get_computed_blocks(request), False
352
353 if not self.prefix_cache_lookup_enabled(request):
354 return self.empty_kv_cache_blocks, 0, 0, False
355
356 fa_group_id = coordinator.full_attention_group_id
357 computed, per_group_hits = coordinator.find_longest_cache_hit_per_group(
358 request.block_hashes, request.num_tokens - 1
359 )
360 if any(hit > per_group_hits[fa_group_id] for hit in per_group_hits):
361 # A lagging group hit deeper than full attention means its
362 # full-attention blocks were evicted; use the reconciled boundary
363 # that every group agrees on.
364 return *self.get_computed_blocks(request), False
365
366 num_local = per_group_hits[fa_group_id]
367 blocks = self.create_kv_cache_blocks(computed)
368 # Per-group lookups do not detect an uncached shared prefix (boundary 0).
369 return blocks, num_local, 0, min(per_group_hits) < num_local
370
371 def allocate_slots(
372 self,
373 request: Request,
374 num_new_tokens: int,
375 num_new_computed_tokens: int = 0,
376 new_computed_blocks: KVCacheBlocks | None = None,
377 num_lookahead_tokens: int = 0,
378 num_external_computed_tokens: int = 0,
379 delay_cache_blocks: bool = False,
380 num_encoder_tokens: int = 0,
381 full_sequence_must_fit: bool = False,
382 reserved_blocks: int = 0,
383 has_scheduled_reqs: bool = True,
384 ) -> KVCacheBlocks | None:
385 """Add slots for a request with new tokens to append.
386
387 Args:
388 request: The request to allocate slots.
389 num_new_tokens: The number of new tokens to be allocated and computed.
390 num_new_computed_tokens: The number of new computed tokens just
391 hitting the prefix caching, excluding external tokens.
392 new_computed_blocks: The cached blocks for the above new computed
393 tokens, grouped as a tuple by kv cache groups.
394 num_lookahead_tokens: The number of speculative tokens to allocate.
395 This is used by spec decode proposers with kv-cache such
396 as eagle.
397 num_external_computed_tokens: The number of tokens that their
398 KV caches are not cached by vLLM but cached by the connector.
399 delay_cache_blocks: Whether to skip caching the blocks. This is
400 used by P/D when allocating blocks used in a KV transfer
401 which will complete in a future step.
402 num_encoder_tokens: The number of encoder tokens to allocate for
403 cross-attention in encoder-decoder models(e.g., Whisper).
404 For decoder-only models, this should be 0.
405 full_sequence_must_fit: Only allocate blocks if the KV cache has enough
406 free blocks to hold the full sequence, accounting for prefix cache hits
407 and sliding window. Used as an admission gate to prevent over-admitting
408 requests when chunked prefill would otherwise only check the first chunk
409 reserved_blocks: Number of free blocks that must be left available for
410 other in-flight sequences to complete. The actual allocation is only
411 made if it fits within (free blocks - reserved_blocks). Used to gate
412 async KV-connector loads so their initial allocation cannot consume
413 blocks an already in-flight (prefilling) sequence is relying on.
414 has_scheduled_reqs: Whether any requests are already scheduled to run
415 this step, controls whether watermark is applied.
416
417 Blocks layout:
418 ```
419 ----------------------------------------------------------------------
420 | < comp > | < new_comp > | < ext_comp > | < new > | < lookahead > |
421 ----------------------------------------------------------------------
422 | < to be computed > |
423 ----------------------------------------------------------------------
424 | < to be allocated > |
425 ----------------------------------------------------------------------
426 | < to be cached (roughly, |
427 | details below)> |
428 ----------------------------------------------------------------------
429 | Prefix-cached tokens from either vLLM |
430 | or connector. Can be safely removed if |
431 | they are outside sliding window. |
432 ----------------------------------------------------------------------
433 | < cached by vLLM > | not cached by |
434 | vLLM, but |
435 | ref_cnt | ref_cnt not | cached by |
436 | increased| increased yet| connector |
437 ----------------------------------------------------------------------
438 ```
439
440 Abbreviations:
441
442 ```
443 comp = request.num_computed_tokens
444 new_comp = num_new_computed_tokens
445 = len(new_computed_blocks) * block_size
446 ext_comp = num_external_computed_tokens, cached by the connector
447 new = num_new_tokens, including unverified draft tokens
448 lookahead = num_lookahead_tokens
449 ```
450
451 NOTE: for new tokens which include both verified and unverified draft
452 tokens, we only cache the verified tokens (by capping the number at
453 `request.num_tokens`).
454
455 The allocation has three stages:
456 - Free unnecessary blocks in `comp` and check
457 if we have sufficient free blocks (return None if not).
458 - Handle prefix tokens (`comp + new_comp + ext_comp`):
459 - Free unnecessary blocks (e.g. outside sliding window)
460 - Allocate new blocks for `ext_comp` tokens inside
461 sliding window
462 - Allocate new blocks for tokens to be computed (`new + lookahead`)
463
464 Returns:
465 A list of new allocated blocks.
466
467 """
468 # A step may need no slots of its own while still adopting computed
469 # tokens: an async KV load, or a chunk that ends inside the replayed
470 # range of a hit (SWA bounded replay).
471 if (
472 num_new_tokens == 0
473 and num_external_computed_tokens == 0
474 and num_new_computed_tokens == 0
475 ):
476 raise ValueError(
477 "num_new_tokens must be greater than 0 when there are no "
478 "computed tokens to adopt"
479 )
480
481 if new_computed_blocks is not None:
482 new_computed_block_list = new_computed_blocks.blocks
483 if self.retained_hit_group_ids:
484 # Only retain the prefix covered by the local and external hits.
485 reused = num_new_computed_tokens + num_external_computed_tokens
486 groups = list(new_computed_block_list)
487 for group_id in self.retained_hit_group_ids:
488 manager = self.coordinator.single_type_managers[group_id]
489 groups[group_id] = groups[group_id][
490 : cdiv(reused, manager.block_size)
491 ]
492 new_computed_block_list = tuple(groups)
493 else:
494 new_computed_block_list = self.empty_kv_cache_blocks.blocks
495
496 # The number of computed tokens is the number of computed tokens plus
497 # the new prefix caching hits
498 num_local_computed_tokens = (
499 request.num_computed_tokens + num_new_computed_tokens
500 )
501 total_computed_tokens = min(
502 num_local_computed_tokens + num_external_computed_tokens,
503 self.max_model_len,
504 )
505
506 watermark_blocks = 0
507 # The watermark is applied to waiting/preempted requests only, and only
508 # when there's at least one request already scheduled.
509 if has_scheduled_reqs and request.status in (
510 RequestStatus.WAITING,
511 RequestStatus.PREEMPTED,
512 ):
513 watermark_blocks = self.watermark_blocks
514
515 # Matches the scheduler's own prefill boundary: `num_tokens - 1`
516 # extends it to resumed requests replaying their output tokens.
517 prefill_end = max(request.num_prompt_tokens, request.num_tokens - 1)
518
519 if full_sequence_must_fit:
520 # First check and fail if the full request sequence won't fit.
521 full_num_tokens = min(request.num_tokens, self.max_model_len)
522
523 num_blocks_to_allocate = self.coordinator.get_num_blocks_to_allocate(
524 request_id=request.request_id,
525 num_tokens=full_num_tokens,
526 new_computed_blocks=new_computed_block_list,
527 num_encoder_tokens=num_encoder_tokens,
528 total_computed_tokens=total_computed_tokens,
529 num_local_computed_tokens=num_local_computed_tokens,
530 num_tokens_main_model=full_num_tokens,
531 apply_admission_cap=True,
532 prefill_end=prefill_end,
533 )
534 required_blocks = num_blocks_to_allocate + watermark_blocks
535 if required_blocks > self.block_pool.get_num_free_blocks():
536 return None
537
538 num_tokens_main_model = total_computed_tokens + num_new_tokens
539 num_tokens_need_slot = min(
540 num_tokens_main_model + num_lookahead_tokens, self.max_model_len
541 )
542
543 # Free the blocks that are skipped during the attention computation
544 # (e.g., tokens outside the sliding window).
545 # We can do this even if we cannot schedule this request due to
546 # insufficient free blocks.
547 # Should call this function before allocating new blocks to reduce
548 # the number of evicted blocks.
549 # Free on the processed-token basis: in-flight steps' attention windows
550 # still read blocks below the optimistic boundary, and rejected spec
551 # tokens can roll it back.
552 self.coordinator.remove_skipped_blocks(
553 request.request_id,
554 max(0, total_computed_tokens - request.num_in_flight_tokens),
555 num_prompt_tokens=request.num_prompt_tokens,
556 )
557
558 num_blocks_to_allocate = self.coordinator.get_num_blocks_to_allocate(
559 request_id=request.request_id,
560 num_tokens=num_tokens_need_slot,
561 new_computed_blocks=new_computed_block_list,
562 num_encoder_tokens=num_encoder_tokens,
563 total_computed_tokens=num_local_computed_tokens
564 + num_external_computed_tokens,
565 num_local_computed_tokens=num_local_computed_tokens,
566 num_tokens_main_model=num_tokens_main_model,
567 prefill_end=prefill_end,
568 )
569
570 # Keep `reserved_blocks` free for other in-flight sequences, and an
571 # additional watermark of headroom for waiting/preempted admissions.
572 available_blocks = self.block_pool.get_num_free_blocks() - reserved_blocks
573 required_blocks = num_blocks_to_allocate + watermark_blocks
574 if required_blocks > available_blocks:
575 # Cannot allocate new blocks
576 return None
577
578 if (
579 new_computed_block_list is not self.empty_kv_cache_blocks.blocks
580 or num_external_computed_tokens > 0
581 ):
582 # Append the new computed blocks to the request blocks until now to
583 # avoid the case where the new blocks cannot be allocated.
584 self.coordinator.allocate_new_computed_blocks(
585 request_id=request.request_id,
586 new_computed_blocks=new_computed_block_list,
587 num_local_computed_tokens=num_local_computed_tokens,
588 num_external_computed_tokens=num_external_computed_tokens,
589 )
590
591 new_blocks = self.coordinator.allocate_new_blocks(
592 request.request_id,
593 num_tokens_need_slot,
594 num_tokens_main_model,
595 num_encoder_tokens,
596 )
597
598 # P/D: delay caching blocks if we have to recv from
599 # remote. Update state for locally cached blocks.
600 if delay_cache_blocks:
601 return self.create_kv_cache_blocks(new_blocks)
602
603 # NOTE(woosuk): We want to commit (cache) up to num_local_computed_tokens
604 # + num_external_computed_tokens + num_new_tokens, but must exclude
605 # "non-committable" tokens (e.g., draft tokens that could be rejected).
606 # Therefore, we cap the number at `request.num_tokens`, ensuring only
607 # "finalized" tokens are cached.
608 num_tokens_to_cache = min(
609 total_computed_tokens + num_new_tokens,
610 request.num_tokens,
611 )
612 self.coordinator.cache_blocks(request, num_tokens_to_cache)
613
614 return self.create_kv_cache_blocks(new_blocks)
615
616 def free(self, request: Request) -> None:
617 """Free the blocks allocated for the request.
618 We free the blocks in reverse order so that the tail blocks are evicted
619 first when caching is enabled.
620
621 Args:
622 request: The request to free the blocks.
623
624 """
625 self.coordinator.free(request.request_id)
626
627 def remove_skipped_blocks(
628 self,
629 request_id: str,
630 processed_computed_tokens: int,
631 num_prompt_tokens: int | None = None,
632 ) -> None:
633 """Remove the blocks that are no longer needed from `blocks` and replace
634 the removed blocks with null_block.
635
636 Args:
637 request_id: The request ID.
638 processed_computed_tokens: Computed-token prefix length covering
639 fully processed and committed tokens only (safe to free).
640 num_prompt_tokens: Optional prompt length for R-SWA gap eviction.
641
642 """
643 self.coordinator.remove_skipped_blocks(
644 request_id, processed_computed_tokens, num_prompt_tokens
645 )
646
647 def pop_blocks_for_free(self, request: Request) -> list[KVCacheBlock]:
648 """Pop the request's bookkeeping and return its blocks without
649 returning them to the block pool. The caller must eventually free
650 them in reverse order (so that tail blocks are evicted first).
651
652 Args:
653 request: The request to pop the blocks for.
654
655 Returns:
656 The request's blocks in allocation order.
657
658 """
659 return self.coordinator.pop_blocks_for_free(request.request_id)
660
661 def evict_blocks(self, block_ids: set[int]) -> None:
662 """Evict blocks from the prefix cache by their block IDs.
663
664 Args:
665 block_ids: Set of block IDs to evict from cache.
666
667 """
668 self.block_pool.evict_blocks(block_ids)
669
670 def reset_prefix_cache(self) -> bool:
671 """Reset prefix cache. This function may be used in RLHF
672 flows to invalidate prefix caching after the weights are updated,
673 or used for resetting prefix caching status for benchmarking.
674
675 Returns:
676 bool: True if the prefix cache is successfully reset,
677 False otherwise.
678
679 """
680 if not self.coordinator.reset_prefix_cache():
681 return False
682 if self.log_stats:
683 assert self.prefix_cache_stats is not None
684 self.prefix_cache_stats.reset = True
685 return True
686
687 def get_num_common_prefix_blocks(self, running_request_id: str) -> list[int]:
688 """Calculate the number of common prefix blocks for each kv cache group.
689
690 The function selects a running request and iterates through its blocks.
691 A block is considered a common prefix block if ALL requests with
692 allocated KV cache share it (i.e., ref_cnt equals the number of entries
693 in req_to_blocks).
694
695 NOTE(woosuk): The number of requests with allocated KV cache is **greater
696 than or equal to** the number of requests scheduled in the current step.
697 This is because having allocated KV cache only indicates that:
698 1. The request has not yet finished, and
699 2. The request holds its blocks unfreed.
700
701 While all scheduled requests must have allocated KV cache, the inverse
702 is not necessarily true. There may be requests with allocated KV cache
703 that are not scheduled in the current step.
704
705 This can result in an edge case where the number of common prefix blocks
706 is 0, even though all scheduled requests share a common prefix. This
707 occurs because there may be unscheduled requests that do not share the
708 common prefix. Currently, this case cannot be easily detected, so the
709 function returns 0 in such cases.
710
711 Args:
712 running_request_id: The request ID of any running request, used to
713 identify the common prefix blocks.
714
715 Returns:
716 list[int]: The number of common prefix blocks for each kv cache
717 group.
718
719 """
720 return self.coordinator.get_num_common_prefix_blocks(running_request_id)
721
722 def take_events(self) -> list[KVCacheEvent]:
723 """Take the KV cache events from the block pool.
724
725 Returns:
726 A list of KV cache events.
727
728 """
729 events = self.block_pool.take_events()
730 for event in events:
731 if not isinstance(event, BlockStored):
732 continue
733 if event.group_idx is None:
734 continue
735 if event.group_idx < 0 or event.group_idx >= len(
736 self.kv_cache_event_metadata
737 ):
738 logger.warning(
739 "Group index `%s` not in KV cache metadata", event.group_idx
740 )
741 continue
742 # Annotate here so BlockPool can keep emitting structural cache
743 # events without owning semantic KV cache spec metadata.
744 kind, sliding_window = self.kv_cache_event_metadata[event.group_idx]
745 event.kv_cache_spec_kind = kind
746 event.kv_cache_spec_sliding_window = sliding_window
747 return events
748
749 def get_blocks(self, request_id: str) -> KVCacheBlocks:
750 """Get the blocks of a request."""
751 return self.create_kv_cache_blocks(self.coordinator.get_blocks(request_id))
752
753 def get_block_ids(self, request_id: str) -> tuple[list[int], ...]:
754 """Get the block ids of a request."""
755 return self.get_blocks(request_id).get_block_ids()
756
757 def get_block_ids_for_computed_tokens(
758 self,
759 request_id: str,
760 num_computed_tokens: int,
761 ) -> tuple[list[int], ...]:
762 """Get block ids covering the request's computed tokens."""
763 block_ids = self.get_block_ids(request_id)
764 clipped_block_ids: list[list[int]] = []
765 for group, ids in zip(self.kv_cache_config.kv_cache_groups, block_ids):
766 spec = group.kv_cache_spec
767 if not isinstance(spec, AttentionSpec) or isinstance(
768 spec, (CrossAttentionSpec, EncoderOnlyAttentionSpec)
769 ):
770 clipped_block_ids.append(ids)
771 continue
772
773 num_valid_blocks = cdiv(num_computed_tokens, spec.block_size)
774 clipped_block_ids.append(ids[:num_valid_blocks])
775 return tuple(clipped_block_ids)
776
777 def estimate_cached_tokens(self, request: Request) -> int:
778 """Estimate the number of tokens cached by the request."""
779 cached_tokens: int | None = None
780 for group, blocks in zip(
781 self.kv_cache_config.kv_cache_groups,
782 self.get_blocks(request.request_id).blocks,
783 ):
784 if not group.kv_cache_spec.prefix_cacheable:
785 continue
786 if isinstance(
787 group.kv_cache_spec,
788 (CrossAttentionSpec, EncoderOnlyAttentionSpec),
789 ):
790 # Cross-attention and encoder-only groups are not prefix cached.
791 continue
792
793 group_cached_tokens = 0
794 for block in blocks:
795 group_cached_tokens = max(
796 group_cached_tokens,
797 block.block_hash_num_tokens or 0,
798 )
799
800 cached_tokens = (
801 group_cached_tokens
802 if cached_tokens is None
803 else min(cached_tokens, group_cached_tokens)
804 )
805
806 return cached_tokens or 0
807
808 def cache_blocks(self, request: Request, num_computed_tokens: int) -> None:
809 """Cache the blocks for the request, if enabled.
810
811 Args:
812 request: The request to cache the blocks.
813 num_computed_tokens: The number of computed tokens, including tokens
814 that are already cached and tokens to be cached.
815
816 """
817 if self.enable_caching:
818 self.coordinator.cache_blocks(request, num_computed_tokens)
819
820 def create_kv_cache_blocks(
821 self, blocks: tuple[list[KVCacheBlock], ...]
822 ) -> KVCacheBlocks:
823 # Only create new KVCacheBlocks for non-empty blocks
824 return KVCacheBlocks(blocks) if any(blocks) else self.empty_kv_cache_blocks
825
826 def truncate_computed_blocks(
827 self, blocks: KVCacheBlocks, num_computed_tokens: int
828 ) -> KVCacheBlocks:
829 """Return a lookup-result view truncated at an aligned token endpoint.
830
831 An external hit can supply the final Mamba state even when the local
832 Mamba group ends before this endpoint. Other groups must cover it.
833 Pure slicing: refcounts are untouched and ``blocks`` is not mutated.
834 """
835 truncated: list[list[KVCacheBlock]] = []
836 for group_blocks, manager, group in zip(
837 blocks.blocks,
838 self.coordinator.single_type_managers,
839 self.kv_cache_config.kv_cache_groups,
840 strict=True,
841 ):
842 if not group.kv_cache_spec.prefix_cacheable:
843 # Scratch groups (e.g. the CSA compressor ring) hold a fixed
844 # block covering no token range, so a lookup result never
845 # carries computed blocks for them and their block size does
846 # not divide the endpoint.
847 assert not group_blocks
848 truncated.append([])
849 continue
850 assert num_computed_tokens % manager.block_size == 0
851 num_blocks = num_computed_tokens // manager.block_size
852 if isinstance(group.kv_cache_spec, MambaSpec):
853 num_blocks = min(num_blocks, len(group_blocks))
854 else:
855 assert num_blocks <= len(group_blocks)
856 truncated.append(list(group_blocks[:num_blocks]))
857 return self.create_kv_cache_blocks(tuple(truncated))
858
859 def take_new_block_ids(self) -> list[int]:
860 """Drain and return new attention block IDs for zeroing."""
861 ids: list[int] = []
862 for mgr in self.coordinator.single_type_managers:
863 ids.extend(mgr.take_new_block_ids())
864 return ids
865
866 def get_zeroing_block_ids_in_range(
867 self, request_id: str, start_token: int, end_token: int
868 ) -> list[int]:
869 """The request's block ids covering [start_token, end_token), from
870 the groups whose new blocks are zeroed by the worker."""
871 ids: list[int] = []
872 for mgr in self.coordinator.single_type_managers:
873 if mgr.records_new_block_ids:
874 start_idx = start_token // mgr.block_size
875 end_idx = cdiv(end_token, mgr.block_size)
876 blocks = mgr.req_to_blocks[request_id]
877 ids.extend(blk.block_id for blk in blocks[start_idx:end_idx])
878 return ids
879
880 def record_blocks_for_zeroing(self, request_id: str, start_token: int) -> None:
881 """Re-record the request's blocks from start_token onwards for
882 zeroing, e.g. blocks a failed async KV load left unwritten.
883
884 start_token must be block-aligned: zeroing a partially-valid block
885 would wipe its valid prefix.
886 """
887 for mgr in self.coordinator.single_type_managers:
888 if mgr.records_new_block_ids:
889 assert start_token % mgr.block_size == 0
890 start_idx = start_token // mgr.block_size
891 blocks = mgr.req_to_blocks[request_id]
892 mgr.new_block_ids.extend(blk.block_id for blk in blocks[start_idx:])
893
894 def take_kv_cache_block_copies(
895 self,
896 ) -> tuple[list[KVCacheBlockCopy], list[KVCacheBlock]]:
897 """Drain pending copies and return their retained endpoints."""
898 pending_copies: list[tuple[KVCacheBlock, KVCacheBlock]] = []
899 for mgr in self.coordinator.single_type_managers:
900 pending_copies.extend(mgr.take_pending_cow_copies())
901 copies = [
902 KVCacheBlockCopy(
903 src_block_id=source_block.block_id,
904 dst_block_id=cow_block.block_id,
905 )
906 for source_block, cow_block in pending_copies
907 ]
908 retained_blocks = [block for pair in pending_copies for block in pair]
909 return copies, retained_blocks
910
911 def take_boundary_state_offloads(
912 self,
913 ) -> dict[str, list[tuple[int, int, int]]]:
914 """Drain this step's boundary-state hand-offs for a KV connector.
915
916 Returns ``{request_id: [(group_id, block_id, boundary_tokens), ...]}``
917 for mamba "align" boundary states: the
918 request's committed boundary-state snapshots and, on a sub-block partial
919 hit, the CoW copy of its last-prompt-boundary state. Only mamba "align"
920 groups contribute; empty otherwise. A connector reads the referenced
921 blocks — never resolving them positionally — and offloads them so a
922 later request can hit that prefix.
923 """
924 offloads: dict[str, list[tuple[int, int, int]]] = {}
925 for mgr in self.coordinator.single_type_managers:
926 for (
927 req_id,
928 group_id,
929 block,
930 boundary_tokens,
931 ) in mgr.take_pending_boundary_state_offloads():
932 offloads.setdefault(req_id, []).append(
933 (group_id, block.block_id, boundary_tokens)
934 )
935 return offloads
936
937 def finalize_partial_tail_offloads(
938 self, request: Request
939 ) -> list[tuple[int, int, int]]:
940 """Consume safe producer partial tails when a request finishes.
941
942 A mamba align table block is a valid boundary source only if no later
943 token was forwarded. The connector pins and queues the exact table
944 block before request cleanup, then releases the pin when every worker
945 reports the store job complete.
946 """
947 offloads: list[tuple[int, int, int]] = []
948 for mgr in self.coordinator.single_type_managers:
949 finalized = mgr.finalize_partial_tail_offload(
950 request.request_id,
951 request.num_computed_tokens,
952 request.num_in_flight_tokens,
953 )
954 if finalized is None:
955 continue
956 group_id, block, boundary_tokens = finalized
957 offloads.append((group_id, block.block_id, boundary_tokens))
958 return offloads
959
960 def new_step_starts(self) -> None:
961 """Notify the coordinator that a new step is starting."""
962 self.coordinator.new_step_starts()