| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- # SPDX-License-Identifier: Apache-2.0
- # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
- from collections.abc import Mapping, Sequence
- from contextlib import AbstractContextManager, nullcontext
- from dataclasses import dataclass
- from typing import Any, cast
- import torch
- from vllm.config import VllmConfig, get_layers_from_vllm_config
- from vllm.model_executor.layers.attention import Attention
- from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
- from vllm.multimodal.inputs import MultiModalFeatureSpec
- from vllm.v1.attention.backend import (
- AttentionCGSupport,
- CommonAttentionMetadata,
- )
- from vllm.v1.kv_cache_interface import (
- AttentionSpec,
- KVCacheConfig,
- KVCacheSpec,
- UniformTypeKVCacheSpecs,
- is_dcp_replicated_spec,
- )
- from vllm.v1.worker.gpu.model_states.interface import ModelSpecificAttnMetadata
- from vllm.v1.worker.utils import (
- AttentionGroup,
- add_kv_sharing_layers_to_kv_cache_groups,
- allocate_kv_cache,
- bind_kv_cache,
- prepare_kernel_block_sizes,
- )
- @dataclass(frozen=True)
- class AttentionCGSupportInfo:
- min_cg_support: AttentionCGSupport = AttentionCGSupport.ALWAYS
- min_cg_attn_backend: str | None = None
- def narrow(
- self, support: AttentionCGSupport, backend: str | None
- ) -> "AttentionCGSupportInfo":
- """Return an info tightened by ``support`` if it is more restrictive.
- Lets attention groups built outside ``init_attn_backend`` (e.g.
- encoder-only layers) contribute to the runner's cudagraph decision.
- """
- if support.value < self.min_cg_support.value:
- return AttentionCGSupportInfo(support, backend)
- return self
- def get_kv_cache_spec(vllm_config: VllmConfig) -> dict[str, KVCacheSpec]:
- kv_cache_spec: dict[str, KVCacheSpec] = {}
- layer_type = cast(type[Any], AttentionLayerBase)
- attn_layers = get_layers_from_vllm_config(vllm_config, layer_type)
- for layer_name, attn_module in attn_layers.items():
- if getattr(attn_module, "kv_sharing_target_layer_name", None):
- # This layer will use KV cache of the sharing target layer.
- continue
- # Skip modules that don't need KV cache (eg encoder-only attention)
- if spec := attn_module.get_kv_cache_spec(vllm_config):
- if isinstance(spec, AttentionSpec):
- spec = attn_module.get_attn_backend().customize_spec(spec)
- kv_cache_spec[layer_name] = spec
- return kv_cache_spec
- def get_shared_kv_cache_layers(vllm_config: VllmConfig):
- attn_layers = get_layers_from_vllm_config(vllm_config, Attention)
- return {
- layer_name: kv_tgt_layer
- for layer_name, attn_module in attn_layers.items()
- if (kv_tgt_layer := attn_module.kv_sharing_target_layer_name)
- }
- def init_attn_backend(
- kv_cache_config: KVCacheConfig,
- vllm_config: VllmConfig,
- device: torch.device,
- active_layer_names: set[str] | None = None,
- ) -> tuple[list[list[AttentionGroup]], AttentionCGSupportInfo, list[int]]:
- # Phase 1: discover attention groups for each kv cache group.
- attn_groups: list[list[AttentionGroup]] = []
- # Add KV-sharing layers to their target's kv cache group so they are
- # discovered alongside the target layer in Phase 1 below.
- add_kv_sharing_layers_to_kv_cache_groups(
- get_shared_kv_cache_layers(vllm_config), kv_cache_config.kv_cache_groups
- )
- # Phase 1: discover attention groups for each kv cache group.
- for kv_cache_group_id, kv_cache_group_spec in enumerate(
- kv_cache_config.kv_cache_groups
- ):
- layer_names = kv_cache_group_spec.layer_names
- if active_layer_names is not None:
- layer_names = list(active_layer_names.intersection(layer_names))
- layer_type = cast(type[Any], AttentionLayerBase)
- attn_layers = get_layers_from_vllm_config(vllm_config, layer_type, layer_names)
- group_map: dict[tuple[tuple[str, str], KVCacheSpec, int], AttentionGroup] = {}
- group_order: list[tuple[tuple[str, str], KVCacheSpec, int]] = []
- for layer_name in layer_names:
- attn_backend = attn_layers[layer_name].get_attn_backend()
- layer_kv_cache_spec: KVCacheSpec = kv_cache_group_spec.kv_cache_spec
- if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
- layer_kv_cache_spec = layer_kv_cache_spec.kv_cache_specs[layer_name]
- # Split on per-rank num_heads_q so layers with different Q-head
- # counts (e.g. a spec-decode draft head and its target) get separate
- # metadata builders.
- num_heads_q = getattr(attn_layers[layer_name], "num_heads", 0)
- key = (attn_backend.full_cls_name(), layer_kv_cache_spec, num_heads_q)
- if key not in group_map:
- group_map[key] = AttentionGroup(
- attn_backend, [layer_name], layer_kv_cache_spec, kv_cache_group_id
- )
- group_order.append(key)
- else:
- group_map[key].layer_names.append(layer_name)
- attn_groups.append([group_map[key] for key in group_order])
- # Phase 2: pick a kernel block size per kv cache group that is supported
- # by all backends within that group.
- kernel_block_sizes = prepare_kernel_block_sizes(kv_cache_config, attn_groups)
- # Phase 3: create metadata builders and determine cudagraph support.
- attn_backend_workspace: torch.Tensor | None = None
- for kv_cache_group_id, groups in enumerate(attn_groups):
- kernel_block_size = None
- if kv_cache_group_id < len(kernel_block_sizes):
- kernel_block_size = kernel_block_sizes[kv_cache_group_id]
- for group in groups:
- group.create_metadata_builders(
- vllm_config=vllm_config,
- device=device,
- kernel_block_size=kernel_block_size,
- num_metadata_builders=1,
- )
- builder = group.get_metadata_builder(0)
- if attn_backend_workspace is None:
- if hasattr(builder, "_get_workspace_buffer"):
- attn_backend_workspace = builder._get_workspace_buffer()
- else:
- if hasattr(builder, "set_workspace_buffer"):
- builder.set_workspace_buffer(attn_backend_workspace)
- attn_cg_support_info = get_attn_cg_support(attn_groups, vllm_config)
- return attn_groups, attn_cg_support_info, kernel_block_sizes
- def get_attn_cg_support(
- attn_groups: list[list[AttentionGroup]],
- vllm_config: VllmConfig,
- checked_layer_names: set[str] | None = None,
- ) -> AttentionCGSupportInfo:
- """Return the weakest CUDA graph support among the checked layers."""
- min_cg_support = AttentionCGSupport.ALWAYS
- min_cg_attn_backend = None
- for groups in attn_groups:
- for group in groups:
- if checked_layer_names is not None and checked_layer_names.isdisjoint(
- group.layer_names
- ):
- continue
- builder = group.get_metadata_builder(0)
- cg_support = builder.get_cudagraph_support(
- vllm_config,
- group.kv_cache_spec,
- )
- if cg_support.value < min_cg_support.value:
- min_cg_support = cg_support
- min_cg_attn_backend = group.backend.__name__
- return AttentionCGSupportInfo(
- min_cg_support=min_cg_support,
- min_cg_attn_backend=min_cg_attn_backend,
- )
- def get_query_lens_mismatch_unsupported_backend(
- attn_groups: list[list[AttentionGroup]],
- checked_layer_names: set[str] | None = None,
- ) -> str | None:
- """Name the first backend needing the CPU query lengths to be exact, if any.
- The attention selector already excludes these when adaptive verification is
- enabled, but models that hard-wire their backend never consult it. See
- AttentionBackend.supports_device_cpu_query_lens_mismatch().
- """
- for groups in attn_groups:
- for group in groups:
- if checked_layer_names is not None and checked_layer_names.isdisjoint(
- group.layer_names
- ):
- continue
- if not group.backend.supports_device_cpu_query_lens_mismatch():
- return group.backend.__name__
- return None
- def init_kv_cache(
- runner_kv_caches: list[torch.Tensor | list[torch.Tensor]],
- forward_context: dict[str, Any],
- kv_cache_config: KVCacheConfig,
- device: torch.device,
- kernel_block_sizes: list[int],
- vllm_config: VllmConfig,
- kv_cache_allocation_context: AbstractContextManager | None = None,
- ) -> dict[str, Any]:
- allocation_context = kv_cache_allocation_context or nullcontext()
- with allocation_context:
- kv_caches = allocate_kv_cache(
- kv_cache_config,
- device,
- vllm_config.cache_config.get_resolved_kv_cache_layout(),
- kernel_block_sizes,
- )
- for layer_name, target in get_shared_kv_cache_layers(vllm_config).items():
- kv_caches[layer_name] = kv_caches[target]
- # Dual-attention models (e.g. LongCat-Flash) put two Attention modules per
- # decoder layer, so a layer name carries two integers (layer + module index).
- num_attn_module = (
- 2
- if vllm_config.model_config.hf_config.model_type
- in ("longcat_flash", "longcat_flash_ngram")
- else 1
- )
- bind_kv_cache(
- kv_caches,
- forward_context,
- runner_kv_caches,
- num_attn_module,
- kv_cache_groups=kv_cache_config.kv_cache_groups,
- )
- return kv_caches
- def build_slot_mappings_by_layer(
- slot_mappings: torch.Tensor, kv_cache_config: KVCacheConfig
- ) -> dict[str, torch.Tensor]:
- slot_mappings_by_layer: dict[str, torch.Tensor] = {}
- kv_cache_groups = kv_cache_config.kv_cache_groups
- for slot_mapping, kv_cache_group in zip(slot_mappings, kv_cache_groups):
- for layer_name in kv_cache_group.layer_names:
- slot_mappings_by_layer[layer_name] = slot_mapping
- return slot_mappings_by_layer
- def build_attn_metadata(
- attn_groups: list[list[AttentionGroup]],
- num_reqs: int,
- num_tokens: int,
- query_start_loc_gpu: torch.Tensor,
- query_start_loc_cpu: torch.Tensor,
- max_query_len: int,
- seq_lens: torch.Tensor,
- max_seq_len: int,
- block_tables: Sequence[torch.Tensor],
- slot_mappings: torch.Tensor,
- kv_cache_config: KVCacheConfig,
- seq_lens_cpu_upper_bound: torch.Tensor | None = None,
- dcp_local_seq_lens: torch.Tensor | None = None,
- positions: torch.Tensor | None = None,
- is_prefilling: torch.Tensor | None = None,
- mm_req_doc_ranges: dict[int, list[tuple[int, int]]] | None = None,
- model_specific_attn_metadata: ModelSpecificAttnMetadata | None = None,
- for_cudagraph_capture: bool = False,
- causal: bool | torch.Tensor | Mapping[int, bool] = True,
- rswa_prefix_lens: torch.Tensor | None = None,
- ) -> dict[str, Any]:
- seq_lens = seq_lens[:num_reqs]
- if dcp_local_seq_lens is not None:
- dcp_local_seq_lens = dcp_local_seq_lens[:num_reqs]
- if seq_lens_cpu_upper_bound is not None:
- seq_lens_cpu_upper_bound = seq_lens_cpu_upper_bound[:num_reqs]
- attn_metadata: dict[str, Any] = {}
- num_kv_cache_groups = len(kv_cache_config.kv_cache_groups)
- for i in range(num_kv_cache_groups):
- block_table = block_tables[i]
- slot_mapping = slot_mappings[i]
- # DCP-replicated groups hold every position on every rank: their
- # builders must see the full seq_lens, not this rank's shard.
- group_dcp_local_seq_lens = (
- None
- if is_dcp_replicated_spec(kv_cache_config.kv_cache_groups[i].kv_cache_spec)
- else dcp_local_seq_lens
- )
- # Per-group causal for hybrid drafters (mixed SWA/full attention).
- group_causal = (
- causal if isinstance(causal, (bool, torch.Tensor)) else causal.get(i, True)
- )
- common_attn_metadata_extra_kwargs = (
- model_specific_attn_metadata.get_extra_common_attn_kwargs(i, num_reqs)
- if model_specific_attn_metadata is not None
- else {}
- )
- # Model-specific metadata (e.g. Mamba hybrid) may supply its own
- # padding-aware is_prefilling, which takes precedence over the default.
- group_is_prefilling = common_attn_metadata_extra_kwargs.pop(
- "is_prefilling", is_prefilling
- )
- common_attn_metadata = CommonAttentionMetadata(
- query_start_loc=query_start_loc_gpu,
- query_start_loc_cpu=query_start_loc_cpu,
- seq_lens=seq_lens,
- seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
- max_seq_len=max_seq_len,
- num_reqs=num_reqs,
- num_actual_tokens=num_tokens,
- max_query_len=max_query_len,
- block_table_tensor=block_table,
- slot_mapping=slot_mapping,
- causal=group_causal,
- dcp_local_seq_lens=group_dcp_local_seq_lens,
- positions=positions,
- is_prefilling=group_is_prefilling,
- mm_req_doc_ranges=mm_req_doc_ranges,
- rswa_prefix_lens=rswa_prefix_lens,
- **common_attn_metadata_extra_kwargs,
- )
- for attn_group in attn_groups[i]:
- attn_metadata_builder = attn_group.get_metadata_builder(0)
- if for_cudagraph_capture:
- metadata = attn_metadata_builder.build_for_cudagraph_capture(
- common_attn_metadata
- )
- else:
- attn_metadata_extra_kwargs = (
- model_specific_attn_metadata.get_extra_attn_kwargs(
- attn_metadata_builder,
- num_reqs,
- )
- if model_specific_attn_metadata is not None
- else {}
- )
- metadata = attn_metadata_builder.build(
- common_prefix_len=0,
- common_attn_metadata=common_attn_metadata,
- **attn_metadata_extra_kwargs,
- )
- for layer_name in attn_group.layer_names:
- attn_metadata[layer_name] = metadata
- return attn_metadata
- def compute_mm_prefix_ranges(
- req_ids: list[str],
- mm_features: dict[str, list[MultiModalFeatureSpec]],
- sliding_window: int | None = None,
- ) -> dict[int, list[tuple[int, int]]]:
- """Compute PrefixLM bidirectional ranges for multimodal tokens.
- Ranges exceeding sliding_window are skipped to prevent early tokens
- from attending across the entire image span.
- """
- req_doc_ranges: dict[int, list[tuple[int, int]]] = {}
- for req_idx, req_id in enumerate(req_ids):
- image_doc_ranges = []
- for mm_feature in mm_features.get(req_id, ()):
- if mm_feature.modality not in ("image", "video"):
- continue
- for r in mm_feature.mm_position.extract_embeds_range():
- if sliding_window is not None and (r[1] - r[0] + 1) > sliding_window:
- continue
- image_doc_ranges.append(r)
- req_doc_ranges[req_idx] = image_doc_ranges
- return req_doc_ranges
|