[Bugfix][Multimodal] Fix Qwen3-Omni use_audio_in_video with mixed image/video inputs (#46213)
Signed-off-by: wendadawen <wendadawen@qq.com> Signed-off-by: Tianyu Guo <guoty9@mail2.sysu.edu.cn> Co-authored-by: Tianyu Guo <guoty9@mail2.sysu.edu.cn>
wendadawen committed
Jul 17, 2026 at 16:31 UTC
69d4f5ef6323a44621b10e0f269c2775db510cc5
8 files changed
+266
-83
tests/models/multimodal/processing/test_qwen2_5_omni_embed.py
+87
-7
@@ -19,6 +19,7 @@ from vllm.model_executor.models.qwen2_5_omni_thinker import (
19
check_interleaved_audio_video,
20
merge_interleaved_embeddings,
21
)
22
+from vllm.multimodal.utils import set_mm_embedding_modality
23
24
# Fake token IDs
25
AUDIO_TOKEN_ID = 1001
@@ -27,6 +28,10 @@ VIDEO_TOKEN_ID = 1003
28
TEXT_TOKEN_ID = 0
29
30
31
+def _mm_embed(shape: tuple[int, ...], value: float, modality: str) -> torch.Tensor:
32
+ return set_mm_embedding_modality(torch.full(shape, value), modality)
33
+
34
+
35
# ---------------------------------------------------------------------------
36
# Helpers
37
# ---------------------------------------------------------------------------
@@ -116,6 +121,24 @@ class TestCheckInterleavedAudioVideo:
121
is_video, is_audio, is_video.sum().item(), is_audio.sum().item()
122
)
123
124
+ def test_multi_video_with_boundary_tokens(self):
125
+ """Two interleaved videos separated by boundary tokens → still True.
126
+
127
+ use_audio_in_video expands each video into a local V/A span bounded by
128
+ non-pad tokens. A global density check would fail across those gaps.
129
+ """
130
+ # [text][V A V A][text boundary][V A V A]
131
+ first_ids, _ = make_interleaved_seq([2, 2], [2, 2], text_prefix=1)
132
+ second_ids, _ = make_interleaved_seq([2, 2], [2, 2], text_prefix=0)
133
+ boundary = torch.tensor([TEXT_TOKEN_ID, TEXT_TOKEN_ID])
134
+ input_ids = torch.cat([first_ids, boundary, second_ids])
135
+ is_multimodal = (input_ids == VIDEO_TOKEN_ID) | (input_ids == AUDIO_TOKEN_ID)
136
+ is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
137
+ is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
138
+ assert check_interleaved_audio_video(
139
+ is_video, is_audio, is_video.sum().item(), is_audio.sum().item()
140
+ )
141
+
142
def test_batched_non_interleaved_no_false_positive(self):
143
"""
144
Regression test for https://github.com/vllm-project/vllm/issues/35394.
@@ -323,13 +346,16 @@ class TestEmbedInputIds:
346
# mm_embeds come in [video, audio] order (video feature first in
347
# mm_features when positions are the same for use_audio_in_video)
348
mm_embeds = [
326
- torch.full((video_n, hidden), video_val),
327
- torch.full((audio_n, hidden), audio_val),
349
+ _mm_embed((video_n, hidden), video_val, "video"),
350
+ _mm_embed((audio_n, hidden), audio_val, "audio"),
351
]
352
353
model, _ = make_mock_model(hidden)
354
+ # Modalities are attached on the embedding tensors (as in encoder gather).
355
result = model.embed_input_ids(
332
- input_ids, mm_embeds, is_multimodal=is_multimodal
356
+ input_ids,
357
+ mm_embeds,
358
+ is_multimodal=is_multimodal,
359
)
360
361
video_pos = (input_ids == VIDEO_TOKEN_ID).nonzero(as_tuple=True)[0]
@@ -362,8 +388,8 @@ class TestMergeInterleavedEmbeddings:
388
389
inputs_embeds = torch.zeros(len(input_ids), hidden)
390
mm_embeds = [
365
- torch.full((num_video, hidden), 30.0),
366
- torch.full((num_audio, hidden), 10.0),
391
+ _mm_embed((num_video, hidden), 30.0, "video"),
392
+ _mm_embed((num_audio, hidden), 10.0, "audio"),
393
]
394
395
result = merge_interleaved_embeddings(
@@ -372,8 +398,6 @@ class TestMergeInterleavedEmbeddings:
398
is_video,
399
is_audio,
400
is_multimodal,
375
- num_video,
376
- num_audio,
401
)
402
403
video_pos = is_video.nonzero(as_tuple=True)[0]
@@ -381,6 +405,62 @@ class TestMergeInterleavedEmbeddings:
405
assert result[video_pos].allclose(torch.full((num_video, hidden), 30.0))
406
assert result[audio_pos].allclose(torch.full((num_audio, hidden), 10.0))
407
408
+ def test_image_and_video_mixed(self):
409
+ """Image embeddings must not be misclassified as video."""
410
+ hidden = 4
411
+ # [text][I I][V A V A]
412
+ tokens = (
413
+ [TEXT_TOKEN_ID] * 2
414
+ + [IMAGE_TOKEN_ID] * 2
415
+ + [VIDEO_TOKEN_ID, AUDIO_TOKEN_ID, VIDEO_TOKEN_ID, AUDIO_TOKEN_ID]
416
+ )
417
+ input_ids = torch.tensor(tokens)
418
+ is_multimodal = (
419
+ (input_ids == IMAGE_TOKEN_ID)
420
+ | (input_ids == VIDEO_TOKEN_ID)
421
+ | (input_ids == AUDIO_TOKEN_ID)
422
+ )
423
+ is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
424
+ is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
425
+ is_image = is_multimodal & (input_ids == IMAGE_TOKEN_ID)
426
+
427
+ inputs_embeds = torch.zeros(len(input_ids), hidden)
428
+ mm_embeds = [
429
+ _mm_embed((2, hidden), 20.0, "image"),
430
+ _mm_embed((2, hidden), 30.0, "video"),
431
+ _mm_embed((2, hidden), 10.0, "audio"),
432
+ ]
433
+ result = merge_interleaved_embeddings(
434
+ inputs_embeds,
435
+ mm_embeds,
436
+ is_video,
437
+ is_audio,
438
+ is_multimodal,
439
+ )
440
+ assert result[is_image.nonzero(as_tuple=True)[0]].allclose(
441
+ torch.full((2, hidden), 20.0)
442
+ )
443
+ assert result[is_video.nonzero(as_tuple=True)[0]].allclose(
444
+ torch.full((2, hidden), 30.0)
445
+ )
446
+ assert result[is_audio.nonzero(as_tuple=True)[0]].allclose(
447
+ torch.full((2, hidden), 10.0)
448
+ )
449
+
450
+ def test_missing_modality_raises(self):
451
+ hidden = 2
452
+ input_ids, is_multimodal = make_interleaved_seq([2], [2])
453
+ is_video = is_multimodal & (input_ids == VIDEO_TOKEN_ID)
454
+ is_audio = is_multimodal & (input_ids == AUDIO_TOKEN_ID)
455
+ with pytest.raises(ValueError, match="Missing modality"):
456
+ merge_interleaved_embeddings(
457
+ torch.zeros(len(input_ids), hidden),
458
+ [torch.zeros(2, hidden), torch.zeros(2, hidden)],
459
+ is_video,
460
+ is_audio,
461
+ is_multimodal,
462
+ )
463
+
464
465
if __name__ == "__main__":
466
pytest.main([__file__, "-v"])
tests/v1/worker/test_encoder_runner.py
+26
@@ -78,6 +78,7 @@ def test_draft_lookahead_uses_boundary_feature_when_cached():
78
79
# f0 covers positions 0..6 (+1 skew); f1's first embed covers position 7.
80
assert len(mm_embeds) == 2
81
+ assert [e.modality for e in mm_embeds] == ["image", "image"]
82
assert bool(is_mm_embed[7])
83
assert int(is_mm_embed.sum()) == 8
84
@@ -94,6 +95,7 @@ def test_draft_lookahead_tolerates_missing_boundary_feature():
95
96
# Only f0 is gathered; f1's boundary position falls back silently.
97
assert len(mm_embeds) == 1
98
+ assert [e.modality for e in mm_embeds] == ["image"]
99
assert not bool(is_mm_embed[7])
100
assert int(is_mm_embed.sum()) == 7
101
@@ -154,4 +156,28 @@ def test_multi_request_batch_gathers_per_request(draft_lookahead):
156
# Both requests contribute a feature; with the +1 skew each marks 7 of its
157
# 8 positions (the skew drops one), otherwise all 8.
158
assert len(mm_embeds) == 2
159
+ assert [e.modality for e in mm_embeds] == ["image", "image"]
160
assert int(is_mm_embed.sum()) == (14 if draft_lookahead else 16)
161
+
162
+
163
+def test_gather_preserves_mixed_modalities():
164
+ """Modalities must be attached on tensors in gather order."""
165
+ video = MultiModalFeatureSpec(
166
+ data=None,
167
+ modality="video",
168
+ identifier="v0",
169
+ mm_position=PlaceholderRange(offset=0, length=4),
170
+ )
171
+ audio = MultiModalFeatureSpec(
172
+ data=None,
173
+ modality="audio",
174
+ identifier="a0",
175
+ mm_position=PlaceholderRange(offset=4, length=4),
176
+ )
177
+ runner = _make_runner([video, audio], cached=[video, audio])
178
+
179
+ mm_embeds, is_mm_embed = _gather(runner, num_scheduled=8, draft_lookahead=0)
180
+
181
+ assert len(mm_embeds) == 2
182
+ assert [e.modality for e in mm_embeds] == ["video", "audio"]
183
+ assert int(is_mm_embed.sum()) == 8
vllm/model_executor/models/qwen2_5_omni_thinker.py
+77
-66
@@ -138,42 +138,44 @@ def check_interleaved_audio_video(
138
num_audio: int,
139
) -> bool:
140
"""
141
- Check if video and audio positions are interleaved in the multimodal region.
142
-
143
- Returns True only for the use_audio_in_video=True case, where video and
144
- audio tokens alternate within a single contiguous region with no gaps.
145
-
146
- A simple range-overlap check produces false positives when multiple
147
- non-interleaved requests are batched together: audio tokens from request N
148
- fall between video tokens from request N and request N+1, making the
149
- global ranges overlap even though each individual request is non-interleaved.
150
-
151
- To distinguish true interleaving from this batching artefact we require
152
- that every position in the combined [first_VA, last_VA] range is occupied
153
- by either a video or an audio token (no text/image gaps).
141
+ Check if video and audio positions are interleaved in any per-video span.
142
+
143
+ For use_audio_in_video=True, each video placeholder is expanded into one
144
+ local span containing only video/audio pad tokens, bounded by non-pad
145
+ tokens such as audio_start/audio_end. Check each contiguous V/A span
146
+ independently instead of requiring all V/A tokens in the whole sequence to
147
+ form one global dense range; otherwise multi-video requests can be
148
+ misclassified as non-interleaved because of the boundary tokens between
149
+ videos.
150
"""
151
if num_video == 0 or num_audio == 0:
152
return False
153
158
- video_pos = is_video.nonzero(as_tuple=True)[0]
159
- audio_pos = is_audio.nonzero(as_tuple=True)[0]
160
-
161
- # Quick range-overlap pre-check (necessary but not sufficient).
162
- if not (
163
- video_pos[0].item() < audio_pos[-1].item()
164
- and audio_pos[0].item() < video_pos[-1].item()
165
- ):
154
+ va_pos = (is_video | is_audio).nonzero(as_tuple=True)[0]
155
+ if len(va_pos) == 0:
156
return False
157
168
- # Density check: for true use_audio_in_video interleaving every position
169
- # in the combined span is a video or audio token. Batched non-interleaved
170
- # requests have text/image tokens between the per-request V and A blocks.
171
- # combined_start/end encompass all V/A tokens, so num_video + num_audio
172
- # equals the number of V/A tokens in range; compare directly to span size.
173
- combined_start = min(video_pos[0].item(), audio_pos[0].item())
174
- combined_end = max(video_pos[-1].item(), audio_pos[-1].item())
175
- total_in_range = combined_end - combined_start + 1
176
- return (num_video + num_audio) == total_in_range
158
+ span_start = 0
159
+ for span_end in range(1, len(va_pos) + 1):
160
+ is_last = span_end == len(va_pos)
161
+ if not is_last and va_pos[span_end].item() == va_pos[span_end - 1].item() + 1:
162
+ continue
163
+
164
+ span = va_pos[span_start:span_end]
165
+ span_is_video = is_video[span]
166
+ span_is_audio = is_audio[span]
167
+ if span_is_video.any() and span_is_audio.any():
168
+ video_offsets = span_is_video.nonzero(as_tuple=True)[0]
169
+ audio_offsets = span_is_audio.nonzero(as_tuple=True)[0]
170
+ if (
171
+ video_offsets[0].item() < audio_offsets[-1].item()
172
+ and audio_offsets[0].item() < video_offsets[-1].item()
173
+ ):
174
+ return True
175
+
176
+ span_start = span_end
177
+
178
+ return False
179
180
181
def merge_interleaved_embeddings(
@@ -182,58 +184,69 @@ def merge_interleaved_embeddings(
184
is_video: torch.Tensor,
185
is_audio: torch.Tensor,
186
is_multimodal: torch.Tensor,
185
- num_video: int,
186
- num_audio: int,
187
) -> torch.Tensor:
188
"""
189
Merge embeddings for interleaved audio-in-video sequences.
190
191
When use_audio_in_video=True, video and audio tokens are interleaved in
192
the token sequence, but embeddings are provided as separate contiguous
193
- tensors (video first, then audio). This function reorders video and audio
194
- embeddings to match sequence position order and scatters them efficiently.
193
+ tensors. This function scatters each modality by the ``modality`` attribute
194
+ attached to each embedding tensor (set during encoder gather) and also
195
+ supports image embeddings in the same interleaved request.
196
197
Args:
198
inputs_embeds: The input embeddings tensor to merge into.
198
- multimodal_embeddings: List of embedding tensors (video, audio, other).
199
+ multimodal_embeddings: List of embedding tensors (video, audio, image).
200
is_video: Boolean mask for video token positions.
201
is_audio: Boolean mask for audio token positions.
202
is_multimodal: Boolean mask for all multimodal token positions.
202
- num_video: Total count of video tokens.
203
- num_audio: Total count of audio tokens.
203
204
Returns:
205
The merged inputs_embeds tensor with multimodal embeddings scattered
206
to their correct positions.
207
"""
209
- # Categorize embeddings by modality based on token counts.
210
- # Embeddings come grouped by modality but order varies (e.g., image, video, audio
211
- # or video, audio depending on input kwargs order).
212
- video_embeds: list[torch.Tensor] = []
213
- audio_embeds: list[torch.Tensor] = []
214
- other_embeds: list[torch.Tensor] = []
215
- video_remaining = num_video
216
- audio_remaining = num_audio
217
-
218
- for emb in multimodal_embeddings:
219
- n = emb.shape[0]
220
- if video_remaining > 0 and n <= video_remaining:
221
- video_embeds.append(emb)
222
- video_remaining -= n
223
- elif audio_remaining > 0 and n <= audio_remaining:
224
- audio_embeds.append(emb)
225
- audio_remaining -= n
226
- else:
227
- other_embeds.append(emb)
208
+ from vllm.multimodal.utils import get_mm_embedding_modalities
209
+
210
+ def _merge_embedding_group(
211
+ mask: torch.Tensor,
212
+ embeddings: Sequence[torch.Tensor],
213
+ modality: str,
214
+ ) -> None:
215
+ num_expected_tokens = mask.sum().item()
216
+ num_actual_tokens = sum(embedding.shape[0] for embedding in embeddings)
217
+ if num_actual_tokens != num_expected_tokens:
218
+ raise ValueError(
219
+ f"Attempted to assign {num_actual_tokens} {modality} tokens "
220
+ f"to {num_expected_tokens} placeholders"
221
+ )
222
+ if embeddings:
223
+ inputs_embeds[mask] = torch.cat(list(embeddings), dim=0)
224
+
225
+ # Qwen Omni multimodal placeholders are image/video/audio; after excluding
226
+ # video and audio positions, the remaining multimodal positions are images.
227
+ is_image = is_multimodal & ~is_video & ~is_audio
228
+
229
+ embedding_modalities = get_mm_embedding_modalities(multimodal_embeddings)
230
+
231
+ video_embeds = [
232
+ embedding
233
+ for embedding, modality in zip(multimodal_embeddings, embedding_modalities)
234
+ if modality == "video"
235
+ ]
236
+ audio_embeds = [
237
+ embedding
238
+ for embedding, modality in zip(multimodal_embeddings, embedding_modalities)
239
+ if modality == "audio"
240
+ ]
241
+ image_embeds = [
242
+ embedding
243
+ for embedding, modality in zip(multimodal_embeddings, embedding_modalities)
244
+ if modality == "image"
245
+ ]
246
229
- # Scatter each modality to its positions
230
- if video_embeds:
231
- inputs_embeds[is_video] = torch.cat(video_embeds, dim=0)
232
- if audio_embeds:
233
- inputs_embeds[is_audio] = torch.cat(audio_embeds, dim=0)
234
- if other_embeds:
235
- other_mask = is_multimodal & ~is_video & ~is_audio
236
- inputs_embeds[other_mask] = torch.cat(other_embeds, dim=0)
247
+ _merge_embedding_group(is_video, video_embeds, "video")
248
+ _merge_embedding_group(is_audio, audio_embeds, "audio")
249
+ _merge_embedding_group(is_image, image_embeds, "image")
250
251
return inputs_embeds
252
@@ -1497,8 +1510,6 @@ class Qwen2_5OmniThinkerForConditionalGeneration(
1510
is_video,
1511
is_audio,
1512
is_multimodal,
1500
- num_video,
1501
- num_audio,
1513
)
1514
1515
# Default: standard merge (no interleaving), same as parent class
vllm/model_executor/models/qwen3_omni_moe_thinker.py
+5
-5
@@ -1833,9 +1833,11 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
1833
# both the deepstack path and the final embedding merge.
1834
video_token_id = self.config.video_token_id
1835
audio_token_id = self.config.audio_token_id
1836
+ image_token_id = self.config.image_token_id
1837
input_ids_cpu = input_ids.cpu()
1838
is_video = is_multimodal & (input_ids_cpu == video_token_id)
1839
is_audio = is_multimodal & (input_ids_cpu == audio_token_id)
1840
+ is_image = is_multimodal & (input_ids_cpu == image_token_id)
1841
num_video = is_video.sum().item()
1842
num_audio = is_audio.sum().item()
1843
@@ -1856,9 +1858,9 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
1858
multimodal_embeddings_multiscale = []
1859
1860
if is_interleaved:
1859
- # Use input_ids-based mask for correct vision positions
1860
- # when audio and video tokens are interleaved.
1861
- is_vision = is_video.clone()
1861
+ # Use input_ids-based mask for all vision positions when
1862
+ # audio and video tokens are interleaved.
1863
+ is_vision = is_video | is_image
1864
else:
1865
is_vision = torch.zeros_like(is_multimodal)
1866
mm_positions = torch.nonzero(is_multimodal, as_tuple=True)[0]
@@ -1917,8 +1919,6 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
1919
is_video,
1920
is_audio,
1921
is_multimodal,
1920
- num_video,
1921
- num_audio,
1922
)
1923
1924
# Default: standard merge (no interleaving), same as parent class.
vllm/multimodal/utils.py
+38
@@ -346,3 +346,41 @@ def fetch_video(
346
allowed_local_media_path="/",
347
)
348
return media_connector.fetch_video(video_url)
349
+
350
+
351
+def set_mm_embedding_modality(embed: "torch.Tensor", modality: str) -> "torch.Tensor":
352
+ """Attach modality metadata to a gathered multimodal embedding tensor.
353
+
354
+ Used by interleaved Omni merge paths that need to group embeddings by
355
+ modality without threading a parallel modalities list through
356
+ ``embed_input_ids``.
357
+ """
358
+ embed.modality = modality # type: ignore[attr-defined]
359
+ return embed
360
+
361
+
362
+def copy_mm_embedding_modality(
363
+ src: "torch.Tensor", dst: "torch.Tensor"
364
+) -> "torch.Tensor":
365
+ """Copy ``modality`` from ``src`` onto ``dst`` if present."""
366
+ modality = getattr(src, "modality", None)
367
+ if modality is not None:
368
+ dst.modality = modality # type: ignore[attr-defined]
369
+ return dst
370
+
371
+
372
+def get_mm_embedding_modalities(
373
+ multimodal_embeddings: Sequence["torch.Tensor"],
374
+) -> list[str]:
375
+ """Collect per-embedding modalities previously set on the tensors."""
376
+ modalities: list[str] = []
377
+ for i, emb in enumerate(multimodal_embeddings):
378
+ modality = getattr(emb, "modality", None)
379
+ if modality is None:
380
+ raise ValueError(
381
+ f"Missing modality on multimodal embedding at index {i}. "
382
+ "Encoder gather must set embed.modality before interleaved "
383
+ "audio-in-video merge."
384
+ )
385
+ modalities.append(modality)
386
+ return modalities
vllm/v1/worker/gpu/mm/encoder_runner.py
+8
-1
@@ -5,7 +5,11 @@ import torch
5
6
from vllm.model_executor.models.interfaces import SupportsMultiModal, supports_realtime
7
from vllm.multimodal.inputs import MultiModalKwargsItem
8
-from vllm.multimodal.utils import get_mm_features_in_window, group_and_batch_mm_kwargs
8
+from vllm.multimodal.utils import (
9
+ get_mm_features_in_window,
10
+ group_and_batch_mm_kwargs,
11
+ set_mm_embedding_modality,
12
+)
13
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
14
from vllm.v1.worker.utils import sanity_check_mm_encoder_outputs
15
@@ -136,6 +140,9 @@ class EncoderRunner:
140
else:
141
mm_embeds_item = encoder_output[start_idx:end_idx]
142
143
+ # Attach modality for Omni interleaved merge (collected on demand).
144
+ set_mm_embedding_modality(mm_embeds_item, mm_feature.modality)
145
+
146
req_start_pos = query_start_loc[i] + start_pos - cur_query_start
147
is_mm_embed[req_start_pos + start_idx : req_start_pos + end_idx] |= (
148
True if is_embed is None else is_embed
vllm/v1/worker/gpu/model_states/mm_pruning.py
+13
-3
@@ -5,7 +5,10 @@ import torch.nn as nn
5
6
from vllm.config import ModelConfig
7
from vllm.model_executor.models.interfaces import supports_multimodal_pruning
8
-from vllm.multimodal.utils import get_mm_features_in_window
8
+from vllm.multimodal.utils import (
9
+ copy_mm_embedding_modality,
10
+ get_mm_features_in_window,
11
+)
12
from vllm.v1.worker.gpu.input_batch import InputBatch
13
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
14
from vllm.v1.worker.gpu.mm.rope import RopeState
@@ -42,7 +45,11 @@ class MultiModalPruner:
45
speculator reuses the target's already-recomputed positions, hence there is
46
no position write-back here.
47
"""
45
- return [mm[:, : self.inputs_embeds_size] for mm in mm_embeds]
48
+ stripped: list[torch.Tensor] = []
49
+ for mm in mm_embeds:
50
+ out = mm[:, : self.inputs_embeds_size]
51
+ stripped.append(copy_mm_embedding_modality(mm, out))
52
+ return stripped
53
54
def recompute(
55
self,
@@ -82,7 +89,10 @@ class MultiModalPruner:
89
num_computed_tokens=num_computed,
90
)
91
self.rope_state.update_prefill_positions(req_idx, new_positions, delta)
85
- cleaned.extend(req_cleaned)
92
+ cleaned.extend(
93
+ copy_mm_embedding_modality(src, dst)
94
+ for src, dst in zip(req_embeds, req_cleaned)
95
+ )
96
97
assert pos == len(mm_embeds)
98
return cleaned
vllm/v1/worker/gpu_model_runner.py
+12
-1
@@ -105,7 +105,12 @@ from vllm.multimodal.inputs import (
105
MultiModalKwargsItem,
106
PlaceholderRange,
107
)
108
-from vllm.multimodal.utils import get_mm_features_in_window, group_and_batch_mm_kwargs
108
+from vllm.multimodal.utils import (
109
+ copy_mm_embedding_modality,
110
+ get_mm_features_in_window,
111
+ group_and_batch_mm_kwargs,
112
+ set_mm_embedding_modality,
113
+)
114
from vllm.platforms import current_platform
115
from vllm.pooling_params import PoolingParams
116
from vllm.sampling_params import SamplingType
@@ -3245,11 +3250,13 @@ class GPUModelRunner(
3250
is_mm_embed[
3251
req_start_pos + start_idx : req_start_pos + end_idx
3252
] |= is_embed
3253
+ set_mm_embedding_modality(mm_embeds_item, mm_feature.modality)
3254
mm_embeds_req.append(mm_embeds_item)
3255
3256
if self.is_multimodal_pruning_enabled and self.uses_mrope:
3257
assert req_state.mrope_positions is not None
3258
should_sync_mrope_positions = True
3259
+ old_mm_embeds_req = mm_embeds_req
3260
mm_embeds_req, new_mrope_positions, new_delta = (
3261
self.model.recompute_mrope_positions(
3262
input_ids=req_state.prompt_token_ids,
@@ -3258,6 +3265,10 @@ class GPUModelRunner(
3265
num_computed_tokens=req_state.num_computed_tokens,
3266
)
3267
)
3268
+ mm_embeds_req = [
3269
+ copy_mm_embedding_modality(src, dst)
3270
+ for src, dst in zip(old_mm_embeds_req, mm_embeds_req)
3271
+ ]
3272
req_state.mrope_positions.copy_(new_mrope_positions)
3273
req_state.mrope_position_delta = new_delta
3274