[2/N][Attention] Enable masked MHA for sparse MLA prefills (#48770)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
Matthew Bonanni committed
Jul 31, 2026 at 09:01 UTC
82ae4164ee016d4daecd2033c26f5c0827984a80
11 files changed
+855
-26
benchmarks/attention_benchmarks/benchmark.py
+17
-1
@@ -617,7 +617,7 @@ def main():
617
"--sparse-mla-mha-variants",
618
nargs="+",
619
default=None,
620
- choices=["dense_mha", "mqa"],
620
+ choices=["dense_mha", "masked_mha", "mqa"],
621
help="Sparse MLA variants to run in mha_vs_mqa mode. Defaults to both.",
622
)
623
@@ -748,6 +748,9 @@ def main():
748
args.sparse_mla_mha_variants = yaml_config.get(
749
"sparse_mla_mha_variants", args.sparse_mla_mha_variants
750
)
751
+ args.sparse_mla_masked_mha_max_seq_len = yaml_config.get(
752
+ "sparse_mla_masked_mha_max_seq_len", None
753
+ )
754
755
# Parameter sweep configuration
756
if "parameter_sweep" in yaml_config:
@@ -1119,6 +1122,7 @@ def main():
1122
console.print(f"Prefill backend: {prefill_backend}")
1123
available_variants = [
1124
("dense_mha", False, "dense"),
1125
+ ("masked_mha", False, "masked"),
1126
("mqa", True, "auto"),
1127
]
1128
requested_variants = getattr(args, "sparse_mla_mha_variants", None)
@@ -1139,6 +1143,9 @@ def main():
1143
]
1144
else:
1145
variants = available_variants
1146
+ masked_mha_max_seq_len = getattr(
1147
+ args, "sparse_mla_masked_mha_max_seq_len", None
1148
+ )
1149
formatter = ResultsFormatter(console)
1150
total = 0
1151
for spec in args.batch_specs:
@@ -1148,6 +1155,10 @@ def main():
1155
variant_label == "dense_mha"
1156
and dense_mha_max_seq_len is not None
1157
and q_len > dense_mha_max_seq_len
1158
+ ) or (
1159
+ variant_label == "masked_mha"
1160
+ and masked_mha_max_seq_len is not None
1161
+ and q_len > masked_mha_max_seq_len
1162
):
1163
continue
1164
total += len(backends)
@@ -1161,6 +1172,10 @@ def main():
1172
variant_label == "dense_mha"
1173
and dense_mha_max_seq_len is not None
1174
and q_len > dense_mha_max_seq_len
1175
+ ) or (
1176
+ variant_label == "masked_mha"
1177
+ and masked_mha_max_seq_len is not None
1178
+ and q_len > masked_mha_max_seq_len
1179
):
1180
continue
1181
config = BenchmarkConfig(
@@ -1190,6 +1205,7 @@ def main():
1205
sparse_mla_dense_mha_max_seq_len=dense_mha_max_seq_len,
1206
sparse_mla_topk_pattern=sparse_mla_topk_pattern,
1207
prefill_backend=prefill_backend,
1208
+ sparse_mla_masked_mha_max_seq_len=masked_mha_max_seq_len,
1209
)
1210
1211
# run_mla_benchmark needs the real backend name
benchmarks/attention_benchmarks/common.py
+2
-1
@@ -299,8 +299,9 @@ class BenchmarkConfig:
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"
302
+ sparse_mla_mha_mode: str = "auto" # "auto", "dense", or "masked"
303
sparse_mla_dense_mha_max_seq_len: int | None = None
304
+ sparse_mla_masked_mha_max_seq_len: int | None = None
305
sparse_mla_topk_pattern: str = "random" # "random", "prefix", "sliding_window"
306
num_splits: int | None = None # FlashAttention split-K (0=auto, 1=disabled)
307
benchmarks/attention_benchmarks/configs/mla_sparse_masked_mha_vs_mqa.yaml
new
+123
@@ -0,0 +1,123 @@
1
+# Sparse MLA benchmark: forward_mha vs forward_mqa crossover plot
2
+#
3
+# Dense pure-prefill sweep to generate a smooth speedup curve. The 100 points
4
+# run from q32 through q262144, approximately even on the log2 scale, and include
5
+# routing boundaries plus extra samples around the 6k-8k crossover region.
6
+# Dense MHA is only benchmarked through q32768 because it dominates runtime above
7
+# that range.
8
+#
9
+# Usage:
10
+# python benchmark.py --config configs/mla_sparse_mha_vs_mqa.yaml
11
+
12
+mode: mha_vs_mqa
13
+
14
+model:
15
+ name: "deepseek-v3"
16
+ num_layers: 60
17
+ num_q_heads: 16
18
+ num_kv_heads: 1
19
+ head_dim: 576
20
+ kv_lora_rank: 512
21
+ qk_nope_head_dim: 128
22
+ qk_rope_head_dim: 64
23
+ v_head_dim: 128
24
+ block_size: 128
25
+ max_model_len: 262144
26
+
27
+batch_specs:
28
+ - "q32"
29
+ - "q36"
30
+ - "q40"
31
+ - "q45"
32
+ - "q50"
33
+ - "q56"
34
+ - "q62"
35
+ - "q70"
36
+ - "q78"
37
+ - "q87"
38
+ - "q97"
39
+ - "q109"
40
+ - "q122"
41
+ - "q136"
42
+ - "q152"
43
+ - "q170"
44
+ - "q190"
45
+ - "q212"
46
+ - "q237"
47
+ - "q255"
48
+ - "q256"
49
+ - "q257"
50
+ - "q265"
51
+ - "q296"
52
+ - "q331"
53
+ - "q370"
54
+ - "q413"
55
+ - "q462"
56
+ - "q516"
57
+ - "q577"
58
+ - "q645"
59
+ - "q721"
60
+ - "q806"
61
+ - "q901"
62
+ - "q1007"
63
+ - "q1125"
64
+ - "q1257"
65
+ - "q1405"
66
+ - "q1571"
67
+ - "q1756"
68
+ - "q1962"
69
+ - "q2048"
70
+ - "q2049"
71
+ - "q2193"
72
+ - "q2451"
73
+ - "q2740"
74
+ - "q3062"
75
+ - "q3422"
76
+ - "q3825"
77
+ - "q4275"
78
+ - "q4778"
79
+ - "q5340"
80
+ - "q5854"
81
+ - "q5969"
82
+ - "q6000"
83
+ - "q6144"
84
+ - "q6367"
85
+ - "q6671"
86
+ - "q6925"
87
+ - "q7168"
88
+ - "q7456"
89
+ - "q7532"
90
+ - "q8192"
91
+ - "q8193"
92
+ - "q8333"
93
+ - "q9314"
94
+ - "q10410"
95
+ - "q11635"
96
+ - "q13004"
97
+ - "q14534"
98
+ - "q16244"
99
+ - "q18156"
100
+ - "q20292"
101
+ - "q22680"
102
+ - "q25349"
103
+ - "q28332"
104
+ - "q31665"
105
+ - "q32768"
106
+ - "q32769"
107
+ - "q35391"
108
+ - "q39556"
109
+ - "q44210"
110
+ - "q49413"
111
+ - "q55227"
112
+ - "q61726"
113
+ - "q65536"
114
+
115
+backends:
116
+ - FLASHMLA_SPARSE
117
+
118
+device: "cuda:0"
119
+profile_memory: true
120
+sparse_mla_dense_mha_max_seq_len: 25349
121
+sparse_mla_masked_mha_max_seq_len: 8192
122
+sparse_mla_mha_variants: ["dense_mha", "masked_mha", "mqa"]
123
+sparse_mla_topk_pattern: "random"
benchmarks/attention_benchmarks/mla_runner.py
+7
@@ -931,10 +931,17 @@ def _run_single_benchmark(
931
prefill_quant_op = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
932
933
fused_output = output_scale is not None and fuse_quant_op
934
+ mha_mode = getattr(config, "sparse_mla_mha_mode", "auto")
935
936
# Build forward function (runs a single decode/prefill pass)
937
def forward_fn():
938
results = []
939
+ impl._sparse_mla_force_masked_mha = ( # type: ignore[attr-defined]
940
+ mha_mode == "masked"
941
+ )
942
+ impl._sparse_mla_force_dense_mha = ( # type: ignore[attr-defined]
943
+ mha_mode == "dense"
944
+ )
945
if has_decode:
946
results.append(impl.forward_mqa(decode_inputs, kv_cache, metadata, layer))
947
if has_prefill:
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 ed4b7342bc8f0489dd9b649d5288867e35fc6a32
42
+ GIT_TAG ec4a882d70b19235c824848db944d84a6cb8f9ee
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/v1/attention/test_sparse_mla_backends.py
+26
-6
@@ -862,6 +862,8 @@ def test_split_prefill_chunks(seq_lens, max_buf, expected):
862
PREFILL_BATCH_SPECS = {
863
"short_dense_mha": BatchSpec(seq_lens=[64, 128], query_lens=[64, 128]),
864
"short_context_dense_mha": BatchSpec(seq_lens=[128, 160], query_lens=[64, 32]),
865
+ "masked_mha": BatchSpec(seq_lens=[256], query_lens=[256]),
866
+ "masked_mha_chunked_context": BatchSpec(seq_lens=[448, 384], query_lens=[256, 256]),
867
}
868
869
@@ -878,7 +880,7 @@ def test_sparse_backend_prefill_correctness(
880
kv_cache_dtype,
881
workspace_init,
882
):
881
- """Test single-pass FA4 dense forward_mha for sparse MLA prefill."""
883
+ """Test single-pass dense and masked MHA for sparse MLA prefill."""
884
backend_cls = FlashMLASparseBackend
885
batch_spec = PREFILL_BATCH_SPECS[batch_name]
886
@@ -892,7 +894,8 @@ def test_sparse_backend_prefill_correctness(
894
qk_rope_head_dim = 64
895
v_head_dim = 128
896
head_size = kv_lora_rank + qk_rope_head_dim
895
- topk_tokens = 512
897
+ masked_mha = batch_name.startswith("masked_mha")
898
+ topk_tokens = 200 if masked_mha else 512
899
900
max_seqlen = max(batch_spec.seq_lens)
901
total_cache_tokens = sum(batch_spec.seq_lens)
@@ -918,6 +921,7 @@ def test_sparse_backend_prefill_correctness(
921
model_type="deepseek_v2",
922
)
923
model_config.dtype = dtype
924
+ model_config.model_arch_config.total_num_attention_heads = num_heads
925
model_config.get_num_attention_heads = MethodType(
926
lambda self, parallel_config: num_heads, model_config
927
)
@@ -942,13 +946,14 @@ def test_sparse_backend_prefill_correctness(
946
947
# Compute dense reference outputs.
948
total_query_tokens = sum(query_lens)
945
- sparse_indices = torch.zeros(
946
- total_query_tokens, topk_tokens, dtype=torch.int32, device=device
949
+ sparse_indices = torch.full(
950
+ (total_query_tokens, topk_tokens), -1, dtype=torch.int32, device=device
951
)
952
953
all_q, all_kv_c_new, all_k_pe_new = [], [], []
954
kv_c_contexts, k_pe_contexts = [], []
955
reference_outputs = []
956
+ global_token_idx = 0
957
958
for i in range(batch_spec.batch_size):
959
s_len = seq_lens[i]
@@ -981,8 +986,15 @@ def test_sparse_backend_prefill_correctness(
986
for j in range(q_len):
987
attend_end = ctx_len + j + 1
988
q_tok = q_mha[j : j + 1] # (1, H, D_qk)
984
- k_attend = k_all[:attend_end] # (N, H, D_qk)
985
- v_attend = v_all[:attend_end] # (N, H, D_v)
989
+ if masked_mha:
990
+ actual_topk = min(topk_tokens, attend_end)
991
+ attend_indices = torch.randperm(attend_end, device=device)[:actual_topk]
992
+ sparse_indices[global_token_idx, :actual_topk] = attend_indices
993
+ k_attend = k_all[attend_indices]
994
+ v_attend = v_all[attend_indices]
995
+ else:
996
+ k_attend = k_all[:attend_end] # (N, H, D_qk)
997
+ v_attend = v_all[:attend_end] # (N, H, D_v)
998
999
q_sdpa = q_tok.unsqueeze(0).transpose(1, 2).float()
1000
k_sdpa = k_attend.unsqueeze(0).transpose(1, 2).float()
@@ -993,6 +1005,7 @@ def test_sparse_backend_prefill_correctness(
1005
)
1006
out = out.transpose(1, 2).squeeze(0) # (1, H, D_v)
1007
reference_outputs.append(out.to(dtype).flatten(start_dim=-2))
1008
+ global_token_idx += 1
1009
1010
all_q.append(q_mha)
1011
all_kv_c_new.append(kv_c_full[ctx_len:])
@@ -1047,6 +1060,13 @@ def test_sparse_backend_prefill_correctness(
1060
1061
builder_cls = backend_cls.get_builder_cls()
1062
builder = builder_cls(kv_cache_spec, ["placeholder"], vllm_config, device)
1063
+ if batch_name == "masked_mha_chunked_context":
1064
+ builder.chunked_prefill_workspace_size = block_size * batch_spec.batch_size
1065
+ builder.chunked_prefill_workspace = torch.empty(
1066
+ (builder.chunked_prefill_workspace_size, head_size),
1067
+ dtype=dtype,
1068
+ device=device,
1069
+ )
1070
# Drive the queries through the dense-MHA prefill path directly (the routing
1071
# threshold would otherwise classify these short queries as MQA decodes).
1072
builder.reorder_batch_threshold = 1
tests/v1/attention/test_sparse_mla_mask.py
new
+29
@@ -0,0 +1,29 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+import torch
6
+
7
+pytest.importorskip("cutlass")
8
+
9
+from vllm.model_executor.layers.attention.sparse_mla_attention import (
10
+ _build_topk_mask,
11
+)
12
+
13
+
14
+@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
15
+def test_build_topk_mask_single_request_matches_generic_path() -> None:
16
+ topk = torch.tensor(
17
+ [[0, 31, 32, 63, -1], [1, 64, 127, -1, -1]],
18
+ dtype=torch.int32,
19
+ device="cuda",
20
+ )
21
+
22
+ num_words = (128 + 31) // 32
23
+ single_out = torch.zeros(1, 2, num_words, dtype=torch.int32, device="cuda")
24
+ generic_out = torch.zeros(2, 1, num_words, dtype=torch.int32, device="cuda")
25
+ single_req = _build_topk_mask([topk], [2], 2, 128, single_out)
26
+ generic = _build_topk_mask([topk[:1], topk[1:]], [1, 1], 1, 128, generic_out)
27
+
28
+ torch.testing.assert_close(single_req[0, 0], generic[0, 0])
29
+ torch.testing.assert_close(single_req[0, 1], generic[1, 0])
vllm/model_executor/layers/attention/mla_attention.py
+62
-5
@@ -772,8 +772,25 @@ class MLAAttention(nn.Module, AttentionLayerBase):
772
num_mha_tokens = q.size(0) - num_mqa_tokens
773
774
if self.impl.is_sparse and num_mha_tokens > 0:
775
- prefill_metadata = getattr(attn_metadata, "prefill", None)
776
- if not getattr(prefill_metadata, "use_dense_mha", False):
775
+ prefill = getattr(attn_metadata, "prefill", None)
776
+ use_dense_mha = getattr(prefill, "use_dense_mha", False)
777
+ prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
778
+ use_masked_mha = (
779
+ self.prefill_backend is not None
780
+ and self.impl.masked_mha_available # type: ignore[attr-defined]
781
+ and self.impl.dcp_world_size <= 1
782
+ and prefill is not None
783
+ and _use_masked_mha(
784
+ backend_name=self.attn_backend.get_name(),
785
+ tensor_parallel_size=self._vllm_config.parallel_config.tensor_parallel_size,
786
+ query_len=prefill.max_query_len,
787
+ seq_len=prefill_max_seq_len,
788
+ )
789
+ )
790
+ use_mha = (use_dense_mha or use_masked_mha) and not (
791
+ self._vllm_config.attention_config.sparse_mla_force_mqa
792
+ )
793
+ if not use_mha:
794
num_mqa_tokens = q.size(0)
795
num_mha_tokens = 0
796
@@ -781,6 +798,11 @@ class MLAAttention(nn.Module, AttentionLayerBase):
798
quant_key is not None
799
and self.prefill_backend is not None
800
and self.prefill_backend.supports_quant_output(quant_key)
801
+ and (
802
+ not self.impl.is_sparse
803
+ or attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
804
+ <= attn_metadata.topk_tokens # type: ignore[attr-defined]
805
+ )
806
and attn_metadata is not None
807
and attn_metadata.prefill is not None
808
and attn_metadata.prefill.chunked_context is None
@@ -1387,6 +1409,7 @@ class MLACommonPrefillMetadata:
1409
seq_tot: list[int]
1410
max_seq_lens: list[int]
1411
seq_lens: torch.Tensor
1412
+ context_lens: torch.Tensor
1413
workspace: torch.Tensor
1414
token_to_seq: torch.Tensor
1415
chunk_total_token: list[int]
@@ -1408,10 +1431,9 @@ class MLACommonPrefillMetadata:
1431
q_data_type: torch.dtype | None = None
1432
output_dtype: torch.dtype | None = None
1433
prefill_backend: MLAPrefillBackend | None = None
1411
- # Whether the prefill suffix is routed through dense MHA.
1412
- # Indexer scoring may be skipped only for a pure-prefill batch,
1413
- # since decode tokens still consume top-k indices.
1434
+ query_lens_cpu: torch.Tensor | None = None
1435
use_dense_mha: bool = False
1436
+ topk_mask_workspace: torch.Tensor | None = None
1437
1438
1439
@dataclass
@@ -1508,6 +1530,39 @@ def get_mla_dims(model_config: ModelConfig) -> MLADims:
1530
)
1531
1532
1533
+_DSV32_MASKED_MHA_THRESHOLDS: dict[str, dict[int, tuple[int | None, ...]]] = {
1534
+ "FLASHMLA_SPARSE": {
1535
+ 1: (1536, 4096, None, None, None),
1536
+ 2: (512, 1024, 4096, None, None),
1537
+ 4: (512, 1024, 1536, 8192, None),
1538
+ 8: (512, 512, 1024, 2048, 16384),
1539
+ },
1540
+ "FLASHINFER_MLA_SPARSE": {
1541
+ 8: (512, 1024, 1024, 2048, 32768),
1542
+ },
1543
+}
1544
+_DSV32_SEQ_LEN_BUCKETS = (2048, 4096, 8192, 16384, 32768)
1545
+
1546
+
1547
+def _use_masked_mha(
1548
+ *,
1549
+ backend_name: str,
1550
+ tensor_parallel_size: int,
1551
+ query_len: int,
1552
+ seq_len: int,
1553
+) -> bool:
1554
+ thresholds = _DSV32_MASKED_MHA_THRESHOLDS.get(backend_name, {}).get(
1555
+ tensor_parallel_size
1556
+ )
1557
+ if thresholds is None:
1558
+ return False
1559
+ for bucket_idx, bucket_seq_len in enumerate(_DSV32_SEQ_LEN_BUCKETS):
1560
+ if seq_len <= bucket_seq_len:
1561
+ min_query_len = thresholds[bucket_idx]
1562
+ return min_query_len is not None and query_len >= min_query_len
1563
+ return False
1564
+
1565
+
1566
@functools.cache
1567
def backend_supports_prefill_query_quantization() -> bool:
1568
"""Check if the selected MLA prefill backend supports query quantization.
@@ -1682,6 +1737,7 @@ def build_mla_chunked_context_metadata(
1737
seq_tot=padded_local_chunk_seq_lens.sum(dim=1).tolist(),
1738
max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1739
seq_lens=chunk_seq_lens,
1740
+ context_lens=context_lens_cpu.to(device, non_blocking=True),
1741
token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
1742
chunk_total_token=chunk_total_token.tolist(),
1743
workspace=chunked_prefill_workspace,
@@ -1705,6 +1761,7 @@ def build_mla_chunked_context_metadata(
1761
seq_tot=chunk_seq_lens.sum(dim=1).tolist(),
1762
max_seq_lens=chunk_seq_lens.max(dim=1).values.tolist(),
1763
seq_lens=chunk_seq_lens,
1764
+ context_lens=context_lens_cpu.to(device, non_blocking=True),
1765
token_to_seq=token_to_seq_cpu.to(device, non_blocking=True),
1766
chunk_total_token=chunk_total_token,
1767
workspace=chunked_prefill_workspace,
vllm/model_executor/layers/attention/sparse_mla_attention.py
+519
-12
@@ -1,29 +1,34 @@
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."""
3
+"""Shared MHA implementation and metadata builder for sparse MLA backends."""
4
5
from shutil import which
6
-from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar
6
+from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar, cast
7
8
import numpy as np
9
import torch
10
11
-from vllm.distributed import get_dcp_group
11
+from vllm import _custom_ops as ops
12
+from vllm.distributed import (
13
+ get_dcp_group,
14
+ get_tensor_model_parallel_world_size,
15
+)
16
from vllm.logger import init_logger
17
from vllm.model_executor.layers.attention.mla_attention import (
18
MLACommonBaseImpl,
19
+ MLACommonMetadata,
20
MLACommonPrefillMetadata,
21
build_mla_chunked_context_metadata,
22
get_mla_dims,
23
)
24
from vllm.platforms import current_platform
25
+from vllm.triton_utils import tl, triton
26
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
-)
27
+from vllm.utils.torch_utils import is_quantized_kv_cache, np_to_pinned_tensor
28
+from vllm.v1.attention.backend import AttentionMetadata, AttentionMetadataBuilder
29
+from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
30
from vllm.v1.attention.backends.utils import split_decodes_and_prefills
31
+from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
32
33
if TYPE_CHECKING:
34
from vllm.config import VllmConfig
@@ -35,6 +40,32 @@ logger = init_logger(__name__)
40
41
T = TypeVar("T", bound=AttentionMetadata)
42
43
+GLOBAL_TOPK_MASK_MAX_BYTES = 64 * 1024 * 1024 # 64 MiB
44
+
45
+
46
+def _is_masked_mha_available(
47
+ num_heads_total: int,
48
+ kv_lora_rank: int,
49
+ qk_nope_head_dim: int,
50
+ qk_rope_head_dim: int,
51
+ v_head_dim: int,
52
+ kv_cache_dtype: str,
53
+) -> bool:
54
+ """Check if masked MHA can ever fire for this model configuration."""
55
+ if not current_platform.is_device_capability_family(100):
56
+ return False
57
+ if (
58
+ num_heads_total != 128
59
+ or kv_lora_rank != 512
60
+ or qk_nope_head_dim != 128
61
+ or qk_rope_head_dim != 64
62
+ or v_head_dim != 128
63
+ ):
64
+ return False
65
+ qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
66
+ fa_version = get_flash_attn_version(head_size=qk_head_dim, head_size_v=v_head_dim)
67
+ return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
68
+
69
70
class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
71
metadata_cls: type[T]
@@ -86,6 +117,20 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
117
dtype=self.model_config.dtype,
118
device=device,
119
)
120
+ self.topk_mask_workspace: torch.Tensor | None = None
121
+ if _is_masked_mha_available(
122
+ self.model_config.model_arch_config.total_num_attention_heads,
123
+ self.mla_dims.kv_lora_rank,
124
+ self.mla_dims.qk_nope_head_dim,
125
+ self.mla_dims.qk_rope_head_dim,
126
+ self.mla_dims.v_head_dim,
127
+ vllm_config.cache_config.cache_dtype,
128
+ ):
129
+ self.topk_mask_workspace = torch.zeros(
130
+ GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
131
+ dtype=torch.int32,
132
+ device=device,
133
+ )
134
layer_prefill_backend = vllm_config.compilation_config.static_forward_context[
135
layer_names[0]
136
].prefill_backend
@@ -194,6 +239,7 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
239
block_table=common_attn_metadata.block_table_tensor[num_decodes:, ...],
240
query_start_loc=prefill_query_start_loc,
241
max_query_len=prefill_max_query_len,
242
+ query_lens_cpu=prefill_query_lens_cpu,
243
chunked_context=self._build_chunked_context_fields(
244
common_attn_metadata,
245
num_decodes,
@@ -207,6 +253,7 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
253
prefill_max_seq_len <= self.topk_tokens
254
and not self.vllm_config.attention_config.sparse_mla_force_mqa
255
),
256
+ topk_mask_workspace=self.topk_mask_workspace,
257
)
258
self._prefill_backend.prepare_metadata(prefill)
259
@@ -260,9 +307,139 @@ class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
307
)
308
309
310
+@triton.jit
311
+def _scatter_topk_kernel(
312
+ mask_ptr,
313
+ topk_ptr,
314
+ cu_q_lens_ptr,
315
+ num_words: tl.constexpr,
316
+ mask_row_stride: tl.constexpr,
317
+ num_topk: tl.constexpr,
318
+ topk_stride: tl.constexpr,
319
+ max_q_len: tl.constexpr,
320
+ BLOCK_TOPK: tl.constexpr,
321
+ BLOCK_WORDS: tl.constexpr,
322
+):
323
+ row_idx = tl.program_id(0)
324
+ row_ptr = mask_ptr + row_idx * mask_row_stride
325
+ word_offsets = tl.arange(0, BLOCK_WORDS)
326
+ tl.store(
327
+ row_ptr + word_offsets,
328
+ tl.zeros([BLOCK_WORDS], dtype=tl.int32),
329
+ mask=word_offsets < num_words,
330
+ )
331
+
332
+ req_idx = row_idx // max_q_len
333
+ q_local = row_idx % max_q_len
334
+ q_start = tl.load(cu_q_lens_ptr + req_idx)
335
+ q_len = tl.load(cu_q_lens_ptr + req_idx + 1) - q_start
336
+ if q_local < q_len:
337
+ src_row = q_start + q_local
338
+ offsets = tl.arange(0, BLOCK_TOPK)
339
+ in_range = offsets < num_topk
340
+ indices = tl.load(
341
+ topk_ptr + src_row * topk_stride + offsets,
342
+ mask=in_range,
343
+ other=-1,
344
+ )
345
+ valid = in_range & (indices >= 0)
346
+ word_indices = indices >> 5
347
+ bits = (1 << (indices & 31)).to(tl.int32)
348
+ tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
349
+
350
+
351
+@triton.jit
352
+def _scatter_topk_single_req_kernel(
353
+ mask_ptr,
354
+ topk_ptr,
355
+ num_words: tl.constexpr,
356
+ mask_row_stride: tl.constexpr,
357
+ num_topk: tl.constexpr,
358
+ topk_stride: tl.constexpr,
359
+ total_q: tl.constexpr,
360
+ BLOCK_TOPK: tl.constexpr,
361
+ BLOCK_WORDS: tl.constexpr,
362
+):
363
+ row_idx = tl.program_id(0)
364
+ row_ptr = mask_ptr + row_idx * mask_row_stride
365
+ word_offsets = tl.arange(0, BLOCK_WORDS)
366
+ tl.store(
367
+ row_ptr + word_offsets,
368
+ tl.zeros([BLOCK_WORDS], dtype=tl.int32),
369
+ mask=word_offsets < num_words,
370
+ )
371
+ if row_idx < total_q:
372
+ offsets = tl.arange(0, BLOCK_TOPK)
373
+ in_range = offsets < num_topk
374
+ indices = tl.load(
375
+ topk_ptr + row_idx * topk_stride + offsets,
376
+ mask=in_range,
377
+ other=-1,
378
+ )
379
+ valid = in_range & (indices >= 0)
380
+ word_indices = indices >> 5
381
+ bits = (1 << (indices & 31)).to(tl.int32)
382
+ tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
383
+
384
+
385
+def _build_topk_mask(
386
+ topk_indices_per_req: list[torch.Tensor],
387
+ q_lens: list[int],
388
+ max_q_len: int,
389
+ max_seq_len: int,
390
+ out: torch.Tensor,
391
+) -> torch.Tensor:
392
+ """Build a bit-packed top-k mask into ``out[:B, :max_Q, :num_words]``."""
393
+ batch_size = len(q_lens)
394
+ num_words = (max_seq_len + 31) // 32
395
+ total_rows = batch_size * max_q_len
396
+ if total_rows == 0:
397
+ return out[:batch_size, :max_q_len, :num_words]
398
+
399
+ total_q = sum(q_lens)
400
+ mask_row_stride = out.stride(-2)
401
+ block_words = triton.next_power_of_2(num_words)
402
+
403
+ if batch_size == 1:
404
+ topk_packed = topk_indices_per_req[0]
405
+ num_topk = topk_packed.shape[1]
406
+ _scatter_topk_single_req_kernel[(max_q_len,)](
407
+ out,
408
+ topk_packed,
409
+ num_words=num_words,
410
+ mask_row_stride=mask_row_stride,
411
+ num_topk=num_topk,
412
+ topk_stride=topk_packed.stride(0),
413
+ total_q=total_q,
414
+ BLOCK_TOPK=triton.next_power_of_2(num_topk),
415
+ BLOCK_WORDS=block_words,
416
+ )
417
+ return out[:1, :max_q_len, :num_words]
418
+
419
+ topk_packed = torch.cat(topk_indices_per_req, dim=0)
420
+ num_topk = topk_packed.shape[1]
421
+ q_lens_tensor = np_to_pinned_tensor(np.asarray(q_lens, dtype=np.int32)).to(
422
+ out.device, non_blocking=True
423
+ )
424
+ cu_q_lens = out.new_zeros(batch_size + 1)
425
+ torch.cumsum(q_lens_tensor, dim=0, out=cu_q_lens[1:])
426
+ _scatter_topk_kernel[(total_rows,)](
427
+ out,
428
+ topk_packed,
429
+ cu_q_lens,
430
+ num_words=num_words,
431
+ mask_row_stride=mask_row_stride,
432
+ num_topk=num_topk,
433
+ topk_stride=topk_packed.stride(0),
434
+ max_q_len=max_q_len,
435
+ BLOCK_TOPK=triton.next_power_of_2(num_topk),
436
+ BLOCK_WORDS=block_words,
437
+ )
438
+ return out[:batch_size, :max_q_len, :num_words]
439
+
440
+
441
class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
264
- """Sparse MLA base: shared dense-MHA prefill (from MLACommonBaseImpl) plus the
265
- sparse top-k MQA decode path. Subclasses implement forward_mqa."""
442
+ """Sparse MLA base with dense and masked-MHA prefill paths."""
443
444
is_sparse = True
445
@@ -278,7 +455,6 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
455
logits_soft_cap: float | None,
456
attn_type: str,
457
kv_sharing_target_layer_name: str | None,
281
- # MLA-specific
458
q_lora_rank: int | None,
459
kv_lora_rank: int,
460
qk_nope_head_dim: int,
@@ -312,7 +488,6 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
488
if indexer is not None
489
else topk_indices_buffer
490
)
315
-
491
self._use_flashinfer_concat_mla_k = (
492
has_flashinfer()
493
and which("ninja") is not None
@@ -320,3 +495,335 @@ class SparseMLACommonImpl(MLACommonBaseImpl[T], Generic[T]):
495
and (self.qk_nope_head_dim == 128)
496
and (self.qk_rope_head_dim == 64)
497
)
498
+ self.masked_mha_available = _is_masked_mha_available(
499
+ num_heads_total=num_heads * get_tensor_model_parallel_world_size(),
500
+ kv_lora_rank=kv_lora_rank,
501
+ qk_nope_head_dim=qk_nope_head_dim,
502
+ qk_rope_head_dim=qk_rope_head_dim,
503
+ v_head_dim=v_head_dim,
504
+ kv_cache_dtype=kv_cache_dtype,
505
+ )
506
+
507
+ @staticmethod
508
+ def _slice_topk_per_req(
509
+ topk_all: torch.Tensor,
510
+ q_lens: list[int],
511
+ ) -> list[torch.Tensor]:
512
+ topk_per_req = []
513
+ offset = 0
514
+ for q_len in q_lens:
515
+ topk_per_req.append(topk_all[offset : offset + q_len])
516
+ offset += q_len
517
+ return topk_per_req
518
+
519
+ @staticmethod
520
+ def _remap_topk_to_ranges(
521
+ topk_per_req: list[torch.Tensor],
522
+ range_starts: list[int] | torch.Tensor,
523
+ range_lens: list[int],
524
+ ) -> list[torch.Tensor]:
525
+ remapped = []
526
+ for topk, start, length in zip(topk_per_req, range_starts, range_lens):
527
+ valid = (topk >= start) & (topk < start + length)
528
+ remapped.append(torch.where(valid, topk - start, -1))
529
+ return remapped
530
+
531
+ def _project_kv(
532
+ self, kv_c_normed: torch.Tensor, k_pe: torch.Tensor
533
+ ) -> tuple[torch.Tensor, torch.Tensor]:
534
+ kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
535
+ -1,
536
+ self.num_heads,
537
+ self.qk_nope_head_dim + self.v_head_dim,
538
+ )
539
+ k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
540
+ return self._concat_k_nope_k_pe(k_nope, k_pe), v
541
+
542
+ @staticmethod
543
+ def _try_build_global_mask(
544
+ topk_per_req: list[torch.Tensor],
545
+ q_lens: list[int],
546
+ max_query_len: int,
547
+ max_seq_len: int,
548
+ topk_mask_workspace: torch.Tensor,
549
+ ) -> torch.Tensor | None:
550
+ """Build a full-sequence top-k mask if it fits within the budget.
551
+
552
+ When the mask fits, it is reused across the suffix and all context
553
+ chunks, avoiding per-chunk mask rebuilds. Returns None when the
554
+ mask is too large, signalling the caller to fall back to per-chunk
555
+ index remapping.
556
+ """
557
+ batch_size = len(q_lens)
558
+ tile_m = 128 if max_query_len <= 128 else 256
559
+ padded_q_len = triton.cdiv(max_query_len, tile_m) * tile_m
560
+ num_words_padded = (max_seq_len + 31) // 32 + 1
561
+ needed = batch_size * padded_q_len * num_words_padded
562
+ if needed * torch.int32.itemsize > GLOBAL_TOPK_MASK_MAX_BYTES:
563
+ return None
564
+
565
+ mask = topk_mask_workspace[:needed].view(
566
+ batch_size, padded_q_len, num_words_padded
567
+ )
568
+ _build_topk_mask(
569
+ topk_per_req,
570
+ q_lens,
571
+ padded_q_len,
572
+ max_seq_len,
573
+ mask,
574
+ )
575
+ return mask
576
+
577
+ def _run_masked_mha(
578
+ self,
579
+ q: torch.Tensor,
580
+ k: torch.Tensor,
581
+ v: torch.Tensor,
582
+ cu_seqlens_q: torch.Tensor,
583
+ cu_seqlens_k: torch.Tensor,
584
+ max_seqlen_q: int,
585
+ max_seqlen_k: int,
586
+ topk_per_req: list[torch.Tensor],
587
+ q_lens: list[int],
588
+ causal: bool,
589
+ return_softmax_lse: bool = False,
590
+ dense_mask: torch.Tensor | None = None,
591
+ key_starts: torch.Tensor | None = None,
592
+ topk_mask_workspace: torch.Tensor | None = None,
593
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
594
+ from vllm.model_executor.layers.attention.sparse_mla_mask import (
595
+ dense_mask_mod,
596
+ offset_dense_mask_mod,
597
+ )
598
+ from vllm.vllm_flash_attn import flash_attn_varlen_func
599
+
600
+ tile_m = 128 if max_seqlen_q <= 128 else 256
601
+ padded_q_len = triton.cdiv(max_seqlen_q, tile_m) * tile_m
602
+ if dense_mask is None:
603
+ batch_size = len(q_lens)
604
+ num_words = (max_seqlen_k + 31) // 32
605
+ assert topk_mask_workspace is not None
606
+ workspace_3d = topk_mask_workspace[
607
+ : batch_size * padded_q_len * num_words
608
+ ].view(batch_size, padded_q_len, num_words)
609
+ dense_mask = _build_topk_mask(
610
+ topk_per_req,
611
+ q_lens,
612
+ padded_q_len,
613
+ max_seqlen_k,
614
+ workspace_3d,
615
+ )
616
+ if key_starts is not None:
617
+ dense_mask[:, 0, -1].copy_(key_starts)
618
+ kwargs = {
619
+ "q": q,
620
+ "k": k,
621
+ "v": v,
622
+ "cu_seqlens_q": cu_seqlens_q,
623
+ "cu_seqlens_k": cu_seqlens_k,
624
+ "max_seqlen_q": max_seqlen_q,
625
+ "max_seqlen_k": max_seqlen_k,
626
+ "softmax_scale": self.scale,
627
+ "return_softmax_lse": return_softmax_lse,
628
+ "fa_version": 4,
629
+ "mask_mod": dense_mask_mod if key_starts is None else offset_dense_mask_mod,
630
+ "aux_tensors": [dense_mask],
631
+ "aux_tensor_leading_dims": [2],
632
+ "causal": causal,
633
+ }
634
+
635
+ return flash_attn_varlen_func(**kwargs)
636
+
637
+ def _compute_context_mha(
638
+ self,
639
+ q: torch.Tensor,
640
+ kv_c_and_k_pe_cache: torch.Tensor,
641
+ prefill_metadata: MLACommonPrefillMetadata,
642
+ k_scale: torch.Tensor,
643
+ q_lens: list[int],
644
+ topk_per_req: list[torch.Tensor],
645
+ dense_mask: torch.Tensor | None = None,
646
+ ) -> tuple[torch.Tensor, torch.Tensor]:
647
+ if self.dcp_world_size > 1:
648
+ raise NotImplementedError(
649
+ "Masked MHA with context does not yet support decode context "
650
+ "parallelism"
651
+ )
652
+
653
+ chunked_context = prefill_metadata.chunked_context
654
+ assert chunked_context is not None
655
+ use_global_mask = dense_mask is not None
656
+ output: torch.Tensor | None = None
657
+ output_lse: torch.Tensor | None = None
658
+ workspace = chunked_context.workspace
659
+
660
+ for i, toks in enumerate(chunked_context.seq_tot):
661
+ if toks == 0:
662
+ continue
663
+ ops.gather_and_maybe_dequant_cache(
664
+ src_cache=kv_c_and_k_pe_cache,
665
+ dst=workspace,
666
+ block_table=prefill_metadata.block_table,
667
+ cu_seq_lens=chunked_context.cu_seq_lens[i],
668
+ token_to_seq=chunked_context.token_to_seq[i],
669
+ num_tokens=chunked_context.chunk_total_token[i],
670
+ kv_cache_dtype=self.kv_cache_dtype,
671
+ scale=k_scale,
672
+ seq_starts=chunked_context.starts[i],
673
+ )
674
+
675
+ chunk_kv_c = workspace[:toks, : self.kv_lora_rank]
676
+ chunk_k_pe = workspace[:toks, self.kv_lora_rank :].unsqueeze(1)
677
+ k, v = self._project_kv(chunk_kv_c, chunk_k_pe)
678
+ chunk_lens = chunked_context.seq_lens[i].tolist()
679
+ chunk_topk = (
680
+ topk_per_req
681
+ if use_global_mask
682
+ else self._remap_topk_to_ranges(
683
+ topk_per_req,
684
+ chunked_context.starts[i],
685
+ chunk_lens,
686
+ )
687
+ )
688
+ attn_out, lse = self._run_masked_mha(
689
+ q=q,
690
+ k=k,
691
+ v=v,
692
+ cu_seqlens_q=prefill_metadata.query_start_loc,
693
+ cu_seqlens_k=chunked_context.cu_seq_lens[i],
694
+ max_seqlen_q=prefill_metadata.max_query_len,
695
+ max_seqlen_k=chunked_context.max_seq_lens[i],
696
+ topk_per_req=chunk_topk,
697
+ q_lens=q_lens,
698
+ causal=False,
699
+ return_softmax_lse=True,
700
+ dense_mask=dense_mask,
701
+ key_starts=(chunked_context.starts[i] if use_global_mask else None),
702
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
703
+ )
704
+
705
+ if output is None:
706
+ output = attn_out
707
+ output_lse = lse
708
+ else:
709
+ assert output_lse is not None
710
+ merge_attn_states(
711
+ output=output,
712
+ output_lse=output_lse,
713
+ prefix_output=output,
714
+ prefix_lse=output_lse,
715
+ suffix_output=attn_out,
716
+ suffix_lse=lse,
717
+ )
718
+
719
+ assert output is not None and output_lse is not None
720
+ return output, output_lse
721
+
722
+ def forward_mha( # type: ignore[override]
723
+ self,
724
+ q: torch.Tensor,
725
+ kv_c_normed: torch.Tensor,
726
+ k_pe: torch.Tensor,
727
+ kv_c_and_k_pe_cache: torch.Tensor,
728
+ attn_metadata: T,
729
+ k_scale: torch.Tensor,
730
+ output: torch.Tensor,
731
+ output_scale: torch.Tensor | None = None,
732
+ ) -> None:
733
+ prefill_max_seq_len = attn_metadata.prefill_max_seq_len # type: ignore[attr-defined]
734
+ topk_tokens = attn_metadata.topk_tokens # type: ignore[attr-defined]
735
+ force_dense = getattr(self, "_sparse_mla_force_dense_mha", False)
736
+ force_masked = getattr(self, "_sparse_mla_force_masked_mha", False)
737
+ if force_dense or (prefill_max_seq_len <= topk_tokens and not force_masked):
738
+ return super().forward_mha(
739
+ q,
740
+ kv_c_normed,
741
+ k_pe,
742
+ kv_c_and_k_pe_cache,
743
+ cast(MLACommonMetadata, attn_metadata),
744
+ k_scale,
745
+ output,
746
+ output_scale,
747
+ )
748
+
749
+ assert output_scale is None
750
+ assert self.masked_mha_available
751
+ prefill_metadata = attn_metadata.prefill # type: ignore[attr-defined]
752
+ assert prefill_metadata is not None
753
+ assert prefill_metadata.query_lens_cpu is not None
754
+ assert self.topk_indices_buffer is not None
755
+
756
+ q_lens = prefill_metadata.query_lens_cpu.tolist()
757
+ num_decode_tokens = attn_metadata.num_decode_tokens # type: ignore[attr-defined]
758
+ topk_all = self.topk_indices_buffer[
759
+ num_decode_tokens : num_decode_tokens + q.shape[0]
760
+ ]
761
+ topk_per_req = self._slice_topk_per_req(topk_all, q_lens)
762
+
763
+ k, v = self._project_kv(kv_c_normed, k_pe)
764
+ chunked_context = prefill_metadata.chunked_context
765
+ if chunked_context is None:
766
+ attn_out = self._run_masked_mha(
767
+ q=q,
768
+ k=k,
769
+ v=v,
770
+ cu_seqlens_q=prefill_metadata.query_start_loc,
771
+ cu_seqlens_k=prefill_metadata.query_start_loc,
772
+ max_seqlen_q=prefill_metadata.max_query_len,
773
+ max_seqlen_k=prefill_metadata.max_query_len,
774
+ topk_per_req=topk_per_req,
775
+ q_lens=q_lens,
776
+ causal=True,
777
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
778
+ )
779
+ assert isinstance(attn_out, torch.Tensor)
780
+ output.copy_(attn_out[..., : self.v_head_dim].flatten(start_dim=-2))
781
+ return
782
+
783
+ context_lens = chunked_context.seq_lens.sum(dim=0).tolist()
784
+ dense_mask = self._try_build_global_mask(
785
+ topk_per_req,
786
+ q_lens,
787
+ prefill_metadata.max_query_len,
788
+ prefill_max_seq_len,
789
+ prefill_metadata.topk_mask_workspace,
790
+ )
791
+ if dense_mask is not None:
792
+ suffix_topk = topk_per_req
793
+ else:
794
+ suffix_topk = self._remap_topk_to_ranges(topk_per_req, context_lens, q_lens)
795
+ suffix_output, suffix_lse = self._run_masked_mha(
796
+ q=q,
797
+ k=k,
798
+ v=v,
799
+ cu_seqlens_q=prefill_metadata.query_start_loc,
800
+ cu_seqlens_k=prefill_metadata.query_start_loc,
801
+ max_seqlen_q=prefill_metadata.max_query_len,
802
+ max_seqlen_k=prefill_metadata.max_query_len,
803
+ topk_per_req=suffix_topk,
804
+ q_lens=q_lens,
805
+ causal=True,
806
+ return_softmax_lse=True,
807
+ dense_mask=dense_mask,
808
+ key_starts=(
809
+ chunked_context.context_lens if dense_mask is not None else None
810
+ ),
811
+ topk_mask_workspace=prefill_metadata.topk_mask_workspace,
812
+ )
813
+ context_output, context_lse = self._compute_context_mha(
814
+ q=q,
815
+ kv_c_and_k_pe_cache=kv_c_and_k_pe_cache,
816
+ prefill_metadata=prefill_metadata,
817
+ k_scale=k_scale,
818
+ q_lens=q_lens,
819
+ topk_per_req=topk_per_req,
820
+ dense_mask=dense_mask,
821
+ )
822
+ merge_attn_states(
823
+ output=output.view(-1, self.num_heads, self.v_head_dim),
824
+ prefix_output=context_output[..., : self.v_head_dim],
825
+ prefix_lse=context_lse,
826
+ suffix_output=suffix_output[..., : self.v_head_dim],
827
+ suffix_lse=suffix_lse,
828
+ prefill_tokens_with_context=chunked_context.prefill_tokens_with_context,
829
+ )
vllm/model_executor/layers/attention/sparse_mla_mask.py
new
+56
@@ -0,0 +1,56 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import cutlass
5
+import cutlass.cute as cute
6
+
7
+from vllm.vllm_flash_attn.cute import utils
8
+
9
+
10
+@cute.jit
11
+def dense_mask_mod(
12
+ batch: cute.TensorSSA,
13
+ head: cute.TensorSSA,
14
+ q_idx: cute.TensorSSA,
15
+ kv_idx: cute.TensorSSA,
16
+ seqlen_info,
17
+ aux_tensors: list,
18
+) -> cute.TensorSSA:
19
+ dense_mask = aux_tensors[0]
20
+ batch_idx = utils.ssa_to_scalar(batch)
21
+ q_idx = utils.ssa_to_scalar(q_idx)
22
+ kv_idx = utils.ssa_to_scalar(kv_idx)
23
+ word_idx = kv_idx >> 5
24
+ bit_idx = cutlass.Uint32(kv_idx & 31)
25
+ word = dense_mask[batch_idx, q_idx, word_idx]
26
+ result = cute.make_rmem_tensor(1, dtype=cutlass.Uint32)
27
+ result[0] = utils.shr_u32(cutlass.Uint32(word), bit_idx)
28
+ return result.load()
29
+
30
+
31
+dense_mask_mod.__vec_size__ = 32
32
+
33
+
34
+@cute.jit
35
+def offset_dense_mask_mod(
36
+ batch: cute.TensorSSA,
37
+ head: cute.TensorSSA,
38
+ q_idx: cute.TensorSSA,
39
+ kv_idx: cute.TensorSSA,
40
+ seqlen_info,
41
+ aux_tensors: list,
42
+) -> cute.TensorSSA:
43
+ dense_mask = aux_tensors[0]
44
+ batch_idx = utils.ssa_to_scalar(batch)
45
+ q_idx = utils.ssa_to_scalar(q_idx)
46
+ key_start = dense_mask[batch_idx, 0, dense_mask.shape[2] - 1]
47
+ kv_idx = utils.ssa_to_scalar(kv_idx) + key_start
48
+ word_idx = kv_idx >> 5
49
+ bit_idx = cutlass.Uint32(kv_idx & 31)
50
+ word = dense_mask[batch_idx, q_idx, word_idx]
51
+ result = cute.make_rmem_tensor(1, dtype=cutlass.Uint32)
52
+ result[0] = utils.shr_u32(cutlass.Uint32(word), bit_idx)
53
+ return result.load()
54
+
55
+
56
+offset_dense_mask_mod.__vec_size__ = 32
vllm/vllm_flash_attn/flash_attn_interface.py
+13
@@ -210,7 +210,9 @@ def flash_attn_varlen_func(
210
cp_tot_seqused_k=None,
211
# FA4 only
212
mask_mod=None,
213
+ block_sparse_tensors=None,
214
aux_tensors=None,
215
+ aux_tensor_leading_dims=None,
216
dynamic_causal: "torch.Tensor | None" = None,
217
):
218
"""dropout_p should be set to 0.0 during evaluation
@@ -385,6 +387,15 @@ def flash_attn_varlen_func(
387
)
388
elif fa_version == 4:
389
assert alibi_slopes is None, "Alibi is not supported in FA4"
390
+ if block_sparse_tensors is not None:
391
+ assert block_sparse_tensors.full_block_cnt is not None, (
392
+ "FA4 block_sparse_tensors must materialize empty full_block_cnt "
393
+ "instead of passing None"
394
+ )
395
+ assert block_sparse_tensors.full_block_idx is not None, (
396
+ "FA4 block_sparse_tensors must materialize empty full_block_idx "
397
+ "instead of passing None"
398
+ )
399
400
from vllm.vllm_flash_attn.cute.interface import _flash_attn_fwd
401
@@ -409,7 +420,9 @@ def flash_attn_varlen_func(
420
out=out,
421
learnable_sink=s_aux,
422
mask_mod=mask_mod,
423
+ block_sparse_tensors=block_sparse_tensors,
424
aux_tensors=aux_tensors,
425
+ aux_tensor_leading_dims=aux_tensor_leading_dims,
426
output_scale=output_scale,
427
)
428
else: