A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Model] Support Qwen3-VL Model Series (#24727)
Signed-off-by: Roger Wang <hey@rogerw.io> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Huang Jie <92386084+JJJYmmm@users.noreply.github.com> Co-authored-by: 松灵 <26085463+wulipc@users.noreply.github.com> Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Roger Wang committed
Sep 16, 2025 at 22:01 UTC
0f7acdd73ca6316c8ae0474c0a9c4fc264e87a7b
13 files changed
+2084
-17
docs/models/supported_models.md
+2
@@ -661,6 +661,8 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
661
| `Qwen2VLForConditionalGeneration` | QVQ, Qwen2-VL | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/QVQ-72B-Preview`, `Qwen/Qwen2-VL-7B-Instruct`, `Qwen/Qwen2-VL-72B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
662
| `Qwen2_5_VLForConditionalGeneration` | Qwen2.5-VL | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/Qwen2.5-VL-3B-Instruct`, `Qwen/Qwen2.5-VL-72B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
663
| `Qwen2_5OmniThinkerForConditionalGeneration` | Qwen2.5-Omni | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>+</sup> | `Qwen/Qwen2.5-Omni-3B`, `Qwen/Qwen2.5-Omni-7B` | ✅︎ | ✅︎ | ✅︎ |
664
+| `Qwen3VLForConditionalGeneration` | Qwen3-VL | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/Qwen3-VL-4B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
665
+| `Qwen3VLMoeForConditionalGeneration` | Qwen3-VL-MOE | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/Qwen3-VL-30B-A3B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
666
| `RForConditionalGeneration` | R-VL-4B | T + I<sup>E+</sup> | `YannQi/R-4B` | | ✅︎ | ✅︎ |
667
| `SkyworkR1VChatModel` | Skywork-R1V-38B | T + I | `Skywork/Skywork-R1V-38B` | | ✅︎ | ✅︎ |
668
| `SmolVLMForConditionalGeneration` | SmolVLM2 | T + I | `SmolVLM2-2.2B-Instruct` | ✅︎ | | ✅︎ |
examples/offline_inference/vision_language.py
+78
@@ -1437,6 +1437,80 @@ def run_qwen2_5_omni(questions: list[str], modality: str):
1437
)
1438
1439
1440
+# Qwen3-VL-Dense
1441
+def run_qwen3_vl(questions: list[str], modality: str) -> ModelRequestData:
1442
+ model_name = "Qwen/Qwen3-VL-4B-Instruct"
1443
+
1444
+ engine_args = EngineArgs(
1445
+ model=model_name,
1446
+ max_model_len=4096,
1447
+ max_num_seqs=5,
1448
+ mm_processor_kwargs={
1449
+ "min_pixels": 28 * 28,
1450
+ "max_pixels": 1280 * 28 * 28,
1451
+ "fps": 1,
1452
+ },
1453
+ limit_mm_per_prompt={modality: 1},
1454
+ )
1455
+
1456
+ if modality == "image":
1457
+ placeholder = "<|image_pad|>"
1458
+ elif modality == "video":
1459
+ placeholder = "<|video_pad|>"
1460
+
1461
+ prompts = [
1462
+ (
1463
+ "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
1464
+ f"<|im_start|>user\n<|vision_start|>{placeholder}<|vision_end|>"
1465
+ f"{question}<|im_end|>\n"
1466
+ "<|im_start|>assistant\n"
1467
+ )
1468
+ for question in questions
1469
+ ]
1470
+
1471
+ return ModelRequestData(
1472
+ engine_args=engine_args,
1473
+ prompts=prompts,
1474
+ )
1475
+
1476
+
1477
+# Qwen3-VL-MOE
1478
+def run_qwen3_vl_moe(questions: list[str], modality: str) -> ModelRequestData:
1479
+ model_name = "Qwen/Qwen3-VL-30B-A3B-Instruct"
1480
+
1481
+ engine_args = EngineArgs(
1482
+ model=model_name,
1483
+ max_model_len=4096,
1484
+ max_num_seqs=5,
1485
+ mm_processor_kwargs={
1486
+ "min_pixels": 28 * 28,
1487
+ "max_pixels": 1280 * 28 * 28,
1488
+ "fps": 1,
1489
+ },
1490
+ limit_mm_per_prompt={modality: 1},
1491
+ )
1492
+
1493
+ if modality == "image":
1494
+ placeholder = "<|image_pad|>"
1495
+ elif modality == "video":
1496
+ placeholder = "<|video_pad|>"
1497
+
1498
+ prompts = [
1499
+ (
1500
+ "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
1501
+ f"<|im_start|>user\n<|vision_start|>{placeholder}<|vision_end|>"
1502
+ f"{question}<|im_end|>\n"
1503
+ "<|im_start|>assistant\n"
1504
+ )
1505
+ for question in questions
1506
+ ]
1507
+
1508
+ return ModelRequestData(
1509
+ engine_args=engine_args,
1510
+ prompts=prompts,
1511
+ )
1512
+
1513
+
1514
# R-4B
1515
def run_r_vl(questions: list[str], modality: str) -> ModelRequestData:
1516
assert modality == "image"
@@ -1645,6 +1719,8 @@ model_example_map = {
1719
"qwen2_vl": run_qwen2_vl,
1720
"qwen2_5_vl": run_qwen2_5_vl,
1721
"qwen2_5_omni": run_qwen2_5_omni,
1722
+ "qwen3_vl": run_qwen3_vl,
1723
+ "qwen3_vl_moe": run_qwen3_vl_moe,
1724
"rvl": run_r_vl,
1725
"skywork_chat": run_skyworkr1v,
1726
"smolvlm": run_smolvlm,
@@ -1658,6 +1734,8 @@ MODELS_NEED_VIDEO_METADATA = [
1734
"glm4_1v",
1735
"glm4_5v",
1736
"glm4_5v_fp8",
1737
+ "qwen3_vl",
1738
+ "qwen3_vl_moe",
1739
]
1740
1741
tests/models/multimodal/processing/test_common.py
+34
-1
@@ -31,6 +31,7 @@ def glm4_1v_patch_mm_data(mm_data: MultiModalDataDict) -> MultiModalDataDict:
31
"""
32
# Ensure video metadata is included
33
if "video" in mm_data:
34
+ # GLM4.1V doesn't support multiple videos
35
video = mm_data["video"]
36
num_frames = len(video)
37
mm_data["video"] = (video, {
@@ -44,6 +45,34 @@ def glm4_1v_patch_mm_data(mm_data: MultiModalDataDict) -> MultiModalDataDict:
45
return mm_data
46
47
48
+def qwen3_vl_patch_mm_data(mm_data: MultiModalDataDict) -> MultiModalDataDict:
49
+ """
50
+ Patch the multimodal data for Qwen3-VL model.
51
+ """
52
+
53
+ def create_metadata(frames: np.ndarray):
54
+ num_frames = len(frames)
55
+ return {
56
+ "total_num_frames": num_frames,
57
+ "fps": 2.0,
58
+ "duration": num_frames / 2.0,
59
+ "video_backend": "opencv",
60
+ "frames_indices": list(range(num_frames)),
61
+ "do_sample_frames": True,
62
+ }
63
+
64
+ # Ensure video metadata is included
65
+ if "video" in mm_data:
66
+ video = mm_data["video"]
67
+ if isinstance(video, list):
68
+ # multiple videos
69
+ mm_data["video"] = [(vid, create_metadata(vid)) for vid in video]
70
+ else:
71
+ # single video
72
+ mm_data["video"] = (video, create_metadata(video))
73
+ return mm_data
74
+
75
+
76
def _test_processing_correctness(
77
model_id_or_arch: str,
78
hit_rate: float,
@@ -182,8 +211,10 @@ _IGNORE_MM_KEYS = {
211
}
212
213
MM_DATA_PATCHES = {
185
- # GLM4.1V requires video metadata to be included in the input
214
+ # GLM4.1V and Qwen3-VL requires video metadata to be included in the input
215
"glm4v": glm4_1v_patch_mm_data,
216
+ "qwen3_vl": qwen3_vl_patch_mm_data,
217
+ "qwen3_vl_moe": qwen3_vl_patch_mm_data,
218
}
219
220
@@ -326,6 +357,8 @@ def _test_processing_correctness_one(
357
"Qwen/Qwen2.5-VL-3B-Instruct",
358
"Qwen/Qwen2-Audio-7B-Instruct",
359
"Qwen/Qwen2.5-Omni-3B",
360
+ "Qwen/Qwen3-VL-4B-Instruct",
361
+ "Qwen/Qwen3-VL-30B-A3B-Instruct",
362
"YannQi/R-4B",
363
"Skywork/Skywork-R1V-38B",
364
"HuggingFaceTB/SmolVLM2-2.2B-Instruct",
tests/models/registry.py
+6
@@ -557,6 +557,12 @@ _MULTIMODAL_EXAMPLE_MODELS = {
557
max_model_len=4096),
558
"Qwen2_5OmniModel": _HfExamplesInfo("Qwen/Qwen2.5-Omni-3B"),
559
"Qwen2_5OmniForConditionalGeneration": _HfExamplesInfo("Qwen/Qwen2.5-Omni-7B-AWQ"), # noqa: E501
560
+ "Qwen3VLForConditionalGeneration": _HfExamplesInfo("Qwen/Qwen3-VL-4B-Instruct", # noqa: E501
561
+ max_model_len=4096,
562
+ min_transformers_version="4.57"), # noqa: E501
563
+ "Qwen3VLMoeForConditionalGeneration": _HfExamplesInfo("Qwen/Qwen3-VL-30B-A3B-Instruct", # noqa: E501
564
+ max_model_len=4096,
565
+ min_transformers_version="4.57"),
566
"RForConditionalGeneration": _HfExamplesInfo("YannQi/R-4B",
567
trust_remote_code=True),
568
"SkyworkR1VChatModel": _HfExamplesInfo("Skywork/Skywork-R1V-38B",
vllm/model_executor/layers/rotary_embedding/__init__.py
+2
@@ -103,6 +103,8 @@ def get_rope(
103
is_neox_style,
104
dtype,
105
mrope_section=rope_scaling["mrope_section"],
106
+ mrope_interleaved=rope_scaling.get("mrope_interleaved",
107
+ False),
108
)
109
else:
110
rotary_emb = RotaryEmbedding(
vllm/model_executor/layers/rotary_embedding/mrope.py
+133
-11
@@ -177,6 +177,18 @@ def triton_mrope(
177
return q, k
178
179
180
+def apply_interleaved_rope(x: torch.Tensor,
181
+ mrope_section: list[int]) -> torch.Tensor:
182
+ """Apply interleaved MRoPE to 3D rotary embeddings.
183
+ Reorganizes frequency layout from chunked [TTT...HHH...WWW] to
184
+ interleaved [THTHWHTHW...TT], preserving frequency continuity.
185
+ """
186
+ x_t = x[0].clone()
187
+ x_t[..., 1:mrope_section[1] * 3:3] = x[1, ..., 1:mrope_section[1] * 3:3]
188
+ x_t[..., 2:mrope_section[2] * 3:3] = x[2, ..., 2:mrope_section[2] * 3:3]
189
+ return x_t
190
+
191
+
192
class MRotaryEmbedding(RotaryEmbedding):
193
"""Rotary Embedding with Multimodal Sections."""
194
@@ -189,6 +201,7 @@ class MRotaryEmbedding(RotaryEmbedding):
201
is_neox_style: bool,
202
dtype: torch.dtype,
203
mrope_section: Optional[list[int]] = None,
204
+ mrope_interleaved: Optional[bool] = False,
205
) -> None:
206
# In Qwen2.5-VL, the maximum index value is related to the duration of
207
# the input video. We enlarge max_position_embeddings to 4 times to get
@@ -198,6 +211,7 @@ class MRotaryEmbedding(RotaryEmbedding):
211
base, is_neox_style, dtype)
212
213
self.mrope_section = mrope_section
214
+ self.mrope_interleaved = mrope_interleaved
215
if self.mrope_section:
216
assert sum(self.mrope_section) == rotary_dim // 2
217
@@ -225,17 +239,20 @@ class MRotaryEmbedding(RotaryEmbedding):
239
cos, sin = cos_sin.chunk(2, dim=-1)
240
if positions.ndim == 2:
241
assert self.mrope_section
228
-
229
- cos = torch.cat([
230
- m[i]
231
- for i, m in enumerate(cos.split(self.mrope_section, dim=-1))
232
- ],
233
- dim=-1)
234
- sin = torch.cat([
235
- m[i]
236
- for i, m in enumerate(sin.split(self.mrope_section, dim=-1))
237
- ],
238
- dim=-1)
242
+ if self.mrope_interleaved:
243
+ cos = apply_interleaved_rope(cos, self.mrope_section)
244
+ sin = apply_interleaved_rope(sin, self.mrope_section)
245
+ else:
246
+ cos = torch.cat([
247
+ m[i] for i, m in enumerate(
248
+ cos.split(self.mrope_section, dim=-1))
249
+ ],
250
+ dim=-1)
251
+ sin = torch.cat([
252
+ m[i] for i, m in enumerate(
253
+ sin.split(self.mrope_section, dim=-1))
254
+ ],
255
+ dim=-1)
256
257
query_shape = query.shape
258
query = query.view(num_tokens, -1, self.head_size)
@@ -265,6 +282,10 @@ class MRotaryEmbedding(RotaryEmbedding):
282
assert positions.ndim == 1 or positions.ndim == 2
283
assert key is not None
284
285
+ if self.mrope_interleaved:
286
+ # TODO: add triton implementation to support mrope-interleaved
287
+ return self.forward_native(positions, query, key)
288
+
289
num_tokens = positions.shape[-1]
290
cos_sin = self.cos_sin_cache[positions]
291
cos, sin = cos_sin.chunk(2, dim=-1)
@@ -388,6 +409,15 @@ class MRotaryEmbedding(RotaryEmbedding):
409
context_len=context_len,
410
seq_len=seq_len,
411
)
412
+ elif hf_config.model_type in ["qwen3_vl", "qwen3_vl_moe"]:
413
+ return cls._qwen3vl_get_input_positions_tensor(
414
+ input_tokens=input_tokens,
415
+ hf_config=hf_config,
416
+ image_grid_thw=image_grid_thw,
417
+ video_grid_thw=video_grid_thw,
418
+ context_len=context_len,
419
+ seq_len=seq_len,
420
+ )
421
elif hf_config.model_type in ["ernie4_5_moe_vl", "ernie4_5_vl"]:
422
return cls._ernie_get_input_positions_tensor(
423
input_tokens=input_tokens,
@@ -526,6 +556,98 @@ class MRotaryEmbedding(RotaryEmbedding):
556
len(input_tokens)).item()
557
return llm_positions, mrope_position_delta
558
559
+ @classmethod
560
+ def _qwen3vl_get_input_positions_tensor(
561
+ cls,
562
+ input_tokens: list[int],
563
+ hf_config: PretrainedConfig,
564
+ image_grid_thw: Union[list[list[int]], torch.Tensor],
565
+ video_grid_thw: Union[list[list[int]], torch.Tensor],
566
+ context_len: int = 0,
567
+ seq_len: Optional[int] = None,
568
+ ) -> tuple[torch.Tensor, int]:
569
+ """Get mrope input positions and delta value."""
570
+
571
+ video_grid_thw = [[1, h, w] for t, h, w in video_grid_thw
572
+ for _ in range(t)]
573
+
574
+ image_token_id = hf_config.image_token_id
575
+ video_token_id = hf_config.video_token_id
576
+ vision_start_token_id = hf_config.vision_start_token_id
577
+ spatial_merge_size = hf_config.vision_config.spatial_merge_size
578
+
579
+ input_tokens_tensor = torch.tensor(input_tokens)
580
+ vision_start_indices = torch.argwhere(
581
+ input_tokens_tensor == vision_start_token_id).squeeze(1)
582
+ vision_tokens = input_tokens_tensor[vision_start_indices + 1]
583
+ image_nums = (vision_tokens == image_token_id).sum()
584
+ video_nums = (vision_tokens == video_token_id).sum()
585
+ llm_pos_ids_list: list = []
586
+
587
+ st = 0
588
+ remain_images, remain_videos = image_nums, video_nums
589
+
590
+ image_index, video_index = 0, 0
591
+ for _ in range(image_nums + video_nums):
592
+ if image_token_id in input_tokens and remain_images > 0:
593
+ ed_image = input_tokens.index(image_token_id, st)
594
+ else:
595
+ ed_image = len(input_tokens) + 1
596
+ if video_token_id in input_tokens and remain_videos > 0:
597
+ ed_video = input_tokens.index(video_token_id, st)
598
+ else:
599
+ ed_video = len(input_tokens) + 1
600
+ if ed_image < ed_video:
601
+ t, h, w = (
602
+ image_grid_thw[image_index][0],
603
+ image_grid_thw[image_index][1],
604
+ image_grid_thw[image_index][2],
605
+ )
606
+ image_index += 1
607
+ remain_images -= 1
608
+ ed = ed_image
609
+ else:
610
+ t, h, w = (
611
+ video_grid_thw[video_index][0],
612
+ video_grid_thw[video_index][1],
613
+ video_grid_thw[video_index][2],
614
+ )
615
+ video_index += 1
616
+ remain_videos -= 1
617
+ ed = ed_video
618
+
619
+ llm_grid_t, llm_grid_h, llm_grid_w = \
620
+ t, h // spatial_merge_size, w // spatial_merge_size
621
+ text_len = ed - st
622
+
623
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(
624
+ llm_pos_ids_list) > 0 else 0
625
+ llm_pos_ids_list.append(
626
+ torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
627
+
628
+ t_index = torch.arange(llm_grid_t).view(-1, 1).expand(
629
+ -1, llm_grid_h * llm_grid_w).flatten()
630
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(
631
+ llm_grid_t, -1, llm_grid_w).flatten()
632
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(
633
+ llm_grid_t, llm_grid_h, -1).flatten()
634
+ llm_pos_ids_list.append(
635
+ torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
636
+ st = ed + llm_grid_t * llm_grid_h * llm_grid_w
637
+
638
+ if st < len(input_tokens):
639
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(
640
+ llm_pos_ids_list) > 0 else 0
641
+ text_len = len(input_tokens) - st
642
+ llm_pos_ids_list.append(
643
+ torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
644
+
645
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
646
+ mrope_position_delta = (llm_positions.max() + 1 -
647
+ len(input_tokens)).item()
648
+ llm_positions = llm_positions[:, context_len:seq_len]
649
+ return llm_positions, mrope_position_delta
650
+
651
@classmethod
652
def _ernie_get_input_positions_tensor(
653
cls,
vllm/model_executor/models/qwen2.py
+1
-1
@@ -285,7 +285,7 @@ class Qwen2Model(nn.Module):
285
decoder_layer_type: type[nn.Module] = Qwen2DecoderLayer):
286
super().__init__()
287
288
- config = vllm_config.model_config.hf_config
288
+ config = vllm_config.model_config.hf_config.get_text_config()
289
cache_config = vllm_config.cache_config
290
quant_config = vllm_config.quant_config
291
vllm/model_executor/models/qwen2_vl.py
+1
-1
@@ -83,7 +83,7 @@ from .vision import get_vit_attn_backend
83
logger = init_logger(__name__)
84
85
# For profile run
86
-_MAX_FRAMES_PER_VIDEO = 16
86
+_MAX_FRAMES_PER_VIDEO = 600
87
88
# === Vision Inputs === #
89
vllm/model_executor/models/qwen3_moe.py
+1
-1
@@ -378,7 +378,7 @@ class Qwen3MoeModel(nn.Module):
378
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
379
super().__init__()
380
381
- config = vllm_config.model_config.hf_config
381
+ config = vllm_config.model_config.hf_config.get_text_config()
382
cache_config = vllm_config.cache_config
383
quant_config = vllm_config.quant_config
384
parallel_config = vllm_config.parallel_config
vllm/model_executor/models/qwen3_vl.py
new
+1478
@@ -0,0 +1,1478 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+# Copyright 2025 The vLLM team.
5
+# Copyright 2025 The Qwen Team.
6
+# Copyright 2025 The HuggingFace Inc. team.
7
+# All rights reserved.
8
+#
9
+# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
10
+# and OPT implementations in this library. It has been modified from its
11
+# original forms to accommodate minor architectural differences compared
12
+# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
13
+#
14
+# Licensed under the Apache License, Version 2.0 (the "License");
15
+# you may not use this file except in compliance with the License.
16
+# You may obtain a copy of the License at
17
+#
18
+# http://www.apache.org/licenses/LICENSE-2.0
19
+#
20
+# Unless required by applicable law or agreed to in writing, software
21
+# distributed under the License is distributed on an "AS IS" BASIS,
22
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
+# See the License for the specific language governing permissions and
24
+# limitations under the License.
25
+"""Inference-only Qwen3VL model compatible with HuggingFace weights."""
26
+from collections.abc import Iterable, Mapping, Sequence
27
+from functools import partial
28
+from typing import Any, Callable, Optional, Union
29
+
30
+import numpy as np
31
+import torch
32
+import torch.nn as nn
33
+import torch.nn.functional as F
34
+from transformers import BatchFeature
35
+from transformers.models.qwen2_vl import Qwen2VLImageProcessorFast
36
+from transformers.models.qwen2_vl.image_processing_qwen2_vl import smart_resize
37
+from transformers.models.qwen3_vl import (Qwen3VLProcessor,
38
+ Qwen3VLVideoProcessor)
39
+from transformers.models.qwen3_vl.configuration_qwen3_vl import (
40
+ Qwen3VLConfig, Qwen3VLVisionConfig)
41
+from transformers.video_utils import VideoMetadata
42
+
43
+from vllm.attention.layer import check_upstream_fa_availability
44
+from vllm.compilation.decorators import support_torch_compile
45
+from vllm.config import VllmConfig
46
+from vllm.distributed import get_pp_group
47
+from vllm.logger import init_logger
48
+from vllm.model_executor import SamplingMetadata
49
+from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
50
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
51
+ RowParallelLinear)
52
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
53
+from vllm.model_executor.layers.quantization import QuantizationConfig
54
+from vllm.model_executor.layers.quantization.gptq import GPTQConfig
55
+from vllm.model_executor.layers.quantization.gptq_marlin import (
56
+ GPTQMarlinConfig)
57
+from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
58
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
59
+from vllm.model_executor.models.module_mapping import MultiModelKeys
60
+from vllm.multimodal import MULTIMODAL_REGISTRY
61
+from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
62
+ MultiModalKwargsItem,
63
+ MultiModalKwargsItems, VideoItem)
64
+from vllm.multimodal.parse import (ImageSize, MultiModalDataItems,
65
+ MultiModalDataParser)
66
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
67
+ PromptReplacement, PromptUpdate,
68
+ PromptUpdateDetails)
69
+from vllm.multimodal.profiling import BaseDummyInputsBuilder
70
+from vllm.platforms import _Backend
71
+from vllm.sequence import IntermediateTensors
72
+from vllm.transformers_utils.config import uses_mrope
73
+from vllm.utils import is_list_of
74
+
75
+from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
76
+ SupportsMultiModal, SupportsPP)
77
+from .qwen2_5_vl import (Qwen2_5_VisionAttention,
78
+ Qwen2_5_VisionRotaryEmbedding,
79
+ Qwen2_5_VLImageEmbeddingInputs, Qwen2_5_VLImageInputs,
80
+ Qwen2_5_VLImagePixelInputs,
81
+ Qwen2_5_VLVideoEmbeddingInputs, Qwen2_5_VLVideoInputs,
82
+ Qwen2_5_VLVideoPixelInputs)
83
+from .qwen2_vl import Qwen2VLProcessingInfo
84
+from .qwen3 import Qwen3ForCausalLM, Qwen3Model
85
+from .utils import (AutoWeightsLoader, PPMissingLayer, WeightsMapper,
86
+ maybe_prefix, merge_multimodal_embeddings)
87
+from .vision import get_vit_attn_backend
88
+
89
+logger = init_logger(__name__)
90
+
91
+
92
+class Qwen3_VisionPatchEmbed(nn.Module):
93
+
94
+ def __init__(
95
+ self,
96
+ patch_size: int = 14,
97
+ temporal_patch_size: int = 2,
98
+ in_channels: int = 3,
99
+ hidden_size: int = 1152,
100
+ ) -> None:
101
+ super().__init__()
102
+ self.patch_size = patch_size
103
+ self.temporal_patch_size = temporal_patch_size
104
+ self.hidden_size = hidden_size
105
+
106
+ kernel_size = (temporal_patch_size, patch_size, patch_size)
107
+ self.proj = nn.Conv3d(in_channels,
108
+ hidden_size,
109
+ kernel_size=kernel_size,
110
+ stride=kernel_size,
111
+ bias=True)
112
+
113
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
114
+ L, C = x.shape
115
+ x = x.view(L, -1, self.temporal_patch_size, self.patch_size,
116
+ self.patch_size)
117
+ x = self.proj(x).view(L, self.hidden_size)
118
+ return x
119
+
120
+
121
+class Qwen3_VisionMLP(nn.Module):
122
+
123
+ def __init__(self,
124
+ in_features: int,
125
+ hidden_features: int,
126
+ bias: bool = False,
127
+ act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
128
+ quant_config: Optional[QuantizationConfig] = None,
129
+ prefix: str = ""):
130
+ super().__init__()
131
+ self.linear_fc1 = ColumnParallelLinear(in_features,
132
+ hidden_features,
133
+ bias=bias,
134
+ quant_config=quant_config,
135
+ return_bias=False,
136
+ prefix=f"{prefix}.linear_fc1")
137
+ self.linear_fc2 = RowParallelLinear(hidden_features,
138
+ in_features,
139
+ bias=bias,
140
+ quant_config=quant_config,
141
+ return_bias=False,
142
+ prefix=f"{prefix}.linear_fc2")
143
+ self.act_fn = act_fn
144
+
145
+ def forward(self, x: torch.Tensor):
146
+ mlp_output = self.linear_fc2(self.act_fn(self.linear_fc1(x)))
147
+ return mlp_output
148
+
149
+
150
+class Qwen3_VisionBlock(nn.Module):
151
+
152
+ def __init__(
153
+ self,
154
+ dim: int,
155
+ num_heads: int,
156
+ mlp_hidden_dim: int,
157
+ act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
158
+ norm_layer: Optional[Callable[[int], nn.Module]] = None,
159
+ quant_config: Optional[QuantizationConfig] = None,
160
+ prefix: str = "",
161
+ ) -> None:
162
+ super().__init__()
163
+ if norm_layer is None:
164
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
165
+ self.norm1 = norm_layer(dim)
166
+ self.norm2 = norm_layer(dim)
167
+ self.attn = Qwen2_5_VisionAttention(embed_dim=dim,
168
+ num_heads=num_heads,
169
+ projection_size=dim,
170
+ quant_config=quant_config,
171
+ prefix=f"{prefix}.attn")
172
+ self.mlp = Qwen3_VisionMLP(dim,
173
+ mlp_hidden_dim,
174
+ act_fn=act_fn,
175
+ bias=True,
176
+ quant_config=quant_config,
177
+ prefix=f"{prefix}.mlp")
178
+
179
+ def forward(
180
+ self,
181
+ x: torch.Tensor,
182
+ cu_seqlens: torch.Tensor,
183
+ rotary_pos_emb: torch.Tensor,
184
+ max_seqlen: Optional[int] = None, # Only used for Flash Attention
185
+ seqlens: Optional[list[int]] = None, # Only used for xFormers
186
+ ) -> torch.Tensor:
187
+ x = x + self.attn(self.norm1(x),
188
+ cu_seqlens=cu_seqlens,
189
+ rotary_pos_emb=rotary_pos_emb,
190
+ max_seqlen=max_seqlen,
191
+ seqlens=seqlens)
192
+
193
+ x = x + self.mlp(self.norm2(x))
194
+ return x
195
+
196
+
197
+class Qwen3_VisionPatchMerger(nn.Module):
198
+
199
+ def __init__(
200
+ self,
201
+ d_model: int,
202
+ context_dim: int,
203
+ norm_layer: Optional[Callable[[int], nn.Module]] = None,
204
+ spatial_merge_size: int = 2,
205
+ use_postshuffle_norm: bool = False,
206
+ quant_config: Optional[QuantizationConfig] = None,
207
+ prefix: str = "",
208
+ ) -> None:
209
+ super().__init__()
210
+ self.hidden_size = context_dim * (spatial_merge_size**2)
211
+
212
+ self.use_postshuffle_norm = use_postshuffle_norm
213
+ if self.use_postshuffle_norm:
214
+ context_dim = self.hidden_size
215
+
216
+ if norm_layer is None:
217
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
218
+ self.use_postshuffle_norm = use_postshuffle_norm
219
+ self.norm = norm_layer(
220
+ self.hidden_size if use_postshuffle_norm else context_dim)
221
+ self.linear_fc1 = ColumnParallelLinear(self.hidden_size,
222
+ self.hidden_size,
223
+ bias=True,
224
+ quant_config=quant_config,
225
+ prefix=f"{prefix}.linear_fc1")
226
+ self.act_fn = nn.GELU()
227
+ self.linear_fc2 = RowParallelLinear(self.hidden_size,
228
+ d_model,
229
+ bias=True,
230
+ quant_config=quant_config,
231
+ prefix=f"{prefix}.linear_fc2")
232
+
233
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
234
+ if self.use_postshuffle_norm:
235
+ x = self.norm(x.view(-1, self.hidden_size))
236
+ else:
237
+ x = self.norm(x).view(-1, self.hidden_size)
238
+
239
+ x_parallel, _ = self.linear_fc1(x)
240
+ x_parallel = self.act_fn(x_parallel)
241
+ out, _ = self.linear_fc2(x_parallel)
242
+ return out
243
+
244
+
245
+class Qwen3_VisionTransformer(nn.Module):
246
+
247
+ def __init__(
248
+ self,
249
+ vision_config: Qwen3VLVisionConfig,
250
+ norm_eps: float = 1e-6,
251
+ quant_config: Optional[QuantizationConfig] = None,
252
+ prefix: str = "",
253
+ ) -> None:
254
+ super().__init__()
255
+ self.hidden_size = vision_config.hidden_size
256
+ self.num_heads = vision_config.num_heads
257
+ self.num_position_embeddings = vision_config.num_position_embeddings
258
+ self.patch_size = vision_config.patch_size
259
+ self.spatial_merge_size = vision_config.spatial_merge_size
260
+ self.spatial_merge_unit = self.spatial_merge_size**2
261
+ self.temporal_patch_size = vision_config.temporal_patch_size
262
+ self.deepstack_visual_indexes = vision_config.deepstack_visual_indexes
263
+
264
+ self.patch_embed = Qwen3_VisionPatchEmbed(
265
+ patch_size=self.patch_size,
266
+ temporal_patch_size=self.temporal_patch_size,
267
+ in_channels=vision_config.in_channels,
268
+ hidden_size=self.hidden_size,
269
+ )
270
+
271
+ self.pos_embed = nn.Embedding(self.num_position_embeddings,
272
+ self.hidden_size)
273
+
274
+ norm_layer = partial(nn.LayerNorm, eps=norm_eps)
275
+ head_dim = self.hidden_size // self.num_heads
276
+ self.rotary_pos_emb = Qwen2_5_VisionRotaryEmbedding(head_dim // 2)
277
+
278
+ self.blocks = nn.ModuleList([
279
+ Qwen3_VisionBlock(
280
+ dim=self.hidden_size,
281
+ num_heads=self.num_heads,
282
+ mlp_hidden_dim=vision_config.intermediate_size,
283
+ act_fn=_ACTIVATION_REGISTRY[vision_config.hidden_act],
284
+ norm_layer=norm_layer,
285
+ quant_config=quant_config,
286
+ prefix=f"{prefix}.blocks.{layer_idx}")
287
+ for layer_idx in range(vision_config.depth)
288
+ ])
289
+
290
+ self.merger = Qwen3_VisionPatchMerger(
291
+ d_model=vision_config.out_hidden_size,
292
+ context_dim=self.hidden_size,
293
+ norm_layer=norm_layer,
294
+ spatial_merge_size=self.spatial_merge_size,
295
+ quant_config=quant_config,
296
+ prefix=f"{prefix}.merger",
297
+ )
298
+
299
+ self.deepstack_merger_list = nn.ModuleList([
300
+ Qwen3_VisionPatchMerger(
301
+ d_model=vision_config.out_hidden_size,
302
+ context_dim=self.hidden_size,
303
+ spatial_merge_size=self.spatial_merge_size,
304
+ use_postshuffle_norm=True,
305
+ norm_layer=norm_layer,
306
+ quant_config=quant_config,
307
+ prefix=f"{prefix}.deepstack_merger_list.{layer_idx}")
308
+ for layer_idx in range(len(self.deepstack_visual_indexes))
309
+ ])
310
+
311
+ self.attn_backend = get_vit_attn_backend(
312
+ head_size=head_dim, dtype=torch.get_default_dtype())
313
+ if self.attn_backend != _Backend.FLASH_ATTN and \
314
+ check_upstream_fa_availability(
315
+ torch.get_default_dtype()):
316
+ self.attn_backend = _Backend.FLASH_ATTN
317
+
318
+ @property
319
+ def dtype(self) -> torch.dtype:
320
+ return self.patch_embed.proj.weight.dtype
321
+
322
+ @property
323
+ def device(self) -> torch.device:
324
+ return self.patch_embed.proj.weight.device
325
+
326
+ def rot_pos_emb(self, grid_thw):
327
+ pos_ids = []
328
+ for t, h, w in grid_thw:
329
+ hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
330
+ hpos_ids = hpos_ids.reshape(
331
+ h // self.spatial_merge_size,
332
+ self.spatial_merge_size,
333
+ w // self.spatial_merge_size,
334
+ self.spatial_merge_size,
335
+ )
336
+ hpos_ids = hpos_ids.permute(0, 2, 1, 3)
337
+ hpos_ids = hpos_ids.flatten()
338
+
339
+ wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
340
+ wpos_ids = wpos_ids.reshape(
341
+ h // self.spatial_merge_size,
342
+ self.spatial_merge_size,
343
+ w // self.spatial_merge_size,
344
+ self.spatial_merge_size,
345
+ )
346
+ wpos_ids = wpos_ids.permute(0, 2, 1, 3)
347
+ wpos_ids = wpos_ids.flatten()
348
+ pos_ids.append(
349
+ torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
350
+ pos_ids = torch.cat(pos_ids, dim=0)
351
+ max_grid_size = grid_thw[:, 1:].max()
352
+ rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
353
+ rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
354
+ return rotary_pos_emb
355
+
356
+ def fast_pos_embed_interpolate(self, grid_thw):
357
+ num_grid_per_side = int(self.num_position_embeddings**0.5)
358
+
359
+ idx_list = [[] for _ in range(4)]
360
+ weight_list = [[] for _ in range(4)]
361
+
362
+ for t, h, w in grid_thw:
363
+ h_idxs = torch.linspace(0,
364
+ num_grid_per_side - 1,
365
+ h,
366
+ dtype=torch.float32)
367
+ w_idxs = torch.linspace(0,
368
+ num_grid_per_side - 1,
369
+ w,
370
+ dtype=torch.float32)
371
+
372
+ h_idxs_floor = h_idxs.to(torch.long)
373
+ w_idxs_floor = w_idxs.to(torch.long)
374
+ h_idxs_ceil = torch.clamp(h_idxs.to(torch.long) + 1,
375
+ max=num_grid_per_side - 1)
376
+ w_idxs_ceil = torch.clamp(w_idxs.to(torch.long) + 1,
377
+ max=num_grid_per_side - 1)
378
+
379
+ dh = h_idxs - h_idxs_floor
380
+ dw = w_idxs - w_idxs_floor
381
+
382
+ idx_list[0].extend(((h_idxs_floor * num_grid_per_side)[None].T +
383
+ w_idxs_floor[None]).flatten().tolist() * t)
384
+ idx_list[1].extend(((h_idxs_floor * num_grid_per_side)[None].T +
385
+ w_idxs_ceil[None]).flatten().tolist() * t)
386
+ idx_list[2].extend(((h_idxs_ceil * num_grid_per_side)[None].T +
387
+ w_idxs_floor[None]).flatten().tolist() * t)
388
+ idx_list[3].extend(((h_idxs_ceil * num_grid_per_side)[None].T +
389
+ w_idxs_ceil[None]).flatten().tolist() * t)
390
+
391
+ weight_list[0].extend(
392
+ ((1 - dh)[None].T * (1 - dw)[None]).flatten().tolist() * t)
393
+ weight_list[1].extend(
394
+ ((1 - dh)[None].T * dw[None]).flatten().tolist() * t)
395
+ weight_list[2].extend(
396
+ (dh[None].T * (1 - dw)[None]).flatten().tolist() * t)
397
+ weight_list[3].extend(
398
+ (dh[None].T * dw[None]).flatten().tolist() * t)
399
+
400
+ device = self.pos_embed.weight.device
401
+ dtype = self.pos_embed.weight.dtype
402
+
403
+ p0 = self.pos_embed(
404
+ torch.tensor(
405
+ idx_list[0], dtype=torch.long, device=device)) * torch.tensor(
406
+ weight_list[0], dtype=dtype, device=device)[:, None]
407
+ p1 = self.pos_embed(
408
+ torch.tensor(
409
+ idx_list[1], dtype=torch.long, device=device)) * torch.tensor(
410
+ weight_list[1], dtype=dtype, device=device)[:, None]
411
+ p2 = self.pos_embed(
412
+ torch.tensor(
413
+ idx_list[2], dtype=torch.long, device=device)) * torch.tensor(
414
+ weight_list[2], dtype=dtype, device=device)[:, None]
415
+ p3 = self.pos_embed(
416
+ torch.tensor(
417
+ idx_list[3], dtype=torch.long, device=device)) * torch.tensor(
418
+ weight_list[3], dtype=dtype, device=device)[:, None]
419
+
420
+ patch_pos_embeds = p0 + p1 + p2 + p3
421
+ patch_pos_embeds = patch_pos_embeds.split(
422
+ [t * h * w for t, h, w in grid_thw])
423
+ patch_pos_embeds_permute = []
424
+ m_size = self.spatial_merge_size
425
+ for pos_embed, (t, h, w) in zip(patch_pos_embeds, grid_thw):
426
+ pos_embed = pos_embed.view(t, h // m_size, m_size, w // m_size,
427
+ m_size, -1).permute(0, 1, 3, 2, 4,
428
+ 5).flatten(0, 4)
429
+ patch_pos_embeds_permute.append(pos_embed)
430
+ patch_pos_embeds = torch.cat(patch_pos_embeds_permute)
431
+ return patch_pos_embeds
432
+
433
+ def compute_attn_mask_seqlen(
434
+ self,
435
+ cu_seqlens: torch.Tensor,
436
+ ) -> tuple[Optional[int], Optional[list[int]]]:
437
+ max_seqlen, seqlens = None, None
438
+ if self.attn_backend == _Backend.FLASH_ATTN:
439
+ max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
440
+ elif self.attn_backend == _Backend.XFORMERS:
441
+ seqlens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
442
+ return max_seqlen, seqlens
443
+
444
+ def forward(
445
+ self,
446
+ x: torch.Tensor,
447
+ grid_thw: list[list[int]],
448
+ ) -> torch.Tensor:
449
+ hidden_states = x.to(device=self.device, dtype=self.dtype)
450
+ hidden_states = self.patch_embed(hidden_states)
451
+
452
+ pos_embeds = self.fast_pos_embed_interpolate(grid_thw)
453
+ hidden_states = hidden_states + pos_embeds
454
+ rotary_pos_emb = self.rot_pos_emb(grid_thw)
455
+
456
+ cu_seqlens = torch.repeat_interleave(
457
+ grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum(
458
+ dim=0,
459
+ dtype=grid_thw.dtype
460
+ if torch.jit.is_tracing() else torch.int32,
461
+ )
462
+ cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
463
+
464
+ hidden_states = hidden_states.unsqueeze(1)
465
+ rotary_pos_emb = rotary_pos_emb.to(hidden_states.device)
466
+ max_seqlen, seqlens = self.compute_attn_mask_seqlen(cu_seqlens)
467
+
468
+ deepstack_feature_lists = []
469
+ for layer_num, blk in enumerate(self.blocks):
470
+ hidden_states = blk(hidden_states,
471
+ cu_seqlens=cu_seqlens,
472
+ rotary_pos_emb=rotary_pos_emb,
473
+ max_seqlen=max_seqlen,
474
+ seqlens=seqlens)
475
+ if layer_num in self.deepstack_visual_indexes:
476
+ deepstack_merger_idx = self.deepstack_visual_indexes.index(
477
+ layer_num)
478
+ deepstack_feature = self.deepstack_merger_list[
479
+ deepstack_merger_idx](hidden_states)
480
+ deepstack_feature_lists.append(deepstack_feature)
481
+ hidden_states = self.merger(hidden_states)
482
+ hidden_states = torch.cat(
483
+ [hidden_states] + deepstack_feature_lists,
484
+ dim=1) # [seq_len, hidden_size * (1 + depth_of_deepstack)]
485
+ return hidden_states
486
+
487
+ def load_weights(self, weights: Iterable[tuple[str,
488
+ torch.Tensor]]) -> set[str]:
489
+ stacked_params_mapping = [
490
+ # (param_name, shard_name, shard_id)
491
+ ("attn.qkv.", "attn.q.", "q"),
492
+ ("attn.qkv.", "attn.k.", "k"),
493
+ ("attn.qkv.", "attn.v.", "v"),
494
+ ]
495
+ params_dict = dict(self.named_parameters(remove_duplicate=False))
496
+ loaded_params: set[str] = set()
497
+
498
+ for name, loaded_weight in weights:
499
+ for (param_name, weight_name, shard_id) in stacked_params_mapping:
500
+ if weight_name not in name:
501
+ continue
502
+ name = name.replace(weight_name, param_name)
503
+
504
+ param = params_dict[name]
505
+ weight_loader = param.weight_loader
506
+ weight_loader(param, loaded_weight, shard_id)
507
+ break
508
+ else:
509
+ param = params_dict[name]
510
+ weight_loader = getattr(param, "weight_loader",
511
+ default_weight_loader)
512
+ weight_loader(param, loaded_weight)
513
+ loaded_params.add(name)
514
+ return loaded_params
515
+
516
+
517
+class Qwen3VLProcessingInfo(Qwen2VLProcessingInfo):
518
+
519
+ def get_hf_config(self):
520
+ return self.ctx.get_hf_config(Qwen3VLConfig)
521
+
522
+ def get_hf_processor(self, **kwargs: object) -> Qwen3VLProcessor:
523
+ return self.ctx.get_hf_processor(
524
+ Qwen3VLProcessor,
525
+ use_fast=kwargs.pop("use_fast", True),
526
+ **kwargs,
527
+ )
528
+
529
+ def get_tokenizer(self):
530
+ return self.ctx.tokenizer
531
+
532
+ def get_image_processor(self,
533
+ **kwargs: object) -> Qwen2VLImageProcessorFast:
534
+ return self.get_hf_processor(**kwargs).image_processor
535
+
536
+ def get_video_processor(self, **kwargs: object) -> Qwen3VLVideoProcessor:
537
+ return self.get_hf_processor(**kwargs).video_processor
538
+
539
+ def _get_vision_info(
540
+ self,
541
+ *,
542
+ image_width: int,
543
+ image_height: int,
544
+ num_frames: int = 2,
545
+ do_resize: bool = True,
546
+ image_processor: Optional[Qwen2VLImageProcessorFast],
547
+ ) -> tuple[ImageSize, int]:
548
+ if image_processor is None:
549
+ image_processor = self.get_image_processor()
550
+
551
+ hf_config = self.get_hf_config()
552
+ vision_config = hf_config.vision_config
553
+ patch_size = vision_config.patch_size
554
+ merge_size = vision_config.spatial_merge_size
555
+ temporal_patch_size = vision_config.temporal_patch_size
556
+
557
+ if do_resize:
558
+ resized_height, resized_width = smart_resize(
559
+ height=image_height,
560
+ width=image_width,
561
+ factor=patch_size * merge_size,
562
+ min_pixels=image_processor.size["shortest_edge"],
563
+ max_pixels=image_processor.size["longest_edge"],
564
+ )
565
+ preprocessed_size = ImageSize(width=resized_width,
566
+ height=resized_height)
567
+ else:
568
+ preprocessed_size = ImageSize(width=image_width,
569
+ height=image_height)
570
+
571
+ padded_num_frames = num_frames + num_frames % temporal_patch_size
572
+
573
+ grid_t = max(padded_num_frames // temporal_patch_size, 1)
574
+ grid_h = preprocessed_size.height // patch_size
575
+ grid_w = preprocessed_size.width // patch_size
576
+
577
+ num_patches = grid_t * grid_h * grid_w
578
+ num_vision_tokens = num_patches // (merge_size**2)
579
+
580
+ return preprocessed_size, num_vision_tokens
581
+
582
+ def _calculate_timestamps(self, indices: list[int] | torch.Tensor,
583
+ video_fps: float, merge_size: int):
584
+ if not isinstance(indices, list):
585
+ indices = indices.tolist()
586
+ if len(indices) % merge_size != 0:
587
+ # don't update metadata's frames_indices directly
588
+ indices = indices + [indices[-1]
589
+ ] * (merge_size - len(indices) % merge_size)
590
+ timestamps = [idx / video_fps for idx in indices]
591
+ timestamps = [(timestamps[i] + timestamps[i + merge_size - 1]) / 2
592
+ for i in range(0, len(timestamps), merge_size)]
593
+ return timestamps
594
+
595
+ def _get_video_second_idx(
596
+ self,
597
+ metadata: dict[str, Any],
598
+ out_item: MultiModalKwargsItem,
599
+ do_sample_frames: Optional[bool] = None,
600
+ sampled_fps: Optional[float] = None) -> list[int]:
601
+ video_processor = self.get_video_processor()
602
+ merge_size = video_processor.merge_size
603
+ indices = metadata["frames_indices"]
604
+
605
+ # metadata["fps"] refers to the true fps of the input video.
606
+ video_fps = metadata["fps"]
607
+ if do_sample_frames is None:
608
+ do_sample_frames = metadata.get("do_sample_frames", False)
609
+
610
+ # If video frames are sampled in HF processor (instead of vLLM
611
+ # video loader), we need to re-calculate the indices from original
612
+ # metadata.
613
+ if do_sample_frames:
614
+ # here video_fps is the fps of the sampled video, and
615
+ # metadata["fps"] refers to the fps of the original video.
616
+ video_fps = sampled_fps if sampled_fps else video_processor.fps
617
+ total_num_frames = metadata["total_num_frames"]
618
+ num_frames = int(total_num_frames / metadata["fps"] * video_fps)
619
+ num_frames = min(
620
+ min(max(num_frames, video_processor.min_frames),
621
+ video_processor.max_frames), total_num_frames)
622
+ indices = np.linspace(0, total_num_frames - 1,
623
+ num_frames).round().astype(int).tolist()
624
+ timestamps = self._calculate_timestamps(indices, video_fps, merge_size)
625
+ return timestamps
626
+
627
+
628
+class Qwen3VLDummyInputsBuilder(BaseDummyInputsBuilder[Qwen3VLProcessingInfo]):
629
+
630
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
631
+ num_images = mm_counts.get("image", 0)
632
+ num_videos = mm_counts.get("video", 0)
633
+
634
+ image_token = "<|vision_start|><|image_pad|><|vision_end|>"
635
+ video_token = "<|vision_start|><|video_pad|><|vision_end|>"
636
+
637
+ return image_token * num_images + video_token * num_videos
638
+
639
+ def get_dummy_mm_data(
640
+ self,
641
+ seq_len: int,
642
+ mm_counts: Mapping[str, int],
643
+ ) -> MultiModalDataDict:
644
+ num_images = mm_counts.get("image", 0)
645
+ num_videos = mm_counts.get("video", 0)
646
+
647
+ target_width, target_height = (
648
+ self.info.get_image_size_with_most_features())
649
+ target_num_frames = self.info.get_num_frames_with_most_features(
650
+ seq_len, mm_counts)
651
+ return {
652
+ "image":
653
+ self._get_dummy_images(width=target_width,
654
+ height=target_height,
655
+ num_images=num_images),
656
+ "video":
657
+ self._get_dummy_videos(
658
+ width=target_width,
659
+ height=target_height,
660
+ num_frames=target_num_frames,
661
+ num_videos=num_videos,
662
+ ),
663
+ }
664
+
665
+ def _get_dummy_videos(
666
+ self,
667
+ *,
668
+ width: int,
669
+ height: int,
670
+ num_frames: int,
671
+ num_videos: int,
672
+ ) -> list[VideoItem]:
673
+ num_frames = max(num_frames, 2)
674
+ video = np.full((num_frames, width, height, 3), 255, dtype=np.uint8)
675
+ video_items = []
676
+ for i in range(num_videos):
677
+ video_metadata = {
678
+ "fps": 2.0,
679
+ "duration": num_frames / 2.0,
680
+ "total_num_frames": num_frames,
681
+ "frames_indices": [i for i in range(num_frames)],
682
+ "video_backend": "opencv",
683
+ "do_sample_frames": False,
684
+ }
685
+ video_item = (video.copy(), video_metadata)
686
+ video_items.append(video_item)
687
+ return video_items
688
+
689
+
690
+class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo]
691
+ ):
692
+
693
+ def _get_data_parser(self) -> MultiModalDataParser:
694
+ return MultiModalDataParser(video_needs_metadata=True)
695
+
696
+ def _call_hf_processor(
697
+ self,
698
+ prompt: str,
699
+ mm_data: Mapping[str, object],
700
+ mm_kwargs: Mapping[str, object],
701
+ tok_kwargs: Mapping[str, object],
702
+ ) -> BatchFeature:
703
+ mm_data = dict(mm_data)
704
+ processor = self.info.get_hf_processor(**mm_kwargs)
705
+
706
+ # Separate video processing from image processing. Because the videos
707
+ # are processed into serval image patches
708
+ if ("videos" in mm_data and isinstance(mm_data["videos"], list)
709
+ and len(mm_data["videos"]) > 0):
710
+ video_grid_thw_lst = []
711
+ pixel_values_videos_lst = []
712
+
713
+ for item_idx, item in enumerate(mm_data.pop("videos", [])):
714
+ video_array, metadata = item
715
+
716
+ # NOTE: @JJJYmmm new attr metadata.frames_indices indicates
717
+ # the sampled frames indices of pre-sampled videos, which is
718
+ # used to calculate the timestamps. Make sure that
719
+ # do_sample_frames in mm_kwargs is false for presampled videos.
720
+
721
+ # NOTE: a copy of is created to update do_sample_frames,
722
+ # otherwise mm_hash for the object will be incorrect.
723
+ video_mm_kwargs = dict(**mm_kwargs)
724
+ if "do_sample_frames" not in video_mm_kwargs:
725
+ # qwen_vl_utils already has "do_sample_frames" in
726
+ # mm_kwargs, don't overwrite it.
727
+ video_mm_kwargs["do_sample_frames"] = metadata.get(
728
+ "do_sample_frames", False)
729
+
730
+ metadata = VideoMetadata(**{
731
+ k: metadata[k]
732
+ for k in metadata if k != "do_sample_frames"
733
+ })
734
+
735
+ video_mm_data = dict()
736
+ video_mm_data["videos"] = [[video_array]]
737
+ video_mm_data["video_metadata"] = [[metadata]]
738
+
739
+ video_outputs = super()._call_hf_processor(
740
+ prompt="<|vision_start|><|video_pad|><|vision_end|>",
741
+ mm_data=video_mm_data,
742
+ mm_kwargs=video_mm_kwargs,
743
+ tok_kwargs=tok_kwargs,
744
+ )
745
+ input_ids = video_outputs.pop("input_ids")
746
+ video_placeholder = processor.tokenizer.batch_decode(
747
+ input_ids)[0]
748
+ prompt = prompt.replace(
749
+ "<|vision_start|><|video_pad|><|vision_end|>",
750
+ video_placeholder,
751
+ 1,
752
+ )
753
+
754
+ video_grid_thw_lst.append(video_outputs["video_grid_thw"])
755
+ pixel_values_videos_lst.append(
756
+ video_outputs["pixel_values_videos"])
757
+ video_outputs = dict(
758
+ pixel_values_videos=torch.cat(pixel_values_videos_lst),
759
+ video_grid_thw=torch.cat(video_grid_thw_lst),
760
+ )
761
+ else:
762
+ video_outputs = dict()
763
+
764
+ processed_outputs = super()._call_hf_processor(
765
+ prompt=prompt,
766
+ mm_data=mm_data,
767
+ mm_kwargs=mm_kwargs,
768
+ tok_kwargs=tok_kwargs,
769
+ )
770
+ combined_outputs = dict(
771
+ processed_outputs,
772
+ **video_outputs,
773
+ )
774
+ return BatchFeature(combined_outputs)
775
+
776
+ def _get_mm_fields_config(
777
+ self,
778
+ hf_inputs: BatchFeature,
779
+ hf_processor_mm_kwargs: Mapping[str, object],
780
+ ) -> Mapping[str, MultiModalFieldConfig]:
781
+ image_grid_thw = hf_inputs.get("image_grid_thw", torch.empty((0, 3)))
782
+ image_grid_sizes = image_grid_thw.prod(-1)
783
+
784
+ video_grid_thw = hf_inputs.get("video_grid_thw", torch.empty((0, 3)))
785
+ video_grid_sizes = video_grid_thw.prod(-1)
786
+
787
+ return dict(
788
+ pixel_values=MultiModalFieldConfig.flat_from_sizes(
789
+ "image", image_grid_sizes),
790
+ image_embeds=MultiModalFieldConfig.flat_from_sizes(
791
+ "image", image_grid_sizes),
792
+ image_grid_thw=MultiModalFieldConfig.batched("image"),
793
+ pixel_values_videos=MultiModalFieldConfig.flat_from_sizes(
794
+ "video", video_grid_sizes),
795
+ video_embeds=MultiModalFieldConfig.flat_from_sizes(
796
+ "video", video_grid_sizes),
797
+ video_grid_thw=MultiModalFieldConfig.batched("video"),
798
+ )
799
+
800
+ def _get_prompt_updates(
801
+ self,
802
+ mm_items: MultiModalDataItems,
803
+ hf_processor_mm_kwargs: Mapping[str, Any],
804
+ out_mm_kwargs: MultiModalKwargsItems,
805
+ ) -> Sequence[PromptUpdate]:
806
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
807
+ image_processor = self.info.get_image_processor(
808
+ **hf_processor_mm_kwargs)
809
+ tokenizer = self.info.get_tokenizer()
810
+ hf_config = self.info.get_hf_config()
811
+
812
+ video_token_id = hf_config.video_token_id
813
+ vision_start_token_id = hf_config.vision_start_token_id
814
+ vision_end_token_id = hf_config.vision_end_token_id
815
+
816
+ merge_length = image_processor.merge_size**2
817
+
818
+ def get_image_replacement_qwen3vl(item_idx: int):
819
+ out_item = out_mm_kwargs["image"][item_idx]
820
+ grid_thw = out_item["image_grid_thw"].data
821
+ assert isinstance(grid_thw, torch.Tensor)
822
+
823
+ num_tokens = int(grid_thw.prod()) // merge_length
824
+ return [hf_processor.image_token_id] * num_tokens
825
+
826
+ def get_video_replacement_qwen3vl(item_idx: int):
827
+ out_item = out_mm_kwargs["video"][item_idx]
828
+ grid_thw = out_item["video_grid_thw"].data
829
+ assert isinstance(grid_thw, torch.Tensor)
830
+
831
+ video, metadata = mm_items["video"][item_idx]
832
+ do_sample_frames = hf_processor_mm_kwargs.get("do_sample_frames")
833
+ sampled_fps = hf_processor_mm_kwargs.get("fps")
834
+ if is_list_of(sampled_fps, float):
835
+ sampled_fps = sampled_fps[item_idx]
836
+ timestamps = self.info._get_video_second_idx(
837
+ metadata, out_item, do_sample_frames, sampled_fps)
838
+
839
+ assert len(timestamps) == grid_thw[0], (
840
+ f"The timestamps length({len(timestamps)}) should be equal "
841
+ f"video length ({grid_thw[0]}).")
842
+
843
+ frames_idx_token = [
844
+ tokenizer.encode(f"<{curr_time:.1f} seconds>",
845
+ add_special_tokens=False)
846
+ for curr_time in timestamps
847
+ ]
848
+ num_tokens_per_frame = int(grid_thw[1:].prod()) // merge_length
849
+ placeholder = []
850
+ for frame_idx in frames_idx_token:
851
+ placeholder.extend(frame_idx)
852
+ placeholder.extend([vision_start_token_id] +
853
+ [video_token_id] * num_tokens_per_frame +
854
+ [vision_end_token_id])
855
+ return PromptUpdateDetails.select_token_id(placeholder,
856
+ video_token_id)
857
+
858
+ return [
859
+ PromptReplacement(
860
+ modality="image",
861
+ target=hf_processor.image_token,
862
+ replacement=get_image_replacement_qwen3vl,
863
+ ),
864
+
865
+ # NOTE: We match string on purpose since searching sequence of
866
+ # token ids takes more time.
867
+ PromptReplacement(
868
+ modality="video",
869
+ target="<|vision_start|><|video_pad|><|vision_end|>",
870
+ replacement=get_video_replacement_qwen3vl,
871
+ ),
872
+ ]
873
+
874
+
875
+@support_torch_compile(
876
+ dynamic_arg_dims={
877
+ "input_ids": 0,
878
+ # positions is of shape (3, seq_len) if mrope is enabled for qwen2-vl,
879
+ # otherwise (seq_len, ).
880
+ "positions": -1,
881
+ "intermediate_tensors": 0,
882
+ "inputs_embeds": 0,
883
+ # the same shape as input_embeds
884
+ "deepstack_input_embeds": 0
885
+ })
886
+class Qwen3LLMModel(Qwen3Model):
887
+
888
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
889
+ super().__init__(vllm_config=vllm_config, prefix=prefix)
890
+ if not get_pp_group().is_first_rank:
891
+ assert self.start_layer >= len(
892
+ vllm_config.model_config.hf_config.vision_config.
893
+ deepstack_visual_indexes), (
894
+ "start_layer should be greater than or equal to "
895
+ "len(deepstack_visual_indexes)")
896
+
897
+ def forward(
898
+ self,
899
+ input_ids: torch.Tensor,
900
+ positions: torch.Tensor,
901
+ intermediate_tensors: Optional[IntermediateTensors] = None,
902
+ inputs_embeds: Optional[torch.Tensor] = None,
903
+ # args for deepstack
904
+ deepstack_input_embeds: Optional[IntermediateTensors] = None,
905
+ ) -> Union[torch.Tensor, IntermediateTensors]:
906
+ if get_pp_group().is_first_rank:
907
+ if inputs_embeds is not None:
908
+ hidden_states = inputs_embeds
909
+ else:
910
+ hidden_states = self.get_input_embeddings(input_ids)
911
+ residual = None
912
+ else:
913
+ assert intermediate_tensors is not None
914
+ hidden_states = intermediate_tensors["hidden_states"]
915
+ residual = intermediate_tensors["residual"]
916
+ for layer_idx, layer in enumerate(
917
+ self.layers[self.start_layer:self.end_layer]):
918
+ layer_idx = layer_idx + self.start_layer
919
+
920
+ hidden_states, residual = layer(
921
+ positions,
922
+ hidden_states,
923
+ residual,
924
+ )
925
+
926
+ if deepstack_input_embeds is not None and \
927
+ layer_idx in range(0, len(deepstack_input_embeds)):
928
+ hidden_states = hidden_states + deepstack_input_embeds[
929
+ f"deepstack_input_embeds_{layer_idx}"]
930
+
931
+ if not get_pp_group().is_last_rank:
932
+ return IntermediateTensors({
933
+ "hidden_states": hidden_states,
934
+ "residual": residual
935
+ })
936
+ hidden_states, _ = self.norm(hidden_states, residual)
937
+ return hidden_states
938
+
939
+
940
+class Qwen3LLMForCausalLM(Qwen3ForCausalLM):
941
+
942
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
943
+ super(Qwen3ForCausalLM, self).__init__()
944
+ config = vllm_config.model_config.hf_config.text_config
945
+ quant_config = vllm_config.quant_config
946
+ lora_config = vllm_config.lora_config
947
+
948
+ self.config = config
949
+ self.lora_config = lora_config
950
+
951
+ self.quant_config = quant_config
952
+ self.model = Qwen3LLMModel(vllm_config=vllm_config, prefix=prefix)
953
+
954
+ if get_pp_group().is_last_rank:
955
+ if config.tie_word_embeddings:
956
+ self.lm_head = self.model.embed_tokens
957
+ else:
958
+ self.lm_head = ParallelLMHead(config.vocab_size,
959
+ config.hidden_size,
960
+ quant_config=quant_config,
961
+ prefix="lm_head")
962
+ else:
963
+ self.lm_head = PPMissingLayer()
964
+
965
+ self.logits_processor = LogitsProcessor(config.vocab_size)
966
+
967
+ self.make_empty_intermediate_tensors = (
968
+ self.model.make_empty_intermediate_tensors)
969
+
970
+
971
+@MULTIMODAL_REGISTRY.register_processor(Qwen3VLMultiModalProcessor,
972
+ info=Qwen3VLProcessingInfo,
973
+ dummy_inputs=Qwen3VLDummyInputsBuilder)
974
+class Qwen3VLForConditionalGeneration(nn.Module, SupportsMultiModal,
975
+ SupportsLoRA, SupportsPP):
976
+ packed_modules_mapping = {
977
+ "qkv_proj": [
978
+ "q_proj",
979
+ "k_proj",
980
+ "v_proj",
981
+ ],
982
+ "gate_up_proj": [
983
+ "gate_proj",
984
+ "up_proj",
985
+ ],
986
+ }
987
+ # To ensure correct weight loading and mapping.
988
+ hf_to_vllm_mapper = WeightsMapper(
989
+ orig_to_new_prefix={
990
+ "model.visual.": "visual.",
991
+ "lm_head.": "language_model.lm_head.",
992
+ "model.language_model.": "language_model.model.",
993
+ })
994
+
995
+ @classmethod
996
+ def get_placeholder_str(cls, modality: str, i: int) -> Optional[str]:
997
+ if modality.startswith("image"):
998
+ return "<|vision_start|><|image_pad|><|vision_end|>"
999
+ if modality.startswith("video"):
1000
+ return "<|vision_start|><|video_pad|><|vision_end|>"
1001
+
1002
+ raise ValueError("Only image or video modality is supported")
1003
+
1004
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
1005
+ super().__init__()
1006
+ config: Qwen3VLConfig = vllm_config.model_config.hf_config
1007
+ quant_config = vllm_config.quant_config
1008
+ multimodal_config = vllm_config.model_config.multimodal_config
1009
+
1010
+ self.config = config
1011
+ self.multimodal_config = multimodal_config
1012
+
1013
+ self.visual = Qwen3_VisionTransformer(
1014
+ config.vision_config,
1015
+ norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1016
+ quant_config=self._maybe_ignore_quant_config(quant_config),
1017
+ prefix=maybe_prefix(prefix, "visual"),
1018
+ )
1019
+
1020
+ self.language_model = Qwen3LLMForCausalLM(vllm_config=vllm_config,
1021
+ prefix=maybe_prefix(
1022
+ prefix,
1023
+ "language_model"))
1024
+
1025
+ self.make_empty_intermediate_tensors = (
1026
+ self.language_model.make_empty_intermediate_tensors)
1027
+
1028
+ self.use_deepstack = hasattr(config.vision_config,
1029
+ 'deepstack_visual_indexes')
1030
+ self.deepstack_num_level = len(
1031
+ config.vision_config.deepstack_visual_indexes
1032
+ ) if self.use_deepstack else 0
1033
+ # register buffer for deepstack
1034
+ self.deepstack_input_embeds = [
1035
+ torch.zeros(vllm_config.scheduler_config.max_num_batched_tokens,
1036
+ config.text_config.hidden_size)
1037
+ for _ in range(self.deepstack_num_level)
1038
+ ] if self.use_deepstack else None
1039
+
1040
+ def _get_deepstack_input_embeds(self,
1041
+ num_tokens: int) -> IntermediateTensors:
1042
+ # get deepstack_input_embeds from buffer, and clear the buffer
1043
+ return IntermediateTensors({
1044
+ f"deepstack_input_embeds_{idx}":
1045
+ self.deepstack_input_embeds[idx][:num_tokens]
1046
+ for idx in range(self.deepstack_num_level)
1047
+ })
1048
+
1049
+ def _set_deepstack_input_embeds(
1050
+ self, deepstack_input_embeds: torch.Tensor) -> None:
1051
+ # set deepstack_input_embeds to buffer
1052
+ num_tokens = deepstack_input_embeds.size(1)
1053
+ if num_tokens > self.deepstack_input_embeds[0].size(0):
1054
+ self.deepstack_input_embeds = [
1055
+ torch.zeros(num_tokens,
1056
+ self.config.text_config.hidden_size,
1057
+ device=self.deepstack_input_embeds[0].device,
1058
+ dtype=self.deepstack_input_embeds[0].dtype)
1059
+ for _ in range(self.deepstack_num_level)
1060
+ ]
1061
+ for idx in range(self.deepstack_num_level):
1062
+ self.deepstack_input_embeds[idx][:num_tokens].copy_(
1063
+ deepstack_input_embeds[idx])
1064
+
1065
+ def _clear_deepstack_input_embeds(self, num_tokens: int) -> None:
1066
+ # clear deepstack_input_embeds in buffer
1067
+ if num_tokens > 0:
1068
+ for idx in range(self.deepstack_num_level):
1069
+ self.deepstack_input_embeds[idx][:num_tokens].zero_()
1070
+
1071
+ def _maybe_ignore_quant_config(self, quant_config: QuantizationConfig):
1072
+ # GPTQ configs do not have a list of ignored modules, however AutoGPTQ
1073
+ # seems to avoid vision encoder sections for some models.
1074
+ if isinstance(quant_config, (GPTQConfig, GPTQMarlinConfig)):
1075
+ return None
1076
+ return quant_config
1077
+
1078
+ def _validate_and_reshape_mm_tensor(self, mm_input: object,
1079
+ name: str) -> torch.Tensor:
1080
+ if not isinstance(mm_input, (torch.Tensor, list)):
1081
+ raise ValueError(f"Incorrect type of {name}. "
1082
+ f"Got type: {type(mm_input)}")
1083
+ if isinstance(mm_input, torch.Tensor):
1084
+ if mm_input.ndim == 2:
1085
+ return mm_input
1086
+ if mm_input.ndim != 3:
1087
+ raise ValueError(f"{name} should be 2D or batched 3D tensor. "
1088
+ f"Got ndim: {mm_input.ndim} "
1089
+ f"(shape={mm_input.shape})")
1090
+ return torch.concat(list(mm_input))
1091
+ else:
1092
+ return torch.concat(mm_input)
1093
+
1094
+ def _parse_and_validate_image_input(
1095
+ self, **kwargs: object) -> Optional[Qwen2_5_VLImageInputs]:
1096
+ pixel_values = kwargs.pop("pixel_values", None)
1097
+ image_embeds = kwargs.pop("image_embeds", None)
1098
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
1099
+
1100
+ if pixel_values is None and image_embeds is None:
1101
+ return None
1102
+
1103
+ if pixel_values is not None:
1104
+ pixel_values = self._validate_and_reshape_mm_tensor(
1105
+ pixel_values, "image pixel values")
1106
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1107
+ image_grid_thw, "image grid_thw")
1108
+
1109
+ if not isinstance(pixel_values, (torch.Tensor, list)):
1110
+ raise ValueError("Incorrect type of image pixel values. "
1111
+ f"Got type: {type(pixel_values)}")
1112
+
1113
+ return Qwen2_5_VLImagePixelInputs(type="pixel_values",
1114
+ pixel_values=pixel_values,
1115
+ image_grid_thw=image_grid_thw)
1116
+
1117
+ if image_embeds is not None:
1118
+ image_embeds = self._validate_and_reshape_mm_tensor(
1119
+ image_embeds, "image embeds")
1120
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1121
+ image_grid_thw, "image grid_thw")
1122
+
1123
+ if not isinstance(image_embeds, torch.Tensor):
1124
+ raise ValueError("Incorrect type of image embeddings. "
1125
+ f"Got type: {type(image_embeds)}")
1126
+ return Qwen2_5_VLImageEmbeddingInputs(
1127
+ type="image_embeds",
1128
+ image_embeds=image_embeds,
1129
+ image_grid_thw=image_grid_thw)
1130
+
1131
+ def _parse_and_validate_video_input(
1132
+ self, **kwargs: object) -> Optional[Qwen2_5_VLVideoInputs]:
1133
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1134
+ video_embeds = kwargs.pop("video_embeds", None)
1135
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
1136
+ second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
1137
+
1138
+ if pixel_values_videos is None and video_embeds is None:
1139
+ return None
1140
+
1141
+ if pixel_values_videos is not None:
1142
+ pixel_values_videos = self._validate_and_reshape_mm_tensor(
1143
+ pixel_values_videos, "video pixel values")
1144
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1145
+ video_grid_thw, "video grid_thw")
1146
+
1147
+ return Qwen2_5_VLVideoPixelInputs(
1148
+ type="pixel_values_videos",
1149
+ pixel_values_videos=pixel_values_videos,
1150
+ video_grid_thw=video_grid_thw,
1151
+ second_per_grid_ts=second_per_grid_ts,
1152
+ )
1153
+
1154
+ if video_embeds is not None:
1155
+ video_embeds = self._validate_and_reshape_mm_tensor(
1156
+ video_embeds, "video embeds")
1157
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1158
+ video_grid_thw, "video grid_thw")
1159
+
1160
+ if not isinstance(video_embeds, torch.Tensor):
1161
+ raise ValueError("Incorrect type of video embeddings. "
1162
+ f"Got type: {type(video_embeds)}")
1163
+ return Qwen2_5_VLVideoEmbeddingInputs(
1164
+ type="video_embeds",
1165
+ video_embeds=video_embeds,
1166
+ video_grid_thw=video_grid_thw)
1167
+
1168
+ def _process_image_input(
1169
+ self,
1170
+ image_input: Qwen2_5_VLImageInputs) -> tuple[torch.Tensor, ...]:
1171
+
1172
+ grid_thw = image_input["image_grid_thw"]
1173
+ assert grid_thw.ndim == 2
1174
+ grid_thw_list = grid_thw.tolist()
1175
+
1176
+ if image_input["type"] == "image_embeds":
1177
+ image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1178
+ else:
1179
+ pixel_values = image_input["pixel_values"].type(self.visual.dtype)
1180
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
1181
+
1182
+ # Split concatenated embeddings for each image item.
1183
+ # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1184
+ merge_size = self.visual.spatial_merge_size
1185
+ sizes = (torch.tensor(grid_thw_list, dtype=torch.long).prod(-1) //
1186
+ (merge_size * merge_size)).tolist()
1187
+ return image_embeds.split(sizes)
1188
+
1189
+ def _process_video_input(
1190
+ self,
1191
+ video_input: Qwen2_5_VLVideoInputs) -> tuple[torch.Tensor, ...]:
1192
+
1193
+ grid_thw = video_input["video_grid_thw"]
1194
+ assert grid_thw.ndim == 2
1195
+ grid_thw_list = grid_thw.tolist()
1196
+
1197
+ if video_input["type"] == "video_embeds":
1198
+ video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1199
+ else:
1200
+ pixel_values_videos = video_input["pixel_values_videos"].type(
1201
+ self.visual.dtype)
1202
+ video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw)
1203
+
1204
+ # Split concatenated embeddings for each video item.
1205
+ # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1206
+ merge_size = self.visual.spatial_merge_size
1207
+ sizes = (torch.tensor(grid_thw_list, dtype=torch.long).prod(-1) //
1208
+ (merge_size * merge_size)).tolist()
1209
+ return video_embeds.split(sizes)
1210
+
1211
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1212
+ mm_input_by_modality = {}
1213
+ for input_key in kwargs:
1214
+ if input_key in ("pixel_values", "image_embeds"
1215
+ ) and "image" not in mm_input_by_modality:
1216
+ mm_input_by_modality[
1217
+ "image"] = self._parse_and_validate_image_input(**kwargs)
1218
+ if input_key in ("pixel_values_videos", "video_embeds"
1219
+ ) and "video" not in mm_input_by_modality:
1220
+ mm_input_by_modality[
1221
+ "video"] = self._parse_and_validate_video_input(**kwargs)
1222
+ return mm_input_by_modality
1223
+
1224
+ def get_language_model(self) -> torch.nn.Module:
1225
+ return self.language_model
1226
+
1227
+ def get_multimodal_embeddings(
1228
+ self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
1229
+
1230
+ mm_input_by_modality = self._parse_and_validate_multimodal_inputs(
1231
+ **kwargs)
1232
+ if not mm_input_by_modality:
1233
+ return None
1234
+
1235
+ # The result multimodal_embeddings is tuple of tensors, with each
1236
+ # tensor correspoending to a multimodal data item (image or video).
1237
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1238
+
1239
+ # NOTE: It is important to iterate over the keys in this dictionary
1240
+ # to preserve the order of the modalities.
1241
+ for modality in mm_input_by_modality:
1242
+ multimodal_input = mm_input_by_modality[modality]
1243
+ if modality == "image":
1244
+ vision_embeddings = self._process_image_input(multimodal_input)
1245
+ multimodal_embeddings += vision_embeddings
1246
+ if modality == "video":
1247
+ video_embeddings = self._process_video_input(multimodal_input)
1248
+ multimodal_embeddings += video_embeddings
1249
+ return multimodal_embeddings
1250
+
1251
+ def _compute_deepstack_embeds(
1252
+ self, input_ids: torch.Tensor, inputs_embeds: torch.Tensor,
1253
+ multimodal_embeddings: MultiModalEmbeddings) -> torch.Tensor:
1254
+ visual_lens = [
1255
+ x.shape[0] if isinstance(x, torch.Tensor) else len(x)
1256
+ for x in multimodal_embeddings
1257
+ ]
1258
+ multimodal_embeddings_cat = torch.cat(multimodal_embeddings, dim=0)
1259
+
1260
+ visual_dim = multimodal_embeddings_cat.shape[-1] // (
1261
+ self.deepstack_num_level + 1)
1262
+
1263
+ main_dim, multi_dim = visual_dim, visual_dim * self.deepstack_num_level
1264
+ multimodal_embeddings_main, multimodal_embeddings_multiscale = torch.split( # noqa:E501
1265
+ multimodal_embeddings_cat, [main_dim, multi_dim],
1266
+ dim=-1)
1267
+
1268
+ multimodal_embeddings = torch.split(multimodal_embeddings_main,
1269
+ visual_lens,
1270
+ dim=0)
1271
+ multimodal_embeddings_multiscale = torch.split(
1272
+ multimodal_embeddings_multiscale, visual_lens, dim=0)
1273
+
1274
+ deepstack_input_embeds = inputs_embeds.new_zeros(
1275
+ inputs_embeds.size(0),
1276
+ self.deepstack_num_level * inputs_embeds.size(1))
1277
+
1278
+ deepstack_input_embeds = merge_multimodal_embeddings(
1279
+ input_ids,
1280
+ deepstack_input_embeds,
1281
+ multimodal_embeddings_multiscale,
1282
+ placeholder_token_id=[
1283
+ self.config.image_token_id, self.config.video_token_id
1284
+ ],
1285
+ )
1286
+ deepstack_input_embeds = deepstack_input_embeds.view(
1287
+ inputs_embeds.shape[0], self.deepstack_num_level,
1288
+ visual_dim).contiguous()
1289
+ deepstack_input_embeds = deepstack_input_embeds.permute(
1290
+ 1, 0, 2).contiguous()
1291
+ return deepstack_input_embeds, multimodal_embeddings
1292
+
1293
+ def get_input_embeddings(
1294
+ self,
1295
+ input_ids: torch.Tensor,
1296
+ multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
1297
+ ) -> torch.Tensor:
1298
+ deepstack_input_embeds = None
1299
+ inputs_embeds = self.language_model.get_input_embeddings(input_ids)
1300
+ if multimodal_embeddings is not None and self.use_deepstack:
1301
+ deepstack_input_embeds, multimodal_embeddings = self._compute_deepstack_embeds( # noqa:E501
1302
+ input_ids, inputs_embeds, multimodal_embeddings)
1303
+ inputs_embeds = merge_multimodal_embeddings(
1304
+ input_ids, inputs_embeds, multimodal_embeddings,
1305
+ [self.config.image_token_id, self.config.video_token_id])
1306
+
1307
+ if self.use_deepstack:
1308
+ if deepstack_input_embeds is None:
1309
+ deepstack_input_embeds = torch.zeros_like(
1310
+ inputs_embeds).unsqueeze(0).repeat(
1311
+ self.deepstack_num_level, 1, 1).contiguous()
1312
+ self._set_deepstack_input_embeds(deepstack_input_embeds)
1313
+
1314
+ return inputs_embeds
1315
+
1316
+ def get_input_embeddings_v0(
1317
+ self,
1318
+ input_ids: torch.Tensor,
1319
+ image_input: Optional[Qwen2_5_VLImageInputs] = None,
1320
+ video_input: Optional[Qwen2_5_VLVideoInputs] = None,
1321
+ ) -> torch.Tensor:
1322
+ inputs_embeds = self.get_input_embeddings(input_ids)
1323
+
1324
+ if self.use_deepstack:
1325
+ visual_dim = inputs_embeds.shape[-1]
1326
+ deepstack_input_embeds = None
1327
+ if image_input is not None or video_input is not None:
1328
+ deepstack_input_embeds = torch.zeros_like(
1329
+ inputs_embeds).unsqueeze(1).repeat(
1330
+ 1, self.deepstack_num_level, 1).flatten(1)
1331
+
1332
+ if image_input is not None:
1333
+ image_embeds = self._process_image_input(image_input)
1334
+ if self.use_deepstack:
1335
+ image_embeds = torch.cat(image_embeds)
1336
+
1337
+ image_embeds, image_embeds_multiscale = image_embeds.split(
1338
+ [visual_dim, visual_dim * self.deepstack_num_level],
1339
+ dim=-1)
1340
+
1341
+ deepstack_input_embeds = merge_multimodal_embeddings(
1342
+ input_ids,
1343
+ deepstack_input_embeds,
1344
+ image_embeds_multiscale,
1345
+ placeholder_token_id=self.config.image_token_id,
1346
+ )
1347
+
1348
+ inputs_embeds = merge_multimodal_embeddings(
1349
+ input_ids,
1350
+ inputs_embeds,
1351
+ image_embeds,
1352
+ placeholder_token_id=self.config.image_token_id,
1353
+ )
1354
+
1355
+ if video_input is not None:
1356
+ video_embeds = self._process_video_input(video_input)
1357
+ if self.use_deepstack:
1358
+ video_embeds = torch.cat(video_embeds)
1359
+
1360
+ video_embeds, video_embeds_multiscale = video_embeds.split(
1361
+ [visual_dim, visual_dim * self.deepstack_num_level],
1362
+ dim=-1)
1363
+
1364
+ deepstack_input_embeds = merge_multimodal_embeddings(
1365
+ input_ids,
1366
+ deepstack_input_embeds,
1367
+ video_embeds_multiscale,
1368
+ placeholder_token_id=self.config.video_token_id,
1369
+ )
1370
+
1371
+ inputs_embeds = merge_multimodal_embeddings(
1372
+ input_ids,
1373
+ inputs_embeds,
1374
+ video_embeds,
1375
+ placeholder_token_id=self.config.video_token_id,
1376
+ )
1377
+
1378
+ if self.use_deepstack and deepstack_input_embeds is not None:
1379
+ deepstack_input_embeds = deepstack_input_embeds.view(
1380
+ inputs_embeds.shape[0], self.deepstack_num_level,
1381
+ visual_dim).permute(1, 0, 2).contiguous()
1382
+ self._set_deepstack_input_embeds(deepstack_input_embeds)
1383
+ return inputs_embeds
1384
+
1385
+ def forward(
1386
+ self,
1387
+ input_ids: torch.Tensor,
1388
+ positions: torch.Tensor,
1389
+ intermediate_tensors: Optional[IntermediateTensors] = None,
1390
+ inputs_embeds: Optional[torch.Tensor] = None,
1391
+ **kwargs: object,
1392
+ ) -> Union[torch.Tensor, IntermediateTensors]:
1393
+ """Run forward pass for Qwen3VL.
1394
+
1395
+ Args:
1396
+ input_ids: Flattened (concatenated) input_ids corresponding to a
1397
+ batch.
1398
+ positions: Flattened (concatenated) position ids corresponding to a
1399
+ batch.
1400
+ **NOTE**: If mrope is enabled (default setting for Qwen3VL
1401
+ opensource models), the shape will be `(3, seq_len)`,
1402
+ otherwise it will be `(seq_len,).
1403
+ pixel_values: Pixel values to be fed to a model.
1404
+ `None` if no images are passed.
1405
+ image_grid_thw: Tensor `(n_images, 3)` of image 3D grid in LLM.
1406
+ `None` if no images are passed.
1407
+ pixel_values_videos: Pixel values of videos to be fed to a model.
1408
+ `None` if no videos are passed.
1409
+ video_grid_thw: Tensor `(n_videos, 3)` of video 3D grid in LLM.
1410
+ `None` if no videos are passed.
1411
+ """
1412
+
1413
+ if intermediate_tensors is not None:
1414
+ inputs_embeds = None
1415
+
1416
+ # NOTE: In v1, inputs_embeds is always generated at model runner from
1417
+ # `get_multimodal_embeddings` and `get_input_embeddings`, this
1418
+ # condition is only for v0 compatibility.
1419
+ elif inputs_embeds is None:
1420
+ image_input = self._parse_and_validate_image_input(**kwargs)
1421
+ video_input = self._parse_and_validate_video_input(**kwargs)
1422
+
1423
+ if image_input is None and video_input is None:
1424
+ inputs_embeds = None
1425
+ else:
1426
+ if uses_mrope(self.config):
1427
+ assert positions.ndim == 2 and positions.size(0) == 3, (
1428
+ "multimodal section rotary embedding requires "
1429
+ f"(3, seq_len) positions, but got {positions.size()}")
1430
+ inputs_embeds = self.get_input_embeddings_v0(
1431
+ input_ids,
1432
+ image_input=image_input,
1433
+ video_input=video_input)
1434
+ input_ids = None
1435
+
1436
+ if self.use_deepstack and inputs_embeds is not None and get_pp_group(
1437
+ ).is_first_rank:
1438
+ deepstack_input_embeds = self._get_deepstack_input_embeds(
1439
+ inputs_embeds.size(0))
1440
+ else:
1441
+ deepstack_input_embeds = None
1442
+
1443
+ hidden_states = self.language_model.model(
1444
+ input_ids=input_ids,
1445
+ positions=positions,
1446
+ intermediate_tensors=intermediate_tensors,
1447
+ inputs_embeds=inputs_embeds,
1448
+ # args for deepstack
1449
+ deepstack_input_embeds=deepstack_input_embeds,
1450
+ )
1451
+
1452
+ if inputs_embeds is not None and get_pp_group().is_first_rank:
1453
+ self._clear_deepstack_input_embeds(inputs_embeds.size(0))
1454
+
1455
+ return hidden_states
1456
+
1457
+ def compute_logits(
1458
+ self,
1459
+ hidden_states: torch.Tensor,
1460
+ sampling_metadata: SamplingMetadata,
1461
+ ) -> Optional[torch.Tensor]:
1462
+ return self.language_model.compute_logits(hidden_states,
1463
+ sampling_metadata)
1464
+
1465
+ def load_weights(self, weights: Iterable[tuple[str,
1466
+ torch.Tensor]]) -> set[str]:
1467
+ loader = AutoWeightsLoader(self)
1468
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1469
+
1470
+ def get_mm_mapping(self) -> MultiModelKeys:
1471
+ """
1472
+ Get the module prefix in multimodal models
1473
+ """
1474
+ return MultiModelKeys.from_string_field(
1475
+ language_model="language_model",
1476
+ connector="model.visual.merger",
1477
+ tower_model="model.visual.",
1478
+ )
vllm/model_executor/models/qwen3_vl_moe.py
new
+344
@@ -0,0 +1,344 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+# Copyright 2025 The vLLM team.
5
+# Copyright 2025 The Qwen Team.
6
+# Copyright 2025 The HuggingFace Inc. team.
7
+# All rights reserved.
8
+#
9
+# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
10
+# and OPT implementations in this library. It has been modified from its
11
+# original forms to accommodate minor architectural differences compared
12
+# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
13
+#
14
+# Licensed under the Apache License, Version 2.0 (the "License");
15
+# you may not use this file except in compliance with the License.
16
+# You may obtain a copy of the License at
17
+#
18
+# http://www.apache.org/licenses/LICENSE-2.0
19
+#
20
+# Unless required by applicable law or agreed to in writing, software
21
+# distributed under the License is distributed on an "AS IS" BASIS,
22
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
+# See the License for the specific language governing permissions and
24
+# limitations under the License.
25
+"""Inference-only Qwen3-VL-MoE model compatible with HuggingFace weights."""
26
+import typing
27
+from collections.abc import Iterable
28
+from typing import Callable, Optional, Union
29
+
30
+import torch
31
+from transformers.models.qwen3_vl_moe.configuration_qwen3_vl_moe import (
32
+ Qwen3VLMoeConfig)
33
+
34
+from vllm.compilation.decorators import support_torch_compile
35
+from vllm.config import VllmConfig
36
+from vllm.distributed import get_pp_group
37
+from vllm.logger import init_logger
38
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
39
+from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
40
+from vllm.model_executor.model_loader.weight_utils import (
41
+ default_weight_loader, maybe_remap_kv_scale_name)
42
+from vllm.multimodal import MULTIMODAL_REGISTRY
43
+from vllm.sequence import IntermediateTensors
44
+
45
+from .qwen3_moe import Qwen3MoeForCausalLM, Qwen3MoeModel
46
+from .qwen3_vl import (Qwen3_VisionTransformer, Qwen3VLDummyInputsBuilder,
47
+ Qwen3VLForConditionalGeneration,
48
+ Qwen3VLMultiModalProcessor, Qwen3VLProcessingInfo)
49
+from .utils import is_pp_missing_parameter, maybe_prefix
50
+
51
+logger = init_logger(__name__)
52
+
53
+
54
+class Qwen3VLMoeProcessingInfo(Qwen3VLProcessingInfo):
55
+
56
+ def get_hf_config(self):
57
+ return self.ctx.get_hf_config(Qwen3VLMoeConfig)
58
+
59
+
60
+@support_torch_compile(
61
+ dynamic_arg_dims={
62
+ "input_ids": 0,
63
+ # positions is of shape (3, seq_len) if mrope is enabled for qwen2-vl,
64
+ # otherwise (seq_len, ).
65
+ "positions": -1,
66
+ "intermediate_tensors": 0,
67
+ "inputs_embeds": 0,
68
+ # the same shape as input_embeds
69
+ "deepstack_input_embeds": 0
70
+ })
71
+class Qwen3MoeLLMModel(Qwen3MoeModel):
72
+
73
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
74
+ super().__init__(vllm_config=vllm_config, prefix=prefix)
75
+ if not get_pp_group().is_first_rank:
76
+ assert self.start_layer >= len(
77
+ vllm_config.model_config.hf_config.vision_config.
78
+ deepstack_visual_indexes), (
79
+ "start_layer should be greater than or equal to "
80
+ "len(deepstack_visual_indexes)")
81
+
82
+ def forward(
83
+ self,
84
+ input_ids: torch.Tensor,
85
+ positions: torch.Tensor,
86
+ intermediate_tensors: Optional[IntermediateTensors] = None,
87
+ inputs_embeds: Optional[torch.Tensor] = None,
88
+ deepstack_input_embeds: Optional[IntermediateTensors] = None,
89
+ ) -> Union[torch.Tensor, IntermediateTensors]:
90
+ if get_pp_group().is_first_rank:
91
+ if inputs_embeds is not None:
92
+ hidden_states = inputs_embeds
93
+ else:
94
+ hidden_states = self.get_input_embeddings(input_ids)
95
+ residual = None
96
+ else:
97
+ assert intermediate_tensors is not None
98
+ hidden_states = intermediate_tensors["hidden_states"]
99
+ residual = intermediate_tensors["residual"]
100
+ for layer_idx, layer in enumerate(
101
+ self.layers[self.start_layer:self.end_layer]):
102
+ layer_idx = layer_idx + self.start_layer
103
+
104
+ hidden_states, residual = layer(
105
+ positions,
106
+ hidden_states,
107
+ residual,
108
+ )
109
+
110
+ if deepstack_input_embeds is not None and \
111
+ layer_idx in range(0, len(deepstack_input_embeds)):
112
+ hidden_states = hidden_states + deepstack_input_embeds[
113
+ f"deepstack_input_embeds_{layer_idx}"]
114
+
115
+ if not get_pp_group().is_last_rank:
116
+ return IntermediateTensors({
117
+ "hidden_states": hidden_states,
118
+ "residual": residual
119
+ })
120
+ hidden_states, _ = self.norm(hidden_states, residual)
121
+ return hidden_states
122
+
123
+ def load_fused_expert_weights(self, name: str, params_dict: dict,
124
+ loaded_weight: torch.Tensor, shard_id: str,
125
+ num_experts: int):
126
+ param = params_dict[name]
127
+ weight_loader = typing.cast(Callable[..., bool], param.weight_loader)
128
+ for expert_id in range(num_experts):
129
+ curr_expert_weight = loaded_weight[expert_id]
130
+ success = weight_loader(param,
131
+ curr_expert_weight,
132
+ name,
133
+ shard_id,
134
+ expert_id,
135
+ return_success=True)
136
+ if not success:
137
+ return False
138
+ return True
139
+
140
+ def load_weights(self, weights: Iterable[tuple[str,
141
+ torch.Tensor]]) -> set[str]:
142
+ stacked_params_mapping = [
143
+ # (param_name, shard_name, shard_id)
144
+ ("qkv_proj", "q_proj", "q"),
145
+ ("qkv_proj", "k_proj", "k"),
146
+ ("qkv_proj", "v_proj", "v"),
147
+ ("gate_up_proj", "gate_proj", 0),
148
+ ("gate_up_proj", "up_proj", 1),
149
+ ]
150
+ # Skip loading extra parameters for GPTQ/modelopt models.
151
+ ignore_suffixes = (".bias", "_bias", ".k_scale", "_k_scale",
152
+ ".v_scale", "_v_scale", ".weight_scale",
153
+ "_weight_scale", ".input_scale", "_input_scale")
154
+ params_dict = dict(self.named_parameters())
155
+ loaded_params: set[str] = set()
156
+ expert_params_mapping = self.get_expert_mapping()
157
+ is_fused_expert = False
158
+ fused_expert_params_mapping = [
159
+ ("experts.w13_weight", "experts.gate_up_proj", 0, "w1"),
160
+ ("experts.w2_weight", "experts.down_proj", 0, "w2"),
161
+ ]
162
+ num_experts = self.config.num_experts
163
+ for name, loaded_weight in weights:
164
+ for (param_name, weight_name, shard_id) in stacked_params_mapping:
165
+ if ("experts.gate_up_proj" in name
166
+ or "experts.down_proj" in name):
167
+ is_fused_expert = True
168
+ expert_params_mapping = fused_expert_params_mapping
169
+
170
+ # Skip non-stacked layers and experts (experts handled below).
171
+ if weight_name not in name:
172
+ continue
173
+ # We have mlp.experts[0].gate_proj in the checkpoint.
174
+ # Since we handle the experts below in expert_params_mapping,
175
+ # we need to skip here BEFORE we update the name, otherwise
176
+ # name will be updated to mlp.experts[0].gate_up_proj, which
177
+ # will then be updated below in expert_params_mapping
178
+ # for mlp.experts[0].gate_gate_up_proj, which breaks load.
179
+ if "mlp.experts" in name:
180
+ continue
181
+ name = name.replace(weight_name, param_name)
182
+ # Skip loading extra parameters for GPTQ/modelopt models.
183
+ if name.endswith(ignore_suffixes) and name not in params_dict:
184
+ continue
185
+ # Skip layers on other devices.
186
+ if is_pp_missing_parameter(name, self):
187
+ continue
188
+ if name.endswith("scale"):
189
+ # Remapping the name of FP8 kv-scale.
190
+ name = maybe_remap_kv_scale_name(name, params_dict)
191
+ if name is None:
192
+ continue
193
+ if name not in params_dict:
194
+ continue
195
+ param = params_dict[name]
196
+ weight_loader = getattr(param, "weight_loader",
197
+ default_weight_loader)
198
+ if weight_loader == default_weight_loader:
199
+ weight_loader(param, loaded_weight)
200
+ else:
201
+ weight_loader(param, loaded_weight, shard_id)
202
+ break
203
+ else:
204
+ is_expert_weight = False
205
+ for mapping in expert_params_mapping:
206
+ param_name, weight_name, expert_id, shard_id = mapping
207
+ if weight_name not in name:
208
+ continue
209
+ # Anyway, this is an expert weight and should not be
210
+ # attempted to load as other weights later
211
+ is_expert_weight = True
212
+ name_mapped = name.replace(weight_name, param_name)
213
+ if is_fused_expert:
214
+ loaded_weight = loaded_weight.transpose(-1,
215
+ -2) # no bias
216
+ if "experts.gate_up_proj" in name:
217
+ loaded_weight = loaded_weight.chunk(2, dim=-2)
218
+ success_w1 = self.load_fused_expert_weights(
219
+ name_mapped, params_dict, loaded_weight[0],
220
+ "w1", num_experts)
221
+ success_w3 = self.load_fused_expert_weights(
222
+ name_mapped, params_dict, loaded_weight[1],
223
+ "w3", num_experts)
224
+ success = success_w1 and success_w3
225
+ else:
226
+ # down_proj
227
+ success = self.load_fused_expert_weights(
228
+ name_mapped, params_dict, loaded_weight,
229
+ shard_id, num_experts)
230
+ else:
231
+ if is_pp_missing_parameter(name_mapped, self):
232
+ continue
233
+ # Skip loading extra parameters for GPTQ/modelopt models
234
+ if name_mapped.endswith(
235
+ ignore_suffixes
236
+ ) and name_mapped not in params_dict:
237
+ continue
238
+ param = params_dict[name_mapped]
239
+ # We should ask the weight loader to return success or
240
+ # not here since otherwise we may skip experts with
241
+ # other available replicas.
242
+ weight_loader = typing.cast(Callable[..., bool],
243
+ param.weight_loader)
244
+ success = weight_loader(param,
245
+ loaded_weight,
246
+ name_mapped,
247
+ shard_id=shard_id,
248
+ expert_id=expert_id,
249
+ return_success=True)
250
+ if success:
251
+ name = name_mapped
252
+ break
253
+ else:
254
+ if is_expert_weight:
255
+ # We've checked that this is an expert weight
256
+ # However it's not mapped locally to this rank
257
+ # So we simply skip it
258
+ continue
259
+ # Skip loading extra parameters for GPTQ/modelopt models.
260
+ if name.endswith(
261
+ ignore_suffixes) and name not in params_dict:
262
+ continue
263
+ # Skip layers on other devices.
264
+ if is_pp_missing_parameter(name, self):
265
+ continue
266
+ # Remapping the name of FP8 kv-scale.
267
+ if name.endswith("kv_scale"):
268
+ remapped_kv_scale_name = name.replace(
269
+ ".kv_scale", ".attn.kv_scale")
270
+ if remapped_kv_scale_name not in params_dict:
271
+ logger.warning_once(
272
+ "Found kv scale in the checkpoint (e.g. %s), but not found the expected name in the model (e.g. %s). kv-scale is not loaded.", # noqa: E501
273
+ name,
274
+ remapped_kv_scale_name,
275
+ )
276
+ continue
277
+ else:
278
+ name = remapped_kv_scale_name
279
+ param = params_dict[name]
280
+ weight_loader = getattr(param, "weight_loader",
281
+ default_weight_loader)
282
+ weight_loader(param, loaded_weight)
283
+ loaded_params.add(name)
284
+ return loaded_params
285
+
286
+
287
+class Qwen3MoeLLMForCausalLM(Qwen3MoeForCausalLM):
288
+
289
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
290
+ super(Qwen3MoeForCausalLM, self).__init__()
291
+ self.config = vllm_config.model_config.hf_config.text_config
292
+ self.quant_config = vllm_config.quant_config
293
+ self.model = Qwen3MoeLLMModel(vllm_config=vllm_config,
294
+ prefix=maybe_prefix(prefix, "model"))
295
+ self.lm_head = ParallelLMHead(self.config.vocab_size,
296
+ self.config.hidden_size,
297
+ quant_config=self.quant_config)
298
+ if self.config.tie_word_embeddings:
299
+ self.lm_head.weight = self.model.embed_tokens.weight
300
+ self.logits_processor = LogitsProcessor(self.config.vocab_size)
301
+ self.make_empty_intermediate_tensors = (
302
+ self.model.make_empty_intermediate_tensors)
303
+
304
+
305
+@MULTIMODAL_REGISTRY.register_processor(Qwen3VLMultiModalProcessor,
306
+ info=Qwen3VLMoeProcessingInfo,
307
+ dummy_inputs=Qwen3VLDummyInputsBuilder)
308
+class Qwen3VLMoeForConditionalGeneration(Qwen3VLForConditionalGeneration):
309
+
310
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
311
+ super(Qwen3VLForConditionalGeneration, self).__init__()
312
+ config: Qwen3VLMoeConfig = vllm_config.model_config.hf_config
313
+ quant_config = vllm_config.quant_config
314
+ multimodal_config = vllm_config.model_config.multimodal_config
315
+
316
+ self.config = config
317
+ self.multimodal_config = multimodal_config
318
+
319
+ self.visual = Qwen3_VisionTransformer(
320
+ config.vision_config,
321
+ norm_eps=getattr(config, "rms_norm_eps", 1e-6),
322
+ quant_config=self._maybe_ignore_quant_config(quant_config),
323
+ prefix=maybe_prefix(prefix, "visual"),
324
+ )
325
+
326
+ self.language_model = Qwen3MoeLLMForCausalLM(vllm_config=vllm_config,
327
+ prefix=maybe_prefix(
328
+ prefix,
329
+ "language_model"))
330
+
331
+ self.make_empty_intermediate_tensors = (
332
+ self.language_model.make_empty_intermediate_tensors)
333
+
334
+ self.use_deepstack = hasattr(config.vision_config,
335
+ 'deepstack_visual_indexes')
336
+ self.deepstack_num_level = len(
337
+ config.vision_config.deepstack_visual_indexes
338
+ ) if self.use_deepstack else 0
339
+ # register buffer for deepstack
340
+ self.deepstack_input_embeds = [
341
+ torch.zeros(vllm_config.scheduler_config.max_num_batched_tokens,
342
+ config.text_config.hidden_size)
343
+ for _ in range(self.deepstack_num_level)
344
+ ] if self.use_deepstack else None
vllm/model_executor/models/registry.py
+3
-1
@@ -259,11 +259,13 @@ _MULTIMODAL_MODELS = {
259
"Qwen2AudioForConditionalGeneration": ("qwen2_audio", "Qwen2AudioForConditionalGeneration"), # noqa: E501
260
"Qwen2_5OmniModel": ("qwen2_5_omni_thinker", "Qwen2_5OmniThinkerForConditionalGeneration"), # noqa: E501
261
"Qwen2_5OmniForConditionalGeneration": ("qwen2_5_omni_thinker", "Qwen2_5OmniThinkerForConditionalGeneration"), # noqa: E501
262
- "UltravoxModel": ("ultravox", "UltravoxModel"),
262
+ "Qwen3VLForConditionalGeneration": ("qwen3_vl", "Qwen3VLForConditionalGeneration"), # noqa: E501
263
+ "Qwen3VLMoeForConditionalGeneration": ("qwen3_vl_moe", "Qwen3VLMoeForConditionalGeneration"), # noqa: E501
264
"SkyworkR1VChatModel": ("skyworkr1v", "SkyworkR1VChatModel"),
265
"Step3VLForConditionalGeneration": ("step3_vl", "Step3VLForConditionalGeneration"), # noqa: E501
266
"TarsierForConditionalGeneration": ("tarsier", "TarsierForConditionalGeneration"), # noqa: E501
267
"Tarsier2ForConditionalGeneration": ("qwen2_vl", "Tarsier2ForConditionalGeneration"), # noqa: E501
268
+ "UltravoxModel": ("ultravox", "UltravoxModel"),
269
"VoxtralForConditionalGeneration": ("voxtral", "VoxtralForConditionalGeneration"), # noqa: E501
270
# [Encoder-decoder]
271
"WhisperForConditionalGeneration": ("whisper", "WhisperForConditionalGeneration"), # noqa: E501
vllm/multimodal/video.py
+1
-1
@@ -156,7 +156,7 @@ class OpenCVVideoBackend(VideoLoader):
156
# can cause incorrect timestamp calculation without num_frames=-1.
157
metadata = {
158
"total_num_frames": num_frames,
159
- "fps": original_fps,
159
+ "fps": num_frames / duration,
160
"duration": duration,
161
"video_backend": "opencv",
162
"frames_indices": list(range(num_frames)),