main
py 1,344 lines 46.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """MLA benchmark runner - shared utilities for MLA benchmarks.
5
6 This module provides helpers for running MLA backends without
7 needing full VllmConfig integration.
8 """
9
10 import statistics
11 import tempfile
12 from pathlib import Path
13
14 import numpy as np
15 import torch
16 from batch_spec import parse_batch_spec
17 from common import (
18 BenchmarkResult,
19 MockHfConfig,
20 MockIndexer,
21 MockLayer,
22 run_do_bench,
23 run_ncu_profile,
24 setup_mla_dims,
25 )
26
27 from vllm.config import (
28 CacheConfig,
29 CompilationConfig,
30 ModelConfig,
31 ParallelConfig,
32 SchedulerConfig,
33 VllmConfig,
34 set_current_vllm_config,
35 )
36 from vllm.model_executor.layers.linear import ColumnParallelLinear
37 from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
38
39
40 def _safe_profile_name(value: str) -> str:
41 return "".join(c if c.isalnum() or c in "._-" else "_" for c in value)
42
43
44 def _create_kv_b_proj(
45 mla_dims: dict,
46 device: torch.device,
47 ):
48 kv_b_proj = ColumnParallelLinear(
49 mla_dims["kv_lora_rank"],
50 mla_dims["num_q_heads"]
51 * (mla_dims["qk_nope_head_dim"] + mla_dims["v_head_dim"]),
52 bias=False,
53 params_dtype=torch.bfloat16,
54 quant_config=None,
55 prefix="benchmark.kv_b_proj",
56 ).to(device)
57 with torch.no_grad():
58 kv_b_proj.weight.copy_(torch.randn_like(kv_b_proj.weight))
59 return kv_b_proj
60
61
62 def _ensure_single_rank_model_parallel() -> None:
63 import torch.distributed as dist
64
65 from vllm.distributed import (
66 ensure_model_parallel_initialized,
67 init_distributed_environment,
68 model_parallel_is_initialized,
69 )
70
71 if not dist.is_available():
72 return
73 if not dist.is_initialized():
74 with tempfile.NamedTemporaryFile(
75 prefix="vllm_bench_dist_", delete=False
76 ) as init_file:
77 distributed_init_method = f"file://{init_file.name}"
78 init_distributed_environment(
79 world_size=1,
80 rank=0,
81 distributed_init_method=distributed_init_method,
82 local_rank=0,
83 backend="nccl",
84 )
85 if not model_parallel_is_initialized():
86 ensure_model_parallel_initialized(1, 1)
87
88
89 # ============================================================================
90 # VllmConfig Creation
91 # ============================================================================
92
93
94 def _add_mock_methods_to_model_config(model_config: ModelConfig) -> None:
95 """Add mock methods for layer-specific queries to ModelConfig.
96
97 These methods are needed by metadata builders but aren't normally
98 present on ModelConfig when used in benchmark contexts.
99 """
100 import types
101
102 model_config.get_num_layers = types.MethodType(lambda self: 1, model_config)
103 model_config.get_sliding_window_for_layer = types.MethodType(
104 lambda self, _i: None, model_config
105 )
106 model_config.get_logits_soft_cap_for_layer = types.MethodType(
107 lambda self, _i: None, model_config
108 )
109 model_config.get_sm_scale_for_layer = types.MethodType(
110 lambda self, _i: 1.0 / model_config.get_head_size() ** 0.5, model_config
111 )
112
113
114 def create_minimal_vllm_config(
115 model_name: str = "deepseek-v3",
116 block_size: int = 128,
117 max_num_seqs: int = 256,
118 max_num_batched_tokens: int = 8192,
119 max_model_len: int = 32768,
120 mla_dims: dict | None = None,
121 index_topk: int | None = None,
122 prefill_backend: str | None = None,
123 kv_cache_dtype: str = "auto",
124 sparse_mla_force_mqa: bool = False,
125 ) -> VllmConfig:
126 """Create minimal VllmConfig for MLA benchmarks.
127
128 Args:
129 model_name: Model name (deepseek-v2, deepseek-v3, etc.) - used if mla_dims not
130 provided
131 block_size: KV cache block size
132 max_num_seqs: Maximum number of sequences
133 max_num_batched_tokens: Maximum number of batched tokens
134 max_model_len: Maximum model context length
135 mla_dims: Optional custom MLA dimensions dict. If not provided, uses
136 setup_mla_dims(model_name)
137 index_topk: Optional topk value for sparse MLA backends. If provided,
138 the config will include index_topk for sparse attention.
139 prefill_backend: Prefill backend name (e.g., "fa3", "fa4", "flashinfer",
140 "trtllm"). Configures the attention config to force
141 the specified prefill backend.
142 kv_cache_dtype: KV cache dtype, e.g. "auto" or "fp8"
143 sparse_mla_force_mqa: If True, forces all sparse MLA tokens through
144 forward_mqa (even prefill tokens).
145
146 Returns:
147 VllmConfig for benchmarking
148
149 """
150 # Get MLA dimensions - use provided or load from model name
151 if mla_dims is None:
152 mla_dims = setup_mla_dims(model_name)
153
154 # Create mock HF config first (avoids downloading from HuggingFace)
155 mock_hf_config = MockHfConfig(mla_dims, index_topk=index_topk)
156
157 # Create a temporary minimal config.json to avoid HF downloads
158 # This ensures consistent ModelConfig construction without network access
159 import json
160 import os
161 import shutil
162 import tempfile
163
164 minimal_config = {
165 "architectures": ["DeepseekV2ForCausalLM"],
166 "model_type": "deepseek_v2",
167 "num_attention_heads": mla_dims["num_q_heads"],
168 "num_key_value_heads": mla_dims["num_kv_heads"],
169 "hidden_size": mla_dims["head_dim"] * mla_dims["num_q_heads"],
170 "torch_dtype": "bfloat16",
171 "max_position_embeddings": 163840, # DeepSeek V3 default
172 "rope_theta": 10000.0,
173 "vocab_size": 128256,
174 }
175
176 # Create temporary directory with config.json
177 temp_dir = tempfile.mkdtemp(prefix="vllm_bench_")
178 config_path = os.path.join(temp_dir, "config.json")
179 with open(config_path, "w") as f:
180 json.dump(minimal_config, f)
181
182 try:
183 # Create model config using local path - no HF downloads
184 model_config = ModelConfig(
185 model=temp_dir, # Use local temp directory
186 tokenizer=None,
187 tokenizer_mode="auto",
188 trust_remote_code=True,
189 dtype="bfloat16",
190 seed=0,
191 max_model_len=max_model_len,
192 quantization=None,
193 enforce_eager=False,
194 max_logprobs=20,
195 disable_sliding_window=False,
196 skip_tokenizer_init=True,
197 served_model_name=None,
198 limit_mm_per_prompt=None,
199 config_format="auto",
200 )
201 finally:
202 # Clean up temporary directory
203 shutil.rmtree(temp_dir, ignore_errors=True)
204
205 # Override with our mock config
206 model_config.hf_config = mock_hf_config
207 model_config.hf_text_config = mock_hf_config
208
209 # Add mock methods for layer-specific queries
210 _add_mock_methods_to_model_config(model_config)
211
212 # Create sub-configs
213 cache_config = CacheConfig(
214 block_size=block_size,
215 gpu_memory_utilization=0.9,
216 cache_dtype=kv_cache_dtype,
217 enable_prefix_caching=False,
218 )
219
220 scheduler_config = SchedulerConfig(
221 max_num_seqs=max_num_seqs,
222 max_num_batched_tokens=max(max_num_batched_tokens, max_num_seqs),
223 max_model_len=max_model_len,
224 is_encoder_decoder=False,
225 enable_chunked_prefill=True,
226 )
227
228 parallel_config = ParallelConfig(
229 tensor_parallel_size=1,
230 )
231
232 compilation_config = CompilationConfig()
233
234 vllm_config = VllmConfig(
235 model_config=model_config,
236 cache_config=cache_config,
237 parallel_config=parallel_config,
238 scheduler_config=scheduler_config,
239 compilation_config=compilation_config,
240 )
241
242 if prefill_backend is not None:
243 prefill_cfg = get_prefill_backend_config(prefill_backend)
244 vllm_config.attention_config.mla_prefill_backend = prefill_cfg[
245 "mla_prefill_backend"
246 ]
247 if prefill_cfg["flash_attn_version"] is not None:
248 vllm_config.attention_config.flash_attn_version = prefill_cfg[
249 "flash_attn_version"
250 ]
251
252 if sparse_mla_force_mqa:
253 vllm_config.attention_config.sparse_mla_force_mqa = True
254
255 return vllm_config
256
257
258 # ============================================================================
259 # Prefill Backend Configuration
260 # ============================================================================
261
262 # Maps prefill backend names to attention config overrides.
263 # FA backends set flash_attn_version and disable non-FA paths.
264 # Non-FA backends enable their specific path and disable others.
265 _PREFILL_BACKEND_CONFIG: dict[str, dict] = {
266 "fa2": {
267 "flash_attn_version": 2,
268 "mla_prefill_backend": MLAPrefillBackendEnum.FLASH_ATTN,
269 },
270 "fa3": {
271 "flash_attn_version": 3,
272 "mla_prefill_backend": MLAPrefillBackendEnum.FLASH_ATTN,
273 },
274 "fa4": {
275 "flash_attn_version": 4,
276 "mla_prefill_backend": MLAPrefillBackendEnum.FLASH_ATTN,
277 },
278 "flashinfer": {
279 "flash_attn_version": None,
280 "mla_prefill_backend": MLAPrefillBackendEnum.FLASHINFER,
281 },
282 "trtllm": {
283 "flash_attn_version": None,
284 "mla_prefill_backend": MLAPrefillBackendEnum.TRTLLM_RAGGED,
285 },
286 "tokenspeed": {
287 "flash_attn_version": None,
288 "mla_prefill_backend": MLAPrefillBackendEnum.TOKENSPEED_MLA,
289 },
290 }
291
292
293 def get_prefill_backend_config(prefill_backend: str) -> dict:
294 """Get attention config overrides for a prefill backend."""
295 if prefill_backend not in _PREFILL_BACKEND_CONFIG:
296 raise ValueError(
297 f"Unknown prefill backend: {prefill_backend!r}. "
298 f"Available: {list(_PREFILL_BACKEND_CONFIG.keys())}"
299 )
300 return _PREFILL_BACKEND_CONFIG[prefill_backend]
301
302
303 # ============================================================================
304 # Decode Backend Configuration
305 # ============================================================================
306
307
308 # Backend-specific properties that can't be inferred from the backend class
309 # Keys are AttentionBackendEnum names (uppercase)
310 _BACKEND_PROPERTIES = {
311 "FLASHMLA": {
312 "query_format": "concat", # Single concatenated tensor (vs tuple)
313 },
314 "FLASHMLA_SPARSE": {
315 "query_format": "concat", # Single concatenated tensor (vs tuple)
316 },
317 "FLASHINFER_MLA_SPARSE": {
318 "query_format": "concat", # Single concatenated tensor (vs tuple)
319 },
320 }
321
322
323 def _get_backend_config(backend: str) -> dict:
324 """Get backend configuration from AttentionBackendEnum.
325
326 Uses the registry to get the backend class and extract configuration
327 from its methods (get_impl_cls, get_builder_cls, is_sparse, etc.).
328
329 Args:
330 backend: Backend name matching AttentionBackendEnum exactly
331 (e.g., "FLASHMLA_SPARSE")
332
333 Returns:
334 Dict with backend configuration
335
336 """
337 from vllm.v1.attention.backend import MultipleOf
338 from vllm.v1.attention.backends.registry import AttentionBackendEnum
339
340 try:
341 backend_enum = AttentionBackendEnum[backend]
342 backend_class = backend_enum.get_class()
343 except (KeyError, ValueError) as e:
344 valid_backends = [e.name for e in AttentionBackendEnum if e.name != "CUSTOM"]
345 raise ValueError(
346 f"Unknown backend: {backend}. "
347 f"Valid MLA backends: {[b for b in valid_backends if 'MLA' in b]}"
348 ) from e
349
350 # Get block size from backend class
351 block_sizes = backend_class.get_supported_kernel_block_sizes()
352 # Use first supported block size (backends typically support one for MLA)
353 block_size = block_sizes[0] if block_sizes else None
354 if isinstance(block_size, MultipleOf):
355 # No fixed block size; fall back to config value
356 block_size = None
357
358 # Check if sparse via class method if available
359 is_sparse = getattr(backend_class, "is_sparse", lambda: False)()
360
361 # Get properties that can't be inferred
362 props = _BACKEND_PROPERTIES.get(backend, {})
363
364 return {
365 "backend_class": backend_class,
366 "impl_class": backend_class.get_impl_cls(),
367 "builder_class": backend_class.get_builder_cls(),
368 "query_format": props.get("query_format", "tuple"),
369 "block_size": block_size,
370 "is_sparse": is_sparse,
371 }
372
373
374 # ============================================================================
375 # Metadata Building Helpers
376 # ============================================================================
377
378
379 def _build_attention_metadata(
380 requests: list,
381 block_size: int,
382 device: torch.device,
383 builder_instance,
384 ) -> tuple:
385 """Build attention metadata from batch requests.
386
387 Args:
388 requests: List of BatchRequest objects
389 block_size: KV cache block size
390 device: Target device
391 builder_instance: Metadata builder instance
392
393 Returns:
394 Tuple of (metadata, kv_cache_num_blocks)
395
396 """
397 q_lens = [r.q_len for r in requests]
398 kv_lens = [r.kv_len for r in requests]
399 total_q = sum(q_lens)
400 max_kv = max(kv_lens)
401
402 # Build query start locations
403 q_start_cpu = torch.tensor(
404 [0] + [sum(q_lens[: i + 1]) for i in range(len(q_lens))],
405 dtype=torch.int32,
406 )
407 q_start_gpu = q_start_cpu.to(device)
408
409 # Build sequence lengths
410 seq_lens_cpu = torch.tensor(kv_lens, dtype=torch.int32)
411 seq_lens_gpu = seq_lens_cpu.to(device)
412
413 # Build block table
414 num_blocks_per_req = [(kv + block_size - 1) // block_size for kv in kv_lens]
415 max_num_blocks = max(num_blocks_per_req)
416
417 block_table_cpu = np.zeros((len(requests), max_num_blocks), dtype=np.int32)
418 current_block = 0
419 for i, num_blocks in enumerate(num_blocks_per_req):
420 for j in range(num_blocks):
421 block_table_cpu[i, j] = current_block
422 current_block += 1
423
424 block_table_gpu = torch.from_numpy(block_table_cpu).to(device)
425
426 # Build slot mapping
427 slot_mapping_list = []
428 for i, (q_len, kv_len, num_blocks) in enumerate(
429 zip(q_lens, kv_lens, num_blocks_per_req)
430 ):
431 context_len = kv_len - q_len
432 for j in range(q_len):
433 token_kv_idx = context_len + j
434 block_idx = token_kv_idx // block_size
435 offset_in_block = token_kv_idx % block_size
436 global_block_id = block_table_cpu[i, block_idx]
437 slot_id = global_block_id * block_size + offset_in_block
438 slot_mapping_list.append(slot_id)
439
440 slot_mapping = torch.tensor(slot_mapping_list, dtype=torch.int64, device=device)
441
442 # Create CommonAttentionMetadata
443 from vllm.v1.attention.backends.utils import CommonAttentionMetadata
444
445 common_attn_metadata = CommonAttentionMetadata(
446 num_reqs=len(requests),
447 max_query_len=max(q_lens),
448 max_seq_len=max_kv,
449 num_actual_tokens=total_q,
450 query_start_loc=q_start_gpu,
451 query_start_loc_cpu=q_start_cpu,
452 seq_lens=seq_lens_gpu,
453 seq_lens_cpu_upper_bound=seq_lens_cpu,
454 slot_mapping=slot_mapping,
455 block_table_tensor=block_table_gpu,
456 dcp_local_seq_lens=None,
457 )
458
459 # Use the production build() method
460 metadata = builder_instance.build(
461 common_prefix_len=0,
462 common_attn_metadata=common_attn_metadata,
463 fast_build=False,
464 )
465
466 return metadata, current_block
467
468
469 def _create_input_tensors(
470 total_q: int,
471 mla_dims: dict,
472 query_format: str,
473 device: torch.device,
474 dtype: torch.dtype,
475 ):
476 """Create input tensors for both decode and prefill modes.
477
478 MLA requires different tensor formats for decode vs prefill:
479 - Decode: Uses kv_lora_rank (512) dimension
480 - Prefill: Uses qk_nope_head_dim (128) to stay under FlashAttention's 256 limit
481
482 Args:
483 total_q: Total number of query tokens
484 mla_dims: MLA dimension configuration
485 query_format: Either "tuple" or "concat"
486 device: Target device
487 dtype: Tensor dtype
488
489 Returns:
490 Tuple of (decode_inputs, prefill_inputs)
491 - decode_inputs: Query tensor(s) for decode mode
492 - prefill_inputs: Dict with 'q', 'k_c_normed', 'k_pe', 'k_scale' for prefill
493
494 """
495 if query_format == "tuple":
496 # Decode mode format: (q_nope, q_pe) where q_nope has kv_lora_rank dim
497 q_nope_decode = torch.randn(
498 total_q,
499 mla_dims["num_q_heads"],
500 mla_dims["kv_lora_rank"],
501 device=device,
502 dtype=dtype,
503 )
504 q_pe = torch.randn(
505 total_q,
506 mla_dims["num_q_heads"],
507 mla_dims["qk_rope_head_dim"],
508 device=device,
509 dtype=dtype,
510 )
511 decode_inputs = (q_nope_decode, q_pe)
512
513 # For prefill, we need q with qk_nope_head_dim instead of kv_lora_rank
514 q_nope_prefill = torch.randn(
515 total_q,
516 mla_dims["num_q_heads"],
517 mla_dims["qk_nope_head_dim"],
518 device=device,
519 dtype=dtype,
520 )
521 prefill_q = torch.cat([q_nope_prefill, q_pe], dim=-1)
522 else: # concat
523 decode_inputs = torch.randn(
524 total_q,
525 mla_dims["num_q_heads"],
526 mla_dims["kv_lora_rank"] + mla_dims["qk_rope_head_dim"],
527 device=device,
528 dtype=dtype,
529 )
530 # For prefill with concat format
531 prefill_q = torch.randn(
532 total_q,
533 mla_dims["num_q_heads"],
534 mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"],
535 device=device,
536 dtype=dtype,
537 )
538
539 # Create additional inputs needed for prefill forward
540 k_c_normed = torch.randn(
541 total_q,
542 mla_dims["kv_lora_rank"],
543 device=device,
544 dtype=dtype,
545 )
546 k_pe = torch.randn(
547 total_q,
548 1, # Single head for MLA
549 mla_dims["qk_rope_head_dim"],
550 device=device,
551 dtype=dtype,
552 )
553 k_scale = torch.ones(1, device=device, dtype=torch.float32)
554
555 output = torch.zeros(
556 total_q,
557 mla_dims["num_q_heads"] * mla_dims["v_head_dim"],
558 device=device,
559 dtype=dtype,
560 )
561
562 prefill_inputs = {
563 "q": prefill_q,
564 "k_c_normed": k_c_normed,
565 "k_pe": k_pe,
566 "k_scale": k_scale,
567 "output": output,
568 }
569
570 return decode_inputs, prefill_inputs
571
572
573 # ============================================================================
574 # Backend Initialization
575 # ============================================================================
576
577
578 def _create_backend_impl(
579 backend_cfg: dict,
580 mla_dims: dict,
581 vllm_config: VllmConfig,
582 device: torch.device,
583 max_num_tokens: int = 8192,
584 index_topk: int | None = None,
585 kv_cache_dtype: str = "auto",
586 ):
587 """Create backend implementation instance.
588
589 Args:
590 backend_cfg: Backend configuration dict from _get_backend_config()
591 mla_dims: MLA dimension configuration
592 vllm_config: VllmConfig instance
593 device: Target device
594 max_num_tokens: Maximum number of tokens for sparse indexer buffer
595 index_topk: Topk value for sparse MLA backends
596 kv_cache_dtype: KV cache dtype, e.g. "auto" or "fp8"
597
598 Returns:
599 Tuple of (impl, layer, builder_instance, indexer)
600
601 """
602 # Get classes from backend config (already resolved by _get_backend_config)
603 impl_class = backend_cfg["impl_class"]
604 builder_class = backend_cfg["builder_class"]
605
606 # Calculate scale
607 scale = 1.0 / np.sqrt(mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"])
608
609 kv_b_proj = _create_kv_b_proj(mla_dims, device)
610
611 # Create indexer for sparse backends
612 indexer = None
613 if backend_cfg.get("is_sparse", False):
614 if index_topk is None:
615 index_topk = 2048 # Default topk for sparse MLA
616 indexer = MockIndexer(
617 max_num_tokens=max_num_tokens,
618 topk_tokens=index_topk,
619 device=device,
620 )
621
622 # Build impl kwargs
623 impl_kwargs = {
624 "num_heads": mla_dims["num_q_heads"],
625 "head_size": mla_dims["head_dim"],
626 "scale": scale,
627 "num_kv_heads": mla_dims["num_kv_heads"],
628 "alibi_slopes": None,
629 "sliding_window": None,
630 "kv_cache_dtype": kv_cache_dtype,
631 "logits_soft_cap": None,
632 "attn_type": "decoder",
633 "kv_sharing_target_layer_name": None,
634 "q_lora_rank": None,
635 "kv_lora_rank": mla_dims["kv_lora_rank"],
636 "qk_nope_head_dim": mla_dims["qk_nope_head_dim"],
637 "qk_rope_head_dim": mla_dims["qk_rope_head_dim"],
638 "qk_head_dim": mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"],
639 "v_head_dim": mla_dims["v_head_dim"],
640 "kv_b_proj": kv_b_proj,
641 }
642
643 # Add indexer for sparse backends
644 if indexer is not None:
645 impl_kwargs["indexer"] = indexer
646
647 # Create impl
648 impl = impl_class(**impl_kwargs)
649
650 # Initialize DCP attributes
651 if not hasattr(impl, "dcp_world_size") or impl.dcp_world_size in (None, -1):
652 impl.dcp_world_size = 1
653 impl.dcp_rank = 0
654
655 # Create KV cache spec for MockLayer
656 from vllm.v1.kv_cache_interface import FullAttentionSpec
657
658 kv_cache_spec = FullAttentionSpec(
659 block_size=backend_cfg["block_size"] or vllm_config.cache_config.block_size,
660 num_kv_heads=1, # MLA uses 1 KV head
661 head_size=576, # MLA head dim
662 dtype=torch.bfloat16,
663 )
664
665 # Create mock layer
666 layer = MockLayer(device, impl=impl, kv_cache_spec=kv_cache_spec)
667
668 # Attach a prefill backend (MLAAttention does this in __init__; the metadata
669 # builder reads layer.prefill_backend from static_forward_context).
670 from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
671
672 prefill_backend_cls = get_mla_prefill_backend(vllm_config)
673 layer.prefill_backend = prefill_backend_cls(
674 num_heads=mla_dims["num_q_heads"],
675 scale=(mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"]) ** -0.5,
676 kv_lora_rank=mla_dims["kv_lora_rank"],
677 qk_nope_head_dim=mla_dims["qk_nope_head_dim"],
678 qk_rope_head_dim=mla_dims["qk_rope_head_dim"],
679 v_head_dim=mla_dims["v_head_dim"],
680 vllm_config=vllm_config,
681 )
682
683 # Create builder instance if needed
684 builder_instance = None
685 if builder_class:
686 # Populate static_forward_context so builder can find the layer
687 # MockLayer inherits from AttentionLayerBase, so isinstance checks pass
688 vllm_config.compilation_config.static_forward_context = {"placeholder": layer}
689
690 builder_instance = builder_class(
691 kv_cache_spec=kv_cache_spec,
692 layer_names=["placeholder"],
693 vllm_config=vllm_config,
694 device=device,
695 )
696
697 return impl, layer, builder_instance, indexer
698
699
700 # ============================================================================
701 # Config Helpers
702 # ============================================================================
703
704
705 def _extract_mla_dims_from_config(config) -> dict | None:
706 """Extract MLA dimensions from BenchmarkConfig if all required fields are present.
707
708 Args:
709 config: BenchmarkConfig instance
710
711 Returns:
712 Dict with MLA dimensions if all fields are provided, None otherwise
713
714 """
715 # Check if all MLA-specific fields are provided
716 if all(
717 [
718 config.kv_lora_rank is not None,
719 config.qk_nope_head_dim is not None,
720 config.qk_rope_head_dim is not None,
721 config.v_head_dim is not None,
722 ]
723 ):
724 return {
725 "kv_lora_rank": config.kv_lora_rank,
726 "qk_nope_head_dim": config.qk_nope_head_dim,
727 "qk_rope_head_dim": config.qk_rope_head_dim,
728 "v_head_dim": config.v_head_dim,
729 "num_q_heads": config.num_q_heads,
730 "num_kv_heads": config.num_kv_heads,
731 "head_dim": config.head_dim,
732 }
733 # Fallback: if MLA fields not fully specified, try to construct from basic fields
734 elif config.head_dim == 576:
735 # This looks like a DeepSeek MLA config, use standard dimensions with custom
736 # head count
737 return {
738 "kv_lora_rank": 512,
739 "qk_nope_head_dim": 128,
740 "qk_rope_head_dim": 64,
741 "v_head_dim": 128,
742 "num_q_heads": config.num_q_heads,
743 "num_kv_heads": config.num_kv_heads,
744 "head_dim": config.head_dim,
745 }
746 return None
747
748
749 # ============================================================================
750 # Benchmark Execution
751 # ============================================================================
752
753
754 def _run_single_benchmark(
755 config,
756 impl,
757 layer,
758 builder_instance,
759 backend_cfg: dict,
760 mla_dims: dict,
761 device: torch.device,
762 indexer=None,
763 kv_cache_dtype: str | None = None,
764 output_scale: float | None = None,
765 fuse_quant_op: bool = False,
766 ) -> BenchmarkResult:
767 """Run a single benchmark iteration.
768
769 Args:
770 config: BenchmarkConfig instance
771 impl: Backend implementation instance
772 layer: MockLayer instance
773 builder_instance: Metadata builder instance
774 backend_cfg: Backend configuration dict
775 mla_dims: MLA dimension configuration
776 device: Target device
777 indexer: Optional MockIndexer for sparse backends
778 kv_cache_dtype: KV cache dtype. None keeps the backend default.
779 output_scale: Static per-tensor FP8 scale for prefill output. None
780 keeps the plain bf16 output (no quantization).
781 fuse_quant_op: With output_scale set, True lets the prefill kernel write
782 FP8 directly; False runs bf16 attention then a standalone static-FP8
783 quant. The delta isolates the saved post-quant kernel.
784
785 Returns:
786 BenchmarkResult with timing statistics
787
788 """
789 # Parse batch spec
790 requests = parse_batch_spec(config.batch_spec)
791 q_lens = [r.q_len for r in requests]
792 kv_lens = [r.kv_len for r in requests]
793 total_q = sum(q_lens)
794 max_kv_len = max(kv_lens)
795
796 # Determine block size
797 block_size = backend_cfg["block_size"] or config.block_size
798
799 # Build metadata
800 metadata, num_blocks = _build_attention_metadata(
801 requests, block_size, device, builder_instance
802 )
803
804 # Create KV cache
805 if kv_cache_dtype is None:
806 kv_cache_dtype = getattr(config, "kv_cache_dtype", "auto")
807 head_size = mla_dims["kv_lora_rank"] + mla_dims["qk_rope_head_dim"]
808 if kv_cache_dtype == "fp8_ds_mla":
809 # FlashMLA sparse custom format: 656 bytes per token, stored as uint8.
810 # Layout: kv_lora_rank fp8 bytes + 4 float32 tile scales
811 # + 2*rope_dim bf16 bytes
812 # = 512 + 16 + 128 = 656 bytes for DeepSeek dims.
813 kv_cache = torch.zeros(
814 num_blocks,
815 block_size,
816 656,
817 device=device,
818 dtype=torch.uint8,
819 )
820 elif kv_cache_dtype == "fp8":
821 from vllm.platforms import current_platform
822
823 kv_cache = torch.zeros(
824 num_blocks,
825 block_size,
826 head_size,
827 device=device,
828 dtype=torch.uint8,
829 ).view(current_platform.fp8_dtype())
830 else:
831 kv_cache = torch.zeros(
832 num_blocks,
833 block_size,
834 head_size,
835 device=device,
836 dtype=torch.bfloat16,
837 )
838
839 # Fill indexer with random indices for sparse backends
840 is_sparse = backend_cfg.get("is_sparse", False)
841 if is_sparse and indexer is not None:
842 indexer.fill_indices(
843 total_q,
844 max_kv_len,
845 getattr(config, "sparse_mla_topk_pattern", "random"),
846 )
847
848 # Determine which forward methods to use based on metadata.
849 # Non-sparse backends use .decode/.prefill sub-objects.
850 # Sparse backends use num_decode_tokens/num_prefills directly.
851 #
852 # sparse_mla_force_mqa overrides: even for prefill metadata, use MQA.
853 force_mqa = getattr(config, "sparse_mla_force_mqa", False)
854 force_dense_mha = getattr(config, "sparse_mla_mha_mode", "auto") == "dense"
855 if force_mqa:
856 has_decode = True
857 has_prefill = False
858 elif is_sparse:
859 has_decode = metadata.num_decode_tokens > 0
860 has_prefill = metadata.num_prefills > 0
861 else:
862 has_decode = metadata.decode is not None
863 has_prefill = metadata.prefill is not None
864 if not has_decode and not has_prefill:
865 raise RuntimeError("Metadata has neither decode nor prefill metadata")
866 if is_sparse and force_dense_mha and not has_prefill:
867 raise RuntimeError(
868 "Sparse MLA dense_mha benchmark did not produce prefill metadata. "
869 "Check reorder_batch_threshold/path forcing."
870 )
871
872 num_decode = (
873 metadata.num_decode_tokens
874 if (has_decode and has_prefill)
875 else total_q
876 if has_decode
877 else 0
878 )
879 num_prefill = total_q - num_decode
880
881 # Some backends requires fp8 queries when using fp8 KV cache.
882 is_fp8_kvcache = kv_cache_dtype.startswith("fp8")
883 quantize_query = is_fp8_kvcache and getattr(
884 impl, "supports_quant_query_input", False
885 )
886
887 # quantize_query forces concat format
888 query_fmt = "concat" if quantize_query else backend_cfg["query_format"]
889
890 # Create decode query tensors
891 if has_decode:
892 decode_inputs, _ = _create_input_tensors(
893 num_decode, mla_dims, query_fmt, device, torch.bfloat16
894 )
895 # Cast decode query to fp8 if the backend supports it
896 if quantize_query:
897 from vllm.platforms import current_platform
898
899 if isinstance(decode_inputs, tuple):
900 decode_inputs = torch.cat(list(decode_inputs), dim=-1)
901 decode_inputs = decode_inputs.to(current_platform.fp8_dtype())
902
903 # Create prefill input tensors
904 if has_prefill:
905 _, prefill_inputs = _create_input_tensors(
906 num_prefill, mla_dims, query_fmt, device, torch.bfloat16
907 )
908
909 # Prefill FP8 output: fused (kernel writes e4m3) vs separate post-quant.
910 prefill_fp8_output = None
911 prefill_output_scale = None
912 prefill_quant_op = None
913 if has_prefill and output_scale is not None:
914 from vllm.platforms import current_platform
915
916 prefill_output_scale = torch.tensor(
917 [output_scale], device=device, dtype=torch.float32
918 )
919 if fuse_quant_op:
920 prefill_fp8_output = torch.empty_like(
921 prefill_inputs["output"], dtype=current_platform.fp8_dtype()
922 )
923 else:
924 from vllm.model_executor.layers.quantization.input_quant_fp8 import (
925 QuantFP8,
926 )
927 from vllm.model_executor.layers.quantization.utils.quant_utils import (
928 GroupShape,
929 )
930
931 prefill_quant_op = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
932
933 fused_output = output_scale is not None and fuse_quant_op
934 mha_mode = getattr(config, "sparse_mla_mha_mode", "auto")
935
936 # Build forward function (runs a single decode/prefill pass)
937 def forward_fn():
938 results = []
939 impl._sparse_mla_force_masked_mha = ( # type: ignore[attr-defined]
940 mha_mode == "masked"
941 )
942 impl._sparse_mla_force_dense_mha = ( # type: ignore[attr-defined]
943 mha_mode == "dense"
944 )
945 if has_decode:
946 results.append(impl.forward_mqa(decode_inputs, kv_cache, metadata, layer))
947 if has_prefill:
948 out = impl.forward_mha(
949 prefill_inputs["q"],
950 prefill_inputs["k_c_normed"],
951 prefill_inputs["k_pe"],
952 kv_cache,
953 metadata,
954 prefill_inputs["k_scale"],
955 prefill_fp8_output if fused_output else prefill_inputs["output"],
956 )
957 if fused_output:
958 out = prefill_fp8_output
959 elif prefill_quant_op is not None:
960 out, _ = prefill_quant_op(
961 prefill_inputs["output"], prefill_output_scale
962 )
963 results.append(out)
964 return results[0] if len(results) == 1 else tuple(results)
965
966 def benchmark_fn():
967 for _ in range(config.num_layers):
968 forward_fn()
969
970 if config.ncu_profile:
971 run_ncu_profile(benchmark_fn)
972 return BenchmarkResult(
973 config=config,
974 mean_time=0.0,
975 median_time=0.0,
976 std_time=0.0,
977 min_time=0.0,
978 max_time=0.0,
979 throughput_tokens_per_sec=0.0,
980 )
981
982 if config.torch_profile:
983 profile_dir = Path(
984 config.torch_profile_dir or "benchmark_outputs/torch_profiles"
985 )
986 profile_dir.mkdir(parents=True, exist_ok=True)
987 trace_name = _safe_profile_name(f"{config.backend}_{config.batch_spec}")
988 trace_path = profile_dir / f"{trace_name}.json"
989 iters = max(config.torch_profile_iters, 1)
990
991 forward_fn()
992 torch.accelerator.synchronize()
993 with torch.profiler.profile(
994 activities=[
995 torch.profiler.ProfilerActivity.CPU,
996 torch.profiler.ProfilerActivity.CUDA,
997 ],
998 record_shapes=True,
999 profile_memory=True,
1000 with_stack=False,
1001 ) as prof:
1002 for _ in range(iters):
1003 forward_fn()
1004 torch.accelerator.synchronize()
1005 prof.step()
1006 prof.export_chrome_trace(str(trace_path))
1007 print(f"Saved PyTorch profiler trace to {trace_path}")
1008 print(
1009 prof.key_averages().table(
1010 sort_by="cuda_time_total",
1011 row_limit=25,
1012 )
1013 )
1014 return BenchmarkResult(
1015 config=config,
1016 mean_time=0.0,
1017 median_time=0.0,
1018 std_time=0.0,
1019 min_time=0.0,
1020 max_time=0.0,
1021 throughput_tokens_per_sec=0.0,
1022 )
1023
1024 all_ms = run_do_bench(benchmark_fn, config.use_cuda_graphs, config.warmup_ms)
1025
1026 # Convert ms to seconds per layer
1027 times = [t / 1000.0 / config.num_layers for t in all_ms]
1028 mean_time = statistics.mean(times)
1029
1030 return BenchmarkResult(
1031 config=config,
1032 mean_time=mean_time,
1033 median_time=statistics.median(times),
1034 std_time=statistics.stdev(times) if len(times) > 1 else 0.0,
1035 min_time=min(times),
1036 max_time=max(times),
1037 throughput_tokens_per_sec=total_q / mean_time if mean_time > 0 else 0,
1038 )
1039
1040
1041 def _run_mla_benchmark_batched(
1042 backend: str,
1043 configs_with_params: list[tuple], # [(config, threshold, num_splits), ...]
1044 index_topk: int = 2048,
1045 prefill_backend: str | None = None,
1046 sparse_mla_force_mqa: bool = False,
1047 output_scale: float | None = None,
1048 fuse_quant_op: bool = False,
1049 ) -> list[BenchmarkResult]:
1050 """Unified batched MLA benchmark runner for all backends.
1051
1052 Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
1053 flashinfer_mla_sparse, flashmla_sparse
1054
1055 This function reuses backend initialization across multiple benchmarks
1056 to avoid setup/teardown overhead.
1057
1058 Args:
1059 backend: Backend name (decode backend used for impl construction)
1060 configs_with_params: List of (config, threshold, num_splits) tuples
1061 - threshold: reorder_batch_threshold (FlashAttn/FlashMLA only)
1062 - num_splits: num_kv_splits (CUTLASS only)
1063 index_topk: Topk value for sparse MLA backends (default 2048)
1064 prefill_backend: Prefill backend name (e.g., "fa3", "fa4").
1065 When set, forces the specified FlashAttention version for prefill.
1066 output_scale: Static per-tensor FP8 scale for prefill output. None
1067 keeps the plain bf16 output (no quantization).
1068 fuse_quant_op: With output_scale set, True lets the prefill kernel write
1069 FP8 directly; False runs bf16 attention then a standalone quant.
1070 sparse_mla_force_mqa: If True, forces all sparse MLA tokens through
1071 forward_mqa (even prefill tokens).
1072
1073 Returns:
1074 List of BenchmarkResult objects
1075
1076 """
1077 if not configs_with_params:
1078 return []
1079
1080 backend_cfg = _get_backend_config(backend)
1081 device = torch.device(configs_with_params[0][0].device)
1082 torch.accelerator.set_device_index(device)
1083
1084 # Determine block size
1085 config_block_size = configs_with_params[0][0].block_size
1086 block_size = backend_cfg["block_size"] or config_block_size
1087
1088 # Extract MLA dimensions from the first config
1089 first_config = configs_with_params[0][0]
1090 mla_dims = _extract_mla_dims_from_config(first_config)
1091
1092 # If config didn't provide MLA dims, fall back to default model
1093 if mla_dims is None:
1094 mla_dims = setup_mla_dims("deepseek-v3")
1095
1096 # Determine if this is a sparse backend
1097 is_sparse = backend_cfg.get("is_sparse", False)
1098
1099 # Extract kv_cache_dtype from the first config
1100 kv_cache_dtype = getattr(first_config, "kv_cache_dtype", "auto")
1101
1102 # FlashMLA sparse only supports "fp8_ds_mla" internally (not generic "fp8").
1103 # Remap here so the user can pass --kv-cache-dtype fp8 regardless of backend.
1104 if backend.upper() == "FLASHMLA_SPARSE" and kv_cache_dtype == "fp8":
1105 kv_cache_dtype = "fp8_ds_mla"
1106
1107 # Compute max total_q across all configs so the metadata builder buffer
1108 # and scheduler config are large enough for all batch specs.
1109 max_total_q = max(
1110 sum(r.q_len for r in parse_batch_spec(cfg.batch_spec))
1111 for cfg, *_ in configs_with_params
1112 )
1113 max_model_len = max(
1114 max_total_q,
1115 max(
1116 getattr(cfg, "max_model_len", None) or 32768
1117 for cfg, *_ in configs_with_params
1118 ),
1119 )
1120
1121 # Create and set vLLM config for MLA (reused across all benchmarks)
1122 vllm_config = create_minimal_vllm_config(
1123 model_name="deepseek-v3", # Used only for model path
1124 block_size=block_size,
1125 max_num_batched_tokens=max_total_q,
1126 max_model_len=max_model_len,
1127 mla_dims=mla_dims, # Use custom dims from config or default
1128 index_topk=index_topk if is_sparse else None,
1129 prefill_backend=prefill_backend,
1130 kv_cache_dtype=kv_cache_dtype,
1131 sparse_mla_force_mqa=sparse_mla_force_mqa,
1132 )
1133
1134 results = []
1135
1136 # Initialize workspace manager (needed by metadata builders)
1137 from vllm.v1.worker.workspace import (
1138 init_workspace_manager,
1139 is_workspace_manager_initialized,
1140 )
1141
1142 if not is_workspace_manager_initialized():
1143 init_workspace_manager(device)
1144
1145 with set_current_vllm_config(vllm_config):
1146 _ensure_single_rank_model_parallel()
1147
1148 # Create backend impl, layer, builder, and indexer (reused across benchmarks)
1149 impl, layer, builder_instance, indexer = _create_backend_impl(
1150 backend_cfg,
1151 mla_dims,
1152 vllm_config,
1153 device,
1154 max_num_tokens=max_total_q,
1155 index_topk=index_topk if is_sparse else None,
1156 kv_cache_dtype=kv_cache_dtype,
1157 )
1158
1159 # Verify the actual prefill backend matches what was requested. The
1160 # selector + impl construction already raise on misuse; here we just
1161 # check the resolved class against the requested name as a sanity guard.
1162 if prefill_backend is not None:
1163 expected_class = {
1164 "fa2": "FlashAttnPrefillBackend",
1165 "fa3": "FlashAttnPrefillBackend",
1166 "fa4": "FlashAttnPrefillBackend",
1167 "flashinfer": "FlashInferPrefillBackend",
1168 "trtllm": "TrtllmRaggedPrefillBackend",
1169 "tokenspeed": "TokenspeedMLAPrefillBackend",
1170 }.get(prefill_backend)
1171 actual_class = type(getattr(layer, "prefill_backend", None)).__name__
1172 if expected_class and actual_class != expected_class:
1173 raise RuntimeError(
1174 f"Prefill backend '{prefill_backend}' requested "
1175 f"{expected_class}, got {actual_class}. Check "
1176 f"attention_config plumbing or installed deps."
1177 )
1178 if prefill_backend in {"fa2", "fa3", "fa4"}:
1179 fa_version = int(prefill_backend[2:])
1180 actual_fa_version = getattr(
1181 layer.prefill_backend, "vllm_flash_attn_version", None
1182 )
1183 if actual_fa_version != fa_version:
1184 raise RuntimeError(
1185 f"Prefill backend '{prefill_backend}' requested FA "
1186 f"version {fa_version}, got "
1187 f"{actual_fa_version} on {actual_class}."
1188 )
1189 # Run each benchmark with the shared impl
1190 for config, threshold, num_splits in configs_with_params:
1191 force_masked_mha = (
1192 is_sparse and getattr(config, "sparse_mla_mha_mode", "auto") == "masked"
1193 )
1194 if force_masked_mha:
1195 impl.masked_mha_available = True
1196 from vllm.model_executor.layers.attention.sparse_mla_attention import (
1197 GLOBAL_TOPK_MASK_MAX_BYTES,
1198 _topk_mask_shape,
1199 )
1200
1201 requests = parse_batch_spec(config.batch_spec)
1202 max_query_len = max(request.q_len for request in requests)
1203 mask_shape = _topk_mask_shape(
1204 len(requests), max_query_len, max_query_len
1205 )
1206 mask_words = max(
1207 GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
1208 mask_shape[0] * mask_shape[1] * mask_shape[2],
1209 )
1210 if (
1211 builder_instance.topk_mask_workspace is None
1212 or builder_instance.topk_mask_workspace.numel() != mask_words
1213 ):
1214 builder_instance.topk_mask_workspace = torch.zeros(
1215 mask_words,
1216 dtype=torch.int32,
1217 device=device,
1218 )
1219
1220 # Set threshold for this benchmark (FlashAttn/FlashMLA only)
1221 original_threshold = None
1222 effective_threshold = threshold
1223 force_dense_mha = (
1224 is_sparse
1225 and getattr(config, "sparse_mla_mha_mode", "auto") == "dense"
1226 and not getattr(config, "sparse_mla_force_mqa", False)
1227 )
1228 if force_dense_mha:
1229 # Sparse MLA normally treats q_len <= 1 as decode. Use an
1230 # impossible threshold so dense_mha benchmarks actually run
1231 # the prefill/MHA path, including q_len=1 short extends.
1232 effective_threshold = -1
1233 if effective_threshold is not None and builder_instance:
1234 original_threshold = builder_instance.reorder_batch_threshold
1235 builder_instance.reorder_batch_threshold = effective_threshold
1236
1237 # Set num_splits for CUTLASS
1238 original_num_splits = None
1239 if num_splits is not None and hasattr(impl, "_num_kv_splits"):
1240 original_num_splits = impl._num_kv_splits
1241 impl._num_kv_splits = num_splits
1242
1243 try:
1244 result = _run_single_benchmark(
1245 config,
1246 impl,
1247 layer,
1248 builder_instance,
1249 backend_cfg,
1250 mla_dims,
1251 device,
1252 indexer=indexer,
1253 kv_cache_dtype=kv_cache_dtype,
1254 output_scale=output_scale,
1255 fuse_quant_op=fuse_quant_op,
1256 )
1257 results.append(result)
1258
1259 finally:
1260 # Restore original threshold
1261 if original_threshold is not None:
1262 builder_instance.reorder_batch_threshold = original_threshold
1263
1264 # Restore original num_splits
1265 if original_num_splits is not None:
1266 impl._num_kv_splits = original_num_splits
1267
1268 return results
1269
1270
1271 # ============================================================================
1272 # Public API
1273 # ============================================================================
1274
1275
1276 def run_mla_benchmark(
1277 backend: str,
1278 config,
1279 reorder_batch_threshold: int | None = None,
1280 num_kv_splits: int | None = None,
1281 index_topk: int = 2048,
1282 prefill_backend: str | None = None,
1283 sparse_mla_force_mqa: bool = False,
1284 output_scale: float | None = None,
1285 fuse_quant_op: bool = False,
1286 ) -> BenchmarkResult | list[BenchmarkResult]:
1287 """Unified MLA benchmark runner for all backends.
1288
1289 Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
1290 flashinfer_mla_sparse, flashmla_sparse
1291
1292 Always uses batched execution internally for optimal performance.
1293
1294 Args:
1295 backend: Backend name (flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
1296 flashinfer_mla_sparse, flashmla_sparse)
1297 config: BenchmarkConfig or list of (BenchmarkConfig, param) tuples
1298 reorder_batch_threshold: Threshold override for FlashAttn/FlashMLA
1299 (single config mode only)
1300 num_kv_splits: Number of KV splits for CUTLASS (single config mode only)
1301 index_topk: Topk value for sparse MLA backends (default 2048)
1302 prefill_backend: Prefill backend name (e.g., "fa3", "fa4").
1303 When set, forces the specified FlashAttention version for prefill.
1304 sparse_mla_force_mqa: If True, forces all sparse MLA tokens through
1305 forward_mqa (even prefill tokens).
1306 output_scale: Static per-tensor FP8 scale for prefill output (None = bf16).
1307 fuse_quant_op: With output_scale set, fuse the FP8 write into the prefill
1308 kernel vs a standalone post-quant kernel. See _run_single_benchmark.
1309
1310 Returns:
1311 BenchmarkResult (single mode) or list of BenchmarkResult (batched mode)
1312
1313 """
1314 # Normalize to batched mode: (config, threshold, num_splits)
1315 if isinstance(config, list):
1316 # Already in batched format
1317 if len(config) > 0 and isinstance(config[0], tuple):
1318 # Format: [(cfg, param), ...] where param is threshold or num_splits
1319 if backend in ("flashattn_mla", "flashmla", "flashmla_sparse"):
1320 configs_with_params = [(cfg, param, None) for cfg, param in config]
1321 else: # cutlass_mla, flashinfer_mla, or sparse backends
1322 configs_with_params = [(cfg, None, param) for cfg, param in config]
1323 else:
1324 # Format: [cfg, ...] - just configs
1325 configs_with_params = [(cfg, None, None) for cfg in config]
1326 return_single = False
1327 else:
1328 # Single config: convert to batched format
1329 configs_with_params = [(config, reorder_batch_threshold, num_kv_splits)]
1330 return_single = True
1331
1332 # Use unified batched execution
1333 results = _run_mla_benchmark_batched(
1334 backend,
1335 configs_with_params,
1336 index_topk,
1337 prefill_backend=prefill_backend,
1338 sparse_mla_force_mqa=sparse_mla_force_mqa,
1339 output_scale=output_scale,
1340 fuse_quant_op=fuse_quant_op,
1341 )
1342
1343 # Return single result or list based on input
1344 return results[0] if return_single else results