class FlashMLASparseImpl(SparseMLACommonImpl[FlashMLASparseMetadata]):
can_return_lse_for_decode: bool = True
supports_dcp: bool = True
@staticmethod
def _compute_fp8_decode_padded_heads(num_heads: int) -> int:
# FP8 decode kernel only supports h_q = 64 or 128
# Compute padded head count for decode
return 64 if num_heads <= 64 else 128
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
topk_indices_buffer: torch.Tensor | None = None,
indexer: "Indexer | None" = None,
**mla_args,
) -> None:
super().__init__(
num_heads,
head_size,
scale,
num_kv_heads,
alibi_slopes,
sliding_window,
kv_cache_dtype,
logits_soft_cap,
attn_type,
kv_sharing_target_layer_name,
indexer=indexer,
topk_indices_buffer=topk_indices_buffer,
**mla_args,
)
self.softmax_scale = scale
# Prefill BF16 kernel requires 64 on Hopper, 128 on Blackwell
self.prefill_padding = (
128 if current_platform.is_device_capability_family(100) else 64
)
self.fp8_decode_padded_heads = self._compute_fp8_decode_padded_heads(num_heads)
vllm_config = get_current_vllm_config()
max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
q_concat_heads = num_heads
if not is_quantized_kv_cache(kv_cache_dtype):
q_concat_heads = (
(num_heads + self.prefill_padding - 1)
// self.prefill_padding
* self.prefill_padding
)
q_concat_shape = (max_tokens, q_concat_heads, head_size)
if is_quantized_kv_cache(kv_cache_dtype):
assert kv_cache_dtype in QUANTIZED_DS_MLA_CACHE_FORMATS, (
"FlashMLA Sparse Attention backend only supports the "
f"{sorted(QUANTIZED_DS_MLA_CACHE_FORMATS)} quantized kv-cache "
f"dtypes, got {kv_cache_dtype}"
)
if self.need_to_return_lse_for_decode and not is_quantized_kv_cache(
kv_cache_dtype
):
raise NotImplementedError(
"DCP for FlashMLA sparse requires an fp8_ds_mla kv-cache; "
"the bf16 sparse path is not supported under DCP."
)
if kv_cache_dtype in QUANTIZED_DS_MLA_CACHE_FORMATS:
# Reserve workspace during initialization
assert vllm_config is not None and vllm_config.model_config is not None
prefill_workspace_size = get_prefill_workspace_size(
vllm_config.model_config.max_model_len
)
self.prefill_workspace_shape = (prefill_workspace_size, head_size)
self.q_concat_buffer, self.prefill_bf16_workspace = (
current_workspace_manager().get_simultaneous(
(q_concat_shape, torch.bfloat16),
(self.prefill_workspace_shape, torch.bfloat16),
)
)
else:
(self.q_concat_buffer,) = current_workspace_manager().get_simultaneous(
(q_concat_shape, torch.bfloat16),
)
def _forward_bf16_kv(
self,
q: torch.Tensor,
kv_c_and_k_pe_cache: torch.Tensor,
topk_indices: torch.Tensor,
attn_metadata: FlashMLASparseMetadata,
actual_num_heads: int,
) -> tuple[torch.Tensor, torch.Tensor | None]:
index_group = self.index_group
if isinstance(index_group, HiSparseMLAIndexGroup):
cache = index_group.cache(self.index_group_index)
else:
cache = None
block_table = attn_metadata.block_table
# req_id_per_token covers the whole batch; slice it to the MQA tokens
# (q may exclude prefill tokens routed to dense MHA).
req_id_per_token = attn_metadata.req_id_per_token[: topk_indices.shape[0]]
decode_out: torch.Tensor | None = None
if cache is not None:
assert isinstance(index_group, HiSparseMLAIndexGroup)
num_decode_tokens = attn_metadata.num_decode_tokens
if num_decode_tokens > 0:
decode_topk, decode_lengths = (
index_group.convert_decode_logical_to_physical_topk(
self.index_group_index,
topk_indices[:num_decode_tokens],
attn_metadata,
return_valid_counts=True,
)
)
decode_out, _ = self._bf16_flash_mla_kernel(
q[:num_decode_tokens],
index_group.physical_kv_cache(self.index_group_index),
decode_topk,
decode_lengths,
actual_num_heads,
)
if num_decode_tokens == q.shape[0]:
return decode_out, None
q = q[num_decode_tokens:]
topk_indices = topk_indices[num_decode_tokens:]
kv_c_and_k_pe_cache, block_table, req_id_per_token = (
index_group.stage_prefill_rows(
self.index_group_index, kv_c_and_k_pe_cache, attn_metadata
)
)
# Convert per-request indices to global slots (decode) or workspace offsets.
kv_rows, block_stride_rows = flat_kv_row_view(
kv_c_and_k_pe_cache, attn_metadata.block_size
)
decode_only = (
attn_metadata.num_decode_tokens
== attn_metadata.num_actual_tokens
== topk_indices.shape[0]
)
uses_host_cache = isinstance(index_group, HiSparseMLAIndexGroup)
if not uses_host_cache and decode_only:
topk_indices, topk_length = self._convert_logical_to_physical_topk(
topk_indices,
attn_metadata,
block_stride_rows=block_stride_rows,
return_valid_counts=True,
)
else:
topk_indices, topk_length = triton_convert_req_index_to_global_index(
req_id_per_token,
block_table,
topk_indices,
BLOCK_SIZE=attn_metadata.block_size,
BLOCK_STRIDE_ROWS=block_stride_rows,
NUM_TOPK_TOKENS=topk_indices.shape[1],
return_valid_counts=True,
)
attn_out, lse = self._bf16_flash_mla_kernel(
q,
kv_rows,
topk_indices,
topk_length,
actual_num_heads,
)
if decode_out is None:
return attn_out, lse
return torch.cat([decode_out, attn_out], dim=0), None
def _forward_fp8_kv_separate_prefill_decode(
self,
q: torch.Tensor,
kv_c_and_k_pe_cache: torch.Tensor,
topk_indices: torch.Tensor,
attn_metadata: FlashMLASparseMetadata,
) -> torch.Tensor:
fp8_metadata = attn_metadata.fp8_extra_metadata
assert isinstance(fp8_metadata, FlashMLASparseMetadata.FP8SeparatePrefillDecode)
num_decodes = fp8_metadata.num_decodes
num_mqa_tokens = q.shape[0]
num_decode_tokens = fp8_metadata.num_decode_tokens
num_prefill_tokens = num_mqa_tokens - num_decode_tokens
assert num_prefill_tokens in (0, fp8_metadata.num_prefill_tokens), (
"FP8 sparse MLA expects either the decode subset or the full batch"
)
decode_topk: torch.Tensor | None = None
index_group = self.index_group
uses_host_cache = isinstance(index_group, HiSparseMLAIndexGroup)
if uses_host_cache and num_decode_tokens > 0:
decode_topk = topk_indices[:num_decode_tokens]
prefill_ready = None
if num_prefill_tokens > 0 and uses_host_cache:
assert fp8_metadata.prefill is not None
first_chunk = fp8_metadata.prefill.chunks[0]
assert isinstance(index_group, HiSparseMLAIndexGroup)
prefill_ready = index_group.gather_fp8_prefill(
self.index_group_index,
kv_c_and_k_pe_cache,
self.prefill_bf16_workspace[: first_chunk.chunk_tot_seqlen],
first_chunk.block_table,
first_chunk.workspace_starts,
len(first_chunk.block_table),
attn_metadata,
first_chunk.req_start_idx,
)
prefill_request_ids = None
prefill_workspace_starts = None
has_prefill_workspace = False
if num_prefill_tokens > 0:
assert fp8_metadata.prefill is not None
prefill_request_ids = fp8_metadata.prefill.request_ids
prefill_workspace_starts = fp8_metadata.prefill.workspace_starts
has_prefill_workspace = True
# Convert per-request indices to global slots (decode) or workspace
# offsets (prefill).
# For FP8 cache: prefill uses workspace mapping (upconverted to BF16)
# For BF16 cache: always use global cache slots (no workspace)
# prefill_workspace_starts has been adjusted in-place per chunk so
# prefill indices automatically come out chunk-local
topk_length = None
if num_prefill_tokens == 0 and not uses_host_cache:
topk_indices, topk_length = self._convert_logical_to_physical_topk(
topk_indices,
attn_metadata,
block_stride_rows=None,
return_valid_counts=True,
)
elif num_prefill_tokens > 0:
topk_indices, topk_length = triton_convert_req_index_to_global_index(
attn_metadata.req_id_per_token[: topk_indices.shape[0]],
attn_metadata.block_table,
topk_indices,
BLOCK_SIZE=attn_metadata.block_size,
NUM_TOPK_TOKENS=topk_indices.shape[1],
HAS_PREFILL_WORKSPACE=has_prefill_workspace,
prefill_workspace_request_ids=prefill_request_ids,
prefill_workspace_starts=prefill_workspace_starts,
return_valid_counts=True,
)
fp8_metadata = attn_metadata.fp8_extra_metadata
assert isinstance(fp8_metadata, FlashMLASparseMetadata.FP8SeparatePrefillDecode)
def _fp8_decode(
q: torch.Tensor,
topk_indices: torch.Tensor,
) -> torch.Tensor:
assert fp8_metadata.decode is not None
if uses_host_cache:
return self._host_backed_fp8_decode(
q,
topk_indices,
attn_metadata,
fp8_metadata.decode.kernel_metadata,
num_decodes,
fp8_metadata.decode.decode_query_len,
)
# Reshape q: (num_decode_tokens, num_heads, head_dim)
# -> (num_decodes, seq_len, num_heads, head_dim)
q = reshape_query_for_spec_decode(q, num_decodes)
seq_len = q.shape[1]
# Reshape topk_indices: (num_decode_tokens, topk)
# -> (num_decodes, seq_len, topk)
topk_indices = topk_indices.view(num_decodes, seq_len, -1)
attn_out, _ = self._fp8_flash_mla_kernel(
q=q,
kv_c_and_k_pe_cache=kv_c_and_k_pe_cache,
topk_indices=topk_indices,
kernel_metadata=fp8_metadata.decode.kernel_metadata,
)
# Reshape output: (num_decodes, seq_len, num_heads, head_dim_v)
# -> (num_decode_tokens, num_heads, head_dim_v)
return reshape_attn_output_for_spec_decode(attn_out)
# Pure decode: direct call without allocation
if num_decode_tokens > 0 and num_prefill_tokens == 0:
assert fp8_metadata.decode is not None
attn_out = _fp8_decode(
q, decode_topk if decode_topk is not None else topk_indices
)
else:
# Mixed or pure prefill: allocate output tensor
attn_out = q.new_empty(
(num_mqa_tokens, self.num_heads, self.kv_lora_rank),
dtype=q.dtype,
device=q.device,
)
if num_decode_tokens > 0:
attn_out[:num_decode_tokens] = _fp8_decode(
q[:num_decode_tokens],
decode_topk
if decode_topk is not None
else topk_indices[:num_decode_tokens],
)
assert fp8_metadata.prefill is not None
for chunk_index, chunk in enumerate(fp8_metadata.prefill.chunks):
chunk_workspace = self.prefill_bf16_workspace[: chunk.chunk_tot_seqlen]
if uses_host_cache and chunk_index > 0:
assert isinstance(index_group, HiSparseMLAIndexGroup)
prefill_ready = index_group.gather_fp8_prefill(
self.index_group_index,
kv_c_and_k_pe_cache,
chunk_workspace,
chunk.block_table,
chunk.workspace_starts,
len(chunk.block_table),
attn_metadata,
chunk.req_start_idx,
)
if uses_host_cache:
assert prefill_ready is not None
current_stream().wait_event(prefill_ready)
elif self.kv_cache_dtype == "fp8_ds_mla":
ops.cp_gather_and_upconvert_fp8_kv_cache(
kv_c_and_k_pe_cache,
chunk_workspace,
chunk.block_table,
chunk.workspace_starts,
len(chunk.block_table),
)
else:
ops.cp_gather_and_upconvert_nvfp4_kv_cache(
kv_c_and_k_pe_cache.view(torch.uint8),
chunk_workspace,
chunk.block_table,
chunk.workspace_starts,
len(chunk.block_table),
)
chunk_q = q[chunk.tokens_slice]
chunk_topk_indices_workspace = topk_indices[chunk.tokens_slice]
assert topk_length is not None
chunk_topk_length = topk_length[chunk.tokens_slice]
attn_out[chunk.tokens_slice], _ = self._bf16_flash_mla_kernel(
chunk_q,
chunk_workspace,
chunk_topk_indices_workspace,
chunk_topk_length,
)
return attn_out
def _forward_fp8_kv_mixed_batch(
self,
q: torch.Tensor,
kv_c_and_k_pe_cache: torch.Tensor,
topk_indices: torch.Tensor,
attn_metadata: FlashMLASparseMetadata,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Mixed batch FP8 forward path that treats all tokens as one batch.
This is equivalent to main branch's approach and avoids the BF16
prefill kernel which has head padding overhead when num_heads is small.
Used when use_mixed_batch is True.
The lse is only returned when DCP needs it, otherwise None.
"""
assert attn_metadata.fp8_extra_metadata is not None
assert isinstance(
attn_metadata.fp8_extra_metadata,
FlashMLASparseMetadata.FP8KernelMetadata,
)
fp8_metadata = attn_metadata.fp8_extra_metadata
block_table = attn_metadata.block_table
# req_id_per_token covers the whole batch; slice it to the MQA tokens
# (q may exclude prefill tokens routed to dense MHA).
req_id_per_token = attn_metadata.req_id_per_token[: topk_indices.shape[0]]
if self.dcp_world_size > 1:
# The indexer emits global token ids; keep this rank's shard and
# convert to local slots. compact_valid_to_front=False keeps the
# scattered -1s, which the fp8 kernel masks natively and the
# empty-row neutralization below relies on. req_id is sliced to
# topk_indices rows (the converter grids from req_id).
topk_indices = triton_filter_and_convert_dcp_index(
req_id_per_token,
block_table,
topk_indices,
dcp_size=self.dcp_world_size,
dcp_rank=self.dcp_rank,
cp_kv_cache_interleave_size=attn_metadata.cp_kv_cache_interleave_size,
BLOCK_SIZE=attn_metadata.block_size,
NUM_TOPK_TOKENS=topk_indices.shape[1],
compact_valid_to_front=False,
)
else:
# Convert per-request indices to global slots (decode) or workspace
# offsets (prefill).
decode_only = (
attn_metadata.num_decode_tokens
== attn_metadata.num_actual_tokens
== topk_indices.shape[0]
)
if decode_only:
topk_indices = self._convert_logical_to_physical_topk(
topk_indices,
attn_metadata,
block_stride_rows=None,
return_valid_counts=False,
)
else:
topk_indices = triton_convert_req_index_to_global_index(
req_id_per_token,
block_table,
topk_indices,
BLOCK_SIZE=attn_metadata.block_size,
NUM_TOPK_TOKENS=topk_indices.shape[1],
)
_attn_out, _lse = self._fp8_flash_mla_kernel(
q=q.unsqueeze(0), # unsqueeze to add batch_dim: (T, H, D) -> (1, T, H, D)
kv_c_and_k_pe_cache=kv_c_and_k_pe_cache,
topk_indices=topk_indices.unsqueeze(0), # (T, topk) -> (1, T, topk)
kernel_metadata=fp8_metadata,
)
# Output is (1, T, H, D_v), squeeze back to (T, H, D_v)
out = _attn_out.squeeze(0)
if not self.need_to_return_lse_for_decode:
return out, None
# Kernel LSE is (1, H, T); the DCP merge consumes (T, H).
lse = _lse.squeeze(0).transpose(0, 1)
# Rows where this rank owns none of the selected tokens (all indices
# -1) have undefined out/lse; (0, -inf) is the identity element of the
# cross-rank LSE merge, so it drops this rank from those rows.
empty_rows = (topk_indices == -1).all(dim=-1)
out.masked_fill_(empty_rows.view(-1, 1, 1), 0.0)
lse.masked_fill_(empty_rows.view(-1, 1), float("-inf"))
# The head-padding slice above can leave `out` non-contiguous, and the
# merge feeds it to reduce_scatter.
return out.contiguous(), lse
def _fp8_flash_mla_kernel(
self,
q: torch.Tensor,
kv_c_and_k_pe_cache: torch.Tensor,
topk_indices: torch.Tensor,
kernel_metadata: FlashMLASparseMetadata.FP8KernelMetadata,
) -> tuple[torch.Tensor, torch.Tensor]:
# q shape: (batch, seq_len, num_heads, head_dim)
actual_num_heads = q.size(2)
padded_num_heads = self.fp8_decode_padded_heads
# Pad query if needed (kernel only supports h_q = 64 or 128)
if actual_num_heads < padded_num_heads:
logger.warning_once(
f"Padding num_heads from {actual_num_heads} to "
f"{padded_num_heads} for FP8 sparse decode kernel"
)
q_padded = q.new_zeros((q.size(0), q.size(1), padded_num_heads, q.size(3)))
q_padded[:, :, :actual_num_heads, :] = q
q = q_padded
out, lse = flash_mla_with_kvcache(
q=q,
k_cache=kv_c_and_k_pe_cache.view(torch.uint8).unsqueeze(-2),
block_table=kernel_metadata.dummy_block_table,
head_dim_v=512,
cache_seqlens=kernel_metadata.cache_lens,
tile_scheduler_metadata=kernel_metadata.scheduler_metadata,
is_fp8_kvcache=True,
indices=topk_indices,
softmax_scale=self.softmax_scale,
)
# Slice output and lse back to actual head count if we padded
if actual_num_heads < padded_num_heads:
out = out[:, :, :actual_num_heads, :]
lse = lse[:, :actual_num_heads, :]
return out, lse
def _host_backed_fp8_decode(
self,
q: torch.Tensor,
topk_indices: torch.Tensor,
attn_metadata: FlashMLASparseMetadata,
kernel_metadata: FlashMLASparseMetadata.FP8KernelMetadata,
num_decodes: int,
decode_query_len: int,
) -> torch.Tensor:
assert isinstance(self.index_group, HiSparseMLAIndexGroup)
physical_topk = self.index_group.convert_decode_logical_to_physical_topk(
self.index_group_index,
topk_indices,
attn_metadata,
return_valid_counts=False,
num_decodes=num_decodes,
decode_query_len=decode_query_len,
)
assert isinstance(physical_topk, torch.Tensor)
q = reshape_query_for_spec_decode(q, num_decodes)
physical_topk = physical_topk.view(num_decodes, q.shape[1], -1)
output, _ = self._fp8_flash_mla_kernel(
q=q,
kv_c_and_k_pe_cache=self.index_group.physical_kv_cache(
self.index_group_index
),
topk_indices=physical_topk,
kernel_metadata=kernel_metadata,
)
return reshape_attn_output_for_spec_decode(output)
def _bf16_flash_mla_kernel(
self,
q: torch.Tensor,
kv_c_and_k_pe_cache: torch.Tensor,
topk_indices: torch.Tensor,
topk_length: torch.Tensor | None = None,
actual_num_heads: int | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
num_tokens = q.shape[0]
kv_c_and_k_pe_cache = kv_c_and_k_pe_cache.view(
-1, 1, kv_c_and_k_pe_cache.shape[-1]
)
# NOTE(Chen): kernel requires num_local_head to be a multiple of
# 64 on hopper and 128 on blackwell. Pad from q's head count, not
# self.num_heads: under DCP the heads are all-gathered before this.
if actual_num_heads is None:
actual_num_heads = q.shape[1]
padded_num_heads = (
(actual_num_heads + self.prefill_padding - 1)
// self.prefill_padding
* self.prefill_padding
)
if q.shape[1] < padded_num_heads:
logger.warning_once(
f"Padding num_heads from {actual_num_heads} to "
f"{padded_num_heads} for BF16 sparse prefill kernel"
)
q_padded = q.new_empty((q.shape[0], padded_num_heads, q.shape[2]))
q_padded[:, :actual_num_heads, :] = q
q = q_padded
topk_indices = topk_indices.view(num_tokens, 1, -1)
output, _, lse = flash_mla_sparse_fwd(
q,
kv_c_and_k_pe_cache,
topk_indices,
self.softmax_scale,
topk_length=topk_length,
)
output = output[:, :actual_num_heads, :]
lse = lse[:, :actual_num_heads]
return output, lse
def forward_mqa(
self,
q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
kv_c_and_k_pe_cache: torch.Tensor,
attn_metadata: FlashMLASparseMetadata,
layer: AttentionLayer,
) -> tuple[torch.Tensor, torch.Tensor | None]:
# NOTE(lucas): for the sparse FlashMLA kernels the kernels want to use
# MQA 576/512 approach for both prefill and decode
# Concatenate q if it's a tuple (ql_nope, q_pe)
actual_num_heads = self.num_heads
if isinstance(q, tuple):
ql_nope, q_pe = q
q = self.q_concat_buffer[: ql_nope.shape[0]]
ops.concat_mla_q(ql_nope, q_pe, q)
else:
actual_num_heads = q.shape[1]
num_actual_toks = q.shape[0]
# Get topk indices
assert self.topk_indices_buffer is not None
topk_indices = self.topk_indices_buffer[:num_actual_toks]
use_fp8_cache = self.kv_cache_dtype in QUANTIZED_DS_MLA_CACHE_FORMATS
lse: torch.Tensor | None = None
if not use_fp8_cache:
attn_out, bf16_lse = self._forward_bf16_kv(
q,
kv_c_and_k_pe_cache,
topk_indices,
attn_metadata,
actual_num_heads,
)
if self.need_to_return_lse_for_decode:
lse = bf16_lse
elif attn_metadata.fp8_use_mixed_batch:
attn_out, lse = self._forward_fp8_kv_mixed_batch(
q, kv_c_and_k_pe_cache, topk_indices, attn_metadata
)
else:
attn_out = self._forward_fp8_kv_separate_prefill_decode(
q, kv_c_and_k_pe_cache, topk_indices, attn_metadata
)
return attn_out, lse