main
py 328 lines 12.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from typing import ClassVar
5
6 import torch
7
8 import vllm.envs as envs
9 from vllm.config.cache import CacheDType
10 from vllm.logger import init_logger
11 from vllm.model_executor.layers.attention.mla_attention import (
12 MLACommonBackend,
13 MLACommonImpl,
14 MLACommonMetadata,
15 MLACommonMetadataBuilder,
16 QueryLenSupport,
17 )
18 from vllm.platforms import current_platform
19 from vllm.platforms.interface import DeviceCapability
20 from vllm.triton_utils import triton
21 from vllm.utils.torch_utils import is_quantized_kv_cache
22 from vllm.v1.attention.backend import (
23 AttentionCGSupport,
24 AttentionLayer,
25 AttentionType,
26 MultipleOf,
27 )
28 from vllm.v1.attention.ops.triton_decode_attention import decode_attention_fwd
29 from vllm.v1.worker.workspace import (
30 current_workspace_manager,
31 is_workspace_manager_initialized,
32 )
33
34 logger = init_logger(__name__)
35
36 # num_kv_splits selection (shared by forward_mqa and the workspace reservation
37 # so the two cannot drift). Both are hardware dependent.
38 _MIN_WORK_PER_SPLIT = 512
39 _SPLIT_OCCUPANCY_MULTIPLIER = 2
40
41
42 def _compute_num_kv_splits(max_seq_len: int, sm_count: int) -> int:
43 # Power of 2 to avoid excessive kernel instantiations, capped by an SM-based
44 # maximum (occupancy multiplier allows multiple blocks per SM
45 # for latency hiding).
46 ideal_splits = triton.next_power_of_2(max(1, max_seq_len // _MIN_WORK_PER_SPLIT))
47 max_splits = sm_count * _SPLIT_OCCUPANCY_MULTIPLIER
48 return min(ideal_splits, max_splits)
49
50
51 class TritonMLAMetadataBuilder(MLACommonMetadataBuilder[MLACommonMetadata]):
52 # forward_mqa flattens a uniform multi-token block to one decode row per
53 # query token, so causal and non-causal blocks both take the decode path.
54 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
55 query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM
56 supports_non_causal_multi_token_decode: ClassVar[bool] = True
57
58 def __init__(self, kv_cache_spec, layer_names, vllm_config, device):
59 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
60 # DCP local sequence lengths are not advanced between draft steps.
61 self.supports_draft_decode_metadata_update = self.dcp_world_size == 1
62 self._reserve_attn_logits_workspace()
63
64 def update_draft_decode_metadata(self, _metadata: MLACommonMetadata) -> None:
65 pass
66
67 def _reserve_attn_logits_workspace(self) -> None:
68 """Pre-size the shared workspace for the decode split-KV attn logits.
69
70 Reserving at the worst case (max_model_len -> max num_kv_splits,
71 max_num_seqs decode tokens) before warmup/cudagraph capture means the
72 per-call ``get_simultaneous`` in ``forward_mqa`` never has to grow the
73 buffer at runtime (which would raise once the workspace is locked).
74 """
75 if not is_workspace_manager_initialized():
76 return
77 # forward_mqa flattens each request's block to query_len decode rows,
78 # and query_len is bounded by the reorder threshold.
79 B = (
80 self.vllm_config.scheduler_config.max_num_seqs
81 * self.reorder_batch_threshold
82 )
83 # DCP all-gathers the query heads before forward_mqa.
84 q_num_heads = self.num_heads * self.dcp_world_size
85 max_splits = _compute_num_kv_splits(
86 self.model_config.max_model_len,
87 current_platform.num_compute_units(),
88 )
89 lse_dim = self.mla_dims.kv_lora_rank + 1
90 current_workspace_manager().get_simultaneous(
91 ((B, q_num_heads, max_splits, lse_dim), torch.float32),
92 )
93
94
95 class TritonMLABackend(MLACommonBackend):
96 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
97 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
98 "auto",
99 "float16",
100 "bfloat16",
101 "fp8",
102 "fp8_e4m3",
103 ]
104
105 @classmethod
106 def get_supported_head_sizes(cls) -> list[int]:
107 return []
108
109 @staticmethod
110 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
111 return [MultipleOf(16)]
112
113 @classmethod
114 def supports_block_size(cls, block_size: int | None) -> bool:
115 if block_size is None:
116 return True
117 return block_size % 16 == 0
118
119 @staticmethod
120 def get_name() -> str:
121 return "TRITON_MLA"
122
123 @classmethod
124 def supports_batch_invariance(cls) -> bool:
125 return True
126
127 @staticmethod
128 def get_impl_cls() -> type["TritonMLAImpl"]:
129 return TritonMLAImpl
130
131 @staticmethod
132 def get_builder_cls() -> type["TritonMLAMetadataBuilder"]:
133 return TritonMLAMetadataBuilder
134
135 @classmethod
136 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
137 return True
138
139 @classmethod
140 def supports_non_causal(cls) -> bool:
141 # DSpark non-causal blocks are flattened to single-token decode rows in
142 # TritonMLAImpl.forward_mqa (decode_attention_fwd has no causal flag /
143 # no intra-block masking). Enables the non-causal AMD MLA path.
144 return True
145
146
147 class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
148 can_return_lse_for_decode: bool = True
149 supports_dcp: bool = True
150
151 def __init__(
152 self,
153 num_heads: int,
154 head_size: int,
155 scale: float,
156 num_kv_heads: int,
157 alibi_slopes: list[float] | None,
158 sliding_window: int | None,
159 kv_cache_dtype: str,
160 logits_soft_cap: float | None,
161 attn_type: str,
162 kv_sharing_target_layer_name: str | None,
163 # MLA Specific Arguments
164 **mla_args,
165 ) -> None:
166 super().__init__(
167 num_heads,
168 head_size,
169 scale,
170 num_kv_heads,
171 alibi_slopes,
172 sliding_window,
173 kv_cache_dtype,
174 logits_soft_cap,
175 attn_type,
176 kv_sharing_target_layer_name,
177 **mla_args,
178 )
179
180 unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
181 if any(unsupported_features):
182 raise NotImplementedError(
183 "TritonMLAImpl does not support one of the following: "
184 "alibi_slopes, sliding_window, logits_soft_cap"
185 )
186
187 if attn_type != AttentionType.DECODER:
188 raise NotImplementedError(
189 "Encoder self-attention and "
190 "encoder/decoder cross-attention "
191 "are not implemented for "
192 "TritonMLAImpl"
193 )
194
195 if current_platform.is_cuda():
196 cap = current_platform.get_device_capability()
197 cap_str = cap.as_version_str() if cap is not None else "unknown"
198 dev = current_platform.get_device_name()
199 if self.kv_cache_dtype.startswith("fp8") and not (
200 current_platform.has_device_capability(89)
201 ):
202 suggested = (
203 "float16" if (cap is None or cap.to_int() < 80) else "bfloat16"
204 )
205 raise ValueError(
206 f"FP8 KV cache is not supported by the Triton MLA backend "
207 f"on {dev} (compute capability {cap_str}); native FP8 "
208 f"(fp8e4nv) requires SM89+. Re-run with "
209 f"--kv-cache-dtype {suggested}."
210 )
211 if self.kv_cache_dtype == "bfloat16" and not (
212 current_platform.has_device_capability(80)
213 ):
214 raise ValueError(
215 f"bfloat16 KV cache is not supported by the Triton MLA "
216 f"backend on {dev} (compute capability {cap_str}); "
217 f"bfloat16 requires SM80+. Re-run with "
218 f"--kv-cache-dtype float16."
219 )
220
221 # For FP8 KV cache, we dequantize to BF16 on load inside the
222 # Triton kernel. Tell the common layer not to quantize queries
223 # to FP8 — we handle FP8 KV cache with BF16 queries (Mode 1).
224 if is_quantized_kv_cache(self.kv_cache_dtype):
225 self.supports_quant_query_input = False
226
227 self._sm_count = current_platform.num_compute_units()
228
229 def forward_mqa(
230 self,
231 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
232 kv_c_and_k_pe_cache: torch.Tensor,
233 attn_metadata: MLACommonMetadata,
234 layer: AttentionLayer,
235 ) -> tuple[torch.Tensor, torch.Tensor | None]:
236 assert kv_c_and_k_pe_cache.numel() > 0
237 assert attn_metadata.decode is not None
238
239 if type(q) is tuple:
240 q = torch.cat(q, dim=-1)
241
242 assert isinstance(q, torch.Tensor)
243 B = q.shape[0]
244 q_num_heads = q.shape[1]
245 o = torch.zeros(
246 B, q_num_heads, self.kv_lora_rank, dtype=q.dtype, device=q.device
247 )
248 lse = torch.zeros(B, q_num_heads, dtype=q.dtype, device=q.device)
249
250 # For batch invariance, use only 1 split to ensure deterministic reduction
251 if envs.VLLM_BATCH_INVARIANT:
252 num_kv_splits = 1
253 else:
254 num_kv_splits = _compute_num_kv_splits(
255 attn_metadata.max_seq_len, self._sm_count
256 )
257
258 # NOTE: the +1 stores the LogSumExp (LSE) that the stage2 kernel uses to
259 # merge partial attention outputs across splits. The scratch is served
260 # from the shared workspace (reserved at max in the metadata builder), so
261 # there is no per-call allocation on the decode hot path. Fall back to a
262 # direct allocation when the workspace manager is not initialized (e.g.
263 # unit tests without a GPUModelRunner).
264 logits_shape = (B, q_num_heads, num_kv_splits, self.kv_lora_rank + 1)
265 if is_workspace_manager_initialized():
266 (attn_logits,) = current_workspace_manager().get_simultaneous(
267 (logits_shape, torch.float32),
268 )
269 else:
270 attn_logits = torch.empty(
271 logits_shape, dtype=torch.float32, device=q.device
272 )
273
274 # Add a head dim of 1
275 kv_c_and_k_pe_cache = kv_c_and_k_pe_cache.unsqueeze(2)
276 kv_c_cache = kv_c_and_k_pe_cache[..., : self.kv_lora_rank]
277 PAGE_SIZE = kv_c_and_k_pe_cache.size(1)
278
279 block_table = attn_metadata.decode.block_table
280 seq_lens = attn_metadata.decode.seq_lens
281 # decode_attention_fwd has no causal flag: it launches one program per
282 # row of q and reads that row's KV extent from seq_lens, so intra-block
283 # causality is expressed as per-row extents. Deriving query_len from the
284 # tensors the kernel indexes keeps the three row counts in step.
285 num_decodes = seq_lens.shape[0]
286 query_len, remainder = divmod(B, num_decodes) if num_decodes else (1, 0)
287 assert remainder == 0, (
288 f"non-uniform decode block: {B} query rows over {num_decodes} requests"
289 )
290 if query_len > 1:
291 block_table = block_table.repeat_interleave(query_len, dim=0)
292 if attn_metadata.causal:
293 # Per-row extents are offsets off the global sequence length;
294 # under DCP seq_lens is this rank's local slice instead.
295 assert self.dcp_world_size == 1, (
296 "causal multi-token decode is not supported with DCP"
297 )
298 # Row t attends the prefix plus block tokens 0..t. Clamp holds
299 # padding rows at extent 0; the kernel skips them either way
300 # (split_kv_end == split_kv_start).
301 offsets = torch.arange(
302 1 - query_len, 1, device=seq_lens.device, dtype=seq_lens.dtype
303 )
304 seq_lens = (seq_lens.unsqueeze(1) + offsets).flatten().clamp(min=0)
305 else:
306 # Non-causal draft block: every row sees the same prefix.
307 seq_lens = seq_lens.repeat_interleave(query_len)
308
309 # Run MQA — always pass layer scales. When KV cache is
310 # BF16 the kernel's `if dtype.is_fp8()` check is a no-op.
311 decode_attention_fwd(
312 q,
313 kv_c_and_k_pe_cache,
314 kv_c_cache,
315 o,
316 lse,
317 block_table,
318 seq_lens,
319 attn_logits,
320 num_kv_splits,
321 self.scale,
322 PAGE_SIZE,
323 k_scale=layer._k_scale,
324 v_scale=layer._k_scale,
325 is_mla=True,
326 )
327
328 return o, lse