main
py 318 lines 9.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for per-request causal/non-causal attention (mixed batches).
4
5 Validates that both triton and flash-attention backends correctly handle
6 batches where some sequences use causal masking and others use non-causal
7 (bidirectional) masking — needed by DiffusionGemma.
8 """
9
10 import pytest
11 import torch
12
13 from vllm.platforms import current_platform
14 from vllm.utils.torch_utils import set_random_seed
15
16 # Mixed causal/non-causal attention is only validated on a subset of GPUs:
17 # the Triton path on Hopper (SM90) and B200 (SM100); the FA4 path on Hopper
18 # (SM90) only.
19 _device_capability = current_platform.get_device_capability()
20 _major = _device_capability.major if _device_capability is not None else None
21
22 NUM_HEADS = [(4, 4), (8, 2)]
23 HEAD_SIZES = [128]
24 BLOCK_SIZES = [16]
25 DTYPES = [torch.bfloat16]
26
27
28 def ref_paged_attn(
29 query: torch.Tensor,
30 key_cache: torch.Tensor,
31 value_cache: torch.Tensor,
32 query_lens: list[int],
33 kv_lens: list[int],
34 block_tables: torch.Tensor,
35 scale: float,
36 per_seq_causal: list[bool],
37 sliding_window: int | None = None,
38 ) -> torch.Tensor:
39 num_seqs = len(query_lens)
40 block_tables_np = block_tables.cpu().numpy()
41 _, block_size, num_kv_heads, head_size = key_cache.shape
42
43 outputs: list[torch.Tensor] = []
44 start_idx = 0
45 for i in range(num_seqs):
46 query_len = query_lens[i]
47 kv_len = kv_lens[i]
48 q = query[start_idx : start_idx + query_len]
49 q = q * scale
50
51 num_kv_blocks = (kv_len + block_size - 1) // block_size
52 block_indices = block_tables_np[i, :num_kv_blocks]
53 k = key_cache[block_indices].view(-1, num_kv_heads, head_size)[:kv_len]
54 v = value_cache[block_indices].view(-1, num_kv_heads, head_size)[:kv_len]
55
56 if q.shape[1] != k.shape[1]:
57 k = torch.repeat_interleave(k, q.shape[1] // k.shape[1], dim=1)
58 v = torch.repeat_interleave(v, q.shape[1] // v.shape[1], dim=1)
59
60 attn = torch.einsum("qhd,khd->hqk", q, k).float()
61
62 if per_seq_causal[i]:
63 mask = torch.triu(
64 torch.ones(query_len, kv_len, device=attn.device),
65 diagonal=kv_len - query_len + 1,
66 ).bool()
67 else:
68 mask = torch.zeros(query_len, kv_len, device=attn.device).bool()
69
70 if sliding_window is not None:
71 sw_mask = (
72 torch.triu(
73 torch.ones(query_len, kv_len, device=attn.device),
74 diagonal=kv_len - (query_len + sliding_window) + 1,
75 )
76 .bool()
77 .logical_not()
78 )
79 mask |= sw_mask
80
81 attn.masked_fill_(mask, float("-inf"))
82 attn = torch.softmax(attn, dim=-1).to(v.dtype)
83 out = torch.einsum("hqk,khd->qhd", attn, v)
84 outputs.append(out)
85 start_idx += query_len
86
87 return torch.cat(outputs, dim=0)
88
89
90 # ---- Triton backend test ----
91
92
93 @pytest.mark.skipif(
94 _major not in (9, 10),
95 reason="Triton mixed causal attention requires Hopper (SM90) or B200 (SM100).",
96 )
97 @pytest.mark.parametrize(
98 "seq_lens",
99 [[(1, 128), (5, 64), (1, 256)]],
100 )
101 @pytest.mark.parametrize(
102 "per_seq_causal",
103 [[True, False, True], [False, True, False], [True, True, False]],
104 )
105 @pytest.mark.parametrize("num_heads", NUM_HEADS)
106 @pytest.mark.parametrize("head_size", HEAD_SIZES)
107 @pytest.mark.parametrize("block_size", BLOCK_SIZES)
108 @pytest.mark.parametrize("dtype", DTYPES)
109 @torch.inference_mode()
110 def test_triton_mixed_causal(
111 seq_lens: list[tuple[int, int]],
112 per_seq_causal: list[bool],
113 num_heads: tuple[int, int],
114 head_size: int,
115 dtype: torch.dtype,
116 block_size: int,
117 ):
118 if not current_platform.is_cuda():
119 pytest.skip("Triton attention requires CUDA")
120
121 from vllm.v1.attention.ops.triton_unified_attention import unified_attention
122
123 set_random_seed(42)
124 device = "cuda"
125
126 num_query_heads, num_kv_heads = num_heads
127 assert len(seq_lens) == len(per_seq_causal)
128
129 query_lens = [s[0] for s in seq_lens]
130 kv_lens = [s[1] for s in seq_lens]
131 num_seqs = len(seq_lens)
132
133 num_query_tokens = sum(query_lens)
134 max_kv_len = max(kv_lens)
135 max_num_blocks = (max_kv_len + block_size - 1) // block_size
136 num_blocks = max_num_blocks * num_seqs + 10
137
138 scale = head_size**-0.5
139 query = torch.randn(
140 num_query_tokens, num_query_heads, head_size, dtype=dtype, device=device
141 )
142 key_cache = torch.randn(
143 num_blocks, block_size, num_kv_heads, head_size, dtype=dtype, device=device
144 )
145 value_cache = torch.randn(
146 num_blocks, block_size, num_kv_heads, head_size, dtype=dtype, device=device
147 )
148
149 block_tables_list = []
150 for i in range(num_seqs):
151 n_blocks = (kv_lens[i] + block_size - 1) // block_size
152 blocks = list(range(i * max_num_blocks, i * max_num_blocks + n_blocks))
153 blocks += [0] * (max_num_blocks - n_blocks)
154 block_tables_list.append(blocks)
155 block_tables = torch.tensor(block_tables_list, dtype=torch.int32, device=device)
156
157 cu_seqlens_q = torch.zeros(num_seqs + 1, dtype=torch.int32, device=device)
158 for i, ql in enumerate(query_lens):
159 cu_seqlens_q[i + 1] = cu_seqlens_q[i] + ql
160
161 seqused_k = torch.tensor(kv_lens, dtype=torch.int32, device=device)
162 max_seqlen_q = max(query_lens)
163 max_seqlen_k = max(kv_lens)
164
165 causal_tensor = torch.tensor(per_seq_causal, dtype=torch.bool, device=device)
166
167 output = torch.empty_like(query)
168 unified_attention(
169 q=query,
170 k=key_cache,
171 v=value_cache,
172 out=output,
173 cu_seqlens_q=cu_seqlens_q,
174 max_seqlen_q=max_seqlen_q,
175 seqused_k=seqused_k,
176 max_seqlen_k=max_seqlen_k,
177 softmax_scale=scale,
178 causal=causal_tensor,
179 window_size=(-1, -1),
180 block_table=block_tables,
181 softcap=0.0,
182 q_descale=None,
183 k_descale=1.0,
184 v_descale=1.0,
185 )
186
187 ref_output = ref_paged_attn(
188 query,
189 key_cache,
190 value_cache,
191 query_lens,
192 kv_lens,
193 block_tables,
194 scale,
195 per_seq_causal,
196 )
197
198 torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
199
200
201 # ---- Flash Attention 4 backend test (native per_seq_causal) ----
202
203
204 @pytest.mark.skipif(
205 _major != 9,
206 reason="FA4 mixed causal attention requires Hopper (SM90).",
207 )
208 @pytest.mark.parametrize(
209 "seq_lens",
210 [[(1, 128), (5, 64), (1, 256)]],
211 )
212 @pytest.mark.parametrize(
213 "per_seq_causal",
214 [[True, False, True], [False, True, False]],
215 )
216 @pytest.mark.parametrize("num_heads", NUM_HEADS)
217 @pytest.mark.parametrize("head_size", HEAD_SIZES)
218 @pytest.mark.parametrize("block_size", BLOCK_SIZES)
219 @pytest.mark.parametrize("dtype", DTYPES)
220 @torch.inference_mode()
221 def test_flash_attn4_mixed_causal(
222 seq_lens: list[tuple[int, int]],
223 per_seq_causal: list[bool],
224 num_heads: tuple[int, int],
225 head_size: int,
226 dtype: torch.dtype,
227 block_size: int,
228 ):
229 if not current_platform.is_cuda():
230 pytest.skip("Flash attention requires CUDA")
231
232 try:
233 from vllm.vllm_flash_attn import (
234 fa_version_unsupported_reason,
235 flash_attn_varlen_func,
236 is_fa_version_supported,
237 )
238 except ImportError:
239 pytest.skip("vllm_flash_attn not available")
240
241 if not is_fa_version_supported(4):
242 reason = fa_version_unsupported_reason(4)
243 pytest.skip(f"FA4 not supported: {reason}")
244
245 set_random_seed(42)
246 device = "cuda"
247
248 num_query_heads, num_kv_heads = num_heads
249 assert len(seq_lens) == len(per_seq_causal)
250
251 query_lens = [s[0] for s in seq_lens]
252 kv_lens = [s[1] for s in seq_lens]
253 num_seqs = len(seq_lens)
254
255 num_query_tokens = sum(query_lens)
256 max_kv_len = max(kv_lens)
257 max_num_blocks = (max_kv_len + block_size - 1) // block_size
258 num_blocks = max_num_blocks * num_seqs + 10
259
260 scale = head_size**-0.5
261 query = torch.randn(
262 num_query_tokens, num_query_heads, head_size, dtype=dtype, device=device
263 )
264 key_cache = torch.randn(
265 num_blocks, block_size, num_kv_heads, head_size, dtype=dtype, device=device
266 )
267 value_cache = torch.randn(
268 num_blocks, block_size, num_kv_heads, head_size, dtype=dtype, device=device
269 )
270
271 block_tables_list = []
272 for i in range(num_seqs):
273 n_blocks = (kv_lens[i] + block_size - 1) // block_size
274 blocks = list(range(i * max_num_blocks, i * max_num_blocks + n_blocks))
275 blocks += [0] * (max_num_blocks - n_blocks)
276 block_tables_list.append(blocks)
277 block_tables = torch.tensor(block_tables_list, dtype=torch.int32, device=device)
278
279 cu_seqlens_q = torch.zeros(num_seqs + 1, dtype=torch.int32, device=device)
280 for i, ql in enumerate(query_lens):
281 cu_seqlens_q[i + 1] = cu_seqlens_q[i] + ql
282
283 seqused_k = torch.tensor(kv_lens, dtype=torch.int32, device=device)
284 per_seq_causal_tensor = torch.tensor(
285 per_seq_causal, dtype=torch.int32, device=device
286 )
287
288 ref_output = ref_paged_attn(
289 query,
290 key_cache,
291 value_cache,
292 query_lens,
293 kv_lens,
294 block_tables,
295 scale,
296 per_seq_causal,
297 )
298
299 output = torch.empty_like(query)
300 flash_attn_varlen_func(
301 q=query,
302 k=key_cache,
303 v=value_cache,
304 out=output,
305 cu_seqlens_q=cu_seqlens_q,
306 max_seqlen_q=max(query_lens),
307 seqused_k=seqused_k,
308 max_seqlen_k=max(kv_lens),
309 softmax_scale=scale,
310 # The kernel must be compiled causal for `dynamic_causal` to take effect.
311 causal=True,
312 block_table=block_tables,
313 softcap=0.0,
314 dynamic_causal=per_seq_causal_tensor,
315 fa_version=4,
316 )
317
318 torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)