[AMD] Support fp8 MLA for diffusion model (#20319)
This commit is contained in:
Regular → Executable
+302
-1
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import aiter
|
import aiter
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
@@ -12,6 +16,47 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend i
|
|||||||
AttentionMetadataBuilder,
|
AttentionMetadataBuilder,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||||
|
from sglang.srt.models.deepseek_common.utils import _use_aiter_gfx95
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_use_fp8_attn = os.environ.get("SGLANG_AITER_FP8_ATTN", "0") == "1"
|
||||||
|
_fp8_dtype = torch.float8_e4m3fn
|
||||||
|
|
||||||
|
# ── MLA prefill ASM kernel constraints ──────────────────────────────
|
||||||
|
# The only available FP8 prefill kernel is the pre-compiled ASM binary
|
||||||
|
# mla_pfl_qh192_vh128_m32x8_n128x1_causal{0,1}.co, originally built for
|
||||||
|
# DeepSeek-style MLA. Four hard constraints:
|
||||||
|
#
|
||||||
|
# 1. GPU arch must be gfx950 (MI350/MI355). The ASM binary is compiled
|
||||||
|
# exclusively for gfx950; it will crash or fail to load on other archs.
|
||||||
|
#
|
||||||
|
# 2. qk_head_dim baked at 192. Models with smaller head dims (e.g.
|
||||||
|
# Wan's 128) are handled by zero-padding Q/K — extra dims contribute
|
||||||
|
# 0 to dot products, preserving correctness.
|
||||||
|
#
|
||||||
|
# 3. v_head_dim baked at 128. Models with V head dim != 128 cannot use
|
||||||
|
# this kernel.
|
||||||
|
#
|
||||||
|
# 4. Kernel tiles over heads in groups of 8 ("m32x8" = 32 tokens x 8
|
||||||
|
# heads per tile). num_heads not divisible by 8 causes OOB reads.
|
||||||
|
# E.g. Ulysses SP degree=4 with 40 heads -> 10 heads/rank -> crash.
|
||||||
|
_MLA_PREFILL_QK_HEAD_DIM = 192
|
||||||
|
_MLA_PREFILL_V_HEAD_DIM = 128
|
||||||
|
_MLA_PREFILL_HEAD_TILE = 8
|
||||||
|
|
||||||
|
|
||||||
|
if _use_fp8_attn:
|
||||||
|
logger.info("DiT FP8 attention enabled via SGLANG_AITER_FP8_ATTN=1")
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_mla_prefill(v_head_dim: int, num_heads: int) -> bool:
|
||||||
|
"""Check if the MLA prefill ASM kernel supports the given shape and GPU."""
|
||||||
|
return (
|
||||||
|
_use_aiter_gfx95
|
||||||
|
and v_head_dim == _MLA_PREFILL_V_HEAD_DIM
|
||||||
|
and num_heads % _MLA_PREFILL_HEAD_TILE == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AITerBackend(AttentionBackend):
|
class AITerBackend(AttentionBackend):
|
||||||
@@ -37,6 +82,225 @@ class AITerBackend(AttentionBackend):
|
|||||||
raise NotImplementedError("AITer backend does not have a metadata builder.")
|
raise NotImplementedError("AITer backend does not have a metadata builder.")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_mla_prefill_metadata(
|
||||||
|
batch_size: int,
|
||||||
|
seq_lens: torch.Tensor,
|
||||||
|
num_heads: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
is_causal: bool,
|
||||||
|
block_size: int = 1,
|
||||||
|
tile_q: int = 256,
|
||||||
|
tile_kv: int = 128,
|
||||||
|
kv_seq_lens: Optional[torch.Tensor] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Build persistent-scheduling metadata required by mla_prefill_ps_asm_fwd.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch_size: number of sequences in the batch.
|
||||||
|
seq_lens: [batch_size] int tensor with per-sequence Q lengths (on CPU).
|
||||||
|
num_heads: number of query heads.
|
||||||
|
num_kv_heads: number of KV heads.
|
||||||
|
is_causal: whether causal masking is used.
|
||||||
|
block_size: KV page size (1 for non-paged token-level layout).
|
||||||
|
tile_q: Q tile size used by the kernel.
|
||||||
|
tile_kv: KV tile granularity.
|
||||||
|
kv_seq_lens: [batch_size] int tensor with per-sequence KV lengths (on CPU).
|
||||||
|
If None, defaults to seq_lens (self-attention).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with all metadata tensors needed by the kernel + reduce.
|
||||||
|
"""
|
||||||
|
if kv_seq_lens is None:
|
||||||
|
kv_seq_lens = seq_lens
|
||||||
|
|
||||||
|
device = "cuda"
|
||||||
|
gqa_ratio = num_heads // num_kv_heads
|
||||||
|
|
||||||
|
qo_indptr = torch.zeros(batch_size + 1, dtype=torch.int32)
|
||||||
|
kv_indptr = torch.zeros(batch_size + 1, dtype=torch.int32)
|
||||||
|
|
||||||
|
qo_indptr[1 : batch_size + 1] = torch.cumsum(seq_lens, dim=0)
|
||||||
|
actual_blocks = (kv_seq_lens + block_size - 1) // block_size
|
||||||
|
kv_indptr[1 : batch_size + 1] = torch.cumsum(actual_blocks, dim=0)
|
||||||
|
num_blocks = int(kv_indptr[-1])
|
||||||
|
|
||||||
|
kv_indices = torch.arange(num_blocks, dtype=torch.int32)
|
||||||
|
|
||||||
|
max_qlen = seq_lens.max()
|
||||||
|
|
||||||
|
qhead_granularity = gqa_ratio
|
||||||
|
qlen_granularity = tile_q // qhead_granularity
|
||||||
|
kvlen_granularity = max(tile_kv, block_size)
|
||||||
|
|
||||||
|
(
|
||||||
|
(work_meta_data_size, work_meta_data_type),
|
||||||
|
(work_indptr_size, work_indptr_type),
|
||||||
|
(work_info_size, work_info_type),
|
||||||
|
(reduce_indptr_size, reduce_indptr_type),
|
||||||
|
(reduce_final_map_size, reduce_final_map_type),
|
||||||
|
(reduce_partial_map_size, reduce_partial_map_type),
|
||||||
|
) = aiter.get_ps_metadata_info_v1(
|
||||||
|
batch_size=batch_size,
|
||||||
|
num_head_k=num_kv_heads,
|
||||||
|
max_qlen=max_qlen,
|
||||||
|
qlen_granularity=qlen_granularity,
|
||||||
|
)
|
||||||
|
|
||||||
|
work_metadata_ptrs = torch.empty(
|
||||||
|
work_meta_data_size, dtype=work_meta_data_type, device=device
|
||||||
|
)
|
||||||
|
work_indptr = torch.empty(work_indptr_size, dtype=work_indptr_type, device=device)
|
||||||
|
work_info = torch.empty(work_info_size, dtype=work_info_type, device=device)
|
||||||
|
reduce_indptr = torch.empty(
|
||||||
|
reduce_indptr_size, dtype=reduce_indptr_type, device=device
|
||||||
|
)
|
||||||
|
reduce_final_map = torch.empty(
|
||||||
|
reduce_final_map_size, dtype=reduce_final_map_type, device=device
|
||||||
|
)
|
||||||
|
reduce_partial_map = torch.empty(
|
||||||
|
reduce_partial_map_size, dtype=reduce_partial_map_type, device=device
|
||||||
|
)
|
||||||
|
|
||||||
|
aiter.get_ps_metadata_v1(
|
||||||
|
qo_indptr.cpu(),
|
||||||
|
kv_indptr.cpu(),
|
||||||
|
seq_lens.cpu().int(),
|
||||||
|
gqa_ratio,
|
||||||
|
num_kv_heads,
|
||||||
|
work_metadata_ptrs,
|
||||||
|
work_indptr,
|
||||||
|
work_info,
|
||||||
|
reduce_indptr,
|
||||||
|
reduce_final_map,
|
||||||
|
reduce_partial_map,
|
||||||
|
qhead_granularity=qhead_granularity,
|
||||||
|
qlen_granularity=qlen_granularity,
|
||||||
|
kvlen_granularity=kvlen_granularity,
|
||||||
|
block_size=block_size,
|
||||||
|
is_causal=is_causal,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"qo_indptr": qo_indptr.to(device),
|
||||||
|
"kv_indptr": kv_indptr.to(device),
|
||||||
|
"kv_indices": kv_indices.to(device),
|
||||||
|
"work_indptr": work_indptr,
|
||||||
|
"work_info": work_info,
|
||||||
|
"reduce_indptr": reduce_indptr,
|
||||||
|
"reduce_final_map": reduce_final_map,
|
||||||
|
"reduce_partial_map": reduce_partial_map,
|
||||||
|
"max_seqlen_q": max_qlen,
|
||||||
|
"tile_q": tile_q,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@torch.compiler.disable
|
||||||
|
def _mla_prefill_ps_attention(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k: torch.Tensor,
|
||||||
|
v: torch.Tensor,
|
||||||
|
softmax_scale: float,
|
||||||
|
is_causal: bool,
|
||||||
|
q_scale: Optional[torch.Tensor] = None,
|
||||||
|
k_scale: Optional[torch.Tensor] = None,
|
||||||
|
v_scale: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Run mla_prefill_ps_asm_fwd + mla_reduce_v1 on 4D batch tensors.
|
||||||
|
|
||||||
|
Reshapes [B, S, H, D] -> varlen [B*S, H, D] with trivial indptr,
|
||||||
|
calls the kernel, then reshapes back.
|
||||||
|
|
||||||
|
Supports cross-attention where q has seq_len S_q and k/v have seq_len S_kv.
|
||||||
|
|
||||||
|
The ASM kernel has qk_head_dim=192 baked in at compile time (see module-level
|
||||||
|
comments). If the model's head dim is smaller (e.g. 128), Q and K are
|
||||||
|
zero-padded along the last dimension to 192 before calling the kernel.
|
||||||
|
The padded zeros contribute nothing to the QK dot product, so attention
|
||||||
|
scores are identical to the unpadded case.
|
||||||
|
"""
|
||||||
|
B, S_q, H, D_q = q.shape
|
||||||
|
S_kv = k.shape[1]
|
||||||
|
D_v = v.shape[-1]
|
||||||
|
device = q.device
|
||||||
|
num_kv_heads = k.shape[2]
|
||||||
|
|
||||||
|
# Zero-pad Q/K head dim to match the kernel's compiled qk_head_dim=192.
|
||||||
|
# Padding with zeros preserves dot-product correctness.
|
||||||
|
pad_qk = _MLA_PREFILL_QK_HEAD_DIM - D_q
|
||||||
|
if pad_qk > 0:
|
||||||
|
q = torch.nn.functional.pad(q, (0, pad_qk))
|
||||||
|
k = torch.nn.functional.pad(k, (0, pad_qk))
|
||||||
|
D_q_kernel = q.shape[-1]
|
||||||
|
|
||||||
|
q_varlen = q.reshape(B * S_q, H, D_q_kernel).contiguous()
|
||||||
|
k_varlen = k.reshape(B * S_kv, num_kv_heads, D_q_kernel).contiguous()
|
||||||
|
v_varlen = v.reshape(B * S_kv, num_kv_heads, D_v).contiguous()
|
||||||
|
|
||||||
|
q_seq_lens = torch.full((B,), S_q, dtype=torch.int32)
|
||||||
|
kv_seq_lens = torch.full((B,), S_kv, dtype=torch.int32)
|
||||||
|
|
||||||
|
meta = _build_mla_prefill_metadata(
|
||||||
|
batch_size=B,
|
||||||
|
seq_lens=q_seq_lens,
|
||||||
|
kv_seq_lens=kv_seq_lens,
|
||||||
|
num_heads=H,
|
||||||
|
num_kv_heads=num_kv_heads,
|
||||||
|
is_causal=is_causal,
|
||||||
|
block_size=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_s = B * S_q
|
||||||
|
tile_q = meta["tile_q"]
|
||||||
|
|
||||||
|
output = torch.empty((total_s, H, D_v), dtype=torch.bfloat16, device=device)
|
||||||
|
logits = torch.empty(
|
||||||
|
(meta["reduce_partial_map"].size(0) * tile_q, H, D_v),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
attn_lse = torch.empty(
|
||||||
|
(meta["reduce_partial_map"].size(0) * tile_q, H),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
final_lse = torch.empty((total_s, H), dtype=torch.float32, device=device)
|
||||||
|
|
||||||
|
aiter.mla_prefill_ps_asm_fwd(
|
||||||
|
q_varlen,
|
||||||
|
k_varlen,
|
||||||
|
v_varlen,
|
||||||
|
meta["qo_indptr"],
|
||||||
|
meta["kv_indptr"],
|
||||||
|
meta["kv_indices"],
|
||||||
|
meta["work_indptr"],
|
||||||
|
meta["work_info"],
|
||||||
|
meta["max_seqlen_q"],
|
||||||
|
softmax_scale,
|
||||||
|
is_causal,
|
||||||
|
logits,
|
||||||
|
attn_lse,
|
||||||
|
output,
|
||||||
|
q_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
aiter.mla_reduce_v1(
|
||||||
|
logits,
|
||||||
|
attn_lse,
|
||||||
|
meta["reduce_indptr"],
|
||||||
|
meta["reduce_final_map"],
|
||||||
|
meta["reduce_partial_map"],
|
||||||
|
tile_q,
|
||||||
|
output,
|
||||||
|
final_lse,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output.view(B, S_q, H, D_v)
|
||||||
|
|
||||||
|
|
||||||
class AITerImpl(AttentionImpl):
|
class AITerImpl(AttentionImpl):
|
||||||
"""
|
"""
|
||||||
Implementation of attention using AITemplate.
|
Implementation of attention using AITemplate.
|
||||||
@@ -59,7 +323,9 @@ class AITerImpl(AttentionImpl):
|
|||||||
)
|
)
|
||||||
self.causal = causal
|
self.causal = causal
|
||||||
self.dropout_p = dropout_p
|
self.dropout_p = dropout_p
|
||||||
|
self.softmax_scale = softmax_scale
|
||||||
|
|
||||||
|
@torch.compiler.disable
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
query: torch.Tensor,
|
query: torch.Tensor,
|
||||||
@@ -68,7 +334,9 @@ class AITerImpl(AttentionImpl):
|
|||||||
attn_metadata: AttentionMetadata | None = None,
|
attn_metadata: AttentionMetadata | None = None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""
|
"""
|
||||||
Performs attention using aiter.flash_attn_func.
|
Performs attention using one of:
|
||||||
|
- _mla_prefill_ps_attention (FP8, SGLANG_AITER_FP8_ATTN=1)
|
||||||
|
- flash_attn_func (BF16, default or FP8 fallback for unsupported shapes)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
query: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
||||||
@@ -79,6 +347,39 @@ class AITerImpl(AttentionImpl):
|
|||||||
Returns:
|
Returns:
|
||||||
Output tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
Output tensor of shape [batch_size, seq_len, num_heads, head_dim]
|
||||||
"""
|
"""
|
||||||
|
if _use_fp8_attn:
|
||||||
|
if query.dtype != _fp8_dtype:
|
||||||
|
q_fp8, q_scale = aiter.per_tensor_quant(query, quant_dtype=_fp8_dtype)
|
||||||
|
k_fp8, k_scale = aiter.per_tensor_quant(key, quant_dtype=_fp8_dtype)
|
||||||
|
v_fp8, v_scale = aiter.per_tensor_quant(value, quant_dtype=_fp8_dtype)
|
||||||
|
else:
|
||||||
|
q_fp8, k_fp8, v_fp8 = query, key, value
|
||||||
|
one = torch.tensor(1.0, dtype=torch.float32, device=query.device)
|
||||||
|
q_scale = k_scale = v_scale = one
|
||||||
|
|
||||||
|
if _can_use_mla_prefill(v_fp8.shape[-1], q_fp8.shape[2]):
|
||||||
|
return _mla_prefill_ps_attention(
|
||||||
|
q_fp8,
|
||||||
|
k_fp8,
|
||||||
|
v_fp8,
|
||||||
|
softmax_scale=self.softmax_scale,
|
||||||
|
is_causal=self.causal,
|
||||||
|
q_scale=q_scale,
|
||||||
|
k_scale=k_scale,
|
||||||
|
v_scale=v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.warning_once(
|
||||||
|
"FP8 MLA prefill kernel unsupported "
|
||||||
|
"(need gfx950, v_head_dim=%d, num_heads divisible by %d; "
|
||||||
|
"got v_head_dim=%d, num_heads=%d). Falling back to BF16.",
|
||||||
|
_MLA_PREFILL_V_HEAD_DIM,
|
||||||
|
_MLA_PREFILL_HEAD_TILE,
|
||||||
|
v_fp8.shape[-1],
|
||||||
|
q_fp8.shape[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
# BF16 path
|
||||||
output, _ = aiter.flash_attn_func(
|
output, _ = aiter.flash_attn_func(
|
||||||
query,
|
query,
|
||||||
key,
|
key,
|
||||||
|
|||||||
Reference in New Issue
Block a user