A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Attention] FlashAttention MLA cudagraph support (#23958)
Signed-off-by: Matthew Bonanni <mbonanni001@gmail.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
Matthew Bonanni committed
Sep 8, 2025 at 15:05 UTC
620db1fc5879329dae62367238f97d446904a3ee
7 files changed
+118
-29
tests/compile/piecewise/test_full_cudagraph.py
+11
-1
@@ -61,6 +61,16 @@ backend_configs = {
61
"cudagraph_mode": "FULL_AND_PIECEWISE",
62
},
63
specific_gpu_arch=(9, 0)),
64
+ # FlashAttention MLA on Hopper
65
+ "FlashAttentionMLA":
66
+ BackendConfig(name="FlashAttentionMLA",
67
+ env_vars={
68
+ "VLLM_ATTENTION_BACKEND": "FLASH_ATTN_MLA",
69
+ },
70
+ comp_config={
71
+ "cudagraph_mode": "FULL_DECODE_ONLY",
72
+ },
73
+ specific_gpu_arch=(9, 0)),
74
# Cutlass MLA on Blackwell
75
"CutlassMLA":
76
BackendConfig(
@@ -102,7 +112,7 @@ backend_configs = {
112
test_params_full_cudagraph = []
113
114
# deepseek-ai/DeepSeek-V2-Lite with MLA
105
-MLA_backends = ["FlashMLA", "CutlassMLA"]
115
+MLA_backends = ["FlashMLA", "FlashAttentionMLA", "CutlassMLA"]
116
for mla_backend in MLA_backends:
117
test_params_full_cudagraph.append(
118
pytest.param(
tests/v1/attention/test_mla_backends.py
-5
@@ -73,7 +73,6 @@ def create_and_prepopulate_kv_cache(
73
kv_c_contexts: list[torch.Tensor],
74
k_pe_contexts: list[torch.Tensor],
75
block_size: int,
76
- num_kv_heads: int,
76
head_size: int,
77
dtype: torch.dtype,
78
device: torch.device,
@@ -87,7 +86,6 @@ def create_and_prepopulate_kv_cache(
86
k_pe_contexts: List of key positional embedding context tensors
87
for each sequence
88
block_size: Size of each block
90
- num_kv_heads: Number of KV heads (should be 1 for MLA)
89
head_size: Size of each head (latent dimension)
90
dtype: Data type for the cache
91
device: Device to create the cache on
@@ -285,8 +283,6 @@ def test_backend_correctness(dist_init, batch_spec_name: str, model: str):
283
query_lens = batch_spec.query_lens
284
num_q_heads = vllm_config.model_config.get_num_attention_heads(
285
vllm_config.parallel_config)
288
- num_kv_heads = vllm_config.model_config.get_num_kv_heads(
289
- vllm_config.parallel_config)
286
head_size = vllm_config.model_config.get_head_size()
287
dtype = _convert_dtype_to_torch(vllm_config.model_config.dtype)
288
block_size = vllm_config.cache_config.block_size
@@ -476,7 +472,6 @@ def test_backend_correctness(dist_init, batch_spec_name: str, model: str):
472
kv_c_contexts=kv_c_contexts,
473
k_pe_contexts=k_pe_contexts,
474
block_size=block_size,
479
- num_kv_heads=num_kv_heads,
475
head_size=head_size,
476
dtype=dtype,
477
device=device,
tests/v1/cudagraph/test_cudagraph_mode.py
+10
@@ -62,6 +62,16 @@ backend_configs = {
62
"cudagraph_mode": "FULL_AND_PIECEWISE",
63
},
64
specific_gpu_arch=(9, 0)),
65
+ # FlashAttention MLA on Hopper
66
+ "FlashAttentionMLA":
67
+ BackendConfig(name="FlashAttentionMLA",
68
+ env_vars={
69
+ "VLLM_ATTENTION_BACKEND": "FLASH_ATTN_MLA",
70
+ },
71
+ comp_config={
72
+ "cudagraph_mode": "FULL_DECODE_ONLY",
73
+ },
74
+ specific_gpu_arch=(9, 0)),
75
# FA2
76
"FA2":
77
BackendConfig(name="FA2",
vllm/v1/attention/backends/mla/common.py
+14
-8
@@ -443,11 +443,13 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
443
self.metadata_cls = metadata_cls \
444
if metadata_cls is not None else MLACommonMetadata
445
self.kv_cache_spec = kv_cache_spec
446
- self.device = device
446
scheduler_config = vllm_config.scheduler_config
447
self.model_config = vllm_config.model_config
449
- cache_config = vllm_config.cache_config
448
parallel_config = vllm_config.parallel_config
449
+ cache_config = vllm_config.cache_config
450
+ self.compilation_config = vllm_config.compilation_config
451
+ self.device = device
452
+
453
self.num_heads = self.model_config.get_num_attention_heads(
454
parallel_config)
455
self.mla_dims = get_mla_dims(self.model_config)
@@ -608,10 +610,12 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
610
prefill.prefill_main = self._fi_prefill_main
611
prefill.prefill_chunks = self._fi_prefill_chunks
612
611
- def _build_decode(
612
- self, block_table_tensor: torch.Tensor, seq_lens_cpu: torch.Tensor,
613
- seq_lens_device: torch.Tensor, query_start_loc_cpu: torch.Tensor,
614
- query_start_loc_device: torch.Tensor) -> MLACommonDecodeMetadata:
613
+ def _build_decode(self, block_table_tensor: torch.Tensor,
614
+ seq_lens_cpu: torch.Tensor,
615
+ seq_lens_device: torch.Tensor,
616
+ query_start_loc_cpu: torch.Tensor,
617
+ query_start_loc_device: torch.Tensor,
618
+ num_decode_tokens: int) -> MLACommonDecodeMetadata:
619
return MLACommonDecodeMetadata(
620
block_table=block_table_tensor,
621
seq_lens=seq_lens_device,
@@ -624,11 +628,12 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
628
Currently, only decode is supported for full cudagraphs with MLA.
629
"""
630
m = common_attn_metadata
627
- assert m.num_reqs == m.num_actual_tokens, \
631
+ assert m.num_reqs <= (m.num_actual_tokens *
632
+ self.reorder_batch_threshold), \
633
"MLA only supports decode-only full CUDAGraph capture. " \
634
"Make sure all cudagraph capture sizes <= max_num_seq."
635
631
- assert m.max_query_len == 1 # decode-only
636
+ assert m.max_query_len <= self.reorder_batch_threshold # decode only
637
638
return self.build(0, m)
639
@@ -819,6 +824,7 @@ class MLACommonMetadataBuilder(AttentionMetadataBuilder[M]):
824
seq_lens_device=seq_lens[:num_decodes],
825
query_start_loc_cpu=query_start_loc_cpu[:num_decodes + 1],
826
query_start_loc_device=query_start_loc[:num_decodes + 1],
827
+ num_decode_tokens=num_decode_tokens,
828
)
829
830
attn_metadata = self.metadata_cls(
vllm/v1/attention/backends/mla/flashattn_mla.py
+71
-6
@@ -17,11 +17,16 @@ from vllm.v1.attention.backends.mla.common import (MLACommonBackend,
17
MLACommonImpl,
18
MLACommonMetadata,
19
MLACommonMetadataBuilder)
20
+from vllm.v1.attention.backends.utils import AttentionCGSupport
21
from vllm.v1.kv_cache_interface import AttentionSpec
22
from vllm.vllm_flash_attn import flash_attn_varlen_func, get_scheduler_metadata
23
24
logger = init_logger(__name__)
25
26
+# NOTE(matt): This is an arbitrary number, copied from
27
+# woosuk's implementation in standard FlashAttention backend
28
+_DEFAULT_MAX_NUM_SPLITS_FOR_CUDA_GRAPH = 16
29
+
30
31
class FlashAttnMLABackend(MLACommonBackend):
32
@@ -48,6 +53,7 @@ class FlashAttnMLADecodeMetadata(MLACommonDecodeMetadata):
53
max_query_len: int
54
max_seq_len: int
55
scheduler_metadata: Optional[torch.Tensor] = None
56
+ max_num_splits: int = 0
57
58
59
@dataclass
@@ -57,14 +63,41 @@ class FlashAttnMLAMetadata(MLACommonMetadata[FlashAttnMLADecodeMetadata]):
63
64
class FlashAttnMLAMetadataBuilder(
65
MLACommonMetadataBuilder[FlashAttnMLAMetadata]):
66
+ cudagraph_support: ClassVar[AttentionCGSupport] = \
67
+ AttentionCGSupport.UNIFORM_BATCH
68
+
69
reorder_batch_threshold: ClassVar[int] = 512
70
71
def __init__(self, kv_cache_spec: AttentionSpec, layer_names: list[str],
72
vllm_config: VllmConfig, device: torch.device):
73
super().__init__(kv_cache_spec, layer_names, vllm_config, device,
74
FlashAttnMLAMetadata)
75
+ self.max_num_splits = 0 # No upper bound on the number of splits.
76
self.fa_aot_schedule = (get_flash_attn_version() == 3)
77
78
+ self.use_full_cuda_graph = \
79
+ self.compilation_config.cudagraph_mode.has_full_cudagraphs()
80
+
81
+ if self.use_full_cuda_graph and self.fa_aot_schedule:
82
+ self.max_cudagraph_size = self.compilation_config.max_capture_size
83
+
84
+ if self.max_cudagraph_size > 992:
85
+ # This condition derives from FA3's internal heuristic.
86
+ # TODO(woosuk): Support larger cudagraph sizes.
87
+ raise ValueError(
88
+ "Capture size larger than 992 is not supported for "
89
+ "full cuda graph.")
90
+
91
+ self.scheduler_metadata = torch.zeros(
92
+ vllm_config.scheduler_config.max_num_seqs + 1,
93
+ dtype=torch.int32,
94
+ device=self.device,
95
+ )
96
+ # When using cuda graph, we need to set the upper bound of the
97
+ # number of splits so that large enough intermediate buffers are
98
+ # pre-allocated during capture.
99
+ self.max_num_splits = _DEFAULT_MAX_NUM_SPLITS_FOR_CUDA_GRAPH
100
+
101
def _schedule_decode(self, num_reqs, cu_query_lens, max_query_len, seqlens,
102
max_seq_len, causal):
103
if self.fa_aot_schedule:
@@ -81,14 +114,16 @@ class FlashAttnMLAMetadataBuilder(
114
page_size=self.page_size,
115
cu_seqlens_q=cu_query_lens,
116
causal=causal,
117
+ num_splits=self.max_num_splits,
118
)
119
return None
120
87
- def _build_decode(
88
- self, block_table_tensor: torch.Tensor, seq_lens_cpu: torch.Tensor,
89
- seq_lens_device: torch.Tensor, query_start_loc_cpu: torch.Tensor,
90
- query_start_loc_device: torch.Tensor
91
- ) -> FlashAttnMLADecodeMetadata:
121
+ def _build_decode(self, block_table_tensor: torch.Tensor,
122
+ seq_lens_cpu: torch.Tensor,
123
+ seq_lens_device: torch.Tensor,
124
+ query_start_loc_cpu: torch.Tensor,
125
+ query_start_loc_device: torch.Tensor,
126
+ num_decode_tokens: int) -> FlashAttnMLADecodeMetadata:
127
query_lens_cpu = (query_start_loc_cpu[1:] - query_start_loc_cpu[:-1])
128
max_query_len = query_lens_cpu.max().item()
129
max_seq_len = seq_lens_cpu.max().item()
@@ -102,6 +137,29 @@ class FlashAttnMLAMetadataBuilder(
137
causal=True,
138
)
139
140
+ # For FA3 + full cudagraph
141
+ max_num_splits = 0
142
+ if self.use_full_cuda_graph and scheduler_metadata is not None:
143
+ n = scheduler_metadata.shape[0]
144
+ # Ensure the persistent buffer is large enough
145
+ assert n <= self.scheduler_metadata.shape[0], \
146
+ f"Scheduler metadata size {n} exceeds buffer size " + \
147
+ f"{self.scheduler_metadata.shape[0]}"
148
+ self.scheduler_metadata[:n] = scheduler_metadata
149
+ # NOTE(woosuk): We should zero out the rest of the scheduler
150
+ # metadata to guarantee the correctness. Otherwise, some thread
151
+ # blocks may use the invalid scheduler metadata and overwrite the
152
+ # output buffer.
153
+ self.scheduler_metadata[n:] = 0
154
+ scheduler_metadata = self.scheduler_metadata[:n]
155
+
156
+ if num_decode_tokens <= self.max_cudagraph_size:
157
+ # NOTE(woosuk): Setting num_splits > 1 may increase the memory
158
+ # usage, because the intermediate buffers of size [num_splits,
159
+ # num_heads, num_tokens, head_size] are allocated. Therefore,
160
+ # we only set num_splits when using cuda graphs.
161
+ max_num_splits = self.max_num_splits
162
+
163
return FlashAttnMLADecodeMetadata(
164
block_table=block_table_tensor,
165
seq_lens=seq_lens_device,
@@ -109,6 +167,7 @@ class FlashAttnMLAMetadataBuilder(
167
max_query_len=max_query_len,
168
max_seq_len=max_seq_len,
169
scheduler_metadata=scheduler_metadata,
170
+ max_num_splits=max_num_splits,
171
)
172
173
@@ -175,12 +234,17 @@ class FlashAttnMLAImpl(MLACommonImpl[FlashAttnMLAMetadata]):
234
kv_c_cache = kv_c_and_k_pe_cache[..., :self.kv_lora_rank]
235
k_pe_cache = kv_c_and_k_pe_cache[..., self.kv_lora_rank:]
236
237
+ # NOTE(matt): During CUDA graph capture, max_query_len can be 0, but the
238
+ # kernel uses this to calculate grid dimensions. Ensure it's at least 1
239
+ # to prevent invalid grid configuration during graph capture.
240
+ max_seqlen_q = max(attn_metadata.decode.max_query_len, 1)
241
+
242
o = flash_attn_varlen_func(
243
q=q_pe,
244
k=k_pe_cache.unsqueeze(-2), # Add head dim of 1
245
v=kv_c_cache.unsqueeze(-2), # Add head dim of 1
246
q_v=q_nope,
183
- max_seqlen_q=attn_metadata.decode.max_query_len,
247
+ max_seqlen_q=max_seqlen_q,
248
cu_seqlens_q=attn_metadata.decode.query_start_loc,
249
max_seqlen_k=attn_metadata.decode.max_seq_len,
250
seqused_k=attn_metadata.decode.seq_lens,
@@ -189,6 +253,7 @@ class FlashAttnMLAImpl(MLACommonImpl[FlashAttnMLAMetadata]):
253
causal=True,
254
fa_version=3, # only version 3 is supported
255
scheduler_metadata=attn_metadata.decode.scheduler_metadata,
256
+ num_splits=attn_metadata.decode.max_num_splits,
257
)
258
259
return self._v_up_proj(o)
vllm/v1/attention/backends/mla/flashmla.py
+6
-5
@@ -62,7 +62,6 @@ class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
62
super().__init__(kv_cache_spec, layer_names, vllm_config, device,
63
FlashMLAMetadata)
64
65
- self.compilation_config = vllm_config.compilation_config
65
self.num_q_heads = vllm_config.model_config.get_num_attention_heads(
66
vllm_config.parallel_config)
67
@@ -85,10 +84,12 @@ class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
84
device=self.device,
85
dtype=torch.int32)
86
88
- def _build_decode(
89
- self, block_table_tensor: torch.Tensor, seq_lens_cpu: torch.Tensor,
90
- seq_lens_device: torch.Tensor, query_start_loc_cpu: torch.Tensor,
91
- query_start_loc_device: torch.Tensor) -> FlashMLADecodeMetadata:
87
+ def _build_decode(self, block_table_tensor: torch.Tensor,
88
+ seq_lens_cpu: torch.Tensor,
89
+ seq_lens_device: torch.Tensor,
90
+ query_start_loc_cpu: torch.Tensor,
91
+ query_start_loc_device: torch.Tensor,
92
+ num_decode_tokens: int) -> FlashMLADecodeMetadata:
93
tile_scheduler_metadata, num_splits = \
94
get_mla_metadata(
95
seq_lens_device,
vllm/v1/attention/backends/mla/rocm_aiter_mla.py
+6
-4
@@ -104,10 +104,12 @@ class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]):
104
dtype=torch.int32,
105
device=device)
106
107
- def _build_decode(
108
- self, block_table_tensor: torch.Tensor, seq_lens_cpu: torch.Tensor,
109
- seq_lens_device: torch.Tensor, query_start_loc_cpu: torch.Tensor,
110
- query_start_loc_device: torch.Tensor) -> AiterMLADecodeMetadata:
107
+ def _build_decode(self, block_table_tensor: torch.Tensor,
108
+ seq_lens_cpu: torch.Tensor,
109
+ seq_lens_device: torch.Tensor,
110
+ query_start_loc_cpu: torch.Tensor,
111
+ query_start_loc_device: torch.Tensor,
112
+ num_decode_tokens: int) -> AiterMLADecodeMetadata:
113
page_size = self.kv_cache_spec.block_size
114
block_table_bounds = (seq_lens_device + page_size - 1) // page_size
115
device = self.device