main
py 840 lines 33 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import math
4 from collections import defaultdict
5 from collections.abc import Iterable, Mapping, Sequence
6 from dataclasses import dataclass, field
7 from itertools import product as iprod
8 from typing import Any
9
10 import numpy as np
11 import torch
12
13 from vllm.config import CacheConfig, VllmConfig
14 from vllm.logger import init_logger
15 from vllm.model_executor.layers.attention import Attention
16 from vllm.model_executor.layers.mamba.mamba_mixer2 import share_replayssm_ring_trackers
17 from vllm.model_executor.layers.utils import warmup_rocm_skinny_gemm_workspaces
18 from vllm.model_executor.models.interfaces import MultiModalEmbeddings
19 from vllm.model_executor.models.utils import extract_layer_index
20 from vllm.platforms import current_platform
21 from vllm.triton_utils import tl, triton
22 from vllm.utils.mem_utils import MemorySnapshot, format_gib
23 from vllm.utils.torch_utils import async_tensor_h2d
24 from vllm.v1.attention.backend import (
25 AttentionBackend,
26 AttentionMetadataBuilder,
27 MultipleOf,
28 )
29 from vllm.v1.core.kv_cache_utils import KVCacheBlockCopy
30 from vllm.v1.kv_cache_interface import (
31 AttentionSpec,
32 EncoderOnlyAttentionSpec,
33 KVCacheConfig,
34 KVCacheGroupSpec,
35 KVCacheLayout,
36 KVCacheSpec,
37 MambaSpec,
38 MLAAttentionSpec,
39 UniformTypeKVCacheSpecs,
40 create_kv_cache_views,
41 )
42 from vllm.v1.worker.block_table import get_block_table_width
43
44 logger = init_logger(__name__)
45
46
47 def raise_if_nan_logits(num_nans_in_logits: Mapping[str, int]) -> None:
48 if not any(num_nans_in_logits.values()):
49 return
50
51 corrupted_requests = {
52 req_id: num_nans
53 for req_id, num_nans in num_nans_in_logits.items()
54 if num_nans > 0
55 }
56 raise RuntimeError(f"NaNs detected in logits: {corrupted_requests}")
57
58
59 @triton.jit
60 def _zero_kv_blocks_kernel(
61 seg_addrs_ptr,
62 seg_block_strides_ptr,
63 seg_page_sizes_ptr,
64 block_ids_ptr,
65 BLOCK_SIZE: tl.constexpr,
66 ):
67 """Zero KV cache blocks across all segments in a single launch.
68
69 Each segment is a contiguous region of one block's data. Layer-compact
70 layouts have one segment per layer buffer; dimensions physically outside
71 the block dim (separate head groups under LHBNC) and virtual block splits
72 each get their own segment.
73
74 Segments may have different block strides and page sizes (e.g. packed
75 KV views or models with multiple KV cache groups like MLA + DSA
76 indexer). Each segment's block stride determines where a logical block
77 begins, while its page size determines how many elements are cleared.
78
79 seg_addrs_ptr holds absolute byte addresses (int64) for each segment,
80 allowing segments to live in different CUDA allocations.
81
82 Programs are mapped directly onto a 3-D grid as
83 (block_index, seg_index, chunk_index).
84 """
85 block_index = tl.program_id(0)
86 seg_index = tl.program_id(1)
87 chunk_index = tl.program_id(2)
88 block_stride_el = tl.load(seg_block_strides_ptr + seg_index)
89 page_size_el = tl.load(seg_page_sizes_ptr + seg_index)
90 chunk_offset = chunk_index.to(tl.int64) * BLOCK_SIZE
91 if chunk_offset >= page_size_el:
92 return
93 block_id = tl.load(block_ids_ptr + block_index)
94 seg_addr = tl.load(seg_addrs_ptr + seg_index)
95 ptr = tl.cast(seg_addr, tl.pointer_type(tl.int32))
96 block_offset = block_id.to(tl.int64) * block_stride_el.to(tl.int64)
97 cols = chunk_offset + tl.arange(0, BLOCK_SIZE).to(tl.int64)
98 tl.store(
99 ptr + block_offset + cols,
100 tl.zeros([BLOCK_SIZE], dtype=tl.int32),
101 mask=cols < page_size_el,
102 )
103
104
105 class KVBlockZeroer:
106 """Manages efficient zeroing of KV cache blocks via a Triton kernel.
107
108 Construct once after KV caches are allocated to precompute segment
109 addresses, then call :meth:`zero_block_ids` each step to zero
110 newly-allocated blocks.
111 """
112
113 def __init__(
114 self,
115 device: torch.device,
116 attn_groups_iter: Iterable["AttentionGroup"],
117 kernel_block_sizes: list[int],
118 static_forward_context: dict[str, Any],
119 num_blocks: int,
120 runner_only_attn_layers: set[str] | None = None,
121 ) -> None:
122 """Precompute the absolute-address table for the Triton zeroing kernel.
123
124 Each entry is the absolute byte address of a segment start on the
125 GPU, so segments in different CUDA allocations work correctly.
126
127 Per-layer views are standardized ``[B, H, N, C]`` with blocks at dim 0; dims
128 physically outside B (separate head groups under LHBNC) each get their own
129 segment. A segment's page spans everything inside its block, so under BHLNC it
130 also covers the block's other layers -- safe, since block IDs are global pool
131 indices and a newly allocated block owns its whole tile.
132
133 Block IDs from the scheduler reference logical blocks whose size
134 may differ from the kernel block size (virtual block splitting).
135 Each virtual block is represented as an independent segment so its
136 physical block stride and zeroed page span remain independent.
137
138 Only AttentionSpec layers are processed; Mamba layers are skipped.
139 """
140 self.device = device
141 self._meta: (
142 tuple[torch.Tensor, torch.Tensor, torch.Tensor, int, int, int] | None
143 ) = None
144
145 if runner_only_attn_layers is None:
146 runner_only_attn_layers = set()
147 # Overlaid layers (packed layouts) share a base address but may have
148 # different page sizes; keep the widest span per address so newly
149 # allocated blocks are fully zeroed for every overlaying group.
150 seen_ptrs: dict[int, int] = {}
151 seg_addrs: list[int] = []
152 seg_block_strides: list[int] = []
153 seg_page_sizes: list[int] = []
154
155 for group in attn_groups_iter:
156 spec = group.kv_cache_spec
157 if not isinstance(spec, AttentionSpec):
158 continue
159 if group.kv_cache_group_id >= len(kernel_block_sizes):
160 continue
161 kernel_bs = kernel_block_sizes[group.kv_cache_group_id]
162 assert spec.block_size % kernel_bs == 0
163 for layer_name in group.layer_names:
164 if layer_name in runner_only_attn_layers:
165 continue
166 kv = static_forward_context[layer_name].kv_cache
167 if not isinstance(kv, torch.Tensor):
168 continue
169 if kv.device.type != self.device.type:
170 continue
171 dp = kv.data_ptr()
172
173 assert kv.shape[0] % num_blocks == 0, (
174 f"{layer_name}: {kv.shape[0]} kernel blocks is not a "
175 f"multiple of {num_blocks} logical blocks"
176 )
177 ratio = kv.shape[0] // num_blocks
178
179 el = kv.element_size()
180 block_stride_bytes = kv.stride(0) * el
181 assert block_stride_bytes % 4 == 0
182 assert kv.shape[0] % ratio == 0
183 outer_dims = [
184 d
185 for d in range(1, kv.ndim)
186 if kv.stride(d) * el > block_stride_bytes
187 ]
188 outer_strides = [kv.stride(d) * el for d in outer_dims]
189 inner_dims = [d for d in range(1, kv.ndim) if d not in outer_dims]
190 kernel_page_bytes = el + sum(
191 (kv.shape[d] - 1) * kv.stride(d) * el for d in inner_dims
192 )
193 assert kernel_page_bytes % 4 == 0
194 logical_block_stride_bytes = block_stride_bytes * ratio
195 for outer in iprod(*(range(kv.shape[d]) for d in outer_dims)):
196 off_bytes = sum(i * s for i, s in zip(outer, outer_strides))
197 assert (dp + off_bytes) % 4 == 0
198 for virtual_index in range(ratio):
199 addr = dp + off_bytes + virtual_index * block_stride_bytes
200 if (idx := seen_ptrs.get(addr)) is not None:
201 assert (
202 seg_block_strides[idx]
203 == logical_block_stride_bytes // 4
204 )
205 seg_page_sizes[idx] = max(
206 seg_page_sizes[idx], kernel_page_bytes // 4
207 )
208 continue
209 seen_ptrs[addr] = len(seg_addrs)
210 seg_addrs.append(addr)
211 seg_block_strides.append(logical_block_stride_bytes // 4)
212 seg_page_sizes.append(kernel_page_bytes // 4)
213
214 if not seg_addrs:
215 self._meta = None
216 return
217
218 max_page_size_el = max(seg_page_sizes)
219 blk_size = min(1 << (max_page_size_el - 1).bit_length(), 1024)
220 self._meta = (
221 torch.tensor(seg_addrs, dtype=torch.uint64, device=self.device),
222 torch.tensor(seg_block_strides, dtype=torch.int64, device=self.device),
223 torch.tensor(seg_page_sizes, dtype=torch.int64, device=self.device),
224 (max_page_size_el + blk_size - 1) // blk_size,
225 blk_size,
226 len(seg_addrs),
227 )
228
229 def zero_block_ids(self, block_ids: list[int]) -> None:
230 """Zero the KV cache memory for the given block IDs."""
231 if not block_ids or self._meta is None:
232 return
233 (
234 seg_addrs,
235 seg_block_strides,
236 seg_page_sizes,
237 max_chunks,
238 blk_size,
239 n_segs,
240 ) = self._meta
241 n_blocks = len(block_ids)
242 idx = async_tensor_h2d(block_ids, device=self.device, dtype=torch.int64)
243 grid = (n_blocks, n_segs, max_chunks)
244 _zero_kv_blocks_kernel[grid](
245 seg_addrs,
246 seg_block_strides,
247 seg_page_sizes,
248 idx,
249 BLOCK_SIZE=blk_size,
250 )
251
252 def warmup(self, num_kv_blocks: int) -> None:
253 """JIT-compile the zeroing kernel before the first real request."""
254 if num_kv_blocks > 0:
255 self.zero_block_ids([0])
256
257
258 @dataclass
259 class AttentionGroup:
260 backend: type[AttentionBackend]
261 layer_names: list[str]
262 kv_cache_spec: KVCacheSpec
263 kv_cache_group_id: int
264 # When ubatching is enabled we will have a metadata builder for each ubatch
265 # so that if they use internal persistent buffers for cudagraphs, and they
266 # won't have to worry about conflicting with the other ubatches.
267 metadata_builders: list[AttentionMetadataBuilder] = field(
268 default_factory=lambda: []
269 )
270
271 def create_metadata_builders(
272 self,
273 vllm_config,
274 device,
275 kernel_block_size: int | None = None,
276 num_metadata_builders: int = 1,
277 ):
278 if kernel_block_size is None:
279 kv_cache_spec_builder = self.kv_cache_spec
280 elif (
281 isinstance(self.kv_cache_spec, MLAAttentionSpec)
282 and self.kv_cache_spec.storage_block_size is not None
283 ):
284 kv_cache_spec_builder = self.kv_cache_spec.copy_with_new_block_size(
285 self.kv_cache_spec.storage_block_size
286 )
287 else:
288 kv_cache_spec_builder = self.kv_cache_spec.copy_with_new_block_size(
289 kernel_block_size
290 )
291 builder_cls = self.backend.get_builder_cls()
292 builder_kwargs = {}
293 if builder_cls.requires_block_table_width:
294 max_num_blocks = self.kv_cache_spec.max_num_blocks_per_req(
295 vllm_config, vllm_config.model_config.max_model_len
296 )
297 builder_kwargs["block_table_width"] = get_block_table_width(
298 max_num_blocks, self.kv_cache_spec.block_size, kernel_block_size
299 )
300 self.metadata_builders = [
301 builder_cls(
302 kv_cache_spec_builder,
303 self.layer_names,
304 vllm_config,
305 device,
306 **builder_kwargs,
307 )
308 for _ in range(num_metadata_builders)
309 ]
310 if kernel_block_size is not None:
311 for builder in self.metadata_builders:
312 builder.set_kernel_block_size(kernel_block_size)
313
314 def get_metadata_builder(self, ubatch_id: int = 0) -> AttentionMetadataBuilder:
315 assert len(self.metadata_builders) > ubatch_id
316 return self.metadata_builders[ubatch_id]
317
318 @property
319 def supports_draft_decode_metadata_update(self) -> bool:
320 return self.get_metadata_builder().supports_draft_decode_metadata_update
321
322 def update_draft_decode_metadata(
323 self,
324 attn_metadata: Mapping[str, Any],
325 ) -> None:
326 metadata = attn_metadata[self.layer_names[0]]
327 self.get_metadata_builder().update_draft_decode_metadata(metadata)
328
329
330 def select_common_block_size(
331 kv_manager_block_size: int,
332 backends: list[type[AttentionBackend]],
333 ) -> int:
334 """Select a block size that is supported by all backends and is a factor of
335 kv_manager_block_size.
336
337 If kv_manager_block_size is supported by all backends, return it directly.
338 Otherwise, return the max supported size.
339
340 Args:
341 kv_manager_block_size: Block size of KV cache.
342 backends: List of attention backend classes.
343
344 Returns:
345 The selected block size.
346
347 Raises:
348 ValueError: If no valid block size found.
349
350 """
351
352 def block_size_is_supported(
353 backends: list[type[AttentionBackend]], block_size: int
354 ) -> bool:
355 """Check if the block size is supported by all backends."""
356 for backend in backends:
357 is_supported = False
358 for supported_size in backend.get_supported_kernel_block_sizes():
359 if isinstance(supported_size, int):
360 if block_size == supported_size:
361 is_supported = True
362 elif isinstance(supported_size, MultipleOf):
363 if block_size % supported_size.base == 0:
364 is_supported = True
365 else:
366 raise ValueError(f"Unknown supported size: {supported_size}")
367 if not is_supported:
368 return False
369 return True
370
371 if block_size_is_supported(backends, kv_manager_block_size):
372 return kv_manager_block_size
373
374 # MultipleOf constraints also accept the manager size if they accept a divisor.
375 # Any remaining candidate must therefore be an explicit size from a backend.
376 candidates = {
377 size
378 for backend in backends
379 for size in backend.get_supported_kernel_block_sizes()
380 if isinstance(size, int) and kv_manager_block_size % size == 0
381 }
382
383 for size in sorted(candidates, reverse=True):
384 if block_size_is_supported(backends, size):
385 return size
386 raise ValueError(
387 f"No common block size for {kv_manager_block_size} ("
388 + "; ".join(
389 f"{b.get_name()}: {b.get_supported_kernel_block_sizes()}" for b in backends
390 )
391 + ")."
392 )
393
394
395 def allocate_kv_cache(
396 kv_cache_config: KVCacheConfig,
397 device: torch.device,
398 layout: KVCacheLayout,
399 kernel_block_sizes: list[int] | None = None,
400 ) -> dict[str, torch.Tensor]:
401 """Allocate the KV cache and view it as ``[B, H, N, C]`` per layer.
402
403 Every KVCacheTensor places its layers in the same backing allocation: layer ``l`` of
404 block ``b`` starts at ``offset + l * layer_stride + b * block_stride``. Cache
405 groups overlay each other, so tensors may address the same bytes.
406 """
407 if not kv_cache_config.kv_cache_tensors:
408 return {}
409
410 sizes = {tensor.size for tensor in kv_cache_config.kv_cache_tensors}
411 assert len(sizes) == 1, "KV cache tensors must share one backing allocation."
412 raw_size = sizes.pop()
413 # wvSplitKrc's process-lifetime static workspaces (csrc/rocm/skinny_gemms.cu)
414 # are created lazily on the first qualifying GEMM. Force that now, before
415 # the giant backing allocation below: if one landed in this segment's
416 # rounding tail it would pin the whole segment at engine shutdown.
417 if current_platform.is_rocm():
418 warmup_rocm_skinny_gemm_workspaces(device)
419 # Pad to the page granularity MoRIIO needs to register the shared
420 # backing as a single RDMA memory region. Other platforms keep the
421 # exact-size allocation (see #53974), so anything reading
422 # storage.nbytes() has to tolerate the tail on ROCm alone.
423 page_size = 4096
424 buf_size = ((raw_size + page_size - 1) // page_size) * page_size
425 else:
426 buf_size = raw_size
427 buf = torch.zeros(buf_size, dtype=torch.int8, device=device)
428
429 kv_caches: dict[str, torch.Tensor] = {}
430 for tensor in kv_cache_config.kv_cache_tensors:
431 layer_name = tensor.layers[0]
432 group_id, group = next(
433 (group_id, group)
434 for group_id, group in enumerate(kv_cache_config.kv_cache_groups)
435 if layer_name in group.layer_names
436 )
437 spec = group.kv_cache_spec
438 if isinstance(spec, UniformTypeKVCacheSpecs):
439 spec = spec.kv_cache_specs[layer_name]
440
441 if not spec.has_layer_views:
442 kv_caches.update((name, buf) for name in tensor.layers)
443 continue
444
445 num_blocks = kv_cache_config.num_blocks_of(tensor)
446 kernel_block_size = None
447 if kernel_block_sizes is not None and group_id < len(kernel_block_sizes):
448 kernel_block_size = kernel_block_sizes[group_id]
449 if isinstance(spec, MLAAttentionSpec) and spec.storage_block_size is not None:
450 kernel_block_size = spec.storage_block_size
451
452 views = create_kv_cache_views(
453 buf,
454 spec,
455 num_blocks,
456 layout,
457 tensor,
458 kernel_block_size=kernel_block_size,
459 )
460 kv_caches.update(zip(tensor.layers, views))
461 return kv_caches
462
463
464 def prepare_kernel_block_sizes(
465 kv_cache_config: KVCacheConfig, attn_groups: list[list[AttentionGroup]]
466 ) -> list[int]:
467 """Generate kernel_block_sizes that matches each block_size.
468
469 For attention backends that support virtual block splitting,
470 use the supported block sizes from the backend.
471 For other backends (like Mamba), use the same block size (no splitting).
472
473 Args:
474 kv_cache_config: The KV cache configuration.
475 attn_groups: Attention groups indexed by KV cache group id.
476
477 Returns:
478 List of kernel block sizes for each cache group.
479
480 """
481 kernel_block_sizes = []
482 for kv_cache_gid, kv_cache_group in enumerate(kv_cache_config.kv_cache_groups):
483 kv_cache_spec = kv_cache_group.kv_cache_spec
484 if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
485 # All layers in the UniformTypeKVCacheSpecs have the same type,
486 # pick an arbitrary one to dispatch.
487 kv_cache_spec = next(iter(kv_cache_spec.kv_cache_specs.values()))
488 if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec):
489 continue
490 if not kv_cache_spec.has_layer_views:
491 kernel_block_sizes.append(kv_cache_spec.block_size)
492 elif isinstance(kv_cache_spec, AttentionSpec):
493 # This is an attention backend that supports virtual block splitting.
494 kv_manager_block_size = kv_cache_group.kv_cache_spec.block_size
495 group_backends = [g.backend for g in attn_groups[kv_cache_gid]]
496 selected_kernel_size = select_common_block_size(
497 kv_manager_block_size, group_backends
498 )
499 kernel_block_sizes.append(selected_kernel_size)
500 elif isinstance(kv_cache_spec, MambaSpec):
501 # This is likely Mamba or other non-attention cache, no splitting.
502 kernel_block_sizes.append(kv_cache_spec.block_size)
503 else:
504 raise NotImplementedError(
505 f"unknown kv cache spec {kv_cache_group.kv_cache_spec}"
506 )
507 return kernel_block_sizes
508
509
510 def sanity_check_mm_encoder_outputs(
511 mm_embeddings: MultiModalEmbeddings,
512 expected_num_items: int,
513 ) -> None:
514 """Perform sanity checks for the result of
515 [`vllm.model_executor.models.SupportsMultiModal.embed_multimodal`][].
516 """
517 assert isinstance(mm_embeddings, (list, tuple, torch.Tensor)), (
518 "Expected multimodal embeddings to be a list/tuple of 2D tensors, "
519 f"or a single 3D tensor, but got {type(mm_embeddings)} "
520 "instead. This is most likely due to incorrect implementation "
521 "of the model's `embed_multimodal` method."
522 )
523
524 assert len(mm_embeddings) == expected_num_items, (
525 "Expected number of multimodal embeddings to match number of "
526 f"input items: {expected_num_items}, but got {len(mm_embeddings)=} "
527 "instead. This is most likely due to incorrect implementation "
528 "of the model's `embed_multimodal` method."
529 )
530
531 assert all(e.ndim == 2 for e in mm_embeddings), (
532 "Expected multimodal embeddings to be a sequence of 2D tensors, "
533 f"but got tensors with shapes {[e.shape for e in mm_embeddings]} "
534 "instead. This is most likely due to incorrect implementation "
535 "of the model's `embed_multimodal` method."
536 )
537
538
539 def request_memory(
540 init_snapshot: MemorySnapshot,
541 cache_config: CacheConfig,
542 external_weight_memory: int = 0,
543 ) -> int:
544 """Calculate the amount of memory required by vLLM, then validate
545 that the current amount of free memory is sufficient for that.
546 """
547 requested_memory = math.ceil(
548 init_snapshot.total_memory * cache_config.gpu_memory_utilization
549 )
550
551 if external_weight_memory > 0:
552 engine_memory = requested_memory - external_weight_memory
553 if engine_memory <= 0:
554 raise ValueError(
555 f"Externally held weights on device {init_snapshot.device_} "
556 f"({format_gib(external_weight_memory)}/"
557 f"{format_gib(init_snapshot.total_memory)} GiB) exceed the "
558 "desired GPU memory utilization "
559 f"({cache_config.gpu_memory_utilization}, "
560 f"{format_gib(requested_memory)} GiB). Increase GPU memory "
561 "utilization or reduce GPU memory used by other processes."
562 )
563 if init_snapshot.free_memory < engine_memory:
564 raise ValueError(
565 f"Free memory on device {init_snapshot.device_} "
566 f"({format_gib(init_snapshot.free_memory)}/"
567 f"{format_gib(init_snapshot.total_memory)} GiB) on startup "
568 "is less than the engine's budget after excluding "
569 "external process's weights "
570 f"({cache_config.gpu_memory_utilization}, "
571 f"{format_gib(engine_memory)} GiB). Decrease GPU memory "
572 "utilization or reduce GPU memory used by other processes."
573 )
574 logger.info_once(
575 "Weights are held outside this process: of the %s GiB "
576 "utilization budget, %s GiB is externally held and "
577 "%s GiB remains for the engine's own allocations.",
578 format_gib(requested_memory),
579 format_gib(external_weight_memory),
580 format_gib(engine_memory),
581 )
582 return engine_memory
583
584 if init_snapshot.free_memory < requested_memory:
585 raise ValueError(
586 f"Free memory on device {init_snapshot.device_} "
587 f"({format_gib(init_snapshot.free_memory)}/"
588 f"{format_gib(init_snapshot.total_memory)} GiB) on startup "
589 f"is less than desired GPU memory utilization "
590 f"({cache_config.gpu_memory_utilization}, "
591 f"{format_gib(requested_memory)} GiB). Decrease GPU memory "
592 f"utilization or reduce GPU memory used by other processes."
593 )
594
595 return requested_memory
596
597
598 def add_kv_sharing_layers_to_kv_cache_groups(
599 shared_kv_cache_layers: dict[str, str],
600 kv_cache_groups: list[KVCacheGroupSpec],
601 runner_only_attn_layers: set[str] | None = None,
602 ) -> None:
603 """Sets up KV cache sharing by reusing the allocated KV caches in `kv_caches`
604 for layers that do not allocate its own KV cache, based on the mapping in
605 `shared_kv_cache_layers`. Adds these layers to the corresponding KV cache
606 group, which is needed to ensure that attention metadata is assigned later.
607
608 Args:
609 shared_kv_cache_layers: Layer pairings for cross-layer KV sharing.
610 If an Attention layer `layer_name` is in the keys of this dict, it
611 means this layer will perform attention using the keys and values
612 from the KV cache of `shared_kv_cache_layers[layer_name]`.
613 kv_cache_groups: The KV cache groups of the model.
614 runner_only_attn_layers: Attention layers handled by the runner only,
615 which are excluded from the KV cache groups.
616
617 """
618 if not shared_kv_cache_layers:
619 return
620
621 layer_to_kv_cache_group: dict[str, KVCacheGroupSpec] = {}
622 for kv_cache_group in kv_cache_groups:
623 for layer_name in kv_cache_group.layer_names:
624 layer_to_kv_cache_group[layer_name] = kv_cache_group
625
626 for layer_name, target_layer_name in shared_kv_cache_layers.items():
627 tgt_kv_cache_group = layer_to_kv_cache_group[target_layer_name]
628 tgt_kv_cache_group.layer_names.append(layer_name)
629
630 if runner_only_attn_layers is not None:
631 runner_only_attn_layers.add(layer_name)
632
633
634 def bind_kv_cache(
635 kv_caches: dict[str, torch.Tensor],
636 forward_context: dict[str, Attention],
637 runner_kv_caches: list[torch.Tensor],
638 num_attn_module: int = 1,
639 kv_cache_groups: Sequence[KVCacheGroupSpec] | None = None,
640 ) -> None:
641 """Bind the allocated KV cache to both ModelRunner and forward context so
642 that the KV cache can be used in the forward pass.
643
644 This function:
645 1) Fills the ModelRunner's kv cache list (`runner_kv_caches`) with
646 kv_caches.
647 2) Associates each attention layer in the `forward_context` with its
648 corresponding KV cache in kv_caches.
649
650 Args:
651 kv_caches: The allocated kv_caches with layer names as keys.
652 num_attn_module: Number of attention modules per layer entry.
653 forward_context: The global forward context containing all Attention
654 layers with layer names as keys.
655 runner_kv_caches: The kv_cache declared by ModelRunner.
656 kv_cache_groups: The KV cache groups of the model, used to resolve
657 layers that share a KV cache.
658
659 """
660 # Bind kv_caches to ModelRunner
661 assert len(runner_kv_caches) == 0
662
663 # Convert kv_caches dict to a list of tensors in the order of layer_index.
664 index2name = defaultdict(list)
665 for layer_name in kv_caches:
666 index2name[extract_layer_index(layer_name, num_attn_module)].append(layer_name)
667
668 for layer_index in sorted(index2name.keys()):
669 layer_names = index2name[layer_index]
670 if len(layer_names) > 1:
671 # One typical case is encoder-decoder model, e.g., bart.
672 # The cross attention and self attention in the same decoder layer
673 # has different layer_name but the same layer_index.
674
675 # TODO - analyze where runner_kv_caches is used and the right
676 # way to ensure it properly reflects multiple attention layers
677 # in the same decoder block.
678 current_platform.check_runner_kv_caches_multi_layer()
679 for layer_name in layer_names:
680 runner_kv_caches.append(kv_caches[layer_name])
681
682 bind_kv_cache_to_layers(
683 kv_caches, forward_context, num_attn_module, kv_cache_groups
684 )
685
686
687 def bind_kv_cache_to_layers(
688 kv_caches: dict[str, torch.Tensor],
689 forward_context: dict[str, Attention],
690 num_attn_module: int = 1,
691 kv_cache_groups: Sequence[KVCacheGroupSpec] | None = None,
692 ) -> None:
693 """Bind layer caches and share ReplaySSM trackers in model-layer order."""
694 # Bind kv_caches to forward context. Each layer's bind_kv_cache unpacks
695 # its raw allocation into the per-layer view(s) it needs (e.g. Mamba
696 # splits conv/ssm), so the kv_caches dict can hold a single tensor per
697 # layer for the KV connector to register.
698 for layer_name, kv_cache in kv_caches.items():
699 forward_context[layer_name].bind_kv_cache(kv_cache)
700
701 ordered_layer_names = sorted(
702 kv_caches, key=lambda name: extract_layer_index(name, num_attn_module)
703 )
704 share_replayssm_ring_trackers(ordered_layer_names, forward_context, kv_cache_groups)
705
706
707 def clear_layer_kv_caches(layers: Iterable[Any]) -> None:
708 """Detach the KV/state cache tensors installed by bind_kv_cache().
709
710 The model object can outlive the runner (e.g. LLMEngine's finalizer keeps
711 it reachable until engine deletion), so dropping the runner's references
712 alone does not release the KV cache memory on teardown paths.
713 """
714 for layer in layers:
715 if not hasattr(layer, "kv_cache"):
716 continue
717 kv_cache = layer.kv_cache
718 layer.kv_cache = torch.tensor([]) if isinstance(kv_cache, torch.Tensor) else []
719 # Clean up quantized KV cache scale views
720 # (int8_per_token_head, fp8_per_token_head)
721 if hasattr(layer, "impl"):
722 if hasattr(layer.impl, "_k_scale_cache"):
723 layer.impl._k_scale_cache = None
724 if hasattr(layer.impl, "_v_scale_cache"):
725 layer.impl._v_scale_cache = None
726
727
728 def copy_kv_cache_blocks_inplace(
729 kv_caches: Iterable[torch.Tensor],
730 num_blocks: int,
731 kv_cache_block_copies: Sequence[KVCacheBlockCopy],
732 ) -> None:
733 if not kv_cache_block_copies:
734 return
735
736 indices_np = np.array(kv_cache_block_copies, dtype=np.int64)
737 indices: torch.Tensor | None = None
738 seen: set[tuple[torch.device, int]] = set()
739 copied_storages: set[tuple[torch.device, int]] = set()
740 for cache in kv_caches:
741 # Layers sharing KV (cross-layer sharing) alias the same view; copy it
742 # once. data_ptr distinguishes per-layer views of a shared allocation.
743 key = (cache.device, cache.data_ptr())
744 if key in seen:
745 continue
746 seen.add(key)
747
748 if indices is None:
749 indices = async_tensor_h2d(indices_np, device=cache.device)
750 assert cache.device == indices.device
751 src, dst = indices.unbind(dim=1)
752
753 kernel_blocks_per_block, remainder = divmod(cache.shape[0], num_blocks)
754 assert remainder == 0, (
755 f"{cache.shape[0]} kernel blocks not divisible by "
756 f"{num_blocks} scheduler blocks"
757 )
758 storage = cache.untyped_storage()
759 storage_key = (cache.device, storage.data_ptr())
760 scheduler_block_stride = (
761 cache.stride(0) * cache.element_size() * kernel_blocks_per_block
762 )
763 if storage.nbytes() == num_blocks * scheduler_block_stride:
764 if storage_key in copied_storages:
765 continue
766 copied_storages.add(storage_key)
767 blocks = torch.empty(0, dtype=torch.uint8, device=cache.device)
768 blocks.set_(storage)
769 blocks = blocks.view(num_blocks, -1)
770 else:
771 # Fold virtual block splitting into the shape so that dim 0 counts
772 # scheduler blocks; unflatten of dim 0 is always a view.
773 blocks = cache.unflatten(0, (num_blocks, kernel_blocks_per_block))
774 blocks[dst] = blocks[src]
775
776
777 def is_uniform_query_len(num_reqs: int, num_tokens: int, max_query_len: int) -> bool:
778 """Whether every request in the batch has the same query length.
779
780 Shape test only; use ``get_uniform_decode_token_count`` to classify a
781 scheduled batch, since a prompt chunk can have a decode batch's shape.
782 """
783 return num_reqs > 0 and num_tokens == max_query_len * num_reqs
784
785
786 def get_uniform_decode_token_count(
787 num_reqs: int, num_tokens: int, max_query_len: int, decode_graph_eligible: bool
788 ) -> int | None:
789 """Per-request token count of a uniform decode batch, or None."""
790 if decode_graph_eligible and is_uniform_query_len(
791 num_reqs, num_tokens, max_query_len
792 ):
793 return max_query_len
794 return None
795
796
797 def is_residual_scattered_for_sp(
798 vllm_config: VllmConfig, num_input_tokens: int
799 ) -> bool:
800 """Check if the residual tensor is scattered for sequence parallelism.
801
802 The residual tensor is scattered across tensor parallel ranks when sequence
803 parallelism and tensor parallelism is enabled. SP is only supported in
804 full-graph compilation mode.
805 """
806 if not vllm_config.compilation_config.pass_config.enable_sp:
807 return False
808
809 tp = vllm_config.parallel_config.tensor_parallel_size
810
811 if tp == 1:
812 return False
813
814 assert (
815 vllm_config.compilation_config.use_inductor_graph_partition
816 or not vllm_config.compilation_config.splitting_ops
817 ), "Sequence parallelism requires full-graph compilation"
818
819 # When sequence parallelism is enabled, we always pad num_input_tokens
820 # to be a multiple of tensor_parallel_size (tp) earlier.
821 assert num_input_tokens % tp == 0
822
823 return True
824
825
826 @dataclass
827 class EncoderTimingStats:
828 """Per-request timing statistics for encoder forward pass."""
829
830 encoder_forward_secs: float = 0.0
831 """Time spent in vision encoder forward pass (seconds)."""
832
833 num_encoder_calls: int = 0
834 """Number of times encoder was called for this request."""
835
836 def to_dict(self) -> dict[str, float | int]:
837 return {
838 "encoder_forward_secs": self.encoder_forward_secs,
839 "num_encoder_calls": self.num_encoder_calls,
840 }