[Attention] Enable masked MHA for GLM-5 head dimensions (#53785)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Matthew Bonanni committed
Aug 27, 2026 at 17:17 UTC
de9250ac9e9b249133fff14e15d9248a1ebbcdb8
10 files changed
+527
-74
benchmarks/attention_benchmarks/benchmark.py
+32
-6
@@ -1143,12 +1143,26 @@ def main():
1143
]
1144
else:
1145
variants = available_variants
1146
+ model_sweep = getattr(args, "model_parameter_sweep", None)
1147
+ if model_sweep is None:
1148
+ num_q_heads_values = [args.num_q_heads]
1149
+ elif model_sweep.param_name == "num_q_heads":
1150
+ num_q_heads_values = model_sweep.values
1151
+ else:
1152
+ raise ValueError(
1153
+ "mha_vs_mqa mode only supports a num_q_heads model parameter sweep"
1154
+ )
1155
+ benchmark_specs = [
1156
+ (spec, num_q_heads)
1157
+ for spec in args.batch_specs
1158
+ for num_q_heads in num_q_heads_values
1159
+ ]
1160
masked_mha_max_seq_len = getattr(
1161
args, "sparse_mla_masked_mha_max_seq_len", None
1162
)
1163
formatter = ResultsFormatter(console)
1164
total = 0
1151
- for spec in args.batch_specs:
1165
+ for spec, _ in benchmark_specs:
1166
q_len = max(request.q_len for request in parse_batch_spec(spec))
1167
for variant_label, _, _ in variants:
1168
if (
@@ -1164,7 +1178,7 @@ def main():
1178
total += len(backends)
1179
1180
with tqdm(total=total, desc="Benchmarking") as pbar:
1167
- for spec in args.batch_specs:
1181
+ for spec, num_q_heads in benchmark_specs:
1182
q_len = max(request.q_len for request in parse_batch_spec(spec))
1183
for backend in backends:
1184
for variant_label, force_mqa, mha_mode in variants:
@@ -1178,12 +1192,17 @@ def main():
1192
and q_len > masked_mha_max_seq_len
1193
):
1194
continue
1195
+ result_backend = f"{backend}_{variant_label}"
1196
+ if model_sweep is not None:
1197
+ result_backend = model_sweep.get_label(
1198
+ result_backend, num_q_heads
1199
+ )
1200
config = BenchmarkConfig(
1182
- backend=f"{backend}_{variant_label}",
1201
+ backend=result_backend,
1202
batch_spec=spec,
1203
num_layers=args.num_layers,
1204
head_dim=args.head_dim,
1186
- num_q_heads=args.num_q_heads,
1205
+ num_q_heads=num_q_heads,
1206
num_kv_heads=args.num_kv_heads,
1207
block_size=args.block_size,
1208
device=args.device,
@@ -1211,7 +1230,7 @@ def main():
1230
# run_mla_benchmark needs the real backend name
1231
from mla_runner import run_mla_benchmark as run_mla
1232
1214
- run_label = f"{backend}_{variant_label} {spec}"
1233
+ run_label = f"{backend}_{variant_label} {num_q_heads}h {spec}"
1234
pbar.set_postfix_str(run_label)
1235
1236
try:
@@ -1248,7 +1267,14 @@ def main():
1267
1268
# Display results with variant labels as separate "backends"
1269
console.print("\n[bold green]MHA vs MQA Results:[/]")
1251
- variant_backends = [f"{b}_{v}" for b in backends for v, _, _ in variants]
1270
+ variant_backends = []
1271
+ for num_q_heads in num_q_heads_values:
1272
+ for backend in backends:
1273
+ for variant, _, _ in variants:
1274
+ label = f"{backend}_{variant}"
1275
+ if model_sweep is not None:
1276
+ label = model_sweep.get_label(label, num_q_heads)
1277
+ variant_backends.append(label)
1278
formatter.print_table(all_results, variant_backends)
1279
1280
# Handle model parameter sweep mode
benchmarks/attention_benchmarks/configs/mla_sparse_masked_mha_vs_mqa_glm5.yaml
new
+59
@@ -0,0 +1,59 @@
1
+mode: mha_vs_mqa
2
+model:
3
+ name: "glm-5.x"
4
+ num_layers: 78
5
+ num_q_heads: 64
6
+ num_kv_heads: 1
7
+ head_dim: 576
8
+ kv_lora_rank: 512
9
+ qk_nope_head_dim: 192
10
+ qk_rope_head_dim: 64
11
+ v_head_dim: 256
12
+ block_size: 128
13
+ max_model_len: 131072
14
+model_parameter_sweep:
15
+ param_name: "num_q_heads"
16
+ values: [64, 32, 16, 8]
17
+ label_format: "{backend}_{value}h"
18
+batch_specs:
19
+ - "q256s2048"
20
+ - "q512s2048"
21
+ - "q1024s2048"
22
+ - "q1536s2048"
23
+ - "q2048"
24
+ - "q256s4096"
25
+ - "q512s4096"
26
+ - "q1024s4096"
27
+ - "q1536s4096"
28
+ - "q2048s4096"
29
+ - "q4096"
30
+ - "q256s8192"
31
+ - "q512s8192"
32
+ - "q1024s8192"
33
+ - "q1536s8192"
34
+ - "q2048s8192"
35
+ - "q4096s8192"
36
+ - "q8192"
37
+ - "q256s16384"
38
+ - "q512s16384"
39
+ - "q1024s16384"
40
+ - "q1536s16384"
41
+ - "q2048s16384"
42
+ - "q4096s16384"
43
+ - "q8192s16384"
44
+ - "q16384"
45
+ - "q256s32768"
46
+ - "q512s32768"
47
+ - "q1024s32768"
48
+ - "q1536s32768"
49
+ - "q2048s32768"
50
+ - "q4096s32768"
51
+ - "q8192s32768"
52
+ - "q16384s32768"
53
+ - "q32768"
54
+backends: ["FLASHMLA_SPARSE", "FLASHINFER_MLA_SPARSE"]
55
+device: "cuda:0"
56
+profile_memory: false
57
+sparse_mla_masked_mha_max_seq_len: 131072
58
+sparse_mla_mha_variants: ["masked_mha", "mqa"]
59
+sparse_mla_topk_pattern: "random"
benchmarks/attention_benchmarks/mla_runner.py
+32
@@ -313,6 +313,9 @@ _BACKEND_PROPERTIES = {
313
"FLASHMLA_SPARSE": {
314
"query_format": "concat", # Single concatenated tensor (vs tuple)
315
},
316
+ "FLASHINFER_MLA_SPARSE": {
317
+ "query_format": "concat", # Single concatenated tensor (vs tuple)
318
+ },
319
}
320
321
@@ -1184,6 +1187,35 @@ def _run_mla_benchmark_batched(
1187
)
1188
# Run each benchmark with the shared impl
1189
for config, threshold, num_splits in configs_with_params:
1190
+ force_masked_mha = (
1191
+ is_sparse and getattr(config, "sparse_mla_mha_mode", "auto") == "masked"
1192
+ )
1193
+ if force_masked_mha:
1194
+ impl.masked_mha_available = True
1195
+ from vllm.model_executor.layers.attention.sparse_mla_attention import (
1196
+ GLOBAL_TOPK_MASK_MAX_BYTES,
1197
+ _topk_mask_shape,
1198
+ )
1199
+
1200
+ requests = parse_batch_spec(config.batch_spec)
1201
+ max_query_len = max(request.q_len for request in requests)
1202
+ mask_shape = _topk_mask_shape(
1203
+ len(requests), max_query_len, max_query_len
1204
+ )
1205
+ mask_words = max(
1206
+ GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
1207
+ mask_shape[0] * mask_shape[1] * mask_shape[2],
1208
+ )
1209
+ if (
1210
+ builder_instance.topk_mask_workspace is None
1211
+ or builder_instance.topk_mask_workspace.numel() != mask_words
1212
+ ):
1213
+ builder_instance.topk_mask_workspace = torch.zeros(
1214
+ mask_words,
1215
+ dtype=torch.int32,
1216
+ device=device,
1217
+ )
1218
+
1219
# Set threshold for this benchmark (FlashAttn/FlashMLA only)
1220
original_threshold = None
1221
effective_threshold = threshold
cmake/external_projects/vllm_flash_attn.cmake
+1
-1
@@ -39,7 +39,7 @@ else()
39
FetchContent_Declare(
40
vllm-flash-attn
41
GIT_REPOSITORY https://github.com/vllm-project/flash-attention.git
42
- GIT_TAG 4eb29359a51b4fe67c8c0f0c5e931236971aa492
42
+ GIT_TAG 06bdd47c0d0383daf6a2ff0c418faff9c6da16e5
43
GIT_PROGRESS TRUE
44
# Don't share the vllm-flash-attn build between build types
45
BINARY_DIR ${CMAKE_BINARY_DIR}/vllm-flash-attn
tests/model_executor/layers/test_mla_short_prefill_indexer.py
+75
-7
@@ -8,6 +8,7 @@ import torch
8
9
import vllm.model_executor.layers.sparse_attn_indexer as sparse_indexer
10
from vllm.config import CUDAGraphMode
11
+from vllm.models.deepseek_v32 import attention as deepseek_v32_attention
12
from vllm.models.deepseek_v32.attention import DeepseekV32Attention
13
from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerMetadata
14
@@ -49,7 +50,6 @@ def make_mla_metadata(*, use_dense_mha: bool = True, num_decode_tokens: int = 0)
50
[
51
"short",
52
"threshold_mismatch",
52
- "mqa_only_layer",
53
"force_mqa",
54
"mla_decode",
55
"capture",
@@ -141,11 +141,7 @@ def test_short_prefill_updates_k_cache_before_scoring_decision(
141
topk_indices = torch.full((7, 2048), 17, dtype=torch.int32)
142
143
def run_indexer():
144
- if batch_kind == "mqa_only_layer":
145
- assert not DeepseekV32Attention.supports_dense_mha_prefill
146
- dense_mha_layer = ""
147
- else:
148
- dense_mha_layer = MLA_LAYER
144
+ assert DeepseekV32Attention.supports_dense_mha_prefill
145
return sparse_indexer.sparse_attn_indexer(
146
hidden_states,
147
INDEXER_LAYER,
@@ -163,7 +159,7 @@ def test_short_prefill_updates_k_cache_before_scoring_decision(
159
topk_indices,
160
False,
161
False,
166
- dense_mha_layer,
162
+ MLA_LAYER,
163
)
164
165
if should_skip:
@@ -222,3 +218,75 @@ def test_skipped_k_cache_insert_accepts_no_k(
218
219
assert result is topk_indices
220
assert torch.all(topk_indices == -1)
221
+
222
+
223
+@pytest.mark.parametrize("fp8_query", [False, True])
224
+def test_deepseek_v32_dispatches_selected_mha(
225
+ monkeypatch: pytest.MonkeyPatch,
226
+ fp8_query: bool,
227
+) -> None:
228
+ attn_metadata = SimpleNamespace(num_actual_tokens=2)
229
+ kv_cache = torch.empty(1)
230
+ monkeypatch.setattr(
231
+ deepseek_v32_attention,
232
+ "get_attention_context",
233
+ lambda _: (attn_metadata, None, kv_cache, None),
234
+ )
235
+
236
+ observed = {}
237
+
238
+ def record_forward_impl(*args):
239
+ observed["args"] = args
240
+
241
+ layer = SimpleNamespace(
242
+ indexer=None,
243
+ skip_topk=False,
244
+ layer_name=MLA_LAYER,
245
+ use_pcp=False,
246
+ _fp8_query=fp8_query,
247
+ _use_sparse_mha=lambda _: True,
248
+ rotary_emb=lambda _positions, q: (q + 1, None),
249
+ forward_impl=record_forward_impl,
250
+ )
251
+ q_nope = torch.randn(2, 1, 2)
252
+ q_pe = torch.randn(2, 1, 2)
253
+ mqa_q = torch.randn(2, 1, 2)
254
+ mha_q = torch.cat((q_nope, q_pe + 1 if fp8_query else mqa_q), dim=-1)
255
+ kv_c = torch.empty(2, 2)
256
+ k_pe = torch.empty(2, 2)
257
+ output = torch.empty(2, 2)
258
+
259
+ DeepseekV32Attention._sparse_indexer_and_attn(
260
+ layer,
261
+ torch.arange(2),
262
+ torch.empty(2, 2),
263
+ q_nope,
264
+ q_pe,
265
+ None,
266
+ None,
267
+ None,
268
+ kv_c,
269
+ k_pe,
270
+ torch.empty(2, 1, 2),
271
+ mqa_q,
272
+ output,
273
+ )
274
+
275
+ expected_args = (
276
+ mha_q,
277
+ kv_c,
278
+ kv_cache,
279
+ attn_metadata,
280
+ output,
281
+ )
282
+ actual_args = observed["args"]
283
+ torch.testing.assert_close(actual_args[0], mha_q)
284
+ assert actual_args[2].shape == (2, 1, 2)
285
+ assert actual_args[2].data_ptr() == k_pe.data_ptr()
286
+ assert all(
287
+ actual is expected
288
+ for actual, expected in zip(
289
+ actual_args[1:2] + actual_args[3:],
290
+ expected_args[1:],
291
+ )
292
+ )
tests/v1/attention/test_mla_backends.py
+119
@@ -27,6 +27,7 @@ from vllm.model_executor.layers.attention.mla_attention import (
27
MLAAttention,
28
QueryLenSupport,
29
_DecodeConcatQuantFP8,
30
+ _use_masked_mha,
31
build_mla_chunked_context_metadata,
32
)
33
from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
@@ -68,6 +69,124 @@ if current_platform.is_rocm():
69
DEVICE_TYPE = current_platform.device_type
70
71
72
+@pytest.mark.parametrize(
73
+ ("tensor_parallel_size", "query_len", "expected"),
74
+ [
75
+ (1, 8192, True),
76
+ (1, 9216, False),
77
+ (2, 20480, True),
78
+ (2, 24576, False),
79
+ (4, 48 * 1024, True),
80
+ (4, 56 * 1024, False),
81
+ (8, 112 * 1024, True),
82
+ (8, 128 * 1024, False),
83
+ ],
84
+)
85
+def test_glm5_masked_mha_pure_prefill_routing(
86
+ tensor_parallel_size, query_len, expected
87
+):
88
+ assert (
89
+ _use_masked_mha(
90
+ backend_name="FLASHMLA_SPARSE",
91
+ tensor_parallel_size=tensor_parallel_size,
92
+ qk_head_dim=256,
93
+ v_head_dim=256,
94
+ query_len=query_len,
95
+ seq_len=query_len,
96
+ has_context=False,
97
+ )
98
+ is expected
99
+ )
100
+
101
+
102
+@pytest.mark.parametrize(
103
+ ("tensor_parallel_size", "query_len", "seq_len", "expected"),
104
+ [
105
+ (1, 8192, 8192, False),
106
+ (2, 4096, 6144, True),
107
+ (2, 4096, 8192, False),
108
+ (2, 8192, 10240, True),
109
+ (4, 16384, 24576, True),
110
+ (4, 16384, 32768, False),
111
+ (4, 32768, 34 * 1024, True),
112
+ (4, 32768, 36 * 1024, False),
113
+ (8, 32768, 49152, True),
114
+ (8, 32768, 65536, False),
115
+ ],
116
+)
117
+def test_glm5_masked_mha_context_routing(
118
+ tensor_parallel_size, query_len, seq_len, expected
119
+):
120
+ assert (
121
+ _use_masked_mha(
122
+ backend_name="FLASHMLA_SPARSE",
123
+ tensor_parallel_size=tensor_parallel_size,
124
+ qk_head_dim=256,
125
+ v_head_dim=256,
126
+ query_len=query_len,
127
+ seq_len=seq_len,
128
+ has_context=True,
129
+ )
130
+ is expected
131
+ )
132
+
133
+
134
+@pytest.mark.parametrize(
135
+ ("tensor_parallel_size", "query_len", "seq_len", "has_context", "expected"),
136
+ [
137
+ (4, 36 * 1024, 36 * 1024, False, True),
138
+ (4, 40 * 1024, 40 * 1024, False, False),
139
+ (4, 4 * 1024, 12 * 1024, True, True),
140
+ (4, 4 * 1024, 16 * 1024, True, False),
141
+ (4, 24 * 1024, 28 * 1024, True, True),
142
+ (4, 32 * 1024, 36 * 1024, True, False),
143
+ (8, 64 * 1024, 64 * 1024, False, True),
144
+ (8, 68 * 1024, 68 * 1024, False, False),
145
+ (8, 4 * 1024, 20 * 1024, True, True),
146
+ (8, 16 * 1024, 32 * 1024, True, True),
147
+ (8, 48 * 1024, 64 * 1024, True, True),
148
+ (8, 56 * 1024, 72 * 1024, True, True),
149
+ (8, 63 * 1024, 79 * 1024, True, False),
150
+ ],
151
+)
152
+def test_glm5_flashinfer_masked_mha_routing(
153
+ tensor_parallel_size, query_len, seq_len, has_context, expected
154
+):
155
+ assert (
156
+ _use_masked_mha(
157
+ backend_name="FLASHINFER_MLA_SPARSE",
158
+ tensor_parallel_size=tensor_parallel_size,
159
+ qk_head_dim=256,
160
+ v_head_dim=256,
161
+ query_len=query_len,
162
+ seq_len=seq_len,
163
+ has_context=has_context,
164
+ )
165
+ is expected
166
+ )
167
+
168
+
169
+def test_masked_mha_routing_is_dimension_specific():
170
+ assert _use_masked_mha(
171
+ backend_name="FLASHMLA_SPARSE",
172
+ tensor_parallel_size=1,
173
+ qk_head_dim=192,
174
+ v_head_dim=128,
175
+ query_len=1536,
176
+ seq_len=2048,
177
+ has_context=True,
178
+ )
179
+ assert not _use_masked_mha(
180
+ backend_name="FLASHMLA_SPARSE",
181
+ tensor_parallel_size=1,
182
+ qk_head_dim=128,
183
+ v_head_dim=128,
184
+ query_len=1536,
185
+ seq_len=2048,
186
+ has_context=True,
187
+ )
188
+
189
+
190
@pytest.mark.parametrize(
191
("cache_dtype", "expected_quant_mode"),
192
[
tests/v1/attention/test_sparse_mla_backends.py
+42
-5
@@ -21,6 +21,7 @@ from tests.v1.attention.utils import (
21
)
22
from vllm import _custom_ops as ops
23
from vllm.config import set_current_vllm_config
24
+from vllm.model_executor.layers.attention.mla_attention import _use_masked_mha
25
from vllm.model_executor.layers.attention.sparse_mla_attention import (
26
GLOBAL_TOPK_MASK_MAX_BYTES,
27
_masked_mha_workspace_fits,
@@ -884,6 +885,35 @@ def test_masked_mha_workspace_fits_single_request_boundary(max_query_len, expect
885
)
886
887
888
+@pytest.mark.parametrize(
889
+ ("backend_name", "tensor_parallel_size", "query_len"),
890
+ [
891
+ ("FLASHMLA_SPARSE", 4, 48 * 1024),
892
+ ("FLASHMLA_SPARSE", 8, 112 * 1024),
893
+ ("FLASHINFER_MLA_SPARSE", 4, 36 * 1024),
894
+ ("FLASHINFER_MLA_SPARSE", 8, 64 * 1024),
895
+ ],
896
+)
897
+def test_masked_mha_workspace_guards_long_routing_policy(
898
+ backend_name, tensor_parallel_size, query_len
899
+):
900
+ assert _use_masked_mha(
901
+ backend_name=backend_name,
902
+ tensor_parallel_size=tensor_parallel_size,
903
+ qk_head_dim=256,
904
+ v_head_dim=256,
905
+ query_len=query_len,
906
+ seq_len=query_len,
907
+ has_context=False,
908
+ )
909
+ assert not _masked_mha_workspace_fits(
910
+ batch_size=1,
911
+ max_query_len=query_len,
912
+ max_context_chunk_seq_len=0,
913
+ workspace_numel=GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
914
+ )
915
+
916
+
917
def test_masked_mha_workspace_fits_accounts_for_batch_and_context():
918
"""Request count and context chunk length are independent multipliers."""
919
base = dict(batch_size=2, max_query_len=2048, max_context_chunk_seq_len=2048)
@@ -912,14 +942,25 @@ PREFILL_BATCH_SPECS = {
942
)
943
@pytest.mark.parametrize("batch_name", list(PREFILL_BATCH_SPECS.keys()))
944
@pytest.mark.parametrize("kv_cache_dtype", ["auto"])
945
+@pytest.mark.parametrize(
946
+ ("num_heads", "qk_nope_head_dim", "qk_rope_head_dim", "v_head_dim"),
947
+ [
948
+ pytest.param(128, 128, 64, 128, id="deepseek_hd192_v128"),
949
+ pytest.param(64, 192, 64, 256, id="glm5_hd256_v256"),
950
+ ],
951
+)
952
def test_sparse_backend_prefill_correctness(
953
default_vllm_config,
954
dist_init,
955
batch_name,
956
kv_cache_dtype,
957
+ num_heads,
958
+ qk_nope_head_dim,
959
+ qk_rope_head_dim,
960
+ v_head_dim,
961
workspace_init,
962
):
922
- """Test single-pass dense and masked MHA for sparse MLA prefill."""
963
+ """Test dense and masked MHA across supported sparse MLA dimensions."""
964
backend_cls = FlashMLASparseBackend
965
batch_spec = PREFILL_BATCH_SPECS[batch_name]
966
@@ -927,11 +968,7 @@ def test_sparse_backend_prefill_correctness(
968
dtype = torch.bfloat16
969
block_size = 64
970
930
- num_heads = 128
971
kv_lora_rank = 512
932
- qk_nope_head_dim = 128
933
- qk_rope_head_dim = 64
934
- v_head_dim = 128
972
head_size = kv_lora_rank + qk_rope_head_dim
973
masked_mha = batch_name.startswith("masked_mha")
974
topk_tokens = 200 if masked_mha else 512
vllm/model_executor/layers/attention/mla_attention.py
+126
-34
@@ -827,25 +827,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
827
use_mha = True
828
829
if self.impl.is_sparse and num_mha_tokens > 0:
830
- prefill = getattr(attn_metadata, "prefill", None)
831
- use_dense_mha = getattr(prefill, "use_dense_mha", False)
832
- prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
833
- use_masked_mha = (
834
- self.prefill_backend is not None
835
- and self.impl.masked_mha_available # type: ignore[attr-defined]
836
- and self.impl.dcp_world_size <= 1
837
- and prefill is not None
838
- and _use_masked_mha(
839
- backend_name=self.attn_backend.get_name(),
840
- tensor_parallel_size=self._vllm_config.parallel_config.tensor_parallel_size,
841
- query_len=prefill.max_query_len,
842
- seq_len=prefill_max_seq_len,
843
- )
844
- and self.impl.masked_mha_workspace_fits(prefill) # type: ignore[attr-defined]
845
- )
846
- use_mha = (use_dense_mha or use_masked_mha) and not (
847
- self._vllm_config.attention_config.sparse_mla_force_mqa
848
- )
830
+ use_mha = self._use_sparse_mha(attn_metadata)
831
if not use_mha:
832
num_mqa_tokens = q.size(0)
833
num_mha_tokens = 0
@@ -1063,6 +1045,31 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1045
output_padded[num_actual_toks:].zero_()
1046
return output_padded
1047
1048
+ def _use_sparse_mha(self, attn_metadata: "MLACommonMetadata") -> bool:
1049
+ prefill = attn_metadata.prefill
1050
+ if prefill is None:
1051
+ return False
1052
+ use_masked_mha = (
1053
+ self.prefill_backend is not None
1054
+ and self.impl.masked_mha_available # type: ignore[attr-defined]
1055
+ and self.impl.dcp_world_size <= 1
1056
+ and _use_masked_mha(
1057
+ backend_name=self.attn_backend.get_name(),
1058
+ tensor_parallel_size=(
1059
+ self._vllm_config.parallel_config.tensor_parallel_size
1060
+ ),
1061
+ qk_head_dim=self.qk_nope_head_dim + self.qk_rope_head_dim,
1062
+ v_head_dim=self.v_head_dim,
1063
+ query_len=prefill.max_query_len,
1064
+ seq_len=attn_metadata.prefill_max_seq_len, # type: ignore[attr-defined]
1065
+ has_context=prefill.chunked_context is not None,
1066
+ )
1067
+ and self.impl.masked_mha_workspace_fits(prefill) # type: ignore[attr-defined]
1068
+ )
1069
+ return (prefill.use_dense_mha or use_masked_mha) and not (
1070
+ self._vllm_config.attention_config.sparse_mla_force_mqa
1071
+ )
1072
+
1073
def process_weights_after_loading(self, act_dtype: torch.dtype):
1074
# Let per-backend impls do their own weight packing first (no-op
1075
# unless overridden), mirroring Attention.process_weights_after_loading.
@@ -1619,36 +1626,121 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
1626
)
1627
1628
1622
-_DSV32_MASKED_MHA_THRESHOLDS: dict[str, dict[int, tuple[int | None, ...]]] = {
1623
- "FLASHMLA_SPARSE": {
1624
- 1: (1536, 4096, None, None, None),
1625
- 2: (512, 1024, 4096, None, None),
1626
- 4: (512, 1024, 1536, 8192, None),
1627
- 8: (512, 512, 1024, 2048, 16384),
1629
+_MaskedMHARanges = tuple[tuple[int, int], ...]
1630
+
1631
+_MASKED_MHA_THRESHOLDS: dict[
1632
+ tuple[int, int],
1633
+ dict[str, dict[int, tuple[_MaskedMHARanges, _MaskedMHARanges | None]]],
1634
+] = {
1635
+ (192, 128): {
1636
+ "FLASHMLA_SPARSE": {
1637
+ 1: (((2048, 1536), (4096, 4096)), None),
1638
+ 2: (((2048, 512), (4096, 1024), (8192, 4096)), None),
1639
+ 4: (
1640
+ ((2048, 512), (4096, 1024), (8192, 1536), (16384, 8192)),
1641
+ None,
1642
+ ),
1643
+ 8: (
1644
+ (
1645
+ (2048, 512),
1646
+ (4096, 512),
1647
+ (8192, 1024),
1648
+ (16384, 2048),
1649
+ (32768, 16384),
1650
+ ),
1651
+ None,
1652
+ ),
1653
+ },
1654
+ "FLASHINFER_MLA_SPARSE": {
1655
+ 8: (
1656
+ (
1657
+ (2048, 512),
1658
+ (4096, 1024),
1659
+ (8192, 1024),
1660
+ (16384, 2048),
1661
+ (32768, 32768),
1662
+ ),
1663
+ None,
1664
+ ),
1665
+ },
1666
},
1629
- "FLASHINFER_MLA_SPARSE": {
1630
- 8: (512, 1024, 1024, 2048, 32768),
1667
+ (256, 256): {
1668
+ "FLASHMLA_SPARSE": {
1669
+ 1: (((8 * 1024, 0),), ()),
1670
+ 2: (
1671
+ ((20 * 1024, 0),),
1672
+ ((6 * 1024, 4 * 1024), (10 * 1024, 8 * 1024)),
1673
+ ),
1674
+ 4: (
1675
+ ((48 * 1024, 0),),
1676
+ (
1677
+ (8 * 1024, 4 * 1024),
1678
+ (16 * 1024, 8 * 1024),
1679
+ (24 * 1024, 16 * 1024),
1680
+ (34 * 1024, 32 * 1024),
1681
+ ),
1682
+ ),
1683
+ 8: (
1684
+ ((112 * 1024, 0),),
1685
+ (
1686
+ (8 * 1024, 4 * 1024),
1687
+ (16 * 1024, 8 * 1024),
1688
+ (32 * 1024, 16 * 1024),
1689
+ (48 * 1024, 32 * 1024),
1690
+ ),
1691
+ ),
1692
+ },
1693
+ "FLASHINFER_MLA_SPARSE": {
1694
+ 4: (
1695
+ ((36 * 1024, 0),),
1696
+ (
1697
+ (12 * 1024, 4 * 1024),
1698
+ (16 * 1024, 8 * 1024),
1699
+ (24 * 1024, 16 * 1024),
1700
+ (28 * 1024, 24 * 1024),
1701
+ ),
1702
+ ),
1703
+ 8: (
1704
+ ((64 * 1024, 0),),
1705
+ (
1706
+ (20 * 1024, 4 * 1024),
1707
+ (24 * 1024, 8 * 1024),
1708
+ (32 * 1024, 16 * 1024),
1709
+ (48 * 1024, 32 * 1024),
1710
+ (56 * 1024, 40 * 1024),
1711
+ (64 * 1024, 48 * 1024),
1712
+ (72 * 1024, 56 * 1024),
1713
+ ),
1714
+ ),
1715
+ },
1716
},
1717
}
1633
-_DSV32_SEQ_LEN_BUCKETS = (2048, 4096, 8192, 16384, 32768)
1718
1719
1720
def _use_masked_mha(
1721
*,
1722
backend_name: str,
1723
tensor_parallel_size: int,
1724
+ qk_head_dim: int,
1725
+ v_head_dim: int,
1726
query_len: int,
1727
seq_len: int,
1728
+ has_context: bool,
1729
) -> bool:
1643
- thresholds = _DSV32_MASKED_MHA_THRESHOLDS.get(backend_name, {}).get(
1644
- tensor_parallel_size
1730
+ thresholds = (
1731
+ _MASKED_MHA_THRESHOLDS.get((qk_head_dim, v_head_dim), {})
1732
+ .get(backend_name, {})
1733
+ .get(tensor_parallel_size)
1734
)
1735
if thresholds is None:
1736
return False
1648
- for bucket_idx, bucket_seq_len in enumerate(_DSV32_SEQ_LEN_BUCKETS):
1649
- if seq_len <= bucket_seq_len:
1650
- min_query_len = thresholds[bucket_idx]
1651
- return min_query_len is not None and query_len >= min_query_len
1737
+ pure_prefill_ranges, context_ranges = thresholds
1738
+ ranges = pure_prefill_ranges
1739
+ if has_context and context_ranges is not None:
1740
+ ranges = context_ranges
1741
+ for max_seq_len, min_query_len in ranges:
1742
+ if seq_len <= max_seq_len:
1743
+ return query_len >= min_query_len
1744
return False
1745
1746
vllm/model_executor/layers/attention/sparse_mla_attention.py
+18
-7
@@ -88,16 +88,27 @@ def _is_masked_mha_available(
88
"""Check if masked MHA can ever fire for this model configuration."""
89
if not current_platform.is_device_capability_family(100):
90
return False
91
- if (
92
- num_heads_total != 128
93
- or kv_lora_rank != 512
94
- or qk_nope_head_dim != 128
95
- or qk_rope_head_dim != 64
96
- or v_head_dim != 128
91
+ model_dims = (
92
+ num_heads_total,
93
+ kv_lora_rank,
94
+ qk_nope_head_dim,
95
+ qk_rope_head_dim,
96
+ v_head_dim,
97
+ )
98
+ if model_dims not in (
99
+ (128, 512, 128, 64, 128),
100
+ (64, 512, 192, 64, 256),
101
):
102
return False
103
qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
100
- fa_version = get_flash_attn_version(head_size=qk_head_dim, head_size_v=v_head_dim)
104
+ if qk_head_dim == 256 and v_head_dim == 256:
105
+ # This path uses contiguous K/V, so it does not need the paged-KV
106
+ # features that keep head-dim 256 disabled in the general FA selector.
107
+ fa_version = get_flash_attn_version()
108
+ else:
109
+ fa_version = get_flash_attn_version(
110
+ head_size=qk_head_dim, head_size_v=v_head_dim
111
+ )
112
return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
113
114
vllm/models/deepseek_v32/attention.py
+23
-14
@@ -119,8 +119,6 @@ class DeepseekV32Attention(MLAAttention):
119
indexer: "DeepseekV32Indexer | None"
120
indexer_cls: "type[DeepseekV32Indexer]" = DeepseekV32Indexer
121
122
- supports_dense_mha_prefill = False
123
-
122
def __init__(
123
self,
124
vllm_config: VllmConfig,
@@ -303,15 +301,6 @@ class DeepseekV32Attention(MLAAttention):
301
device=hidden_states.device,
302
)
303
forward_context = get_forward_context()
306
- attn_metadata_raw = forward_context.attn_metadata
307
- if isinstance(attn_metadata_raw, dict):
308
- attn_metadata = attn_metadata_raw.get(self.layer_name)
309
- elif isinstance(attn_metadata_raw, list):
310
- attn_metadata = attn_metadata_raw[0].get(self.layer_name)
311
- else:
312
- attn_metadata = attn_metadata_raw
313
- attn_metadata = cast("MLACommonMetadata | None", attn_metadata)
314
-
304
slot_mapping = forward_context.slot_mapping
305
assert isinstance(slot_mapping, dict)
306
mla_slot = slot_mapping.get(self.layer_name)
@@ -337,7 +326,7 @@ class DeepseekV32Attention(MLAAttention):
326
indexer_softmax_scale = 0.0
327
indexer_n_head_scale = 0.0
328
340
- if attn_metadata is None or self.use_pcp:
329
+ if forward_context.attn_metadata is None or self.use_pcp:
330
mla_kv_cache = None
331
mla_k_scale = None
332
indexer_k_cache = None
@@ -346,8 +335,8 @@ class DeepseekV32Attention(MLAAttention):
335
mla_kv_cache = self.kv_cache
336
mla_k_scale = self._k_scale
337
349
- kv_c_out = torch.empty_like(kv_c) if self.use_pcp else None
350
- k_pe_out = torch.empty_like(k_pe) if self.use_pcp else None
338
+ kv_c_out = torch.empty_like(kv_c)
339
+ k_pe_out = torch.empty_like(k_pe)
340
q_c = fused_norm_rope(
341
positions,
342
q_c,
@@ -403,7 +392,10 @@ class DeepseekV32Attention(MLAAttention):
392
)
393
394
self._sparse_indexer_and_attn(
395
+ positions,
396
q_c,
397
+ q_nope,
398
+ q_pe,
399
index_q_fp8,
400
index_k_out,
401
index_weights_out,
@@ -418,7 +410,10 @@ class DeepseekV32Attention(MLAAttention):
410
@eager_break_during_capture
411
def _sparse_indexer_and_attn(
412
self,
413
+ positions: torch.Tensor,
414
q_c: torch.Tensor,
415
+ q_nope: torch.Tensor,
416
+ q_pe: torch.Tensor,
417
index_q_fp8: torch.Tensor | None,
418
index_k: torch.Tensor | None,
419
index_weights_out: torch.Tensor | None,
@@ -498,6 +493,20 @@ class DeepseekV32Attention(MLAAttention):
493
output.zero_()
494
return
495
496
+ if self._use_sparse_mha(attn_metadata):
497
+ assert kv_c is not None and k_pe is not None
498
+ mha_q_pe = self.rotary_emb(positions, q_pe)[0] if self._fp8_query else mqa_q
499
+ mha_q = torch.cat((q_nope, mha_q_pe), dim=-1)
500
+ self.forward_impl(
501
+ mha_q,
502
+ kv_c,
503
+ k_pe.unsqueeze(1),
504
+ kv_cache,
505
+ attn_metadata,
506
+ output,
507
+ )
508
+ return
509
+
510
if self._fp8_kv_needs_view:
511
kv_cache = kv_cache.view(torch.float8_e4m3fn)
512
if self._fp8_query: