main
py 936 lines 34.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Adapted from
5 # https://github.com/huggingface/transformers/blob/v4.28.0/src/transformers/models/llama/modeling_llama.py
6 # Copyright 2025 The vLLM team.
7 # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
8 #
9 # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
10 # and OPT implementations in this library. It has been modified from its
11 # original forms to accommodate minor architectural differences compared
12 # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
13 #
14 # Licensed under the Apache License, Version 2.0 (the "License");
15 # you may not use this file except in compliance with the License.
16 # You may obtain a copy of the License at
17 #
18 # http://www.apache.org/licenses/LICENSE-2.0
19 #
20 # Unless required by applicable law or agreed to in writing, software
21 # distributed under the License is distributed on an "AS IS" BASIS,
22 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 # See the License for the specific language governing permissions and
24 # limitations under the License.
25 """Inference-only IBM Granite speech model."""
26
27 import math
28 from collections.abc import Iterable, Mapping
29 from typing import Annotated
30
31 import torch
32 import torch.nn.functional as F
33 from torch import nn
34 from transformers import BatchFeature, PreTrainedConfig
35
36 from vllm.config import CacheConfig, ModelConfig, SpeechToTextConfig, VllmConfig
37 from vllm.config.multimodal import MultiModalDummyOptions
38 from vllm.config.speech_to_text import SpeechToTextParams
39 from vllm.inputs import MultiModalDataDict, PromptType, TokensPrompt
40 from vllm.model_executor.layers.linear import ColumnParallelLinear, RowParallelLinear
41 from vllm.model_executor.layers.quantization import QuantizationConfig
42 from vllm.model_executor.models.module_mapping import MultiModelKeys
43 from vllm.multimodal import MULTIMODAL_REGISTRY
44 from vllm.multimodal.inputs import (
45 MultiModalFieldConfig,
46 MultiModalKwargsItems,
47 )
48 from vllm.multimodal.parse import (
49 AudioProcessorItems,
50 MultiModalDataItems,
51 MultiModalDataParser,
52 )
53 from vllm.multimodal.processing import (
54 BaseDummyInputsBuilder,
55 BaseMultiModalProcessor,
56 BaseProcessingInfo,
57 PromptReplacement,
58 PromptUpdate,
59 )
60 from vllm.sequence import IntermediateTensors
61 from vllm.tokenizers import cached_tokenizer_from_config
62 from vllm.transformers_utils.processor import cached_processor_from_config
63 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
64 from vllm.utils.tensor_schema import TensorSchema, TensorShape
65 from vllm.utils.torch_utils import PIN_MEMORY
66
67 from .blip2 import Blip2QFormerModel
68 from .interfaces import (
69 MultiModalEmbeddings,
70 SupportsLoRA,
71 SupportsMultiModal,
72 SupportsPP,
73 SupportsTranscription,
74 )
75 from .utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
76
77 # NOTE lang support is based on what is written here:
78 # https://huggingface.co/ibm-granite/granite-speech-3.3-2b
79 # https://huggingface.co/ibm-granite/granite-4.0-1b-speech
80 # Though this may vary from model to model, and also many langs
81 # work pretty well with zero shot.
82 ISO639_1_SUPPORTED_LANGS = {
83 "en": "English",
84 "fr": "French",
85 "de": "German",
86 "ja": "Japanese",
87 "pt": "Portuguese",
88 "es": "Spanish",
89 }
90
91
92 ### Audio Input
93 class GraniteSpeechAudioInputs(TensorSchema):
94 """Audio input features for Granite Speech model.
95
96 Dimensions:
97 - b: Batch size
98 - fi: Number of input features from the Mel spectrogram.
99 - fo: Number of output features, i.e. the embedding size.
100 - 160: Fixed feature dimension for Mel spectrogram features
101 """
102
103 input_features: Annotated[torch.Tensor, TensorShape("b", "fi", 160)]
104 """Audio input features."""
105
106 input_features_mask: Annotated[torch.Tensor, TensorShape("b", "fo")]
107 """Mask for variable length audio features."""
108
109 audio_embed_sizes: Annotated[list[int], TensorShape("b")]
110 """List of audio embedding sizes for each item in batch."""
111
112
113 class GraniteSpeechProcessingInfo(BaseProcessingInfo):
114 def get_data_parser(self):
115 feature_extractor = self.get_hf_processor().audio_processor
116
117 return MultiModalDataParser(
118 target_sr=feature_extractor.melspec_kwargs["sample_rate"],
119 expected_hidden_size=self._get_expected_hidden_size(),
120 )
121
122 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
123 return {"audio": 1}
124
125 # There is no limit to the maximum number of audio tokens that can be
126 # encoded as features; we pick ~5000 as a number that is probably higher
127 # than we would expect to encounter. The sequence of length
128 # get_max_audio_len() produces get_max_audio_tokens().
129 def get_max_audio_tokens(self):
130 return 5001
131
132 def get_max_audio_len(self):
133 return 8000000
134
135
136 ### Input Processing & Multimodal utils
137 class GraniteSpeechMultiModalProcessor(
138 BaseMultiModalProcessor[GraniteSpeechProcessingInfo]
139 ):
140 def _get_mm_fields_config(
141 self,
142 hf_inputs: BatchFeature,
143 hf_processor_mm_kwargs: Mapping[str, object],
144 ) -> Mapping[str, MultiModalFieldConfig]:
145 return dict(
146 input_features=MultiModalFieldConfig.batched("audio"),
147 audio_embed_sizes=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
148 )
149
150 def _get_prompt_updates(
151 self,
152 mm_items: MultiModalDataItems,
153 hf_processor_mm_kwargs: Mapping[str, object],
154 out_mm_kwargs: MultiModalKwargsItems,
155 ) -> list[PromptUpdate]:
156 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
157 tokenizer = self.info.get_tokenizer()
158 feature_extractor = processor.audio_processor
159 vocab = tokenizer.get_vocab()
160
161 # Use getattr with default to be compatible with transformers<4.48
162 audio_token = getattr(processor, "audio_token", "<|audio|>")
163 audio_token_id = vocab[audio_token]
164
165 def get_replacement(item_idx: int):
166 audios = mm_items.get_items("audio", AudioProcessorItems)
167 audio = audios.get(item_idx)
168 if audio is None:
169 raise ValueError(f"Missing audio item {item_idx}")
170 audio_length = len(audio) if isinstance(audio, list) else audio.shape[-1]
171 num_projector_features = feature_extractor._get_num_audio_features(
172 [audio_length]
173 )[0]
174 return [audio_token_id] * num_projector_features
175
176 return [
177 PromptReplacement(
178 modality="audio",
179 target=[audio_token_id],
180 replacement=get_replacement,
181 )
182 ]
183
184 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
185 return self.dummy_inputs.get_dummy_text(mm_counts)
186
187 def _postprocess_hf_mm_data(
188 self,
189 mm_data: Mapping[str, object],
190 hf_processor_mm_kwargs: Mapping[str, object],
191 processed_data: BatchFeature,
192 ) -> BatchFeature:
193 if "audio" in mm_data:
194 # Calculate the number of audio tokens per entry in the batch;
195 # This is used to split the batch back out after padding.
196 audio_token_index = self.info.get_hf_config().audio_token_index
197 processed_data["audio_embed_sizes"] = (
198 processed_data["input_ids"] == audio_token_index
199 ).sum(-1)
200
201 return processed_data
202
203
204 class GraniteSpeechDummyInputsBuilder(
205 BaseDummyInputsBuilder[GraniteSpeechProcessingInfo]
206 ):
207 def get_dummy_mm_data(
208 self,
209 seq_len: int,
210 mm_counts: Mapping[str, int],
211 mm_options: MultiModalDummyOptions,
212 ) -> MultiModalDataDict:
213 return {
214 "audio": self._get_dummy_audios(
215 length=self.info.get_max_audio_len(),
216 num_audios=mm_counts.get("audio", 0),
217 overrides=mm_options.get("audio"),
218 )
219 }
220
221 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
222 num_audios = mm_counts.get("audio", 0)
223 hf_processor = self.info.get_hf_processor()
224 audio_token = getattr(hf_processor, "audio_token", "<|audio|>")
225 return audio_token * num_audios
226
227
228 ### QFormer Projector
229 class GraniteSpeechEncoderProjector(nn.Module):
230 def __init__(
231 self,
232 config: PreTrainedConfig,
233 cache_config: CacheConfig,
234 quant_config: QuantizationConfig | None = None,
235 prefix: str = "",
236 ):
237 super().__init__()
238 self.hidden_size = config.projector_config.hidden_size
239 self.downsample_rate = config.downsample_rate
240 self.window_size = config.window_size
241 self.num_queries = config.window_size // config.downsample_rate
242
243 self.query = nn.Parameter(
244 torch.zeros(1, self.num_queries, config.projector_config.hidden_size)
245 )
246
247 # NOTE - this is implemented generically in transformers,
248 # but for now we create the QFormer model directly since
249 # all existing models use this for the projector.
250 self.qformer = Blip2QFormerModel(
251 config.projector_config,
252 quant_config=quant_config,
253 cache_config=cache_config,
254 prefix=f"{prefix}.qformer",
255 )
256 self.linear = nn.Linear(
257 config.projector_config.hidden_size, config.text_config.hidden_size
258 )
259
260 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
261 batch_size, seq_len, dim = hidden_states.size()
262 nblocks = math.ceil(seq_len / self.window_size)
263 pad = nblocks * self.window_size - seq_len
264 hidden_states = nn.functional.pad(hidden_states, (0, 0, 0, pad), "constant", 0)
265 hidden_states = hidden_states.view(batch_size * nblocks, self.window_size, dim)
266
267 last_hidden_state = self.qformer(
268 query_embeds=self.query.data,
269 encoder_hidden_states=hidden_states,
270 )
271
272 query_proj = self.linear(
273 last_hidden_state.view(
274 batch_size,
275 nblocks * self.window_size // self.downsample_rate,
276 -1,
277 )
278 )
279 return query_proj
280
281
282 # Encoder - conformer is adapted from: https://github.com/lucidrains/conformer.git
283 # NOTE - it would be nice to see if we can align this with other models using
284 # conformer in vLLM, e.g., phi4mm audio.
285 class GraniteSpeechConformerFeedForward(nn.Module):
286 """Feedforward module for conformer encoder blocks."""
287
288 def __init__(
289 self,
290 config: PreTrainedConfig,
291 quant_config: QuantizationConfig | None = None,
292 prefix: str = "",
293 ):
294 super().__init__()
295 self.pre_norm = nn.LayerNorm(config.hidden_dim)
296
297 self.up_proj = ColumnParallelLinear(
298 input_size=config.hidden_dim,
299 output_size=config.hidden_dim * config.feedforward_mult,
300 quant_config=quant_config,
301 prefix=f"{prefix}.up_proj",
302 )
303 self.silu = nn.SiLU()
304
305 self.down_proj = RowParallelLinear(
306 input_size=config.hidden_dim * config.feedforward_mult,
307 output_size=config.hidden_dim,
308 quant_config=quant_config,
309 prefix=f"{prefix}.down_proj",
310 )
311
312 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
313 hidden_states = self.pre_norm(hidden_states)
314 hidden_states, _ = self.up_proj(hidden_states)
315 hidden_states = self.silu(hidden_states)
316 hidden_states, _ = self.down_proj(hidden_states)
317 return hidden_states
318
319
320 class GraniteSpeechConformerAttention(nn.Module):
321 """Attention for conformer blocks using Shaw's relative positional
322 embeddings. See the following [paper](https://arxiv.org/pdf/1803.02155)
323 for more details.
324 """
325
326 def __init__(self, config: PreTrainedConfig, prefix: str = ""):
327 super().__init__()
328
329 inner_dim = config.dim_head * config.num_heads
330 self.max_pos_emb = config.max_pos_emb
331 self.context_size = config.context_size
332 self.num_heads = config.num_heads
333 self.dim_head = config.dim_head
334 self.scale = self.dim_head**-0.5
335 self.pre_norm = nn.LayerNorm(config.hidden_dim)
336 self.to_q = nn.Linear(config.hidden_dim, inner_dim, bias=False)
337 self.to_kv = nn.Linear(config.hidden_dim, inner_dim * 2, bias=False)
338 self.to_out = nn.Linear(inner_dim, config.hidden_dim)
339 self.rel_pos_emb = nn.Embedding(2 * self.max_pos_emb + 1, self.dim_head)
340
341 if self.context_size <= 0 or self.context_size > self.max_pos_emb:
342 raise ValueError(
343 f"Context size should be > 0 and "
344 f"<= max_pos_emb ({self.max_pos_emb}), "
345 f"got {self.context_size}."
346 )
347
348 def forward(
349 self, hidden_states: torch.Tensor, attention_dists: torch.Tensor
350 ) -> torch.Tensor:
351 hidden_states = self.pre_norm(hidden_states)
352 bsz, num_features, _ = hidden_states.shape
353
354 num_blocks = math.ceil(num_features / self.context_size)
355 remainder = num_features % self.context_size
356 if remainder > 0:
357 # right padding to reach block size
358 hidden_states = torch.nn.functional.pad(
359 hidden_states, (0, 0, 0, self.context_size - remainder)
360 )
361
362 # NOTE: would be nice to try to use qkvparallellinear
363 # here for this block attention implementation if possible
364 query_states = self.to_q(hidden_states)
365 key_states, value_states = self.to_kv(hidden_states).chunk(2, dim=-1)
366
367 query_states = query_states.reshape(
368 bsz, num_blocks, self.context_size, self.num_heads, -1
369 ).transpose(2, 3)
370 key_states = key_states.reshape(
371 bsz, num_blocks, self.context_size, self.num_heads, -1
372 ).transpose(2, 3)
373 value_states = value_states.reshape(
374 bsz, num_blocks, self.context_size, self.num_heads, -1
375 ).transpose(2, 3)
376
377 # shaw's relative positional embedding
378 dist = attention_dists.to(hidden_states.device)
379 rel_pos_emb = self.rel_pos_emb(dist)
380 pos_attn = (
381 torch.einsum("bnhid,ijd->bnhij", query_states, rel_pos_emb) * self.scale
382 )
383
384 if remainder > 0:
385 # masked attention in the extended block
386 mask = torch.ones(
387 self.context_size,
388 self.context_size,
389 dtype=bool,
390 device=hidden_states.device,
391 )
392 mask[:remainder, :remainder] = 0
393 mask_value = -torch.finfo(pos_attn.dtype).max
394 pos_attn[:, -1, :].masked_fill_(mask, mask_value)
395
396 with torch.nn.attention.sdpa_kernel(torch.nn.attention.SDPBackend.MATH):
397 out = F.scaled_dot_product_attention(
398 query_states,
399 key_states,
400 value_states,
401 attn_mask=pos_attn,
402 scale=self.scale,
403 )
404 out = out.transpose(2, 3).reshape(bsz, hidden_states.shape[1], -1)
405 return self.to_out(out[:, :num_features, :])
406
407
408 class GraniteSpeechConformerDepthWiseConv1d(nn.Module):
409 """Wrapper for padded 1D pointwise convolution."""
410
411 def __init__(self, chan_in: int, chan_out: int, kernel_size: int, prefix: str = ""):
412 super().__init__()
413 # Padding for the 1D conv is symmetric or close (i.e., offset by one).
414 pad = kernel_size // 2
415 pad_offset = (kernel_size + 1) % 2
416 self.padding = (pad, pad - pad_offset)
417
418 self.conv = nn.Conv1d(
419 chan_in, chan_out, kernel_size, groups=chan_in, bias=False
420 )
421
422 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
423 hidden_states = F.pad(hidden_states, self.padding)
424 return self.conv(hidden_states)
425
426
427 class GraniteSpeechConformerConvModule(nn.Module):
428 """Conformer conv module consisting of several 1D/depthwise 1D
429 convolutional layers.
430 """
431
432 def __init__(self, config: PreTrainedConfig, prefix: str = ""):
433 super().__init__()
434 inner_dim = config.hidden_dim * config.conv_expansion_factor
435
436 self.norm = nn.LayerNorm(config.hidden_dim)
437 self.up_conv = nn.Conv1d(config.hidden_dim, inner_dim * 2, 1)
438 self.glu = nn.GLU(dim=1)
439 self.depth_conv = GraniteSpeechConformerDepthWiseConv1d(
440 inner_dim,
441 inner_dim,
442 kernel_size=config.conv_kernel_size,
443 prefix=f"{prefix}.depth_conv",
444 )
445 self.silu = nn.SiLU()
446 self.batch_norm = nn.BatchNorm1d(inner_dim)
447 self.down_conv = nn.Conv1d(inner_dim, config.hidden_dim, 1)
448
449 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
450 hidden_states = self.norm(hidden_states)
451 hidden_states = self.up_conv(hidden_states.permute(0, 2, 1))
452 hidden_states = self.glu(hidden_states)
453 hidden_states = self.depth_conv(hidden_states)
454 hidden_states = self.silu(self.batch_norm(hidden_states))
455 hidden_states = self.down_conv(hidden_states).permute(0, 2, 1)
456 return hidden_states
457
458
459 class GraniteSpeechConformerBlock(nn.Module):
460 """Conformer block, consisting largely of linear layers,
461 attention, and convolutional layers."""
462
463 def __init__(self, config: PreTrainedConfig, prefix: str = ""):
464 super().__init__()
465 self.ff1 = GraniteSpeechConformerFeedForward(config, prefix=f"{prefix}.ff1")
466 self.attn = GraniteSpeechConformerAttention(config, prefix=f"{prefix}.attn")
467 self.conv = GraniteSpeechConformerConvModule(config, prefix=f"{prefix}.conv")
468 self.ff2 = GraniteSpeechConformerFeedForward(config, prefix=f"{prefix}.ff2")
469 self.post_norm = nn.LayerNorm(config.hidden_dim)
470
471 def forward(
472 self, hidden_states: torch.Tensor, attention_dists: torch.Tensor
473 ) -> torch.Tensor:
474 hidden_states = 0.5 * self.ff1(hidden_states) + hidden_states
475 hidden_states = (
476 self.attn(hidden_states, attention_dists=attention_dists) + hidden_states
477 )
478 hidden_states = self.conv(hidden_states) + hidden_states
479 hidden_states = 0.5 * self.ff2(hidden_states) + hidden_states
480 hidden_states = self.post_norm(hidden_states)
481 return hidden_states
482
483
484 class GraniteSpeechCTCEncoder(nn.Module):
485 """CTC Encoder comprising conformer blocks and additional linear layers."""
486
487 def __init__(
488 self,
489 config: PreTrainedConfig,
490 prefix: str,
491 quant_config: QuantizationConfig | None = None,
492 ):
493 super().__init__()
494 self.config = config
495
496 # Precompute clamped relative positional encoding distances
497 seq = torch.arange(config.context_size)
498 relpos_dist = seq.view(-1, 1) - seq.view(1, -1)
499 self.attention_dists = (
500 torch.clamp(relpos_dist, -config.context_size, config.context_size)
501 + config.max_pos_emb
502 )
503
504 self.input_linear = nn.Linear(config.input_dim, config.hidden_dim, bias=True)
505 self.layers = nn.ModuleList(
506 [
507 GraniteSpeechConformerBlock(
508 config,
509 prefix=f"{prefix}.layers.{idx}",
510 )
511 for idx in range(config.num_layers)
512 ]
513 )
514
515 self.out = ColumnParallelLinear(
516 input_size=config.hidden_dim,
517 output_size=config.output_dim,
518 bias=True,
519 quant_config=quant_config,
520 prefix=f"{prefix}.out",
521 )
522
523 self.out_mid = RowParallelLinear(
524 input_size=config.output_dim,
525 output_size=config.hidden_dim,
526 bias=True,
527 quant_config=quant_config,
528 prefix=f"{prefix}.out_mid",
529 )
530 self.softmax = nn.Softmax(dim=-1)
531 self.num_layers = config.num_layers
532
533 def forward(self, hidden_states: torch.Tensor):
534 hidden_states = self.input_linear(hidden_states)
535 for idx, layer in enumerate(self.layers, start=1):
536 hidden_states = layer(hidden_states, attention_dists=self.attention_dists)
537
538 if idx == self.num_layers // 2:
539 hidden_states_mid = hidden_states.clone()
540 hidden_states_mid, _ = self.out(hidden_states_mid)
541 hidden_states_mid = self.softmax(hidden_states_mid)
542 hidden_states_mid, _ = self.out_mid(hidden_states_mid)
543 hidden_states += hidden_states_mid
544 return hidden_states
545
546
547 @MULTIMODAL_REGISTRY.register_processor(
548 GraniteSpeechMultiModalProcessor,
549 info=GraniteSpeechProcessingInfo,
550 dummy_inputs=GraniteSpeechDummyInputsBuilder,
551 )
552 class GraniteSpeechForConditionalGeneration(
553 nn.Module,
554 SupportsMultiModal,
555 SupportsPP,
556 SupportsLoRA,
557 SupportsTranscription,
558 ):
559 supported_languages = ISO639_1_SUPPORTED_LANGS
560
561 packed_modules_mapping = {
562 "qkv_proj": [
563 "q_proj",
564 "k_proj",
565 "v_proj",
566 ],
567 "gate_up_proj": [
568 "gate_proj",
569 "up_proj",
570 ],
571 }
572
573 @classmethod
574 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
575 if modality.startswith("audio"):
576 return "<|audio|>"
577
578 raise ValueError("Only audio modality is supported")
579
580 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
581 super().__init__()
582 config = vllm_config.model_config.hf_config
583 quant_config = vllm_config.quant_config
584 cache_config = vllm_config.cache_config
585
586 self.config = config
587 self.quant_config = quant_config
588 self.cache_config = cache_config
589
590 # Check for OOV tokens to see if offsets need to be preserved
591 self.configure_mm_token_handling(
592 vocab_size=config.text_config.vocab_size,
593 mm_token_ids=[config.audio_token_index],
594 )
595
596 with self._mark_language_model(vllm_config):
597 # The language model is typically a Granite LLM
598 self.language_model = init_vllm_registered_model(
599 vllm_config=vllm_config,
600 hf_config=config.text_config,
601 prefix=maybe_prefix(prefix, "language_model"),
602 )
603
604 with self._mark_tower_model(vllm_config, "audio"):
605 self.encoder = self._build_encoder(
606 config=config.encoder_config,
607 quant_config=quant_config,
608 prefix=maybe_prefix(prefix, "encoder"),
609 )
610
611 # Blip2 QFormer
612 self.projector = GraniteSpeechEncoderProjector(
613 config=config,
614 quant_config=quant_config,
615 cache_config=cache_config,
616 prefix=maybe_prefix(prefix, "projector"),
617 )
618
619 self.make_empty_intermediate_tensors = (
620 self.language_model.make_empty_intermediate_tensors
621 )
622
623 def _build_encoder(
624 self,
625 config: PreTrainedConfig,
626 quant_config: QuantizationConfig | None,
627 prefix: str,
628 ) -> "GraniteSpeechCTCEncoder":
629 return GraniteSpeechCTCEncoder(
630 config=config,
631 quant_config=quant_config,
632 prefix=prefix,
633 )
634
635 def _parse_and_validate_audio_input(
636 self,
637 **kwargs: object,
638 ) -> GraniteSpeechAudioInputs | None:
639 input_features = kwargs.pop("input_features", None)
640 input_features_mask = kwargs.pop("input_features_mask", None)
641 audio_embed_sizes = kwargs.pop("audio_embed_sizes", None)
642
643 if input_features is None:
644 return None
645
646 if not isinstance(audio_embed_sizes, torch.Tensor):
647 raise ValueError("audio_embed_sizes must be a tensor")
648
649 # If we have a batch of variable feature length audio clips, we need
650 # to mask the features; usually we would get an input_features_mask
651 # from the processor, but we handle rebuilding it here since
652 # vLLM generally processes everything independently + batches.
653 if input_features_mask is None:
654 input_features_mask = self._build_input_features_mask(audio_embed_sizes)
655
656 if not isinstance(input_features, (torch.Tensor, list)):
657 raise ValueError(
658 "Incorrect type of audio input features. "
659 f"Got type: {type(input_features)}"
660 )
661
662 if input_features_mask is not None and not isinstance(
663 input_features_mask, torch.Tensor
664 ):
665 raise ValueError(
666 "Incorrect type of audio input features mask. "
667 f"Got type: {type(input_features_mask)}"
668 )
669
670 if isinstance(input_features, torch.Tensor):
671 # Granite speech currently only allows one audio token per instance
672 # and features are already unsqueezed in the processor, so one
673 # instance will have shape [1, {num_features}, 160]. As such,
674 # input features will usually be of shape
675 # [bsz, 1, num_features, 160], which we squeeze to be 3D here.
676 if len(input_features.shape) == 4:
677 input_features = input_features.squeeze(1)
678 if len(input_features.shape) != 3:
679 raise ValueError(
680 "Squeezed input features should be 3D but are of shape "
681 f"{input_features.shape}"
682 )
683 input_features = input_features.to(self.encoder.input_linear.weight.dtype)
684
685 else:
686 # Otherwise we have a list of tensors, which are almost certainly
687 # differing in their respective numbers of audio features; when
688 # passed as a batch, we expect a list of 2D var len input features
689 # so unsqueeze them.
690 input_features = [
691 feat.unsqueeze(dim=0) for feat in input_features if feat.ndim == 2
692 ]
693
694 # stack them into a 3D tensor of size [bsz, most_num_features, 160].
695 input_features = self._pad_and_stack_input_features(
696 input_features,
697 ).to(self.encoder.input_linear.weight.dtype)
698
699 return GraniteSpeechAudioInputs(
700 input_features=input_features,
701 input_features_mask=input_features_mask,
702 audio_embed_sizes=audio_embed_sizes.flatten().tolist(),
703 )
704
705 def _build_input_features_mask(
706 self,
707 audio_embed_sizes: torch.Tensor,
708 ) -> torch.Tensor:
709 """Calculate the input features mask, which will generally be used
710 to mask the padded features for all entries in the batch except
711 for those with the most audio features.
712
713 Args:
714 audio_embed_sizes: torch.Tensor
715 Tensor of num features in each seq in the batch.
716
717 Returns:
718 torch.Tensor: Mask of shape (bsz, num_features) to be applied to
719 the audio features prior to splitting the audio embeddings.
720
721 """
722 most_audio_features = int(torch.max(audio_embed_sizes))
723 mask_indices = torch.arange(most_audio_features).view(1, -1)
724 input_features_mask = mask_indices < audio_embed_sizes.view(-1, 1)
725 target_device = self.encoder.input_linear.weight.device
726 if target_device == input_features_mask.device:
727 return input_features_mask
728 masked = input_features_mask.pin_memory() if PIN_MEMORY else input_features_mask
729 return masked.to(target_device, non_blocking=True)
730
731 def _pad_and_stack_input_features(
732 self,
733 input_features: list[torch.Tensor],
734 ) -> torch.Tensor:
735 """Given a list of input features of varying length, pad them to the
736 same length and stack them into a torch.Tensor.
737
738 NOTE: Usually, padding is done in the input processor/feature extractor
739 and zero padded prior to the computation of the Mel features; the
740 resulting values are only constant within a batch and generally nonzero
741 (i.e., slightly negative nums); we should validate that this is okay
742 since we don't use a feature attention mask, but the more important
743 thing is that we apply the input_features_mask with variable len
744 batches.
745
746 Args:
747 input_features: list[torch.Tensor]
748 3D Input features to be coerced into a tensor.
749
750 Returns:
751 torch.Tensor: Tensor of shape [bsz, num_features, 160], where
752 num_features is the max number of features of any entry in the
753 batch.
754
755 """
756 feat_lens = [feats.shape[1] for feats in input_features]
757 padding = [max(feat_lens) - length for length in feat_lens]
758 # TODO (Alex) - Validate that it's okay to zero pad like this;
759 # in transformers we zero pad prior to calculating the speech features,
760 # so the value is not zero and is dependent on the batched features.
761 padded = [
762 torch.nn.functional.pad(feats, (0, 0, 0, pad, 0, 0))
763 for feats, pad in zip(input_features, padding)
764 ]
765 stacked_features = torch.cat(padded, dim=0).to(input_features[0])
766 return stacked_features
767
768 def _process_audio_input(
769 self,
770 audio_input: GraniteSpeechAudioInputs,
771 ) -> tuple[torch.Tensor]:
772 """Compute the audio features to be merged into the LLM embeddings.
773
774 Args:
775 audio_input: GraniteSpeechAudioInputs
776 Audio inputs object containing Mel features, an input features
777 mask, and the (flattened) number of audio tokens per instance.
778
779 Returns:
780 tuple[torch.Tensor]: List of length bsz.
781
782 """
783 # TODO (Alex) - support embedding inputs
784 encoder_embeds = self.encoder(audio_input["input_features"])
785 # [bsz, <max feature size>, 4096]
786 projected_embeds = self.projector(encoder_embeds)
787 # Apply mask on variable length audio features.
788 with gpu_sync_allowed():
789 masked_embeds = projected_embeds[audio_input["input_features_mask"]]
790 # Split variable length features into a tuple
791 return torch.split(masked_embeds, audio_input["audio_embed_sizes"])
792
793 def embed_multimodal(
794 self,
795 **kwargs: object,
796 ) -> MultiModalEmbeddings:
797 """Compute the audio embeddings if audio inputs are present."""
798 audio_input = self._parse_and_validate_audio_input(**kwargs)
799 if audio_input is None:
800 return []
801
802 audio_features = self._process_audio_input(audio_input)
803 return audio_features
804
805 def embed_input_ids(
806 self,
807 input_ids: torch.Tensor,
808 multimodal_embeddings: MultiModalEmbeddings | None = None,
809 *,
810 is_multimodal: torch.Tensor | None = None,
811 ) -> torch.Tensor:
812 # This is to satisfy the type checker for each overload
813 if multimodal_embeddings is None or is_multimodal is None:
814 return super().embed_input_ids(input_ids)
815
816 return super().embed_input_ids(
817 input_ids,
818 multimodal_embeddings=multimodal_embeddings,
819 is_multimodal=is_multimodal,
820 )
821
822 def forward(
823 self,
824 input_ids: torch.Tensor | None,
825 positions: torch.Tensor,
826 intermediate_tensors: IntermediateTensors | None = None,
827 inputs_embeds: torch.Tensor | None = None,
828 **kwargs: object,
829 ) -> torch.Tensor | IntermediateTensors:
830 if intermediate_tensors is not None:
831 inputs_embeds = None
832
833 model_output = self.language_model(
834 input_ids, positions, intermediate_tensors, inputs_embeds
835 )
836 return model_output
837
838 def compute_logits(
839 self,
840 hidden_states: torch.Tensor,
841 ) -> torch.Tensor | None:
842 return self.language_model.compute_logits(hidden_states)
843
844 def load_weights(
845 self,
846 weights: Iterable[tuple[str, torch.Tensor]],
847 ) -> set[str]:
848 loader = AutoWeightsLoader(self)
849 return loader.load_weights(weights)
850
851 def get_mm_mapping(self) -> MultiModelKeys:
852 """Get the module prefix in multimodal models."""
853 return MultiModelKeys.from_string_field(
854 language_model="language_model",
855 connector="projector",
856 tower_model="encoder",
857 )
858
859 ### Support for speech-to-text Transcription
860 @classmethod
861 def get_generation_prompt(
862 cls,
863 stt_params: SpeechToTextParams,
864 ) -> PromptType:
865 """Get the generation prompt to be used for transcription requests."""
866 audio = stt_params.audio
867 model_config = stt_params.model_config
868 task_type = stt_params.task_type
869 to_language = stt_params.to_language
870
871 # Audio placeholders don't use an index, so value doesn't matter
872 audio_tok = cls.get_placeholder_str("audio", 0)
873
874 if task_type == "translate":
875 full_lang_name_to = (
876 cls.supported_languages.get(to_language, to_language)
877 if to_language is not None
878 else ""
879 )
880 user_prompt = f"{audio_tok}translate the speech to {full_lang_name_to}" # noqa: E501
881 elif task_type == "transcribe":
882 user_prompt = (
883 f"{audio_tok}can you transcribe the speech into a written format?" # noqa: E501
884 )
885 else:
886 raise ValueError(f"Unsupported task type {task_type}")
887
888 tokenizer = cached_tokenizer_from_config(model_config)
889 chat = [dict(role="user", content=user_prompt)]
890 prompt = tokenizer.apply_chat_template(
891 chat,
892 tokenize=False,
893 add_generation_prompt=True,
894 )
895
896 prompt_token_ids = tokenizer.encode(prompt)
897
898 return TokensPrompt(
899 prompt_token_ids=prompt_token_ids,
900 multi_modal_data={"audio": audio},
901 )
902
903 # Adapted from https://github.com/huggingface/transformers/blob/v4.56.0/src/transformers/models/granite_speech/feature_extraction_granite_speech.py#L122 # noqa: E501
904 @classmethod
905 def get_num_audio_tokens(
906 cls,
907 audio_duration_s: float,
908 stt_config: SpeechToTextConfig,
909 model_config: ModelConfig,
910 ) -> int | None:
911 """Get the number of audio tokens for an audio duration in sec."""
912 processor = cached_processor_from_config(model_config)
913 hop_length = processor.audio_processor.melspec_kwargs["hop_length"]
914 proj_win_size = processor.audio_processor.projector_window_size
915 ds_rate = processor.audio_processor.projector_downsample_rate
916 effective_window_size = proj_win_size // ds_rate
917
918 raw_length = audio_duration_s * stt_config.sample_rate
919
920 # mel sequence length computation
921 mel_length = raw_length // hop_length + 1
922 # encoder frame takes two mel features
923 encoder_length = mel_length // 2
924 nblocks = math.ceil(encoder_length / proj_win_size)
925 # projector output length
926 return nblocks * effective_window_size
927
928 @classmethod
929 def get_speech_to_text_config(
930 cls, model_config: ModelConfig, task_type: str
931 ) -> SpeechToTextConfig:
932 """Get the stt config for this model."""
933 # Default settings are reasonable for this model and we don't currently
934 # expose this information in the model configs, but this may change in
935 # the future
936 return SpeechToTextConfig()