[ROCm] Add AITER FP8 ViT encoder attention (#49937)
Signed-off-by: Liuyinfeng01 <yinfeliu@amd.com>
yinfengLiu committed
Jul 30, 2026 at 14:22 UTC
f1e8fd27aea5563172a9b2559c42188419a100b2
8 files changed
+563
-45
benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py
new
+124
@@ -0,0 +1,124 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+"""Benchmark complete AITER BF16 and FP8 ViT attention calls on ROCm.
5
+
6
+The FP8 timing includes Q/K/V quantization and attention. Dynamic scales are
7
+calibrated once per input before timing so the measured path matches serving
8
+with a static scale file.
9
+
10
+Example:
11
+ python benchmarks/kernels/benchmark_vit_aiter_fp8_attn.py \
12
+ --seq-lens 2304 4096 8192 16384 --head-dim 72
13
+"""
14
+
15
+from types import SimpleNamespace
16
+
17
+import torch
18
+
19
+from vllm.config import VllmConfig, set_current_vllm_config
20
+from vllm.config.multimodal import MultiModalConfig
21
+from vllm.model_executor.layers.attention.mm_encoder_attention import (
22
+ MMEncoderAttention,
23
+)
24
+from vllm.platforms import current_platform
25
+from vllm.triton_utils import triton
26
+from vllm.utils.argparse_utils import FlexibleArgumentParser
27
+from vllm.v1.attention.backends.registry import AttentionBackendEnum
28
+
29
+
30
+def make_attention(num_heads: int, head_dim: int, fp8: bool) -> MMEncoderAttention:
31
+ mm_config = MultiModalConfig(
32
+ mm_encoder_attn_backend=AttentionBackendEnum.ROCM_AITER_FA,
33
+ mm_encoder_attn_dtype="fp8" if fp8 else None,
34
+ )
35
+ vllm_config = VllmConfig()
36
+ vllm_config.model_config = SimpleNamespace(multimodal_config=mm_config)
37
+ with set_current_vllm_config(vllm_config):
38
+ return MMEncoderAttention(num_heads, head_dim).to("cuda")
39
+
40
+
41
+def bench(
42
+ seq_lens: list[int],
43
+ num_heads: int,
44
+ head_dim: int,
45
+ warmup_ms: int,
46
+ repeat_ms: int,
47
+) -> None:
48
+ if not current_platform.is_rocm():
49
+ raise RuntimeError("This benchmark requires ROCm and AITER.")
50
+
51
+ old_dtype = torch.get_default_dtype()
52
+ torch.set_default_dtype(torch.bfloat16)
53
+ try:
54
+ fp8_attention = make_attention(num_heads, head_dim, fp8=True)
55
+ bf16_attention = make_attention(num_heads, head_dim, fp8=False)
56
+ finally:
57
+ torch.set_default_dtype(old_dtype)
58
+
59
+ print(
60
+ f"{'seq_len':>8} {'BF16 ms':>12} {'FP8 ms':>12} "
61
+ f"{'speedup':>10} {'FP8/BF16':>12}"
62
+ )
63
+ print("-" * 60)
64
+ for seq_len in seq_lens:
65
+ torch.manual_seed(0)
66
+ qkv = torch.randn(
67
+ 1,
68
+ seq_len,
69
+ 3,
70
+ num_heads,
71
+ head_dim,
72
+ device="cuda",
73
+ dtype=torch.bfloat16,
74
+ )
75
+ q, k, v = qkv.unbind(dim=2)
76
+ cu_seqlens = torch.tensor([0, seq_len], device="cuda", dtype=torch.int32)
77
+ max_seqlen = torch.tensor(seq_len, device="cuda", dtype=torch.int32)
78
+
79
+ # Calibrate per-tensor scales once, then benchmark the static-scale path.
80
+ fp8_attention._fp8_dynamic_scale = True
81
+ fp8_attention._forward_aiter_fp8(q, k, v, cu_seqlens, max_seqlen)
82
+ fp8_attention._fp8_dynamic_scale = False
83
+
84
+ bf16_ms = triton.testing.do_bench(
85
+ lambda q=q, k=k, v=v, cu=cu_seqlens, ms=max_seqlen: (
86
+ bf16_attention._forward_fa(q, k, v, cu, ms)
87
+ ),
88
+ warmup=warmup_ms,
89
+ rep=repeat_ms,
90
+ )
91
+ fp8_ms = triton.testing.do_bench(
92
+ lambda q=q, k=k, v=v, cu=cu_seqlens, ms=max_seqlen: (
93
+ fp8_attention._forward_aiter_fp8(q, k, v, cu, ms)
94
+ ),
95
+ warmup=warmup_ms,
96
+ rep=repeat_ms,
97
+ )
98
+ speedup = bf16_ms / fp8_ms
99
+ ratio = fp8_ms / bf16_ms
100
+ print(
101
+ f"{seq_len:>8} {bf16_ms:>12.3f} {fp8_ms:>12.3f} "
102
+ f"{speedup:>9.2f}x {ratio:>12.3f}"
103
+ )
104
+
105
+
106
+if __name__ == "__main__":
107
+ parser = FlexibleArgumentParser(
108
+ description="Benchmark AITER BF16 vs FP8 ViT attention."
109
+ )
110
+ parser.add_argument(
111
+ "--seq-lens", type=int, nargs="+", default=[2304, 4096, 8192, 16384]
112
+ )
113
+ parser.add_argument("--num-heads", type=int, default=16)
114
+ parser.add_argument("--head-dim", type=int, default=72)
115
+ parser.add_argument("--warmup-ms", type=int, default=100)
116
+ parser.add_argument("--repeat-ms", type=int, default=500)
117
+ args = parser.parse_args()
118
+ bench(
119
+ args.seq_lens,
120
+ args.num_heads,
121
+ args.head_dim,
122
+ args.warmup_ms,
123
+ args.repeat_ms,
124
+ )
docs/features/quantization/fp8_vit_attn.md
+27
-8
@@ -3,9 +3,9 @@
3
For visual understanding workloads with large images (e.g. QHD, 4K) and relatively
4
short text prompts/generation, the ViT encoder attention can become a significant
5
bottleneck, especially when the text model is quantized (e.g. NVFP4). vLLM
6
-supports optional FP8 quantization for the ViT encoder attention via the
7
-FlashInfer cuDNN backend. Q/K/V are quantized on-the-fly to FP8 before the
8
-cuDNN attention call.
6
+supports optional FP8 quantization for ViT encoder attention via FlashInfer
7
+cuDNN on NVIDIA GPUs and AITER on AMD GPUs. Q/K/V are quantized on-the-fly to
8
+FP8 before the attention call.
9
10
!!! note
11
- Currently supports Qwen3-VL family models only (`qwen3_vl`, `qwen3_vl_moe`,
@@ -15,20 +15,29 @@ cuDNN attention call.
15
requests. Smaller images may see no speedup due to quantization overhead
16
(3 quantization kernel launches + un-padding).
17
- FP8 tensor-core speedup is more pronounced on GB300 than GB200.
18
+ - On ROCm, packed variable-length image and video batches are supported by
19
+ AITER's native varlen FP8 attention.
20
21
## Requirements
22
21
-- FlashInfer cuDNN backend with cuDNN >= 9.17.1.
23
+- NVIDIA: FlashInfer cuDNN backend with cuDNN >= 9.17.1.
24
+- AMD: AITER with `flash_attn_varlen_fp8_pertensor_func` support on gfx942
25
+ (MI300 series) or gfx950 (MI350 series).
26
27
## Usage
28
25
-Enable FP8 ViT attention by passing `--mm-encoder-attn-dtype fp8` together
26
-with `--mm-encoder-attn-backend FLASHINFER`:
29
+Enable FP8 ViT attention by passing `--mm-encoder-attn-dtype fp8` and selecting
30
+the backend for the current platform:
31
32
```bash
33
vllm serve $MODEL \
34
--mm-encoder-attn-backend FLASHINFER \
35
--mm-encoder-attn-dtype fp8
36
+
37
+# AMD ROCm
38
+vllm serve $MODEL \
39
+ --mm-encoder-attn-backend ROCM_AITER_FA \
40
+ --mm-encoder-attn-dtype fp8
41
```
42
43
By default (no scale file), **dynamic scaling** is used: a 16-entry circular
@@ -45,7 +54,7 @@ reuse them to avoid the dynamic overhead:
54
# Step 1: calibrate and save scales (runs dynamic scaling for 16 passes,
55
# then dumps the learned scales to JSON).
56
vllm bench mm-processor \
48
- --model $MODEL --mm-encoder-attn-backend FLASHINFER \
57
+ --model $MODEL --mm-encoder-attn-backend $MM_ATTN_BACKEND \
58
--mm-encoder-attn-dtype fp8 \
59
--mm-encoder-fp8-scale-save-path /path/to/scales.json \
60
--dataset-name hf --dataset-path lmarena-ai/VisionArena-Chat \
@@ -53,7 +62,7 @@ vllm bench mm-processor \
62
63
# Step 2: serve with static scales (no dynamic overhead).
64
vllm serve $MODEL \
56
- --mm-encoder-attn-backend FLASHINFER \
65
+ --mm-encoder-attn-backend $MM_ATTN_BACKEND \
66
--mm-encoder-attn-dtype fp8 \
67
--mm-encoder-fp8-scale-path /path/to/scales.json
68
```
@@ -94,6 +103,16 @@ Keys `q_scale` / `k_scale` / `v_scale` are accepted as aliases.
103
104
Crossover is around FullHD with 3 images/request. At QHD and above, FP8 wins.
105
106
+**Complete AITER attention call on MI300X** (BF16 input, 16 heads,
107
+head_dim=72; FP8 includes Q/K/V quantization):
108
+
109
+| Sequence length | AITER BF16 | AITER FP8 | Speedup |
110
+| --------------- | ---------- | --------- | ------- |
111
+| 2304 | 0.467 ms | 0.337 ms | **1.38x** |
112
+| 4096 | 0.812 ms | 0.764 ms | **1.06x** |
113
+| 8192 | 2.555 ms | 2.364 ms | **1.08x** |
114
+| 16384 | 9.769 ms | 8.655 ms | **1.13x** |
115
+
116
## Accuracy
117
118
ChartQA, Qwen3-VL-8B-Instruct, 500 samples. FP8 static uses scales calibrated
tests/kernels/attention/test_mha_attn.py
+135
-1
@@ -20,7 +20,7 @@ from vllm.platforms import current_platform
20
from vllm.platforms.cpu import CpuPlatform
21
from vllm.platforms.cuda import CudaPlatform
22
from vllm.platforms.interface import DeviceCapability
23
-from vllm.platforms.rocm import RocmPlatform
23
+from vllm.platforms.rocm import RocmPlatform, on_mi3xx
24
from vllm.utils.torch_utils import set_default_torch_dtype, set_random_seed
25
from vllm.v1.attention.backends.registry import AttentionBackendEnum
26
from vllm.v1.attention.selector import _cached_get_attn_backend
@@ -346,3 +346,137 @@ def test_mha_attn_varlen_forward_flashinfer(
346
torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
347
finally:
348
vllm_config.model_config = old_model_config
349
+
350
+
351
+@pytest.mark.skipif(
352
+ not current_platform.is_rocm() or not on_mi3xx(),
353
+ reason="AITER FP8 attention requires MI300/MI350",
354
+)
355
+@pytest.mark.parametrize("var_seq_len", [[128, 193], [256, 384, 511]])
356
+@pytest.mark.parametrize("head_size", [64, 72, 80])
357
+@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half])
358
+def test_mha_attn_varlen_forward_aiter_fp8(
359
+ default_vllm_config,
360
+ var_seq_len: list[int],
361
+ head_size: int,
362
+ dtype: torch.dtype,
363
+):
364
+ """Compare packed AITER FP8 ViT attention with a BF16/FP16 reference."""
365
+ aiter = pytest.importorskip("aiter")
366
+ if not hasattr(aiter, "flash_attn_varlen_fp8_pertensor_func"):
367
+ pytest.skip("Installed AITER does not provide varlen FP8 attention")
368
+
369
+ num_heads = 4
370
+ set_random_seed(0)
371
+ torch.set_default_device("cuda")
372
+ torch.set_default_dtype(dtype)
373
+
374
+ vllm_config = get_current_vllm_config()
375
+ old_model_config = getattr(vllm_config, "model_config", None)
376
+ minimal_model_config = type(
377
+ "MinimalModelConfig",
378
+ (),
379
+ {
380
+ "multimodal_config": MultiModalConfig(
381
+ mm_encoder_attn_backend=AttentionBackendEnum.ROCM_AITER_FA,
382
+ mm_encoder_attn_dtype="fp8",
383
+ ),
384
+ },
385
+ )()
386
+ vllm_config.model_config = minimal_model_config
387
+ try:
388
+ total_len = sum(var_seq_len)
389
+ # Keep Q/K/V as non-contiguous views, matching interleaved ViT QKV.
390
+ qkv = torch.randn(1, total_len, 3, num_heads, head_size)
391
+ q, k, v = qkv.unbind(dim=2)
392
+ cu_seqlens = torch.tensor(
393
+ [0] + list(itertools.accumulate(var_seq_len)), dtype=torch.int32
394
+ )
395
+ max_seqlen = torch.tensor(max(var_seq_len), dtype=torch.int32)
396
+ scale = 1.0 / head_size**0.5
397
+
398
+ attn = MMEncoderAttention(num_heads, head_size, scale=scale)
399
+ assert attn.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
400
+ assert attn.fp8_enabled
401
+ output = attn(
402
+ q,
403
+ k,
404
+ v,
405
+ cu_seqlens=cu_seqlens,
406
+ max_seqlen=max_seqlen,
407
+ )
408
+
409
+ ref_output = torch.cat(
410
+ [
411
+ ref_attention(q_i, k_i, v_i, scale=scale)
412
+ for q_i, k_i, v_i in zip(
413
+ torch.split(q, var_seq_len, dim=1),
414
+ torch.split(k, var_seq_len, dim=1),
415
+ torch.split(v, var_seq_len, dim=1),
416
+ )
417
+ ],
418
+ dim=1,
419
+ )
420
+ diff = (output.float() - ref_output.float()).abs()
421
+ cosine = torch.nn.functional.cosine_similarity(
422
+ output.float().flatten(),
423
+ ref_output.float().flatten(),
424
+ dim=0,
425
+ )
426
+
427
+ assert output.shape == ref_output.shape
428
+ assert output.dtype == dtype
429
+ assert cosine.item() >= 0.99
430
+ assert diff.mean().item() <= 0.03
431
+ assert diff.max().item() <= 0.30
432
+ finally:
433
+ vllm_config.model_config = old_model_config
434
+
435
+
436
+@pytest.mark.skipif(
437
+ not current_platform.is_rocm(), reason="AITER FP8 attention requires ROCm"
438
+)
439
+def test_mha_attn_aiter_fp8_rejects_unsupported_arch(default_vllm_config):
440
+ vllm_config = get_current_vllm_config()
441
+ old_model_config = getattr(vllm_config, "model_config", None)
442
+ vllm_config.model_config = type(
443
+ "MinimalModelConfig",
444
+ (),
445
+ {
446
+ "multimodal_config": MultiModalConfig(
447
+ mm_encoder_attn_backend=AttentionBackendEnum.ROCM_AITER_FA,
448
+ mm_encoder_attn_dtype="fp8",
449
+ ),
450
+ },
451
+ )()
452
+ try:
453
+ with (
454
+ patch("vllm.platforms.rocm.on_mi3xx", return_value=False),
455
+ pytest.raises(ValueError, match="gfx942 or gfx950"),
456
+ ):
457
+ MMEncoderAttention(4, 64)
458
+ finally:
459
+ vllm_config.model_config = old_model_config
460
+
461
+
462
+@pytest.mark.skipif(
463
+ not current_platform.is_rocm(), reason="AITER FP8 attention requires ROCm"
464
+)
465
+def test_mha_attn_fp8_rejects_wrong_backend(default_vllm_config):
466
+ vllm_config = get_current_vllm_config()
467
+ old_model_config = getattr(vllm_config, "model_config", None)
468
+ vllm_config.model_config = type(
469
+ "MinimalModelConfig",
470
+ (),
471
+ {
472
+ "multimodal_config": MultiModalConfig(
473
+ mm_encoder_attn_backend=AttentionBackendEnum.FLASH_ATTN,
474
+ mm_encoder_attn_dtype="fp8",
475
+ ),
476
+ },
477
+ )()
478
+ try:
479
+ with pytest.raises(ValueError, match="requires either"):
480
+ MMEncoderAttention(4, 64)
481
+ finally:
482
+ vllm_config.model_config = old_model_config
tests/kernels/core/test_vit_fp8_scaling.py
+44
-3
@@ -14,6 +14,7 @@ from vllm.model_executor.layers.attention.mm_encoder_attention import (
14
_FP8_AMAX_HISTORY_LEN,
15
_FP8_MAX,
16
)
17
+from vllm.platforms import current_platform
18
from vllm.utils.flashinfer import (
19
is_flashinfer_cudnn_fp8_prefill_attn_supported,
20
)
@@ -29,8 +30,17 @@ NUM_HEADS = 16
30
HEAD_DIM = 72
31
32
33
+def _is_mi3xx() -> bool:
34
+ if not current_platform.is_rocm():
35
+ return False
36
+
37
+ from vllm.platforms.rocm import on_mi3xx
38
+
39
+ return on_mi3xx()
40
+
41
+
42
@contextlib.contextmanager
33
-def _build_attention(mm_config):
43
+def _build_attention(mm_config, attn_backend=None):
44
"""Yield an MMEncoderAttention with the given multimodal config.
45
46
The VllmConfig context stays active while the test runs so that
@@ -44,7 +54,13 @@ def _build_attention(mm_config):
54
)
55
from vllm.v1.attention.backends.registry import AttentionBackendEnum
56
47
- if not is_flashinfer_cudnn_fp8_prefill_attn_supported():
57
+ if attn_backend is None:
58
+ attn_backend = AttentionBackendEnum.FLASHINFER
59
+
60
+ if (
61
+ attn_backend == AttentionBackendEnum.FLASHINFER
62
+ and not is_flashinfer_cudnn_fp8_prefill_attn_supported()
63
+ ):
64
yield None
65
return
66
@@ -56,7 +72,7 @@ def _build_attention(mm_config):
72
patch(
73
"vllm.model_executor.layers.attention.mm_encoder_attention"
74
".get_vit_attn_backend",
59
- return_value=AttentionBackendEnum.FLASHINFER,
75
+ return_value=attn_backend,
76
),
77
):
78
attn = MMEncoderAttention(
@@ -169,6 +185,31 @@ def test_static_scales_loaded(_make_static_attention) -> None:
185
assert not hasattr(attn, "_fp8_q_amax")
186
187
188
+@pytest.mark.skipif(
189
+ not _is_mi3xx(),
190
+ reason="AITER FP8 attention requires MI300/MI350",
191
+)
192
+def test_aiter_static_scales_loaded(tmp_path) -> None:
193
+ """Verify AITER reuses the existing static FP8 scale loading path."""
194
+ from vllm.config.multimodal import MultiModalConfig
195
+ from vllm.v1.attention.backends.registry import AttentionBackendEnum
196
+
197
+ scale_file = tmp_path / "aiter_scales.json"
198
+ scale_file.write_text(json.dumps({LAYER_0: {"q": 224.0, "k": 198.0, "v": 210.0}}))
199
+ mm_config = MultiModalConfig(
200
+ mm_encoder_attn_dtype="fp8",
201
+ mm_encoder_fp8_scale_path=str(scale_file),
202
+ )
203
+
204
+ with _build_attention(mm_config, AttentionBackendEnum.ROCM_AITER_FA) as attn:
205
+ assert attn is not None
206
+ assert attn.fp8_enabled
207
+ assert not attn._fp8_dynamic_scale
208
+ assert attn._fp8_q_scale.item() == 224.0
209
+ assert attn._fp8_k_scale.item() == 198.0
210
+ assert attn._fp8_v_scale.item() == 210.0
211
+
212
+
213
def test_static_scales_missing_layer(tmp_path) -> None:
214
"""Verify error when requested layer is not in the scale file."""
215
from vllm.config import VllmConfig, set_current_vllm_config
vllm/_aiter_ops.py
+95
@@ -1404,6 +1404,65 @@ def _triton_rotary_embedding_fake(
1404
return
1405
1406
1407
+def _rocm_aiter_fp8_attn_impl(
1408
+ q: torch.Tensor,
1409
+ k: torch.Tensor,
1410
+ v: torch.Tensor,
1411
+ q_descale: torch.Tensor,
1412
+ k_descale: torch.Tensor,
1413
+ v_descale: torch.Tensor,
1414
+ batch_size: int,
1415
+ output_dtype: torch.dtype,
1416
+ scale: float | None = None,
1417
+ cu_seqlens: torch.Tensor | None = None,
1418
+ max_seqlen: torch.Tensor | None = None,
1419
+) -> torch.Tensor:
1420
+ """Run AITER FP8 attention for fixed or packed inputs."""
1421
+ from aiter import flash_attn_varlen_fp8_pertensor_func
1422
+
1423
+ q_len = q.size(1)
1424
+ if cu_seqlens is None:
1425
+ cu_seqlens = torch.arange(
1426
+ 0, (batch_size + 1) * q_len, step=q_len, dtype=torch.int32, device=q.device
1427
+ )
1428
+ max_seqlen_value = q_len if max_seqlen is None else max_seqlen.item()
1429
+
1430
+ q, k, v = (x.flatten(0, 1) for x in (q, k, v))
1431
+ output = flash_attn_varlen_fp8_pertensor_func(
1432
+ q,
1433
+ k,
1434
+ v,
1435
+ q_descale=q_descale,
1436
+ k_descale=k_descale,
1437
+ v_descale=v_descale,
1438
+ cu_seqlens_q=cu_seqlens,
1439
+ cu_seqlens_k=cu_seqlens,
1440
+ max_seqlen_q=max_seqlen_value,
1441
+ max_seqlen_k=max_seqlen_value,
1442
+ causal=False,
1443
+ softmax_scale=scale,
1444
+ )
1445
+ return output.to(output_dtype).reshape(batch_size, q_len, *output.shape[1:])
1446
+
1447
+
1448
+def _rocm_aiter_fp8_attn_fake(
1449
+ q: torch.Tensor,
1450
+ k: torch.Tensor,
1451
+ v: torch.Tensor,
1452
+ q_descale: torch.Tensor,
1453
+ k_descale: torch.Tensor,
1454
+ v_descale: torch.Tensor,
1455
+ batch_size: int,
1456
+ output_dtype: torch.dtype,
1457
+ scale: float | None = None,
1458
+ cu_seqlens: torch.Tensor | None = None,
1459
+ max_seqlen: torch.Tensor | None = None,
1460
+) -> torch.Tensor:
1461
+ return torch.empty(
1462
+ (*q.shape[:-1], v.shape[-1]), device=q.device, dtype=output_dtype
1463
+ )
1464
+
1465
+
1466
# Global flag to ensure ops are registered only once
1467
_OPS_REGISTERED = False
1468
@@ -1959,6 +2018,14 @@ class rocm_aiter_ops:
2018
dispatch_key=current_platform.dispatch_key,
2019
)
2020
2021
+ direct_register_custom_op(
2022
+ op_name="aiter_fp8_attn_wrapper",
2023
+ op_func=_rocm_aiter_fp8_attn_impl,
2024
+ mutates_args=[],
2025
+ fake_impl=_rocm_aiter_fp8_attn_fake,
2026
+ dispatch_key=current_platform.dispatch_key,
2027
+ )
2028
+
2029
direct_register_custom_op(
2030
op_name="rocm_aiter_gemm_a8wfp4",
2031
op_func=_rocm_aiter_gemm_a8wfp4_impl,
@@ -2830,6 +2897,34 @@ class rocm_aiter_ops:
2897
sink_ptr=sink_ptr,
2898
)
2899
2900
+ @staticmethod
2901
+ def fp8_attn_wrapper(
2902
+ q: torch.Tensor,
2903
+ k: torch.Tensor,
2904
+ v: torch.Tensor,
2905
+ q_descale: torch.Tensor,
2906
+ k_descale: torch.Tensor,
2907
+ v_descale: torch.Tensor,
2908
+ batch_size: int,
2909
+ output_dtype: torch.dtype,
2910
+ scale: float | None = None,
2911
+ cu_seqlens: torch.Tensor | None = None,
2912
+ max_seqlen: torch.Tensor | None = None,
2913
+ ) -> torch.Tensor:
2914
+ return torch.ops.vllm.aiter_fp8_attn_wrapper(
2915
+ q,
2916
+ k,
2917
+ v,
2918
+ q_descale,
2919
+ k_descale,
2920
+ v_descale,
2921
+ batch_size,
2922
+ output_dtype,
2923
+ scale,
2924
+ cu_seqlens,
2925
+ max_seqlen,
2926
+ )
2927
+
2928
@staticmethod
2929
def pa_fwd_asm(
2930
Q: torch.Tensor,
vllm/model_executor/layers/attention/mm_encoder_attention.py
+108
-32
@@ -24,6 +24,7 @@ from vllm.model_executor.models.vision import (
24
get_multimodal_config,
25
get_vit_attn_backend,
26
)
27
+from vllm.platforms import current_platform
28
from vllm.utils.flashinfer import (
29
is_flashinfer_cudnn_fp8_prefill_attn_supported,
30
)
@@ -32,6 +33,7 @@ from vllm.utils.torch_utils import async_tensor_h2d
33
from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
34
from vllm.v1.attention.backends.registry import AttentionBackendEnum
35
from vllm.v1.attention.ops.vit_attn_wrappers import (
36
+ vit_aiter_fp8_attn_wrapper,
37
vit_flash_attn_wrapper,
38
vit_flashinfer_wrapper,
39
vit_torch_sdpa_wrapper,
@@ -380,8 +382,8 @@ class MMEncoderAttention(CustomOp):
382
No-op if FP8 is not requested. Raises ``ValueError`` if FP8 is
383
requested but the platform does not support it.
384
"""
383
- # Populate defaults so ``_forward_flashinfer`` can
384
- # check ``self.fp8_enabled`` and others without AttributeError.
385
+ # Populate defaults so backend forward methods can check
386
+ # ``self.fp8_enabled`` and related state without AttributeError.
387
self.fp8_enabled = False
388
self._fp8_dynamic_scale = False
389
self.fp8_quant: QuantFP8 | None = None
@@ -393,13 +395,38 @@ class MMEncoderAttention(CustomOp):
395
if mm_cfg is None or mm_cfg.mm_encoder_attn_dtype != "fp8":
396
return
397
396
- # FP8 path
397
- if not is_flashinfer_cudnn_fp8_prefill_attn_supported():
398
+ if self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA:
399
+ if not current_platform.is_rocm():
400
+ raise ValueError("AITER FP8 ViT attention requires ROCm.")
401
+
402
+ from vllm.platforms.rocm import on_mi3xx
403
+
404
+ if not on_mi3xx():
405
+ raise ValueError(
406
+ "AITER FP8 ViT attention requires an MI300-series or "
407
+ "MI350-series GPU (gfx942 or gfx950)."
408
+ )
409
+ try:
410
+ from aiter import flash_attn_varlen_fp8_pertensor_func # noqa: F401
411
+ except ImportError as exc:
412
+ raise ValueError(
413
+ "mm_encoder_attn_dtype='fp8' with ROCM_AITER_FA requires "
414
+ "an AITER build that provides "
415
+ "flash_attn_varlen_fp8_pertensor_func."
416
+ ) from exc
417
+ elif self.attn_backend == AttentionBackendEnum.FLASHINFER:
418
+ if not is_flashinfer_cudnn_fp8_prefill_attn_supported():
419
+ raise ValueError(
420
+ "mm_encoder_attn_dtype='fp8' requires the FlashInfer "
421
+ "cuDNN backend with cuDNN >= 9.17.1 on Blackwell (SM 100) "
422
+ "or newer. cuDNN's FP8 SDPA path with bf16/fp16 output is "
423
+ "not available on Hopper (H100/H200) or earlier."
424
+ )
425
+ else:
426
raise ValueError(
399
- "mm_encoder_attn_dtype='fp8' requires the FlashInfer "
400
- "cuDNN backend with cuDNN >= 9.17.1 on Blackwell (SM 100) "
401
- "or newer. cuDNN's FP8 SDPA path with bf16/fp16 output is "
402
- "not available on Hopper (H100/H200) or earlier."
427
+ "mm_encoder_attn_dtype='fp8' requires either the "
428
+ "ROCM_AITER_FA backend on ROCm or the FlashInfer cuDNN "
429
+ f"backend on CUDA, got {self.attn_backend}."
430
)
431
432
self.fp8_enabled = True
@@ -642,6 +669,37 @@ class MMEncoderAttention(CustomOp):
669
buffer_wrapped,
670
)
671
672
+ def _quantize_qkv_fp8(
673
+ self,
674
+ query: torch.Tensor,
675
+ key: torch.Tensor,
676
+ value: torch.Tensor,
677
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
678
+ assert self.fp8_quant is not None
679
+
680
+ if self._fp8_dynamic_scale:
681
+ self._record_amax_and_update_scales(query, key, value)
682
+
683
+ query = quantize_fp8_maybe_pad_head_dim(
684
+ query,
685
+ self._fp8_q_scale,
686
+ skip_scale=self.skip_scale_q,
687
+ fp8_quant=self.fp8_quant,
688
+ )
689
+ key = quantize_fp8_maybe_pad_head_dim(
690
+ key,
691
+ self._fp8_k_scale,
692
+ skip_scale=self.skip_scale_k,
693
+ fp8_quant=self.fp8_quant,
694
+ )
695
+ value = quantize_fp8_maybe_pad_head_dim(
696
+ value,
697
+ self._fp8_v_scale,
698
+ skip_scale=self.skip_scale_v,
699
+ fp8_quant=self.fp8_quant,
700
+ )
701
+ return query, key, value
702
+
703
def _forward_flashinfer(
704
self,
705
query: torch.Tensor,
@@ -653,29 +711,7 @@ class MMEncoderAttention(CustomOp):
711
| None = None, # Only used for FlashInfer CuDNN backend
712
) -> torch.Tensor:
713
if self.fp8_enabled:
656
- assert self.fp8_quant is not None
657
-
658
- if self._fp8_dynamic_scale:
659
- self._record_amax_and_update_scales(query, key, value)
660
-
661
- query = quantize_fp8_maybe_pad_head_dim(
662
- query,
663
- self._fp8_q_scale,
664
- skip_scale=self.skip_scale_q,
665
- fp8_quant=self.fp8_quant,
666
- )
667
- key = quantize_fp8_maybe_pad_head_dim(
668
- key,
669
- self._fp8_k_scale,
670
- skip_scale=self.skip_scale_k,
671
- fp8_quant=self.fp8_quant,
672
- )
673
- value = quantize_fp8_maybe_pad_head_dim(
674
- value,
675
- self._fp8_v_scale,
676
- skip_scale=self.skip_scale_v,
677
- fp8_quant=self.fp8_quant,
678
- )
714
+ query, key, value = self._quantize_qkv_fp8(query, key, value)
715
716
output = vit_flashinfer_wrapper(
717
q=query,
@@ -697,6 +733,44 @@ class MMEncoderAttention(CustomOp):
733
734
return output
735
736
+ def _forward_aiter_fp8(
737
+ self,
738
+ query: torch.Tensor,
739
+ key: torch.Tensor,
740
+ value: torch.Tensor,
741
+ cu_seqlens: torch.Tensor | None = None,
742
+ max_seqlen: torch.Tensor | None = None,
743
+ ) -> torch.Tensor:
744
+ assert (cu_seqlens is not None and max_seqlen is not None) or (
745
+ cu_seqlens is None and max_seqlen is None
746
+ ), "cu_seqlens and max_seqlen should be both set or both None."
747
+
748
+ bsz, q_len = query.size()[:2]
749
+ kv_len = key.size(1)
750
+ is_reshaped = query.dim() != 4
751
+ query, key, value = self.view_qkv_to_4d(query, key, value, bsz, q_len, kv_len)
752
+
753
+ query, key, value = self._quantize_qkv_fp8(query, key, value)
754
+
755
+ output = vit_aiter_fp8_attn_wrapper(
756
+ q=query,
757
+ k=key,
758
+ v=value,
759
+ q_descale=self._fp8_q_scale,
760
+ k_descale=self._fp8_k_scale,
761
+ v_descale=self._fp8_v_scale,
762
+ batch_size=bsz,
763
+ output_dtype=self.dtype,
764
+ scale=self.scale,
765
+ cu_seqlens=cu_seqlens,
766
+ max_seqlen=max_seqlen,
767
+ )
768
+ if output.shape[-1] != self.head_size:
769
+ output = output[..., : self.head_size].contiguous()
770
+ if is_reshaped:
771
+ output = output.reshape(bsz, q_len, -1)
772
+ return output
773
+
774
def forward_native(
775
self,
776
query: torch.Tensor,
@@ -719,7 +793,9 @@ class MMEncoderAttention(CustomOp):
793
sequence_lengths: torch.Tensor
794
| None = None, # Only used for FlashInfer CuDNN backend
795
) -> torch.Tensor:
722
- if self.is_flash_attn_backend:
796
+ if self.fp8_enabled and self.attn_backend == AttentionBackendEnum.ROCM_AITER_FA:
797
+ return self._forward_aiter_fp8(query, key, value, cu_seqlens, max_seqlen)
798
+ elif self.is_flash_attn_backend:
799
return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
800
elif self.attn_backend == AttentionBackendEnum.TRITON_ATTN:
801
return self._forward_triton(query, key, value, cu_seqlens, max_seqlen)
vllm/platforms/rocm.py
+1
-1
@@ -657,7 +657,7 @@ class RocmPlatform(Platform):
657
658
from vllm._aiter_ops import rocm_aiter_ops
659
660
- if rocm_aiter_ops.is_enabled() and on_gfx9():
660
+ if rocm_aiter_ops.is_mha_enabled() and on_gfx9():
661
logger.info_once("Using AITER Flash Attention backend for ViT model.")
662
return AttentionBackendEnum.ROCM_AITER_FA
663
vllm/v1/attention/ops/vit_attn_wrappers.py
+29
@@ -18,6 +18,7 @@ import einops
18
import torch
19
import torch.nn.functional as F
20
21
+from vllm._aiter_ops import rocm_aiter_ops
22
from vllm.platforms import current_platform
23
from vllm.utils.torch_utils import direct_register_custom_op
24
@@ -114,6 +115,34 @@ def vit_flash_attn_wrapper(
115
)
116
117
118
+def vit_aiter_fp8_attn_wrapper(
119
+ q: torch.Tensor,
120
+ k: torch.Tensor,
121
+ v: torch.Tensor,
122
+ q_descale: torch.Tensor,
123
+ k_descale: torch.Tensor,
124
+ v_descale: torch.Tensor,
125
+ batch_size: int,
126
+ output_dtype: torch.dtype,
127
+ scale: float | None = None,
128
+ cu_seqlens: torch.Tensor | None = None,
129
+ max_seqlen: torch.Tensor | None = None,
130
+) -> torch.Tensor:
131
+ return rocm_aiter_ops.fp8_attn_wrapper(
132
+ q,
133
+ k,
134
+ v,
135
+ q_descale,
136
+ k_descale,
137
+ v_descale,
138
+ batch_size,
139
+ output_dtype,
140
+ scale,
141
+ cu_seqlens,
142
+ max_seqlen,
143
+ )
144
+
145
+
146
def triton_attn_wrapper(
147
q: torch.Tensor,
148
k: torch.Tensor,