main
py 641 lines 23.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import functools
4 import logging
5 import math
6 from dataclasses import replace
7 from fractions import Fraction
8 from functools import partial
9 from typing import ClassVar, Protocol
10
11 import torch
12 import torch.nn.functional as F
13 from torch import nn
14
15 from vllm.config import CacheConfig, VllmConfig
16 from vllm.distributed import get_tensor_model_parallel_world_size
17 from vllm.model_executor.layers.attention import Attention
18 from vllm.model_executor.layers.layernorm import RMSNorm
19 from vllm.model_executor.layers.linear import (
20 QKVParallelLinear,
21 RowParallelLinear,
22 )
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.mistral import MistralMLP
26 from vllm.model_executor.models.whisper import WhisperPosEmbedType
27 from vllm.utils.math_utils import cdiv
28 from vllm.v1.attention.backend import (
29 AttentionBackend,
30 AttentionCGSupport,
31 AttentionMetadata,
32 AttentionType,
33 CommonAttentionMetadata,
34 subclass_attention_backend_with_overrides,
35 )
36 from vllm.v1.attention.backends.cpu_attn import CPUAttentionBackend
37 from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
38 from vllm.v1.attention.backends.rocm_attn import RocmAttentionBackend
39 from vllm.v1.attention.backends.triton_attn import TritonAttentionBackend
40 from vllm.v1.attention.selector import get_attn_backend
41 from vllm.v1.kv_cache_interface import AttentionSpec, SlidingWindowSpec
42
43 from .utils import make_layers
44
45 AiterFlashAttentionBackend: type[AttentionBackend] | None = None
46 try:
47 from vllm.v1.attention.backends import rocm_aiter_fa
48
49 AiterFlashAttentionBackend = rocm_aiter_fa.AiterFlashAttentionBackend
50 except ImportError:
51 pass
52
53 logger = logging.getLogger(__name__)
54
55 CausalRMSNorm = partial(RMSNorm, eps=1e-5)
56
57
58 class _BlockPoolingAttentionMetadata(Protocol):
59 @property
60 def slot_mapping(self) -> torch.Tensor: ...
61
62
63 def _pad1d(
64 x: torch.Tensor,
65 paddings: tuple[int, int],
66 mode: str = "constant",
67 value: float = 0.0,
68 ) -> torch.Tensor:
69 """Tiny wrapper around F.pad, just to allow for
70 reflect padding on small input.
71 If this is the case, we insert extra 0 padding
72 to the right before the reflection happen.
73 """
74 length = x.shape[-1]
75 padding_left, padding_right = paddings
76 assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
77 if mode == "reflect":
78 max_pad = max(padding_left, padding_right)
79 extra_pad = 0
80 if length <= max_pad:
81 extra_pad = max_pad - length + 1
82 x = F.pad(x, (0, extra_pad))
83 padded = F.pad(x, paddings, mode, value)
84 end = padded.shape[-1] - extra_pad
85 return padded[..., :end]
86 else:
87 return F.pad(x, paddings, mode, value)
88
89
90 class WhisperCausalConv1d(nn.Conv1d):
91 def __init__(
92 self,
93 in_channels: int,
94 out_channels: int,
95 kernel_size: int,
96 stride: int = 1,
97 padding: int = 0,
98 bias: bool = True,
99 ) -> None:
100 super().__init__(
101 in_channels,
102 out_channels,
103 kernel_size,
104 stride=stride,
105 padding=padding,
106 bias=bias,
107 )
108 self._stride = self.stride[0]
109 self._effective_kernel_size = (kernel_size - 1) * self.dilation[0] + 1
110 self._padding_total = self._effective_kernel_size - self._stride
111
112 def forward(self, x: torch.Tensor) -> torch.Tensor:
113 n_frames = (
114 x.shape[-1] - self._effective_kernel_size + self._padding_total
115 ) / self._stride + 1
116 target_length = (math.ceil(n_frames) - 1) * self._stride + (
117 self._effective_kernel_size - self._padding_total
118 )
119 extra_padding = target_length - x.shape[-1]
120 x = _pad1d(x, (self._padding_total, extra_padding), mode="constant")
121 return super().forward(x)
122
123
124 @functools.lru_cache
125 def create_whisper_attention_backend_with_block_pooling(
126 underlying_attn_backend: type[AttentionBackend],
127 block_pool_size: int,
128 sliding_window: int | None = None,
129 ) -> type[AttentionBackend]:
130 prefix = "WhisperCausalAttentionWithBlockPooling_"
131 underlying_builder = underlying_attn_backend.get_builder_cls()
132 underlying_impl = underlying_attn_backend.get_impl_cls()
133
134 class WhisperCausalAttentionWithBlockPoolingBuilder(underlying_builder): # type: ignore
135 # Full cudagraphs only for uniform single-token decode: capture bakes in
136 # tensor addresses, so `build` writes metadata into persistent buffers
137 # (below). Prefill/mixed batches fall back to piecewise.
138 _cudagraph_support: ClassVar[AttentionCGSupport] = (
139 AttentionCGSupport.UNIFORM_SINGLE_TOKEN_DECODE
140 )
141 # Disable the update_block_table fast path: it would bypass block pooling
142 # by splicing the raw slot_mapping/block_table into cached metadata.
143 supports_update_block_table: bool = False
144
145 def __init__(
146 self,
147 kv_cache_spec: AttentionSpec,
148 layer_names: list[str],
149 vllm_config: VllmConfig,
150 device: torch.device,
151 ):
152 kv_cache_spec = replace(
153 kv_cache_spec,
154 block_size=kv_cache_spec.block_size * block_pool_size,
155 tokens_per_state=1,
156 )
157 if isinstance(kv_cache_spec, SlidingWindowSpec):
158 # The manager keeps `sliding_window` in pooled units; the kernel
159 # runs on the expanded sequence, so give it the model's window in
160 # unpooled units (`get_kv_cache_spec` sizes the pooled window so
161 # this window always stays resident).
162 assert sliding_window is not None
163 kv_cache_spec = replace(kv_cache_spec, sliding_window=sliding_window)
164 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
165 # Override model_config-derived values with the actual
166 # encoder values from kv_cache_spec
167 self.num_heads_kv = kv_cache_spec.num_kv_heads
168 self.headdim = kv_cache_spec.head_size
169 # num_heads_q for the encoder is the same as num_kv_heads
170 # (no GQA in whisper encoder)
171 self.num_heads_q = kv_cache_spec.num_kv_heads
172 # Persistent capture-safe buffers for pooled metadata; lazily sized
173 # on first `build` to match the incoming tensors' dtype/device.
174 self._pool_buffers_ready = False
175
176 def _maybe_init_pool_buffers(
177 self, common_attn_metadata: CommonAttentionMetadata
178 ) -> None:
179 if self._pool_buffers_ready:
180 return
181 device = common_attn_metadata.slot_mapping.device
182 sched = self.vllm_config.scheduler_config
183 self._qsl_buf = torch.empty(
184 sched.max_num_seqs + 1,
185 dtype=common_attn_metadata.query_start_loc.dtype,
186 device=device,
187 )
188 self._seq_buf = torch.empty(
189 sched.max_num_seqs,
190 dtype=common_attn_metadata.seq_lens.dtype,
191 device=device,
192 )
193 self._slot_buf = torch.empty(
194 sched.max_num_batched_tokens * block_pool_size,
195 dtype=common_attn_metadata.slot_mapping.dtype,
196 device=device,
197 )
198 self._pool_arange = torch.arange(
199 block_pool_size,
200 dtype=common_attn_metadata.slot_mapping.dtype,
201 device=device,
202 )
203 self._pool_buffers_ready = True
204
205 def build(
206 self,
207 common_prefix_len: int,
208 common_attn_metadata: CommonAttentionMetadata,
209 fast_build: bool = False,
210 ) -> AttentionMetadata:
211 cm = common_attn_metadata
212 self._maybe_init_pool_buffers(cm)
213
214 # Scale query_start_loc / seq_lens into the persistent buffers.
215 n = cm.query_start_loc.numel()
216 assert n <= self._qsl_buf.numel(), (
217 f"{n=} exceeds pooled query_start_loc buffer {self._qsl_buf.numel()}"
218 )
219 qsl_buf = self._qsl_buf[:n]
220 qsl_buf.copy_(cm.query_start_loc)
221 qsl_buf.mul_(block_pool_size)
222
223 r = cm.seq_lens.numel()
224 assert r <= self._seq_buf.numel(), (
225 f"{r=} exceeds pooled seq_lens buffer {self._seq_buf.numel()}"
226 )
227 seq_buf = self._seq_buf[:r]
228 seq_buf.copy_(cm.seq_lens)
229 seq_buf.mul_(block_pool_size)
230
231 # Expand each slot into block_pool_size contiguous slots. Padding
232 # slots (-1) stay -1 after clamp so the KV write is skipped.
233 m = cm.slot_mapping.numel()
234 assert m * block_pool_size <= self._slot_buf.numel(), (
235 f"pooled slot_mapping {m * block_pool_size} exceeds buffer "
236 f"{self._slot_buf.numel()}"
237 )
238 slot_buf = self._slot_buf[: m * block_pool_size]
239 slot_buf.view(m, block_pool_size).copy_(
240 cm.slot_mapping.unsqueeze(1) * block_pool_size + self._pool_arange
241 )
242 slot_buf.clamp_(min=-1)
243
244 # CPU tensors aren't captured; fresh scaled copies avoid mutating
245 # the runner's shared buffers.
246 qsl_cpu = cm.query_start_loc_cpu * block_pool_size
247 seq_lens_cpu_upper_bound = (
248 cm.seq_lens_cpu_upper_bound * block_pool_size
249 if cm.seq_lens_cpu_upper_bound is not None
250 else None
251 )
252
253 new_common_attn_metadata = cm.replace(
254 query_start_loc=qsl_buf,
255 query_start_loc_cpu=qsl_cpu,
256 seq_lens=seq_buf,
257 seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
258 num_actual_tokens=cm.num_actual_tokens * block_pool_size,
259 max_query_len=cm.max_query_len * block_pool_size,
260 max_seq_len=cm.max_seq_len * block_pool_size,
261 slot_mapping=slot_buf,
262 )
263 return super().build(
264 common_prefix_len * block_pool_size,
265 new_common_attn_metadata,
266 fast_build,
267 )
268
269 # NOTE: We need a custom impl so we can use the transformed slot_mapping
270 # computed by `WhisperCausalAttentionWithBlockPoolingBuilder` instead of
271 # the one from `forward_context.slot_mapping` (gpu_model_runner).
272 # This follows the same pattern as CrossAttentionImpl.
273 class WhisperCausalAttentionWithBlockPoolingImpl(underlying_impl): # type: ignore[valid-type,misc]
274 def forward(
275 self,
276 layer: torch.nn.Module,
277 query: torch.Tensor,
278 key: torch.Tensor,
279 value: torch.Tensor,
280 kv_cache: torch.Tensor,
281 attn_metadata: _BlockPoolingAttentionMetadata | None,
282 output: torch.Tensor,
283 output_scale: torch.Tensor | None = None,
284 output_block_scale: torch.Tensor | None = None,
285 ) -> torch.Tensor:
286 if (
287 not underlying_attn_backend.forward_includes_kv_cache_update
288 and attn_metadata is not None
289 and layer.kv_sharing_target_layer_name is None
290 and key is not None
291 and value is not None
292 ):
293 self.do_kv_cache_update(
294 layer, key, value, kv_cache, attn_metadata.slot_mapping
295 )
296
297 return super().forward(
298 layer,
299 query,
300 key,
301 value,
302 kv_cache,
303 attn_metadata,
304 output,
305 output_scale,
306 output_block_scale,
307 )
308
309 _SUPPORTED_BACKENDS = tuple(
310 b
311 for b in (
312 AiterFlashAttentionBackend,
313 CPUAttentionBackend,
314 FlashAttentionBackend,
315 RocmAttentionBackend,
316 TritonAttentionBackend,
317 )
318 if b is not None
319 )
320
321 if not issubclass(underlying_attn_backend, _SUPPORTED_BACKENDS):
322 raise NotImplementedError(
323 f"{underlying_attn_backend} is not yet supported."
324 "Contributions to support more backends are much "
325 "appreciated."
326 )
327
328 if not issubclass(
329 underlying_attn_backend, (CPUAttentionBackend, FlashAttentionBackend)
330 ):
331 logger.info(
332 "Using %s for Whisper causal attention with block pooling. "
333 "This backend was recently enabled for this model. "
334 "If you encounter any accuracy or performance issues, "
335 "please open an issue at "
336 "https://github.com/vllm-project/vllm/issues "
337 "with the [ROCm] tag so it can be triaged by the "
338 "appropriate team.",
339 underlying_attn_backend.get_name(),
340 )
341
342 attn_backend = subclass_attention_backend_with_overrides(
343 name_prefix=prefix,
344 attention_backend_cls=underlying_attn_backend,
345 overrides={
346 "get_builder_cls": lambda: WhisperCausalAttentionWithBlockPoolingBuilder,
347 "get_impl_cls": lambda: WhisperCausalAttentionWithBlockPoolingImpl,
348 "forward_includes_kv_cache_update": True,
349 },
350 )
351
352 return attn_backend
353
354
355 class WhisperCausalAttentionWithBlockPooling(Attention):
356 """Attention layer with block pooling."""
357
358 def __init__(
359 self,
360 num_heads: int,
361 head_size: int,
362 scale: float,
363 num_kv_heads: int | None = None,
364 alibi_slopes: list[float] | None = None,
365 cache_config: CacheConfig | None = None,
366 quant_config: QuantizationConfig | None = None,
367 logits_soft_cap: float | None = None,
368 per_layer_sliding_window: int | None = None,
369 prefix: str = "",
370 attn_type: str = AttentionType.DECODER,
371 kv_sharing_target_layer_name: str | None = None,
372 block_pool_size: int = 1,
373 attn_backend: type[AttentionBackend] | None = None,
374 **extra_impl_args,
375 ) -> None:
376 self.block_pool_size = block_pool_size
377 dtype = torch.get_default_dtype()
378
379 if cache_config is not None:
380 kv_cache_dtype = cache_config.cache_dtype
381 else:
382 kv_cache_dtype = "auto"
383
384 underlying_attn_backend = get_attn_backend(
385 head_size,
386 dtype,
387 kv_cache_dtype,
388 attn_type=attn_type,
389 )
390 attn_backend = create_whisper_attention_backend_with_block_pooling(
391 underlying_attn_backend, block_pool_size, per_layer_sliding_window
392 )
393
394 super().__init__(
395 num_heads=num_heads,
396 head_size=head_size,
397 scale=scale,
398 num_kv_heads=num_kv_heads,
399 alibi_slopes=alibi_slopes,
400 cache_config=cache_config,
401 quant_config=quant_config,
402 logits_soft_cap=logits_soft_cap,
403 per_layer_sliding_window=per_layer_sliding_window,
404 prefix=prefix,
405 attn_type=attn_type,
406 kv_sharing_target_layer_name=kv_sharing_target_layer_name,
407 attn_backend=attn_backend,
408 **extra_impl_args,
409 )
410
411 def get_kv_cache_spec(self, vllm_config: VllmConfig):
412 kv_cache_spec = super().get_kv_cache_spec(vllm_config)
413 assert isinstance(kv_cache_spec, AttentionSpec)
414 kv_cache_spec = replace(
415 kv_cache_spec,
416 tokens_per_state=Fraction(1, self.block_pool_size),
417 )
418 if isinstance(kv_cache_spec, SlidingWindowSpec):
419 # The manager counts blocks in pooled units, so express the window in
420 # pooled units to avoid reserving `block_pool_size`x too many blocks.
421 # The `+1` is an eviction margin: block-aligned eviction only keeps
422 # `pooled - 1` pooled tokens, so this guarantees the kernel's full
423 # (unpooled) window still stays resident.
424 pooled = cdiv(kv_cache_spec.sliding_window, self.block_pool_size) + 1
425 kv_cache_spec = replace(kv_cache_spec, sliding_window=pooled)
426 return kv_cache_spec
427
428
429 class WhisperCausalAttention(nn.Module):
430 def __init__(
431 self,
432 embed_dim: int,
433 num_heads: int,
434 head_dim: int,
435 max_position_embeddings: int,
436 bias: bool = True,
437 attn_type: AttentionType = AttentionType.DECODER,
438 per_layer_sliding_window: int | None = None,
439 block_pool_size: int = 1,
440 cache_config: CacheConfig | None = None,
441 quant_config: QuantizationConfig | None = None,
442 prefix: str = "",
443 ):
444 super().__init__()
445 self.embed_dim = embed_dim
446 tp_size = get_tensor_model_parallel_world_size()
447 self.total_num_heads = num_heads
448 assert self.total_num_heads % tp_size == 0
449 self.num_heads = self.total_num_heads // tp_size
450 if self.total_num_heads >= tp_size:
451 # Number of heads is greater than TP size, so we partition
452 # the KV heads across multiple tensor parallel GPUs.
453 assert self.total_num_heads % tp_size == 0
454 else:
455 # Number of heads is less than TP size, so we replicate
456 # the KV heads across multiple tensor parallel GPUs.
457 assert tp_size % self.total_num_heads == 0
458 self.num_kv_heads = max(1, self.total_num_heads // tp_size)
459 self.head_dim = head_dim
460 self.q_size = self.num_heads * self.head_dim
461 self.kv_size = self.num_kv_heads * self.head_dim
462 self.attn_type = attn_type
463
464 self.scaling = self.head_dim**-0.5
465
466 self._init_qkv(embed_dim, bias, quant_config, prefix=prefix)
467 self.out_proj = RowParallelLinear(
468 input_size=self.total_num_heads * self.head_dim,
469 output_size=embed_dim,
470 bias=bias,
471 quant_config=quant_config,
472 prefix=f"{prefix}.out_proj",
473 )
474 assert block_pool_size > 1, (
475 f"Causal attention only supports block_pool_size>1, not {block_pool_size}."
476 )
477 self.attn = WhisperCausalAttentionWithBlockPooling(
478 self.num_heads,
479 self.head_dim,
480 self.scaling,
481 num_kv_heads=self.num_kv_heads,
482 cache_config=cache_config,
483 quant_config=quant_config,
484 prefix=f"{prefix}.attn",
485 attn_type=AttentionType.DECODER,
486 per_layer_sliding_window=per_layer_sliding_window,
487 block_pool_size=block_pool_size,
488 )
489
490 assert per_layer_sliding_window is not None, (
491 "rope can only used in combination with a sliding window"
492 )
493 self._init_rotary_emb(max_position_embeddings)
494
495 def _init_rotary_emb(self, max_position_embeddings: int) -> None:
496 self.rotary_emb = get_rope(
497 self.head_dim,
498 max_position=max_position_embeddings,
499 is_neox_style=False,
500 rope_parameters={"rope_theta": 1e6},
501 )
502
503 def _init_qkv(
504 self,
505 embed_dim: int,
506 bias: bool = True,
507 quant_config: QuantizationConfig | None = None,
508 prefix: str = "",
509 ) -> None:
510 self.qkv_proj = QKVParallelLinear(
511 hidden_size=embed_dim,
512 head_size=self.head_dim,
513 total_num_heads=self.total_num_heads,
514 total_num_kv_heads=self.total_num_heads,
515 bias=bias,
516 quant_config=quant_config,
517 prefix=f"{prefix}.qkv_proj",
518 )
519
520 def forward(
521 self,
522 hidden_states: torch.Tensor,
523 positions: torch.Tensor | None = None,
524 ):
525 qkv, _ = self.qkv_proj(hidden_states)
526 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
527
528 assert positions is not None
529 q, k = self.rotary_emb(positions, q, k)
530
531 attn_output = self.attn(q, k, v)
532
533 output, _ = self.out_proj(attn_output)
534
535 return output
536
537
538 class WhisperCausalEncoderLayer(nn.Module):
539 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
540 super().__init__()
541 config = vllm_config.model_config.hf_config
542 sliding_window = getattr(config, "sliding_window", None)
543 block_pool_size = config.block_pool_size
544 assert block_pool_size > 1
545
546 cache_config = vllm_config.cache_config
547 quant_config = vllm_config.quant_config
548
549 self.embed_dim = config.d_model
550 self.head_dim = self.embed_dim // config.encoder_attention_heads
551 self.self_attn = WhisperCausalAttention(
552 embed_dim=self.embed_dim,
553 num_heads=config.encoder_attention_heads,
554 head_dim=config.encoder_head_dim,
555 max_position_embeddings=config.max_position_embeddings,
556 block_pool_size=block_pool_size,
557 per_layer_sliding_window=sliding_window,
558 cache_config=cache_config,
559 quant_config=quant_config,
560 prefix=f"{prefix}.self_attn",
561 )
562 self.self_attn_layer_norm = CausalRMSNorm(self.embed_dim)
563
564 self.mlp = MistralMLP(
565 hidden_size=config.d_model,
566 intermediate_size=config.encoder_ffn_dim,
567 hidden_act="silu",
568 quant_config=quant_config,
569 bias=True,
570 gate_up_proj_bias=False,
571 prefix=f"{prefix}.mlp",
572 )
573 self.final_layer_norm = CausalRMSNorm(self.embed_dim)
574
575 def forward(
576 self,
577 hidden_states: torch.Tensor,
578 positions: torch.Tensor | None = None,
579 ):
580 residual = hidden_states
581 hidden_states = self.self_attn_layer_norm(hidden_states)
582 hidden_states = self.self_attn(hidden_states=hidden_states, positions=positions)
583 hidden_states = residual + hidden_states
584 residual = hidden_states
585 hidden_states = self.final_layer_norm(hidden_states)
586 hidden_states = self.mlp(hidden_states)
587 hidden_states = residual + hidden_states
588
589 return hidden_states
590
591
592 class WhisperCausalEncoder(nn.Module):
593 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
594 super().__init__()
595 config = vllm_config.model_config.hf_config
596 embed_dim = config.d_model
597
598 assert WhisperPosEmbedType(config.pos_embed) == WhisperPosEmbedType.ROPE
599 assert config.is_causal
600
601 self.num_mel_bins = config.num_mel_bins
602 self.max_source_positions = config.max_source_positions
603 self.embed_scale = math.sqrt(embed_dim) if config.scale_embedding else 1.0
604
605 self.conv1 = WhisperCausalConv1d(self.num_mel_bins, embed_dim, kernel_size=3)
606 self.conv2 = WhisperCausalConv1d(embed_dim, embed_dim, stride=2, kernel_size=3)
607
608 self.total_stride = self.conv1.stride[0] * self.conv2.stride[0]
609 self.start_layer, self.end_layer, self.layers = make_layers(
610 config.encoder_layers,
611 lambda prefix: WhisperCausalEncoderLayer(
612 vllm_config=vllm_config, prefix=f"{prefix}.layers"
613 ),
614 prefix=f"{prefix}.layers",
615 )
616 self.layer_norm = CausalRMSNorm(config.d_model)
617
618 def forward_conv(
619 self, input_features: torch.Tensor | list[torch.Tensor]
620 ) -> torch.Tensor:
621 hidden_states = []
622 for features in input_features:
623 embeds = nn.functional.gelu(self.conv1(features))
624 embeds = nn.functional.gelu(self.conv2(embeds))
625
626 embeds = embeds.transpose(-1, -2).to(embeds.dtype)
627
628 hidden_states.append(embeds)
629
630 hidden_states = torch.cat(hidden_states)
631
632 return hidden_states
633
634 def forward(
635 self, hidden_states: torch.Tensor, positions: torch.Tensor
636 ) -> torch.Tensor:
637 for encoder_layer in self.layers:
638 hidden_states = encoder_layer(hidden_states, positions)
639
640 hidden_states = self.layer_norm(hidden_states)
641 return hidden_states