| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608 |
- # SPDX-License-Identifier: Apache-2.0
- # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
- from __future__ import annotations
- import copy
- from collections import Counter
- from collections.abc import Collection, Sequence
- from dataclasses import dataclass, field, fields, replace
- from enum import Enum, IntEnum
- from fractions import Fraction
- from functools import cached_property
- from math import prod
- from typing import TYPE_CHECKING, TypeVar
- import torch
- from typing_extensions import Self
- from vllm.logger import init_logger
- from vllm.utils.math_utils import cdiv, round_up
- from vllm.utils.torch_utils import get_dtype_size
- from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
- from vllm.v1.kv_cache_layout import _DIM_B, _DIM_L, KVCacheLayout
- from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
- if TYPE_CHECKING:
- from vllm.config import VllmConfig
- logger = init_logger(__name__)
- _SpecT = TypeVar("_SpecT", bound="KVCacheSpec")
- # ---------------------------------------------------------------------------
- # KV cache quantization mode
- # ---------------------------------------------------------------------------
- class KVQuantMode(IntEnum):
- """KV cache quantization mode.
- Used by attention backends and kernels to dispatch quantization logic
- without string matching on ``kv_cache_dtype``.
- """
- NONE = 0
- FP8_PER_TENSOR = 1 # per-tensor scales (current fp8 path)
- INT8_PER_TOKEN_HEAD = 2 # per-token-head dynamic scales for int8
- FP8_PER_TOKEN_HEAD = 3 # per-token-head dynamic scales for fp8
- INT4_PER_TOKEN_HEAD = 4 # packed 2×int4/byte, RHT + asymmetric zp
- NVFP4 = 5 # packed fp4 data + fp8 block scales
- # Hadamard-rotated Lloyd-Max quant, packed K+V per slot.
- TURBOQUANT_K8V4 = 6
- TURBOQUANT_4BIT_NC = 7
- TURBOQUANT_K3V4_NC = 8
- TURBOQUANT_3BIT_NC = 9
- NVFP4_DS_MLA = 10 # opaque-bytes NVFP4 DS-MLA layouts (FlashMLA sparse)
- @property
- def is_per_token_head(self) -> bool:
- """True for any per-token-head quantization mode."""
- return self in (
- KVQuantMode.INT8_PER_TOKEN_HEAD,
- KVQuantMode.FP8_PER_TOKEN_HEAD,
- KVQuantMode.INT4_PER_TOKEN_HEAD,
- )
- @property
- def is_nvfp4(self) -> bool:
- """True for NVFP4 packed quantization mode."""
- return self == KVQuantMode.NVFP4
- @property
- def is_turboquant(self) -> bool:
- """True for any turboquant quantization mode."""
- return self in (
- KVQuantMode.TURBOQUANT_K8V4,
- KVQuantMode.TURBOQUANT_4BIT_NC,
- KVQuantMode.TURBOQUANT_K3V4_NC,
- KVQuantMode.TURBOQUANT_3BIT_NC,
- )
- def get_kv_quant_mode(kv_cache_dtype: str) -> KVQuantMode:
- """Map a ``kv_cache_dtype`` string to a :class:`KVQuantMode`."""
- if kv_cache_dtype == "int4_per_token_head":
- return KVQuantMode.INT4_PER_TOKEN_HEAD
- if kv_cache_dtype == "int8_per_token_head":
- return KVQuantMode.INT8_PER_TOKEN_HEAD
- if kv_cache_dtype == "fp8_per_token_head":
- return KVQuantMode.FP8_PER_TOKEN_HEAD
- # Must precede the ``nvfp4`` prefix test below, which would otherwise match.
- if kv_cache_dtype == "nvfp4_ds_mla":
- # Page size is keyed on cache_dtype_str in the MLA specs, not
- # nvfp4_kv_cache_full_dim.
- return KVQuantMode.NVFP4_DS_MLA
- if kv_cache_dtype.startswith("nvfp4"):
- return KVQuantMode.NVFP4
- if isinstance(kv_cache_dtype, str) and kv_cache_dtype.startswith("turboquant_"):
- return KVQuantMode[kv_cache_dtype.upper()]
- if isinstance(kv_cache_dtype, str) and kv_cache_dtype.startswith("fp8"):
- return KVQuantMode.FP8_PER_TENSOR
- return KVQuantMode.NONE
- def is_quantized_kv_cache(kv_cache_dtype: str) -> bool:
- return get_kv_quant_mode(kv_cache_dtype) != KVQuantMode.NONE
- def replace_as(
- spec: KVCacheSpec,
- target_cls: type[_SpecT],
- *,
- drop: Collection[str] = (),
- **changes,
- ) -> _SpecT:
- """``dataclasses.replace``, but rebuilding *spec* as *target_cls*
- e.g. ``SlidingWindowSpec`` -> ``FullAttentionSpec``
- Every field of *spec* must exist on *target_cls* unless named in *drop*;
- fields only *target_cls* has keep their default values.
- """
- kwargs = {
- f.name: getattr(spec, f.name)
- for f in fields(spec)
- if f.init and f.name not in drop
- }
- kwargs.update(changes)
- return target_cls(**kwargs)
- def kv_cache_uses_per_token_head_scales(kv_cache_dtype: str) -> bool:
- """Return True if *kv_cache_dtype* needs per-token-head scales."""
- return get_kv_quant_mode(kv_cache_dtype).is_per_token_head
- class KVCacheSpecKind(str, Enum):
- FULL_ATTENTION = "full_attention"
- MLA_ATTENTION = "mla_attention"
- SLIDING_WINDOW = "sliding_window"
- SLIDING_WINDOW_MLA = "sliding_window_mla"
- MAMBA = "mamba"
- CHUNKED_LOCAL_ATTENTION = "chunked_local_attention"
- SINK_FULL_ATTENTION = "sink_full_attention"
- ENCODER_ONLY_ATTENTION = "encoder_only_attention"
- CROSS_ATTENTION = "cross_attention"
- UNKNOWN = "unknown"
- class SparseCacheRole(str, Enum):
- SPARSE = "sparse"
- INDEXER = "indexer"
- @dataclass(frozen=True)
- class KVCacheSpec:
- """A base class for specifying the KV cache format of one layer."""
- # number of tokens in a block
- block_size: int
- dcp_sharded: bool = field(default=False, kw_only=True)
- """Whether DCP shards this cache's token positions across ranks."""
- block_stride_alignment: int | None = field(default=None, kw_only=True)
- """Required byte alignment between physical blocks, including packed layers."""
- def __post_init__(self):
- if self.block_stride_alignment is not None and self.block_stride_alignment <= 0:
- raise ValueError("block_stride_alignment must be positive")
- @property
- def prefix_cacheable(self) -> bool:
- """Whether this spec's group participates in prefix caching."""
- return True
- @property
- def prefix_replay_tokens(self) -> int:
- """DeepSeek-V4.1 only: bounded replay. Currently only for DSV41 SWA."""
- return 0
- @property
- def num_heads(self) -> int:
- raise NotImplementedError
- @property
- def tokens_per_state(self) -> int | Fraction:
- raise NotImplementedError
- @property
- def state_content_size_bytes(self) -> int:
- raise NotImplementedError
- @property
- def page_size_bytes(self) -> int:
- """The size of a page with `block_size` tokens in bytes.
- Returns:
- The page size
- """
- raise NotImplementedError
- @property
- def num_states(self) -> int:
- return self.get_num_kernel_states(self.block_size)
- def get_num_kernel_states(self, kernel_block_size: int) -> int:
- if self.tokens_per_state > 0:
- return kernel_block_size // self.tokens_per_state
- return 1
- @property
- def block_table_token_alignment(self) -> int | None:
- return 128
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- """The maximum possible memory usage of this KV cache in bytes.
- Returns:
- The KV cache size in bytes
- """
- raise NotImplementedError
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- """The number of block table entries needed per request, i.e. the row
- length of the worker-side block table for this cache group.
- Args:
- vllm_config: The vllm config.
- max_len: The maximum sequence length to size for, including the
- encoder length for encoder-decoder models.
- """
- return cdiv(max_len, self.block_size)
- def copy_with_new_block_size(self, block_size: int) -> Self:
- """Create a new KVCacheSpec from self but replacing the block size."""
- return replace(self, block_size=block_size)
- @classmethod
- def merge(cls, specs: list[Self]) -> Self:
- """Merge a list of KVCacheSpec objects into a single KVCacheSpec object."""
- if not all(spec == specs[0] for spec in specs[1:]):
- raise AssertionError(
- "All layers in the same KV cache group must be the same."
- )
- return copy.deepcopy(specs[0])
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- """Whether this KVCacheSpec is uniform with all specs of all layers."""
- uniform_type_base_spec = KVCacheSpecRegistry.get_uniform_type_base_spec(self)
- assert uniform_type_base_spec is not None, (
- f"Unsupported KV cache spec type: {type(self)}. "
- "Please register it using @register_kv_cache_spec decorator."
- )
- return all(
- isinstance(spec, uniform_type_base_spec) for spec in kv_cache_specs.values()
- )
- @property
- def has_layer_views(self) -> bool:
- """Whether generic allocation creates per-layer cache views.
- Specs without a per-layer shape keep the raw backing tensor and lay it
- out themselves. They also have no attention module of their own, so
- they take no part in attention-backend or kernel-block selection.
- """
- return True
- @property
- def uses_slot_mapping(self) -> bool:
- """Whether the worker computes a per-token slot mapping for this spec.
- Specs that address their pages themselves (raw storage, ring buffers)
- take no slot mapping row.
- """
- return True
- def group_kernel_blocks(cache: torch.Tensor, num_blocks: int) -> torch.Tensor:
- """View a kernel-block-granular layer cache with manager blocks as dim 0.
- Kernel block splitting subdivides each manager block into uniformly strided
- kernel blocks, so grouping is a pure view: ``(num_blocks * ratio, ...)``
- """
- if cache.shape[0] == num_blocks:
- return cache
- assert cache.shape[0] % num_blocks == 0
- return cache.unflatten(0, (num_blocks, -1))
- def compute_layer_kv_cache_shape_bytes(
- spec: KVCacheSpec,
- num_blocks: int,
- kernel_block_size: int | None = None,
- ) -> tuple[int, ...]:
- """Return the 4D logical shape ``(B, H, N, C)`` where C is in bytes."""
- bs = kernel_block_size if kernel_block_size is not None else spec.block_size
- assert spec.block_size % bs == 0, (
- f"Kernel block size {bs} must divide KV cache block size {spec.block_size}."
- )
- blocks_per_page = spec.block_size // bs
- return (
- num_blocks * blocks_per_page,
- spec.num_heads,
- spec.get_num_kernel_states(bs),
- spec.state_content_size_bytes,
- )
- def compute_layout_strides(
- spec: KVCacheSpec,
- num_blocks: int,
- num_layers: int,
- layout: KVCacheLayout,
- kernel_block_size: int | None = None,
- fixed_strides: tuple[int | None, ...] = (None,) * 5,
- ) -> tuple[int, ...]:
- """Byte strides in logical ``[L, B, H, N, C]`` axis order."""
- assert len(fixed_strides) == 5
- assert all(stride is None or stride > 0 for stride in fixed_strides)
- shape = (
- num_layers,
- *compute_layer_kv_cache_shape_bytes(spec, num_blocks, kernel_block_size),
- )
- order = layout.stride_order
- padded_page_size = getattr(spec, "page_size_padded", None)
- if padded_page_size is not None:
- assert kernel_block_size is None or kernel_block_size == spec.block_size, (
- "Padded KV pages do not support kernel block splitting."
- )
- page_grid_end = max(order.index(_DIM_L), order.index(_DIM_B)) + 1
- page_grid_shape = tuple(shape[dim] for dim in order[:page_grid_end])
- assert prod(page_grid_shape) == num_layers * num_blocks, (
- "Page padding requires dimensions outside the page tail to be L, B, "
- f"or singleton; got {layout.name} with shape {shape}."
- )
- strides = [0] * 5
- current_stride = 1
- for physical_idx, dim in reversed(tuple(enumerate(order))):
- if padded_page_size is not None and physical_idx == page_grid_end - 1:
- current_stride = max(current_stride, padded_page_size)
- assert current_stride % padded_page_size == 0
- strides[dim] = fixed_strides[dim] or current_stride
- current_stride = strides[dim] * shape[dim]
- return tuple(strides)
- def create_kv_cache_views(
- raw: torch.Tensor,
- spec: KVCacheSpec,
- num_blocks: int,
- layout: KVCacheLayout,
- kv_cache_tensor: KVCacheTensor,
- kernel_block_size: int | None = None,
- ) -> list[torch.Tensor]:
- """View a flat int8 buffer as one 4D ``[B, H, N, C]`` view per layer.
- Block ``b`` of layer ``l`` starts at the tensor offset plus its layer and
- block stride contributions.
- """
- num_layers = len(kv_cache_tensor.layers)
- layer_stride = kv_cache_tensor.layer_stride
- block_stride = kv_cache_tensor.block_stride
- shape_bytes = compute_layer_kv_cache_shape_bytes(
- spec, num_blocks, kernel_block_size
- )
- ratio = shape_bytes[0] // num_blocks
- if ratio > 1:
- # Kernel blocks subdivide a manager block into `ratio` equal pieces, so
- # they sit a constant stride apart only if a block is one dense page: no
- # padding at its end, and no other layer's page before the next block.
- dense_page_size = prod(compute_layer_kv_cache_shape_bytes(spec, 1)[1:])
- if block_stride != dense_page_size:
- raise ValueError(
- f"The resolved KV cache layout ({layout.name}) does not store "
- "blocks as dense, unpadded pages (block stride "
- f"{block_stride} != page {dense_page_size}), so a manager "
- f"block cannot be split into {ratio} kernel blocks of "
- f"{kernel_block_size} tokens. Reduce --block-size to "
- f"{kernel_block_size} or set VLLM_KV_CACHE_LAYOUT to a "
- "layer-compact layout (e.g. LBNHC)."
- )
- assert block_stride % ratio == 0, (
- f"Block stride {block_stride} must divide into {ratio} equal kernel blocks."
- )
- block_stride //= ratio
- logical_shape = (num_layers, *shape_bytes)
- strides = compute_layout_strides(
- spec,
- num_blocks,
- num_layers,
- layout,
- kernel_block_size,
- fixed_strides=(layer_stride, block_stride, None, None, None),
- )
- dtype = getattr(spec, "dtype", None)
- view_5d = torch.as_strided(
- raw,
- size=logical_shape,
- stride=strides,
- storage_offset=raw.storage_offset() + kv_cache_tensor.offset,
- )
- views = []
- for layer_idx in range(num_layers):
- cache_logical = view_5d[layer_idx]
- if dtype is not None:
- cache_logical = cache_logical.view(dtype)
- views.append(cache_logical)
- return views
- @dataclass(frozen=True, kw_only=True)
- class HiSparseHotSpec(KVCacheSpec):
- """Ephemeral per-request HiSparse hot-cache allocation."""
- page_size: int
- blocks_per_request: int
- @property
- def prefix_cacheable(self) -> bool:
- return False
- @property
- def page_size_bytes(self) -> int:
- return self.page_size
- @property
- def block_table_token_alignment(self) -> int | None:
- return None
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- return self.blocks_per_request * self.page_size
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- return self.blocks_per_request
- @property
- def has_layer_views(self) -> bool:
- return False
- @property
- def uses_slot_mapping(self) -> bool:
- return False
- @dataclass(frozen=True, kw_only=True)
- class HiSparseResidentSpec(KVCacheSpec):
- """Reclaimable GPU-resident pages for host-backed HiSparse KV."""
- page_size: int
- @property
- def prefix_cacheable(self) -> bool:
- return False
- @property
- def page_size_bytes(self) -> int:
- return self.page_size
- def max_admission_blocks_per_request(
- self, max_in_flight_tokens: int, max_model_len: int
- ) -> int:
- num_tokens = min(max_in_flight_tokens, max_model_len)
- return cdiv(num_tokens, self.block_size)
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- max_blocks = self.max_admission_blocks_per_request(
- max_in_flight_tokens=vllm_config.max_in_flight_tokens,
- max_model_len=vllm_config.model_config.max_model_len,
- )
- return max_blocks * self.page_size
- @property
- def has_layer_views(self) -> bool:
- return False
- @dataclass(frozen=True, kw_only=True)
- class AttentionSpec(KVCacheSpec):
- dcp_sharded: bool = True
- max_tp_shards: int | None = None
- """Distinct shards this cache splits into across TP; TP ranks beyond this
- hold replicas. None: unknown, treated as not replicated."""
- num_kv_heads: int
- head_size: int
- dtype: torch.dtype
- head_size_v: int = None # type: ignore[assignment]
- kv_quant_mode: KVQuantMode = KVQuantMode.NONE
- page_size_padded: int | None = None
- num_head_slots: int | None = None
- """H of the logical ``[B, H, N, C]`` page when packing diverges from one
- slot per KV head. None means one slot per KV head. Published by the backend.
- """
- state_content_bytes: int | None = None
- """C in bytes when packed; None means dense K/V content."""
- tokens_per_state: int | Fraction = 1
- """Tokens covered by one stored state. Ints > 1 compress multiple tokens
- into one state (DSv4 sparse MLA); fractions < 1 store multiple states per
- token (Whisper block pooling: ``Fraction(1, block_pool_size)``)."""
- def __post_init__(self):
- super().__post_init__()
- if self.head_size_v is None:
- object.__setattr__(self, "head_size_v", self.head_size)
- @property
- def num_heads(self) -> int:
- if self.num_head_slots is not None:
- return self.num_head_slots
- return self.num_kv_heads
- @property
- def state_content_size_bytes(self) -> int:
- """Bytes per (head slot, stored state) cell of the page."""
- if self.state_content_bytes is not None:
- return self.state_content_bytes
- return (self.head_size + self.head_size_v) * get_dtype_size(self.dtype)
- @property
- def unpadded_page_size_bytes(self) -> int:
- return self.num_heads * self.num_states * self.state_content_size_bytes
- @property
- def page_size_bytes(self) -> int:
- if self.page_size_padded is not None:
- assert self.page_size_padded >= self.unpadded_page_size_bytes
- return self.page_size_padded
- return self.unpadded_page_size_bytes
- @property
- def real_page_size_bytes(self) -> int:
- """Alias of ``unpadded_page_size_bytes``
- TODO(lucas): follow up with TPU backend to see if we can remove this property.
- """
- return self.unpadded_page_size_bytes
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- parallel_config = vllm_config.parallel_config
- kv_shard_count = (
- parallel_config.decode_context_parallel_size if self.dcp_sharded else 1
- )
- return cdiv(max_len, self.block_size * kv_shard_count)
- @dataclass(frozen=True, kw_only=True)
- class FullAttentionSpec(AttentionSpec):
- """When hybrid allocator is disabled and the model contains both full
- attention layers and sliding window attention layers, sliding
- window attention are regarded as full attention in KV cache manager
- (blocks are allocated for all tokens), while computed as sliding window
- attention in model runner.
- In this case, we use FullAttentionSpec and record the sliding window size.
- """
- sliding_window: int | None = None
- """
- Default to None for not using sliding window attention.
- """
- attention_chunk_size: int | None = None
- non_causal: bool = False
- """
- Whether the layer attends non-causally (e.g. Prefix LM). Carried on the
- spec so the engine core, which collects specs from all workers before the
- scheduler is built, can adjust scheduling policy (chunked prefill / prefix
- caching) regardless of tensor-parallel layout. It does not affect the KV
- cache layout itself.
- """
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- max_model_len = vllm_config.model_config.max_model_len
- dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
- if self.dcp_sharded and dcp_world_size > 1:
- max_model_len = cdiv(max_model_len, dcp_world_size)
- return cdiv(max_model_len, self.block_size) * self.page_size_bytes
- @classmethod
- def merge_window_sizes(cls, window_sizes: set[int]) -> int | None:
- if len(window_sizes) == 0:
- return None
- elif len(window_sizes) == 1:
- return window_sizes.pop()
- else:
- raise ValueError(
- "All attention layers in the same KV cache group must have the "
- "same window size."
- )
- @classmethod
- def merge(cls, specs: list[Self]) -> Self:
- """Merge a list of FullAttentionSpec objects into a single
- FullAttentionSpec object.
- """
- assert all(isinstance(spec, FullAttentionSpec) for spec in specs), (
- "All attention layers in the same KV cache group must be FullAttentionSpec."
- )
- sliding_window = set(
- spec.sliding_window for spec in specs if spec.sliding_window is not None
- )
- attention_chunk_size = set(
- spec.attention_chunk_size
- for spec in specs
- if spec.attention_chunk_size is not None
- )
- assert not any(isinstance(spec, MLAAttentionSpec) for spec in specs), (
- "MLAAttentionSpec should be merged in MLAAttentionSpec.merge"
- )
- merged_spec = cls(
- block_size=specs[0].block_size,
- block_stride_alignment=specs[0].block_stride_alignment,
- num_kv_heads=specs[0].num_kv_heads,
- head_size=specs[0].head_size,
- head_size_v=specs[0].head_size_v,
- dtype=specs[0].dtype,
- kv_quant_mode=specs[0].kv_quant_mode,
- dcp_sharded=specs[0].dcp_sharded,
- max_tp_shards=specs[0].max_tp_shards,
- page_size_padded=specs[0].page_size_padded,
- num_head_slots=specs[0].num_head_slots,
- state_content_bytes=specs[0].state_content_bytes,
- tokens_per_state=specs[0].tokens_per_state,
- sliding_window=cls.merge_window_sizes(sliding_window),
- attention_chunk_size=cls.merge_window_sizes(attention_chunk_size),
- # If any layer in the group is non-causal, treat the group as
- # non-causal so the engine core disables incompatible scheduling.
- non_causal=any(spec.non_causal for spec in specs),
- )
- for spec in specs:
- for f in fields(AttentionSpec):
- assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
- "All attention layers in the same KV cache group must have "
- "the same attention spec."
- )
- assert (merged_spec.sliding_window is not None) + (
- merged_spec.attention_chunk_size is not None
- ) <= 1, (
- "Model with both sliding window layers and chunked local attention "
- "layers is not supported."
- )
- return merged_spec
- def _apply_alignment_padding(spec: MLAAttentionSpec | SlidingWindowMLASpec):
- if spec.alignment is None:
- return
- actual_page_size = spec.real_page_size_bytes
- padded_page_size = round_up(actual_page_size, spec.alignment)
- if padded_page_size != actual_page_size:
- object.__setattr__(spec, "page_size_padded", padded_page_size)
- @dataclass(frozen=True, kw_only=True)
- class MLAAttentionSpec(FullAttentionSpec):
- # TODO(Lucas/Chen): less hacky way to do this
- cache_dtype_str: str | None = None
- # DeepseekV4 only fields. Non-DeepseekV4 MLA models leave these at defaults.
- alignment: int | None = None # Default to None for no padding.
- model_version: str | None = None
- cache_role: SparseCacheRole = SparseCacheRole.SPARSE
- is_index_group_leader: bool = False
- storage_block_size: int | None = None
- """Token width used to view storage when it differs from the kernel block."""
- # Group capability enabled when any member flattens a non-causal query block
- # into decode rows. Runtime metadata still selects causal vs. non-causal mode.
- non_causal_multi_token_decode: bool = False
- # MLA stores a single latent vector per state; there is no separate V.
- head_size_v: int = 0
- def __post_init__(self):
- super().__post_init__()
- _apply_alignment_padding(self)
- @classmethod
- def merge(cls, specs: list[Self]) -> Self:
- assert all(isinstance(spec, MLAAttentionSpec) for spec in specs), (
- "All attention layers in the same KV cache group must be MLAAttentionSpec."
- )
- cache_dtype_str_set = set(spec.cache_dtype_str for spec in specs)
- tokens_per_state_set = set(spec.tokens_per_state for spec in specs)
- model_version_set = set(spec.model_version for spec in specs)
- cache_role_set = {spec.cache_role for spec in specs}
- index_group_leader_set = {spec.is_index_group_leader for spec in specs}
- storage_block_size_set = set(spec.storage_block_size for spec in specs)
- block_stride_alignment_set = {spec.block_stride_alignment for spec in specs}
- assert (
- len(cache_dtype_str_set) == 1
- and len(tokens_per_state_set) == 1
- and len(model_version_set) == 1
- and len(cache_role_set) == 1
- and len(index_group_leader_set) == 1
- and len(storage_block_size_set) == 1
- and len(block_stride_alignment_set) == 1
- ), (
- "All attention layers in the same KV cache group must use the same "
- "quantization method, tokens per state, model version, cache role, "
- "index-sharing role, storage block size and block stride alignment."
- )
- merged_spec = cls(
- block_size=specs[0].block_size,
- num_kv_heads=specs[0].num_kv_heads,
- head_size=specs[0].head_size,
- dtype=specs[0].dtype,
- kv_quant_mode=specs[0].kv_quant_mode,
- dcp_sharded=specs[0].dcp_sharded,
- max_tp_shards=specs[0].max_tp_shards,
- page_size_padded=specs[0].page_size_padded,
- num_head_slots=specs[0].num_head_slots,
- state_content_bytes=specs[0].state_content_bytes,
- cache_dtype_str=cache_dtype_str_set.pop(),
- tokens_per_state=tokens_per_state_set.pop(),
- model_version=model_version_set.pop(),
- cache_role=cache_role_set.pop(),
- is_index_group_leader=index_group_leader_set.pop(),
- storage_block_size=storage_block_size_set.pop(),
- block_stride_alignment=block_stride_alignment_set.pop(),
- non_causal_multi_token_decode=any(
- spec.non_causal_multi_token_decode for spec in specs
- ),
- )
- for spec in specs:
- for f in fields(AttentionSpec):
- assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
- "All attention layers in the same KV cache group must have "
- "the same attention spec."
- )
- return merged_spec
- @dataclass(frozen=True, kw_only=True)
- class HiddenStateCacheSpec(MLAAttentionSpec):
- """Marker for hidden-state cache layers used by extract_hidden_states."""
- pass
- @dataclass(frozen=True, kw_only=True)
- class RSWASpec(FullAttentionSpec):
- """KV cache spec for Reference Sliding Window Attention (R-SWA).
- Prefill (image + text prompt) tokens are always globally visible.
- Only the last ``rswa_window`` generated tokens are kept in the KV cache;
- gap blocks (between the prefill tail and the current decode window) are
- evicted during each decode step to bound memory at
- O(prefix_blocks + window_blocks).
- """
- rswa_window: int
- @classmethod
- def merge(cls, specs: list[RSWASpec]) -> RSWASpec:
- assert all(isinstance(spec, RSWASpec) for spec in specs), (
- "All attention layers in the same KV cache group must be RSWASpec."
- )
- rswa_windows = {spec.rswa_window for spec in specs}
- assert len(rswa_windows) == 1, (
- f"All R-SWA layers must share the same rswa_window, got {rswa_windows}"
- )
- # Delegate common field merging to the parent, then reattach rswa_window.
- base = FullAttentionSpec.merge(specs) # type: ignore[arg-type]
- return cls(
- block_size=base.block_size,
- block_stride_alignment=base.block_stride_alignment,
- num_kv_heads=base.num_kv_heads,
- head_size=base.head_size,
- head_size_v=base.head_size_v,
- dtype=base.dtype,
- kv_quant_mode=base.kv_quant_mode,
- dcp_sharded=base.dcp_sharded,
- max_tp_shards=base.max_tp_shards,
- page_size_padded=base.page_size_padded,
- num_head_slots=base.num_head_slots,
- state_content_bytes=base.state_content_bytes,
- tokens_per_state=base.tokens_per_state,
- sliding_window=base.sliding_window,
- attention_chunk_size=base.attention_chunk_size,
- non_causal=base.non_causal,
- rswa_window=rswa_windows.pop(),
- )
- @dataclass(frozen=True, kw_only=True)
- class ChunkedLocalAttentionSpec(AttentionSpec):
- attention_chunk_size: int
- def max_admission_blocks_per_request(
- self, max_in_flight_tokens: int, max_model_len: int
- ) -> int:
- """Per-request admission cap, in blocks.
- Single source of truth for both startup pool sizing
- (`max_memory_usage_bytes`) and the runtime admission gate, so requests
- admitted by startup can also be admitted at runtime.
- `max_in_flight_tokens` is the max tokens scheduled but not yet settled
- (one batch per concurrent step); see `VllmConfig.max_in_flight_tokens`.
- """
- # During chunked prefill, we hold KV for at most one chunk window plus
- # the in-flight tokens, since frees happen on the processed-token basis.
- num_tokens = min(
- self.attention_chunk_size + max_in_flight_tokens, max_model_len
- )
- return cdiv(num_tokens, self.block_size)
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- max_blocks = self.max_admission_blocks_per_request(
- max_in_flight_tokens=vllm_config.max_in_flight_tokens,
- max_model_len=vllm_config.model_config.max_model_len,
- )
- return max_blocks * self.page_size_bytes
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(
- isinstance(spec, ChunkedLocalAttentionSpec)
- and spec.attention_chunk_size == self.attention_chunk_size
- for spec in kv_cache_specs.values()
- )
- @dataclass(frozen=True, kw_only=True)
- class SlidingWindowSpec(AttentionSpec):
- sliding_window: int
- # The trailing edge of the window is extended by ``extra_retained_tokens``
- # so that those extra trailing tokens' blocks are retained (but not
- # attended). This is needed for multi-module spec decoding which can
- # re-prefill the last num_spec_prefill_tokens - 1 tokens from the end
- # of the sequence, and thus needs to delay freeing/caching of blocks.
- extra_retained_tokens: int = 0
- def max_admission_blocks_per_request(
- self, max_in_flight_tokens: int, max_model_len: int
- ) -> int:
- """Per-request admission cap, in blocks.
- Single source of truth for both startup pool sizing
- (`max_memory_usage_bytes`) and the runtime admission gate. Per-request
- real-held blocks plateau at this bound because
- `SlidingWindowManager.remove_skipped_blocks` runs from `allocate_slots`
- before each chunk's `get_num_blocks_to_allocate`.
- `max_in_flight_tokens` is the max tokens scheduled but not yet settled
- (one batch per concurrent step); see `VllmConfig.max_in_flight_tokens`.
- """
- # During chunked prefill, we hold KV for the last `sliding_window-1`
- # computed tokens plus the in-flight tokens (frees happen on the
- # processed-token basis); never more than `max_model_len`. An additional
- # `extra_retained_tokens` trailing tokens are kept alive below the
- # window for multi-module spec decoding, and must be accounted here too.
- num_tokens = min(
- self.sliding_window - 1 + self.extra_retained_tokens + max_in_flight_tokens,
- max_model_len,
- )
- # +1 because the sliding window may not start from the beginning of
- # the block. E.g. block size 4 and num_token 4 needs two blocks
- # [XXCD][EF] to store the 6-token window [CDEF].
- return cdiv(num_tokens, self.block_size) + 1
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- assert (
- not self.dcp_sharded
- or vllm_config.parallel_config.decode_context_parallel_size == 1
- ), "DCP not support sliding window."
- max_blocks = self.max_admission_blocks_per_request(
- max_in_flight_tokens=vllm_config.max_in_flight_tokens,
- max_model_len=vllm_config.model_config.max_model_len,
- )
- return max_blocks * self.page_size_bytes
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(
- isinstance(spec, SlidingWindowSpec)
- and spec.sliding_window == self.sliding_window
- for spec in kv_cache_specs.values()
- )
- @dataclass(frozen=True, kw_only=True)
- class CircularBufferSpec(AttentionSpec):
- """One block per request holding the raw keys of the token group that
- is still being compressed.
- ``block_size`` is the ring capacity. It must exceed the compression ratio
- by the speculative lookahead: a speculative step stores all of its rows,
- drafts included, before acceptance is known, while the next step still
- reads the open group's committed keys from the ring.
- """
- @property
- def block_table_token_alignment(self) -> int | None:
- return None
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- # The ring occupies one block per request for its whole lifetime.
- del vllm_config
- return self.page_size_bytes
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- del vllm_config, max_len
- return 1
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(
- isinstance(spec, CircularBufferSpec) for spec in kv_cache_specs.values()
- )
- @property
- def prefix_cacheable(self) -> bool:
- return False
- @property
- def uses_slot_mapping(self) -> bool:
- return False
- @dataclass(frozen=True, kw_only=True)
- class SlidingWindowMLASpec(SlidingWindowSpec):
- """Sliding window attention with MLA cache format."""
- cache_dtype_str: str | None = None
- # DeepseekV4-only: see MLAAttentionSpec.model_version.
- alignment: int | None = None # Default to None for no padding.
- model_version: str | None = None
- bounded_replay: bool = False
- # MLA stores a single latent vector per state; there is no separate V.
- head_size_v: int = 0
- def __post_init__(self):
- assert self.model_version in (None, "deepseek_v4"), (
- f"Unsupported model version: {self.model_version}"
- )
- super().__post_init__()
- _apply_alignment_padding(self)
- @property
- def prefix_cacheable(self) -> bool:
- return not self.bounded_replay
- @property
- def prefix_replay_tokens(self) -> int:
- return self.sliding_window if self.bounded_replay else 0
- @classmethod
- def merge(cls, specs: list[Self]) -> Self:
- assert all(isinstance(spec, SlidingWindowMLASpec) for spec in specs), (
- "All attention layers in the same KV cache group must be "
- "SlidingWindowMLASpec."
- )
- cache_dtype_str_set = set(spec.cache_dtype_str for spec in specs)
- tokens_per_state_set = set(spec.tokens_per_state for spec in specs)
- model_version_set = set(spec.model_version for spec in specs)
- sliding_window_set = set(spec.sliding_window for spec in specs)
- extra_retained_set = set(spec.extra_retained_tokens for spec in specs)
- bounded_replay_set = set(spec.bounded_replay for spec in specs)
- block_stride_alignment_set = {spec.block_stride_alignment for spec in specs}
- assert len({spec.dcp_sharded for spec in specs}) == 1
- assert len({spec.max_tp_shards for spec in specs}) == 1
- assert (
- len(cache_dtype_str_set) == 1
- and len(tokens_per_state_set) == 1
- and len(model_version_set) == 1
- and len(sliding_window_set) == 1
- and len(extra_retained_set) == 1
- and len(bounded_replay_set) == 1
- and len(block_stride_alignment_set) == 1
- ), (
- "All attention layers in the same KV cache group must use the same "
- "quantization method, tokens per state, model version, sliding "
- "window size, retained token count, and replay policy."
- )
- return cls(
- block_size=specs[0].block_size,
- block_stride_alignment=block_stride_alignment_set.pop(),
- num_kv_heads=specs[0].num_kv_heads,
- head_size=specs[0].head_size,
- dtype=specs[0].dtype,
- dcp_sharded=specs[0].dcp_sharded,
- max_tp_shards=specs[0].max_tp_shards,
- page_size_padded=specs[0].page_size_padded,
- num_head_slots=specs[0].num_head_slots,
- state_content_bytes=specs[0].state_content_bytes,
- sliding_window=sliding_window_set.pop(),
- extra_retained_tokens=extra_retained_set.pop(),
- cache_dtype_str=cache_dtype_str_set.pop(),
- tokens_per_state=tokens_per_state_set.pop(),
- model_version=model_version_set.pop(),
- bounded_replay=bounded_replay_set.pop(),
- )
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(
- isinstance(spec, SlidingWindowMLASpec)
- and spec.sliding_window == self.sliding_window
- and spec.bounded_replay == self.bounded_replay
- for spec in kv_cache_specs.values()
- )
- @dataclass(frozen=True, kw_only=True)
- class KpoolTailSpec(SlidingWindowSpec):
- """One-block circular scratch cache for a kpool indexer's raw tail."""
- def max_admission_blocks_per_request(
- self, max_in_flight_tokens: int, max_model_len: int
- ) -> int:
- return 1
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- return 1
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(isinstance(spec, KpoolTailSpec) for spec in kv_cache_specs.values())
- @property
- def prefix_cacheable(self) -> bool:
- return False
- @property
- def uses_slot_mapping(self) -> bool:
- return False
- @dataclass(frozen=True)
- class MambaSpec(KVCacheSpec):
- shapes: tuple[tuple[int, ...], ...]
- dtypes: tuple[torch.dtype, ...]
- page_size_padded: int | None = None
- mamba_type: MambaAttentionBackendEnum = MambaAttentionBackendEnum.MAMBA2
- mamba_cache_mode: str = "none"
- num_speculative_blocks: int = 0
- num_prefill_checkpoint_blocks: int = 0
- prefill_checkpoint_alignment: int | None = None
- num_heads: int = 1
- tokens_per_state: int = -1
- # False: the state is sharded across TP ranks (e.g. GDN). True: every TP
- # rank holds the full state (e.g. the replicated PLE conv state).
- tp_replicated: bool = False
- @property
- def state_content_size_bytes(self) -> int:
- return sum(
- prod(shape) * get_dtype_size(dtype)
- for (shape, dtype) in zip(self.shapes, self.dtypes)
- )
- @property
- def real_page_size_bytes(self) -> int:
- return self.state_content_size_bytes
- @property
- def page_size_bytes(self) -> int:
- page_size = sum(
- prod(shape) * get_dtype_size(dtype)
- for (shape, dtype) in zip(self.shapes, self.dtypes)
- )
- if self.page_size_padded is not None:
- assert self.page_size_padded >= page_size
- return self.page_size_padded
- return page_size
- @property
- def block_table_token_alignment(self) -> int | None:
- return None
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- if vllm_config.cache_config.mamba_cache_mode == "all":
- max_model_len = vllm_config.model_config.max_model_len
- return (
- cdiv(max_model_len, self.block_size) + self.num_speculative_blocks
- ) * self.page_size_bytes
- elif vllm_config.cache_config.mamba_cache_mode == "align":
- return self.page_size_bytes * (
- 2 + self.num_speculative_blocks + self.num_prefill_checkpoint_blocks
- )
- else:
- return self.page_size_bytes * (1 + self.num_speculative_blocks)
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- # Mamba state is replicated across DCP/PCP ranks, never sharded, so
- # no CP scaling applies.
- if vllm_config.cache_config.mamba_cache_mode == "align":
- # Block table rows are position-indexed over the full sequence
- # even though only 2 + num_speculative_blocks state blocks are
- # resident at a time (earlier states are nulled out by
- # remove_skipped_blocks), so the row length must cover max_len
- # rather than max_memory_usage_bytes.
- return cdiv(max_len, self.block_size) + self.num_speculative_blocks
- return cdiv(self.max_memory_usage_bytes(vllm_config), self.page_size_bytes)
- def is_uniform_with_collection(
- self, kv_cache_specs: dict[str, KVCacheSpec]
- ) -> bool:
- return all(
- isinstance(spec, MambaSpec)
- and spec.num_speculative_blocks == self.num_speculative_blocks
- and spec.num_prefill_checkpoint_blocks == self.num_prefill_checkpoint_blocks
- and spec.prefill_checkpoint_alignment == self.prefill_checkpoint_alignment
- and spec.page_size_bytes == self.page_size_bytes
- and spec.tp_replicated == self.tp_replicated
- for spec in kv_cache_specs.values()
- )
- def get_mamba_prefill_checkpoint_position(
- num_tokens: int,
- hash_block_size: int,
- drop_eagle_block: bool,
- ) -> int:
- """Return the reusable Mamba checkpoint boundary for a prefill."""
- checkpoint_position = (num_tokens - 1) // hash_block_size * hash_block_size
- if drop_eagle_block:
- checkpoint_position -= hash_block_size
- return max(checkpoint_position, 0)
- def is_mamba_prefill_checkpoint_valid(
- query_start: int,
- query_end: int,
- checkpoint_position: int,
- hash_block_size: int,
- mamba_block_size: int,
- checkpoint_alignment: int | None,
- ) -> bool:
- """Whether a backend can export the checkpoint in this query."""
- if checkpoint_alignment is None:
- return False
- assert checkpoint_alignment > 0
- initial_state_col = (query_start - 1) // mamba_block_size
- checkpoint_col = cdiv(query_end, mamba_block_size) - 2
- return (
- query_start % hash_block_size == 0
- and checkpoint_col > initial_state_col
- and query_start + hash_block_size <= checkpoint_position
- and query_start < checkpoint_position < query_end
- and (checkpoint_position - query_start) % checkpoint_alignment == 0
- )
- @dataclass(frozen=True)
- class EncoderOnlyAttentionSpec(AttentionSpec):
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- # Encoder-only layers do not need KV cache
- return 0
- @dataclass(frozen=True)
- class CrossAttentionSpec(AttentionSpec):
- """KV cache spec for cross-attention layers in encoder-decoder models."""
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- # For cross-attention, we need to cache encoder states
- # Get encoder length (e.g., 1500 for Whisper).
- max_encoder_len = vllm_config.scheduler_config.max_num_encoder_input_tokens
- return cdiv(max_encoder_len, self.block_size) * self.page_size_bytes
- @dataclass(frozen=True)
- class SinkFullAttentionSpec(FullAttentionSpec):
- sink_len: int | None = None
- @classmethod
- def merge(cls, specs: list[Self]) -> Self:
- """Merge a list of FullAttentionSpec objects into a single
- FullAttentionSpec object.
- """
- assert all(isinstance(spec, FullAttentionSpec) for spec in specs), (
- "All attention layers in the same KV cache group must be FullAttentionSpec."
- )
- sliding_window = set(
- spec.sliding_window for spec in specs if spec.sliding_window is not None
- )
- attention_chunk_size = set(
- spec.attention_chunk_size
- for spec in specs
- if spec.attention_chunk_size is not None
- )
- assert not any(isinstance(spec, MLAAttentionSpec) for spec in specs), (
- "MLAAttentionSpec should be merged in MLAAttentionSpec.merge"
- )
- merged_spec = cls(
- block_size=specs[0].block_size,
- num_kv_heads=specs[0].num_kv_heads,
- head_size=specs[0].head_size,
- head_size_v=specs[0].head_size_v,
- sink_len=specs[0].sink_len,
- block_stride_alignment=specs[0].block_stride_alignment,
- dtype=specs[0].dtype,
- kv_quant_mode=specs[0].kv_quant_mode,
- dcp_sharded=specs[0].dcp_sharded,
- max_tp_shards=specs[0].max_tp_shards,
- page_size_padded=specs[0].page_size_padded,
- num_head_slots=specs[0].num_head_slots,
- state_content_bytes=specs[0].state_content_bytes,
- sliding_window=cls.merge_window_sizes(sliding_window),
- attention_chunk_size=cls.merge_window_sizes(attention_chunk_size),
- non_causal=any(spec.non_causal for spec in specs),
- )
- for spec in specs:
- for f in fields(AttentionSpec):
- assert getattr(spec, f.name) == getattr(merged_spec, f.name), (
- "All attention layers in the same KV cache group must have "
- "the same attention spec."
- )
- assert (merged_spec.sliding_window is not None) + (
- merged_spec.attention_chunk_size is not None
- ) <= 1, (
- "Model with both sliding window layers and chunked local attention "
- "layers is not supported."
- )
- return merged_spec
- @dataclass(frozen=True)
- class UniformTypeKVCacheSpecs(KVCacheSpec):
- """A KV cache spec for multiple layers with the same type of attention. Here,
- same types means always need the same number of token slots. For example,
- sliding window attentions with different window sizes are not the same type
- and should not be merged into one UniformTypeKVCacheSpecs.
- """
- kv_cache_specs: dict[str, KVCacheSpec]
- def __post_init__(self):
- super().__post_init__()
- if self.kv_cache_specs:
- object.__setattr__(self, "dcp_sharded", self.first_spec.dcp_sharded)
- @property
- def prefix_cacheable(self) -> bool:
- return all(spec.prefix_cacheable for spec in self.kv_cache_specs.values())
- @property
- def prefix_replay_tokens(self) -> int:
- return max(spec.prefix_replay_tokens for spec in self.kv_cache_specs.values())
- @property
- def first_spec(self) -> KVCacheSpec:
- """Return the first spec in the group."""
- return next(iter(self.kv_cache_specs.values()))
- @property
- def page_size_bytes(self) -> int:
- return sum(spec.page_size_bytes for spec in self.kv_cache_specs.values())
- @property
- def block_table_token_alignment(self) -> int | None:
- return self.first_spec.block_table_token_alignment
- def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
- max_num_pages = max(
- cdiv(spec.max_memory_usage_bytes(vllm_config), spec.page_size_bytes)
- for spec in self.kv_cache_specs.values()
- )
- return max_num_pages * self.page_size_bytes
- def max_num_blocks_per_req(self, vllm_config: VllmConfig, max_len: int) -> int:
- # Metadata builders are constructed from the per-layer spec, so the base
- # cdiv(max_len, block_size) would drop its DCP sharding and size the
- # block table wider than those builders expect.
- widths = {
- spec.max_num_blocks_per_req(vllm_config, max_len)
- for spec in self.kv_cache_specs.values()
- }
- assert len(widths) == 1, (
- "All layers in the same KV cache group must need the same number "
- f"of block table entries, got {sorted(widths)}."
- )
- return next(iter(widths))
- @classmethod
- def is_uniform_type(cls, kv_cache_specs: dict[str, KVCacheSpec]) -> bool:
- """Whether all layers have the same type of KV cache spec.
- Uses the registry to determine grouping base classes, so custom specs
- that inherit from FullAttentionSpec are treated as full attention.
- """
- block_sizes = set(spec.block_size for spec in kv_cache_specs.values())
- if len(block_sizes) > 1:
- # Different block sizes, not uniform.
- return False
- if len({spec.dcp_sharded for spec in kv_cache_specs.values()}) > 1:
- return False
- first_spec = next(iter(kv_cache_specs.values()))
- return first_spec.is_uniform_with_collection(kv_cache_specs)
- @classmethod
- def from_specs(cls, kv_cache_specs: dict[str, KVCacheSpec]) -> Self | None:
- """Return a SameTypeKVCacheSpecs object if all layers have the same type
- of KV cache spec. Return None if not.
- """
- if cls.is_uniform_type(kv_cache_specs):
- block_size = next(iter(kv_cache_specs.values())).block_size
- return cls(block_size=block_size, kv_cache_specs=kv_cache_specs)
- else:
- return None
- def get_max_layers_per_page_size(self) -> int:
- """Max number of layers sharing a page size. For a balanced bucket
- this equals the number of repetitions of the layer pattern."""
- return Counter(
- spec.page_size_bytes for spec in self.kv_cache_specs.values()
- ).most_common(1)[0][1]
- def max_memory_usage_pages(self, vllm_config: VllmConfig) -> int:
- return max(
- cdiv(spec.max_memory_usage_bytes(vllm_config), spec.page_size_bytes)
- for spec in self.kv_cache_specs.values()
- )
- @property
- def uses_slot_mapping(self) -> bool:
- return self.first_spec.uses_slot_mapping
- def iter_layer_specs(kv_cache_spec: KVCacheSpec) -> Collection[KVCacheSpec]:
- """The per-layer specs a KV cache group spec covers.
- ``UniformTypeKVCacheSpecs`` groups keep one spec per layer; every other
- spec describes its group on its own. Returns the layer specs either way so
- callers do not have to special-case the wrapper.
- """
- if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
- return kv_cache_spec.kv_cache_specs.values()
- return (kv_cache_spec,)
- def is_full_attention_spec(kv_cache_spec: KVCacheSpec) -> bool:
- """Whether a KV cache group spec is (or wraps) full attention.
- ``UniformTypeKVCacheSpecs`` is not itself a ``FullAttentionSpec``, so a bare
- isinstance check misses groups that carry the wrapper -- DeepSeek-V4's MLA
- layers, or any model taking the ``UniformTypeKVCacheSpecs.from_specs`` path.
- Every layer must be full attention: a group holding a recycling
- (sliding-window) layer has no stable slot layout, so callers that key data
- by slot cannot use it.
- """
- layer_specs = iter_layer_specs(kv_cache_spec)
- return len(layer_specs) > 0 and all(
- isinstance(spec, FullAttentionSpec) for spec in layer_specs
- )
- def get_kv_cache_spec_kind(kv_cache_spec: KVCacheSpec) -> KVCacheSpecKind:
- if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
- inner_kinds = {
- get_kv_cache_spec_kind(spec)
- for spec in kv_cache_spec.kv_cache_specs.values()
- }
- if len(inner_kinds) == 1:
- return next(iter(inner_kinds))
- # A group is only formed when all members share one registered
- # uniform_type_base_spec, so UNKNOWN would discard what the merge
- # already established.
- base_specs = {
- KVCacheSpecRegistry.get_uniform_type_base_spec(spec)
- for spec in kv_cache_spec.kv_cache_specs.values()
- }
- if len(base_specs) == 1 and next(iter(base_specs)) is FullAttentionSpec:
- return KVCacheSpecKind.FULL_ATTENTION
- return KVCacheSpecKind.UNKNOWN
- # Keep subclass checks before base classes so specialized specs keep their
- # more precise kind.
- if isinstance(kv_cache_spec, SlidingWindowMLASpec):
- return KVCacheSpecKind.SLIDING_WINDOW_MLA
- if isinstance(kv_cache_spec, MLAAttentionSpec):
- return KVCacheSpecKind.MLA_ATTENTION
- if isinstance(kv_cache_spec, SinkFullAttentionSpec):
- return KVCacheSpecKind.SINK_FULL_ATTENTION
- if isinstance(kv_cache_spec, FullAttentionSpec):
- return KVCacheSpecKind.FULL_ATTENTION
- if isinstance(kv_cache_spec, ChunkedLocalAttentionSpec):
- return KVCacheSpecKind.CHUNKED_LOCAL_ATTENTION
- if isinstance(kv_cache_spec, SlidingWindowSpec):
- return KVCacheSpecKind.SLIDING_WINDOW
- if isinstance(kv_cache_spec, MambaSpec):
- return KVCacheSpecKind.MAMBA
- if isinstance(kv_cache_spec, EncoderOnlyAttentionSpec):
- return KVCacheSpecKind.ENCODER_ONLY_ATTENTION
- if isinstance(kv_cache_spec, CrossAttentionSpec):
- return KVCacheSpecKind.CROSS_ATTENTION
- return KVCacheSpecKind.UNKNOWN
- def get_kv_cache_spec_sliding_window(kv_cache_spec: KVCacheSpec) -> int | None:
- if isinstance(kv_cache_spec, UniformTypeKVCacheSpecs):
- inner_windows = {
- get_kv_cache_spec_sliding_window(spec)
- for spec in kv_cache_spec.kv_cache_specs.values()
- }
- return next(iter(inner_windows)) if len(inner_windows) == 1 else None
- if isinstance(kv_cache_spec, SlidingWindowSpec):
- return kv_cache_spec.sliding_window
- return None
- @dataclass
- class KVCacheTensor:
- """A class for specifying how the workers should initialize the KV cache.
- Placement of a set of same-shaped layers in the KV cache allocation.
- Layer ``layers[l]``'s page for block ``b`` starts at
- ``offset + l * layer_stride + b * block_stride`` bytes into the backing
- allocation of ``size`` bytes. Layer-outermost layouts give each layer a
- contiguous region (``layer_stride = page * num_blocks``,
- ``block_stride = page``); block-outermost layouts make each block a
- block of all layers' pages (``layer_stride = page``, ``block_stride`` =
- the packed block). Tensors whose address ranges overlap
- alias the same bytes: cache groups overlay each other, which is sound
- because a block ID is owned by one group at a time.
- """
- size: int # total size of the backing allocation in bytes
- layers: list[str] # layer names in L order
- layer_stride: int
- block_stride: int
- offset: int = 0 # byte offset of layers[0]'s block 0
- host_resident: bool = False
- class KVCacheGroupRole(str, Enum):
- DEFAULT = "default"
- HISPARSE_SOURCE = "hisparse_source"
- HISPARSE_INDEXER = "hisparse_indexer"
- @dataclass
- class KVCacheGroupSpec:
- """Represents a group of model layers that share the same KV cache block table.
- These layers are regarded as one layer in the KV cache manager.
- """
- # The names of model layers in this group
- layer_names: list[str]
- # The KV cache spec of this manager layer
- kv_cache_spec: KVCacheSpec
- # Whether this group contains EAGLE/MTP draft attention layers.
- is_eagle_group: bool = False
- # Host groups use the dedicated HiSparse pool; others share the device pool.
- host_resident: bool = False
- # Whether this group is part of the externally transferable KV state.
- # Ephemeral accelerator-side replicas are rebuilt from their transferable
- # source group after a connector load.
- enable_kv_transfer: bool = True
- role: KVCacheGroupRole = KVCacheGroupRole.DEFAULT
- @dataclass
- class KVCacheConfig:
- """The KV cache configuration of a model."""
- num_blocks: int
- """The number of KV cache blocks"""
- kv_cache_tensors: list[KVCacheTensor]
- """How should model runner initialize the KV cache tensors for each layer"""
- kv_cache_groups: list[KVCacheGroupSpec]
- """
- The kv cache groups of the model.
- For models with only one type of attention, there is only one group that
- contains all layers.
- For models with multiple types of attention, there will be multiple groups,
- see `_get_kv_cache_config_uniform_page_size` for more details.
- """
- prefix_cache_retention_interval: int | None = None
- """Resolved retention policy for local prefix-cache checkpoints."""
- kv_cache_layout: str | None = None
- """The KV cache layout resolved by the engine core, adopted by all workers."""
- hisparse_host_num_blocks: int | None = None
- """Capacity of the dedicated HiSparse host-block manager, when enabled."""
- hisparse_host_block_stride: int | None = None
- """Physical bytes between consecutive HiSparse host blocks."""
- hisparse_shared_host_pool: bool = False
- """Whether local TP ranks share one physical HiSparse host pool."""
- kv_tp_replicas: int = 1
- """Consecutive TP ranks holding identical KV for every layer (1: none)."""
- draft_num_blocks: int | None = None
- """GLM-5-Next with a drafter: blocks in the drafter groups' own pool, sized
- for their sliding windows instead of one draft page per target block."""
- draft_layer_names: tuple[str, ...] = ()
- """The drafter layers whose tensors address that pool."""
- @cached_property
- def transfer_group_ids(self) -> tuple[int, ...]:
- """IDs of cache groups that participate in external KV transfer."""
- return tuple(
- group_id
- for group_id, group in enumerate(self.kv_cache_groups)
- if group.enable_kv_transfer
- )
- @cached_property
- def prefix_cacheable_group_ids(self) -> tuple[int, ...]:
- """IDs of transferable groups eligible for hash-addressed stores."""
- return tuple(
- group_id
- for group_id in self.transfer_group_ids
- if self.kv_cache_groups[group_id].kv_cache_spec.prefix_cacheable
- )
- @cached_property
- def prefix_cacheable_groups(self) -> tuple[KVCacheGroupSpec, ...]:
- """Transferable groups eligible for hash-addressed stores."""
- return tuple(
- self.kv_cache_groups[group_id]
- for group_id in self.prefix_cacheable_group_ids
- )
- @cached_property
- def transfer_groups(self) -> tuple[KVCacheGroupSpec, ...]:
- """Cache groups that participate in external KV transfer."""
- return tuple(
- self.kv_cache_groups[group_id] for group_id in self.transfer_group_ids
- )
- @cached_property
- def transfer_group_index_by_layer(self) -> dict[str, int]:
- """Transfer-group tuple index for each participating layer."""
- return {
- layer_name: group_index
- for group_index, group in enumerate(self.transfer_groups)
- for layer_name in group.layer_names
- }
- def select_transfer_block_ids(
- self, block_ids: Sequence[list[int]]
- ) -> tuple[list[int], ...]:
- """Select block IDs for externally transferable cache groups."""
- if len(block_ids) != len(self.kv_cache_groups):
- raise ValueError(
- f"Expected {len(self.kv_cache_groups)} KV cache groups, "
- f"got {len(block_ids)}."
- )
- return tuple(block_ids[group_id] for group_id in self.transfer_group_ids)
- @property
- def host_group_ids(self) -> tuple[int, ...]:
- return tuple(
- group_id
- for group_id, group in enumerate(self.kv_cache_groups)
- if group.host_resident
- )
- def num_blocks_of(self, tensor: KVCacheTensor) -> int:
- """Number of blocks addressable by the pool backing ``tensor``."""
- if self.draft_num_blocks is not None and tensor.layers[0] in self.draft_layer_names:
- return self.draft_num_blocks
- if not tensor.host_resident:
- return self.num_blocks
- assert self.hisparse_host_num_blocks is not None
- return self.hisparse_host_num_blocks
- @property
- def has_mamba_layers(self) -> bool:
- return any(
- isinstance(spec, MambaSpec)
- for group in self.kv_cache_groups
- for spec in iter_layer_specs(group.kv_cache_spec)
- )
- @property
- def has_mixed_precision_kv_cache(self) -> bool:
- """Whether device attention caches use more than one precision."""
- kv_cache_precisions: set[tuple[torch.dtype, KVQuantMode]] = set()
- for group in self.kv_cache_groups:
- if group.host_resident:
- continue
- kv_cache_precisions.update(
- (spec.dtype, spec.kv_quant_mode)
- for spec in iter_layer_specs(group.kv_cache_spec)
- if isinstance(spec, AttentionSpec)
- )
- return len(kv_cache_precisions) > 1
- @property
- def needs_kv_cache_zeroing(self) -> bool:
- """Whether newly allocated KV cache blocks must be zeroed before use.
- Required for Mamba layers, whose state is read before it is fully written
- (#35219), and for mixed-precision caches, where a block reused across
- groups can be reinterpreted under a different precision and decode stale
- bytes to NaN/Inf. Uniform-precision caches skip zeroing.
- """
- return self.has_mamba_layers or self.has_mixed_precision_kv_cache
|