A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 8, 08:45 UTC
[Attention] Add FlashInfer Sparse MLA backend (#33451)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com>
Matthew Bonanni committed
Feb 12, 2026 at 12:21 UTC
f2c47886fdbabfeae7ddad871ee7889ee472d026
24 files changed
+1180
-407
benchmarks/attention_benchmarks/benchmark.py
+28
-19
@@ -43,6 +43,7 @@ from common import (
43
ModelParameterSweep,
44
ParameterSweep,
45
ResultsFormatter,
46
+ batch_spec_sort_key,
47
is_mla_backend,
48
)
49
@@ -218,10 +219,13 @@ def run_model_parameter_sweep(
219
by_param_and_spec[key].append(r)
220
break
221
221
- # Sort by param value then spec
222
+ # Sort by param value then spec (batch_size, q_len, kv_len)
223
sorted_keys = sorted(
224
by_param_and_spec.keys(),
224
- key=lambda x: (int(x[0]) if x[0].isdigit() else x[0], x[1]),
225
+ key=lambda x: (
226
+ int(x[0]) if x[0].isdigit() else x[0],
227
+ batch_spec_sort_key(x[1]),
228
+ ),
229
)
230
231
current_param_value = None
@@ -330,7 +334,7 @@ def run_parameter_sweep(
334
by_spec[spec] = []
335
by_spec[spec].append(r)
336
333
- for spec in sorted(by_spec.keys()):
337
+ for spec in sorted(by_spec.keys(), key=batch_spec_sort_key):
338
results = by_spec[spec]
339
best = min(results, key=lambda r: r.mean_time)
340
console.print(
@@ -496,15 +500,18 @@ def main():
500
if "description" in yaml_config:
501
console.print(f"[dim]{yaml_config['description']}[/]")
502
499
- # Override args with YAML values
500
- # (YAML takes precedence unless CLI arg was explicitly set)
501
- # Backend(s)
502
- if "backend" in yaml_config:
503
- args.backend = yaml_config["backend"]
504
- args.backends = None
505
- elif "backends" in yaml_config:
506
- args.backends = yaml_config["backends"]
507
- args.backend = None
503
+ # Override args with YAML values, but CLI args take precedence
504
+ # Check if CLI provided backends (they would be non-None and not default)
505
+ cli_backends_provided = args.backends is not None or args.backend is not None
506
+
507
+ # Backend(s) - only use YAML if CLI didn't specify
508
+ if not cli_backends_provided:
509
+ if "backend" in yaml_config:
510
+ args.backend = yaml_config["backend"]
511
+ args.backends = None
512
+ elif "backends" in yaml_config:
513
+ args.backends = yaml_config["backends"]
514
+ args.backend = None
515
516
# Check for special modes
517
if "mode" in yaml_config:
@@ -544,13 +551,15 @@ def main():
551
args.num_kv_heads = model.get("num_kv_heads", args.num_kv_heads)
552
args.block_size = model.get("block_size", args.block_size)
553
547
- # Benchmark settings
548
- if "benchmark" in yaml_config:
549
- bench = yaml_config["benchmark"]
550
- args.device = bench.get("device", args.device)
551
- args.repeats = bench.get("repeats", args.repeats)
552
- args.warmup_iters = bench.get("warmup_iters", args.warmup_iters)
553
- args.profile_memory = bench.get("profile_memory", args.profile_memory)
554
+ # Benchmark settings (top-level keys)
555
+ if "device" in yaml_config:
556
+ args.device = yaml_config["device"]
557
+ if "repeats" in yaml_config:
558
+ args.repeats = yaml_config["repeats"]
559
+ if "warmup_iters" in yaml_config:
560
+ args.warmup_iters = yaml_config["warmup_iters"]
561
+ if "profile_memory" in yaml_config:
562
+ args.profile_memory = yaml_config["profile_memory"]
563
564
# Parameter sweep configuration
565
if "parameter_sweep" in yaml_config:
benchmarks/attention_benchmarks/common.py
+63
-5
@@ -16,13 +16,32 @@ from batch_spec import get_batch_type, parse_batch_spec
16
from rich.console import Console
17
from rich.table import Table
18
19
+
20
+def batch_spec_sort_key(spec: str) -> tuple[int, int, int]:
21
+ """
22
+ Extract sorting key from batch spec: (batch_size, max_q_len, max_kv_len).
23
+
24
+ This ensures results are sorted by batch size first, then query length,
25
+ then sequence length, rather than alphabetically.
26
+ """
27
+ try:
28
+ requests = parse_batch_spec(spec)
29
+ batch_size = len(requests)
30
+ max_q_len = max(r.q_len for r in requests) if requests else 0
31
+ max_kv_len = max(r.kv_len for r in requests) if requests else 0
32
+ return (batch_size, max_q_len, max_kv_len)
33
+ except Exception:
34
+ # Fallback for unparseable specs
35
+ return (0, 0, 0)
36
+
37
+
38
# Mock classes for vLLM attention infrastructure
39
40
41
class MockHfConfig:
42
"""Mock HuggingFace config that satisfies vLLM's requirements."""
43
25
- def __init__(self, mla_dims: dict):
44
+ def __init__(self, mla_dims: dict, index_topk: int | None = None):
45
self.num_attention_heads = mla_dims["num_q_heads"]
46
self.num_key_value_heads = mla_dims["num_kv_heads"]
47
self.hidden_size = mla_dims["head_dim"] * mla_dims["num_q_heads"]
@@ -33,6 +52,8 @@ class MockHfConfig:
52
self.qk_rope_head_dim = mla_dims["qk_rope_head_dim"]
53
self.v_head_dim = mla_dims["v_head_dim"]
54
self.qk_head_dim = mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"]
55
+ if index_topk is not None:
56
+ self.index_topk = index_topk
57
58
def get_text_config(self):
59
return self
@@ -83,6 +104,38 @@ class MockKVBProj:
104
return (result,) # Return as tuple to match ColumnParallelLinear API
105
106
107
+class MockIndexer:
108
+ """Mock Indexer for sparse MLA backends.
109
+
110
+ Provides topk_indices_buffer that sparse MLA backends use to determine
111
+ which KV cache slots to attend to for each token.
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ max_num_tokens: int,
117
+ topk_tokens: int,
118
+ device: torch.device,
119
+ ):
120
+ self.topk_tokens = topk_tokens
121
+ self.topk_indices_buffer = torch.zeros(
122
+ (max_num_tokens, topk_tokens),
123
+ dtype=torch.int32,
124
+ device=device,
125
+ )
126
+
127
+ def fill_random_indices(self, num_tokens: int, max_kv_len: int):
128
+ """Fill topk_indices_buffer with random valid indices for benchmarking."""
129
+ indices = torch.randint(
130
+ 0,
131
+ max_kv_len,
132
+ (num_tokens, self.topk_tokens),
133
+ dtype=torch.int32,
134
+ device=self.topk_indices_buffer.device,
135
+ )
136
+ self.topk_indices_buffer[:num_tokens] = indices
137
+
138
+
139
class MockLayer(AttentionLayerBase):
140
"""Mock attention layer with scale parameters and impl.
141
@@ -327,6 +380,9 @@ class ResultsFormatter:
380
specs_order.append(spec)
381
by_spec[spec][r.config.backend] = r
382
383
+ # Sort specs by (batch_size, q_len, kv_len) instead of alphabetically
384
+ specs_order = sorted(by_spec.keys(), key=batch_spec_sort_key)
385
+
386
# Create shortened backend names for display
387
def shorten_backend_name(name: str) -> str:
388
"""Shorten long backend names for table display."""
@@ -493,10 +549,11 @@ def get_attention_scale(head_dim: int) -> float:
549
550
def is_mla_backend(backend: str) -> bool:
551
"""
496
- Check if backend is an MLA backend using the backend's is_mla() property.
552
+ Check if backend is an MLA backend using the AttentionBackendEnum.
553
554
Args:
499
- backend: Backend name (e.g., "CUTLASS_MLA", "FLASHINFER_MLA")
555
+ backend: Backend name matching AttentionBackendEnum exactly
556
+ (e.g., "FLASHMLA_SPARSE")
557
558
Returns:
559
True if the backend is an MLA backend, False otherwise
@@ -504,7 +561,8 @@ def is_mla_backend(backend: str) -> bool:
561
from vllm.v1.attention.backends.registry import AttentionBackendEnum
562
563
try:
507
- backend_class = AttentionBackendEnum[backend.upper()].get_class()
564
+ backend_enum = AttentionBackendEnum[backend]
565
+ backend_class = backend_enum.get_class()
566
return backend_class.is_mla()
509
- except (KeyError, ValueError, ImportError):
567
+ except (KeyError, ValueError, ImportError, AttributeError):
568
return False
benchmarks/attention_benchmarks/configs/mla_decode.yaml
+19
-10
@@ -3,7 +3,7 @@
3
model:
4
name: "deepseek-v3"
5
num_layers: 60
6
- num_q_heads: 128
6
+ num_q_heads: 128 # Base value, can be swept for TP simulation
7
num_kv_heads: 1 # MLA uses single latent KV
8
head_dim: 576
9
kv_lora_rank: 512
@@ -12,6 +12,13 @@ model:
12
v_head_dim: 128
13
block_size: 128 # CUTLASS MLA and FlashAttn MLA use 128
14
15
+# Model parameter sweep: simulate tensor parallelism by varying num_q_heads
16
+# TP=1: 128 heads, TP=2: 64 heads, TP=4: 32 heads, TP=8: 16 heads
17
+model_parameter_sweep:
18
+ param_name: "num_q_heads"
19
+ values: [128, 64, 32, 16]
20
+ label_format: "{backend}_{value}h"
21
+
22
batch_specs:
23
# Small batches, varying sequence lengths
24
- "16q1s512" # 16 requests, 512 KV cache
@@ -34,28 +41,30 @@ batch_specs:
41
# Very large batches
42
- "128q1s1k" # 128 requests, 1k KV cache
43
- "128q1s2k" # 128 requests, 2k KV cache
44
+ - "128q1s4k" # 128 requests, 4k KV cache
45
+ - "128q1s8k" # 128 requests, 8k KV cache
46
47
# Long context
48
- "32q1s16k" # 32 requests, 16k KV cache
49
- "32q1s32k" # 32 requests, 32k KV cache
50
51
backends:
43
- - cutlass_mla
44
- - flashinfer_mla
45
- - flashattn_mla # Hopper only
46
- - flashmla # Hopper only
52
+ - CUTLASS_MLA
53
+ - FLASHINFER_MLA
54
+ - FLASH_ATTN_MLA # Hopper only
55
+ - FLASHMLA # Hopper only
56
57
device: "cuda:0"
49
-repeats: 5
50
-warmup_iters: 3
58
+repeats: 100
59
+warmup_iters: 10
60
profile_memory: true
61
62
# Backend-specific tuning
54
-cutlass_mla:
63
+CUTLASS_MLA:
64
num_kv_splits: auto # or specific value like 4, 8, 16
65
57
-flashattn_mla:
66
+FLASH_ATTN_MLA:
67
reorder_batch_threshold: 512
68
60
-flashmla:
69
+FLASHMLA:
70
reorder_batch_threshold: 1
benchmarks/attention_benchmarks/configs/mla_mixed_batch.yaml
+4
-4
@@ -45,10 +45,10 @@ batch_specs:
45
- "4q4k_60q1s4k" # 4 prefill + 60 decode
46
47
backends:
48
- - cutlass_mla
49
- - flashinfer_mla
50
- - flashattn_mla # Hopper only
51
- - flashmla # Hopper only
48
+ - CUTLASS_MLA
49
+ - FLASHINFER_MLA
50
+ - FLASH_ATTN_MLA # Hopper only
51
+ - FLASHMLA # Hopper only
52
53
device: "cuda:0"
54
repeats: 5
benchmarks/attention_benchmarks/configs/mla_prefill.yaml
new
+62
@@ -0,0 +1,62 @@
1
+# MLA prefill-only benchmark configuration for sparse backends
2
+
3
+model:
4
+ name: "deepseek-v3"
5
+ num_layers: 60
6
+ num_q_heads: 128
7
+ num_kv_heads: 1
8
+ head_dim: 576
9
+ kv_lora_rank: 512
10
+ qk_nope_head_dim: 128
11
+ qk_rope_head_dim: 64
12
+ v_head_dim: 128
13
+ block_size: 128
14
+
15
+# Model parameter sweep: simulate tensor parallelism by varying num_q_heads
16
+# TP=1: 128 heads, TP=2: 64 heads, TP=4: 32 heads, TP=8: 16 heads
17
+model_parameter_sweep:
18
+ param_name: "num_q_heads"
19
+ values: [128, 64, 32, 16]
20
+ label_format: "{backend}_{value}h"
21
+
22
+batch_specs:
23
+ # Pure prefill
24
+ - "1q512"
25
+ - "1q1k"
26
+ - "1q2k"
27
+ - "1q4k"
28
+ - "1q8k"
29
+
30
+ # Batched pure prefill
31
+ - "2q512"
32
+ - "2q1k"
33
+ - "2q2k"
34
+ - "2q4k"
35
+ - "2q8k"
36
+ - "4q512"
37
+ - "4q1k"
38
+ - "4q2k"
39
+ - "4q4k"
40
+ - "4q8k"
41
+ - "8q512"
42
+ - "8q1k"
43
+ - "8q2k"
44
+ - "8q4k"
45
+ - "8q8k"
46
+
47
+ # Extend
48
+ - "1q512s4k"
49
+ - "1q512s8k"
50
+ - "1q1ks8k"
51
+ - "1q2ks8k"
52
+ - "1q2ks16k"
53
+ - "1q4ks16k"
54
+
55
+backends:
56
+ - FLASHMLA_SPARSE
57
+ - FLASHINFER_MLA_SPARSE
58
+
59
+device: "cuda:0"
60
+repeats: 10
61
+warmup_iters: 3
62
+profile_memory: true
benchmarks/attention_benchmarks/configs/reorder_threshold.yaml
+5
-6
@@ -6,7 +6,7 @@
6
description: "Decode vs Prefill pipeline crossover analysis"
7
8
# Test FlashAttn MLA
9
-backend: flashattn_mla
9
+backend: FLASH_ATTN_MLA
10
11
# Mode: decode_vs_prefill comparison (special sweep mode)
12
# For each batch spec, we'll test both decode and prefill pipelines
@@ -62,11 +62,10 @@ model:
62
block_size: 128
63
64
# Benchmark settings
65
-benchmark:
66
- device: "cuda:0"
67
- repeats: 15 # More repeats for spec decode variance
68
- warmup_iters: 5
69
- profile_memory: false
65
+device: "cuda:0"
66
+repeats: 15 # More repeats for spec decode variance
67
+warmup_iters: 5
68
+profile_memory: false
69
70
# Output
71
output:
benchmarks/attention_benchmarks/configs/speculative_decode.yaml
+7
-8
@@ -41,18 +41,17 @@ batch_specs:
41
42
# Backends that support query length > 1
43
backends:
44
- - flashattn_mla # reorder_batch_threshold = 512
45
- - flashmla # reorder_batch_threshold = 1 (tunable)
44
+ - FLASH_ATTN_MLA # reorder_batch_threshold = 512
45
+ - FLASHMLA # reorder_batch_threshold = 1 (tunable)
46
47
# FlashInfer-MLA also supports uniform spec-as-decode but with different mechanism
48
-# - flashinfer_mla
48
+# - FLASHINFER_MLA
49
50
# Benchmark settings
51
-benchmark:
52
- device: "cuda:0"
53
- repeats: 10 # More repeats for statistical significance
54
- warmup_iters: 5
55
- profile_memory: false
51
+device: "cuda:0"
52
+repeats: 10 # More repeats for statistical significance
53
+warmup_iters: 5
54
+profile_memory: false
55
56
# Test these threshold values for optimization
57
parameter_sweep:
benchmarks/attention_benchmarks/configs/standard_attention.yaml
+4
-4
@@ -36,11 +36,11 @@ batch_specs:
36
- "q1ks2k" # 1k query, 2k sequence
37
- "2q1ks4k" # 2 requests: 1k query, 4k sequence
38
39
-# Available backends: flash, triton, flashinfer
39
+# Available backends: FLASH_ATTN, TRITON_ATTN, FLASHINFER
40
backends:
41
- - flash
42
- - triton
43
- - flashinfer
41
+ - FLASH_ATTN
42
+ - TRITON_ATTN
43
+ - FLASHINFER
44
45
device: "cuda:0"
46
repeats: 5
benchmarks/attention_benchmarks/mla_runner.py
+136
-81
@@ -8,14 +8,13 @@ This module provides helpers for running MLA backends without
8
needing full VllmConfig integration.
9
"""
10
11
-import importlib
12
-
11
import numpy as np
12
import torch
13
from batch_spec import parse_batch_spec
14
from common import (
15
BenchmarkResult,
16
MockHfConfig,
17
+ MockIndexer,
18
MockKVBProj,
19
MockLayer,
20
setup_mla_dims,
@@ -62,6 +61,7 @@ def create_minimal_vllm_config(
61
block_size: int = 128,
62
max_num_seqs: int = 256,
63
mla_dims: dict | None = None,
64
+ index_topk: int | None = None,
65
) -> VllmConfig:
66
"""
67
Create minimal VllmConfig for MLA benchmarks.
@@ -73,6 +73,8 @@ def create_minimal_vllm_config(
73
max_num_seqs: Maximum number of sequences
74
mla_dims: Optional custom MLA dimensions dict. If not provided, uses
75
setup_mla_dims(model_name)
76
+ index_topk: Optional topk value for sparse MLA backends. If provided,
77
+ the config will include index_topk for sparse attention.
78
79
Returns:
80
VllmConfig for benchmarking
@@ -82,7 +84,7 @@ def create_minimal_vllm_config(
84
mla_dims = setup_mla_dims(model_name)
85
86
# Create mock HF config first (avoids downloading from HuggingFace)
85
- mock_hf_config = MockHfConfig(mla_dims)
87
+ mock_hf_config = MockHfConfig(mla_dims, index_topk=index_topk)
88
89
# Create a temporary minimal config.json to avoid HF downloads
90
# This ensures consistent ModelConfig construction without network access
@@ -120,16 +122,12 @@ def create_minimal_vllm_config(
122
seed=0,
123
max_model_len=32768,
124
quantization=None,
123
- quantization_param_path=None,
125
enforce_eager=False,
125
- max_context_len_to_capture=None,
126
- max_seq_len_to_capture=8192,
126
max_logprobs=20,
127
disable_sliding_window=False,
128
skip_tokenizer_init=True,
129
served_model_name=None,
130
limit_mm_per_prompt=None,
132
- use_async_output_proc=True,
131
config_format="auto",
132
)
133
finally:
@@ -180,56 +178,65 @@ def create_minimal_vllm_config(
178
# ============================================================================
179
180
183
-# Backend name to class name prefix mapping
184
-_BACKEND_NAME_MAP = {
185
- "flashattn_mla": "FlashAttnMLA",
186
- "flashmla": "FlashMLA",
187
- "flashinfer_mla": "FlashInferMLA",
188
- "cutlass_mla": "CutlassMLA",
189
-}
190
-
191
-# Special properties that differ from defaults
181
+# Backend-specific properties that can't be inferred from the backend class
182
+# Keys are AttentionBackendEnum names (uppercase)
183
_BACKEND_PROPERTIES = {
193
- "flashmla": {
184
+ "FLASHMLA": {
185
"query_format": "concat", # Single concatenated tensor (vs tuple)
195
- "block_size": 64, # FlashMLA uses fixed block size
186
},
197
- "flashinfer_mla": {
198
- "block_size": 64, # FlashInfer MLA only supports 32 or 64
187
+ "FLASHMLA_SPARSE": {
188
+ "query_format": "concat", # Single concatenated tensor (vs tuple)
189
},
190
}
191
192
193
def _get_backend_config(backend: str) -> dict:
194
"""
205
- Get backend configuration using naming conventions.
206
-
207
- All MLA backends follow the pattern:
208
- - Module: vllm.v1.attention.backends.mla.{backend}
209
- - Impl: {Name}Impl
210
- - Metadata: {Name}Metadata (or MLACommonMetadata)
211
- - DecodeMetadata: {Name}DecodeMetadata (or MLACommonDecodeMetadata)
212
- - MetadataBuilder: {Name}MetadataBuilder
195
+ Get backend configuration from AttentionBackendEnum.
196
+
197
+ Uses the registry to get the backend class and extract configuration
198
+ from its methods (get_impl_cls, get_builder_cls, is_sparse, etc.).
199
+
200
+ Args:
201
+ backend: Backend name matching AttentionBackendEnum exactly
202
+ (e.g., "FLASHMLA_SPARSE")
203
+
204
+ Returns:
205
+ Dict with backend configuration
206
"""
214
- if backend not in _BACKEND_NAME_MAP:
215
- raise ValueError(f"Unknown backend: {backend}")
207
+ from vllm.v1.attention.backends.registry import AttentionBackendEnum
208
217
- name = _BACKEND_NAME_MAP[backend]
209
+ try:
210
+ backend_enum = AttentionBackendEnum[backend]
211
+ backend_class = backend_enum.get_class()
212
+ except (KeyError, ValueError) as e:
213
+ valid_backends = [e.name for e in AttentionBackendEnum if e.name != "CUSTOM"]
214
+ raise ValueError(
215
+ f"Unknown backend: {backend}. "
216
+ f"Valid MLA backends: {[b for b in valid_backends if 'MLA' in b]}"
217
+ ) from e
218
+
219
+ # Get block size from backend class
220
+ block_sizes = backend_class.get_supported_kernel_block_sizes()
221
+ # Use first supported block size (backends typically support one for MLA)
222
+ block_size = block_sizes[0] if block_sizes else None
223
+ if hasattr(block_size, "value"):
224
+ # Handle MultipleOf enum
225
+ block_size = None
226
+
227
+ # Check if sparse via class method if available
228
+ is_sparse = getattr(backend_class, "is_sparse", lambda: False)()
229
+
230
+ # Get properties that can't be inferred
231
props = _BACKEND_PROPERTIES.get(backend, {})
232
220
- # Check if backend uses common metadata (FlashInfer, CUTLASS)
221
- uses_common = backend in ("flashinfer_mla", "cutlass_mla")
222
-
233
return {
224
- "module": f"vllm.v1.attention.backends.mla.{backend}",
225
- "impl_class": f"{name}Impl",
226
- "metadata_class": "MLACommonMetadata" if uses_common else f"{name}Metadata",
227
- "decode_metadata_class": "MLACommonDecodeMetadata"
228
- if uses_common
229
- else f"{name}DecodeMetadata",
230
- "builder_class": f"{name}MetadataBuilder",
234
+ "backend_class": backend_class,
235
+ "impl_class": backend_class.get_impl_cls(),
236
+ "builder_class": backend_class.get_builder_cls(),
237
"query_format": props.get("query_format", "tuple"),
232
- "block_size": props.get("block_size", None),
238
+ "block_size": block_size,
239
+ "is_sparse": is_sparse,
240
}
241
242
@@ -447,22 +454,26 @@ def _create_backend_impl(
454
mla_dims: dict,
455
vllm_config: VllmConfig,
456
device: torch.device,
457
+ max_num_tokens: int = 8192,
458
+ index_topk: int | None = None,
459
):
460
"""
461
Create backend implementation instance.
462
463
Args:
455
- backend_cfg: Backend configuration dict
464
+ backend_cfg: Backend configuration dict from _get_backend_config()
465
mla_dims: MLA dimension configuration
466
vllm_config: VllmConfig instance
467
device: Target device
468
+ max_num_tokens: Maximum number of tokens for sparse indexer buffer
469
+ index_topk: Topk value for sparse MLA backends
470
471
Returns:
461
- Tuple of (impl, layer, builder_instance)
472
+ Tuple of (impl, layer, builder_instance, indexer)
473
"""
463
- # Import backend classes
464
- backend_module = importlib.import_module(backend_cfg["module"])
465
- impl_class = getattr(backend_module, backend_cfg["impl_class"])
474
+ # Get classes from backend config (already resolved by _get_backend_config)
475
+ impl_class = backend_cfg["impl_class"]
476
+ builder_class = backend_cfg["builder_class"]
477
478
# Calculate scale
479
scale = 1.0 / np.sqrt(mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"])
@@ -474,26 +485,44 @@ def _create_backend_impl(
485
v_head_dim=mla_dims["v_head_dim"],
486
)
487
488
+ # Create indexer for sparse backends
489
+ indexer = None
490
+ if backend_cfg.get("is_sparse", False):
491
+ if index_topk is None:
492
+ index_topk = 2048 # Default topk for sparse MLA
493
+ indexer = MockIndexer(
494
+ max_num_tokens=max_num_tokens,
495
+ topk_tokens=index_topk,
496
+ device=device,
497
+ )
498
+
499
+ # Build impl kwargs
500
+ impl_kwargs = {
501
+ "num_heads": mla_dims["num_q_heads"],
502
+ "head_size": mla_dims["head_dim"],
503
+ "scale": scale,
504
+ "num_kv_heads": mla_dims["num_kv_heads"],
505
+ "alibi_slopes": None,
506
+ "sliding_window": None,
507
+ "kv_cache_dtype": "auto",
508
+ "logits_soft_cap": None,
509
+ "attn_type": "decoder",
510
+ "kv_sharing_target_layer_name": None,
511
+ "q_lora_rank": None,
512
+ "kv_lora_rank": mla_dims["kv_lora_rank"],
513
+ "qk_nope_head_dim": mla_dims["qk_nope_head_dim"],
514
+ "qk_rope_head_dim": mla_dims["qk_rope_head_dim"],
515
+ "qk_head_dim": mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"],
516
+ "v_head_dim": mla_dims["v_head_dim"],
517
+ "kv_b_proj": mock_kv_b_proj,
518
+ }
519
+
520
+ # Add indexer for sparse backends
521
+ if indexer is not None:
522
+ impl_kwargs["indexer"] = indexer
523
+
524
# Create impl
478
- impl = impl_class(
479
- num_heads=mla_dims["num_q_heads"],
480
- head_size=mla_dims["head_dim"],
481
- scale=scale,
482
- num_kv_heads=mla_dims["num_kv_heads"],
483
- alibi_slopes=None,
484
- sliding_window=None,
485
- kv_cache_dtype="auto",
486
- logits_soft_cap=None,
487
- attn_type="decoder",
488
- kv_sharing_target_layer_name=None,
489
- q_lora_rank=None,
490
- kv_lora_rank=mla_dims["kv_lora_rank"],
491
- qk_nope_head_dim=mla_dims["qk_nope_head_dim"],
492
- qk_rope_head_dim=mla_dims["qk_rope_head_dim"],
493
- qk_head_dim=mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"],
494
- v_head_dim=mla_dims["v_head_dim"],
495
- kv_b_proj=mock_kv_b_proj,
496
- )
525
+ impl = impl_class(**impl_kwargs)
526
527
# Initialize DCP attributes
528
if not hasattr(impl, "dcp_world_size") or impl.dcp_world_size in (None, -1):
@@ -515,9 +544,7 @@ def _create_backend_impl(
544
545
# Create builder instance if needed
546
builder_instance = None
518
- if backend_cfg["builder_class"]:
519
- builder_class = getattr(backend_module, backend_cfg["builder_class"])
520
-
547
+ if builder_class:
548
# Populate static_forward_context so builder can find the layer
549
# MockLayer inherits from AttentionLayerBase, so isinstance checks pass
550
vllm_config.compilation_config.static_forward_context = {"placeholder": layer}
@@ -529,7 +556,7 @@ def _create_backend_impl(
556
device=device,
557
)
558
532
- return impl, layer, builder_instance
559
+ return impl, layer, builder_instance, indexer
560
561
562
# ============================================================================
@@ -594,6 +621,7 @@ def _run_single_benchmark(
621
backend_cfg: dict,
622
mla_dims: dict,
623
device: torch.device,
624
+ indexer=None,
625
) -> BenchmarkResult:
626
"""
627
Run a single benchmark iteration.
@@ -606,6 +634,7 @@ def _run_single_benchmark(
634
backend_cfg: Backend configuration dict
635
mla_dims: MLA dimension configuration
636
device: Target device
637
+ indexer: Optional MockIndexer for sparse backends
638
639
Returns:
640
BenchmarkResult with timing statistics
@@ -613,7 +642,9 @@ def _run_single_benchmark(
642
# Parse batch spec
643
requests = parse_batch_spec(config.batch_spec)
644
q_lens = [r.q_len for r in requests]
645
+ kv_lens = [r.kv_len for r in requests]
646
total_q = sum(q_lens)
647
+ max_kv_len = max(kv_lens)
648
649
# Determine block size
650
block_size = backend_cfg["block_size"] or config.block_size
@@ -641,8 +672,16 @@ def _run_single_benchmark(
672
torch.bfloat16,
673
)
674
644
- # Determine which forward method to use based on metadata
645
- if metadata.decode is not None:
675
+ # Fill indexer with random indices for sparse backends
676
+ is_sparse = backend_cfg.get("is_sparse", False)
677
+ if is_sparse and indexer is not None:
678
+ indexer.fill_random_indices(total_q, max_kv_len)
679
+
680
+ # Determine which forward method to use
681
+ if is_sparse:
682
+ # Sparse backends use forward_mqa
683
+ forward_fn = lambda: impl.forward_mqa(decode_inputs, kv_cache, metadata, layer)
684
+ elif metadata.decode is not None:
685
forward_fn = lambda: impl._forward_decode(
686
decode_inputs, kv_cache, metadata, layer
687
)
@@ -693,11 +732,13 @@ def _run_single_benchmark(
732
def _run_mla_benchmark_batched(
733
backend: str,
734
configs_with_params: list[tuple], # [(config, threshold, num_splits), ...]
735
+ index_topk: int = 2048,
736
) -> list[BenchmarkResult]:
737
"""
738
Unified batched MLA benchmark runner for all backends.
739
700
- Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla
740
+ Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
741
+ flashinfer_mla_sparse, flashmla_sparse
742
743
This function reuses backend initialization across multiple benchmarks
744
to avoid setup/teardown overhead.
@@ -707,6 +748,7 @@ def _run_mla_benchmark_batched(
748
configs_with_params: List of (config, threshold, num_splits) tuples
749
- threshold: reorder_batch_threshold (FlashAttn/FlashMLA only)
750
- num_splits: num_kv_splits (CUTLASS only)
751
+ index_topk: Topk value for sparse MLA backends (default 2048)
752
753
Returns:
754
List of BenchmarkResult objects
@@ -730,19 +772,27 @@ def _run_mla_benchmark_batched(
772
if mla_dims is None:
773
mla_dims = setup_mla_dims("deepseek-v3")
774
775
+ # Determine if this is a sparse backend
776
+ is_sparse = backend_cfg.get("is_sparse", False)
777
+
778
# Create and set vLLM config for MLA (reused across all benchmarks)
779
vllm_config = create_minimal_vllm_config(
780
model_name="deepseek-v3", # Used only for model path
781
block_size=block_size,
782
mla_dims=mla_dims, # Use custom dims from config or default
783
+ index_topk=index_topk if is_sparse else None,
784
)
785
786
results = []
787
788
with set_current_vllm_config(vllm_config):
743
- # Create backend impl, layer, and builder (reused across benchmarks)
744
- impl, layer, builder_instance = _create_backend_impl(
745
- backend_cfg, mla_dims, vllm_config, device
789
+ # Create backend impl, layer, builder, and indexer (reused across benchmarks)
790
+ impl, layer, builder_instance, indexer = _create_backend_impl(
791
+ backend_cfg,
792
+ mla_dims,
793
+ vllm_config,
794
+ device,
795
+ index_topk=index_topk if is_sparse else None,
796
)
797
798
# Run each benchmark with the shared impl
@@ -768,6 +818,7 @@ def _run_mla_benchmark_batched(
818
backend_cfg,
819
mla_dims,
820
device,
821
+ indexer=indexer,
822
)
823
results.append(result)
824
@@ -793,20 +844,24 @@ def run_mla_benchmark(
844
config,
845
reorder_batch_threshold: int | None = None,
846
num_kv_splits: int | None = None,
847
+ index_topk: int = 2048,
848
) -> BenchmarkResult | list[BenchmarkResult]:
849
"""
850
Unified MLA benchmark runner for all backends.
851
800
- Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla
852
+ Works for: flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
853
+ flashinfer_mla_sparse, flashmla_sparse
854
855
Always uses batched execution internally for optimal performance.
856
857
Args:
805
- backend: Backend name (flashattn_mla, flashmla, flashinfer_mla, cutlass_mla)
858
+ backend: Backend name (flashattn_mla, flashmla, flashinfer_mla, cutlass_mla,
859
+ flashinfer_mla_sparse, flashmla_sparse)
860
config: BenchmarkConfig or list of (BenchmarkConfig, param) tuples
861
reorder_batch_threshold: Threshold override for FlashAttn/FlashMLA
862
(single config mode only)
863
num_kv_splits: Number of KV splits for CUTLASS (single config mode only)
864
+ index_topk: Topk value for sparse MLA backends (default 2048)
865
866
Returns:
867
BenchmarkResult (single mode) or list of BenchmarkResult (batched mode)
@@ -816,9 +871,9 @@ def run_mla_benchmark(
871
# Already in batched format
872
if len(config) > 0 and isinstance(config[0], tuple):
873
# Format: [(cfg, param), ...] where param is threshold or num_splits
819
- if backend in ("flashattn_mla", "flashmla"):
874
+ if backend in ("flashattn_mla", "flashmla", "flashmla_sparse"):
875
configs_with_params = [(cfg, param, None) for cfg, param in config]
821
- else: # cutlass_mla or flashinfer_mla
876
+ else: # cutlass_mla, flashinfer_mla, or sparse backends
877
configs_with_params = [(cfg, None, param) for cfg, param in config]
878
else:
879
# Format: [cfg, ...] - just configs
@@ -830,7 +885,7 @@ def run_mla_benchmark(
885
return_single = True
886
887
# Use unified batched execution
833
- results = _run_mla_benchmark_batched(backend, configs_with_params)
888
+ results = _run_mla_benchmark_batched(backend, configs_with_params, index_topk)
889
890
# Return single result or list based on input
891
return results[0] if return_single else results
benchmarks/attention_benchmarks/runner.py
+24
-27
@@ -40,29 +40,29 @@ from vllm.v1.kv_cache_interface import FullAttentionSpec
40
# ============================================================================
41
42
43
-_BACKEND_CONFIG = {
44
- "flash": {
45
- "module": "vllm.v1.attention.backends.flash_attn",
46
- "backend_class": "FlashAttentionBackend",
47
- },
48
- "triton": {
49
- "module": "vllm.v1.attention.backends.triton_attn",
50
- "backend_class": "TritonAttentionBackend",
51
- },
52
- "flashinfer": {
53
- "module": "vllm.v1.attention.backends.flashinfer",
54
- "backend_class": "FlashInferBackend",
55
- },
56
-}
43
+def _get_backend_config(backend: str) -> dict:
44
+ """
45
+ Get backend configuration from AttentionBackendEnum.
46
47
+ Args:
48
+ backend: Backend name matching AttentionBackendEnum exactly
49
+ (e.g., "FLASH_ATTN", "TRITON_ATTN", "FLASHINFER")
50
59
-def _get_backend_config(backend: str) -> dict:
60
- if backend not in _BACKEND_CONFIG:
51
+ Returns:
52
+ Dict with backend_class
53
+ """
54
+ from vllm.v1.attention.backends.registry import AttentionBackendEnum
55
+
56
+ try:
57
+ backend_enum = AttentionBackendEnum[backend]
58
+ backend_class = backend_enum.get_class()
59
+ except (KeyError, ValueError) as e:
60
+ valid_backends = [b.name for b in AttentionBackendEnum if b.name != "CUSTOM"]
61
raise ValueError(
62
- f"Unknown backend: {backend}. "
63
- f"Available: {', '.join(_BACKEND_CONFIG.keys())}"
64
- )
65
- return _BACKEND_CONFIG[backend]
62
+ f"Unknown backend: {backend}. Valid backends: {valid_backends}"
63
+ ) from e
64
+
65
+ return {"backend_class": backend_class}
66
67
68
@contextmanager
@@ -205,10 +205,7 @@ def _create_backend_impl(
205
dtype: torch.dtype,
206
):
207
"""Create backend implementation instance."""
208
- import importlib
209
-
210
- backend_module = importlib.import_module(backend_cfg["module"])
211
- backend_class = getattr(backend_module, backend_cfg["backend_class"])
208
+ backend_class = backend_cfg["backend_class"]
209
210
scale = get_attention_scale(config.head_dim)
211
@@ -247,7 +244,7 @@ def _create_metadata_builder(
244
245
# Flashinfer needs get_per_layer_parameters mocked since we don't have
246
# real model layers registered
250
- if backend_name == "flashinfer":
247
+ if backend_name == "FLASHINFER":
248
import unittest.mock
249
250
from vllm.v1.attention.backends.utils import PerLayerParameters
@@ -438,7 +435,7 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
435
"""
436
Run standard attention benchmark with real kernels.
437
441
- Supports: flash, triton, flashinfer
438
+ Supports: FLASH_ATTN, TRITON_ATTN, FLASHINFER
439
440
Args:
441
config: Benchmark configuration
@@ -453,7 +450,7 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
450
451
requests = parse_batch_spec(config.batch_spec)
452
456
- if config.backend == "flashinfer":
453
+ if config.backend == "FLASHINFER":
454
requests = reorder_for_flashinfer(requests)
455
456
q_lens = [r.q_len for r in requests]
docs/design/attention_backends.md
+2
@@ -128,6 +128,7 @@ Priority is **1 = highest** (tried first).
128
| 4 | `FLASHMLA` |
129
| 5 | `TRITON_MLA` |
130
| 6 | `FLASHMLA_SPARSE` |
131
+| 7 | `FLASHINFER_MLA_SPARSE` |
132
133
**Ampere/Hopper (SM 8.x-9.x):**
134
@@ -204,6 +205,7 @@ configuration.
205
|---------|--------|-----------|-------------|------------|------|--------|-----------|-----|-----------------|--------------|
206
| `CUTLASS_MLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
207
| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | 10.x |
208
+| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `bfloat16` | 32, 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x |
209
| `FLASHMLA` | fp16, bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
210
| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 576 | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
211
| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
tests/v1/attention/test_sparse_mla_backends.py
+180
-70
@@ -1,11 +1,10 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Unit tests for the FlashMLA sparse backend utilities."""
3
+"""Unit tests for the sparse MLA backends and utilities."""
4
5
import math
6
from types import MethodType, SimpleNamespace
7
8
-import numpy as np
8
import pytest
9
import torch
10
@@ -25,6 +24,9 @@ from vllm.config import set_current_vllm_config
24
from vllm.model_executor.layers.linear import ColumnParallelLinear
25
from vllm.platforms import current_platform
26
from vllm.utils.math_utils import cdiv
27
+from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
28
+ FlashInferMLASparseBackend,
29
+)
30
from vllm.v1.attention.backends.mla.flashmla_sparse import (
31
FlashMLASparseBackend,
32
triton_convert_req_index_to_global_index,
@@ -156,31 +158,47 @@ def _quantize_dequantize_fp8_ds_mla(
158
return dequant_kv_c, dequant_k_pe
159
160
161
+@pytest.mark.parametrize(
162
+ "backend_cls",
163
+ [FlashMLASparseBackend, FlashInferMLASparseBackend],
164
+ ids=["FlashMLA", "FlashInfer"],
165
+)
166
@pytest.mark.parametrize("batch_name", list(SPARSE_BACKEND_BATCH_SPECS.keys()))
160
-@pytest.mark.parametrize("kv_cache_dtype", ["fp8_ds_mla", "auto"])
167
+@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_ds_mla"])
168
@pytest.mark.parametrize("tensor_parallel_size", [1, 2, 4])
162
-@pytest.mark.skipif(
163
- torch.cuda.get_device_capability() < (9, 0),
164
- reason="FlashMLASparseBackend requires CUDA 9.0 or higher",
165
-)
169
+@pytest.mark.parametrize("block_size", [32, 64])
170
def test_sparse_backend_decode_correctness(
171
default_vllm_config,
172
dist_init,
173
+ backend_cls,
174
batch_name,
175
kv_cache_dtype,
176
tensor_parallel_size,
177
+ block_size,
178
workspace_init,
179
):
174
- if current_platform.is_rocm():
175
- pytest.skip("ROCm does not support fp8_ds_mla data type for kv cache.")
180
+ if kv_cache_dtype not in backend_cls.supported_kv_cache_dtypes:
181
+ pytest.skip(f"{backend_cls.get_name()} does not support {kv_cache_dtype}")
182
177
- if not torch.cuda.is_available():
178
- pytest.skip("CUDA is required for sparse MLA decode test")
183
+ supported_block_sizes = backend_cls.get_supported_kernel_block_sizes()
184
+ if block_size not in supported_block_sizes:
185
+ pytest.skip(
186
+ f"{backend_cls.get_name()} does not support block_size={block_size}"
187
+ )
188
180
- device = torch.device("cuda")
181
- dtype = torch.bfloat16
189
+ if backend_cls == FlashMLASparseBackend:
190
+ ok, reason = flashmla.is_flashmla_sparse_supported()
191
+ if not ok:
192
+ pytest.skip(reason)
193
+ elif backend_cls == FlashInferMLASparseBackend:
194
+ if not current_platform.has_device_capability(100):
195
+ pytest.skip("FlashInferMLASparseBackend requires SM 10.0 or higher")
196
197
batch_spec = SPARSE_BACKEND_BATCH_SPECS[batch_name]
198
+ use_fp8_ds_mla_quantization = kv_cache_dtype == "fp8_ds_mla"
199
+
200
+ device = torch.device("cuda")
201
+ dtype = torch.bfloat16
202
203
# Model hyper-parameters (kept intentionally small for the unit test)
204
total_num_heads = 128
@@ -192,11 +210,10 @@ def test_sparse_backend_decode_correctness(
210
qk_rope_head_dim = 64
211
v_head_dim = 128
212
head_size = kv_lora_rank + qk_rope_head_dim
195
- topk_tokens = 2048
213
+ topk_tokens = 128
214
215
max_seqlen = max(batch_spec.seq_lens)
216
total_cache_tokens = sum(batch_spec.seq_lens)
199
- block_size = 64
217
218
# Note: We use TP=1 to avoid multi-GPU requirements in CI.
219
# The test simulates head partitioning via mocked methods below.
@@ -247,11 +264,55 @@ def test_sparse_backend_decode_correctness(
264
seq_lens = batch_spec.seq_lens
265
query_lens = batch_spec.query_lens
266
267
+ # Pre-compute positions and sparse indices for all tokens.
268
+ # We need these BEFORE computing the reference to use sparse attention masks.
269
+ total_query_tokens = sum(query_lens)
270
+ positions = []
271
+ for i in range(batch_spec.batch_size):
272
+ s_len = seq_lens[i]
273
+ q_len = query_lens[i]
274
+ ctx_len = s_len - q_len
275
+ for q_idx in range(q_len):
276
+ positions.append(ctx_len + q_idx)
277
+
278
+ # Create sparse indices with UNIQUE per-token offsets to catch bugs where
279
+ # the kernel uses wrong indices for some tokens (e.g., due to incorrect
280
+ # tensor shapes like [1, num_tokens, ...] instead of [num_tokens, 1, ...]).
281
+ # Also include -1 masked indices to verify the kernel handles them correctly.
282
+ sparse_indices = torch.empty(
283
+ total_query_tokens, topk_tokens, dtype=torch.int32, device=device
284
+ )
285
+ for tok_idx in range(total_query_tokens):
286
+ max_valid_idx = positions[tok_idx]
287
+ offset = tok_idx * 7 # Prime number for varied offsets
288
+ # Use only half the topk indices as valid, mask the rest with -1
289
+ # This tests that the kernel correctly ignores -1 indices
290
+ num_valid = min(topk_tokens // 2, max_valid_idx + 1)
291
+ if num_valid > 0:
292
+ valid_range = torch.arange(num_valid, device=device, dtype=torch.int32)
293
+ tok_indices = (valid_range + offset) % (max_valid_idx + 1)
294
+ # Pad with -1 for the remaining positions
295
+ tok_indices = torch.cat(
296
+ [
297
+ tok_indices,
298
+ torch.full(
299
+ (topk_tokens - num_valid,), -1, device=device, dtype=torch.int32
300
+ ),
301
+ ]
302
+ )
303
+ else:
304
+ tok_indices = torch.full(
305
+ (topk_tokens,), -1, device=device, dtype=torch.int32
306
+ )
307
+ tok_indices[0] = 0 # At least one valid index
308
+ sparse_indices[tok_idx] = tok_indices
309
+
310
all_q_vllm, all_kv_c_vllm, all_k_pe_vllm = [], [], []
311
kv_c_contexts, k_pe_contexts = [], []
312
reference_outputs = []
313
314
kv_cache_scale = torch.tensor(1.0, dtype=torch.float32, device=device)
315
+ global_token_idx = 0
316
317
for i in range(batch_spec.batch_size):
318
s_len = seq_lens[i]
@@ -268,40 +329,53 @@ def test_sparse_backend_decode_correctness(
329
kv_c_full = torch.rand(s_len, kv_lora_rank, dtype=dtype, device=device)
330
k_pe_full = torch.rand(s_len, 1, qk_rope_head_dim, dtype=dtype, device=device)
331
271
- # SM100 (Blackwell) uses float -> e8m0 -> bf16 scale conversion
272
- # which truncates scales to powers of 2. Simulate this in reference.
273
- is_sm100 = torch.cuda.get_device_capability()[0] >= 10
274
- kv_c_full, k_pe_full = _quantize_dequantize_fp8_ds_mla(
275
- kv_c_full,
276
- k_pe_full.squeeze(1),
277
- block_size=vllm_config.cache_config.block_size,
278
- scale=kv_cache_scale,
279
- simulate_sm100_e8m0_scales=is_sm100,
280
- )
332
+ if use_fp8_ds_mla_quantization:
333
+ is_sm100 = torch.cuda.get_device_capability()[0] >= 10
334
+ kv_c_full, k_pe_squeezed = _quantize_dequantize_fp8_ds_mla(
335
+ kv_c_full,
336
+ k_pe_full.squeeze(1),
337
+ block_size=block_size,
338
+ scale=kv_cache_scale,
339
+ simulate_sm100_e8m0_scales=is_sm100,
340
+ )
341
+ k_pe_full = k_pe_squeezed.unsqueeze(1)
342
343
q_nope, q_pe = q_c.split([qk_nope_head_dim, qk_rope_head_dim], dim=-1)
344
ql_nope = torch.einsum("qnh,lnh->qnl", q_nope, W_UK)
345
q_mqa = torch.cat([ql_nope, q_pe], dim=-1)
346
286
- k_mqa = torch.cat([kv_c_full, k_pe_full], dim=-1)
287
- k_mqa = k_mqa.unsqueeze(1).expand(-1, num_heads, -1)
288
- v_mqa = kv_c_full.unsqueeze(1).expand(-1, num_heads, -1)
347
+ k_mqa = torch.cat([kv_c_full, k_pe_full.squeeze(1)], dim=-1)
348
+ v_mqa = kv_c_full
349
290
- attn_mask = torch.ones(q_len, s_len, dtype=torch.bool, device=device)
291
- causal_mask = torch.tril(torch.ones(q_len, q_len, device=device))
292
- attn_mask[:, ctx_len:] = causal_mask
350
+ # Compute sparse SDPA reference per query token using its sparse indices
351
+ for q_idx in range(q_len):
352
+ tok_sparse_idx = sparse_indices[global_token_idx]
353
+ valid_mask = tok_sparse_idx >= 0
354
+ valid_indices = tok_sparse_idx[valid_mask].long()
355
294
- q_sdpa_in = q_mqa.unsqueeze(0).transpose(1, 2)
295
- k_sdpa_in = k_mqa.unsqueeze(0).transpose(1, 2)
296
- v_sdpa_in = v_mqa.unsqueeze(0).transpose(1, 2)
356
+ q_tok = q_mqa[q_idx : q_idx + 1] # [1, num_heads, head_dim]
357
+ k_sparse = k_mqa[valid_indices] # [num_valid, head_dim]
358
+ v_sparse = v_mqa[valid_indices] # [num_valid, kv_lora_rank]
359
298
- sdpa_out = torch.nn.functional.scaled_dot_product_attention(
299
- q_sdpa_in, k_sdpa_in, v_sdpa_in, attn_mask=attn_mask, scale=scale
300
- )
301
- sdpa_out = sdpa_out.transpose(1, 2).squeeze(0)
360
+ k_sparse = k_sparse.unsqueeze(1).expand(-1, num_heads, -1)
361
+ v_sparse = v_sparse.unsqueeze(1).expand(-1, num_heads, -1)
362
+
363
+ # SDPA: [1, num_heads, 1, head_dim] x [1, num_heads, num_valid, head_dim]
364
+ q_sdpa_in = q_tok.unsqueeze(0).transpose(1, 2)
365
+ k_sdpa_in = k_sparse.unsqueeze(0).transpose(1, 2)
366
+ v_sdpa_in = v_sparse.unsqueeze(0).transpose(1, 2)
367
+
368
+ sdpa_out = torch.nn.functional.scaled_dot_product_attention(
369
+ q_sdpa_in, k_sdpa_in, v_sdpa_in, scale=scale
370
+ )
371
+ sdpa_out = sdpa_out.transpose(1, 2).squeeze(
372
+ 0
373
+ ) # [1, num_heads, kv_lora_rank]
374
303
- sdpa_out = torch.einsum("qnl,lnv->qnv", sdpa_out, W_UV)
304
- reference_outputs.append(sdpa_out.flatten(start_dim=-2))
375
+ sdpa_out = torch.einsum("qnl,lnv->qnv", sdpa_out, W_UV)
376
+ reference_outputs.append(sdpa_out.flatten(start_dim=-2))
377
+
378
+ global_token_idx += 1
379
380
all_q_vllm.append(q_c)
381
all_kv_c_vllm.append(kv_c_full[ctx_len:])
@@ -334,42 +408,18 @@ def test_sparse_backend_decode_correctness(
408
num_blocks=vllm_config.cache_config.num_gpu_blocks,
409
common_attn_metadata=common_attn_metadata,
410
randomize_blocks=False,
337
- kv_cache_dtype=vllm_config.cache_config.cache_dtype,
411
+ kv_cache_dtype=kv_cache_dtype if use_fp8_ds_mla_quantization else "auto",
412
scale=kv_cache_scale,
413
)
414
341
- builder_cls = FlashMLASparseBackend.get_builder_cls()
415
+ builder_cls = backend_cls.get_builder_cls()
416
builder = builder_cls(kv_cache_spec, ["placeholder"], vllm_config, device)
417
metadata = builder.build(
418
common_prefix_len=0, common_attn_metadata=common_attn_metadata
419
)
420
347
- starts = np.asarray(common_attn_metadata.query_start_loc_cpu, dtype=np.int32)
348
- seg_lengths = np.diff(starts)
349
- positions = np.arange(starts[-1], dtype=np.int32) - np.repeat(
350
- starts[:-1], seg_lengths
351
- )
352
- seq_lengths = np.asarray(common_attn_metadata.seq_lens.cpu(), dtype=np.int32)
353
- prefix_lengths = seq_lengths - seg_lengths
354
- positions += np.repeat(prefix_lengths, seg_lengths)
355
-
356
- pos_gpu = torch.as_tensor(positions, device=device, dtype=torch.int32)
357
- topk = metadata.topk_tokens
358
- debug_indices = torch.arange(topk, device=device, dtype=torch.int32).unsqueeze(0)
359
- token_positions = pos_gpu.unsqueeze(1)
360
- causal_mask = debug_indices <= token_positions
361
- debug_indices = torch.where(
362
- causal_mask, debug_indices, torch.full_like(debug_indices, -1)
363
- )
364
-
365
- # FlashMLASparseImpl now reads top-k indices from the indexer-provided
366
- # buffer, so emulate that contract with a simple namespace mock.
367
- debug_indices = debug_indices.expand(metadata.num_actual_tokens, -1).clone()
368
- mock_indexer = SimpleNamespace(topk_indices_buffer=debug_indices)
369
-
370
- ok, reason = flashmla.is_flashmla_sparse_supported()
371
- if not ok:
372
- pytest.skip(reason)
421
+ # Use the pre-computed sparse_indices for the mock indexer
422
+ mock_indexer = SimpleNamespace(topk_indices_buffer=sparse_indices)
423
424
kv_b_proj_weight = torch.cat([W_UK, W_UV], dim=-1)
425
kv_b_proj_weight = kv_b_proj_weight.view(
@@ -383,7 +433,7 @@ def test_sparse_backend_decode_correctness(
433
).to(device=device, dtype=dtype)
434
mock_kv_b_proj.weight = torch.nn.Parameter(kv_b_proj_weight.T.contiguous())
435
386
- impl_cls = FlashMLASparseBackend.get_impl_cls()
436
+ impl_cls = backend_cls.get_impl_cls()
437
with set_current_vllm_config(vllm_config):
438
impl = impl_cls(
439
num_heads=num_heads,
@@ -441,7 +491,7 @@ def test_sparse_backend_decode_correctness(
491
492
# FP8 quantization introduces some error, but should be within reasonable bounds
493
# BF16 (auto) should be very accurate, FP8 allows slightly more tolerance
444
- if kv_cache_dtype == "fp8_ds_mla":
494
+ if kv_cache_dtype.startswith("fp8"):
495
torch.testing.assert_close(backend_output, sdpa_reference, rtol=0.05, atol=0.05)
496
else:
497
torch.testing.assert_close(backend_output, sdpa_reference, rtol=0.01, atol=0.01)
@@ -636,3 +686,63 @@ def test_triton_convert_req_index_to_global_index_with_prefill_workspace(block_s
686
def test_split_prefill_chunks(seq_lens, max_buf, expected):
687
out = split_prefill_chunks(seq_lens, max_buf)
688
assert out == expected
689
+
690
+
691
+def test_triton_convert_returns_valid_counts():
692
+ """Test that return_valid_counts correctly counts non-negative indices."""
693
+ device = torch.device("cuda")
694
+ num_tokens = 8
695
+ num_requests = 2
696
+ max_blocks_per_req = 10
697
+ block_size = 64
698
+ num_topk_tokens = 128
699
+
700
+ req_id = torch.tensor([0, 0, 0, 0, 1, 1, 1, 1], dtype=torch.int32, device=device)
701
+ block_table = torch.arange(
702
+ num_requests * max_blocks_per_req, dtype=torch.int32, device=device
703
+ ).view(num_requests, max_blocks_per_req)
704
+
705
+ # Create token indices with varying numbers of valid entries
706
+ # Token 0: 64 valid, 64 invalid (-1)
707
+ # Token 1: 32 valid, 96 invalid
708
+ # Token 2: 128 valid (all)
709
+ # Token 3: 1 valid, 127 invalid
710
+ # etc.
711
+ token_indices = torch.full(
712
+ (num_tokens, num_topk_tokens), -1, dtype=torch.int32, device=device
713
+ )
714
+ expected_valid = []
715
+ for i in range(num_tokens):
716
+ num_valid = [64, 32, 128, 1, 64, 32, 128, 1][i]
717
+ token_indices[i, :num_valid] = torch.arange(
718
+ num_valid, dtype=torch.int32, device=device
719
+ ) % (block_size * max_blocks_per_req)
720
+ expected_valid.append(num_valid)
721
+
722
+ expected_valid_tensor = torch.tensor(
723
+ expected_valid, dtype=torch.int32, device=device
724
+ )
725
+
726
+ # Test with return_valid_counts=True
727
+ result, valid_counts = triton_convert_req_index_to_global_index(
728
+ req_id,
729
+ block_table,
730
+ token_indices,
731
+ BLOCK_SIZE=block_size,
732
+ NUM_TOPK_TOKENS=num_topk_tokens,
733
+ return_valid_counts=True,
734
+ )
735
+
736
+ torch.testing.assert_close(valid_counts, expected_valid_tensor, rtol=0, atol=0)
737
+
738
+ # Test that return_valid_counts=False returns only the indices
739
+ result_only = triton_convert_req_index_to_global_index(
740
+ req_id,
741
+ block_table,
742
+ token_indices,
743
+ BLOCK_SIZE=block_size,
744
+ NUM_TOPK_TOKENS=num_topk_tokens,
745
+ return_valid_counts=False,
746
+ )
747
+ assert isinstance(result_only, torch.Tensor)
748
+ torch.testing.assert_close(result_only, result, rtol=0, atol=0)
tools/pre_commit/generate_attention_backend_docs.py
+42
-2
@@ -901,10 +901,50 @@ def parse_cuda_priority_lists() -> dict[str, list[str]]:
901
902
903
def _get_backends_from_return(stmts: list) -> list[str]:
904
- """Extract backend names from return statements in a list of statements."""
904
+ """Extract backend names from return statements in a list of statements.
905
+
906
+ Handles starred unpacking (e.g. ``*sparse_backends``) by resolving the
907
+ variable from assignments found in the same statement list. When the
908
+ variable is conditionally assigned (inside an ``if/else``), the ``else``
909
+ branch value is used as the representative default.
910
+ """
911
+ # Collect variable assignments so we can resolve starred expressions.
912
+ # For conditional assignments, last-written (else branch) wins.
913
+ var_assigns: dict[str, list[str]] = {}
914
+ for stmt in stmts:
915
+ if isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.List):
916
+ for target in stmt.targets:
917
+ if isinstance(target, ast.Name):
918
+ var_assigns[target.id] = [
919
+ e.attr for e in stmt.value.elts if isinstance(e, ast.Attribute)
920
+ ]
921
+ elif isinstance(stmt, ast.If):
922
+ for branch in (stmt.body, stmt.orelse):
923
+ for branch_stmt in branch:
924
+ if isinstance(branch_stmt, ast.Assign) and isinstance(
925
+ branch_stmt.value, ast.List
926
+ ):
927
+ for target in branch_stmt.targets:
928
+ if isinstance(target, ast.Name):
929
+ var_assigns[target.id] = [
930
+ e.attr
931
+ for e in branch_stmt.value.elts
932
+ if isinstance(e, ast.Attribute)
933
+ ]
934
+
935
for stmt in stmts:
936
if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.List):
907
- return [e.attr for e in stmt.value.elts if isinstance(e, ast.Attribute)]
937
+ backends: list[str] = []
938
+ for e in stmt.value.elts:
939
+ if isinstance(e, ast.Attribute):
940
+ backends.append(e.attr)
941
+ elif (
942
+ isinstance(e, ast.Starred)
943
+ and isinstance(e.value, ast.Name)
944
+ and e.value.id in var_assigns
945
+ ):
946
+ backends.extend(var_assigns[e.value.id])
947
+ return backends
948
return []
949
950
vllm/model_executor/layers/attention/mla_attention.py
+1
@@ -334,6 +334,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
334
block_size,
335
use_mla=True,
336
use_sparse=use_sparse,
337
+ num_heads=self.num_heads,
338
)
339
340
if (
vllm/platforms/cpu.py
+1
@@ -129,6 +129,7 @@ class CpuPlatform(Platform):
129
cls,
130
selected_backend: "AttentionBackendEnum",
131
attn_selector_config: "AttentionSelectorConfig",
132
+ num_heads: int | None = None,
133
) -> str:
134
if selected_backend and selected_backend != AttentionBackendEnum.CPU_ATTN:
135
logger.info("Cannot use %s backend on CPU.", selected_backend)
vllm/platforms/cuda.py
+43
-8
@@ -45,17 +45,29 @@ torch.backends.cuda.enable_cudnn_sdp(False)
45
def _get_backend_priorities(
46
use_mla: bool,
47
device_capability: DeviceCapability,
48
+ num_heads: int | None = None,
49
) -> list[AttentionBackendEnum]:
50
"""Get backend priorities with lazy import to avoid circular dependency."""
51
if use_mla:
52
if device_capability.major == 10:
53
+ # Prefer FlashInfer at low head counts (FlashMLA uses padding)
54
+ if num_heads is not None and num_heads <= 16:
55
+ sparse_backends = [
56
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
57
+ AttentionBackendEnum.FLASHMLA_SPARSE,
58
+ ]
59
+ else:
60
+ sparse_backends = [
61
+ AttentionBackendEnum.FLASHMLA_SPARSE,
62
+ AttentionBackendEnum.FLASHINFER_MLA_SPARSE,
63
+ ]
64
return [
65
AttentionBackendEnum.FLASHINFER_MLA,
66
AttentionBackendEnum.CUTLASS_MLA,
67
AttentionBackendEnum.FLASH_ATTN_MLA,
68
AttentionBackendEnum.FLASHMLA,
69
AttentionBackendEnum.TRITON_MLA,
58
- AttentionBackendEnum.FLASHMLA_SPARSE,
70
+ *sparse_backends,
71
]
72
else:
73
return [
@@ -182,6 +194,8 @@ class CudaPlatformBase(Platform):
194
use_flashmla = False
195
use_cutlass_mla = False
196
use_flashinfer_mla = False
197
+ use_flashmla_sparse = False
198
+ use_flashinfer_mla_sparse = False
199
200
from vllm.v1.attention.ops.flashmla import is_flashmla_dense_supported
201
@@ -217,6 +231,10 @@ class CudaPlatformBase(Platform):
231
use_flashmla = backend == AttentionBackendEnum.FLASHMLA
232
use_cutlass_mla = backend == AttentionBackendEnum.CUTLASS_MLA
233
use_flashinfer_mla = backend == AttentionBackendEnum.FLASHINFER_MLA
234
+ use_flashmla_sparse = backend == AttentionBackendEnum.FLASHMLA_SPARSE
235
+ use_flashinfer_mla_sparse = (
236
+ backend == AttentionBackendEnum.FLASHINFER_MLA_SPARSE
237
+ )
238
239
if (
240
use_flashmla
@@ -242,12 +260,24 @@ class CudaPlatformBase(Platform):
260
"Forcing kv cache block size to 64 for FlashInferMLA backend."
261
)
262
245
- # TODO(Chen): remove this hacky code
246
- if use_sparse and cache_config.block_size != 64:
247
- cache_config.block_size = 64
248
- logger.info(
249
- "Forcing kv cache block size to 64 for FlashMLASparse backend."
250
- )
263
+ if use_sparse:
264
+ if not (use_flashmla_sparse or use_flashinfer_mla_sparse):
265
+ use_flashmla_sparse = True
266
+
267
+ if use_flashmla_sparse and cache_config.block_size != 64:
268
+ cache_config.block_size = 64
269
+ logger.info(
270
+ "Forcing kv cache block size to 64 for FlashMLASparse backend."
271
+ )
272
+ elif use_flashinfer_mla_sparse and cache_config.block_size not in (
273
+ 32,
274
+ 64,
275
+ ):
276
+ cache_config.block_size = 64
277
+ logger.info(
278
+ "Forcing kv cache block size to 64 for FlashInferMLASparse "
279
+ "backend."
280
+ )
281
282
scheduler_config = vllm_config.scheduler_config
283
# Note: model_config may be None during testing
@@ -276,6 +306,7 @@ class CudaPlatformBase(Platform):
306
cls,
307
device_capability: DeviceCapability,
308
attn_selector_config: "AttentionSelectorConfig",
309
+ num_heads: int | None = None,
310
) -> tuple[
311
list[tuple["AttentionBackendEnum", int]],
312
dict["AttentionBackendEnum", list[str]],
@@ -284,7 +315,9 @@ class CudaPlatformBase(Platform):
315
invalid_reasons = {}
316
317
backend_priorities = _get_backend_priorities(
287
- attn_selector_config.use_mla, device_capability
318
+ attn_selector_config.use_mla,
319
+ device_capability,
320
+ num_heads,
321
)
322
for priority, backend in enumerate(backend_priorities):
323
try:
@@ -307,6 +340,7 @@ class CudaPlatformBase(Platform):
340
cls,
341
selected_backend: "AttentionBackendEnum",
342
attn_selector_config: "AttentionSelectorConfig",
343
+ num_heads: int | None = None,
344
) -> str:
345
device_capability = cls.get_device_capability()
346
assert device_capability is not None
@@ -336,6 +370,7 @@ class CudaPlatformBase(Platform):
370
valid_backends_priorities, invalid_reasons = cls.get_valid_backends(
371
device_capability=device_capability,
372
attn_selector_config=attn_selector_config,
373
+ num_heads=num_heads,
374
)
375
reasons_str = (
376
"{"
vllm/platforms/interface.py
+1
@@ -233,6 +233,7 @@ class Platform:
233
cls,
234
selected_backend: "AttentionBackendEnum",
235
attn_selector_config: "AttentionSelectorConfig",
236
+ num_heads: int | None = None,
237
) -> str:
238
"""Get the attention backend class of a device."""
239
return ""
vllm/platforms/rocm.py
+1
@@ -265,6 +265,7 @@ class RocmPlatform(Platform):
265
cls,
266
selected_backend: "AttentionBackendEnum",
267
attn_selector_config: "AttentionSelectorConfig",
268
+ num_heads: int | None = None,
269
) -> str:
270
from vllm._aiter_ops import rocm_aiter_ops
271
vllm/platforms/xpu.py
+1
@@ -48,6 +48,7 @@ class XPUPlatform(Platform):
48
cls,
49
selected_backend: "AttentionBackendEnum",
50
attn_selector_config: "AttentionSelectorConfig",
51
+ num_heads: int | None = None,
52
) -> str:
53
from vllm.v1.attention.backends.utils import set_kv_cache_layout
54
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
new
+353
@@ -0,0 +1,353 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""FlashInfer MLA Sparse Attention Backend.
4
+
5
+This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
6
+for models like DeepSeek-V3.2 that use index-based sparse attention.
7
+
8
+For sparse MLA:
9
+- block_tables shape changes from [batch_size, max_num_blocks] (dense)
10
+ to [batch_size, q_len_per_request, sparse_mla_top_k] (sparse)
11
+- The sparse indices represent physical cache slot positions to attend to
12
+- sparse_mla_top_k parameter must be set to the topk value
13
+"""
14
+
15
+from dataclasses import dataclass
16
+from typing import TYPE_CHECKING, ClassVar
17
+
18
+import numpy as np
19
+import torch
20
+from flashinfer.decode import trtllm_batch_decode_with_kv_cache_mla
21
+
22
+from vllm.config import VllmConfig
23
+from vllm.config.cache import CacheDType
24
+from vllm.logger import init_logger
25
+from vllm.model_executor.layers.attention.mla_attention import (
26
+ get_mla_dims,
27
+)
28
+from vllm.platforms.interface import DeviceCapability
29
+from vllm.v1.attention.backend import (
30
+ AttentionBackend,
31
+ AttentionCGSupport,
32
+ AttentionLayer,
33
+ AttentionMetadata,
34
+ AttentionMetadataBuilder,
35
+ AttentionType,
36
+ CommonAttentionMetadata,
37
+ MultipleOf,
38
+ SparseMLAAttentionImpl,
39
+)
40
+from vllm.v1.attention.backends.mla.sparse_utils import (
41
+ triton_convert_req_index_to_global_index,
42
+)
43
+from vllm.v1.attention.backends.utils import KVCacheLayoutType
44
+from vllm.v1.kv_cache_interface import AttentionSpec
45
+
46
+if TYPE_CHECKING:
47
+ from vllm.model_executor.models.deepseek_v2 import Indexer
48
+
49
+logger = init_logger(__name__)
50
+
51
+FLASHINFER_MLA_SPARSE_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
52
+
53
+
54
+class FlashInferMLASparseBackend(AttentionBackend):
55
+ """FlashInfer MLA backend with sparse attention support.
56
+
57
+ This backend uses the FlashInfer TRT-LLM MLA kernel with sparse_mla_top_k
58
+ for models like DeepSeek-V3.2 that use index-based sparse attention.
59
+ """
60
+
61
+ accept_output_buffer: bool = True
62
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
63
+ supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
64
+ "auto",
65
+ "bfloat16",
66
+ ]
67
+
68
+ @staticmethod
69
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
70
+ return [32, 64]
71
+
72
+ @staticmethod
73
+ def get_name() -> str:
74
+ return "FLASHINFER_MLA_SPARSE"
75
+
76
+ @staticmethod
77
+ def get_impl_cls() -> type["FlashInferMLASparseImpl"]:
78
+ return FlashInferMLASparseImpl
79
+
80
+ @staticmethod
81
+ def get_builder_cls() -> type["FlashInferMLASparseMetadataBuilder"]:
82
+ return FlashInferMLASparseMetadataBuilder
83
+
84
+ @classmethod
85
+ def get_supported_head_sizes(cls) -> list[int]:
86
+ return [576]
87
+
88
+ @classmethod
89
+ def is_mla(cls) -> bool:
90
+ return True
91
+
92
+ @classmethod
93
+ def is_sparse(cls) -> bool:
94
+ return True
95
+
96
+ @classmethod
97
+ def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
98
+ # FlashInfer sparse MLA targets Blackwell (SM 10.x)
99
+ return capability.major == 10
100
+
101
+ @classmethod
102
+ def supports_combination(
103
+ cls,
104
+ head_size: int,
105
+ dtype: torch.dtype,
106
+ kv_cache_dtype: CacheDType | None,
107
+ block_size: int,
108
+ use_mla: bool,
109
+ has_sink: bool,
110
+ use_sparse: bool,
111
+ device_capability: DeviceCapability,
112
+ ) -> str | None:
113
+ # FlashInfer MLA sparse kernel requires qk_nope_head_dim == 128
114
+ from vllm.config import get_current_vllm_config
115
+
116
+ vllm_config = get_current_vllm_config()
117
+ if vllm_config.model_config is not None:
118
+ hf_text_config = vllm_config.model_config.hf_text_config
119
+ qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 1)
120
+ if qk_nope_head_dim != 128:
121
+ return (
122
+ f"FlashInfer MLA Sparse kernel requires qk_nope_head_dim == 128, "
123
+ f"but got {qk_nope_head_dim}"
124
+ )
125
+ # Check for index_topk which indicates sparse model
126
+ if not hasattr(hf_text_config, "index_topk"):
127
+ return "FlashInfer MLA Sparse requires model with index_topk config"
128
+ return None
129
+
130
+ @staticmethod
131
+ def get_kv_cache_shape(
132
+ num_blocks: int,
133
+ block_size: int,
134
+ num_kv_heads: int, # assumed to be 1 for MLA
135
+ head_size: int,
136
+ cache_dtype_str: str = "auto",
137
+ ) -> tuple[int, ...]:
138
+ return (num_blocks, block_size, head_size)
139
+
140
+ @classmethod
141
+ def get_required_kv_cache_layout(cls) -> "KVCacheLayoutType | None":
142
+ return "HND"
143
+
144
+
145
+@dataclass
146
+class FlashInferMLASparseMetadata(AttentionMetadata):
147
+ """Attention metadata for FlashInfer MLA Sparse backend."""
148
+
149
+ num_reqs: int
150
+ max_query_len: int
151
+ max_seq_len: int
152
+ num_actual_tokens: int
153
+
154
+ # Query start locations
155
+ query_start_loc: torch.Tensor
156
+ slot_mapping: torch.Tensor
157
+ block_table: torch.Tensor
158
+ req_id_per_token: torch.Tensor
159
+
160
+ # Sequence lengths for all requests (context + query)
161
+ seq_lens: torch.Tensor
162
+
163
+ # Sparse-specific
164
+ block_size: int = 64
165
+ topk_tokens: int = 2048
166
+
167
+
168
+class FlashInferMLASparseMetadataBuilder(
169
+ AttentionMetadataBuilder[FlashInferMLASparseMetadata]
170
+):
171
+ """Builder for FlashInfer MLA Sparse attention metadata."""
172
+
173
+ _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
174
+
175
+ def __init__(
176
+ self,
177
+ kv_cache_spec: AttentionSpec,
178
+ layer_names: list[str],
179
+ vllm_config: VllmConfig,
180
+ device: torch.device,
181
+ ) -> None:
182
+ self.vllm_config = vllm_config
183
+ self.layer_names = layer_names
184
+ self.kv_cache_spec = kv_cache_spec
185
+ self.model_config = vllm_config.model_config
186
+ self.device = device
187
+
188
+ self.mla_dims = get_mla_dims(self.model_config)
189
+ self.topk_tokens = vllm_config.model_config.hf_config.index_topk
190
+
191
+ self.req_id_per_token_buffer = torch.empty(
192
+ (vllm_config.scheduler_config.max_num_batched_tokens,),
193
+ dtype=torch.int32,
194
+ device=device,
195
+ )
196
+
197
+ def build(
198
+ self,
199
+ common_prefix_len: int,
200
+ common_attn_metadata: CommonAttentionMetadata,
201
+ fast_build: bool = False,
202
+ ) -> FlashInferMLASparseMetadata:
203
+ cm = common_attn_metadata
204
+ num_tokens = cm.num_actual_tokens
205
+
206
+ # Build req_id_per_token mapping
207
+ starts = np.asarray(cm.query_start_loc_cpu, dtype=np.int32)
208
+ seg_lengths = np.diff(starts)
209
+ req_id_per_token = np.repeat(
210
+ np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
211
+ )
212
+
213
+ # Zero-fill for cudagraphs
214
+ self.req_id_per_token_buffer.fill_(0)
215
+ self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
216
+ torch.from_numpy(req_id_per_token), non_blocking=True
217
+ )
218
+ req_id_per_token_tensor = self.req_id_per_token_buffer[:num_tokens]
219
+
220
+ return FlashInferMLASparseMetadata(
221
+ num_reqs=cm.num_reqs,
222
+ max_query_len=cm.max_query_len,
223
+ max_seq_len=cm.max_seq_len,
224
+ num_actual_tokens=cm.num_actual_tokens,
225
+ query_start_loc=cm.query_start_loc,
226
+ slot_mapping=cm.slot_mapping,
227
+ block_table=cm.block_table_tensor,
228
+ req_id_per_token=req_id_per_token_tensor,
229
+ seq_lens=cm.seq_lens,
230
+ block_size=self.kv_cache_spec.block_size,
231
+ topk_tokens=self.topk_tokens,
232
+ )
233
+
234
+
235
+# Global workspace buffer (lazily initialized)
236
+_fi_sparse_workspace: torch.Tensor | None = None
237
+
238
+
239
+def _get_workspace_buffer(device: torch.device) -> torch.Tensor:
240
+ global _fi_sparse_workspace
241
+ if _fi_sparse_workspace is None:
242
+ _fi_sparse_workspace = torch.zeros(
243
+ FLASHINFER_MLA_SPARSE_WORKSPACE_BUFFER_SIZE,
244
+ dtype=torch.uint8,
245
+ device=device,
246
+ )
247
+ return _fi_sparse_workspace
248
+
249
+
250
+class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata]):
251
+ """FlashInfer MLA Sparse implementation.
252
+
253
+ Uses the TRT-LLM MLA kernel with sparse_mla_top_k parameter for
254
+ sparse attention computation.
255
+ """
256
+
257
+ def __init__(
258
+ self,
259
+ num_heads: int,
260
+ head_size: int,
261
+ scale: float,
262
+ num_kv_heads: int,
263
+ alibi_slopes: list[float] | None,
264
+ sliding_window: int | None,
265
+ kv_cache_dtype: str,
266
+ logits_soft_cap: float | None,
267
+ attn_type: str,
268
+ kv_sharing_target_layer_name: str | None,
269
+ # MLA Specific Arguments
270
+ topk_indice_buffer: torch.Tensor | None = None,
271
+ indexer: "Indexer | None" = None,
272
+ **mla_args,
273
+ ) -> None:
274
+ unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
275
+ if any(unsupported_features):
276
+ raise NotImplementedError(
277
+ "FlashInferMLASparseImpl does not support one of the following: "
278
+ "alibi_slopes, sliding_window, logits_soft_cap"
279
+ )
280
+
281
+ if attn_type != AttentionType.DECODER:
282
+ raise NotImplementedError(
283
+ "Encoder self-attention and "
284
+ "encoder/decoder cross-attention "
285
+ "are not implemented for "
286
+ "FlashInferMLASparseImpl"
287
+ )
288
+
289
+ self.num_heads = num_heads
290
+ self.head_size = head_size
291
+ self.scale = float(scale)
292
+ self.num_kv_heads = num_kv_heads
293
+ self.kv_cache_dtype = kv_cache_dtype
294
+
295
+ # MLA-specific dimensions
296
+ self.kv_lora_rank: int = mla_args["kv_lora_rank"]
297
+ self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
298
+ self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
299
+
300
+ assert indexer is not None, "Indexer required for sparse MLA"
301
+ self.topk_indices_buffer: torch.Tensor | None = indexer.topk_indices_buffer
302
+
303
+ self._workspace_buffer: torch.Tensor | None = None
304
+ self.bmm1_scale: float | None = None
305
+ self.bmm2_scale: float | None = None
306
+
307
+ def forward_mqa(
308
+ self,
309
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
310
+ kv_c_and_k_pe_cache: torch.Tensor,
311
+ attn_metadata: FlashInferMLASparseMetadata,
312
+ layer: AttentionLayer,
313
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
314
+ if isinstance(q, tuple):
315
+ q = torch.cat(q, dim=-1)
316
+
317
+ num_actual_toks = q.shape[0]
318
+
319
+ assert self.topk_indices_buffer is not None
320
+ topk_indices = self.topk_indices_buffer[:num_actual_toks]
321
+
322
+ topk_indices_physical, seq_lens = triton_convert_req_index_to_global_index(
323
+ attn_metadata.req_id_per_token[:num_actual_toks],
324
+ attn_metadata.block_table,
325
+ topk_indices,
326
+ BLOCK_SIZE=attn_metadata.block_size,
327
+ NUM_TOPK_TOKENS=topk_indices.shape[1],
328
+ return_valid_counts=True,
329
+ )
330
+
331
+ if self._workspace_buffer is None:
332
+ self._workspace_buffer = _get_workspace_buffer(q.device)
333
+
334
+ if self.bmm1_scale is None:
335
+ self.bmm1_scale = layer._q_scale_float * layer._k_scale_float * self.scale
336
+ if self.bmm2_scale is None:
337
+ self.bmm2_scale = layer._v_scale_float
338
+
339
+ o = trtllm_batch_decode_with_kv_cache_mla(
340
+ query=q.unsqueeze(1),
341
+ kv_cache=kv_c_and_k_pe_cache.unsqueeze(1),
342
+ workspace_buffer=self._workspace_buffer,
343
+ qk_nope_head_dim=self.qk_nope_head_dim,
344
+ kv_lora_rank=self.kv_lora_rank,
345
+ qk_rope_head_dim=self.qk_rope_head_dim,
346
+ block_tables=topk_indices_physical.unsqueeze(1),
347
+ seq_lens=seq_lens,
348
+ max_seq_len=attn_metadata.topk_tokens,
349
+ bmm1_scale=self.bmm1_scale,
350
+ bmm2_scale=self.bmm2_scale,
351
+ sparse_mla_top_k=attn_metadata.topk_tokens,
352
+ )
353
+ return o.view(-1, o.shape[-2], o.shape[-1]), None
vllm/v1/attention/backends/mla/flashmla_sparse.py
+3
-161
@@ -15,7 +15,6 @@ from vllm.model_executor.layers.attention.mla_attention import (
15
)
16
from vllm.platforms import current_platform
17
from vllm.platforms.interface import DeviceCapability
18
-from vllm.triton_utils import tl, triton
18
from vllm.v1.attention.backend import (
19
AttentionBackend,
20
AttentionCGSupport,
@@ -26,6 +25,9 @@ from vllm.v1.attention.backend import (
25
MultipleOf,
26
SparseMLAAttentionImpl,
27
)
28
+from vllm.v1.attention.backends.mla.sparse_utils import (
29
+ triton_convert_req_index_to_global_index,
30
+)
31
from vllm.v1.attention.backends.utils import (
32
reshape_attn_output_for_spec_decode,
33
reshape_query_for_spec_decode,
@@ -203,166 +205,6 @@ class FlashMLASparseMetadata(AttentionMetadata):
205
fp8_use_mixed_batch: bool = False
206
207
206
-# Kernel with prefill workspace support
207
-@triton.jit
208
-def _convert_req_index_to_global_index_kernel(
209
- req_id_ptr, # int32 [num_tokens]
210
- block_table_ptr, # int32 [num_requests, max_num_blocks_per_req]
211
- token_indices_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
212
- out_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
213
- prefill_request_id_ptr, # int32 [num_tokens], -1 for decode, >=0 for prefill
214
- workspace_starts_ptr, # int32 [num_prefill_reqs+1] or nullptr
215
- # shapes (compile-time where possible)
216
- max_num_blocks_per_req: tl.constexpr,
217
- BLOCK_SIZE: tl.constexpr,
218
- BLOCK_N: tl.constexpr, # tile width along columns
219
- HAS_PREFILL: tl.constexpr,
220
- # strides (in elements)
221
- bt_stride0,
222
- bt_stride1,
223
- ti_stride0,
224
- ti_stride1,
225
- out_stride0,
226
- out_stride1,
227
-):
228
- # program_id(0) -> token_id (row)
229
- # program_id(1) -> tile index along columns
230
- token_id = tl.program_id(0)
231
- tile_id = tl.program_id(1)
232
-
233
- # Each program covers BLOCK_N consecutive columns
234
- indice_id = tile_id * BLOCK_N + tl.arange(0, BLOCK_N)
235
-
236
- # Load request id for this token (no mask: grid is exact)
237
- req = tl.load(req_id_ptr + token_id)
238
-
239
- # Load token indices for this tile
240
- ti_ptr = token_indices_ptr + token_id * ti_stride0 + indice_id * ti_stride1
241
- tok = tl.load(ti_ptr) # int32
242
-
243
- # Only token == -1 should propagate as -1
244
- is_invalid_tok = tok < 0
245
- is_prefill = False
246
- if HAS_PREFILL:
247
- prefill_req_id = tl.load(prefill_request_id_ptr + token_id)
248
- is_prefill = prefill_req_id >= 0
249
- # Compute block id and in-block offset
250
- block_id = tok // BLOCK_SIZE
251
- inblock_off = tok % BLOCK_SIZE
252
-
253
- # Guard block_table access
254
- valid_block = (block_id < max_num_blocks_per_req) & (block_id >= 0)
255
- bt_ptr = block_table_ptr + req * bt_stride0 + block_id * bt_stride1
256
- is_invalid_tok |= ~valid_block
257
- base = tl.load(bt_ptr, mask=valid_block & ~is_prefill, other=0)
258
- out_val = base * BLOCK_SIZE + inblock_off
259
-
260
- # Override with prefill output if prefill is enabled
261
- if HAS_PREFILL:
262
- workspace_start = tl.load(
263
- workspace_starts_ptr + prefill_req_id, mask=is_prefill, other=0
264
- )
265
- prefill_out = workspace_start + tok
266
- out_val = tl.where(is_prefill, prefill_out, out_val)
267
- out_val = tl.where(is_invalid_tok, -1, out_val)
268
-
269
- # Store results
270
- out_ptr_ij = out_ptr + token_id * out_stride0 + indice_id * out_stride1
271
- tl.store(out_ptr_ij, out_val)
272
-
273
-
274
-def triton_convert_req_index_to_global_index(
275
- req_id: torch.Tensor, # int32 [num_tokens]
276
- block_table: torch.Tensor, # int32 [num_requests, max_num_blocks_per_req]
277
- token_indices: torch.Tensor, # int32 [num_tokens, NUM_TOPK_TOKENS]
278
- BLOCK_SIZE: int = 64,
279
- NUM_TOPK_TOKENS: int = 2048,
280
- BLOCK_N: int = 128, # tile width along columns
281
- HAS_PREFILL_WORKSPACE: bool = False,
282
- prefill_workspace_request_ids: torch.Tensor | None = None,
283
- prefill_workspace_starts: torch.Tensor | None = None,
284
-):
285
- """
286
- out[token_id, indice_id] =
287
- block_table[req_id[token_id],
288
- token_indices[token_id, indice_id] // BLOCK_SIZE] * BLOCK_SIZE
289
- + token_indices[token_id, indice_id] % BLOCK_SIZE
290
-
291
- Only when token_indices[token_id, indice_id] == -1 do we output -1.
292
- For safety, we also output -1 if the derived block_id would be
293
- out-of-bounds.
294
-
295
- When HAS_PREFILL_WORKSPACE is True, prefill tokens are mapped to workspace offsets
296
- instead of global cache slots. prefill_workspace_request_ids and
297
- prefill_workspace_starts must be provided.
298
-
299
- prefill_workspace_request_ids: int32 [num_tokens], -1 for decode else
300
- prefill request index (maps to prefill_workspace_starts)
301
- prefill_workspace_starts: int32 [num_prefills], 0-indexed workspace
302
- starts for each prefill request
303
- """
304
- assert req_id.dtype == torch.int32
305
- assert block_table.dtype == torch.int32
306
- assert token_indices.dtype == torch.int32
307
- assert token_indices.shape[1] == NUM_TOPK_TOKENS
308
- assert NUM_TOPK_TOKENS % BLOCK_N == 0, (
309
- f"NUM_TOPK_TOKENS ({NUM_TOPK_TOKENS}) must be divisible by BLOCK_N ({BLOCK_N})"
310
- )
311
-
312
- if HAS_PREFILL_WORKSPACE:
313
- assert prefill_workspace_request_ids is not None
314
- assert prefill_workspace_starts is not None
315
- assert prefill_workspace_request_ids.dtype == torch.int32
316
- assert prefill_workspace_starts.dtype == torch.int32
317
-
318
- num_tokens = req_id.shape[0]
319
- max_num_blocks_per_req = block_table.shape[1]
320
- tiles_per_row = NUM_TOPK_TOKENS // BLOCK_N
321
-
322
- # Ensure contiguous tensors on the same device
323
- req_id_c = req_id.contiguous()
324
- block_table_c = block_table.contiguous()
325
- token_indices_c = token_indices.contiguous()
326
- out = torch.empty_like(token_indices_c)
327
-
328
- # Strides in elements
329
- bt_stride0, bt_stride1 = block_table_c.stride()
330
- ti_stride0, ti_stride1 = token_indices_c.stride()
331
- out_stride0, out_stride1 = out.stride()
332
-
333
- # Prepare prefill pointers
334
- if HAS_PREFILL_WORKSPACE:
335
- assert prefill_workspace_request_ids is not None # for mypy
336
- assert prefill_workspace_starts is not None # for mypy
337
- assert prefill_workspace_request_ids.is_contiguous()
338
- assert prefill_workspace_starts.is_contiguous()
339
-
340
- # Exact 2D grid: tokens × column tiles
341
- grid = (num_tokens, tiles_per_row)
342
-
343
- _convert_req_index_to_global_index_kernel[grid](
344
- req_id_c,
345
- block_table_c,
346
- token_indices_c,
347
- out,
348
- prefill_workspace_request_ids,
349
- prefill_workspace_starts,
350
- # shapes / constexprs
351
- max_num_blocks_per_req,
352
- BLOCK_SIZE,
353
- BLOCK_N,
354
- HAS_PREFILL_WORKSPACE,
355
- # strides
356
- bt_stride0,
357
- bt_stride1,
358
- ti_stride0,
359
- ti_stride1,
360
- out_stride0,
361
- out_stride1,
362
- )
363
- return out
364
-
365
-
208
def get_prefill_workspace_size(max_model_len: int):
209
# NOTE(Lucas): 5 is a magic number for controlling the prefill buffer size.
210
# May be tuned later.
vllm/v1/attention/backends/mla/sparse_utils.py
new
+191
@@ -0,0 +1,191 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Utility functions for sparse MLA backends."""
4
+
5
+import torch
6
+
7
+from vllm.triton_utils import tl, triton
8
+
9
+
10
+# Kernel with prefill workspace support and valid count tracking
11
+@triton.jit
12
+def _convert_req_index_to_global_index_kernel(
13
+ req_id_ptr, # int32 [num_tokens]
14
+ block_table_ptr, # int32 [num_requests, max_num_blocks_per_req]
15
+ token_indices_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
16
+ out_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
17
+ valid_count_ptr, # int32 [num_tokens] - output valid count per row
18
+ prefill_request_id_ptr, # int32 [num_tokens], -1 for decode, >=0 for prefill
19
+ workspace_starts_ptr, # int32 [num_prefill_reqs+1] or nullptr
20
+ # shapes (compile-time where possible)
21
+ max_num_blocks_per_req: tl.constexpr,
22
+ BLOCK_SIZE: tl.constexpr,
23
+ BLOCK_N: tl.constexpr, # tile width along columns
24
+ HAS_PREFILL: tl.constexpr,
25
+ COUNT_VALID: tl.constexpr, # whether to count valid indices
26
+ # strides (in elements)
27
+ bt_stride0,
28
+ bt_stride1,
29
+ ti_stride0,
30
+ ti_stride1,
31
+ out_stride0,
32
+ out_stride1,
33
+):
34
+ # program_id(0) -> token_id (row)
35
+ # program_id(1) -> tile index along columns
36
+ token_id = tl.program_id(0)
37
+ tile_id = tl.program_id(1)
38
+
39
+ # Each program covers BLOCK_N consecutive columns
40
+ indice_id = tile_id * BLOCK_N + tl.arange(0, BLOCK_N)
41
+
42
+ # Load request id for this token (no mask: grid is exact)
43
+ req = tl.load(req_id_ptr + token_id)
44
+
45
+ # Load token indices for this tile
46
+ ti_ptr = token_indices_ptr + token_id * ti_stride0 + indice_id * ti_stride1
47
+ tok = tl.load(ti_ptr) # int32
48
+
49
+ # Only token == -1 should propagate as -1
50
+ is_invalid_tok = tok < 0
51
+ is_prefill = False
52
+ if HAS_PREFILL:
53
+ prefill_req_id = tl.load(prefill_request_id_ptr + token_id)
54
+ is_prefill = prefill_req_id >= 0
55
+ # Compute block id and in-block offset
56
+ block_id = tok // BLOCK_SIZE
57
+ inblock_off = tok % BLOCK_SIZE
58
+
59
+ # Guard block_table access
60
+ valid_block = (block_id < max_num_blocks_per_req) & (block_id >= 0)
61
+ bt_ptr = block_table_ptr + req * bt_stride0 + block_id * bt_stride1
62
+ is_invalid_tok |= ~valid_block
63
+ base = tl.load(bt_ptr, mask=valid_block & ~is_prefill, other=0)
64
+ out_val = base * BLOCK_SIZE + inblock_off
65
+
66
+ # Override with prefill output if prefill is enabled
67
+ if HAS_PREFILL:
68
+ workspace_start = tl.load(
69
+ workspace_starts_ptr + prefill_req_id, mask=is_prefill, other=0
70
+ )
71
+ prefill_out = workspace_start + tok
72
+ out_val = tl.where(is_prefill, prefill_out, out_val)
73
+ out_val = tl.where(is_invalid_tok, -1, out_val)
74
+
75
+ # Store results
76
+ out_ptr_ij = out_ptr + token_id * out_stride0 + indice_id * out_stride1
77
+ tl.store(out_ptr_ij, out_val)
78
+
79
+ # Count valid indices in this tile and atomically add to row total
80
+ if COUNT_VALID:
81
+ tile_valid_count = tl.sum((~is_invalid_tok).to(tl.int32))
82
+ tl.atomic_add(valid_count_ptr + token_id, tile_valid_count)
83
+
84
+
85
+def triton_convert_req_index_to_global_index(
86
+ req_id: torch.Tensor, # int32 [num_tokens]
87
+ block_table: torch.Tensor, # int32 [num_requests, max_num_blocks_per_req]
88
+ token_indices: torch.Tensor, # int32 [num_tokens, NUM_TOPK_TOKENS]
89
+ BLOCK_SIZE: int = 64,
90
+ NUM_TOPK_TOKENS: int = 2048,
91
+ BLOCK_N: int = 128, # tile width along columns
92
+ HAS_PREFILL_WORKSPACE: bool = False,
93
+ prefill_workspace_request_ids: torch.Tensor | None = None,
94
+ prefill_workspace_starts: torch.Tensor | None = None,
95
+ return_valid_counts: bool = False,
96
+) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
97
+ """
98
+ out[token_id, indice_id] =
99
+ block_table[req_id[token_id],
100
+ token_indices[token_id, indice_id] // BLOCK_SIZE] * BLOCK_SIZE
101
+ + token_indices[token_id, indice_id] % BLOCK_SIZE
102
+
103
+ Only when token_indices[token_id, indice_id] == -1 do we output -1.
104
+ For safety, we also output -1 if the derived block_id would be
105
+ out-of-bounds.
106
+
107
+ When HAS_PREFILL_WORKSPACE is True, prefill tokens are mapped to workspace offsets
108
+ instead of global cache slots. prefill_workspace_request_ids and
109
+ prefill_workspace_starts must be provided.
110
+
111
+ prefill_workspace_request_ids: int32 [num_tokens], -1 for decode else
112
+ prefill request index (maps to prefill_workspace_starts)
113
+ prefill_workspace_starts: int32 [num_prefills], 0-indexed workspace
114
+ starts for each prefill request
115
+
116
+ When return_valid_counts is True, also returns the count of valid (non -1)
117
+ indices per row, computed during the same kernel pass (no extra overhead).
118
+ """
119
+ assert req_id.dtype == torch.int32
120
+ assert block_table.dtype == torch.int32
121
+ assert token_indices.dtype == torch.int32
122
+ assert token_indices.shape[1] == NUM_TOPK_TOKENS
123
+ assert NUM_TOPK_TOKENS % BLOCK_N == 0, (
124
+ f"NUM_TOPK_TOKENS ({NUM_TOPK_TOKENS}) must be divisible by BLOCK_N ({BLOCK_N})"
125
+ )
126
+
127
+ if HAS_PREFILL_WORKSPACE:
128
+ assert prefill_workspace_request_ids is not None
129
+ assert prefill_workspace_starts is not None
130
+ assert prefill_workspace_request_ids.dtype == torch.int32
131
+ assert prefill_workspace_starts.dtype == torch.int32
132
+
133
+ num_tokens = req_id.shape[0]
134
+ max_num_blocks_per_req = block_table.shape[1]
135
+ tiles_per_row = NUM_TOPK_TOKENS // BLOCK_N
136
+
137
+ # Ensure contiguous tensors on the same device
138
+ req_id_c = req_id.contiguous()
139
+ block_table_c = block_table.contiguous()
140
+ token_indices_c = token_indices.contiguous()
141
+ out = torch.empty_like(token_indices_c)
142
+
143
+ # Allocate valid count buffer if needed (must be zero-initialized for atomics)
144
+ valid_counts: torch.Tensor | None = None
145
+ if return_valid_counts:
146
+ valid_counts = torch.zeros(
147
+ num_tokens, dtype=torch.int32, device=token_indices.device
148
+ )
149
+
150
+ # Strides in elements
151
+ bt_stride0, bt_stride1 = block_table_c.stride()
152
+ ti_stride0, ti_stride1 = token_indices_c.stride()
153
+ out_stride0, out_stride1 = out.stride()
154
+
155
+ # Prepare prefill pointers
156
+ if HAS_PREFILL_WORKSPACE:
157
+ assert prefill_workspace_request_ids is not None # for mypy
158
+ assert prefill_workspace_starts is not None # for mypy
159
+ assert prefill_workspace_request_ids.is_contiguous()
160
+ assert prefill_workspace_starts.is_contiguous()
161
+
162
+ # Exact 2D grid: tokens × column tiles
163
+ grid = (num_tokens, tiles_per_row)
164
+
165
+ _convert_req_index_to_global_index_kernel[grid](
166
+ req_id_c,
167
+ block_table_c,
168
+ token_indices_c,
169
+ out,
170
+ valid_counts,
171
+ prefill_workspace_request_ids,
172
+ prefill_workspace_starts,
173
+ # shapes / constexprs
174
+ max_num_blocks_per_req,
175
+ BLOCK_SIZE,
176
+ BLOCK_N,
177
+ HAS_PREFILL_WORKSPACE,
178
+ return_valid_counts,
179
+ # strides
180
+ bt_stride0,
181
+ bt_stride1,
182
+ ti_stride0,
183
+ ti_stride1,
184
+ out_stride0,
185
+ out_stride1,
186
+ )
187
+
188
+ if return_valid_counts:
189
+ assert valid_counts is not None
190
+ return out, valid_counts
191
+ return out
vllm/v1/attention/backends/registry.py
+4
@@ -62,6 +62,10 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
62
FLASHINFER_MLA = (
63
"vllm.v1.attention.backends.mla.flashinfer_mla.FlashInferMLABackend"
64
)
65
+ FLASHINFER_MLA_SPARSE = (
66
+ "vllm.v1.attention.backends.mla.flashinfer_mla_sparse."
67
+ "FlashInferMLASparseBackend"
68
+ )
69
TRITON_MLA = "vllm.v1.attention.backends.mla.triton_mla.TritonMLABackend"
70
CUTLASS_MLA = "vllm.v1.attention.backends.mla.cutlass_mla.CutlassMLABackend"
71
FLASHMLA = "vllm.v1.attention.backends.mla.flashmla.FlashMLABackend"
vllm/v1/attention/selector.py
+5
-2
@@ -53,6 +53,7 @@ def get_attn_backend(
53
use_sparse: bool = False,
54
use_mm_prefix: bool = False,
55
attn_type: str | None = None,
56
+ num_heads: int | None = None,
57
) -> type[AttentionBackend]:
58
"""Selects which attention backend to use and lazily imports it."""
59
@@ -66,7 +67,6 @@ def get_attn_backend(
67
from vllm.config import get_current_vllm_config
68
69
vllm_config = get_current_vllm_config()
69
- backend_enum = vllm_config.attention_config.backend
70
71
attn_selector_config = AttentionSelectorConfig(
72
head_size=head_size,
@@ -81,8 +81,9 @@ def get_attn_backend(
81
)
82
83
return _cached_get_attn_backend(
84
- backend=backend_enum,
84
+ backend=vllm_config.attention_config.backend,
85
attn_selector_config=attn_selector_config,
86
+ num_heads=num_heads,
87
)
88
89
@@ -90,12 +91,14 @@ def get_attn_backend(
91
def _cached_get_attn_backend(
92
backend,
93
attn_selector_config: AttentionSelectorConfig,
94
+ num_heads: int | None = None,
95
) -> type[AttentionBackend]:
96
from vllm.platforms import current_platform
97
98
attention_cls = current_platform.get_attn_backend_cls(
99
backend,
100
attn_selector_config=attn_selector_config,
101
+ num_heads=num_heads,
102
)
103
if not attention_cls:
104
raise ValueError(