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 13, 15:45 UTC
[Core][MM] Add mechanism to configure multimodal fields which should stay on CPU (#28168)
Signed-off-by: Lukas Geiger <lukas.geiger94@gmail.com>
Lukas Geiger committed
Nov 7, 2025 at 12:14 UTC
e0919f331d12dc5dbdefd0775bb6f94dd2fab4e2
7 files changed
+68
-74
vllm/model_executor/models/interfaces.py
+6
-1
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from collections.abc import Callable, Iterable, Mapping, MutableSequence
4
+from collections.abc import Callable, Iterable, Mapping, MutableSequence, Set
5
from typing import (
6
TYPE_CHECKING,
7
ClassVar,
@@ -81,6 +81,11 @@ class SupportsMultiModal(Protocol):
81
`vllm.multimodal.utils.group_mm_kwargs_by_modality` to use.
82
"""
83
84
+ multimodal_cpu_fields: ClassVar[Set[str]] = frozenset()
85
+ """
86
+ A set indicating CPU-only multimodal fields.
87
+ """
88
+
89
@classmethod
90
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
91
"""
vllm/model_executor/models/qwen2_5_vl.py
+3
-12
@@ -1090,6 +1090,7 @@ class Qwen2_5_VLForConditionalGeneration(
1090
SupportsMRoPE,
1091
):
1092
merge_by_field_config = True
1093
+ multimodal_cpu_fields = {"image_grid_thw", "video_grid_thw"}
1094
1095
packed_modules_mapping = {
1096
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
@@ -1364,13 +1365,8 @@ class Qwen2_5_VLForConditionalGeneration(
1365
image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
1366
1367
# Split concatenated embeddings for each image item.
1367
- # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1368
merge_size = self.visual.spatial_merge_size
1369
- sizes = (
1370
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1371
- // (merge_size * merge_size)
1372
- ).tolist()
1373
-
1369
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1370
return image_embeds.split(sizes)
1371
1372
def _postprocess_image_embeds_evs(
@@ -1430,12 +1426,7 @@ class Qwen2_5_VLForConditionalGeneration(
1426
1427
# Split concatenated embeddings for each video item.
1428
merge_size = self.visual.spatial_merge_size
1433
- # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1434
- sizes = (
1435
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1436
- // (merge_size * merge_size)
1437
- ).tolist()
1438
-
1429
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1430
return video_embeds.split(sizes)
1431
1432
def _postprocess_video_embeds_evs(
vllm/model_executor/models/qwen2_vl.py
+18
-20
@@ -798,21 +798,27 @@ class Qwen2VisionTransformer(nn.Module):
798
def forward(
799
self,
800
x: torch.Tensor,
801
- grid_thw: list[list[int]],
801
+ grid_thw: torch.Tensor | list[list[int]],
802
) -> torch.Tensor:
803
# patchify
804
x = x.to(device=self.device, dtype=self.dtype)
805
x = self.patch_embed(x)
806
807
+ if isinstance(grid_thw, list):
808
+ grid_thw_list = grid_thw
809
+ grid_thw = torch.tensor(grid_thw, dtype=torch.int32)
810
+ else:
811
+ grid_thw_list = grid_thw.tolist()
812
+
813
# compute position embedding
808
- rotary_pos_emb = self.rot_pos_emb(grid_thw)
814
+ rotary_pos_emb = self.rot_pos_emb(grid_thw_list)
815
816
# compute cu_seqlens
811
- grid_thw_ = torch.tensor(grid_thw, device=x.device, dtype=torch.long)
817
cu_seqlens = torch.repeat_interleave(
813
- grid_thw_[:, 1] * grid_thw_[:, 2], grid_thw_[:, 0]
818
+ grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
819
).cumsum(dim=0, dtype=torch.int32)
815
- cu_seqlens = F.pad(cu_seqlens, (1, 0), "constant", 0)
820
+ cu_seqlens = torch.cat([cu_seqlens.new_zeros(1), cu_seqlens])
821
+ cu_seqlens = cu_seqlens.to(self.device, non_blocking=True)
822
823
# transformers
824
x = x.unsqueeze(1)
@@ -1211,6 +1217,7 @@ class Qwen2VLForConditionalGeneration(
1217
nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP, SupportsMRoPE
1218
):
1219
merge_by_field_config = True
1220
+ multimodal_cpu_fields = {"image_grid_thw", "video_grid_thw"}
1221
1222
# To ensure correct weight loading and mapping.
1223
hf_to_vllm_mapper = WeightsMapper(
@@ -1458,7 +1465,6 @@ class Qwen2VLForConditionalGeneration(
1465
) -> tuple[torch.Tensor, ...]:
1466
grid_thw = image_input["image_grid_thw"]
1467
assert grid_thw.ndim == 2
1461
- grid_thw_list = grid_thw.tolist()
1468
1469
if image_input["type"] == "image_embeds":
1470
image_embeds = image_input["image_embeds"]
@@ -1467,18 +1473,14 @@ class Qwen2VLForConditionalGeneration(
1473
1474
if self.use_data_parallel:
1475
return run_dp_sharded_mrope_vision_model(
1470
- self.visual, pixel_values, grid_thw_list, rope_type="rope_3d"
1476
+ self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d"
1477
)
1478
else:
1473
- image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
1479
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
1480
1481
# Split concatenated embeddings for each image item.
1482
merge_size = self.visual.spatial_merge_size
1477
- sizes = (
1478
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1479
- // (merge_size * merge_size)
1480
- ).tolist()
1481
-
1483
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1484
return image_embeds.split(sizes)
1485
1486
def _process_video_input(
@@ -1486,26 +1488,22 @@ class Qwen2VLForConditionalGeneration(
1488
) -> tuple[torch.Tensor, ...]:
1489
grid_thw = video_input["video_grid_thw"]
1490
assert grid_thw.ndim == 2
1489
- grid_thw_list = grid_thw.tolist()
1491
1492
if video_input["type"] == "video_embeds":
1493
video_embeds = video_input["video_embeds"]
1494
else:
1495
pixel_values_videos = video_input["pixel_values_videos"]
1496
if self.use_data_parallel:
1497
+ grid_thw_list = grid_thw.tolist()
1498
return run_dp_sharded_mrope_vision_model(
1499
self.visual, pixel_values_videos, grid_thw_list, rope_type="rope_3d"
1500
)
1501
else:
1500
- video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw_list)
1502
+ video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw)
1503
1504
# Split concatenated embeddings for each video item.
1505
merge_size = self.visual.spatial_merge_size
1504
- sizes = (
1505
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1506
- // (merge_size * merge_size)
1507
- ).tolist()
1508
-
1506
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1507
return video_embeds.split(sizes)
1508
1509
def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
vllm/model_executor/models/qwen3_vl.py
+21
-34
@@ -414,16 +414,10 @@ class Qwen3_VisionTransformer(nn.Module):
414
def device(self) -> torch.device:
415
return self.patch_embed.proj.weight.device
416
417
- def rot_pos_emb(self, grid_thw):
417
+ def rot_pos_emb(self, grid_thw: list[list[int]]):
418
pos_ids = []
419
- # Support both Tensor and list inputs for DP path
420
- if isinstance(grid_thw, list):
421
- grid_list = grid_thw
422
- max_grid_size = max(max(h, w) for _, h, w in grid_list)
423
- else:
424
- grid_list = grid_thw.tolist()
425
- max_grid_size = int(grid_thw[:, 1:].max().item())
426
- for t, h, w in grid_list:
419
+ max_grid_size = max(max(h, w) for _, h, w in grid_thw)
420
+ for t, h, w in grid_thw:
421
hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
422
hpos_ids = hpos_ids.reshape(
423
h // self.spatial_merge_size,
@@ -527,24 +521,25 @@ class Qwen3_VisionTransformer(nn.Module):
521
def forward(
522
self,
523
x: torch.Tensor,
530
- grid_thw: list[list[int]],
524
+ grid_thw: torch.Tensor | list[list[int]],
525
) -> torch.Tensor:
526
hidden_states = x.to(device=self.device, dtype=self.dtype, non_blocking=True)
527
hidden_states = self.patch_embed(hidden_states)
528
535
- pos_embeds = self.fast_pos_embed_interpolate(grid_thw)
529
+ if isinstance(grid_thw, list):
530
+ grid_thw_list = grid_thw
531
+ grid_thw = torch.tensor(grid_thw, dtype=torch.int32)
532
+ else:
533
+ grid_thw_list = grid_thw.tolist()
534
+
535
+ pos_embeds = self.fast_pos_embed_interpolate(grid_thw_list)
536
hidden_states = hidden_states + pos_embeds
537
- rotary_pos_emb = self.rot_pos_emb(grid_thw)
537
+ rotary_pos_emb = self.rot_pos_emb(grid_thw_list)
538
rotary_pos_emb = rotary_pos_emb.to(hidden_states.device, non_blocking=True)
539
540
- grid_thw_tensor = torch.tensor(grid_thw, dtype=torch.int32)
541
-
540
cu_seqlens = torch.repeat_interleave(
543
- grid_thw_tensor[:, 1] * grid_thw_tensor[:, 2], grid_thw_tensor[:, 0]
544
- ).cumsum(
545
- dim=0,
546
- dtype=grid_thw_tensor.dtype if torch.jit.is_tracing() else torch.int32,
547
- )
541
+ grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
542
+ ).cumsum(dim=0, dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32)
543
cu_seqlens = torch.cat([cu_seqlens.new_zeros(1), cu_seqlens])
544
545
hidden_states = hidden_states.unsqueeze(1)
@@ -1177,6 +1172,7 @@ class Qwen3VLForConditionalGeneration(
1172
nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP, SupportsMRoPE
1173
):
1174
merge_by_field_config = True
1175
+ multimodal_cpu_fields = {"image_grid_thw", "video_grid_thw"}
1176
1177
packed_modules_mapping = {
1178
"qkv_proj": [
@@ -1356,7 +1352,6 @@ class Qwen3VLForConditionalGeneration(
1352
) -> tuple[torch.Tensor, ...]:
1353
grid_thw = image_input["image_grid_thw"]
1354
assert grid_thw.ndim == 2
1359
- grid_thw_list = grid_thw.tolist()
1355
1356
if image_input["type"] == "image_embeds":
1357
image_embeds = image_input["image_embeds"].type(self.visual.dtype)
@@ -1364,18 +1359,14 @@ class Qwen3VLForConditionalGeneration(
1359
pixel_values = image_input["pixel_values"].type(self.visual.dtype)
1360
if self.use_data_parallel:
1361
return run_dp_sharded_mrope_vision_model(
1367
- self.visual, pixel_values, grid_thw_list, rope_type="rope_3d"
1362
+ self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d"
1363
)
1364
else:
1370
- image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
1365
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
1366
1367
# Split concatenated embeddings for each image item.
1373
- # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1368
merge_size = self.visual.spatial_merge_size
1375
- sizes = (
1376
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1377
- // (merge_size * merge_size)
1378
- ).tolist()
1369
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1370
return image_embeds.split(sizes)
1371
1372
def _process_video_input(
@@ -1383,7 +1374,6 @@ class Qwen3VLForConditionalGeneration(
1374
) -> tuple[torch.Tensor, ...]:
1375
grid_thw = video_input["video_grid_thw"]
1376
assert grid_thw.ndim == 2
1386
- grid_thw_list = grid_thw.tolist()
1377
1378
if video_input["type"] == "video_embeds":
1379
video_embeds = video_input["video_embeds"].type(self.visual.dtype)
@@ -1392,19 +1382,16 @@ class Qwen3VLForConditionalGeneration(
1382
self.visual.dtype
1383
)
1384
if self.use_data_parallel:
1385
+ grid_thw_list = grid_thw.tolist()
1386
return run_dp_sharded_mrope_vision_model(
1387
self.visual, pixel_values_videos, grid_thw_list, rope_type="rope_3d"
1388
)
1389
else:
1399
- video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw_list)
1390
+ video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw)
1391
1392
# Split concatenated embeddings for each video item.
1402
- # Using prod on grid_thw_list instead of grid_thw.prod avoids CUDA sync
1393
merge_size = self.visual.spatial_merge_size
1404
- sizes = (
1405
- torch.tensor(grid_thw_list, dtype=torch.long).prod(-1)
1406
- // (merge_size * merge_size)
1407
- ).tolist()
1394
+ sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1395
return video_embeds.split(sizes)
1396
1397
def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
vllm/multimodal/utils.py
+13
-7
@@ -3,7 +3,7 @@
3
4
import asyncio
5
import atexit
6
-from collections.abc import Iterable
6
+from collections.abc import Iterable, Set
7
from concurrent.futures import ThreadPoolExecutor
8
from itertools import groupby
9
from pathlib import Path
@@ -402,6 +402,7 @@ def group_mm_kwargs_by_modality(
402
device: torch.types.Device = None,
403
pin_memory: bool = False,
404
merge_by_field_config: bool | None = None,
405
+ multimodal_cpu_fields: Set[str] = frozenset(),
406
) -> Iterable[tuple[str, int, BatchedTensorInputs]]:
407
"""Group consecutive `MultiModalKwargsItem`s from `mm_kwargs` with the same
408
modality together into the same `MultiModalKwargs` instance.
@@ -443,12 +444,17 @@ def group_mm_kwargs_by_modality(
444
)
445
446
if device is not None:
446
- mm_kwargs_group = json_map_leaves(
447
- lambda x: x.to(device=device, non_blocking=True)
448
- if isinstance(x, torch.Tensor)
449
- else x,
450
- mm_kwargs_group,
451
- )
447
+ mm_kwargs_group = {
448
+ k: json_map_leaves(
449
+ lambda x: x.to(device=device, non_blocking=True)
450
+ if isinstance(x, torch.Tensor)
451
+ else x,
452
+ v,
453
+ )
454
+ if k not in multimodal_cpu_fields
455
+ else v
456
+ for k, v in mm_kwargs_group.items()
457
+ }
458
else:
459
mm_kwargs_group = MultiModalKwargs.as_kwargs(
460
MultiModalKwargs.batch(
vllm/v1/worker/gpu_model_runner.py
+5
@@ -938,6 +938,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
938
device=self.device,
939
pin_memory=self.pin_memory,
940
merge_by_field_config=model.merge_by_field_config,
941
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
942
):
943
mm_kwargs_combined.update(mm_kwargs_group)
944
@@ -1768,6 +1769,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1769
device=self.device,
1770
pin_memory=self.pin_memory,
1771
merge_by_field_config=model.merge_by_field_config,
1772
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
1773
):
1774
curr_group_outputs = []
1775
@@ -1794,6 +1796,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1796
device=self.device,
1797
pin_memory=self.pin_memory,
1798
merge_by_field_config=model.merge_by_field_config,
1799
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
1800
)
1801
)
1802
@@ -1936,6 +1939,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1939
device=self.device,
1940
pin_memory=self.pin_memory,
1941
merge_by_field_config=model.merge_by_field_config,
1942
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
1943
):
1944
# Add the grouped features to encoder_features dict
1945
# This allows the model to receive them as kwargs (e.g.,
@@ -3292,6 +3296,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3296
device=self.device,
3297
pin_memory=self.pin_memory,
3298
merge_by_field_config=model.merge_by_field_config,
3299
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
3300
)
3301
)
3302
vllm/v1/worker/tpu_model_runner.py
+2
@@ -952,6 +952,7 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
952
device=self.device,
953
pin_memory=self.pin_memory,
954
merge_by_field_config=model.merge_by_field_config,
955
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
956
):
957
# Run the encoder.
958
# `curr_group_outputs` is either of the following:
@@ -2037,6 +2038,7 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2038
device=self.device,
2039
pin_memory=self.pin_memory,
2040
merge_by_field_config=model.merge_by_field_config,
2041
+ multimodal_cpu_fields=model.multimodal_cpu_fields,
2042
)
2043
)
2044