main
py 486 lines 16 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test:
4
5 * Tests for MMEncoderAttention layer
6 """
7
8 import itertools
9 from unittest.mock import patch
10
11 import numpy as np
12 import pytest
13 import torch
14
15 from vllm.config import get_current_vllm_config
16 from vllm.config.multimodal import MultiModalConfig
17 from vllm.model_executor.layers.attention import MMEncoderAttention
18 from vllm.platforms import current_platform
19 from vllm.platforms.cpu import CpuPlatform
20 from vllm.platforms.cuda import CudaPlatform
21 from vllm.platforms.interface import DeviceCapability
22 from vllm.platforms.rocm import RocmPlatform, on_mi3xx
23 from vllm.utils.torch_utils import set_default_torch_dtype, set_random_seed
24 from vllm.v1.attention.backends.registry import AttentionBackendEnum
25 from vllm.v1.attention.selector import _cached_get_attn_backend
26
27
28 @pytest.fixture(autouse=True)
29 def reset_test_state():
30 """Clear cached selectors and restore process-wide torch defaults."""
31 default_device = torch.get_default_device()
32 default_dtype = torch.get_default_dtype()
33 _cached_get_attn_backend.cache_clear()
34 try:
35 yield
36 finally:
37 torch.set_default_device(default_device)
38 torch.set_default_dtype(default_dtype)
39 _cached_get_attn_backend.cache_clear()
40
41
42 devices = ["cpu"]
43 if current_platform.is_cuda():
44 devices.append("cuda")
45 if current_platform.is_rocm():
46 devices.append("hip")
47
48
49 @pytest.mark.parametrize("device", devices)
50 def test_mha_attn_platform(default_vllm_config, device: str):
51 """Test the attention selector between different platform and device."""
52 torch.set_default_dtype(torch.float16)
53
54 if device == "cpu":
55 with (
56 patch("vllm.model_executor.models.vision.current_platform", CpuPlatform()),
57 ):
58 attn = MMEncoderAttention(16, 64, scale=1)
59 assert attn.attn_backend == AttentionBackendEnum.TORCH_SDPA
60 elif device == "hip":
61 with (
62 patch("vllm.model_executor.models.vision.current_platform", RocmPlatform()),
63 ):
64 attn = MMEncoderAttention(16, 64, scale=1)
65 assert attn.attn_backend == AttentionBackendEnum.FLASH_ATTN
66 else:
67 # Test CUDA with head_size=64 (divisible by 32)
68 # - should use vLLM's FlashAttention
69 with (
70 patch("vllm.model_executor.models.vision.current_platform", CudaPlatform()),
71 ):
72 attn = MMEncoderAttention(16, 64, scale=1)
73 assert attn.attn_backend == AttentionBackendEnum.FLASH_ATTN
74
75 # Test CUDA with head_size=72 (not divisible by 32)
76 # - should use vLLM's FlashAttention
77 with (
78 patch("vllm.model_executor.models.vision.current_platform", CudaPlatform()),
79 ):
80 attn = MMEncoderAttention(16, 72, scale=1)
81 assert attn.attn_backend == AttentionBackendEnum.FLASH_ATTN
82
83 # Test CUDA with head_size=72 (not divisible by 32)
84 # - should use vLLM's FlashAttention
85 with (
86 patch("vllm.model_executor.models.vision.current_platform", CudaPlatform()),
87 set_default_torch_dtype(torch.float32),
88 ):
89 attn = MMEncoderAttention(16, 72, scale=1)
90 assert attn.attn_backend == AttentionBackendEnum.TRITON_ATTN
91
92 # Test Turing (pre-Ampere, sm_75): FlashAttention requires sm>=80,
93 # and Triton no longer supports MMA on Turing, so we expect that
94 # TORCH_SDPA is used for MMEncoderAttention.
95 with (
96 patch("vllm.model_executor.models.vision.current_platform", CudaPlatform()),
97 patch.object(
98 CudaPlatform,
99 "get_device_capability",
100 return_value=DeviceCapability(major=7, minor=5),
101 ),
102 ):
103 attn = MMEncoderAttention(16, 64, scale=1)
104 assert attn.attn_backend == AttentionBackendEnum.TORCH_SDPA
105
106
107 def ref_attention(
108 query: torch.Tensor,
109 key: torch.Tensor,
110 value: torch.Tensor,
111 scale: float,
112 ) -> torch.Tensor:
113 """Native implementation of scaled dot product attention without mask:
114 - query, key, value: [batch_size, seq_len, num_heads, head_size]
115 - attn_mask: [batch_size, seq_len, seq_len]
116 """
117 query, key, value = (x.transpose(1, 2) for x in (query, key, value))
118 attn_weights = scale * torch.matmul(query, key.transpose(2, 3))
119 attn_weights = torch.softmax(attn_weights, dim=-1).to(value.dtype)
120 out = torch.matmul(attn_weights, value).transpose(1, 2)
121 return out
122
123
124 BATCH_SIZES = [1, 16]
125 SEQ_LENS = [1]
126 VAR_SEQ_LENS = [
127 [2, 2],
128 [2, 3, 4],
129 ]
130 NUM_HEADS = [1, 16]
131 NUM_KV_HEADS = [1]
132 HEAD_SIZES = [64, 80]
133 # flshattF and tritonflashattF supported: {torch.float16, torch.bfloat16}
134 DTYPES = (
135 [torch.half, torch.bfloat16, torch.float]
136 if not current_platform.is_rocm()
137 else [torch.half, torch.bfloat16]
138 )
139 CUDA_DEVICES = ["cuda"]
140
141
142 @pytest.mark.parametrize("batch_size", BATCH_SIZES)
143 @pytest.mark.parametrize("seq_len", SEQ_LENS)
144 @pytest.mark.parametrize("num_heads", NUM_HEADS)
145 @pytest.mark.parametrize("num_kv_heads", NUM_KV_HEADS)
146 @pytest.mark.parametrize("head_size", HEAD_SIZES)
147 @pytest.mark.parametrize("dtype", DTYPES)
148 @pytest.mark.parametrize("device", CUDA_DEVICES)
149 def test_mha_attn_forward(
150 default_vllm_config,
151 batch_size: int,
152 seq_len: int,
153 num_heads: int,
154 num_kv_heads: int,
155 head_size: int,
156 dtype: torch.dtype,
157 device: str,
158 ):
159 set_random_seed(0)
160 torch.set_default_device(device)
161 torch.set_default_dtype(dtype)
162
163 q = torch.randn(batch_size, seq_len, num_heads * head_size)
164 k = torch.randn(batch_size, seq_len, num_kv_heads * head_size)
165 v = torch.randn(batch_size, seq_len, num_kv_heads * head_size)
166 scale = 1.0 / head_size**0.5
167 attn = MMEncoderAttention(
168 num_heads, head_size, scale=scale, num_kv_heads=num_kv_heads
169 )
170 output = attn(q, k, v)
171
172 assert num_heads % num_kv_heads == 0
173 num_queries_per_kv = num_heads // num_kv_heads
174 q = q.reshape(batch_size, seq_len, num_heads, head_size)
175 k = k.reshape(batch_size, seq_len, num_kv_heads, head_size)
176 v = v.reshape(batch_size, seq_len, num_kv_heads, head_size)
177 if num_queries_per_kv > 1:
178 k = torch.repeat_interleave(k, num_queries_per_kv, dim=2)
179 v = torch.repeat_interleave(v, num_queries_per_kv, dim=2)
180
181 ref_output = ref_attention(
182 q,
183 k,
184 v,
185 scale=scale,
186 ).reshape(batch_size, seq_len, num_heads * head_size)
187 tol_kwargs = (
188 dict(rtol=1e-3, atol=1e-3)
189 if attn.attn_backend == AttentionBackendEnum.TRITON_ATTN
190 else {}
191 )
192 torch.testing.assert_close(output, ref_output, **tol_kwargs)
193
194
195 @pytest.mark.parametrize("var_seq_len", VAR_SEQ_LENS)
196 @pytest.mark.parametrize("num_heads", NUM_HEADS)
197 @pytest.mark.parametrize("num_kv_heads", NUM_KV_HEADS)
198 @pytest.mark.parametrize("head_size", HEAD_SIZES)
199 @pytest.mark.parametrize("dtype", DTYPES)
200 @pytest.mark.parametrize("device", CUDA_DEVICES)
201 def test_mha_attn_varlen_forward(
202 default_vllm_config,
203 var_seq_len: list[int],
204 num_heads: int,
205 num_kv_heads: int,
206 head_size: int,
207 dtype: torch.dtype,
208 device: str,
209 ):
210 set_random_seed(0)
211 torch.set_default_device(device)
212 torch.set_default_dtype(dtype)
213
214 q = torch.randn(1, sum(var_seq_len), num_heads, head_size)
215 k = torch.randn(1, sum(var_seq_len), num_kv_heads, head_size)
216 v = torch.randn(1, sum(var_seq_len), num_kv_heads, head_size)
217 cu_seqlens = torch.tensor(
218 [0] + list(itertools.accumulate(var_seq_len)), dtype=torch.int32
219 )
220 scale = 1.0 / head_size**0.5
221 attn = MMEncoderAttention(
222 num_heads, head_size, scale=scale, num_kv_heads=num_kv_heads
223 )
224 output = attn(
225 q, k, v, cu_seqlens=cu_seqlens, max_seqlen=torch.tensor(max(var_seq_len))
226 )
227
228 assert num_heads % num_kv_heads == 0
229 num_queries_per_kv = num_heads // num_kv_heads
230 if num_queries_per_kv > 1:
231 k = torch.repeat_interleave(k, num_queries_per_kv, dim=2)
232 v = torch.repeat_interleave(v, num_queries_per_kv, dim=2)
233
234 ref_output = []
235 for q_i, k_i, v_i in zip(
236 torch.split(q, var_seq_len, dim=1),
237 torch.split(k, var_seq_len, dim=1),
238 torch.split(v, var_seq_len, dim=1),
239 ):
240 output_i = ref_attention(
241 q_i,
242 k_i,
243 v_i,
244 scale=scale,
245 )
246 ref_output.append(output_i)
247 ref_output = torch.cat(ref_output, dim=1)
248 torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
249
250
251 @pytest.mark.parametrize("var_seq_len", VAR_SEQ_LENS)
252 @pytest.mark.parametrize(
253 "dtype",
254 [torch.bfloat16, torch.half],
255 )
256 @pytest.mark.parametrize("device", CUDA_DEVICES)
257 def test_mha_attn_varlen_forward_flashinfer(
258 default_vllm_config,
259 var_seq_len: list[int],
260 dtype: torch.dtype,
261 device: str,
262 ):
263 """Test MMEncoderAttention varlen forward with FLASHINFER backend (head_size=72).
264
265 Exercises the path that uses --mm-encoder-attn-backend=FLASHINFER with
266 recomputed cu_seqlens, max_seqlen, and sequence_lengths as in qwen3_vl
267 vision encoder.
268 """
269 pytest.importorskip("flashinfer")
270
271 num_heads = 16
272 head_size = 72
273 set_random_seed(0)
274 torch.set_default_device(device)
275 torch.set_default_dtype(dtype)
276
277 # Override vllm config so get_vit_attn_backend returns FLASHINFER (simulates
278 # --mm-encoder-attn-backend=FLASHINFER).
279 vllm_config = get_current_vllm_config()
280 old_model_config = vllm_config.model_config
281 minimal_model_config = type(
282 "MinimalModelConfig",
283 (),
284 {
285 "multimodal_config": MultiModalConfig(
286 mm_encoder_attn_backend=AttentionBackendEnum.FLASHINFER
287 ),
288 },
289 )()
290 vllm_config.model_config = minimal_model_config
291 try:
292 total_len = sum(var_seq_len)
293 # Stride of second dim = 3 * num_heads * head_size (same as qwen2_5_vl
294 # after qkv rearrange and unbind: qkv shape (b, s, 3, head, head_dim)).
295 qkv = torch.randn(1, total_len, 3, num_heads, head_size)
296 q, k, v = qkv.unbind(dim=2)
297
298 cu_seqlens_np = np.array(
299 [0] + list(itertools.accumulate(var_seq_len)), dtype=np.int32
300 )
301 hidden_size = num_heads * head_size
302 tp_size = 1
303
304 sequence_lengths = MMEncoderAttention.maybe_compute_seq_lens(
305 AttentionBackendEnum.FLASHINFER,
306 cu_seqlens_np,
307 device,
308 )
309
310 max_seqlen_val = MMEncoderAttention.compute_max_seqlen(
311 AttentionBackendEnum.FLASHINFER, cu_seqlens_np
312 )
313 max_seqlen = torch.tensor(max_seqlen_val, device=device, dtype=torch.int32)
314
315 cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
316 AttentionBackendEnum.FLASHINFER,
317 cu_seqlens_np,
318 hidden_size,
319 tp_size,
320 device,
321 )
322
323 scale = 1.0 / head_size**0.5
324 attn = MMEncoderAttention(
325 num_heads,
326 head_size,
327 scale=scale,
328 num_kv_heads=num_heads,
329 )
330 assert attn.attn_backend == AttentionBackendEnum.FLASHINFER
331
332 output = attn(
333 q,
334 k,
335 v,
336 cu_seqlens=cu_seqlens,
337 max_seqlen=max_seqlen,
338 sequence_lengths=sequence_lengths,
339 )
340
341 ref_output = []
342 for q_i, k_i, v_i in zip(
343 torch.split(q, var_seq_len, dim=1),
344 torch.split(k, var_seq_len, dim=1),
345 torch.split(v, var_seq_len, dim=1),
346 ):
347 output_i = ref_attention(q_i, k_i, v_i, scale=scale)
348 ref_output.append(output_i)
349 ref_output = torch.cat(ref_output, dim=1)
350 torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
351 finally:
352 vllm_config.model_config = old_model_config
353
354
355 @pytest.mark.skipif(
356 not current_platform.is_rocm() or not on_mi3xx(),
357 reason="AITER FP8 attention requires MI300/MI350",
358 )
359 @pytest.mark.parametrize("var_seq_len", [[128, 193], [256, 384, 511]])
360 @pytest.mark.parametrize("head_size", [64, 72, 80])
361 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.half])
362 def test_mha_attn_varlen_forward_aiter_fp8(
363 default_vllm_config,
364 var_seq_len: list[int],
365 head_size: int,
366 dtype: torch.dtype,
367 ):
368 """Compare packed AITER FP8 ViT attention with a BF16/FP16 reference."""
369 aiter = pytest.importorskip("aiter")
370 if not hasattr(aiter, "flash_attn_varlen_fp8_pertensor_func"):
371 pytest.skip("Installed AITER does not provide varlen FP8 attention")
372
373 num_heads = 4
374 set_random_seed(0)
375 torch.set_default_device("cuda")
376 torch.set_default_dtype(dtype)
377
378 vllm_config = get_current_vllm_config()
379 old_model_config = vllm_config.model_config
380 minimal_model_config = type(
381 "MinimalModelConfig",
382 (),
383 {
384 "multimodal_config": MultiModalConfig(
385 mm_encoder_attn_backend=AttentionBackendEnum.ROCM_AITER_FA,
386 mm_encoder_attn_dtype="fp8",
387 ),
388 },
389 )()
390 vllm_config.model_config = minimal_model_config
391 try:
392 total_len = sum(var_seq_len)
393 # Keep Q/K/V as non-contiguous views, matching interleaved ViT QKV.
394 qkv = torch.randn(1, total_len, 3, num_heads, head_size)
395 q, k, v = qkv.unbind(dim=2)
396 cu_seqlens = torch.tensor(
397 [0] + list(itertools.accumulate(var_seq_len)), dtype=torch.int32
398 )
399 max_seqlen = torch.tensor(max(var_seq_len), dtype=torch.int32)
400 scale = 1.0 / head_size**0.5
401
402 attn = MMEncoderAttention(num_heads, head_size, scale=scale)
403 assert attn.attn_backend == AttentionBackendEnum.ROCM_AITER_FA
404 assert attn.fp8_enabled
405 output = attn(
406 q,
407 k,
408 v,
409 cu_seqlens=cu_seqlens,
410 max_seqlen=max_seqlen,
411 )
412
413 ref_output = torch.cat(
414 [
415 ref_attention(q_i, k_i, v_i, scale=scale)
416 for q_i, k_i, v_i in zip(
417 torch.split(q, var_seq_len, dim=1),
418 torch.split(k, var_seq_len, dim=1),
419 torch.split(v, var_seq_len, dim=1),
420 )
421 ],
422 dim=1,
423 )
424 diff = (output.float() - ref_output.float()).abs()
425 cosine = torch.nn.functional.cosine_similarity(
426 output.float().flatten(),
427 ref_output.float().flatten(),
428 dim=0,
429 )
430
431 assert output.shape == ref_output.shape
432 assert output.dtype == dtype
433 assert cosine.item() >= 0.99
434 assert diff.mean().item() <= 0.03
435 assert diff.max().item() <= 0.30
436 finally:
437 vllm_config.model_config = old_model_config
438
439
440 @pytest.mark.skipif(
441 not current_platform.is_rocm(), reason="AITER FP8 attention requires ROCm"
442 )
443 def test_mha_attn_aiter_fp8_rejects_unsupported_arch(default_vllm_config):
444 vllm_config = get_current_vllm_config()
445 old_model_config = vllm_config.model_config
446 vllm_config.model_config = type(
447 "MinimalModelConfig",
448 (),
449 {
450 "multimodal_config": MultiModalConfig(
451 mm_encoder_attn_backend=AttentionBackendEnum.ROCM_AITER_FA,
452 mm_encoder_attn_dtype="fp8",
453 ),
454 },
455 )()
456 try:
457 with (
458 patch("vllm.platforms.rocm.on_mi3xx", return_value=False),
459 pytest.raises(ValueError, match="gfx942 or gfx950"),
460 ):
461 MMEncoderAttention(4, 64)
462 finally:
463 vllm_config.model_config = old_model_config
464
465
466 @pytest.mark.skipif(
467 not current_platform.is_rocm(), reason="AITER FP8 attention requires ROCm"
468 )
469 def test_mha_attn_fp8_rejects_wrong_backend(default_vllm_config):
470 vllm_config = get_current_vllm_config()
471 old_model_config = vllm_config.model_config
472 vllm_config.model_config = type(
473 "MinimalModelConfig",
474 (),
475 {
476 "multimodal_config": MultiModalConfig(
477 mm_encoder_attn_backend=AttentionBackendEnum.FLASH_ATTN,
478 mm_encoder_attn_dtype="fp8",
479 ),
480 },
481 )()
482 try:
483 with pytest.raises(ValueError, match="requires either"):
484 MMEncoderAttention(4, 64)
485 finally:
486 vllm_config.model_config = old_model_config