[1/N] Add dense MHA path for sparse MLA short sequences (#47327)

Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Matthew Bonanni committed Jul 13, 2026 at 20:29 UTC c4f5cd60dae386d106c9b8a12dbab24e2e9dda0b
21 files changed +1991 -647
benchmarks/attention_benchmarks/benchmark.py
+175 -3
@@ -75,7 +75,11 @@ def run_mla_benchmark(config: BenchmarkConfig, **kwargs) -> BenchmarkResult:
75 from mla_runner import run_mla_benchmark as run_mla
76
77 return run_mla(
78 - config.backend, config, prefill_backend=config.prefill_backend, **kwargs
78 + config.backend,
79 + config,
80 + prefill_backend=config.prefill_backend,
81 + sparse_mla_force_mqa=config.sparse_mla_force_mqa,
82 + **kwargs,
83 )
84
85
@@ -592,6 +596,30 @@ def main():
596 default="profile",
597 help="Output file name for ncu profile (default: 'profile').",
598 )
599 + parser.add_argument(
600 + "--torch-profile",
601 + action="store_true",
602 + default=False,
603 + help="Collect a PyTorch profiler Chrome trace for each benchmark run.",
604 + )
605 + parser.add_argument(
606 + "--torch-profile-dir",
607 + default=None,
608 + help="Directory for PyTorch profiler traces.",
609 + )
610 + parser.add_argument(
611 + "--torch-profile-iters",
612 + type=int,
613 + default=3,
614 + help="Number of forward passes to record per PyTorch profiler trace.",
615 + )
616 + parser.add_argument(
617 + "--sparse-mla-mha-variants",
618 + nargs="+",
619 + default=None,
620 + choices=["dense_mha", "mqa"],
621 + help="Sparse MLA variants to run in mha_vs_mqa mode. Defaults to both.",
622 + )
623
624 # Parameter sweep (use YAML config for advanced sweeps)
625 parser.add_argument(
@@ -641,6 +669,7 @@ def main():
669
670 # Prefill backends (e.g., ["fa3", "fa4"])
671 args.prefill_backends = yaml_config.get("prefill_backends", None)
672 + args.prefill_backend = yaml_config.get("prefill_backend", None)
673
674 # FP8 output benchmark knobs; CLI wins.
675 if args.fp8_output_scale is None:
@@ -683,6 +712,9 @@ def main():
712 args.num_q_heads = model.get("num_q_heads", args.num_q_heads)
713 args.num_kv_heads = model.get("num_kv_heads", args.num_kv_heads)
714 args.block_size = model.get("block_size", args.block_size)
715 + args.max_model_len = model.get(
716 + "max_model_len", getattr(args, "max_model_len", None)
717 + )
718 # MLA-specific dimensions
719 args.kv_lora_rank = model.get("kv_lora_rank", args.kv_lora_rank)
720 args.qk_nope_head_dim = model.get("qk_nope_head_dim", args.qk_nope_head_dim)
@@ -701,6 +733,21 @@ def main():
733 args.cuda_graphs = yaml_config["cuda_graphs"]
734 if "ncu_profile" in yaml_config:
735 args.ncu_profile = yaml_config["ncu_profile"]
736 + if "torch_profile" in yaml_config:
737 + args.torch_profile = yaml_config["torch_profile"]
738 + if "torch_profile_dir" in yaml_config:
739 + args.torch_profile_dir = yaml_config["torch_profile_dir"]
740 + if "torch_profile_iters" in yaml_config:
741 + args.torch_profile_iters = yaml_config["torch_profile_iters"]
742 + args.sparse_mla_topk_pattern = yaml_config.get(
743 + "sparse_mla_topk_pattern", "random"
744 + )
745 + args.sparse_mla_dense_mha_max_seq_len = yaml_config.get(
746 + "sparse_mla_dense_mha_max_seq_len", None
747 + )
748 + args.sparse_mla_mha_variants = yaml_config.get(
749 + "sparse_mla_mha_variants", args.sparse_mla_mha_variants
750 + )
751
752 # Parameter sweep configuration
753 if "parameter_sweep" in yaml_config:
@@ -842,8 +889,6 @@ def main():
889 num_kv_heads=args.num_kv_heads,
890 block_size=args.block_size,
891 device=args.device,
845 - repeats=args.repeats,
846 - warmup_iters=args.warmup_iters,
892 profile_memory=args.profile_memory,
893 kv_cache_dtype=args.kv_cache_dtype,
894 use_cuda_graphs=args.cuda_graphs,
@@ -1063,6 +1108,133 @@ def main():
1108 f"\n [yellow]Prefill always faster for batch_size={bs}[/]"
1109 )
1110
1111 + # Handle MHA vs MQA comparison mode for sparse MLA
1112 + elif hasattr(args, "mode") and args.mode == "mha_vs_mqa":
1113 + console.print("[yellow]Mode: MHA vs MQA comparison for sparse MLA[/]")
1114 +
1115 + sparse_mla_topk_pattern = getattr(args, "sparse_mla_topk_pattern", "random")
1116 + dense_mha_max_seq_len = getattr(args, "sparse_mla_dense_mha_max_seq_len", None)
1117 + prefill_backend = getattr(args, "prefill_backend", None)
1118 + if prefill_backend:
1119 + console.print(f"Prefill backend: {prefill_backend}")
1120 + available_variants = [
1121 + ("dense_mha", False, "dense"),
1122 + ("mqa", True, "auto"),
1123 + ]
1124 + requested_variants = getattr(args, "sparse_mla_mha_variants", None)
1125 + if requested_variants is not None:
1126 + valid_variants = {label for label, _, _ in available_variants}
1127 + invalid_variants = sorted(set(requested_variants) - valid_variants)
1128 + if invalid_variants:
1129 + raise ValueError(
1130 + "Invalid sparse_mla_mha_variants entries: "
1131 + f"{invalid_variants}. Valid variants are: "
1132 + f"{sorted(valid_variants)}"
1133 + )
1134 + requested_variant_set = set(requested_variants)
1135 + variants = [
1136 + variant
1137 + for variant in available_variants
1138 + if variant[0] in requested_variant_set
1139 + ]
1140 + else:
1141 + variants = available_variants
1142 + formatter = ResultsFormatter(console)
1143 + total = 0
1144 + for spec in args.batch_specs:
1145 + q_len = max(request.q_len for request in parse_batch_spec(spec))
1146 + for variant_label, _, _ in variants:
1147 + if (
1148 + variant_label == "dense_mha"
1149 + and dense_mha_max_seq_len is not None
1150 + and q_len > dense_mha_max_seq_len
1151 + ):
1152 + continue
1153 + total += len(backends)
1154 +
1155 + with tqdm(total=total, desc="Benchmarking") as pbar:
1156 + for spec in args.batch_specs:
1157 + q_len = max(request.q_len for request in parse_batch_spec(spec))
1158 + for backend in backends:
1159 + for variant_label, force_mqa, mha_mode in variants:
1160 + if (
1161 + variant_label == "dense_mha"
1162 + and dense_mha_max_seq_len is not None
1163 + and q_len > dense_mha_max_seq_len
1164 + ):
1165 + continue
1166 + config = BenchmarkConfig(
1167 + backend=f"{backend}_{variant_label}",
1168 + batch_spec=spec,
1169 + num_layers=args.num_layers,
1170 + head_dim=args.head_dim,
1171 + num_q_heads=args.num_q_heads,
1172 + num_kv_heads=args.num_kv_heads,
1173 + block_size=args.block_size,
1174 + device=args.device,
1175 + max_model_len=getattr(args, "max_model_len", None),
1176 + kv_cache_dtype=args.kv_cache_dtype,
1177 + profile_memory=args.profile_memory,
1178 + use_cuda_graphs=args.cuda_graphs,
1179 + ncu_profile=args.ncu_profile,
1180 + torch_profile=args.torch_profile,
1181 + torch_profile_dir=args.torch_profile_dir,
1182 + torch_profile_iters=args.torch_profile_iters,
1183 + warmup_ms=args.warmup_ms,
1184 + kv_lora_rank=getattr(args, "kv_lora_rank", None),
1185 + qk_nope_head_dim=getattr(args, "qk_nope_head_dim", None),
1186 + qk_rope_head_dim=getattr(args, "qk_rope_head_dim", None),
1187 + v_head_dim=getattr(args, "v_head_dim", None),
1188 + sparse_mla_force_mqa=force_mqa,
1189 + sparse_mla_mha_mode=mha_mode,
1190 + sparse_mla_dense_mha_max_seq_len=dense_mha_max_seq_len,
1191 + sparse_mla_topk_pattern=sparse_mla_topk_pattern,
1192 + prefill_backend=prefill_backend,
1193 + )
1194 +
1195 + # run_mla_benchmark needs the real backend name
1196 + from mla_runner import run_mla_benchmark as run_mla
1197 +
1198 + run_label = f"{backend}_{variant_label} {spec}"
1199 + pbar.set_postfix_str(run_label)
1200 +
1201 + try:
1202 + result = run_mla(
1203 + backend,
1204 + config,
1205 + prefill_backend=prefill_backend,
1206 + sparse_mla_force_mqa=force_mqa,
1207 + )
1208 + except Exception as e:
1209 + result = BenchmarkResult(
1210 + config=config,
1211 + mean_time=float("inf"),
1212 + median_time=float("inf"),
1213 + std_time=0,
1214 + min_time=float("inf"),
1215 + max_time=float("inf"),
1216 + error=str(e),
1217 + )
1218 +
1219 + all_results.append(result)
1220 + if args.output_csv:
1221 + formatter.save_csv(all_results, args.output_csv)
1222 + if args.output_json:
1223 + formatter.save_json(all_results, args.output_json)
1224 +
1225 + if not result.success:
1226 + console.print(
1227 + f"[red]Error {backend}_{variant_label} "
1228 + f"{spec}: {result.error}[/]"
1229 + )
1230 +
1231 + pbar.update(1)
1232 +
1233 + # Display results with variant labels as separate "backends"
1234 + console.print("\n[bold green]MHA vs MQA Results:[/]")
1235 + variant_backends = [f"{b}_{v}" for b in backends for v, _, _ in variants]
1236 + formatter.print_table(all_results, variant_backends)
1237 +
1238 # Handle model parameter sweep mode
1239 elif hasattr(args, "model_parameter_sweep") and args.model_parameter_sweep:
1240 # Model parameter sweep
benchmarks/attention_benchmarks/common.py
+68 -37
@@ -4,8 +4,10 @@
4 """Common utilities for attention benchmarking."""
5
6 import csv
7 +import gc
8 import json
9 import math
10 +from collections.abc import Sequence
11 from dataclasses import asdict, dataclass
12 from pathlib import Path
13 from typing import Any
@@ -44,10 +46,13 @@ def run_do_bench(
46 kwargs: dict[str, Any] = {"return_mode": "all"}
47 if use_cuda_graphs:
48 result = triton.testing.do_bench_cudagraph(benchmark_fn, **kwargs)
49 + gc.collect()
50 + torch.accelerator.empty_cache()
51 else:
52 if warmup_ms is not None:
53 kwargs["warmup"] = warmup_ms
54 result = triton.testing.do_bench(benchmark_fn, **kwargs)
55 + torch.accelerator.synchronize()
56 return result
57
58
@@ -91,42 +96,6 @@ except ImportError:
96 AttentionLayerBase = object # Fallback
97
98
94 -class MockKVBProj:
95 - """Mock KV projection layer for MLA prefill mode.
96 -
97 - Mimics ColumnParallelLinear behavior for kv_b_proj in MLA backends.
98 - Projects kv_c_normed to [qk_nope_head_dim + v_head_dim] per head.
99 - """
100 -
101 - def __init__(self, num_heads: int, qk_nope_head_dim: int, v_head_dim: int):
102 - self.num_heads = num_heads
103 - self.qk_nope_head_dim = qk_nope_head_dim
104 - self.v_head_dim = v_head_dim
105 - self.out_dim = qk_nope_head_dim + v_head_dim
106 - self.weight = torch.empty(0, dtype=torch.bfloat16)
107 -
108 - def __call__(self, x: torch.Tensor) -> tuple[torch.Tensor]:
109 - """
110 - Project kv_c_normed to output space.
111 -
112 - Args:
113 - x: Input tensor [num_tokens, kv_lora_rank]
114 -
115 - Returns:
116 - Tuple containing output tensor
117 - [num_tokens, num_heads, qk_nope_head_dim + v_head_dim]
118 - """
119 - num_tokens = x.shape[0]
120 - result = torch.randn(
121 - num_tokens,
122 - self.num_heads,
123 - self.out_dim,
124 - device=x.device,
125 - dtype=x.dtype,
126 - )
127 - return (result,) # Return as tuple to match ColumnParallelLinear API
128 -
129 -
99 class MockIndexer:
100 """Mock Indexer for sparse MLA backends.
101
@@ -158,6 +127,60 @@ class MockIndexer:
127 )
128 self.topk_indices_buffer[:num_tokens] = indices
129
130 + def fill_indices(
131 + self,
132 + num_tokens: int,
133 + max_kv_len: int,
134 + pattern: str = "random",
135 + requests: Sequence[Any] | None = None,
136 + ):
137 + if pattern == "random":
138 + self.fill_random_indices(num_tokens, max_kv_len)
139 + return
140 + if pattern == "prefix":
141 + indices = torch.arange(
142 + self.topk_tokens,
143 + dtype=torch.int32,
144 + device=self.topk_indices_buffer.device,
145 + )
146 + indices = (indices % max_kv_len).expand(num_tokens, -1)
147 + self.topk_indices_buffer[:num_tokens] = indices
148 + return
149 + if pattern == "sliding_window":
150 + if requests is None:
151 + start = max(max_kv_len - self.topk_tokens, 0)
152 + indices = torch.arange(
153 + start,
154 + start + self.topk_tokens,
155 + dtype=torch.int32,
156 + device=self.topk_indices_buffer.device,
157 + )
158 + indices = indices.clamp(max=max_kv_len - 1).expand(num_tokens, -1)
159 + self.topk_indices_buffer[:num_tokens] = indices
160 + return
161 +
162 + rows = []
163 + offsets = torch.arange(
164 + self.topk_tokens,
165 + dtype=torch.int32,
166 + device=self.topk_indices_buffer.device,
167 + ) - (self.topk_tokens - 1)
168 + for request in requests:
169 + q_len = request.q_len
170 + kv_len = request.kv_len
171 + context_len = kv_len - q_len
172 + positions = torch.arange(
173 + context_len,
174 + kv_len,
175 + dtype=torch.int32,
176 + device=self.topk_indices_buffer.device,
177 + )
178 + row_indices = positions[:, None] + offsets[None, :]
179 + rows.append(row_indices.clamp(min=0, max=kv_len - 1))
180 + self.topk_indices_buffer[:num_tokens] = torch.cat(rows, dim=0)
181 + return
182 + raise ValueError(f"Unknown sparse MLA topk pattern: {pattern}")
183 +
184
185 class MockLayer(AttentionLayerBase):
186 """Mock attention layer with scale parameters and impl.
@@ -252,10 +275,14 @@ class BenchmarkConfig:
275 num_kv_heads: int
276 block_size: int
277 device: str
278 + max_model_len: int | None = None
279 dtype: torch.dtype = torch.float16
280 profile_memory: bool = False
257 - use_cuda_graphs: bool = False
281 + use_cuda_graphs: bool = True
282 ncu_profile: bool = False
283 + torch_profile: bool = False
284 + torch_profile_dir: str | None = None
285 + torch_profile_iters: int = 3
286 warmup_ms: int | None = None
287
288 # "auto" or "fp8"
@@ -271,6 +298,10 @@ class BenchmarkConfig:
298 # Backend-specific tuning
299 num_kv_splits: int | None = None # CUTLASS MLA
300 reorder_batch_threshold: int | None = None # FlashAttn MLA, FlashMLA
301 + sparse_mla_force_mqa: bool = False # Force MQA path for sparse MLA
302 + sparse_mla_mha_mode: str = "auto" # "auto" or "dense"
303 + sparse_mla_dense_mha_max_seq_len: int | None = None
304 + sparse_mla_topk_pattern: str = "random" # "random", "prefix", "sliding_window"
305 num_splits: int | None = None # FlashAttention split-K (0=auto, 1=disabled)
306
307
benchmarks/attention_benchmarks/configs/mla_sparse_mha_vs_mqa.yaml new
+474
@@ -0,0 +1,474 @@
1 +# Sparse MLA benchmark: forward_mha vs forward_mqa
2 +#
3 +# Usage:
4 +# python benchmark.py --config configs/mla_sparse_mha_vs_mqa.yaml
5 +#
6 +# Heatmap grid:
7 +# - batch_size: 1, 2, 4, 8, 16, 32
8 +# - seq_len: 32, 64, 128, 256, 512, 1024, 2048
9 +# - q_len: powers of two through seq_len
10 +#
11 +# Specs with q_len < seq_len include context; the q_len == seq_len diagonal
12 +# covers pure prefill.
13 +# The model shape below is the DP case. For the TP8 run, manually change
14 +# model.num_q_heads from 128 to 16 before rerunning this benchmark.
15 +
16 +mode: mha_vs_mqa
17 +
18 +model:
19 + name: "deepseek-v3"
20 + num_layers: 60
21 + num_q_heads: 128
22 + num_kv_heads: 1
23 + head_dim: 576
24 + kv_lora_rank: 512
25 + qk_nope_head_dim: 128
26 + qk_rope_head_dim: 64
27 + v_head_dim: 128
28 + block_size: 128
29 + max_model_len: 2048
30 +
31 +batch_specs:
32 + # Batch size 1
33 + # seq_len = 32
34 + - "1q1s32"
35 + - "1q2s32"
36 + - "1q4s32"
37 + - "1q8s32"
38 + - "1q16s32"
39 + - "1q32"
40 + # seq_len = 64
41 + - "1q1s64"
42 + - "1q2s64"
43 + - "1q4s64"
44 + - "1q8s64"
45 + - "1q16s64"
46 + - "1q32s64"
47 + - "1q64"
48 + # seq_len = 128
49 + - "1q1s128"
50 + - "1q2s128"
51 + - "1q4s128"
52 + - "1q8s128"
53 + - "1q16s128"
54 + - "1q32s128"
55 + - "1q64s128"
56 + - "1q128"
57 + # seq_len = 256
58 + - "1q1s256"
59 + - "1q2s256"
60 + - "1q4s256"
61 + - "1q8s256"
62 + - "1q16s256"
63 + - "1q32s256"
64 + - "1q64s256"
65 + - "1q128s256"
66 + - "1q256"
67 + # seq_len = 512
68 + - "1q1s512"
69 + - "1q2s512"
70 + - "1q4s512"
71 + - "1q8s512"
72 + - "1q16s512"
73 + - "1q32s512"
74 + - "1q64s512"
75 + - "1q128s512"
76 + - "1q256s512"
77 + - "1q512"
78 + # seq_len = 1024
79 + - "1q1s1024"
80 + - "1q2s1024"
81 + - "1q4s1024"
82 + - "1q8s1024"
83 + - "1q16s1024"
84 + - "1q32s1024"
85 + - "1q64s1024"
86 + - "1q128s1024"
87 + - "1q256s1024"
88 + - "1q512s1024"
89 + - "1q1024"
90 + # seq_len = 2048
91 + - "1q1s2048"
92 + - "1q2s2048"
93 + - "1q4s2048"
94 + - "1q8s2048"
95 + - "1q16s2048"
96 + - "1q32s2048"
97 + - "1q64s2048"
98 + - "1q128s2048"
99 + - "1q256s2048"
100 + - "1q512s2048"
101 + - "1q1024s2048"
102 + - "1q2048"
103 +
104 + # Batch size 2
105 + # seq_len = 32
106 + - "2q1s32"
107 + - "2q2s32"
108 + - "2q4s32"
109 + - "2q8s32"
110 + - "2q16s32"
111 + - "2q32"
112 + # seq_len = 64
113 + - "2q1s64"
114 + - "2q2s64"
115 + - "2q4s64"
116 + - "2q8s64"
117 + - "2q16s64"
118 + - "2q32s64"
119 + - "2q64"
120 + # seq_len = 128
121 + - "2q1s128"
122 + - "2q2s128"
123 + - "2q4s128"
124 + - "2q8s128"
125 + - "2q16s128"
126 + - "2q32s128"
127 + - "2q64s128"
128 + - "2q128"
129 + # seq_len = 256
130 + - "2q1s256"
131 + - "2q2s256"
132 + - "2q4s256"
133 + - "2q8s256"
134 + - "2q16s256"
135 + - "2q32s256"
136 + - "2q64s256"
137 + - "2q128s256"
138 + - "2q256"
139 + # seq_len = 512
140 + - "2q1s512"
141 + - "2q2s512"
142 + - "2q4s512"
143 + - "2q8s512"
144 + - "2q16s512"
145 + - "2q32s512"
146 + - "2q64s512"
147 + - "2q128s512"
148 + - "2q256s512"
149 + - "2q512"
150 + # seq_len = 1024
151 + - "2q1s1024"
152 + - "2q2s1024"
153 + - "2q4s1024"
154 + - "2q8s1024"
155 + - "2q16s1024"
156 + - "2q32s1024"
157 + - "2q64s1024"
158 + - "2q128s1024"
159 + - "2q256s1024"
160 + - "2q512s1024"
161 + - "2q1024"
162 + # seq_len = 2048
163 + - "2q1s2048"
164 + - "2q2s2048"
165 + - "2q4s2048"
166 + - "2q8s2048"
167 + - "2q16s2048"
168 + - "2q32s2048"
169 + - "2q64s2048"
170 + - "2q128s2048"
171 + - "2q256s2048"
172 + - "2q512s2048"
173 + - "2q1024s2048"
174 + - "2q2048"
175 +
176 + # Batch size 4
177 + # seq_len = 32
178 + - "4q1s32"
179 + - "4q2s32"
180 + - "4q4s32"
181 + - "4q8s32"
182 + - "4q16s32"
183 + - "4q32"
184 + # seq_len = 64
185 + - "4q1s64"
186 + - "4q2s64"
187 + - "4q4s64"
188 + - "4q8s64"
189 + - "4q16s64"
190 + - "4q32s64"
191 + - "4q64"
192 + # seq_len = 128
193 + - "4q1s128"
194 + - "4q2s128"
195 + - "4q4s128"
196 + - "4q8s128"
197 + - "4q16s128"
198 + - "4q32s128"
199 + - "4q64s128"
200 + - "4q128"
201 + # seq_len = 256
202 + - "4q1s256"
203 + - "4q2s256"
204 + - "4q4s256"
205 + - "4q8s256"
206 + - "4q16s256"
207 + - "4q32s256"
208 + - "4q64s256"
209 + - "4q128s256"
210 + - "4q256"
211 + # seq_len = 512
212 + - "4q1s512"
213 + - "4q2s512"
214 + - "4q4s512"
215 + - "4q8s512"
216 + - "4q16s512"
217 + - "4q32s512"
218 + - "4q64s512"
219 + - "4q128s512"
220 + - "4q256s512"
221 + - "4q512"
222 + # seq_len = 1024
223 + - "4q1s1024"
224 + - "4q2s1024"
225 + - "4q4s1024"
226 + - "4q8s1024"
227 + - "4q16s1024"
228 + - "4q32s1024"
229 + - "4q64s1024"
230 + - "4q128s1024"
231 + - "4q256s1024"
232 + - "4q512s1024"
233 + - "4q1024"
234 + # seq_len = 2048
235 + - "4q1s2048"
236 + - "4q2s2048"
237 + - "4q4s2048"
238 + - "4q8s2048"
239 + - "4q16s2048"
240 + - "4q32s2048"
241 + - "4q64s2048"
242 + - "4q128s2048"
243 + - "4q256s2048"
244 + - "4q512s2048"
245 + - "4q1024s2048"
246 + - "4q2048"
247 +
248 + # Batch size 8
249 + # seq_len = 32
250 + - "8q1s32"
251 + - "8q2s32"
252 + - "8q4s32"
253 + - "8q8s32"
254 + - "8q16s32"
255 + - "8q32"
256 + # seq_len = 64
257 + - "8q1s64"
258 + - "8q2s64"
259 + - "8q4s64"
260 + - "8q8s64"
261 + - "8q16s64"
262 + - "8q32s64"
263 + - "8q64"
264 + # seq_len = 128
265 + - "8q1s128"
266 + - "8q2s128"
267 + - "8q4s128"
268 + - "8q8s128"
269 + - "8q16s128"
270 + - "8q32s128"
271 + - "8q64s128"
272 + - "8q128"
273 + # seq_len = 256
274 + - "8q1s256"
275 + - "8q2s256"
276 + - "8q4s256"
277 + - "8q8s256"
278 + - "8q16s256"
279 + - "8q32s256"
280 + - "8q64s256"
281 + - "8q128s256"
282 + - "8q256"
283 + # seq_len = 512
284 + - "8q1s512"
285 + - "8q2s512"
286 + - "8q4s512"
287 + - "8q8s512"
288 + - "8q16s512"
289 + - "8q32s512"
290 + - "8q64s512"
291 + - "8q128s512"
292 + - "8q256s512"
293 + - "8q512"
294 + # seq_len = 1024
295 + - "8q1s1024"
296 + - "8q2s1024"
297 + - "8q4s1024"
298 + - "8q8s1024"
299 + - "8q16s1024"
300 + - "8q32s1024"
301 + - "8q64s1024"
302 + - "8q128s1024"
303 + - "8q256s1024"
304 + - "8q512s1024"
305 + - "8q1024"
306 + # seq_len = 2048
307 + - "8q1s2048"
308 + - "8q2s2048"
309 + - "8q4s2048"
310 + - "8q8s2048"
311 + - "8q16s2048"
312 + - "8q32s2048"
313 + - "8q64s2048"
314 + - "8q128s2048"
315 + - "8q256s2048"
316 + - "8q512s2048"
317 + - "8q1024s2048"
318 + - "8q2048"
319 +
320 + # Batch size 16
321 + # seq_len = 32
322 + - "16q1s32"
323 + - "16q2s32"
324 + - "16q4s32"
325 + - "16q8s32"
326 + - "16q16s32"
327 + - "16q32"
328 + # seq_len = 64
329 + - "16q1s64"
330 + - "16q2s64"
331 + - "16q4s64"
332 + - "16q8s64"
333 + - "16q16s64"
334 + - "16q32s64"
335 + - "16q64"
336 + # seq_len = 128
337 + - "16q1s128"
338 + - "16q2s128"
339 + - "16q4s128"
340 + - "16q8s128"
341 + - "16q16s128"
342 + - "16q32s128"
343 + - "16q64s128"
344 + - "16q128"
345 + # seq_len = 256
346 + - "16q1s256"
347 + - "16q2s256"
348 + - "16q4s256"
349 + - "16q8s256"
350 + - "16q16s256"
351 + - "16q32s256"
352 + - "16q64s256"
353 + - "16q128s256"
354 + - "16q256"
355 + # seq_len = 512
356 + - "16q1s512"
357 + - "16q2s512"
358 + - "16q4s512"
359 + - "16q8s512"
360 + - "16q16s512"
361 + - "16q32s512"
362 + - "16q64s512"
363 + - "16q128s512"
364 + - "16q256s512"
365 + - "16q512"
366 + # seq_len = 1024
367 + - "16q1s1024"
368 + - "16q2s1024"
369 + - "16q4s1024"
370 + - "16q8s1024"
371 + - "16q16s1024"
372 + - "16q32s1024"
373 + - "16q64s1024"
374 + - "16q128s1024"
375 + - "16q256s1024"
376 + - "16q512s1024"
377 + - "16q1024"
378 + # seq_len = 2048
379 + - "16q1s2048"
380 + - "16q2s2048"
381 + - "16q4s2048"
382 + - "16q8s2048"
383 + - "16q16s2048"
384 + - "16q32s2048"
385 + - "16q64s2048"
386 + - "16q128s2048"
387 + - "16q256s2048"
388 + - "16q512s2048"
389 + - "16q1024s2048"
390 + - "16q2048"
391 +
392 + # Batch size 32
393 + # seq_len = 32
394 + - "32q1s32"
395 + - "32q2s32"
396 + - "32q4s32"
397 + - "32q8s32"
398 + - "32q16s32"
399 + - "32q32"
400 + # seq_len = 64
401 + - "32q1s64"
402 + - "32q2s64"
403 + - "32q4s64"
404 + - "32q8s64"
405 + - "32q16s64"
406 + - "32q32s64"
407 + - "32q64"
408 + # seq_len = 128
409 + - "32q1s128"
410 + - "32q2s128"
411 + - "32q4s128"
412 + - "32q8s128"
413 + - "32q16s128"
414 + - "32q32s128"
415 + - "32q64s128"
416 + - "32q128"
417 + # seq_len = 256
418 + - "32q1s256"
419 + - "32q2s256"
420 + - "32q4s256"
421 + - "32q8s256"
422 + - "32q16s256"
423 + - "32q32s256"
424 + - "32q64s256"
425 + - "32q128s256"
426 + - "32q256"
427 + # seq_len = 512
428 + - "32q1s512"
429 + - "32q2s512"
430 + - "32q4s512"
431 + - "32q8s512"
432 + - "32q16s512"
433 + - "32q32s512"
434 + - "32q64s512"
435 + - "32q128s512"
436 + - "32q256s512"
437 + - "32q512"
438 + # seq_len = 1024
439 + - "32q1s1024"
440 + - "32q2s1024"
441 + - "32q4s1024"
442 + - "32q8s1024"
443 + - "32q16s1024"
444 + - "32q32s1024"
445 + - "32q64s1024"
446 + - "32q128s1024"
447 + - "32q256s1024"
448 + - "32q512s1024"
449 + - "32q1024"
450 + # seq_len = 2048
451 + - "32q1s2048"
452 + - "32q2s2048"
453 + - "32q4s2048"
454 + - "32q8s2048"
455 + - "32q16s2048"
456 + - "32q32s2048"
457 + - "32q64s2048"
458 + - "32q128s2048"
459 + - "32q256s2048"
460 + - "32q512s2048"
461 + - "32q1024s2048"
462 + - "32q2048"
463 +
464 +backends:
465 + - FLASHMLA_SPARSE
466 +
467 +device: "cuda:0"
468 +profile_memory: false
469 +sparse_mla_dense_mha_max_seq_len: 2048
470 +sparse_mla_topk_pattern: "random"
471 +
472 +output:
473 + csv: "benchmark_output/mla_sparse_mha_vs_mqa.csv"
474 + json: "benchmark_output/mla_sparse_mha_vs_mqa.json"
benchmarks/attention_benchmarks/mla_runner.py
+171 -17
@@ -9,6 +9,8 @@ needing full VllmConfig integration.
9 """
10
11 import statistics
12 +import tempfile
13 +from pathlib import Path
14
15 import numpy as np
16 import torch
@@ -17,7 +19,6 @@ from common import (
19 BenchmarkResult,
20 MockHfConfig,
21 MockIndexer,
20 - MockKVBProj,
22 MockLayer,
23 run_do_bench,
24 run_ncu_profile,
@@ -33,8 +34,59 @@ from vllm.config import (
34 VllmConfig,
35 set_current_vllm_config,
36 )
37 +from vllm.model_executor.layers.linear import ColumnParallelLinear
38 from vllm.v1.attention.backends.mla.prefill.registry import MLAPrefillBackendEnum
39
40 +
41 +def _safe_profile_name(value: str) -> str:
42 + return "".join(c if c.isalnum() or c in "._-" else "_" for c in value)
43 +
44 +
45 +def _create_kv_b_proj(
46 + mla_dims: dict,
47 + device: torch.device,
48 +):
49 + kv_b_proj = ColumnParallelLinear(
50 + mla_dims["kv_lora_rank"],
51 + mla_dims["num_q_heads"]
52 + * (mla_dims["qk_nope_head_dim"] + mla_dims["v_head_dim"]),
53 + bias=False,
54 + params_dtype=torch.bfloat16,
55 + quant_config=None,
56 + prefix="benchmark.kv_b_proj",
57 + ).to(device)
58 + with torch.no_grad():
59 + kv_b_proj.weight.copy_(torch.randn_like(kv_b_proj.weight))
60 + return kv_b_proj
61 +
62 +
63 +def _ensure_single_rank_model_parallel() -> None:
64 + import torch.distributed as dist
65 +
66 + from vllm.distributed import (
67 + ensure_model_parallel_initialized,
68 + init_distributed_environment,
69 + model_parallel_is_initialized,
70 + )
71 +
72 + if not dist.is_available():
73 + return
74 + if not dist.is_initialized():
75 + with tempfile.NamedTemporaryFile(
76 + prefix="vllm_bench_dist_", delete=False
77 + ) as init_file:
78 + distributed_init_method = f"file://{init_file.name}"
79 + init_distributed_environment(
80 + world_size=1,
81 + rank=0,
82 + distributed_init_method=distributed_init_method,
83 + local_rank=0,
84 + backend="nccl",
85 + )
86 + if not model_parallel_is_initialized():
87 + ensure_model_parallel_initialized(1, 1)
88 +
89 +
90 # ============================================================================
91 # VllmConfig Creation
92 # ============================================================================
@@ -66,10 +118,12 @@ def create_minimal_vllm_config(
118 block_size: int = 128,
119 max_num_seqs: int = 256,
120 max_num_batched_tokens: int = 8192,
121 + max_model_len: int = 32768,
122 mla_dims: dict | None = None,
123 index_topk: int | None = None,
124 prefill_backend: str | None = None,
125 kv_cache_dtype: str = "auto",
126 + sparse_mla_force_mqa: bool = False,
127 ) -> VllmConfig:
128 """
129 Create minimal VllmConfig for MLA benchmarks.
@@ -86,6 +140,8 @@ def create_minimal_vllm_config(
140 prefill_backend: Prefill backend name (e.g., "fa3", "fa4", "flashinfer",
141 "trtllm"). Configures the attention config to force
142 the specified prefill backend.
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
@@ -131,7 +187,7 @@ def create_minimal_vllm_config(
187 trust_remote_code=True,
188 dtype="bfloat16",
189 seed=0,
134 - max_model_len=32768,
190 + max_model_len=max_model_len,
191 quantization=None,
192 enforce_eager=False,
193 max_logprobs=20,
@@ -163,7 +219,7 @@ def create_minimal_vllm_config(
219 scheduler_config = SchedulerConfig(
220 max_num_seqs=max_num_seqs,
221 max_num_batched_tokens=max(max_num_batched_tokens, max_num_seqs),
166 - max_model_len=32768,
222 + max_model_len=max_model_len,
223 is_encoder_decoder=False,
224 enable_chunked_prefill=True,
225 )
@@ -192,6 +248,9 @@ def create_minimal_vllm_config(
248 "flash_attn_version"
249 ]
250
251 + if sparse_mla_force_mqa:
252 + vllm_config.attention_config.sparse_mla_force_mqa = True
253 +
254 return vllm_config
255
256
@@ -548,12 +607,7 @@ def _create_backend_impl(
607 # Calculate scale
608 scale = 1.0 / np.sqrt(mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"])
609
551 - # Create mock kv_b_proj layer for prefill mode
552 - mock_kv_b_proj = MockKVBProj(
553 - num_heads=mla_dims["num_q_heads"],
554 - qk_nope_head_dim=mla_dims["qk_nope_head_dim"],
555 - v_head_dim=mla_dims["v_head_dim"],
556 - )
610 + kv_b_proj = _create_kv_b_proj(mla_dims, device)
611
612 # Create indexer for sparse backends
613 indexer = None
@@ -584,7 +638,7 @@ def _create_backend_impl(
638 "qk_rope_head_dim": mla_dims["qk_rope_head_dim"],
639 "qk_head_dim": mla_dims["qk_nope_head_dim"] + mla_dims["qk_rope_head_dim"],
640 "v_head_dim": mla_dims["v_head_dim"],
587 - "kv_b_proj": mock_kv_b_proj,
641 + "kv_b_proj": kv_b_proj,
642 }
643
644 # Add indexer for sparse backends
@@ -785,14 +839,35 @@ def _run_single_benchmark(
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:
788 - indexer.fill_random_indices(total_q, max_kv_len)
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.
791 - # Sparse MLA backends always use forward_mqa
792 - has_decode = is_sparse or getattr(metadata, "decode", None) is not None
793 - has_prefill = not is_sparse and getattr(metadata, "prefill", None) is not None
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
@@ -871,7 +946,6 @@ def _run_single_benchmark(
946 metadata,
947 prefill_inputs["k_scale"],
948 prefill_fp8_output if fused_output else prefill_inputs["output"],
874 - prefill_output_scale if fused_output else None,
949 )
950 if fused_output:
951 out = prefill_fp8_output
@@ -898,6 +972,48 @@ def _run_single_benchmark(
972 throughput_tokens_per_sec=0.0,
973 )
974
975 + if config.torch_profile:
976 + profile_dir = Path(
977 + config.torch_profile_dir or "benchmark_outputs/torch_profiles"
978 + )
979 + profile_dir.mkdir(parents=True, exist_ok=True)
980 + trace_name = _safe_profile_name(f"{config.backend}_{config.batch_spec}")
981 + trace_path = profile_dir / f"{trace_name}.json"
982 + iters = max(config.torch_profile_iters, 1)
983 +
984 + forward_fn()
985 + torch.accelerator.synchronize()
986 + with torch.profiler.profile(
987 + activities=[
988 + torch.profiler.ProfilerActivity.CPU,
989 + torch.profiler.ProfilerActivity.CUDA,
990 + ],
991 + record_shapes=True,
992 + profile_memory=True,
993 + with_stack=False,
994 + ) as prof:
995 + for _ in range(iters):
996 + forward_fn()
997 + torch.accelerator.synchronize()
998 + prof.step()
999 + prof.export_chrome_trace(str(trace_path))
1000 + print(f"Saved PyTorch profiler trace to {trace_path}")
1001 + print(
1002 + prof.key_averages().table(
1003 + sort_by="cuda_time_total",
1004 + row_limit=25,
1005 + )
1006 + )
1007 + return BenchmarkResult(
1008 + config=config,
1009 + mean_time=0.0,
1010 + median_time=0.0,
1011 + std_time=0.0,
1012 + min_time=0.0,
1013 + max_time=0.0,
1014 + throughput_tokens_per_sec=0.0,
1015 + )
1016 +
1017 all_ms = run_do_bench(benchmark_fn, config.use_cuda_graphs, config.warmup_ms)
1018
1019 # Convert ms to seconds per layer
@@ -920,6 +1036,7 @@ def _run_mla_benchmark_batched(
1036 configs_with_params: list[tuple], # [(config, threshold, num_splits), ...]
1037 index_topk: int = 2048,
1038 prefill_backend: str | None = None,
1039 + sparse_mla_force_mqa: bool = False,
1040 output_scale: float | None = None,
1041 fuse_quant_op: bool = False,
1042 ) -> list[BenchmarkResult]:
@@ -940,6 +1057,8 @@ def _run_mla_benchmark_batched(
1057 index_topk: Topk value for sparse MLA backends (default 2048)
1058 prefill_backend: Prefill backend name (e.g., "fa3", "fa4").
1059 When set, forces the specified FlashAttention version for prefill.
1060 + sparse_mla_force_mqa: If True, forces all sparse MLA tokens through
1061 + forward_mqa (even prefill tokens).
1062
1063 Returns:
1064 List of BenchmarkResult objects
@@ -980,21 +1099,41 @@ def _run_mla_benchmark_batched(
1099 sum(r.q_len for r in parse_batch_spec(cfg.batch_spec))
1100 for cfg, *_ in configs_with_params
1101 )
1102 + max_model_len = max(
1103 + max_total_q,
1104 + max(
1105 + getattr(cfg, "max_model_len", None) or 32768
1106 + for cfg, *_ in configs_with_params
1107 + ),
1108 + )
1109
1110 # Create and set vLLM config for MLA (reused across all benchmarks)
1111 vllm_config = create_minimal_vllm_config(
1112 model_name="deepseek-v3", # Used only for model path
1113 block_size=block_size,
1114 max_num_batched_tokens=max_total_q,
1115 + max_model_len=max_model_len,
1116 mla_dims=mla_dims, # Use custom dims from config or default
1117 index_topk=index_topk if is_sparse else None,
1118 prefill_backend=prefill_backend,
1119 kv_cache_dtype=kv_cache_dtype,
1120 + sparse_mla_force_mqa=sparse_mla_force_mqa,
1121 )
1122
1123 results = []
1124
1125 + # Initialize workspace manager (needed by metadata builders)
1126 + from vllm.v1.worker.workspace import (
1127 + init_workspace_manager,
1128 + is_workspace_manager_initialized,
1129 + )
1130 +
1131 + if not is_workspace_manager_initialized():
1132 + init_workspace_manager(device)
1133 +
1134 with set_current_vllm_config(vllm_config):
1135 + _ensure_single_rank_model_parallel()
1136 +
1137 # Create backend impl, layer, builder, and indexer (reused across benchmarks)
1138 impl, layer, builder_instance, indexer = _create_backend_impl(
1139 backend_cfg,
@@ -1040,9 +1179,20 @@ def _run_mla_benchmark_batched(
1179 for config, threshold, num_splits in configs_with_params:
1180 # Set threshold for this benchmark (FlashAttn/FlashMLA only)
1181 original_threshold = None
1043 - if threshold is not None and builder_instance:
1182 + effective_threshold = threshold
1183 + force_dense_mha = (
1184 + is_sparse
1185 + and getattr(config, "sparse_mla_mha_mode", "auto") == "dense"
1186 + and not getattr(config, "sparse_mla_force_mqa", False)
1187 + )
1188 + if force_dense_mha:
1189 + # Sparse MLA normally treats q_len <= 1 as decode. Use an
1190 + # impossible threshold so dense_mha benchmarks actually run
1191 + # the prefill/MHA path, including q_len=1 short extends.
1192 + effective_threshold = -1
1193 + if effective_threshold is not None and builder_instance:
1194 original_threshold = builder_instance.reorder_batch_threshold
1045 - builder_instance.reorder_batch_threshold = threshold
1195 + builder_instance.reorder_batch_threshold = effective_threshold
1196
1197 # Set num_splits for CUTLASS
1198 original_num_splits = None
@@ -1090,6 +1240,7 @@ def run_mla_benchmark(
1240 num_kv_splits: int | None = None,
1241 index_topk: int = 2048,
1242 prefill_backend: str | None = None,
1243 + sparse_mla_force_mqa: bool = False,
1244 output_scale: float | None = None,
1245 fuse_quant_op: bool = False,
1246 ) -> BenchmarkResult | list[BenchmarkResult]:
@@ -1111,6 +1262,8 @@ def run_mla_benchmark(
1262 index_topk: Topk value for sparse MLA backends (default 2048)
1263 prefill_backend: Prefill backend name (e.g., "fa3", "fa4").
1264 When set, forces the specified FlashAttention version for prefill.
1265 + sparse_mla_force_mqa: If True, forces all sparse MLA tokens through
1266 + forward_mqa (even prefill tokens).
1267 output_scale: Static per-tensor FP8 scale for prefill output (None = bf16).
1268 fuse_quant_op: With output_scale set, fuse the FP8 write into the prefill
1269 kernel vs a standalone post-quant kernel. See _run_single_benchmark.
@@ -1142,6 +1295,7 @@ def run_mla_benchmark(
1295 configs_with_params,
1296 index_topk,
1297 prefill_backend=prefill_backend,
1298 + sparse_mla_force_mqa=sparse_mla_force_mqa,
1299 output_scale=output_scale,
1300 fuse_quant_op=fuse_quant_op,
1301 )
docs/design/attention_backends.md
+1 -1
@@ -205,7 +205,7 @@ hardware and configuration.
205
206 | Backend | Description | Dtypes | Compute Cap. | Notes |
207 | ------- | ----------- | ------ | ------------ | ----- |
208 -| `FLASH_ATTN`‡ | FlashAttention varlen (FA2/FA3/FA4) | fp16, bf16 | Any | FA4 on SM100+, FA3 on SM90, FA2 otherwise |
208 +| `FLASH_ATTN`‡ | FlashAttention varlen (FA2/FA3/FA4) | fp16, bf16 | Any | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) only |
209 | `TRTLLM_RAGGED` | TensorRT-LLM ragged attention | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) or (qk_nope_head_dim=192, qk_rope_head_dim=64, v_head_dim=256) only |
210 | `FLASHINFER` | FlashInfer CUTLASS backend | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) only |
211 | `TOKENSPEED_MLA` | | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) only |
tests/v1/attention/test_dspark_noncausal_sparse_mla.py
+17
@@ -304,6 +304,23 @@ def _run_sparse_backend_vs_sdpa(
304 scale=kv_cache_scale,
305 )
306
307 + # The sparse builder clones the layer's dense-MHA prefill backend from
308 + # static_forward_context; register a mock layer carrying one.
309 + from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
310 +
311 + prefill_backend = get_mla_prefill_backend(vllm_config)(
312 + num_heads=num_heads,
313 + scale=scale,
314 + kv_lora_rank=kv_lora_rank,
315 + qk_nope_head_dim=qk_nope_head_dim,
316 + qk_rope_head_dim=qk_rope_head_dim,
317 + v_head_dim=v_head_dim,
318 + vllm_config=vllm_config,
319 + )
320 + vllm_config.compilation_config.static_forward_context["placeholder"] = (
321 + SimpleNamespace(prefill_backend=prefill_backend)
322 + )
323 +
324 builder = backend_cls.get_builder_cls()(
325 kv_cache_spec, ["placeholder"], vllm_config, device
326 )
tests/v1/attention/test_mla_backends.py
+9 -4
@@ -1031,6 +1031,11 @@ def run_attention_backend(
1031 @pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8", "fp8_e4m3"])
1032 @pytest.mark.parametrize(("q_scale", "k_scale"), [(1.0, 1.0), (2.0, 3.0)])
1033 @pytest.mark.parametrize("prefill_backend", PREFILL_BACKENDS_TO_TEST)
1034 +@pytest.mark.parametrize(
1035 + ("qk_nope_head_dim", "v_head_dim"),
1036 + [(128, 128), (192, 256)],
1037 + ids=["deepseek", "glm"],
1038 +)
1039 def test_backend_correctness(
1040 default_vllm_config,
1041 dist_init,
@@ -1042,6 +1047,8 @@ def test_backend_correctness(
1047 q_scale: float,
1048 k_scale: float,
1049 prefill_backend: MLAPrefillBackendEnum,
1050 + qk_nope_head_dim: int,
1051 + v_head_dim: int,
1052 ):
1053 """
1054 Test that all backends produce similar outputs to a reference implementation
@@ -1090,9 +1097,9 @@ def test_backend_correctness(
1097 MLAPrefillSelectorConfig(
1098 dtype=torch.bfloat16,
1099 mla_dimensions=MLADimensions(
1093 - qk_nope_head_dim=128,
1100 + qk_nope_head_dim=qk_nope_head_dim,
1101 qk_rope_head_dim=64,
1095 - v_head_dim=128,
1102 + v_head_dim=v_head_dim,
1103 ),
1104 ),
1105 )
@@ -1168,8 +1175,6 @@ def test_backend_correctness(
1175 dtype = _convert_dtype_to_torch(vllm_config.model_config.dtype)
1176 kv_lora_rank = 512
1177 qk_rope_head_dim = 64
1171 - qk_nope_head_dim = 128
1172 - v_head_dim = 128
1178 total_head_size = kv_lora_rank + qk_rope_head_dim
1179 assert kv_lora_rank + qk_rope_head_dim == head_size, (
1180 f"MLA dimensions don't match: {total_head_size} != {head_size}"
tests/v1/attention/test_sparse_mla_backends.py
+269
@@ -442,6 +442,23 @@ def test_sparse_backend_decode_correctness(
442 scale=kv_cache_scale,
443 )
444
445 + # The sparse builder clones the layer's dense-MHA prefill backend from
446 + # static_forward_context; register a mock layer carrying one.
447 + from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
448 +
449 + prefill_backend = get_mla_prefill_backend(vllm_config)(
450 + num_heads=num_heads,
451 + scale=scale,
452 + kv_lora_rank=kv_lora_rank,
453 + qk_nope_head_dim=qk_nope_head_dim,
454 + qk_rope_head_dim=qk_rope_head_dim,
455 + v_head_dim=v_head_dim,
456 + vllm_config=vllm_config,
457 + )
458 + vllm_config.compilation_config.static_forward_context["placeholder"] = (
459 + SimpleNamespace(prefill_backend=prefill_backend)
460 + )
461 +
462 builder_cls = backend_cls.get_builder_cls()
463 builder = builder_cls(kv_cache_spec, ["placeholder"], vllm_config, device)
464 metadata = builder.build(
@@ -722,6 +739,258 @@ def test_split_prefill_chunks(seq_lens, max_buf, expected):
739 assert out == expected
740
741
742 +PREFILL_BATCH_SPECS = {
743 + "short_dense_mha": BatchSpec(seq_lens=[64, 128], query_lens=[64, 128]),
744 + "short_context_dense_mha": BatchSpec(seq_lens=[128, 160], query_lens=[64, 32]),
745 +}
746 +
747 +
748 +@pytest.mark.skipif(
749 + torch.cuda.get_device_capability()[0] < 10,
750 + reason="Sparse MLA forward_mha requires FA4 (SM100+)",
751 +)
752 +@pytest.mark.parametrize("batch_name", list(PREFILL_BATCH_SPECS.keys()))
753 +@pytest.mark.parametrize("kv_cache_dtype", ["auto"])
754 +def test_sparse_backend_prefill_correctness(
755 + default_vllm_config,
756 + dist_init,
757 + batch_name,
758 + kv_cache_dtype,
759 + workspace_init,
760 +):
761 + """Test single-pass FA4 dense forward_mha for sparse MLA prefill."""
762 + backend_cls = FlashMLASparseBackend
763 + batch_spec = PREFILL_BATCH_SPECS[batch_name]
764 +
765 + device = torch.device("cuda")
766 + dtype = torch.bfloat16
767 + block_size = 64
768 +
769 + num_heads = 128
770 + kv_lora_rank = 512
771 + qk_nope_head_dim = 128
772 + qk_rope_head_dim = 64
773 + v_head_dim = 128
774 + head_size = kv_lora_rank + qk_rope_head_dim
775 + topk_tokens = 512
776 +
777 + max_seqlen = max(batch_spec.seq_lens)
778 + total_cache_tokens = sum(batch_spec.seq_lens)
779 +
780 + vllm_config = create_vllm_config(
781 + model_name="deepseek-ai/DeepSeek-V2-Lite-Chat",
782 + tensor_parallel_size=1,
783 + max_model_len=max_seqlen,
784 + num_gpu_blocks=max(2048, cdiv(total_cache_tokens, block_size) + 1),
785 + block_size=block_size,
786 + hf_config_override={
787 + "index_topk": topk_tokens,
788 + "attn_module_list_cfg": [{"topk_tokens": topk_tokens}],
789 + },
790 + )
791 + model_config = vllm_config.model_config
792 + model_config.hf_text_config = SimpleNamespace(
793 + q_lora_rank=None,
794 + kv_lora_rank=kv_lora_rank,
795 + qk_nope_head_dim=qk_nope_head_dim,
796 + qk_rope_head_dim=qk_rope_head_dim,
797 + v_head_dim=v_head_dim,
798 + model_type="deepseek_v2",
799 + )
800 + model_config.dtype = dtype
801 + model_config.get_num_attention_heads = MethodType(
802 + lambda self, parallel_config: num_heads, model_config
803 + )
804 + model_config.get_num_kv_heads = MethodType(
805 + lambda self, parallel_config: 1, model_config
806 + )
807 + model_config.get_head_size = MethodType(lambda self: head_size, model_config)
808 + model_config.get_sliding_window = MethodType(lambda self: None, model_config)
809 +
810 + kv_cache_spec = create_standard_kv_cache_spec(vllm_config)
811 + scale = 1.0 / math.sqrt(qk_nope_head_dim + qk_rope_head_dim)
812 +
813 + torch.manual_seed(42)
814 +
815 + W_UK = torch.rand(
816 + kv_lora_rank, num_heads, qk_nope_head_dim, dtype=dtype, device=device
817 + )
818 + W_UV = torch.rand(kv_lora_rank, num_heads, v_head_dim, dtype=dtype, device=device)
819 +
820 + seq_lens = batch_spec.seq_lens
821 + query_lens = batch_spec.query_lens
822 +
823 + # Compute dense reference outputs.
824 + total_query_tokens = sum(query_lens)
825 + sparse_indices = torch.zeros(
826 + total_query_tokens, topk_tokens, dtype=torch.int32, device=device
827 + )
828 +
829 + all_q, all_kv_c_new, all_k_pe_new = [], [], []
830 + kv_c_contexts, k_pe_contexts = [], []
831 + reference_outputs = []
832 +
833 + for i in range(batch_spec.batch_size):
834 + s_len = seq_lens[i]
835 + q_len = query_lens[i]
836 + ctx_len = s_len - q_len
837 +
838 + q_mha = torch.rand(
839 + q_len,
840 + num_heads,
841 + qk_nope_head_dim + qk_rope_head_dim,
842 + dtype=dtype,
843 + device=device,
844 + )
845 + kv_c_full = torch.rand(s_len, kv_lora_rank, dtype=dtype, device=device)
846 + k_pe_full = torch.rand(s_len, 1, qk_rope_head_dim, dtype=dtype, device=device)
847 +
848 + # Decompress all KV for reference
849 + kv_b_weight = torch.cat([W_UK, W_UV], dim=-1).view(
850 + kv_lora_rank, num_heads * (qk_nope_head_dim + v_head_dim)
851 + )
852 + kv_decompressed = (kv_c_full @ kv_b_weight).view(
853 + s_len, num_heads, qk_nope_head_dim + v_head_dim
854 + )
855 + k_nope_all, v_all = kv_decompressed.split(
856 + [qk_nope_head_dim, v_head_dim], dim=-1
857 + )
858 + k_pe_expanded = k_pe_full.expand(-1, num_heads, -1)
859 + k_all = torch.cat([k_nope_all, k_pe_expanded], dim=-1)
860 +
861 + for j in range(q_len):
862 + attend_end = ctx_len + j + 1
863 + q_tok = q_mha[j : j + 1] # (1, H, D_qk)
864 + k_attend = k_all[:attend_end] # (N, H, D_qk)
865 + v_attend = v_all[:attend_end] # (N, H, D_v)
866 +
867 + q_sdpa = q_tok.unsqueeze(0).transpose(1, 2).float()
868 + k_sdpa = k_attend.unsqueeze(0).transpose(1, 2).float()
869 + v_sdpa = v_attend.unsqueeze(0).transpose(1, 2).float()
870 +
871 + out = torch.nn.functional.scaled_dot_product_attention(
872 + q_sdpa, k_sdpa, v_sdpa, scale=scale
873 + )
874 + out = out.transpose(1, 2).squeeze(0) # (1, H, D_v)
875 + reference_outputs.append(out.to(dtype).flatten(start_dim=-2))
876 +
877 + all_q.append(q_mha)
878 + all_kv_c_new.append(kv_c_full[ctx_len:])
879 + all_k_pe_new.append(k_pe_full[ctx_len:])
880 + kv_c_contexts.append(kv_c_full)
881 + k_pe_contexts.append(k_pe_full)
882 +
883 + query_cat = torch.cat(all_q, dim=0)
884 + kv_c_cat = torch.cat(all_kv_c_new, dim=0)
885 + k_pe_cat = torch.cat(all_k_pe_new, dim=0)
886 + ref_output = torch.cat(reference_outputs, dim=0)
887 +
888 + vllm_config.cache_config.cache_dtype = kv_cache_dtype
889 + vllm_config.model_config.hf_config.index_topk = topk_tokens
890 +
891 + common_attn_metadata = create_common_attn_metadata(
892 + batch_spec,
893 + vllm_config.cache_config.block_size,
894 + device,
895 + arange_block_indices=True,
896 + )
897 +
898 + kv_cache = create_and_prepopulate_kv_cache(
899 + kv_c_contexts=kv_c_contexts,
900 + k_pe_contexts=k_pe_contexts,
901 + block_size=block_size,
902 + head_size=head_size,
903 + dtype=dtype,
904 + device=device,
905 + num_blocks=vllm_config.cache_config.num_gpu_blocks,
906 + common_attn_metadata=common_attn_metadata,
907 + randomize_blocks=False,
908 + kv_cache_dtype=kv_cache_dtype,
909 + )
910 +
911 + # The sparse builder clones the layer's dense-MHA prefill backend from
912 + # static_forward_context; register a mock layer carrying one.
913 + from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
914 +
915 + prefill_backend = get_mla_prefill_backend(vllm_config)(
916 + num_heads=num_heads,
917 + scale=scale,
918 + kv_lora_rank=kv_lora_rank,
919 + qk_nope_head_dim=qk_nope_head_dim,
920 + qk_rope_head_dim=qk_rope_head_dim,
921 + v_head_dim=v_head_dim,
922 + vllm_config=vllm_config,
923 + )
924 + vllm_config.compilation_config.static_forward_context["placeholder"] = (
925 + SimpleNamespace(prefill_backend=prefill_backend)
926 + )
927 +
928 + builder_cls = backend_cls.get_builder_cls()
929 + builder = builder_cls(kv_cache_spec, ["placeholder"], vllm_config, device)
930 + # Drive the queries through the dense-MHA prefill path directly (the routing
931 + # threshold would otherwise classify these short queries as MQA decodes).
932 + builder.reorder_batch_threshold = 1
933 + metadata = builder.build(
934 + common_prefix_len=0, common_attn_metadata=common_attn_metadata
935 + )
936 +
937 + mock_indexer = SimpleNamespace(topk_indices_buffer=sparse_indices)
938 +
939 + kv_b_proj_weight = torch.cat([W_UK, W_UV], dim=-1).view(
940 + kv_lora_rank, num_heads * (qk_nope_head_dim + v_head_dim)
941 + )
942 +
943 + mock_kv_b_proj = ColumnParallelLinear(
944 + input_size=kv_lora_rank,
945 + output_size=num_heads * (qk_nope_head_dim + v_head_dim),
946 + bias=False,
947 + ).to(device=device, dtype=dtype)
948 + mock_kv_b_proj.weight = torch.nn.Parameter(kv_b_proj_weight.T.contiguous())
949 +
950 + impl_cls = backend_cls.get_impl_cls()
951 + with set_current_vllm_config(vllm_config):
952 + impl = impl_cls(
953 + num_heads=num_heads,
954 + head_size=head_size,
955 + scale=scale,
956 + num_kv_heads=1,
957 + alibi_slopes=None,
958 + sliding_window=None,
959 + kv_cache_dtype=kv_cache_dtype,
960 + logits_soft_cap=None,
961 + attn_type="decoder",
962 + kv_sharing_target_layer_name=None,
963 + q_lora_rank=None,
964 + kv_lora_rank=kv_lora_rank,
965 + qk_nope_head_dim=qk_nope_head_dim,
966 + qk_rope_head_dim=qk_rope_head_dim,
967 + qk_head_dim=qk_nope_head_dim + qk_rope_head_dim,
968 + v_head_dim=v_head_dim,
969 + kv_b_proj=mock_kv_b_proj,
970 + indexer=mock_indexer,
971 + )
972 + impl.process_weights_after_loading(dtype)
973 +
974 + out_buffer = torch.empty(
975 + total_query_tokens, num_heads * v_head_dim, dtype=dtype, device=device
976 + )
977 +
978 + with torch.inference_mode():
979 + impl.forward_mha(
980 + q=query_cat,
981 + kv_c_normed=kv_c_cat,
982 + k_pe=k_pe_cat,
983 + kv_c_and_k_pe_cache=kv_cache,
984 + attn_metadata=metadata,
985 + k_scale=torch.tensor(1.0, device=device),
986 + output=out_buffer,
987 + )
988 +
989 + assert out_buffer.shape == ref_output.shape
990 + assert torch.isfinite(out_buffer).all(), "Non-finite values in output"
991 + torch.testing.assert_close(out_buffer, ref_output, rtol=0.01, atol=0.01)
992 +
993 +
994 @pytest.mark.parametrize(
995 "seq_lens,query_lens,workspace_size,max_logits_bytes,expected",
996 [
vllm/config/attention.py
+5
@@ -59,6 +59,11 @@ class AttentionConfig:
59 use_non_causal: bool = False
60 """Whether to use non-causal (bidirectional) attention."""
61
62 + sparse_mla_force_mqa: bool = False
63 + """Force sparse MLA to use forward_mqa for all requests, including prefill.
64 + When False (default), pure prefill batches use forward_mha when implemented.
65 + Set to True to always use the MQA path."""
66 +
67 flex_attn_block_m: int | None = None
68 """Triton kernel BLOCK_M tile size for flex attention.
69 Must be a power of 2 >= 16. If None and VLLM_BATCH_INVARIANT=1,
vllm/model_executor/layers/attention/mla_attention.py
+315 -255
@@ -259,7 +259,6 @@ from vllm.v1.attention.backend import (
259 AttentionType,
260 CommonAttentionMetadata,
261 MLAAttentionImpl,
262 - SparseMLAAttentionImpl,
262 )
263 from vllm.v1.attention.backends.mla.prefill import (
264 MLAPrefillBackend,
@@ -490,16 +489,30 @@ class MLAAttention(nn.Module, AttentionLayerBase):
489 raise ValueError(f"Duplicate layer name: {prefix}")
490 compilation_config.static_forward_context[prefix] = self
491
493 - prefill_backend_cls = get_mla_prefill_backend(vllm_config)
494 - self.prefill_backend = prefill_backend_cls(
495 - num_heads=self.num_heads,
496 - scale=self.scale,
497 - kv_lora_rank=self.kv_lora_rank,
498 - qk_nope_head_dim=self.qk_nope_head_dim,
499 - qk_rope_head_dim=self.qk_rope_head_dim,
500 - v_head_dim=self.v_head_dim,
501 - vllm_config=vllm_config,
502 - )
492 + self.prefill_backend: MLAPrefillBackend | None
493 + try:
494 + prefill_backend_cls = get_mla_prefill_backend(vllm_config)
495 + except ValueError:
496 + if (
497 + not self.impl.is_sparse
498 + or vllm_config.attention_config.mla_prefill_backend is not None
499 + ):
500 + raise
501 + logger.warning_once(
502 + "No MLA prefill backend supports this model; sparse MLA will use the "
503 + "top-k MQA path only (no dense-MHA prefill)."
504 + )
505 + self.prefill_backend = None
506 + else:
507 + self.prefill_backend = prefill_backend_cls(
508 + num_heads=self.num_heads,
509 + scale=self.scale,
510 + kv_lora_rank=self.kv_lora_rank,
511 + qk_nope_head_dim=self.qk_nope_head_dim,
512 + qk_rope_head_dim=self.qk_rope_head_dim,
513 + v_head_dim=self.v_head_dim,
514 + vllm_config=vllm_config,
515 + )
516
517 self.kv_cache = torch.tensor([])
518
@@ -693,23 +706,28 @@ class MLAAttention(nn.Module, AttentionLayerBase):
706 if fp8_attention and self.kv_cache_dtype != "fp8_ds_mla":
707 kv_cache = kv_cache.view(current_platform.fp8_dtype())
708
696 - # Sparse MLA impls only support forward_mqa (decode-style attention)
697 - is_sparse_impl = isinstance(self.impl, SparseMLAAttentionImpl)
698 -
699 - if is_sparse_impl:
700 - num_mqa_tokens = q.size(0)
701 - num_mha_tokens = 0
702 - else:
703 - assert (
704 - attn_metadata.num_decodes is not None
705 - and attn_metadata.num_prefills is not None
706 - and attn_metadata.num_decode_tokens is not None
709 + assert (
710 + attn_metadata.num_decodes is not None
711 + and attn_metadata.num_prefills is not None
712 + and attn_metadata.num_decode_tokens is not None
713 + )
714 + num_mqa_tokens = attn_metadata.num_decode_tokens
715 + num_mha_tokens = q.size(0) - num_mqa_tokens
716 +
717 + if self.impl.is_sparse and num_mha_tokens > 0:
718 + prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
719 + use_mha = (
720 + self.prefill_backend is not None
721 + and prefill_max_seq_len <= attn_metadata.topk_tokens # type: ignore[attr-defined]
722 + and not self._vllm_config.attention_config.sparse_mla_force_mqa
723 )
708 - num_mqa_tokens = attn_metadata.num_decode_tokens
709 - num_mha_tokens = q.size(0) - num_mqa_tokens
724 + if not use_mha:
725 + num_mqa_tokens = q.size(0)
726 + num_mha_tokens = 0
727
728 mha_use_quant_output = (
729 quant_key is not None
730 + and self.prefill_backend is not None
731 and self.prefill_backend.supports_quant_output(quant_key)
732 and attn_metadata is not None
733 and attn_metadata.prefill is not None
@@ -807,7 +825,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
825 mqa_q = get_dcp_group().all_gather(mqa_q, dim=1)
826
827 # call decode attn
810 - if not is_sparse_impl:
828 + if not self.impl.is_sparse:
829 assert attn_metadata.decode is not None
830 attn_out, lse = self.impl.forward_mqa(mqa_q, kv_cache, attn_metadata, self) # type: ignore[attr-defined]
831
@@ -1398,6 +1416,182 @@ def backend_supports_prefill_query_quantization() -> bool:
1416 )
1417
1418
1419 +def build_mla_chunked_context_metadata(
1420 + *,
1421 + context_lens_cpu: torch.Tensor,
1422 + prefill_query_start_loc_cpu: torch.Tensor,
1423 + num_prefills: int,
1424 + chunked_prefill_workspace: torch.Tensor,
1425 + chunked_prefill_workspace_size: int,
1426 + block_size: int,
1427 + align_chunk_to_block: bool,
1428 + device: torch.device,
1429 + dcp_world_size: int,
1430 + dcp_local_block_size: int,
1431 + dcp_virtual_block_size: int,
1432 +) -> "MLACommonPrefillMetadata.ChunkedContextMetadata | None":
1433 + """Build chunked-context metadata for an MLA prefill.
1434 +
1435 + Shared by dense and sparse builders. Splits each prefill's context
1436 + into workspace-sized chunks and, under DCP, plans the per-rank interleaved
1437 + local chunks the all-gather reduction consumes.
1438 +
1439 + Args:
1440 + context_lens_cpu: Per-prefill context length (seq_len - query_len).
1441 + prefill_query_start_loc_cpu: Prefill query cumulative offsets (0-based).
1442 + num_prefills: Number of prefill requests.
1443 + chunked_prefill_workspace: Scratch buffer the context gather writes to.
1444 + chunked_prefill_workspace_size: Row capacity of the workspace.
1445 + block_size: KV cache page size for chunk-start alignment.
1446 + align_chunk_to_block: Round the chunk size down to ``block_size``.
1447 + device: Target device for the returned tensors.
1448 + dcp_world_size: Decode-context-parallel world size (1 if disabled).
1449 + dcp_local_block_size: Per-rank interleave block size for DCP.
1450 + dcp_virtual_block_size: ``dcp_local_block_size * dcp_world_size``.
1451 +
1452 + Returns:
1453 + The chunked-context metadata, or None when no prefill has any context.
1454 + """
1455 + # NOTE: it is recommended you read the `Chunked Prefill` section in the
1456 + # comment at the top of the file before trying to understand this code.
1457 + max_context_len = context_lens_cpu.max().item()
1458 + if max_context_len <= 0:
1459 + return None
1460 + num_prefills_with_context = int((context_lens_cpu > 0).sum().item())
1461 +
1462 + # Currently we allocate an equal amount of workspace for each prefill with
1463 + # context; we could probably use a more advanced algorithm here and allocate
1464 + # more workspace to prefills with longer context lengths.
1465 + max_context_chunk = chunked_prefill_workspace_size // num_prefills_with_context
1466 + if align_chunk_to_block:
1467 + # The `gather_and_maybe_dequant_cache` kernel cannot handle chunk
1468 + # starts that are not aligned to block_size, so round down.
1469 + max_context_chunk = round_down(max_context_chunk, block_size)
1470 + assert max_context_chunk > 0
1471 +
1472 + num_chunks = cdiv(max_context_len, max_context_chunk)
1473 + # e.g. max_context_chunk=256, num_chunks=3, num_prefills=4 ->
1474 + # [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512]]
1475 + # Note(simon): this is done on CPU because of downstream's use of `to_list`.
1476 + chunk_starts = torch.empty(
1477 + num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
1478 + ).copy_(
1479 + torch.arange(num_chunks, dtype=torch.int32)
1480 + .multiply_(max_context_chunk)
1481 + .unsqueeze(1)
1482 + )
1483 + chunk_ends = torch.min(
1484 + context_lens_cpu.unsqueeze(0), chunk_starts + max_context_chunk
1485 + )
1486 + chunk_seq_lens = (chunk_ends - chunk_starts).clamp(min=0)
1487 +
1488 + cu_seq_lens_cpu = torch.zeros(
1489 + num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
1490 + )
1491 + torch.cumsum(chunk_seq_lens, dim=1, out=cu_seq_lens_cpu[:, 1:], dtype=torch.int32)
1492 + chunk_total_token = cu_seq_lens_cpu[:, -1]
1493 +
1494 + max_tokens_over_chunk = chunk_total_token.max().item()
1495 + token_to_seq_cpu = torch.zeros(
1496 + (num_chunks, max_tokens_over_chunk), dtype=torch.int32, pin_memory=True
1497 + )
1498 + req_indices = torch.arange(num_prefills, dtype=torch.int32)
1499 + for i in range(num_chunks):
1500 + token_to_seq = torch.repeat_interleave(req_indices, chunk_seq_lens[i])
1501 + token_to_seq_cpu[i, : token_to_seq.shape[0]] = token_to_seq
1502 +
1503 + prefill_tokens_with_context = prefill_query_start_loc_cpu[
1504 + num_prefills_with_context
1505 + ].item()
1506 +
1507 + metadata_cls = MLACommonPrefillMetadata.ChunkedContextMetadata
1508 + if dcp_world_size > 1:
1509 + local_context_lens_allranks = get_dcp_local_seq_lens(
1510 + context_lens_cpu, dcp_world_size, None, dcp_local_block_size
1511 + )
1512 + # Note(qcs): Per-rank local context lengths, padded to
1513 + # `dcp_local_block_size`.
1514 + padded_local_context_lens_cpu: torch.Tensor = (
1515 + cdiv(context_lens_cpu, dcp_virtual_block_size) * dcp_local_block_size
1516 + )
1517 + # Note(hc): The above max_context_chunk already enforces block_size
1518 + # alignment; DCP only requires block_size be divisible by dcp_world_size,
1519 + # because DCP uses cp_gather_cache, which does not require chunk starts
1520 + # aligned to block_size.
1521 + assert max_context_chunk % dcp_world_size == 0
1522 + padded_local_max_context_chunk = (
1523 + cdiv(max_context_chunk, dcp_virtual_block_size) * dcp_local_block_size
1524 + )
1525 + local_chunk_starts = torch.empty(
1526 + num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
1527 + ).copy_(
1528 + torch.arange(num_chunks, dtype=torch.int32)
1529 + .multiply_(padded_local_max_context_chunk)
1530 + .unsqueeze(1)
1531 + )
1532 + local_chunk_ends = torch.min(
1533 + padded_local_context_lens_cpu.unsqueeze(0),
1534 + local_chunk_starts + padded_local_max_context_chunk,
1535 + )
1536 + padded_local_chunk_seq_lens = (local_chunk_ends - local_chunk_starts).clamp(
1537 + min=0
1538 + )
1539 +
1540 + padded_local_cu_seq_lens_cpu = torch.zeros(
1541 + num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
1542 + )
1543 + torch.cumsum(
1544 + padded_local_chunk_seq_lens,
1545 + dim=1,
1546 + out=padded_local_cu_seq_lens_cpu[:, 1:],
1547 + dtype=torch.int32,
1548 + )
1549 + max_padded_local_tokens = padded_local_cu_seq_lens_cpu[:, -1].max().item()
1550 + padded_local_token_to_seq_cpu = torch.zeros(
1551 + (num_chunks, max_padded_local_tokens), dtype=torch.int32
1552 + )
1553 + for i in range(num_chunks):
1554 + tts = torch.repeat_interleave(req_indices, padded_local_chunk_seq_lens[i])
1555 + padded_local_token_to_seq_cpu[i, : tts.shape[0]] = tts
1556 +
1557 + chunked_context_metadata = metadata_cls(
1558 + cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1559 + starts=local_chunk_starts.to(device, non_blocking=True),
1560 + seq_tot=padded_local_chunk_seq_lens.sum(dim=1).tolist(),
1561 + max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1562 + seq_lens=chunk_seq_lens,
1563 + token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
1564 + chunk_total_token=chunk_total_token.tolist(),
1565 + workspace=chunked_prefill_workspace,
1566 + prefill_tokens_with_context=prefill_tokens_with_context,
1567 + padded_local_chunk_seq_lens=padded_local_chunk_seq_lens.tolist(),
1568 + local_context_lens_allranks=local_context_lens_allranks.tolist(),
1569 + padded_local_cu_seq_lens=padded_local_cu_seq_lens_cpu.to(
1570 + device, non_blocking=True
1571 + ),
1572 + padded_local_token_to_seq=padded_local_token_to_seq_cpu.to(
1573 + device, non_blocking=True
1574 + ),
1575 + cu_seq_lens_lst=cu_seq_lens_cpu.tolist(),
1576 + chunk_size=padded_local_max_context_chunk,
1577 + )
1578 + else:
1579 + chunked_context_metadata = metadata_cls(
1580 + cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1581 + starts=chunk_starts.to(device, non_blocking=True),
1582 + seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
1583 + max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1584 + seq_lens=chunk_seq_lens,
1585 + token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
1586 + chunk_total_token=chunk_total_token,
1587 + workspace=chunked_prefill_workspace,
1588 + prefill_tokens_with_context=prefill_tokens_with_context,
1589 + )
1590 +
1591 + assert max(chunked_context_metadata.max_seq_lens) <= chunked_prefill_workspace_size
1592 + return chunked_context_metadata
1593 +
1594 +
1595 class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1596 """
1597 NOTE: Please read the comment at the top of the file before trying to
@@ -1662,8 +1856,6 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1856 context_lens_cpu = (
1857 seq_lens_cpu[reqs_start:num_reqs] - prefill_query_lens_cpu
1858 )
1665 - max_context_len_cpu = context_lens_cpu.max().item()
1666 - num_prefills_with_context_cpu = (context_lens_cpu > 0).sum().item()
1859 prefill_query_start_loc = (
1860 query_start_loc[reqs_start:] - query_start_loc[reqs_start]
1861 )
@@ -1671,188 +1863,19 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
1863 query_start_loc_cpu[reqs_start:] - query_start_loc_cpu[reqs_start]
1864 )
1865
1674 - chunked_context_metadata = None
1675 - if max_context_len_cpu > 0:
1676 - # NOTE: it is recommend you read the `Chunked Prefill` section
1677 - # in the comment at the top of the file before trying to
1678 - # understand the following code
1679 -
1680 - # currently we allocate an equal amount of workspace for each
1681 - # prefill in the batch, we could probably use a more advanced
1682 - # algorithm here and allocate more workspace to prefills with
1683 - # longer context lengths
1684 - max_context_chunk = (
1685 - self.chunked_prefill_workspace_size // num_prefills_with_context_cpu
1686 - )
1687 -
1688 - # align max_context_chunk to page_size by rounding down,
1689 - # currently the `gather_and_maybe_dequant_cache` kernel
1690 - # cannot handle `context_chunk_starts` that are not aligned
1691 - # to page_size
1692 - max_context_chunk = round_down(max_context_chunk, self.page_size)
1693 -
1694 - assert max_context_chunk > 0
1695 - num_chunks = cdiv(max_context_len_cpu, max_context_chunk)
1696 -
1697 - # if `max_context_chunk = 256`, `num_chunks = 3`, and
1698 - # `num_prefills_with_context = 4`, create a tensor that looks
1699 - # like
1700 - # [[0, 0, 0, 0], [256, 256, 256, 256], [512, 512, 512, 512]]
1701 - # Note(simon): this is done in CPU because of downstream's
1702 - # of `to_list`.
1703 - chunk_starts = torch.empty(
1704 - num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
1705 - ).copy_(
1706 - torch.arange(num_chunks, dtype=torch.int32)
1707 - .multiply_(max_context_chunk)
1708 - .unsqueeze(1)
1709 - )
1710 - chunk_ends = torch.min(
1711 - context_lens_cpu.unsqueeze(0), chunk_starts + max_context_chunk
1712 - )
1713 - chunk_seq_lens = (chunk_ends - chunk_starts).clamp(min=0)
1714 -
1715 - cu_seq_lens_cpu = torch.zeros(
1716 - num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
1717 - )
1718 - torch.cumsum(
1719 - chunk_seq_lens, dim=1, out=cu_seq_lens_cpu[:, 1:], dtype=torch.int32
1720 - )
1721 - chunk_total_token = cu_seq_lens_cpu[:, -1]
1722 -
1723 - max_token_num_over_chunk = chunk_total_token.max().item()
1724 - token_to_seq_tensor_cpu = torch.zeros(
1725 - [num_chunks, max_token_num_over_chunk],
1726 - dtype=torch.int32,
1727 - pin_memory=True,
1728 - )
1729 - range_idx = torch.arange(num_prefills, dtype=torch.int32)
1730 - for i in range(num_chunks):
1731 - chunk_token_to_seq_tensor = torch.repeat_interleave(
1732 - range_idx, chunk_seq_lens[i]
1733 - )
1734 - chunk_len = chunk_token_to_seq_tensor.shape[0]
1735 - token_to_seq_tensor_cpu[i, :chunk_len] = chunk_token_to_seq_tensor
1736 -
1737 - if self.dcp_world_size > 1:
1738 - local_context_lens_allranks = get_dcp_local_seq_lens(
1739 - context_lens_cpu,
1740 - self.dcp_world_size,
1741 - None,
1742 - self.dcp_local_block_size,
1743 - )
1744 - # Note(qcs): The max local context lengths
1745 - # padded to `dcp_local_block_size`.
1746 - padded_local_context_lens_cpu: torch.Tensor = (
1747 - cdiv(
1748 - context_lens_cpu,
1749 - self.dcp_virtual_block_size,
1750 - )
1751 - * self.dcp_local_block_size
1752 - )
1753 - # Note(hc): The above max_context_chunk already enforces
1754 - # block_size alignment, DCP just need the block_size can
1755 - # be divisible by dcp_world_size, because DCP use
1756 - # cp_gather_cache which not require `cp_chunk_starts`
1757 - # aligned to page_size.
1758 - assert max_context_chunk % self.dcp_world_size == 0
1759 - padded_local_max_context_chunk_across_ranks = (
1760 - cdiv(
1761 - max_context_chunk,
1762 - self.dcp_virtual_block_size,
1763 - )
1764 - * self.dcp_local_block_size
1765 - )
1766 - local_chunk_starts = torch.empty(
1767 - num_chunks, num_prefills, dtype=torch.int32, pin_memory=True
1768 - ).copy_(
1769 - torch.arange(num_chunks, dtype=torch.int32)
1770 - .multiply_(padded_local_max_context_chunk_across_ranks)
1771 - .unsqueeze(1)
1772 - )
1773 - local_chunk_ends = torch.min(
1774 - padded_local_context_lens_cpu.unsqueeze(0),
1775 - local_chunk_starts
1776 - + padded_local_max_context_chunk_across_ranks,
1777 - )
1778 - padded_local_chunk_seq_lens = (
1779 - local_chunk_ends - local_chunk_starts
1780 - ).clamp(min=0)
1781 -
1782 - padded_local_cu_chunk_seq_lens_cpu = torch.zeros(
1783 - num_chunks, num_prefills + 1, dtype=torch.int32, pin_memory=True
1784 - )
1785 - torch.cumsum(
1786 - padded_local_chunk_seq_lens,
1787 - dim=1,
1788 - out=padded_local_cu_chunk_seq_lens_cpu[:, 1:],
1789 - dtype=torch.int32,
1790 - )
1791 - max_padded_local_tokens_over_chunk = (
1792 - padded_local_cu_chunk_seq_lens_cpu[:, -1].max().item()
1793 - )
1794 - padded_local_token_to_seq_tensor_cpu = torch.zeros(
1795 - [num_chunks, max_padded_local_tokens_over_chunk],
1796 - dtype=torch.int32,
1797 - )
1798 - for i in range(num_chunks):
1799 - chunk_token_to_seq_tensor = torch.repeat_interleave(
1800 - range_idx, padded_local_chunk_seq_lens[i]
1801 - )
1802 - chunk_len = chunk_token_to_seq_tensor.shape[0]
1803 - padded_local_token_to_seq_tensor_cpu[i, :chunk_len] = (
1804 - chunk_token_to_seq_tensor
1805 - )
1806 -
1807 - prefill_tokens_with_context = None
1808 - if num_prefills_with_context_cpu > 0:
1809 - prefill_tokens_with_context = prefill_query_start_loc_cpu[
1810 - num_prefills_with_context_cpu
1811 - ].item()
1812 - _ChunkedMetadata = MLACommonPrefillMetadata.ChunkedContextMetadata
1813 - if self.dcp_world_size > 1:
1814 - chunked_context_metadata = _ChunkedMetadata(
1815 - cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1816 - starts=local_chunk_starts.to(device, non_blocking=True),
1817 - seq_tot=padded_local_chunk_seq_lens.sum(dim=1).tolist(),
1818 - max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1819 - seq_lens=chunk_seq_lens,
1820 - token_to_seq=token_to_seq_tensor_cpu.to(
1821 - device, non_blocking=True
1822 - ),
1823 - chunk_total_token=chunk_total_token.tolist(),
1824 - workspace=self.chunked_prefill_workspace,
1825 - padded_local_chunk_seq_lens=padded_local_chunk_seq_lens.tolist(),
1826 - local_context_lens_allranks=local_context_lens_allranks.tolist(),
1827 - padded_local_cu_seq_lens=padded_local_cu_chunk_seq_lens_cpu.to(
1828 - device, non_blocking=True
1829 - ),
1830 - padded_local_token_to_seq=padded_local_token_to_seq_tensor_cpu.to(
1831 - device, non_blocking=True
1832 - ),
1833 - cu_seq_lens_lst=cu_seq_lens_cpu.tolist(),
1834 - chunk_size=padded_local_max_context_chunk_across_ranks,
1835 - prefill_tokens_with_context=prefill_tokens_with_context,
1836 - )
1837 - else:
1838 - chunked_context_metadata = _ChunkedMetadata(
1839 - cu_seq_lens=cu_seq_lens_cpu.to(device, non_blocking=True),
1840 - starts=chunk_starts.to(device, non_blocking=True),
1841 - seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
1842 - max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1843 - seq_lens=chunk_seq_lens,
1844 - token_to_seq=token_to_seq_tensor_cpu.to(
1845 - device, non_blocking=True
1846 - ),
1847 - chunk_total_token=chunk_total_token,
1848 - workspace=self.chunked_prefill_workspace,
1849 - prefill_tokens_with_context=prefill_tokens_with_context,
1850 - )
1851 -
1852 - assert (
1853 - max(chunked_context_metadata.max_seq_lens)
1854 - <= self.chunked_prefill_workspace_size
1855 - )
1866 + chunked_context_metadata = build_mla_chunked_context_metadata(
1867 + context_lens_cpu=context_lens_cpu,
1868 + prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
1869 + num_prefills=num_prefills,
1870 + chunked_prefill_workspace=self.chunked_prefill_workspace,
1871 + chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
1872 + block_size=self.page_size,
1873 + align_chunk_to_block=True,
1874 + device=device,
1875 + dcp_world_size=self.dcp_world_size,
1876 + dcp_local_block_size=self.dcp_local_block_size,
1877 + dcp_virtual_block_size=self.dcp_virtual_block_size,
1878 + )
1879
1880 prefill_metadata = MLACommonPrefillMetadata(
1881 block_table=block_table_tensor[reqs_start:, ...],
@@ -1985,19 +2008,14 @@ def reorg_kvcache(
2008 return reorganized_kv_c_normed, reorganized_k_pe
2009
2010
1988 -class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2011 +class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
2012 """
1990 - NOTE: Please read the comment at the top of the file before trying to
1991 - understand this class
2013 + Shared MLA base providing dense-MHA prefill (via the selected
2014 + MLAPrefillBackend) for both dense and sparse impls; subclasses add decode
2015 + (``forward_mqa``).
2016 """
2017
1994 - def fused_output_quant_supported(self, quant_key):
1995 - return quant_key in (
1996 - kFp8StaticTensorSym,
1997 - kNvfp4Dynamic,
1998 - kFp8Dynamic128Sym,
1999 - kFp8Dynamic64Sym,
2000 - )
2018 + _use_flashinfer_concat_mla_k: bool
2019
2020 def __init__(
2021 self,
@@ -2005,60 +2023,25 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2023 head_size: int,
2024 scale: float,
2025 num_kv_heads: int,
2008 - alibi_slopes: list[float] | None,
2009 - sliding_window: int | None,
2026 kv_cache_dtype: str,
2011 - logits_soft_cap: float | None,
2012 - attn_type: str,
2013 - kv_sharing_target_layer_name: str | None,
2014 - # MLA Specific Arguments
2015 - q_lora_rank: int | None,
2027 kv_lora_rank: int,
2028 qk_nope_head_dim: int,
2029 qk_rope_head_dim: int,
2030 qk_head_dim: int,
2031 v_head_dim: int,
2032 kv_b_proj: ColumnParallelLinear,
2022 - # DSV3.2 MLA Specific Arguments
2023 - indexer: object | None = None,
2024 - q_pad_num_heads: int | None = None,
2033 ) -> None:
2026 - if kv_sharing_target_layer_name is not None:
2027 - raise NotImplementedError("KV sharing is not supported for MLA")
2028 -
2034 self.num_heads = num_heads
2035 self.head_size = head_size
2036 self.scale = float(scale)
2037 self.num_kv_heads = num_kv_heads
2038 self.kv_cache_dtype = kv_cache_dtype
2034 -
2035 - self.q_lora_rank = q_lora_rank
2039 self.kv_lora_rank = kv_lora_rank
2040 self.qk_nope_head_dim = qk_nope_head_dim
2041 self.qk_rope_head_dim = qk_rope_head_dim
2042 self.qk_head_dim = qk_head_dim
2043 self.v_head_dim = v_head_dim
2044 self.kv_b_proj = kv_b_proj
2042 - self.indexer = indexer
2043 - self.q_pad_num_heads = q_pad_num_heads
2044 - self.supports_quant_query_input = True
2045 - self.is_aiter_triton_fp8_bmm_enabled = rocm_aiter_ops.is_fp8bmm_enabled()
2046 -
2047 - # Use flashinfer's optimized concat_mla_k kernel when available.
2048 - # The kernel is optimized for DeepSeek V3 dimensions:
2049 - # num_heads=128, nope_dim=128, rope_dim=64
2050 - self._use_flashinfer_concat_mla_k = (
2051 - has_flashinfer()
2052 - and (self.num_heads == 128)
2053 - and (self.qk_nope_head_dim == 128)
2054 - and (self.qk_rope_head_dim == 64)
2055 - )
2056 -
2057 - self.dcp_world_size: int = -1
2058 -
2059 - self.cp_kv_cache_interleave_size: int = (
2060 - get_current_vllm_config().parallel_config.cp_kv_cache_interleave_size
2061 - )
2045
2046 def _concat_k_nope_k_pe(
2047 self, k_nope: torch.Tensor, k_pe: torch.Tensor
@@ -2341,7 +2324,7 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2324
2325 return output, output_lse
2326
2344 - def forward_mha(
2327 + def forward_mha( # type: ignore[override]
2328 self,
2329 q: torch.Tensor,
2330 kv_c_normed: torch.Tensor,
@@ -2428,6 +2411,83 @@ class MLACommonImpl(MLAAttentionImpl[M], Generic[M]):
2411 output_prefill = output_prefill.flatten(start_dim=-2)
2412 output.copy_(output_prefill)
2413
2414 +
2415 +class MLACommonImpl(MLACommonBaseImpl[M], Generic[M]):
2416 + """
2417 + NOTE: Please read the comment at the top of the file before trying to
2418 + understand this class
2419 + """
2420 +
2421 + def fused_output_quant_supported(self, quant_key):
2422 + return quant_key in (
2423 + kFp8StaticTensorSym,
2424 + kNvfp4Dynamic,
2425 + kFp8Dynamic128Sym,
2426 + kFp8Dynamic64Sym,
2427 + )
2428 +
2429 + def __init__(
2430 + self,
2431 + num_heads: int,
2432 + head_size: int,
2433 + scale: float,
2434 + num_kv_heads: int,
2435 + alibi_slopes: list[float] | None,
2436 + sliding_window: int | None,
2437 + kv_cache_dtype: str,
2438 + logits_soft_cap: float | None,
2439 + attn_type: str,
2440 + kv_sharing_target_layer_name: str | None,
2441 + # MLA Specific Arguments
2442 + q_lora_rank: int | None,
2443 + kv_lora_rank: int,
2444 + qk_nope_head_dim: int,
2445 + qk_rope_head_dim: int,
2446 + qk_head_dim: int,
2447 + v_head_dim: int,
2448 + kv_b_proj: ColumnParallelLinear,
2449 + # DSV3.2 MLA Specific Arguments
2450 + indexer: object | None = None,
2451 + q_pad_num_heads: int | None = None,
2452 + ) -> None:
2453 + if kv_sharing_target_layer_name is not None:
2454 + raise NotImplementedError("KV sharing is not supported for MLA")
2455 +
2456 + super().__init__(
2457 + num_heads,
2458 + head_size,
2459 + scale,
2460 + num_kv_heads,
2461 + kv_cache_dtype,
2462 + kv_lora_rank,
2463 + qk_nope_head_dim,
2464 + qk_rope_head_dim,
2465 + qk_head_dim,
2466 + v_head_dim,
2467 + kv_b_proj,
2468 + )
2469 + self.q_lora_rank = q_lora_rank
2470 + self.indexer = indexer
2471 + self.q_pad_num_heads = q_pad_num_heads
2472 + self.supports_quant_query_input = True
2473 + self.is_aiter_triton_fp8_bmm_enabled = rocm_aiter_ops.is_fp8bmm_enabled()
2474 +
2475 + # Use flashinfer's optimized concat_mla_k kernel when available.
2476 + # The kernel is optimized for DeepSeek V3 dimensions:
2477 + # num_heads=128, nope_dim=128, rope_dim=64
2478 + self._use_flashinfer_concat_mla_k = (
2479 + has_flashinfer()
2480 + and (self.num_heads == 128)
2481 + and (self.qk_nope_head_dim == 128)
2482 + and (self.qk_rope_head_dim == 64)
2483 + )
2484 +
2485 + self.dcp_world_size: int = -1
2486 +
2487 + self.cp_kv_cache_interleave_size: int = (
2488 + get_current_vllm_config().parallel_config.cp_kv_cache_interleave_size
2489 + )
2490 +
2491 @abstractmethod
2492 def forward_mqa(
2493 self,
vllm/model_executor/layers/attention/sparse_mla_attention.py new
+316
@@ -0,0 +1,316 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Shared forward_mha implementation and metadata builder for sparse MLA backends."""
4 +
5 +from shutil import which
6 +from typing import TYPE_CHECKING, Generic, TypeVar
7 +
8 +import numpy as np
9 +import torch
10 +
11 +from vllm.distributed import get_dcp_group
12 +from vllm.logger import init_logger
13 +from vllm.model_executor.layers.attention.mla_attention import (
14 + MLACommonBaseImpl,
15 + MLACommonPrefillMetadata,
16 + build_mla_chunked_context_metadata,
17 + get_mla_dims,
18 +)
19 +from vllm.platforms import current_platform
20 +from vllm.utils.flashinfer import has_flashinfer
21 +from vllm.utils.torch_utils import np_to_pinned_tensor
22 +from vllm.v1.attention.backend import (
23 + AttentionMetadata,
24 + AttentionMetadataBuilder,
25 +)
26 +from vllm.v1.attention.backends.utils import split_decodes_and_prefills
27 +
28 +if TYPE_CHECKING:
29 + from vllm.config import VllmConfig
30 + from vllm.model_executor.layers.linear import ColumnParallelLinear
31 + from vllm.v1.attention.backend import CommonAttentionMetadata
32 + from vllm.v1.kv_cache_interface import AttentionSpec
33 +
34 +logger = init_logger(__name__)
35 +
36 +T = TypeVar("T", bound=AttentionMetadata)
37 +
38 +
39 +class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
40 + metadata_cls: type[T]
41 +
42 + def __init__(
43 + self,
44 + kv_cache_spec: "AttentionSpec",
45 + layer_names: list[str],
46 + vllm_config: "VllmConfig",
47 + device: torch.device,
48 + ) -> None:
49 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
50 + self.vllm_config = vllm_config
51 + self.device = device
52 + self.model_config = vllm_config.model_config
53 + self.mla_dims = get_mla_dims(self.model_config)
54 + self.topk_tokens: int = vllm_config.model_config.hf_config.index_topk
55 + self.req_id_per_token_buffer = torch.empty(
56 + (vllm_config.scheduler_config.max_num_batched_tokens,),
57 + dtype=torch.int32,
58 + device=device,
59 + )
60 + parallel_config = vllm_config.parallel_config
61 + try:
62 + self.dcp_world_size = get_dcp_group().world_size
63 + self.dcp_rank = get_dcp_group().rank_in_group
64 + except AssertionError:
65 + # DCP might not be initialized in testing
66 + self.dcp_world_size = 1
67 + self.dcp_rank = 0
68 + self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
69 + self.dcp_local_block_size = self.cp_kv_cache_interleave_size
70 + self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
71 +
72 + self.chunked_prefill_workspace_size = (
73 + self.determine_chunked_prefill_workspace_size(vllm_config)
74 + )
75 + workspace_head_size = (
76 + self.mla_dims.kv_lora_rank + self.mla_dims.qk_rope_head_dim
77 + )
78 + workspace_rows = self.chunked_prefill_workspace_size
79 + if self.dcp_world_size > 1:
80 + # DCP gathers each rank's local KV shard into the workspace, so it
81 + # needs an extra 1/DCP rows beyond the TP allocation.
82 + assert self.chunked_prefill_workspace_size % self.dcp_world_size == 0
83 + workspace_rows += self.chunked_prefill_workspace_size // self.dcp_world_size
84 + self.chunked_prefill_workspace = torch.empty(
85 + (workspace_rows, workspace_head_size),
86 + dtype=self.model_config.dtype,
87 + device=device,
88 + )
89 + layer_prefill_backend = vllm_config.compilation_config.static_forward_context[
90 + layer_names[0]
91 + ].prefill_backend
92 + self._prefill_backend = (
93 + layer_prefill_backend.clone() if layer_prefill_backend is not None else None
94 + )
95 +
96 + @staticmethod
97 + def determine_chunked_prefill_workspace_size(vllm_config: "VllmConfig") -> int:
98 + scheduler_config = vllm_config.scheduler_config
99 + cache_config = vllm_config.cache_config
100 + model_config = vllm_config.model_config
101 + topk_tokens = model_config.hf_config.index_topk
102 +
103 + workspace_size = min(
104 + max(
105 + 8 * model_config.max_model_len,
106 + 4 * scheduler_config.max_num_seqs * cache_config.block_size,
107 + ),
108 + 64 * 1024,
109 + scheduler_config.max_num_seqs * topk_tokens,
110 + )
111 + return max(
112 + workspace_size,
113 + scheduler_config.max_num_seqs * cache_config.block_size,
114 + )
115 +
116 + def _build_req_id_per_token(
117 + self,
118 + common_attn_metadata: "CommonAttentionMetadata",
119 + ) -> torch.Tensor:
120 + num_tokens = common_attn_metadata.num_actual_tokens
121 + starts = np.asarray(common_attn_metadata.query_start_loc_cpu, dtype=np.int32)
122 + seg_lengths = np.diff(starts)
123 + req_id_per_token = np.repeat(
124 + np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
125 + )
126 + self.req_id_per_token_buffer.fill_(0)
127 + self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
128 + np_to_pinned_tensor(req_id_per_token), non_blocking=True
129 + )
130 + return self.req_id_per_token_buffer[:num_tokens]
131 +
132 + def _build_chunked_context_fields(
133 + self,
134 + common_attn_metadata: "CommonAttentionMetadata",
135 + num_decodes: int,
136 + num_prefills: int,
137 + prefill_query_lens_cpu: torch.Tensor | None,
138 + ) -> "MLACommonPrefillMetadata.ChunkedContextMetadata | None":
139 + if num_prefills == 0 or prefill_query_lens_cpu is None:
140 + return None
141 +
142 + seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
143 + assert seq_lens_cpu is not None
144 + context_lens_cpu = (
145 + seq_lens_cpu[num_decodes : num_decodes + num_prefills]
146 + - prefill_query_lens_cpu
147 + )
148 + qsl_cpu = common_attn_metadata.query_start_loc_cpu
149 + prefill_query_start_loc_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
150 +
151 + return build_mla_chunked_context_metadata(
152 + context_lens_cpu=context_lens_cpu,
153 + prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
154 + num_prefills=num_prefills,
155 + chunked_prefill_workspace=self.chunked_prefill_workspace,
156 + chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
157 + block_size=self.kv_cache_spec.block_size,
158 + align_chunk_to_block=current_platform.is_cuda(),
159 + device=self.device,
160 + dcp_world_size=self.dcp_world_size,
161 + dcp_local_block_size=self.dcp_local_block_size,
162 + dcp_virtual_block_size=self.dcp_virtual_block_size,
163 + )
164 +
165 + def build(
166 + self,
167 + common_prefix_len: int,
168 + common_attn_metadata: "CommonAttentionMetadata",
169 + fast_build: bool = False,
170 + ) -> T:
171 + req_id_per_token = self._build_req_id_per_token(common_attn_metadata)
172 +
173 + num_decodes, num_prefills, num_decode_tokens, _ = split_decodes_and_prefills(
174 + common_attn_metadata,
175 + decode_threshold=self.reorder_batch_threshold or 1,
176 + )
177 + (
178 + prefill_query_start_loc,
179 + prefill_max_query_len,
180 + prefill_query_lens_cpu,
181 + ) = self._build_prefill_fields(common_attn_metadata, num_decodes, num_prefills)
182 +
183 + prefill_max_seq_len = 0
184 + prefill: MLACommonPrefillMetadata | None = None
185 + if num_prefills > 0 and self._prefill_backend is not None:
186 + seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
187 + assert seq_lens_cpu is not None
188 + prefill_max_seq_len = int(
189 + seq_lens_cpu[num_decodes : num_decodes + num_prefills].max().item()
190 + )
191 + prefill = MLACommonPrefillMetadata(
192 + block_table=common_attn_metadata.block_table_tensor[num_decodes:, ...],
193 + query_start_loc=prefill_query_start_loc,
194 + max_query_len=prefill_max_query_len,
195 + chunked_context=self._build_chunked_context_fields(
196 + common_attn_metadata,
197 + num_decodes,
198 + num_prefills,
199 + prefill_query_lens_cpu,
200 + ),
201 + q_data_type=self.model_config.dtype,
202 + output_dtype=self.model_config.dtype,
203 + prefill_backend=self._prefill_backend,
204 + )
205 + self._prefill_backend.prepare_metadata(prefill)
206 +
207 + return self.metadata_cls( # type: ignore[call-arg]
208 + num_reqs=common_attn_metadata.num_reqs,
209 + max_query_len=common_attn_metadata.max_query_len,
210 + max_seq_len=common_attn_metadata.max_seq_len,
211 + num_actual_tokens=common_attn_metadata.num_actual_tokens,
212 + query_start_loc=common_attn_metadata.query_start_loc,
213 + slot_mapping=common_attn_metadata.slot_mapping,
214 + block_table=common_attn_metadata.block_table_tensor,
215 + req_id_per_token=req_id_per_token,
216 + seq_lens=common_attn_metadata.seq_lens,
217 + block_size=self.kv_cache_spec.block_size,
218 + topk_tokens=self.topk_tokens,
219 + num_decodes=num_decodes,
220 + num_prefills=num_prefills,
221 + num_decode_tokens=num_decode_tokens,
222 + prefill_max_seq_len=prefill_max_seq_len,
223 + prefill=prefill,
224 + cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
225 + )
226 +
227 + @staticmethod
228 + def _build_prefill_fields(
229 + common_attn_metadata: "CommonAttentionMetadata",
230 + num_decodes: int,
231 + num_prefills: int,
232 + ) -> tuple[
233 + torch.Tensor | None, # prefill_query_start_loc
234 + int, # prefill_max_query_len
235 + torch.Tensor | None, # prefill_query_lens_cpu
236 + ]:
237 + if num_prefills == 0:
238 + return None, 0, None
239 +
240 + offset = common_attn_metadata.query_start_loc[num_decodes]
241 + prefill_query_start_loc = (
242 + common_attn_metadata.query_start_loc[num_decodes:] - offset
243 + )
244 +
245 + qsl_cpu = common_attn_metadata.query_start_loc_cpu
246 + prefill_qsl_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
247 + prefill_query_lens = prefill_qsl_cpu[1:] - prefill_qsl_cpu[:-1]
248 + prefill_max_query_len = int(prefill_query_lens.max().item())
249 +
250 + return (
251 + prefill_query_start_loc,
252 + prefill_max_query_len,
253 + prefill_query_lens,
254 + )
255 +
256 +
257 +class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
258 + """Sparse MLA base: shared dense-MHA prefill (from MLACommonBaseImpl) plus the
259 + sparse top-k MQA decode path. Subclasses implement forward_mqa."""
260 +
261 + is_sparse = True
262 +
263 + def __init__(
264 + self,
265 + num_heads: int,
266 + head_size: int,
267 + scale: float,
268 + num_kv_heads: int,
269 + alibi_slopes: list[float] | None,
270 + sliding_window: int | None,
271 + kv_cache_dtype: str,
272 + logits_soft_cap: float | None,
273 + attn_type: str,
274 + kv_sharing_target_layer_name: str | None,
275 + # MLA-specific
276 + q_lora_rank: int | None,
277 + kv_lora_rank: int,
278 + qk_nope_head_dim: int,
279 + qk_rope_head_dim: int,
280 + qk_head_dim: int,
281 + v_head_dim: int,
282 + kv_b_proj: "ColumnParallelLinear",
283 + indexer: object | None = None,
284 + topk_indices_buffer: torch.Tensor | None = None,
285 + q_pad_num_heads: int | None = None,
286 + ) -> None:
287 + super().__init__(
288 + num_heads,
289 + head_size,
290 + scale,
291 + num_kv_heads,
292 + kv_cache_dtype,
293 + kv_lora_rank,
294 + qk_nope_head_dim,
295 + qk_rope_head_dim,
296 + qk_head_dim,
297 + v_head_dim,
298 + kv_b_proj,
299 + )
300 +
301 + # The indexer carries the shared buffer for normal layers and tests;
302 + # the explicitly-passed buffer covers backbone skip layers, whose
303 + # indexer is not constructed (see deepseek_v2.py).
304 + self.topk_indices_buffer: torch.Tensor | None = (
305 + indexer.topk_indices_buffer # type: ignore[attr-defined]
306 + if indexer is not None
307 + else topk_indices_buffer
308 + )
309 +
310 + self._use_flashinfer_concat_mla_k = (
311 + has_flashinfer()
312 + and which("ninja") is not None
313 + and (self.num_heads == 128)
314 + and (self.qk_nope_head_dim == 128)
315 + and (self.qk_rope_head_dim == 64)
316 + )
vllm/v1/attention/backend.py
+4 -81
@@ -781,6 +781,10 @@ class AttentionImplBase(ABC, Generic[T]):
781 method - subclasses define their own forward interfaces.
782 """
783
784 + # Whether this impl uses a sparse (top-k) attention path. Used by MLA to
785 + # route between the dense-MHA prefill and sparse-MQA paths.
786 + is_sparse: ClassVar[bool] = False
787 +
788 # Required attributes that all impls should have
789 num_heads: int
790 head_size: int
@@ -976,7 +980,6 @@ class MLAAttentionImpl(AttentionImplBase[T], Generic[T]):
980 ) -> None:
981 raise NotImplementedError
982
979 - @abstractmethod
983 def forward_mha(
984 self,
985 q: torch.Tensor,
@@ -1038,86 +1041,6 @@ class MLAAttentionImpl(AttentionImplBase[T], Generic[T]):
1041 )
1042
1043
1041 -class SparseMLAAttentionImpl(AttentionImplBase[T], Generic[T]):
1042 - """Sparse MLA attention implementation with only forward_mqa method.
1043 -
1044 - Sparse MLA implementations only support decode (MQA-style) attention.
1045 - They do not support prefill (MHA-style) attention.
1046 - """
1047 -
1048 - def fused_output_quant_supported(self, quant_key: "QuantKey"):
1049 - """
1050 - Does this attention implementation support fused output quantization.
1051 - Since MLA quantization is done manually in forward_impl (common code),
1052 - all MLA backends support it by default.
1053 - """
1054 - return quant_key in (
1055 - kFp8StaticTensorSym,
1056 - kNvfp4Dynamic,
1057 - kFp8Dynamic128Sym,
1058 - kFp8Dynamic64Sym,
1059 - )
1060 -
1061 - @abstractmethod
1062 - def __init__(
1063 - self,
1064 - num_heads: int,
1065 - head_size: int,
1066 - scale: float,
1067 - num_kv_heads: int,
1068 - alibi_slopes: list[float] | None,
1069 - sliding_window: int | None,
1070 - kv_cache_dtype: str,
1071 - logits_soft_cap: float | None,
1072 - attn_type: str,
1073 - kv_sharing_target_layer_name: str | None,
1074 - # MLA Specific Arguments
1075 - q_lora_rank: int | None,
1076 - kv_lora_rank: int,
1077 - qk_nope_head_dim: int,
1078 - qk_rope_head_dim: int,
1079 - qk_head_dim: int,
1080 - v_head_dim: int,
1081 - kv_b_proj: "ColumnParallelLinear",
1082 - indexer: object | None = None,
1083 - q_pad_num_heads: int | None = None,
1084 - ) -> None:
1085 - raise NotImplementedError
1086 -
1087 - @abstractmethod
1088 - def forward_mqa(
1089 - self,
1090 - q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
1091 - kv_c_and_k_pe_cache: torch.Tensor,
1092 - attn_metadata: T,
1093 - layer: AttentionLayer,
1094 - ) -> tuple[torch.Tensor, torch.Tensor | None]:
1095 - """MQA-style decode forward pass."""
1096 - raise NotImplementedError
1097 -
1098 - def do_kv_cache_update(
1099 - self,
1100 - kv_c_normed: torch.Tensor,
1101 - k_pe: torch.Tensor,
1102 - kv_cache: torch.Tensor,
1103 - slot_mapping: torch.Tensor,
1104 - kv_cache_dtype: str,
1105 - k_scale: torch.Tensor,
1106 - ) -> None:
1107 - if kv_cache.numel() == 0:
1108 - return
1109 - from vllm import _custom_ops as ops
1110 -
1111 - ops.concat_and_cache_mla(
1112 - kv_c_normed,
1113 - k_pe.squeeze(1),
1114 - kv_cache,
1115 - slot_mapping.flatten(),
1116 - kv_cache_dtype=kv_cache_dtype,
1117 - scale=k_scale,
1118 - )
1119 -
1120 -
1044 def subclass_attention_backend(
1045 name_prefix: str,
1046 attention_backend_cls: type[AttentionBackend],
vllm/v1/attention/backends/mla/flashattn_mla_sparse.py
+36 -63
@@ -4,22 +4,23 @@
4 from dataclasses import dataclass
5 from typing import Any, ClassVar
6
7 -import numpy as np
7 import torch
8
9 from vllm.config import VllmConfig
10 from vllm.config.cache import CacheDType
11 +from vllm.model_executor.layers.attention.mla_attention import MLACommonPrefillMetadata
12 +from vllm.model_executor.layers.attention.sparse_mla_attention import (
13 + SparseMLACommonImpl,
14 + SparseMLACommonMetadataBuilder,
15 +)
16 from vllm.platforms.interface import DeviceCapability
13 -from vllm.utils.torch_utils import np_to_pinned_tensor
17 from vllm.v1.attention.backend import (
18 AttentionBackend,
19 AttentionCGSupport,
20 AttentionLayer,
21 AttentionMetadata,
19 - AttentionMetadataBuilder,
20 - CommonAttentionMetadata,
22 + MLAAttentionImpl,
23 MultipleOf,
22 - SparseMLAAttentionImpl,
24 )
25 from vllm.v1.attention.backends.fa_utils import flash_attn_supports_mla
26 from vllm.v1.attention.backends.mla.sparse_utils import (
@@ -50,7 +51,7 @@ class FlashAttnMLASparseBackend(AttentionBackend):
51 return FlashAttnMLASparseMetadataBuilder
52
53 @staticmethod
53 - def get_impl_cls() -> type[SparseMLAAttentionImpl[Any]]:
54 + def get_impl_cls() -> type[MLAAttentionImpl[Any]]:
55 return FlashAttnMLASparseImpl
56
57 @classmethod
@@ -125,13 +126,21 @@ class FlashAttnMLASparseMetadata(AttentionMetadata):
126
127 block_table: torch.Tensor
128 req_id_per_token: torch.Tensor
129 + seq_lens: torch.Tensor
130 block_size: int = 64
131 topk_tokens: int = 2048
132 + num_decodes: int = 0
133 + num_prefills: int = 0
134 + num_decode_tokens: int = 0
135 + prefill_max_seq_len: int = 0
136 + prefill: MLACommonPrefillMetadata | None = None
137 + cp_kv_cache_interleave_size: int = 1
138
139
140 class FlashAttnMLASparseMetadataBuilder(
133 - AttentionMetadataBuilder[FlashAttnMLASparseMetadata]
141 + SparseMLACommonMetadataBuilder[FlashAttnMLASparseMetadata]
142 ):
143 + metadata_cls = FlashAttnMLASparseMetadata
144 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
145
146 def __init__(
@@ -141,55 +150,16 @@ class FlashAttnMLASparseMetadataBuilder(
150 vllm_config: VllmConfig,
151 device: torch.device,
152 ) -> None:
144 - self.vllm_config = vllm_config
145 - self.layer_names = layer_names
146 - self.kv_cache_spec = kv_cache_spec
147 - self.model_config = vllm_config.model_config
148 - self.device = device
149 -
150 - self._init_reorder_batch_threshold(1, supports_spec_as_decode=True)
151 -
152 - self.topk_tokens = vllm_config.model_config.hf_config.index_topk
153 - self.req_id_per_token_buffer = torch.empty(
154 - (vllm_config.scheduler_config.max_num_batched_tokens,),
155 - dtype=torch.int32,
156 - device=device,
157 - )
158 -
159 - def build(
160 - self,
161 - common_prefix_len: int,
162 - common_attn_metadata: CommonAttentionMetadata,
163 - fast_build: bool = False,
164 - ) -> FlashAttnMLASparseMetadata:
165 - cm = common_attn_metadata
166 - num_tokens = cm.num_actual_tokens
167 - starts = np.asarray(cm.query_start_loc_cpu, dtype=np.int32)
168 - seg_lengths = np.diff(starts)
169 - req_id_per_token = np.repeat(
170 - np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
171 - )
172 -
173 - self.req_id_per_token_buffer.fill_(0)
174 - self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
175 - np_to_pinned_tensor(req_id_per_token), non_blocking=True
176 - )
153 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
154
178 - return FlashAttnMLASparseMetadata(
179 - num_reqs=cm.num_reqs,
180 - max_query_len=cm.max_query_len,
181 - max_seq_len=cm.max_seq_len,
182 - num_actual_tokens=cm.num_actual_tokens,
183 - query_start_loc=cm.query_start_loc,
184 - slot_mapping=cm.slot_mapping,
185 - block_table=cm.block_table_tensor,
186 - req_id_per_token=self.req_id_per_token_buffer[:num_tokens],
187 - block_size=self.kv_cache_spec.block_size,
188 - topk_tokens=self.topk_tokens,
155 + num_q_heads = self.model_config.get_num_attention_heads(
156 + vllm_config.parallel_config
157 )
158 + threshold = {16: 128, 32: 128, 64: 256, 128: 256}.get(num_q_heads, 256)
159 + self._init_reorder_batch_threshold(threshold, supports_spec_as_decode=True)
160
161
192 -class FlashAttnMLASparseImpl(SparseMLAAttentionImpl[FlashAttnMLASparseMetadata]):
162 +class FlashAttnMLASparseImpl(SparseMLACommonImpl[FlashAttnMLASparseMetadata]):
163 def __init__(
164 self,
165 num_heads: int,
@@ -217,22 +187,25 @@ class FlashAttnMLASparseImpl(SparseMLAAttentionImpl[FlashAttnMLASparseMetadata])
187 "FlashAttnMLASparseImpl currently supports only FP16/BF16 KV cache."
188 )
189
220 - self.num_heads = num_heads
221 - self.head_size = head_size
222 - self.scale = float(scale)
223 - self.num_kv_heads = num_kv_heads
224 - self.kv_cache_dtype = kv_cache_dtype
225 - self.kv_lora_rank: int = mla_args["kv_lora_rank"]
226 - self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
227 - self.topk_indices_buffer: torch.Tensor | None = (
228 - indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
190 + super().__init__(
191 + num_heads,
192 + head_size,
193 + scale,
194 + num_kv_heads,
195 + alibi_slopes,
196 + sliding_window,
197 + kv_cache_dtype,
198 + logits_soft_cap,
199 + attn_type,
200 + kv_sharing_target_layer_name,
201 + indexer=indexer,
202 + topk_indices_buffer=topk_indices_buffer,
203 + **mla_args,
204 )
205 assert self.topk_indices_buffer is not None, (
206 "Indexer or topk_indices_buffer required for sparse MLA"
207 )
208 self.supports_quant_query_input = False
234 - self.dcp_world_size = -1
235 - self.q_pad_num_heads = None
209
210 def forward_mqa(
211 self,
vllm/v1/attention/backends/mla/flashinfer_mla_sparse.py
+37 -94
@@ -5,37 +5,33 @@
5 from dataclasses import dataclass
6 from typing import TYPE_CHECKING, ClassVar
7
8 -import numpy as np
8 import torch
9
10 from vllm import envs
11 from vllm.config import VllmConfig
12 from vllm.config.cache import CacheDType
13 from vllm.logger import init_logger
15 -from vllm.model_executor.layers.attention.mla_attention import (
16 - get_mla_dims,
14 +from vllm.model_executor.layers.attention.mla_attention import MLACommonPrefillMetadata
15 +from vllm.model_executor.layers.attention.sparse_mla_attention import (
16 + SparseMLACommonImpl,
17 + SparseMLACommonMetadataBuilder,
18 )
19 from vllm.platforms.interface import DeviceCapability
19 -from vllm.utils.torch_utils import is_quantized_kv_cache, np_to_pinned_tensor
20 +from vllm.utils.torch_utils import is_quantized_kv_cache
21 from vllm.v1.attention.backend import (
22 AttentionBackend,
23 AttentionCGSupport,
24 AttentionLayer,
25 AttentionMetadata,
25 - AttentionMetadataBuilder,
26 AttentionType,
27 - CommonAttentionMetadata,
27 + MLAAttentionImpl,
28 MultipleOf,
29 - SparseMLAAttentionImpl,
29 )
30 from vllm.v1.attention.backends.mla.sparse_utils import (
31 triton_convert_req_index_to_global_index,
32 triton_filter_and_convert_dcp_index,
33 )
35 -from vllm.v1.attention.backends.utils import (
36 - KVCacheLayoutType,
37 - split_decodes_and_prefills,
38 -)
34 +from vllm.v1.attention.backends.utils import KVCacheLayoutType
35 from vllm.v1.kv_cache_interface import AttentionSpec
36
37 if TYPE_CHECKING:
@@ -85,7 +81,7 @@ class FlashInferMLASparseTRTLLMBackend(_FlashInferMLASparseBackendBase):
81 return [32, 64]
82
83 @staticmethod
88 - def get_impl_cls() -> type[SparseMLAAttentionImpl]:
84 + def get_impl_cls() -> type[MLAAttentionImpl]:
85 return FlashInferMLASparseImpl
86
87 @classmethod
@@ -162,7 +158,7 @@ class FlashInferMLASparseSM120Backend(_FlashInferMLASparseBackendBase):
158 return [64, 256]
159
160 @staticmethod
165 - def get_impl_cls() -> type[SparseMLAAttentionImpl]:
161 + def get_impl_cls() -> type[MLAAttentionImpl]:
162 from vllm.v1.attention.backends.mla.flashinfer_mla_sparse_sm120 import (
163 FlashInferMLASparseSM120Impl,
164 )
@@ -256,7 +252,10 @@ class FlashInferMLASparseMetadata(AttentionMetadata):
252 # Sequence lengths for all requests (context + query)
253 seq_lens: torch.Tensor
254 num_decodes: int
255 + num_prefills: int
256 num_decode_tokens: int
257 + prefill_max_seq_len: int = 0
258 + prefill: MLACommonPrefillMetadata | None = None
259
260 # Sparse-specific
261 block_size: int = 64
@@ -265,10 +264,11 @@ class FlashInferMLASparseMetadata(AttentionMetadata):
264
265
266 class FlashInferMLASparseMetadataBuilder(
268 - AttentionMetadataBuilder[FlashInferMLASparseMetadata]
267 + SparseMLACommonMetadataBuilder[FlashInferMLASparseMetadata]
268 ):
269 """Builder for FlashInfer MLA Sparse attention metadata."""
270
271 + metadata_cls = FlashInferMLASparseMetadata
272 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
273
274 def __init__(
@@ -278,75 +278,20 @@ class FlashInferMLASparseMetadataBuilder(
278 vllm_config: VllmConfig,
279 device: torch.device,
280 ) -> None:
281 - self.vllm_config = vllm_config
282 - self.layer_names = layer_names
283 - self.kv_cache_spec = kv_cache_spec
284 - self.model_config = vllm_config.model_config
285 - self.device = device
286 -
287 - self.mla_dims = get_mla_dims(self.model_config)
288 - self.topk_tokens = vllm_config.model_config.hf_config.index_topk
281 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
282
283 + num_q_heads = vllm_config.model_config.get_num_attention_heads(
284 + vllm_config.parallel_config
285 + )
286 + threshold = {8: 128, 16: 128, 32: 128, 64: 256, 128: 1024}.get(
287 + num_q_heads, 1024
288 + )
289 self._init_reorder_batch_threshold(
291 - 1,
290 + threshold,
291 supports_spec_as_decode=True,
292 supports_dcp_with_varlen=True,
293 )
294
296 - self.req_id_per_token_buffer = torch.empty(
297 - (vllm_config.scheduler_config.max_num_batched_tokens,),
298 - dtype=torch.int32,
299 - device=device,
300 - )
301 -
302 - def build(
303 - self,
304 - common_prefix_len: int,
305 - common_attn_metadata: CommonAttentionMetadata,
306 - fast_build: bool = False,
307 - ) -> FlashInferMLASparseMetadata:
308 - cm = common_attn_metadata
309 - num_tokens = cm.num_actual_tokens
310 - assert self.reorder_batch_threshold is not None
311 - num_decodes, _, num_decode_tokens, _ = split_decodes_and_prefills(
312 - cm,
313 - decode_threshold=self.reorder_batch_threshold,
314 - treat_short_extends_as_decodes=True,
315 - )
316 -
317 - # Build req_id_per_token mapping
318 - starts = np.asarray(cm.query_start_loc_cpu, dtype=np.int32)
319 - seg_lengths = np.diff(starts)
320 - req_id_per_token = np.repeat(
321 - np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
322 - )
323 -
324 - # Zero-fill for cudagraphs
325 - self.req_id_per_token_buffer.fill_(0)
326 - self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
327 - np_to_pinned_tensor(req_id_per_token), non_blocking=True
328 - )
329 - req_id_per_token_tensor = self.req_id_per_token_buffer[:num_tokens]
330 -
331 - return FlashInferMLASparseMetadata(
332 - num_reqs=cm.num_reqs,
333 - max_query_len=cm.max_query_len,
334 - max_seq_len=cm.max_seq_len,
335 - num_actual_tokens=cm.num_actual_tokens,
336 - query_start_loc=cm.query_start_loc,
337 - slot_mapping=cm.slot_mapping,
338 - block_table=cm.block_table_tensor,
339 - req_id_per_token=req_id_per_token_tensor,
340 - seq_lens=cm.seq_lens,
341 - num_decodes=num_decodes,
342 - num_decode_tokens=num_decode_tokens,
343 - block_size=self.kv_cache_spec.block_size,
344 - topk_tokens=self.topk_tokens,
345 - cp_kv_cache_interleave_size=(
346 - self.vllm_config.parallel_config.cp_kv_cache_interleave_size
347 - ),
348 - )
349 -
295
296 # Global workspace buffer (lazily initialized)
297 _fi_sparse_workspace: torch.Tensor | None = None
@@ -365,7 +310,7 @@ def _get_workspace_buffer(device: torch.device) -> torch.Tensor:
310 return _fi_sparse_workspace
311
312
368 -class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata]):
313 +class FlashInferMLASparseImpl(SparseMLACommonImpl[FlashInferMLASparseMetadata]):
314 """FlashInfer MLA Sparse implementation.
315
316 Uses the TRT-LLM MLA kernel with sparse_mla_top_k parameter for
@@ -407,22 +352,20 @@ class FlashInferMLASparseImpl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata
352 "FlashInferMLASparseImpl"
353 )
354
410 - self.num_heads = num_heads
411 - self.head_size = head_size
412 - self.scale = float(scale)
413 - self.num_kv_heads = num_kv_heads
414 - self.kv_cache_dtype = kv_cache_dtype
415 -
416 - # MLA-specific dimensions
417 - self.kv_lora_rank: int = mla_args["kv_lora_rank"]
418 - self.qk_nope_head_dim: int = mla_args["qk_nope_head_dim"]
419 - self.qk_rope_head_dim: int = mla_args["qk_rope_head_dim"]
420 -
421 - # The indexer carries the shared buffer for normal layers and tests;
422 - # the explicitly-passed buffer covers backbone skip layers, whose
423 - # indexer is not constructed (see deepseek_v2.py).
424 - self.topk_indices_buffer: torch.Tensor | None = (
425 - indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
355 + super().__init__(
356 + num_heads,
357 + head_size,
358 + scale,
359 + num_kv_heads,
360 + alibi_slopes,
361 + sliding_window,
362 + kv_cache_dtype,
363 + logits_soft_cap,
364 + attn_type,
365 + kv_sharing_target_layer_name,
366 + indexer=indexer,
367 + topk_indices_buffer=topk_indices_buffer,
368 + **mla_args,
369 )
370
371 self._workspace_buffer: torch.Tensor | None = None
vllm/v1/attention/backends/mla/flashinfer_mla_sparse_sm120.py
+4 -2
@@ -9,7 +9,7 @@ import torch
9 from vllm.v1.attention.backend import (
10 AttentionLayer,
11 AttentionType,
12 - SparseMLAAttentionImpl,
12 + MLAAttentionImpl,
13 )
14 from vllm.v1.attention.backends.mla.flashinfer_mla_sparse import (
15 FlashInferMLASparseMetadata,
@@ -29,9 +29,11 @@ def _kv_scale_format_for_model(model_type: str | None) -> str:
29 return "pow2_fp32"
30
31
32 -class FlashInferMLASparseSM120Impl(SparseMLAAttentionImpl[FlashInferMLASparseMetadata]):
32 +class FlashInferMLASparseSM120Impl(MLAAttentionImpl[FlashInferMLASparseMetadata]):
33 """SM120 FlashInfer sparse-MLA implementation."""
34
35 + is_sparse = True
36 +
37 def __init__(
38 self,
39 num_heads: int,
vllm/v1/attention/backends/mla/flashmla_sparse.py
+55 -73
@@ -1,31 +1,30 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from dataclasses import dataclass
4 -from typing import TYPE_CHECKING, Any, ClassVar
4 +from typing import TYPE_CHECKING, ClassVar
5
6 -import numpy as np
6 import torch
7
8 from vllm import _custom_ops as ops
9 from vllm.config import VllmConfig, get_current_vllm_config
10 from vllm.config.cache import CacheDType
11 from vllm.logger import init_logger
13 -from vllm.model_executor.layers.attention.mla_attention import (
14 - get_mla_dims,
12 +from vllm.model_executor.layers.attention.mla_attention import MLACommonPrefillMetadata
13 +from vllm.model_executor.layers.attention.sparse_mla_attention import (
14 + SparseMLACommonImpl,
15 + SparseMLACommonMetadataBuilder,
16 )
17 from vllm.platforms import current_platform
18 from vllm.platforms.interface import DeviceCapability
19 from vllm.utils.platform_utils import num_compute_units
19 -from vllm.utils.torch_utils import is_quantized_kv_cache, np_to_pinned_tensor
20 +from vllm.utils.torch_utils import is_quantized_kv_cache
21 from vllm.v1.attention.backend import (
22 AttentionBackend,
23 AttentionCGSupport,
24 AttentionLayer,
25 AttentionMetadata,
25 - AttentionMetadataBuilder,
26 CommonAttentionMetadata,
27 MultipleOf,
28 - SparseMLAAttentionImpl,
28 )
29 from vllm.v1.attention.backends.mla.sparse_utils import (
30 triton_convert_req_index_to_global_index,
@@ -109,7 +108,7 @@ class FlashMLASparseBackend(AttentionBackend):
108 return FlashMLASparseMetadataBuilder
109
110 @staticmethod
112 - def get_impl_cls() -> type[SparseMLAAttentionImpl[Any]]:
111 + def get_impl_cls() -> type["FlashMLASparseImpl"]:
112 return FlashMLASparseImpl
113
114 @classmethod
@@ -159,6 +158,14 @@ class FlashMLASparseMetadata(AttentionMetadata):
158 block_size: int = 64
159 topk_tokens: int = 2048
160
161 + num_decodes: int = 0
162 + num_prefills: int = 0
163 + num_decode_tokens: int = 0
164 + seq_lens: torch.Tensor | None = None
165 + prefill_max_seq_len: int = 0
166 + prefill: MLACommonPrefillMetadata | None = None
167 + cp_kv_cache_interleave_size: int = 1
168 +
169 @dataclass
170 class FP8KernelMetadata:
171 scheduler_metadata: FlashMLASchedMeta
@@ -228,8 +235,11 @@ def get_prefill_workspace_size(max_model_len: int):
235 return max_model_len * 5
236
237
231 -class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetadata]):
238 +class FlashMLASparseMetadataBuilder(
239 + SparseMLACommonMetadataBuilder[FlashMLASparseMetadata]
240 +):
241 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
242 + metadata_cls = FlashMLASparseMetadata
243
244 def __init__(
245 self,
@@ -238,29 +248,30 @@ class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetad
248 vllm_config: VllmConfig,
249 device: torch.device,
250 ) -> None:
241 - self.vllm_config = vllm_config
242 - self.layer_names = layer_names
251 + super().__init__(kv_cache_spec, layer_names, vllm_config, device)
252 cache_config = vllm_config.cache_config
244 - self.kv_cache_spec = kv_cache_spec
245 - self.model_config = vllm_config.model_config
253 parallel_config = vllm_config.parallel_config
247 - self.device = device
254
249 - # Classify single-token queries (plus num_speculative_tokens via
250 - # supports_spec_as_decode=True) as decodes; longer queries go to
251 - # prefill.
252 - self._init_reorder_batch_threshold(1, supports_spec_as_decode=True)
255 + num_q_heads = self.model_config.get_num_attention_heads(parallel_config)
256 + if current_platform.is_device_capability_family(100):
257 + threshold = {8: 128, 16: 128, 32: 128, 64: 256, 128: 1024}.get(
258 + num_q_heads, 1024
259 + )
260 + else:
261 + threshold = {16: 128, 32: 128, 64: 256, 128: 256}.get(num_q_heads, 256)
262 + self._init_reorder_batch_threshold(
263 + threshold,
264 + supports_spec_as_decode=True,
265 + )
266
267 sm_count = num_compute_units(device.index)
268
269 self.num_heads = self.model_config.get_num_attention_heads(parallel_config)
257 - self.mla_dims = get_mla_dims(self.model_config)
270 # FP8 decode kernel only supports h_q = 64 or 128, so we need to pad
271 self.fp8_decode_padded_heads = (
272 FlashMLASparseImpl._compute_fp8_decode_padded_heads(self.num_heads)
273 )
274
263 - self.topk_tokens = vllm_config.model_config.hf_config.index_topk
275 self.use_fp8_kv_cache = cache_config.cache_dtype == "fp8_ds_mla"
276 max_num_seqs = vllm_config.scheduler_config.max_num_seqs
277 # Shape: [max_num_seqs], all elements = topk_tokens (constant for full-CG)
@@ -306,11 +317,6 @@ class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetad
317 dtype=torch.int32,
318 device=device,
319 )
309 - self.req_id_per_token_buffer = torch.empty(
310 - (vllm_config.scheduler_config.max_num_batched_tokens,),
311 - dtype=torch.int32,
312 - device=device,
313 - )
320
321 def _build_fp8_mixed_decode_prefill(
322 self,
@@ -493,51 +499,24 @@ class FlashMLASparseMetadataBuilder(AttentionMetadataBuilder[FlashMLASparseMetad
499 common_attn_metadata: CommonAttentionMetadata,
500 fast_build: bool = False,
501 ) -> FlashMLASparseMetadata:
496 - cm = common_attn_metadata
497 - num_tokens = cm.num_actual_tokens
498 - starts = np.asarray(cm.query_start_loc_cpu, dtype=np.int32)
499 - seg_lengths = np.diff(starts)
500 - req_id_per_token = np.repeat(
501 - np.arange(seg_lengths.shape[0], dtype=np.int32), seg_lengths
502 - )
503 - # Zero-fill for cudagraphs
504 - self.req_id_per_token_buffer.fill_(0)
505 - self.req_id_per_token_buffer[: req_id_per_token.shape[0]].copy_(
506 - np_to_pinned_tensor(req_id_per_token), non_blocking=True
507 - )
508 - req_id_per_token = self.req_id_per_token_buffer[:num_tokens]
502 + metadata = super().build(common_prefix_len, common_attn_metadata, fast_build)
503
510 - fp8_extra_metadata: (
511 - FlashMLASparseMetadata.FP8SeparatePrefillDecode
512 - | FlashMLASparseMetadata.FP8KernelMetadata
513 - | None
514 - ) = None
504 fp8_use_mixed_batch = self.num_heads < MIN_HEADS_FOR_BF16_PREFILL
505 + metadata.fp8_use_mixed_batch = fp8_use_mixed_batch
506 if self.use_fp8_kv_cache:
507 if fp8_use_mixed_batch:
518 - fp8_extra_metadata = self._build_fp8_mixed_decode_prefill(cm)
508 + metadata.fp8_extra_metadata = self._build_fp8_mixed_decode_prefill(
509 + common_attn_metadata
510 + )
511 else:
520 - fp8_extra_metadata = self._build_fp8_separate_prefill_decode(cm)
521 -
522 - metadata = FlashMLASparseMetadata(
523 - num_reqs=cm.num_reqs,
524 - max_query_len=cm.max_query_len,
525 - max_seq_len=cm.max_seq_len,
526 - num_actual_tokens=cm.num_actual_tokens,
527 - query_start_loc=cm.query_start_loc,
528 - slot_mapping=cm.slot_mapping,
529 - block_table=cm.block_table_tensor,
530 - req_id_per_token=req_id_per_token,
531 - block_size=self.kv_cache_spec.block_size,
532 - topk_tokens=self.topk_tokens,
533 - fp8_extra_metadata=fp8_extra_metadata,
534 - fp8_use_mixed_batch=fp8_use_mixed_batch,
535 - )
512 + metadata.fp8_extra_metadata = self._build_fp8_separate_prefill_decode(
513 + common_attn_metadata
514 + )
515
516 return metadata
517
518
540 -class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
519 +class FlashMLASparseImpl(SparseMLACommonImpl[FlashMLASparseMetadata]):
520 @staticmethod
521 def _compute_fp8_decode_padded_heads(num_heads: int) -> int:
522 # FP8 decode kernel only supports h_q = 64 or 128
@@ -561,19 +540,22 @@ class FlashMLASparseImpl(SparseMLAAttentionImpl[FlashMLASparseMetadata]):
540 indexer: "Indexer | None" = None,
541 **mla_args,
542 ) -> None:
564 - self.num_heads = num_heads
565 - self.head_size = head_size
566 - self.scale = float(scale)
567 - self.num_kv_heads = num_kv_heads
568 - self.kv_cache_dtype = kv_cache_dtype
569 - self.kv_lora_rank: int = mla_args["kv_lora_rank"]
570 - self.softmax_scale = scale
571 - # The indexer carries the shared buffer for normal layers and tests;
572 - # the explicitly-passed buffer covers backbone skip layers, whose
573 - # indexer is not constructed (see deepseek_v2.py).
574 - self.topk_indices_buffer: torch.Tensor | None = (
575 - indexer.topk_indices_buffer if indexer is not None else topk_indices_buffer
543 + super().__init__(
544 + num_heads,
545 + head_size,
546 + scale,
547 + num_kv_heads,
548 + alibi_slopes,
549 + sliding_window,
550 + kv_cache_dtype,
551 + logits_soft_cap,
552 + attn_type,
553 + kv_sharing_target_layer_name,
554 + indexer=indexer,
555 + topk_indices_buffer=topk_indices_buffer,
556 + **mla_args,
557 )
558 + self.softmax_scale = scale
559 # Prefill BF16 kernel requires 64 on Hopper, 128 on Blackwell
560 self.prefill_padding = (
561 128 if current_platform.is_device_capability_family(100) else 64
vllm/v1/attention/backends/mla/indexer.py
+6 -3
@@ -238,7 +238,9 @@ def get_max_prefill_buffer_size(vllm_config: VllmConfig):
238
239
240 class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
241 - reorder_batch_threshold: int = 1
241 + # The indexer opts out of the shared reorder-threshold vote (see __init__),
242 + # so this is None; its own split uses self.decode_threshold.
243 + reorder_batch_threshold: int | None = None
244
245 @classmethod
246 def get_cudagraph_support(
@@ -285,7 +287,8 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
287 )
288
289 next_n = self.num_speculative_tokens + 1
288 - self.reorder_batch_threshold += self.num_speculative_tokens
290 + self.decode_threshold = next_n
291 + self.reorder_batch_threshold = None
292 # NOTE: SM100 datacenter GPUs support any next_n natively via the
293 # multi-atom paged MQA logits kernels (FP8 and FP4 indexer
294 # caches). Outside the SM100 family the FP8
@@ -565,7 +568,7 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
568 num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
569 split_decodes_and_prefills(
570 common_attn_metadata,
568 - decode_threshold=self.reorder_batch_threshold,
571 + decode_threshold=self.decode_threshold,
572 require_uniform=not self.use_flattening,
573 )
574 )
vllm/v1/attention/backends/mla/prefill/flash_attn.py
+16 -3
@@ -3,7 +3,7 @@
3 """FlashAttention backend for MLA prefill."""
4
5 import functools
6 -from typing import TYPE_CHECKING
6 +from typing import TYPE_CHECKING, ClassVar
7
8 import torch
9
@@ -25,7 +25,10 @@ from vllm.v1.attention.backends.fa_utils import (
25 get_flash_attn_version,
26 is_flash_attn_varlen_func_available,
27 )
28 -from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
28 +from vllm.v1.attention.backends.mla.prefill.base import (
29 + MLADimensions,
30 + MLAPrefillBackend,
31 +)
32
33 if TYPE_CHECKING:
34 from vllm.config import VllmConfig
@@ -40,6 +43,14 @@ else:
43 class FlashAttnPrefillBackend(MLAPrefillBackend):
44 """FlashAttention backend for MLA prefill."""
45
46 + supported_mla_dimensions: ClassVar[list[MLADimensions]] = [
47 + MLADimensions(
48 + qk_nope_head_dim=128,
49 + qk_rope_head_dim=64,
50 + v_head_dim=128,
51 + ),
52 + ]
53 +
54 @staticmethod
55 def get_name() -> str:
56 return "FLASH_ATTN"
@@ -76,7 +87,9 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
87 )
88 qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
89 self.flash_attn_varlen_func = flash_attn_varlen_func
79 - self.vllm_flash_attn_version = get_flash_attn_version(head_size=qk_head_dim)
90 + self.vllm_flash_attn_version = get_flash_attn_version(
91 + head_size=qk_head_dim, head_size_v=v_head_dim
92 + )
93 if self.vllm_flash_attn_version is not None:
94 self.flash_attn_varlen_func = functools.partial(
95 flash_attn_varlen_func, fa_version=self.vllm_flash_attn_version
vllm/v1/attention/backends/mla/rocm_aiter_mla_sparse.py
+4 -2
@@ -24,8 +24,8 @@ from vllm.v1.attention.backend import (
24 AttentionMetadata,
25 AttentionMetadataBuilder,
26 CommonAttentionMetadata,
27 + MLAAttentionImpl,
28 MultipleOf,
28 - SparseMLAAttentionImpl,
29 )
30 from vllm.v1.attention.backends.mla.rocm_aiter_mla import (
31 AiterMLAHelper,
@@ -626,7 +626,9 @@ def reference_mla_sparse_prefill(
626 return (result, lse)
627
628
629 -class ROCMAiterMLASparseImpl(SparseMLAAttentionImpl[ROCMAiterMLASparseMetadata]):
629 +class ROCMAiterMLASparseImpl(MLAAttentionImpl[ROCMAiterMLASparseMetadata]):
630 + is_sparse = True
631 +
632 def __init__(
633 self,
634 num_heads: int,
vllm/v1/attention/backends/mla/sparse_swa.py
+5 -7
@@ -292,8 +292,7 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
292 - Chunked prefill (aligns with the indexer's chunking)
293 """
294
295 - # Base threshold: query_len <= 1 is decode
296 - reorder_batch_threshold: int = 1
295 + reorder_batch_threshold: int | None = None
296 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
297
298 def __init__(self, *args, **kwargs):
@@ -315,14 +314,13 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
314 )
315 # Decode can have query_len up to
316 # 1 + (2 if parallel drafting else 1) * num_speculative_tokens.
318 - # This MUST match the flashmla_sparse / indexer threshold so that
319 - # all backends agree on the decode/prefill split.
317 + # sparse_swa has no MQA-vs-dense-MHA routing, so multi-token queries take
318 + # the prefill path and the decode/prefill split stays at that width.
319 spec_mult = (
320 2 if (spec_config is not None and spec_config.parallel_drafting) else 1
321 )
323 - self.decode_threshold = (
324 - self.reorder_batch_threshold + spec_mult * self.num_speculative_tokens
325 - )
322 + self.decode_threshold = 1 + spec_mult * self.num_speculative_tokens
323 + self.reorder_batch_threshold = None
324
325 hf_config = self.vllm_config.model_config.hf_config
326 assert hasattr(hf_config, "sliding_window")
vllm/v1/attention/backends/mla/xpu_mla_sparse.py
+4 -2
@@ -21,7 +21,7 @@ from vllm.v1.attention.backend import (
21 AttentionMetadata,
22 AttentionMetadataBuilder,
23 CommonAttentionMetadata,
24 - SparseMLAAttentionImpl,
24 + MLAAttentionImpl,
25 )
26 from vllm.v1.attention.backends.mla.flashmla_sparse import (
27 triton_convert_req_index_to_global_index,
@@ -170,7 +170,9 @@ class XPUMLASparseMetadataBuilder(AttentionMetadataBuilder[XPUMLASparseMetadata]
170 return metadata
171
172
173 -class XPUMLASparseImpl(SparseMLAAttentionImpl[XPUMLASparseMetadata]):
173 +class XPUMLASparseImpl(MLAAttentionImpl[XPUMLASparseMetadata]):
174 + is_sparse = True
175 +
176 def __init__(
177 self,
178 num_heads: int,