Files
vLLM/vllm/v1/attention/backends/flashinfer.py
T
2026-10-12 10:41:07 -07:00

2795 lines
117 KiB
Python
Executable File

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Attention layer with FlashInfer."""
from dataclasses import dataclass, replace
from enum import Enum
from functools import partial
from typing import ClassVar
import numpy as np
import torch
from flashinfer import (
BatchAttentionWithAttentionSinkWrapper,
BatchDecodeWithPagedKVCacheWrapper,
BatchPrefillWithPagedKVCacheWrapper,
BatchPrefillWithRaggedKVCacheWrapper,
get_seq_lens,
nvfp4_quantize_append_paged_kv_cache_with_slot_mapping,
)
from flashinfer.decode import fast_decode_plan, trtllm_batch_decode_with_kv_cache
from flashinfer.prefill import trtllm_batch_context_with_kv_cache
from flashinfer.utils import FP4Tensor
from typing_extensions import override
from vllm import _custom_ops as custom_ops
from vllm import envs
from vllm.config import (
CUDAGraphMode,
VllmConfig,
get_current_vllm_config_or_none,
)
from vllm.config.cache import CacheDType
from vllm.distributed.parallel_state import (
get_dcp_group,
get_dcp_world_size_and_rank,
)
from vllm.logger import init_logger
from vllm.model_executor.layers.quantization.utils.quant_utils import (
QuantKey,
kFp8StaticTensorSym,
kNvfp4Dynamic,
)
from vllm.platforms import current_platform
from vllm.platforms.interface import DeviceCapability
from vllm.triton_utils import tl, triton
from vllm.utils.flashinfer import (
can_use_trtllm_attention,
flashinfer_xqa_batch_decode_with_kv_cache,
force_use_trtllm_attention,
pin_host_range_buf,
supports_trtllm_attention,
use_trtllm_attention,
)
from vllm.utils.gpu_sync_debug import gpu_sync_allowed
from vllm.utils.math_utils import cdiv
from vllm.utils.torch_utils import (
PIN_MEMORY,
canonicalize_singleton_dim_strides,
get_dtype_size,
is_quantized_kv_cache,
is_strictly_contiguous,
nvfp4_kv_cache_full_dim,
nvfp4_split_data_scale,
)
from vllm.v1.attention.backend import (
AttentionBackend,
AttentionCGSupport,
AttentionImpl,
AttentionMetadataBuilder,
AttentionType,
CommonAttentionMetadata,
MultipleOf,
max_decode_query_len,
)
from vllm.v1.attention.backends.utils import (
get_dcp_local_seq_lens,
get_flashinfer_layout_string,
get_num_attention_heads_from_layers,
get_per_layer_parameters,
infer_global_hyperparameters,
log2_lse_to_ln,
split_decodes_and_prefills,
)
from vllm.v1.attention.ops.dcp import (
cp_lse_ag_out_rs,
dcp_a2a_lse_reduce,
)
from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
from vllm.v1.kv_cache_interface import (
AttentionSpec,
KVCacheLayout,
KVCacheSpec,
KVQuantMode,
iter_layer_specs,
)
from vllm.v1.utils import CpuGpuBuffer
FLASHINFER_WORKSPACE_BUFFER_SIZE_BATCH_INVARIANT = 2048 * 1024 * 1024
FLASHINFER_PREFILL_WORKSPACE_BYTES_PER_ELEM = 16
FP8_DTYPE = current_platform.fp8_dtype()
FP4_DTYPE = torch.uint8
logger = init_logger(__name__)
trtllm_workspace_buffer = None
def _nvfp4_kv_on_fa2() -> bool:
return any(current_platform.is_device_capability_family(f) for f in (80, 90, 120))
_KVPair = tuple[torch.Tensor, torch.Tensor]
def _nvfp4_kv_views(
kv_cache: torch.Tensor, num_kv_heads: int
) -> tuple[_KVPair, _KVPair, str]:
"""Views of NVFP4 pages laid out as [K data | K scale | V data | V scale]."""
num_blocks, num_slots, block_size, full_dim = kv_cache.shape
hnd = kv_cache.stride()[1:] == (block_size * full_dim, full_dim, 1)
if not hnd and kv_cache.stride()[1:] != (full_dim, num_slots * full_dim, 1):
raise ValueError(f"NVFP4 KV pages must be dense, got {kv_cache.stride()}")
dims = (num_kv_heads, block_size) if hnd else (block_size, num_kv_heads)
side_size = num_kv_heads * block_size * full_dim
(k_data, k_scale), (v_data, v_scale) = (
nvfp4_split_data_scale(
kv_cache.as_strided(
(num_blocks, *dims, full_dim),
(kv_cache.stride(0), dims[1] * full_dim, full_dim, 1),
kv_cache.storage_offset() + side * side_size,
)
)
for side in range(2)
)
return (k_data, v_data), (k_scale, v_scale), "HND" if hnd else "NHD"
def _get_trtllm_workspace_buffer():
global trtllm_workspace_buffer
if trtllm_workspace_buffer is None:
trtllm_workspace_buffer = torch.zeros(
envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE, dtype=torch.uint8, device="cuda"
)
return trtllm_workspace_buffer
def _pack_draft_block_bool_mask(
bool_mask: torch.Tensor, num_packed: int
) -> torch.Tensor:
"""Pack a boolean draft mask into XQA's uint16 layout."""
num_rows = bool_mask.shape[0]
bits = 1 << torch.arange(32, device=bool_mask.device, dtype=torch.int64)
mask_u32 = (
(bool_mask.view(num_rows, num_packed, 32).to(torch.int64) * bits)
.sum(dim=-1)
.to(torch.uint32)
)
return mask_u32.view(torch.uint16).reshape(num_rows, num_packed * 2)
def _make_xqa_draft_block_mask(
q_len: int, causal: bool, device: torch.device
) -> torch.Tensor:
"""Build a packed XQA draft mask for a uniform request."""
num_packed = (q_len + 31) // 32
padded = num_packed * 32
q_idx = torch.arange(q_len, device=device).unsqueeze(1)
kv_idx = torch.arange(padded, device=device).unsqueeze(0)
bool_mask = kv_idx <= q_idx if causal else (kv_idx < q_len).expand(q_len, padded)
return _pack_draft_block_bool_mask(bool_mask, num_packed)
def _make_xqa_ragged_draft_block_mask(
q_lens: list[int], max_q_len: int, causal: bool, device: torch.device
) -> torch.Tensor:
"""Build a packed XQA draft mask for a ragged batch."""
num_packed = (max_q_len + 31) // 32
padded = num_packed * 32
q_lens_t = torch.tensor(q_lens, device=device)
row_lens = torch.repeat_interleave(q_lens_t, q_lens_t)
request_starts = torch.cumsum(q_lens_t, dim=0) - q_lens_t
row_starts = torch.repeat_interleave(request_starts, q_lens_t)
q_idx = torch.arange(sum(q_lens), device=device) - row_starts
kv_idx = torch.arange(padded, device=device).unsqueeze(0)
bool_mask = kv_idx < row_lens.unsqueeze(1)
if causal:
bool_mask &= kv_idx <= q_idx.unsqueeze(1)
return _pack_draft_block_bool_mask(bool_mask, num_packed)
@triton.jit
def _trtllm_prefill_attn_kvfp8_dequant(
kv_cache_ptr,
block_tables_prefill_ptr,
block_table_stride,
mock_kv_cache_ptr,
k_scale_ptr,
v_scale_ptr,
src_stride_page,
src_stride_kv,
src_stride_head,
src_stride_block,
src_stride_head_size,
DST_K_CACHE_STRIDE: tl.constexpr,
DST_KV_CACHE_STRIDE: tl.constexpr,
HEAD_STRIDE: tl.constexpr,
HEAD_SIZE: tl.constexpr,
NUM_KV_HEADS: tl.constexpr,
):
batch_idx = tl.program_id(0).to(tl.int64)
mock_block_table_idx = tl.program_id(1).to(tl.int64)
orig_page_num = tl.load(
block_tables_prefill_ptr + batch_idx * block_table_stride + mock_block_table_idx
).to(tl.int64)
if orig_page_num <= 0:
return
dequant_dtype = mock_kv_cache_ptr.dtype.element_ty
k_scale_val = tl.load(k_scale_ptr)
v_scale_val = tl.load(v_scale_ptr)
mock_page_idx = batch_idx * block_table_stride + mock_block_table_idx + 1
logical_offsets = tl.arange(0, HEAD_STRIDE)
block_offsets = logical_offsets // HEAD_SIZE
head_size_offsets = logical_offsets % HEAD_SIZE
for h in range(NUM_KV_HEADS):
h_off = tl.cast(h, tl.int64)
# Read K from source (supports non-contiguous page/kv/head strides)
src_k = (
orig_page_num * src_stride_page
+ h_off * src_stride_head
+ block_offsets * src_stride_block
+ head_size_offsets * src_stride_head_size
)
fp8_k = tl.load(kv_cache_ptr + src_k)
dequant_k = (fp8_k.to(tl.float32) * k_scale_val).to(dequant_dtype)
# Write K to contiguous mock cache
dst_k = mock_page_idx * DST_KV_CACHE_STRIDE + h * HEAD_STRIDE + logical_offsets
tl.store(mock_kv_cache_ptr + dst_k, dequant_k)
# Read V from source (offset by src_stride_kv for the V half)
src_v = (
orig_page_num * src_stride_page
+ src_stride_kv
+ h_off * src_stride_head
+ block_offsets * src_stride_block
+ head_size_offsets * src_stride_head_size
)
fp8_v = tl.load(kv_cache_ptr + src_v)
dequant_v = (fp8_v.to(tl.float32) * v_scale_val).to(dequant_dtype)
# Write V to contiguous mock cache
dst_v = (
mock_page_idx * DST_KV_CACHE_STRIDE
+ DST_K_CACHE_STRIDE
+ h * HEAD_STRIDE
+ logical_offsets
)
tl.store(mock_kv_cache_ptr + dst_v, dequant_v)
def trtllm_prefill_attn_kvfp8_dequant(
kv_cache: torch.Tensor,
block_tables_prefill: torch.Tensor,
k_scale: torch.Tensor,
v_scale: torch.Tensor,
dequant_dtype: torch.dtype,
) -> tuple[torch.Tensor, torch.Tensor]:
batch_size, num_of_page_per_token = block_tables_prefill.shape
s = kv_cache.shape
assert s[1] == 2
assert dequant_dtype in (torch.bfloat16, torch.float16)
# Logical source layout is (B, 2, H, N, D). The tensor may be a
# non-contiguous view, so the Triton kernel indexes it with actual strides.
strides = kv_cache.stride()
num_kv_heads, block_size, head_size = s[2], s[3], s[4]
head_stride = block_size * head_size
k_cache_stride = num_kv_heads * head_stride
kv_cache_stride = k_cache_stride * s[1]
new_s = (batch_size * num_of_page_per_token + 1, s[1], s[2], s[3], s[4])
# mock kv cache contains just the pages needed by this prefill
mock_kv_cache = torch.empty(new_s, dtype=dequant_dtype, device=kv_cache.device)
# we simply sequentially index the pages needed by this prefill
mock_block_table = torch.arange(
start=1,
end=batch_size * num_of_page_per_token + 1,
dtype=torch.int32,
device=block_tables_prefill.device,
).reshape(batch_size, num_of_page_per_token)
grid = (batch_size, num_of_page_per_token)
_trtllm_prefill_attn_kvfp8_dequant[grid](
kv_cache,
block_tables_prefill,
num_of_page_per_token,
mock_kv_cache,
k_scale,
v_scale,
strides[0],
strides[1],
strides[2],
strides[3],
strides[4],
k_cache_stride,
kv_cache_stride,
head_stride,
head_size,
num_kv_heads,
)
return mock_kv_cache, mock_block_table
class BatchDCPPrefillWrapper:
def __init__(
self,
kv_layout: str,
workspace_buffer: torch.Tensor | None = None,
dcp_a2a: bool = False,
):
if dcp_a2a:
self._dcp_combine = partial(dcp_a2a_lse_reduce, is_lse_base_on_e=False)
else:
self._dcp_combine = partial(cp_lse_ag_out_rs, is_lse_base_on_e=False)
self._context = BatchPrefillWithPagedKVCacheWrapper(workspace_buffer, kv_layout)
self._new_tokens = BatchPrefillWithRaggedKVCacheWrapper(workspace_buffer)
def plan(
self,
qo_indptr_cpu: torch.Tensor,
paged_kv_indptr_cpu: torch.Tensor,
paged_kv_indices: torch.Tensor,
paged_kv_last_page_len_cpu: torch.Tensor,
kv_lens_cpu: torch.Tensor,
page_size: int,
num_qo_heads: int,
dcp_world_size: int,
num_kv_heads: int,
head_dim: int,
sm_scale: float,
window_left: int,
logits_soft_cap: float | None,
q_data_type: torch.dtype,
kv_cache_dtype: torch.dtype,
prefill_fixed_split_size: int,
disable_split_kv: bool,
):
"""Plan the prefill operation with given parameters."""
self._context.plan(
qo_indptr=qo_indptr_cpu,
paged_kv_indptr=paged_kv_indptr_cpu,
paged_kv_indices=paged_kv_indices,
paged_kv_last_page_len=paged_kv_last_page_len_cpu,
seq_lens=kv_lens_cpu,
num_qo_heads=num_qo_heads * dcp_world_size,
num_kv_heads=num_kv_heads,
head_dim_qk=head_dim,
page_size=page_size,
causal=False, # This is context run
sm_scale=sm_scale,
window_left=window_left,
logits_soft_cap=logits_soft_cap,
q_data_type=q_data_type,
kv_data_type=kv_cache_dtype,
fixed_split_size=prefill_fixed_split_size,
disable_split_kv=disable_split_kv,
)
self._new_tokens.plan(
qo_indptr=qo_indptr_cpu,
kv_indptr=qo_indptr_cpu,
num_qo_heads=num_qo_heads,
num_kv_heads=num_kv_heads,
head_dim_qk=head_dim,
head_dim_vo=head_dim,
causal=True, # This is newtokens run
sm_scale=sm_scale,
window_left=window_left,
logits_soft_cap=logits_soft_cap,
q_data_type=q_data_type,
)
def run(
self,
layer: torch.nn.Module,
prefill_query: torch.Tensor,
kv_cache_tuple: tuple[torch.Tensor, torch.Tensor],
key: torch.Tensor,
value: torch.Tensor,
out: torch.Tensor,
):
prefill_query_across_dcp = get_dcp_group().all_gather(
prefill_query.contiguous(), dim=1
)
output_context_tmp, lse_context_tmp = self._context.run(
prefill_query_across_dcp,
kv_cache_tuple,
k_scale=layer._k_scale_float,
v_scale=layer._v_scale_float,
return_lse=True,
)
output_context, lse_context = self._dcp_combine(
output_context_tmp,
lse_context_tmp,
get_dcp_group(),
return_lse=True,
)
lse_context = log2_lse_to_ln(lse_context.transpose(0, 1).contiguous())
output_query, lse_query = self._new_tokens.run(
prefill_query,
key,
value,
return_lse=True,
)
lse_query = log2_lse_to_ln(lse_query.transpose(0, 1).contiguous())
merge_attn_states(
out,
output_context,
lse_context,
output_query,
lse_query,
)
return out
class FlashInferBackend(AttentionBackend):
@classmethod
def customize_spec(cls, spec: "AttentionSpec") -> "AttentionSpec":
"""NVFP4 stores K and V as separate per-head slots of packed fp4 data
plus fp8 block scales."""
if spec.state_content_bytes is not None or not spec.kv_quant_mode.is_nvfp4:
return spec
hs_k = nvfp4_kv_cache_full_dim(spec.head_size)
hs_v = nvfp4_kv_cache_full_dim(spec.head_size_v)
assert hs_k == hs_v, "nvfp4 with asymmetric K/V head sizes not yet supported"
return replace(
spec,
num_head_slots=2 * spec.num_kv_heads,
state_content_bytes=hs_k * get_dtype_size(spec.dtype),
)
supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
"auto",
"float16",
"bfloat16",
"fp8",
"fp8_e4m3",
"fp8_e5m2",
"nvfp4",
"nvfp4_4over6",
]
@staticmethod
def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
# Page sizes >= 128 only run on the trtllm-gen dynamic kernel (GQA/MQA
# on Blackwell); advertise them only when usable so selection never
# picks a large kernel block we cannot serve.
use_large_pages = False
vllm_config = get_current_vllm_config_or_none()
if vllm_config is not None and vllm_config.model_config is not None:
pc = vllm_config.parallel_config
mc = vllm_config.model_config
num_qo_heads = mc.get_num_attention_heads(pc)
num_kv_heads = mc.get_num_kv_heads(pc)
use_large_pages = (
num_kv_heads > 0
and num_qo_heads // num_kv_heads > 1
and current_platform.is_device_capability_family(100)
and can_use_trtllm_attention(num_qo_heads, num_kv_heads)
)
if not use_large_pages:
return [16, 32, 64]
return [16, 32, 64, 128, 256, 512, 1024]
@staticmethod
def get_name() -> str:
return "FLASHINFER"
@classmethod
def supports_non_causal(cls) -> bool:
return True
@classmethod
def supports_device_cpu_query_lens_mismatch(cls) -> bool:
# Only trtllm-gen decode (SM100 family) reads query offsets from the
# device tensor; XQA and wrapper decode plan from qo_indptr_cpu, and
# under DCP spec queries are routed to the prefill path.
if not current_platform.is_device_capability_family(100):
return False
vllm_config = get_current_vllm_config_or_none()
if vllm_config is None or vllm_config.model_config is None:
return True
pc = vllm_config.parallel_config
mc = vllm_config.model_config
return pc.decode_context_parallel_size == 1 and can_use_trtllm_attention(
mc.get_num_attention_heads(pc), mc.get_num_kv_heads(pc), is_prefill=False
)
@classmethod
def supports_sliding_window(cls) -> bool:
return True
@staticmethod
def get_impl_cls() -> type["FlashInferImpl"]:
return FlashInferImpl
@staticmethod
def get_builder_cls() -> type["FlashInferMetadataBuilder"]:
return FlashInferMetadataBuilder
@staticmethod
def get_dtype_for_flashinfer(kv_cache_dtype: str) -> torch.dtype:
if kv_cache_dtype in ("fp8", "fp8_e4m3"):
return torch.float8_e4m3fn
elif kv_cache_dtype == "fp8_e5m2":
return torch.float8_e5m2
elif kv_cache_dtype.startswith("nvfp4"):
return torch.uint8
else:
raise ValueError(f"Unrecognized dtype: {kv_cache_dtype}")
@classmethod
def supports_kv_cache_dtype(cls, kv_cache_dtype: CacheDType | None) -> bool:
if kv_cache_dtype is not None and kv_cache_dtype.startswith("nvfp4"):
if _nvfp4_kv_on_fa2():
# The slot-mapping writer has no scale search.
return kv_cache_dtype == "nvfp4"
return (
current_platform.is_device_capability_family(100)
and supports_trtllm_attention(is_prefill=True)
and supports_trtllm_attention(is_prefill=False)
)
return super().supports_kv_cache_dtype(kv_cache_dtype)
@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:
if (
kv_cache_dtype is not None
and kv_cache_dtype.startswith("nvfp4")
and device_capability.major in (8, 9, 12)
):
if head_size == 64:
return "fa2 prefill misreads an NVFP4 KV cache at head_size 64"
if has_sink:
return "the sink prefill wrapper cannot read an NVFP4 KV cache"
return None
@classmethod
def get_supported_head_sizes(cls) -> list[int]:
# https://github.com/flashinfer-ai/flashinfer/blob/3d55c71a62052c590c130897d3a3db49b14fcc34/include/flashinfer/utils.cuh#L157
return [64, 128, 256, 512]
@classmethod
def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
# FlashInfer supports SM75+, but is currently broken on SM75 (Turing):
# https://github.com/flashinfer-ai/flashinfer/issues/3620 (fix:
# https://github.com/flashinfer-ai/flashinfer/pull/3621). Temporarily
# raise the floor to SM80 so it is not auto-selected on SM75 until
# that fix lands; revert to DeviceCapability(7, 5) once it does.
return capability >= DeviceCapability(8, 0) and capability <= DeviceCapability(
12, 1
)
@classmethod
def supports_sink(cls) -> bool:
"""Whether FlashInfer can serve attention sinks on this platform."""
from vllm.utils.flashinfer import (
force_use_trtllm_attention,
)
if force_use_trtllm_attention() is False:
return False
if current_platform.is_device_capability(
90
) or current_platform.is_device_capability_family(120):
return supports_trtllm_attention(is_prefill=False)
if not current_platform.is_device_capability_family(100):
return False
return supports_trtllm_attention(
is_prefill=False
) and supports_trtllm_attention(is_prefill=True)
@classmethod
def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...] | None:
capability = current_platform.get_device_capability()
if capability is not None and capability.major == 10:
# The trtllm-gen kernels consume head-major block interiors; the L/B
# nesting outside the block is immaterial to them.
return (KVCacheLayout.LBHNC, KVCacheLayout.BLHNC)
return super().supported_kv_cache_layouts()
forward_includes_kv_cache_update: bool = False
@dataclass
class FIPrefill:
"""Metadata for the native FlashInfer prefill pathway (non-TRTLLM)."""
wrapper: BatchPrefillWithPagedKVCacheWrapper | BatchDCPPrefillWrapper
@dataclass
class FIDecode:
"""Metadata for the native FlashInfer decode pathway (non-TRTLLM)."""
wrapper: BatchDecodeWithPagedKVCacheWrapper
class FlashInferDecodeKernel(Enum):
"""Decode kernels selected inside the FlashInfer backend."""
XQA = "xqa"
TRTLLM_GEN = "trtllm-gen"
def _is_xqa_head_dim_supported(head_dim: int) -> bool:
return 16 <= head_dim <= 256 and head_dim % 16 == 0
@dataclass
class TRTLLMPrefill:
"""Metadata for the TRTLLM prefill pathway."""
block_tables: torch.Tensor
"""
The slice of the block table tensor corresponding *only* to prefill requests.
Shape: [num_prefills, max_num_blocks_per_seq]
"""
seq_lens: torch.Tensor
"""
The slice of the sequence lengths tensor corresponding *only* to prefill requests.
Shape: [num_prefills]
"""
cum_seq_lens_q: torch.Tensor
cum_seq_lens_kv: torch.Tensor
max_q_len: int
"""
The maximum query length *among prefill requests*.
"""
max_seq_len: int
"""The maximum sequence length for KV Cache."""
@dataclass
class FlashInferTrtllmAPIDecode:
"""Metadata for XQA and trtllm-gen decode."""
kernel: FlashInferDecodeKernel
block_tables: torch.Tensor
"""
The slice of the block table tensor corresponding *only* to decode requests.
Shape: [num_decodes, max_num_blocks_per_seq]
"""
seq_lens: torch.Tensor
"""
The slice of the sequence lengths tensor corresponding *only* to decode requests.
Shape: [num_decodes]
"""
max_seq_len: int
"""The maximum sequence length for KV Cache."""
q_len_per_req: int = 1
"""Query tokens per request for uniform speculative decode."""
q_cu_seq_lens: torch.Tensor | None = None
"""Cumulative query lengths for ragged XQA decode."""
mask: torch.Tensor | None = None
"""Packed XQA draft mask."""
dcp_query_start_loc: torch.Tensor | None = None
"""Decode query boundaries for masking empty DCP shards during reduction."""
@dataclass
class FlashInferMetadata:
kv_cache_layout: KVCacheLayout
num_actual_tokens: int
"""Total number of tokens in the batch (excluding padding)."""
slot_mapping: torch.Tensor
"""Tensor for writing K/V to the cache. Shape: [num_actual_tokens]"""
# The data types of the query for prefill and decode.
# On SM90, these two data types may be different.
q_data_type_prefill: torch.dtype
q_data_type_decode: torch.dtype
num_decodes: int
num_decode_tokens: int
num_prefills: int
num_prefill_tokens: int
causal: bool
prefill: FIPrefill | TRTLLMPrefill | None
"""
Holds the metadata for the prefill portion of the batch.
Will be `None` if `num_prefill_tokens == 0`.
"""
decode: FIDecode | FlashInferTrtllmAPIDecode | None
"""
Holds the metadata for the decode portion of the batch.
Will be `None` if `num_decode_tokens == 0`.
"""
class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
kv_cache_spec: AttentionSpec
reorder_batch_threshold: int = 1
def __init__(
self,
kv_cache_spec: AttentionSpec,
layer_names: list[str],
vllm_config: VllmConfig,
device: torch.device,
):
super().__init__(kv_cache_spec, layer_names, vllm_config, device)
self.cache_config = vllm_config.cache_config
self.model_config = vllm_config.model_config
self.attention_config = vllm_config.attention_config
self._num_speculative_tokens = vllm_config.num_speculative_tokens
self._workspace_buffer = None
self._prefill_wrapper: (
BatchPrefillWithPagedKVCacheWrapper | BatchDCPPrefillWrapper | None
) = None # Wrapper for prefill/append
self._noncausal_prefill_wrapper: BatchPrefillWithPagedKVCacheWrapper | None = (
None # Wrapper for non-causal prefill (DFlash)
)
self._decode_wrapper = None # Wrapper for decode (general shape)
if envs.VLLM_BATCH_INVARIANT:
self.decode_fixed_split_size = 2048
self.prefill_fixed_split_size = 4096
self.disable_split_kv = True
else:
self.decode_fixed_split_size = -1
self.prefill_fixed_split_size = -1
self.disable_split_kv = False
self.compilation_config = vllm_config.compilation_config
self.max_num_batched_tokens = (
vllm_config.scheduler_config.max_num_batched_tokens
)
max_num_pages_per_req = cdiv(
self.model_config.max_model_len, self.kv_cache_spec.block_size
)
max_num_reqs = vllm_config.scheduler_config.max_num_seqs
self.max_num_reqs = max_num_reqs
max_num_pages = max_num_reqs * max_num_pages_per_req
# Persistent uniform masks keep stable addresses for CUDA graphs.
self._decode_mask_cache: dict[tuple[int, bool], torch.Tensor] = {}
speculative_config = vllm_config.speculative_config
num_spec_tokens = (
speculative_config.num_speculative_tokens
if speculative_config is not None
else 0
)
self.enable_cuda_graph = (
self.compilation_config.cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
)
if self.enable_cuda_graph:
# For full cudagraph capture, one `decode_wrapper` for each batch
# size is needed for FlashInfer.
self._decode_wrappers_cudagraph: dict[
int, BatchDecodeWithPagedKVCacheWrapper
] = {}
self._decode_cudagraph_max_bs = (1 + num_spec_tokens) * max_num_reqs
if self.compilation_config.max_cudagraph_capture_size is not None:
self._decode_cudagraph_max_bs = min(
self._decode_cudagraph_max_bs,
self.compilation_config.max_cudagraph_capture_size,
)
self.dcp_world_size, self.dcp_rank = get_dcp_world_size_and_rank(
kv_cache_spec.dcp_sharded
)
self.dcp_kv_cache_interleave_size = (
vllm_config.parallel_config.cp_kv_cache_interleave_size
)
self.use_dcp = self.dcp_world_size > 1
self.dcp_a2a = (
self.use_dcp and vllm_config.parallel_config.dcp_comm_backend == "a2a"
)
# Compatible with models with non-uniform per-layer head counts.
self.num_qo_heads = get_num_attention_heads_from_layers(
vllm_config, layer_names
) or self.model_config.get_num_attention_heads(self.vllm_config.parallel_config)
self.num_kv_heads = self.kv_cache_spec.num_kv_heads
self.head_dim = self.kv_cache_spec.head_size
self.page_size = self.kv_cache_spec.block_size
if self.kv_cache_spec.kv_quant_mode != KVQuantMode.NONE:
self.cache_dtype = self.cache_config.cache_dtype
# Cannot use self.kv_cache_spec.dtype here because kv_cache_spec
# storage dtype may not be the same as the op dtype (uint8 vs fp8_e4m3)
self.is_kvcache_nvfp4 = self.cache_dtype.startswith("nvfp4")
# fa2 plans NVFP4 by its uint8 storage dtype.
self.nvfp4_fa2 = self.is_kvcache_nvfp4 and _nvfp4_kv_on_fa2()
self.nvfp4_trtllm = self.is_kvcache_nvfp4 and not self.nvfp4_fa2
if self.nvfp4_fa2 and self.use_dcp:
raise NotImplementedError(
"The DCP prefill wrapper cannot read NVFP4 block scales."
)
if self.nvfp4_fa2 and self.head_dim == 64:
raise NotImplementedError("fa2 prefill misreads NVFP4 at head_size 64.")
if self.nvfp4_trtllm:
if (
force_use_trtllm_attention() is False
or not supports_trtllm_attention(is_prefill=True)
or not supports_trtllm_attention(is_prefill=False)
):
raise ValueError(
f"--kv-cache-dtype {self.cache_dtype} requires the "
"SM100 trtllm-gen "
"FlashInfer path."
)
# The scale search only affects the store kernel. FlashInfer
# reads both variants using the same NVFP4 layout.
self.kv_cache_dtype = "nvfp4"
else:
self.kv_cache_dtype = FlashInferBackend.get_dtype_for_flashinfer(
self.cache_dtype
)
else:
self.cache_dtype = "auto"
self.is_kvcache_nvfp4 = False
self.nvfp4_fa2 = self.nvfp4_trtllm = False
assert self.kv_cache_spec.dtype == self.model_config.dtype
self.kv_cache_dtype = self.kv_cache_spec.dtype
# Compute per-phase Q dtype. On SM90 (XQA decode), the prefill and
# decode phases require different Q dtypes when the KV cache is FP8
# (FP8-Q for the FI native prefill, BF16/FP16-Q for XQA decode),
# so both values must be tracked independently.
self.q_data_type_prefill = self.get_q_data_type(is_prefill=True)
self.q_data_type_decode = self.get_q_data_type(is_prefill=False)
# Prefer TRTLLM/XQA for decoding whenever supported. The decode kernel
# must be selected statically for FULL cudagraph capture.
# XQA reads an NVFP4 cache only on SM12x; SM90 decodes it with fa2.
can_use_xqa_or_trtllm_gen_decode = can_use_trtllm_attention(
self.num_qo_heads, self.num_kv_heads, is_prefill=False
) and (not self.nvfp4_fa2 or current_platform.is_device_capability_family(120))
# Page sizes >= 128 require the trtllm-gen GQA/MQA path (guaranteed by
# get_supported_kernel_block_sizes).
assert self.page_size <= 64 or (
current_platform.is_device_capability_family(100)
and can_use_xqa_or_trtllm_gen_decode
and self.num_qo_heads // self.num_kv_heads > 1
), f"Unexpected FlashInfer page size {self.page_size} without trtllm-gen GQA"
self.use_trtllm_decode_attention = can_use_xqa_or_trtllm_gen_decode
self.flashinfer_trtllm_api_decode_kernel: FlashInferDecodeKernel | None = (
self._get_flashinfer_trtllm_api_decode_kernel()
if can_use_xqa_or_trtllm_gen_decode
else None
)
# The dedicated FlashInfer XQA API accepts head dimensions in
# [16, 256] that are divisible by 16. Some hybrid-attention models
# (for example Gemma 4) use XQA-compatible sliding-attention groups
# alongside global-attention groups with head_dim=512. Resolve the
# backend per KV-cache group so the eligible groups still use XQA and
# the wider groups fall back to native FlashInfer decode.
if (
self.flashinfer_trtllm_api_decode_kernel == FlashInferDecodeKernel.XQA
and not _is_xqa_head_dim_supported(self.head_dim)
):
logger.warning_once(
"FlashInfer XQA decode does not support head_dim=%d; "
"reverting this KV-cache group to native FlashInfer decode.",
self.head_dim,
)
self.use_trtllm_decode_attention = False
self.flashinfer_trtllm_api_decode_kernel = None
if (
self.use_dcp
and self.flashinfer_trtllm_api_decode_kernel == FlashInferDecodeKernel.XQA
):
logger.warning_once(
"FlashInfer XQA decode does not support returning LSE and "
"therefore does not support DCP, reverting to native FlashInfer "
"decode."
)
self.use_trtllm_decode_attention = False
self.flashinfer_trtllm_api_decode_kernel = None
self.use_xqa = (
self.flashinfer_trtllm_api_decode_kernel == FlashInferDecodeKernel.XQA
)
self.use_trtllm_gen_varlen_decode = self._uses_trtllm_gen_varlen_decode(
vllm_config, self.flashinfer_trtllm_api_decode_kernel, self.use_dcp
)
# Fused draft steps advance seq_lens in place and keep block tables fixed,
# so one trtllm-gen decode build serves every step (not DCP-local seq_lens).
self.supports_draft_decode_metadata_update = (
self.flashinfer_trtllm_api_decode_kernel
== FlashInferDecodeKernel.TRTLLM_GEN
and not self.use_dcp
)
self._init_reorder_batch_threshold(
1,
supports_spec_as_decode=(
self.flashinfer_trtllm_api_decode_kernel is not None
),
# trtllm-gen decode receives no cp_rank/global-seq-len information,
# so its end-aligned causal mask is wrong for q_len > 1 over the
# DCP-interleaved local KV shard (spec token i misses up to
# (dcp_world_size - 1) * (q_len - 1 - i) KV entries, including its
# own). Keep the threshold at 1 under DCP so spec queries take the
# DCP-aware prefill path, until the kernel is CP-aware (compare
# flash_attn_varlen_func's cp_world_size/cp_rank/cp_tot_seqused_k).
supports_dcp_with_varlen=False,
)
# Global hyperparameters shared by all attention layers
# TODO: discard this for trtllm-gen backend
per_layer_parameters = get_per_layer_parameters(
vllm_config, layer_names, FlashInferImpl
)
self.global_hyperparameters = infer_global_hyperparameters(per_layer_parameters)
self.sm_scale = self.global_hyperparameters.sm_scale
self.window_left = self.global_hyperparameters.window_left
self.logits_soft_cap = self.global_hyperparameters.logits_soft_cap
self.has_sinks = self.global_hyperparameters.has_sinks
if self.has_sinks and (self.nvfp4_fa2 or not FlashInferBackend.supports_sink()):
raise NotImplementedError(
"FlashInfer backend currently does not support attention "
"sinks, please use trtllm on blackwell or flash attention on "
"earlier GPUs."
)
capability = current_platform.get_device_capability()
arch = f"sm{capability.major}{capability.minor}" if capability else "unknown"
decode_backend = (
self.flashinfer_trtllm_api_decode_kernel.value
if self.flashinfer_trtllm_api_decode_kernel is not None
else "flashinfer-native"
)
logger.info_once(
"FlashInfer resolved query dtypes: prefill=%s, decode=%s, "
"decode_backend=%s, kv_cache_dtype=%s, arch=%s",
self.q_data_type_prefill,
self.q_data_type_decode,
decode_backend,
self.kv_cache_dtype,
arch,
)
# Preparing persistent buffers
self.paged_kv_indptr = CpuGpuBuffer(
max_num_reqs + 1, dtype=torch.int32, device=self.device, pin_memory=False
)
self.paged_kv_indices = torch.zeros(
max_num_pages, dtype=torch.int32, device=self.device
)
self.paged_kv_last_page_len = CpuGpuBuffer(
max_num_reqs, dtype=torch.int32, device=self.device, pin_memory=False
)
@property
def kv_cache_layout(self) -> KVCacheLayout:
return self.cache_config.get_resolved_kv_cache_layout()
# Keep SM90 prefill/decode Q dtype selection in one place.
def get_q_data_type(self, is_prefill: bool) -> torch.dtype:
# The user sets --attention-config.disable_flashinfer_q_quantization
# to 1 explicitly, use model dtype for query.
if self.vllm_config.attention_config.disable_flashinfer_q_quantization:
return self.model_config.dtype
# self.cache_dtype is resolved per KV-cache group: it is "auto" when
# this group is unquantized (e.g. --kv-cache-dtype-skip-layers), even
# if cache_config requests a quantized dtype globally.
cache_dtype = self.cache_dtype
# On SM90/SM12x, XQA decode requires BF16/FP16-Q even with FP8 KV cache.
# FI native prefill on SM90 still uses FP8-Q in that case; SM12x prefill
# is fa2-only and keeps the model dtype (handled below).
if (
(
current_platform.is_device_capability(90)
or current_platform.is_device_capability_family(120)
)
and not is_prefill
and force_use_trtllm_attention() is not False
and cache_dtype.startswith("fp8")
):
return self.model_config.dtype
# Otherwise, match Q dtype to the KV cache dtype.
if cache_dtype.startswith("fp8"):
# FP8-Q requires an fp8 tensor-core attention path. SM90 uses
# native FA3 for prefill; XQA decode on SM90/SM12x is handled
# above with model-dtype Q, while SM100 uses trtllm-gen.
# Architectures with only fa2 (e.g. SM89, SM120) cannot
# consume FP8 queries, so keep the model dtype for Q there.
if current_platform.is_device_capability(
90
) or current_platform.is_device_capability_family(100):
return FlashInferBackend.get_dtype_for_flashinfer(cache_dtype)
return self.model_config.dtype
if cache_dtype.startswith("nvfp4"):
if self.nvfp4_fa2:
return self.model_config.dtype
return FlashInferBackend.get_dtype_for_flashinfer("fp8_e4m3")
return self.kv_cache_spec.dtype
@override # type: ignore[misc]
@classmethod
def get_cudagraph_support(
cls: type["FlashInferMetadataBuilder"],
vllm_config: VllmConfig,
kv_cache_spec: KVCacheSpec,
) -> AttentionCGSupport:
"""Get the cudagraph support level for FlashInfer attention."""
# XQA lacks LSE for DCP; DCP also cannot graph variable-length trtllm-gen.
if vllm_config.parallel_config.decode_context_parallel_size > 1:
return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
kv_specs = iter_layer_specs(kv_cache_spec)
num_qo_heads = vllm_config.model_config.get_num_attention_heads(
vllm_config.parallel_config
)
is_xqa_arch = current_platform.is_device_capability(
90
) or current_platform.is_device_capability_family(120)
has_uniform_batch_support: bool = len(kv_specs) > 0
for spec in kv_specs:
if not isinstance(spec, AttentionSpec):
# FlashInfer only applies to attention, so we don't consider other types
# of KV spec (e.g. Mamba) here. This is mostly for type checking.
continue
if not can_use_trtllm_attention(
num_qo_heads=num_qo_heads,
num_kv_heads=spec.num_kv_heads,
is_prefill=False,
) or (
is_xqa_arch
and (
(
spec.kv_quant_mode.is_nvfp4
and not current_platform.is_device_capability_family(120)
)
or not _is_xqa_head_dim_supported(spec.head_size)
)
):
has_uniform_batch_support = False
break
use_non_causal = vllm_config.attention_config.use_non_causal
if has_uniform_batch_support and (not use_non_causal or is_xqa_arch):
return AttentionCGSupport.UNIFORM_BATCH
else:
return AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
@override # type: ignore[misc]
@classmethod
def get_varlen_cudagraph_max_query_len(
cls: type["FlashInferMetadataBuilder"],
vllm_config: VllmConfig,
kv_cache_spec: KVCacheSpec,
) -> int | None:
# The uniform gate already covers DCP, head geometry, the XQA head-dim
# limit and causality, and yields None for subclasses that force NEVER.
if (
cls.get_cudagraph_support(vllm_config, kv_cache_spec)
!= AttentionCGSupport.UNIFORM_BATCH
):
return None
# The kernel selector asserts off the trtllm-capable architectures, so
# call it only past the gate, as get_cudagraph_support does.
if not cls._uses_trtllm_gen_varlen_decode(
vllm_config,
cls._get_flashinfer_trtllm_api_decode_kernel(),
use_dcp=vllm_config.parallel_config.decode_context_parallel_size > 1,
):
return None
# Longer requests are split off as prefills.
return max_decode_query_len(vllm_config)
def _get_workspace_buffer(self):
if self._workspace_buffer is None:
buffer_size = envs.VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE
if envs.VLLM_BATCH_INVARIANT:
buffer_size = FLASHINFER_WORKSPACE_BUFFER_SIZE_BATCH_INVARIANT
else:
# FlashInfer prefill temp buffers (batch_prefill_tmp_v, ...)
# scale with the prefill chunk and query-head footprint, NOT
# context length. The fixed ~394 MiB default is too small for
# wide-head models at the default 8192-token chunk on some
# archs (e.g. sm_120), where FlashInfer hard-errors instead of
# growing. Size to the batch's head footprint; never shrink
# below the configured default.
est = (
self.max_num_batched_tokens
* self.num_qo_heads
* self.head_dim
* FLASHINFER_PREFILL_WORKSPACE_BYTES_PER_ELEM
)
buffer_size = max(buffer_size, est)
self._workspace_buffer = torch.zeros(
buffer_size, dtype=torch.uint8, device=self.device
)
return self._workspace_buffer
def set_workspace_buffer(self, workspace_buffer: torch.Tensor):
self._workspace_buffer = workspace_buffer
@staticmethod
def _uses_trtllm_gen_varlen_decode(
vllm_config: VllmConfig,
decode_kernel: FlashInferDecodeKernel | None,
use_dcp: bool,
) -> bool:
"""Whether decode reads per-request query lengths from the device
qo_indptr. Adaptive verification trims drafts on device, and only
trtllm-gen supports that (the selector already rejects the other
configurations)."""
speculative_config = vllm_config.speculative_config
return (
speculative_config is not None
and speculative_config.enable_adaptive_verification
and decode_kernel == FlashInferDecodeKernel.TRTLLM_GEN
and not use_dcp
)
@staticmethod
def _get_flashinfer_trtllm_api_decode_kernel() -> FlashInferDecodeKernel:
if current_platform.is_device_capability(
90
) or current_platform.is_device_capability_family(120):
return FlashInferDecodeKernel.XQA
assert current_platform.is_device_capability_family(100)
return FlashInferDecodeKernel.TRTLLM_GEN
def _compute_decode_query_lens(
self,
qo_indptr: torch.Tensor,
qo_indptr_cpu: torch.Tensor,
num_decodes: int,
num_decode_tokens: int,
) -> tuple[int, torch.Tensor | None, list[int] | None]:
"""Return the query width, ragged offsets, and effective query lengths."""
if num_decodes == 0 or num_decode_tokens == 0:
return 1, None, None
if not self.use_xqa:
decode_q_lens = (
qo_indptr_cpu[1 : num_decodes + 1] - qo_indptr_cpu[:num_decodes]
)
q_len_per_req = num_decode_tokens // num_decodes
if num_decode_tokens % num_decodes == 0 and bool(
(decode_q_lens == q_len_per_req).all().item()
):
return q_len_per_req, None, None
# CUDA-graph padding can leave zero-length requests after a
# uniform speculative decode batch. Keep the packed query and
# describe it through the trtllm-gen varlen API.
max_q_len = int(decode_q_lens.max().item())
if max_q_len > 1:
return max_q_len, qo_indptr[: num_decodes + 1], None
return 1, None, None
decode_q_lens = qo_indptr_cpu[1 : num_decodes + 1] - qo_indptr_cpu[:num_decodes]
nonzero = decode_q_lens[decode_q_lens > 0]
q_len_per_req = int(nonzero.max().item()) if nonzero.numel() > 0 else 1
uniform = nonzero.numel() <= 1 or bool((nonzero == nonzero[0]).all().item())
if uniform and num_decode_tokens == num_decodes * q_len_per_req:
return q_len_per_req, None, None
if q_len_per_req <= 1:
return 1, None, None
q_lens = decode_q_lens.tolist()
return (
q_len_per_req,
qo_indptr[: num_decodes + 1],
q_lens,
)
def _get_decode_mask(
self,
q_len_per_req: int,
ragged_q_lens: list[int] | None,
num_decodes: int,
causal: bool,
) -> torch.Tensor | None:
"""Return the packed XQA draft mask for speculative decode."""
if q_len_per_req <= 1:
return None
if ragged_q_lens is None:
key = (q_len_per_req, causal)
buf = self._decode_mask_cache.get(key)
if buf is None:
per_req = _make_xqa_draft_block_mask(q_len_per_req, causal, self.device)
buf = (
per_req.unsqueeze(0).expand(self.max_num_reqs, -1, -1).contiguous()
)
self._decode_mask_cache[key] = buf
return buf[:num_decodes]
return _make_xqa_ragged_draft_block_mask(
ragged_q_lens, q_len_per_req, causal, self.device
)
def _get_prefill_wrapper(
self,
causal: bool = True,
) -> BatchPrefillWithPagedKVCacheWrapper | BatchDCPPrefillWrapper:
if not causal:
if self.use_dcp:
raise NotImplementedError(
"FlashInfer non-causal prefill is not supported with DCP yet."
)
if self.is_kvcache_nvfp4:
raise NotImplementedError(
"FlashInfer non-causal attention is not supported with "
"NVFP4 KV cache."
)
if self._noncausal_prefill_wrapper is None:
if self.has_sinks and (
current_platform.is_device_capability(90)
or current_platform.is_device_capability_family(120)
):
self._noncausal_prefill_wrapper = (
BatchAttentionWithAttentionSinkWrapper(
self._get_workspace_buffer(),
get_flashinfer_layout_string(self.kv_cache_layout),
backend="auto",
q_data_type=self.q_data_type_prefill,
kv_data_type=self.kv_cache_dtype,
head_dim_qk=self.head_dim,
head_dim_vo=self.head_dim,
window_left=self.window_left,
)
)
else:
self._noncausal_prefill_wrapper = (
BatchPrefillWithPagedKVCacheWrapper(
self._get_workspace_buffer(),
get_flashinfer_layout_string(self.kv_cache_layout),
backend="auto",
)
)
return self._noncausal_prefill_wrapper
if self._prefill_wrapper is None:
if self.use_dcp:
self._prefill_wrapper = BatchDCPPrefillWrapper(
kv_layout=get_flashinfer_layout_string(self.kv_cache_layout),
workspace_buffer=self._get_workspace_buffer(),
dcp_a2a=self.dcp_a2a,
)
else:
if self.has_sinks and (
current_platform.is_device_capability(90)
or current_platform.is_device_capability_family(120)
):
assert not self.is_kvcache_nvfp4
self._prefill_wrapper = BatchAttentionWithAttentionSinkWrapper(
self._get_workspace_buffer(),
get_flashinfer_layout_string(self.kv_cache_layout),
backend="auto",
q_data_type=self.q_data_type_prefill,
kv_data_type=self.kv_cache_dtype,
head_dim_qk=self.head_dim,
head_dim_vo=self.head_dim,
window_left=self.window_left,
)
else:
# NVFP4 KV cache requires the trtllm-gen backend inside
# the wrapper on SM100; SM8x, SM90 and SM12x read it with fa2.
backend = "trtllm-gen" if self.nvfp4_trtllm else "auto"
self._prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(
self._get_workspace_buffer(),
get_flashinfer_layout_string(self.kv_cache_layout),
backend=backend,
)
assert self._prefill_wrapper is not None
return self._prefill_wrapper
def _get_decode_wrapper(self, batch_size: int, use_cudagraph: bool = False):
if use_cudagraph:
decode_wrapper = self._decode_wrappers_cudagraph.get(batch_size, None)
else:
decode_wrapper = self._decode_wrapper
if decode_wrapper is None:
if use_cudagraph:
paged_kv_indptr = self.paged_kv_indptr.gpu[: batch_size + 1]
paged_kv_indices = self.paged_kv_indices
paged_kv_last_page_len = self.paged_kv_last_page_len.gpu[:batch_size]
else:
paged_kv_indptr = None
paged_kv_indices = None
paged_kv_last_page_len = None
# NVFP4 KV cache requires the trtllm-gen backend inside
# the wrapper on SM100; SM8x, SM90 and SM12x read it with fa2.
backend = "trtllm-gen" if self.nvfp4_trtllm else "auto"
decode_wrapper = BatchDecodeWithPagedKVCacheWrapper(
self._get_workspace_buffer(),
get_flashinfer_layout_string(self.kv_cache_layout),
use_cuda_graph=use_cudagraph,
paged_kv_indptr_buffer=paged_kv_indptr,
paged_kv_indices_buffer=paged_kv_indices,
paged_kv_last_page_len_buffer=paged_kv_last_page_len,
# Tensor cores are enabled by default because the perf would be
# at least as good as cuda cores for all attention ops in latest
# gpus.
use_tensor_cores=True,
backend=backend,
)
# save the decode wrapper
if use_cudagraph:
self._decode_wrappers_cudagraph[batch_size] = decode_wrapper
else:
self._decode_wrapper = decode_wrapper
return decode_wrapper
def _compute_flashinfer_kv_metadata(
self,
num_blocks_np: np.ndarray,
seq_lens_np: np.ndarray,
block_table_tensor: torch.Tensor,
num_reqs: int,
page_size: int,
) -> torch.Tensor:
"""Compute paged_kv_indptr, paged_kv_indices and paged_kv_last_page_len.
Results are stored in self.paged_kv_indptr,
self.paged_kv_indices, self.paged_kv_last_page_len buffers.
Returns paged_kv_indices, a GPU tensor with shape [num_actual_pages].
"""
# write self.paged_kv_indptr_cpu inplace (0-index is always 0)
np.cumsum(
num_blocks_np, dtype=np.int32, out=self.paged_kv_indptr.np[1 : num_reqs + 1]
)
paged_kv_indptr = self.paged_kv_indptr.copy_to_gpu(num_reqs + 1)
# write self.paged_kv_indices inplace
num_actual_pages = self.paged_kv_indptr.np[num_reqs]
paged_kv_indices = self.paged_kv_indices[:num_actual_pages]
_copy_page_indices_kernel[(num_reqs,)](
paged_kv_indices,
block_table_tensor,
block_table_tensor.stride(0),
paged_kv_indptr,
BLOCK_SIZE=1024,
)
# write self.paged_kv_last_page_len_cpu inplace
paged_kv_last_page_len_np = seq_lens_np % page_size
self.paged_kv_last_page_len.np[:num_reqs] = np.where(
(paged_kv_last_page_len_np == 0) & (seq_lens_np != 0),
page_size,
paged_kv_last_page_len_np,
)
self.paged_kv_last_page_len.copy_to_gpu(num_reqs)
return paged_kv_indices
def build(
self,
common_prefix_len: int,
common_attn_metadata: CommonAttentionMetadata,
fast_build: bool = False,
) -> FlashInferMetadata:
num_reqs = common_attn_metadata.num_reqs
num_actual_tokens = common_attn_metadata.num_actual_tokens
causal = common_attn_metadata.causal
route_decode = causal or self.use_xqa
if route_decode:
num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
split_decodes_and_prefills(
common_attn_metadata,
decode_threshold=self.reorder_batch_threshold,
require_uniform=not (
self.use_xqa or self.use_trtllm_gen_varlen_decode
),
)
)
else:
num_decodes = 0
num_prefills = num_reqs
num_decode_tokens = 0
num_prefill_tokens = num_actual_tokens
page_size = self.page_size
max_seq_len = common_attn_metadata.max_seq_len
seq_lens = common_attn_metadata.seq_lens
block_table_tensor = common_attn_metadata.block_table_tensor
qo_indptr = common_attn_metadata.query_start_loc
qo_indptr_cpu = common_attn_metadata.query_start_loc_cpu
# Step 1: Decide which dispatch modes to use:
# - Prefill (FI native or TRTLLM)
# - Decode (FI native, XQA, or trtllm-gen)
uses_spec_reorder = self.reorder_batch_threshold > 1
# Page sizes >= 128 must use trtllm-gen; force it for prefill too.
prefill_force_trtllm = (
True if page_size >= 128 else self.attention_config.use_trtllm_attention
)
prefill_use_trtllm = causal and use_trtllm_attention(
self.num_qo_heads,
self.num_kv_heads,
num_prefill_tokens,
max_seq_len,
self.dcp_world_size,
self.cache_dtype,
self.q_data_type_prefill,
is_prefill=True,
force_use_trtllm=prefill_force_trtllm,
has_sinks=self.has_sinks,
has_spec=uses_spec_reorder,
)
decode_with_flashinfer_trtllm_api = self.use_trtllm_decode_attention and (
causal or self.use_xqa
)
if not causal and self.use_dcp:
raise NotImplementedError(
"FlashInfer non-causal prefill is not supported with DCP yet."
)
if not causal and self.use_trtllm_decode_attention:
logger.warning_once(
"Using FlashInfer for draft model non-causal attention; TRTLLM "
"can still be used for target model causal attention."
)
all_uses_trtllm = causal and (
(num_prefills == 0 or prefill_use_trtllm)
and (num_decodes == 0 or decode_with_flashinfer_trtllm_api)
)
if not all_uses_trtllm:
if self.has_sinks and self.use_dcp:
raise NotImplementedError(
"FlashInfer BatchDCPPrefillWrapper does not support attention "
"sinks. Please use TRTLLM on Blackwell or FlashAttention on "
"earlier GPUs."
)
if not self.global_hyperparameters.has_same_window_lefts:
raise ValueError(
"Window left is not the same for all layers. "
"One potential fix is to set disable_sliding_window=True"
)
assert self.global_hyperparameters.has_same_all_params, (
"FlashInfer backend currently only supports models in which "
"all layers share the same values for the following "
"hyperparameters: `window_left`, `logits_soft_cap`, "
"`sm_scale`."
)
# Step 2: Initialize the output metadata
# Leave prefill/decode empty, to be populated
# case by case depending on the batch contents and backend selection.
attn_metadata = FlashInferMetadata(
kv_cache_layout=self.kv_cache_layout,
num_actual_tokens=num_actual_tokens,
slot_mapping=common_attn_metadata.slot_mapping,
q_data_type_prefill=self.q_data_type_prefill,
q_data_type_decode=self.q_data_type_decode,
num_decodes=num_decodes,
num_decode_tokens=num_decode_tokens,
num_prefills=num_prefills,
num_prefill_tokens=num_prefill_tokens,
causal=causal,
prefill=None,
decode=None,
)
# Guard access to seq_lens_cpu, which may not always be needed
# and can be expensive to retrieve in async mode.
# When all attention (both prefill and decode) uses TRTLLM,
# seq_lens_cpu is not needed since TRTLLM paths use GPU tensors
# (block_tables, seq_lens) directly.
needs_seq_lens_cpu = self.use_dcp or not all_uses_trtllm
if needs_seq_lens_cpu:
if (
self._num_speculative_tokens == 0
and common_attn_metadata.seq_lens_cpu_upper_bound is not None
):
# No speculative token accounting, so the upper bound is exact.
seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
else:
with gpu_sync_allowed():
seq_lens_cpu = common_attn_metadata.seq_lens.cpu()
else:
seq_lens_cpu = None
# Adjust seq_lens_cpu for DCP
if self.use_dcp:
assert seq_lens_cpu is not None
if num_prefills > 0:
# Other attention groups may reuse the same common metadata.
seq_lens_cpu = seq_lens_cpu.clone()
qo_indptr_prefill_cpu = (
qo_indptr_cpu[num_decodes:] - qo_indptr_cpu[num_decodes]
)
query_lens_prefill_cpu = (
qo_indptr_prefill_cpu[1:] - qo_indptr_prefill_cpu[:-1]
)
seq_lens_cpu[num_decodes:] = (
seq_lens_cpu[num_decodes:] - query_lens_prefill_cpu
)
seq_lens_cpu = get_dcp_local_seq_lens(
seq_lens_cpu,
self.dcp_world_size,
self.dcp_rank,
self.dcp_kv_cache_interleave_size,
)
# Native paged attention consumes rank-local lengths (context only
# for DCP prefills), so derive its page counts after the conversion.
if seq_lens_cpu is not None:
seq_lens_np = seq_lens_cpu.numpy()
num_blocks_np = (seq_lens_np + (page_size - 1)) // page_size
else:
seq_lens_np = None
num_blocks_np = None
# Compute paged_kv_indices if necessary
# paged_kv_indices is only needed for FlashInfer native paths;
# XQA/trtllm-gen paths use block_tables directly on GPU.
needs_native_paged_prefill = num_prefills > 0 and not prefill_use_trtllm
needs_native_paged_decode = (
num_decodes > 0 and not decode_with_flashinfer_trtllm_api
)
needs_paged_kv_indices = needs_native_paged_prefill or needs_native_paged_decode
if needs_paged_kv_indices:
assert num_blocks_np is not None
assert seq_lens_np is not None
paged_kv_indices = self._compute_flashinfer_kv_metadata(
num_blocks_np,
seq_lens_np,
block_table_tensor,
num_reqs,
page_size,
)
else:
paged_kv_indices = None
# Step 3: Handle prefill and decode pathways case by case
## PREFILL PATHWAY
if num_prefills > 0:
# Slices for shared prefill metadata
prefill_start = num_decodes
qo_indptr_prefill_cpu = (
qo_indptr_cpu[prefill_start:] - qo_indptr_cpu[prefill_start]
)
assert qo_indptr_prefill_cpu.shape[0] == num_prefills + 1
if prefill_use_trtllm:
# TRTLLM prefill has no cross-rank combine for DCP-sharded KV;
# use_trtllm_attention never selects it when DCP is enabled.
assert not self.use_dcp
# Create GPU versions
qo_indptr_prefill_gpu = (
qo_indptr[prefill_start:] - qo_indptr[prefill_start]
)
# Compute cum_seq_lens_kv on GPU to avoid CPU sync.
# This is the cumulative sum of the number of KV cache
# blocks per prefill request.
prefill_seq_lens = seq_lens[prefill_start:]
num_blocks_per_req = (prefill_seq_lens + page_size - 1) // page_size
paged_kv_indptr_prefill_gpu = self.paged_kv_indptr.gpu[
prefill_start : num_reqs + 1
]
# Assign to slice to avoid cpu sync.
paged_kv_indptr_prefill_gpu[:1] = 0
torch.cumsum(
num_blocks_per_req,
dim=0,
out=paged_kv_indptr_prefill_gpu[1:],
)
# Compute max_q_len for prefill requests
query_lens_prefill_cpu = (
qo_indptr_prefill_cpu[1:] - qo_indptr_prefill_cpu[:-1]
)
max_q_len_prefill = int(query_lens_prefill_cpu.max().item())
prefill_block_tables = block_table_tensor[prefill_start:]
if (
self.q_data_type_prefill != FP8_DTYPE
and self.cache_dtype.startswith("fp8")
):
seq_lens_cpu_upper_bound = (
common_attn_metadata.seq_lens_cpu_upper_bound
)
max_prefill_seq_len = max_seq_len
if seq_lens_cpu_upper_bound is not None:
max_prefill_seq_len = int(
seq_lens_cpu_upper_bound[prefill_start:num_reqs]
.max()
.item()
)
# Dequantization allocates one page per table entry and
# indexes rows by their logical width.
prefill_block_tables = canonicalize_singleton_dim_strides(
prefill_block_tables[
:, : cdiv(max_prefill_seq_len, page_size)
].contiguous()
)
attn_metadata.prefill = TRTLLMPrefill(
block_tables=prefill_block_tables,
seq_lens=prefill_seq_lens,
cum_seq_lens_q=qo_indptr_prefill_gpu,
cum_seq_lens_kv=paged_kv_indptr_prefill_gpu,
max_q_len=max_q_len_prefill,
max_seq_len=max_seq_len,
)
else:
prefill_wrapper = self._get_prefill_wrapper(causal=attn_metadata.causal)
# Slicing CPU buffers that are only needed for FI native prefills
paged_kv_last_page_len_prefill_cpu = self.paged_kv_last_page_len.cpu[
prefill_start:num_reqs
]
assert paged_kv_last_page_len_prefill_cpu.shape[0] == num_prefills
paged_kv_indptr_prefill_cpu = self.paged_kv_indptr.cpu[
prefill_start : num_reqs + 1
]
assert paged_kv_indptr_prefill_cpu.shape[0] == num_prefills + 1
# plan() copies these to the GPU with non_blocking=True;
# stage them in pinned memory so the copies stay async
# (the reused buffers themselves are intentionally not pinned).
if PIN_MEMORY:
qo_indptr_prefill_cpu = qo_indptr_prefill_cpu.pin_memory()
paged_kv_indptr_prefill_cpu = (
paged_kv_indptr_prefill_cpu.pin_memory()
)
paged_kv_last_page_len_prefill_cpu = (
paged_kv_last_page_len_prefill_cpu.pin_memory()
)
# plan() also copies kv_lens to the GPU with non_blocking=True,
# deriving them via get_seq_lens() into unpinned memory when
# seq_lens is not passed; pass an explicit pinned copy of the
# same values so that copy stays async too.
kv_lens_prefill_cpu = get_seq_lens(
paged_kv_indptr_prefill_cpu,
paged_kv_last_page_len_prefill_cpu,
page_size,
)
if PIN_MEMORY:
kv_lens_prefill_cpu = kv_lens_prefill_cpu.pin_memory()
if self.use_dcp:
assert isinstance(prefill_wrapper, BatchDCPPrefillWrapper)
prefill_wrapper.plan(
qo_indptr_cpu=qo_indptr_prefill_cpu,
paged_kv_indptr_cpu=paged_kv_indptr_prefill_cpu,
paged_kv_indices=paged_kv_indices,
paged_kv_last_page_len_cpu=paged_kv_last_page_len_prefill_cpu,
kv_lens_cpu=kv_lens_prefill_cpu,
page_size=self.page_size,
num_qo_heads=self.num_qo_heads,
dcp_world_size=self.dcp_world_size,
num_kv_heads=self.num_kv_heads,
head_dim=self.head_dim,
sm_scale=self.sm_scale,
window_left=self.window_left,
logits_soft_cap=self.logits_soft_cap,
q_data_type=self.q_data_type_prefill,
kv_cache_dtype=self.kv_cache_dtype,
prefill_fixed_split_size=self.prefill_fixed_split_size,
disable_split_kv=self.disable_split_kv,
)
else:
assert isinstance(
prefill_wrapper,
BatchPrefillWithPagedKVCacheWrapper,
)
# NVFP4 trtllm kernel only supports FP8 output;
# use FP8 o_data_type so the wrapper matches the
# FP8 output buffer allocated in forward().
o_dtype = (
FP8_DTYPE if self.nvfp4_trtllm else self.model_config.dtype
)
prefill_wrapper.plan(
qo_indptr=qo_indptr_prefill_cpu,
paged_kv_indptr=paged_kv_indptr_prefill_cpu,
paged_kv_indices=paged_kv_indices,
paged_kv_last_page_len=paged_kv_last_page_len_prefill_cpu,
seq_lens=kv_lens_prefill_cpu,
num_qo_heads=self.num_qo_heads,
num_kv_heads=self.num_kv_heads,
head_dim_qk=self.head_dim,
page_size=self.page_size,
causal=attn_metadata.causal,
sm_scale=self.sm_scale,
window_left=self.window_left,
logits_soft_cap=self.logits_soft_cap,
q_data_type=self.q_data_type_prefill,
kv_data_type=self.kv_cache_dtype,
o_data_type=o_dtype,
fixed_split_size=self.prefill_fixed_split_size,
disable_split_kv=self.disable_split_kv,
)
attn_metadata.prefill = FIPrefill(wrapper=prefill_wrapper)
## DECODE PATHWAY
if num_decodes > 0:
if decode_with_flashinfer_trtllm_api:
assert self.flashinfer_trtllm_api_decode_kernel is not None
seq_lens_decode = seq_lens[:num_decodes]
if self.use_dcp:
assert common_attn_metadata.dcp_local_seq_lens is not None
seq_lens_decode = common_attn_metadata.dcp_local_seq_lens[
:num_decodes
]
q_len_per_req, q_cu_seq_lens, ragged_q_lens = (
self._compute_decode_query_lens(
qo_indptr, qo_indptr_cpu, num_decodes, num_decode_tokens
)
)
decode_mask = None
if self.use_xqa:
decode_mask = self._get_decode_mask(
q_len_per_req,
ragged_q_lens,
num_decodes,
bool(causal),
)
elif self.use_trtllm_gen_varlen_decode:
# CPU placeholders may underestimate query lengths;
# use a safe upper bound.
q_len_per_req = min(
common_attn_metadata.max_query_len, self.reorder_batch_threshold
)
# Always pass GPU offsets so a graph captured with uniform
# query lengths can replay batches with variable query lengths.
q_cu_seq_lens = qo_indptr[: num_decodes + 1]
attn_metadata.decode = FlashInferTrtllmAPIDecode(
kernel=self.flashinfer_trtllm_api_decode_kernel,
block_tables=block_table_tensor[:num_decodes],
seq_lens=seq_lens_decode,
max_seq_len=max_seq_len,
q_len_per_req=q_len_per_req,
q_cu_seq_lens=q_cu_seq_lens,
mask=decode_mask,
dcp_query_start_loc=(
qo_indptr[: num_decodes + 1] if self.use_dcp else None
),
)
else:
assert seq_lens_cpu is not None
pure_decode = num_prefills == 0
use_cudagraph = (
self.enable_cuda_graph
and pure_decode
and num_decode_tokens <= self._decode_cudagraph_max_bs
)
num_input_tokens = num_decode_tokens
decode_wrapper = self._get_decode_wrapper(
num_input_tokens, use_cudagraph
)
# Use the persistent buffer with padding length,
# instead of the same address but chunked version
# in atten_metadata when using cudagraph.
# NVFP4 trtllm kernel only supports FP8 output;
# use FP8 o_data_type so the wrapper matches the
# FP8 output buffer allocated in forward().
o_dtype = FP8_DTYPE if self.nvfp4_trtllm else self.model_config.dtype
paged_kv_indptr_cpu = self.paged_kv_indptr.cpu[: num_input_tokens + 1]
paged_kv_last_page_len_cpu = self.paged_kv_last_page_len.cpu[
:num_input_tokens
]
# plan() copies these to the GPU with non_blocking=True;
# stage them in pinned memory so the copies stay async
# (the reused buffers themselves are intentionally not pinned).
if PIN_MEMORY:
paged_kv_indptr_cpu = paged_kv_indptr_cpu.pin_memory()
paged_kv_last_page_len_cpu = paged_kv_last_page_len_cpu.pin_memory()
# plan() also copies kv_lens to the GPU with non_blocking=True
# (trtllm-gen/cute-dsl backends), deriving them via
# get_seq_lens() into unpinned memory when seq_lens is not
# passed; pass an explicit pinned copy of the same values
# so that copy stays async too.
kv_lens_decode_cpu = get_seq_lens(
paged_kv_indptr_cpu, paged_kv_last_page_len_cpu, page_size
)
if PIN_MEMORY:
kv_lens_decode_cpu = kv_lens_decode_cpu.pin_memory()
fast_plan_decode(
decode_wrapper,
indptr_cpu=paged_kv_indptr_cpu,
indices=paged_kv_indices,
last_page_len_cpu=paged_kv_last_page_len_cpu,
seq_lens_cpu=kv_lens_decode_cpu,
num_qo_heads=self.num_qo_heads * self.dcp_world_size,
num_kv_heads=self.num_kv_heads,
head_dim=self.head_dim,
page_size=self.page_size,
# Disable flashinfer's pos encoding and use vllm's rope.
pos_encoding_mode="NONE",
sm_scale=self.sm_scale,
window_left=self.window_left,
logits_soft_cap=self.logits_soft_cap,
q_data_type=self.q_data_type_decode,
kv_data_type=self.kv_cache_dtype,
o_data_type=o_dtype,
fixed_split_size=self.decode_fixed_split_size,
disable_split_kv=self.disable_split_kv,
)
attn_metadata.decode = FIDecode(wrapper=decode_wrapper)
return attn_metadata
def update_draft_decode_metadata(self, _metadata: FlashInferMetadata) -> None:
pass
class FlashInferImpl(AttentionImpl):
can_return_lse_for_decode: bool = True
supports_dcp: bool = True
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 = None,
attn_type: AttentionType = AttentionType.DECODER,
kv_sharing_target_layer_name: int | None = None,
sinks: torch.Tensor | None = None,
) -> None:
self.num_heads = num_heads
self.head_size = head_size
self.scale = float(scale)
self.num_kv_heads = num_kv_heads
if alibi_slopes is not None:
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
self.alibi_slopes = alibi_slopes
if sliding_window is None:
self.sliding_window = (-1, -1)
else:
self.sliding_window = (sliding_window - 1, 0)
self.window_left = (
self.sliding_window[0] if self.sliding_window is not None else -1
)
self.cache_dtype = kv_cache_dtype
self.is_kvcache_nvfp4 = kv_cache_dtype.startswith("nvfp4")
self.nvfp4_fa2 = self.is_kvcache_nvfp4 and _nvfp4_kv_on_fa2()
self.nvfp4_trtllm = self.is_kvcache_nvfp4 and not self.nvfp4_fa2
self.kv_cache_dtype = "nvfp4" if self.is_kvcache_nvfp4 else kv_cache_dtype
self.fp4_data_dim = head_size // 2 if self.is_kvcache_nvfp4 else 0
self.logits_soft_cap = logits_soft_cap
self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
if attn_type != AttentionType.DECODER:
raise NotImplementedError(
"Encoder self-attention and "
"encoder/decoder cross-attention "
"are not implemented for "
"FlashInferImpl"
)
self.sinks: torch.Tensor | None = None
# Keep the source so RL weight updates can refresh the runtime tensor.
self._sinks_source = sinks
if sinks is not None:
if sinks.shape[0] != num_heads:
raise ValueError(
"Sinks must have the same number of heads as the number of "
f"heads in the layer. Expected {num_heads}, but got "
f"{sinks.shape[0]}."
)
self.sinks = sinks
self.supports_xqa_or_trtllm_gen_decode = can_use_trtllm_attention(
num_heads, num_kv_heads, is_prefill=False
)
vllm_config = get_current_vllm_config_or_none()
# Query pre-quantization needs a single dtype for the whole query tensor.
# SM90 XQA needs BF16/FP16-Q for decode and FP8 for prefill,
# so only enable this for SM100 trtllm-gen where both use FP8-Q.
self.supports_quant_query_input = (
self.supports_xqa_or_trtllm_gen_decode
and is_quantized_kv_cache(self.kv_cache_dtype)
and current_platform.is_device_capability_family(100)
and vllm_config is not None
and not vllm_config.attention_config.disable_flashinfer_q_quantization
)
self.bmm1_scale: float | None = None
self.bmm2_scale: float | None = None
self.o_sf_scale: float | None = None
# Pre-allocated FP8 output buffer for NVFP4 without fused output quant.
if self.nvfp4_trtllm and vllm_config is not None:
max_num_tokens = vllm_config.scheduler_config.max_num_batched_tokens
self._nvfp4_fp8_out = torch.empty(
(max_num_tokens, num_heads, head_size),
dtype=FP8_DTYPE,
device="cuda",
)
else:
self._nvfp4_fp8_out = None
dcp_a2a = (
vllm_config is not None
and vllm_config.parallel_config.decode_context_parallel_size > 1
and vllm_config.parallel_config.dcp_comm_backend == "a2a"
)
if dcp_a2a:
self.dcp_combine = partial(dcp_a2a_lse_reduce, is_lse_base_on_e=False)
else:
self.dcp_combine = partial(cp_lse_ag_out_rs, is_lse_base_on_e=False)
def fused_output_quant_supported(self, quant_key: QuantKey):
if quant_key == kNvfp4Dynamic and self.is_kvcache_nvfp4:
logger.warning_once(
"fuse_attn_quant: not registering the NVFP4-output attention "
"fusion with an NVFP4 KV cache (no such trtllm-gen kernel). "
"Layers whose o_proj takes NVFP4 input keep the unfused path "
"(FP8 -> BF16 -> o_proj quant); FP8-output fusion is unaffected."
)
return False
# XQA does not support FP8/NVFP4 output, so require trtllm-gen
# (SM100+) here. Without that we cannot fuse the output quant.
return (
self.supports_xqa_or_trtllm_gen_decode
and is_quantized_kv_cache(self.kv_cache_dtype)
and current_platform.is_device_capability_family(100)
and quant_key in (kFp8StaticTensorSym, kNvfp4Dynamic)
)
# FlashInfer requires attention sinks to be float32
def process_weights_after_loading(self, act_dtype: torch.dtype):
source_sinks = self._sinks_source
if source_sinks is None:
return
if source_sinks.dtype == torch.float32:
self.sinks = source_sinks
elif self.sinks is None or self.sinks.dtype != torch.float32:
self.sinks = source_sinks.to(torch.float32)
else:
self.sinks.copy_(source_sinks)
def get_xqa_bmm1_scale(self, layer: torch.nn.Module, q_data_type: torch.dtype):
bmm1_scale = self.scale
if is_quantized_kv_cache(self.kv_cache_dtype):
if q_data_type in (torch.float8_e4m3fn, torch.float8_e5m2):
bmm1_scale *= layer._q_scale_float
bmm1_scale *= layer._k_scale_float
return bmm1_scale
# SM90 may need FP8-Q for native prefill and BF16/FP16-Q for XQA decode,
# so quantize only the slice whose target dtype differs.
def maybe_quant_query(
self,
query: torch.Tensor,
q_data_type: torch.dtype,
scale: torch.Tensor,
) -> torch.Tensor:
if query.dtype != q_data_type:
assert query.dtype in [torch.float16, torch.bfloat16]
assert q_data_type in [torch.float8_e4m3fn, torch.float8_e5m2]
assert query.dim() == 3
num_tokens = query.shape[0]
num_heads = query.shape[1]
head_size = query.shape[2]
assert query.stride(2) == 1 and query.stride(1) == head_size
query_quantized, _ = custom_ops.scaled_fp8_quant(
query.view(num_tokens, num_heads * head_size), scale=scale
)
return query_quantized.view(num_tokens, num_heads, head_size)
return query
def forward(
self,
layer: torch.nn.Module,
query: torch.Tensor,
key: torch.Tensor | None,
value: torch.Tensor | None,
kv_cache: torch.Tensor,
attn_metadata: FlashInferMetadata,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
output_block_scale: torch.Tensor | None = None,
) -> torch.Tensor:
"""Forward pass with FlashInfer.
Args:
layer: The attention layer, providing the q/k/v quantization scales.
query: shape = [num_tokens, num_heads, head_size]
key: shape = [num_tokens, num_kv_heads, head_size], or None for a
KV-sharing decoder layer.
value: shape = [num_tokens, num_kv_heads, head_size], or None for a
KV-sharing decoder layer.
kv_cache: [num_blocks, num_kv_heads, block_size, 2*head_size]
attn_metadata: Metadata for attention.
output: Tensor that the attention result is written into.
output_scale: Scale for fused output quantization. Enables the
attention+quantization fusion path when provided.
output_block_scale: Block scale for fused output quantization,
required for nvfp4 output and rejected for fp8 output.
Returns:
shape = [num_tokens, num_heads * head_size]
"""
if attn_metadata is None:
# Profiling run.
return output.fill_(0)
if self.bmm1_scale is None:
self.bmm1_scale = self.scale
if is_quantized_kv_cache(self.kv_cache_dtype):
self.bmm1_scale *= layer._q_scale_float * layer._k_scale_float
if self.bmm2_scale is None:
self.bmm2_scale = 1.0
if is_quantized_kv_cache(self.kv_cache_dtype):
self.bmm2_scale *= layer._v_scale_float
prefill_use_trtllm = isinstance(attn_metadata.prefill, TRTLLMPrefill)
decode_kernel = (
attn_metadata.decode.kernel
if isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode)
else None
)
decode_with_xqa = decode_kernel == FlashInferDecodeKernel.XQA
decode_with_trtllm_gen = decode_kernel == FlashInferDecodeKernel.TRTLLM_GEN
decode_with_flashinfer_trtllm_api = decode_with_xqa or decode_with_trtllm_gen
# The attn+quant fusion happens when output_scale is provided.
if output_scale is None:
assert output_block_scale is None, (
"output_block_scale is not supported when fusion has not happened"
)
else:
assert attn_metadata.q_data_type_prefill == FP8_DTYPE, (
"Query must be FP8 when attn+quant fusion happened for prefill."
)
assert attn_metadata.q_data_type_decode == FP8_DTYPE, (
"Query must be FP8 when attn+quant fusion happened for decode."
)
assert (attn_metadata.num_prefills == 0 or prefill_use_trtllm) and (
attn_metadata.num_decodes == 0 or decode_with_trtllm_gen
), "Output quant fusion requires TRTLLM prefill/trtllm-gen decode"
if output.dtype == FP8_DTYPE:
assert output_block_scale is None, (
"output_block_scale should not be provided for fp8 output"
)
elif output.dtype == FP4_DTYPE:
assert output_block_scale is not None, (
"output_block_scale is required for nvfp4 output"
)
else:
raise ValueError(f"Unsupported output dtype: {output.dtype}")
# TRTLLM attn kernel requires to scale to pass as a host scalar,
# store the o scale as a host scalar in warmup run with cuda graph
# not enabled
if layer._o_scale_float is None:
layer._o_scale_float = output_scale.cpu().item()
if output.dtype == FP8_DTYPE:
self.bmm2_scale = self.bmm2_scale / layer._o_scale_float
elif output.dtype == FP4_DTYPE:
self.o_sf_scale = layer._o_scale_float
# IMPORTANT!
# NOTE(woosuk): With piece-wise CUDA graphs, this method is executed in
# eager-mode PyTorch. Thus, we need to be careful about any CPU overhead
# in this method. For example, `view` and `slice` (or `[:n]`) operations
# are surprisingly slow even in the case they do not invoke any GPU ops.
# Minimize the PyTorch ops in this method as much as possible.
# Whenever making a change in this method, please benchmark the
# performance to make sure it does not introduce any overhead.
num_actual_tokens = attn_metadata.num_actual_tokens
kv_cache_layout = attn_metadata.kv_cache_layout
# FlashInfer treats uint8 KV cache as NVFP4. vLLM stores FP8 KV cache
# as uint8 bytes, so pass FP8 caches with their logical dtype.
if not self.is_kvcache_nvfp4 and kv_cache.dtype == torch.uint8:
fp8_view_dtype = None
if self.kv_cache_dtype in ("fp8", "fp8_e4m3", torch.float8_e4m3fn):
fp8_view_dtype = torch.float8_e4m3fn
elif self.kv_cache_dtype in ("fp8_e5m2", torch.float8_e5m2):
fp8_view_dtype = torch.float8_e5m2
if fp8_view_dtype is not None:
kv_cache = kv_cache.view(fp8_view_dtype)
# Inputs and outputs may be padded for CUDA graphs. Keep the original
# query for single-token XQA, whose non-ragged API requires one row for
# every (including padded) decode request.
query_padded = query
query = query[:num_actual_tokens]
output_padded = output
output = output[:num_actual_tokens]
# When using spec decoding, num_decodes can be < num_decode_tokens
# because some decode requests may have more than one query token.
num_decode_tokens = attn_metadata.num_decode_tokens
num_prefill_tokens = attn_metadata.num_prefill_tokens
stride_order = kv_cache_layout.layer_view_order
kv_cache_permute = kv_cache.permute(*stride_order) # HND and contiguous
# Fix degenerate strides on any size-1 dimension (e.g. num_kv_heads=1
# with TP=8). PyTorch permits non-canonical strides on size-1 dims;
# CUDA TMA requires ≥16-byte alignment on all non-outermost strides.
# canonicalize_singleton_dim_strides patches metadata via as_strided —
# zero-copy. See vllm.utils.torch_utils.
fixed = canonicalize_singleton_dim_strides(kv_cache_permute)
if fixed is not kv_cache_permute:
logger.debug(
"Canonicalized degenerate KV cache strides (FlashInfer): "
"shape=%s, strides before=%s, strides after=%s",
kv_cache_permute.shape,
kv_cache_permute.stride(),
fixed.stride(),
)
kv_cache_permute = fixed
# Split K/V — zero-copy views. NVFP4 stores K/V as separate head
# groups; other dtypes pack K/V in the content dim.
hs = self.head_size
nvfp4_kv_data = None
nvfp4_kv_block_scales = None
if self.is_kvcache_nvfp4:
k_cache, v_cache = kv_cache.split(self.num_kv_heads, dim=1)
kv_cache_tuple = (
canonicalize_singleton_dim_strides(k_cache.permute(*stride_order)),
canonicalize_singleton_dim_strides(v_cache.permute(*stride_order)),
)
nvfp4_kv_data, nvfp4_kv_block_scales, _ = _nvfp4_kv_views(
kv_cache, self.num_kv_heads
)
else:
kv_cache_tuple = kv_cache_permute.split(hs, dim=-1)
use_dcp = self.dcp_world_size > 1
if decode_with_xqa:
assert not use_dcp
assert self.o_sf_scale is None
assert output.dtype != FP4_DTYPE
# Regular attention (common case).
# Decodes are at the front and prefills are at the back.
if num_prefill_tokens > 0:
prefill_query = query[num_decode_tokens:]
assert prefill_query.shape[0] == num_prefill_tokens
# Convert query to the expected dtype for prefill if needed.
prefill_query = self.maybe_quant_query(
prefill_query,
attn_metadata.q_data_type_prefill,
layer._q_scale,
)
if not prefill_use_trtllm:
assert isinstance(attn_metadata.prefill, FIPrefill)
prefill_wrapper = attn_metadata.prefill.wrapper
assert prefill_wrapper is not None
if use_dcp:
if key is None or value is None:
raise NotImplementedError(
"FlashInfer DCP prefill does not support KV-sharing layers"
)
assert isinstance(prefill_wrapper, BatchDCPPrefillWrapper)
assert prefill_wrapper._context._window_left == self.window_left
assert prefill_wrapper._context._logits_soft_cap == (
self.logits_soft_cap or 0.0
)
assert prefill_wrapper._context._sm_scale == self.scale
assert not prefill_wrapper._context._causal
assert prefill_wrapper._new_tokens._window_left == self.window_left
assert prefill_wrapper._new_tokens._logits_soft_cap == (
self.logits_soft_cap or 0.0
)
assert prefill_wrapper._new_tokens._sm_scale == self.scale
assert prefill_wrapper._new_tokens._causal
prefill_wrapper.run(
layer,
prefill_query,
kv_cache_tuple,
key[num_decode_tokens:num_actual_tokens],
value[num_decode_tokens:num_actual_tokens],
out=output[num_decode_tokens:],
)
else:
assert isinstance(
prefill_wrapper, BatchPrefillWithPagedKVCacheWrapper
)
assert prefill_wrapper._window_left == self.window_left
assert prefill_wrapper._logits_soft_cap == (
self.logits_soft_cap or 0.0
)
assert prefill_wrapper._sm_scale == self.scale
assert prefill_wrapper._causal == attn_metadata.causal
if self.is_kvcache_nvfp4:
kv_cache_for_fi = nvfp4_kv_data
else:
kv_cache_for_fi = kv_cache_tuple
kv_cache_sf = (
nvfp4_kv_block_scales if self.is_kvcache_nvfp4 else None
)
# NVFP4 trtllm kernel only supports FP8 output.
# Use a pre-allocated FP8 buffer and dequantize
# afterwards.
needs_fp8_out_prefill = (
self.nvfp4_trtllm and output.dtype != FP8_DTYPE
)
if needs_fp8_out_prefill:
out_prefill = self._nvfp4_fp8_out[:num_prefill_tokens]
else:
out_prefill = output[num_decode_tokens:]
if isinstance(
prefill_wrapper, BatchAttentionWithAttentionSinkWrapper
):
assert self.sinks is not None
prefill_wrapper.run(
prefill_query,
kv_cache_for_fi,
self.sinks,
self.scale * layer._q_scale_float * layer._k_scale_float,
v_scale=layer._v_scale_float,
out=out_prefill,
)
else:
prefill_wrapper.run(
prefill_query,
kv_cache_for_fi,
# The fa2 NVFP4 query stays in the model dtype.
q_scale=1.0 if self.nvfp4_fa2 else layer._q_scale_float,
k_scale=layer._k_scale_float,
v_scale=layer._v_scale_float,
out=out_prefill,
kv_cache_sf=kv_cache_sf,
)
if needs_fp8_out_prefill:
output[
num_decode_tokens : num_decode_tokens + num_prefill_tokens
].copy_(out_prefill)
else:
assert isinstance(attn_metadata.prefill, TRTLLMPrefill)
# prefill_query may be non-contiguous or have degenerate strides
# on size=1 dims. contiguous() ensures memory layout; then
# canonicalize_singleton_dim_strides fixes any remaining
# degenerate strides on size=1 dims for TMA alignment.
prefill_query = prefill_query.contiguous()
prefill_query = canonicalize_singleton_dim_strides(prefill_query)
workspace_buffer = _get_trtllm_workspace_buffer()
block_tables_prefill = attn_metadata.prefill.block_tables
seq_lens_prefill = attn_metadata.prefill.seq_lens
# This path needs to be enabled with VLLM_KV_CACHE_LAYOUT = HND
assert get_flashinfer_layout_string(kv_cache_layout) == "HND"
assert is_strictly_contiguous(prefill_query)
assert is_strictly_contiguous(workspace_buffer)
assert is_strictly_contiguous(block_tables_prefill)
assert is_strictly_contiguous(seq_lens_prefill)
if output.dtype == FP4_DTYPE:
assert self.o_sf_scale is not None
out = FP4Tensor(
data=output[num_decode_tokens:],
scale=output_block_scale,
scale_start_index=num_decode_tokens,
original_shape=prefill_query.shape,
)
else:
assert self.o_sf_scale is None
out = output[num_decode_tokens:]
# NVFP4 trtllm kernel only supports FP8 output.
# Use a pre-allocated FP8 buffer and dequantize afterwards.
needs_fp8_out = self.is_kvcache_nvfp4 and output.dtype != FP8_DTYPE
if needs_fp8_out:
out = self._nvfp4_fp8_out[:num_prefill_tokens]
prefill_kv_block_scales = None
if self.is_kvcache_nvfp4:
# NVFP4 trtllm-gen kernel requires FP8 query.
assert attn_metadata.q_data_type_prefill == FP8_DTYPE, (
"NVFP4 KV cache requires FP8 quantized queries for "
"trtllm-gen prefill. Set "
"disable_flashinfer_q_quantization=False."
)
mock_kv_cache = nvfp4_kv_data
mock_block_table = block_tables_prefill
prefill_kv_block_scales = nvfp4_kv_block_scales
elif (
attn_metadata.q_data_type_prefill != FP8_DTYPE
and self.kv_cache_dtype.startswith("fp8")
):
# TRTLLM prefill attention does not support BF16 Q
# and fp8 kv cache. So to enable prefill attention
# with fp8 kv cache, we can construct a mock block
# and mock kv cache with BF16 KV involved in the prefill.
kv_cache_permute = canonicalize_singleton_dim_strides(
kv_cache_permute
)
kv_strides = kv_cache_permute.stride()
assert (
kv_strides[-1] == 1
and kv_strides[-2] == kv_cache_permute.shape[-1]
), (
"KV cache inner dims (block_size, head_size) must be "
f"contiguous, got strides {kv_strides}"
)
# fp8 uses (B, H, N, 2*hs); reshape to (B, 2, H, N, hs)
# for the dequant kernel — zero-copy view. The dequant
# kernel handles the interleaved K/V block stride.
B_kv, H_kv, N_kv = kv_cache_permute.shape[:3]
kv_cache_5d = kv_cache_permute.view(B_kv, H_kv, N_kv, 2, hs)
kv_cache_5d = kv_cache_5d.permute(0, 3, 1, 2, 4)
mock_kv_cache, mock_block_table = trtllm_prefill_attn_kvfp8_dequant(
kv_cache_5d,
block_tables_prefill,
layer._k_scale,
layer._v_scale,
attn_metadata.q_data_type_prefill,
)
else:
mock_kv_cache = kv_cache_tuple
mock_block_table = block_tables_prefill
trtllm_batch_context_with_kv_cache(
query=prefill_query,
kv_cache=mock_kv_cache,
workspace_buffer=workspace_buffer,
block_tables=mock_block_table,
seq_lens=seq_lens_prefill,
max_q_len=attn_metadata.prefill.max_q_len,
max_kv_len=attn_metadata.prefill.max_seq_len,
bmm1_scale=self.bmm1_scale,
bmm2_scale=self.bmm2_scale,
batch_size=attn_metadata.num_prefills,
cum_seq_lens_q=attn_metadata.prefill.cum_seq_lens_q,
cum_seq_lens_kv=attn_metadata.prefill.cum_seq_lens_kv,
window_left=self.window_left,
sinks=self.sinks,
o_sf_scale=self.o_sf_scale,
out=out,
kv_cache_sf=prefill_kv_block_scales,
)
if needs_fp8_out:
output[
num_decode_tokens : num_decode_tokens + num_prefill_tokens
].copy_(out[:num_prefill_tokens])
if num_decode_tokens > 0:
decode_query_tokens = num_decode_tokens
if decode_with_xqa:
assert isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode)
if attn_metadata.decode.q_len_per_req == 1:
decode_query_tokens = attn_metadata.num_decodes
decode_query = query_padded[:decode_query_tokens]
assert decode_query.shape[0] == decode_query_tokens
# Convert query to the expected dtype for decode if needed.
decode_query = self.maybe_quant_query(
decode_query,
attn_metadata.q_data_type_decode,
layer._q_scale,
)
if not decode_with_flashinfer_trtllm_api:
assert isinstance(attn_metadata.decode, FIDecode)
decode_wrapper = attn_metadata.decode.wrapper
assert decode_wrapper is not None
assert decode_wrapper._window_left == self.window_left
assert decode_wrapper._logits_soft_cap == (self.logits_soft_cap or 0.0)
assert decode_wrapper._sm_scale == self.scale
if self.is_kvcache_nvfp4:
kv_cache_for_fi = nvfp4_kv_data
else:
kv_cache_for_fi = kv_cache_tuple
kv_cache_sf = nvfp4_kv_block_scales if self.is_kvcache_nvfp4 else None
# NVFP4 kernel only supports FP8 output.
# Use a pre-allocated FP8 buffer and dequantize afterwards.
needs_fp8_out = self.nvfp4_trtllm and output.dtype != FP8_DTYPE
if needs_fp8_out:
out_decode = self._nvfp4_fp8_out[:num_decode_tokens]
else:
out_decode = output[:num_decode_tokens]
if use_dcp:
decode_query = get_dcp_group().all_gather(
decode_query.contiguous(), dim=-2
)
output_tmp = torch.empty_like(decode_query)
lse = torch.empty(
(decode_query.size(0), decode_query.size(1)),
dtype=torch.float32,
device=decode_query.device,
)
decode_wrapper.run(
decode_query,
kv_cache_for_fi,
q_scale=layer._q_scale_float,
k_scale=layer._k_scale_float,
v_scale=layer._v_scale_float,
out=output_tmp,
lse=lse,
return_lse=True,
kv_cache_sf=kv_cache_sf,
sinks=self.sinks,
)
output[:num_decode_tokens] = self.dcp_combine(
output_tmp,
lse,
get_dcp_group(),
)
else:
decode_wrapper.run(
decode_query,
kv_cache_for_fi,
q_scale=1.0 if self.nvfp4_fa2 else layer._q_scale_float,
k_scale=layer._k_scale_float,
v_scale=layer._v_scale_float,
out=out_decode,
kv_cache_sf=kv_cache_sf,
sinks=self.sinks,
)
if needs_fp8_out:
output[:num_decode_tokens].copy_(out_decode)
else:
assert isinstance(attn_metadata.decode, FlashInferTrtllmAPIDecode)
# decode_query may be non-contiguous or have degenerate strides
# on size=1 dims. contiguous() ensures memory layout; then
# canonicalize_singleton_dim_strides fixes any remaining
# degenerate strides on size=1 dims for TMA alignment.
decode_query = decode_query.contiguous()
decode_query = canonicalize_singleton_dim_strides(decode_query)
workspace_buffer = _get_trtllm_workspace_buffer()
block_tables_decode = attn_metadata.decode.block_tables
seq_lens_decode = attn_metadata.decode.seq_lens
# trtllm-gen needs HND layout on SM100. XQA is selected
# separately on SM90 and does not use this SM100 layout gate.
if decode_with_trtllm_gen:
assert get_flashinfer_layout_string(kv_cache_layout) == "HND"
else:
assert decode_with_xqa
assert is_strictly_contiguous(decode_query)
assert is_strictly_contiguous(workspace_buffer)
assert is_strictly_contiguous(block_tables_decode)
assert is_strictly_contiguous(seq_lens_decode)
kv_cache_permute = canonicalize_singleton_dim_strides(kv_cache_permute)
kv_strides = kv_cache_permute.stride()
assert (
kv_strides[-1] == 1 and kv_strides[-2] == kv_cache_permute.shape[-1]
), (
"KV cache inner dims (block_size, head_size) must be "
f"contiguous, got strides {kv_strides}"
)
if use_dcp:
assert decode_with_trtllm_gen
if output.dtype == FP4_DTYPE:
raise NotImplementedError(
"DCP decode with FlashInfer trtllm-gen does not support "
"FP4 attention output yet."
)
decode_query = get_dcp_group().all_gather(
decode_query.contiguous(), dim=-2
)
decode_query = canonicalize_singleton_dim_strides(decode_query)
if decode_with_xqa:
bmm1_scale = self.get_xqa_bmm1_scale(
layer, attn_metadata.q_data_type_decode
)
q_len_per_req: int | None = attn_metadata.decode.q_len_per_req
flashinfer_xqa_batch_decode_with_kv_cache(
query=decode_query,
kv_cache=(
nvfp4_kv_data if self.is_kvcache_nvfp4 else kv_cache_tuple
),
kv_cache_sf=nvfp4_kv_block_scales,
workspace_buffer=workspace_buffer,
block_tables=block_tables_decode,
seq_lens=seq_lens_decode,
max_seq_len=attn_metadata.decode.max_seq_len,
bmm1_scale=bmm1_scale,
bmm2_scale=self.bmm2_scale,
window_left=self.window_left,
out=output_padded[:decode_query_tokens],
sinks=self.sinks,
kv_layout=get_flashinfer_layout_string(kv_cache_layout),
q_len_per_req=q_len_per_req,
mask=attn_metadata.decode.mask,
q_cu_seq_lens=attn_metadata.decode.q_cu_seq_lens,
)
return output_padded
assert decode_with_trtllm_gen
if output.dtype == FP4_DTYPE:
assert self.o_sf_scale is not None
out = FP4Tensor(
data=output[:num_decode_tokens],
scale=output_block_scale,
scale_start_index=0,
original_shape=decode_query.shape,
)
else:
assert self.o_sf_scale is None
out = output[:num_decode_tokens]
# NVFP4 trtllm kernel only supports FP8 output.
# Use a pre-allocated FP8 buffer and dequantize afterwards.
needs_fp8_out = self.is_kvcache_nvfp4 and output.dtype != FP8_DTYPE
if needs_fp8_out:
out = self._nvfp4_fp8_out[:num_decode_tokens]
q_cu_seq_lens = attn_metadata.decode.q_cu_seq_lens
max_q_len = None
q_len_per_req = None
if q_cu_seq_lens is not None:
# q_len_per_req must stay None for the kernel's varlen path.
max_q_len = attn_metadata.decode.q_len_per_req
elif num_decode_tokens % attn_metadata.num_decodes != 0:
# This gets triggered when the dummy_run forces
# attention to be initialized with q_len = 0
q_len_per_req = 1
else:
q_len_per_req = num_decode_tokens // attn_metadata.num_decodes
lse = None
if use_dcp:
out = torch.empty(
decode_query.shape,
dtype=output.dtype,
device=decode_query.device,
)
lse = torch.empty(
(decode_query.size(0), decode_query.size(1)),
dtype=torch.float32,
device=decode_query.device,
)
trtllm_batch_decode_with_kv_cache(
query=decode_query,
kv_cache=(
nvfp4_kv_data if self.is_kvcache_nvfp4 else kv_cache_tuple
),
workspace_buffer=workspace_buffer,
block_tables=block_tables_decode,
seq_lens=seq_lens_decode,
max_seq_len=attn_metadata.decode.max_seq_len,
bmm1_scale=self.bmm1_scale,
bmm2_scale=self.bmm2_scale,
window_left=self.window_left,
sinks=self.sinks,
o_sf_scale=self.o_sf_scale,
out=out,
kv_layout=get_flashinfer_layout_string(kv_cache_layout),
backend=attn_metadata.decode.kernel.value,
q_len_per_req=q_len_per_req,
max_q_len=max_q_len,
cum_seq_lens_q=q_cu_seq_lens,
kv_cache_sf=(
nvfp4_kv_block_scales if self.is_kvcache_nvfp4 else None
),
lse=lse,
return_lse=self.need_to_return_lse_for_decode,
)
if use_dcp:
assert isinstance(out, torch.Tensor)
assert lse is not None
output[:num_decode_tokens] = self.dcp_combine(
out,
lse,
get_dcp_group(),
seq_lens=seq_lens_decode,
query_start_loc=attn_metadata.decode.dcp_query_start_loc,
)
elif needs_fp8_out:
output[:num_decode_tokens].copy_(out)
return output_padded
def do_kv_cache_update(
self,
layer: torch.nn.Module,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
slot_mapping: torch.Tensor,
) -> None:
if self.kv_sharing_target_layer_name is None:
# Reshape the input keys and values and store them in the cache.
# Skip this if sharing KV cache with an earlier attention layer.
# NOTE(woosuk): Here, key and value are padded while slot_mapping is
# not padded. However, we don't need to do key[:num_actual_tokens]
# and value[:num_actual_tokens] because the reshape_and_cache_flash
# op uses the slot_mapping's shape to determine the number of
# actual tokens.
if self.nvfp4_fa2:
data, scales, kv_layout = _nvfp4_kv_views(kv_cache, self.num_kv_heads)
nvfp4_quantize_append_paged_kv_cache_with_slot_mapping(
key,
value,
slot_mapping,
data,
scales,
layer._k_scale,
layer._v_scale,
kv_layout=kv_layout,
)
return
if self.is_kvcache_nvfp4:
# (B, 2*H, N, full_dim) -> ((B, N, H, full_dim),
# (B, N, H, full_dim));
# K heads first, then V heads.
k_cache, v_cache = kv_cache.transpose(1, 2).split(
self.num_kv_heads, dim=-2
)
else:
# (B, H, N, 2*hs) -> ((B, N, H, hs), (B, N, H, hs))
k_cache, v_cache = kv_cache.transpose(1, 2).split(
self.head_size, dim=-1
)
torch.ops._C_cache_ops.reshape_and_cache_flash(
key,
value,
k_cache,
v_cache,
slot_mapping,
self.cache_dtype,
layer._k_scale,
layer._v_scale,
)
def fast_plan_decode(
self, # decode wrapper
indptr_cpu: torch.Tensor,
indices: torch.Tensor,
last_page_len_cpu: torch.Tensor,
seq_lens_cpu: torch.Tensor,
num_qo_heads: int,
num_kv_heads: int,
head_dim: int,
page_size: int,
pos_encoding_mode: str = "NONE",
window_left: int = -1,
logits_soft_cap: float | None = None,
q_data_type: str | torch.dtype | None = "float16",
kv_data_type: str | torch.dtype | None = None,
o_data_type: str | torch.dtype | None = None,
data_type: str | torch.dtype | None = None,
sm_scale: float | None = None,
rope_scale: float | None = None,
rope_theta: float | None = None,
non_blocking: bool = True,
fixed_split_size: int = -1,
disable_split_kv: bool = False,
) -> None:
"""A faster version of BatchDecodeWithPagedKVCacheWrapper::plan used for
cudagraph capture/replay, while the no cudagraph version turns back
to the original plan.
using original plan after passing host-side buffers:
- only host-to-device copy of indptr and last_page_len buffers
Modifications for cudagraph:
- only host-to-device copy of indptr and last_page_len buffers.
- avoid device-to-device copy of indices buffer.
Part of the code get inspiration from the original plan from FlashInfer repo
and the implementation of fast_decode_plan for FlashInfer in SGlang repo.
"""
# Warm up with the original plan if it is first call, and always run the
# original plan if we run for dynamic shape. For fixed shape (cudagraph),
# this warm up is to generate the _cached_module for the decode wrapper.
if not self.is_cuda_graph_enabled or getattr(self, "vllm_first_call", True):
# plan() copies qo_indptr from flashinfer's internal cached host
# arange buffer (_get_range_buf) with non_blocking=True; pin the
# cache entry so the copy stays async.
pin_host_range_buf(len(last_page_len_cpu) + 1)
self.plan(
indptr=indptr_cpu,
indices=indices,
last_page_len=last_page_len_cpu,
num_qo_heads=num_qo_heads,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
page_size=page_size,
pos_encoding_mode=pos_encoding_mode,
window_left=window_left,
logits_soft_cap=logits_soft_cap,
q_data_type=q_data_type,
kv_data_type=kv_data_type,
o_data_type=o_data_type,
data_type=data_type,
sm_scale=sm_scale,
rope_scale=rope_scale,
rope_theta=rope_theta,
non_blocking=non_blocking,
block_tables=None,
seq_lens=seq_lens_cpu,
fixed_split_size=fixed_split_size,
disable_split_kv=disable_split_kv,
)
self.vllm_first_call = False
return
assert self.is_cuda_graph_enabled, "Should be cudagraph only here"
fast_decode_plan(
self,
indptr=indptr_cpu,
indices=indices,
last_page_len=last_page_len_cpu,
num_qo_heads=num_qo_heads,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
page_size=page_size,
pos_encoding_mode=pos_encoding_mode,
window_left=window_left,
logits_soft_cap=logits_soft_cap,
q_data_type=q_data_type,
kv_data_type=kv_data_type,
data_type=data_type,
sm_scale=sm_scale,
rope_scale=rope_scale,
rope_theta=rope_theta,
non_blocking=non_blocking,
fixed_split_size=fixed_split_size,
disable_split_kv=disable_split_kv,
)
@triton.jit
def _copy_page_indices_kernel(
page_indices,
block_table,
block_table_stride,
cu_num_blocks,
BLOCK_SIZE: tl.constexpr,
):
req_idx = tl.program_id(0)
row_ptr = block_table + req_idx * block_table_stride
start_idx = tl.load(cu_num_blocks + req_idx)
end_idx = tl.load(cu_num_blocks + req_idx + 1)
num_blocks = end_idx - start_idx
offset = tl.arange(0, BLOCK_SIZE)
for i in tl.range(0, num_blocks, BLOCK_SIZE):
block_ids = tl.load(row_ptr + i + offset, mask=i + offset < num_blocks)
tl.store(
page_indices + start_idx + i + offset,
block_ids,
mask=i + offset < num_blocks,
)