main
py 1,494 lines 46.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import functools
5 import math
6 from types import SimpleNamespace
7
8 import pytest
9 import torch
10
11 from vllm.model_executor.kernels.linear.zentorch_utils import has_zentorch_op
12 from vllm.platforms import CpuArchEnum, current_platform
13 from vllm.utils.torch_utils import set_random_seed
14 from vllm.v1.attention.backend import AttentionType
15 from vllm.v1.attention.backends.cpu_attn import _get_attn_isa
16 from vllm.v1.attention.backends.zentorch_sdpa import (
17 should_use_zentorch_sdpa,
18 zentorch_sdpa_attn,
19 )
20
21 if not current_platform.is_cpu():
22 pytest.skip("skipping CPU-only tests", allow_module_level=True)
23
24 from vllm._custom_ops import (
25 cpu_attention_with_kv_cache,
26 cpu_attn_get_scheduler_metadata,
27 cpu_attn_reshape_and_cache,
28 )
29
30 # Enable AMX tile data registers so isolated runs (e.g. -k fp8_amx) don't rely
31 # on ref_paged_attn's einsum to trigger oneDNN's _init_amx() first.
32 if torch.cpu._is_amx_tile_supported():
33 torch.cpu._init_amx()
34
35 NUM_HEADS = [
36 (4, 4),
37 (8, 2),
38 (9, 3),
39 ]
40 HEAD_SIZES = [96, 128, 512]
41 HEAD_SIZES_VEC16 = [96, 80, 112, 128]
42 QTYPES = [torch.bfloat16, torch.half, torch.float32]
43 SLIDING_WINDOWS = [None, 256]
44 NUM_BLOCKS = [
45 1024,
46 ]
47 SEQ_LENS = [ # (q_len, kv_len)
48 [(1, 213), (1, 1), (1, 312), (1, 7), (1, 7812)], # decode batch
49 [(2345, 2345), (5, 5), (3, 16), (134, 5131)], # prefill batch
50 [(992, 2456), (1, 1234), (98, 1145), (1, 4162), (2345, 2345)], # mixed batch
51 ]
52 _FP8_ATOL = {"fp8_e4m3": 0.2, "fp8_e5m2": 0.3}
53 _FP8_RTOL = 0.1
54 ENCODER_SEQ_LENS = [
55 [1, 678, 2367, 145, 4162, 36, 7812],
56 ]
57 # Uniform (dense zentorch_sdpa call) and ragged (per-sequence loop).
58 ZEN_ENCODER_SEQ_LENS = [
59 [128, 128, 128],
60 [1, 67, 233, 5],
61 ]
62 _skip_no_zentorch_sdpa = pytest.mark.skipif(
63 not current_platform.is_zen_cpu() or not has_zentorch_op(["zentorch_sdpa_attn"]),
64 reason="zentorch_sdpa requires a Zen CPU with the op registered",
65 )
66 # should_use_zentorch_sdpa additionally gates each dtype on its ISA.
67 _skip_no_avx512_bf16 = pytest.mark.skipif(
68 not torch.cpu._is_avx512_bf16_supported(),
69 reason="zentorch_sdpa bfloat16 requires AVX512-BF16",
70 )
71 _skip_no_avx512 = pytest.mark.skipif(
72 not torch.cpu._is_avx512_supported(),
73 reason="zentorch_sdpa float32 requires AVX512",
74 )
75
76
77 def get_attn_isa(
78 block_size: int | None = None,
79 dtype: torch.dtype | None = None,
80 ):
81 # Delegate to _get_attn_isa so the fallback path applies the same arch
82 # gating (e.g. RISC-V RVV is only chosen when the build's hardcoded
83 # VLEN=128 kernel is actually present; on VLEN=256 / scalar hosts it
84 # correctly falls through to vec/vec16).
85 return _get_attn_isa(
86 dtype if dtype is not None else torch.bfloat16,
87 block_size if block_size else 32,
88 )
89
90
91 # rand number generation takes too much time, cache rand tensors
92 @functools.lru_cache(maxsize=128, typed=False)
93 def tensor_cache(elem_num: int, dtype: torch.dtype, tag: str = "none") -> torch.Tensor:
94 tensor = torch.randn(elem_num, dtype=dtype)
95
96 return tensor
97
98
99 def _get_alibi_slopes(total_num_heads: int) -> torch.Tensor:
100 closest_power_of_2 = 2 ** math.floor(math.log2(total_num_heads))
101 base = torch.tensor(
102 2 ** (-(2 ** -(math.log2(closest_power_of_2) - 3))),
103 dtype=torch.float32,
104 )
105 powers = torch.arange(1, 1 + closest_power_of_2, dtype=torch.int32)
106 slopes = torch.pow(base, powers)
107
108 if closest_power_of_2 != total_num_heads:
109 extra_base = torch.tensor(
110 2 ** (-(2 ** -(math.log2(2 * closest_power_of_2) - 3))),
111 dtype=torch.float32,
112 )
113 num_remaining_heads = min(
114 closest_power_of_2, total_num_heads - closest_power_of_2
115 )
116 extra_powers = torch.arange(
117 start=1, end=1 + 2 * num_remaining_heads, step=2, dtype=torch.int32
118 )
119 slopes = torch.cat([slopes, torch.pow(extra_base, extra_powers)], dim=0)
120 return slopes.float()
121
122
123 def ref_paged_attn(
124 query: torch.Tensor,
125 key_cache: torch.Tensor,
126 value_cache: torch.Tensor,
127 query_lens: list[int],
128 kv_lens: list[int],
129 block_tables: torch.Tensor,
130 scale: float,
131 sliding_window: int | None = None,
132 soft_cap: float | None = None,
133 alibi_slopes: torch.Tensor | None = None,
134 s_aux: torch.Tensor | None = None,
135 dynamic_causal: list[bool] | None = None,
136 ) -> torch.Tensor:
137 num_seqs = len(query_lens)
138 block_tables = block_tables.cpu().numpy()
139 _, block_size, num_kv_heads, head_size = key_cache.shape
140 dtype = query.dtype
141
142 outputs: list[torch.Tensor] = []
143 start_idx = 0
144
145 if alibi_slopes is not None:
146 alibi_slopes = alibi_slopes[:, None, None]
147
148 if s_aux is not None:
149 s_aux = s_aux.float()
150 s_aux = s_aux[:, None, None]
151
152 for i in range(num_seqs):
153 query_len = query_lens[i]
154 kv_len = kv_lens[i]
155 q = query[start_idx : start_idx + query_len].float()
156 q *= scale
157
158 num_kv_blocks = (kv_len + block_size - 1) // block_size
159 block_indices = block_tables[i, :num_kv_blocks]
160
161 k = key_cache[block_indices].view(-1, num_kv_heads, head_size)
162 k = k[:kv_len].float()
163 v = value_cache[block_indices].view(-1, num_kv_heads, head_size)
164 v = v[:kv_len].float()
165
166 if q.shape[1] != k.shape[1]:
167 k = torch.repeat_interleave(k, q.shape[1] // k.shape[1], dim=1)
168 v = torch.repeat_interleave(v, q.shape[1] // v.shape[1], dim=1)
169 attn = torch.einsum("qhd,khd->hqk", q, k).float()
170 empty_mask = torch.ones(query_len, kv_len)
171
172 if dynamic_causal is None or dynamic_causal[i]:
173 mask = torch.triu(empty_mask, diagonal=kv_len - query_len + 1).bool()
174 if sliding_window is not None:
175 sliding_window_mask = (
176 torch.triu(
177 empty_mask, diagonal=kv_len - (query_len + sliding_window) + 1
178 )
179 .bool()
180 .logical_not()
181 )
182 mask |= sliding_window_mask
183 else:
184 if sliding_window is not None:
185 mask = (
186 torch.triu(
187 empty_mask, diagonal=1 - sliding_window + kv_len - query_len
188 ).bool()
189 ^ torch.triu(
190 empty_mask, diagonal=sliding_window + kv_len - query_len
191 ).bool()
192 ).logical_not()
193 else:
194 mask = empty_mask.logical_not()
195
196 if soft_cap is not None:
197 attn = soft_cap * torch.tanh(attn / soft_cap)
198
199 if alibi_slopes is not None:
200 q_start_pos = kv_len - query_len
201 q_pos = q_start_pos + torch.arange(0, query_len)[None, :, None]
202 kv_pos = torch.arange(0, kv_len)[None, None, :]
203 dist = q_pos - kv_pos
204 alibi_bias = -alibi_slopes * dist
205 attn += alibi_bias
206
207 attn.masked_fill_(mask, float("-inf"))
208
209 if s_aux is not None:
210 s_aux_ext = s_aux.repeat(1, query_len, 1)
211 attn = torch.cat((s_aux_ext, attn), dim=-1)
212
213 attn = torch.softmax(attn, dim=-1)
214
215 if s_aux is not None:
216 attn = attn[:, :, 1:]
217
218 out = torch.einsum("hqk,khd->qhd", attn, v).to(dtype=dtype)
219
220 outputs.append(out)
221 start_idx += query_len
222
223 return torch.cat(outputs, dim=0)
224
225
226 def ref_varlen_encoder_attn(
227 query: torch.Tensor, # [token, q_head_num, head_dim]
228 key: torch.Tensor, # [token, kv_head_num, head_dim]
229 value: torch.Tensor,
230 seq_lens: list[int],
231 scale: float,
232 sliding_window: int | None = None,
233 alibi_slopes: torch.Tensor | None = None,
234 ) -> torch.Tensor:
235 num_seqs = len(seq_lens)
236 dtype = query.dtype
237
238 output = torch.empty_like(query)
239
240 start_idx = 0
241 for i in range(num_seqs):
242 seq_len = seq_lens[i]
243 q = query[start_idx : start_idx + seq_len].float()
244 k = key[start_idx : start_idx + seq_len].float()
245 v = value[start_idx : start_idx + seq_len].float()
246 q *= scale
247
248 if q.shape[1] != k.shape[1]:
249 k = torch.repeat_interleave(k, q.shape[1] // k.shape[1], dim=1)
250 v = torch.repeat_interleave(v, q.shape[1] // v.shape[1], dim=1)
251 attn = torch.einsum("qhd,khd->hqk", q, k).float()
252 empty_mask = torch.ones(seq_len, seq_len)
253 if sliding_window is not None:
254 mask = (
255 torch.triu(empty_mask, diagonal=1 - sliding_window).bool()
256 ^ torch.triu(empty_mask, diagonal=sliding_window).bool()
257 ).logical_not()
258 else:
259 mask = empty_mask.logical_not()
260
261 if alibi_slopes is not None:
262 q_pos = torch.arange(0, seq_len)[None, :, None]
263 kv_pos = torch.arange(0, seq_len)[None, None, :]
264 attn += -alibi_slopes[:, None, None].float() * (q_pos - kv_pos)
265
266 attn.masked_fill_(mask, float("-inf"))
267 attn = torch.softmax(attn, dim=-1)
268 out = torch.einsum("hqk,khd->qhd", attn, v).to(dtype=dtype)
269 output[start_idx : start_idx + seq_len].copy_(out)
270
271 start_idx += seq_len
272
273 return output
274
275
276 @torch.inference_mode()
277 def varlen_encoder_attention(
278 seq_lens: list[int],
279 num_heads: tuple[int, int],
280 head_size: int,
281 sliding_window: int | None,
282 dtype: torch.dtype,
283 block_size: int,
284 isa: str,
285 ) -> None:
286 set_random_seed(0)
287 num_seqs = len(seq_lens)
288 num_query_heads = num_heads[0]
289 num_kv_heads = num_heads[1]
290 assert num_query_heads % num_kv_heads == 0
291 scale = head_size**-0.5
292 token_num = sum(seq_lens)
293
294 seq_lens_tensor = torch.tensor(seq_lens, dtype=torch.int32)
295 query_start_loc = torch.zeros(num_seqs, dtype=torch.int32)
296 torch.cumsum(seq_lens_tensor[:-1], 0, out=query_start_loc[1:])
297 block_nums = (seq_lens_tensor + block_size - 1) // block_size
298 start_block_ids = torch.zeros_like(seq_lens_tensor)
299 torch.cumsum(block_nums[:-1], 0, out=start_block_ids[1:])
300 total_block_num: int = block_nums.sum().item()
301 max_block_num = block_nums.max().item()
302 block_offsets = torch.arange(0, max_block_num, dtype=torch.int32)
303 encoder_block_table = start_block_ids[:, None] + block_offsets[None, :]
304 slot_mapping_list = []
305 slot_start_idx = 0
306 for i in range(num_seqs):
307 block_num = block_nums[i].item()
308 seq_len = seq_lens[i]
309 slot_mapping_list.append(torch.arange(slot_start_idx, slot_start_idx + seq_len))
310 slot_start_idx += block_num * block_size
311 slot_mapping = torch.cat(slot_mapping_list)
312
313 query = tensor_cache(
314 elem_num=token_num * num_query_heads * head_size,
315 dtype=dtype,
316 tag="query",
317 )
318 query = query.view(
319 token_num,
320 num_query_heads,
321 head_size,
322 )
323
324 key_value = tensor_cache(
325 elem_num=2 * token_num * num_kv_heads * head_size,
326 dtype=dtype,
327 tag="kv",
328 )
329 key_value = key_value.view(
330 2,
331 token_num,
332 num_kv_heads,
333 head_size,
334 )
335 key, value = key_value.unbind(0)
336
337 # KV cache for CPU attention
338 packed_key_value_cache = torch.zeros(
339 total_block_num, num_kv_heads, block_size, head_size * 2, dtype=dtype
340 )
341 packed_key_value_cache = packed_key_value_cache.view(
342 (total_block_num, num_kv_heads, block_size * 2, -1)
343 )
344 packed_key_cache, packed_value_cache = packed_key_value_cache.chunk(2, dim=2)
345
346 cu_query_lens = torch.tensor([0] + seq_lens, dtype=torch.int32).cumsum(
347 dim=0, dtype=torch.int32
348 )
349 kv_lens_tensor = torch.tensor(seq_lens, dtype=torch.int32)
350
351 # use reshape_and_cache to pack key_cache and value_cache
352 cpu_attn_reshape_and_cache(
353 key=key.view(-1, num_kv_heads, head_size),
354 value=value.view(-1, num_kv_heads, head_size),
355 key_cache=packed_key_cache,
356 value_cache=packed_value_cache,
357 slot_mapping=slot_mapping,
358 isa=isa,
359 )
360
361 metadata = cpu_attn_get_scheduler_metadata(
362 num_reqs=num_seqs,
363 num_heads=num_query_heads,
364 num_kv_heads=num_kv_heads,
365 head_dim=head_size,
366 seq_lens=kv_lens_tensor,
367 dtype=dtype,
368 query_start_loc=cu_query_lens,
369 causal=False,
370 sliding_window_size=sliding_window if sliding_window is not None else -1,
371 isa=isa,
372 enable_kv_split=False,
373 )
374
375 out_without_split = torch.empty_like(query)
376 cpu_attention_with_kv_cache(
377 query=query,
378 key_cache=packed_key_cache,
379 value_cache=packed_value_cache,
380 output=out_without_split,
381 query_start_loc=cu_query_lens,
382 seq_lens=kv_lens_tensor,
383 scale=scale,
384 causal=False,
385 alibi_slopes=None,
386 sliding_window=sliding_window if sliding_window is not None else -1,
387 block_table=encoder_block_table,
388 softcap=0,
389 scheduler_metadata=metadata,
390 s_aux=None,
391 )
392
393 metadata = cpu_attn_get_scheduler_metadata(
394 num_reqs=num_seqs,
395 num_heads=num_query_heads,
396 num_kv_heads=num_kv_heads,
397 head_dim=head_size,
398 seq_lens=kv_lens_tensor,
399 dtype=dtype,
400 query_start_loc=cu_query_lens,
401 causal=False,
402 sliding_window_size=sliding_window if sliding_window is not None else -1,
403 isa=isa,
404 enable_kv_split=True,
405 )
406
407 out_with_split = torch.empty_like(query)
408 cpu_attention_with_kv_cache(
409 query=query,
410 key_cache=packed_key_cache,
411 value_cache=packed_value_cache,
412 output=out_with_split,
413 query_start_loc=cu_query_lens,
414 seq_lens=kv_lens_tensor,
415 scale=scale,
416 causal=False,
417 alibi_slopes=None,
418 sliding_window=sliding_window if sliding_window is not None else -1,
419 block_table=encoder_block_table,
420 softcap=0,
421 scheduler_metadata=metadata,
422 s_aux=None,
423 )
424
425 ref_output = ref_varlen_encoder_attn(
426 query=query,
427 key=key,
428 value=value,
429 seq_lens=seq_lens,
430 scale=scale,
431 sliding_window=sliding_window,
432 )
433 atol, rtol = 1.5e-2, 1e-2
434
435 (
436 torch.testing.assert_close(out_with_split, ref_output, atol=atol, rtol=rtol),
437 f"{torch.max(torch.abs(out_with_split - ref_output))}",
438 )
439 (
440 torch.testing.assert_close(out_without_split, ref_output, atol=atol, rtol=rtol),
441 f"{torch.max(torch.abs(out_without_split - ref_output))}",
442 )
443
444
445 @torch.inference_mode()
446 def varlen_with_paged_kv(
447 seq_lens: list[tuple[int, int]],
448 num_heads: tuple[int, int],
449 head_size: int,
450 sliding_window: int | None,
451 dtype: torch.dtype,
452 block_size: int,
453 soft_cap: float | None,
454 num_blocks: int,
455 use_alibi: bool,
456 use_sink: bool,
457 isa: str,
458 kv_cache_dtype: str = "auto",
459 k_scale: float = 1.0,
460 v_scale: float = 1.0,
461 dynamic_causal: list[bool] | None = None,
462 s_aux_dtype: torch.dtype = torch.bfloat16,
463 ) -> None:
464 set_random_seed(0)
465 num_seqs = len(seq_lens)
466 query_lens = [x[0] for x in seq_lens]
467 kv_lens = [x[1] for x in seq_lens]
468 num_query_heads = num_heads[0]
469 num_kv_heads = num_heads[1]
470 assert num_query_heads % num_kv_heads == 0
471 max_kv_len = max(kv_lens)
472 scale = head_size**-0.5
473 token_num = sum(query_lens)
474 dynamic_causal_tensor = (
475 torch.tensor(dynamic_causal, dtype=torch.bool)
476 if dynamic_causal is not None
477 else None
478 )
479
480 # for n heads the set of slopes is the geometric sequence that starts
481 # 2^(-8/n)
482 alibi_slopes = _get_alibi_slopes(num_query_heads) if use_alibi else None
483
484 s_aux = 15 * torch.rand((num_query_heads,), dtype=s_aux_dtype) if use_sink else None
485
486 is_fp8 = kv_cache_dtype != "auto"
487 if is_fp8 and current_platform.get_cpu_architecture() != CpuArchEnum.X86:
488 pytest.skip("FP8 KV cache only supported on x86")
489
490 query = tensor_cache(
491 elem_num=token_num * num_query_heads * head_size,
492 dtype=dtype,
493 )
494 query = query.view(
495 token_num,
496 num_query_heads,
497 head_size,
498 )
499
500 key_value = tensor_cache(
501 elem_num=2 * num_blocks * num_kv_heads * block_size * head_size,
502 dtype=dtype,
503 )
504 key_value = key_value.view(
505 2,
506 num_blocks,
507 block_size,
508 num_kv_heads,
509 head_size,
510 )
511 if is_fp8:
512 # Clamp KV to [-1, 1] so FP8 quantization error (<=12.5% for E4M3,
513 # <=25% for E5M2) stays within the test tolerances regardless of
514 # which tensor_cache values happen to be in use.
515 key_value = key_value.clamp(-1, 1)
516 key_cache, value_cache = key_value.unbind(0)
517
518 # KV cache for CPU attention
519 cache_dtype = torch.uint8 if is_fp8 else dtype
520 packed_key_value_cache = torch.empty(
521 num_blocks, num_kv_heads, block_size, head_size * 2, dtype=cache_dtype
522 )
523 packed_key_value_cache = packed_key_value_cache.view(
524 (num_blocks, num_kv_heads, block_size * 2, -1)
525 )
526 packed_key_cache, packed_value_cache = packed_key_value_cache.chunk(2, dim=2)
527
528 cu_query_lens = torch.tensor([0] + query_lens, dtype=torch.int32).cumsum(
529 dim=0, dtype=torch.int32
530 )
531 kv_lens_tensor = torch.tensor(kv_lens, dtype=torch.int32)
532 max_num_blocks_per_seq = (max_kv_len + block_size - 1) // block_size
533 block_tables = torch.randint(
534 0, num_blocks, (num_seqs, max_num_blocks_per_seq), dtype=torch.int32
535 )
536
537 # use reshape_and_cache to pack key_cache and value_cache
538 slot_mapping = torch.arange(0, num_blocks * block_size, dtype=torch.int64)
539 fp8_kwargs: dict = (
540 dict(k_scale=k_scale, v_scale=v_scale, kv_cache_dtype=kv_cache_dtype)
541 if is_fp8
542 else {}
543 )
544 cpu_attn_reshape_and_cache(
545 key=key_cache.view(-1, num_kv_heads, head_size),
546 value=value_cache.view(-1, num_kv_heads, head_size),
547 key_cache=packed_key_cache,
548 value_cache=packed_value_cache,
549 slot_mapping=slot_mapping,
550 isa=isa,
551 **fp8_kwargs,
552 )
553
554 metadata = cpu_attn_get_scheduler_metadata(
555 num_reqs=num_seqs,
556 num_heads=num_query_heads,
557 num_kv_heads=num_kv_heads,
558 head_dim=head_size,
559 seq_lens=kv_lens_tensor,
560 dtype=dtype,
561 query_start_loc=cu_query_lens,
562 causal=dynamic_causal is None,
563 sliding_window_size=sliding_window if sliding_window is not None else -1,
564 isa=isa,
565 enable_kv_split=False,
566 dynamic_causal=dynamic_causal_tensor,
567 kv_cache_dtype=kv_cache_dtype,
568 )
569
570 out_without_split = torch.empty_like(query)
571 if s_aux is not None and s_aux.dtype != torch.bfloat16:
572 s_aux = s_aux.to(torch.float32)
573 cpu_attention_with_kv_cache(
574 query=query,
575 key_cache=packed_key_cache,
576 value_cache=packed_value_cache,
577 output=out_without_split,
578 query_start_loc=cu_query_lens,
579 seq_lens=kv_lens_tensor,
580 scale=scale,
581 causal=dynamic_causal is None,
582 alibi_slopes=alibi_slopes,
583 sliding_window=sliding_window if sliding_window is not None else -1,
584 block_table=block_tables,
585 softcap=soft_cap if soft_cap is not None else 0,
586 scheduler_metadata=metadata,
587 s_aux=s_aux,
588 dynamic_causal=dynamic_causal_tensor,
589 **fp8_kwargs,
590 )
591
592 metadata = cpu_attn_get_scheduler_metadata(
593 num_reqs=num_seqs,
594 num_heads=num_query_heads,
595 num_kv_heads=num_kv_heads,
596 head_dim=head_size,
597 seq_lens=kv_lens_tensor,
598 dtype=dtype,
599 query_start_loc=cu_query_lens,
600 causal=dynamic_causal is None,
601 sliding_window_size=sliding_window if sliding_window is not None else -1,
602 isa=isa,
603 enable_kv_split=True,
604 dynamic_causal=dynamic_causal_tensor,
605 kv_cache_dtype=kv_cache_dtype,
606 )
607
608 out_with_split = torch.empty_like(query)
609 cpu_attention_with_kv_cache(
610 query=query,
611 key_cache=packed_key_cache,
612 value_cache=packed_value_cache,
613 output=out_with_split,
614 query_start_loc=cu_query_lens,
615 seq_lens=kv_lens_tensor,
616 scale=scale,
617 causal=dynamic_causal is None,
618 alibi_slopes=alibi_slopes,
619 sliding_window=sliding_window if sliding_window is not None else -1,
620 block_table=block_tables,
621 softcap=soft_cap if soft_cap is not None else 0,
622 scheduler_metadata=metadata,
623 s_aux=s_aux,
624 dynamic_causal=dynamic_causal_tensor,
625 **fp8_kwargs,
626 )
627
628 if is_fp8:
629 # Build a float KV cache via the non-FP8 path and run float attention
630 # to use as the reference.
631 ref_key_cache = torch.empty(
632 num_blocks, num_kv_heads, block_size, head_size, dtype=dtype
633 )
634 ref_value_cache = torch.empty_like(ref_key_cache)
635 cpu_attn_reshape_and_cache(
636 key=key_cache.view(-1, num_kv_heads, head_size),
637 value=value_cache.view(-1, num_kv_heads, head_size),
638 key_cache=ref_key_cache,
639 value_cache=ref_value_cache,
640 slot_mapping=slot_mapping,
641 isa=isa,
642 )
643 ref_metadata = cpu_attn_get_scheduler_metadata(
644 num_reqs=num_seqs,
645 num_heads=num_query_heads,
646 num_kv_heads=num_kv_heads,
647 head_dim=head_size,
648 seq_lens=kv_lens_tensor,
649 dtype=dtype,
650 query_start_loc=cu_query_lens,
651 causal=dynamic_causal is None,
652 sliding_window_size=(sliding_window if sliding_window is not None else -1),
653 isa=isa,
654 enable_kv_split=True,
655 dynamic_causal=dynamic_causal_tensor,
656 kv_cache_dtype="auto",
657 )
658 ref_output = torch.empty_like(query)
659 cpu_attention_with_kv_cache(
660 query=query,
661 key_cache=ref_key_cache,
662 value_cache=ref_value_cache,
663 output=ref_output,
664 query_start_loc=cu_query_lens,
665 seq_lens=kv_lens_tensor,
666 scale=scale,
667 causal=dynamic_causal is None,
668 alibi_slopes=alibi_slopes,
669 sliding_window=sliding_window if sliding_window is not None else -1,
670 block_table=block_tables,
671 softcap=soft_cap if soft_cap is not None else 0,
672 scheduler_metadata=ref_metadata,
673 s_aux=s_aux,
674 dynamic_causal=dynamic_causal_tensor,
675 )
676 atol = _FP8_ATOL[kv_cache_dtype]
677 rtol = _FP8_RTOL
678 else:
679 ref_output = ref_paged_attn(
680 query=query,
681 key_cache=key_cache,
682 value_cache=value_cache,
683 query_lens=query_lens,
684 kv_lens=kv_lens,
685 block_tables=block_tables,
686 scale=scale,
687 sliding_window=sliding_window,
688 soft_cap=soft_cap,
689 alibi_slopes=alibi_slopes,
690 s_aux=s_aux,
691 dynamic_causal=dynamic_causal,
692 )
693 atol, rtol = 1.5e-2, 1e-2
694
695 (
696 torch.testing.assert_close(out_with_split, ref_output, atol=atol, rtol=rtol),
697 f"{torch.max(torch.abs(out_with_split - ref_output))}",
698 )
699 (
700 torch.testing.assert_close(out_without_split, ref_output, atol=atol, rtol=rtol),
701 f"{torch.max(torch.abs(out_without_split - ref_output))}",
702 )
703
704
705 @pytest.mark.parametrize("seq_lens", ENCODER_SEQ_LENS)
706 @pytest.mark.parametrize("num_heads", NUM_HEADS)
707 @pytest.mark.parametrize("head_size", HEAD_SIZES)
708 @pytest.mark.parametrize(
709 "block_size",
710 [
711 128,
712 ],
713 )
714 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
715 @pytest.mark.parametrize("dtype", QTYPES)
716 @pytest.mark.parametrize("isa", ["vec"])
717 def test_varlen_encoder_attention_vec(
718 seq_lens: list[int],
719 num_heads: tuple[int, int],
720 head_size: int,
721 sliding_window: int | None,
722 dtype: torch.dtype,
723 block_size: int,
724 isa: str,
725 ) -> None:
726 varlen_encoder_attention(
727 seq_lens=seq_lens,
728 num_heads=num_heads,
729 head_size=head_size,
730 sliding_window=sliding_window,
731 dtype=dtype,
732 block_size=block_size,
733 isa=isa,
734 )
735
736
737 @pytest.mark.parametrize("seq_lens", ENCODER_SEQ_LENS)
738 @pytest.mark.parametrize("num_heads", NUM_HEADS)
739 @pytest.mark.parametrize("head_size", HEAD_SIZES)
740 @pytest.mark.parametrize(
741 "block_size",
742 [
743 128,
744 ],
745 )
746 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
747 @pytest.mark.parametrize("dtype", [torch.bfloat16])
748 @pytest.mark.parametrize("isa", ["neon"])
749 @pytest.mark.skipif(
750 current_platform.get_cpu_architecture() != CpuArchEnum.ARM,
751 reason="Not an Arm CPU.",
752 )
753 def test_varlen_encoder_attention_neon(
754 seq_lens: list[int],
755 num_heads: tuple[int, int],
756 head_size: int,
757 sliding_window: int | None,
758 dtype: torch.dtype,
759 block_size: int,
760 isa: str,
761 ) -> None:
762 varlen_encoder_attention(
763 seq_lens=seq_lens,
764 num_heads=num_heads,
765 head_size=head_size,
766 sliding_window=sliding_window,
767 dtype=dtype,
768 block_size=block_size,
769 isa=isa,
770 )
771
772
773 @pytest.mark.parametrize("seq_lens", ENCODER_SEQ_LENS)
774 @pytest.mark.parametrize("num_heads", NUM_HEADS)
775 @pytest.mark.parametrize("head_size", HEAD_SIZES)
776 @pytest.mark.parametrize(
777 "block_size",
778 [
779 128,
780 ],
781 )
782 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
783 @pytest.mark.parametrize("dtype", [torch.bfloat16])
784 @pytest.mark.parametrize("isa", ["amx"])
785 @pytest.mark.skipif(not torch.cpu._is_amx_tile_supported(), reason="no AMX support.")
786 def test_varlen_encoder_attention_amx(
787 seq_lens: list[int],
788 num_heads: tuple[int, int],
789 head_size: int,
790 sliding_window: int | None,
791 dtype: torch.dtype,
792 block_size: int,
793 isa: str,
794 ) -> None:
795 varlen_encoder_attention(
796 seq_lens=seq_lens,
797 num_heads=num_heads,
798 head_size=head_size,
799 sliding_window=sliding_window,
800 dtype=dtype,
801 block_size=block_size,
802 isa=isa,
803 )
804
805
806 @torch.inference_mode()
807 def varlen_encoder_zentorch_sdpa(
808 seq_lens: list[int],
809 num_heads: tuple[int, int],
810 head_size: int,
811 sliding_window: int | None,
812 dtype: torch.dtype,
813 use_alibi: bool,
814 ) -> None:
815 set_random_seed(0)
816 num_query_heads, num_kv_heads = num_heads
817 assert num_query_heads % num_kv_heads == 0
818 scale = head_size**-0.5
819 token_num = sum(seq_lens)
820
821 query = tensor_cache(
822 token_num * num_query_heads * head_size, dtype, tag="query"
823 ).view(token_num, num_query_heads, head_size)
824 key_value = tensor_cache(
825 2 * token_num * num_kv_heads * head_size, dtype, tag="kv"
826 ).view(2, token_num, num_kv_heads, head_size)
827 key, value = key_value.unbind(0)
828
829 query_start_loc = torch.zeros(len(seq_lens) + 1, dtype=torch.int32)
830 torch.cumsum(torch.tensor(seq_lens, dtype=torch.int32), 0, out=query_start_loc[1:])
831 alibi_slopes = _get_alibi_slopes(num_query_heads) if use_alibi else None
832 output = torch.empty_like(query)
833
834 zentorch_sdpa_attn(
835 query,
836 key,
837 value,
838 output,
839 SimpleNamespace(query_start_loc=query_start_loc, causal=False),
840 scale,
841 sliding_window if sliding_window is not None else -1,
842 alibi_slopes,
843 )
844 ref_output = ref_varlen_encoder_attn(
845 query=query,
846 key=key,
847 value=value,
848 seq_lens=seq_lens,
849 scale=scale,
850 sliding_window=sliding_window,
851 alibi_slopes=alibi_slopes,
852 )
853 torch.testing.assert_close(output, ref_output, atol=1.5e-2, rtol=1e-2)
854
855
856 _ZENTORCH_SDPA_DTYPES = [
857 pytest.param(torch.bfloat16, marks=_skip_no_avx512_bf16),
858 pytest.param(torch.float32, marks=_skip_no_avx512),
859 ]
860
861
862 @_skip_no_zentorch_sdpa
863 @pytest.mark.parametrize("seq_lens", ZEN_ENCODER_SEQ_LENS)
864 @pytest.mark.parametrize("num_heads", NUM_HEADS)
865 @pytest.mark.parametrize("head_size", [128])
866 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
867 @pytest.mark.parametrize("dtype", _ZENTORCH_SDPA_DTYPES)
868 @pytest.mark.parametrize("use_alibi", [False, True])
869 def test_zentorch_sdpa_attn(
870 seq_lens: list[int],
871 num_heads: tuple[int, int],
872 head_size: int,
873 sliding_window: int | None,
874 dtype: torch.dtype,
875 use_alibi: bool,
876 ) -> None:
877 varlen_encoder_zentorch_sdpa(
878 seq_lens=seq_lens,
879 num_heads=num_heads,
880 head_size=head_size,
881 sliding_window=sliding_window,
882 dtype=dtype,
883 use_alibi=use_alibi,
884 )
885
886
887 @_skip_no_zentorch_sdpa
888 @pytest.mark.parametrize(
889 "attn_type", [AttentionType.ENCODER_ONLY, AttentionType.ENCODER]
890 )
891 @pytest.mark.parametrize("dtype", _ZENTORCH_SDPA_DTYPES)
892 def test_should_use_zentorch_sdpa(attn_type: str, dtype: torch.dtype) -> None:
893 assert should_use_zentorch_sdpa(attn_type, dtype)
894
895
896 @_skip_no_zentorch_sdpa
897 @pytest.mark.parametrize(
898 "attn_type,dtype",
899 [
900 (AttentionType.DECODER, torch.bfloat16),
901 (AttentionType.ENCODER_ONLY, torch.float16),
902 ],
903 )
904 def test_should_not_use_zentorch_sdpa(attn_type: str, dtype: torch.dtype) -> None:
905 assert not should_use_zentorch_sdpa(attn_type, dtype)
906
907
908 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3", "fp8_e5m2"])
909 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
910 @pytest.mark.parametrize("num_heads", NUM_HEADS)
911 @pytest.mark.parametrize("head_size", HEAD_SIZES)
912 @pytest.mark.parametrize("block_size", [96, 128])
913 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
914 @pytest.mark.parametrize("dtype", QTYPES)
915 @pytest.mark.parametrize("soft_cap", [None])
916 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
917 @pytest.mark.parametrize("use_alibi", [False])
918 @pytest.mark.parametrize("use_sink", [False])
919 @pytest.mark.parametrize("isa", ["vec"])
920 def test_varlen_with_paged_kv_normal_vec(
921 seq_lens: list[tuple[int, int]],
922 num_heads: tuple[int, int],
923 head_size: int,
924 sliding_window: int | None,
925 dtype: torch.dtype,
926 block_size: int,
927 soft_cap: float | None,
928 num_blocks: int,
929 use_alibi: bool,
930 use_sink: bool,
931 isa: str,
932 kv_cache_dtype: str,
933 ) -> None:
934 varlen_with_paged_kv(
935 seq_lens=seq_lens,
936 num_heads=num_heads,
937 head_size=head_size,
938 sliding_window=sliding_window,
939 dtype=dtype,
940 block_size=block_size,
941 soft_cap=soft_cap,
942 num_blocks=num_blocks,
943 use_alibi=use_alibi,
944 use_sink=use_sink,
945 isa=isa,
946 kv_cache_dtype=kv_cache_dtype,
947 )
948
949
950 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3", "fp8_e5m2"])
951 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
952 @pytest.mark.parametrize("num_heads", NUM_HEADS)
953 @pytest.mark.parametrize("head_size", HEAD_SIZES)
954 @pytest.mark.parametrize("block_size", [96, 128])
955 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
956 @pytest.mark.parametrize("dtype", [torch.bfloat16])
957 @pytest.mark.parametrize("soft_cap", [None])
958 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
959 @pytest.mark.parametrize("use_alibi", [False])
960 @pytest.mark.parametrize("use_sink", [False])
961 @pytest.mark.parametrize("isa", ["amx"])
962 @pytest.mark.skipif(not torch.cpu._is_amx_tile_supported(), reason="no AMX support.")
963 def test_varlen_with_paged_kv_normal_amx(
964 seq_lens: list[tuple[int, int]],
965 num_heads: tuple[int, int],
966 head_size: int,
967 sliding_window: int | None,
968 dtype: torch.dtype,
969 block_size: int,
970 soft_cap: float | None,
971 num_blocks: int,
972 use_alibi: bool,
973 use_sink: bool,
974 isa: str,
975 kv_cache_dtype: str,
976 ) -> None:
977 varlen_with_paged_kv(
978 seq_lens=seq_lens,
979 num_heads=num_heads,
980 head_size=head_size,
981 sliding_window=sliding_window,
982 dtype=dtype,
983 block_size=block_size,
984 soft_cap=soft_cap,
985 num_blocks=num_blocks,
986 use_alibi=use_alibi,
987 use_sink=use_sink,
988 isa=isa,
989 kv_cache_dtype=kv_cache_dtype,
990 )
991
992
993 @pytest.mark.skipif(not torch.cpu._is_amx_tile_supported(), reason="no AMX support.")
994 def test_varlen_with_paged_kv_fp8_large_prefill_amx() -> None:
995 varlen_with_paged_kv(
996 seq_lens=[(1024, 1024)] * 4,
997 num_heads=(16, 2),
998 head_size=256,
999 sliding_window=None,
1000 dtype=torch.bfloat16,
1001 block_size=2176,
1002 soft_cap=None,
1003 num_blocks=4,
1004 use_alibi=False,
1005 use_sink=False,
1006 isa="amx",
1007 kv_cache_dtype="fp8_e4m3",
1008 )
1009
1010
1011 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1012 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1013 @pytest.mark.parametrize("head_size", HEAD_SIZES_VEC16)
1014 @pytest.mark.parametrize("block_size", [48])
1015 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1016 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1017 @pytest.mark.parametrize("soft_cap", [None])
1018 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1019 @pytest.mark.parametrize("use_alibi", [False])
1020 @pytest.mark.parametrize("use_sink", [False])
1021 @pytest.mark.parametrize("isa", ["vec16"])
1022 def test_varlen_with_paged_kv_normal_vec16(
1023 seq_lens: list[tuple[int, int]],
1024 num_heads: tuple[int, int],
1025 head_size: int,
1026 sliding_window: int | None,
1027 dtype: torch.dtype,
1028 block_size: int,
1029 soft_cap: float | None,
1030 num_blocks: int,
1031 use_alibi: bool,
1032 use_sink: bool,
1033 isa: str,
1034 ) -> None:
1035 varlen_with_paged_kv(
1036 seq_lens=seq_lens,
1037 num_heads=num_heads,
1038 head_size=head_size,
1039 sliding_window=sliding_window,
1040 dtype=dtype,
1041 block_size=block_size,
1042 soft_cap=soft_cap,
1043 num_blocks=num_blocks,
1044 use_alibi=use_alibi,
1045 use_sink=use_sink,
1046 isa=isa,
1047 )
1048
1049
1050 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1051 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1052 @pytest.mark.parametrize("head_size", HEAD_SIZES)
1053 @pytest.mark.parametrize("block_size", [96, 128])
1054 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1055 @pytest.mark.parametrize("dtype", QTYPES)
1056 @pytest.mark.parametrize("soft_cap", [None])
1057 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1058 @pytest.mark.parametrize("use_alibi", [False])
1059 @pytest.mark.parametrize("use_sink", [False])
1060 @pytest.mark.parametrize("isa", ["neon"])
1061 @pytest.mark.skipif(
1062 current_platform.get_cpu_architecture() != CpuArchEnum.ARM,
1063 reason="Not an Arm CPU.",
1064 )
1065 def test_varlen_with_paged_kv_normal_neon(
1066 seq_lens: list[tuple[int, int]],
1067 num_heads: tuple[int, int],
1068 head_size: int,
1069 sliding_window: int | None,
1070 dtype: torch.dtype,
1071 block_size: int,
1072 soft_cap: float | None,
1073 num_blocks: int,
1074 use_alibi: bool,
1075 use_sink: bool,
1076 isa: str,
1077 ) -> None:
1078 varlen_with_paged_kv(
1079 seq_lens=seq_lens,
1080 num_heads=num_heads,
1081 head_size=head_size,
1082 sliding_window=sliding_window,
1083 dtype=dtype,
1084 block_size=block_size,
1085 soft_cap=soft_cap,
1086 num_blocks=num_blocks,
1087 use_alibi=use_alibi,
1088 use_sink=use_sink,
1089 isa=isa,
1090 )
1091
1092
1093 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3"])
1094 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1095 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1096 @pytest.mark.parametrize("head_size", HEAD_SIZES)
1097 @pytest.mark.parametrize("block_size", [96, 128])
1098 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1099 @pytest.mark.parametrize("dtype", QTYPES)
1100 @pytest.mark.parametrize("soft_cap", [None])
1101 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1102 @pytest.mark.parametrize("use_alibi", [False])
1103 @pytest.mark.parametrize("use_sink", [False])
1104 @pytest.mark.parametrize("isa", ["rvv"])
1105 @pytest.mark.skipif(
1106 current_platform.get_cpu_architecture() != CpuArchEnum.RISCV,
1107 reason="Not a RISC-V CPU.",
1108 )
1109 def test_varlen_with_paged_kv_normal_rvv(
1110 seq_lens: list[tuple[int, int]],
1111 num_heads: tuple[int, int],
1112 head_size: int,
1113 sliding_window: int | None,
1114 dtype: torch.dtype,
1115 block_size: int,
1116 soft_cap: float | None,
1117 num_blocks: int,
1118 use_alibi: bool,
1119 use_sink: bool,
1120 isa: str,
1121 kv_cache_dtype: str,
1122 ) -> None:
1123 varlen_with_paged_kv(
1124 seq_lens=seq_lens,
1125 num_heads=num_heads,
1126 head_size=head_size,
1127 sliding_window=sliding_window,
1128 dtype=dtype,
1129 block_size=block_size,
1130 soft_cap=soft_cap,
1131 num_blocks=num_blocks,
1132 use_alibi=use_alibi,
1133 use_sink=use_sink,
1134 isa=isa,
1135 kv_cache_dtype=kv_cache_dtype,
1136 )
1137
1138
1139 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3"])
1140 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1141 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1142 @pytest.mark.parametrize("head_size", [96])
1143 @pytest.mark.parametrize("block_size", [128])
1144 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1145 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1146 @pytest.mark.parametrize("soft_cap", [50])
1147 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1148 @pytest.mark.parametrize("use_alibi", [False])
1149 @pytest.mark.parametrize("use_sink", [False])
1150 @pytest.mark.parametrize("isa", [get_attn_isa()])
1151 def test_varlen_with_paged_kv_softcap(
1152 seq_lens: list[tuple[int, int]],
1153 num_heads: tuple[int, int],
1154 head_size: int,
1155 sliding_window: int | None,
1156 dtype: torch.dtype,
1157 block_size: int,
1158 soft_cap: float | None,
1159 num_blocks: int,
1160 use_alibi: bool,
1161 use_sink: bool,
1162 isa: str,
1163 kv_cache_dtype: str,
1164 ) -> None:
1165 varlen_with_paged_kv(
1166 seq_lens=seq_lens,
1167 num_heads=num_heads,
1168 head_size=head_size,
1169 sliding_window=sliding_window,
1170 dtype=dtype,
1171 block_size=block_size,
1172 soft_cap=soft_cap,
1173 num_blocks=num_blocks,
1174 use_alibi=use_alibi,
1175 use_sink=use_sink,
1176 isa=isa,
1177 kv_cache_dtype=kv_cache_dtype,
1178 )
1179
1180
1181 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3"])
1182 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1183 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1184 @pytest.mark.parametrize("head_size", [96])
1185 @pytest.mark.parametrize("block_size", [128])
1186 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1187 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1188 @pytest.mark.parametrize("soft_cap", [None])
1189 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1190 @pytest.mark.parametrize("use_alibi", [True])
1191 @pytest.mark.parametrize("use_sink", [False])
1192 @pytest.mark.parametrize("isa", [get_attn_isa()])
1193 def test_varlen_with_paged_kv_alibi(
1194 seq_lens: list[tuple[int, int]],
1195 num_heads: tuple[int, int],
1196 head_size: int,
1197 sliding_window: int | None,
1198 dtype: torch.dtype,
1199 block_size: int,
1200 soft_cap: float | None,
1201 num_blocks: int,
1202 use_alibi: bool,
1203 use_sink: bool,
1204 isa: str,
1205 kv_cache_dtype: str,
1206 ) -> None:
1207 varlen_with_paged_kv(
1208 seq_lens=seq_lens,
1209 num_heads=num_heads,
1210 head_size=head_size,
1211 sliding_window=sliding_window,
1212 dtype=dtype,
1213 block_size=block_size,
1214 soft_cap=soft_cap,
1215 num_blocks=num_blocks,
1216 use_alibi=use_alibi,
1217 use_sink=use_sink,
1218 isa=isa,
1219 kv_cache_dtype=kv_cache_dtype,
1220 )
1221
1222
1223 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3"])
1224 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1225 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1226 @pytest.mark.parametrize("head_size", [96])
1227 @pytest.mark.parametrize("block_size", [128])
1228 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1229 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1230 @pytest.mark.parametrize("soft_cap", [None])
1231 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1232 @pytest.mark.parametrize("use_alibi", [False])
1233 @pytest.mark.parametrize("use_sink", [True])
1234 @pytest.mark.parametrize("isa", [get_attn_isa()])
1235 def test_varlen_with_paged_kv_sink(
1236 seq_lens: list[tuple[int, int]],
1237 num_heads: tuple[int, int],
1238 head_size: int,
1239 sliding_window: int | None,
1240 dtype: torch.dtype,
1241 block_size: int,
1242 soft_cap: float | None,
1243 num_blocks: int,
1244 use_alibi: bool,
1245 use_sink: bool,
1246 isa: str,
1247 kv_cache_dtype: str,
1248 ) -> None:
1249 varlen_with_paged_kv(
1250 seq_lens=seq_lens,
1251 num_heads=num_heads,
1252 head_size=head_size,
1253 sliding_window=sliding_window,
1254 dtype=dtype,
1255 block_size=block_size,
1256 soft_cap=soft_cap,
1257 num_blocks=num_blocks,
1258 use_alibi=use_alibi,
1259 use_sink=use_sink,
1260 isa=isa,
1261 kv_cache_dtype=kv_cache_dtype,
1262 )
1263
1264
1265 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8_e4m3"])
1266 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1267 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1268 @pytest.mark.parametrize("head_size", [96])
1269 @pytest.mark.parametrize("block_size", [128])
1270 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1271 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1272 @pytest.mark.parametrize("soft_cap", [None])
1273 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1274 @pytest.mark.parametrize("use_alibi", [False])
1275 @pytest.mark.parametrize("use_sink", [True])
1276 @pytest.mark.parametrize("isa", [get_attn_isa()])
1277 @pytest.mark.parametrize("s_aux_dtype", [torch.float16])
1278 def test_varlen_with_paged_kv_sink_fp16(
1279 seq_lens: list[tuple[int, int]],
1280 num_heads: tuple[int, int],
1281 head_size: int,
1282 sliding_window: int | None,
1283 dtype: torch.dtype,
1284 block_size: int,
1285 soft_cap: float | None,
1286 num_blocks: int,
1287 use_alibi: bool,
1288 use_sink: bool,
1289 isa: str,
1290 kv_cache_dtype: str,
1291 s_aux_dtype: torch.dtype,
1292 ) -> None:
1293 varlen_with_paged_kv(
1294 seq_lens=seq_lens,
1295 num_heads=num_heads,
1296 head_size=head_size,
1297 sliding_window=sliding_window,
1298 dtype=dtype,
1299 block_size=block_size,
1300 soft_cap=soft_cap,
1301 num_blocks=num_blocks,
1302 use_alibi=use_alibi,
1303 use_sink=use_sink,
1304 isa=isa,
1305 kv_cache_dtype=kv_cache_dtype,
1306 s_aux_dtype=s_aux_dtype,
1307 )
1308
1309
1310 @pytest.mark.parametrize(
1311 "kv_cache_dtype",
1312 [
1313 "auto",
1314 ],
1315 )
1316 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1317 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1318 @pytest.mark.parametrize(
1319 "head_size",
1320 [
1321 128,
1322 ],
1323 )
1324 @pytest.mark.parametrize("block_size", [96, 128])
1325 @pytest.mark.parametrize("sliding_window", SLIDING_WINDOWS)
1326 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1327 @pytest.mark.parametrize("soft_cap", [None])
1328 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1329 @pytest.mark.parametrize("use_alibi", [False])
1330 @pytest.mark.parametrize("use_sink", [False])
1331 @pytest.mark.parametrize("isa", ["amx"])
1332 @pytest.mark.skipif(not torch.cpu._is_amx_tile_supported(), reason="no AMX support.")
1333 def test_varlen_with_paged_kv_dynamic_causal(
1334 seq_lens: list[tuple[int, int]],
1335 num_heads: tuple[int, int],
1336 head_size: int,
1337 sliding_window: int | None,
1338 dtype: torch.dtype,
1339 block_size: int,
1340 soft_cap: float | None,
1341 num_blocks: int,
1342 use_alibi: bool,
1343 use_sink: bool,
1344 isa: str,
1345 kv_cache_dtype: str,
1346 ) -> None:
1347 dynamic_causal = [bool(i % 2) for i in range(len(seq_lens))]
1348 varlen_with_paged_kv(
1349 seq_lens=seq_lens,
1350 num_heads=num_heads,
1351 head_size=head_size,
1352 sliding_window=sliding_window,
1353 dtype=dtype,
1354 block_size=block_size,
1355 soft_cap=soft_cap,
1356 num_blocks=num_blocks,
1357 use_alibi=use_alibi,
1358 use_sink=use_sink,
1359 isa=isa,
1360 kv_cache_dtype=kv_cache_dtype,
1361 dynamic_causal=dynamic_causal,
1362 )
1363
1364
1365 # ---------------------------------------------------------------------------
1366 # AMX_FP8 (Diamond Rapids) tests
1367 # ---------------------------------------------------------------------------
1368
1369
1370 def _amx_fp8_available() -> bool:
1371 """Return True iff the runtime reports AMX_FP8 capability."""
1372 return torch.cpu._is_amx_tile_supported() and torch.ops._C.cpu_attn_has_isa(
1373 "amx_fp8"
1374 )
1375
1376
1377 @pytest.mark.parametrize("kv_cache_dtype", ["fp8_e4m3", "fp8_e5m2"])
1378 @pytest.mark.parametrize("seq_lens", SEQ_LENS)
1379 @pytest.mark.parametrize("num_heads", NUM_HEADS)
1380 @pytest.mark.parametrize("head_size", [64, 128, 256])
1381 @pytest.mark.parametrize("block_size", [32, 64])
1382 @pytest.mark.parametrize("sliding_window", [None])
1383 @pytest.mark.parametrize("dtype", [torch.bfloat16])
1384 @pytest.mark.parametrize("soft_cap", [None])
1385 @pytest.mark.parametrize("num_blocks", NUM_BLOCKS)
1386 @pytest.mark.parametrize("use_alibi", [False])
1387 @pytest.mark.parametrize("use_sink", [False])
1388 @pytest.mark.parametrize("isa", ["amx_fp8"])
1389 @pytest.mark.skipif(
1390 not _amx_fp8_available(), reason="no AMX_FP8 support (requires Diamond Rapids)."
1391 )
1392 def test_varlen_with_paged_kv_amx_fp8(
1393 seq_lens: list[tuple[int, int]],
1394 num_heads: tuple[int, int],
1395 head_size: int,
1396 sliding_window: int | None,
1397 dtype: torch.dtype,
1398 block_size: int,
1399 soft_cap: float | None,
1400 num_blocks: int,
1401 use_alibi: bool,
1402 use_sink: bool,
1403 isa: str,
1404 kv_cache_dtype: str,
1405 ) -> None:
1406 """Test AMX_FP8 native FP8×FP8 attention (Diamond Rapids).
1407
1408 Verifies that:
1409 - QK uses _tile_dpfp8ps (native FP8 MMA, no K dequant).
1410 - PV uses native FP8 MMA for both E4M3 and E5M2.
1411 - Output cosine similarity vs fp32 reference > 0.99.
1412 """
1413 if block_size % 64 != 0:
1414 pytest.skip("native FP8 PV requires block_size divisible by 64")
1415
1416 varlen_with_paged_kv(
1417 seq_lens=seq_lens,
1418 num_heads=num_heads,
1419 head_size=head_size,
1420 sliding_window=sliding_window,
1421 dtype=dtype,
1422 block_size=block_size,
1423 soft_cap=soft_cap,
1424 num_blocks=num_blocks,
1425 use_alibi=use_alibi,
1426 use_sink=use_sink,
1427 isa=isa,
1428 kv_cache_dtype=kv_cache_dtype,
1429 )
1430
1431
1432 @pytest.mark.skipif(
1433 not _amx_fp8_available(), reason="no AMX_FP8 support (requires Diamond Rapids)."
1434 )
1435 def test_amx_fp8_qk_covers_full_64_token_group() -> None:
1436 head_size = 64
1437 block_size = 64
1438 query = torch.ones((1, 1, head_size), dtype=torch.bfloat16)
1439 key = torch.empty((block_size, 1, head_size), dtype=torch.bfloat16)
1440 value = torch.empty_like(key)
1441 key[:32].fill_(-1)
1442 key[32:].fill_(1)
1443 value[:32].fill_(-1)
1444 value[32:].fill_(1)
1445
1446 key_cache = torch.empty((1, 1, block_size, head_size), dtype=torch.uint8)
1447 value_cache = torch.empty_like(key_cache)
1448 slot_mapping = torch.arange(block_size, dtype=torch.int64)
1449 cpu_attn_reshape_and_cache(
1450 key=key,
1451 value=value,
1452 key_cache=key_cache,
1453 value_cache=value_cache,
1454 slot_mapping=slot_mapping,
1455 isa="amx_fp8",
1456 kv_cache_dtype="fp8_e4m3",
1457 )
1458
1459 query_start_loc = torch.tensor([0, 1], dtype=torch.int32)
1460 seq_lens = torch.tensor([block_size], dtype=torch.int32)
1461 metadata = cpu_attn_get_scheduler_metadata(
1462 num_reqs=1,
1463 num_heads=1,
1464 num_kv_heads=1,
1465 head_dim=head_size,
1466 seq_lens=seq_lens,
1467 dtype=torch.bfloat16,
1468 query_start_loc=query_start_loc,
1469 causal=True,
1470 sliding_window_size=-1,
1471 isa="amx_fp8",
1472 enable_kv_split=False,
1473 kv_cache_dtype="fp8_e4m3",
1474 )
1475 output = torch.empty_like(query)
1476 cpu_attention_with_kv_cache(
1477 query=query,
1478 key_cache=key_cache,
1479 value_cache=value_cache,
1480 output=output,
1481 query_start_loc=query_start_loc,
1482 seq_lens=seq_lens,
1483 scale=head_size**-0.5,
1484 causal=True,
1485 alibi_slopes=None,
1486 sliding_window=-1,
1487 block_table=torch.tensor([[0]], dtype=torch.int32),
1488 softcap=0,
1489 scheduler_metadata=metadata,
1490 s_aux=None,
1491 kv_cache_dtype="fp8_e4m3",
1492 )
1493
1494 torch.testing.assert_close(output, torch.ones_like(output), atol=0.05, rtol=0)