main
py 458 lines 16.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """AMD ROCm QSA owner with Triton kernels."""
4
5 from __future__ import annotations
6
7 from typing import ClassVar, cast
8
9 import torch
10 from torch import nn
11 from transformers import Qwen4ExpTextConfig
12
13 from vllm.config import VllmConfig
14 from vllm.config.cache import CacheDType
15 from vllm.distributed import get_tensor_model_parallel_world_size
16 from vllm.forward_context import get_forward_context
17 from vllm.model_executor.layers.attention.attention import (
18 set_default_quant_scales,
19 )
20 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
21 from vllm.model_executor.layers.layernorm import GemmaRMSNorm
22 from vllm.model_executor.layers.linear import QKVParallelLinear, RowParallelLinear
23 from vllm.model_executor.layers.quantization import QuantizationConfig
24 from vllm.model_executor.layers.rotary_embedding import get_rope
25 from vllm.model_executor.models.qwen3_next import Qwen3NextAttention
26 from vllm.platforms import current_platform
27 from vllm.utils.torch_utils import (
28 LayerNameType,
29 _encode_layer_name,
30 _resolve_layer_name,
31 canonicalize_singleton_dim_strides,
32 direct_register_custom_op,
33 kv_cache_dtype_str_to_dtype,
34 )
35 from vllm.v1.attention.backend import (
36 AttentionBackend,
37 AttentionCGSupport,
38 AttentionType,
39 MultipleOf,
40 )
41 from vllm.v1.attention.backends.fa_utils import is_flash_attn_varlen_func_available
42 from vllm.v1.attention.backends.flash_attn import (
43 FlashAttentionBackend,
44 FlashAttentionImpl,
45 FlashAttentionMetadata,
46 FlashAttentionMetadataBuilder,
47 )
48 from vllm.v1.kv_cache_interface import (
49 FullAttentionSpec,
50 KVCacheSpec,
51 get_kv_quant_mode,
52 )
53
54 from ..common.qsa_cache import QSAForwardMetadata
55 from . import model
56 from .indexer_qsa import QSAIndexer
57
58
59 class Qwen4ExpQSAMetadataBuilder(FlashAttentionMetadataBuilder):
60 """Flash metadata supporting uniform decode and target-verify graphs."""
61
62 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
63
64
65 class Qwen4ExpQSAFlashAttentionBackend(FlashAttentionBackend):
66 """FullAttentionSpec backend used by the merged QSA owner."""
67
68 supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
69 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = ["auto", "bfloat16"]
70
71 @staticmethod
72 def get_name() -> str:
73 return "QWEN4_EXP_QSA_TRITON"
74
75 @staticmethod
76 def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]:
77 # QSA consumes manager pages directly and does not use FA4 paged attention.
78 return [MultipleOf(16)]
79
80 @staticmethod
81 def get_impl_cls() -> type[Qwen4ExpQSAFlashAttentionImpl]:
82 return Qwen4ExpQSAFlashAttentionImpl
83
84 @staticmethod
85 def get_builder_cls() -> type[Qwen4ExpQSAMetadataBuilder]:
86 return Qwen4ExpQSAMetadataBuilder
87
88 @classmethod
89 def is_sparse(cls) -> bool:
90 return True
91
92 @classmethod
93 def supports_kv_connector(cls) -> bool:
94 return False
95
96
97 class Qwen4ExpQSAFlashAttentionImpl(FlashAttentionImpl):
98 """Run paged sparse GQA with the QSA Triton kernel."""
99
100 supports_dcp: bool = False
101 supports_pcp: bool = False
102
103 def __init__(self, *args, **kwargs) -> None:
104 super().__init__(*args, **kwargs)
105 if not is_flash_attn_varlen_func_available():
106 raise NotImplementedError("Qwen4Exp QSA requires FlashAttention")
107 if self.dcp_world_size != 1:
108 raise NotImplementedError(
109 "Qwen4Exp QSA does not support decode context parallelism"
110 )
111 if self.kv_cache_dtype not in ("auto", "bfloat16"):
112 raise NotImplementedError("Qwen4Exp QSA requires a BF16 main KV cache")
113 self.supports_quant_query_input = False
114
115 def forward_qsa(
116 self,
117 layer: torch.nn.Module,
118 query: torch.Tensor,
119 key: torch.Tensor,
120 value: torch.Tensor,
121 kv_cache: torch.Tensor,
122 attn_metadata: FlashAttentionMetadata,
123 output: torch.Tensor,
124 token_to_req: torch.Tensor,
125 output_scale: torch.Tensor | None = None,
126 output_block_scale: torch.Tensor | None = None,
127 ) -> torch.Tensor:
128 del key, value
129 if output_scale is not None or output_block_scale is not None:
130 raise NotImplementedError("QSA does not support fused output quantization")
131 if self.alibi_slopes is not None or self.sinks is not None:
132 raise NotImplementedError("QSA does not support ALiBi or attention sinks")
133 if self.sliding_window != (-1, -1):
134 raise NotImplementedError("QSA does not support sliding-window attention")
135
136 num_tokens = attn_metadata.num_actual_tokens
137 output.zero_()
138 if num_tokens == 0:
139 return output
140
141 topk_buffer = getattr(layer, "topk_indices_buffer", None)
142 if topk_buffer is None:
143 raise RuntimeError("QSA owner did not provide its top-k buffer")
144 logical_indices = topk_buffer[:num_tokens]
145 token_to_req = token_to_req[:num_tokens]
146 key_cache, value_cache = kv_cache.transpose(1, 2).split(self.head_size, dim=-1)
147 key_cache = canonicalize_singleton_dim_strides(key_cache)
148 value_cache = canonicalize_singleton_dim_strides(value_cache)
149 if key_cache.dtype != torch.bfloat16 or query.dtype != torch.bfloat16:
150 raise NotImplementedError("Qwen4Exp QSA requires BF16 Q/K/V")
151
152 from .ops.qsa import qsa_sparse_paged_attention
153
154 qsa_sparse_paged_attention(
155 query[:num_tokens],
156 key_cache,
157 value_cache,
158 logical_indices,
159 attn_metadata.block_table,
160 token_to_req,
161 output[:num_tokens],
162 )
163 return output
164
165
166 class Qwen4ExpQSAAttention(Qwen3NextAttention, AttentionLayerBase):
167 """Merged Qwen full-attention owner with a QSA index side branch."""
168
169 supports_dcp = False
170
171 def __init__(
172 self,
173 *,
174 vllm_config: VllmConfig,
175 config: Qwen4ExpTextConfig,
176 layer_id: int,
177 quant_config: QuantizationConfig | None = None,
178 reduce_results: bool = True,
179 prefix: str = "",
180 ) -> None:
181 nn.Module.__init__(self)
182 cache_config = vllm_config.cache_config
183 model_config = vllm_config.model_config
184 if cache_config is None:
185 raise ValueError("Qwen4Exp QSA requires a paged KV cache")
186 if model_config.dtype != torch.bfloat16:
187 raise NotImplementedError("Qwen4Exp QSA currently requires BF16")
188 if cache_config.cache_dtype not in ("auto", "bfloat16"):
189 raise NotImplementedError("Qwen4Exp QSA requires a BF16 main KV cache")
190 if getattr(quant_config, "kv_cache_scheme", None) is not None:
191 raise NotImplementedError("Qwen4Exp QSA does not support KV quantization")
192 parallel_config = vllm_config.parallel_config
193 if (
194 parallel_config.prefill_context_parallel_size > 1
195 or parallel_config.decode_context_parallel_size > 1
196 ):
197 raise NotImplementedError(
198 "Qwen4Exp QSA does not support context parallelism"
199 )
200 if not getattr(config, "is_causal", True):
201 raise NotImplementedError("Qwen4Exp QSA requires causal decoder attention")
202
203 self.config = config
204 self.hidden_size = int(config.hidden_size)
205 tp_size = get_tensor_model_parallel_world_size()
206 self.total_num_heads = int(config.num_attention_heads)
207 if self.total_num_heads % tp_size:
208 raise ValueError("QSA attention heads must be divisible by TP size")
209 self.num_heads = self.total_num_heads // tp_size
210 self.total_num_kv_heads = int(config.num_key_value_heads)
211 if self.total_num_kv_heads >= tp_size:
212 if self.total_num_kv_heads % tp_size:
213 raise ValueError("QSA KV heads must be divisible by TP size")
214 elif tp_size % self.total_num_kv_heads:
215 raise ValueError("TP size must be divisible by replicated QSA KV heads")
216 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
217 self.head_dim = int(config.head_dim or self.hidden_size // self.num_heads)
218 self.q_size = self.num_heads * self.head_dim
219 self.kv_size = self.num_kv_heads * self.head_dim
220 self.scaling = self.head_dim**-0.5
221 self.dual_chunk_attention_config = getattr(
222 config, "dual_chunk_attention_config", None
223 )
224 if self.dual_chunk_attention_config is not None:
225 raise NotImplementedError("Qwen4Exp QSA does not support dual-chunk RoPE")
226 # Qwen4Exp full-attention checkpoints always pack a sigmoid output
227 # gate next to Q, even when an inherited config default says otherwise.
228 self.attn_output_gate = True
229
230 self.qkv_proj = QKVParallelLinear(
231 self.hidden_size,
232 self.head_dim,
233 self.total_num_heads * (1 + self.attn_output_gate),
234 self.total_num_kv_heads,
235 bias=False,
236 quant_config=model.without_modelopt_fp4(quant_config),
237 prefix=f"{prefix}.qkv_proj",
238 )
239 self.o_proj = RowParallelLinear(
240 self.total_num_heads * self.head_dim,
241 self.hidden_size,
242 bias=False,
243 reduce_results=reduce_results,
244 quant_config=quant_config,
245 prefix=f"{prefix}.o_proj",
246 )
247 self.rotary_emb = get_rope(
248 head_size=self.head_dim,
249 max_position=config.max_position_embeddings,
250 rope_parameters=config.rope_parameters,
251 )
252 self.q_norm = GemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
253 self.k_norm = GemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
254
255 mm_config = model_config.multimodal_config
256 text_only = mm_config is None or mm_config.language_model_only
257 self.use_fused_qk_norm_rope_gate = (
258 self.attn_output_gate
259 and getattr(self.rotary_emb, "is_neox_style", False)
260 and current_platform.is_cuda()
261 and text_only
262 )
263
264 self.layer_name = f"{prefix}.attn"
265 self.attn_type = AttentionType.DECODER
266 self.kv_cache_dtype = cache_config.cache_dtype
267 self.kv_cache_torch_dtype = kv_cache_dtype_str_to_dtype(
268 self.kv_cache_dtype, model_config
269 )
270 if self.kv_cache_torch_dtype != torch.bfloat16:
271 raise NotImplementedError("Qwen4Exp QSA requires BF16 cache storage")
272 self.kv_sharing_target_layer_name = None
273 self.kv_cache = torch.tensor([])
274 set_default_quant_scales(self, register_buffer=True)
275
276 self.attn_backend = Qwen4ExpQSAFlashAttentionBackend
277 self.impl = Qwen4ExpQSAFlashAttentionImpl(
278 self.num_heads,
279 self.head_dim,
280 self.scaling,
281 self.num_kv_heads,
282 None,
283 None,
284 self.kv_cache_dtype,
285 None,
286 AttentionType.DECODER,
287 None,
288 )
289 self.indexer = QSAIndexer(
290 vllm_config=vllm_config,
291 config=config,
292 layer_id=layer_id,
293 rotary_emb=self.rotary_emb,
294 quant_config=quant_config,
295 prefix=f"{prefix}.indexer",
296 )
297 max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
298 self.register_buffer(
299 "topk_indices_buffer",
300 torch.empty(
301 max_tokens,
302 self.indexer.output_width,
303 dtype=torch.int32,
304 ),
305 persistent=False,
306 )
307
308 static_context = vllm_config.compilation_config.static_forward_context
309 if self.layer_name in static_context:
310 raise ValueError(f"Duplicate layer name: {self.layer_name}")
311 static_context[self.layer_name] = self
312
313 def get_attn_backend(self) -> type[AttentionBackend]:
314 return self.attn_backend
315
316 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
317 return FullAttentionSpec(
318 block_size=vllm_config.cache_config.block_size,
319 num_kv_heads=self.num_kv_heads,
320 head_size=self.head_dim,
321 head_size_v=self.head_dim,
322 dtype=self.kv_cache_torch_dtype,
323 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
324 )
325
326 def _run_qsa(
327 self,
328 hidden_states: torch.Tensor,
329 positions: torch.Tensor,
330 query: torch.Tensor,
331 key: torch.Tensor,
332 value: torch.Tensor,
333 output: torch.Tensor,
334 ) -> None:
335 metadata = get_forward_context().attn_metadata
336 if isinstance(metadata, list):
337 metadata = metadata[0]
338 if not isinstance(metadata, dict):
339 output.zero_()
340 return
341 main_metadata = cast(FlashAttentionMetadata, metadata[self.layer_name])
342 if self.kv_cache.numel() == 0:
343 raise RuntimeError("QSA main K/V cache is not bound")
344
345 num_tokens = main_metadata.num_actual_tokens
346 side_metadata = cast(
347 QSAForwardMetadata,
348 metadata[self.indexer.raw_key_cache.prefix],
349 )
350 if side_metadata.num_actual_tokens != num_tokens:
351 raise RuntimeError("QSA main and side metadata token counts disagree")
352 selected = self.indexer(
353 hidden_states,
354 positions,
355 self.topk_indices_buffer[:num_tokens],
356 )
357 if selected.shape != (
358 num_tokens,
359 self.indexer.output_width,
360 ):
361 raise RuntimeError("QSA indexer returned an invalid selection shape")
362 impl = cast(Qwen4ExpQSAFlashAttentionImpl, self.impl)
363 impl.do_kv_cache_update(
364 self,
365 key,
366 value,
367 self.kv_cache,
368 main_metadata.slot_mapping,
369 )
370 impl.forward_qsa(
371 self,
372 query,
373 key,
374 value,
375 self.kv_cache,
376 main_metadata,
377 output,
378 token_to_req=side_metadata.token_to_req,
379 )
380
381 def forward(
382 self,
383 positions: torch.Tensor,
384 hidden_states: torch.Tensor,
385 ) -> torch.Tensor:
386 qkv, _ = self.qkv_proj(hidden_states)
387 q, k, v, gate = self._project_qkv_gate(qkv, positions)
388 num_tokens = hidden_states.shape[0]
389 query = q.view(num_tokens, self.num_heads, self.head_dim)
390 key = k.view(num_tokens, self.num_kv_heads, self.head_dim)
391 value = v.view(num_tokens, self.num_kv_heads, self.head_dim)
392 attn_output = torch.empty_like(query)
393 encoded_layer_name = _encode_layer_name(self.layer_name)
394 if current_platform.opaque_attention_op():
395 torch.ops.vllm.qwen4_exp_qsa_with_output(
396 hidden_states,
397 positions,
398 query,
399 key,
400 value,
401 attn_output,
402 encoded_layer_name,
403 )
404 else:
405 qwen4_exp_qsa_with_output(
406 hidden_states,
407 positions,
408 query,
409 key,
410 value,
411 attn_output,
412 encoded_layer_name,
413 )
414 flat_output = attn_output.view(num_tokens, -1)
415 if gate is not None:
416 flat_output = flat_output * torch.sigmoid(gate)
417 output, _ = self.o_proj(flat_output)
418 return output
419
420
421 def qwen4_exp_qsa_with_output(
422 hidden_states: torch.Tensor,
423 positions: torch.Tensor,
424 query: torch.Tensor,
425 key: torch.Tensor,
426 value: torch.Tensor,
427 output: torch.Tensor,
428 layer_name: LayerNameType,
429 ) -> None:
430 """Run the complete QSA state/update/attend transaction."""
431 layer_name = _resolve_layer_name(layer_name)
432 layer = get_forward_context().no_compile_layers[layer_name]
433 if not isinstance(layer, Qwen4ExpQSAAttention):
434 raise TypeError(f"{layer_name} is not a Qwen4Exp QSA owner")
435 layer._run_qsa(
436 hidden_states,
437 positions,
438 query,
439 key,
440 value,
441 output,
442 )
443
444
445 direct_register_custom_op(
446 op_name="qwen4_exp_qsa_with_output",
447 op_func=qwen4_exp_qsa_with_output,
448 mutates_args=["output"],
449 )
450
451
452 __all__ = [
453 "QSAIndexer",
454 "Qwen4ExpQSAAttention",
455 "Qwen4ExpQSAFlashAttentionBackend",
456 "Qwen4ExpQSAFlashAttentionImpl",
457 "qwen4_exp_qsa_with_output",
458 ]