Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a962321
[Attention] Integrate SM90 d512 FP8 MMA with vLLM routing
Oct 9, 2026
0530c91
[Test] Supply parallel config for SM90 attention fixtures
Oct 9, 2026
d20b103
[Test] Narrow FP8 rejection reason for mypy
katec846 Oct 14, 2026
5fc01d3
[Attention] Consolidate SM90 FP8 wrapper dispatch flags
katec846 Oct 14, 2026
7ea8800
[Attention] Guard unvalidated SM90 FP8 noncausal full attention
katec846 Oct 15, 2026
b283eac
[Review] Trim SM90 FP8 selector tests and comments
katec846 Oct 15, 2026
e9344ed
[Review] Remove redundant Diff-KV probe cache check
katec846 Oct 15, 2026
fc937f5
[Review] Remove remaining added attention tests
katec846 Oct 15, 2026
847cee6
[Review] Clarify FP8 MMA comments and noncausal error
katec846 Oct 15, 2026
ac73cba
[Review] Simplify SM90 FP8 guards and error messages
katec846 Oct 15, 2026
6c178bc
[Attention] Remove the unreachable FA4 -> FA3 FP8 fallback on SM90
simon-veitner-redhat Oct 21, 2026
f116077
[Attention] Reject DCP with FA4 SM90 FP8 KV at impl init
simon-veitner-redhat Oct 21, 2026
d0d39b0
[Test] Cover FA4 SM90 FP8 MMA routing in a CPU selector test
simon-veitner-redhat Oct 21, 2026
9fe5c82
[CI] Add Hopper GSM8K eval for Gemma-4 with FP8 KV (FA4 SM90 FP8 MMA)
simon-veitner-redhat Oct 21, 2026
237043e
Update SM90 FP8 MMA FlashAttention review fixes
katec846 Oct 27, 2026
3a01ead
Pin merged FlashAttention SM90 FP8 MMA commit
katec846 Oct 29, 2026
0ffdc07
Merge branch 'main' into fa4-fp8mma-draft-20261007
MatthewBonanni Nov 4, 2026
501e7e3
Skip SM90 FP8 MMA selection test without CUDA FA extensions
MatthewBonanni Nov 4, 2026
f2d8259
Merge remote-tracking branch 'upstream-main-now' into fix-60372-ci
MatthewBonanni Nov 4, 2026
78e295c
Merge branch 'main' into fa4-fp8mma-draft-20261007
DarkLight1337 Nov 5, 2026
cf3b814
[Test] Remove obsolete input dtype from Zen MoE conversion test
katec846 Nov 5, 2026
feba607
Merge branch 'main' into fa4-fp8mma-draft-20261007
DarkLight1337 Nov 5, 2026
419626d
Move Gemma-4 FP8 KV eval into the H100 large-model nightly
MatthewBonanni Nov 5, 2026
7894b61
Set Gemma-4 FP8 KV GSM8K reference to the measured 0.937
MatthewBonanni Nov 5, 2026
f2e9e8f
Merge branch 'main' into fa4-fp8mma-draft-20261007
MatthewBonanni Nov 5, 2026
7928f5f
Merge branch 'main' into fa4-fp8mma-draft-20261007
MatthewBonanni Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .buildkite/test_areas/lm_eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,22 @@ steps:
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-watermark-dspark-tp2.txt

# SM90-only path (FA4 FP8 MMA for head_size=512), so no AMD mirror.
- label: ":nvidia: (H100) LM Eval Hopper FP8 KV"
key: lm-eval-hopper-fp8-kv-2xh100
device: h100
num_devices: 2
timeout_in_minutes: 30
source_file_dependencies:
- cmake/external_projects/vllm_flash_attn.cmake
- vllm/v1/attention/backends/fa_utils.py
- vllm/v1/attention/backends/flash_attn.py
- vllm/vllm_flash_attn/
- tests/evals/gsm8k/configs/gemma-4-31B-it-FP8-dynamic-fp8kv-TP2.yaml
- tests/evals/gsm8k/configs/models-hopper-fp8-kv.txt
commands:
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-hopper-fp8-kv.txt

- label: ":nvidia: (B200) [Nightly] LM Eval Spec Decode"
key: lm-eval-spec-decode-4xb200
timeout_in_minutes: 120
Expand Down
2 changes: 1 addition & 1 deletion cmake/external_projects/vllm_flash_attn.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ else()
FetchContent_Declare(
vllm-flash-attn
GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
GIT_TAG b273a02621d3c94bc747df67204f591b67f38fae
GIT_TAG 9ed6b1291b2a4ef8cbd2fd4fa7ebad6f280d9ee9
GIT_PROGRESS TRUE
# Don't share the vllm-flash-attn build between build types
BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Gemma-4 full-attention layers (head_size=512) with an FP8 KV cache take the
# FA4 SM90 FP8 MMA path on Hopper; the sliding-window layers use FA3 FP8.
# The hf-override makes the model text-only. Without it Gemma-4 is an
# mm-prefix LM, which FLASH_ATTN does not support with FP8 KV on SM90.
model_name: "RedHatAI/gemma-4-31B-it-FP8-dynamic"
# Measured 0.970 on 2x H200 with this harness and FA PR 199.
accuracy_threshold: 0.96
num_questions: 1319
num_fewshot: 5
use_chat_completions: true
max_tokens: 512
server_args: >-
--tensor-parallel-size 2
--max-model-len 4096
--kv-cache-dtype fp8
--attention-backend FLASH_ATTN
--hf-overrides '{"text_config":{"use_bidirectional_attention":null}}'
1 change: 1 addition & 0 deletions tests/evals/gsm8k/configs/models-hopper-fp8-kv.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gemma-4-31B-it-FP8-dynamic-fp8kv-TP2.yaml
179 changes: 179 additions & 0 deletions tests/v1/attention/test_flash_attn_sm90_fp8_mma_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Routing of the FA4 SM90 FP8 MMA path (head_size=512 with an FP8 KV cache).

The device capability is mocked, so no GPU is needed.
"""

from contextlib import contextmanager
from unittest.mock import MagicMock, patch

import pytest
import torch

from vllm.platforms.interface import DeviceCapability

fa_interface = pytest.importorskip("vllm.vllm_flash_attn.flash_attn_interface")

from vllm.v1.attention.backends.fa_utils import get_flash_attn_version # noqa: E402
from vllm.v1.attention.backends.flash_attn import ( # noqa: E402
FlashAttentionBackend,
FlashAttentionImpl,
)

SM80 = DeviceCapability(8, 0)
SM90 = DeviceCapability(9, 0)
SM100 = DeviceCapability(10, 0)


@contextmanager
def _device(capability, *, head_size, kv_cache_dtype, fa_version=None, dcp=1):
platform = MagicMock()
platform.is_xpu.return_value = False
platform.is_rocm.return_value = False
platform.is_cuda.return_value = True
platform.get_device_capability.return_value = capability
platform.is_device_capability_family.side_effect = (
lambda family, device_id=0: capability.to_int() // 10 == family // 10
)
config = MagicMock()
config.attention_config.flash_attn_version = fa_version
config.model_config.is_diffusion = False
config.model_config.get_head_size.return_value = head_size
config.cache_config.cache_dtype = kv_cache_dtype
config.parallel_config.decode_context_parallel_size = dcp
config.parallel_config.dcp_comm_backend = "ag_rs"
backends = "vllm.v1.attention.backends"
with (
patch(f"{backends}.fa_utils.current_platform", platform),
patch(f"{backends}.flash_attn.current_platform", platform),
patch.object(fa_interface, "is_fa_version_supported", return_value=True),
patch("vllm.config.get_current_vllm_config_or_none", return_value=config),
patch(
f"{backends}.flash_attn.get_current_vllm_config_or_none",
return_value=config,
),
patch(
"vllm.distributed.parallel_state.get_dcp_world_size_and_rank",
return_value=(dcp, 0),
),
):
yield


def _supports_combination(capability, head_size, kv_cache_dtype, block_size=64):
return FlashAttentionBackend.supports_combination(
head_size=head_size,
dtype=torch.bfloat16,
kv_cache_dtype=kv_cache_dtype,
block_size=block_size,
use_mla=False,
has_sink=False,
use_sparse=False,
use_mm_prefix=False,
device_capability=capability,
)


def _impl(head_size, kv_cache_dtype):
return FlashAttentionImpl(
num_heads=8,
head_size=head_size,
scale=head_size**-0.5,
num_kv_heads=1,
alibi_slopes=None,
sliding_window=None,
kv_cache_dtype=kv_cache_dtype,
)


@pytest.mark.parametrize(
"head_size,kv_cache_dtype,fa_version,sm90_fp8_mma",
[
(512, "fp8", 4, True),
(512, "fp8_e4m3", 4, True),
# Gemma-4 sliding-window layers stay on FA3 FP8 with an FP8 query.
(256, "fp8", 3, False),
(512, "auto", 4, False),
],
)
def test_sm90_routing(head_size, kv_cache_dtype, fa_version, sm90_fp8_mma):
with _device(SM90, head_size=head_size, kv_cache_dtype=kv_cache_dtype):
assert get_flash_attn_version(head_size=head_size) == fa_version
assert _supports_combination(SM90, head_size, kv_cache_dtype) is None
impl = _impl(head_size, kv_cache_dtype)
assert impl.vllm_flash_attn_version == fa_version
# The FP8 MMA kernel quantizes Q in-kernel, so vLLM must not quantize it.
assert impl.supports_quant_query_input is not sm90_fp8_mma


def test_sm90_fp8_mma_block_size():
with _device(SM90, head_size=512, kv_cache_dtype="fp8"):
assert FlashAttentionBackend.get_supported_kernel_block_sizes() == [64]
assert "block size" in _supports_combination(SM90, 512, "fp8", 16)
with _device(SM90, head_size=512, kv_cache_dtype="auto"):
assert _supports_combination(SM90, 512, "auto", 16) is None


@pytest.mark.parametrize(
"capability,head_size,fa_version",
[
(SM90, 256, 4), # forced FA4 with an FP8 KV cache needs head_size=512
(SM100, 512, None), # FA4 on Blackwell does not support head_size=512
(SM80, 512, None), # FA2 has no FP8 KV cache
],
)
def test_fp8_kv_rejected(capability, head_size, fa_version):
with _device(
capability, head_size=head_size, kv_cache_dtype="fp8", fa_version=fa_version
):
assert _supports_combination(capability, head_size, "fp8") is not None
with pytest.raises(NotImplementedError, match="fp8 kv-cache"):
_impl(head_size, "fp8")


@pytest.mark.parametrize(
"head_size,kv_cache_dtype,rejected",
[(512, "fp8", True), (512, "auto", False), (256, "fp8", False)],
)
def test_sm90_fp8_mma_rejects_dcp(head_size, kv_cache_dtype, rejected):
with _device(SM90, head_size=head_size, kv_cache_dtype=kv_cache_dtype, dcp=2):
if rejected:
with pytest.raises(NotImplementedError, match="context parallelism"):
_impl(head_size, kv_cache_dtype)
else:
assert _impl(head_size, kv_cache_dtype).dcp_world_size == 2


@pytest.mark.parametrize(
"head_size,q_dtype,causal,match",
[
# FA4 + FP8 KV below d512 raises; no silent switch to FA3.
(256, torch.float8_e4m3fn, True, "q=torch.float8_e4m3fn/256"),
(128, torch.float8_e4m3fn, False, "head_size=512"),
(256, torch.bfloat16, True, "q=torch.bfloat16/256"),
(512, torch.bfloat16, False, "noncausal"),
],
)
def test_wrapper_rejects_unsupported_sm90_fp8(head_size, q_dtype, causal, match):
q = torch.zeros(4, 8, head_size, dtype=q_dtype)
k = torch.zeros(1, 64, 1, head_size, dtype=torch.float8_e4m3fn)
fa3 = MagicMock()
with (
patch("torch.cuda.get_device_capability", return_value=(9, 0)),
patch.object(torch.ops, "_vllm_fa3_C", fa3, create=True),
pytest.raises(NotImplementedError, match=match),
):
fa_interface.flash_attn_varlen_func(
q,
k,
k,
max_seqlen_q=4,
cu_seqlens_q=torch.tensor([0, 4], dtype=torch.int32),
max_seqlen_k=4,
seqused_k=torch.tensor([4], dtype=torch.int32),
block_table=torch.zeros(1, 1, dtype=torch.int32),
causal=causal,
fa_version=4,
)
fa3.fwd.assert_not_called()
2 changes: 1 addition & 1 deletion vllm/v1/attention/backends/fa_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def flash_attn_supports_kv_cache_dtype(
)
is_sm90 = current_platform.is_device_capability_family(90)
sm90_fp8_kv_supported = fa_version == 3 or (
fa_version == 4 and head_size in (None, 512)
fa_version == 4 and head_size in (None, 512) and head_size_v in (None, 512)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this restriction needs to be stricken? or it's just missing from previous PR.
Will removing this change breaks Gemma4 + FP8 KV + FP8 MMA for hd 512?

@katec846 katec846 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check was missing from the previous vLLM integration. PR164 already requires equal K/V head dimensions, but the vLLM support check only checked the Q/K dimension.

Removing this addition would not break Gemma-4 with FP8 KV + FP8 MMA, since its full-attention Q/K/V dimensions are all 512. It would, however, allow unsupported Diff-KV shapes through backend selection.

)
return (sm90_fp8_kv_supported and is_sm90) or (
fa_version == 4 and current_platform.is_device_capability_family(100)
Expand Down
31 changes: 22 additions & 9 deletions vllm/v1/attention/backends/flash_attn.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,16 @@ def supports_combination(
use_mm_prefix: bool,
device_capability: DeviceCapability,
) -> str | None:
if (
device_capability == DeviceCapability(9, 0)
and kv_cache_dtype is not None
and is_quantized_kv_cache(kv_cache_dtype)
and head_size == 512
):
if block_size is not None and block_size % 64 != 0:
return "SM90 d512 FP8 KV requires a block size divisible by 64"
if use_sparse:
return "SM90 d512 FP8 KV does not support block sparsity"
if has_sink and device_capability < DeviceCapability(9, 0):
return "sink not supported on compute capability < 9.0"
if (
Expand All @@ -486,16 +496,13 @@ def supports_combination(
and not flash_attn_supports_kv_cache_dtype(
kv_cache_dtype,
head_size=head_size,
head_size_v=head_size,
head_size_v=cls.head_size_v,
has_sinks=has_sink,
kv_cache_block_size=block_size,
supports_fa4_hd256=True,
)
):
return (
"FP8 KV cache requires FA3 on SM90, FA4 with head_size=512 "
"on SM90, or FA4 on SM100"
)
return "FP8 KV requires FA3 (SM90), equal-d512 FA4 (SM90), or FA4 (SM100)."
if (
use_mm_prefix
and get_flash_attn_version(
Expand Down Expand Up @@ -705,14 +712,14 @@ def __init__(
self.block_size = kv_cache_spec.block_size

self.max_num_splits = 0 # No upper bound on the number of splits.
self.aot_schedule = get_flash_attn_version() == 3
head_size_v = getattr(kv_cache_spec, "head_size_v", None)
fa_version = get_flash_attn_version(
head_size=self.headdim,
head_size_v=head_size_v,
kv_cache_block_size=self.block_size,
supports_fa4_hd256=True,
)
self.aot_schedule = fa_version == 3
self.fa4_hd256 = fa_version == 4 and uses_fa4_hd256_kernel(
self.headdim, head_size_v
)
Expand Down Expand Up @@ -1152,9 +1159,8 @@ def __init__(
"heads in the layer"
)

# FA4's SM90 FP8-KV path consumes native FP16/BF16 Q and dequantizes
# FP8 K/V in-kernel. Other FA4 paths (notably SM100) still require Q,
# K, and V to have the same FP8 dtype.
# FA4's SM90 d512 FP8-KV path consumes native FP16/BF16 Q. Other FA4
# FP8 paths (notably SM100) require Q, K, and V to share an FP8 dtype.
uses_sm90_fa4_fp8_kv_dequant = (
self.vllm_flash_attn_version == 4
and current_platform.is_device_capability_family(90)
Expand All @@ -1163,6 +1169,13 @@ def __init__(
self.supports_quant_query_input = flash_attn_supports_quant_query_input() and (
not uses_sm90_fa4_fp8_kv_dequant
)
if uses_sm90_fa4_fp8_kv_dequant and self.dcp_world_size > 1:
# DCP context attention is non-causal, which the SM90 FP8 MMA
# kernel rejects at dispatch time.
raise NotImplementedError(
"FA4 SM90 FP8 KV cache does not support decode context "
"parallelism; use an unquantized KV cache."
)

dcp_a2a = (
vllm_config is not None
Expand Down
14 changes: 14 additions & 0 deletions vllm/v1/attention/backends/flash_attn_diffkv.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from vllm.v1.attention.backend import AttentionType
from vllm.v1.attention.backends.fa_utils import (
flash_attn_supports_kv_cache_dtype,
get_flash_attn_version,
is_flash_attn_varlen_func_available,
)
Expand Down Expand Up @@ -86,6 +87,19 @@ def __init__(self, *args, **kwargs) -> None:
head_size_v=FlashAttentionDiffKVBackend.head_size_v,
has_sinks=self.sinks is not None,
)
# An explicitly requested backend can bypass automatic selection.
if is_quantized_kv_cache(
self.kv_cache_dtype
) and not flash_attn_supports_kv_cache_dtype(
self.kv_cache_dtype,
requires_alibi=self.alibi_slopes is not None,
head_size=self.head_size,
head_size_v=FlashAttentionDiffKVBackend.head_size_v,
has_sinks=self.sinks is not None,
):
raise NotImplementedError(
"Unsupported FP8 Diff-KV configuration; use an unquantized KV cache."
)
self.fa4_hd256 = False

def do_kv_cache_update(
Expand Down
Loading
Loading