@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c6e7404cc5
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 9, 10:00 UTC

[Multimodal] Simplify MM input definitions (#33331)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>

Cyrus Leung committed Jan 29, 2026 at 21:32 UTC c6e7404cc5713a926e8b6c187b5f197a5436e9ff
17 files changed +142 -164
tests/distributed/test_shm_storage.py
+5 -7
@@ -23,18 +23,16 @@ from vllm.multimodal.inputs import (
23 )
24
25
26 -def _dummy_elem(modality: str, key: str, size: int):
26 +def _dummy_elem(size: int):
27 return MultiModalFieldElem(
28 - modality=modality,
29 - key=key,
28 data=torch.empty((size,), dtype=torch.int8),
29 field=MultiModalSharedField(batch_size=1),
30 )
31
32
35 -def _dummy_item(modality: str, size_by_key: dict[str, int]):
36 - return MultiModalKwargsItem.from_elems(
37 - [_dummy_elem(modality, key, size) for key, size in size_by_key.items()]
33 +def _dummy_item(size_by_key: dict[str, int]):
34 + return MultiModalKwargsItem(
35 + {key: _dummy_elem(size) for key, size in size_by_key.items()}
36 )
37
38
@@ -61,7 +59,7 @@ class TestSingleWriterShmObjectStorage(unittest.TestCase):
59 def test_minimal_put_get_cycle(self):
60 """Test basic put and get operations."""
61 key = "test_key"
64 - value = _dummy_item("text", {"field1": 10, "field2": 20})
62 + value = _dummy_item({"field1": 10, "field2": 20})
63
64 # Put operation
65 address, monotonic_id = self.storage.put(key, value)
tests/models/multimodal/processing/test_tensor_schema.py
+5 -1
@@ -119,7 +119,11 @@ def create_batched_mm_kwargs(
119 )["mm_kwargs"].require_data()
120
121 return group_mm_kwargs_by_modality(
122 - [item for modality in supported_mm_limits for item in mm_kwargs[modality]]
122 + [
123 + (modality, item)
124 + for modality in supported_mm_limits
125 + for item in mm_kwargs[modality]
126 + ]
127 )
128
129
tests/multimodal/test_cache.py
+15 -21
@@ -36,8 +36,6 @@ pytestmark = pytest.mark.cpu_test
36
37
38 def _dummy_elem(
39 - modality: str,
40 - key: str,
39 size: int,
40 *,
41 rng: np.random.RandomState | None = None,
@@ -48,21 +46,18 @@ def _dummy_elem(
46 data = torch.from_numpy(rng.randint(4, size=(size,), dtype=np.int8))
47
48 return MultiModalFieldElem(
51 - modality=modality,
52 - key=key,
49 data=data,
50 field=MultiModalSharedField(batch_size=1),
51 )
52
53
54 def _dummy_item(
59 - modality: str,
55 size_by_key: dict[str, int],
56 *,
57 rng: np.random.RandomState | None = None,
58 ):
64 - return MultiModalKwargsItem.from_elems(
65 - [_dummy_elem(modality, key, size, rng=rng) for key, size in size_by_key.items()]
59 + return MultiModalKwargsItem(
60 + {key: _dummy_elem(size, rng=rng) for key, size in size_by_key.items()}
61 )
62
63
@@ -71,19 +66,19 @@ def _dummy_items(
66 *,
67 rng: np.random.RandomState | None = None,
68 ):
74 - return MultiModalKwargsItems.from_seq(
75 - [
76 - _dummy_item(modality, size_by_key, rng=rng)
69 + return MultiModalKwargsItems(
70 + {
71 + modality: [_dummy_item(size_by_key, rng=rng)]
72 for modality, size_by_key in size_by_key_modality.items()
78 - ]
73 + }
74 )
75
76
77 @pytest.mark.parametrize(
78 ("item", "expected_size"),
79 [
85 - (_dummy_item("a", {"a1": 100}), 100),
86 - (_dummy_item("a", {"a1": 100, "a2": 110}), 210),
80 + (_dummy_item({"a1": 100}), 100),
81 + (_dummy_item({"a1": 100, "a2": 110}), 210),
82 (_dummy_items({"a": {"a1": 100, "a2": 110}, "b": {"b1": 120, "b2": 130}}), 460), # noqa: E501
83 ],
84 )
@@ -143,7 +138,7 @@ def _compare_caches(
138
139 rng = np.random.RandomState(seed)
140 all_items = [
146 - _dummy_item("item", {"key": item_size_gb}, rng=rng)
141 + _dummy_item({"key": item_size_gb}, rng=rng)
142 for _ in range(int(item_capacity / hit_rate))
143 ]
144 all_hashes = [
@@ -245,13 +240,13 @@ def _run_test_cache_eviction_lru(
240 "image_C",
241 ]
242 request1_items = {
248 - h: MultiModalKwargsItem.dummy(h, nbytes=2 * base_item_size)
243 + h: MultiModalKwargsItem.dummy(nbytes=2 * base_item_size)
244 for h in request1_hashes
245 }
246
247 request2_hashes = ["image_D", "image_E", "image_A", "image_C"]
248 request2_items = {
254 - h: MultiModalKwargsItem.dummy(h, nbytes=1 * base_item_size)
249 + h: MultiModalKwargsItem.dummy(nbytes=1 * base_item_size)
250 for h in request2_hashes
251 }
252
@@ -356,15 +351,14 @@ def _run_test_cache_eviction_shm(
351 ):
352 request1_hashes = ["image_A", "image_B", "image_C"]
353 request1_items = {
359 - h: MultiModalKwargsItem.dummy(h, nbytes=5 * base_item_size)
360 - for h in request1_hashes
354 + h: MultiModalKwargsItem.dummy(5 * base_item_size) for h in request1_hashes
355 }
356 request1_items_p0_result = []
357
358 request2_hashes = ["image_G", "image_A"]
359 request2_items = {
360 h: MultiModalKwargsItem.dummy(
367 - h, nbytes=(5 if h in request1_hashes else 2) * base_item_size
361 + (5 if h in request1_hashes else 2) * base_item_size
362 )
363 for h in request2_hashes
364 }
@@ -373,7 +367,7 @@ def _run_test_cache_eviction_shm(
367 request3_hashes = ["image_G", "image_H", "image_I", "image_B"]
368 request3_items = {
369 h: MultiModalKwargsItem.dummy(
376 - h, nbytes=(5 if h in request1_hashes else 2) * base_item_size
370 + (5 if h in request1_hashes else 2) * base_item_size
371 )
372 for h in request3_hashes
373 }
@@ -532,7 +526,7 @@ def test_processor_cache_shared_across_loras():
526 lora_a_identifier = f"12345:{base_mm_hash}"
527 lora_b_identifier = f"67890:{base_mm_hash}"
528
535 - item_data = MultiModalKwargsItem.dummy("test_image", nbytes=1024)
529 + item_data = MultiModalKwargsItem.dummy(1024)
530
531 feature_lora_a = MultiModalFeatureSpec(
532 data=item_data,
tests/v1/core/test_kv_cache_utils.py
+1 -1
@@ -77,7 +77,7 @@ def make_request(
77 for j, position in enumerate(mm_positions):
78 identifier = mm_hashes[j] if mm_hashes else f"hash_{j}"
79 mm_feature = MultiModalFeatureSpec(
80 - data=MultiModalKwargsItem.dummy("dummy_m"),
80 + data=MultiModalKwargsItem.dummy(),
81 mm_position=position,
82 identifier=identifier,
83 modality="image",
tests/v1/core/test_prefix_caching.py
+1 -1
@@ -68,7 +68,7 @@ def make_request(
68 for j, position in enumerate(mm_positions):
69 identifier = mm_hashes[j] if mm_hashes else f"hash_{j}"
70 mm_feature = MultiModalFeatureSpec(
71 - data=MultiModalKwargsItem.dummy("dummy_m"),
71 + data=MultiModalKwargsItem.dummy(),
72 mm_position=position,
73 identifier=identifier,
74 modality="image",
tests/v1/core/test_priority_scheduler_random.py
+1 -1
@@ -56,7 +56,7 @@ def _create_random_request(
56 for j, position in enumerate(mm_positions):
57 identifier = f"{request_id}_hash_{j}"
58 mm_feature = MultiModalFeatureSpec(
59 - data=MultiModalKwargsItem.dummy("dummy_m"),
59 + data=MultiModalKwargsItem.dummy(),
60 mm_position=position,
61 identifier=identifier,
62 modality="image",
tests/v1/core/test_scheduler.py
+1 -1
@@ -1707,7 +1707,7 @@ def create_requests_with_priority(
1707 # Unique dummy hash for each mm item
1708 identifier = f"hash{i}_{j}"
1709 mm_feature = MultiModalFeatureSpec(
1710 - data=MultiModalKwargsItem.dummy("dummy_m"),
1710 + data=MultiModalKwargsItem.dummy(),
1711 mm_position=position,
1712 identifier=identifier,
1713 modality="image",
tests/v1/core/utils.py
+1 -1
@@ -236,7 +236,7 @@ def create_requests(
236 # Unique dummy hash for each mm item
237 identifier = f"hash{i}_{j}"
238 mm_feature = MultiModalFeatureSpec(
239 - data=MultiModalKwargsItem.dummy("dummy_m"),
239 + data=MultiModalKwargsItem.dummy(),
240 mm_position=position,
241 identifier=identifier,
242 modality="image",
tests/v1/streaming_input/test_gpu_model_runner_streaming.py
+2 -2
@@ -131,7 +131,7 @@ def test_e2e_streaming_with_multimodal_features(mock_model_runner_with_input_bat
131
132 # Step 1: Create initial request state with one multimodal feature
133 mm_feature_1 = MultiModalFeatureSpec(
134 - data=MultiModalKwargsItem.dummy("audio"),
134 + data=MultiModalKwargsItem.dummy(),
135 modality="audio",
136 identifier="audio_1",
137 mm_position=PlaceholderRange(offset=2, length=10),
@@ -158,7 +158,7 @@ def test_e2e_streaming_with_multimodal_features(mock_model_runner_with_input_bat
158 # The scheduler has already set prompt_token_ids to the full sequence
159 # (original prompt + intermediate outputs + new prompt with new multimodal feature)
160 mm_feature_2 = MultiModalFeatureSpec(
161 - data=MultiModalKwargsItem.dummy("audio"),
161 + data=MultiModalKwargsItem.dummy(),
162 modality="audio",
163 identifier="audio_2",
164 mm_position=PlaceholderRange(offset=15, length=5),
tests/v1/streaming_input/test_scheduler_streaming.py
+2 -2
@@ -174,7 +174,7 @@ class TestStreamingScheduler(unittest.TestCase):
174 scheduler = create_scheduler()
175
176 mm_feature = MultiModalFeatureSpec(
177 - data=MultiModalKwargsItem.dummy("audio"),
177 + data=MultiModalKwargsItem.dummy(),
178 modality="audio",
179 identifier="",
180 mm_position=PlaceholderRange(offset=1, length=1),
@@ -187,7 +187,7 @@ class TestStreamingScheduler(unittest.TestCase):
187 session.num_computed_tokens = len(session.prompt_token_ids)
188
189 mm_feature = MultiModalFeatureSpec(
190 - data=MultiModalKwargsItem.dummy("audio"),
190 + data=MultiModalKwargsItem.dummy(),
191 modality="audio",
192 identifier="",
193 mm_position=PlaceholderRange(offset=2, length=1),
tests/v1/test_serial_utils.py
+9 -14
@@ -104,14 +104,10 @@ class MyRequest(msgspec.Struct):
104
105 def test_multimodal_kwargs():
106 e1 = MultiModalFieldElem(
107 - "audio",
108 - "a0",
107 torch.zeros(1000, dtype=torch.bfloat16),
108 MultiModalBatchedField(),
109 )
110 e2 = MultiModalFieldElem(
113 - "video",
114 - "v0",
111 [torch.zeros(1000, dtype=torch.int8) for _ in range(4)],
112 MultiModalFlatField(
113 slices=[[slice(1, 2, 3), slice(4, 5, 6)], [slice(None, 2)]],
@@ -119,21 +115,20 @@ def test_multimodal_kwargs():
115 ),
116 )
117 e3 = MultiModalFieldElem(
122 - "image",
123 - "i0",
118 torch.zeros(1000, dtype=torch.int32),
119 MultiModalSharedField(batch_size=4),
120 )
121 e4 = MultiModalFieldElem(
128 - "image",
129 - "i1",
122 torch.zeros(1000, dtype=torch.int32),
123 MultiModalFlatField(slices=[slice(1, 2, 3), slice(4, 5, 6)], dim=2),
124 )
133 - audio = MultiModalKwargsItem.from_elems([e1])
134 - video = MultiModalKwargsItem.from_elems([e2])
135 - image = MultiModalKwargsItem.from_elems([e3, e4])
136 - mm = MultiModalKwargsItems.from_seq([audio, video, image])
125 + mm = MultiModalKwargsItems(
126 + {
127 + "audio": [MultiModalKwargsItem({"a0": e1})],
128 + "video": [MultiModalKwargsItem({"v0": e2})],
129 + "image": [MultiModalKwargsItem({"i0": e3, "i1": e4})],
130 + }
131 + )
132
133 # pack mm kwargs into a mock request so that it can be decoded properly
134 req = MyRequest([mm])
@@ -147,8 +142,8 @@ def test_multimodal_kwargs():
142
143 total_len = sum(memoryview(x).cast("B").nbytes for x in encoded)
144
150 - # expected total encoding length, should be 14395, +-20 for minor changes
151 - assert 14375 <= total_len <= 14425
145 + # expected total encoding length, should be 14319, +-20 for minor changes
146 + assert 14300 <= total_len <= 14340
147 decoded = decoder.decode(encoded).mm[0]
148 assert isinstance(decoded, MultiModalKwargsItems)
149
vllm/multimodal/cache.py
+14 -17
@@ -463,8 +463,8 @@ class ShmObjectStoreSenderCache(BaseMultiModalProcessorCache):
463 ring_buffer=ring_buffer,
464 serde_class=MsgpackSerde,
465 )
466 - # cache (prompt_updates, modality) for P0 only
467 - self._p0_cache: dict[str, tuple[Sequence[ResolvedPromptUpdate], str]] = {}
466 + # cache prompt_updates for P0 only
467 + self._p0_cache: dict[str, Sequence[ResolvedPromptUpdate]] = {}
468
469 self._hits = 0
470 self._total = 0
@@ -495,23 +495,22 @@ class ShmObjectStoreSenderCache(BaseMultiModalProcessorCache):
495 self._total += 1
496
497 address, monotonic_id = self._shm_cache.get_cached(mm_hash)
498 - prompt_updates, modality = self._p0_cache[mm_hash]
499 - return self.address_as_item(address, monotonic_id, modality), prompt_updates
498 + prompt_updates = self._p0_cache[mm_hash]
499 + return self.address_as_item(address, monotonic_id), prompt_updates
500
501 assert mm_item is not None, f"Expected a cached item for {mm_hash=}"
502 + item, prompt_updates = mm_item
503
504 self._total += 1
505
506 try:
506 - address, monotonic_id = self._shm_cache.put(mm_hash, mm_item[0])
507 + address, monotonic_id = self._shm_cache.put(mm_hash, item)
508 # Try to remove dangling items if p0 cache is too large.
509 if len(self._p0_cache) >= 2 * len(self._shm_cache.key_index):
510 self.remove_dangling_items()
510 - self._p0_cache[mm_hash] = mm_item[1], mm_item[0].modality
511 - address_item = self.address_as_item(
512 - address, monotonic_id, mm_item[0].modality
513 - )
514 - return address_item, mm_item[1]
511 +
512 + self._p0_cache[mm_hash] = prompt_updates
513 + return self.address_as_item(address, monotonic_id), prompt_updates
514 except (ValueError, MemoryError) as e:
515 # put may fail if the object is too large or
516 # the cache is full.
@@ -550,22 +549,20 @@ class ShmObjectStoreSenderCache(BaseMultiModalProcessorCache):
549 del self._p0_cache[mm_hash]
550
551 def address_as_item(
553 - self, address: int, monotonic_id: int, modality: str
552 + self,
553 + address: int,
554 + monotonic_id: int,
555 ) -> MultiModalKwargsItem:
556 addr_elem = MultiModalFieldElem(
556 - modality=modality,
557 - key="address",
557 data=address,
558 field=MultiModalBatchedField(),
559 )
560 id_elem = MultiModalFieldElem(
562 - modality=modality,
563 - key="monotonic_id",
561 data=monotonic_id,
562 field=MultiModalBatchedField(),
563 )
567 - mm_item = MultiModalKwargsItem.from_elems([addr_elem, id_elem])
568 - return mm_item
564 +
565 + return MultiModalKwargsItem({"address": addr_elem, "monotonic_id": id_elem})
566
567
568 class BaseMultiModalReceiverCache(
vllm/multimodal/inputs.py
+65 -72
@@ -23,7 +23,7 @@ import numpy as np
23 from PIL.Image import Image
24 from typing_extensions import NotRequired, TypeVar
25
26 -from vllm.utils.collection_utils import full_groupby, is_list_of
26 +from vllm.utils.collection_utils import is_list_of
27 from vllm.utils.import_utils import LazyLoader
28 from vllm.utils.jsontree import json_map_leaves
29
@@ -336,25 +336,33 @@ class MultiModalFeatureSpec:
336 """
337 Represents a single multimodal input with its processed data and metadata.
338
339 - Used by the V1 engine to track multimodal data through processing and
340 - caching. A request containing multiple multimodal items will have one
341 - MultiModalFeatureSpec per item.
339 + Used to track multimodal data through processing and caching.
340 + A request containing multiple multimodal items will have one
341 + `MultiModalFeatureSpec` per item.
342 """
343
344 data: Optional["MultiModalKwargsItem"]
345 - """Multimodal data for this feature"""
345 + """
346 + Represents multimodal data for this feature.
347 +
348 + Can be `None` if the item is cached, to skip IPC between API server
349 + and engine core processes.
350 + """
351
352 modality: str
348 - """Based on the input, e.g., "image", "audio", "video"."""
353 + """The input modality, e.g., `"image"`, `"audio"`, `"video"`."""
354
355 identifier: str
351 - """mm_hash or uuid for caching encoder outputs."""
356 + """The hash for caching encoder outputs (with LoRA prefix if applicable)."""
357
358 mm_position: PlaceholderRange
354 - """e.g., PlaceholderRange(offset=2, length=336)"""
359 + """
360 + The location of the `modality` tokens corresponding to this item
361 + in the prompt, e.g., `PlaceholderRange(offset=2, length=336)`.
362 + """
363
364 mm_hash: str | None = None
357 - """Base mm_hash for processor cache (without LoRA prefix)."""
365 + """The hash for caching processor outputs (without LoRA prefix)."""
366
367 @staticmethod
368 def gather_kwargs(features: list["MultiModalFeatureSpec"], keys: set[str]):
@@ -373,23 +381,10 @@ class MultiModalFeatureSpec:
381 @dataclass
382 class MultiModalFieldElem:
383 """
376 - Represents a keyword argument inside a
384 + Represents a processed keyword argument to pass to a model for a
385 [`MultiModalKwargsItem`][vllm.multimodal.inputs.MultiModalKwargsItem].
386 """
387
380 - modality: str
381 - """
382 - The modality of the corresponding multi-modal item.
383 - Each multi-modal item can consist of multiple keyword arguments.
384 - """
385 -
386 - key: str
387 - """
388 - The key of this field in
389 - [`MultiModalKwargsItem`][vllm.multimodal.inputs.MultiModalKwargsItem],
390 - i.e. the name of the keyword argument to be passed to the model.
391 - """
392 -
388 data: NestedTensors
389 """
390 The tensor data of this field in
@@ -417,11 +412,7 @@ class MultiModalFieldElem:
412 else:
413 data_equal = nested_tensors_equal(self.data, other.data)
414
420 - return (
421 - (self.modality, self.key) == (other.modality, other.key)
422 - and data_equal
423 - and type(self.field) is type(other.field)
424 - ) # noqa: E721
415 + return data_equal and type(self.field) is type(other.field) # noqa: E721
416
417
418 @dataclass(frozen=True, kw_only=True)
@@ -438,13 +429,8 @@ class BaseMultiModalField(ABC):
429 when `MultiModalKwargsItems.get_data()` is called to batch the data.
430 """
431
441 - def _field_factory(self, *, modality: str, key: str):
442 - f = partial(
443 - MultiModalFieldElem,
444 - modality=modality,
445 - key=key,
446 - field=self,
447 - )
432 + def _field_factory(self):
433 + f = partial(MultiModalFieldElem, field=self)
434
435 # Allow passing data as positional argument
436 def factory(data: NestedTensors) -> MultiModalFieldElem:
@@ -519,7 +505,7 @@ class MultiModalBatchedField(BaseMultiModalField):
505 key: str,
506 data: NestedTensors,
507 ) -> Sequence[MultiModalFieldElem]:
522 - field_factory = self._field_factory(modality=modality, key=key)
508 + field_factory = self._field_factory()
509 return [field_factory(item) for item in data]
510
511 def _reduce_data(
@@ -565,7 +551,7 @@ class MultiModalFlatField(BaseMultiModalField):
551 key: str,
552 data: NestedTensors,
553 ) -> Sequence[MultiModalFieldElem]:
568 - field_factory = self._field_factory(modality=modality, key=key)
554 + field_factory = self._field_factory()
555 if not is_list_of(self.slices, slice, check="all"):
556 assert isinstance(data, torch.Tensor), (
557 "torch.Tensor is required for multiple slices"
@@ -664,7 +650,7 @@ class MultiModalSharedField(BaseMultiModalField):
650 key: str,
651 data: NestedTensors,
652 ) -> Sequence[MultiModalFieldElem]:
667 - field_factory = self._field_factory(modality=modality, key=key)
653 + field_factory = self._field_factory()
654 return [field_factory(data)] * self.batch_size
655
656 def _reduce_data(
@@ -899,37 +885,19 @@ class MultiModalFieldConfig:
885
886 class MultiModalKwargsItem(UserDict[str, MultiModalFieldElem]):
887 """
902 - A collection of
903 - [`MultiModalFieldElem`][vllm.multimodal.inputs.MultiModalFieldElem]
904 - corresponding to a data item in
888 + A dictionary of processed keyword arguments to pass to the model,
889 + corresponding to a single item in
890 [`MultiModalDataItems`][vllm.multimodal.parse.MultiModalDataItems].
891 """
892
893 @staticmethod
909 - def dummy(modality: str, nbytes: int = 1):
894 + def dummy(nbytes: int = 1):
895 """Convenience class for testing."""
896 mm_elem = MultiModalFieldElem(
912 - modality=modality,
913 - key="dummy",
897 data=torch.empty(nbytes, dtype=torch.uint8),
898 field=MultiModalSharedField(batch_size=1),
899 )
917 - return MultiModalKwargsItem.from_elems([mm_elem])
918 -
919 - @staticmethod
920 - def from_elems(elems: Sequence[MultiModalFieldElem]):
921 - return MultiModalKwargsItem({elem.key: elem for elem in elems})
922 -
923 - def __init__(self, data: Mapping[str, MultiModalFieldElem] = {}) -> None:
924 - super().__init__(data)
925 -
926 - modalities = {elem.modality for elem in self.values()}
927 - assert len(modalities) == 1, f"Found different modalities={modalities}"
928 - self._modality = next(iter(modalities))
929 -
930 - @property
931 - def modality(self) -> str:
932 - return self._modality
900 + return MultiModalKwargsItem({"dummy": mm_elem})
901
902 def get_data(self) -> dict[str, NestedTensors]:
903 return {key: elem.data for key, elem in self.items()}
@@ -945,9 +913,38 @@ _I = TypeVar(
913
914 class MultiModalKwargsItems(UserDict[str, Sequence[_I]]):
915 """
948 - A dictionary of
949 - [`MultiModalKwargsItem`][vllm.multimodal.inputs.MultiModalKwargsItem]s
950 - by modality.
916 + A dictionary of processed multi-modal inputs by modality.
917 +
918 + For example, given a processor that processes
919 + images into `pixel_values` and `image_grid_thw`,
920 + and audios into `input_audio_features`,
921 + a prompt with 2 images and 1 audio will be processed
922 + into a `MultiModalKwargsItems` with the following structure:
923 +
924 + ```python
925 + MultiModalKwargsItems(
926 + {
927 + "image": [
928 + # For the first image
929 + MultiModalKwargsItem({"pixel_values": ..., "image_grid_thw": ...}),
930 + # For the second imgae
931 + MultiModalKwargsItem({"pixel_values": ..., "image_grid_thw": ...}),
932 + ],
933 + "audio": [
934 + # For the first audio
935 + MultiModalKwargsItem({"input_audio_features": ...}),
936 + ],
937 + }
938 + )
939 + ```
940 +
941 + Unlike HF processing which returns all items
942 + in a single dictionary with batched keyword arguments,
943 + we split up the items because some of them may already be cached.
944 + Also, items from multiple requests may be batched together to improve throughput,
945 + using the logic defined by the
946 + [`BaseMultiModalField`][vllm.multimodal.inputs.BaseMultiModalField]
947 + for each keyword argument.
948 """
949
950 @staticmethod
@@ -967,7 +964,7 @@ class MultiModalKwargsItems(UserDict[str, Sequence[_I]]):
964 elems_by_key[key] = elems
965 keys_by_modality[config.modality].add(key)
966
970 - items = list[MultiModalKwargsItem]()
967 + items_by_modality = dict[str, list[MultiModalKwargsItem]]()
968 for modality, keys in keys_by_modality.items():
969 elems_in_modality = {k: elems_by_key[k] for k in keys}
970 batch_sizes = {k: len(v) for k, v in elems_in_modality.items()}
@@ -979,15 +976,11 @@ class MultiModalKwargsItems(UserDict[str, Sequence[_I]]):
976 )
977
978 batch_size = next(iter(batch_sizes.values()))
982 - for item_idx in range(batch_size):
983 - elems = [v[item_idx] for v in elems_in_modality.values()]
984 - items.append(MultiModalKwargsItem.from_elems(elems))
985 -
986 - return MultiModalKwargsItems.from_seq(items)
979 + items_by_modality[modality] = [
980 + MultiModalKwargsItem({k: v[i] for k, v in elems_in_modality.items()})
981 + for i in range(batch_size)
982 + ]
983
988 - @staticmethod
989 - def from_seq(items: Sequence[MultiModalKwargsItem]):
990 - items_by_modality = full_groupby(items, key=lambda x: x.modality)
984 return MultiModalKwargsItems(items_by_modality)
985
986 def __getitem__(self, modality: str) -> Sequence[_I]:
vllm/multimodal/utils.py
+4 -4
@@ -467,7 +467,7 @@ def argsort_mm_positions(
467
468
469 def group_mm_kwargs_by_modality(
470 - mm_kwargs: list[MultiModalKwargsItem],
470 + mm_kwargs: list[tuple[str, MultiModalKwargsItem]],
471 *,
472 device: torch.types.Device = None,
473 pin_memory: bool = False,
@@ -485,9 +485,9 @@ def group_mm_kwargs_by_modality(
485 """
486 from vllm.multimodal.inputs import MultiModalKwargsItems
487
488 - for modality, items in groupby(mm_kwargs, key=lambda item: item.modality):
489 - items_lst = list(items)
490 - mm_kwargs_items = MultiModalKwargsItems.from_seq(items_lst)
488 + for modality, group in groupby(mm_kwargs, key=lambda x: x[0]):
489 + items_lst = [item for _, item in group]
490 + mm_kwargs_items = MultiModalKwargsItems({modality: items_lst})
491 mm_kwargs_data = mm_kwargs_items.get_data(
492 device=device,
493 pin_memory=pin_memory,
vllm/v1/serial_utils.py
+5 -7
@@ -242,13 +242,11 @@ class MsgpackEncoder:
242 for modality, itemlist in items.items()
243 }
244
245 - def _encode_mm_item(self, item: MultiModalKwargsItem) -> list[dict[str, Any]]:
246 - return [self._encode_mm_field_elem(elem) for elem in item.values()]
245 + def _encode_mm_item(self, item: MultiModalKwargsItem) -> dict[str, Any]:
246 + return {key: self._encode_mm_field_elem(elem) for key, elem in item.items()}
247
248 def _encode_mm_field_elem(self, elem: MultiModalFieldElem) -> dict[str, Any]:
249 return {
250 - "modality": elem.modality,
251 - "key": elem.key,
250 "data": (
251 None if elem.data is None else self._encode_nested_tensors(elem.data)
252 ),
@@ -383,9 +381,9 @@ class MsgpackDecoder:
381 }
382 )
383
386 - def _decode_mm_item(self, obj: list[Any]) -> MultiModalKwargsItem:
387 - return MultiModalKwargsItem.from_elems(
388 - [self._decode_mm_field_elem(v) for v in obj]
384 + def _decode_mm_item(self, obj: dict[str, Any]) -> MultiModalKwargsItem:
385 + return MultiModalKwargsItem(
386 + {key: self._decode_mm_field_elem(elem) for key, elem in obj.items()}
387 )
388
389 def _decode_mm_field_elem(self, obj: dict[str, Any]) -> MultiModalFieldElem:
vllm/v1/worker/gpu/mm/encoder_runner.py
+5 -4
@@ -43,9 +43,9 @@ class EncoderRunner:
43 def prepare_mm_inputs(
44 self,
45 scheduled_encoder_inputs: dict[str, list[int]],
46 - ) -> tuple[list[str], list[MultiModalKwargsItem]]:
46 + ) -> tuple[list[str], list[tuple[str, MultiModalKwargsItem]]]:
47 mm_hashes: list[str] = []
48 - mm_kwargs: list[MultiModalKwargsItem] = []
48 + mm_kwargs: list[tuple[str, MultiModalKwargsItem]] = []
49 for req_id, encoder_input_ids in scheduled_encoder_inputs.items():
50 mm_features = self.req_id_to_mm_features[req_id]
51 for mm_input_id in encoder_input_ids:
@@ -53,7 +53,8 @@ class EncoderRunner:
53 if mm_feature.data is None:
54 continue
55 mm_hashes.append(mm_feature.identifier)
56 - mm_kwargs.append(mm_feature.data)
56 + mm_kwargs.append((mm_feature.modality, mm_feature.data))
57 +
58 return mm_hashes, mm_kwargs
59
60 @torch.inference_mode()
@@ -61,7 +62,7 @@ class EncoderRunner:
62 self,
63 model: SupportsMultiModal,
64 mm_hashes: list[str],
64 - mm_kwargs: list[MultiModalKwargsItem],
65 + mm_kwargs: list[tuple[str, MultiModalKwargsItem]],
66 ) -> list[torch.Tensor]:
67 if not mm_hashes:
68 return []
vllm/v1/worker/gpu_model_runner.py
+6 -8
@@ -1217,11 +1217,11 @@ class GPUModelRunner(
1217 if not scheduler_output or not self.is_multimodal_raw_input_only_model:
1218 return {}
1219
1220 - mm_kwargs = list[MultiModalKwargsItem]()
1220 + mm_kwargs = list[tuple[str, MultiModalKwargsItem]]()
1221 for req in scheduler_output.scheduled_new_reqs:
1222 for feature in req.mm_features:
1223 if feature.data is not None:
1224 - mm_kwargs.append(feature.data)
1224 + mm_kwargs.append((feature.modality, feature.data))
1225
1226 # Input all modalities at once
1227 mm_kwargs_combined: BatchedTensorInputs = {}
@@ -2219,7 +2219,7 @@ class GPUModelRunner(
2219 scheduler_output: "SchedulerOutput",
2220 ) -> tuple[
2221 list[str],
2222 - list[MultiModalKwargsItem],
2222 + list[tuple[str, MultiModalKwargsItem]],
2223 list[tuple[str, PlaceholderRange]],
2224 ]:
2225 """Batch multimodal inputs from scheduled encoder inputs.
@@ -2239,7 +2239,7 @@ class GPUModelRunner(
2239 return [], [], []
2240
2241 mm_hashes = list[str]()
2242 - mm_kwargs = list[MultiModalKwargsItem]()
2242 + mm_kwargs = list[tuple[str, MultiModalKwargsItem]]()
2243 # Multimodal LoRA reference info to map each multimodal item
2244 # back to its request & position
2245 mm_lora_refs = list[tuple[str, PlaceholderRange]]()
@@ -2252,7 +2252,7 @@ class GPUModelRunner(
2252 continue
2253
2254 mm_hashes.append(mm_feature.identifier)
2255 - mm_kwargs.append(mm_feature.data)
2255 + mm_kwargs.append((mm_feature.modality, mm_feature.data))
2256 mm_lora_refs.append((req_id, mm_feature.mm_position))
2257
2258 return mm_hashes, mm_kwargs, mm_lora_refs
@@ -4475,12 +4475,10 @@ class GPUModelRunner(
4475 # but not read from the cache
4476 assert dummy_mm_item is not None, "Item should not already be cached"
4477
4478 - dummy_mm_items = [dummy_mm_item] * max_items_per_batch
4479 -
4478 return next(
4479 mm_kwargs_group
4480 for _, _, mm_kwargs_group in group_mm_kwargs_by_modality(
4483 - dummy_mm_items,
4481 + [(modality, dummy_mm_item)] * max_items_per_batch,
4482 device=self.device,
4483 pin_memory=self.pin_memory,
4484 )