main
py 2,375 lines 88.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Copyright 2025 The Qwen team.
4 # Copyright 2023 The vLLM team.
5 # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
6 #
7 # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
8 # and OPT implementations in this library. It has been modified from its
9 # original forms to accommodate minor architectural differences compared
10 # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
11 #
12 # Licensed under the Apache License, Version 2.0 (the "License");
13 # you may not use this file except in compliance with the License.
14 # You may obtain a copy of the License at
15 #
16 # http://www.apache.org/licenses/LICENSE-2.0
17 #
18 # Unless required by applicable law or agreed to in writing, software
19 # distributed under the License is distributed on an "AS IS" BASIS,
20 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 # See the License for the specific language governing permissions and
22 # limitations under the License.
23 """Inference-only Qwen3-Omni-Moe model (thinker part)."""
24
25 from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
26 from functools import partial
27 from typing import Any, ClassVar
28
29 import numpy as np
30 import torch
31 import torch.nn as nn
32 import torch.nn.functional as F
33 from transformers import PreTrainedConfig
34 from transformers.feature_extraction_utils import BatchFeature
35 from transformers.models.qwen3_omni_moe.configuration_qwen3_omni_moe import (
36 Qwen3OmniMoeAudioEncoderConfig,
37 Qwen3OmniMoeConfig,
38 Qwen3OmniMoeThinkerConfig,
39 )
40 from transformers.models.qwen3_omni_moe.processing_qwen3_omni_moe import (
41 Qwen3OmniMoeProcessor,
42 )
43 from transformers.models.whisper import WhisperFeatureExtractor
44 from typing_extensions import TypeVar
45
46 from vllm.compilation.decorators import support_torch_compile
47 from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
48 from vllm.config.speech_to_text import SpeechToTextParams
49 from vllm.distributed import get_pp_group, get_tensor_model_parallel_world_size
50 from vllm.inputs import PromptType, TextPrompt
51 from vllm.logger import init_logger
52 from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
53 from vllm.model_executor.layers.attention.mm_encoder_attention import (
54 MMEncoderAttention,
55 )
56 from vllm.model_executor.layers.conv import Conv3dLayer
57 from vllm.model_executor.layers.linear import (
58 ColumnParallelLinear,
59 QKVParallelLinear,
60 ReplicatedLinear,
61 RowParallelLinear,
62 )
63 from vllm.model_executor.layers.logits_processor import LogitsProcessor
64 from vllm.model_executor.layers.quantization import QuantizationConfig
65 from vllm.model_executor.layers.rotary_embedding import get_rope
66 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
67 from vllm.model_executor.models.module_mapping import MultiModelKeys
68 from vllm.model_executor.models.qwen2_audio import Qwen2AudioProcessingInfo
69 from vllm.multimodal import MULTIMODAL_REGISTRY
70 from vllm.multimodal.inputs import (
71 MultiModalFeatureSpec,
72 MultiModalKwargsItem,
73 MultiModalKwargsItems,
74 )
75 from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems
76 from vllm.multimodal.processing.processor import (
77 BaseProcessingInfo,
78 HFMultiModalInputs,
79 MultiModalProcessingResult,
80 MultiModalPromptUpdates,
81 PlaceholderFeaturesInfo,
82 PromptReplacement,
83 PromptUpdate,
84 PromptUpdateDetails,
85 )
86 from vllm.sequence import IntermediateTensors
87 from vllm.transformers_utils.processor import cached_processor_from_config
88 from vllm.utils.gpu_sync_debug import gpu_sync_allowed
89 from vllm.utils.torch_utils import async_tensor_h2d
90 from vllm.v1.attention.backends.registry import AttentionBackendEnum
91
92 from .interfaces import (
93 MultiModalEmbeddings,
94 SupportsEagle3,
95 SupportsLoRA,
96 SupportsMRoPE,
97 SupportsMultiModal,
98 SupportsPP,
99 SupportsTranscription,
100 )
101 from .qwen2_5_omni_thinker import (
102 Qwen2_5OmniAudioFeatureInputs,
103 Qwen2_5OmniConditionalGenerationMixin,
104 Qwen2_5OmniThinkerDummyInputsBuilder,
105 Qwen2_5OmniThinkerMultiModalDataParser,
106 Qwen2_5OmniThinkerMultiModalProcessor,
107 _get_feature_tensor,
108 check_interleaved_audio_video,
109 merge_interleaved_embeddings,
110 )
111 from .qwen2_5_vl import (
112 Qwen2_5_VisionAttention,
113 Qwen2_5_VLImageInputs,
114 Qwen2_5_VLProcessingInfo,
115 Qwen2_5_VLVideoInputs,
116 )
117 from .qwen3_moe import Qwen3MoeForCausalLM, Qwen3MoeModel
118 from .utils import (
119 AutoWeightsLoader,
120 StageMissingLayer,
121 WeightsMapper,
122 _merge_multimodal_embeddings,
123 maybe_prefix,
124 )
125 from .vision import get_vit_attn_backend
126
127 logger = init_logger(__name__)
128
129 # Speech input languages supported by Qwen3-Omni
130 # From: https://huggingface.co/Qwen/Qwen3-Omni-30B-A3B-Instruct
131 ISO639_1_SUPPORTED_LANGS = {
132 "en": "English",
133 "zh": "Chinese",
134 "ko": "Korean",
135 "ja": "Japanese",
136 "de": "German",
137 "ru": "Russian",
138 "it": "Italian",
139 "fr": "French",
140 "es": "Spanish",
141 "pt": "Portuguese",
142 "ms": "Malay",
143 "nl": "Dutch",
144 "id": "Indonesian",
145 "tr": "Turkish",
146 "vi": "Vietnamese",
147 "yue": "Cantonese",
148 "ar": "Arabic",
149 "ur": "Urdu",
150 }
151
152
153 def _get_feat_extract_output_lengths(input_lengths: torch.Tensor):
154 input_lengths_leave = input_lengths % 100
155 feat_lengths = (input_lengths_leave - 1) // 2 + 1
156 output_lengths = (
157 ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
158 )
159 return output_lengths
160
161
162 # ============= Audio Encoder Components =============
163
164
165 class SinusoidsPositionEmbedding(nn.Module):
166 """Sinusoidal position embedding for audio encoder."""
167
168 def __init__(self, length: int, channels: int, max_timescale: int = 10000):
169 super().__init__()
170 self.length = length
171 self.channels = channels
172 self.max_timescale = max_timescale
173
174 if channels % 2 != 0:
175 raise ValueError("SinusoidsPositionEmbedding needs even channels input")
176
177 log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
178 inv_timescales = torch.exp(
179 -log_timescale_increment * torch.arange(channels // 2).float()
180 )
181 scaled_time = (
182 torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
183 )
184 positional_embedding = torch.cat(
185 [torch.sin(scaled_time), torch.cos(scaled_time)], dim=1
186 )
187 self.register_buffer(
188 "positional_embedding", positional_embedding, persistent=False
189 )
190
191 def forward(self, seqlen: int) -> torch.Tensor:
192 return self.positional_embedding[:seqlen, :]
193
194
195 class Qwen3OmniMoeAudioAttention(nn.Module):
196 """Multi-headed attention for Qwen3-Omni Audio Encoder using MMEncoderAttention."""
197
198 def __init__(
199 self,
200 config: Qwen3OmniMoeAudioEncoderConfig,
201 prefix: str = "",
202 ):
203 super().__init__()
204 self.embed_dim = config.d_model
205 self.num_heads = config.encoder_attention_heads
206 self.head_dim = self.embed_dim // self.num_heads
207 # Audio encoder uses 20 heads. Shard across TP when divisible
208 # (e.g. TP=2/4); otherwise keep unreplicated (e.g. TP=8).
209 tp_size = get_tensor_model_parallel_world_size()
210 disable_tp = self.num_heads % tp_size != 0
211 effective_tp = 1 if disable_tp else tp_size
212 self.num_local_heads = self.num_heads // effective_tp
213
214 if (self.head_dim * self.num_heads) != self.embed_dim:
215 raise ValueError(
216 f"embed_dim must be divisible by num_heads (got `embed_dim`: "
217 f"{self.embed_dim} and `num_heads`: {self.num_heads})."
218 )
219
220 self.scaling = self.head_dim**-0.5
221
222 self.qkv_proj = QKVParallelLinear(
223 hidden_size=self.embed_dim,
224 head_size=self.head_dim,
225 total_num_heads=self.num_heads,
226 total_num_kv_heads=self.num_heads,
227 bias=True,
228 disable_tp=disable_tp,
229 prefix=f"{prefix}.qkv_proj",
230 )
231
232 self.out_proj = RowParallelLinear(
233 input_size=self.embed_dim,
234 output_size=self.embed_dim,
235 bias=True,
236 disable_tp=disable_tp,
237 prefix=f"{prefix}.out_proj",
238 )
239
240 self.attn = MMEncoderAttention(
241 num_heads=self.num_local_heads,
242 head_size=self.head_dim,
243 scale=self.scaling,
244 prefix=f"{prefix}.attn",
245 )
246
247 def forward(
248 self,
249 hidden_states: torch.Tensor,
250 cu_seqlens: torch.Tensor,
251 max_seqlen: torch.Tensor | None,
252 ) -> torch.Tensor:
253 seq_length, _ = hidden_states.size()
254 qkv, _ = self.qkv_proj(hidden_states)
255 q, k, v = qkv.chunk(3, dim=-1)
256 q = q.view(1, seq_length, -1, self.head_dim)
257 k = k.view(1, seq_length, -1, self.head_dim)
258 v = v.view(1, seq_length, -1, self.head_dim)
259
260 attn_output = self.attn(
261 query=q,
262 key=k,
263 value=v,
264 cu_seqlens=cu_seqlens,
265 max_seqlen=max_seqlen,
266 )
267
268 attn_output = attn_output.view(seq_length, -1)
269 output, _ = self.out_proj(attn_output)
270 return output
271
272
273 class Qwen3OmniMoeAudioEncoderLayer(nn.Module):
274 """Transformer encoder layer for Qwen3-Omni Audio Encoder."""
275
276 def __init__(
277 self,
278 config: Qwen3OmniMoeAudioEncoderConfig,
279 prefix: str = "",
280 ):
281 super().__init__()
282 self.embed_dim = config.d_model
283 self.self_attn = Qwen3OmniMoeAudioAttention(
284 config, prefix=f"{prefix}.self_attn"
285 )
286 self.self_attn_layer_norm = nn.LayerNorm(self.embed_dim)
287 self.activation_fn = _ACTIVATION_REGISTRY[config.activation_function]
288 tp_size = get_tensor_model_parallel_world_size()
289 disable_tp = config.encoder_attention_heads % tp_size != 0
290 self.fc1 = ColumnParallelLinear(
291 self.embed_dim,
292 config.encoder_ffn_dim,
293 bias=True,
294 disable_tp=disable_tp,
295 prefix=f"{prefix}.fc1",
296 )
297 self.fc2 = RowParallelLinear(
298 config.encoder_ffn_dim,
299 self.embed_dim,
300 bias=True,
301 disable_tp=disable_tp,
302 prefix=f"{prefix}.fc2",
303 )
304 self.final_layer_norm = nn.LayerNorm(self.embed_dim)
305
306 def forward(
307 self,
308 hidden_states: torch.Tensor,
309 cu_seqlens: torch.Tensor,
310 max_seqlen: torch.Tensor | None,
311 ) -> torch.Tensor:
312 """Args:
313 hidden_states: Input tensor of shape (seq_len, hidden_size)
314 cu_seqlens: Cumulative sequence lengths
315 max_seqlen: Maximum sequence length in the batch
316
317 """
318 residual = hidden_states
319 hidden_states = self.self_attn_layer_norm(hidden_states)
320 hidden_states = self.self_attn(
321 hidden_states=hidden_states,
322 cu_seqlens=cu_seqlens,
323 max_seqlen=max_seqlen,
324 )
325 hidden_states = residual + hidden_states
326
327 residual = hidden_states
328 hidden_states = self.final_layer_norm(hidden_states)
329 hidden_states, _ = self.fc1(hidden_states)
330 hidden_states = self.activation_fn(hidden_states)
331 hidden_states, _ = self.fc2(hidden_states)
332 hidden_states = residual + hidden_states
333
334 # Clamp for numerical stability with fp16
335 if hidden_states.dtype == torch.float16:
336 clamp_value = torch.finfo(hidden_states.dtype).max - 1000
337 hidden_states = torch.clamp(
338 hidden_states, min=-clamp_value, max=clamp_value
339 )
340
341 return hidden_states
342
343
344 class Qwen3OmniMoeAudioEncoder(nn.Module):
345 """vLLM-native Qwen3-Omni Audio Encoder."""
346
347 hf_to_vllm_mapper = WeightsMapper(
348 orig_to_new_stacked={
349 ".self_attn.q_proj.": (".self_attn.qkv_proj.", "q"),
350 ".self_attn.k_proj.": (".self_attn.qkv_proj.", "k"),
351 ".self_attn.v_proj.": (".self_attn.qkv_proj.", "v"),
352 }
353 )
354
355 def __init__(
356 self,
357 config: Qwen3OmniMoeAudioEncoderConfig,
358 prefix: str = "",
359 ):
360 super().__init__()
361
362 embed_dim = config.d_model
363 self.num_mel_bins = config.num_mel_bins
364 self.max_source_positions = config.max_source_positions
365 self.n_window = config.n_window
366 self.n_window_infer = config.n_window_infer
367 self.conv_chunksize = config.conv_chunksize
368
369 # Position embedding
370 self.positional_embedding = SinusoidsPositionEmbedding(
371 self.max_source_positions, embed_dim
372 )
373
374 # Convolutional layers for mel-spectrogram processing
375 self.conv2d1 = nn.Conv2d(1, config.downsample_hidden_size, 3, 2, padding=1)
376 self.conv2d2 = nn.Conv2d(
377 config.downsample_hidden_size,
378 config.downsample_hidden_size,
379 3,
380 2,
381 padding=1,
382 )
383 self.conv2d3 = nn.Conv2d(
384 config.downsample_hidden_size,
385 config.downsample_hidden_size,
386 3,
387 2,
388 padding=1,
389 )
390
391 conv_out_dim = config.downsample_hidden_size * (
392 (((config.num_mel_bins + 1) // 2 + 1) // 2 + 1) // 2
393 )
394 self.conv_out = ReplicatedLinear(
395 conv_out_dim,
396 config.d_model,
397 bias=False,
398 return_bias=False,
399 prefix=f"{prefix}.conv_out",
400 )
401
402 # Transformer encoder layers
403 self.layers = nn.ModuleList(
404 [
405 Qwen3OmniMoeAudioEncoderLayer(
406 config,
407 prefix=f"{prefix}.layers.{i}",
408 )
409 for i in range(config.encoder_layers)
410 ]
411 )
412
413 # Output layers
414 self.ln_post = nn.LayerNorm(config.d_model)
415 self.proj1 = ReplicatedLinear(
416 config.d_model,
417 config.d_model,
418 bias=True,
419 return_bias=False,
420 prefix=f"{prefix}.proj1",
421 )
422 self.act = _ACTIVATION_REGISTRY[config.activation_function]
423 self.proj2 = ReplicatedLinear(
424 config.d_model,
425 config.output_dim,
426 bias=True,
427 return_bias=False,
428 prefix=f"{prefix}.proj2",
429 )
430
431 # Get attention backend
432 self.attn_backend = get_vit_attn_backend(
433 head_size=config.d_model // config.encoder_attention_heads,
434 dtype=torch.get_default_dtype(),
435 )
436
437 def compute_attn_mask_seqlen(self, cu_seqlens: torch.Tensor) -> torch.Tensor | None:
438 """Compute max_seqlen only for flash attention backends."""
439 max_seqlen = None
440 if self.attn_backend in {
441 AttentionBackendEnum.FLASH_ATTN,
442 AttentionBackendEnum.ROCM_AITER_FA,
443 AttentionBackendEnum.TRITON_ATTN,
444 }:
445 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
446 return max_seqlen
447
448 @property
449 def dtype(self) -> torch.dtype:
450 return self.conv2d1.weight.dtype
451
452 @property
453 def device(self) -> torch.device:
454 return self.conv2d1.weight.device
455
456 def forward(
457 self,
458 input_features: torch.Tensor,
459 feature_lens: torch.Tensor,
460 aftercnn_lens: torch.Tensor,
461 ):
462 # Compute chunk information.
463 chunk_num = torch.ceil(feature_lens / (self.n_window * 2)).long()
464
465 with gpu_sync_allowed():
466 total_chunks = int(chunk_num.sum())
467
468 chunk_lengths = async_tensor_h2d(
469 [self.n_window * 2] * total_chunks,
470 dtype=torch.long,
471 device=feature_lens.device,
472 )
473 tail_chunk_index = F.pad(chunk_num, (1, 0), value=-1).cumsum(0)[1:]
474 chunk_lengths[tail_chunk_index] = feature_lens % (self.n_window * 2)
475 chunk_lengths[chunk_lengths == 0] = self.n_window * 2
476
477 # Split input features into chunks and pad
478 with gpu_sync_allowed():
479 chunk_lengths_list = chunk_lengths.tolist()
480 chunk_list = input_features.T.split(chunk_lengths_list, dim=0)
481 padded_feature = nn.utils.rnn.pad_sequence(
482 chunk_list, batch_first=True
483 ).transpose(1, 2)
484
485 # Compute feature lengths after CNN
486 feature_lens_after_cnn = self._get_cnn_output_lengths(chunk_lengths)
487 # Vectorized mask creation: avoid creating many small tensors.
488 with gpu_sync_allowed():
489 max_len_after_cnn = feature_lens_after_cnn.max().item()
490 indices = torch.arange(max_len_after_cnn, device=padded_feature.device)
491 padded_mask_after_cnn = indices.unsqueeze(0) < feature_lens_after_cnn.unsqueeze(
492 1
493 )
494
495 # Add channel dimension for conv2d
496 padded_feature = padded_feature.unsqueeze(1)
497
498 # Apply convolutional layers (chunk if needed to avoid OOM)
499 if padded_feature.size(0) <= self.conv_chunksize:
500 # Fast path: no chunking needed
501 padded_embed = F.gelu(self.conv2d1(padded_feature))
502 padded_embed = F.gelu(self.conv2d2(padded_embed))
503 padded_embed = F.gelu(self.conv2d3(padded_embed))
504 else:
505 # Chunked processing to avoid OOM
506 padded_embeds = []
507 for chunk in padded_feature.split(self.conv_chunksize, dim=0):
508 padded_embed = F.gelu(self.conv2d1(chunk))
509 padded_embed = F.gelu(self.conv2d2(padded_embed))
510 padded_embed = F.gelu(self.conv2d3(padded_embed))
511 padded_embeds.append(padded_embed)
512 padded_embed = torch.cat(padded_embeds, dim=0)
513
514 # (batch, channels, freq, time) -> (batch, time, channels*freq)
515 b, c, f, t = padded_embed.size()
516 padded_embed = self.conv_out(
517 padded_embed.permute(0, 3, 1, 2).contiguous().view(b, t, c * f)
518 )
519
520 # Add positional embedding
521 positional_embedding = (
522 self.positional_embedding.positional_embedding[: padded_embed.shape[1], :]
523 .unsqueeze(0)
524 .to(padded_embed.dtype)
525 )
526 padded_embed = padded_embed + positional_embedding
527
528 with gpu_sync_allowed():
529 hidden_states = padded_embed[padded_mask_after_cnn]
530 # Use tolist() for efficient batch conversion from tensor to Python.
531 aftercnn_lens_list = aftercnn_lens.tolist()
532
533 # Compute cumulative sequence lengths for chunked attention
534 cu_chunk_lens = [0]
535 window_aftercnn = padded_mask_after_cnn.shape[-1] * (
536 self.n_window_infer // (self.n_window * 2)
537 )
538 for cnn_len in aftercnn_lens_list:
539 num_full_chunks = cnn_len // window_aftercnn
540 remainder = cnn_len % window_aftercnn
541 cu_chunk_lens.extend([window_aftercnn] * num_full_chunks)
542 if remainder:
543 cu_chunk_lens.append(remainder)
544 cu_seqlens = async_tensor_h2d(
545 cu_chunk_lens, dtype=torch.int32, device=aftercnn_lens.device
546 ).cumsum(-1, dtype=torch.int32)
547
548 max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
549
550 # Apply transformer layers
551 for encoder_layer in self.layers:
552 hidden_states = encoder_layer(
553 hidden_states,
554 cu_seqlens,
555 max_seqlen,
556 )
557
558 # Apply output layers
559 hidden_states = self.ln_post(hidden_states)
560 hidden_states = self.proj1(hidden_states)
561 hidden_states = self.act(hidden_states)
562 hidden_states = self.proj2(hidden_states)
563
564 return hidden_states
565
566 def _get_cnn_output_lengths(self, input_lengths: torch.Tensor) -> torch.Tensor:
567 """Compute output lengths after the three conv2d layers."""
568 lengths = input_lengths
569 for _ in range(3):
570 lengths = (lengths - 1) // 2 + 1
571 return lengths
572
573 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
574 loader = AutoWeightsLoader(self)
575 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
576
577
578 class Qwen3_VisionPatchEmbed(nn.Module):
579 def __init__(
580 self,
581 patch_size: int = 14,
582 temporal_patch_size: int = 2,
583 in_channels: int = 3,
584 hidden_size: int = 1152,
585 ) -> None:
586 super().__init__()
587 self.patch_size = patch_size
588 self.temporal_patch_size = temporal_patch_size
589 self.hidden_size = hidden_size
590
591 kernel_size = (temporal_patch_size, patch_size, patch_size)
592 self.proj = Conv3dLayer(
593 in_channels,
594 hidden_size,
595 kernel_size=kernel_size,
596 stride=kernel_size,
597 bias=True,
598 )
599
600 def forward(self, x: torch.Tensor) -> torch.Tensor:
601 L, _ = x.shape
602 x = x.view(L, -1, self.temporal_patch_size, self.patch_size, self.patch_size)
603 x = self.proj(x).view(L, self.hidden_size)
604 return x
605
606
607 class Qwen3_VisionMLP(nn.Module):
608 def __init__(
609 self,
610 in_features: int,
611 hidden_features: int,
612 bias: bool = False,
613 act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
614 quant_config: QuantizationConfig | None = None,
615 prefix: str = "",
616 ):
617 super().__init__()
618 self.linear_fc1 = ColumnParallelLinear(
619 in_features,
620 hidden_features,
621 bias=bias,
622 quant_config=quant_config,
623 return_bias=False,
624 prefix=f"{prefix}.linear_fc1",
625 )
626 self.linear_fc2 = RowParallelLinear(
627 hidden_features,
628 in_features,
629 bias=bias,
630 quant_config=quant_config,
631 return_bias=False,
632 prefix=f"{prefix}.linear_fc2",
633 )
634 self.act_fn = act_fn
635
636 def forward(self, x: torch.Tensor):
637 mlp_output = self.linear_fc2(self.act_fn(self.linear_fc1(x)))
638 return mlp_output
639
640
641 class Qwen3_VisionBlock(nn.Module):
642 def __init__(
643 self,
644 dim: int,
645 num_heads: int,
646 mlp_hidden_dim: int,
647 act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
648 norm_layer: Callable[[int], nn.Module] | None = None,
649 quant_config: QuantizationConfig | None = None,
650 prefix: str = "",
651 ) -> None:
652 super().__init__()
653 if norm_layer is None:
654 norm_layer = partial(nn.LayerNorm, eps=1e-6)
655 self.norm1 = norm_layer(dim)
656 self.norm2 = norm_layer(dim)
657 self.attn = Qwen2_5_VisionAttention(
658 embed_dim=dim,
659 num_heads=num_heads,
660 projection_size=dim,
661 quant_config=quant_config,
662 prefix=f"{prefix}.attn",
663 )
664 self.mlp = Qwen3_VisionMLP(
665 dim,
666 mlp_hidden_dim,
667 act_fn=act_fn,
668 bias=True,
669 quant_config=quant_config,
670 prefix=f"{prefix}.mlp",
671 )
672
673 def forward(
674 self,
675 x: torch.Tensor,
676 cu_seqlens: torch.Tensor,
677 rotary_pos_emb_cos: torch.Tensor,
678 rotary_pos_emb_sin: torch.Tensor,
679 max_seqlen: torch.Tensor | None, # Only used for Flash Attention
680 sequence_lengths: torch.Tensor | None, # Only used for FlashInfer CuDNN backend
681 ) -> torch.Tensor:
682 x = x + self.attn(
683 self.norm1(x),
684 cu_seqlens=cu_seqlens,
685 rotary_pos_emb_cos=rotary_pos_emb_cos,
686 rotary_pos_emb_sin=rotary_pos_emb_sin,
687 max_seqlen=max_seqlen,
688 sequence_lengths=sequence_lengths,
689 )
690
691 x = x + self.mlp(self.norm2(x))
692 return x
693
694
695 class Qwen3_VisionPatchMerger(nn.Module):
696 def __init__(
697 self,
698 d_model: int,
699 context_dim: int,
700 norm_layer: Callable[[int], nn.Module] | None = None,
701 spatial_merge_size: int = 2,
702 use_postshuffle_norm: bool = False,
703 quant_config: QuantizationConfig | None = None,
704 prefix: str = "",
705 ) -> None:
706 super().__init__()
707 self.hidden_size = context_dim * (spatial_merge_size**2)
708
709 self.use_postshuffle_norm = use_postshuffle_norm
710 if self.use_postshuffle_norm:
711 context_dim = self.hidden_size
712
713 if norm_layer is None:
714 norm_layer = partial(nn.LayerNorm, eps=1e-6)
715 self.use_postshuffle_norm = use_postshuffle_norm
716 self.ln_q = norm_layer(
717 self.hidden_size if use_postshuffle_norm else context_dim
718 )
719 self.mlp = nn.ModuleList(
720 [
721 ColumnParallelLinear(
722 self.hidden_size,
723 self.hidden_size,
724 bias=True,
725 quant_config=quant_config,
726 prefix=f"{prefix}.mlp.0",
727 ),
728 nn.GELU(),
729 RowParallelLinear(
730 self.hidden_size,
731 d_model,
732 bias=True,
733 quant_config=quant_config,
734 prefix=f"{prefix}.mlp.2",
735 ),
736 ]
737 )
738
739 def forward(self, x: torch.Tensor) -> torch.Tensor:
740 if self.use_postshuffle_norm:
741 x = self.ln_q(x.view(-1, self.hidden_size))
742 else:
743 x = self.ln_q(x).view(-1, self.hidden_size)
744
745 mlp_fc1, mlp_act, mlp_fc2 = self.mlp
746 x_parallel, _ = mlp_fc1(x)
747 x_parallel = mlp_act(x_parallel)
748 out, _ = mlp_fc2(x_parallel)
749 return out
750
751
752 class Qwen3Omni_VisionTransformer(nn.Module):
753 hf_to_vllm_mapper = WeightsMapper(
754 orig_to_new_stacked={
755 ".attn.q.": (".attn.qkv.", "q"),
756 ".attn.k.": (".attn.qkv.", "k"),
757 ".attn.v.": (".attn.qkv.", "v"),
758 }
759 )
760
761 def __init__(
762 self,
763 vision_config,
764 norm_eps: float = 1e-6,
765 quant_config: QuantizationConfig | None = None,
766 prefix: str = "",
767 ) -> None:
768 super().__init__()
769 self.hidden_size = vision_config.hidden_size
770 self.num_heads = vision_config.num_heads
771 self.image_size = vision_config.image_size
772 self.patch_size = vision_config.patch_size
773 self.spatial_merge_size = vision_config.spatial_merge_size
774 self.spatial_merge_unit = self.spatial_merge_size**2
775 self.temporal_patch_size = vision_config.temporal_patch_size
776 self.num_grid_per_side = self.image_size // self.patch_size
777 self.apply_vit_abs_pos_embed = vision_config.apply_vit_abs_pos_embed
778 self.deepstack_visual_indexes = vision_config.deepstack_visual_indexes
779
780 self.patch_embed = Qwen3_VisionPatchEmbed(
781 patch_size=self.patch_size,
782 temporal_patch_size=self.temporal_patch_size,
783 in_channels=vision_config.in_channels,
784 hidden_size=self.hidden_size,
785 )
786
787 # vit pos embedding, TODO: spatial_patch_size vs patch_size
788 if self.apply_vit_abs_pos_embed:
789 self.pos_embed = nn.Embedding(self.num_grid_per_side**2, self.hidden_size)
790 else:
791 self.pos_embed = nn.Parameter(
792 torch.empty([1, self.num_grid_per_side**2, self.hidden_size])
793 )
794
795 norm_layer = partial(nn.LayerNorm, eps=norm_eps)
796 head_dim = self.hidden_size // self.num_heads
797 self.rotary_pos_emb = get_rope(
798 head_size=head_dim,
799 max_position=8192,
800 is_neox_style=True,
801 rope_parameters={"partial_rotary_factor": 0.5},
802 )
803
804 self.blocks = nn.ModuleList(
805 [
806 Qwen3_VisionBlock(
807 dim=self.hidden_size,
808 num_heads=self.num_heads,
809 mlp_hidden_dim=vision_config.intermediate_size,
810 act_fn=_ACTIVATION_REGISTRY[vision_config.hidden_act],
811 norm_layer=norm_layer,
812 quant_config=quant_config,
813 prefix=f"{prefix}.blocks.{layer_idx}",
814 )
815 for layer_idx in range(vision_config.depth)
816 ]
817 )
818 self.merger = Qwen3_VisionPatchMerger(
819 d_model=vision_config.out_hidden_size,
820 context_dim=self.hidden_size,
821 norm_layer=norm_layer,
822 spatial_merge_size=self.spatial_merge_size,
823 quant_config=quant_config,
824 prefix=f"{prefix}.merger",
825 )
826 if self.deepstack_visual_indexes is not None:
827 self.merger_list = nn.ModuleList(
828 [
829 Qwen3_VisionPatchMerger(
830 d_model=vision_config.out_hidden_size,
831 context_dim=self.hidden_size,
832 spatial_merge_size=self.spatial_merge_size,
833 use_postshuffle_norm=True,
834 norm_layer=norm_layer,
835 quant_config=quant_config,
836 prefix=f"{prefix}.merger_list.{layer_idx}",
837 )
838 for layer_idx in range(len(self.deepstack_visual_indexes))
839 ]
840 )
841
842 self.attn_backend = get_vit_attn_backend(
843 head_size=head_dim,
844 dtype=torch.get_default_dtype(),
845 )
846
847 @property
848 def dtype(self) -> torch.dtype:
849 return self.patch_embed.proj.weight.dtype
850
851 @property
852 def device(self) -> torch.device:
853 return self.patch_embed.proj.weight.device
854
855 def rot_pos_emb(self, grid_thw):
856 max_grid_size = grid_thw[:, 1:].max()
857
858 # Use pre-computed cos_sin_cache from RotaryEmbedding
859 cos, sin = self.rotary_pos_emb.get_cos_sin(max_grid_size)
860
861 pos_ids = []
862 for t, h, w in grid_thw:
863 hpos_ids = torch.arange(h, device=cos.device).unsqueeze(1).expand(-1, w)
864 hpos_ids = hpos_ids.reshape(
865 h // self.spatial_merge_size,
866 self.spatial_merge_size,
867 w // self.spatial_merge_size,
868 self.spatial_merge_size,
869 )
870 hpos_ids = hpos_ids.permute(0, 2, 1, 3)
871 hpos_ids = hpos_ids.flatten()
872
873 wpos_ids = torch.arange(w, device=cos.device).unsqueeze(0).expand(h, -1)
874 wpos_ids = wpos_ids.reshape(
875 h // self.spatial_merge_size,
876 self.spatial_merge_size,
877 w // self.spatial_merge_size,
878 self.spatial_merge_size,
879 )
880 wpos_ids = wpos_ids.permute(0, 2, 1, 3)
881 wpos_ids = wpos_ids.flatten()
882 pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
883 pos_ids = torch.cat(pos_ids, dim=0)
884
885 cos_combined = cos[pos_ids].flatten(1)
886 sin_combined = sin[pos_ids].flatten(1)
887
888 return cos_combined, sin_combined
889
890 def fast_pos_embed_interpolate(self, grid_thw: list[list[int]]) -> torch.Tensor:
891 num_grid_per_side = self.num_grid_per_side
892 m_size = self.spatial_merge_size
893 hidden_dim = self.pos_embed.embedding_dim
894
895 outputs = []
896 for t, h, w in grid_thw:
897 h_idxs = torch.linspace(
898 0, num_grid_per_side - 1, h, dtype=torch.float32, device=self.device
899 )
900 w_idxs = torch.linspace(
901 0, num_grid_per_side - 1, w, dtype=torch.float32, device=self.device
902 )
903
904 h_floor = h_idxs.to(torch.long)
905 w_floor = w_idxs.to(torch.long)
906 h_ceil = torch.clamp(h_floor + 1, max=num_grid_per_side - 1)
907 w_ceil = torch.clamp(w_floor + 1, max=num_grid_per_side - 1)
908
909 dh = h_idxs - h_floor
910 dw = w_idxs - w_floor
911
912 # Create meshgrid view for all h, w vars
913 dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij")
914 h_floor_grid, w_floor_grid = torch.meshgrid(h_floor, w_floor, indexing="ij")
915 h_ceil_grid, w_ceil_grid = torch.meshgrid(h_ceil, w_ceil, indexing="ij")
916 h_floor_grid_idx = h_floor_grid * num_grid_per_side
917 h_ceil_grid_idx = h_ceil_grid * num_grid_per_side
918
919 # original computation of weights
920 # w00 = (1 - dh_grid) * (1 - dw_grid)
921 # w01 = (1 - dh_grid) * dw_grid
922 # w10 = dh_grid * (1 - dw_grid)
923 # w11 = dh_grid * dw_grid
924 # we reuse w11 here to avoid duplicate
925 # dh_grid * dw_grid computation
926 w11 = dh_grid * dw_grid
927 w10 = dh_grid - w11
928 w01 = dw_grid - w11
929 w00 = 1 - dh_grid - dw_grid + w11
930
931 idx00 = h_floor_grid_idx + w_floor_grid
932 idx01 = h_floor_grid_idx + w_ceil_grid
933 idx10 = h_ceil_grid_idx + w_floor_grid
934 idx11 = h_ceil_grid_idx + w_ceil_grid
935
936 indices = torch.stack([idx00, idx01, idx10, idx11], dim=0).reshape(4, -1)
937 weights = torch.stack([w00, w01, w10, w11], dim=0).reshape(4, -1, 1)
938 weights = weights.to(dtype=self.dtype, device=self.device)
939
940 embeds = self.pos_embed(indices)
941 weighted_embeds = embeds * weights
942 p0, p1, p2, p3 = weighted_embeds.unbind(dim=0)
943 combined = p0 + p1 + p2 + p3
944
945 combined = combined.view(h * w, hidden_dim)
946 repeated = combined.unsqueeze(0).expand(t, -1, -1).contiguous()
947 repeated = repeated.view(
948 t, h // m_size, m_size, w // m_size, m_size, hidden_dim
949 )
950 repeated = repeated.permute(0, 1, 3, 2, 4, 5).reshape(-1, hidden_dim)
951 outputs.append(repeated)
952
953 return torch.cat(outputs, dim=0)
954
955 def compute_attn_mask_seqlen(
956 self,
957 cu_seqlens: torch.Tensor,
958 ) -> torch.Tensor:
959 max_seqlen = torch.zeros([], device=cu_seqlens.device)
960 if self.attn_backend in {
961 AttentionBackendEnum.FLASH_ATTN,
962 AttentionBackendEnum.ROCM_AITER_FA,
963 AttentionBackendEnum.TRITON_ATTN,
964 }:
965 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
966 return max_seqlen
967
968 def forward(
969 self,
970 x: torch.Tensor,
971 grid_thw: torch.Tensor,
972 ) -> torch.Tensor:
973 hidden_states = x.to(device=self.device, dtype=self.dtype)
974 hidden_states = self.patch_embed(hidden_states)
975
976 if self.apply_vit_abs_pos_embed:
977 pos_embeds = self.fast_pos_embed_interpolate(grid_thw)
978 hidden_states = hidden_states + pos_embeds
979 rotary_pos_emb_cos, rotary_pos_emb_sin = self.rot_pos_emb(grid_thw)
980
981 # RDNA3 (gfx11) specific bug workaround: torch.repeat_interleave triggers
982 # kernel crashes. We attempt the operation and catch the RuntimeError
983 # to switch to a vectorized cumsum + searchsorted approach.
984 try:
985 cu_seqlens = torch.repeat_interleave(
986 grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
987 ).cumsum(
988 dim=0,
989 dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,
990 )
991 cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
992 except RuntimeError:
993 logger.warning(
994 "torch.repeat_interleave not executable, "
995 "switching to vectorized searchsorted implementation."
996 )
997 repeat_counts = grid_thw[:, 0]
998 values = grid_thw[:, 1] * grid_thw[:, 2]
999 repeat_cumsum = repeat_counts.cumsum(0)
1000 total_items = repeat_cumsum[-1].item()
1001
1002 indices = torch.searchsorted(
1003 repeat_cumsum,
1004 torch.arange(total_items, device=grid_thw.device),
1005 right=True,
1006 )
1007 cu_seqlens = values[indices].cumsum(
1008 dim=0,
1009 dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,
1010 )
1011 cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
1012
1013 # Move cu_seqlens to GPU; grid_thw may be on CPU during profile_run
1014 # and FA3 vit attention requires cu_seqlens on CUDA.
1015 cu_seqlens = async_tensor_h2d(cu_seqlens, self.device)
1016 hidden_states = hidden_states.unsqueeze(1)
1017 rotary_pos_emb_cos = rotary_pos_emb_cos.to(hidden_states.device)
1018 rotary_pos_emb_sin = rotary_pos_emb_sin.to(hidden_states.device)
1019 max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
1020
1021 # Recompute cu_seqlens in numpy from grid_thw to avoid GPU->CPU sync
1022 grid_thw_np = grid_thw.cpu().numpy()
1023 cu_seqlens_np = np.repeat(
1024 grid_thw_np[:, 1] * grid_thw_np[:, 2], grid_thw_np[:, 0]
1025 ).cumsum(axis=0, dtype=np.int32)
1026 cu_seqlens_np = np.concatenate([np.zeros(1, dtype=np.int32), cu_seqlens_np])
1027 sequence_lengths = MMEncoderAttention.maybe_compute_seq_lens(
1028 self.attn_backend,
1029 cu_seqlens_np,
1030 self.device,
1031 )
1032
1033 hidden_states_list = []
1034 deepstack_visual_indexes = self.deepstack_visual_indexes
1035
1036 for layer_num, blk in enumerate(self.blocks):
1037 hidden_states = blk(
1038 hidden_states,
1039 cu_seqlens=cu_seqlens,
1040 rotary_pos_emb_cos=rotary_pos_emb_cos,
1041 rotary_pos_emb_sin=rotary_pos_emb_sin,
1042 max_seqlen=max_seqlen,
1043 sequence_lengths=sequence_lengths,
1044 )
1045 if (
1046 deepstack_visual_indexes is not None
1047 and layer_num in deepstack_visual_indexes
1048 ):
1049 hidden_states_list.append(hidden_states)
1050
1051 hidden_states = self.merger(hidden_states)
1052
1053 # processing deepstack
1054 if deepstack_visual_indexes is not None:
1055 processed_hidden_states_list = [hidden_states]
1056 for idx, x in enumerate(hidden_states_list):
1057 x = self.merger_list[idx](x)
1058 processed_hidden_states_list.append(x)
1059 # we cat the original visual features and deepstack features
1060 # along the feature dim
1061 hidden_states = torch.cat(
1062 processed_hidden_states_list, dim=1
1063 ) # [seq_len, hidden_size * (1 + depth_of_deepstack)]
1064
1065 return hidden_states
1066
1067 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1068 loader = AutoWeightsLoader(self)
1069 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1070
1071
1072 @support_torch_compile(
1073 dynamic_arg_dims={
1074 "input_ids": 0,
1075 "positions": -1,
1076 "intermediate_tensors": 0,
1077 "inputs_embeds": 0,
1078 "deepstack_input_embeds": 0,
1079 }
1080 )
1081 class Qwen3MoeLLMModel(Qwen3MoeModel):
1082 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1083 super().__init__(vllm_config=vllm_config, prefix=prefix)
1084
1085 self.deepstack_multiscale_layer_start = 1
1086
1087 def forward(
1088 self,
1089 input_ids: torch.Tensor | None,
1090 positions: torch.Tensor,
1091 intermediate_tensors: IntermediateTensors | None = None,
1092 inputs_embeds: torch.Tensor | None = None,
1093 deepstack_input_embeds: IntermediateTensors | None = None,
1094 ) -> torch.Tensor | IntermediateTensors | tuple[torch.Tensor, list[torch.Tensor]]:
1095 if get_pp_group().is_first_rank:
1096 if inputs_embeds is not None:
1097 hidden_states = inputs_embeds
1098 else:
1099 hidden_states = self.embed_input_ids(input_ids)
1100 residual = None
1101 else:
1102 assert intermediate_tensors is not None
1103 hidden_states = intermediate_tensors["hidden_states"]
1104 residual = intermediate_tensors["residual"]
1105 aux_hidden_states = self._maybe_add_hidden_state(
1106 [], self.start_layer, hidden_states, residual
1107 )
1108 for layer_idx, layer in enumerate(
1109 self.layers[self.start_layer : self.end_layer]
1110 ):
1111 layer_idx = layer_idx + self.start_layer
1112
1113 hidden_states, residual = layer(
1114 positions,
1115 hidden_states,
1116 residual,
1117 )
1118
1119 if deepstack_input_embeds is not None and layer_idx in range(
1120 0, len(deepstack_input_embeds)
1121 ):
1122 hidden_states = (
1123 hidden_states
1124 + deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"]
1125 )
1126 self._maybe_add_hidden_state(
1127 aux_hidden_states, layer_idx + 1, hidden_states, residual
1128 )
1129
1130 if not get_pp_group().is_last_rank:
1131 return IntermediateTensors(
1132 {"hidden_states": hidden_states, "residual": residual}
1133 )
1134 hidden_states, _ = self.norm(hidden_states, residual)
1135 if aux_hidden_states:
1136 return hidden_states, aux_hidden_states
1137 return hidden_states
1138
1139
1140 class Qwen3MoeLLMForCausalLM(Qwen3MoeForCausalLM):
1141 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1142 super(Qwen3MoeForCausalLM, self).__init__()
1143 config = vllm_config.model_config.hf_config
1144 quant_config = vllm_config.quant_config
1145 self.config = config
1146 self.quant_config = quant_config
1147 self.model = Qwen3MoeLLMModel(
1148 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
1149 )
1150 self.lm_head = ParallelLMHead(
1151 config.vocab_size, config.hidden_size, quant_config=quant_config
1152 )
1153 if self.config.tie_word_embeddings:
1154 self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
1155 self.logits_processor = LogitsProcessor(config.vocab_size)
1156 self.make_empty_intermediate_tensors = (
1157 self.model.make_empty_intermediate_tensors
1158 )
1159
1160
1161 class Qwen3OmniMoeThinkerMultiModalDataParser(Qwen2_5OmniThinkerMultiModalDataParser):
1162 # Keep the existing raw-media fallback for vision inputs.
1163 embedding_fields = {
1164 "audio": Qwen2_5OmniThinkerMultiModalDataParser.embedding_fields["audio"],
1165 }
1166
1167
1168 class Qwen3OmniMoeThinkerProcessingInfo(
1169 Qwen2AudioProcessingInfo, Qwen2_5_VLProcessingInfo
1170 ):
1171 def get_hf_config(self):
1172 return self.ctx.get_hf_config(Qwen3OmniMoeConfig).thinker_config
1173
1174 def get_hf_processor(self, **kwargs: object) -> Qwen3OmniMoeProcessor:
1175 processor = self.ctx.get_hf_processor(
1176 Qwen3OmniMoeProcessor,
1177 **kwargs,
1178 )
1179 if not hasattr(processor, "audio_token"):
1180 processor.audio_token = "<|audio_pad|>"
1181 if not hasattr(processor, "image_token"):
1182 processor.image_token = "<|image_pad|>"
1183 if not hasattr(processor, "video_token"):
1184 processor.video_token = "<|video_pad|>"
1185 return processor
1186
1187 def get_feature_extractor(self, **kwargs: object):
1188 hf_processor = self.get_hf_processor(**kwargs)
1189 feature_extractor = hf_processor.feature_extractor # type: ignore
1190 assert isinstance(feature_extractor, WhisperFeatureExtractor)
1191 return feature_extractor
1192
1193 def get_data_parser(self):
1194 return Qwen3OmniMoeThinkerMultiModalDataParser(
1195 spatial_merge_size=self.get_hf_config().vision_config.spatial_merge_size,
1196 target_sr=self.get_feature_extractor().sampling_rate,
1197 target_channels=self.get_target_channels(),
1198 expected_hidden_size=self._get_expected_hidden_size(),
1199 allow_missing_mm_embeddings=self.allow_missing_mm_embeddings,
1200 )
1201
1202 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
1203 return {"audio": None, "image": None, "video": None}
1204
1205 def get_mm_max_tokens_per_item(
1206 self,
1207 seq_len: int,
1208 mm_counts: Mapping[str, int] | None = None,
1209 ) -> Mapping[str, int]:
1210 mm_counts = mm_counts or {}
1211 requested_modalities = {m for m, c in mm_counts.items() if c > 0}
1212 mm_max_tokens: dict[str, int] = {}
1213
1214 if requested_modalities & {"image", "video"}:
1215 vl_tokens = Qwen2_5_VLProcessingInfo.get_mm_max_tokens_per_item(
1216 self,
1217 seq_len=seq_len,
1218 mm_counts=mm_counts,
1219 )
1220 mm_max_tokens.update(
1221 {
1222 m: vl_tokens[m]
1223 for m in ["image", "video"]
1224 if m in requested_modalities
1225 }
1226 )
1227
1228 if "audio" in requested_modalities:
1229 audio_tokens = Qwen2AudioProcessingInfo.get_mm_max_tokens_per_item(
1230 self,
1231 seq_len=seq_len,
1232 mm_counts=mm_counts,
1233 )
1234 mm_max_tokens["audio"] = audio_tokens["audio"]
1235
1236 return mm_max_tokens
1237
1238
1239 Qwen3OmniMoeThinkerDummyInputsBuilder = Qwen2_5OmniThinkerDummyInputsBuilder[
1240 Qwen3OmniMoeThinkerProcessingInfo
1241 ]
1242
1243 _I = TypeVar("_I", bound=BaseProcessingInfo, default=Qwen3OmniMoeThinkerProcessingInfo)
1244
1245
1246 class Qwen3OmniMoeThinkerMultiModalProcessor(
1247 Qwen2_5OmniThinkerMultiModalProcessor[_I],
1248 ):
1249 def _get_hf_mm_inputs(
1250 self,
1251 mm_items: MultiModalDataItems,
1252 hf_kwargs: Mapping[str, object],
1253 ) -> HFMultiModalInputs:
1254 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
1255
1256 audios = hf_inputs.hf_data.get("audio")
1257 if audios:
1258 assert isinstance(audios, (list, tuple))
1259 feature_extractor = self.info.get_hf_processor().feature_extractor
1260 hop_length = feature_extractor.hop_length
1261
1262 def pad_to_hop_length(x: np.ndarray) -> np.ndarray:
1263 length = x.shape[-1]
1264 if length % hop_length != 0:
1265 pad_length = hop_length - (length % hop_length)
1266 x = np.pad(x, (0, pad_length), mode="constant", constant_values=0)
1267 return x
1268
1269 # To make sure the cache works with padding=True, we pre-padded
1270 # the audio to a multiple of hop_length.
1271 hf_inputs.hf_data["audio"] = [
1272 pad_to_hop_length(audio)
1273 if isinstance(audio, np.ndarray)
1274 else (pad_to_hop_length(audio[0]), audio[1])
1275 for audio in audios
1276 ]
1277
1278 return hf_inputs
1279
1280 def _postprocess_hf_mm_data(
1281 self,
1282 mm_data: Mapping[str, object],
1283 hf_processor_mm_kwargs: Mapping[str, object],
1284 processed_data: BatchFeature,
1285 ) -> BatchFeature:
1286 processed_data = super()._postprocess_hf_mm_data(
1287 mm_data, hf_processor_mm_kwargs, processed_data
1288 )
1289
1290 if (
1291 "audio_feature_lengths" in processed_data
1292 and "feature_attention_mask" in processed_data
1293 ):
1294 audios = mm_data.get("audio", [])
1295 if audios:
1296 assert isinstance(audios, (list, tuple))
1297 feature_extractor = self.info.get_hf_processor(
1298 **hf_processor_mm_kwargs
1299 ).feature_extractor
1300 hop_length = feature_extractor.hop_length
1301
1302 audio_num_frames = []
1303 for audio in audios:
1304 audio_length = (
1305 len(audio[0]) if isinstance(audio, tuple) else len(audio)
1306 )
1307 num_frame = (
1308 (audio_length // hop_length)
1309 if audio_length % hop_length == 0
1310 else (audio_length // hop_length - 1)
1311 )
1312 if hf_processor_mm_kwargs.get("truncation", False):
1313 num_frame = min(
1314 num_frame, feature_extractor.n_samples // hop_length
1315 )
1316 audio_num_frames.append(num_frame)
1317
1318 processed_data["feature_attention_mask"] = [
1319 torch.ones(num_frame) for num_frame in audio_num_frames
1320 ]
1321 processed_data["audio_feature_lengths"] = torch.tensor(audio_num_frames)
1322
1323 return processed_data
1324
1325 def _maybe_apply_prompt_updates(
1326 self,
1327 mm_items: MultiModalDataItems,
1328 mm_res: MultiModalProcessingResult,
1329 ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
1330 """Qwen3-Omni reimplements this function to handle `use_audio_in_video`."""
1331 mm_item_counts = mm_items.get_all_counts()
1332 prompt_ids = mm_res.prompt_ids
1333 mm_kwargs = mm_res.kwargs
1334 mm_prompt_updates = mm_res.prompt_updates
1335
1336 self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
1337
1338 use_audio_in_video = False
1339 if "video" in mm_kwargs:
1340 for item in mm_kwargs["video"]:
1341 if item and item["use_audio_in_video"].data:
1342 use_audio_in_video = True
1343 else:
1344 use_audio_in_video = False
1345 # for mutilmodality cache
1346 if any(item is None for item in mm_kwargs["video"]):
1347 video_token_id = self.info.get_hf_config().video_token_id
1348 audio_token_id = self.info.get_hf_config().audio_token_id
1349 video_audio_item_num = sum(
1350 id in (video_token_id, audio_token_id) for id in prompt_ids
1351 )
1352 audio_updates_num = len(mm_prompt_updates.get("audio", []))
1353 video_updates_num = len(mm_prompt_updates.get("video", []))
1354 if video_audio_item_num != video_updates_num + audio_updates_num:
1355 use_audio_in_video = True
1356
1357 if use_audio_in_video and "audio" in mm_prompt_updates:
1358 filtered_updates = {
1359 k: v for k, v in mm_prompt_updates.items() if k != "audio"
1360 }
1361 prompt_ids, mm_placeholders = self._apply_prompt_updates(
1362 prompt_ids,
1363 filtered_updates,
1364 )
1365 # Derive audio placeholders from video placeholders
1366 mm_placeholders = self._derive_audio_from_video_placeholders(
1367 mm_placeholders, mm_prompt_updates
1368 )
1369 else:
1370 prompt_ids, mm_placeholders = self._apply_prompt_updates(
1371 prompt_ids,
1372 mm_prompt_updates,
1373 )
1374
1375 self._validate_mm_placeholders(mm_placeholders, mm_item_counts)
1376
1377 return prompt_ids, mm_placeholders
1378
1379 def get_updates_use_audio_in_video(
1380 self,
1381 thinker_config: PreTrainedConfig,
1382 audio_len: int,
1383 video_grid_thw: list[int] | torch.Tensor,
1384 video_second_per_grid_t: float,
1385 ) -> list[int]:
1386 shift = 0
1387 audio_token_id = thinker_config.audio_token_id
1388 video_token_id = thinker_config.video_token_id
1389 audio_start_token_id = thinker_config.audio_start_token_id
1390 audio_end_token_id = thinker_config.audio_end_token_id
1391 spatial_merge_size = thinker_config.vision_config.spatial_merge_size
1392 position_id_per_seconds = thinker_config.position_id_per_seconds
1393 audio_token_indices = np.arange(next(iter([audio_len])))
1394 curr_video_grid_thw = next(iter([video_grid_thw]))
1395 height = curr_video_grid_thw[1] // spatial_merge_size
1396 width = curr_video_grid_thw[2] // spatial_merge_size
1397 video_token_indices = np.arange(curr_video_grid_thw[0]).reshape(-1, 1, 1)
1398 video_token_indices = np.broadcast_to(
1399 video_token_indices, (video_token_indices.shape[0], height, width)
1400 ).reshape(-1)
1401 video_token_indices = (
1402 (video_token_indices + shift)
1403 * next(iter([video_second_per_grid_t]))
1404 * position_id_per_seconds
1405 )
1406 video_data_index, audio_data_index = 0, 0
1407 updates = [audio_start_token_id]
1408 while video_data_index < len(video_token_indices) and audio_data_index < len(
1409 audio_token_indices
1410 ):
1411 if (
1412 video_token_indices[video_data_index]
1413 <= audio_token_indices[audio_data_index]
1414 ):
1415 updates += [video_token_id]
1416 video_data_index += 1
1417 else:
1418 updates += [audio_token_id]
1419 audio_data_index += 1
1420 if video_data_index < len(video_token_indices):
1421 updates += [video_token_id] * (len(video_token_indices) - video_data_index)
1422 if audio_data_index < len(audio_token_indices):
1423 updates += [audio_token_id] * (len(audio_token_indices) - audio_data_index)
1424 updates += [audio_end_token_id]
1425 return updates
1426
1427 def _get_prompt_updates(
1428 self,
1429 mm_items: MultiModalDataItems,
1430 hf_processor_mm_kwargs: Mapping[str, Any],
1431 out_mm_kwargs: MultiModalKwargsItems,
1432 ) -> Sequence[PromptUpdate]:
1433 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1434 tokenizer = self.info.get_tokenizer()
1435 assert isinstance(self.info, Qwen2_5_VLProcessingInfo)
1436 image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
1437 vocab = tokenizer.get_vocab()
1438
1439 audio_token = processor.audio_token
1440 image_token = processor.image_token
1441 video_token = processor.video_token
1442 audio_token_id = vocab[audio_token]
1443 image_token_id = vocab[image_token]
1444 video_token_id = vocab[video_token]
1445
1446 out_mm_data = out_mm_kwargs.get_data()
1447 audio_feature_lengths = out_mm_data.get("audio_feature_lengths")
1448 feature_attention_mask = out_mm_data.get("feature_attention_mask")
1449 if audio_feature_lengths is None and feature_attention_mask is None:
1450 audio_output_lengths = []
1451 elif audio_feature_lengths is not None:
1452 audio_output_lens = _get_feat_extract_output_lengths(audio_feature_lengths)
1453 audio_output_lengths = audio_output_lens.tolist()
1454 elif feature_attention_mask is not None:
1455 assert isinstance(feature_attention_mask, torch.Tensor)
1456 audio_output_lens = _get_feat_extract_output_lengths(
1457 feature_attention_mask.sum(-1)
1458 )
1459 audio_output_lengths = audio_output_lens.tolist()
1460
1461 # number of audios read from video.
1462 audio_in_video_item_idx = 0
1463 audio_item_idx = 0
1464
1465 def get_replacement_qwen2_audio(item_idx: int):
1466 nonlocal audio_item_idx
1467 item_idx += audio_in_video_item_idx
1468
1469 audio_item_idx += 1
1470
1471 num_features = audio_output_lengths[item_idx]
1472 if num_features == 0:
1473 audios = mm_items.get_items("audio", AudioProcessorItems)
1474 audio = audios.get(item_idx)
1475 assert audio is not None
1476 raise ValueError(
1477 f"The audio {audio} (len={len(audio)}) is too short "
1478 "to be represented inside the model"
1479 )
1480
1481 return [audio_token_id] * num_features
1482
1483 def get_replacement_qwen2_vision(item_idx: int, modality: str):
1484 grid_thw = out_mm_data[f"{modality}_grid_thw"][item_idx]
1485 assert isinstance(grid_thw, torch.Tensor)
1486 merge_length = image_processor.merge_size**2
1487
1488 token_id = image_token_id if modality == "image" else video_token_id
1489 return [token_id] * (int(grid_thw.prod()) // merge_length)
1490
1491 use_audio_in_video = hf_processor_mm_kwargs.get("use_audio_in_video", False)
1492 thinker_config = self.info.get_hf_config()
1493
1494 def get_replacement_qwen2_use_audio_in_video(item_idx: int):
1495 nonlocal audio_in_video_item_idx
1496 audio_num_features = audio_output_lengths[audio_in_video_item_idx]
1497 video_grid_thw = out_mm_data["video_grid_thw"][item_idx]
1498
1499 audio_in_video_item_idx += 1
1500
1501 second_per_grid_ts = hf_processor_mm_kwargs.get("second_per_grid_ts", None)
1502 if second_per_grid_ts:
1503 video_second_per_grid_t = second_per_grid_ts[item_idx]
1504 else:
1505 video_second_per_grid_t = 2.0
1506
1507 placeholder = self.get_updates_use_audio_in_video(
1508 thinker_config=thinker_config,
1509 audio_len=audio_num_features,
1510 video_grid_thw=video_grid_thw,
1511 video_second_per_grid_t=video_second_per_grid_t,
1512 )
1513 return PromptUpdateDetails.select_token_id(
1514 placeholder, embed_token_id=video_token_id
1515 )
1516
1517 video_replacement_fn = (
1518 get_replacement_qwen2_use_audio_in_video
1519 if use_audio_in_video
1520 else partial(get_replacement_qwen2_vision, modality="video")
1521 )
1522
1523 return [
1524 PromptReplacement(
1525 modality="audio",
1526 target=[audio_token_id],
1527 replacement=get_replacement_qwen2_audio,
1528 ),
1529 PromptReplacement(
1530 modality="image",
1531 target=[image_token_id],
1532 replacement=partial(get_replacement_qwen2_vision, modality="image"),
1533 ),
1534 PromptReplacement(
1535 modality="video",
1536 target=[video_token_id],
1537 replacement=video_replacement_fn,
1538 ),
1539 ]
1540
1541 def _derive_audio_from_video_placeholders(
1542 self,
1543 placeholders: Mapping[str, list[PlaceholderFeaturesInfo]],
1544 mm_prompt_updates: MultiModalPromptUpdates,
1545 ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
1546 """Helper to derive audio placeholders from video placeholders when
1547 use_audio_in_video=True.
1548 """
1549 if "video" not in placeholders:
1550 return placeholders
1551
1552 # Validate audio and video counts match
1553 num_videos = len(placeholders["video"])
1554 num_audios = len(mm_prompt_updates.get("audio", []))
1555 if num_audios != num_videos:
1556 raise ValueError(
1557 f"use_audio_in_video requires equal number of audio and video items, "
1558 f"got {num_audios=}, {num_videos=}"
1559 )
1560
1561 tokenizer = self.info.get_tokenizer()
1562 processor = self.info.get_hf_processor()
1563 audio_token_id = tokenizer.get_vocab()[processor.audio_token]
1564
1565 result_placeholders = dict(placeholders)
1566 audio_placeholders = []
1567
1568 # Each video is paired with one audio
1569 for video_idx, video_placeholder in enumerate(placeholders["video"]):
1570 # Create is_embed mask selecting only audio tokens
1571 audio_is_embed = torch.tensor(video_placeholder.tokens) == audio_token_id
1572
1573 audio_placeholder = PlaceholderFeaturesInfo(
1574 modality="audio",
1575 item_idx=video_idx,
1576 start_idx=video_placeholder.start_idx,
1577 tokens=video_placeholder.tokens,
1578 is_embed=audio_is_embed,
1579 )
1580 audio_placeholders.append(audio_placeholder)
1581
1582 result_placeholders["audio"] = audio_placeholders
1583 return result_placeholders
1584
1585
1586 class Qwen3OmniMoeConditionalGenerationMixin(Qwen2_5OmniConditionalGenerationMixin):
1587 def _process_audio_input(
1588 self,
1589 audio_input: Qwen2_5OmniAudioFeatureInputs,
1590 ) -> tuple[torch.Tensor, ...]:
1591 input_features = audio_input["input_features"]
1592 # audio_feature_lengths is keep_on_cpu; the audio tower derives
1593 # device placement from feature_lens, so move it explicitly.
1594 audio_feature_lengths = async_tensor_h2d(
1595 audio_input["audio_feature_lengths"], input_features.device
1596 )
1597
1598 audio_output_lengths = _get_feat_extract_output_lengths(audio_feature_lengths)
1599
1600 audio_features = self.audio_tower(
1601 input_features.to(self.audio_tower.dtype),
1602 feature_lens=audio_feature_lengths,
1603 aftercnn_lens=audio_output_lengths,
1604 )
1605 return audio_features.split(audio_output_lengths.tolist())
1606
1607
1608 @MULTIMODAL_REGISTRY.register_processor(
1609 Qwen3OmniMoeThinkerMultiModalProcessor,
1610 info=Qwen3OmniMoeThinkerProcessingInfo,
1611 dummy_inputs=Qwen3OmniMoeThinkerDummyInputsBuilder,
1612 )
1613 class Qwen3OmniMoeThinkerForConditionalGeneration(
1614 nn.Module,
1615 SupportsMultiModal,
1616 SupportsLoRA,
1617 SupportsPP,
1618 SupportsMRoPE,
1619 SupportsEagle3,
1620 Qwen3OmniMoeConditionalGenerationMixin,
1621 SupportsTranscription,
1622 ):
1623 is_3d_moe_weight: ClassVar[bool] = True
1624 supports_tower_connector_lora: ClassVar[bool] = True
1625 hf_to_vllm_mapper = WeightsMapper(
1626 orig_to_new_prefix={
1627 "thinker.lm_head.": "language_model.lm_head.",
1628 "thinker.model.": "language_model.model.",
1629 "thinker.": "",
1630 "talker.": None,
1631 "code2wav.": None,
1632 # Adapter trained/saved directly from the thinker model
1633 "model.": "language_model.model.",
1634 "lm_head.": "language_model.lm_head.",
1635 }
1636 )
1637
1638 packed_modules_mapping = {
1639 "qkv_proj": [
1640 "q_proj",
1641 "k_proj",
1642 "v_proj",
1643 ],
1644 "gate_up_proj": [
1645 "gate_proj",
1646 "up_proj",
1647 ],
1648 "qkv": ["qkv"], # for visual encoder
1649 }
1650
1651 supported_languages = ISO639_1_SUPPORTED_LANGS
1652
1653 @classmethod
1654 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1655 if modality.startswith("image"):
1656 return "<|vision_start|><|image_pad|><|vision_end|>"
1657 if modality.startswith("video"):
1658 return "<|vision_start|><|video_pad|><|vision_end|>"
1659 if modality.startswith("audio"):
1660 return "<|audio_start|><|audio_pad|><|audio_end|>"
1661
1662 raise ValueError("Only image, video or audio modality is supported")
1663
1664 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1665 super().__init__()
1666 self.vllm_config = vllm_config # needed for torch compile forward context
1667 thinker_config: Qwen3OmniMoeThinkerConfig = (
1668 vllm_config.model_config.hf_config.thinker_config
1669 )
1670 # MoE LoRA uses the architecture to determine its weight layout.
1671 thinker_config.architectures = vllm_config.model_config.hf_config.architectures
1672 quant_config = vllm_config.quant_config
1673 multimodal_config = vllm_config.model_config.multimodal_config
1674 self.config = thinker_config
1675 self.multimodal_config = multimodal_config
1676 self.quant_config = quant_config
1677
1678 with self._mark_tower_model(vllm_config, "audio"):
1679 self.audio_tower = Qwen3OmniMoeAudioEncoder(
1680 thinker_config.audio_config,
1681 prefix=maybe_prefix(prefix, "audio_tower"),
1682 )
1683
1684 with self._mark_tower_model(vllm_config, {"image", "video"}):
1685 self.visual = Qwen3Omni_VisionTransformer(
1686 vision_config=thinker_config.vision_config,
1687 norm_eps=getattr(thinker_config.text_config, "rms_norm_eps", 1e-6),
1688 quant_config=quant_config,
1689 prefix=maybe_prefix(prefix, "visual"),
1690 )
1691
1692 self.use_deepstack = hasattr(
1693 thinker_config.vision_config, "deepstack_visual_indexes"
1694 ) and not isinstance(self.visual, StageMissingLayer)
1695 self.deepstack_num_level = (
1696 len(thinker_config.vision_config.deepstack_visual_indexes)
1697 if self.use_deepstack
1698 else 0
1699 )
1700 self.visual_dim = thinker_config.vision_config.out_hidden_size
1701 self.multiscale_dim = self.visual_dim * self.deepstack_num_level
1702
1703 if self.use_deepstack:
1704 self.deepstack_input_embeds = [
1705 torch.zeros(
1706 vllm_config.scheduler_config.max_num_batched_tokens,
1707 thinker_config.text_config.hidden_size,
1708 )
1709 for _ in range(self.deepstack_num_level)
1710 ]
1711 # Tracks the valid token span currently stored in the buffer.
1712 # Zero means there is no active deepstack payload to consume.
1713 self.deepstack_input_embeds_num_tokens = 0
1714
1715 with self._mark_language_model(vllm_config):
1716 self.language_model = Qwen3MoeLLMForCausalLM(
1717 vllm_config=vllm_config.with_hf_config(
1718 thinker_config.text_config,
1719 architectures=["Qwen3MoeForCausalLM"],
1720 ),
1721 prefix=maybe_prefix(prefix, "language_model"),
1722 )
1723
1724 self.make_empty_intermediate_tensors = (
1725 self.language_model.make_empty_intermediate_tensors
1726 )
1727
1728 def _get_deepstack_input_embeds(
1729 self,
1730 num_tokens: int,
1731 ) -> IntermediateTensors | None:
1732 if not getattr(self, "deepstack_input_embeds", None):
1733 return None # If vision tower is skipped
1734 if num_tokens > self.deepstack_input_embeds[0].size(0):
1735 self._resize_deepstack_input_embeds(num_tokens)
1736
1737 # get deepstack_input_embeds from buffer, and clear the buffer
1738 return IntermediateTensors(
1739 {
1740 f"deepstack_input_embeds_{idx}": self.deepstack_input_embeds[idx][
1741 :num_tokens
1742 ]
1743 for idx in range(self.deepstack_num_level)
1744 }
1745 )
1746
1747 def _resize_deepstack_input_embeds(self, num_tokens: int) -> None:
1748 self.deepstack_input_embeds = [
1749 torch.zeros(
1750 num_tokens,
1751 self.config.text_config.hidden_size,
1752 device=self.deepstack_input_embeds[0].device,
1753 dtype=self.deepstack_input_embeds[0].dtype,
1754 )
1755 for _ in range(self.deepstack_num_level)
1756 ]
1757
1758 def _set_deepstack_input_embeds(self, deepstack_input_embeds: torch.Tensor) -> None:
1759 if not getattr(self, "deepstack_input_embeds", None):
1760 return
1761
1762 # set deepstack_input_embeds to buffer
1763 num_tokens = deepstack_input_embeds.size(1)
1764 if num_tokens > self.deepstack_input_embeds[0].size(0):
1765 self._resize_deepstack_input_embeds(num_tokens)
1766 for idx in range(self.deepstack_num_level):
1767 self.deepstack_input_embeds[idx][:num_tokens].copy_(
1768 deepstack_input_embeds[idx]
1769 )
1770 self.deepstack_input_embeds_num_tokens = num_tokens
1771
1772 def _clear_deepstack_input_embeds(self, num_tokens: int) -> None:
1773 if not getattr(self, "deepstack_input_embeds", None):
1774 return
1775 if getattr(self, "deepstack_input_embeds_num_tokens", 0) == 0:
1776 return
1777
1778 # clear deepstack_input_embeds in buffer
1779 if num_tokens > 0:
1780 for idx in range(self.deepstack_num_level):
1781 self.deepstack_input_embeds[idx][:num_tokens].zero_()
1782 self.deepstack_input_embeds_num_tokens = 0
1783
1784 def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1785 mm_input_by_modality: dict[
1786 str,
1787 Qwen2_5_VLImageInputs
1788 | Qwen2_5_VLVideoInputs
1789 | Qwen2_5OmniAudioFeatureInputs
1790 | None,
1791 ] = {}
1792
1793 # Preserve the order of modalities if there are multiple of them
1794 # from the order of kwargs.
1795 for input_key in kwargs:
1796 if (
1797 input_key in ("pixel_values", "image_embeds")
1798 and "image" not in mm_input_by_modality
1799 ):
1800 mm_input_by_modality["image"] = self._parse_and_validate_image_input(
1801 **kwargs
1802 )
1803 if (
1804 input_key in ("pixel_values_videos", "video_embeds")
1805 and "video" not in mm_input_by_modality
1806 ):
1807 mm_input_by_modality["video"] = self._parse_and_validate_video_input(
1808 **kwargs
1809 )
1810 if (
1811 input_key in ("input_audio_features")
1812 and "audio" not in mm_input_by_modality
1813 ):
1814 mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(
1815 **kwargs
1816 )
1817 return mm_input_by_modality
1818
1819 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
1820 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
1821 if not mm_input_by_modality:
1822 return []
1823
1824 # The result multimodal_embeddings is tuple of tensors, with each
1825 # tensor corresponding to a multimodal data item (image or video).
1826 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1827
1828 # NOTE: It is important to iterate over the keys in this dictionary
1829 # to preserve the order of the modalities.
1830 for modality in mm_input_by_modality:
1831 multimodal_input = mm_input_by_modality[modality]
1832 if modality == "image":
1833 image_embeddings = self._process_image_input(multimodal_input)
1834 multimodal_embeddings += tuple(image_embeddings)
1835 if modality == "video":
1836 video_embeddings = self._process_video_input(multimodal_input)
1837 multimodal_embeddings += tuple(video_embeddings)
1838 if modality == "audio":
1839 audio_embeddings = self._process_audio_input(multimodal_input)
1840 multimodal_embeddings += tuple(audio_embeddings)
1841 return multimodal_embeddings
1842
1843 def embed_input_ids(
1844 self,
1845 input_ids: torch.Tensor,
1846 multimodal_embeddings: MultiModalEmbeddings | None = None,
1847 *,
1848 is_multimodal: torch.Tensor | None = None,
1849 ) -> torch.Tensor:
1850 inputs_embeds = self._embed_text_input_ids(
1851 input_ids,
1852 self.language_model.embed_input_ids,
1853 is_multimodal=is_multimodal,
1854 )
1855
1856 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
1857 return inputs_embeds
1858
1859 # Detect interleaved audio-in-video early, since it affects
1860 # both the deepstack path and the final embedding merge.
1861 video_token_id = self.config.video_token_id
1862 audio_token_id = self.config.audio_token_id
1863 image_token_id = self.config.image_token_id
1864 input_ids_cpu = input_ids.cpu()
1865 is_video = is_multimodal & (input_ids_cpu == video_token_id)
1866 is_audio = is_multimodal & (input_ids_cpu == audio_token_id)
1867 is_image = is_multimodal & (input_ids_cpu == image_token_id)
1868 num_video = is_video.sum().item()
1869 num_audio = is_audio.sum().item()
1870
1871 is_interleaved = check_interleaved_audio_video(
1872 is_video, is_audio, num_video, num_audio
1873 )
1874
1875 deepstack_input_embeds = None
1876 # split the feat dim to obtain multi-scale visual feature
1877 has_vision_embeddings = [
1878 embeddings.shape[-1] != self.config.text_config.hidden_size
1879 for embeddings in multimodal_embeddings
1880 ]
1881 if self.visual.deepstack_visual_indexes is not None and any(
1882 has_vision_embeddings
1883 ):
1884 multimodal_embeddings = list(multimodal_embeddings)
1885 multiscale_len = len(self.visual.deepstack_visual_indexes)
1886 multimodal_embeddings_multiscale = []
1887
1888 if is_interleaved:
1889 # Use input_ids-based mask for all vision positions when
1890 # audio and video tokens are interleaved.
1891 is_vision = is_video | is_image
1892 else:
1893 is_vision = torch.zeros_like(is_multimodal)
1894 mm_positions = torch.nonzero(is_multimodal, as_tuple=True)[0]
1895 mm_position_idx = 0
1896
1897 for index, embeddings in enumerate(multimodal_embeddings):
1898 num_tokens = embeddings.shape[0]
1899
1900 # Vision embeddings
1901 if embeddings.shape[-1] != self.config.text_config.hidden_size:
1902 visual_dim = embeddings.shape[-1] // (multiscale_len + 1)
1903 multi_dim = visual_dim * multiscale_len
1904 embeddings_main, embeddings_multiscale = torch.split(
1905 embeddings, [visual_dim, multi_dim], dim=-1
1906 )
1907 multimodal_embeddings[index] = embeddings_main
1908 multimodal_embeddings_multiscale.append(embeddings_multiscale)
1909 if not is_interleaved:
1910 current_positions = mm_positions[
1911 mm_position_idx : mm_position_idx + num_tokens
1912 ]
1913 is_vision[current_positions] = True
1914
1915 # Audio embeddings
1916 else:
1917 if not is_interleaved:
1918 current_positions = mm_positions[
1919 mm_position_idx : mm_position_idx + num_tokens
1920 ]
1921 is_vision[current_positions] = False
1922
1923 if not is_interleaved:
1924 mm_position_idx += num_tokens
1925
1926 deepstack_input_embeds = inputs_embeds.new_zeros(
1927 inputs_embeds.size(0), multiscale_len * inputs_embeds.size(1)
1928 )
1929 deepstack_input_embeds = _merge_multimodal_embeddings(
1930 inputs_embeds=deepstack_input_embeds,
1931 multimodal_embeddings=multimodal_embeddings_multiscale,
1932 is_multimodal=is_vision,
1933 )
1934 deepstack_input_embeds = (
1935 deepstack_input_embeds.view(
1936 inputs_embeds.shape[0], multiscale_len, visual_dim
1937 )
1938 .permute(1, 0, 2)
1939 .contiguous()
1940 )
1941 self._set_deepstack_input_embeds(deepstack_input_embeds)
1942
1943 if is_interleaved:
1944 return merge_interleaved_embeddings(
1945 inputs_embeds,
1946 multimodal_embeddings,
1947 is_video,
1948 is_audio,
1949 is_multimodal,
1950 )
1951
1952 # Default: standard merge (no interleaving), same as parent class.
1953 # multimodal_embeddings may have been updated above (deepstack
1954 # main-scale). Use super() to stay consistent with the parent
1955 # implementation and avoid issues seen in Qwen2.5-Omni (#34506).
1956 return super().embed_input_ids(
1957 input_ids,
1958 multimodal_embeddings=multimodal_embeddings,
1959 is_multimodal=is_multimodal,
1960 )
1961
1962 def forward(
1963 self,
1964 input_ids: torch.Tensor | None,
1965 positions: torch.Tensor,
1966 intermediate_tensors: IntermediateTensors | None = None,
1967 inputs_embeds: torch.Tensor | None = None,
1968 **kwargs: object,
1969 ) -> torch.Tensor | IntermediateTensors:
1970 if intermediate_tensors is not None:
1971 inputs_embeds = None
1972
1973 if inputs_embeds is not None and get_pp_group().is_first_rank:
1974 deepstack_input_embeds = self._get_deepstack_input_embeds(
1975 inputs_embeds.size(0)
1976 )
1977 else:
1978 deepstack_input_embeds = None
1979
1980 hidden_states = self.language_model.model(
1981 input_ids,
1982 positions,
1983 intermediate_tensors,
1984 inputs_embeds=inputs_embeds,
1985 # args for deepstack
1986 deepstack_input_embeds=deepstack_input_embeds,
1987 )
1988
1989 if inputs_embeds is not None and get_pp_group().is_first_rank:
1990 self._clear_deepstack_input_embeds(inputs_embeds.size(0))
1991
1992 return hidden_states
1993
1994 def compute_logits(
1995 self,
1996 hidden_states: torch.Tensor,
1997 ) -> torch.Tensor | None:
1998 return self.language_model.compute_logits(hidden_states)
1999
2000 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
2001 loader = AutoWeightsLoader(self)
2002 loaded_weights = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
2003
2004 return loaded_weights
2005
2006 def _compute_audio_token_count(self, audio_feature_length: int) -> int:
2007 """Compute audio tokens from feature length using Qwen3-Omni formula."""
2008 return _get_feat_extract_output_lengths(
2009 torch.tensor([audio_feature_length])
2010 ).item()
2011
2012 def _get_audio_for_video_mapping(
2013 self, mm_features: list[MultiModalFeatureSpec]
2014 ) -> tuple[dict[int, int], set[int]]:
2015 """Map video offset -> paired audio_feature_length for use_audio_in_video.
2016
2017 When use_audio_in_video=True, audio is interleaved within video.
2018 The pairing is based on feature order in mm_features.
2019
2020 Returns:
2021 Tuple of (video_offset -> audio_feature_length mapping,
2022 set of paired audio offsets to skip)
2023
2024 """
2025 videos_with_audio = [
2026 f
2027 for f in mm_features
2028 if f.modality == "video"
2029 and f.data is not None
2030 and f.data.get("use_audio_in_video")
2031 and _get_feature_tensor(f, "use_audio_in_video").item()
2032 ]
2033 audios = [f for f in mm_features if f.modality == "audio"]
2034
2035 mapping: dict[int, int] = {}
2036 paired_audio_offsets: set[int] = set()
2037 for i, video_f in enumerate(videos_with_audio):
2038 if i < len(audios):
2039 audio_len = int(
2040 _get_feature_tensor(audios[i], "audio_feature_lengths").item()
2041 )
2042 mapping[video_f.mm_position.offset] = audio_len
2043 paired_audio_offsets.add(audios[i].mm_position.offset)
2044 return mapping, paired_audio_offsets
2045
2046 def iter_mm_features(
2047 self, mm_features: list[MultiModalFeatureSpec]
2048 ) -> Iterator[tuple[int, str, dict[str, Any]]]:
2049 """Iterate over multimodal features sorted by position offset.
2050
2051 Yields: (offset, modality, feature_data) where feature_data contains:
2052 - image: {"grid_t", "grid_h", "grid_w", "t_factor"}
2053 - video: {"grid_t", "grid_h", "grid_w", "t_factor",
2054 "use_audio_in_video", "audio_feature_length"}
2055 - audio: {"audio_feature_length"}
2056 """
2057 config = self.config
2058 spatial_merge_size = config.vision_config.spatial_merge_size
2059 position_id_per_seconds = config.position_id_per_seconds
2060
2061 sorted_features = sorted(mm_features, key=lambda f: f.mm_position.offset)
2062 audio_for_video, paired_audio_offsets = self._get_audio_for_video_mapping(
2063 sorted_features
2064 )
2065
2066 for mm_feature in sorted_features:
2067 offset = mm_feature.mm_position.offset
2068 modality = mm_feature.modality
2069 assert mm_feature.data is not None
2070
2071 if modality == "image":
2072 t, h, w = _get_feature_tensor(mm_feature, "image_grid_thw").tolist()
2073 yield (
2074 offset,
2075 "image",
2076 {
2077 "grid_t": t,
2078 "grid_h": h // spatial_merge_size,
2079 "grid_w": w // spatial_merge_size,
2080 "t_factor": position_id_per_seconds,
2081 },
2082 )
2083 elif modality == "video":
2084 t, h, w = _get_feature_tensor(mm_feature, "video_grid_thw").tolist()
2085 second_per_grid_ts = 2.0
2086 if mm_feature.data.get("second_per_grid_ts"):
2087 second_per_grid_ts = _get_feature_tensor(
2088 mm_feature, "second_per_grid_ts"
2089 ).item()
2090 use_audio_in_video = bool(
2091 mm_feature.data.get("use_audio_in_video")
2092 and _get_feature_tensor(mm_feature, "use_audio_in_video").item()
2093 )
2094
2095 yield (
2096 offset,
2097 "video",
2098 {
2099 "grid_t": t,
2100 "grid_h": h // spatial_merge_size,
2101 "grid_w": w // spatial_merge_size,
2102 "t_factor": second_per_grid_ts * position_id_per_seconds,
2103 "use_audio_in_video": use_audio_in_video,
2104 "audio_feature_length": audio_for_video.get(offset),
2105 },
2106 )
2107 elif modality == "audio":
2108 if offset not in paired_audio_offsets:
2109 audio_len = _get_feature_tensor(
2110 mm_feature, "audio_feature_lengths"
2111 ).item()
2112 yield offset, "audio", {"audio_feature_length": audio_len}
2113
2114 def _compute_interleaved_positions(
2115 self, start_idx: int, data: dict[str, Any]
2116 ) -> tuple[np.ndarray, int]:
2117 """Compute positions for interleaved video+audio using Qwen3 token-by-token
2118 interleaving logic.
2119
2120 Returns: (position_ids [3, N], total_token_count)
2121 """
2122 grid_t = data["grid_t"]
2123 grid_h = data["grid_h"]
2124 grid_w = data["grid_w"]
2125 t_factor = data["t_factor"]
2126 audio_feature_length = data["audio_feature_length"]
2127
2128 audio_len = self._compute_audio_token_count(audio_feature_length)
2129
2130 h_index = np.tile(
2131 np.arange(grid_h).reshape(1, -1, 1), (grid_t, 1, grid_w)
2132 ).flatten()
2133 w_index = np.tile(
2134 np.arange(grid_w).reshape(1, 1, -1), (grid_t, grid_h, 1)
2135 ).flatten()
2136 t_index_raw = np.arange(grid_t)
2137 t_index_scaled = (t_index_raw * t_factor).astype(np.int64)
2138 t_index = np.repeat(t_index_scaled, grid_h * grid_w)
2139
2140 video_pos = np.stack([t_index, h_index, w_index]) + start_idx
2141 audio_pos = np.broadcast_to(np.arange(audio_len), (3, audio_len)) + start_idx
2142
2143 video_t_values = video_pos[0]
2144 audio_t_values = audio_pos[0]
2145
2146 pos_ids_list: list[np.ndarray] = []
2147 video_idx, audio_idx = 0, 0
2148 num_video = grid_t * grid_h * grid_w
2149
2150 while video_idx < num_video and audio_idx < audio_len:
2151 if video_t_values[video_idx] <= audio_t_values[audio_idx]:
2152 pos_ids_list.append(video_pos[:, video_idx : video_idx + 1])
2153 video_idx += 1
2154 else:
2155 pos_ids_list.append(audio_pos[:, audio_idx : audio_idx + 1])
2156 audio_idx += 1
2157
2158 if video_idx < num_video:
2159 pos_ids_list.append(video_pos[:, video_idx:])
2160 if audio_idx < audio_len:
2161 pos_ids_list.append(audio_pos[:, audio_idx:])
2162
2163 total_tokens = num_video + audio_len
2164 return np.concatenate(pos_ids_list, axis=1), total_tokens
2165
2166 @classmethod
2167 def get_speech_to_text_config(
2168 cls, model_config: ModelConfig, task_type: str
2169 ) -> SpeechToTextConfig:
2170 processor = cached_processor_from_config(
2171 model_config, processor_cls=Qwen3OmniMoeProcessor
2172 )
2173 return SpeechToTextConfig(
2174 max_audio_clip_s=processor.feature_extractor.chunk_length,
2175 sample_rate=processor.feature_extractor.sampling_rate,
2176 min_energy_split_window_size=None,
2177 )
2178
2179 @classmethod
2180 def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
2181 """Construct a transcription/translation prompt for Qwen3-Omni."""
2182 audio = stt_params.audio
2183 stt_config = stt_params.stt_config
2184 model_config = stt_params.model_config
2185 language = stt_params.language
2186 task_type = stt_params.task_type
2187 to_language = stt_params.to_language
2188 request_prompt = stt_params.request_prompt
2189 # Transcribe this audio [into <language>] | for transcription
2190 # Translate this audio [from <language> into <to_language>] | for translation
2191 instruction = "Transcribe" if task_type == "transcribe" else "Translate"
2192 instruction += " this audio"
2193
2194 # Default to_language to English for translation
2195 if task_type == "translate" and to_language is None:
2196 to_language = "en"
2197
2198 # Get full language names from supported_languages mapping
2199 full_lang_name = (
2200 cls.supported_languages.get(language, "") if language is not None else ""
2201 )
2202 full_lang_name_to = (
2203 cls.supported_languages.get(to_language, "")
2204 if to_language is not None
2205 else ""
2206 )
2207
2208 if task_type == "transcribe" and full_lang_name:
2209 instruction += f" into {full_lang_name}"
2210 elif task_type == "translate":
2211 if full_lang_name:
2212 instruction += f" from {full_lang_name}"
2213 if full_lang_name_to:
2214 instruction += f" into {full_lang_name_to}"
2215
2216 instruction += "."
2217
2218 if request_prompt:
2219 instruction += f" {request_prompt}"
2220
2221 processor = cached_processor_from_config(
2222 model_config, processor_cls=Qwen3OmniMoeProcessor
2223 )
2224 # Audio placeholder format: <|audio_start|><|audio_pad|><|audio_end|>
2225 audio_placeholder = "<|audio_start|><|audio_pad|><|audio_end|>"
2226 user_content = f"{audio_placeholder}{instruction}"
2227
2228 messages = [{"role": "user", "content": user_content}]
2229 prompt = processor.apply_chat_template(
2230 messages,
2231 tokenize=False,
2232 add_generation_prompt=True,
2233 )
2234
2235 audio_data = (audio, stt_config.sample_rate)
2236 prompts_dict: TextPrompt = {
2237 "multi_modal_data": {"audio": audio_data},
2238 "prompt": prompt,
2239 }
2240 return prompts_dict
2241
2242 def get_mrope_input_positions(
2243 self,
2244 input_tokens: list[int],
2245 mm_features: list[MultiModalFeatureSpec],
2246 ) -> tuple[torch.Tensor, int]:
2247 """Compute M-RoPE input positions using mm_features directly."""
2248 seq_len = len(input_tokens)
2249
2250 llm_pos_ids_list: list[np.ndarray] = []
2251 st = 0
2252
2253 for offset, modality, data in self.iter_mm_features(mm_features):
2254 text_len = offset - st
2255 st_idx = int(llm_pos_ids_list[-1].max()) + 1 if llm_pos_ids_list else 0
2256
2257 if text_len > 0:
2258 llm_pos_ids_list.append(
2259 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
2260 )
2261 st_idx += text_len
2262
2263 bos_pos = np.broadcast_to(np.array([st_idx]), (3, 1))
2264 llm_pos_ids_list.append(bos_pos)
2265 st_idx += 1
2266
2267 if modality == "audio":
2268 audio_tokens = self._compute_audio_token_count(
2269 data["audio_feature_length"]
2270 )
2271 audio_pos = (
2272 np.broadcast_to(np.arange(audio_tokens), (3, audio_tokens)) + st_idx
2273 )
2274 llm_pos_ids_list.append(audio_pos)
2275 st_idx = int(audio_pos.max()) + 1
2276
2277 eos_pos = np.broadcast_to(np.array([st_idx]), (3, 1))
2278 llm_pos_ids_list.append(eos_pos)
2279 st = offset + 1 + audio_tokens + 1
2280
2281 elif modality == "image":
2282 grid_t = data["grid_t"]
2283 grid_h = data["grid_h"]
2284 grid_w = data["grid_w"]
2285 t_factor = data["t_factor"]
2286
2287 grid_indices = np.indices((grid_t, grid_h, grid_w))
2288 if t_factor != 1.0:
2289 grid_indices[0] = (grid_indices[0] * t_factor).astype(np.int64)
2290 llm_pos_ids_list.append(grid_indices.reshape(3, -1) + st_idx)
2291
2292 image_len = grid_t * grid_h * grid_w
2293 st_idx = int(llm_pos_ids_list[-1].max()) + 1
2294
2295 eos_pos = np.broadcast_to(np.array([st_idx]), (3, 1))
2296 llm_pos_ids_list.append(eos_pos)
2297 st = offset + 1 + image_len + 1
2298
2299 elif modality == "video":
2300 grid_t = data["grid_t"]
2301 grid_h = data["grid_h"]
2302 grid_w = data["grid_w"]
2303 t_factor = data["t_factor"]
2304
2305 if not data["use_audio_in_video"]:
2306 grid_indices = np.indices((grid_t, grid_h, grid_w))
2307 if t_factor != 1.0:
2308 grid_indices[0] = (grid_indices[0] * t_factor).astype(np.int64)
2309 llm_pos_ids_list.append(grid_indices.reshape(3, -1) + st_idx)
2310
2311 video_len = grid_t * grid_h * grid_w
2312 st_idx = int(llm_pos_ids_list[-1].max()) + 1
2313
2314 eos_pos = np.broadcast_to(np.array([st_idx]), (3, 1))
2315 llm_pos_ids_list.append(eos_pos)
2316 st = offset + 1 + video_len + 1
2317 else:
2318 audio_bos_pos = np.broadcast_to(np.array([st_idx - 1]), (3, 1))
2319 llm_pos_ids_list.append(audio_bos_pos)
2320
2321 pos_ids, _ = self._compute_interleaved_positions(st_idx, data)
2322 llm_pos_ids_list.append(pos_ids)
2323 st_idx = int(pos_ids.max()) + 1
2324
2325 eos_pos = np.broadcast_to(np.array([st_idx]), (3, 1))
2326 llm_pos_ids_list.append(eos_pos)
2327 llm_pos_ids_list.append(eos_pos)
2328
2329 video_len = grid_t * grid_h * grid_w
2330 audio_len = self._compute_audio_token_count(
2331 data["audio_feature_length"]
2332 )
2333 st = offset + 2 + video_len + audio_len + 2
2334
2335 if st < seq_len:
2336 st_idx = int(llm_pos_ids_list[-1].max()) + 1 if llm_pos_ids_list else 0
2337 text_len = seq_len - st
2338 llm_pos_ids_list.append(
2339 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
2340 )
2341
2342 llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
2343 if llm_positions.shape[1] != seq_len:
2344 raise RuntimeError("Position ids length mismatch with input ids length")
2345
2346 mrope_position_delta = int(llm_positions.max()) + 1 - seq_len
2347 return torch.from_numpy(llm_positions), mrope_position_delta
2348
2349 def get_mm_mapping(self) -> MultiModelKeys:
2350 """Get the module prefix in multimodal models"""
2351 return MultiModelKeys.from_string_field(
2352 language_model="language_model",
2353 connector="visual.merger",
2354 tower_model=["visual.", "audio_tower."],
2355 )
2356
2357 def get_mm_lora_token_counts(
2358 self,
2359 *,
2360 modality: str,
2361 mm_kwargs: MultiModalKwargsItem | None,
2362 num_mm_embeds: int,
2363 ) -> tuple[int, int | None]:
2364 if modality in ("image", "video"):
2365 hf_config = self.config
2366 vision_config = hf_config.vision_config
2367 merge_size = vision_config.spatial_merge_size
2368 tower_tokens = num_mm_embeds * merge_size**2
2369 connector_tokens = num_mm_embeds
2370
2371 if modality == "audio":
2372 tower_tokens = num_mm_embeds
2373 connector_tokens = num_mm_embeds
2374
2375 return tower_tokens, connector_tokens