main
py 322 lines 11.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """TokenSpeed CuTe DSL MLA decode backend (Blackwell, FP8 KV cache only)."""
4
5 from typing import TYPE_CHECKING, ClassVar
6
7 import torch
8
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.interface import DeviceCapability
19 from vllm.utils.torch_utils import is_quantized_kv_cache
20 from vllm.v1.attention.backend import (
21 AttentionCGSupport,
22 AttentionLayer,
23 AttentionType,
24 MultipleOf,
25 )
26
27 if TYPE_CHECKING:
28 from vllm.config import VllmConfig
29 from vllm.v1.kv_cache_interface import AttentionSpec
30
31 logger = init_logger(__name__)
32
33 # Workspace upper bound for tokenspeed_mla_decode (per-device, lazy):
34 # num_sms * num_heads * MAX_Q_LEN * (kv_lora_rank + 1) * sizeof(float32)
35 # Matches the kernel's `get_workspace_size` formula. MAX_Q_LEN=8 covers up to
36 # EAGLE3 / MTP-2 spec decoding query lengths; larger q_len fails the kernel's
37 # own buffer check.
38 _TOKENSPEED_MAX_Q_LEN = 8
39
40 _g_workspace: dict[torch.device, torch.Tensor] = {}
41
42
43 def _get_workspace(
44 device: torch.device, num_heads: int, kv_lora_rank: int
45 ) -> torch.Tensor:
46 from tokenspeed_mla import get_num_sm
47
48 needed = (
49 get_num_sm(device) * num_heads * _TOKENSPEED_MAX_Q_LEN * (kv_lora_rank + 1) * 4
50 )
51 existing = _g_workspace.get(device)
52 if existing is None or existing.numel() < needed:
53 _g_workspace[device] = torch.empty(needed, dtype=torch.int8, device=device)
54 return _g_workspace[device]
55
56
57 class TokenspeedMLAMetadataBuilder(MLACommonMetadataBuilder[MLACommonMetadata]):
58 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
59 query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM
60 # The kernel accepts an explicit causal mask, so a non-causal DSpark
61 # block can remain fused instead of being flattened to single tokens.
62 supports_non_causal_multi_token_decode: ClassVar[bool] = True
63 supports_non_causal_multi_token_dcp: ClassVar[bool] = True
64
65 def __init__(
66 self,
67 kv_cache_spec: "AttentionSpec",
68 layer_names: list[str],
69 vllm_config: "VllmConfig",
70 device: torch.device,
71 ) -> None:
72 super().__init__(
73 kv_cache_spec,
74 layer_names,
75 vllm_config,
76 device,
77 MLACommonMetadata,
78 supports_dcp_with_varlen=True,
79 )
80
81
82 class TokenspeedMLABackend(MLACommonBackend):
83 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
84 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
85 "fp8",
86 "fp8_e4m3",
87 ]
88
89 @staticmethod
90 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
91 return [32, 64]
92
93 @staticmethod
94 def get_name() -> str:
95 return "TOKENSPEED_MLA"
96
97 @staticmethod
98 def get_impl_cls() -> type["TokenspeedMLAImpl"]:
99 return TokenspeedMLAImpl
100
101 @staticmethod
102 def get_builder_cls() -> type["TokenspeedMLAMetadataBuilder"]:
103 return TokenspeedMLAMetadataBuilder
104
105 @classmethod
106 def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
107 return capability.major == 10
108
109 @classmethod
110 def supports_non_causal(cls) -> bool:
111 return True
112
113 @classmethod
114 def supports_combination(
115 cls,
116 head_size: int,
117 dtype: torch.dtype,
118 kv_cache_dtype: CacheDType | None,
119 block_size: int | None,
120 use_mla: bool,
121 has_sink: bool,
122 use_sparse: bool,
123 use_mm_prefix: bool,
124 device_capability: DeviceCapability,
125 ) -> str | None:
126 # Surface a clear install hint up front rather than letting a raw
127 # ModuleNotFoundError fire deep inside `forward_mqa` at first request.
128 try:
129 import tokenspeed_mla # noqa: F401
130 except ImportError:
131 return (
132 "tokenspeed_mla package is not installed. "
133 "Install it with: `uv pip install tokenspeed-mla`"
134 )
135
136 # tokenspeed_mla CuTe DSL kernel is shape-specialized for DeepSeek R1
137 # MLA dimensions (qk_nope=128, qk_rope=64, v=128). Reject anything else.
138 from vllm.config import get_current_vllm_config
139
140 vllm_config = get_current_vllm_config()
141 if vllm_config.model_config is not None:
142 hf_text_config = vllm_config.model_config.hf_text_config
143 qk_nope_head_dim = getattr(hf_text_config, "qk_nope_head_dim", 0)
144 qk_rope_head_dim = getattr(hf_text_config, "qk_rope_head_dim", 0)
145 v_head_dim = getattr(hf_text_config, "v_head_dim", 0)
146 if qk_nope_head_dim != 128 or qk_rope_head_dim != 64 or v_head_dim != 128:
147 return (
148 "tokenspeed_mla requires DeepSeek R1 MLA dimensions "
149 "(qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128), "
150 f"got ({qk_nope_head_dim}, {qk_rope_head_dim}, {v_head_dim})"
151 )
152 return None
153
154
155 class TokenspeedMLAImpl(MLACommonImpl[MLACommonMetadata]):
156 can_return_lse_for_decode: bool = True
157 supports_dcp: bool = True
158 # tokenspeed_mla_decode returns LSE in log2 units; its own DCP test merges
159 # partial outputs with exp2(lse).
160 lse_base_on_e: bool = False
161
162 def __init__(
163 self,
164 num_heads: int,
165 head_size: int,
166 scale: float,
167 num_kv_heads: int,
168 alibi_slopes: list[float] | None,
169 sliding_window: int | None,
170 kv_cache_dtype: str,
171 logits_soft_cap: float | None,
172 attn_type: str,
173 kv_sharing_target_layer_name: str | None,
174 # MLA Specific Arguments
175 **mla_args,
176 ) -> None:
177 super().__init__(
178 num_heads,
179 head_size,
180 scale,
181 num_kv_heads,
182 alibi_slopes,
183 sliding_window,
184 kv_cache_dtype,
185 logits_soft_cap,
186 attn_type,
187 kv_sharing_target_layer_name,
188 **mla_args,
189 )
190
191 unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
192 if any(unsupported_features):
193 raise NotImplementedError(
194 "TokenspeedMLAImpl does not support one of the following: "
195 "alibi_slopes, sliding_window, logits_soft_cap"
196 )
197
198 if attn_type != AttentionType.DECODER:
199 raise NotImplementedError(
200 "Encoder self-attention and "
201 "encoder/decoder cross-attention "
202 "are not implemented for "
203 "TokenspeedMLAImpl"
204 )
205
206 if not is_quantized_kv_cache(self.kv_cache_dtype):
207 raise NotImplementedError(
208 "TokenspeedMLAImpl requires an FP8 KV cache "
209 "(--kv-cache-dtype fp8 or fp8_e4m3); "
210 f"got kv_cache_dtype={self.kv_cache_dtype!r}."
211 )
212
213 # Allocate (or fetch the cached) workspace lazily on first forward —
214 # __init__ runs before the device is necessarily set on the worker;
215 # we know it for sure at forward time when we see the input tensor.
216 self._workspace_buffer: torch.Tensor | None = None
217 self.softmax_scale: float | None = None
218 self.output_scale: float | None = None
219
220 # Pre-JIT BF16 and FP8 prefill kernels here too — decode impl always
221 # runs when tokenspeed is selected, prefill backend may not (user can
222 # pair with flash_attn / trtllm). Idempotent.
223 from tokenspeed_mla import warmup_compile_prefill
224
225 for q_dtype in (torch.bfloat16, torch.float8_e4m3fn):
226 warmup_compile_prefill(
227 q_dtype=q_dtype,
228 d_qk=self.qk_nope_head_dim + self.qk_rope_head_dim,
229 d_v=self.v_head_dim,
230 enable_pdl=False,
231 )
232
233 def forward_mqa(
234 self,
235 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
236 kv_c_and_k_pe_cache: torch.Tensor,
237 attn_metadata: MLACommonMetadata,
238 layer: AttentionLayer,
239 ) -> tuple[torch.Tensor, torch.Tensor | None]:
240 from tokenspeed_mla import tokenspeed_mla_decode
241
242 assert kv_c_and_k_pe_cache.numel() > 0
243 assert attn_metadata.decode is not None
244
245 if isinstance(q, tuple):
246 q_nope, q_pe = q
247 q = torch.cat([q_nope, q_pe], dim=-1)
248
249 # supports_quant_query_input=True (set in MLACommonImpl) tells the
250 # pipeline to concat+FP8-quantize Q upstream via _decode_concat_quant_fp8_op.
251 # The kernel is shape-specialized for FP8 Q + FP8 KV, so anything else
252 # here means the upstream quant didn't run and the kernel will produce
253 # garbage.
254 assert q.dtype == torch.float8_e4m3fn, (
255 f"TokenspeedMLAImpl expected FP8 query (supports_quant_query_input=True), "
256 f"got {q.dtype}. Pipeline isinstance(q, tuple)={isinstance(q, tuple)}, "
257 f"q_scale={layer._q_scale_float}, k_scale={layer._k_scale_float}."
258 )
259
260 num_decodes = attn_metadata.num_decodes
261 num_decode_tokens = attn_metadata.num_decode_tokens
262 block_tables = attn_metadata.decode.block_table
263 seq_lens = attn_metadata.decode.seq_lens
264 causal_seqs = attn_metadata.decode.dcp_tot_seq_lens
265
266 # tokenspeed_mla_decode expects query shape
267 # (num_decodes, q_len_per_request, num_heads, head_dim).
268 if num_decode_tokens % num_decodes != 0:
269 logger.warning_once(
270 """TokenspeedMLAImpl got a query of uneven length.
271 This usually indicates an issue in batch reordering
272 or incorrect setup in dummy_run."""
273 )
274 q = q.unsqueeze(1)
275 else:
276 q = q.view(num_decodes, -1, q.shape[-2], q.shape[-1])
277
278 if self.softmax_scale is None:
279 # FP8 KV cache is mandatory for this backend, so q_scale/k_scale
280 # always apply. softmax_scale is bmm1; output_scale is bmm2 — both
281 # required to recover the correct attention output from the FP8
282 # KV cache (V is stored as V_real/k_scale).
283 self.softmax_scale = (
284 self.scale * layer._q_scale_float * layer._k_scale_float
285 )
286 self.output_scale = layer._k_scale_float
287
288 if self._workspace_buffer is None:
289 # Parallelism can change the runtime query head count.
290 self._workspace_buffer = _get_workspace(
291 q.device, q.shape[-2], self.kv_lora_rank
292 )
293
294 # vLLM kv_c_and_k_pe_cache is already (num_blocks, block_size, head_size).
295 # tokenspeed_mla_decode wants 3D — pass as-is (no unsqueeze, unlike trtllm).
296 return_lse = self.need_to_return_lse_for_decode
297 kernel_out = tokenspeed_mla_decode(
298 query=q,
299 kv_cache=kv_c_and_k_pe_cache,
300 workspace_buffer=self._workspace_buffer,
301 kv_lora_rank=self.kv_lora_rank,
302 qk_rope_head_dim=self.qk_rope_head_dim,
303 block_tables=block_tables,
304 seq_lens=seq_lens,
305 max_seq_len=attn_metadata.max_seq_len,
306 softmax_scale=self.softmax_scale,
307 output_scale=self.output_scale,
308 enable_pdl=False,
309 return_lse=return_lse,
310 causal_mask=attn_metadata.causal,
311 causal_seqs=causal_seqs if self.dcp_world_size > 1 else None,
312 cp_world=self.dcp_world_size,
313 cp_rank=self.dcp_rank,
314 )
315 if return_lse:
316 o, lse = kernel_out
317 lse = lse.view(-1, lse.shape[-1])
318 else:
319 o, lse = kernel_out, None
320
321 o = o.view(-1, o.shape[-2], o.shape[-1])
322 return o, lse