Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1089 lines
39 KiB
Python
1089 lines
39 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, replace
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
|
kFp8Dynamic64Sym,
|
|
kFp8Dynamic128Sym,
|
|
kFp8StaticTensorSym,
|
|
kNvfp4Dynamic,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from vllm.config import VllmConfig
|
|
from vllm.config.cache import CacheDType
|
|
from vllm.model_executor.layers.linear import ColumnParallelLinear
|
|
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
|
|
from vllm.platforms.interface import DeviceCapability
|
|
from vllm.v1.hisparse.runtime import HiSparseCacheHandle
|
|
from vllm.v1.kv_cache_interface import (
|
|
AttentionSpec,
|
|
KVCacheLayout,
|
|
KVCacheSpec,
|
|
KVQuantMode,
|
|
)
|
|
|
|
from vllm.v1.kv_cache_interface import KVCacheLayout, get_kv_quant_mode
|
|
|
|
|
|
class AttentionType(str, Enum):
|
|
"""Attention type.
|
|
Use string to be compatible with `torch.compile`.
|
|
"""
|
|
|
|
DECODER = "decoder"
|
|
"""Decoder attention between previous layer Q/K/V."""
|
|
ENCODER = "encoder"
|
|
"""Encoder attention between previous layer Q/K/V for encoder-decoder."""
|
|
ENCODER_ONLY = "encoder_only"
|
|
"""Encoder attention between previous layer Q/K/V."""
|
|
ENCODER_DECODER = "encoder_decoder"
|
|
"""Attention between dec. Q and enc. K/V for encoder-decoder."""
|
|
|
|
|
|
class MultipleOf:
|
|
base: int
|
|
|
|
def __init__(self, base: int):
|
|
self.base = base
|
|
|
|
|
|
class AttentionBackend(ABC):
|
|
"""Abstract class for attention backends."""
|
|
|
|
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
|
|
supported_kv_cache_dtypes: ClassVar[list["CacheDType"]] = [
|
|
"auto",
|
|
"float16",
|
|
"bfloat16",
|
|
]
|
|
|
|
# Does attention's forward() include kv cache update?
|
|
forward_includes_kv_cache_update: bool = True
|
|
|
|
@staticmethod
|
|
def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
|
return [MultipleOf(1)]
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def get_name() -> str:
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def get_impl_cls() -> type["AttentionImplBase"]:
|
|
raise NotImplementedError
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def get_builder_cls(): # -> Type["AttentionMetadataBuilder"]:
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def full_cls_name(cls) -> tuple[str, str]:
|
|
return (cls.__module__, cls.__qualname__)
|
|
|
|
@classmethod
|
|
def get_supported_head_sizes(cls) -> list[int]:
|
|
return []
|
|
|
|
@classmethod
|
|
def supports_head_size(cls, head_size: int) -> bool:
|
|
supported_head_sizes = cls.get_supported_head_sizes()
|
|
return (not supported_head_sizes) or head_size in supported_head_sizes
|
|
|
|
@classmethod
|
|
def supports_dtype(cls, dtype: torch.dtype) -> bool:
|
|
return dtype in cls.supported_dtypes
|
|
|
|
@classmethod
|
|
def supports_kv_cache_dtype(cls, kv_cache_dtype: "CacheDType | None") -> bool:
|
|
if kv_cache_dtype is None:
|
|
return True
|
|
return (not cls.supported_kv_cache_dtypes) or (
|
|
kv_cache_dtype in cls.supported_kv_cache_dtypes
|
|
)
|
|
|
|
@classmethod
|
|
def supports_block_size(cls, block_size: int | None) -> bool:
|
|
if block_size is None:
|
|
return True
|
|
|
|
supported_kernel_block_sizes = cls.get_supported_kernel_block_sizes()
|
|
if not supported_kernel_block_sizes:
|
|
return True
|
|
|
|
for supported_size in supported_kernel_block_sizes:
|
|
if isinstance(supported_size, MultipleOf):
|
|
supported_size = supported_size.base
|
|
# With hybrid_blocks feature, the framework-level block size
|
|
# only needs to be a multiple of the kernel's requirement,
|
|
# even if the kernel requires a fixed block_size.
|
|
if block_size % supported_size == 0:
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def customize_spec(cls, spec: "AttentionSpec") -> "AttentionSpec":
|
|
"""Adjust the layer's KV cache spec for this backend's kernels. Used when the
|
|
kernels want KV packed in a specific way.
|
|
|
|
NOTE: temporary compatibility API. Today the Attention layer builds the spec
|
|
from the model config and the backend only gets to adjust it post-hoc; the end
|
|
state is for the backend to build and return the spec directly, at which point
|
|
this hook goes away.
|
|
|
|
(see: https://github.com/vllm-project/vllm/issues/42449)
|
|
"""
|
|
return spec
|
|
|
|
@classmethod
|
|
def get_preferred_block_size(cls, default_block_size: int) -> int:
|
|
supported_sizes = cls.get_supported_kernel_block_sizes()
|
|
if not supported_sizes:
|
|
return default_block_size
|
|
|
|
if cls.supports_block_size(default_block_size):
|
|
return default_block_size
|
|
|
|
return min(s.base if isinstance(s, MultipleOf) else s for s in supported_sizes)
|
|
|
|
@classmethod
|
|
def is_mla(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_sink(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_alibi_sqrt(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_mm_prefix(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def is_sparse(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_per_head_quant_scales(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_sliding_window(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_rswa(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_non_causal(cls) -> bool:
|
|
"""Check if backend supports non-causal (bidirectional) attention
|
|
for decoder models.
|
|
|
|
Unlike ENCODER_ONLY attention type which implies a different
|
|
execution model, this refers to non-causal attention within the
|
|
standard paged-KV-cache decoder path.
|
|
"""
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_batch_invariance(cls) -> bool:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_kv_connector(cls) -> bool:
|
|
return True
|
|
|
|
@classmethod
|
|
def supports_device_cpu_query_lens_mismatch(cls) -> bool:
|
|
"""Whether this backend can run a batch whose device query_start_loc disagrees
|
|
with the CPU one; backends that plan off the CPU query lengths must opt out.
|
|
|
|
Currently only verification requests are affected: adaptive verification trims
|
|
their drafts on device. On the CPU the draft budget is evenly distributed across
|
|
requests, so the total draft budget, the decode/prefill split point and the CPU
|
|
prefill query lengths all stay correct.
|
|
|
|
SSM backends opt out: their recurrent-state planning is built from the CPU
|
|
per-request boundaries, which the trimmed batch no longer matches.
|
|
"""
|
|
return not cls.is_ssm()
|
|
|
|
@classmethod
|
|
def supports_pcp(cls) -> bool:
|
|
try:
|
|
return cls.get_impl_cls().supports_pcp
|
|
except NotImplementedError:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_dcp(cls) -> bool:
|
|
try:
|
|
return cls.get_impl_cls().supports_dcp
|
|
except NotImplementedError:
|
|
return False
|
|
|
|
@classmethod
|
|
def supports_non_causal_dcp(cls) -> bool:
|
|
builder_cls = cls.get_builder_cls()
|
|
return bool(getattr(builder_cls, "supports_non_causal_multi_token_dcp", False))
|
|
|
|
@classmethod
|
|
def supports_attn_type(cls, attn_type: str) -> bool:
|
|
"""Check if backend supports a given attention type.
|
|
|
|
By default, only supports decoder attention.
|
|
Backends should override this to support other attention types.
|
|
"""
|
|
return attn_type == AttentionType.DECODER
|
|
|
|
@classmethod
|
|
def supports_compute_capability(cls, capability: "DeviceCapability") -> bool:
|
|
return True
|
|
|
|
@classmethod
|
|
def supports_combination(
|
|
cls,
|
|
head_size: int,
|
|
dtype: torch.dtype,
|
|
kv_cache_dtype: "CacheDType | None",
|
|
block_size: int | None,
|
|
use_mla: bool,
|
|
has_sink: bool,
|
|
use_sparse: bool,
|
|
use_mm_prefix: bool,
|
|
device_capability: "DeviceCapability",
|
|
) -> str | None:
|
|
return None
|
|
|
|
@classmethod
|
|
def validate_configuration(
|
|
cls,
|
|
head_size: int,
|
|
dtype: torch.dtype,
|
|
kv_cache_dtype: "CacheDType | None",
|
|
block_size: int | None,
|
|
use_mla: bool,
|
|
has_sink: bool,
|
|
use_sparse: bool,
|
|
use_mm_prefix: bool,
|
|
use_per_head_quant_scales: bool,
|
|
device_capability: "DeviceCapability",
|
|
attn_type: str,
|
|
has_sliding_window: bool = False,
|
|
use_non_causal: bool = False,
|
|
use_batch_invariant: bool = False,
|
|
use_kv_connector: bool = False,
|
|
use_pcp: bool = False,
|
|
use_adaptive_verification: bool = False,
|
|
use_dcp: bool = False,
|
|
use_rswa: bool = False,
|
|
) -> list[str]:
|
|
invalid_reasons = []
|
|
if not cls.supports_head_size(head_size):
|
|
invalid_reasons.append("head_size not supported")
|
|
if not cls.supports_dtype(dtype):
|
|
invalid_reasons.append("dtype not supported")
|
|
if not cls.supports_kv_cache_dtype(kv_cache_dtype):
|
|
invalid_reasons.append("kv_cache_dtype not supported")
|
|
if not cls.supports_block_size(block_size):
|
|
invalid_reasons.append("block_size not supported")
|
|
if use_mm_prefix and not cls.supports_mm_prefix():
|
|
invalid_reasons.append(
|
|
"partial multimodal token full attention not supported"
|
|
)
|
|
if use_mla != cls.is_mla():
|
|
if use_mla:
|
|
invalid_reasons.append("MLA not supported")
|
|
else:
|
|
invalid_reasons.append("non-MLA not supported")
|
|
if has_sink and not cls.supports_sink():
|
|
invalid_reasons.append("attention sinks not supported")
|
|
if use_sparse != cls.is_sparse():
|
|
if use_sparse:
|
|
invalid_reasons.append("sparse not supported")
|
|
else:
|
|
invalid_reasons.append("non-sparse not supported")
|
|
if use_per_head_quant_scales and not cls.supports_per_head_quant_scales():
|
|
invalid_reasons.append("per-head quant scales not supported")
|
|
if not cls.supports_compute_capability(device_capability):
|
|
invalid_reasons.append("compute capability not supported")
|
|
if not cls.supports_attn_type(attn_type):
|
|
invalid_reasons.append(f"attention type {attn_type} not supported")
|
|
if has_sliding_window and not cls.supports_sliding_window():
|
|
invalid_reasons.append("sliding window not supported")
|
|
if use_rswa and not cls.supports_rswa():
|
|
invalid_reasons.append("R-SWA not supported")
|
|
if use_non_causal and not cls.supports_non_causal():
|
|
invalid_reasons.append("non-causal attention not supported")
|
|
if use_mla and use_non_causal and use_dcp and not cls.supports_non_causal_dcp():
|
|
invalid_reasons.append("non-causal MLA attention with DCP not supported")
|
|
if use_batch_invariant and not cls.supports_batch_invariance():
|
|
invalid_reasons.append("batch invariance not supported")
|
|
if use_kv_connector and not cls.supports_kv_connector():
|
|
invalid_reasons.append("KV connector not supported")
|
|
if use_pcp and not cls.supports_pcp():
|
|
invalid_reasons.append("PCP not supported")
|
|
if use_dcp and not cls.supports_dcp():
|
|
invalid_reasons.append("DCP not supported")
|
|
if (
|
|
use_adaptive_verification
|
|
and not cls.supports_device_cpu_query_lens_mismatch()
|
|
):
|
|
invalid_reasons.append(
|
|
"device-cpu query lens mismatch not supported, "
|
|
"this is needed for adaptive verification"
|
|
)
|
|
combination_reason = cls.supports_combination(
|
|
head_size,
|
|
dtype,
|
|
kv_cache_dtype,
|
|
block_size,
|
|
use_mla,
|
|
has_sink,
|
|
use_sparse,
|
|
use_mm_prefix,
|
|
device_capability,
|
|
)
|
|
if combination_reason is not None:
|
|
invalid_reasons.append(combination_reason)
|
|
return invalid_reasons
|
|
|
|
@classmethod
|
|
def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...] | None:
|
|
"""Layouts this backend's kernels can consume, most preferred first, or
|
|
None when the kernels consume any layout and express no preference."""
|
|
return None
|
|
|
|
@classmethod
|
|
def is_ssm(cls) -> bool:
|
|
return False
|
|
|
|
|
|
class AttentionMetadata:
|
|
pass
|
|
|
|
|
|
T = TypeVar("T", bound=AttentionMetadata)
|
|
|
|
|
|
@dataclass
|
|
class CommonAttentionMetadata:
|
|
"""Per-batch attention metadata, shared across layers and backends.
|
|
AttentionMetadataBuilder instances use it to construct per-layer metadata.
|
|
|
|
For many of the tensors we keep both GPU and CPU versions.
|
|
"""
|
|
|
|
query_start_loc: torch.Tensor
|
|
query_start_loc_cpu: torch.Tensor
|
|
"""(batch_size + 1,), the start location of each request in query Tensor"""
|
|
|
|
seq_lens: torch.Tensor
|
|
"""(batch_size,), the number of computed tokens for each request"""
|
|
|
|
num_reqs: int
|
|
"""Number of requests"""
|
|
# TODO(lucas): rename to num_tokens since it may be padded and this is misleading
|
|
num_actual_tokens: int
|
|
"""Total number of tokens in batch"""
|
|
max_query_len: int
|
|
"""Longest query in batch"""
|
|
max_seq_len: int
|
|
"""Longest context length (may be an upper bound)"""
|
|
|
|
block_table_tensor: torch.Tensor
|
|
slot_mapping: torch.Tensor
|
|
|
|
causal: bool | torch.Tensor = True
|
|
|
|
# Needed by FastPrefillAttentionBuilder
|
|
logits_indices_padded: torch.Tensor | None = None
|
|
num_logits_indices: int | None = None
|
|
max_logits_per_req: int | None = None
|
|
|
|
# Needed by CrossAttentionBuilder
|
|
encoder_seq_lens: torch.Tensor | None = None
|
|
encoder_seq_lens_cpu: np.ndarray | None = None
|
|
|
|
dcp_local_seq_lens: torch.Tensor | None = None
|
|
"""Sequence lengths of the local rank in decode context parallelism world"""
|
|
|
|
dcp_local_seq_lens_cpu_upper_bound: torch.Tensor | None = None
|
|
"""(batch_size,) CPU upper bound on dcp_local_seq_lens. Under PCP+DCP it
|
|
holds, on every row, the largest shard any DCP rank has of the row's
|
|
whole request, identical on every PCP rank, so the sparse backends pad
|
|
their KV gather to it."""
|
|
|
|
positions: torch.Tensor | None = None
|
|
"""(num_actual_tokens,) token positions. Optional; set when the caller
|
|
has positions available so that builders can pre-compute position-dependent
|
|
sparse metadata for DeepSeek V4 C128A layers."""
|
|
|
|
is_prefilling: torch.Tensor | None = None
|
|
"""(batch_size,) bool tensor: True if request is still in prefill phase
|
|
(num_computed_tokens < num_prompt_tokens). Used by some backends to
|
|
distinguish actual decodes from short extends."""
|
|
|
|
seq_lens_cpu_upper_bound: torch.Tensor | None = None
|
|
"""(batch_size,) CPU upper bound on seq_lens. Precise for prefill rows
|
|
and for all rows outside async spec decode; optimistic for async-spec
|
|
decode rows (assumes every draft was accepted). Not safe for kernels
|
|
that need exact per-row context lengths on decode rows."""
|
|
|
|
req_idx: np.ndarray | None = None
|
|
"""(batch_size,) index of each row's request in the runner's request
|
|
table. Rows of one request are adjacent, so equal neighbours are PCP
|
|
chunks sharing one KV context."""
|
|
|
|
mm_req_doc_ranges: dict[int, list[tuple[int, int]]] | None = None
|
|
"""PrefixLM bidirectional ranges for multimodal tokens. Maps
|
|
request index to list of (start, end) token position ranges
|
|
where bidirectional attention should apply. None for text-only
|
|
batches or non-PrefixLM models. A request's ranges must not overlap."""
|
|
|
|
rswa_prefix_lens: torch.Tensor | None = None
|
|
"""(batch_size,) per-request prefix length (prompt/image token count) for
|
|
Reference Sliding Window Attention (R-SWA). Tokens with logical index below
|
|
this stay globally visible; later (generated) tokens additionally see a
|
|
fixed sliding window. None disables R-SWA. The attention backend copies this
|
|
into its own persistent buffer and reads ``rswa_window`` from model config."""
|
|
|
|
replayssm_decode_base_cpu: torch.Tensor | None = None
|
|
"""(batch_size,) CPU ring origin for Mamba2 ReplaySSM decode: num_computed
|
|
at the current decode run's last full-state write. write_pos counts from
|
|
here, so a preemption-resumed request re-anchors past the prompt boundary."""
|
|
|
|
_num_computed_tokens_cache: torch.Tensor | None = None
|
|
_token_to_req_indices_cache: torch.Tensor | None = None
|
|
|
|
def batch_size(self) -> int:
|
|
return self.seq_lens.shape[0]
|
|
|
|
def naive_query_lens(self) -> torch.Tensor:
|
|
"""Naive because it assumes that query ends where the next query starts."""
|
|
return self.query_start_loc[1:] - self.query_start_loc[:-1]
|
|
|
|
def replace(self, **kwargs) -> "CommonAttentionMetadata":
|
|
return replace(self, **kwargs)
|
|
|
|
def compute_num_computed_tokens(self) -> torch.Tensor:
|
|
"""Compute num_computed_tokens on device (seq_lens - query_lens)."""
|
|
if self._num_computed_tokens_cache is None:
|
|
query_lens = self.query_start_loc[1:] - self.query_start_loc[:-1]
|
|
self._num_computed_tokens_cache = self.seq_lens - query_lens
|
|
return self._num_computed_tokens_cache
|
|
|
|
def token_to_req_indices(self, buffer: torch.Tensor) -> torch.Tensor:
|
|
"""Build or reuse the per-token request index mapping."""
|
|
num_tokens = self.num_actual_tokens
|
|
if self._token_to_req_indices_cache is not None:
|
|
assert self._token_to_req_indices_cache.device == buffer.device
|
|
assert self._token_to_req_indices_cache.dtype == torch.int32
|
|
assert self._token_to_req_indices_cache.shape[0] >= num_tokens
|
|
return self._token_to_req_indices_cache[:num_tokens]
|
|
|
|
# Built from the device query_start_loc: adaptive verification decides the
|
|
# per-request draft split on device, so the CPU copy carries the right total
|
|
# but not the right per-request boundaries. Padding requests have a query
|
|
# length of zero and are skipped by the device boundary search.
|
|
num_mapped_tokens = int(self.query_start_loc_cpu[-1])
|
|
from vllm.v1.attention.ops.metadata import _token_request_mapping_kernel
|
|
|
|
num_output_tokens = max(num_mapped_tokens, num_tokens)
|
|
assert buffer.shape[0] >= num_output_tokens
|
|
_token_request_mapping_kernel[((num_output_tokens + 255) // 256,)](
|
|
self.query_start_loc,
|
|
buffer,
|
|
self.query_start_loc.shape[0] - 1,
|
|
num_mapped_tokens,
|
|
num_output_tokens,
|
|
num_warps=4,
|
|
)
|
|
self._token_to_req_indices_cache = buffer[:num_output_tokens]
|
|
return self._token_to_req_indices_cache[:num_tokens]
|
|
|
|
# TODO(lucas): remove once we have FULL-CG spec-decode support
|
|
def unpadded(
|
|
self, num_actual_tokens: int, num_actual_reqs: int
|
|
) -> "CommonAttentionMetadata":
|
|
maybe_slice_reqs = lambda x: x[:num_actual_reqs] if x is not None else None
|
|
return CommonAttentionMetadata(
|
|
query_start_loc=self.query_start_loc[: num_actual_reqs + 1],
|
|
query_start_loc_cpu=self.query_start_loc_cpu[: num_actual_reqs + 1],
|
|
seq_lens=self.seq_lens[:num_actual_reqs],
|
|
num_reqs=num_actual_reqs,
|
|
num_actual_tokens=num_actual_tokens,
|
|
max_query_len=self.max_query_len,
|
|
max_seq_len=self.max_seq_len,
|
|
block_table_tensor=self.block_table_tensor[:num_actual_reqs],
|
|
slot_mapping=self.slot_mapping[:num_actual_tokens],
|
|
causal=self.causal[:num_actual_reqs]
|
|
if isinstance(self.causal, torch.Tensor)
|
|
else self.causal,
|
|
logits_indices_padded=self.logits_indices_padded,
|
|
num_logits_indices=self.num_logits_indices,
|
|
max_logits_per_req=self.max_logits_per_req,
|
|
encoder_seq_lens=maybe_slice_reqs(self.encoder_seq_lens),
|
|
encoder_seq_lens_cpu=maybe_slice_reqs(self.encoder_seq_lens_cpu),
|
|
dcp_local_seq_lens=maybe_slice_reqs(self.dcp_local_seq_lens),
|
|
dcp_local_seq_lens_cpu_upper_bound=maybe_slice_reqs(
|
|
self.dcp_local_seq_lens_cpu_upper_bound
|
|
),
|
|
seq_lens_cpu_upper_bound=maybe_slice_reqs(self.seq_lens_cpu_upper_bound),
|
|
is_prefilling=maybe_slice_reqs(self.is_prefilling),
|
|
req_idx=maybe_slice_reqs(self.req_idx),
|
|
rswa_prefix_lens=maybe_slice_reqs(self.rswa_prefix_lens),
|
|
replayssm_decode_base_cpu=maybe_slice_reqs(self.replayssm_decode_base_cpu),
|
|
)
|
|
|
|
|
|
M = TypeVar("M")
|
|
|
|
|
|
class AttentionCGSupport(Enum):
|
|
"""Constants for the cudagraph support of the attention backend
|
|
Here we do not consider the cascade attention, as currently
|
|
it is never cudagraph supported."""
|
|
|
|
ALWAYS = 3
|
|
"""Cudagraph always supported; supports mixed-prefill-decode"""
|
|
UNIFORM_BATCH = 2
|
|
"""Cudagraph supported for batches the only contain query lengths that are
|
|
the same, this can be used for spec-decode
|
|
i.e. "decodes" are 1 + num_speculative_tokens"""
|
|
UNIFORM_SINGLE_TOKEN_DECODE = 1
|
|
"""Cudagraph supported for batches the only contain query_len==1 decodes"""
|
|
NEVER = 0
|
|
"""NO cudagraph support"""
|
|
|
|
|
|
class AttentionMetadataBuilder(ABC, Generic[M]):
|
|
# Does this backend/builder support CUDA Graphs for attention (default: no).
|
|
# Do not access directly. Call get_cudagraph_support() instead.
|
|
_cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.NEVER
|
|
# Does this backend/builder reorder the batch?
|
|
# If not, set this to None. Otherwise set it to the query
|
|
# length that will be pulled into the front of the batch.
|
|
reorder_batch_threshold: int | None = None
|
|
# Does this backend/builder support updating the block table in existing
|
|
# metadata
|
|
supports_update_block_table: bool = False
|
|
# Whether the builder constructor requires the block-table width.
|
|
requires_block_table_width: ClassVar[bool] = False
|
|
# Whether all step-dependent draft decode metadata can be updated in place,
|
|
# allowing one metadata build to be reused across autoregressive draft steps.
|
|
supports_draft_decode_metadata_update: bool = False
|
|
|
|
@abstractmethod
|
|
def __init__(
|
|
self,
|
|
kv_cache_spec: "KVCacheSpec",
|
|
layer_names: list[str],
|
|
vllm_config: "VllmConfig",
|
|
device: torch.device,
|
|
):
|
|
self.kv_cache_spec = kv_cache_spec
|
|
self.layer_names = layer_names
|
|
self.vllm_config = vllm_config
|
|
self.device = device
|
|
self.kernel_block_size: int | None = None
|
|
|
|
def set_kernel_block_size(self, kernel_block_size: int) -> None:
|
|
self.kernel_block_size = kernel_block_size
|
|
|
|
@classmethod
|
|
def get_cudagraph_support(
|
|
cls: type["AttentionMetadataBuilder"],
|
|
vllm_config: "VllmConfig",
|
|
kv_cache_spec: "KVCacheSpec",
|
|
) -> AttentionCGSupport:
|
|
"""Get the cudagraph support level of this builder class."""
|
|
return cls._cudagraph_support
|
|
|
|
def _init_reorder_batch_threshold(
|
|
self,
|
|
reorder_batch_threshold: int | None = 1,
|
|
supports_spec_as_decode: bool = False,
|
|
supports_dcp_with_varlen: bool = False,
|
|
) -> None:
|
|
self.reorder_batch_threshold = reorder_batch_threshold
|
|
if self.reorder_batch_threshold is not None and supports_spec_as_decode:
|
|
# If the backend supports spec-as-decode kernels, then we can set
|
|
# the reorder_batch_threshold based on the number of speculative
|
|
# tokens from the config.
|
|
speculative_config = self.vllm_config.speculative_config
|
|
if (
|
|
speculative_config is not None
|
|
and speculative_config.num_speculative_tokens is not None
|
|
):
|
|
max_num_queries_for_spec = (
|
|
1
|
|
+ (2 if speculative_config.parallel_drafting else 1)
|
|
* speculative_config.num_speculative_tokens
|
|
)
|
|
self.reorder_batch_threshold = max(
|
|
self.reorder_batch_threshold,
|
|
max_num_queries_for_spec,
|
|
)
|
|
|
|
if (
|
|
self.vllm_config.parallel_config.decode_context_parallel_size > 1
|
|
and not supports_dcp_with_varlen
|
|
):
|
|
self.reorder_batch_threshold = 1
|
|
|
|
@abstractmethod
|
|
def build(
|
|
self,
|
|
common_prefix_len: int,
|
|
common_attn_metadata: CommonAttentionMetadata,
|
|
fast_build: bool = False,
|
|
) -> M:
|
|
"""Central method that builds attention metadata.
|
|
Some builders (MLA) require reorder_batch to be called prior to build.
|
|
|
|
Args:
|
|
common_prefix_len: The length of the common prefix of the batch.
|
|
common_attn_metadata: The common attention metadata.
|
|
fast_build: The meta-data will prioritize speed of building over
|
|
then speed at execution. Can be used for spec-decode where the
|
|
result of a build call may only be used for few layers/iters.
|
|
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def update_block_table(
|
|
self,
|
|
metadata: M,
|
|
blk_table: torch.Tensor,
|
|
slot_mapping: torch.Tensor,
|
|
) -> M:
|
|
"""Update the block table for the attention metadata.
|
|
Faster when theres multiple kv-cache groups that create virtually the
|
|
same metadata but just with different block tables.
|
|
|
|
Only needs to be implemented if supports_update_block_table is True.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def build_for_cudagraph_capture(
|
|
self, common_attn_metadata: CommonAttentionMetadata
|
|
) -> M:
|
|
"""Build attention metadata for CUDA graph capture. Uses build by default.
|
|
Subclasses that override this method should call self.build or
|
|
super().build_for_cudagraph_capture.
|
|
"""
|
|
return self.build(
|
|
common_prefix_len=0, common_attn_metadata=common_attn_metadata
|
|
)
|
|
|
|
def build_for_drafting(
|
|
self,
|
|
common_attn_metadata: CommonAttentionMetadata,
|
|
draft_index: int,
|
|
) -> M:
|
|
"""Build attention metadata for draft model. Uses build by default.
|
|
|
|
Args:
|
|
common_attn_metadata: The common attention metadata.
|
|
draft_index: The index of the current draft operation.
|
|
When speculating a chain of tokens, this index refers to the
|
|
draft attempt for the i-th token.
|
|
For tree-based attention, this index instead refers to the
|
|
draft attempt for the i-th level in the tree of tokens.
|
|
|
|
"""
|
|
return self.build(
|
|
common_prefix_len=0,
|
|
common_attn_metadata=common_attn_metadata,
|
|
fast_build=True,
|
|
)
|
|
|
|
def update_draft_decode_metadata(self, metadata: M) -> None:
|
|
"""Update step-dependent draft decode metadata in place.
|
|
|
|
The fused draft loop may call this method during full CUDA graph
|
|
capture. CUDA graph replay does not run this Python method, so
|
|
implementations must emit capture-safe operations and keep replayed
|
|
tensor state in persistent storage.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def use_cascade_attention(
|
|
self,
|
|
common_prefix_len: int,
|
|
query_lens: np.ndarray,
|
|
num_query_heads: int,
|
|
num_kv_heads: int,
|
|
use_alibi: bool,
|
|
use_sliding_window: bool,
|
|
use_local_attention: bool,
|
|
num_sms: int,
|
|
dcp_world_size: int,
|
|
) -> bool:
|
|
return False
|
|
|
|
|
|
class AttentionLayer(Protocol):
|
|
_q_scale: torch.Tensor
|
|
_k_scale: torch.Tensor
|
|
_k_scale_cpu: torch.Tensor
|
|
_v_scale: torch.Tensor
|
|
_v_scale_cpu: torch.Tensor
|
|
_q_scale_float: float
|
|
_k_scale_float: float
|
|
_v_scale_float: float
|
|
_prob_scale: torch.Tensor
|
|
|
|
def forward(
|
|
self,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
kv_cache: torch.Tensor,
|
|
attn_metadata: AttentionMetadata,
|
|
) -> torch.Tensor: ...
|
|
|
|
|
|
class AttentionImplBase(ABC, Generic[T]):
|
|
"""Base class for attention implementations.
|
|
|
|
Contains common attributes and initialization logic shared by both
|
|
standard AttentionImpl and MLAAttentionImpl. Does not define a forward
|
|
method - subclasses define their own forward interfaces.
|
|
"""
|
|
|
|
# Whether this impl uses a sparse (top-k) attention path. Used by MLA to
|
|
# route between the dense-MHA prefill and sparse-MQA paths.
|
|
is_sparse: ClassVar[bool] = False
|
|
|
|
# Whether this impl provides a dense-MHA prefill path (forward_mha). Sparse
|
|
# impls without one run the top-k MQA path for all requests.
|
|
supports_dense_mha_prefill: ClassVar[bool] = True
|
|
|
|
# Required attributes that all impls should have
|
|
num_heads: int
|
|
head_size: int
|
|
scale: float
|
|
|
|
# Whether the attention impl can return the softmax lse for decode.
|
|
# Some features like decode context parallelism require the softmax lse.
|
|
can_return_lse_for_decode: bool = False
|
|
|
|
# Base of the logarithm used by this backend when returning softmax lse.
|
|
# True => natural log (lse = ln(sum(exp(qk))))
|
|
# -- e.g. Triton MLA, FlashAttention, FlashMLA, Cutlass MLA
|
|
# False => base 2 (lse = log2(sum(exp(qk))))
|
|
# -- e.g. FlashInfer trtllm-gen MLA
|
|
# The DCP combine kernel (cp_lse_ag_out_rs / dcp_a2a_lse_reduce in
|
|
# vllm/v1/attention/ops/dcp.py) branches on this via its IS_BASE_E
|
|
# constexpr; getting it wrong silently corrupts the cross-shard
|
|
# softmax denominator.
|
|
lse_base_on_e: bool = True
|
|
|
|
# Whether the attention impl supports Prefill Context Parallelism.
|
|
supports_pcp: bool = False
|
|
# Whether the attention impl supports Decode Context Parallelism.
|
|
supports_dcp: bool = False
|
|
# Whether the attention impl(or ops) supports MTP
|
|
# when cp_kv_cache_interleave_size > 1
|
|
supports_mtp_with_cp_non_trivial_interleave_size: bool = False
|
|
|
|
# some attention backends might not always want to return lse
|
|
# even if they can return lse (for efficiency reasons)
|
|
need_to_return_lse_for_decode: bool = False
|
|
|
|
# Whether this attention implementation supports pre-quantized query input.
|
|
# When True, the attention layer will quantize queries before passing them
|
|
# to this backend, allowing torch.compile to fuse the quantization with
|
|
# previous operations. This is typically supported when using FP8 KV cache
|
|
# with compatible attention kernels (e.g., TRT-LLM).
|
|
# Subclasses should set this in __init__.
|
|
# TODO add support to more backends:
|
|
# https://github.com/vllm-project/vllm/issues/25584
|
|
supports_quant_query_input: bool = False
|
|
|
|
dcp_world_size: int
|
|
dcp_rank: int
|
|
|
|
pcp_world_size: int
|
|
pcp_rank: int
|
|
|
|
total_cp_world_size: int
|
|
total_cp_rank: int
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
# use __new__ so that all subclasses will call this
|
|
self = super().__new__(cls)
|
|
try:
|
|
from vllm.distributed.parallel_state import get_dcp_group
|
|
|
|
self.dcp_world_size = get_dcp_group().world_size
|
|
self.dcp_rank = get_dcp_group().rank_in_group
|
|
except AssertionError:
|
|
# DCP might not be initialized in testing
|
|
self.dcp_world_size = 1
|
|
self.dcp_rank = 0
|
|
try:
|
|
from vllm.distributed.parallel_state import get_pcp_group
|
|
|
|
self.pcp_world_size = get_pcp_group().world_size
|
|
self.pcp_rank = get_pcp_group().rank_in_group
|
|
except AssertionError:
|
|
self.pcp_world_size = 1
|
|
self.pcp_rank = 0
|
|
self.total_cp_world_size = self.dcp_world_size
|
|
self.total_cp_rank = self.dcp_rank
|
|
|
|
self.need_to_return_lse_for_decode = (
|
|
self.dcp_world_size > 1 and self.can_return_lse_for_decode
|
|
)
|
|
return self
|
|
|
|
def process_weights_after_loading(self, act_dtype: torch.dtype):
|
|
pass
|
|
|
|
def get_impl_variants(self) -> tuple["AttentionImplBase", ...]:
|
|
return (self,)
|
|
|
|
def prepare_for_batch(self, attn_metadata: T | None) -> None:
|
|
"""Prepare implementation-specific state for the current batch."""
|
|
|
|
|
|
class AttentionImpl(AttentionImplBase[T], Generic[T]):
|
|
"""Standard attention implementation with forward method."""
|
|
|
|
kv_cache_dtype: str
|
|
|
|
@property
|
|
def kv_quant_mode(self) -> "KVQuantMode":
|
|
"""Return the KV cache quantization mode for this layer."""
|
|
return get_kv_quant_mode(self.kv_cache_dtype)
|
|
|
|
@abstractmethod
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
scale: float,
|
|
num_kv_heads: int | None = None,
|
|
alibi_slopes: list[float] | None = None,
|
|
sliding_window: int | None = None,
|
|
kv_cache_dtype: str = "auto",
|
|
logits_soft_cap: float | None = None,
|
|
attn_type: str = AttentionType.DECODER,
|
|
kv_sharing_target_layer_name: str | None = None,
|
|
) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def forward(
|
|
self,
|
|
layer: AttentionLayer,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
kv_cache: torch.Tensor,
|
|
attn_metadata: T,
|
|
output: torch.Tensor,
|
|
output_scale: torch.Tensor | None = None,
|
|
output_block_scale: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
raise NotImplementedError
|
|
|
|
def fused_output_quant_supported(self, quant_key: "QuantKey") -> bool:
|
|
"""Does this attention implementation support fused output quantization.
|
|
This is used by the AttnFusionPass to only fuse output quantization
|
|
onto implementations that support it.
|
|
|
|
Args:
|
|
quant_key: QuantKey object that describes the quantization op
|
|
|
|
Returns:
|
|
is fusion supported for this type of quantization
|
|
|
|
"""
|
|
return False
|
|
|
|
def fused_qk_norm_rope_kvcache_supported(self):
|
|
"""Does this attention implementation support fused QKNorm+RoPE+KVCache fusion.
|
|
This is used by the QkNormRopeKvCachePattern to only fuse the QKNorm ops
|
|
with the RoPE ops and the KV cache update for implementations that support it.
|
|
"""
|
|
return False
|
|
|
|
def fused_rope_kvcache_supported(self):
|
|
"""Does this attention implementation support RoPE+KVCache fusion.
|
|
This is used by the RopeKVCacheFusionPass to only fuse the RoPE ops
|
|
with the KV cache update for implementations that support it.
|
|
"""
|
|
return False
|
|
|
|
def do_qk_norm_rope_kvcache_update(
|
|
self,
|
|
layer: AttentionLayer,
|
|
qkv: torch.Tensor,
|
|
q_out: torch.Tensor,
|
|
k_out: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
q_weight: torch.Tensor,
|
|
k_weight: torch.Tensor,
|
|
rms_norm_eps: float,
|
|
cos_sin_cache: torch.Tensor,
|
|
is_neox: bool,
|
|
kv_cache: torch.Tensor,
|
|
layer_slot_mapping: torch.Tensor,
|
|
):
|
|
"""If `fused_qk_norm_rope_kvcache_supported` returns True, this method
|
|
will be called by the fused custom op. Applies QK-norm + RoPE and
|
|
writes K/V to the KV cache. Results are written to the pre-allocated
|
|
q_out and k_out tensors; V is split from QKV at the graph level.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def do_rope_and_kv_cache_update(
|
|
self,
|
|
layer: AttentionLayer,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
cos_sin_cache: torch.Tensor,
|
|
is_neox: bool,
|
|
kv_cache: torch.Tensor,
|
|
layer_slot_mapping: torch.Tensor,
|
|
):
|
|
"""If `fused_rope_kvcache_supported` returns True, this method will be called
|
|
by torch.ops.vllm.fused_rope_and_unified_kv_cache_update
|
|
to perform the inplace RoPE and KV cache update.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
|
|
class MLAAttentionImpl(AttentionImplBase[T], Generic[T]):
|
|
"""MLA attention implementation with forward_mqa and forward_mha methods."""
|
|
|
|
hisparse_cache: "HiSparseCacheHandle | None" = None
|
|
supports_pcp: bool = True
|
|
|
|
@abstractmethod
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
scale: float,
|
|
num_kv_heads: int,
|
|
alibi_slopes: list[float] | None,
|
|
sliding_window: int | None,
|
|
kv_cache_dtype: str,
|
|
logits_soft_cap: float | None,
|
|
attn_type: str,
|
|
kv_sharing_target_layer_name: str | None,
|
|
# MLA Specific Arguments
|
|
q_lora_rank: int | None,
|
|
kv_lora_rank: int,
|
|
qk_nope_head_dim: int,
|
|
qk_rope_head_dim: int,
|
|
qk_head_dim: int,
|
|
v_head_dim: int,
|
|
kv_b_proj: "ColumnParallelLinear",
|
|
indexer: object | None = None,
|
|
q_pad_num_heads: int | None = None,
|
|
) -> None:
|
|
raise NotImplementedError
|
|
|
|
def forward_mha(
|
|
self,
|
|
q: torch.Tensor,
|
|
kv_c_normed: torch.Tensor,
|
|
k_pe: torch.Tensor,
|
|
kv_c_and_k_pe_cache: torch.Tensor,
|
|
attn_metadata: T,
|
|
k_scale: torch.Tensor,
|
|
output: torch.Tensor,
|
|
output_scale: torch.Tensor | None = None,
|
|
) -> None:
|
|
"""MHA-style prefill forward pass."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def forward_mqa(
|
|
self,
|
|
q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
|
|
kv_c_and_k_pe_cache: torch.Tensor,
|
|
attn_metadata: T,
|
|
layer: AttentionLayer,
|
|
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
|
"""MQA-style decode forward pass."""
|
|
raise NotImplementedError
|
|
|
|
def fused_output_quant_supported(self, quant_key: "QuantKey"):
|
|
"""Does this attention implementation support fused output quantization.
|
|
Since MLA quantization is done manually in forward_impl (common code),
|
|
all MLA backends support it by default.
|
|
"""
|
|
return quant_key in (
|
|
kFp8StaticTensorSym,
|
|
kNvfp4Dynamic,
|
|
kFp8Dynamic128Sym,
|
|
kFp8Dynamic64Sym,
|
|
)
|
|
|
|
def do_kv_cache_update(
|
|
self,
|
|
kv_c_normed: torch.Tensor,
|
|
k_pe: torch.Tensor,
|
|
kv_cache: torch.Tensor,
|
|
slot_mapping: torch.Tensor,
|
|
kv_cache_dtype: str,
|
|
k_scale: torch.Tensor,
|
|
) -> None:
|
|
if kv_cache.numel() == 0:
|
|
return
|
|
from vllm import _custom_ops as ops
|
|
|
|
ops.concat_and_cache_mla(
|
|
kv_c_normed,
|
|
k_pe.squeeze(1),
|
|
kv_cache,
|
|
slot_mapping.flatten(),
|
|
kv_cache_dtype=kv_cache_dtype,
|
|
scale=k_scale,
|
|
)
|
|
|
|
|
|
def subclass_attention_backend(
|
|
name_prefix: str,
|
|
attention_backend_cls: type[AttentionBackend],
|
|
builder_cls: type[AttentionMetadataBuilder[M]],
|
|
) -> type[AttentionBackend]:
|
|
"""Return a new subclass where `get_builder_cls` returns `builder_cls`."""
|
|
name: str = name_prefix + attention_backend_cls.__name__ # type: ignore
|
|
|
|
return type(
|
|
name, (attention_backend_cls,), {"get_builder_cls": lambda: builder_cls}
|
|
)
|
|
|
|
|
|
def subclass_attention_backend_with_overrides(
|
|
name_prefix: str,
|
|
attention_backend_cls: type[AttentionBackend],
|
|
overrides: dict[str, Any],
|
|
) -> type[AttentionBackend]:
|
|
name: str = name_prefix + attention_backend_cls.__name__ # type: ignore
|
|
return type(name, (attention_backend_cls,), overrides)
|