main
py 352 lines 12.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from dataclasses import dataclass
5 from typing import ClassVar
6
7 import torch
8
9 import vllm.envs as envs
10 from vllm.config import VllmConfig
11 from vllm.config.cache import CacheDType
12 from vllm.logger import init_logger
13 from vllm.model_executor.layers.attention.mla_attention import (
14 MLACommonBackend,
15 MLACommonDecodeMetadata,
16 MLACommonImpl,
17 MLACommonMetadata,
18 MLACommonMetadataBuilder,
19 QueryLenSupport,
20 )
21 from vllm.platforms.interface import DeviceCapability
22 from vllm.utils.platform_utils import num_compute_units
23 from vllm.utils.torch_utils import is_quantized_kv_cache
24 from vllm.v1.attention.backend import (
25 AttentionCGSupport,
26 AttentionLayer,
27 AttentionType,
28 MultipleOf,
29 )
30 from vllm.v1.attention.backends.utils import (
31 reshape_attn_output_for_spec_decode,
32 reshape_query_for_spec_decode,
33 )
34 from vllm.v1.attention.ops.flashmla import (
35 FlashMLASchedMeta,
36 flash_mla_with_kvcache,
37 flash_mla_with_kvcache_fp8,
38 get_mla_metadata,
39 get_mla_metadata_dense_fp8,
40 is_flashmla_dense_supported,
41 )
42 from vllm.v1.kv_cache_interface import AttentionSpec
43
44 logger = init_logger(__name__)
45
46
47 class FlashMLABackend(MLACommonBackend):
48 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
49 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
50 "auto",
51 "float16",
52 "bfloat16",
53 "fp8",
54 "fp8_e4m3",
55 ]
56
57 @staticmethod
58 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
59 return [64]
60
61 @staticmethod
62 def get_name() -> str:
63 return "FLASHMLA"
64
65 @staticmethod
66 def get_builder_cls() -> type["FlashMLAMetadataBuilder"]:
67 return FlashMLAMetadataBuilder
68
69 @staticmethod
70 def get_impl_cls() -> type["FlashMLAImpl"]:
71 return FlashMLAImpl
72
73 @classmethod
74 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
75 return capability.major in [9, 10]
76
77 @classmethod
78 def supports_combination(
79 cls,
80 head_size: int,
81 dtype: torch.dtype,
82 kv_cache_dtype: CacheDType | None,
83 block_size: int | None,
84 use_mla: bool,
85 has_sink: bool,
86 use_sparse: bool,
87 use_mm_prefix: bool,
88 device_capability: DeviceCapability,
89 ) -> str | None:
90 if use_sparse:
91 from vllm.v1.attention.ops.flashmla import is_flashmla_sparse_supported
92
93 return is_flashmla_sparse_supported()[1]
94 else:
95 from vllm.v1.attention.ops.flashmla import is_flashmla_dense_supported
96
97 return is_flashmla_dense_supported()[1]
98
99
100 @dataclass
101 class FlashMLADecodeMetadata(MLACommonDecodeMetadata):
102 scheduler_metadata: FlashMLASchedMeta
103
104
105 @dataclass
106 class FlashMLAMetadata(MLACommonMetadata[FlashMLADecodeMetadata]):
107 pass
108
109
110 class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
111 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
112 query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM
113 reorder_batch_threshold: int = 128 # process small prefills with decode pathway
114 # ^ TODO(matt): tune this
115
116 def __init__(
117 self,
118 kv_cache_spec: AttentionSpec,
119 layer_names: list[str],
120 vllm_config: VllmConfig,
121 device: torch.device,
122 ):
123 super().__init__(
124 kv_cache_spec, layer_names, vllm_config, device, FlashMLAMetadata
125 )
126
127 self.num_q_heads = vllm_config.model_config.get_num_attention_heads(
128 vllm_config.parallel_config
129 )
130
131 self.cg_buf_tile_scheduler_metadata = None
132 self.cg_buf_num_splits = None
133 self.is_fp8_kvcache = is_quantized_kv_cache(
134 vllm_config.cache_config.cache_dtype
135 )
136
137 num_sms = num_compute_units(self.device.index)
138
139 if self.compilation_config.cudagraph_mode.has_full_cudagraphs():
140 self.cg_buf_tile_scheduler_metadata = torch.zeros(
141 # Upper bound on size (<= #SMs, TileSchedulerMetaDataSize)
142 # TileSchedulerMetaDataSize = 8
143 (num_sms, 8),
144 device=self.device,
145 dtype=torch.int32,
146 )
147 self.cg_buf_num_splits = torch.empty(
148 (vllm_config.scheduler_config.max_num_seqs + 1),
149 device=self.device,
150 dtype=torch.int32,
151 )
152
153 def _build_decode(
154 self,
155 block_table_tensor: torch.Tensor,
156 seq_lens_device: torch.Tensor,
157 max_seq_len: int,
158 query_start_loc_cpu: torch.Tensor,
159 query_start_loc_device: torch.Tensor,
160 num_decode_tokens: int,
161 max_query_len: int,
162 dcp_tot_seq_lens_device: torch.Tensor | None,
163 ) -> FlashMLADecodeMetadata:
164 query_lens_cpu = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
165 # we use the max but all should be the same due to uniform length requirement
166 max_query_len = query_lens_cpu.max().item()
167 num_q_heads = self.num_q_heads
168 if self.dcp_world_size > 1:
169 num_q_heads *= self.dcp_world_size
170 num_q_tokens_per_head_k = max_query_len * num_q_heads // 1
171 scheduler_metadata, _ = get_mla_metadata(
172 seq_lens_device,
173 num_q_tokens_per_head_k,
174 1, # MQA for the decode path
175 is_fp8_kvcache=self.is_fp8_kvcache,
176 )
177 if self.is_fp8_kvcache:
178 tile_scheduler_metadata, num_splits = get_mla_metadata_dense_fp8(
179 seq_lens_device,
180 num_q_tokens_per_head_k,
181 1, # MQA for the decode path
182 )
183
184 # Copy FP8 metadata into persistent CUDA graph buffers
185 if self.compilation_config.cudagraph_mode.has_full_cudagraphs():
186 assert self.cg_buf_tile_scheduler_metadata is not None
187 assert self.cg_buf_num_splits is not None
188 n = tile_scheduler_metadata.size(0)
189 assert n <= self.cg_buf_tile_scheduler_metadata.size(0)
190 self.cg_buf_tile_scheduler_metadata[:n].copy_(tile_scheduler_metadata)
191 tile_scheduler_metadata = self.cg_buf_tile_scheduler_metadata[:n]
192
193 n = num_splits.size(0)
194 assert n <= self.cg_buf_num_splits.size(0)
195 self.cg_buf_num_splits[:n].copy_(num_splits)
196 num_splits = self.cg_buf_num_splits[:n]
197
198 scheduler_metadata.tile_scheduler_metadata = tile_scheduler_metadata
199 scheduler_metadata.num_splits = num_splits
200
201 return FlashMLADecodeMetadata(
202 block_table=block_table_tensor,
203 seq_lens=seq_lens_device,
204 scheduler_metadata=scheduler_metadata,
205 dcp_tot_seq_lens=dcp_tot_seq_lens_device,
206 )
207
208
209 class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
210 can_return_lse_for_decode: bool = True
211 supports_dcp: bool = True
212
213 def __init__(
214 self,
215 num_heads: int,
216 head_size: int,
217 scale: float,
218 num_kv_heads: int,
219 alibi_slopes: list[float] | None,
220 sliding_window: int | None,
221 kv_cache_dtype: str,
222 logits_soft_cap: float | None,
223 attn_type: str,
224 kv_sharing_target_layer_name: str | None,
225 # MLA Specific Arguments
226 **mla_args,
227 ) -> None:
228 super().__init__(
229 num_heads,
230 head_size,
231 scale,
232 num_kv_heads,
233 alibi_slopes,
234 sliding_window,
235 kv_cache_dtype,
236 logits_soft_cap,
237 attn_type,
238 kv_sharing_target_layer_name,
239 **mla_args,
240 )
241
242 is_supported, reason = is_flashmla_dense_supported()
243 assert is_supported, reason
244
245 unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
246 if any(unsupported_features):
247 raise NotImplementedError(
248 "FlashMLAImpl does not support one of the following: "
249 "alibi_slopes, sliding_window, logits_soft_cap"
250 )
251
252 if attn_type != AttentionType.DECODER:
253 raise NotImplementedError(
254 "Encoder self-attention and "
255 "encoder/decoder cross-attention "
256 "are not implemented for "
257 "FlashMLAImpl"
258 )
259
260 def forward_mqa(
261 self,
262 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
263 kv_c_and_k_pe_cache: torch.Tensor,
264 attn_metadata: FlashMLAMetadata,
265 layer: AttentionLayer,
266 ) -> tuple[torch.Tensor, torch.Tensor | None]:
267 # TODO: (zyongye) decode function for mla here
268 assert kv_c_and_k_pe_cache.numel() > 0
269 assert attn_metadata.decode is not None
270
271 if type(q) is tuple:
272 q = torch.cat(q, dim=-1)
273
274 # mypy assertion: q is now always a tensor
275 assert isinstance(q, torch.Tensor)
276
277 num_decodes = attn_metadata.num_decodes
278 q = reshape_query_for_spec_decode(q, num_decodes)
279
280 scheduler_metadata = attn_metadata.decode.scheduler_metadata
281 if envs.VLLM_BATCH_INVARIANT and not is_quantized_kv_cache(self.kv_cache_dtype):
282 device = q.device
283 dtype = torch.int32
284
285 B = q.shape[0]
286 # block_table shape: [batch_size, max_num_blocks_per_seq]
287 # The number of blocks per sequence is in the second dimension
288 topk = attn_metadata.decode.block_table.shape[-1]
289 B_TOPK = 64
290 assert topk % B_TOPK == 0, f"topk ({topk}) must be divisible by {B_TOPK}"
291 end_block_idx = topk // B_TOPK
292
293 # Single partition => num_sm_parts = 1
294 # TileSchedulerMetaDataSize = 8, layout:
295 # [begin_idx, begin_block_idx, end_idx, end_block_idx,
296 # begin_n_split_idx, _, _, _]
297 tile_scheduler_metadata = torch.zeros((1, 8), dtype=dtype, device=device)
298 tile_scheduler_metadata[0, 0] = 0 # begin_idx
299 tile_scheduler_metadata[0, 1] = 0 # sched_begin_block_idx
300 tile_scheduler_metadata[0, 2] = B - 1 # end_idx
301 tile_scheduler_metadata[0, 3] = end_block_idx
302 tile_scheduler_metadata[0, 4] = 0 # begin_n_split_idx
303 # fields [5..7] stay 0
304
305 # Non-split path ignores num_splits, but the API requires it:
306 # zeros of length B+1
307 num_splits = torch.zeros((B + 1,), dtype=dtype, device=device)
308 scheduler_metadata.tile_scheduler_metadata = tile_scheduler_metadata
309 scheduler_metadata.num_splits = num_splits
310
311 if is_quantized_kv_cache(self.kv_cache_dtype):
312 o, lse = flash_mla_with_kvcache_fp8(
313 q=q,
314 k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
315 block_table=attn_metadata.decode.block_table,
316 cache_seqlens=attn_metadata.decode.seq_lens,
317 head_dim_v=self.kv_lora_rank,
318 tile_scheduler_metadata=scheduler_metadata.tile_scheduler_metadata,
319 num_splits=scheduler_metadata.num_splits,
320 softmax_scale=self.scale,
321 causal=True,
322 descale_q=layer._q_scale.reshape(1),
323 descale_k=layer._k_scale.reshape(1),
324 )
325 else:
326 o, lse = flash_mla_with_kvcache(
327 q=q,
328 k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
329 block_table=attn_metadata.decode.block_table,
330 cache_seqlens=attn_metadata.decode.seq_lens,
331 head_dim_v=self.kv_lora_rank,
332 tile_scheduler_metadata=scheduler_metadata,
333 softmax_scale=self.scale,
334 causal=True,
335 is_fp8_kvcache=False,
336 )
337
338 o = reshape_attn_output_for_spec_decode(o)
339
340 if self.need_to_return_lse_for_decode:
341 # FlashMLA returns LSE as [batch, heads, seq_len]; the DCP reducer
342 # consumes [tokens, heads]. Flattening matters under spec-decode,
343 # where seq_len > 1. Only DCP consumes lse, so skip the copy
344 # otherwise.
345 num_decodes, q_num_heads, seq_len = lse.shape
346 lse = (
347 lse.permute(0, 2, 1)
348 .reshape(num_decodes * seq_len, q_num_heads)
349 .contiguous()
350 )
351
352 return o, lse