main
py 976 lines 36.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Adapted from https://github.com/fixie-ai/ultravox/blob/ecd58c4041030bae2ad15aa6bcf04ab43199ea02/ultravox/model/ultravox_model.py
5 """PyTorch Ultravox model."""
6
7 import math
8 from collections.abc import Iterable, Mapping, Sequence
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.models.whisper import WhisperFeatureExtractor
17
18 from vllm.config import VllmConfig
19 from vllm.config.multimodal import MultiModalDummyOptions
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
30 from vllm.multimodal.inputs import (
31 MultiModalFieldConfig,
32 MultiModalKwargsItem,
33 MultiModalKwargsItems,
34 NestedTensors,
35 )
36 from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
37 from vllm.multimodal.processing import (
38 BaseDummyInputsBuilder,
39 BaseMultiModalProcessor,
40 BaseProcessingInfo,
41 PromptReplacement,
42 PromptUpdate,
43 cached_encode,
44 )
45 from vllm.multimodal.processing.processor import HFMultiModalInputs
46 from vllm.renderers import TokenizeParams
47 from vllm.sequence import IntermediateTensors
48 from vllm.transformers_utils.configs.ultravox import UltravoxConfig
49 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
50 from vllm.utils.tensor_schema import TensorSchema, TensorShape
51
52 from .interfaces import (
53 MultiModalEmbeddings,
54 SupportsLoRA,
55 SupportsMultiModal,
56 SupportsPP,
57 )
58 from .utils import (
59 AutoWeightsLoader,
60 WeightsMapper,
61 flatten_bn,
62 init_vllm_registered_model,
63 maybe_prefix,
64 )
65 from .whisper import (
66 WhisperEncoder,
67 WhisperEncoderLayer,
68 _create_fake_bias_for_k_proj,
69 )
70
71 _AUDIO_PLACEHOLDER_OVERRIDE = "<|audio|>"
72 _MAX_ENCODER_BATCH_SIZE = 16
73
74
75 class UltravoxAudioFeatureInputs(TensorSchema):
76 """Dimensions:
77 - b: batch size
78 - n: number of chunks
79 - t: Time frames (M)
80 - nmb: Number of mel bins
81 """
82
83 type: Literal["audio_features"]
84 data: Annotated[
85 torch.Tensor | list[torch.Tensor] | list[list[torch.Tensor]],
86 TensorShape("bn", "nmb", "t"),
87 ]
88 lens: Annotated[torch.Tensor, TensorShape("bn")]
89 """
90 Length of the audio frames per chunk. Used for attention mask in WhisperEncoder.
91 """
92 token_len: Annotated[torch.Tensor, TensorShape("bn")]
93 """Length of the audio tokens per chunk. Used for flattening the audio features."""
94 num_chunks: Annotated[torch.Tensor, TensorShape("n")]
95 """Number of chunks per audio. Used for flattening the audio features."""
96
97
98 class UltravoxAudioEmbeddingInputs(TensorSchema):
99 """Dimensions:
100 - b: batch size
101 - na: number of audios
102 - afs: audio feature size
103 - hs: hidden size
104 """
105
106 type: Literal["audio_embeds"]
107 data: Annotated[
108 torch.Tensor | list[torch.Tensor], TensorShape("b", "na", "afs", "hs")
109 ]
110
111
112 UltravoxAudioInputs: TypeAlias = (
113 UltravoxAudioFeatureInputs | UltravoxAudioEmbeddingInputs
114 )
115
116
117 class UltravoxMultiModalDataParser(MultiModalDataParser):
118 embedding_fields = {
119 "audio": {"audio_embeds": "values", "audio_num_tokens": "metadata"},
120 }
121
122
123 class UltravoxProcessingInfo(BaseProcessingInfo):
124 def get_hf_processor(self, **kwargs: object) -> ProcessorMixin:
125 config = self.ctx.model_config.hf_config
126 hf_processor = self.ctx.get_hf_processor(**kwargs)
127
128 # NOTE: Ultravox processing definition uses '<|eot_id|>' as the
129 # placeholder that will cause confusion with the actual end of turn
130 # token, thus we override placeholder with a reserved token.
131 hf_processor.audio_token_replacement = _AUDIO_PLACEHOLDER_OVERRIDE
132 hf_processor.audio_replacement_token_id = config.audio_token_index
133
134 return hf_processor
135
136 def get_feature_extractor(self, **kwargs: object) -> WhisperFeatureExtractor:
137 hf_processor = self.get_hf_processor(**kwargs)
138
139 # Changed in https://huggingface.co/fixie-ai/ultravox-v0_5-llama-3_2-1b/commit/9a3c571b8fdaf1e66dd3ea61bbcb6db5c70a438e
140 audio_processor = hf_processor.audio_processor # type: ignore
141 if isinstance(audio_processor, WhisperFeatureExtractor):
142 return audio_processor
143
144 feature_extractor = audio_processor.feature_extractor # type: ignore
145 assert isinstance(feature_extractor, WhisperFeatureExtractor)
146 return feature_extractor
147
148 def get_default_tok_params(self) -> TokenizeParams:
149 return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
150
151 def get_data_parser(self):
152 feature_extractor = self.get_feature_extractor()
153
154 return UltravoxMultiModalDataParser(
155 target_sr=feature_extractor.sampling_rate,
156 target_channels=self.get_target_channels(),
157 expected_hidden_size=self._get_expected_hidden_size(),
158 allow_missing_mm_embeddings=self.allow_missing_mm_embeddings,
159 )
160
161 def get_target_channels(self) -> int:
162 """Return target audio channels for Ultravox models (mono)."""
163 return 1
164
165 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
166 return {"audio": None}
167
168
169 class UltravoxDummyInputsBuilder(BaseDummyInputsBuilder[UltravoxProcessingInfo]):
170 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
171 num_audios = mm_counts.get("audio", 0)
172
173 return "<|audio|>" * num_audios
174
175 def get_dummy_mm_data(
176 self,
177 seq_len: int,
178 mm_counts: Mapping[str, int],
179 mm_options: MultiModalDummyOptions,
180 ) -> MultiModalDataDict:
181 feature_extractor = self.info.get_feature_extractor()
182
183 sampling_rate = feature_extractor.sampling_rate
184 audio_len = (
185 feature_extractor.chunk_length * sampling_rate * _MAX_ENCODER_BATCH_SIZE
186 )
187
188 return {
189 "audio": self._get_dummy_audios(
190 length=audio_len,
191 num_audios=mm_counts.get("audio", 0),
192 overrides=mm_options.get("audio"),
193 )
194 }
195
196
197 class UltravoxMultiModalProcessor(BaseMultiModalProcessor[UltravoxProcessingInfo]):
198 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
199 return self.dummy_inputs.get_dummy_text(mm_counts)
200
201 def _get_hf_mm_inputs(
202 self,
203 mm_items: MultiModalDataItems,
204 hf_kwargs: Mapping[str, object],
205 ) -> HFMultiModalInputs:
206 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
207
208 # The Ultravox processor accepts "audios" instead of "audio"
209 hf_data = hf_inputs.hf_data
210 if "audio" in hf_data:
211 hf_data["audios"] = hf_data.pop("audio")
212
213 feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
214 normalized_kwargs = dict(hf_inputs.hf_kwargs)
215 # The remote processor already hardcodes truncation=False and forwards
216 # its kwargs to the audio processor, where another value would collide.
217 normalized_kwargs.pop("truncation", None)
218 return hf_inputs._replace(
219 hf_kwargs=dict(
220 normalized_kwargs,
221 sampling_rate=feature_extractor.sampling_rate,
222 include_audio_num_chunks=True,
223 )
224 )
225
226 def _postprocess_hf_mm_data(
227 self,
228 mm_data: Mapping[str, object],
229 hf_processor_mm_kwargs: Mapping[str, object],
230 processed_data: BatchFeature,
231 ) -> BatchFeature:
232 if "audio_values" in processed_data:
233 processed_data["audio_features"] = processed_data.pop("audio_values")
234
235 return processed_data
236
237 def _get_mm_fields_config(
238 self,
239 hf_inputs: BatchFeature,
240 hf_processor_mm_kwargs: Mapping[str, object],
241 ) -> Mapping[str, MultiModalFieldConfig]:
242 num_chunks = hf_inputs.get("audio_num_chunks", torch.zeros(0))
243 return dict(
244 # to handle longer than 30s audio, each audio might be split
245 # into multiple chunks as such, their batch dimension can be
246 # higher than the number of audio samples
247 audio_features=MultiModalFieldConfig.flat_from_sizes("audio", num_chunks),
248 audio_token_len=MultiModalFieldConfig.flat_from_sizes("audio", num_chunks),
249 # Only ever used to derive the encoder attention metadata on the
250 # host, so keep it there.
251 audio_lens=MultiModalFieldConfig.flat_from_sizes(
252 "audio", num_chunks, keep_on_cpu=True
253 ),
254 # num_chunks can convert audio_chunked to audio batch dimension
255 audio_num_chunks=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
256 audio_embeds=MultiModalFieldConfig.batched("audio"),
257 audio_num_tokens=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
258 )
259
260 def _get_prompt_updates(
261 self,
262 mm_items: MultiModalDataItems,
263 hf_processor_mm_kwargs: Mapping[str, Any],
264 out_mm_kwargs: MultiModalKwargsItems,
265 ) -> Sequence[PromptUpdate]:
266 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
267 tokenizer = self.info.get_tokenizer()
268
269 replacement_id = hf_processor.audio_replacement_token_id # type: ignore
270
271 # Each audio can be split into multiple chunks.
272 # chunks_start_idx[i] indicates the start index of the chunks
273 # belonging to the i-th audio.
274 out_mm_data = out_mm_kwargs.get_data()
275 num_chunks = out_mm_data.get("audio_num_chunks", torch.zeros(0))
276 chunks_start_idx: torch.Tensor = torch.cumsum(
277 num_chunks, dim=0, dtype=torch.int32
278 )
279 chunks_start_idx = torch.cat(
280 [torch.tensor([0], dtype=torch.int32), chunks_start_idx]
281 )
282
283 def get_replacement_ultravox(item_idx: int):
284 if "audio_num_tokens" in out_mm_data:
285 audio_num_tokens = out_mm_data["audio_num_tokens"]
286 assert isinstance(audio_num_tokens, torch.Tensor)
287 return [replacement_id] * int(audio_num_tokens[item_idx])
288 start = chunks_start_idx[item_idx]
289 end = chunks_start_idx[item_idx + 1]
290 audio_token_lens = out_mm_data["audio_token_len"]
291 assert isinstance(audio_token_lens, torch.Tensor)
292 audio_token_len = audio_token_lens[start:end].sum()
293 return [replacement_id] * int(audio_token_len) # type: ignore
294
295 return [
296 PromptReplacement(
297 modality="audio",
298 # `<|audio|>` is paired with `replacement_id` in
299 # `UltravoxProcessingInfo.get_hf_processor`, but it is not
300 # guaranteed to be a single vocab entry, so match the encoded
301 # placeholder text instead of `replacement_id`
302 target=cached_encode(
303 tokenizer,
304 hf_processor.audio_token_replacement, # type: ignore
305 add_special_tokens=False,
306 ),
307 replacement=get_replacement_ultravox,
308 )
309 ]
310
311
312 class StackAudioFrames(nn.Module):
313 """Stack the audio embedding frames to reduce the sequence length by a factor
314 of `stack_factor`.
315 """
316
317 def __init__(self, stack_factor: int = 8):
318 super().__init__()
319 self.stack_factor = stack_factor
320
321 def forward(self, audio_embeds: torch.Tensor) -> torch.Tensor:
322 B, T, C = audio_embeds.shape
323 T_pad = (T + self.stack_factor - 1) // self.stack_factor * self.stack_factor
324 audio_embeds = F.pad(audio_embeds, (0, 0, 0, T_pad - T))
325 B, T, C = audio_embeds.shape
326 audio_embeds = audio_embeds.view(
327 B, T // self.stack_factor, C * self.stack_factor
328 )
329 return audio_embeds
330
331
332 def _build_chunk_attn_metadata(
333 attn: MMEncoderAttention,
334 feature_lens: torch.Tensor,
335 seq_len: int,
336 hidden_size: int,
337 device: torch.device,
338 ) -> dict[str, torch.Tensor | None]:
339 """Segmented varlen attention metadata for a padded batch of audio chunks.
340
341 Each padded row contributes up to two sequences to `cu_seqlens`: its valid
342 frames and its padding tail. Attention therefore never crosses a
343 valid/padding boundary (equivalent to the key-padding mask the HF
344 implementation uses), while every row still flows through the
345 (potentially LoRA-wrapped) linears, keeping the per-chunk token counts
346 constant as required by `get_mm_lora_token_counts`. Queries at padding
347 positions produce (garbage) outputs, which are trimmed by `audio_token_len`
348 downstream.
349 """
350 batch_size = feature_lens.shape[0]
351 starts = np.arange(batch_size, dtype=np.int64) * seq_len
352 lens_np = feature_lens.cpu().numpy().astype(np.int64)
353 bounds = np.stack([starts + lens_np, starts + seq_len], axis=1).reshape(-1)
354 cu_seqlens_np = np.concatenate(([0], bounds))
355 # Fully-valid rows produce empty padding segments; drop the duplicates.
356 cu_seqlens_np = np.unique(cu_seqlens_np).astype(np.int32)
357
358 attn_backend = attn.attn_backend
359 sequence_lengths = MMEncoderAttention.maybe_compute_seq_lens(
360 attn_backend, cu_seqlens_np, device
361 )
362 max_seqlen = torch.tensor(
363 MMEncoderAttention.compute_max_seqlen(attn_backend, cu_seqlens_np),
364 dtype=torch.int32,
365 )
366 cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
367 attn_backend,
368 cu_seqlens_np,
369 hidden_size,
370 get_tensor_model_parallel_world_size(),
371 device,
372 )
373 return {
374 "cu_seqlens": cu_seqlens,
375 "max_seqlen": max_seqlen,
376 "sequence_lengths": sequence_lengths,
377 }
378
379
380 # Maps HF whisper encoder layer names to vLLM's fused qkv_proj and nested
381 # mlp; `_create_fake_bias_for_k_proj` supplies the fused bias' k-slice
382 # (HF whisper k_proj has no bias).
383 _WHISPER_ENCODER_MAPPER = WeightsMapper(
384 orig_to_new_substr={".fc1.": ".mlp.fc1.", ".fc2.": ".mlp.fc2."},
385 orig_to_new_stacked={
386 ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
387 ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
388 ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
389 },
390 )
391
392
393 def _load_whisper_layer_weights(
394 module: nn.Module, weights: Iterable[tuple[str, torch.Tensor]]
395 ) -> set[str]:
396 weights = _create_fake_bias_for_k_proj(weights, ".self_attn.k_proj.weight")
397 loader = AutoWeightsLoader(module)
398 return loader.load_weights(weights, mapper=_WHISPER_ENCODER_MAPPER)
399
400
401 class UltravoxFeedForwardProjector(nn.Module):
402 def __init__(
403 self,
404 config: UltravoxConfig,
405 *,
406 quant_config: QuantizationConfig | None = None,
407 prefix: str = "",
408 ):
409 super().__init__()
410 self.hidden_dim = config.hidden_size
411 self._pad_and_stack = StackAudioFrames(config.stack_factor)
412 dim_in = config.audio_config.hidden_size * config.stack_factor
413 self.ln_pre = RMSNorm(dim_in)
414 # ReplicatedLinear (a vLLM-native linear) so the connector can be
415 # wrapped for LoRA; a plain nn.Linear is left unwrapped by from_layer.
416 self.linear_1 = ReplicatedLinear(
417 dim_in,
418 self.hidden_dim,
419 bias=False,
420 quant_config=quant_config,
421 prefix=f"{prefix}.linear_1",
422 )
423 dim_mid = self.hidden_dim
424
425 if config.projector_act == "swiglu":
426 self.act = MulAndSilu()
427 dim_mid = dim_mid // 2
428 else:
429 self.act = get_act_fn(config.projector_act)
430
431 dim_out = config.text_config.hidden_size
432 self.linear_2 = ReplicatedLinear(
433 dim_mid,
434 dim_out,
435 bias=False,
436 quant_config=quant_config,
437 prefix=f"{prefix}.linear_2",
438 )
439
440 # Ultravox v0.4.1 and below use layer_norm after the second linear layer
441 # while v0.5.0 and above uses layer_norm after the first linear layer.
442 if config.projector_ln_mid:
443 self.ln_mid: nn.Module = RMSNorm(dim_mid)
444 self.ln_post = nn.Identity()
445 else:
446 self.ln_mid = nn.Identity()
447 self.ln_post = RMSNorm(dim_out)
448
449 def forward(
450 self, audio_features: torch.Tensor, audio_token_len: torch.Tensor
451 ) -> torch.Tensor:
452 audio_features = self._pad_and_stack(audio_features)
453 audio_features = self.ln_pre(audio_features)
454 hidden_states, _ = self.linear_1(audio_features)
455 hidden_states = self.act(hidden_states)
456 hidden_states = self.ln_mid(hidden_states)
457 hidden_states, _ = self.linear_2(hidden_states)
458 hidden_states = self.ln_post(hidden_states)
459 return hidden_states
460
461
462 class UltravoxTransformerProjector(nn.Module):
463 def __init__(
464 self,
465 vllm_config: VllmConfig,
466 *,
467 prefix: str = "",
468 ):
469 super().__init__()
470 config: UltravoxConfig = vllm_config.model_config.hf_config
471 quant_config = vllm_config.quant_config
472 audio_config = config.audio_config
473
474 self._pad_and_stack = StackAudioFrames(config.stack_factor)
475 dim_in = audio_config.hidden_size * config.stack_factor
476
477 self.ln_pre = RMSNorm(dim_in)
478 self.linear_in = ReplicatedLinear(
479 dim_in,
480 audio_config.d_model,
481 bias=True,
482 quant_config=quant_config,
483 prefix=f"{prefix}.linear_in",
484 )
485
486 self.embed_positions = nn.Embedding(
487 audio_config.max_source_positions,
488 audio_config.d_model,
489 )
490
491 audio_vllm_config = vllm_config.with_hf_config(
492 audio_config, architectures=["UltravoxModel"]
493 )
494 self.layers = nn.ModuleList(
495 [
496 WhisperEncoderLayer(
497 vllm_config=audio_vllm_config,
498 prefix=f"{prefix}.layers.{idx}",
499 )
500 for idx in range(config.num_projector_layers)
501 ]
502 )
503 self._d_model = audio_config.d_model
504
505 self.ln_post = RMSNorm(audio_config.d_model)
506 self.linear_out = ReplicatedLinear(
507 audio_config.d_model,
508 config.text_config.hidden_size,
509 bias=True,
510 quant_config=quant_config,
511 prefix=f"{prefix}.linear_out",
512 )
513
514 def forward(
515 self, audio_features: torch.Tensor, audio_token_len: torch.Tensor
516 ) -> torch.Tensor:
517 audio_features = self._pad_and_stack(audio_features)
518
519 hidden_states = self.ln_pre(audio_features)
520 hidden_states, _ = self.linear_in(hidden_states)
521
522 positions = self.embed_positions(
523 torch.arange(hidden_states.size(1), device=hidden_states.device)
524 )
525 hidden_states = hidden_states + positions
526
527 batch_size, seq_len, d_model = hidden_states.shape
528 attn_metadata = _build_chunk_attn_metadata(
529 self.layers[0].self_attn.attn,
530 audio_token_len,
531 seq_len,
532 d_model,
533 hidden_states.device,
534 )
535 hidden_states = hidden_states.reshape(batch_size * seq_len, d_model)
536 for layer in self.layers:
537 hidden_states = layer(hidden_states, **attn_metadata)
538 hidden_states = hidden_states.reshape(batch_size, seq_len, d_model)
539
540 hidden_states = self.ln_post(hidden_states)
541 hidden_states, _ = self.linear_out(hidden_states)
542 return hidden_states
543
544 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
545 return _load_whisper_layer_weights(self, weights)
546
547
548 class UltravoxWhisperEncoder(WhisperEncoder):
549 """Ultravox's `ModifiedWhisperEncoder` on top of vLLM's `WhisperEncoder`.
550
551 Like the original (a modified HF whisper encoder,
552 see https://github.com/huggingface/transformers/issues/25744), it accepts
553 mel inputs shorter than 30s (positions are sliced to the input length) and
554 confines attention to each chunk's valid frames based on `audio_lens`
555 (via segmented `cu_seqlens` instead of a dense key-padding mask), so its
556 outputs match the HF implementation for valid positions. The linears are
557 vLLM-native so the tower can be wrapped for LoRA.
558 """
559
560 @property
561 def dtype(self) -> torch.dtype:
562 return self.conv1.weight.dtype
563
564 @property
565 def max_context_length(self) -> int:
566 return self.max_source_positions * self.total_stride
567
568 def _get_feat_extract_output_lengths(
569 self, input_lengths: torch.Tensor
570 ) -> torch.Tensor:
571 return (input_lengths - 1) // 2 + 1
572
573 # Ultravox requires chunk lengths in addition to Whisper's input features.
574 def forward( # type: ignore[override]
575 self,
576 input_features: torch.Tensor,
577 audio_lens: torch.Tensor,
578 ) -> torch.Tensor:
579 expected_seq_length = self.max_context_length
580 if input_features.shape[-1] > expected_seq_length:
581 raise ValueError(
582 f"Whisper expects the mel input features to be of length "
583 f"{expected_seq_length} or less, but found "
584 f"{input_features.shape[-1]}. Make sure to pad the input mel "
585 f"features to {expected_seq_length}."
586 )
587
588 inputs_embeds = nn.functional.gelu(self.conv1(input_features))
589 inputs_embeds = nn.functional.gelu(self.conv2(inputs_embeds))
590
591 inputs_embeds = inputs_embeds.permute(0, 2, 1)
592 embed_pos = self.embed_positions.weight[: inputs_embeds.size(-2)]
593
594 hidden_states = (inputs_embeds + embed_pos).to(inputs_embeds.dtype)
595
596 batch_size, seq_len, d_model = hidden_states.shape
597 attn_metadata = _build_chunk_attn_metadata(
598 self.layers[0].self_attn.attn,
599 self._get_feat_extract_output_lengths(audio_lens),
600 seq_len,
601 d_model,
602 hidden_states.device,
603 )
604 hidden_states = hidden_states.reshape(batch_size * seq_len, d_model)
605 for encoder_layer in self.layers:
606 hidden_states = encoder_layer(hidden_states, **attn_metadata)
607 hidden_states = hidden_states.reshape(batch_size, seq_len, d_model)
608
609 hidden_states = self.layer_norm(hidden_states)
610 return hidden_states
611
612 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
613 return _load_whisper_layer_weights(self, weights)
614
615
616 @MULTIMODAL_REGISTRY.register_processor(
617 UltravoxMultiModalProcessor,
618 info=UltravoxProcessingInfo,
619 dummy_inputs=UltravoxDummyInputsBuilder,
620 )
621 class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
622 packed_modules_mapping = {
623 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
624 "gate_up_proj": ["gate_proj", "up_proj"],
625 }
626
627 hf_to_vllm_mapper = WeightsMapper(
628 orig_to_new_prefix={
629 "audio_tower.model.encoder.": "audio_tower.",
630 # A whisper checkpoint loaded via `audio_model_id` also carries
631 # decoder and LM-head weights the tower does not use.
632 "audio_tower.model.": None,
633 "audio_tower.proj_out.": None,
634 }
635 )
636
637 supports_tower_connector_lora = True
638
639 @classmethod
640 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
641 if modality.startswith("audio"):
642 return "<|audio|>"
643
644 raise ValueError("Only audio modality is supported")
645
646 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
647 super().__init__()
648 config: UltravoxConfig = vllm_config.model_config.hf_config
649 multimodal_config = vllm_config.model_config.multimodal_config
650 quant_config = vllm_config.quant_config
651 lora_config = vllm_config.lora_config
652 self.config = config
653 self.multi_modal_config = multimodal_config
654 assert self.multi_modal_config
655
656 # LoRA on the tower/connector requires per-item token counts that are
657 # predictable from the placeholder count alone (see
658 # `get_mm_lora_token_counts`), so pad every audio chunk's mel input
659 # to the tower's full context instead of the batch's max length.
660 self.pad_audio_to_max_context = bool(
661 lora_config is not None and lora_config.enable_tower_connector_lora
662 )
663
664 self.configure_mm_token_handling(
665 self.config.vocab_size,
666 [self.config.audio_token_index],
667 )
668
669 # The towers live in their own Hub repos, so the revision of the
670 # Ultravox repo does not apply to them.
671 self.secondary_weights = []
672 if config.audio_model_id is not None:
673 # this prefix is not for initialization, but for loading weights
674 # note the trailing dot
675 self.secondary_weights.append(
676 DefaultModelLoader.Source(
677 model_or_path=config.audio_model_id,
678 revision=None,
679 prefix="audio_tower.",
680 )
681 )
682 if config.text_model_id is not None:
683 # this prefix is not for initialization, but for loading weights
684 # note the trailing dot
685 self.secondary_weights.append(
686 DefaultModelLoader.Source(
687 model_or_path=config.text_model_id,
688 revision=None,
689 prefix="language_model.",
690 )
691 )
692
693 with self._mark_tower_model(vllm_config, "audio"):
694 self.audio_tower = UltravoxWhisperEncoder(
695 vllm_config=vllm_config.with_hf_config(
696 config.audio_config, architectures=["UltravoxModel"]
697 ),
698 prefix=maybe_prefix(prefix, "audio_tower"),
699 enable_pp=False,
700 )
701 if config.num_projector_layers > 0:
702 self.multi_modal_projector = UltravoxTransformerProjector(
703 vllm_config,
704 prefix=maybe_prefix(prefix, "multi_modal_projector"),
705 )
706 else:
707 self.multi_modal_projector = UltravoxFeedForwardProjector(
708 config,
709 quant_config=quant_config,
710 prefix=maybe_prefix(prefix, "multi_modal_projector"),
711 )
712
713 with self._mark_language_model(vllm_config):
714 self.language_model = init_vllm_registered_model(
715 vllm_config=vllm_config,
716 hf_config=config.wrapped_model_config,
717 prefix=maybe_prefix(prefix, "language_model"),
718 )
719
720 self.make_empty_intermediate_tensors = (
721 self.language_model.make_empty_intermediate_tensors
722 )
723
724 def get_mm_mapping(self) -> MultiModelKeys:
725 """Get the module prefix in multimodal models."""
726 return MultiModelKeys.from_string_field(
727 language_model="language_model.",
728 connector="multi_modal_projector.",
729 tower_model="audio_tower.",
730 )
731
732 def _get_max_tokens_per_chunk(self) -> int:
733 # Audio is chunked at the tower's full context (30s -> 1500 encoder
734 # frames); the projector stacks `stack_factor` frames per LM token, so
735 # a full chunk yields ceil(max_source_positions / stack_factor) tokens
736 # and only an audio's last chunk can yield fewer.
737 return math.ceil(
738 self.config.audio_config.max_source_positions / self.config.stack_factor
739 )
740
741 def get_mm_lora_token_counts(
742 self,
743 *,
744 modality: str,
745 mm_kwargs: MultiModalKwargsItem | None,
746 num_mm_embeds: int,
747 ) -> tuple[int, int | None]:
748 del modality, mm_kwargs
749 # With `pad_audio_to_max_context` (enforced when tower/connector LoRA
750 # is enabled), the tower's LoRA-wrapped linears always run on
751 # `max_source_positions` conv-downsampled tokens per chunk, regardless
752 # of the valid frame count.
753 num_chunks = math.ceil(num_mm_embeds / self._get_max_tokens_per_chunk())
754 tower_tokens = num_chunks * self.config.audio_config.max_source_positions
755 # The connector runs on the frame-stacked tower output. Stacking pads
756 # each chunk to a multiple of `stack_factor`, so this is
757 # ceil(max_source_positions / stack_factor) tokens per chunk (188 for
758 # whisper's 1500), not `tower_tokens // stack_factor` (187).
759 connector_tokens = num_chunks * self._get_max_tokens_per_chunk()
760 return tower_tokens, connector_tokens
761
762 def _audio_features_to_embeddings(
763 self,
764 input_features: torch.Tensor,
765 audio_lens: torch.Tensor,
766 audio_token_len: torch.Tensor,
767 ) -> torch.Tensor:
768 audio_features = input_features.to(self.audio_tower.dtype)
769 batch_size = audio_features.size(0)
770 audio_embeddings = []
771
772 # Process audio features in batches to keep memory usage predictable.
773 # With tower/connector LoRA, the punica kernels always map the first
774 # `x.size(0)` entries of the token-LoRA mapping (set once for the whole
775 # scheduled encoder batch) to the rows of each linear's input, so
776 # splitting the batch would apply the wrong LoRA ids to every chunk
777 # after the first sub-batch; process all chunks in a single pass.
778 encoder_batch_size = (
779 max(batch_size, 1)
780 if self.pad_audio_to_max_context
781 else _MAX_ENCODER_BATCH_SIZE
782 )
783 for start in range(0, batch_size, encoder_batch_size):
784 end = min(start + encoder_batch_size, batch_size)
785 # Process through audio tower
786 batch_features = self.audio_tower(
787 audio_features[start:end], audio_lens[start:end]
788 )
789 batch_features = batch_features.to(self.audio_tower.dtype)
790
791 # Process through projector
792 batch_embeddings = self.multi_modal_projector(
793 batch_features, audio_token_len[start:end]
794 )
795 audio_embeddings.append(batch_embeddings)
796
797 # Concatenate results
798 audio_embeddings = torch.cat(audio_embeddings, dim=0)
799 return audio_embeddings
800
801 def _parse_and_validate_audio_input(
802 self, **kwargs: object
803 ) -> UltravoxAudioInputs | None:
804 audio_features = kwargs.pop("audio_features", None)
805 audio_embeds = kwargs.pop("audio_embeds", None)
806 audio_lens = kwargs.pop("audio_lens", None)
807 audio_token_len = kwargs.pop("audio_token_len", None)
808 audio_num_chunks = kwargs.pop("audio_num_chunks", None)
809
810 if audio_features is None and audio_embeds is None:
811 return None
812
813 if audio_features is not None:
814 return UltravoxAudioFeatureInputs(
815 type="audio_features",
816 data=audio_features,
817 lens=audio_lens,
818 token_len=audio_token_len,
819 num_chunks=audio_num_chunks,
820 )
821
822 if audio_embeds is not None:
823 return UltravoxAudioEmbeddingInputs(type="audio_embeds", data=audio_embeds)
824
825 raise AssertionError("This line should be unreachable.")
826
827 def _process_audio_input(
828 self,
829 audio_input: UltravoxAudioInputs,
830 ) -> NestedTensors | tuple[torch.Tensor, ...]:
831 if audio_input["type"] == "audio_embeds":
832 return audio_input["data"]
833
834 # Pad and concatenate audio features
835 # [[B1, 80, M1], [B2, 80, M2]] -> [B1+B2, 80, max(M1, M2)]
836 audio_features = pad_and_concat_to_dim3(audio_input["data"])
837
838 if self.pad_audio_to_max_context:
839 # Pad every chunk to the tower's full context so the number of
840 # tokens processed by the tower/connector per chunk is constant,
841 # keeping the LoRA token mappings computed by
842 # `get_mm_lora_token_counts` exact.
843 # Attention to the extra padding is masked via `audio_lens`.
844 # Over-long inputs are not trimmed here; the tower raises on them.
845 pad_len = self.audio_tower.max_context_length - audio_features.shape[-1]
846 if pad_len > 0:
847 audio_features = F.pad(audio_features, (0, pad_len))
848
849 audio_lens = audio_input["lens"]
850 audio_token_len = audio_input["token_len"]
851
852 embeddings = self._audio_features_to_embeddings(
853 audio_features, audio_lens, audio_token_len
854 )
855
856 # We should flatten and concatenate embeddings based on token lengths
857 # For example, with token_len = [4, 2, 3], flattened_embeddings will be
858 # concat(embeddings[0][:4], embeddings[1][:2], embeddings[2][:3])
859
860 # Create a mask of valid indices based on token lengths
861 max_len = embeddings.shape[1]
862 indices = torch.arange(max_len, device=embeddings.device).expand(
863 embeddings.shape[0], -1
864 )
865 mask = indices < audio_token_len[:, None]
866
867 with gpu_sync_allowed():
868 # Apply mask and flatten
869 flattened_embeddings = embeddings[mask]
870
871 # Return one tensor per input audio
872 embed_lens = [
873 chunk_lens.sum().item()
874 for chunk_lens in audio_token_len.split(
875 audio_input["num_chunks"].tolist()
876 )
877 ]
878 return flattened_embeddings.split(embed_lens)
879
880 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
881 audio_input = self._parse_and_validate_audio_input(**kwargs)
882 if audio_input is None:
883 return []
884 audio_embeddings = self._process_audio_input(audio_input)
885 return audio_embeddings
886
887 def embed_input_ids(
888 self,
889 input_ids: torch.Tensor,
890 multimodal_embeddings: MultiModalEmbeddings | None = None,
891 *,
892 is_multimodal: torch.Tensor | None = None,
893 ) -> torch.Tensor:
894 # This is to satisfy the type checker for each overload
895 if multimodal_embeddings is None or is_multimodal is None:
896 return super().embed_input_ids(input_ids)
897
898 return super().embed_input_ids(
899 input_ids,
900 multimodal_embeddings=multimodal_embeddings,
901 is_multimodal=is_multimodal,
902 )
903
904 def forward(
905 self,
906 input_ids: torch.Tensor | None,
907 positions: torch.Tensor,
908 intermediate_tensors: torch.Tensor | None = None,
909 inputs_embeds: torch.Tensor | None = None,
910 **kwargs: object,
911 ) -> torch.Tensor | IntermediateTensors:
912 """Run forward pass for Ultravox.
913
914 One key thing to understand is the `input_ids` already accounts for the
915 positions of the to-be-inserted audio embeddings. The to-be-inserted
916 audio has a size that is essentially 6.25 tokens per second of audio.
917
918 This way, the `positions` and `attn_metadata` are consistent
919 with the `input_ids`.
920
921 Args:
922 input_ids: Flattened (concatenated) input_ids corresponding to a
923 batch.
924 positions: Position indices for the input tokens.
925 intermediate_tensors: Intermediate tensors from prior forward pass.
926 inputs_embeds: Optional tensor of input embeddings.
927 **kwargs: Multimodal inputs for this batch, forwarded to the
928 multimodal embedding path.
929
930 """
931 if intermediate_tensors is not None:
932 inputs_embeds = None
933
934 language_model = self.language_model
935 if hasattr(language_model, "language_model"):
936 language_model = language_model.language_model
937
938 hidden_states = language_model.model(
939 input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds
940 )
941 return hidden_states
942
943 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
944 return self.language_model.compute_logits(hidden_states)
945
946 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
947 loader = AutoWeightsLoader(self)
948 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
949
950
951 def pad_and_concat_to_dim3(
952 features: torch.Tensor | list[torch.Tensor] | list[list[torch.Tensor]],
953 ) -> torch.Tensor:
954 """Pad and concatenate a list of tensors.
955
956 output:
957 Tensor of shape [B, C, M] where M is the maximum length of the input
958 tensors, B is the sum of the batch sizes of the input tensors.
959 C must be the same for all input tensors.
960 """
961 if isinstance(features, torch.Tensor):
962 if features.ndim > 3:
963 # Flatten [B, N, 80, M] -> [B * N, 80, M]
964 features = flatten_bn(features)
965
966 return features
967
968 tensors = [pad_and_concat_to_dim3(f) for f in features]
969
970 max_len = max(f.shape[-1] for f in tensors)
971 # Ensure all features have dim=3
972 tensors = [f.view(-1, *f.shape[-2:]) for f in tensors]
973 # Pad and concatenate:
974 # [[B1, 80, M1], [B2, 80, M2]] -> [B1+B2, 80, max(M1, M2)]
975 tensors = [F.pad(f, (0, max_len - f.shape[-1])) for f in tensors]
976 return torch.cat(tensors)