main
py 978 lines 36.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Inference-only FunAudioChat model compatible with HuggingFace weights.
5
6 FunAudioChat is a Qwen3 text model augmented with:
7 - a continuous audio encoder (Whisper-mel frontend + transformer)
8 - a discrete audio encoder (speech tokenizer + projector)
9
10 In the HF implementation, audio features are scattered into `<|AUDIO|>` token
11 positions via `inputs_embeds`, while `position_ids` (RoPE) remains standard 1D.
12 """
13
14 from __future__ import annotations
15
16 from collections.abc import Iterable, Mapping, Sequence
17 from functools import cached_property
18 from typing import Any
19
20 import numpy as np
21 import torch
22 import torch.nn as nn
23 from transformers import TokenizersBackend, WhisperFeatureExtractor
24 from transformers.activations import get_activation
25 from transformers.feature_extraction_utils import BatchFeature
26 from transformers.modeling_outputs import BaseModelOutput
27
28 from vllm.config import VllmConfig
29 from vllm.config.multimodal import MultiModalDummyOptions
30 from vllm.inputs import MultiModalDataDict
31 from vllm.model_executor.layers.attention.mm_encoder_attention import MMEncoderAttention
32 from vllm.model_executor.layers.linear import QKVParallelLinear, RowParallelLinear
33 from vllm.multimodal import MULTIMODAL_REGISTRY
34 from vllm.multimodal.inputs import (
35 MultiModalFieldConfig,
36 MultiModalKwargsItems,
37 )
38 from vllm.multimodal.parse import (
39 AudioProcessorItems,
40 MultiModalDataItems,
41 MultiModalDataParser,
42 )
43 from vllm.multimodal.processing import (
44 BaseDummyInputsBuilder,
45 BaseMultiModalProcessor,
46 BaseProcessingInfo,
47 PromptReplacement,
48 PromptUpdate,
49 PromptUpdateDetails,
50 )
51 from vllm.sequence import IntermediateTensors
52 from vllm.utils.import_utils import _has_module
53
54 from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
55 from .utils import (
56 AutoWeightsLoader,
57 WeightsMapper,
58 init_vllm_registered_model,
59 maybe_prefix,
60 )
61
62
63 class _SinusoidsPositionEmbedding(nn.Module):
64 def __init__(self, length: int, channels: int, max_timescale: float = 10000.0):
65 super().__init__()
66 if channels % 2 != 0:
67 raise ValueError("SinusoidsPositionEmbedding needs even channels input")
68
69 log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
70 inv_timescales = torch.exp(
71 -log_timescale_increment * torch.arange(channels // 2).float()
72 )
73 scaled_time = (
74 torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
75 )
76 self.register_buffer(
77 "positional_embedding",
78 torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1),
79 persistent=False,
80 )
81
82
83 class FunAudioChatAudioAttention(nn.Module):
84 """Multi-headed attention used inside the continuous audio tower."""
85
86 hf_to_vllm_mapper = WeightsMapper(
87 orig_to_new_stacked={
88 ".q_proj": (".qkv_proj", "q"),
89 ".k_proj": (".qkv_proj", "k"),
90 ".v_proj": (".qkv_proj", "v"),
91 }
92 )
93
94 def __init__(self, config: Any):
95 super().__init__()
96 self.embed_dim = int(config.d_model)
97 self.total_num_heads = int(config.encoder_attention_heads)
98 self.dropout = float(getattr(config, "attention_dropout", 0.0))
99 self.head_dim = self.embed_dim // self.total_num_heads
100 self.num_key_value_groups = 1 # needed for eager attention
101 self.config = config
102
103 if self.head_dim * self.total_num_heads != self.embed_dim:
104 raise ValueError(
105 "embed_dim must be divisible by num_heads "
106 f"(got embed_dim={self.embed_dim}, "
107 f"num_heads={self.total_num_heads})."
108 )
109 self.scaling = self.head_dim**-0.5
110 self.attention_dropout = 0.0
111 self.is_decoder = False
112 self.is_causal = False
113
114 self.qkv_proj = QKVParallelLinear(
115 self.embed_dim,
116 self.head_dim,
117 self.total_num_heads,
118 bias=True,
119 )
120 self.num_heads = self.qkv_proj.num_heads
121 self.num_kv_heads = self.qkv_proj.num_kv_heads
122 self.q_size = self.num_heads * self.head_dim
123 self.kv_size = self.num_kv_heads * self.head_dim
124
125 self.attn = MMEncoderAttention(
126 num_heads=self.num_heads,
127 head_size=self.head_dim,
128 scale=self.scaling,
129 num_kv_heads=self.num_kv_heads,
130 prefix="funaudiochat_audio_tower.attn",
131 )
132 self.out_proj = RowParallelLinear(
133 self.embed_dim,
134 self.embed_dim,
135 bias=True,
136 )
137
138 with torch.no_grad():
139 if self.qkv_proj.bias is not None:
140 # HF FunAudioChat uses bias=False for k_proj. Ensure the missing
141 # shard starts as zeros, while allowing q/v shards to load.
142 self.qkv_proj.bias.zero_()
143
144 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
145 loader = AutoWeightsLoader(self)
146 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
147
148 def forward(
149 self,
150 hidden_states: torch.Tensor,
151 cu_seqlens: torch.Tensor | None = None,
152 attention_mask: torch.Tensor | None = None,
153 **kwargs: object,
154 ) -> torch.Tensor:
155 del kwargs
156 del attention_mask
157 seq_length, _ = hidden_states.size()
158
159 qkv, _ = self.qkv_proj(hidden_states)
160 query_states, key_states, value_states = qkv.split(
161 [self.q_size, self.kv_size, self.kv_size], dim=-1
162 )
163
164 max_seqlen: torch.Tensor | None = None
165 if cu_seqlens is not None:
166 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
167
168 attn_output = self.attn(
169 query_states.reshape(1, seq_length, self.q_size),
170 key_states.reshape(1, seq_length, self.kv_size),
171 value_states.reshape(1, seq_length, self.kv_size),
172 cu_seqlens=cu_seqlens,
173 max_seqlen=max_seqlen,
174 ).reshape(seq_length, -1)
175
176 output, _ = self.out_proj(attn_output)
177 return output
178
179
180 class FunAudioChatAudioEncoderLayer(nn.Module):
181 def __init__(self, config: Any):
182 super().__init__()
183 self.embed_dim = int(config.d_model)
184 self.self_attn = FunAudioChatAudioAttention(config)
185 self.self_attn_layer_norm = nn.LayerNorm(self.embed_dim)
186 self.dropout = float(config.dropout)
187 self.activation_fn = get_activation(str(config.activation_function))
188 self.activation_dropout = float(config.activation_dropout)
189 self.fc1 = nn.Linear(self.embed_dim, int(config.encoder_ffn_dim))
190 self.fc2 = nn.Linear(int(config.encoder_ffn_dim), self.embed_dim)
191 self.final_layer_norm = nn.LayerNorm(self.embed_dim)
192
193 def forward(
194 self,
195 hidden_states: torch.Tensor,
196 cu_seqlens: torch.Tensor,
197 attention_mask: torch.Tensor | None = None,
198 **kwargs: object,
199 ) -> tuple[torch.Tensor]:
200 residual = hidden_states
201 hidden_states = self.self_attn_layer_norm(hidden_states)
202 hidden_states = self.self_attn(
203 hidden_states=hidden_states,
204 cu_seqlens=cu_seqlens,
205 attention_mask=attention_mask,
206 **kwargs,
207 )
208 hidden_states = residual + hidden_states
209
210 residual = hidden_states
211 hidden_states = self.final_layer_norm(hidden_states)
212 hidden_states = self.activation_fn(self.fc1(hidden_states))
213 hidden_states = nn.functional.dropout(
214 hidden_states, p=self.activation_dropout, training=self.training
215 )
216 hidden_states = self.fc2(hidden_states)
217 hidden_states = nn.functional.dropout(
218 hidden_states, p=self.dropout, training=self.training
219 )
220 hidden_states = residual + hidden_states
221
222 return (hidden_states,)
223
224
225 class FunAudioChatAudioEncoder(nn.Module):
226 """Continuous audio tower."""
227
228 def __init__(self, config: Any):
229 super().__init__()
230 self.config = config
231
232 embed_dim = int(config.d_model)
233 self.num_mel_bins = int(config.num_mel_bins)
234 self.max_source_positions = int(config.max_source_positions)
235 self.embed_scale = (embed_dim**0.5) if bool(config.scale_embedding) else 1.0
236 self.n_window = int(config.n_window)
237
238 self.conv1 = nn.Conv1d(self.num_mel_bins, embed_dim, kernel_size=3, padding=1)
239 self.conv2 = nn.Conv1d(embed_dim, embed_dim, kernel_size=3, stride=2, padding=1)
240 self.layers = nn.ModuleList(
241 [
242 FunAudioChatAudioEncoderLayer(config)
243 for _ in range(int(config.encoder_layers))
244 ]
245 )
246 self.ln_post = nn.LayerNorm(embed_dim)
247 self.avg_pooler = nn.AvgPool1d(2, stride=2)
248 self.proj = nn.Linear(embed_dim, int(config.output_dim))
249 self.positional_embedding = _SinusoidsPositionEmbedding(
250 self.max_source_positions, embed_dim
251 )
252
253 # Present in HF weights even if unused during S2T.
254 self.audio_bos_eos_token = nn.Embedding(2, int(config.output_dim))
255
256 @property
257 def dtype(self) -> torch.dtype:
258 return self.conv1.weight.dtype
259
260 def forward(
261 self,
262 input_features: torch.Tensor,
263 feature_lens: torch.Tensor,
264 aftercnn_lens: torch.Tensor,
265 speech_maxlen: int,
266 **kwargs: object,
267 ) -> BaseModelOutput:
268 # For max-length audio (300s => ~7500 speech frames at 25Hz), the
269 # Torch SDPA path can be prohibitively memory hungry (~O(n^2) inside the
270 # longest chunks). Require FlashAttention for such inputs to avoid OOM
271 # and performance cliffs.
272 if int(speech_maxlen) >= 7500:
273 if not _has_module("flash_attn"):
274 raise RuntimeError(
275 "FunAudioChat long audio (~300s) requires FlashAttention-2 "
276 "for the continuous audio tower, but `flash_attn` is not "
277 "installed in the runtime environment."
278 )
279 if not getattr(
280 self.layers[0].self_attn.attn, "is_flash_attn_backend", False
281 ):
282 raise RuntimeError(
283 "FunAudioChat long audio (~300s) requires FlashAttention for the "
284 "continuous audio tower, but the selected MM encoder attention "
285 "backend is not FlashAttention."
286 )
287
288 # Handle empty / invalid items (feature_lens == 0) without crashing.
289 original_batch_size = int(feature_lens.size(0))
290 device = input_features.device
291
292 valid_mask = feature_lens > 0
293 valid_indices = torch.where(valid_mask)[0]
294
295 if valid_indices.numel() == 0:
296 output_dim = int(self.proj.out_features)
297 return BaseModelOutput(
298 last_hidden_state=torch.zeros(
299 (original_batch_size, speech_maxlen, output_dim),
300 device=device,
301 dtype=self.proj.weight.dtype,
302 )
303 )
304
305 input_features_list = input_features.split(feature_lens.tolist(), dim=1)
306 valid_input_features_list = [input_features_list[int(i)] for i in valid_indices]
307 valid_input_features = torch.cat(valid_input_features_list, dim=1)
308
309 valid_feature_lens = feature_lens[valid_mask]
310 valid_aftercnn_lens = aftercnn_lens[valid_mask]
311
312 chunk_num = torch.ceil(valid_feature_lens / (self.n_window * 2)).long()
313
314 chunk_lengths_list: list[int] = []
315 full_chunk_len = self.n_window * 2
316 for i, length in enumerate(valid_feature_lens):
317 num_chunks_for_sample = int(chunk_num[i].item())
318 if num_chunks_for_sample == 0:
319 continue
320 chunk_lengths_list.extend([full_chunk_len] * (num_chunks_for_sample - 1))
321 last_chunk_len = int(length.item()) % full_chunk_len
322 if last_chunk_len == 0:
323 last_chunk_len = full_chunk_len
324 chunk_lengths_list.append(last_chunk_len)
325
326 chunk_lengths = torch.tensor(
327 chunk_lengths_list, dtype=torch.long, device=device
328 )
329
330 chunk_list = valid_input_features.split(chunk_lengths.tolist(), dim=1)
331 padded_feature, padded_mask, padded_mask_after_cnn = (
332 self.padded_and_mask_function(
333 chunk_list, chunk_lengths, padding_value=0, padding_side="right"
334 )
335 )
336
337 padded_embed = nn.functional.gelu(self.conv1(padded_feature)) * padded_mask
338 padded_embed = nn.functional.gelu(self.conv2(padded_embed)).transpose(1, 2)
339
340 padded_embed = padded_embed + self.positional_embedding.positional_embedding[
341 : padded_embed.shape[1], :
342 ].unsqueeze(0).to(padded_embed.dtype)
343
344 hidden_states = padded_embed[padded_mask_after_cnn]
345 cu_seqlens = torch.cat(
346 (
347 torch.zeros(1, device=padded_mask_after_cnn.device, dtype=torch.int32),
348 padded_mask_after_cnn.sum(1).cumsum(0),
349 )
350 ).to(torch.int32)
351
352 for encoder_layer in self.layers:
353 (hidden_states,) = encoder_layer(
354 hidden_states,
355 cu_seqlens=cu_seqlens,
356 **kwargs,
357 )
358
359 hidden_states_list = hidden_states.split(valid_aftercnn_lens.tolist(), dim=0)
360
361 pooled_list: list[torch.Tensor] = []
362 pooled_lengths: list[int] = []
363 for each_audio_states in hidden_states_list:
364 seq_len = int(each_audio_states.shape[0])
365 if seq_len >= 2:
366 pooled = nn.functional.avg_pool1d(
367 each_audio_states.transpose(0, 1), kernel_size=2, stride=2
368 ).transpose(0, 1)
369 else:
370 pooled = each_audio_states
371 pooled_list.append(pooled)
372 pooled_lengths.append(int(pooled.shape[0]))
373
374 pooled_concat = torch.cat(pooled_list, dim=0)
375 processed_concat = self.proj(self.ln_post(pooled_concat))
376 processed_audio_list = list(processed_concat.split(pooled_lengths, dim=0))
377
378 output_dim = (
379 int(processed_audio_list[0].shape[-1])
380 if processed_audio_list
381 else int(self.proj.out_features)
382 )
383 output_hidden_states = torch.zeros(
384 (original_batch_size, speech_maxlen, output_dim),
385 dtype=processed_audio_list[0].dtype
386 if processed_audio_list
387 else self.proj.weight.dtype,
388 device=device,
389 )
390
391 for valid_idx, processed in zip(valid_indices, processed_audio_list):
392 seq_len = min(int(processed.shape[0]), int(speech_maxlen))
393 output_hidden_states[int(valid_idx), :seq_len] = processed[:seq_len]
394
395 return BaseModelOutput(last_hidden_state=output_hidden_states)
396
397 def padded_and_mask_function(
398 self,
399 tensor_list: Sequence[torch.Tensor],
400 tensor_len: torch.Tensor,
401 padding_value: float = 0.0,
402 padding_side: str = "right",
403 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
404 max_len = int(tensor_len.max().item())
405 dim = int(tensor_list[0].shape[0])
406 padded_tensor = torch.full(
407 size=(len(tensor_list), dim, max_len),
408 fill_value=padding_value,
409 dtype=self.dtype,
410 device=tensor_list[0].device,
411 )
412
413 batch_mask = torch.zeros(
414 (len(tensor_len), max_len), dtype=torch.long, device=padded_tensor.device
415 )
416 for i, length in enumerate(tensor_len):
417 length_val = int(length.item())
418 batch_mask[i, :length_val] = 1
419 padded_tensor[i, :, :length_val] = tensor_list[i]
420
421 feature_lens_after_cnn = (tensor_len - 1) // 2 + 1
422 max_len_after_cnn = int(feature_lens_after_cnn.max().item())
423 batch_mask_after_cnn = torch.zeros(
424 (len(tensor_len), max_len_after_cnn),
425 dtype=torch.long,
426 device=padded_tensor.device,
427 )
428 for i, length in enumerate(feature_lens_after_cnn):
429 batch_mask_after_cnn[i, : int(length.item())] = 1
430
431 if padding_side != "right":
432 raise NotImplementedError("Only right padding is supported.")
433
434 return (
435 padded_tensor,
436 batch_mask.unsqueeze(1).to(padded_tensor.dtype),
437 batch_mask_after_cnn.bool(),
438 )
439
440 # From the HF FunAudioChat implementation.
441 def _get_feat_extract_output_lengths(
442 self, input_lengths: torch.LongTensor
443 ) -> tuple[torch.LongTensor, torch.LongTensor]:
444 input_lengths = (input_lengths - 1) // 2 + 1
445 output_lengths = (input_lengths - 2) // 2 + 1
446 return input_lengths, output_lengths
447
448
449 class FunAudioChatDiscreteEncoder(nn.Module):
450 """Discrete audio encoder (speech tokenizer -> grouped embeddings)."""
451
452 def __init__(self, config: Any):
453 super().__init__()
454 self.padding_idx = int(config.pad_token_id)
455 self.group_size = int(config.group_size)
456 self.hidden_size = int(config.output_dim)
457 self.continuous_features_mode = getattr(
458 config, "continuous_features_mode", "add"
459 )
460 self.embed_tokens = nn.Embedding(
461 int(config.codebook_size), self.hidden_size, self.padding_idx
462 )
463 self.output_matching = nn.Linear(self.hidden_size, self.hidden_size, bias=False)
464 self.continual_output_matching = nn.Linear(
465 self.hidden_size, self.hidden_size, bias=False
466 )
467
468 def forward(
469 self,
470 audio_ids: torch.Tensor,
471 continuous_audio_features: torch.Tensor | None = None,
472 continuous_audio_output_lengths: torch.Tensor | None = None,
473 feature_exist_mask: torch.Tensor | None = None,
474 ) -> torch.Tensor:
475 del continuous_audio_output_lengths
476
477 inputs_embeds = self.embed_tokens(audio_ids)
478 hidden_states = inputs_embeds.reshape(
479 inputs_embeds.shape[0], -1, self.group_size * self.hidden_size
480 )
481 hidden_states = hidden_states.reshape(
482 hidden_states.shape[0], -1, self.group_size, self.hidden_size
483 ).mean(dim=2)
484 hidden_states = self.output_matching(hidden_states)
485
486 if continuous_audio_features is not None:
487 continuous_audio_features = continuous_audio_features.reshape(
488 continuous_audio_features.shape[0],
489 -1,
490 self.group_size,
491 self.hidden_size,
492 ).mean(dim=2)
493 continuous_audio_hidden_states = self.continual_output_matching(
494 continuous_audio_features
495 )
496
497 if feature_exist_mask is None:
498 feature_exist_mask = torch.ones(
499 (hidden_states.shape[0],),
500 dtype=torch.bool,
501 device=hidden_states.device,
502 )
503 if self.continuous_features_mode == "add":
504 hidden_states[feature_exist_mask] += continuous_audio_hidden_states
505 else:
506 hidden_states[feature_exist_mask] = continuous_audio_hidden_states
507
508 return hidden_states
509
510 def _get_feat_extract_output_lengths(
511 self, input_lengths: torch.LongTensor
512 ) -> tuple[torch.LongTensor, torch.LongTensor]:
513 output_lengths = (input_lengths + self.group_size - 1) // self.group_size
514 return input_lengths, output_lengths
515
516
517 class FunAudioChatProcessingInfo(BaseProcessingInfo):
518 token_fps: int = 25
519
520 @cached_property
521 def feature_extractor(self) -> WhisperFeatureExtractor:
522 return WhisperFeatureExtractor.from_pretrained(
523 self.model_id,
524 revision=self.ctx.model_config.revision,
525 )
526
527 @cached_property
528 def speech_tokenizer(self) -> TokenizersBackend:
529 return TokenizersBackend.from_pretrained(
530 self.model_id,
531 subfolder="speech_tokenizer",
532 revision=self.ctx.model_config.revision,
533 )
534
535 def get_feature_extractor(self) -> WhisperFeatureExtractor:
536 return self.feature_extractor
537
538 def get_speech_tokenizer(self) -> TokenizersBackend:
539 return self.speech_tokenizer
540
541 def get_data_parser(self):
542 return MultiModalDataParser(
543 target_sr=int(self.feature_extractor.sampling_rate),
544 target_channels=self.get_target_channels(),
545 expected_hidden_size=self._get_expected_hidden_size(),
546 )
547
548 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
549 return {"audio": None}
550
551 def get_target_channels(self) -> int:
552 return 1
553
554 def get_mm_max_tokens_per_item(
555 self,
556 seq_len: int,
557 mm_counts: Mapping[str, int],
558 ) -> Mapping[str, int] | None:
559 # The discrete audio encoder downsamples 25Hz frames with group_size=5,
560 # so for a 300s clip the max number of `<|AUDIO|>` placeholders is 1500.
561 cfg = self.get_hf_config()
562 audio_cfg = getattr(cfg, "audio_config", None)
563 max_audio_tokens = int(getattr(audio_cfg, "max_source_positions", 1500))
564 return {"audio": max_audio_tokens}
565
566 def get_audio_group_size(self) -> int:
567 cfg = self.get_hf_config()
568 audio_cfg = getattr(cfg, "audio_config", None)
569 return int(getattr(audio_cfg, "group_size", 5))
570
571
572 class FunAudioChatDummyInputsBuilder(
573 BaseDummyInputsBuilder[FunAudioChatProcessingInfo]
574 ):
575 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
576 num_audios = mm_counts.get("audio", 0)
577 return "<|audio_bos|><|AUDIO|><|audio_eos|>" * int(num_audios)
578
579 def get_dummy_mm_data(
580 self,
581 seq_len: int,
582 mm_counts: Mapping[str, int],
583 mm_options: MultiModalDummyOptions,
584 ) -> MultiModalDataDict:
585 feature_extractor = self.info.get_feature_extractor()
586 sampling_rate = int(feature_extractor.sampling_rate)
587
588 # Dummy inputs are used for profiling; construct the worst-case audio
589 # length that maximizes the number of encoder tokens.
590 cfg = self.info.get_hf_config()
591 audio_cfg = getattr(cfg, "audio_config", None)
592 max_audio_tokens = int(getattr(audio_cfg, "max_source_positions", 1500))
593 group_size = self.info.get_audio_group_size()
594 token_fps = int(getattr(self.info, "token_fps", 25))
595 target_num_frames = max(1, max_audio_tokens) * max(1, group_size)
596 audio_len = max(
597 1,
598 (target_num_frames * sampling_rate + token_fps - 1) // token_fps,
599 )
600
601 return {
602 "audio": self._get_dummy_audios(
603 length=audio_len,
604 num_audios=int(mm_counts.get("audio", 0)),
605 overrides=mm_options.get("audio"),
606 )
607 }
608
609
610 class FunAudioChatMultiModalProcessor(
611 BaseMultiModalProcessor[FunAudioChatProcessingInfo]
612 ):
613 def _apply_hf_processor_main(
614 self,
615 mm_items: MultiModalDataItems,
616 hf_kwargs: Mapping[str, object],
617 ) -> BatchFeature:
618 mm_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
619 mm_items, hf_kwargs
620 )
621
622 if not mm_data:
623 return self._finalize_hf_mm_data(mm_data, hf_kwargs, passthrough_data)
624
625 prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
626
627 tokenizer = self.info.get_tokenizer()
628 input_ids = torch.tensor([tokenizer.encode(prompt_text)])
629
630 audios = mm_data.get("audio", [])
631 if not audios:
632 processed_data = BatchFeature({"input_ids": input_ids})
633 return self._finalize_hf_mm_data(
634 mm_data, hf_kwargs, passthrough_data, processed_data
635 )
636 assert isinstance(audios, Sequence)
637
638 feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
639 sr = int(feature_extractor.sampling_rate)
640 min_samples = int(getattr(feature_extractor, "n_fft", 400) or 400)
641
642 wavs: list[np.ndarray] = []
643 speech_strs: list[str] = []
644
645 speech_tokenizer = self.info.get_speech_tokenizer()
646 pad_token = speech_tokenizer.pad_token or "<|audio_pad|>"
647 for audio in audios:
648 if isinstance(audio, torch.Tensor):
649 audio = audio.detach().cpu().numpy()
650 audio_np = np.asarray(audio, dtype=np.float32)
651
652 if min_samples > 0 and audio_np.shape[0] < min_samples:
653 audio_np = np.pad(
654 audio_np, (0, min_samples - audio_np.shape[0]), mode="constant"
655 )
656
657 wavs.append(audio_np)
658 num_frames = int(
659 (float(audio_np.shape[0]) / float(sr)) * float(self.info.token_fps)
660 )
661 speech_strs.append(pad_token * max(1, int(num_frames)))
662
663 audio_group_size = self.info.get_audio_group_size()
664 speech_inputs = speech_tokenizer(
665 speech_strs,
666 return_attention_mask=True,
667 return_token_type_ids=False,
668 padding=True,
669 pad_to_multiple_of=audio_group_size,
670 return_tensors="pt",
671 )
672
673 wav_inputs = feature_extractor(
674 wavs,
675 sampling_rate=sr,
676 return_attention_mask=True,
677 padding="max_length",
678 return_tensors="pt",
679 )
680
681 mm_inputs: dict[str, torch.Tensor] = {
682 "speech_ids": speech_inputs["input_ids"],
683 "speech_attention_mask": speech_inputs["attention_mask"],
684 "input_features": wav_inputs["input_features"],
685 "feature_attention_mask": wav_inputs["attention_mask"],
686 "feature_exist_mask": torch.ones((len(wavs),), dtype=torch.bool),
687 }
688
689 processed_data = BatchFeature({"input_ids": input_ids, **mm_inputs})
690 return self._finalize_hf_mm_data(
691 mm_data, hf_kwargs, passthrough_data, processed_data
692 )
693
694 def _get_mm_fields_config(
695 self,
696 hf_inputs: BatchFeature,
697 hf_processor_mm_kwargs: Mapping[str, object],
698 ) -> Mapping[str, MultiModalFieldConfig]:
699 return {
700 "speech_ids": MultiModalFieldConfig.batched("audio"),
701 "speech_attention_mask": MultiModalFieldConfig.batched(
702 "audio", keep_on_cpu=True
703 ),
704 "input_features": MultiModalFieldConfig.batched("audio"),
705 "feature_attention_mask": MultiModalFieldConfig.batched("audio"),
706 "feature_exist_mask": MultiModalFieldConfig.batched("audio"),
707 }
708
709 def _get_prompt_updates(
710 self,
711 mm_items: MultiModalDataItems,
712 hf_processor_mm_kwargs: Mapping[str, object],
713 out_mm_kwargs: MultiModalKwargsItems,
714 ) -> Sequence[PromptUpdate]:
715 tokenizer = self.info.get_tokenizer()
716 vocab = tokenizer.get_vocab()
717
718 audio_token = "<|AUDIO|>"
719 audio_token_id = vocab[audio_token]
720
721 out_mm_data = out_mm_kwargs.get_data()
722 speech_attention_mask = out_mm_data.get("speech_attention_mask")
723 if speech_attention_mask is None:
724 audio_output_lengths: list[int] = []
725 else:
726 assert isinstance(speech_attention_mask, torch.Tensor)
727 speech_lengths = speech_attention_mask.sum(-1)
728 group_size = self.info.get_audio_group_size()
729 audio_output_lengths = (
730 (speech_lengths + group_size - 1) // group_size
731 ).tolist()
732
733 def get_replacement_funaudiochat(item_idx: int):
734 num_features = (
735 int(audio_output_lengths[item_idx]) if audio_output_lengths else 1
736 )
737 if num_features <= 0:
738 audios = mm_items.get_items("audio", AudioProcessorItems)
739 audio_len = audios.get_audio_length(item_idx)
740 raise ValueError(
741 f"The audio (len={audio_len}) is too short to be "
742 "represented inside the model"
743 )
744
745 audio_tokens = [audio_token_id] * num_features
746 return PromptUpdateDetails.select_token_id(
747 audio_tokens,
748 embed_token_id=audio_token_id,
749 )
750
751 return [
752 PromptReplacement(
753 modality="audio",
754 target=[audio_token_id],
755 replacement=get_replacement_funaudiochat,
756 )
757 ]
758
759
760 @MULTIMODAL_REGISTRY.register_processor(
761 FunAudioChatMultiModalProcessor,
762 info=FunAudioChatProcessingInfo,
763 dummy_inputs=FunAudioChatDummyInputsBuilder,
764 )
765 class FunAudioChatForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
766 hf_to_vllm_mapper = WeightsMapper(orig_to_new_prefix={"audio_invert_tower.": None})
767
768 @classmethod
769 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
770 if modality.startswith("audio"):
771 return "<|audio_bos|><|AUDIO|><|audio_eos|>"
772
773 raise ValueError("Only audio modality is supported")
774
775 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
776 super().__init__()
777 config = vllm_config.model_config.hf_config
778 quant_config = vllm_config.quant_config
779 multimodal_config = vllm_config.model_config.multimodal_config
780 self.config = config
781 self.multimodal_config = multimodal_config
782 self.quant_config = quant_config
783
784 with self._mark_tower_model(vllm_config, "audio"):
785 self.continuous_audio_tower = FunAudioChatAudioEncoder(config.audio_config)
786 self.audio_tower = FunAudioChatDiscreteEncoder(config.audio_config)
787
788 with self._mark_language_model(vllm_config):
789 self.language_model = init_vllm_registered_model(
790 vllm_config=vllm_config,
791 hf_config=config.text_config,
792 prefix=maybe_prefix(prefix, "language_model"),
793 architectures=["Qwen3ForCausalLM"],
794 )
795
796 self.make_empty_intermediate_tensors = (
797 self.language_model.make_empty_intermediate_tensors
798 )
799
800 def _get_continuous_audio_features(
801 self,
802 input_features: torch.Tensor,
803 feature_attention_mask: torch.Tensor,
804 speech_maxlen: int,
805 ) -> tuple[torch.Tensor, torch.Tensor]:
806 # Align mask and features to avoid indexing errors when padding differs.
807 if (
808 input_features.dim() == 3
809 and feature_attention_mask.shape[1] != input_features.shape[-1]
810 ):
811 min_len = min(
812 int(feature_attention_mask.shape[1]), int(input_features.shape[-1])
813 )
814 feature_attention_mask = feature_attention_mask[:, :min_len]
815 input_features = input_features[:, :, :min_len]
816
817 feature_lens = torch.sum(feature_attention_mask, dim=1)
818
819 flat_features = input_features.permute(0, 2, 1)[
820 feature_attention_mask.bool()
821 ].permute(1, 0)
822
823 audio_feat_lengths, audio_output_lengths = (
824 self.continuous_audio_tower._get_feat_extract_output_lengths(feature_lens)
825 )
826
827 audio_outputs = self.continuous_audio_tower(
828 flat_features,
829 feature_lens=feature_lens,
830 aftercnn_lens=audio_feat_lengths,
831 speech_maxlen=speech_maxlen,
832 )
833 return audio_outputs.last_hidden_state, audio_output_lengths
834
835 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
836 speech_ids = kwargs.get("speech_ids")
837 speech_attention_mask = kwargs.get("speech_attention_mask")
838 input_features = kwargs.get("input_features")
839 feature_attention_mask = kwargs.get("feature_attention_mask")
840 feature_exist_mask = kwargs.get("feature_exist_mask")
841
842 if speech_ids is None:
843 return []
844
845 pad_id = int(getattr(self.audio_tower, "padding_idx", 0))
846
847 if not isinstance(speech_ids, torch.Tensor):
848 if (
849 isinstance(speech_ids, (list, tuple))
850 and len(speech_ids) > 0
851 and all(isinstance(t, torch.Tensor) for t in speech_ids)
852 ):
853 speech_ids_tensors = []
854 for t in speech_ids:
855 if t.dim() == 2 and t.shape[0] == 1:
856 t = t.squeeze(0)
857 if t.dim() != 1:
858 raise TypeError(
859 "FunAudioChat speech_ids must be a 1D tensor per item "
860 f"(got shape={tuple(t.shape)})"
861 )
862 speech_ids_tensors.append(t)
863 speech_ids = nn.utils.rnn.pad_sequence(
864 speech_ids_tensors,
865 batch_first=True,
866 padding_value=pad_id,
867 )
868 else:
869 raise TypeError(
870 "FunAudioChat speech_ids must be a Tensor or a sequence of Tensors "
871 f"(got {type(speech_ids)})"
872 )
873
874 if speech_attention_mask is None:
875 speech_attention_mask = speech_ids.ne(pad_id).to(dtype=torch.int64)
876
877 if not isinstance(speech_attention_mask, torch.Tensor):
878 if (
879 isinstance(speech_attention_mask, (list, tuple))
880 and len(speech_attention_mask) > 0
881 and all(isinstance(t, torch.Tensor) for t in speech_attention_mask)
882 ):
883 mask_tensors = []
884 for t in speech_attention_mask:
885 if t.dim() == 2 and t.shape[0] == 1:
886 t = t.squeeze(0)
887 if t.dim() != 1:
888 raise TypeError(
889 "FunAudioChat speech_attention_mask must be a 1D tensor "
890 f"per item (got shape={tuple(t.shape)})"
891 )
892 mask_tensors.append(t)
893 speech_attention_mask = nn.utils.rnn.pad_sequence(
894 mask_tensors,
895 batch_first=True,
896 padding_value=0,
897 )
898 else:
899 raise TypeError(
900 "FunAudioChat speech_attention_mask must be a Tensor or a "
901 f"sequence of Tensors (got {type(speech_attention_mask)})"
902 )
903
904 group_size = int(self.audio_tower.group_size)
905 speech_maxlen = int(speech_ids.shape[-1])
906
907 # Ensure token length is divisible by group_size.
908 target_len = ((speech_maxlen + group_size - 1) // group_size) * group_size
909 if target_len > speech_maxlen:
910 pad_id = int(self.audio_tower.padding_idx)
911 pad_len = target_len - speech_maxlen
912 speech_ids = nn.functional.pad(speech_ids, (0, pad_len), value=pad_id)
913 speech_attention_mask = nn.functional.pad(
914 speech_attention_mask, (0, pad_len), value=0
915 )
916 speech_maxlen = int(speech_ids.shape[-1])
917
918 continuous_audio_features = None
919 continuous_audio_output_lengths = None
920 if input_features is not None and feature_attention_mask is not None:
921 assert isinstance(input_features, torch.Tensor)
922 assert isinstance(feature_attention_mask, torch.Tensor)
923 continuous_audio_features, continuous_audio_output_lengths = (
924 self._get_continuous_audio_features(
925 input_features=input_features,
926 feature_attention_mask=feature_attention_mask,
927 speech_maxlen=speech_maxlen,
928 )
929 )
930
931 if feature_exist_mask is None:
932 feature_exist_mask = torch.ones(
933 (speech_ids.shape[0],), dtype=torch.bool, device=speech_ids.device
934 )
935 assert isinstance(feature_exist_mask, torch.Tensor)
936
937 audio_features = self.audio_tower(
938 speech_ids,
939 continuous_audio_features=continuous_audio_features,
940 continuous_audio_output_lengths=continuous_audio_output_lengths,
941 feature_exist_mask=feature_exist_mask,
942 )
943
944 _, audio_output_lengths = self.audio_tower._get_feat_extract_output_lengths(
945 speech_attention_mask.sum(-1)
946 )
947 lengths = audio_output_lengths.tolist()
948
949 embeds = tuple(
950 audio_features[i, : int(length)] for i, length in enumerate(lengths)
951 )
952 return embeds
953
954 def forward(
955 self,
956 input_ids: torch.Tensor,
957 positions: torch.Tensor,
958 intermediate_tensors: IntermediateTensors | None = None,
959 inputs_embeds: torch.Tensor | None = None,
960 **kwargs: object,
961 ) -> torch.Tensor | IntermediateTensors:
962 del kwargs
963 if intermediate_tensors is not None:
964 inputs_embeds = None
965
966 return self.language_model.model(
967 input_ids,
968 positions,
969 intermediate_tensors,
970 inputs_embeds=inputs_embeds,
971 )
972
973 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
974 return self.language_model.compute_logits(hidden_states)
975
976 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
977 loader = AutoWeightsLoader(self)
978 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)