@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 19a9b169bf
Fork 0

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

Add Qwen3-Omni moe thinker (#25550)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk> Signed-off-by: Roger Wang <hey@rogerw.io> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Xiong Wang <feizi.wx@alibaba-inc.com> Co-authored-by: DarkLight1337 <tlleungac@connect.ust.hk> Co-authored-by: Roger Wang <hey@rogerw.io> Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>

Xiong Wang committed Oct 11, 2025 at 01:00 UTC 19a9b169bf1b58b4311f5f3bfa37550328741506
6 files changed +1795 -36
docs/models/supported_models.md
+2 -2
@@ -714,6 +714,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
714 | `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` | ✅︎ | ✅︎ | ✅︎ |
715 | `Qwen3VLForConditionalGeneration` | Qwen3-VL | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/Qwen3-VL-4B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
716 | `Qwen3VLMoeForConditionalGeneration` | Qwen3-VL-MOE | T + I<sup>E+</sup> + V<sup>E+</sup> | `Qwen/Qwen3-VL-30B-A3B-Instruct`, etc. | ✅︎ | ✅︎ | ✅︎ |
717 +| `Qwen3OmniMoeThinkerForConditionalGeneration` | Qwen3-Omni | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>+</sup> | `Qwen/Qwen3-Omni-30B-A3B-Instruct`, `Qwen/Qwen3-Omni-30B-A3B-Thinking` | ✅︎ | ✅︎ | ✅︎ |
718 | `RForConditionalGeneration` | R-VL-4B | T + I<sup>E+</sup> | `YannQi/R-4B` | | ✅︎ | ✅︎ |
719 | `SkyworkR1VChatModel` | Skywork-R1V-38B | T + I | `Skywork/Skywork-R1V-38B` | | ✅︎ | ✅︎ |
720 | `SmolVLMForConditionalGeneration` | SmolVLM2 | T + I | `SmolVLM2-2.2B-Instruct` | ✅︎ | | ✅︎ |
@@ -804,8 +805,7 @@ Some models are supported only via the [Transformers backend](#transformers). Th
805 Our PaliGemma implementations have the same problem as Gemma 3 (see above) for both V0 and V1.
806
807 !!! note
807 - For Qwen2.5-Omni, reading audio from video pre-processing (`--mm-processor-kwargs '{"use_audio_in_video": true}'`)
808 - is currently supported on V0 (but not V1), because overlapping modalities is not yet supported in V1.
808 + For Qwen2.5-Omni and Qwen3-Omni, reading audio from video pre-processing (`--mm-processor-kwargs '{"use_audio_in_video": true}'`) is currently work in progress and not yet supported.
809
810 #### Transcription
811
tests/models/multimodal/processing/test_common.py
+1
@@ -384,6 +384,7 @@ def _test_processing_correctness_one(
384 "Qwen/Qwen2.5-Omni-3B",
385 "Qwen/Qwen3-VL-4B-Instruct",
386 "Qwen/Qwen3-VL-30B-A3B-Instruct",
387 + "Qwen/Qwen3-Omni-30B-A3B-Instruct",
388 "YannQi/R-4B",
389 "Skywork/Skywork-R1V-38B",
390 "HuggingFaceTB/SmolVLM2-2.2B-Instruct",
tests/models/registry.py
+5
@@ -773,6 +773,11 @@ _MULTIMODAL_EXAMPLE_MODELS = {
773 min_transformers_version="4.57",
774 is_available_online=False,
775 ),
776 + "Qwen3OmniMoeForConditionalGeneration": _HfExamplesInfo(
777 + "Qwen/Qwen3-Omni-30B-A3B-Instruct",
778 + max_model_len=4096,
779 + min_transformers_version="4.57",
780 + ),
781 "RForConditionalGeneration": _HfExamplesInfo("YannQi/R-4B", trust_remote_code=True),
782 "SkyworkR1VChatModel": _HfExamplesInfo(
783 "Skywork/Skywork-R1V-38B", trust_remote_code=True
vllm/model_executor/layers/rotary_embedding/mrope.py
+374 -34
@@ -971,17 +971,9 @@ class MRotaryEmbedding(RotaryEmbedding):
971 llm_pos_ids_list.append(
972 torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx
973 )
974 -
974 t_index = (
976 - (
977 - torch.arange(llm_grid_t)
978 - .view(-1, 1)
979 - .expand(-1, llm_grid_h * llm_grid_w)
980 - )
981 - .long()
982 - .flatten()
983 - )
984 -
975 + torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w)
976 + ).flatten()
977 h_index = (
978 torch.arange(llm_grid_h)
979 .view(1, -1, 1)
@@ -1042,7 +1034,6 @@ class MRotaryEmbedding(RotaryEmbedding):
1034
1035 st = 0
1036 remain_images, remain_videos = image_nums, video_nums
1045 -
1037 image_index, video_index = 0, 0
1038 for _ in range(image_nums + video_nums):
1039 video_second_per_grid_t = 0.0
@@ -1093,19 +1084,11 @@ class MRotaryEmbedding(RotaryEmbedding):
1084 llm_pos_ids_list.append(
1085 torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx
1086 )
1096 -
1087 t_index = (
1098 - (
1099 - torch.arange(llm_grid_t)
1100 - .view(-1, 1)
1101 - .expand(-1, llm_grid_h * llm_grid_w)
1102 - * video_second_per_grid_t
1103 - * tokens_per_second
1104 - )
1105 - .long()
1106 - .flatten()
1107 - )
1108 -
1088 + torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w)
1089 + * video_second_per_grid_t
1090 + * tokens_per_second
1091 + ).flatten()
1092 h_index = (
1093 torch.arange(llm_grid_h)
1094 .view(1, -1, 1)
@@ -1136,6 +1119,339 @@ class MRotaryEmbedding(RotaryEmbedding):
1119
1120 return llm_positions, mrope_position_delta
1121
1122 + @classmethod
1123 + def _omni3_get_input_positions_tensor(
1124 + cls,
1125 + config,
1126 + input_ids: torch.Tensor,
1127 + image_grid_thw: torch.Tensor,
1128 + video_grid_thw: torch.Tensor,
1129 + use_audio_in_video: bool = False,
1130 + audio_seqlens: Optional[torch.Tensor] = None,
1131 + second_per_grids: Optional[torch.Tensor] = None,
1132 + ) -> tuple[torch.Tensor, torch.Tensor]:
1133 + def _get_feat_extract_output_lengths(input_lengths: torch.LongTensor):
1134 + input_lengths_leave = input_lengths % 100
1135 + feat_lengths = (input_lengths_leave - 1) // 2 + 1
1136 + output_lengths = (
1137 + ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
1138 + )
1139 + return output_lengths
1140 +
1141 + if input_ids is None or input_ids.ndim != 1:
1142 + raise ValueError("_omni3_get_input_positions_tensor expects 1D input_ids")
1143 +
1144 + seq_len = input_ids.shape[0]
1145 + device = input_ids.device
1146 + dtype = input_ids.dtype
1147 +
1148 + if image_grid_thw is not None:
1149 + image_grid_thw = image_grid_thw.to(device=device, dtype=torch.long)
1150 + if video_grid_thw is not None:
1151 + video_grid_thw = video_grid_thw.to(device=device, dtype=torch.long)
1152 +
1153 + if second_per_grids is None:
1154 + if video_grid_thw is not None and video_grid_thw.numel() > 0:
1155 + second_per_grids = torch.ones(
1156 + video_grid_thw.shape[0], dtype=torch.float32, device=device
1157 + )
1158 + else:
1159 + second_per_grids = torch.tensor([], dtype=torch.float32, device=device)
1160 + else:
1161 + second_per_grids = second_per_grids.to(device=device, dtype=torch.float32)
1162 +
1163 + if audio_seqlens is not None:
1164 + audio_seqlens = audio_seqlens.to(device=device, dtype=torch.long)
1165 +
1166 + spatial_merge_size = config.vision_config.spatial_merge_size
1167 + image_token_id = config.image_token_id
1168 + video_token_id = config.video_token_id
1169 + audio_token_id = config.audio_token_id
1170 + vision_start_token_id = config.vision_start_token_id
1171 + audio_start_token_id = config.audio_start_token_id
1172 + position_id_per_seconds = config.position_id_per_seconds
1173 +
1174 + vision_start_indices = torch.argwhere(
1175 + input_ids == vision_start_token_id
1176 + ).squeeze(1)
1177 + if vision_start_indices.numel() > 0:
1178 + vision_tokens = input_ids[vision_start_indices + 1]
1179 + else:
1180 + vision_tokens = input_ids.new_empty((0,), dtype=input_ids.dtype)
1181 + audio_nums = torch.sum(input_ids == audio_start_token_id)
1182 + image_nums = (vision_tokens == image_token_id).sum()
1183 + video_nums = (
1184 + (vision_tokens == audio_start_token_id).sum()
1185 + if use_audio_in_video
1186 + else (vision_tokens == video_token_id).sum()
1187 + )
1188 +
1189 + input_tokens = input_ids.tolist()
1190 + llm_pos_ids_list: list[torch.Tensor] = []
1191 + st = 0
1192 + image_idx = 0
1193 + video_idx = 0
1194 + audio_idx = 0
1195 + remain_images, remain_videos, remain_audios = image_nums, video_nums, audio_nums # noqa: E501
1196 + multimodal_nums = (
1197 + image_nums + audio_nums
1198 + if use_audio_in_video
1199 + else image_nums + video_nums + audio_nums
1200 + ) # noqa: E501
1201 +
1202 + for _ in range(multimodal_nums):
1203 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1204 + if (image_token_id in input_tokens or video_token_id in input_tokens) and (
1205 + remain_videos > 0 or remain_images > 0
1206 + ):
1207 + ed_vision_start = input_tokens.index(vision_start_token_id, st)
1208 + else:
1209 + ed_vision_start = len(input_tokens) + 1
1210 + if audio_token_id in input_tokens and remain_audios > 0:
1211 + ed_audio_start = input_tokens.index(audio_start_token_id, st)
1212 + else:
1213 + ed_audio_start = len(input_tokens) + 1
1214 + min_ed = min(ed_vision_start, ed_audio_start)
1215 +
1216 + if min_ed == ed_audio_start:
1217 + text_len = min_ed - st
1218 + if text_len != 0:
1219 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1220 + llm_pos_ids_list.append(
1221 + torch.arange(text_len, device=device, dtype=torch.long)
1222 + .view(1, -1)
1223 + .expand(3, -1)
1224 + + st_idx
1225 + )
1226 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1227 + bos_len = 1
1228 + llm_pos_ids_list.append(
1229 + torch.arange(bos_len, device=device, dtype=torch.long)
1230 + .view(1, -1)
1231 + .expand(3, -1)
1232 + + st_idx
1233 + )
1234 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1235 + audio_len = _get_feat_extract_output_lengths(audio_seqlens[audio_idx])
1236 + llm_pos_ids = (
1237 + torch.arange(audio_len, device=device, dtype=torch.long)
1238 + .view(1, -1)
1239 + .expand(3, -1)
1240 + + st_idx
1241 + )
1242 + llm_pos_ids_list.append(llm_pos_ids)
1243 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1244 + eos_len = 1
1245 + llm_pos_ids_list.append(
1246 + torch.arange(eos_len, device=device, dtype=torch.long)
1247 + .view(1, -1)
1248 + .expand(3, -1)
1249 + + st_idx
1250 + )
1251 + st += text_len + bos_len + audio_len + eos_len
1252 + audio_idx += 1
1253 + remain_audios -= 1
1254 + elif (
1255 + min_ed == ed_vision_start
1256 + and input_ids[ed_vision_start + 1] == image_token_id
1257 + ):
1258 + text_len = min_ed - st
1259 + if text_len != 0:
1260 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1261 + llm_pos_ids_list.append(
1262 + torch.arange(text_len, device=device, dtype=torch.long)
1263 + .view(1, -1)
1264 + .expand(3, -1)
1265 + + st_idx
1266 + )
1267 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1268 + bos_len = 1
1269 + llm_pos_ids_list.append(
1270 + torch.arange(bos_len, device=device, dtype=torch.long)
1271 + .view(1, -1)
1272 + .expand(3, -1)
1273 + + st_idx
1274 + )
1275 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1276 + grid_t = image_grid_thw[image_idx][0]
1277 + grid_hs = image_grid_thw[:, 1]
1278 + grid_ws = image_grid_thw[:, 2]
1279 + t_index = torch.arange(grid_t, device=device) * position_id_per_seconds
1280 + llm_pos_ids = cls._get_llm_pos_ids_for_vision(
1281 + st_idx, image_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1282 + )
1283 + image_len = image_grid_thw[image_idx].prod() // (spatial_merge_size**2)
1284 + llm_pos_ids_list.append(llm_pos_ids)
1285 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1286 + eos_len = 1
1287 + llm_pos_ids_list.append(
1288 + torch.arange(eos_len, device=device, dtype=torch.long)
1289 + .view(1, -1)
1290 + .expand(3, -1)
1291 + + st_idx
1292 + )
1293 + st += text_len + bos_len + image_len + eos_len
1294 + image_idx += 1
1295 + remain_images -= 1
1296 + elif (
1297 + min_ed == ed_vision_start
1298 + and input_ids[ed_vision_start + 1] == video_token_id
1299 + and not use_audio_in_video
1300 + ):
1301 + text_len = min_ed - st
1302 + if text_len != 0:
1303 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1304 + llm_pos_ids_list.append(
1305 + torch.arange(text_len, device=device, dtype=torch.long)
1306 + .view(1, -1)
1307 + .expand(3, -1)
1308 + + st_idx
1309 + )
1310 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1311 + bos_len = 1
1312 + llm_pos_ids_list.append(
1313 + torch.arange(bos_len, device=device, dtype=torch.long)
1314 + .view(1, -1)
1315 + .expand(3, -1)
1316 + + st_idx
1317 + )
1318 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1319 + grid_t = video_grid_thw[video_idx][0]
1320 + grid_hs = video_grid_thw[:, 1]
1321 + grid_ws = video_grid_thw[:, 2]
1322 + t_index = (
1323 + torch.arange(grid_t, device=device)
1324 + * float(second_per_grids[video_idx].item())
1325 + * position_id_per_seconds
1326 + )
1327 + llm_pos_ids = cls._get_llm_pos_ids_for_vision(
1328 + st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1329 + )
1330 + video_len = video_grid_thw[video_idx].prod() // (spatial_merge_size**2)
1331 + llm_pos_ids_list.append(llm_pos_ids)
1332 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1333 + eos_len = 1
1334 + llm_pos_ids_list.append(
1335 + torch.arange(eos_len, device=device, dtype=torch.long)
1336 + .view(1, -1)
1337 + .expand(3, -1)
1338 + + st_idx
1339 + )
1340 + st += text_len + bos_len + video_len + eos_len
1341 + video_idx += 1
1342 + remain_videos -= 1
1343 + elif (
1344 + min_ed == ed_vision_start
1345 + and ed_vision_start + 1 == ed_audio_start
1346 + and use_audio_in_video
1347 + ):
1348 + text_len = min_ed - st
1349 + if text_len != 0:
1350 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1351 + llm_pos_ids_list.append(
1352 + torch.arange(text_len, device=device, dtype=torch.long)
1353 + .view(1, -1)
1354 + .expand(3, -1)
1355 + + st_idx
1356 + )
1357 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1358 + bos_len = 1
1359 + bos_block = (
1360 + torch.arange(bos_len, device=device, dtype=torch.long)
1361 + .view(1, -1)
1362 + .expand(3, -1)
1363 + + st_idx
1364 + )
1365 + llm_pos_ids_list.append(bos_block)
1366 + llm_pos_ids_list.append(bos_block)
1367 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1368 + audio_len = _get_feat_extract_output_lengths(audio_seqlens[audio_idx])
1369 + audio_llm_pos_ids = (
1370 + torch.arange(audio_len, device=device, dtype=torch.long)
1371 + .view(1, -1)
1372 + .expand(3, -1)
1373 + + st_idx
1374 + )
1375 + grid_t = video_grid_thw[video_idx][0]
1376 + grid_hs = video_grid_thw[:, 1]
1377 + grid_ws = video_grid_thw[:, 2]
1378 + t_index = (
1379 + torch.arange(grid_t, device=device)
1380 + * float(second_per_grids[video_idx].item())
1381 + * position_id_per_seconds
1382 + )
1383 + video_llm_pos_ids = cls._get_llm_pos_ids_for_vision(
1384 + st_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1385 + )
1386 + video_data_index, audio_data_index = 0, 0
1387 + while (
1388 + video_data_index < video_llm_pos_ids.shape[-1]
1389 + and audio_data_index < audio_llm_pos_ids.shape[-1]
1390 + ):
1391 + if (
1392 + video_llm_pos_ids[0][video_data_index]
1393 + <= audio_llm_pos_ids[0][audio_data_index]
1394 + ):
1395 + llm_pos_ids_list.append(
1396 + video_llm_pos_ids[
1397 + :, video_data_index : video_data_index + 1
1398 + ]
1399 + )
1400 + video_data_index += 1
1401 + else:
1402 + llm_pos_ids_list.append(
1403 + audio_llm_pos_ids[
1404 + :, audio_data_index : audio_data_index + 1
1405 + ]
1406 + )
1407 + audio_data_index += 1
1408 + if video_data_index < video_llm_pos_ids.shape[-1]:
1409 + llm_pos_ids_list.append(
1410 + video_llm_pos_ids[
1411 + :, video_data_index : video_llm_pos_ids.shape[-1]
1412 + ]
1413 + )
1414 + if audio_data_index < audio_llm_pos_ids.shape[-1]:
1415 + llm_pos_ids_list.append(
1416 + audio_llm_pos_ids[
1417 + :, audio_data_index : audio_llm_pos_ids.shape[-1]
1418 + ]
1419 + )
1420 + video_len = video_grid_thw[video_idx].prod() // (spatial_merge_size**2)
1421 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1422 + eos_len = 1
1423 + eos_block = (
1424 + torch.arange(eos_len, device=device, dtype=torch.long)
1425 + .view(1, -1)
1426 + .expand(3, -1)
1427 + + st_idx
1428 + )
1429 + llm_pos_ids_list.append(eos_block)
1430 + llm_pos_ids_list.append(eos_block)
1431 + st += text_len + bos_len * 2 + audio_len + video_len + eos_len * 2 # noqa: E501
1432 + audio_idx += 1
1433 + video_idx += 1
1434 + remain_videos -= 1
1435 + remain_audios -= 1
1436 +
1437 + if st < len(input_tokens):
1438 + st_idx = llm_pos_ids_list[-1].max() + 1 if llm_pos_ids_list else 0
1439 + text_len = len(input_tokens) - st
1440 + llm_pos_ids_list.append(
1441 + torch.arange(text_len, device=device, dtype=torch.long)
1442 + .view(1, -1)
1443 + .expand(3, -1)
1444 + + st_idx
1445 + )
1446 +
1447 + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
1448 + if llm_positions.shape[1] != seq_len:
1449 + raise RuntimeError("Position ids length mismatch with input ids length")
1450 +
1451 + position_ids = llm_positions.to(device=device, dtype=dtype)
1452 + mrope_position_delta = llm_positions.max() + 1 - seq_len
1453 + return position_ids, mrope_position_delta
1454 +
1455 @classmethod
1456 def _omni_get_input_positions_tensor(
1457 cls,
@@ -1168,7 +1484,38 @@ class MRotaryEmbedding(RotaryEmbedding):
1484 # TODO(fyabc): refactor and share more code with
1485 # _vl_get_input_positions_tensor.
1486
1487 + model_type = hf_config.model_type
1488 thinker_config = hf_config.thinker_config
1489 +
1490 + if isinstance(image_grid_thw, list):
1491 + image_grid_thw = torch.tensor(image_grid_thw)
1492 + if isinstance(video_grid_thw, list):
1493 + video_grid_thw = torch.tensor(video_grid_thw)
1494 +
1495 + if "qwen3_omni" in model_type:
1496 + input_tensor = torch.tensor(input_tokens)
1497 + audio_lengths_tensor = audio_feature_lengths
1498 + if audio_lengths_tensor is not None and not isinstance(
1499 + audio_lengths_tensor, torch.Tensor
1500 + ):
1501 + audio_lengths_tensor = torch.as_tensor(
1502 + audio_lengths_tensor, dtype=torch.long
1503 + )
1504 + second_per_grids_tensor = (
1505 + torch.tensor(second_per_grid_ts) if second_per_grid_ts else None
1506 + )
1507 +
1508 + llm_positions, mrope_position_delta = cls._omni3_get_input_positions_tensor( # noqa: E501
1509 + thinker_config,
1510 + input_tensor,
1511 + image_grid_thw,
1512 + video_grid_thw,
1513 + use_audio_in_video,
1514 + audio_lengths_tensor,
1515 + second_per_grids_tensor,
1516 + )
1517 + return llm_positions, mrope_position_delta
1518 +
1519 audio_token_id = thinker_config.audio_token_index
1520 image_token_id = thinker_config.image_token_index
1521 video_token_id = thinker_config.video_token_index
@@ -1182,11 +1529,6 @@ class MRotaryEmbedding(RotaryEmbedding):
1529 thinker_config.vision_config, "tokens_per_second", 25
1530 )
1531
1185 - if isinstance(image_grid_thw, list):
1186 - image_grid_thw = torch.tensor(image_grid_thw)
1187 - if isinstance(video_grid_thw, list):
1188 - video_grid_thw = torch.tensor(video_grid_thw)
1189 -
1532 src_item = input_tokens
1533 audio_seqlens = audio_feature_lengths
1534 if not second_per_grid_ts:
@@ -1232,7 +1574,7 @@ class MRotaryEmbedding(RotaryEmbedding):
1574 grid_t = image_grid_thw[image_idx][0]
1575 grid_hs = image_grid_thw[:, 1]
1576 grid_ws = image_grid_thw[:, 2]
1235 - t_index = (torch.arange(grid_t) * 1 * tokens_per_second).long()
1577 + t_index = torch.arange(grid_t) * 1 * tokens_per_second
1578 llm_pos_ids = cls._get_llm_pos_ids_for_vision(
1579 start_idx, image_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1580 )
@@ -1250,7 +1592,7 @@ class MRotaryEmbedding(RotaryEmbedding):
1592 torch.arange(grid_t)
1593 * second_per_grid_ts[video_idx]
1594 * tokens_per_second
1253 - ).long()
1595 + )
1596 llm_pos_ids = cls._get_llm_pos_ids_for_vision(
1597 start_idx, video_idx, spatial_merge_size, t_index, grid_hs, grid_ws
1598 )
@@ -1277,7 +1619,7 @@ class MRotaryEmbedding(RotaryEmbedding):
1619 torch.arange(grid_t)
1620 * second_per_grid_ts[video_idx]
1621 * tokens_per_second
1280 - ).long()
1622 + )
1623 t_index_split_chunk = cls._split_list_into_ranges(
1624 t_index, t_ntoken_per_chunk
1625 )
@@ -1452,9 +1794,7 @@ class MRotaryEmbedding(RotaryEmbedding):
1794 grid_h = video_grid_thw[1]
1795 grid_w = video_grid_thw[2]
1796 t_ntoken_per_chunk = int(tokens_per_second * seconds_per_chunk)
1455 - t_index = (
1456 - torch.arange(grid_t) * video_second_per_grid_t * tokens_per_second
1457 - ).long()
1797 + t_index = torch.arange(grid_t) * video_second_per_grid_t * tokens_per_second
1798 t_index_split_chunk = cls._split_list_into_ranges(t_index, t_ntoken_per_chunk)
1799
1800 updates = [audio_start_token_id]
vllm/model_executor/models/qwen3_omni_moe_thinker.py new
+1409
@@ -0,0 +1,1409 @@
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 Iterable, Mapping, Sequence
26 +from functools import partial
27 +from typing import Any, Callable, Optional, Union
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 + Qwen3OmniMoeConfig,
37 + Qwen3OmniMoeThinkerConfig,
38 +)
39 +from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import (
40 + Qwen3OmniMoeAudioEncoder,
41 +)
42 +from transformers.models.qwen3_omni_moe.processing_qwen3_omni_moe import (
43 + Qwen3OmniMoeProcessor,
44 +)
45 +from transformers.models.whisper import WhisperFeatureExtractor
46 +
47 +from vllm.attention.backends.registry import _Backend
48 +from vllm.attention.layer import check_upstream_fa_availability
49 +from vllm.compilation.decorators import support_torch_compile
50 +from vllm.config import VllmConfig
51 +from vllm.distributed import get_pp_group
52 +from vllm.logger import init_logger
53 +from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
54 +from vllm.model_executor.layers.linear import ColumnParallelLinear, RowParallelLinear
55 +from vllm.model_executor.layers.logits_processor import LogitsProcessor
56 +from vllm.model_executor.layers.quantization import QuantizationConfig
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.qwen2_audio import (
60 + Qwen2AudioFeatureInputs,
61 + Qwen2AudioProcessingInfo,
62 +)
63 +from vllm.multimodal import MULTIMODAL_REGISTRY
64 +from vllm.multimodal.inputs import MultiModalKwargsItems
65 +from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems
66 +from vllm.multimodal.processing import (
67 + BaseMultiModalProcessor,
68 + MultiModalPromptUpdates,
69 + PlaceholderFeaturesInfo,
70 + PromptReplacement,
71 + PromptUpdate,
72 +)
73 +from vllm.sequence import IntermediateTensors
74 +
75 +from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
76 +
77 +# yapf conflicts with isort for this block
78 +# yapf: disable
79 +from .qwen2_5_omni_thinker import (
80 + Qwen2_5OmniConditionalGenerationMixin,
81 + Qwen2_5OmniThinkerDummyInputsBuilder,
82 + Qwen2_5OmniThinkerMultiModalProcessor,
83 + Qwen2_5OmniThinkerProcessingInfo,
84 +)
85 +
86 +# yapf: enable
87 +from .qwen2_5_vl import (
88 + Qwen2_5_VisionAttention,
89 + Qwen2_5_VisionRotaryEmbedding,
90 + Qwen2_5_VLProcessingInfo,
91 +)
92 +from .qwen3_moe import Qwen3MoeForCausalLM, Qwen3MoeModel
93 +from .utils import (
94 + AutoWeightsLoader,
95 + WeightsMapper,
96 + _merge_multimodal_embeddings,
97 + maybe_prefix,
98 +)
99 +from .vision import get_vit_attn_backend
100 +
101 +try:
102 + import flash_attn
103 +except (ImportError, ModuleNotFoundError):
104 + flash_attn = None
105 +
106 +logger = init_logger(__name__)
107 +
108 +
109 +class Qwen3_VisionPatchEmbed(nn.Module):
110 + def __init__(
111 + self,
112 + patch_size: int = 14,
113 + temporal_patch_size: int = 2,
114 + in_channels: int = 3,
115 + hidden_size: int = 1152,
116 + ) -> None:
117 + super().__init__()
118 + self.patch_size = patch_size
119 + self.temporal_patch_size = temporal_patch_size
120 + self.hidden_size = hidden_size
121 +
122 + kernel_size = (temporal_patch_size, patch_size, patch_size)
123 + self.proj = nn.Conv3d(
124 + in_channels,
125 + hidden_size,
126 + kernel_size=kernel_size,
127 + stride=kernel_size,
128 + bias=True,
129 + )
130 +
131 + def forward(self, x: torch.Tensor) -> torch.Tensor:
132 + L, C = x.shape
133 + x = x.view(L, -1, self.temporal_patch_size, self.patch_size, self.patch_size)
134 + x = self.proj(x).view(L, self.hidden_size)
135 + return x
136 +
137 +
138 +class Qwen3_VisionMLP(nn.Module):
139 + def __init__(
140 + self,
141 + in_features: int,
142 + hidden_features: int,
143 + bias: bool = False,
144 + act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
145 + quant_config: Optional[QuantizationConfig] = None,
146 + prefix: str = "",
147 + ):
148 + super().__init__()
149 + self.linear_fc1 = ColumnParallelLinear(
150 + in_features,
151 + hidden_features,
152 + bias=bias,
153 + quant_config=quant_config,
154 + return_bias=False,
155 + prefix=f"{prefix}.linear_fc1",
156 + )
157 + self.linear_fc2 = RowParallelLinear(
158 + hidden_features,
159 + in_features,
160 + bias=bias,
161 + quant_config=quant_config,
162 + return_bias=False,
163 + prefix=f"{prefix}.linear_fc2",
164 + )
165 + self.act_fn = act_fn
166 +
167 + def forward(self, x: torch.Tensor):
168 + mlp_output = self.linear_fc2(self.act_fn(self.linear_fc1(x)))
169 + return mlp_output
170 +
171 +
172 +class Qwen3_VisionBlock(nn.Module):
173 + def __init__(
174 + self,
175 + dim: int,
176 + num_heads: int,
177 + mlp_hidden_dim: int,
178 + act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
179 + norm_layer: Optional[Callable[[int], nn.Module]] = None,
180 + quant_config: Optional[QuantizationConfig] = None,
181 + prefix: str = "",
182 + ) -> None:
183 + super().__init__()
184 + if norm_layer is None:
185 + norm_layer = partial(nn.LayerNorm, eps=1e-6)
186 + self.norm1 = norm_layer(dim)
187 + self.norm2 = norm_layer(dim)
188 + self.attn = Qwen2_5_VisionAttention(
189 + embed_dim=dim,
190 + num_heads=num_heads,
191 + projection_size=dim,
192 + quant_config=quant_config,
193 + prefix=f"{prefix}.attn",
194 + )
195 + self.mlp = Qwen3_VisionMLP(
196 + dim,
197 + mlp_hidden_dim,
198 + act_fn=act_fn,
199 + bias=True,
200 + quant_config=quant_config,
201 + prefix=f"{prefix}.mlp",
202 + )
203 +
204 + def forward(
205 + self,
206 + x: torch.Tensor,
207 + cu_seqlens: torch.Tensor,
208 + rotary_pos_emb: torch.Tensor,
209 + max_seqlen: Optional[int] = None, # Only used for Flash Attention
210 + seqlens: Optional[list[int]] = None, # Only used for xFormers
211 + ) -> torch.Tensor:
212 + x = x + self.attn(
213 + self.norm1(x),
214 + cu_seqlens=cu_seqlens,
215 + rotary_pos_emb=rotary_pos_emb,
216 + max_seqlen=max_seqlen,
217 + seqlens=seqlens,
218 + )
219 +
220 + x = x + self.mlp(self.norm2(x))
221 + return x
222 +
223 +
224 +class Qwen3_VisionPatchMerger(nn.Module):
225 + def __init__(
226 + self,
227 + d_model: int,
228 + context_dim: int,
229 + norm_layer: Optional[Callable[[int], nn.Module]] = None,
230 + spatial_merge_size: int = 2,
231 + use_postshuffle_norm: bool = False,
232 + quant_config: Optional[QuantizationConfig] = None,
233 + prefix: str = "",
234 + ) -> None:
235 + super().__init__()
236 + self.hidden_size = context_dim * (spatial_merge_size**2)
237 +
238 + self.use_postshuffle_norm = use_postshuffle_norm
239 + if self.use_postshuffle_norm:
240 + context_dim = self.hidden_size
241 +
242 + if norm_layer is None:
243 + norm_layer = partial(nn.LayerNorm, eps=1e-6)
244 + self.use_postshuffle_norm = use_postshuffle_norm
245 + self.ln_q = norm_layer(
246 + self.hidden_size if use_postshuffle_norm else context_dim
247 + )
248 + self.mlp = nn.ModuleList(
249 + [
250 + ColumnParallelLinear(
251 + self.hidden_size,
252 + self.hidden_size,
253 + bias=True,
254 + quant_config=quant_config,
255 + prefix=f"{prefix}.mlp.0",
256 + ),
257 + nn.GELU(),
258 + RowParallelLinear(
259 + self.hidden_size,
260 + d_model,
261 + bias=True,
262 + quant_config=quant_config,
263 + prefix=f"{prefix}.mlp.2",
264 + ),
265 + ]
266 + )
267 +
268 + def forward(self, x: torch.Tensor) -> torch.Tensor:
269 + if self.use_postshuffle_norm:
270 + x = self.ln_q(x.view(-1, self.hidden_size))
271 + else:
272 + x = self.ln_q(x).view(-1, self.hidden_size)
273 +
274 + mlp_fc1, mlp_act, mlp_fc2 = self.mlp
275 + x_parallel, _ = mlp_fc1(x)
276 + x_parallel = mlp_act(x_parallel)
277 + out, _ = mlp_fc2(x_parallel)
278 + return out
279 +
280 +
281 +class Qwen3Omni_VisionTransformer(nn.Module):
282 + def __init__(
283 + self,
284 + vision_config,
285 + norm_eps: float = 1e-6,
286 + quant_config: Optional[QuantizationConfig] = None,
287 + prefix: str = "",
288 + ) -> None:
289 + super().__init__()
290 + self.hidden_size = vision_config.hidden_size
291 + self.num_heads = vision_config.num_heads
292 + self.image_size = vision_config.image_size
293 + self.patch_size = vision_config.patch_size
294 + self.spatial_merge_size = vision_config.spatial_merge_size
295 + self.spatial_merge_unit = self.spatial_merge_size**2
296 + self.temporal_patch_size = vision_config.temporal_patch_size
297 + self.num_grid_per_side = self.image_size // self.patch_size
298 + self.apply_vit_abs_pos_embed = vision_config.apply_vit_abs_pos_embed
299 + self.deepstack_visual_indexes = vision_config.deepstack_visual_indexes
300 +
301 + self.patch_embed = Qwen3_VisionPatchEmbed(
302 + patch_size=self.patch_size,
303 + temporal_patch_size=self.temporal_patch_size,
304 + in_channels=vision_config.in_channels,
305 + hidden_size=self.hidden_size,
306 + )
307 +
308 + # vit pos embeding, TODO: spatial_patch_size vs patch_size
309 + if self.apply_vit_abs_pos_embed:
310 + self.pos_embed = nn.Embedding(self.num_grid_per_side**2, self.hidden_size)
311 + else:
312 + self.pos_embed = nn.Parameter(
313 + torch.empty([1, self.num_grid_per_side**2, self.hidden_size])
314 + )
315 +
316 + norm_layer = partial(nn.LayerNorm, eps=norm_eps)
317 + head_dim = self.hidden_size // self.num_heads
318 + self.rotary_pos_emb = Qwen2_5_VisionRotaryEmbedding(head_dim // 2)
319 +
320 + self.blocks = nn.ModuleList(
321 + [
322 + Qwen3_VisionBlock(
323 + dim=self.hidden_size,
324 + num_heads=self.num_heads,
325 + mlp_hidden_dim=vision_config.intermediate_size,
326 + act_fn=_ACTIVATION_REGISTRY[vision_config.hidden_act],
327 + norm_layer=norm_layer,
328 + quant_config=quant_config,
329 + prefix=f"{prefix}.blocks.{layer_idx}",
330 + )
331 + for layer_idx in range(vision_config.depth)
332 + ]
333 + )
334 + self.merger = Qwen3_VisionPatchMerger(
335 + d_model=vision_config.out_hidden_size,
336 + context_dim=self.hidden_size,
337 + norm_layer=norm_layer,
338 + spatial_merge_size=self.spatial_merge_size,
339 + quant_config=quant_config,
340 + prefix=f"{prefix}.merger",
341 + )
342 + if self.deepstack_visual_indexes is not None:
343 + self.merger_list = nn.ModuleList(
344 + [
345 + Qwen3_VisionPatchMerger(
346 + d_model=vision_config.out_hidden_size,
347 + context_dim=self.hidden_size,
348 + spatial_merge_size=self.spatial_merge_size,
349 + use_postshuffle_norm=True,
350 + norm_layer=norm_layer,
351 + quant_config=quant_config,
352 + prefix=f"{prefix}.merger_list.{layer_idx}",
353 + )
354 + for layer_idx in range(len(self.deepstack_visual_indexes))
355 + ]
356 + )
357 +
358 + self.attn_backend = get_vit_attn_backend(
359 + head_size=head_dim, dtype=torch.get_default_dtype()
360 + )
361 + if self.attn_backend != _Backend.FLASH_ATTN and check_upstream_fa_availability(
362 + torch.get_default_dtype()
363 + ):
364 + self.attn_backend = _Backend.FLASH_ATTN
365 +
366 + @property
367 + def dtype(self) -> torch.dtype:
368 + return self.patch_embed.proj.weight.dtype
369 +
370 + @property
371 + def device(self) -> torch.device:
372 + return self.patch_embed.proj.weight.device
373 +
374 + def rot_pos_emb(self, grid_thw):
375 + pos_ids = []
376 + for t, h, w in grid_thw:
377 + hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
378 + hpos_ids = hpos_ids.reshape(
379 + h // self.spatial_merge_size,
380 + self.spatial_merge_size,
381 + w // self.spatial_merge_size,
382 + self.spatial_merge_size,
383 + )
384 + hpos_ids = hpos_ids.permute(0, 2, 1, 3)
385 + hpos_ids = hpos_ids.flatten()
386 +
387 + wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
388 + wpos_ids = wpos_ids.reshape(
389 + h // self.spatial_merge_size,
390 + self.spatial_merge_size,
391 + w // self.spatial_merge_size,
392 + self.spatial_merge_size,
393 + )
394 + wpos_ids = wpos_ids.permute(0, 2, 1, 3)
395 + wpos_ids = wpos_ids.flatten()
396 + pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
397 + pos_ids = torch.cat(pos_ids, dim=0)
398 + max_grid_size = grid_thw[:, 1:].max()
399 + rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
400 + rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
401 + return rotary_pos_emb
402 +
403 + def fast_pos_embed_interpolate(self, grid_thw: list[list[int]]) -> torch.Tensor:
404 + num_grid_per_side = self.num_grid_per_side
405 + m_size = self.spatial_merge_size
406 + hidden_dim = self.pos_embed.embedding_dim
407 +
408 + outputs = []
409 + for t, h, w in grid_thw:
410 + h_idxs = torch.linspace(
411 + 0, num_grid_per_side - 1, h, dtype=torch.float32, device=self.device
412 + )
413 + w_idxs = torch.linspace(
414 + 0, num_grid_per_side - 1, w, dtype=torch.float32, device=self.device
415 + )
416 +
417 + h_floor = h_idxs.to(torch.long)
418 + w_floor = w_idxs.to(torch.long)
419 + h_ceil = torch.clamp(h_floor + 1, max=num_grid_per_side - 1)
420 + w_ceil = torch.clamp(w_floor + 1, max=num_grid_per_side - 1)
421 +
422 + dh = h_idxs - h_floor
423 + dw = w_idxs - w_floor
424 +
425 + # Create meshgrid view for all h, w vars
426 + dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij")
427 + h_floor_grid, w_floor_grid = torch.meshgrid(h_floor, w_floor, indexing="ij")
428 + h_ceil_grid, w_ceil_grid = torch.meshgrid(h_ceil, w_ceil, indexing="ij")
429 + h_floor_grid_idx = h_floor_grid * num_grid_per_side
430 + h_ceil_grid_idx = h_ceil_grid * num_grid_per_side
431 +
432 + # original computation of weights
433 + # w00 = (1 - dh_grid) * (1 - dw_grid)
434 + # w01 = (1 - dh_grid) * dw_grid
435 + # w10 = dh_grid * (1 - dw_grid)
436 + # w11 = dh_grid * dw_grid
437 + # we reuse w11 here to avoid duplicate
438 + # dh_grid * dw_grid computation
439 + w11 = dh_grid * dw_grid
440 + w10 = dh_grid - w11
441 + w01 = dw_grid - w11
442 + w00 = 1 - dh_grid - dw_grid + w11
443 +
444 + idx00 = h_floor_grid_idx + w_floor_grid
445 + idx01 = h_floor_grid_idx + w_ceil_grid
446 + idx10 = h_ceil_grid_idx + w_floor_grid
447 + idx11 = h_ceil_grid_idx + w_ceil_grid
448 +
449 + indices = torch.stack([idx00, idx01, idx10, idx11], dim=0).reshape(4, -1)
450 + weights = torch.stack([w00, w01, w10, w11], dim=0).reshape(4, -1, 1)
451 + weights = weights.to(dtype=self.dtype, device=self.device)
452 +
453 + embeds = self.pos_embed(indices)
454 + weighted_embeds = embeds * weights
455 + p0, p1, p2, p3 = weighted_embeds.unbind(dim=0)
456 + combined = p0 + p1 + p2 + p3
457 +
458 + combined = combined.view(h * w, hidden_dim)
459 + repeated = combined.unsqueeze(0).expand(t, -1, -1).contiguous()
460 + repeated = repeated.view(
461 + t, h // m_size, m_size, w // m_size, m_size, hidden_dim
462 + )
463 + repeated = repeated.permute(0, 1, 3, 2, 4, 5).reshape(-1, hidden_dim)
464 + outputs.append(repeated)
465 +
466 + return torch.cat(outputs, dim=0)
467 +
468 + def compute_attn_mask_seqlen(
469 + self,
470 + cu_seqlens: torch.Tensor,
471 + ) -> tuple[Optional[int], Optional[list[int]]]:
472 + max_seqlen, seqlens = None, None
473 + if self.attn_backend == _Backend.FLASH_ATTN:
474 + max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
475 + elif self.attn_backend == _Backend.XFORMERS:
476 + seqlens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
477 + return max_seqlen, seqlens
478 +
479 + def forward(
480 + self,
481 + x: torch.Tensor,
482 + grid_thw: list[list[int]],
483 + ) -> torch.Tensor:
484 + hidden_states = x.to(device=self.device, dtype=self.dtype)
485 + hidden_states = self.patch_embed(hidden_states)
486 +
487 + if self.apply_vit_abs_pos_embed:
488 + pos_embeds = self.fast_pos_embed_interpolate(grid_thw)
489 + hidden_states = hidden_states + pos_embeds
490 + rotary_pos_emb = self.rot_pos_emb(grid_thw)
491 +
492 + cu_seqlens = torch.repeat_interleave(
493 + grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
494 + ).cumsum(
495 + dim=0,
496 + dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,
497 + )
498 + cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
499 +
500 + hidden_states = hidden_states.unsqueeze(1)
501 + rotary_pos_emb = rotary_pos_emb.to(hidden_states.device)
502 + max_seqlen, seqlens = self.compute_attn_mask_seqlen(cu_seqlens)
503 +
504 + hidden_states_list = []
505 + deepstack_visual_indexes = self.deepstack_visual_indexes
506 +
507 + for layer_num, blk in enumerate(self.blocks):
508 + hidden_states = blk(
509 + hidden_states,
510 + cu_seqlens=cu_seqlens,
511 + rotary_pos_emb=rotary_pos_emb,
512 + max_seqlen=max_seqlen,
513 + seqlens=seqlens,
514 + )
515 + if (
516 + deepstack_visual_indexes is not None
517 + and layer_num in deepstack_visual_indexes
518 + ):
519 + hidden_states_list.append(hidden_states)
520 +
521 + hidden_states = self.merger(hidden_states)
522 +
523 + # processing deepstack
524 + if deepstack_visual_indexes is not None:
525 + processed_hidden_states_list = [hidden_states]
526 + for idx, x in enumerate(hidden_states_list):
527 + x = self.merger_list[idx](x)
528 + processed_hidden_states_list.append(x)
529 + # we cat the original visual features and deepstack features
530 + # along the feature dim
531 + hidden_states = torch.cat(
532 + processed_hidden_states_list, dim=1
533 + ) # [seq_len, hidden_size * (1 + depth_of_deepstack)]
534 +
535 + return hidden_states
536 +
537 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
538 + stacked_params_mapping = [
539 + # (param_name, shard_name, shard_id)
540 + ("attn.qkv.", "attn.q.", "q"),
541 + ("attn.qkv.", "attn.k.", "k"),
542 + ("attn.qkv.", "attn.v.", "v"),
543 + ]
544 + params_dict = dict(self.named_parameters(remove_duplicate=False))
545 + loaded_params: set[str] = set()
546 +
547 + for name, loaded_weight in weights:
548 + for param_name, weight_name, shard_id in stacked_params_mapping:
549 + if weight_name not in name:
550 + continue
551 + name = name.replace(weight_name, param_name)
552 +
553 + param = params_dict[name]
554 + weight_loader = param.weight_loader
555 + weight_loader(param, loaded_weight, shard_id)
556 + break
557 + else:
558 + param = params_dict[name]
559 + weight_loader = getattr(param, "weight_loader", default_weight_loader)
560 + weight_loader(param, loaded_weight)
561 + loaded_params.add(name)
562 + return loaded_params
563 +
564 +
565 +@support_torch_compile(
566 + dynamic_arg_dims={
567 + "input_ids": 0,
568 + "positions": -1,
569 + "intermediate_tensors": 0,
570 + "inputs_embeds": 0,
571 + "deepstack_input_embeds": 0,
572 + }
573 +)
574 +class Qwen3MoeLLMModel(Qwen3MoeModel):
575 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
576 + super().__init__(vllm_config=vllm_config, prefix=prefix)
577 +
578 + self.deepstack_multiscale_layer_start = 1
579 +
580 + def forward(
581 + self,
582 + input_ids: torch.Tensor,
583 + positions: torch.Tensor,
584 + intermediate_tensors: Optional[IntermediateTensors] = None,
585 + inputs_embeds: Optional[torch.Tensor] = None,
586 + deepstack_input_embeds: Optional[IntermediateTensors] = None,
587 + ) -> Union[torch.Tensor, IntermediateTensors]:
588 + if get_pp_group().is_first_rank:
589 + if inputs_embeds is not None:
590 + hidden_states = inputs_embeds
591 + else:
592 + hidden_states = self.get_input_embeddings(input_ids)
593 + residual = None
594 + else:
595 + assert intermediate_tensors is not None
596 + hidden_states = intermediate_tensors["hidden_states"]
597 + residual = intermediate_tensors["residual"]
598 + for layer_idx, layer in enumerate(
599 + self.layers[self.start_layer : self.end_layer]
600 + ):
601 + layer_idx = layer_idx + self.start_layer
602 +
603 + hidden_states, residual = layer(
604 + positions,
605 + hidden_states,
606 + residual,
607 + )
608 +
609 + if deepstack_input_embeds is not None and layer_idx in range(
610 + 0, len(deepstack_input_embeds)
611 + ):
612 + hidden_states = (
613 + hidden_states
614 + + deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"]
615 + )
616 +
617 + if not get_pp_group().is_last_rank:
618 + return IntermediateTensors(
619 + {"hidden_states": hidden_states, "residual": residual}
620 + )
621 + hidden_states, _ = self.norm(hidden_states, residual)
622 + return hidden_states
623 +
624 +
625 +class Qwen3MoeLLMForCausalLM(Qwen3MoeForCausalLM):
626 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
627 + super(Qwen3MoeForCausalLM, self).__init__()
628 + config = vllm_config.model_config.hf_config
629 + quant_config = vllm_config.quant_config
630 + self.config = config
631 + self.quant_config = quant_config
632 + self.model = Qwen3MoeLLMModel(
633 + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
634 + )
635 + self.lm_head = ParallelLMHead(
636 + config.vocab_size, config.hidden_size, quant_config=quant_config
637 + )
638 + if self.config.tie_word_embeddings:
639 + self.lm_head.weight = self.model.embed_tokens.weight
640 + self.logits_processor = LogitsProcessor(config.vocab_size)
641 + self.make_empty_intermediate_tensors = (
642 + self.model.make_empty_intermediate_tensors
643 + )
644 +
645 +
646 +class Qwen3OmniMoeThinkerProcessingInfo(
647 + Qwen2AudioProcessingInfo, Qwen2_5_VLProcessingInfo
648 +):
649 + def get_hf_config(self):
650 + return self.ctx.get_hf_config(Qwen3OmniMoeConfig).thinker_config
651 +
652 + def get_hf_processor(self, **kwargs: object) -> Qwen3OmniMoeProcessor:
653 + processor = self.ctx.get_hf_processor(
654 + Qwen3OmniMoeProcessor,
655 + use_fast=kwargs.pop("use_fast", True),
656 + **kwargs,
657 + )
658 + if not hasattr(processor, "audio_token"):
659 + processor.audio_token = "<|audio_pad|>"
660 + if not hasattr(processor, "image_token"):
661 + processor.image_token = "<|image_pad|>"
662 + if not hasattr(processor, "video_token"):
663 + processor.video_token = "<|video_pad|>"
664 + return processor
665 +
666 + def get_feature_extractor(self, **kwargs: object):
667 + hf_processor = self.get_hf_processor(**kwargs)
668 + feature_extractor = hf_processor.feature_extractor # type: ignore
669 + assert isinstance(feature_extractor, WhisperFeatureExtractor)
670 + return feature_extractor
671 +
672 + def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
673 + return {"audio": None, "image": None, "video": None}
674 +
675 +
676 +Qwen3OmniMoeThinkerDummyInputsBuilder = Qwen2_5OmniThinkerDummyInputsBuilder
677 +
678 +
679 +class Qwen3OmniMoeThinkerMultiModalProcessor(
680 + Qwen2_5OmniThinkerMultiModalProcessor,
681 +):
682 + def _get_feat_extract_output_lengths(
683 + self, input_lengths: torch.Tensor
684 + ) -> torch.Tensor:
685 + input_lengths_leave = input_lengths % 100
686 + feat_lengths = (input_lengths_leave - 1) // 2 + 1
687 + output_lengths = (
688 + ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
689 + )
690 + return feat_lengths, output_lengths
691 +
692 + def _call_hf_processor(
693 + self,
694 + prompt: str,
695 + mm_data: Mapping[str, object],
696 + mm_kwargs: Mapping[str, object],
697 + tok_kwargs: Mapping[str, object],
698 + ) -> BatchFeature:
699 + mm_data = dict(mm_data)
700 + audios = mm_data.pop("audios", [])
701 +
702 + def pad_to_hop_length(x: np.ndarray, hop_length: int) -> np.ndarray:
703 + length = x.shape[-1]
704 + if length % hop_length != 0:
705 + pad_length = hop_length - (length % hop_length)
706 + x = np.pad(x, (0, pad_length), mode="constant", constant_values=0)
707 + return x
708 +
709 + # NOTE: WhisperFeatureExtractor cannot handle empty list of audios
710 + if audios:
711 + # NOTE: Qwen3-Omni processor accept "audio"
712 + # To make sure the cache works with padding=True, we pre-padded
713 + # the audio to multiple of hop_length.
714 + hop_length = self.info.get_feature_extractor().hop_length
715 + mm_data["audio"] = [
716 + pad_to_hop_length(audio, hop_length)
717 + if isinstance(audio, np.ndarray)
718 + else (pad_to_hop_length(audio[0], hop_length), audio[1])
719 + for audio in audios
720 + ]
721 + mm_kwargs = dict(
722 + **mm_kwargs,
723 + )
724 +
725 + hf_inputs = super()._call_hf_processor(
726 + prompt=prompt,
727 + mm_data=mm_data,
728 + mm_kwargs=mm_kwargs,
729 + tok_kwargs=tok_kwargs,
730 + )
731 +
732 + if (
733 + "audio_feature_lengths" in hf_inputs
734 + and "feature_attention_mask" in hf_inputs
735 + and (audios := mm_data.get("audio", []))
736 + ):
737 + hop_length = self.info.get_feature_extractor().hop_length
738 + audio_num_frames = []
739 + for _, audio in enumerate(audios):
740 + audio_length = len(audio[0]) if isinstance(audio, tuple) else len(audio)
741 + num_frame = (
742 + (audio_length // hop_length)
743 + if audio_length % hop_length == 0
744 + else (audio_length // hop_length - 1)
745 + )
746 + audio_num_frames.append(num_frame)
747 + hf_inputs["feature_attention_mask"] = [
748 + torch.ones(num_frame) for num_frame in audio_num_frames
749 + ]
750 + hf_inputs["audio_feature_lengths"] = torch.tensor(audio_num_frames)
751 + return hf_inputs
752 +
753 + def _maybe_apply_prompt_updates(
754 + self,
755 + mm_items: MultiModalDataItems,
756 + prompt_ids: list[int],
757 + mm_kwargs: MultiModalKwargsItems,
758 + mm_prompt_updates: MultiModalPromptUpdates,
759 + is_update_applied: bool,
760 + ) -> tuple[list[int], str, Mapping[str, list[PlaceholderFeaturesInfo]]]:
761 + """
762 + Qwen3-Omni reimplements this function to handle `use_audio_in_video`.
763 + """
764 + mm_item_counts = mm_items.get_all_counts()
765 + self._validate_mm_kwargs(mm_kwargs, mm_item_counts)
766 +
767 + use_audio_in_video = False
768 + if "video" in mm_kwargs:
769 + for item in mm_kwargs["video"]:
770 + if item and item["use_audio_in_video"].data:
771 + use_audio_in_video = True
772 + else:
773 + use_audio_in_video = False
774 +
775 + if use_audio_in_video and "video" in mm_item_counts:
776 + assert "audio" in mm_item_counts
777 + mm_item_counts["audio"] -= mm_item_counts["video"]
778 +
779 + # Special case with `use_audio_in_video=True`
780 + if use_audio_in_video:
781 + if is_update_applied:
782 + prompt_ids = self._get_raw_input_ids(prompt_ids, use_audio_in_video)
783 + (
784 + prompt_ids,
785 + mm_placeholders,
786 + ) = self._apply_prompt_updates(
787 + prompt_ids,
788 + mm_prompt_updates,
789 + )
790 + self._validate_mm_placeholders(mm_placeholders, mm_item_counts)
791 + # normal case with `use_audio_in_video=False`
792 + elif is_update_applied:
793 + mm_placeholders = self._find_mm_placeholders(
794 + prompt_ids,
795 + mm_prompt_updates,
796 + )
797 + self._validate_mm_placeholders(
798 + mm_placeholders,
799 + mm_item_counts,
800 + )
801 + else:
802 + prompt_ids, mm_placeholders = self._apply_prompt_updates(
803 + prompt_ids,
804 + mm_prompt_updates,
805 + )
806 + self._validate_mm_placeholders(
807 + mm_placeholders,
808 + mm_item_counts,
809 + )
810 +
811 + return prompt_ids, mm_placeholders
812 +
813 + def get_updates_use_audio_in_video(
814 + self,
815 + thinker_config: PretrainedConfig,
816 + audio_len: int,
817 + video_grid_thw: Union[list[int], torch.Tensor],
818 + video_second_per_grid_t: float,
819 + ) -> list[int]:
820 + shift = 0
821 + audio_token_id = thinker_config.audio_token_id
822 + video_token_id = thinker_config.video_token_id
823 + audio_start_token_id = thinker_config.audio_start_token_id
824 + audio_end_token_id = thinker_config.audio_end_token_id
825 + spatial_merge_size = thinker_config.vision_config.spatial_merge_size
826 + position_id_per_seconds = thinker_config.position_id_per_seconds
827 + audio_token_indices = np.arange(next(iter([audio_len])))
828 + curr_video_grid_thw = next(iter([video_grid_thw]))
829 + height = curr_video_grid_thw[1] // spatial_merge_size
830 + width = curr_video_grid_thw[2] // spatial_merge_size
831 + video_token_indices = np.arange(curr_video_grid_thw[0]).reshape(-1, 1, 1)
832 + video_token_indices = np.broadcast_to(
833 + video_token_indices, (video_token_indices.shape[0], height, width)
834 + ).reshape(-1)
835 + video_token_indices = (
836 + (video_token_indices + shift)
837 + * next(iter([video_second_per_grid_t]))
838 + * position_id_per_seconds
839 + )
840 + video_data_index, audio_data_index = 0, 0
841 + updates = [audio_start_token_id]
842 + while video_data_index < len(video_token_indices) and audio_data_index < len(
843 + audio_token_indices
844 + ):
845 + if (
846 + video_token_indices[video_data_index]
847 + <= audio_token_indices[audio_data_index]
848 + ):
849 + updates += [video_token_id]
850 + video_data_index += 1
851 + else:
852 + updates += [audio_token_id]
853 + audio_data_index += 1
854 + if video_data_index < len(video_token_indices):
855 + updates += [video_token_id] * (len(video_token_indices) - video_data_index)
856 + if audio_data_index < len(audio_token_indices):
857 + updates += [audio_token_id] * (len(audio_token_indices) - audio_data_index)
858 + updates += [audio_end_token_id]
859 + return updates
860 +
861 + def _get_prompt_updates(
862 + self,
863 + mm_items: MultiModalDataItems,
864 + hf_processor_mm_kwargs: Mapping[str, Any],
865 + out_mm_kwargs: MultiModalKwargsItems,
866 + ) -> Sequence[PromptUpdate]:
867 + processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
868 + tokenizer = self.info.get_tokenizer()
869 + image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
870 + vocab = tokenizer.get_vocab()
871 +
872 + audio_token = processor.audio_token
873 + image_token = processor.image_token
874 + video_token = processor.video_token
875 + audio_token_id = vocab[audio_token]
876 + image_token_id = vocab[image_token]
877 + video_token_id = vocab[video_token]
878 +
879 + out_mm_data = out_mm_kwargs.get_data()
880 + audio_feature_lengths = out_mm_data.get("audio_feature_lengths")
881 + feature_attention_mask = out_mm_data.get("feature_attention_mask")
882 + if audio_feature_lengths is None and feature_attention_mask is None:
883 + audio_output_lengths = []
884 + elif audio_feature_lengths is not None:
885 + _, audio_output_lens = self._get_feat_extract_output_lengths(
886 + audio_feature_lengths
887 + )
888 + audio_output_lengths = audio_output_lens.tolist()
889 + elif feature_attention_mask is not None:
890 + assert isinstance(feature_attention_mask, torch.Tensor)
891 + _, audio_output_lens = self._get_feat_extract_output_lengths(
892 + feature_attention_mask.sum(-1)
893 + )
894 + audio_output_lengths = audio_output_lens.tolist()
895 +
896 + # number of audios read from video.
897 + audio_in_video_item_idx = 0
898 + audio_item_idx = 0
899 +
900 + def get_replacement_qwen2_audio(item_idx: int):
901 + nonlocal audio_item_idx
902 + item_idx += audio_in_video_item_idx
903 +
904 + audio_item_idx += 1
905 +
906 + num_features = audio_output_lengths[item_idx]
907 + if num_features == 0:
908 + audios = mm_items.get_items("audio", AudioProcessorItems)
909 + audio = audios.get(item_idx)
910 + raise ValueError(
911 + f"The audio {audio} (len={len(audio)}) is too short "
912 + "to be represented inside the model"
913 + )
914 +
915 + return [audio_token_id] * num_features
916 +
917 + def get_replacement_qwen2_vision(item_idx: int, modality: str):
918 + grid_thw = out_mm_data[f"{modality}_grid_thw"][item_idx]
919 + assert isinstance(grid_thw, torch.Tensor)
920 + merge_length = image_processor.merge_size**2
921 +
922 + token_id = image_token_id if modality == "image" else video_token_id
923 + return [token_id] * (int(grid_thw.prod()) // merge_length)
924 +
925 + use_audio_in_video = hf_processor_mm_kwargs.get("use_audio_in_video", False)
926 + thinker_config = self.info.get_hf_config()
927 +
928 + def get_replacement_qwen2_use_audio_in_video(item_idx: int):
929 + nonlocal audio_in_video_item_idx
930 + audio_num_features = audio_output_lengths[audio_item_idx + item_idx]
931 + video_grid_thw = out_mm_data["video_grid_thw"][item_idx]
932 +
933 + audio_in_video_item_idx += 1
934 +
935 + second_per_grid_ts = hf_processor_mm_kwargs.get("second_per_grid_ts", None)
936 + if second_per_grid_ts:
937 + video_second_per_grid_t = second_per_grid_ts[item_idx]
938 + else:
939 + video_second_per_grid_t = 1.0
940 +
941 + return self.get_updates_use_audio_in_video(
942 + thinker_config=thinker_config,
943 + audio_len=audio_num_features,
944 + video_grid_thw=video_grid_thw,
945 + video_second_per_grid_t=video_second_per_grid_t,
946 + )
947 +
948 + video_replacement_fn = (
949 + get_replacement_qwen2_use_audio_in_video
950 + if use_audio_in_video
951 + else partial(get_replacement_qwen2_vision, modality="video")
952 + )
953 +
954 + return [
955 + PromptReplacement(
956 + modality="audio",
957 + target=audio_token,
958 + replacement=get_replacement_qwen2_audio,
959 + ),
960 + PromptReplacement(
961 + modality="image",
962 + target=image_token,
963 + replacement=partial(get_replacement_qwen2_vision, modality="image"),
964 + ),
965 + PromptReplacement(
966 + modality="video",
967 + target=video_token,
968 + replacement=video_replacement_fn,
969 + ),
970 + ]
971 +
972 + def _validate_mm_placeholders(
973 + self,
974 + mm_placeholders: Mapping[str, list[PlaceholderFeaturesInfo]],
975 + mm_item_counts: Mapping[str, int],
976 + ) -> None:
977 + BaseMultiModalProcessor[
978 + Qwen2_5OmniThinkerProcessingInfo
979 + ]._validate_mm_placeholders(self, mm_placeholders, mm_item_counts)
980 +
981 + def _get_raw_input_ids(
982 + self,
983 + token_ids: list[int],
984 + use_audio_in_video: bool = False,
985 + ) -> list[int]:
986 + tokenizer = self.info.get_tokenizer()
987 + vision_bos_token = tokenizer.encode(tokenizer.vision_bos_token)[0]
988 + vision_eos_token = tokenizer.encode(tokenizer.vision_eos_token)[0]
989 + audio_bos_token = tokenizer.encode(tokenizer.audio_bos_token)[0]
990 + audio_eos_token = tokenizer.encode(tokenizer.audio_eos_token)[0]
991 + audio_token = tokenizer.encode("<|audio_pad|>")[0]
992 + image_token = tokenizer.encode("<|image_pad|>")[0]
993 + video_token = tokenizer.encode("<|video_pad|>")[0]
994 +
995 + result = token_ids[:]
996 + if use_audio_in_video:
997 + while True:
998 + start = None
999 + for i in range(len(result) - 1):
1000 + if result[i : i + 2] == [vision_bos_token, audio_bos_token]:
1001 + start = i
1002 + break
1003 + if start is not None:
1004 + end = None
1005 + for i in range(start + 2, len(result) - 1):
1006 + if result[i : i + 2] == [audio_eos_token, vision_eos_token]:
1007 + end = i
1008 + break
1009 + if end is not None:
1010 + result = (
1011 + result[:start]
1012 + + [vision_bos_token, video_token, vision_eos_token]
1013 + + result[end + 2 :]
1014 + )
1015 + else:
1016 + break
1017 +
1018 + for mm_token in [audio_token, image_token, video_token]:
1019 + compressed = []
1020 + for x in result:
1021 + if x != mm_token or (not compressed or compressed[-1] != mm_token):
1022 + compressed.append(x)
1023 + result = compressed
1024 +
1025 + return result
1026 +
1027 +
1028 +class Qwen3OmniMoeConditionalGenerationMixin(Qwen2_5OmniConditionalGenerationMixin):
1029 + def _validate_and_reshape_mm_tensor(
1030 + self, mm_input: object, name: str, dim: int = 0
1031 + ) -> torch.Tensor:
1032 + if not isinstance(mm_input, (torch.Tensor, list)):
1033 + raise ValueError(f"Incorrect type of {name}. Got type: {type(mm_input)}")
1034 + if name == "feature_attention_mask":
1035 + dim = -1
1036 + if isinstance(mm_input, torch.Tensor):
1037 + return torch.concat(list(mm_input), dim=dim)
1038 + else:
1039 + if isinstance(mm_input[0], list):
1040 + return torch.concat(
1041 + [torch.concat(mm_input[i], dim=dim) for i in range(len(mm_input))],
1042 + dim=dim,
1043 + )
1044 + else:
1045 + return torch.concat(mm_input, dim=dim)
1046 +
1047 + def _get_feat_extract_output_lengths(
1048 + self, input_lengths: torch.Tensor
1049 + ) -> torch.Tensor:
1050 + input_lengths_leave = input_lengths % 100
1051 + feat_lengths = (input_lengths_leave - 1) // 2 + 1
1052 + output_lengths = (
1053 + ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
1054 + )
1055 + return output_lengths, output_lengths
1056 +
1057 + def _process_audio_input(
1058 + self,
1059 + audio_input: Qwen2AudioFeatureInputs,
1060 + audio_hashes: list[str] = None,
1061 + cached_audio_features: torch.Tensor = None,
1062 + ) -> torch.Tensor:
1063 + input_features = audio_input["input_features"]
1064 + audio_feature_lengths = audio_input["audio_feature_lengths"]
1065 +
1066 + if input_features.ndim == 3:
1067 + assert input_features.shape[0] == 1
1068 + input_features = input_features.squeeze(0)
1069 +
1070 + if not isinstance(audio_feature_lengths, torch.Tensor):
1071 + audio_feature_lengths = torch.cat(audio_feature_lengths)
1072 + if audio_feature_lengths.ndim == 2:
1073 + audio_feature_lengths = audio_feature_lengths.reshape(-1)
1074 +
1075 + audio_feat_lengths, audio_output_lengths = (
1076 + self._get_feat_extract_output_lengths(audio_feature_lengths)
1077 + )
1078 +
1079 + audio_outputs = self.audio_tower(
1080 + input_features.to(self.audio_tower.dtype),
1081 + feature_lens=audio_feature_lengths,
1082 + aftercnn_lens=audio_feat_lengths,
1083 + )
1084 + audio_features = audio_outputs.last_hidden_state
1085 + return audio_features.split(audio_output_lengths.tolist())
1086 +
1087 +
1088 +@MULTIMODAL_REGISTRY.register_processor(
1089 + Qwen3OmniMoeThinkerMultiModalProcessor,
1090 + info=Qwen3OmniMoeThinkerProcessingInfo,
1091 + dummy_inputs=Qwen3OmniMoeThinkerDummyInputsBuilder,
1092 +)
1093 +class Qwen3OmniMoeThinkerForConditionalGeneration(
1094 + nn.Module,
1095 + SupportsMultiModal,
1096 + SupportsPP,
1097 + Qwen3OmniMoeConditionalGenerationMixin,
1098 +):
1099 + hf_to_vllm_mapper = WeightsMapper(
1100 + orig_to_new_prefix={
1101 + "thinker.lm_head.": "language_model.lm_head.",
1102 + "thinker.model.": "language_model.model.",
1103 + "thinker.": "",
1104 + }
1105 + )
1106 +
1107 + @classmethod
1108 + def get_placeholder_str(cls, modality: str, i: int) -> Optional[str]:
1109 + if modality.startswith("image"):
1110 + return "<|vision_start|><|image_pad|><|vision_end|>"
1111 + if modality.startswith("video"):
1112 + return "<|vision_start|><|video_pad|><|vision_end|>"
1113 + if modality.startswith("audio"):
1114 + return "<|audio_start|><|audio_pad|><|audio_end|>"
1115 +
1116 + raise ValueError("Only image, video or audio modality is supported")
1117 +
1118 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1119 + super().__init__()
1120 + thinker_config: Qwen3OmniMoeThinkerConfig = (
1121 + vllm_config.model_config.hf_config.thinker_config
1122 + )
1123 + quant_config = vllm_config.quant_config
1124 + multimodal_config = vllm_config.model_config.multimodal_config
1125 + self.config = thinker_config
1126 + self.multimodal_config = multimodal_config
1127 +
1128 + # force "use_flash_attention_2=True" to audio tower to align
1129 + # the results.
1130 + if flash_attn is not None:
1131 + audio_config = thinker_config.audio_config
1132 + audio_config._attn_implementation_autoset = True
1133 + audio_config._attn_implementation = "flash_attention_2"
1134 + else:
1135 + logger.warning(
1136 + "flash_attn is not available, the model may not yield the "
1137 + "exactly same result as the transformers implementation "
1138 + "in the audio tower part."
1139 + )
1140 +
1141 + self.audio_tower = Qwen3OmniMoeAudioEncoder(thinker_config.audio_config)
1142 +
1143 + self.visual = Qwen3Omni_VisionTransformer(
1144 + vision_config=thinker_config.vision_config,
1145 + norm_eps=getattr(thinker_config.text_config, "rms_norm_eps", 1e-6),
1146 + quant_config=quant_config,
1147 + prefix=maybe_prefix(prefix, "visual"),
1148 + )
1149 + self.quant_config = quant_config
1150 +
1151 + self.language_model = Qwen3MoeLLMForCausalLM(
1152 + vllm_config=vllm_config.with_hf_config(
1153 + thinker_config.text_config, architectures=["Qwen3MoeForCausalLM"]
1154 + ),
1155 + prefix=maybe_prefix(prefix, "language_model"),
1156 + )
1157 +
1158 + self.make_empty_intermediate_tensors = (
1159 + self.language_model.make_empty_intermediate_tensors
1160 + )
1161 +
1162 + self.use_deepstack = hasattr(
1163 + thinker_config.vision_config, "deepstack_visual_indexes"
1164 + )
1165 + self.deepstack_num_level = (
1166 + len(thinker_config.vision_config.deepstack_visual_indexes)
1167 + if self.use_deepstack
1168 + else 0
1169 + )
1170 + # register buffer for deepstack
1171 + self.deepstack_input_embeds = (
1172 + [
1173 + torch.zeros(
1174 + vllm_config.scheduler_config.max_num_batched_tokens,
1175 + thinker_config.text_config.hidden_size,
1176 + )
1177 + for _ in range(self.deepstack_num_level)
1178 + ]
1179 + if self.use_deepstack
1180 + else None
1181 + )
1182 + self.visual_dim = thinker_config.vision_config.out_hidden_size
1183 + self.multiscale_dim = self.visual_dim * self.deepstack_num_level
1184 +
1185 + def _get_deepstack_input_embeds(self, num_tokens: int) -> IntermediateTensors:
1186 + # get deepstack_input_embeds from buffer, and clear the buffer
1187 + return IntermediateTensors(
1188 + {
1189 + f"deepstack_input_embeds_{idx}": self.deepstack_input_embeds[idx][
1190 + :num_tokens
1191 + ]
1192 + for idx in range(self.deepstack_num_level)
1193 + }
1194 + )
1195 +
1196 + def _set_deepstack_input_embeds(self, deepstack_input_embeds: torch.Tensor) -> None:
1197 + # set deepstack_input_embeds to buffer
1198 + num_tokens = deepstack_input_embeds.size(1)
1199 + if num_tokens > self.deepstack_input_embeds[0].size(0):
1200 + self.deepstack_input_embeds = [
1201 + torch.zeros(
1202 + num_tokens,
1203 + self.config.text_config.hidden_size,
1204 + device=self.deepstack_input_embeds[0].device,
1205 + dtype=self.deepstack_input_embeds[0].dtype,
1206 + )
1207 + for _ in range(self.deepstack_num_level)
1208 + ]
1209 + for idx in range(self.deepstack_num_level):
1210 + self.deepstack_input_embeds[idx][:num_tokens].copy_(
1211 + deepstack_input_embeds[idx]
1212 + )
1213 +
1214 + def _clear_deepstack_input_embeds(self, num_tokens: int) -> None:
1215 + # clear deepstack_input_embeds in buffer
1216 + if num_tokens > 0:
1217 + for idx in range(self.deepstack_num_level):
1218 + self.deepstack_input_embeds[idx][:num_tokens].zero_()
1219 +
1220 + def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1221 + mm_input_by_modality = {}
1222 +
1223 + # Preserve the order of modalities if there are multiple of them
1224 + # from the order of kwargs.
1225 + for input_key in kwargs:
1226 + if (
1227 + input_key in ("pixel_values", "image_embeds")
1228 + and "image" not in mm_input_by_modality
1229 + ):
1230 + mm_input_by_modality["image"] = self._parse_and_validate_image_input(
1231 + **kwargs
1232 + )
1233 + if (
1234 + input_key in ("pixel_values_videos", "video_embeds")
1235 + and "video" not in mm_input_by_modality
1236 + ):
1237 + mm_input_by_modality["video"] = self._parse_and_validate_video_input(
1238 + **kwargs
1239 + )
1240 + if (
1241 + input_key in ("input_audio_features")
1242 + and "audio" not in mm_input_by_modality
1243 + ):
1244 + mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(
1245 + **kwargs
1246 + )
1247 + return mm_input_by_modality
1248 +
1249 + def get_language_model(self) -> torch.nn.Module:
1250 + return self.language_model
1251 +
1252 + def get_multimodal_embeddings(
1253 + self, **kwargs: object
1254 + ) -> Optional[MultiModalEmbeddings]:
1255 + mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
1256 + if not mm_input_by_modality:
1257 + return []
1258 +
1259 + # The result multimodal_embeddings is tuple of tensors, with each
1260 + # tensor correspoending to a multimodal data item (image or video).
1261 + multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1262 +
1263 + # NOTE: It is important to iterate over the keys in this dictionary
1264 + # to preserve the order of the modalities.
1265 + for modality in mm_input_by_modality:
1266 + multimodal_input = mm_input_by_modality[modality]
1267 + if modality == "image":
1268 + vision_embeddings = self._process_image_input(multimodal_input)
1269 + multimodal_embeddings += vision_embeddings
1270 + if modality == "video":
1271 + video_embeddings = self._process_video_input(multimodal_input)
1272 + multimodal_embeddings += video_embeddings
1273 + if modality == "audio":
1274 + audio_embeddings = self._process_audio_input(multimodal_input)
1275 + multimodal_embeddings += audio_embeddings
1276 + return multimodal_embeddings
1277 +
1278 + def get_input_embeddings(
1279 + self,
1280 + input_ids: torch.Tensor,
1281 + multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
1282 + *,
1283 + is_multimodal: Optional[torch.Tensor] = None,
1284 + handle_oov_mm_token: bool = False,
1285 + ) -> torch.Tensor:
1286 + inputs_embeds = self._get_text_embeddings(
1287 + input_ids,
1288 + self.language_model.get_input_embeddings,
1289 + is_multimodal=is_multimodal,
1290 + handle_oov_mm_token=handle_oov_mm_token,
1291 + )
1292 +
1293 + if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
1294 + return inputs_embeds
1295 +
1296 + deepstack_input_embeds = None
1297 + # TODO (ywang96): support overlapping modalitiy embeddings so that
1298 + # `use_audio_in_video` will work on V1.
1299 + # split the feat dim to obtain multi-scale visual feature
1300 + has_vision_embeddings = [
1301 + embeddings.shape[-1] != self.config.text_config.hidden_size
1302 + for embeddings in multimodal_embeddings
1303 + ]
1304 + if self.visual.deepstack_visual_indexes is not None and any(
1305 + has_vision_embeddings
1306 + ):
1307 + multiscale_len = len(self.visual.deepstack_visual_indexes)
1308 + multimodal_embeddings_multiscale = []
1309 + is_vision = torch.zeros_like(is_multimodal)
1310 + mm_positions = torch.nonzero(is_multimodal, as_tuple=True)[0]
1311 + mm_position_idx = 0
1312 + for index, embeddings in enumerate(multimodal_embeddings):
1313 + num_tokens = embeddings.shape[0]
1314 + current_positions = mm_positions[
1315 + mm_position_idx : mm_position_idx + num_tokens
1316 + ]
1317 +
1318 + # Vision embeddings
1319 + if embeddings.shape[-1] != self.config.text_config.hidden_size:
1320 + visual_dim = embeddings.shape[-1] // (multiscale_len + 1)
1321 + multi_dim = visual_dim * multiscale_len
1322 + embeddings_main, embeddings_multiscale = torch.split(
1323 + embeddings, [visual_dim, multi_dim], dim=-1
1324 + )
1325 + multimodal_embeddings[index] = embeddings_main
1326 + multimodal_embeddings_multiscale.append(embeddings_multiscale)
1327 + is_vision[current_positions] = True
1328 +
1329 + # Audio embeddings
1330 + else:
1331 + is_vision[current_positions] = False
1332 +
1333 + mm_position_idx += num_tokens
1334 +
1335 + deepstack_input_embeds = inputs_embeds.new_zeros(
1336 + inputs_embeds.size(0), multiscale_len * inputs_embeds.size(1)
1337 + )
1338 + deepstack_input_embeds = _merge_multimodal_embeddings(
1339 + inputs_embeds=deepstack_input_embeds,
1340 + multimodal_embeddings=multimodal_embeddings_multiscale,
1341 + is_multimodal=is_vision,
1342 + )
1343 + deepstack_input_embeds = (
1344 + deepstack_input_embeds.view(
1345 + inputs_embeds.shape[0], multiscale_len, visual_dim
1346 + )
1347 + .permute(1, 0, 2)
1348 + .contiguous()
1349 + )
1350 + self._set_deepstack_input_embeds(deepstack_input_embeds)
1351 +
1352 + inputs_embeds = _merge_multimodal_embeddings(
1353 + inputs_embeds=inputs_embeds,
1354 + multimodal_embeddings=multimodal_embeddings,
1355 + is_multimodal=is_multimodal,
1356 + )
1357 +
1358 + return inputs_embeds
1359 +
1360 + def forward(
1361 + self,
1362 + input_ids: torch.Tensor,
1363 + positions: torch.Tensor,
1364 + intermediate_tensors: Optional[IntermediateTensors] = None,
1365 + inputs_embeds: Optional[torch.Tensor] = None,
1366 + **kwargs: object,
1367 + ) -> Union[torch.Tensor, IntermediateTensors]:
1368 + if intermediate_tensors is not None:
1369 + inputs_embeds = None
1370 +
1371 + if (
1372 + self.use_deepstack
1373 + and inputs_embeds is not None
1374 + and get_pp_group().is_first_rank
1375 + ):
1376 + deepstack_input_embeds = self._get_deepstack_input_embeds(
1377 + inputs_embeds.size(0)
1378 + )
1379 + else:
1380 + deepstack_input_embeds = None
1381 +
1382 + hidden_states = self.language_model.model(
1383 + input_ids,
1384 + positions,
1385 + intermediate_tensors,
1386 + inputs_embeds=inputs_embeds,
1387 + # args for deepstack
1388 + deepstack_input_embeds=deepstack_input_embeds,
1389 + )
1390 +
1391 + if inputs_embeds is not None and get_pp_group().is_first_rank:
1392 + self._clear_deepstack_input_embeds(inputs_embeds.size(0))
1393 +
1394 + return hidden_states
1395 +
1396 + def compute_logits(
1397 + self,
1398 + hidden_states: torch.Tensor,
1399 + ) -> Optional[torch.Tensor]:
1400 + return self.language_model.compute_logits(hidden_states)
1401 +
1402 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1403 + loader = AutoWeightsLoader(
1404 + self,
1405 + skip_prefixes=["talker.", "code2wav."],
1406 + )
1407 + loaded_weights = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1408 +
1409 + return loaded_weights
vllm/model_executor/models/registry.py
+4
@@ -355,6 +355,10 @@ _MULTIMODAL_MODELS = {
355 "qwen2_5_omni_thinker",
356 "Qwen2_5OmniThinkerForConditionalGeneration",
357 ),
358 + "Qwen3OmniMoeForConditionalGeneration": (
359 + "qwen3_omni_moe_thinker",
360 + "Qwen3OmniMoeThinkerForConditionalGeneration",
361 + ),
362 "Qwen3VLForConditionalGeneration": ("qwen3_vl", "Qwen3VLForConditionalGeneration"), # noqa: E501
363 "Qwen3VLMoeForConditionalGeneration": (
364 "qwen3_vl_moe",