main
py 1,975 lines 70.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for v1 MLA backends without GPUModelRunner dependency.
4
5 Known Issues:
6 - FLASH_ATTN_MLA backend occasionally produces NaN values in
7 test_backend_correctness[mixed_small] when run after
8 test_backend_correctness[small_prefill], but passes when run alone.
9 """
10
11 import sys
12 from types import MethodType, SimpleNamespace
13
14 import pytest
15 import torch
16
17 from tests.v1.attention.utils import (
18 BatchSpec,
19 create_common_attn_metadata,
20 create_vllm_config,
21 try_get_attention_backend,
22 )
23 from vllm import _custom_ops as ops
24 from vllm.config.vllm import set_current_vllm_config
25 from vllm.model_executor.layers.attention import mla_attention as mla_attention_module
26 from vllm.model_executor.layers.attention.mla_attention import (
27 MLAAttention,
28 MLACommonBaseImpl,
29 QueryLenSupport,
30 _DecodeConcatQuantFP8,
31 _use_masked_mha,
32 build_mla_chunked_context_metadata,
33 )
34 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
35 from vllm.platforms import current_platform
36 from vllm.utils.math_utils import cdiv
37 from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE
38 from vllm.v1.attention.backend import CommonAttentionMetadata
39 from vllm.v1.attention.backends.fa_utils import flash_attn_supports_mla
40 from vllm.v1.attention.backends.mla import flashmla as flashmla_module
41 from vllm.v1.attention.backends.mla import tokenspeed_mla as tokenspeed_mla_module
42 from vllm.v1.attention.backends.mla.prefill import (
43 MLAPrefillBackendEnum,
44 get_mla_prefill_backend,
45 )
46 from vllm.v1.attention.backends.mla.prefill.base import MLADimensions
47 from vllm.v1.attention.backends.mla.prefill.selector import (
48 MLAPrefillSelectorConfig,
49 )
50 from vllm.v1.attention.backends.registry import AttentionBackendEnum
51 from vllm.v1.attention.ops.flashmla import is_flashmla_dense_supported
52 from vllm.v1.kv_cache_interface import (
53 KVQuantMode,
54 MLAAttentionSpec,
55 )
56 from vllm.v1.worker.block_table import get_block_table_width
57
58 BACKENDS_TO_TEST = [
59 AttentionBackendEnum.CUTLASS_MLA,
60 AttentionBackendEnum.FLASHMLA,
61 AttentionBackendEnum.FLASH_ATTN_MLA,
62 AttentionBackendEnum.FLASHINFER_MLA,
63 AttentionBackendEnum.TRITON_MLA,
64 AttentionBackendEnum.TOKENSPEED_MLA,
65 ]
66
67 if current_platform.is_rocm():
68 BACKENDS_TO_TEST.append(AttentionBackendEnum.ROCM_AITER_MLA)
69
70 DEVICE_TYPE = current_platform.device_type
71
72
73 @pytest.mark.parametrize(
74 ("tensor_parallel_size", "query_len", "expected"),
75 [
76 (1, 8192, True),
77 (1, 9216, False),
78 (2, 20480, True),
79 (2, 24576, False),
80 (4, 48 * 1024, True),
81 (4, 56 * 1024, False),
82 (8, 112 * 1024, True),
83 (8, 128 * 1024, False),
84 ],
85 )
86 def test_glm5_masked_mha_pure_prefill_routing(
87 tensor_parallel_size, query_len, expected
88 ):
89 assert (
90 _use_masked_mha(
91 backend_name="FLASHMLA_SPARSE",
92 tensor_parallel_size=tensor_parallel_size,
93 qk_head_dim=256,
94 v_head_dim=256,
95 query_len=query_len,
96 seq_len=query_len,
97 has_context=False,
98 )
99 is expected
100 )
101
102
103 @pytest.mark.parametrize(
104 ("tensor_parallel_size", "query_len", "seq_len", "expected"),
105 [
106 (1, 8192, 8192, False),
107 (2, 4096, 6144, True),
108 (2, 4096, 8192, False),
109 (2, 8192, 10240, True),
110 (4, 16384, 24576, True),
111 (4, 16384, 32768, False),
112 (4, 32768, 34 * 1024, True),
113 (4, 32768, 36 * 1024, False),
114 (8, 32768, 49152, True),
115 (8, 32768, 65536, False),
116 ],
117 )
118 def test_glm5_masked_mha_context_routing(
119 tensor_parallel_size, query_len, seq_len, expected
120 ):
121 assert (
122 _use_masked_mha(
123 backend_name="FLASHMLA_SPARSE",
124 tensor_parallel_size=tensor_parallel_size,
125 qk_head_dim=256,
126 v_head_dim=256,
127 query_len=query_len,
128 seq_len=seq_len,
129 has_context=True,
130 )
131 is expected
132 )
133
134
135 @pytest.mark.parametrize(
136 ("tensor_parallel_size", "query_len", "seq_len", "has_context", "expected"),
137 [
138 (4, 36 * 1024, 36 * 1024, False, True),
139 (4, 40 * 1024, 40 * 1024, False, False),
140 (4, 4 * 1024, 12 * 1024, True, True),
141 (4, 4 * 1024, 16 * 1024, True, False),
142 (4, 24 * 1024, 28 * 1024, True, True),
143 (4, 32 * 1024, 36 * 1024, True, False),
144 (8, 64 * 1024, 64 * 1024, False, True),
145 (8, 68 * 1024, 68 * 1024, False, False),
146 (8, 4 * 1024, 20 * 1024, True, True),
147 (8, 16 * 1024, 32 * 1024, True, True),
148 (8, 48 * 1024, 64 * 1024, True, True),
149 (8, 56 * 1024, 72 * 1024, True, True),
150 (8, 63 * 1024, 79 * 1024, True, False),
151 ],
152 )
153 def test_glm5_flashinfer_masked_mha_routing(
154 tensor_parallel_size, query_len, seq_len, has_context, expected
155 ):
156 assert (
157 _use_masked_mha(
158 backend_name="FLASHINFER_MLA_SPARSE",
159 tensor_parallel_size=tensor_parallel_size,
160 qk_head_dim=256,
161 v_head_dim=256,
162 query_len=query_len,
163 seq_len=seq_len,
164 has_context=has_context,
165 )
166 is expected
167 )
168
169
170 @pytest.mark.parametrize("qk_rope_head_dim", [64, 0], ids=["rope", "nope"])
171 def test_concat_k_nope_k_pe_matches_torch_cat(qk_rope_head_dim):
172 """The K concat used by the MLA prefill context loop must equal torch.cat of
173 k_nope with the broadcast k_pe; with no RoPE part it returns k_nope itself
174 instead of allocating and copying."""
175 torch.manual_seed(0)
176 num_tokens, num_heads, qk_nope_head_dim = 5, 4, 256
177 k_nope = torch.randn(num_tokens, num_heads, qk_nope_head_dim, dtype=torch.bfloat16)
178 k_pe = torch.randn(num_tokens, 1, qk_rope_head_dim, dtype=torch.bfloat16)
179 impl = SimpleNamespace(_use_flashinfer_concat_mla_k=False)
180
181 k = MLACommonBaseImpl._concat_k_nope_k_pe(impl, k_nope, k_pe)
182
183 expected = torch.cat([k_nope, k_pe.expand(-1, num_heads, -1)], dim=-1)
184 assert k.shape == (num_tokens, num_heads, qk_nope_head_dim + qk_rope_head_dim)
185 torch.testing.assert_close(k, expected, rtol=0, atol=0)
186 assert (k.data_ptr() == k_nope.data_ptr()) == (qk_rope_head_dim == 0)
187
188
189 def test_masked_mha_routing_is_dimension_specific():
190 assert _use_masked_mha(
191 backend_name="FLASHMLA_SPARSE",
192 tensor_parallel_size=1,
193 qk_head_dim=192,
194 v_head_dim=128,
195 query_len=1536,
196 seq_len=2048,
197 has_context=True,
198 )
199 assert not _use_masked_mha(
200 backend_name="FLASHMLA_SPARSE",
201 tensor_parallel_size=1,
202 qk_head_dim=128,
203 v_head_dim=128,
204 query_len=1536,
205 seq_len=2048,
206 has_context=True,
207 )
208
209
210 @pytest.mark.parametrize(
211 ("cache_dtype", "expected_quant_mode"),
212 [
213 ("auto", KVQuantMode.NONE),
214 ("fp8_ds_mla", KVQuantMode.FP8_PER_TENSOR),
215 ],
216 )
217 def test_mla_kv_cache_spec_uses_layer_cache_dtype(
218 cache_dtype: str, expected_quant_mode: KVQuantMode
219 ):
220 layer = SimpleNamespace(
221 attn_backend=flashmla_module.FlashMLABackend,
222 kv_cache_dtype=cache_dtype,
223 head_size=576,
224 indexer=None,
225 non_causal_multi_token_decode=False,
226 sliding_window=None,
227 )
228 layer._uses_flat_kv_cache = MethodType(MLAAttention._uses_flat_kv_cache, layer)
229 vllm_config = SimpleNamespace(
230 cache_config=SimpleNamespace(block_size=64), model_config=None
231 )
232
233 spec = MLAAttention.get_kv_cache_spec(layer, vllm_config)
234
235 assert isinstance(spec, MLAAttentionSpec)
236 assert spec.cache_dtype_str == cache_dtype
237 assert spec.kv_quant_mode == expected_quant_mode
238 if cache_dtype == "fp8_ds_mla":
239 assert spec.page_size_bytes == 64 * 656
240
241
242 @pytest.mark.cpu_test
243 def test_dcp_chunked_context_accepts_non_virtual_block_aligned_prefix():
244 context_lens_cpu = torch.tensor([65, 96], dtype=torch.int32)
245 prefill_query_start_loc_cpu = torch.tensor([0, 1, 2], dtype=torch.int32)
246
247 metadata = build_mla_chunked_context_metadata(
248 context_lens_cpu=context_lens_cpu,
249 prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
250 chunked_prefill_workspace=torch.empty(0),
251 chunked_prefill_workspace_size=128,
252 block_size=64,
253 align_chunk_to_block=True,
254 device=torch.device("cpu"),
255 dcp_world_size=4,
256 dcp_local_block_size=1,
257 dcp_virtual_block_size=4,
258 )
259
260 assert metadata is not None
261 assert metadata.context_lens.tolist() == [65, 96]
262 assert [chunk.seq_lens.tolist() for chunk in metadata.chunks] == [[65], [96]]
263 assert [chunk.starts.tolist() for chunk in metadata.chunks] == [[0], [0]]
264 assert [chunk.local_context_lens_allranks for chunk in metadata.chunks] == [
265 [[17, 16, 16, 16]],
266 [[24, 24, 24, 24]],
267 ]
268 assert [chunk.padded_local_seq_lens for chunk in metadata.chunks] == [
269 [17],
270 [24],
271 ]
272 assert [chunk.padded_local_cu_seq_lens.tolist() for chunk in metadata.chunks] == [
273 [0, 17],
274 [0, 24],
275 ]
276 assert [chunk.cu_seq_lens.tolist() for chunk in metadata.chunks] == [
277 [0, 65],
278 [0, 96],
279 ]
280 assert [chunk.num_context_tokens for chunk in metadata.chunks] == [65, 96]
281 assert [chunk.num_local_context_tokens for chunk in metadata.chunks] == [17, 24]
282
283
284 # Remove sm100 backends from the list if not using sm100
285 if not torch.cuda.is_available() or torch.cuda.get_device_properties(0).major < 10:
286 BACKENDS_TO_TEST.remove(AttentionBackendEnum.CUTLASS_MLA)
287 BACKENDS_TO_TEST.remove(AttentionBackendEnum.FLASHINFER_MLA)
288 BACKENDS_TO_TEST.remove(AttentionBackendEnum.TOKENSPEED_MLA)
289
290 # Remove FLASH_ATTN_MLA from the list if not supported
291 if not flash_attn_supports_mla():
292 BACKENDS_TO_TEST.remove(AttentionBackendEnum.FLASH_ATTN_MLA)
293
294 # Remove FLASHMLA from the list if not supported
295 if not is_flashmla_dense_supported()[0]:
296 BACKENDS_TO_TEST.remove(AttentionBackendEnum.FLASHMLA)
297
298 # Remove TOKENSPEED_MLA if the optional package is not installed
299 if AttentionBackendEnum.TOKENSPEED_MLA in BACKENDS_TO_TEST:
300 try:
301 import tokenspeed_mla # noqa: F401
302 except ImportError:
303 BACKENDS_TO_TEST.remove(AttentionBackendEnum.TOKENSPEED_MLA)
304
305
306 def test_mla_post_load_preserves_runtime_weight_addresses(monkeypatch):
307 layer = MLAAttention.__new__(MLAAttention)
308 torch.nn.Module.__init__(layer)
309 layer.kv_lora_rank = 2
310 layer.num_heads = 2
311 layer.qk_nope_head_dim = 3
312 layer.v_head_dim = 4
313 layer.kv_b_proj = torch.nn.Module()
314 layer.kv_b_proj.weight = torch.nn.Parameter(
315 torch.arange(28.0, dtype=torch.float16).reshape(14, 2)
316 )
317 layer.kv_b_proj.quant_method = None
318 layer.is_aiter_triton_fp4_bmm_enabled = False
319 layer.is_aiter_triton_fp8_bmm_enabled = False
320 layer.is_amx_bmm_enabled = False
321 layer.dcp_q_replicate = False
322 layer.quant_config = None
323 layer.layer_name = "test"
324 layer.impl = SimpleNamespace(process_weights_after_loading=lambda act_dtype: None)
325
326 monkeypatch.setattr(
327 mla_attention_module, "set_default_quant_scales", lambda *_, **__: None
328 )
329
330 with torch.no_grad():
331 layer.process_weights_after_loading(torch.float32)
332 assert isinstance(layer.W_UV, torch.nn.Parameter)
333 assert isinstance(layer.W_UK_T, torch.nn.Parameter)
334 w_uv_ptr = layer.W_UV.data_ptr()
335 w_uk_t_ptr = layer.W_UK_T.data_ptr()
336 old_w_uv = layer.W_UV.clone()
337 old_w_uk_t = layer.W_UK_T.clone()
338
339 layer.kv_b_proj.weight.add_(100)
340 layer.process_weights_after_loading(torch.float32)
341
342 assert layer.W_UV.data_ptr() == w_uv_ptr
343 assert layer.W_UK_T.data_ptr() == w_uk_t_ptr
344 torch.testing.assert_close(layer.W_UV, old_w_uv + 100)
345 torch.testing.assert_close(layer.W_UK_T, old_w_uk_t + 100)
346
347
348 # Validate parameter combinations during collection, before GPU fixtures run.
349 PREFILL_BACKENDS_TO_TEST: list[MLAPrefillBackendEnum] = []
350 if current_platform.is_cuda():
351 PREFILL_BACKENDS_TO_TEST.extend(
352 [
353 MLAPrefillBackendEnum.FLASH_ATTN,
354 MLAPrefillBackendEnum.FLASHINFER,
355 MLAPrefillBackendEnum.TRTLLM_RAGGED,
356 MLAPrefillBackendEnum.TOKENSPEED_MLA,
357 ]
358 )
359 elif current_platform.is_rocm():
360 PREFILL_BACKENDS_TO_TEST.append(MLAPrefillBackendEnum.ROCM_AITER_FA)
361
362 MLA_DIMENSIONS_TO_TEST = [
363 ("deepseek", 128, 128),
364 ("glm", 192, 256),
365 ]
366
367
368 def _prefill_backend_dimension_params():
369 device_capability = current_platform.get_device_capability()
370 params = []
371 for prefill_backend in PREFILL_BACKENDS_TO_TEST:
372 for dimensions_id, qk_nope_head_dim, v_head_dim in MLA_DIMENSIONS_TO_TEST:
373 if device_capability is None:
374 invalid_reasons = ["device capability unavailable"]
375 else:
376 try:
377 invalid_reasons = (
378 prefill_backend.get_class().validate_configuration(
379 device_capability,
380 MLAPrefillSelectorConfig(
381 dtype=torch.bfloat16,
382 mla_dimensions=MLADimensions(
383 qk_nope_head_dim=qk_nope_head_dim,
384 qk_rope_head_dim=64,
385 v_head_dim=v_head_dim,
386 ),
387 ),
388 )
389 )
390 except ImportError:
391 invalid_reasons = ["ImportError"]
392
393 marks = []
394 if invalid_reasons:
395 marks.append(
396 pytest.mark.skip(
397 reason=(
398 f"Prefill backend {prefill_backend.name} unavailable: "
399 f"{invalid_reasons}"
400 )
401 )
402 )
403 params.append(
404 pytest.param(
405 prefill_backend,
406 qk_nope_head_dim,
407 v_head_dim,
408 id=f"{dimensions_id}-{prefill_backend}",
409 marks=marks,
410 )
411 )
412 return params
413
414
415 SPEC_DECODE_BACKENDS = []
416 for backend in BACKENDS_TO_TEST:
417 builder_cls, _ = try_get_attention_backend(backend)
418 query_len_support = getattr(
419 builder_cls, "query_len_support", QueryLenSupport.SINGLE_ONLY
420 )
421 if query_len_support != QueryLenSupport.SINGLE_ONLY:
422 SPEC_DECODE_BACKENDS.append(backend)
423
424 BACKEND_BLOCK_SIZES = {}
425 for backend in BACKENDS_TO_TEST:
426 supported_sizes = backend.get_class().get_supported_kernel_block_sizes()
427 if supported_sizes:
428 default_size = supported_sizes[0]
429 block_size = (
430 default_size if isinstance(default_size, int) else default_size.base
431 )
432 else:
433 block_size = 16
434 BACKEND_BLOCK_SIZES[backend] = block_size
435
436 torch.manual_seed(42)
437
438
439 def _convert_dtype_to_torch(dtype):
440 """Convert ModelDType to torch.dtype."""
441 if isinstance(dtype, str):
442 if dtype == "auto":
443 return torch.float16 # Default dtype for testing
444 elif dtype in STR_DTYPE_TO_TORCH_DTYPE:
445 return STR_DTYPE_TO_TORCH_DTYPE[dtype]
446 else:
447 raise ValueError(f"Unknown dtype: {dtype}")
448 elif isinstance(dtype, torch.dtype):
449 return dtype
450 else:
451 raise ValueError(f"Unknown dtype: {dtype}")
452
453
454 # Define common batch configurations
455 BATCH_SPECS = {
456 "small_decode": BatchSpec(seq_lens=[32, 40], query_lens=[1, 1]),
457 "small_prefill": BatchSpec(seq_lens=[32, 40], query_lens=[8, 8]),
458 "mixed_small": BatchSpec(seq_lens=[32, 40, 48, 56], query_lens=[1, 1, 5, 5]),
459 "medium_decode": BatchSpec(
460 seq_lens=[128, 256, 512, 1024, 128, 256, 512, 1024],
461 query_lens=[1, 1, 1, 1, 1, 1, 1, 1],
462 ),
463 "medium_prefill": BatchSpec(
464 seq_lens=[256, 512, 1024, 2048], query_lens=[16, 16, 16, 16]
465 ),
466 "mixed_medium": BatchSpec(
467 seq_lens=[512, 1024, 2048, 512, 1024, 2048], query_lens=[1, 1, 1, 7, 7, 7]
468 ),
469 "large_decode": BatchSpec(seq_lens=[2048] * 32, query_lens=[1] * 32),
470 "large_prefill": BatchSpec(seq_lens=[4096] * 8, query_lens=[32] * 8),
471 "single_decode": BatchSpec(seq_lens=[1024], query_lens=[1]),
472 "single_prefill": BatchSpec(seq_lens=[1024], query_lens=[64]),
473 "spec_decode_small": BatchSpec(
474 seq_lens=[128, 256, 512, 1024], query_lens=[4, 4, 4, 4]
475 ),
476 "spec_decode_medium": BatchSpec(
477 seq_lens=[512, 1024, 2048, 512, 1024, 2048], query_lens=[8, 8, 8, 8, 8, 8]
478 ),
479 "chunked_context_prefill": BatchSpec(
480 seq_lens=[1568, 520, 8, 80, 96, 8],
481 query_lens=[32, 8, 8, 16, 16, 8],
482 ),
483 }
484
485
486 def create_and_prepopulate_kv_cache(
487 kv_c_contexts: list[torch.Tensor],
488 k_pe_contexts: list[torch.Tensor],
489 block_size: int,
490 head_size: int,
491 dtype: torch.dtype,
492 device: torch.device,
493 num_blocks: int,
494 common_attn_metadata: CommonAttentionMetadata,
495 randomize_blocks: bool = True,
496 kv_cache_dtype: str | None = None,
497 scale: float | torch.Tensor = 1.0,
498 ) -> torch.Tensor:
499 """Create and prepopulate an MLA KV cache with context data.
500
501 Args:
502 kv_c_contexts: List of latent KV context tensors for each sequence
503 k_pe_contexts: List of key positional embedding context tensors
504 for each sequence
505 block_size: Size of each block
506 head_size: Size of each head (latent dimension)
507 dtype: Data type for the cache
508 device: Device to create the cache on
509 num_blocks: Total number of blocks in the cache
510 common_attn_metadata: Common attention metadata
511 randomize_blocks: Whether to randomly permute blocks
512 or use sequential order
513 kv_cache_dtype: Optional kv cache dtype string. For fp8 cache dtype,
514 the cache is populated via concat_and_cache_mla.
515 scale: Scaling factor forwarded to concat_and_cache_mla when the
516 fp8 cache layout is requested.
517
518 Returns:
519 MLA KV cache tensor
520
521 """
522 batch_size = len(kv_c_contexts)
523 seq_lens = common_attn_metadata.seq_lens.cpu()
524 query_lens = (
525 common_attn_metadata.query_start_loc_cpu[1:]
526 - common_attn_metadata.query_start_loc_cpu[:-1]
527 )
528 context_lens = seq_lens - query_lens
529 block_table = common_attn_metadata.block_table_tensor
530 slot_mapping = common_attn_metadata.slot_mapping
531
532 use_nvfp4_ds_mla = kv_cache_dtype == "nvfp4_ds_mla"
533 fp8_attention = (
534 bool(kv_cache_dtype and kv_cache_dtype.startswith("fp8")) or use_nvfp4_ds_mla
535 )
536 use_fp8_ds_mla = kv_cache_dtype == "fp8_ds_mla"
537
538 if fp8_attention:
539 if use_fp8_ds_mla:
540 kv_lora_rank = kv_c_contexts[0].shape[-1]
541 rope_dim = k_pe_contexts[0].shape[-1]
542 # 4 * 4: 4 float32 scale values for 128-element tiles
543 # 2 * rope_dim: 16-bit RoPE values
544 kv_entry_size = kv_lora_rank + 4 * 4 + 2 * rope_dim
545 elif use_nvfp4_ds_mla:
546 kv_lora_rank = kv_c_contexts[0].shape[-1]
547 rope_dim = k_pe_contexts[0].shape[-1]
548 # e2m1-packed NoPE + unscaled e4m3 rope + per-16 e4m3 NoPE SFs,
549 # padded to 16B
550 kv_entry_size = (
551 cdiv(kv_lora_rank // 2 + rope_dim + kv_lora_rank // 16, 16) * 16
552 )
553 else:
554 kv_entry_size = head_size
555
556 kv_cache = torch.zeros(
557 num_blocks, 1, block_size, kv_entry_size, dtype=torch.uint8, device=device
558 )
559 scale_tensor = (
560 scale
561 if isinstance(scale, torch.Tensor)
562 else torch.tensor(scale, dtype=torch.float32, device=device)
563 )
564 scale_tensor = scale_tensor.to(device=device, dtype=torch.float32)
565 else:
566 # Create MLA KV cache: (num_blocks, num_heads=1, block_size, head_size)
567 kv_cache = torch.zeros(
568 num_blocks, 1, block_size, head_size, dtype=dtype, device=device
569 )
570 kv_cache_flat = kv_cache.view(-1, head_size)
571
572 # Populate the cache with the context tokens
573 # Start from block_id=1 since block_id=0 is considered the null block
574 start_block_idx = 1
575 for i in range(batch_size):
576 kv_c_context, k_pe_context = kv_c_contexts[i], k_pe_contexts[i]
577 context_len = kv_c_context.shape[0]
578 if context_len == 0:
579 start_block_idx += cdiv(int(seq_lens[i]), block_size)
580 continue
581
582 start = start_block_idx * block_size
583
584 if fp8_attention:
585 slots = torch.arange(context_len, device=device, dtype=torch.long) + start
586 ops.concat_and_cache_mla(
587 kv_c_context,
588 k_pe_context.squeeze(1),
589 kv_cache.squeeze(1),
590 slots,
591 kv_cache_dtype=kv_cache_dtype,
592 scale=scale_tensor,
593 )
594 else:
595 kv_context = torch.cat([kv_c_context, k_pe_context.squeeze(1)], dim=-1)
596 end = start + kv_context.shape[0]
597 kv_cache_flat[start:end, ...] = kv_context
598
599 # Stay block aligned and allocate enough blocks for the new tokens
600 start_block_idx += cdiv(int(seq_lens[i]), block_size)
601
602 blocks_end = start_block_idx
603
604 # Permute the context blocks (excluding block 0 which is null)
605 if randomize_blocks:
606 perm = (
607 torch.randperm(blocks_end - 1) + 1
608 ) # Random permutation starting from block 1
609 else:
610 perm = torch.arange(1, blocks_end) # Sequential order starting from block 1
611
612 inv_perm = torch.zeros(blocks_end, dtype=torch.long, device=device)
613 inv_perm[1:] = torch.argsort(perm) + 1 # Add 1 to account for starting from block 1
614 kv_cache[1:blocks_end, ...] = kv_cache[perm, ...]
615
616 # Construct the right block table
617 # Start from block_id=1 since block_id=0 is considered the null block
618 start_block_idx = 1
619 for i in range(batch_size):
620 num_blocks_for_seq = cdiv(int(seq_lens[i]), block_size)
621 start = start_block_idx
622 end = start + num_blocks_for_seq
623 block_table[i, :num_blocks_for_seq] = inv_perm[start:end]
624 block_table[i, num_blocks_for_seq:] = 0
625 start_block_idx += num_blocks_for_seq
626
627 # Create a realistic slot mapping that corresponds to the block table
628 for i in range(batch_size):
629 token_offsets = torch.arange(int(query_lens[i])) + int(context_lens[i])
630 block_indices = token_offsets // block_size
631 token_inter_block_offsets = token_offsets % block_size
632 start = common_attn_metadata.query_start_loc_cpu[i]
633 end = common_attn_metadata.query_start_loc_cpu[i + 1]
634 slot_mapping[start:end] = block_table[
635 i, block_indices
636 ] * block_size + token_inter_block_offsets.to(device)
637
638 return kv_cache
639
640
641 class MockSparseMLAAttentionLayer:
642 """A mock sparse MLA attention layer for testing.
643
644 Sparse MLA implementations only support forward_mqa (decode-style attention)
645 for all tokens, so this class only implements that path.
646
647 Unlike regular MLA impls, sparse MLA impls don't have W_UK_T and W_UV
648 attributes. These transformations are done by the layer (MLAAttention),
649 not the impl. This mock layer accepts these weight matrices directly.
650 """
651
652 def __init__(
653 self,
654 impl,
655 num_heads: int,
656 qk_nope_head_dim: int,
657 qk_rope_head_dim: int,
658 v_head_dim: int,
659 kv_lora_rank: int,
660 device: torch.device,
661 W_UK: torch.Tensor,
662 W_UV: torch.Tensor,
663 q_scale: float,
664 k_scale: float,
665 ):
666 self.impl = impl
667 self.num_heads = num_heads
668 self.qk_nope_head_dim = qk_nope_head_dim
669 self.qk_rope_head_dim = qk_rope_head_dim
670 self.v_head_dim = v_head_dim
671 self.q_lora_rank = None
672 self.kv_lora_rank = kv_lora_rank
673
674 # Compute weight matrices in the format expected by forward_impl
675 # W_UK shape: (L, N, P) -> W_UK_T shape: (N, P, L)
676 self.W_UK_T = W_UK.permute(1, 2, 0)
677 # W_UV shape: (L, N, V) -> (N, L, V)
678 self.W_UV = W_UV.transpose(0, 1)
679
680 # Scale attributes needed by attention backends
681 self._q_scale = torch.tensor(q_scale, device=device)
682 self._k_scale = torch.tensor(k_scale, device=device)
683 self._v_scale = torch.tensor(float("nan"), device=device)
684 self._prob_scale = torch.tensor(1.0, device=device)
685 self._q_scale_float = q_scale
686 self._k_scale_float = k_scale
687 self._v_scale_float = float("nan")
688
689 self._decode_concat_quant_fp8_op = _DecodeConcatQuantFP8(
690 static=True,
691 group_shape=GroupShape.PER_TENSOR,
692 compile_native=True,
693 )
694
695 def forward_impl(
696 self,
697 q: torch.Tensor,
698 kv_c: torch.Tensor,
699 k_pe: torch.Tensor,
700 kv_cache: torch.Tensor,
701 attn_metadata,
702 output: torch.Tensor,
703 ) -> torch.Tensor:
704 """Forward for sparse MLA - uses forward_mqa for all tokens."""
705 kv_cache_dtype = getattr(self.impl, "kv_cache_dtype", "auto")
706 fp8_attention = kv_cache_dtype.startswith("fp8")
707
708 # Impls see the bind-time-squeezed [B, N, C] cache; mirror bind_kv_cache.
709 if kv_cache.ndim == 4:
710 kv_cache = kv_cache.squeeze(1)
711
712 # Write to KV cache
713 if kv_cache.numel() > 0:
714 ops.concat_and_cache_mla(
715 kv_c,
716 k_pe.squeeze(1),
717 kv_cache,
718 attn_metadata.slot_mapping.flatten(),
719 kv_cache_dtype=kv_cache_dtype,
720 scale=self._k_scale,
721 )
722
723 if fp8_attention and kv_cache_dtype != "fp8_ds_mla":
724 kv_cache = kv_cache.view(current_platform.fp8_dtype())
725
726 num_tokens = q.shape[0]
727
728 # Sparse MLA uses forward_mqa for all tokens
729 # Split q into nope and pe parts
730 mqa_q_nope, mqa_q_pe = q.split(
731 [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1
732 )
733
734 # Convert from (B, N, P) to (N, B, P)
735 mqa_q_nope = mqa_q_nope.transpose(0, 1)
736
737 # Multiply (N, B, P) x (N, P, L) -> (N, B, L)
738 mqa_ql_nope = torch.bmm(mqa_q_nope, self.W_UK_T)
739
740 # Convert from (N, B, L) to (B, N, L)
741 mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
742
743 if fp8_attention and self.impl.supports_quant_query_input:
744 assert mqa_ql_nope.shape[0] == mqa_q_pe.shape[0]
745 assert mqa_ql_nope.shape[1] == mqa_q_pe.shape[1]
746 mqa_q = self._decode_concat_quant_fp8_op(
747 mqa_ql_nope, mqa_q_pe, self._q_scale
748 )
749 else:
750 mqa_q = (mqa_ql_nope, mqa_q_pe)
751
752 attn_out, _ = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self)
753
754 # v_up projection: multiply by W_UV
755 # attn_out shape: (B, N, L) where L = kv_lora_rank
756 # W_UV shape: (N, L, V)
757 # output shape: (B, N, V) -> flatten to (B, N*V)
758 decode_output = torch.bmm(attn_out.transpose(0, 1), self.W_UV).transpose(0, 1)
759 output[:num_tokens] = decode_output.reshape(
760 num_tokens, self.num_heads * self.v_head_dim
761 )
762
763 return output
764
765
766 class MockMLAAttentionLayer(MLAAttention):
767 """A mock MLA attention layer for testing.
768
769 This replicates the forward_impl logic from MLAAttention to allow
770 testing MLA backends without the full layer infrastructure.
771
772 Subclasses MLAAttention so that backends that filter
773 `static_forward_context` by `isinstance(layer, MLAAttention)` (e.g.
774 FlashInfer prefill, which reads sm_scale through that filter) see the
775 mock as a real MLA layer. MLAAttention.__init__ is intentionally
776 skipped — it would create its own impl/prefill_backend and self-register
777 in static_forward_context, which fights what the test sets up below.
778 """
779
780 def __init__(
781 self,
782 impl,
783 num_heads: int,
784 qk_nope_head_dim: int,
785 qk_rope_head_dim: int,
786 v_head_dim: int,
787 kv_lora_rank: int,
788 device: torch.device,
789 kv_b_proj,
790 q_scale: float,
791 k_scale: float,
792 ):
793 torch.nn.Module.__init__(self)
794 self.impl = impl
795 self.num_heads = num_heads
796 self.qk_nope_head_dim = qk_nope_head_dim
797 self.qk_rope_head_dim = qk_rope_head_dim
798 self.v_head_dim = v_head_dim
799 self.q_lora_rank = None
800 self.kv_lora_rank = kv_lora_rank
801
802 # Compute weight matrices from kv_b_proj (like MLAAttention does)
803 # This replicates MLAAttention.process_weights_after_loading logic
804 kv_b_proj_weight = kv_b_proj.weight.T
805 kv_b_proj_weight = kv_b_proj_weight.view(
806 kv_lora_rank,
807 num_heads,
808 qk_nope_head_dim + v_head_dim,
809 )
810 W_UK, W_UV = kv_b_proj_weight.split([qk_nope_head_dim, v_head_dim], dim=-1)
811 # Convert from (L, N, V) to (N, L, V)
812 self.W_UV = W_UV.transpose(0, 1)
813 # Convert from (L, N, P) to (N, P, L)
814 self.W_UK_T = W_UK.permute(1, 2, 0)
815
816 # Scale attributes needed by attention backends
817 self._q_scale = torch.tensor(q_scale, device=device)
818 self._k_scale = torch.tensor(k_scale, device=device)
819 self._v_scale = torch.tensor(float("nan"), device=device)
820 self._prob_scale = torch.tensor(1.0, device=device)
821 self._q_scale_float = q_scale
822 self._k_scale_float = k_scale
823 self._v_scale_float = float("nan")
824
825 self._decode_concat_quant_fp8_op = _DecodeConcatQuantFP8(
826 static=True,
827 group_shape=GroupShape.PER_TENSOR,
828 compile_native=True,
829 )
830
831 def get_attn_backend(self):
832 raise NotImplementedError
833
834 def get_kv_cache_spec(self, vllm_config):
835 raise NotImplementedError
836
837 def forward_impl(
838 self,
839 q: torch.Tensor,
840 kv_c: torch.Tensor,
841 k_pe: torch.Tensor,
842 kv_cache: torch.Tensor,
843 attn_metadata,
844 output: torch.Tensor,
845 ) -> torch.Tensor:
846 """Replicates MLAAttention.forward_impl logic for testing."""
847 # Impls see the bind-time-squeezed [B, N, C] cache; mirror bind_kv_cache.
848 if kv_cache.ndim == 4:
849 kv_cache = kv_cache.squeeze(1)
850
851 # Write to KV cache
852 kv_cache_dtype = getattr(self.impl, "kv_cache_dtype", "auto")
853 fp8_attention = kv_cache_dtype.startswith("fp8")
854 if kv_cache.numel() > 0:
855 ops.concat_and_cache_mla(
856 kv_c,
857 k_pe.squeeze(1),
858 kv_cache,
859 attn_metadata.slot_mapping.flatten(),
860 kv_cache_dtype=kv_cache_dtype,
861 scale=self._k_scale,
862 )
863
864 if fp8_attention and kv_cache_dtype != "fp8_ds_mla":
865 kv_cache = kv_cache.view(current_platform.fp8_dtype())
866
867 # Determine decode vs prefill split
868 num_decode_tokens = attn_metadata.num_decode_tokens or 0
869 has_decode = (attn_metadata.num_decodes or 0) > 0
870 has_prefill = (attn_metadata.num_prefills or 0) > 0
871
872 # Run prefill with forward_mha
873 if has_prefill:
874 prefill_q = q[num_decode_tokens:]
875 prefill_k_pe = k_pe[num_decode_tokens:]
876 prefill_k_c = kv_c[num_decode_tokens:]
877 self.impl.forward_mha(
878 prefill_q,
879 prefill_k_c,
880 prefill_k_pe,
881 kv_cache,
882 attn_metadata,
883 self._k_scale,
884 output=output[num_decode_tokens:],
885 )
886
887 # Run decode with forward_mqa
888 if has_decode:
889 decode_q = q[:num_decode_tokens]
890
891 # Split q into nope and pe parts
892 mqa_q_nope, mqa_q_pe = decode_q.split(
893 [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1
894 )
895
896 # Convert from (B, N, P) to (N, B, P)
897 mqa_q_nope = mqa_q_nope.transpose(0, 1)
898
899 # Multiply (N, B, P) x (N, P, L) -> (N, B, L)
900 mqa_ql_nope = torch.bmm(mqa_q_nope, self.W_UK_T)
901
902 # Convert from (N, B, L) to (B, N, L)
903 mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
904
905 if fp8_attention and self.impl.supports_quant_query_input:
906 assert mqa_ql_nope.shape[0] == mqa_q_pe.shape[0]
907 assert mqa_ql_nope.shape[1] == mqa_q_pe.shape[1]
908 mqa_q = self._decode_concat_quant_fp8_op(
909 mqa_ql_nope, mqa_q_pe, self._q_scale
910 )
911 else:
912 mqa_q = (mqa_ql_nope, mqa_q_pe)
913 if self.impl.dcp_world_size > 1:
914 if isinstance(mqa_q, tuple):
915 mqa_q = torch.cat(mqa_q, dim=-1)
916 mqa_q = mla_attention_module.get_dcp_group().all_gather(mqa_q, dim=1)
917
918 attn_out, _ = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self)
919
920 # v_up projection: multiply by W_UV
921 # attn_out shape: (B, N, L) where L = kv_lora_rank
922 # W_UV shape: (N, L, V)
923 # output shape: (B, N, V) -> flatten to (B, N*V)
924 decode_output = torch.bmm(attn_out.transpose(0, 1), self.W_UV).transpose(
925 0, 1
926 )
927 output[:num_decode_tokens] = decode_output.reshape(
928 num_decode_tokens, self.num_heads * self.v_head_dim
929 )
930
931 return output
932
933
934 def test_mock_mla_dcp_fp8_decode_gathers_quantized_query(
935 monkeypatch, default_vllm_config
936 ):
937 if not torch.cuda.is_available():
938 pytest.skip("CUDA is required for FP8 decode query quantization path.")
939
940 device = torch.device(f"{DEVICE_TYPE}:0")
941 num_tokens = 2
942 num_heads = 2
943 qk_nope_head_dim = 4
944 qk_rope_head_dim = 2
945 v_head_dim = 3
946 kv_lora_rank = 5
947
948 class _DummyKVProj:
949 def __init__(self):
950 # Shape expected by MockMLAAttentionLayer.__init__
951 self.weight = torch.randn(
952 num_heads * (qk_nope_head_dim + v_head_dim),
953 kv_lora_rank,
954 device=device,
955 dtype=torch.float32,
956 )
957
958 class _FakeImpl:
959 def __init__(self):
960 self.kv_cache_dtype = "fp8"
961 self.supports_quant_query_input = True
962 self.dcp_world_size = 2
963 self.forward_q = None
964
965 def forward_mha(self, *args, **kwargs):
966 return None
967
968 def forward_mqa(self, q, kv_cache, attn_metadata, layer):
969 self.forward_q = q
970 assert isinstance(q, torch.Tensor)
971 bsz, _, _ = q.shape
972 return (
973 torch.zeros(
974 bsz,
975 num_heads,
976 kv_lora_rank,
977 device=q.device,
978 dtype=torch.float32,
979 ),
980 None,
981 )
982
983 class _FakeDCPGroup:
984 def __init__(self):
985 self.calls = 0
986 self.input_dtype = None
987 self.input_shape = None
988
989 def all_gather(self, x, dim=1):
990 self.calls += 1
991 self.input_dtype = x.dtype
992 self.input_shape = tuple(x.shape)
993 return torch.cat([x, x], dim=dim)
994
995 fake_group = _FakeDCPGroup()
996 monkeypatch.setattr(mla_attention_module, "get_dcp_group", lambda: fake_group)
997
998 impl = _FakeImpl()
999 with set_current_vllm_config(default_vllm_config):
1000 layer = MockMLAAttentionLayer(
1001 impl=impl,
1002 num_heads=num_heads,
1003 qk_nope_head_dim=qk_nope_head_dim,
1004 qk_rope_head_dim=qk_rope_head_dim,
1005 v_head_dim=v_head_dim,
1006 kv_lora_rank=kv_lora_rank,
1007 device=device,
1008 kv_b_proj=_DummyKVProj(),
1009 q_scale=1.0,
1010 k_scale=1.0,
1011 )
1012
1013 q = torch.randn(
1014 num_tokens,
1015 num_heads,
1016 qk_nope_head_dim + qk_rope_head_dim,
1017 device=device,
1018 dtype=torch.float32,
1019 )
1020 kv_c = torch.randn(num_tokens, kv_lora_rank, device=device, dtype=torch.float32)
1021 k_pe = torch.randn(
1022 num_tokens, 1, qk_rope_head_dim, device=device, dtype=torch.float32
1023 )
1024 kv_cache = torch.empty(0, device=device, dtype=torch.float32)
1025 output = torch.empty(
1026 num_tokens, num_heads * v_head_dim, device=device, dtype=torch.float32
1027 )
1028
1029 class _AttnMeta:
1030 num_decode_tokens = num_tokens
1031 num_decodes = 1
1032 num_prefills = 0
1033 slot_mapping = torch.empty(0, dtype=torch.long, device=device)
1034
1035 layer.forward_impl(q, kv_c, k_pe, kv_cache, _AttnMeta(), output)
1036
1037 assert fake_group.calls == 1
1038 assert fake_group.input_dtype == current_platform.fp8_dtype()
1039 assert fake_group.input_shape == (
1040 num_tokens,
1041 num_heads,
1042 kv_lora_rank + qk_rope_head_dim,
1043 )
1044 assert isinstance(impl.forward_q, torch.Tensor)
1045 assert tuple(impl.forward_q.shape) == (
1046 num_tokens,
1047 num_heads * impl.dcp_world_size,
1048 kv_lora_rank + qk_rope_head_dim,
1049 )
1050
1051
1052 def test_tokenspeed_mla_noncausal_capability():
1053 builder = tokenspeed_mla_module.TokenspeedMLAMetadataBuilder
1054 assert builder.supports_non_causal_multi_token_decode
1055 assert builder.supports_non_causal_multi_token_dcp
1056 assert tokenspeed_mla_module.TokenspeedMLABackend.supports_non_causal()
1057
1058
1059 def test_flashinfer_mla_dspark_dcp_supports_target_and_draft(monkeypatch):
1060 flashinfer_mla_module = pytest.importorskip(
1061 "vllm.v1.attention.backends.mla.flashinfer_mla"
1062 )
1063 vllm_config = SimpleNamespace(
1064 speculative_config=SimpleNamespace(method="dspark"),
1065 parallel_config=SimpleNamespace(decode_context_parallel_size=2),
1066 model_config=None,
1067 )
1068 monkeypatch.setattr(
1069 flashinfer_mla_module,
1070 "get_current_vllm_config",
1071 lambda: vllm_config,
1072 )
1073
1074 backend = flashinfer_mla_module.FlashInferMLABackend
1075 builder = flashinfer_mla_module.FlashInferMLAMetadataBuilder
1076 reason = backend.supports_combination(
1077 head_size=576,
1078 dtype=torch.bfloat16,
1079 kv_cache_dtype="fp8",
1080 block_size=64,
1081 use_mla=True,
1082 has_sink=False,
1083 use_sparse=False,
1084 use_mm_prefix=False,
1085 device_capability=SimpleNamespace(),
1086 )
1087
1088 assert reason is None
1089 assert backend.supports_non_causal()
1090 assert builder.supports_non_causal_multi_token_decode
1091 assert backend.supports_non_causal_dcp()
1092
1093
1094 @pytest.mark.parametrize(
1095 ("causal", "tokens_per_decode", "dcp_world_size", "dcp_rank"),
1096 [
1097 pytest.param(True, 1, 2, 1, id="causal-dcp"),
1098 pytest.param(False, 3, 1, 0, id="noncausal-multi-token"),
1099 ],
1100 )
1101 def test_tokenspeed_mla_decode_contract(
1102 monkeypatch, causal, tokens_per_decode, dcp_world_size, dcp_rank
1103 ):
1104 decode_call = None
1105 num_decodes = 2
1106 num_decode_tokens = num_decodes * tokens_per_decode
1107 num_heads = 128
1108 kv_lora_rank = 512
1109 qk_rope_head_dim = 64
1110 head_size = kv_lora_rank + qk_rope_head_dim
1111 block_size = 64
1112 num_blocks = 4
1113 max_seq_len = 24
1114
1115 def fake_decode(**kwargs):
1116 nonlocal decode_call
1117 decode_call = kwargs
1118 q = kwargs["query"]
1119 out = torch.empty(
1120 q.shape[0],
1121 q.shape[1],
1122 q.shape[2],
1123 kv_lora_rank,
1124 dtype=torch.bfloat16,
1125 )
1126 lse = torch.empty(q.shape[0], q.shape[1], q.shape[2], dtype=torch.float32)
1127 return out, lse
1128
1129 monkeypatch.setitem(
1130 sys.modules,
1131 "tokenspeed_mla",
1132 SimpleNamespace(tokenspeed_mla_decode=fake_decode),
1133 )
1134
1135 impl = object.__new__(tokenspeed_mla_module.TokenspeedMLAImpl)
1136 impl.dcp_world_size = dcp_world_size
1137 impl.dcp_rank = dcp_rank
1138 impl.cp_kv_cache_interleave_size = 1
1139 impl.need_to_return_lse_for_decode = True
1140 impl.kv_lora_rank = kv_lora_rank
1141 impl.qk_rope_head_dim = qk_rope_head_dim
1142 impl.num_heads = num_heads
1143 impl.scale = 1.0
1144 impl.softmax_scale = 1.0
1145 impl.output_scale = 1.0
1146 impl._workspace_buffer = torch.empty(1, dtype=torch.int8)
1147
1148 metadata = SimpleNamespace(
1149 num_decodes=num_decodes,
1150 num_decode_tokens=num_decode_tokens,
1151 max_seq_len=max_seq_len,
1152 causal=causal,
1153 decode=SimpleNamespace(
1154 block_table=torch.empty((num_decodes, 1), dtype=torch.int32),
1155 seq_lens=torch.tensor([16, max_seq_len], dtype=torch.int32),
1156 dcp_tot_seq_lens=torch.tensor([16, max_seq_len], dtype=torch.int32),
1157 ),
1158 )
1159 q = torch.empty(num_decode_tokens, num_heads, head_size, dtype=torch.float8_e4m3fn)
1160 kv_cache = torch.empty(
1161 num_blocks,
1162 block_size,
1163 head_size,
1164 dtype=torch.float8_e4m3fn,
1165 )
1166
1167 out, lse = impl.forward_mqa(
1168 q,
1169 kv_cache,
1170 metadata,
1171 SimpleNamespace(_q_scale_float=2.0, _k_scale_float=3.0),
1172 )
1173
1174 assert out.shape == (num_decode_tokens, num_heads, kv_lora_rank)
1175 assert lse is not None
1176 assert lse.shape == (num_decode_tokens, num_heads)
1177
1178 assert decode_call is not None
1179 assert decode_call["query"].shape == (
1180 num_decodes,
1181 tokens_per_decode,
1182 num_heads,
1183 head_size,
1184 )
1185 torch.testing.assert_close(decode_call["seq_lens"], metadata.decode.seq_lens)
1186 torch.testing.assert_close(decode_call["block_tables"], metadata.decode.block_table)
1187 if dcp_world_size > 1:
1188 torch.testing.assert_close(
1189 decode_call["causal_seqs"], metadata.decode.dcp_tot_seq_lens
1190 )
1191 else:
1192 assert decode_call["causal_seqs"] is None
1193 assert decode_call["causal_mask"] is causal
1194 assert decode_call["return_lse"] is True
1195 assert decode_call["cp_world"] == dcp_world_size
1196 assert decode_call["cp_rank"] == dcp_rank
1197
1198
1199 @pytest.mark.parametrize("is_fp8_kvcache", [False, True], ids=["bf16", "fp8"])
1200 def test_flashmla_dcp_decode_metadata_uses_gathered_query_heads(
1201 monkeypatch, is_fp8_kvcache
1202 ):
1203 class _FakeSchedulerMetadata:
1204 tile_scheduler_metadata = None
1205 num_splits = None
1206
1207 base_call: tuple[torch.Tensor, int, int, bool] | None = None
1208 fp8_call: tuple[torch.Tensor, int, int] | None = None
1209
1210 def fake_get_mla_metadata(
1211 seq_lens_device,
1212 num_q_tokens_per_head_k,
1213 num_heads_k,
1214 is_fp8_kvcache=False,
1215 ):
1216 nonlocal base_call
1217 base_call = (
1218 seq_lens_device,
1219 num_q_tokens_per_head_k,
1220 num_heads_k,
1221 is_fp8_kvcache,
1222 )
1223 return _FakeSchedulerMetadata(), None
1224
1225 def fake_get_mla_metadata_dense_fp8(
1226 seq_lens_device, num_q_tokens_per_head_k, num_heads_k
1227 ):
1228 nonlocal fp8_call
1229 fp8_call = (
1230 seq_lens_device,
1231 num_q_tokens_per_head_k,
1232 num_heads_k,
1233 )
1234 return (
1235 torch.empty((0, 8), dtype=torch.int32),
1236 torch.empty((0,), dtype=torch.int32),
1237 )
1238
1239 monkeypatch.setattr(flashmla_module, "get_mla_metadata", fake_get_mla_metadata)
1240 monkeypatch.setattr(
1241 flashmla_module,
1242 "get_mla_metadata_dense_fp8",
1243 fake_get_mla_metadata_dense_fp8,
1244 )
1245
1246 builder = object.__new__(flashmla_module.FlashMLAMetadataBuilder)
1247 builder.num_q_heads = 4
1248 builder.dcp_world_size = 2
1249 builder.is_fp8_kvcache = is_fp8_kvcache
1250 builder.compilation_config = type(
1251 "_CompilationConfig",
1252 (),
1253 {
1254 "cudagraph_mode": type(
1255 "_CudaGraphMode",
1256 (),
1257 {"has_full_cudagraphs": lambda self: False},
1258 )()
1259 },
1260 )()
1261
1262 seq_lens = torch.tensor([16, 24], dtype=torch.int32)
1263 query_start_loc = torch.tensor([0, 1, 2], dtype=torch.int32)
1264
1265 metadata = builder._build_decode(
1266 block_table_tensor=torch.empty((2, 1), dtype=torch.int32),
1267 seq_lens_device=seq_lens,
1268 max_seq_len=24,
1269 query_start_loc_cpu=query_start_loc,
1270 query_start_loc_device=query_start_loc,
1271 num_decode_tokens=2,
1272 max_query_len=1,
1273 dcp_tot_seq_lens_device=None,
1274 )
1275
1276 assert base_call is not None
1277 assert base_call[0] is seq_lens
1278 assert base_call[1:] == (8, 1, is_fp8_kvcache)
1279 if is_fp8_kvcache:
1280 assert metadata.scheduler_metadata.tile_scheduler_metadata is not None
1281 assert metadata.scheduler_metadata.num_splits is not None
1282 assert fp8_call is not None
1283 assert fp8_call[0] is seq_lens
1284 assert fp8_call[1:] == (8, 1)
1285 else:
1286 assert metadata.scheduler_metadata.tile_scheduler_metadata is None
1287 assert metadata.scheduler_metadata.num_splits is None
1288 assert fp8_call is None
1289
1290
1291 def run_attention_backend(
1292 backend: AttentionBackendEnum,
1293 kv_cache_spec: MLAAttentionSpec,
1294 layer_names: list[str],
1295 vllm_config,
1296 device: torch.device,
1297 common_attn_metadata: CommonAttentionMetadata,
1298 query: torch.Tensor,
1299 kv_c: torch.Tensor,
1300 k_pe: torch.Tensor,
1301 kv_cache: torch.Tensor,
1302 kv_lora_rank: int,
1303 qk_nope_head_dim: int,
1304 qk_rope_head_dim: int,
1305 v_head_dim: int,
1306 mock_kv_b_proj,
1307 q_scale: float,
1308 k_scale: float,
1309 kv_cache_dtype: str = "auto",
1310 prefill_backend: MLAPrefillBackendEnum | None = None,
1311 chunked_prefill_workspace_size: int | None = None,
1312 ) -> torch.Tensor:
1313 """Run attention computation using the specified backend's AttentionImpl."""
1314 builder_cls, impl_cls = try_get_attention_backend(backend)
1315
1316 # Force the prefill backend selection (None means auto-select).
1317 vllm_config.attention_config.mla_prefill_backend = prefill_backend
1318
1319 # Set the current vllm config so that get_current_vllm_config() works
1320 # in the backend implementations
1321 with set_current_vllm_config(vllm_config):
1322 # Instantiate MLA implementation
1323 num_heads = vllm_config.model_config.get_num_attention_heads(
1324 vllm_config.parallel_config
1325 )
1326 num_kv_heads = vllm_config.model_config.get_num_kv_heads(
1327 vllm_config.parallel_config
1328 )
1329 head_size = vllm_config.model_config.get_head_size()
1330 # Production MLA passes 1/sqrt(qk_head_dim) (the prefill scale) to the
1331 # impl and forwards the same value to the prefill backend. FLASHINFER
1332 # prefill reads sm_scale back from impl.scale via global_hyperparameters
1333 # at plan() time, so impl.scale must agree with prefill_backend.scale.
1334 scale = (qk_nope_head_dim + qk_rope_head_dim) ** -0.5
1335 impl = impl_cls(
1336 num_heads=num_heads,
1337 head_size=head_size,
1338 scale=scale,
1339 num_kv_heads=num_kv_heads,
1340 alibi_slopes=None,
1341 sliding_window=None,
1342 kv_cache_dtype=kv_cache_dtype,
1343 logits_soft_cap=None,
1344 attn_type="decoder",
1345 kv_sharing_target_layer_name=None,
1346 q_lora_rank=None,
1347 kv_lora_rank=kv_lora_rank,
1348 qk_nope_head_dim=qk_nope_head_dim,
1349 qk_rope_head_dim=qk_rope_head_dim,
1350 qk_head_dim=qk_nope_head_dim + qk_rope_head_dim,
1351 v_head_dim=v_head_dim,
1352 kv_b_proj=mock_kv_b_proj,
1353 )
1354
1355 # Process weights on the impl
1356 act_dtype = _convert_dtype_to_torch(vllm_config.model_config.dtype)
1357 impl.process_weights_after_loading(act_dtype)
1358
1359 # Initialize DCP attributes (normally set by MLAAttention.forward
1360 # before calling forward_mha, see mla_attention.py:511-512)
1361 if impl.dcp_world_size == -1:
1362 impl.dcp_world_size = 1
1363
1364 # Create mock MLA layer
1365 mock_layer = MockMLAAttentionLayer(
1366 impl=impl,
1367 num_heads=num_heads,
1368 qk_nope_head_dim=qk_nope_head_dim,
1369 qk_rope_head_dim=qk_rope_head_dim,
1370 v_head_dim=v_head_dim,
1371 kv_lora_rank=kv_lora_rank,
1372 device=device,
1373 kv_b_proj=mock_kv_b_proj,
1374 q_scale=q_scale,
1375 k_scale=k_scale,
1376 )
1377
1378 # Attach prefill backend (normally created by MLAAttention.__init__)
1379 prefill_scale = (qk_nope_head_dim + qk_rope_head_dim) ** -0.5
1380 prefill_backend_cls = get_mla_prefill_backend(vllm_config)
1381 mock_layer.prefill_backend = prefill_backend_cls(
1382 num_heads=num_heads,
1383 scale=prefill_scale,
1384 kv_lora_rank=kv_lora_rank,
1385 qk_nope_head_dim=qk_nope_head_dim,
1386 qk_rope_head_dim=qk_rope_head_dim,
1387 v_head_dim=v_head_dim,
1388 vllm_config=vllm_config,
1389 )
1390
1391 # Populate static_forward_context with mock attention layers
1392 for layer_name in layer_names:
1393 vllm_config.compilation_config.static_forward_context[layer_name] = (
1394 mock_layer
1395 )
1396
1397 # Build metadata
1398 builder = builder_cls(kv_cache_spec, layer_names, vllm_config, device)
1399 if chunked_prefill_workspace_size is not None:
1400 builder.chunked_prefill_workspace_size = chunked_prefill_workspace_size
1401 builder.chunked_prefill_workspace = (
1402 builder.chunked_prefill_workspace.new_empty(
1403 chunked_prefill_workspace_size, head_size
1404 )
1405 )
1406 attn_metadata = builder.build(
1407 common_prefix_len=0,
1408 common_attn_metadata=common_attn_metadata,
1409 )
1410
1411 # Create output buffer
1412 num_tokens = query.shape[0]
1413 output = torch.empty(
1414 num_tokens, num_heads * v_head_dim, dtype=query.dtype, device=query.device
1415 )
1416
1417 # Run forward pass
1418 output = mock_layer.forward_impl(
1419 query, kv_c, k_pe, kv_cache, attn_metadata, output
1420 )
1421
1422 return output
1423
1424
1425 def _run_backend_correctness(
1426 default_vllm_config,
1427 dist_init,
1428 workspace_init,
1429 batch_spec_name: str,
1430 model: str,
1431 tensor_parallel_size: int,
1432 kv_cache_dtype: str,
1433 q_scale: float,
1434 k_scale: float,
1435 prefill_backend: MLAPrefillBackendEnum,
1436 qk_nope_head_dim: int,
1437 v_head_dim: int,
1438 chunked_prefill_workspace_size: int | None = None,
1439 ):
1440 """Test that all backends produce similar outputs to a reference implementation
1441 using torch.nn.functional.scaled_dot_product_attention.
1442
1443 This test works by:
1444 1. Generating a batch of sequences with specified context and query lengths.
1445 2. Computing a ground-truth attention output using torch.sdpa on
1446 contiguous Q, K, and V tensors.
1447 3. Simulating vLLM's paged KV cache: It takes the context portion of the
1448 K/V tensors and manually places them into a paged buffer according to
1449 the test's (randomly generated) block table.
1450 4. Running each vLLM attention backend with the new queries and the
1451 simulated paged KV cache.
1452 5. Comparing the vLLM backend's output to the ground-truth SDPA output.
1453
1454 Note: When tensor_parallel_size > 1, we simulate the head partitioning
1455 by overriding the model config to use fewer heads, without requiring
1456 multiple GPUs. This tests that backends work correctly with different
1457 head counts.
1458 """
1459 # Filter backends to those that support the requested kv_cache_dtype
1460 backends_to_test = [
1461 b
1462 for b in BACKENDS_TO_TEST
1463 if kv_cache_dtype in b.get_class().supported_kv_cache_dtypes
1464 ]
1465 if (
1466 q_scale != 1.0 or k_scale != 1.0
1467 ) and AttentionBackendEnum.CUTLASS_MLA in backends_to_test:
1468 # CUTLASS_MLA does not support non-1 Q/K scales
1469 backends_to_test.remove(AttentionBackendEnum.CUTLASS_MLA)
1470 if not backends_to_test:
1471 pytest.skip(f"No backends support kv_cache_dtype={kv_cache_dtype}")
1472
1473 batch_spec = BATCH_SPECS[batch_spec_name]
1474 is_spec_decode_test = batch_spec_name.startswith("spec_decode")
1475 unique_block_sizes = sorted(set(BACKEND_BLOCK_SIZES[b] for b in backends_to_test))
1476 default_block_size = unique_block_sizes[0]
1477 required_blocks = sum(
1478 (seq_len + default_block_size - 1) // default_block_size
1479 for seq_len in batch_spec.seq_lens
1480 )
1481 # Add 1 for null block at index 0, and some buffer
1482 num_gpu_blocks = required_blocks + 1 + 100
1483
1484 hf_config_override = None
1485 if tensor_parallel_size > 1:
1486 from vllm.config import ModelConfig
1487
1488 temp_config = ModelConfig(model=model, max_model_len=1)
1489 original_num_heads = temp_config.hf_text_config.num_attention_heads
1490 original_num_kv_heads = getattr(
1491 temp_config.hf_text_config, "num_key_value_heads", None
1492 )
1493 hf_config_override = {
1494 "num_attention_heads": original_num_heads // tensor_parallel_size,
1495 }
1496 if original_num_kv_heads is not None:
1497 hf_config_override["num_key_value_heads"] = max(
1498 1, original_num_kv_heads // tensor_parallel_size
1499 )
1500
1501 vllm_config = create_vllm_config(
1502 model_name=model,
1503 tensor_parallel_size=1, # Always use TP=1 to avoid multi-GPU requirements
1504 max_model_len=max(batch_spec.seq_lens),
1505 num_gpu_blocks=num_gpu_blocks,
1506 block_size=default_block_size,
1507 hf_config_override=hf_config_override,
1508 )
1509 vllm_config.cache_config.cache_dtype = kv_cache_dtype
1510
1511 # For spec decode tests, add a speculative_config to set the reorder_batch_threshold
1512 if is_spec_decode_test:
1513 from vllm.config import SpeculativeConfig
1514
1515 # Get the query length from the batch spec (they should all be uniform)
1516 query_len = batch_spec.query_lens[0]
1517 # Set num_speculative_tokens to query_len - 1
1518 # (since threshold is 1 + num_spec_tokens)
1519 # Use ngram method which doesn't require a draft model
1520 vllm_config.speculative_config = SpeculativeConfig(
1521 method="ngram", num_speculative_tokens=query_len - 1
1522 )
1523
1524 device = torch.device(f"{DEVICE_TYPE}:0")
1525
1526 # 1. Setup
1527 batch_size = batch_spec.batch_size
1528 seq_lens = batch_spec.seq_lens
1529 query_lens = batch_spec.query_lens
1530 num_q_heads = vllm_config.model_config.get_num_attention_heads(
1531 vllm_config.parallel_config
1532 )
1533 head_size = vllm_config.model_config.get_head_size()
1534 dtype = _convert_dtype_to_torch(vllm_config.model_config.dtype)
1535 kv_lora_rank = 512
1536 qk_rope_head_dim = 64
1537 total_head_size = kv_lora_rank + qk_rope_head_dim
1538 assert kv_lora_rank + qk_rope_head_dim == head_size, (
1539 f"MLA dimensions don't match: {total_head_size} != {head_size}"
1540 )
1541 qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
1542 prefill_scale = qk_head_dim**-0.5
1543 # MLA reuses prefill_scale for the decode path: production sets
1544 # impl.scale = 1/sqrt(qk_head_dim) and the decode kernels apply it even
1545 # though the latent attention runs at head_size dimensions. Keeping the
1546 # reference here in sync with run_attention_backend's impl.scale.
1547 decode_scale = prefill_scale
1548
1549 # 2. Generate data and compute SDPA reference output for MLA
1550 all_q_vllm, all_kv_c_vllm, all_k_pe_vllm = [], [], []
1551 all_sdpa_outputs: list[list[torch.Tensor]] = []
1552 kv_c_contexts, k_pe_contexts = [], []
1553
1554 # Create shared MLA weight matrices for consistency across all sequences
1555 W_UK = torch.randn(
1556 kv_lora_rank, num_q_heads, qk_nope_head_dim, dtype=dtype, device=device
1557 )
1558 W_UV = torch.randn(
1559 kv_lora_rank, num_q_heads, v_head_dim, dtype=dtype, device=device
1560 )
1561
1562 # Scale weights to produce realistic magnitude outputs.
1563 # Without scaling, projection output has std ~sqrt(kv_lora_rank) ≈ 22.6,
1564 # causing extreme attention scores and numerical instability in LSE merging.
1565 weight_scale = 1.0 / (kv_lora_rank**0.5)
1566 W_UK = W_UK * weight_scale
1567 W_UV = W_UV * weight_scale
1568
1569 kv_b_proj_weight = torch.cat([W_UK, W_UV], dim=-1)
1570
1571 for i, backend in enumerate(backends_to_test):
1572 all_sdpa_outputs.append([])
1573
1574 for i in range(batch_size):
1575 s_len = seq_lens[i]
1576 q_len = query_lens[i]
1577 context_len = s_len - q_len
1578
1579 # Generate MLA tensors
1580 # Q has both nope and rope components:
1581 # [q_len, num_heads, qk_nope_head_dim + qk_rope_head_dim]
1582 q_c = torch.randn(
1583 q_len,
1584 num_q_heads,
1585 qk_nope_head_dim + qk_rope_head_dim,
1586 dtype=dtype,
1587 device=device,
1588 )
1589
1590 # KV_C (latent K/V): [s_len, kv_lora_rank]
1591 kv_c_full = torch.randn(s_len, kv_lora_rank, dtype=dtype, device=device)
1592
1593 # K_PE (rope component): [s_len, 1, qk_rope_head_dim]
1594 k_pe_full = torch.randn(s_len, 1, qk_rope_head_dim, dtype=dtype, device=device)
1595
1596 # Determine if this sequence uses the decode pipeline or prefill
1597 # pipeline for each backend
1598 # NOTE: For spec decode tests with uniform query_len > 1, backends that
1599 # support spec decode (FLASH_ATTN_MLA with varlen support, FLASHMLA with
1600 # uniform support) will use the decode pipeline (MQA-style), while
1601 # backends that only support single-token queries will use the prefill
1602 # pipeline (MHA-style). This ensures the reference implementation
1603 # matches each backend's actual decode/prefill pipeline path.
1604 is_decode = []
1605 for backend_idx, backend in enumerate(backends_to_test):
1606 builder_cls, _ = try_get_attention_backend(backend)
1607 if is_spec_decode_test:
1608 query_len_support = getattr(
1609 builder_cls, "query_len_support", QueryLenSupport.SINGLE_ONLY
1610 )
1611 supports_spec = query_len_support != QueryLenSupport.SINGLE_ONLY
1612 is_decode.append(supports_spec)
1613 else:
1614 threshold = getattr(builder_cls, "reorder_batch_threshold", None)
1615 query_len_support = getattr(
1616 builder_cls, "query_len_support", QueryLenSupport.SINGLE_ONLY
1617 )
1618 within_threshold = q_len <= threshold if threshold else False
1619 if (
1620 within_threshold
1621 and query_len_support == QueryLenSupport.UNIFORM
1622 and i > 0
1623 ):
1624 first_q_len = query_lens[0]
1625 within_threshold = q_len == first_q_len
1626 is_decode.append(within_threshold)
1627
1628 # Split q into nope and rope components
1629 q_nope, q_pe = q_c.split([qk_nope_head_dim, qk_rope_head_dim], dim=-1)
1630
1631 #######################################################
1632 # Decode path: MQA-style attention in latent space
1633 # Transform q_nope to latent space: q_nope @ W_UK
1634 # q_nope: [1, num_heads, qk_nope_head_dim]
1635 # W_UK: [kv_lora_rank, num_heads, qk_nope_head_dim]
1636 ql_nope = torch.einsum(
1637 "qnh,lnh->qnl", q_nope, W_UK
1638 ) # [1, num_heads, kv_lora_rank]
1639
1640 # Build MQA attention inputs
1641 # Q: [1, num_heads, kv_lora_rank + qk_rope_head_dim]
1642 q_mqa = torch.cat([ql_nope, q_pe], dim=-1)
1643 # K: [s_len, kv_lora_rank + qk_rope_head_dim]
1644 # (broadcasted to all heads)
1645 k_mqa = torch.cat([kv_c_full, k_pe_full.squeeze(1)], dim=-1)
1646 k_mqa = k_mqa.unsqueeze(1).expand(-1, num_q_heads, -1)
1647 # V: [s_len, kv_lora_rank] (broadcasted to all heads)
1648 v_mqa = kv_c_full.unsqueeze(1).expand(-1, num_q_heads, -1)
1649
1650 # Create custom attention mask for decode path:
1651 # - Query tokens can attend to all context tokens
1652 # - Query tokens can only attend to query tokens up to their position
1653 attn_mask = torch.ones(q_len, s_len, dtype=torch.bool, device=device)
1654 # Apply causal mask only to the query portion (context_len onwards)
1655 causal_mask = torch.tril(torch.ones(q_len, q_len, device=device))
1656 attn_mask[:, context_len:] = causal_mask
1657
1658 # SDPA expects (N, H, L, D)
1659 q_sdpa_in = q_mqa.unsqueeze(0).transpose(1, 2)
1660 k_sdpa_in = k_mqa.unsqueeze(0).transpose(1, 2)
1661 v_sdpa_in = v_mqa.unsqueeze(0).transpose(1, 2)
1662
1663 sdpa_out_i_decode = torch.nn.functional.scaled_dot_product_attention(
1664 q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=decode_scale
1665 )
1666 sdpa_out_i_decode = sdpa_out_i_decode.transpose(1, 2).squeeze(
1667 0
1668 ) # [1, num_heads, kv_lora_rank]
1669
1670 # Project back to output space: sdpa_out @ W_UV
1671 sdpa_out_i_decode = torch.einsum("qnl,lnv->qnv", sdpa_out_i_decode, W_UV)
1672 sdpa_out_i_decode = sdpa_out_i_decode.flatten(start_dim=-2)
1673
1674 #######################################################
1675 # Prefill path: MHA-style attention with full sequence
1676 # Apply kv_b_proj to the full kv_c tensor
1677 kv_nope_full = torch.einsum("sl,lnh->snh", kv_c_full, kv_b_proj_weight)
1678 k_nope_full, v_full = kv_nope_full.split([qk_nope_head_dim, v_head_dim], dim=-1)
1679
1680 # Build attention inputs for full sequence
1681 q_mha = torch.cat([q_nope, q_pe], dim=-1) # [q_len, num_heads, total_dim]
1682 k_pe_full_expanded = k_pe_full.expand(-1, num_q_heads, -1)
1683 k_full = torch.cat([k_nope_full, k_pe_full_expanded], dim=-1)
1684
1685 # Create custom attention mask:
1686 # - Query tokens can attend to all context tokens
1687 # - Query tokens can only attend to query tokens up to their pos
1688 attn_mask = torch.ones(q_len, s_len, dtype=torch.bool, device=device)
1689 # Apply causal mask only to the query portion (context_len onwards)
1690 causal_mask = torch.tril(torch.ones(q_len, q_len, device=device))
1691 attn_mask[:, context_len:] = causal_mask
1692
1693 # SDPA expects (N, H, L, D)
1694 q_sdpa_in = q_mha.unsqueeze(0).transpose(1, 2)
1695 k_sdpa_in = k_full.unsqueeze(0).transpose(1, 2)
1696 v_sdpa_in = v_full.unsqueeze(0).transpose(1, 2)
1697
1698 # Single attention call with custom mask
1699 sdpa_out_i_prefill = torch.nn.functional.scaled_dot_product_attention(
1700 q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=prefill_scale
1701 )
1702 sdpa_out_i_prefill = sdpa_out_i_prefill.transpose(1, 2).squeeze(0)
1703 sdpa_out_i_prefill = sdpa_out_i_prefill.flatten(start_dim=-2)
1704
1705 for backend_idx, backend in enumerate(backends_to_test):
1706 if is_decode[backend_idx]:
1707 all_sdpa_outputs[backend_idx].append(sdpa_out_i_decode)
1708 else:
1709 all_sdpa_outputs[backend_idx].append(sdpa_out_i_prefill)
1710
1711 # Inputs for vLLM MLA backends are just the new tokens
1712 all_q_vllm.append(q_c)
1713 all_kv_c_vllm.append(kv_c_full[context_len:]) # New kv_c tokens
1714 all_k_pe_vllm.append(k_pe_full[context_len:]) # New k_pe tokens
1715
1716 # Contextual K/V data used to populate the paged cache (MLA format)
1717 kv_c_contexts.append(kv_c_full[:context_len])
1718 k_pe_contexts.append(k_pe_full[:context_len])
1719
1720 # Concatenate all sequences (no reordering needed)
1721 query_vllm = torch.cat(all_q_vllm, dim=0)
1722 kv_c_vllm = torch.cat(all_kv_c_vllm, dim=0)
1723 k_pe_vllm = torch.cat(all_k_pe_vllm, dim=0)
1724 sdpa_outputs = {}
1725 for backend_idx, backend in enumerate(backends_to_test):
1726 sdpa_outputs[backend] = torch.cat(all_sdpa_outputs[backend_idx], dim=0)
1727
1728 # Create mock kv_b_proj using the same weights as reference implementation
1729 from vllm.model_executor.layers.linear import ColumnParallelLinear
1730
1731 mock_kv_b_proj = ColumnParallelLinear(
1732 input_size=kv_lora_rank,
1733 output_size=num_q_heads * (qk_nope_head_dim + v_head_dim),
1734 bias=False,
1735 ).to(device=device, dtype=dtype)
1736
1737 # Set the mock weights to match our reference implementation
1738 # Reshape W_UK and W_UV to match the expected kv_b_proj format
1739 # [kv_lora_rank, num_heads, qk_nope_head_dim + v_head_dim]
1740 kv_b_proj_weight = kv_b_proj_weight.view(
1741 kv_lora_rank, num_q_heads * (qk_nope_head_dim + v_head_dim)
1742 )
1743 mock_kv_b_proj.weight = torch.nn.Parameter(kv_b_proj_weight.T, requires_grad=False)
1744
1745 # 3. Create metadata and KV caches for each block size
1746 # Group backends by block size and test each group
1747 metadata_per_block_size = {}
1748 kv_cache_per_block_size = {}
1749
1750 for block_size in unique_block_sizes:
1751 # Create metadata for this block size
1752 common_attn_metadata = create_common_attn_metadata(
1753 batch_spec, block_size, device
1754 )
1755
1756 current_block_num = common_attn_metadata.block_table_tensor.shape[1]
1757 padded_block_num = get_block_table_width(current_block_num, block_size)
1758 if padding_cols := padded_block_num - current_block_num:
1759 padding = torch.zeros(
1760 (common_attn_metadata.block_table_tensor.shape[0], padding_cols),
1761 dtype=torch.int32,
1762 device=device,
1763 )
1764 common_attn_metadata.block_table_tensor = torch.cat(
1765 [common_attn_metadata.block_table_tensor, padding], dim=1
1766 )
1767
1768 metadata_per_block_size[block_size] = common_attn_metadata
1769
1770 # Create KV cache for this block size
1771 required_blocks_for_size = sum(
1772 (seq_len + block_size - 1) // block_size for seq_len in batch_spec.seq_lens
1773 )
1774 num_blocks_for_size = required_blocks_for_size + 1 + 100
1775
1776 kv_cache = create_and_prepopulate_kv_cache(
1777 kv_c_contexts=kv_c_contexts,
1778 k_pe_contexts=k_pe_contexts,
1779 block_size=block_size,
1780 head_size=head_size,
1781 dtype=dtype,
1782 device=device,
1783 num_blocks=num_blocks_for_size,
1784 common_attn_metadata=common_attn_metadata,
1785 randomize_blocks=True,
1786 kv_cache_dtype=kv_cache_dtype,
1787 scale=k_scale,
1788 )
1789 kv_cache_per_block_size[block_size] = kv_cache
1790
1791 # 4. Run vLLM backends and compare
1792 rtol = 1e-2
1793 atol = {
1794 "auto": 1e-2,
1795 "fp8": 1.5e-1,
1796 "fp8_e4m3": 1.5e-1,
1797 }[kv_cache_dtype]
1798 failures = []
1799 for backend_idx, backend_name in enumerate(backends_to_test):
1800 # Skip backends that don't support spec decode for spec decode tests
1801 if is_spec_decode_test and backend_name not in SPEC_DECODE_BACKENDS:
1802 continue
1803
1804 # Get the appropriate block_size, metadata, and cache for this backend
1805 block_size = BACKEND_BLOCK_SIZES[backend_name]
1806 common_attn_metadata = metadata_per_block_size[block_size]
1807 kv_cache = kv_cache_per_block_size[block_size]
1808
1809 # Create kv_cache_spec with the correct block_size for this backend
1810 backend_kv_cache_spec = MLAAttentionSpec(
1811 block_size=block_size,
1812 num_kv_heads=vllm_config.model_config.get_num_kv_heads(
1813 vllm_config.parallel_config
1814 ),
1815 head_size=vllm_config.model_config.get_head_size(),
1816 dtype=vllm_config.model_config.dtype,
1817 sliding_window=vllm_config.model_config.get_sliding_window(),
1818 cache_dtype_str=kv_cache_dtype,
1819 )
1820
1821 backend_output = run_attention_backend(
1822 backend_name,
1823 backend_kv_cache_spec,
1824 ["placeholder"],
1825 vllm_config,
1826 device,
1827 common_attn_metadata,
1828 query_vllm,
1829 kv_c_vllm,
1830 k_pe_vllm,
1831 kv_cache,
1832 kv_lora_rank,
1833 qk_nope_head_dim,
1834 qk_rope_head_dim,
1835 v_head_dim,
1836 mock_kv_b_proj,
1837 prefill_backend=prefill_backend,
1838 q_scale=q_scale,
1839 k_scale=k_scale,
1840 kv_cache_dtype=kv_cache_dtype,
1841 chunked_prefill_workspace_size=chunked_prefill_workspace_size,
1842 )
1843
1844 # Use backend_idx to get the correct SDPA output for this backend
1845 expected_output = sdpa_outputs[backend_name]
1846
1847 # Check shape and dtype consistency
1848 try:
1849 assert backend_output.shape == expected_output.shape, (
1850 f"[{backend_name}] shape {backend_output.shape} != "
1851 f"SDPA shape {expected_output.shape}"
1852 )
1853 assert backend_output.dtype == expected_output.dtype, (
1854 f"[{backend_name}] dtype {backend_output.dtype} != "
1855 f"SDPA dtype {expected_output.dtype}"
1856 )
1857
1858 assert torch.isfinite(backend_output).all(), (
1859 f"[{backend_name}] produced non-finite values"
1860 )
1861
1862 max_diff = torch.max(torch.abs(backend_output - expected_output)).item()
1863 max_rel_diff = torch.max(
1864 torch.abs(backend_output - expected_output) / torch.abs(expected_output)
1865 ).item()
1866 all_close = torch.allclose(
1867 backend_output, expected_output, rtol=rtol, atol=atol
1868 )
1869
1870 assert all_close, (
1871 f"[{backend_name}] output differs from SDPA baseline. "
1872 f"Max diff: {max_diff:.6f}, max rel diff: {max_rel_diff:.6f})"
1873 )
1874 except AssertionError as e:
1875 failures.append(str(e))
1876
1877 # Report all failures at once
1878 if failures:
1879 # Create a summary for the single-line failure message
1880 backend_names = []
1881 for f in failures:
1882 if "[AttentionBackendEnum." in f:
1883 backend_name = f.split("[")[1].split("]")[0]
1884 backend_names.append(backend_name)
1885
1886 summary = f"{len(failures)} backend(s) failed: {', '.join(backend_names)}"
1887 detailed_msg = "\n".join(failures)
1888 pytest.fail(f"{summary}\n{detailed_msg}")
1889
1890
1891 @pytest.mark.parametrize(
1892 "batch_spec_name",
1893 [
1894 "small_decode",
1895 "small_prefill",
1896 "mixed_small",
1897 "medium_decode",
1898 "medium_prefill",
1899 "mixed_medium",
1900 "large_decode",
1901 "large_prefill",
1902 "single_decode",
1903 "single_prefill",
1904 "spec_decode_small",
1905 "spec_decode_medium",
1906 ],
1907 )
1908 @pytest.mark.parametrize("model", ["deepseek-ai/DeepSeek-R1"])
1909 @pytest.mark.parametrize("tensor_parallel_size", [1, 4, 8, 16])
1910 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
1911 @pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
1912 @pytest.mark.parametrize(
1913 ("prefill_backend", "qk_nope_head_dim", "v_head_dim"),
1914 _prefill_backend_dimension_params(),
1915 )
1916 def test_backend_correctness(
1917 default_vllm_config,
1918 dist_init,
1919 workspace_init,
1920 batch_spec_name: str,
1921 model: str,
1922 tensor_parallel_size: int,
1923 kv_cache_dtype: str,
1924 q_scale: float,
1925 k_scale: float,
1926 prefill_backend: MLAPrefillBackendEnum,
1927 qk_nope_head_dim: int,
1928 v_head_dim: int,
1929 ):
1930 _run_backend_correctness(
1931 default_vllm_config,
1932 dist_init,
1933 workspace_init,
1934 batch_spec_name,
1935 model,
1936 tensor_parallel_size,
1937 kv_cache_dtype,
1938 q_scale,
1939 k_scale,
1940 prefill_backend,
1941 qk_nope_head_dim,
1942 v_head_dim,
1943 )
1944
1945
1946 @pytest.mark.parametrize(
1947 ("prefill_backend", "qk_nope_head_dim", "v_head_dim"),
1948 _prefill_backend_dimension_params(),
1949 )
1950 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
1951 def test_chunked_context_backend_correctness(
1952 default_vllm_config,
1953 dist_init,
1954 workspace_init,
1955 prefill_backend: MLAPrefillBackendEnum,
1956 qk_nope_head_dim: int,
1957 v_head_dim: int,
1958 kv_cache_dtype: str,
1959 ):
1960 """Split, packed, and context-free requests match the SDPA reference."""
1961 _run_backend_correctness(
1962 default_vllm_config,
1963 dist_init,
1964 workspace_init,
1965 batch_spec_name="chunked_context_prefill",
1966 model="deepseek-ai/DeepSeek-R1",
1967 tensor_parallel_size=16,
1968 kv_cache_dtype=kv_cache_dtype,
1969 q_scale=1.0,
1970 k_scale=1.0,
1971 prefill_backend=prefill_backend,
1972 qk_nope_head_dim=qk_nope_head_dim,
1973 v_head_dim=v_head_dim,
1974 chunked_prefill_workspace_size=1024,
1975 )