feat: MLA prefill enable FA4 fp8 output (#43050)

Signed-off-by: Carl You <4531192+carlyou@users.noreply.github.com>
This commit is contained in:
Carl Y
2026-06-16 07:10:59 -07:00
committed by GitHub
parent ce3ef17bec
commit eb04c769d3
13 changed files with 489 additions and 20 deletions
+82 -1
View File
@@ -53,6 +53,16 @@ from common import (
from vllm.v1.worker.workspace import init_workspace_manager
def _str2bool(v) -> bool:
if isinstance(v, bool):
return v
if v.lower() in ("true", "1", "yes", "t"):
return True
if v.lower() in ("false", "0", "no", "f"):
return False
raise argparse.ArgumentTypeError(f"expected a boolean, got {v!r}")
def run_standard_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
"""Run standard attention benchmark (Flash/Triton/FlashInfer)."""
from runner import run_attention_benchmark
@@ -485,6 +495,20 @@ def main():
help="Prefill backends to compare (fa2, fa3, fa4). "
"Uses the first decode backend for impl construction.",
)
parser.add_argument(
"--fp8-output-scale",
type=float,
help="Static per-tensor scale enabling the MLA prefill FP8-output "
"comparison on FA4 (fused write vs standalone post-quant).",
)
parser.add_argument(
"--fuse-quant-op",
nargs="+",
type=_str2bool,
help="FP8-output write path(s) to run: false = bf16 attention + "
"standalone static-FP8 quant, true = FA4 writes FP8 directly. "
"Default: both.",
)
# Batch specifications
parser.add_argument(
@@ -618,6 +642,12 @@ def main():
# Prefill backends (e.g., ["fa3", "fa4"])
args.prefill_backends = yaml_config.get("prefill_backends", None)
# FP8 output benchmark knobs; CLI wins.
if args.fp8_output_scale is None:
args.fp8_output_scale = yaml_config.get("fp8_output_scale", None)
if args.fuse_quant_op is None:
args.fuse_quant_op = yaml_config.get("fuse_quant_op", None)
# Check for special modes
args.mode = yaml_config.get("mode", None)
@@ -787,8 +817,59 @@ def main():
"skipped (timings are placeholder zeros).[/]"
)
# FA4 fused FP8 output vs standalone post-quant, on the same fa4 kernel:
# the delta is the post-quant kernel the fused path removes.
fp8_output_scale = getattr(args, "fp8_output_scale", None)
if fp8_output_scale is not None:
decode_backend = backends[0]
fuse_variants = args.fuse_quant_op or [False, True]
label_of = {False: "post_quant", True: "fused"}
console.print(
f"[yellow]FP8 output comparison @ scale={fp8_output_scale} "
f"(prefill=fa4, decode impl={decode_backend})[/]"
)
fp8_results = []
total = len(fuse_variants) * len(args.batch_specs)
with tqdm(total=total, desc="FP8 output benchmarking") as pbar:
for spec in args.batch_specs:
for fuse in fuse_variants:
config = BenchmarkConfig(
backend=decode_backend,
batch_spec=spec,
num_layers=args.num_layers,
head_dim=args.head_dim,
num_q_heads=args.num_q_heads,
num_kv_heads=args.num_kv_heads,
block_size=args.block_size,
device=args.device,
repeats=args.repeats,
warmup_iters=args.warmup_iters,
profile_memory=args.profile_memory,
kv_cache_dtype=args.kv_cache_dtype,
use_cuda_graphs=args.cuda_graphs,
prefill_backend="fa4",
)
result = run_benchmark(
config, output_scale=fp8_output_scale, fuse_quant_op=fuse
)
label = label_of[fuse]
labeled_config = replace(result.config, backend=label)
result = replace(result, config=labeled_config)
fp8_results.append(result)
if not result.success:
console.print(f"[red]Error {label} {spec}: {result.error}[/]")
pbar.update(1)
console.print("\n[bold green]FP8 Output Results:[/]")
formatter = ResultsFormatter(console)
labels = [label_of[f] for f in fuse_variants]
formatter.print_table(fp8_results, labels, compare_to_fastest=True)
all_results = fp8_results
# Handle special mode: decode_vs_prefill comparison
if hasattr(args, "mode") and args.mode == "decode_vs_prefill":
elif hasattr(args, "mode") and args.mode == "decode_vs_prefill":
console.print("[yellow]Mode: Decode vs Prefill pipeline comparison[/]")
console.print(
"[dim]For each query length, testing both decode and prefill pipelines[/]"
@@ -0,0 +1,44 @@
# MLA prefill FP8-output microbenchmark (FA4).
# Compares the fused FP8 write against bf16 attention + a standalone static-FP8
# quant; the delta is the post-quant kernel the fused path removes.
# DeepSeek-Coder-V2-Lite dims; FA4 needs SM100/110.
#
# Usage:
# python benchmark.py --config configs/mla_fa4_fp8_output.yaml
description: "MLA prefill FA4 fused-FP8 output vs post-quant"
model:
name: "deepseek-v2-lite"
num_layers: 27
num_q_heads: 16
num_kv_heads: 1
head_dim: 576
kv_lora_rank: 512
qk_nope_head_dim: 128
qk_rope_head_dim: 64
v_head_dim: 128
block_size: 128
# Pure prefill (q_len == kv_len) so every token goes through forward_mha.
batch_specs:
- "q512"
- "q1k"
- "q2k"
- "q4k"
- "q8k"
- "2q4k"
- "4q4k"
- "8q4k"
# Only used to construct the MLA impl; the pure-prefill specs skip decode.
decode_backends:
- CUTLASS_MLA
# Sweep the two FP8 write paths (prefill backend is fixed to fa4).
fp8_output_scale: 0.1
fuse_quant_op: [false, true]
device: "cuda:0"
repeats: 50
warmup_iters: 10
+64 -11
View File
@@ -708,6 +708,8 @@ def _run_single_benchmark(
device: torch.device,
indexer=None,
kv_cache_dtype: str | None = None,
output_scale: float | None = None,
fuse_quant_op: bool = False,
) -> BenchmarkResult:
"""
Run a single benchmark iteration.
@@ -721,6 +723,11 @@ def _run_single_benchmark(
mla_dims: MLA dimension configuration
device: Target device
indexer: Optional MockIndexer for sparse backends
output_scale: Static per-tensor FP8 scale for prefill output. None
keeps the plain bf16 output (no quantization).
fuse_quant_op: With output_scale set, True lets the prefill kernel write
FP8 directly; False runs bf16 attention then a standalone static-FP8
quant. The delta isolates the saved post-quant kernel.
Returns:
BenchmarkResult with timing statistics
@@ -824,23 +831,55 @@ def _run_single_benchmark(
num_prefill, mla_dims, query_fmt, device, torch.bfloat16
)
# Prefill FP8 output: fused (kernel writes e4m3) vs separate post-quant.
prefill_fp8_output = None
prefill_output_scale = None
prefill_quant_op = None
if has_prefill and output_scale is not None:
from vllm.platforms import current_platform
prefill_output_scale = torch.tensor(
[output_scale], device=device, dtype=torch.float32
)
if fuse_quant_op:
prefill_fp8_output = torch.empty_like(
prefill_inputs["output"], dtype=current_platform.fp8_dtype()
)
else:
from vllm.model_executor.layers.quantization.input_quant_fp8 import (
QuantFP8,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import (
GroupShape,
)
prefill_quant_op = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
fused_output = output_scale is not None and fuse_quant_op
# Build forward function (runs a single decode/prefill pass)
def forward_fn():
results = []
if has_decode:
results.append(impl.forward_mqa(decode_inputs, kv_cache, metadata, layer))
if has_prefill:
results.append(
impl.forward_mha(
prefill_inputs["q"],
prefill_inputs["k_c_normed"],
prefill_inputs["k_pe"],
kv_cache,
metadata,
prefill_inputs["k_scale"],
prefill_inputs["output"],
)
out = impl.forward_mha(
prefill_inputs["q"],
prefill_inputs["k_c_normed"],
prefill_inputs["k_pe"],
kv_cache,
metadata,
prefill_inputs["k_scale"],
prefill_fp8_output if fused_output else prefill_inputs["output"],
prefill_output_scale if fused_output else None,
)
if fused_output:
out = prefill_fp8_output
elif prefill_quant_op is not None:
out, _ = prefill_quant_op(
prefill_inputs["output"], prefill_output_scale
)
results.append(out)
return results[0] if len(results) == 1 else tuple(results)
def benchmark_fn():
@@ -881,6 +920,8 @@ def _run_mla_benchmark_batched(
configs_with_params: list[tuple], # [(config, threshold, num_splits), ...]
index_topk: int = 2048,
prefill_backend: str | None = None,
output_scale: float | None = None,
fuse_quant_op: bool = False,
) -> list[BenchmarkResult]:
"""
Unified batched MLA benchmark runner for all backends.
@@ -1020,6 +1061,8 @@ def _run_mla_benchmark_batched(
device,
indexer=indexer,
kv_cache_dtype=kv_cache_dtype,
output_scale=output_scale,
fuse_quant_op=fuse_quant_op,
)
results.append(result)
@@ -1047,6 +1090,8 @@ def run_mla_benchmark(
num_kv_splits: int | None = None,
index_topk: int = 2048,
prefill_backend: str | None = None,
output_scale: float | None = None,
fuse_quant_op: bool = False,
) -> BenchmarkResult | list[BenchmarkResult]:
"""
Unified MLA benchmark runner for all backends.
@@ -1066,6 +1111,9 @@ def run_mla_benchmark(
index_topk: Topk value for sparse MLA backends (default 2048)
prefill_backend: Prefill backend name (e.g., "fa3", "fa4").
When set, forces the specified FlashAttention version for prefill.
output_scale: Static per-tensor FP8 scale for prefill output (None = bf16).
fuse_quant_op: With output_scale set, fuse the FP8 write into the prefill
kernel vs a standalone post-quant kernel. See _run_single_benchmark.
Returns:
BenchmarkResult (single mode) or list of BenchmarkResult (batched mode)
@@ -1090,7 +1138,12 @@ def run_mla_benchmark(
# Use unified batched execution
results = _run_mla_benchmark_batched(
backend, configs_with_params, index_topk, prefill_backend=prefill_backend
backend,
configs_with_params,
index_topk,
prefill_backend=prefill_backend,
output_scale=output_scale,
fuse_quant_op=fuse_quant_op,
)
# Return single result or list based on input
@@ -0,0 +1,207 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for MLA prefill backend fused-quant-output support.
Covers two things:
* `MLAPrefillBackend.supports_quant_output`, the capability gate that decides
whether the prefill kernel writes quantized output directly (FA4 native
fused FP8, see flash-attention#135) instead of the post-quant path.
* The numerical equivalence of that fused FP8 write versus the bf16-attention
+ standalone static-FP8-quant path it replaces (GPU-only, SM100/SM110).
"""
from unittest.mock import patch
import pytest
import torch
from vllm.model_executor.layers.quantization.utils.quant_utils import (
kFp8Dynamic128Sym,
kFp8StaticTensorSym,
kNvfp4Dynamic,
)
from vllm.platforms.interface import DeviceCapability
from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
from vllm.v1.attention.backends.mla.prefill.flash_attn import (
FlashAttnPrefillBackend,
)
_FA_MODULE = "vllm.v1.attention.backends.mla.prefill.flash_attn"
class _DummyPrefillBackend(MLAPrefillBackend):
"""Concrete backend that does NOT override supports_quant_output."""
@staticmethod
def get_name() -> str:
return "DUMMY"
def run_prefill_new_tokens(self, *args, **kwargs): # pragma: no cover
raise NotImplementedError
def run_prefill_context_chunk(self, *args, **kwargs): # pragma: no cover
raise NotImplementedError
@pytest.mark.parametrize(
"quant_key", [kFp8StaticTensorSym, kFp8Dynamic128Sym, kNvfp4Dynamic, None]
)
def test_base_backend_never_supports_quant_output(quant_key):
"""The base default opts every backend out unless it overrides."""
backend = object.__new__(_DummyPrefillBackend)
assert backend.supports_quant_output(quant_key) is False
def _make_fa_backend(version: int | None, is_vllm_fa: bool):
"""Build a FlashAttnPrefillBackend without running its heavy __init__."""
backend = object.__new__(FlashAttnPrefillBackend)
backend.vllm_flash_attn_version = version
backend._is_vllm_fa = is_vllm_fa
return backend
@pytest.mark.parametrize(
("version", "is_vllm_fa", "dc_major", "quant_key", "expected"),
[
# FA4 + vLLM-FA + Blackwell SM100/SM110 + static FP8 -> fused.
(4, True, 10, kFp8StaticTensorSym, True),
(4, True, 11, kFp8StaticTensorSym, True),
# Wrong compute capability (SM90 / SM120) -> not supported (#135).
(4, True, 9, kFp8StaticTensorSym, False),
(4, True, 12, kFp8StaticTensorSym, False),
# Not FA4.
(3, True, 10, kFp8StaticTensorSym, False),
(2, True, 10, kFp8StaticTensorSym, False),
(None, True, 10, kFp8StaticTensorSym, False),
# Upstream (ROCm) flash-attn, not vLLM-FA.
(4, False, 10, kFp8StaticTensorSym, False),
# Quant keys not wired through FA4 yet.
(4, True, 10, kFp8Dynamic128Sym, False),
(4, True, 10, kNvfp4Dynamic, False),
],
)
def test_flash_attn_supports_quant_output(
version, is_vllm_fa, dc_major, quant_key, expected
):
backend = _make_fa_backend(version, is_vllm_fa)
with patch(f"{_FA_MODULE}.current_platform") as plat:
plat.get_device_capability.return_value = DeviceCapability(
major=dc_major, minor=0
)
assert backend.supports_quant_output(quant_key) is expected
def test_flash_attn_supports_quant_output_unknown_device():
"""A None device capability (e.g. capability probe failed) is safe."""
backend = _make_fa_backend(version=4, is_vllm_fa=True)
with patch(f"{_FA_MODULE}.current_platform") as plat:
plat.get_device_capability.return_value = None
assert backend.supports_quant_output(kFp8StaticTensorSym) is False
def test_flash_attn_prefill_backend_signature_accepts_fused_kwargs():
"""run_prefill_new_tokens must accept out/output_scale so the direct
(non-**kwargs) call in forward_mha type- and runtime-checks."""
import inspect
params = inspect.signature(
FlashAttnPrefillBackend.run_prefill_new_tokens
).parameters
assert "out" in params
assert "output_scale" in params
# The base contract must expose them too (Liskov / direct call site).
base_params = inspect.signature(MLAPrefillBackend.run_prefill_new_tokens).parameters
assert "out" in base_params
assert "output_scale" in base_params
def test_mla_impl_forward_mha_accepts_output_scale():
"""The abstract MLA impl forward_mha must carry output_scale so every
override (and the unconditional forward_impl call) stays compatible."""
import inspect
from vllm.v1.attention.backend import MLAAttentionImpl
params = inspect.signature(MLAAttentionImpl.forward_mha).parameters
assert "output_scale" in params
assert params["output_scale"].default is None
def _fused_fp8_skip_reason() -> str | None:
"""FA4 fused FP8 output needs a real Blackwell SM100/SM110 GPU."""
if not torch.cuda.is_available():
return "requires CUDA"
major = torch.cuda.get_device_capability()[0]
if major not in (10, 11):
return f"FA4 fused FP8 output requires SM100/SM110, got SM{major}x"
return None
_FUSED_FP8_SKIP = _fused_fp8_skip_reason()
@pytest.mark.skipif(_FUSED_FP8_SKIP is not None, reason=_FUSED_FP8_SKIP or "")
def test_fa4_fused_fp8_output_matches_post_quant(default_vllm_config):
"""FA4's fused FP8 write (output_scale, flash-attention#135) must match the
bf16-attention + standalone static-FP8-quant path it replaces, since
production uses the same output_scale for both."""
from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
from vllm.platforms import current_platform
from vllm.vllm_flash_attn import flash_attn_varlen_func
torch.manual_seed(0)
device = torch.device("cuda")
fp8_dtype = current_platform.fp8_dtype()
# MLA prefill head dims (post kv_b_proj): q/k = qk_nope(128)+qk_rope(64),
# v = v_head_dim(128); DeepSeek-V2-Lite has 16 query heads.
num_heads, qk_head_dim, v_head_dim, seqlen = 16, 192, 128, 512
cu_seqlens = torch.tensor([0, seqlen], dtype=torch.int32, device=device)
q = torch.randn(seqlen, num_heads, qk_head_dim, dtype=torch.bfloat16, device=device)
k = torch.randn(seqlen, num_heads, qk_head_dim, dtype=torch.bfloat16, device=device)
v = torch.randn(seqlen, num_heads, v_head_dim, dtype=torch.bfloat16, device=device)
fa_kwargs = dict(
cu_seqlens_q=cu_seqlens,
cu_seqlens_k=cu_seqlens,
max_seqlen_q=seqlen,
max_seqlen_k=seqlen,
causal=True,
fa_version=4,
)
# Reference: bf16 attention, then standalone static per-tensor FP8 quant.
out_bf16 = flash_attn_varlen_func(q=q, k=k, v=v, **fa_kwargs)
out_2d = out_bf16.reshape(seqlen, num_heads * v_head_dim)
# Scale the amax near e4m3 max so the check uses the representable range.
finfo = torch.finfo(fp8_dtype)
scale = (out_2d.abs().max() / finfo.max).to(torch.float32).reshape(1)
quant_op = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
ref_fp8, _ = quant_op(out_2d, scale)
# Feature: FA4 writes e4m3 into the (tokens, heads*dim) buffer directly.
fused_fp8 = torch.empty(
seqlen, num_heads * v_head_dim, dtype=fp8_dtype, device=device
)
flash_attn_varlen_func(
q=q,
k=k,
v=v,
out=fused_fp8.view(seqlen, num_heads, v_head_dim),
output_scale=scale,
**fa_kwargs,
)
# Non-degenerate (catches a no-op / all-zero write).
assert torch.isfinite(fused_fp8.float()).all()
assert fused_fp8.float().abs().any()
# e4m3 has 3 mantissa bits, so allow ~1 mantissa step of rounding slack.
ref = ref_fp8.float() * scale
got = fused_fp8.float() * scale
torch.testing.assert_close(got, ref, rtol=0.125, atol=float(scale) * 2)
# ...and most elements land in the exact same fp8 bucket.
exact = (fused_fp8.view(torch.uint8) == ref_fp8.view(torch.uint8)).float().mean()
assert exact > 0.9, f"only {exact:.1%} of fused FP8 outputs matched the baseline"
@@ -687,7 +687,23 @@ class MLAAttention(nn.Module, AttentionLayerBase):
num_mqa_tokens = attn_metadata.num_decode_tokens
num_mha_tokens = q.size(0) - num_mqa_tokens
mha_use_quant_output = (
quant_key is not None
and self.prefill_backend.supports_quant_output(quant_key)
and attn_metadata is not None
and attn_metadata.prefill is not None
and attn_metadata.prefill.chunked_context is None
and self.impl.dcp_world_size <= 1
)
if num_mha_tokens > 0:
if mha_use_quant_output:
mha_output = quant_output
mha_output_scale = output_scale
else:
mha_output = output
mha_output_scale = None
self.impl.forward_mha( # type: ignore[attr-defined]
q[num_mqa_tokens:],
k_c_normed[num_mqa_tokens:],
@@ -695,7 +711,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
kv_cache,
attn_metadata,
self._k_scale,
output=output[num_mqa_tokens:],
output=mha_output[num_mqa_tokens:num_actual_toks],
output_scale=mha_output_scale,
)
if num_mqa_tokens > 0:
@@ -794,13 +811,15 @@ class MLAAttention(nn.Module, AttentionLayerBase):
self._v_up_proj(attn_out, out=mqa_output_slice)
if quant_key is not None:
# Quantize the BF16 computation result into the quantized output
actual = output[:num_actual_toks]
quant_idx = num_mqa_tokens if mha_use_quant_output else num_actual_toks
if quant_idx == 0:
return quant_output
actual = output[:quant_idx]
if quant_key == kNvfp4Dynamic:
# NVFP4: two FP4 values packed into one uint8
assert output_block_scale is not None
fp4_data, fp4_scales = ops.scaled_fp4_quant(actual, output_scale)
quant_output[:num_actual_toks].copy_(fp4_data)
quant_output[:quant_idx].copy_(fp4_data)
output_block_scale[: fp4_scales.shape[0]].copy_(fp4_scales)
elif quant_key in (kFp8Dynamic128Sym, kFp8Dynamic64Sym):
# Per-group FP8
@@ -812,8 +831,8 @@ class MLAAttention(nn.Module, AttentionLayerBase):
finfo = torch.finfo(_FP8_DTYPE)
torch.ops._C.per_token_group_fp8_quant(
actual,
quant_output[:num_actual_toks],
output_block_scale[:num_actual_toks],
quant_output[:quant_idx],
output_block_scale[:quant_idx],
quant_group_size,
1e-10, # eps
finfo.min,
@@ -825,7 +844,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
elif quant_key == kFp8StaticTensorSym:
# Static FP8 quantization
fp8_data, _ = self._quant_fp8_op(actual, output_scale)
quant_output[:num_actual_toks].copy_(fp8_data)
quant_output[:quant_idx].copy_(fp8_data)
else:
raise ValueError(f"Unsupported quant_key: {quant_key}")
return quant_output
@@ -2254,6 +2273,7 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
attn_metadata: MLACommonMetadata,
k_scale: torch.Tensor,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
) -> None:
assert attn_metadata.prefill is not None
assert self.dcp_world_size != -1
@@ -2267,6 +2287,9 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
q = q.to(prefill_metadata.q_data_type)
has_context = prefill_metadata.chunked_context is not None
assert output_scale is None or not has_context, (
"Fused FP8 output is only wired for the non-chunked-context path"
)
kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
-1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
@@ -2283,6 +2306,12 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
k=k,
v=v,
return_softmax_lse=has_context,
out=(
output.view(-1, self.num_heads, self.v_head_dim)
if output_scale is not None
else None
),
output_scale=output_scale,
)
if has_context:
@@ -2312,7 +2341,8 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
suffix_lse=suffix_lse,
prefill_tokens_with_context=prefill_metadata.chunked_context.prefill_tokens_with_context,
)
else:
elif output_scale is None:
# With output_scale set, backend already wrote into `output` in place.
assert isinstance(output_prefill, torch.Tensor)
output_prefill = output_prefill.flatten(start_dim=-2)
output.copy_(output_prefill)
+1
View File
@@ -899,6 +899,7 @@ class MLAAttentionImpl(AttentionImplBase[T], Generic[T]):
attn_metadata: T,
k_scale: torch.Tensor,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
) -> None:
"""MHA-style prefill forward pass."""
raise NotImplementedError
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
from vllm.model_executor.layers.attention.mla_attention import (
MLACommonPrefillMetadata,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
from vllm.platforms.interface import DeviceCapability
from vllm.v1.attention.backends.mla.prefill.selector import (
MLAPrefillSelectorConfig,
@@ -44,6 +45,12 @@ class MLAPrefillBackend(ABC):
def is_available(cls) -> bool:
return True
def supports_quant_output(self, quant_key: "QuantKey") -> bool:
"""Whether `run_prefill_new_tokens` can write quantized output
directly (fused) for the given quant key, skipping the post-quant
pass. Overridden by backends that support it."""
return False
@classmethod
def validate_configuration(
cls,
@@ -107,6 +114,8 @@ class MLAPrefillBackend(ABC):
k: torch.Tensor,
v: torch.Tensor,
return_softmax_lse: bool,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
raise NotImplementedError
@@ -8,6 +8,9 @@ from typing import TYPE_CHECKING
import torch
import vllm.envs as envs
from vllm.model_executor.layers.quantization.utils.quant_utils import (
kFp8StaticTensorSym,
)
from vllm.platforms import current_platform
from vllm.v1.attention.backends.fa_utils import (
get_flash_attn_version,
@@ -17,6 +20,7 @@ from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
if TYPE_CHECKING:
from vllm.config import VllmConfig
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
if is_flash_attn_varlen_func_available():
from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
@@ -87,6 +91,16 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
# Track whether we're using vllm's FA or upstream (for ROCm)
self._is_vllm_fa = current_platform.is_cuda() or current_platform.is_xpu()
def supports_quant_output(self, quant_key: "QuantKey") -> bool:
device_capability = current_platform.get_device_capability()
return (
self.vllm_flash_attn_version == 4
and self._is_vllm_fa
and device_capability is not None
and device_capability[0] in (10, 11)
and quant_key == kFp8StaticTensorSym
)
def _flash_attn_varlen_diff_headdims(
self,
q: torch.Tensor,
@@ -94,6 +108,8 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
v: torch.Tensor,
return_softmax_lse: bool = False,
softmax_scale: float | None = None,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
maybe_padded_v = v
@@ -104,10 +120,13 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
if self._is_vllm_fa:
kwargs["return_softmax_lse"] = return_softmax_lse
kwargs["out"] = out
kwargs["output_scale"] = output_scale
else:
# ROCm leverages the upstream flash_attn, which takes a parameter
# called "return_attn_probs" instead of return_softmax_lse
kwargs["return_attn_probs"] = return_softmax_lse
assert out is None and output_scale is None
if envs.VLLM_BATCH_INVARIANT:
kwargs["num_splits"] = 1
@@ -140,6 +159,8 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
k: torch.Tensor,
v: torch.Tensor,
return_softmax_lse: bool,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
return self._flash_attn_varlen_diff_headdims(
q=q,
@@ -152,6 +173,8 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
softmax_scale=self.scale,
causal=True,
return_softmax_lse=return_softmax_lse,
out=out,
output_scale=output_scale,
)
def run_prefill_context_chunk(
@@ -188,6 +188,8 @@ class FlashInferPrefillBackend(MLAPrefillBackend):
k: torch.Tensor,
v: torch.Tensor,
return_softmax_lse: bool,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
assert self._prefill_main is not None
@@ -115,6 +115,8 @@ class TokenspeedMLAPrefillBackend(MLAPrefillBackend):
k: torch.Tensor,
v: torch.Tensor,
return_softmax_lse: bool,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
from tokenspeed_mla import tokenspeed_mla_prefill
@@ -83,6 +83,8 @@ class TrtllmRaggedPrefillBackend(MLAPrefillBackend):
k: torch.Tensor,
v: torch.Tensor,
return_softmax_lse: bool,
out: torch.Tensor | None = None,
output_scale: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
from flashinfer.prefill import trtllm_ragged_attention_deepseek
@@ -841,6 +841,7 @@ class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]):
attn_metadata: MLACommonMetadata,
k_scale: torch.Tensor,
output: torch.Tensor,
output_scale: torch.Tensor | None = None,
) -> None:
"""Dispatch prefill to the FP8 ASM kernel when available.
@@ -866,6 +867,7 @@ class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]):
attn_metadata,
k_scale,
output,
output_scale,
)
assert attn_metadata.prefill is not None
@@ -881,8 +883,13 @@ class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]):
attn_metadata,
k_scale,
output,
output_scale,
)
assert output_scale is None, (
"fused FP8 output not supported by the AITER FP8 MLA prefill path"
)
kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
-1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
)
@@ -200,6 +200,8 @@ def flash_attn_varlen_func(
k_descale=None,
v_descale=None,
num_splits: int = 0,
# FA4 Only
output_scale=None,
# Version selector
fa_version: int = DEFAULT_FA_VERSION,
s_aux=None,
@@ -273,6 +275,11 @@ def flash_attn_varlen_func(
"seqused_k must be provided if block_table is provided"
)
assert output_scale is None or fa_version == 4, (
f"Fused FP8 output (output_scale) is only supported by FA4, "
f"got fa_version={fa_version}"
)
if softmax_scale is None:
softmax_scale = q.shape[-1] ** (-0.5)
# custom op does not support non-tuple input
@@ -403,6 +410,7 @@ def flash_attn_varlen_func(
learnable_sink=s_aux,
mask_mod=mask_mod,
aux_tensors=aux_tensors,
output_scale=output_scale,
)
else:
raise ValueError(f"Unsupported FA version: {fa_version}")