main
py 478 lines 15.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import math
4 from collections.abc import Iterable, Mapping, Sequence
5 from typing import Annotated, cast
6
7 import torch
8 from torch import nn
9 from transformers import (
10 BatchFeature,
11 Qwen2Config,
12 )
13
14 from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
15 from vllm.config.multimodal import MultiModalDummyOptions
16 from vllm.config.speech_to_text import SpeechToTextParams
17 from vllm.inputs import MultiModalDataDict, PromptType
18 from vllm.logger import init_logger
19 from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
20 from vllm.model_executor.layers.linear import (
21 ReplicatedLinear,
22 )
23 from vllm.model_executor.layers.logits_processor import LogitsProcessor
24 from vllm.model_executor.models.whisper_utils import (
25 ISO639_1_SUPPORTED_LANGS,
26 )
27 from vllm.multimodal import MULTIMODAL_REGISTRY
28 from vllm.multimodal.inputs import (
29 MultiModalFieldConfig,
30 MultiModalKwargsItems,
31 )
32 from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
33 from vllm.multimodal.processing import (
34 BaseDummyInputsBuilder,
35 BaseMultiModalProcessor,
36 BaseProcessingInfo,
37 PromptReplacement,
38 PromptUpdate,
39 PromptUpdateDetails,
40 )
41 from vllm.multimodal.processing.processor import HFMultiModalInputs
42 from vllm.transformers_utils.processor import cached_processor_from_config
43 from vllm.transformers_utils.processors.fireredasr2 import (
44 FireRedASR2FeatureExtractor,
45 )
46 from vllm.utils.tensor_schema import TensorSchema, TensorShape
47
48 from .conformer_encoder import ConformerEncoder
49 from .interfaces import (
50 MultiModalEmbeddings,
51 SupportsMultiModal,
52 SupportsTranscription,
53 _require_is_multimodal,
54 )
55 from .qwen2 import Qwen2ForCausalLM
56 from .utils import (
57 AutoWeightsLoader,
58 WeightsMapper,
59 _merge_multimodal_embeddings,
60 maybe_prefix,
61 )
62
63 logger = init_logger(__name__)
64
65
66 class FireRedASR2AudioInputs(TensorSchema):
67 """Dimensions:
68 - b: Batch size
69 - nmb: Number of mel bins
70 - t: Time frames (M)
71 """
72
73 input_features: Annotated[
74 list[torch.Tensor] | None,
75 TensorShape("b", "nmb", "t"),
76 ]
77 speech_lengths: Annotated[
78 list[torch.Tensor] | None,
79 TensorShape("b"),
80 ]
81 fake_token_lengths: Annotated[
82 list[torch.Tensor] | None,
83 TensorShape("b"),
84 ]
85
86
87 class FireRedASR2Adapter(nn.Module):
88 def __init__(self, encoder_dim: int, llm_dim: int, downsample_rate: int = 2):
89 super().__init__()
90 self.ds = downsample_rate
91 self.linear1 = ReplicatedLinear(
92 input_size=encoder_dim * downsample_rate,
93 output_size=llm_dim,
94 bias=True,
95 )
96 self.relu = _ACTIVATION_REGISTRY["relu"]
97 self.linear2 = ReplicatedLinear(
98 input_size=llm_dim,
99 output_size=llm_dim,
100 bias=True,
101 )
102
103 def forward(self, x, x_lens):
104 batch_size, seq_len, feat_dim = x.size()
105 num_frames_to_discard = seq_len % self.ds
106 if num_frames_to_discard > 0:
107 x = x[:, :-num_frames_to_discard, :]
108 seq_len = x.size(1)
109
110 x = x.contiguous()
111 x = x.view(batch_size, seq_len // self.ds, feat_dim * self.ds)
112
113 x, _ = self.linear1(x)
114 x = self.relu(x)
115 x, _ = self.linear2(x)
116
117 new_x_lens = torch.clamp(x_lens, max=seq_len) // self.ds
118 return x, new_x_lens
119
120
121 class FireRedASR2Encoder(nn.Module):
122 def __init__(
123 self,
124 *,
125 vllm_config: VllmConfig,
126 ):
127 super().__init__()
128 self.audio_encoder = ConformerEncoder(
129 **vllm_config.model_config.hf_config.audio_encoder_conf
130 )
131
132
133 class FireRedASR2Model(nn.Module):
134 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
135 super().__init__()
136 self.encoder = FireRedASR2Encoder(
137 vllm_config=vllm_config,
138 )
139 encoder_dim = self.encoder.audio_encoder.odim
140 llm_dim = vllm_config.model_config.hf_config.hidden_size
141 self.encoder_projector = FireRedASR2Adapter(
142 encoder_dim,
143 llm_dim,
144 vllm_config.model_config.hf_config.encoder_downsample_rate,
145 )
146
147 self.decoder = Qwen2ForCausalLM(
148 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "decoder")
149 )
150
151 def forward(
152 self,
153 input_ids: torch.Tensor | None,
154 positions: torch.Tensor,
155 inputs_embeds: torch.Tensor | None = None,
156 ) -> torch.Tensor:
157 decoder_outputs = self.decoder(
158 input_ids=input_ids,
159 positions=positions,
160 inputs_embeds=inputs_embeds,
161 )
162 return decoder_outputs
163
164 def get_encoder_outputs(
165 self,
166 speech: torch.Tensor | list[torch.Tensor] | None,
167 speech_lengths: torch.Tensor | list[torch.Tensor] | None,
168 ) -> torch.Tensor | None:
169 encoder_outs, enc_lengths, enc_mask = self.encoder.audio_encoder(
170 speech, speech_lengths
171 )
172 speech_features, speech_lens = self.encoder_projector(encoder_outs, enc_lengths)
173 return speech_features
174
175
176 class FireRedASR2ProcessingInfo(BaseProcessingInfo):
177 def get_hf_config(self) -> Qwen2Config:
178 return self.ctx.get_hf_config(Qwen2Config)
179
180 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
181 return {"audio": 1}
182
183 def get_feature_extractor(self, **kwargs: object) -> FireRedASR2FeatureExtractor:
184 hf_processor = self.get_hf_processor(**kwargs)
185 feature_extractor = hf_processor.feature_extractor # type: ignore
186 assert isinstance(feature_extractor, FireRedASR2FeatureExtractor)
187 return feature_extractor
188
189 def get_data_parser(self) -> MultiModalDataParser:
190 feature_extractor = self.get_feature_extractor()
191 return MultiModalDataParser(
192 target_sr=feature_extractor.sampling_rate,
193 target_channels=self.get_target_channels(),
194 )
195
196 def get_target_channels(self) -> int:
197 return 1
198
199
200 class FireRedASR2DummyInputsBuilder(BaseDummyInputsBuilder[FireRedASR2ProcessingInfo]):
201 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
202 num_audios = mm_counts.get("audio", 0)
203
204 return "<|AUDIO|>" * num_audios
205
206 def get_dummy_mm_data(
207 self,
208 seq_len: int,
209 mm_counts: Mapping[str, int],
210 mm_options: MultiModalDummyOptions,
211 ) -> MultiModalDataDict:
212 feature_extractor = self.info.get_feature_extractor()
213
214 sampling_rate = feature_extractor.sampling_rate
215 audio_len = feature_extractor.chunk_length * sampling_rate
216
217 ret = {
218 "audio": self._get_dummy_audios(
219 length=audio_len,
220 num_audios=mm_counts.get("audio", 0),
221 overrides=mm_options.get("audio"),
222 )
223 }
224 return ret
225
226
227 class FireRedASR2MultiModalProcessor(
228 BaseMultiModalProcessor[FireRedASR2ProcessingInfo]
229 ):
230 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
231 return self.dummy_inputs.get_dummy_text(mm_counts)
232
233 def _get_hf_mm_inputs(
234 self,
235 mm_items: MultiModalDataItems,
236 hf_kwargs: Mapping[str, object],
237 ) -> HFMultiModalInputs:
238 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
239
240 feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
241 return hf_inputs._replace(
242 hf_kwargs=dict(
243 hf_inputs.hf_kwargs,
244 sampling_rate=feature_extractor.sampling_rate,
245 )
246 )
247
248 def _get_mm_fields_config(
249 self,
250 hf_inputs: BatchFeature,
251 hf_processor_mm_kwargs: Mapping[str, object],
252 ) -> Mapping[str, MultiModalFieldConfig]:
253 return dict(
254 input_features=MultiModalFieldConfig.batched("audio"),
255 speech_lengths=MultiModalFieldConfig.batched("audio"),
256 fake_token_lengths=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
257 )
258
259 def _get_prompt_updates(
260 self,
261 mm_items: MultiModalDataItems,
262 hf_processor_mm_kwargs: Mapping[str, object],
263 out_mm_kwargs: MultiModalKwargsItems,
264 ) -> Sequence[PromptUpdate]:
265 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
266 tokenizer = self.info.get_tokenizer()
267 vocab = tokenizer.get_vocab()
268
269 audio_token = getattr(processor, "audio_token", "<|AUDIO|>")
270
271 audio_token_id = vocab[audio_token]
272
273 out_mm_data = out_mm_kwargs.get_data()
274
275 fake_token_lengths = out_mm_data.get("fake_token_lengths")
276
277 if fake_token_lengths is None:
278 audio_output_lengths = []
279 else:
280 assert isinstance(fake_token_lengths, torch.Tensor)
281
282 audio_output_lengths = fake_token_lengths.tolist()
283
284 def get_replacement_fireredasr2_audio(item_idx: int):
285 num_features = audio_output_lengths[item_idx]
286
287 audio_tokens = [audio_token_id] * int(num_features)
288
289 return PromptUpdateDetails.select_token_id(
290 audio_tokens,
291 embed_token_id=audio_token_id,
292 )
293
294 return [
295 PromptReplacement(
296 modality="audio",
297 target=[audio_token_id],
298 replacement=get_replacement_fireredasr2_audio,
299 )
300 ]
301
302
303 @MULTIMODAL_REGISTRY.register_processor(
304 FireRedASR2MultiModalProcessor,
305 info=FireRedASR2ProcessingInfo,
306 dummy_inputs=FireRedASR2DummyInputsBuilder,
307 )
308 class FireRedASR2ForConditionalGeneration(
309 nn.Module, SupportsTranscription, SupportsMultiModal
310 ):
311 packed_modules_mapping = {
312 "self_attn.qkv_proj": [
313 "self_attn.q_proj",
314 "self_attn.k_proj",
315 "self_attn.v_proj",
316 ],
317 "encoder_attn.kv_proj": ["encoder_attn.k_proj", "encoder_attn.v_proj"],
318 }
319
320 hf_to_vllm_mapper = WeightsMapper(
321 orig_to_new_substr={
322 "llm.": "model.decoder.",
323 "encoder.": "model.encoder.audio_encoder.",
324 "encoder_projector.": "model.encoder_projector.",
325 "net.0": "pre_layer_norm",
326 "net.1": "linear_expand",
327 "net.4": "linear_project",
328 },
329 orig_to_new_prefix={
330 "model.encoder.audio_encoder.positional_encoding.pe": None,
331 },
332 )
333
334 supports_transcription_only = True
335 supports_segment_timestamp = True
336 supported_languages = ISO639_1_SUPPORTED_LANGS
337
338 @classmethod
339 def validate_language(cls, language: str | None) -> str | None:
340 if language is None:
341 # TODO language should be optional and can be guessed.
342 # For now we default to en. See
343 # https://github.com/huggingface/transformers/blob/main/src/transformers/models/whisper/generation_whisper.py#L1520
344 logger.warning(
345 "Defaulting to language='en'. If you wish to transcribe "
346 "audio in a different language, pass the `language` field "
347 "in the TranscriptionRequest."
348 )
349 language = "en"
350 return super().validate_language(language)
351
352 @classmethod
353 def get_generation_prompt(
354 cls,
355 stt_params: SpeechToTextParams,
356 ) -> PromptType:
357 audio = stt_params.audio
358 stt_config = stt_params.stt_config
359 language = stt_params.language
360
361 if language is None:
362 raise ValueError(
363 "Language must be specified when creating the fireredasr2 prompt"
364 )
365
366 prompt_str = "<|im_start|>user\n<|AUDIO|>请转写音频为文字<|im_end|>\n<|im_start|>assistant\n" # noqa: E501
367 prompt = {
368 "prompt": prompt_str,
369 "multi_modal_data": {
370 "audio": (audio, stt_config.sample_rate),
371 },
372 }
373 return cast(PromptType, prompt)
374
375 @classmethod
376 def get_speech_to_text_config(
377 cls, model_config: ModelConfig, task_type: str
378 ) -> SpeechToTextConfig:
379 processor = cached_processor_from_config(model_config)
380
381 return SpeechToTextConfig(
382 max_audio_clip_s=processor.feature_extractor.chunk_length,
383 sample_rate=processor.feature_extractor.sampling_rate,
384 )
385
386 @classmethod
387 def get_num_audio_tokens(
388 cls,
389 audio_duration_s: float,
390 stt_config: SpeechToTextConfig,
391 model_config: ModelConfig,
392 ) -> int | None:
393 processor = cached_processor_from_config(model_config)
394 hop_length = processor.feature_extractor.hop_length
395 assert hop_length is not None
396 return math.ceil(audio_duration_s * stt_config.sample_rate / hop_length)
397
398 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
399 super().__init__()
400 config = vllm_config.model_config.hf_config
401 self.config = config
402 self.dtype = vllm_config.model_config.dtype
403
404 with self._mark_composite_model(
405 vllm_config,
406 language_targets=Qwen2ForCausalLM,
407 tower_targets={"audio": (FireRedASR2Encoder, FireRedASR2Adapter)},
408 ):
409 self.model = FireRedASR2Model(
410 vllm_config=vllm_config,
411 prefix=maybe_prefix(prefix, "model"),
412 )
413
414 logit_scale = getattr(config, "logit_scale", 1.0)
415 self.logits_processor = LogitsProcessor(config.vocab_size, scale=logit_scale)
416
417 def forward(
418 self,
419 input_ids: torch.Tensor,
420 positions: torch.Tensor,
421 inputs_embeds: torch.Tensor | None = None,
422 **kwargs,
423 ) -> torch.Tensor:
424 decoder_outputs = self.model(
425 input_ids=input_ids,
426 positions=positions,
427 inputs_embeds=inputs_embeds,
428 )
429 return decoder_outputs
430
431 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
432 audio_input = self._parse_and_validate_audio_input(**kwargs)
433
434 speech = audio_input["input_features"]
435 speech_lengths = audio_input["speech_lengths"].to(torch.int32)
436 enc_output = self.model.get_encoder_outputs(
437 speech=speech, speech_lengths=speech_lengths
438 )
439
440 return enc_output
441
442 def embed_input_ids(
443 self,
444 input_ids: torch.Tensor,
445 multimodal_embeddings: MultiModalEmbeddings | None = None,
446 *,
447 is_multimodal: torch.Tensor | None = None,
448 ) -> torch.Tensor:
449 inputs_embeds = self.model.decoder.embed_input_ids(input_ids)
450
451 ret = _merge_multimodal_embeddings(
452 inputs_embeds=inputs_embeds,
453 multimodal_embeddings=multimodal_embeddings,
454 is_multimodal=_require_is_multimodal(is_multimodal),
455 )
456 return ret
457
458 def _parse_and_validate_audio_input(
459 self, **kwargs: object
460 ) -> FireRedASR2AudioInputs:
461 input_features = kwargs.pop("input_features", None)
462 speech_lengths = kwargs.pop("speech_lengths", None)
463 fake_token_lengths = kwargs.pop("fake_token_lengths", None)
464
465 return FireRedASR2AudioInputs(
466 input_features=input_features,
467 speech_lengths=speech_lengths,
468 fake_token_lengths=fake_token_lengths,
469 )
470
471 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
472 logits = self.logits_processor(self.model.decoder.lm_head, hidden_states)
473 return logits
474
475 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
476 loader = AutoWeightsLoader(self)
477
478 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)