[Model][LoRA] Add tower/connector LoRA support for Ultravox (#48215)

Signed-off-by: arthurgao2003 <arthurgao2003@gmail.com> Signed-off-by: Arthur Gao <arthurgao2003@gmail.com> Signed-off-by: 高杨懿 <15312196+gao-yangyi@user.noreply.gitee.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 高杨懿 <15312196+gao-yangyi@user.noreply.gitee.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Yangyi Gao committed Aug 13, 2026 at 13:57 UTC b908a21f9a449592ca06352381d9118dbc39c6d2
2 files changed +345 -129
vllm/model_executor/models/ultravox.py
+277 -119
@@ -4,27 +4,26 @@
4 # Adapted from https://github.com/fixie-ai/ultravox/blob/ecd58c4041030bae2ad15aa6bcf04ab43199ea02/ultravox/model/ultravox_model.py
5 """PyTorch Ultravox model."""
6
7 -import copy
7 +import math
8 from collections.abc import Iterable, Mapping, Sequence
9 -from types import SimpleNamespace
9 from typing import Annotated, Any, Literal, TypeAlias
10
11 +import numpy as np
12 import torch
13 from torch import nn
14 from torch.nn import functional as F
15 from transformers import BatchFeature, ProcessorMixin
16 -from transformers.modeling_utils import ModuleUtilsMixin
16 from transformers.models.whisper import WhisperFeatureExtractor
18 -from transformers.models.whisper.modeling_whisper import (
19 - WhisperEncoder,
20 - WhisperEncoderLayer,
21 -)
17
18 from vllm.config import VllmConfig
19 from vllm.config.multimodal import BaseDummyOptions
20 +from vllm.distributed import get_tensor_model_parallel_world_size
21 from vllm.inputs import MultiModalDataDict
22 from vllm.model_executor.layers.activation import MulAndSilu, get_act_fn
23 +from vllm.model_executor.layers.attention import MMEncoderAttention
24 from vllm.model_executor.layers.layernorm import RMSNorm
25 +from vllm.model_executor.layers.linear import ReplicatedLinear
26 +from vllm.model_executor.layers.quantization import QuantizationConfig
27 from vllm.model_executor.model_loader import DefaultModelLoader
28 from vllm.model_executor.models.module_mapping import MultiModelKeys
29 from vllm.multimodal import MULTIMODAL_REGISTRY
@@ -59,6 +58,11 @@ from .utils import (
58 init_vllm_registered_model,
59 maybe_prefix,
60 )
61 +from .whisper import (
62 + WhisperEncoder,
63 + WhisperEncoderLayer,
64 + _create_fake_bias_for_k_proj,
65 +)
66
67 _AUDIO_PLACEHOLDER_OVERRIDE = "<|audio|>"
68 _MAX_ENCODER_BATCH_SIZE = 16
@@ -304,14 +308,97 @@ class StackAudioFrames(nn.Module):
308 return audio_embeds
309
310
311 +def _build_chunk_attn_metadata(
312 + attn: MMEncoderAttention,
313 + feature_lens: torch.Tensor,
314 + seq_len: int,
315 + hidden_size: int,
316 + device: torch.device,
317 +) -> dict[str, torch.Tensor | None]:
318 + """Segmented varlen attention metadata for a padded batch of audio chunks.
319 +
320 + Each padded row contributes up to two sequences to `cu_seqlens`: its valid
321 + frames and its padding tail. Attention therefore never crosses a
322 + valid/padding boundary (equivalent to the key-padding mask the HF
323 + implementation uses), while every row still flows through the
324 + (potentially LoRA-wrapped) linears, keeping the per-chunk token counts
325 + constant as required by `get_num_mm_encoder_tokens` /
326 + `get_num_mm_connector_tokens`. Queries at padding positions produce
327 + (garbage) outputs, which are trimmed by `audio_token_len` downstream.
328 + """
329 + batch_size = feature_lens.shape[0]
330 + starts = np.arange(batch_size, dtype=np.int64) * seq_len
331 + lens_np = feature_lens.cpu().numpy().astype(np.int64)
332 + bounds = np.stack([starts + lens_np, starts + seq_len], axis=1).reshape(-1)
333 + cu_seqlens_np = np.concatenate(([0], bounds))
334 + # Fully-valid rows produce empty padding segments; drop the duplicates.
335 + cu_seqlens_np = np.unique(cu_seqlens_np).astype(np.int32)
336 +
337 + attn_backend = attn.attn_backend
338 + sequence_lengths = MMEncoderAttention.maybe_compute_seq_lens(
339 + attn_backend, cu_seqlens_np, device
340 + )
341 + max_seqlen = torch.tensor(
342 + MMEncoderAttention.compute_max_seqlen(attn_backend, cu_seqlens_np),
343 + dtype=torch.int32,
344 + )
345 + cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
346 + attn_backend,
347 + cu_seqlens_np,
348 + hidden_size,
349 + get_tensor_model_parallel_world_size(),
350 + device,
351 + )
352 + return {
353 + "cu_seqlens": cu_seqlens,
354 + "max_seqlen": max_seqlen,
355 + "sequence_lengths": sequence_lengths,
356 + }
357 +
358 +
359 +# Maps HF whisper encoder layer names to vLLM's fused qkv_proj and nested
360 +# mlp; `_create_fake_bias_for_k_proj` supplies the fused bias' k-slice
361 +# (HF whisper k_proj has no bias).
362 +_WHISPER_ENCODER_MAPPER = WeightsMapper(
363 + orig_to_new_substr={".fc1.": ".mlp.fc1.", ".fc2.": ".mlp.fc2."},
364 + orig_to_new_stacked={
365 + ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
366 + ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
367 + ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
368 + },
369 +)
370 +
371 +
372 +def _load_whisper_layer_weights(
373 + module: nn.Module, weights: Iterable[tuple[str, torch.Tensor]]
374 +) -> set[str]:
375 + weights = _create_fake_bias_for_k_proj(weights, ".self_attn.k_proj.weight")
376 + loader = AutoWeightsLoader(module)
377 + return loader.load_weights(weights, mapper=_WHISPER_ENCODER_MAPPER)
378 +
379 +
380 class UltravoxFeedForwardProjector(nn.Module):
308 - def __init__(self, config: UltravoxConfig):
381 + def __init__(
382 + self,
383 + config: UltravoxConfig,
384 + *,
385 + quant_config: QuantizationConfig | None = None,
386 + prefix: str = "",
387 + ):
388 super().__init__()
389 self.hidden_dim = config.hidden_size
390 self._pad_and_stack = StackAudioFrames(config.stack_factor)
391 dim_in = config.audio_config.hidden_size * config.stack_factor
392 self.ln_pre = RMSNorm(dim_in)
314 - self.linear_1 = nn.Linear(dim_in, self.hidden_dim, bias=False)
393 + # ReplicatedLinear (a vLLM-native linear) so the connector can be
394 + # wrapped for LoRA; a plain nn.Linear is left unwrapped by from_layer.
395 + self.linear_1 = ReplicatedLinear(
396 + dim_in,
397 + self.hidden_dim,
398 + bias=False,
399 + quant_config=quant_config,
400 + prefix=f"{prefix}.linear_1",
401 + )
402 dim_mid = self.hidden_dim
403
404 if config.projector_act == "swiglu":
@@ -321,7 +408,13 @@ class UltravoxFeedForwardProjector(nn.Module):
408 self.act = get_act_fn(config.projector_act)
409
410 dim_out = config.text_config.hidden_size
324 - self.linear_2 = nn.Linear(dim_mid, dim_out, bias=False)
411 + self.linear_2 = ReplicatedLinear(
412 + dim_mid,
413 + dim_out,
414 + bias=False,
415 + quant_config=quant_config,
416 + prefix=f"{prefix}.linear_2",
417 + )
418
419 # Ultravox v0.4.1 and below use layer_norm after the second linear layer
420 # while v0.5.0 and above uses layer_norm after the first linear layer.
@@ -337,42 +430,64 @@ class UltravoxFeedForwardProjector(nn.Module):
430 ) -> torch.Tensor:
431 audio_features = self._pad_and_stack(audio_features)
432 audio_features = self.ln_pre(audio_features)
340 - hidden_states = self.linear_1(audio_features)
433 + hidden_states, _ = self.linear_1(audio_features)
434 hidden_states = self.act(hidden_states)
435 hidden_states = self.ln_mid(hidden_states)
343 - hidden_states = self.linear_2(hidden_states)
436 + hidden_states, _ = self.linear_2(hidden_states)
437 hidden_states = self.ln_post(hidden_states)
438 return hidden_states
439
440
348 -class UltravoxTransformerProjector(nn.Module, ModuleUtilsMixin):
349 - def __init__(self, config: UltravoxConfig):
441 +class UltravoxTransformerProjector(nn.Module):
442 + def __init__(
443 + self,
444 + vllm_config: VllmConfig,
445 + *,
446 + prefix: str = "",
447 + ):
448 super().__init__()
351 - self.config = SimpleNamespace(is_decoder=False)
449 + config: UltravoxConfig = vllm_config.model_config.hf_config
450 + quant_config = vllm_config.quant_config
451 + audio_config = config.audio_config
452
453 self._pad_and_stack = StackAudioFrames(config.stack_factor)
354 - dim_in = config.audio_config.hidden_size * config.stack_factor
355 -
356 - projector_audio_config = copy.deepcopy(config.audio_config)
454 + dim_in = audio_config.hidden_size * config.stack_factor
455
456 self.ln_pre = RMSNorm(dim_in)
359 - self.linear_in = nn.Linear(dim_in, projector_audio_config.d_model)
457 + self.linear_in = ReplicatedLinear(
458 + dim_in,
459 + audio_config.d_model,
460 + bias=True,
461 + quant_config=quant_config,
462 + prefix=f"{prefix}.linear_in",
463 + )
464
465 self.embed_positions = nn.Embedding(
362 - projector_audio_config.max_source_positions,
363 - projector_audio_config.d_model,
466 + audio_config.max_source_positions,
467 + audio_config.d_model,
468 )
469
470 + audio_vllm_config = vllm_config.with_hf_config(
471 + audio_config, architectures=["UltravoxModel"]
472 + )
473 self.layers = nn.ModuleList(
474 [
368 - WhisperEncoderLayer(projector_audio_config)
369 - for _ in range(config.num_projector_layers)
475 + WhisperEncoderLayer(
476 + vllm_config=audio_vllm_config,
477 + prefix=f"{prefix}.layers.{idx}",
478 + )
479 + for idx in range(config.num_projector_layers)
480 ]
481 )
372 -
373 - self.ln_post = RMSNorm(projector_audio_config.d_model)
374 - self.linear_out = nn.Linear(
375 - projector_audio_config.d_model, config.text_config.hidden_size
482 + self._d_model = audio_config.d_model
483 +
484 + self.ln_post = RMSNorm(audio_config.d_model)
485 + self.linear_out = ReplicatedLinear(
486 + audio_config.d_model,
487 + config.text_config.hidden_size,
488 + bias=True,
489 + quant_config=quant_config,
490 + prefix=f"{prefix}.linear_out",
491 )
492
493 def forward(
@@ -380,100 +495,65 @@ class UltravoxTransformerProjector(nn.Module, ModuleUtilsMixin):
495 ) -> torch.Tensor:
496 audio_features = self._pad_and_stack(audio_features)
497
383 - max_len_stacked = audio_features.shape[1]
384 - attention_mask = torch.arange(max_len_stacked, device=audio_features.device)[
385 - None, :
386 - ].lt(audio_token_len[:, None])
387 - extended_attention_mask = self.get_extended_attention_mask(
388 - attention_mask, attention_mask.shape, audio_features.dtype
389 - )
390 -
498 hidden_states = self.ln_pre(audio_features)
392 - hidden_states = self.linear_in(hidden_states)
499 + hidden_states, _ = self.linear_in(hidden_states)
500
501 positions = self.embed_positions(
502 torch.arange(hidden_states.size(1), device=hidden_states.device)
503 )
504 hidden_states = hidden_states + positions
505
506 + batch_size, seq_len, d_model = hidden_states.shape
507 + attn_metadata = _build_chunk_attn_metadata(
508 + self.layers[0].self_attn.attn,
509 + audio_token_len,
510 + seq_len,
511 + d_model,
512 + hidden_states.device,
513 + )
514 + hidden_states = hidden_states.reshape(batch_size * seq_len, d_model)
515 for layer in self.layers:
400 - hidden_states = layer(
401 - hidden_states,
402 - attention_mask=extended_attention_mask,
403 - )
404 - # BC version that allows for the old tupled output
405 - if isinstance(hidden_states, tuple):
406 - hidden_states = hidden_states[0]
516 + hidden_states = layer(hidden_states, **attn_metadata)
517 + hidden_states = hidden_states.reshape(batch_size, seq_len, d_model)
518
519 hidden_states = self.ln_post(hidden_states)
409 - hidden_states = self.linear_out(hidden_states)
520 + hidden_states, _ = self.linear_out(hidden_states)
521 return hidden_states
522
523 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
524 + return _load_whisper_layer_weights(self, weights)
525
413 -class ModifiedWhisperEncoder(WhisperEncoder):
414 - """
415 - Encoder portion of OpenAI's Whisper model.
416 -
417 - This implementation is a slightly modified version of HF Transformers'
418 - Whisper Encoder, with only a few fixes:
419 - 1. base_model_prefix updated to allow for doing `.from_pretrained`
420 - directly on the encoder
421 - 2. allow less than 30 second of audio padding to be passed in:
422 - - relaxed ValueError check for `input_features` length to be less
423 - than or equal to `expected_seq_length` instead of strictly equal
424 - - embed_pos is now sliced to match the length of `inputs_embeds`
425 -
426 - Original: https://github.com/huggingface/transformers/blob/main/src/transformers/models/whisper/modeling_whisper.py
427 - See commentary: https://github.com/huggingface/transformers/issues/25744
428 - """
526
430 - base_model_prefix = "model.encoder"
527 +class UltravoxWhisperEncoder(WhisperEncoder):
528 + """Ultravox's `ModifiedWhisperEncoder` on top of vLLM's `WhisperEncoder`.
529
432 - def __init__(self, *args, **kwargs):
433 - super().__init__(*args, **kwargs)
434 - self.config.is_decoder = False
530 + Like the original (a modified HF whisper encoder,
531 + see https://github.com/huggingface/transformers/issues/25744), it accepts
532 + mel inputs shorter than 30s (positions are sliced to the input length) and
533 + confines attention to each chunk's valid frames based on `audio_lens`
534 + (via segmented `cu_seqlens` instead of a dense key-padding mask), so its
535 + outputs match the HF implementation for valid positions. The linears are
536 + vLLM-native so the tower can be wrapped for LoRA.
537 + """
538
539 @property
437 - def max_context_length(self):
438 - return (
439 - self.config.max_source_positions
440 - * self.conv1.stride[0]
441 - * self.conv2.stride[0]
442 - )
540 + def dtype(self) -> torch.dtype:
541 + return self.conv1.weight.dtype
542
444 - def get_attention_mask_by_audio_len(
445 - self, audio_lens: torch.Tensor | None, hidden_states: torch.Tensor
446 - ):
447 - """
448 - Create attention mask based on audio lengths to mask out padding tokens
449 - For each sample in batch:
450 - - Convert raw audio length to feature length after convolutions
451 - - Create bool mask: True for valid positions and False for padding
452 - - Convert to attention mask format expected by transformer layers
453 - (1.0 for positions to attend to, large negative for positions to ignore)
454 - This masking ensures consistent behavior between training and inference
455 - by preventing the model from attending to padding tokens in both cases
456 - """
457 - if audio_lens is None:
458 - return None
543 + @property
544 + def max_context_length(self) -> int:
545 + return self.max_source_positions * self.total_stride
546
460 - audio_feature_len = self._get_feat_extract_output_lengths(audio_lens)
461 - max_seq_len = hidden_states.shape[1]
462 - attention_mask = torch.arange(max_seq_len, device=hidden_states.device)[
463 - None, :
464 - ].lt(audio_feature_len.view(-1, 1))
465 - attention_mask = self.get_extended_attention_mask(
466 - attention_mask,
467 - None,
468 - dtype=hidden_states.dtype,
469 - )
470 - return attention_mask
547 + def _get_feat_extract_output_lengths(
548 + self, input_lengths: torch.Tensor
549 + ) -> torch.Tensor:
550 + return (input_lengths - 1) // 2 + 1
551
552 def forward(
553 self,
554 input_features: torch.Tensor,
475 - audio_lens: torch.Tensor | None = None,
476 - ):
555 + audio_lens: torch.Tensor,
556 + ) -> torch.Tensor:
557 expected_seq_length = self.max_context_length
558 if input_features.shape[-1] > expected_seq_length:
559 raise ValueError(
@@ -489,25 +569,27 @@ class ModifiedWhisperEncoder(WhisperEncoder):
569 inputs_embeds = inputs_embeds.permute(0, 2, 1)
570 embed_pos = self.embed_positions.weight[: inputs_embeds.size(-2)]
571
492 - hidden_states = inputs_embeds + embed_pos
493 - hidden_states = nn.functional.dropout(
494 - hidden_states, p=self.dropout, training=self.training
495 - )
496 -
497 - attention_mask = self.get_attention_mask_by_audio_len(audio_lens, hidden_states)
572 + hidden_states = (inputs_embeds + embed_pos).to(inputs_embeds.dtype)
573
574 + batch_size, seq_len, d_model = hidden_states.shape
575 + attn_metadata = _build_chunk_attn_metadata(
576 + self.layers[0].self_attn.attn,
577 + self._get_feat_extract_output_lengths(audio_lens),
578 + seq_len,
579 + d_model,
580 + hidden_states.device,
581 + )
582 + hidden_states = hidden_states.reshape(batch_size * seq_len, d_model)
583 for encoder_layer in self.layers:
500 - hidden_states = encoder_layer(
501 - hidden_states,
502 - attention_mask,
503 - )
504 - # BC version that allows for the old tupled output
505 - if isinstance(hidden_states, tuple):
506 - hidden_states = hidden_states[0]
584 + hidden_states = encoder_layer(hidden_states, **attn_metadata)
585 + hidden_states = hidden_states.reshape(batch_size, seq_len, d_model)
586
587 hidden_states = self.layer_norm(hidden_states)
588 return hidden_states
589
590 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
591 + return _load_whisper_layer_weights(self, weights)
592 +
593
594 @MULTIMODAL_REGISTRY.register_processor(
595 UltravoxMultiModalProcessor,
@@ -521,7 +603,13 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
603 }
604
605 hf_to_vllm_mapper = WeightsMapper(
524 - orig_to_new_prefix={"audio_tower.model.encoder.": "audio_tower."}
606 + orig_to_new_prefix={
607 + "audio_tower.model.encoder.": "audio_tower.",
608 + # A whisper checkpoint loaded via `audio_model_id` also carries
609 + # decoder and LM-head weights the tower does not use.
610 + "audio_tower.model.": None,
611 + "audio_tower.proj_out.": None,
612 + }
613 )
614
615 @classmethod
@@ -535,10 +623,20 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
623 super().__init__()
624 config: UltravoxConfig = vllm_config.model_config.hf_config
625 multimodal_config = vllm_config.model_config.multimodal_config
626 + quant_config = vllm_config.quant_config
627 + lora_config = vllm_config.lora_config
628 self.config = config
629 self.multi_modal_config = multimodal_config
630 assert self.multi_modal_config
631
632 + # LoRA on the tower/connector requires per-item token counts that are
633 + # predictable from the placeholder count alone (see
634 + # `get_num_mm_encoder_tokens`), so pad every audio chunk's mel input
635 + # to the tower's full context instead of the batch's max length.
636 + self.pad_audio_to_max_context = bool(
637 + lora_config is not None and lora_config.enable_tower_connector_lora
638 + )
639 +
640 self.configure_mm_token_handling(
641 self.config.vocab_size,
642 [self.config.audio_token_index],
@@ -569,11 +667,24 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
667 )
668
669 with self._mark_tower_model(vllm_config, "audio"):
572 - self.audio_tower = ModifiedWhisperEncoder(config.audio_config)
670 + self.audio_tower = UltravoxWhisperEncoder(
671 + vllm_config=vllm_config.with_hf_config(
672 + config.audio_config, architectures=["UltravoxModel"]
673 + ),
674 + prefix=maybe_prefix(prefix, "audio_tower"),
675 + enable_pp=False,
676 + )
677 if config.num_projector_layers > 0:
574 - self.multi_modal_projector = UltravoxTransformerProjector(config)
678 + self.multi_modal_projector = UltravoxTransformerProjector(
679 + vllm_config,
680 + prefix=maybe_prefix(prefix, "multi_modal_projector"),
681 + )
682 else:
576 - self.multi_modal_projector = UltravoxFeedForwardProjector(config)
683 + self.multi_modal_projector = UltravoxFeedForwardProjector(
684 + config,
685 + quant_config=quant_config,
686 + prefix=maybe_prefix(prefix, "multi_modal_projector"),
687 + )
688
689 with self._mark_language_model(vllm_config):
690 self.language_model = init_vllm_registered_model(
@@ -596,6 +707,32 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
707 tower_model="audio_tower.",
708 )
709
710 + def _get_max_tokens_per_chunk(self) -> int:
711 + # Audio is chunked at the tower's full context (30s -> 1500 encoder
712 + # frames); the projector stacks `stack_factor` frames per LM token, so
713 + # a full chunk yields ceil(max_source_positions / stack_factor) tokens
714 + # and only an audio's last chunk can yield fewer.
715 + return math.ceil(
716 + self.config.audio_config.max_source_positions / self.config.stack_factor
717 + )
718 +
719 + def get_num_mm_encoder_tokens(self, num_audio_tokens: int) -> int:
720 + # With `pad_audio_to_max_context` (enforced when tower/connector LoRA
721 + # is enabled), the tower's LoRA-wrapped linears always run on
722 + # `max_source_positions` conv-downsampled tokens per chunk, regardless
723 + # of the valid frame count.
724 + num_chunks = math.ceil(num_audio_tokens / self._get_max_tokens_per_chunk())
725 + return num_chunks * self.config.audio_config.max_source_positions
726 +
727 + def get_num_mm_connector_tokens(self, num_encoder_tokens: int) -> int:
728 + # The connector runs on the frame-stacked tower output. Stacking pads
729 + # each chunk to a multiple of `stack_factor`, so this is
730 + # ceil(max_source_positions / stack_factor) tokens per chunk (188 for
731 + # whisper's 1500), not `num_encoder_tokens // stack_factor` (187).
732 + max_source_positions = self.config.audio_config.max_source_positions
733 + num_chunks = num_encoder_tokens // max_source_positions
734 + return num_chunks * self._get_max_tokens_per_chunk()
735 +
736 def _audio_features_to_embeddings(
737 self,
738 input_features: torch.Tensor,
@@ -606,9 +743,19 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
743 batch_size = audio_features.size(0)
744 audio_embeddings = []
745
609 - # Process audio features in batches to keep memory usage predictable
610 - for start in range(0, batch_size, _MAX_ENCODER_BATCH_SIZE):
611 - end = min(start + _MAX_ENCODER_BATCH_SIZE, batch_size)
746 + # Process audio features in batches to keep memory usage predictable.
747 + # With tower/connector LoRA, the punica kernels always map the first
748 + # `x.size(0)` entries of the token-LoRA mapping (set once for the whole
749 + # scheduled encoder batch) to the rows of each linear's input, so
750 + # splitting the batch would apply the wrong LoRA ids to every chunk
751 + # after the first sub-batch; process all chunks in a single pass.
752 + encoder_batch_size = (
753 + max(batch_size, 1)
754 + if self.pad_audio_to_max_context
755 + else _MAX_ENCODER_BATCH_SIZE
756 + )
757 + for start in range(0, batch_size, encoder_batch_size):
758 + end = min(start + encoder_batch_size, batch_size)
759 # Process through audio tower
760 batch_features = self.audio_tower(
761 audio_features[start:end], audio_lens[start:end]
@@ -662,6 +809,17 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
809 # [[B1, 80, M1], [B2, 80, M2]] -> [B1+B2, 80, max(M1, M2)]
810 audio_features = pad_and_concat_to_dim3(audio_input["data"])
811
812 + if self.pad_audio_to_max_context:
813 + # Pad every chunk to the tower's full context so the number of
814 + # tokens processed by the tower/connector per chunk is constant,
815 + # keeping the LoRA token mappings computed by
816 + # `get_num_mm_encoder_tokens`/`get_num_mm_connector_tokens` exact.
817 + # Attention to the extra padding is masked via `audio_lens`.
818 + # Over-long inputs are not trimmed here; the tower raises on them.
819 + pad_len = self.audio_tower.max_context_length - audio_features.shape[-1]
820 + if pad_len > 0:
821 + audio_features = F.pad(audio_features, (0, pad_len))
822 +
823 audio_lens = audio_input["lens"]
824 audio_token_len = audio_input["token_len"]
825
@@ -755,7 +913,7 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
913 return self.language_model.compute_logits(hidden_states)
914
915 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
758 - loader = AutoWeightsLoader(self, ignore_unexpected_prefixes=["audio_tower."])
916 + loader = AutoWeightsLoader(self)
917 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
918
919
vllm/model_executor/models/whisper.py
+68 -10
@@ -47,6 +47,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
47 from vllm.model_executor.models.whisper_utils import (
48 ISO639_1_SUPPORTED_LANGS,
49 )
50 +from vllm.model_executor.offloader import get_offloader
51 from vllm.multimodal import MULTIMODAL_REGISTRY
52 from vllm.multimodal.inputs import (
53 MultiModalFieldConfig,
@@ -114,6 +115,10 @@ class WhisperEncoderAttention(MMEncoderAttention):
115 query: torch.Tensor,
116 key: torch.Tensor,
117 value: torch.Tensor,
118 + cu_seqlens: torch.Tensor | None = None,
119 + max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
120 + # Only used for FlashInfer CuDNN backend.
121 + sequence_lengths: torch.Tensor | None = None,
122 ) -> torch.Tensor:
123 """
124 Input shape: batch_size x seq_len x hidden_size
@@ -126,7 +131,14 @@ class WhisperEncoderAttention(MMEncoderAttention):
131 value = value.unsqueeze(0)
132
133 # Call the parent forward method
129 - out = super().forward(query, key, value)
134 + out = super().forward(
135 + query,
136 + key,
137 + value,
138 + cu_seqlens=cu_seqlens,
139 + max_seqlen=max_seqlen,
140 + sequence_lengths=sequence_lengths,
141 + )
142
143 if is_2d:
144 out = out.squeeze(0)
@@ -240,11 +252,29 @@ class WhisperAttention(nn.Module):
252 def forward(
253 self,
254 hidden_states: torch.Tensor,
255 + cu_seqlens: torch.Tensor | None = None,
256 + max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
257 + # Only used for FlashInfer CuDNN backend.
258 + sequence_lengths: torch.Tensor | None = None,
259 ):
260 qkv, _ = self.qkv_proj(hidden_states)
261 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
262
247 - attn_output = self.attn(q, k, v)
263 + if cu_seqlens is None:
264 + attn_output = self.attn(q, k, v)
265 + else:
266 + assert self.attn_type == AttentionType.ENCODER, (
267 + "Variable-length attention metadata is only supported for "
268 + "encoder self-attention."
269 + )
270 + attn_output = self.attn(
271 + q,
272 + k,
273 + v,
274 + cu_seqlens=cu_seqlens,
275 + max_seqlen=max_seqlen,
276 + sequence_lengths=sequence_lengths,
277 + )
278
279 output, _ = self.out_proj(attn_output)
280
@@ -381,10 +411,19 @@ class WhisperEncoderLayer(nn.Module):
411 def forward(
412 self,
413 hidden_states: torch.Tensor,
414 + cu_seqlens: torch.Tensor | None = None,
415 + max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
416 + # Only used for FlashInfer CuDNN backend.
417 + sequence_lengths: torch.Tensor | None = None,
418 ):
419 residual = hidden_states
420 hidden_states = self.self_attn_layer_norm(hidden_states)
387 - hidden_states = self.self_attn(hidden_states=hidden_states)
421 + hidden_states = self.self_attn(
422 + hidden_states=hidden_states,
423 + cu_seqlens=cu_seqlens,
424 + max_seqlen=max_seqlen,
425 + sequence_lengths=sequence_lengths,
426 + )
427 hidden_states = residual + hidden_states
428 residual = hidden_states
429 hidden_states = self.final_layer_norm(hidden_states)
@@ -457,7 +496,12 @@ class WhisperDecoderLayer(nn.Module):
496
497 class WhisperEncoder(nn.Module):
498 def __init__(
460 - self, *, vllm_config: VllmConfig, prefix: str = "", init_in_fp32: bool = False
499 + self,
500 + *,
501 + vllm_config: VllmConfig,
502 + prefix: str = "",
503 + init_in_fp32: bool = False,
504 + enable_pp: bool = True,
505 ):
506 super().__init__()
507 config = vllm_config.model_config.hf_config
@@ -474,13 +518,27 @@ class WhisperEncoder(nn.Module):
518 self.conv2 = nn.Conv1d(embed_dim, embed_dim, stride=2, kernel_size=3, padding=1)
519
520 self.total_stride = self.conv1.stride[0] * self.conv2.stride[0]
477 - self.start_layer, self.end_layer, self.layers = make_layers(
478 - config.encoder_layers,
479 - lambda prefix: WhisperEncoderLayer(
521 +
522 + def create_layer(prefix: str) -> nn.Module:
523 + return WhisperEncoderLayer(
524 vllm_config=vllm_config, prefix=f"{prefix}.layers"
481 - ),
482 - prefix=f"{prefix}.layers",
483 - )
525 + )
526 +
527 + if enable_pp:
528 + self.start_layer, self.end_layer, self.layers = make_layers(
529 + config.encoder_layers,
530 + create_layer,
531 + prefix=f"{prefix}.layers",
532 + )
533 + else:
534 + self.start_layer = 0
535 + self.end_layer = config.encoder_layers
536 + self.layers = nn.ModuleList(
537 + get_offloader().wrap_modules(
538 + create_layer(prefix=f"{prefix}.layers.{idx}")
539 + for idx in range(config.encoder_layers)
540 + )
541 + )
542 self.layer_norm = nn.LayerNorm(config.d_model)
543
544 if self.pos_embed_type not in (