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
[Refactor] Allow optional MultiModalKwargsItem in IPC (#23022)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Aug 16, 2025 at 19:30 UTC
4dff91c93da668f4cca3f80aa3a94622d21c34fc
10 files changed
+59
-108
tests/v1/core/test_kv_cache_utils.py
+2
-10
@@ -7,9 +7,7 @@ import pytest
7
import torch
8
9
from vllm.config import ModelConfig, SchedulerConfig, VllmConfig
10
-from vllm.multimodal.inputs import (MultiModalBatchedField,
11
- MultiModalFieldElem, MultiModalKwargsItem,
12
- PlaceholderRange)
10
+from vllm.multimodal.inputs import MultiModalKwargsItem, PlaceholderRange
11
from vllm.sampling_params import SamplingParams
12
from vllm.utils import GiB_bytes, sha256, sha256_cbor_64bit
13
from vllm.v1.core.kv_cache_manager import KVCacheManager
@@ -42,13 +40,7 @@ def make_request(
40
if mm_positions is None:
41
mm_kwargs = None
42
else:
45
- mm_elem = MultiModalFieldElem(
46
- modality="dummy_m",
47
- key="dummy_k",
48
- data=None,
49
- field=MultiModalBatchedField(),
50
- )
51
- mm_item = MultiModalKwargsItem.from_elems([mm_elem])
43
+ mm_item = MultiModalKwargsItem.dummy("dummy_m")
44
mm_kwargs = [mm_item] * len(mm_positions)
45
46
return Request(request_id=request_id,
tests/v1/core/test_prefix_caching.py
+2
-10
@@ -9,9 +9,7 @@ import pytest
9
import torch
10
11
from vllm.distributed.kv_events import AllBlocksCleared, BlockRemoved
12
-from vllm.multimodal.inputs import (MultiModalBatchedField,
13
- MultiModalFieldElem, MultiModalKwargsItem,
14
- PlaceholderRange)
12
+from vllm.multimodal.inputs import MultiModalKwargsItem, PlaceholderRange
13
from vllm.sampling_params import SamplingParams
14
from vllm.utils import sha256, sha256_cbor_64bit
15
from vllm.v1.core.block_pool import BlockPool
@@ -37,13 +35,7 @@ def make_request(
35
if mm_positions is None:
36
mm_kwargs = None
37
else:
40
- mm_elem = MultiModalFieldElem(
41
- modality="dummy_m",
42
- key="dummy_k",
43
- data=None,
44
- field=MultiModalBatchedField(),
45
- )
46
- mm_item = MultiModalKwargsItem.from_elems([mm_elem])
38
+ mm_item = MultiModalKwargsItem.dummy("dummy_m")
39
mm_kwargs = [mm_item] * len(mm_positions)
40
41
return Request(request_id=request_id,
tests/v1/core/test_scheduler.py
+2
-10
@@ -8,9 +8,7 @@ import torch
8
9
from vllm.config import (CacheConfig, KVTransferConfig, ModelConfig,
10
SchedulerConfig, SpeculativeConfig, VllmConfig)
11
-from vllm.multimodal.inputs import (MultiModalBatchedField,
12
- MultiModalFieldElem, MultiModalKwargsItem,
13
- PlaceholderRange)
11
+from vllm.multimodal.inputs import MultiModalKwargsItem, PlaceholderRange
12
from vllm.sampling_params import GuidedDecodingParams, SamplingParams
13
from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
14
from vllm.v1.core.sched.scheduler import Scheduler
@@ -1328,13 +1326,7 @@ def create_requests_with_priority(
1326
for i in range(num_requests):
1327
if mm_positions is not None:
1328
mm_position = mm_positions[i]
1331
- mm_elem = MultiModalFieldElem(
1332
- modality="dummy_m",
1333
- key="dummy_k",
1334
- data=None,
1335
- field=MultiModalBatchedField(),
1336
- )
1337
- mm_item = MultiModalKwargsItem.from_elems([mm_elem])
1329
+ mm_item = MultiModalKwargsItem.dummy("dummy_m")
1330
mm_kwargs = [mm_item] * len(mm_position)
1331
else:
1332
mm_position = None
tests/v1/core/utils.py
+2
-10
@@ -6,9 +6,7 @@ import torch
6
7
from vllm.config import (CacheConfig, KVTransferConfig, ModelConfig,
8
SchedulerConfig, SpeculativeConfig, VllmConfig)
9
-from vllm.multimodal.inputs import (MultiModalBatchedField,
10
- MultiModalFieldElem, MultiModalKwargsItem,
11
- PlaceholderRange)
9
+from vllm.multimodal.inputs import MultiModalKwargsItem, PlaceholderRange
10
from vllm.sampling_params import SamplingParams
11
from vllm.v1.core.kv_cache_utils import (get_request_block_hasher,
12
init_none_hash)
@@ -143,13 +141,7 @@ def create_requests(
141
for i in range(num_requests):
142
if mm_positions is not None:
143
mm_position = mm_positions[i]
146
- mm_elem = MultiModalFieldElem(
147
- modality="dummy_m",
148
- key="dummy_k",
149
- data=None,
150
- field=MultiModalBatchedField(),
151
- )
152
- mm_item = MultiModalKwargsItem.from_elems([mm_elem])
144
+ mm_item = MultiModalKwargsItem.dummy("dummy_m")
145
mm_kwargs = [mm_item] * len(mm_position)
146
mm_hashes = ["hash"] * len(mm_position)
147
else:
vllm/multimodal/inputs.py
+17
-45
@@ -4,7 +4,7 @@
4
from abc import ABC, abstractmethod
5
from collections import UserDict, defaultdict
6
from collections.abc import Mapping, Sequence
7
-from dataclasses import dataclass, replace
7
+from dataclasses import dataclass
8
from functools import partial
9
from itertools import accumulate
10
from typing import (TYPE_CHECKING, Any, Literal, Optional, TypedDict, TypeVar,
@@ -218,7 +218,7 @@ class MultiModalFieldElem:
218
i.e. the name of the keyword argument to be passed to the model.
219
"""
220
221
- data: Optional[NestedTensors]
221
+ data: NestedTensors
222
"""
223
The tensor data of this field in
224
[`MultiModalKwargs`][vllm.multimodal.inputs.MultiModalKwargs],
@@ -315,13 +315,8 @@ class BaseMultiModalField(ABC):
315
if len(set(field_types)) > 1:
316
raise ValueError(f"Cannot merge different {field_types=}")
317
318
- validated_data = list[NestedTensors]()
319
- for i, elem in enumerate(elems):
320
- assert elem.data is not None, (
321
- f"Cannot merge with empty `elems[{i}]`")
322
- validated_data.append(elem.data)
323
-
324
- return self._reduce_data(validated_data, pin_memory=pin_memory)
318
+ batch = [elem.data for elem in elems]
319
+ return self._reduce_data(batch, pin_memory=pin_memory)
320
321
322
@dataclass(frozen=True)
@@ -643,6 +638,17 @@ class MultiModalKwargsItem(UserDict[str, MultiModalFieldElem]):
638
[`MultiModalDataItems`][vllm.multimodal.parse.MultiModalDataItems].
639
"""
640
641
+ @staticmethod
642
+ def dummy(modality: str):
643
+ """Convenience class for testing."""
644
+ mm_elem = MultiModalFieldElem(
645
+ modality=modality,
646
+ key="dummy",
647
+ data=torch.empty(1),
648
+ field=MultiModalSharedField(1),
649
+ )
650
+ return MultiModalKwargsItem.from_elems([mm_elem])
651
+
652
@staticmethod
653
def from_elems(elems: Sequence[MultiModalFieldElem]):
654
return MultiModalKwargsItem({elem.key: elem for elem in elems})
@@ -654,46 +660,12 @@ class MultiModalKwargsItem(UserDict[str, MultiModalFieldElem]):
660
assert len(modalities) == 1, f"Found different modalities={modalities}"
661
self._modality = next(iter(modalities))
662
657
- self._is_empty = any(elem.data is None for elem in self.values())
658
-
663
@property
664
def modality(self) -> str:
665
return self._modality
666
663
- @property
664
- def is_empty(self) -> bool:
665
- return self._is_empty
666
-
667
- def get_data(self) -> Optional[Mapping[str, NestedTensors]]:
668
- if self._is_empty:
669
- return None
670
-
671
- out_data = dict[str, NestedTensors]()
672
- for key, elem in self.items():
673
- assert elem.data is not None, (
674
- f"Cannot get data of empty `elem[{key!r}]`")
675
- out_data[key] = elem.data
676
-
677
- return out_data
678
-
679
- def require_data(self) -> Mapping[str, NestedTensors]:
680
- if (data := self.get_data()) is None:
681
- raise RuntimeError("Cannot get data of empty item")
682
-
683
- return data
684
-
685
- # These methods create a new item to avoid mutating cached items in place
686
- def with_data(self, data: Mapping[str, NestedTensors]):
687
- return MultiModalKwargsItem({
688
- key: replace(elem, data=data[key])
689
- for key, elem in self.items()
690
- })
691
-
692
- def without_data(self):
693
- return MultiModalKwargsItem({
694
- key: replace(elem, data=None)
695
- for key, elem in self.items()
696
- })
667
+ def get_data(self) -> Mapping[str, NestedTensors]:
668
+ return {key: elem.data for key, elem in self.items()}
669
670
671
# NOTE: UserDict is for V0 compatibility.
vllm/v1/engine/__init__.py
+2
-1
@@ -3,6 +3,7 @@
3
4
import enum
5
import time
6
+from collections.abc import Sequence
7
from typing import Any, Optional, Union
8
9
import msgspec
@@ -47,7 +48,7 @@ class EngineCoreRequest(
48
49
request_id: str
50
prompt_token_ids: list[int]
50
- mm_kwargs: Optional[list[MultiModalKwargsItem]]
51
+ mm_kwargs: Optional[Sequence[Optional[MultiModalKwargsItem]]]
52
mm_hashes: Optional[list[str]]
53
mm_placeholders: Optional[list[PlaceholderRange]]
54
sampling_params: Optional[SamplingParams]
vllm/v1/engine/mm_input_cache.py
+18
-15
@@ -1,11 +1,12 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from collections.abc import Mapping
4
-from typing import TYPE_CHECKING
3
+from collections.abc import Sequence
4
+from typing import TYPE_CHECKING, Optional
5
6
from vllm.multimodal import MultiModalRegistry
7
from vllm.multimodal.cache import MultiModalCache, MultiModalCacheItemMetadata
8
-from vllm.multimodal.inputs import MultiModalKwargsItem, NestedTensors
8
+from vllm.multimodal.inputs import MultiModalKwargsItem
9
+from vllm.utils import is_list_of
10
11
if TYPE_CHECKING:
12
from vllm.config import ModelConfig
@@ -58,21 +59,21 @@ class MultiModalInputCacheClient:
59
60
def get_and_update(
61
self,
61
- mm_kwargs: list[MultiModalKwargsItem],
62
+ mm_kwargs: Sequence[MultiModalKwargsItem],
63
mm_hashes: list[str],
63
- ) -> list[MultiModalKwargsItem]:
64
+ ) -> list[Optional[MultiModalKwargsItem]]:
65
if not self.enabled:
65
- return mm_kwargs
66
+ return list(mm_kwargs)
67
68
assert len(mm_kwargs) == len(mm_hashes)
69
69
- out_mm_items = list[MultiModalKwargsItem]()
70
+ out_mm_items = list[Optional[MultiModalKwargsItem]]()
71
for mm_item, mm_hash in zip(mm_kwargs, mm_hashes):
72
if self.mm_cache.get(mm_hash) is not None:
72
- out_mm_items.append(mm_item.without_data())
73
+ out_mm_items.append(None)
74
else:
75
self.mm_cache[mm_hash] = \
75
- MultiModalCacheItemMetadata.wraps(mm_item.require_data())
76
+ MultiModalCacheItemMetadata.wraps(mm_item)
77
out_mm_items.append(mm_item)
78
79
return out_mm_items
@@ -91,25 +92,27 @@ class MultiModalInputCacheServer:
92
self.enabled = mm_registry.enable_mm_input_cache(model_config)
93
self.mm_cache = MultiModalCache.get_lru_cache(
94
model_config.get_mm_input_cache_gb(),
94
- Mapping[str, NestedTensors],
95
+ MultiModalKwargsItem,
96
)
97
98
def get_and_update(
99
self,
99
- mm_kwargs: list[MultiModalKwargsItem],
100
+ mm_kwargs: Sequence[Optional[MultiModalKwargsItem]],
101
mm_hashes: list[str],
102
) -> list[MultiModalKwargsItem]:
103
if not self.enabled:
103
- return mm_kwargs
104
+ mm_kwargs_lst = list(mm_kwargs)
105
+ assert is_list_of(mm_kwargs_lst, MultiModalKwargsItem)
106
+ return mm_kwargs_lst
107
108
assert len(mm_kwargs) == len(mm_hashes)
109
110
out_mm_items = list[MultiModalKwargsItem]()
111
for mm_item, mm_hash in zip(mm_kwargs, mm_hashes):
109
- if (mm_data := mm_item.get_data()) is None:
110
- out_mm_items.append(mm_item.with_data(self.mm_cache[mm_hash]))
112
+ if mm_item is None:
113
+ out_mm_items.append(self.mm_cache[mm_hash])
114
else:
112
- self.mm_cache[mm_hash] = mm_data
115
+ self.mm_cache[mm_hash] = mm_item
116
out_mm_items.append(mm_item)
117
118
return out_mm_items
vllm/v1/engine/processor.py
+7
-3
@@ -17,6 +17,7 @@ from vllm.multimodal.utils import argsort_mm_positions
17
from vllm.pooling_params import PoolingParams
18
from vllm.sampling_params import SamplingParams
19
from vllm.transformers_utils.tokenizer_group import TokenizerGroup
20
+from vllm.utils import is_list_of
21
from vllm.v1.engine import EngineCoreRequest
22
from vllm.v1.engine.mm_input_cache import MultiModalInputCacheClient
23
from vllm.v1.structured_output.backend_guidance import (
@@ -295,7 +296,7 @@ class Processor:
296
pooling_params = params.clone()
297
298
# Multimodal related.
298
- sorted_mm_inputs: Optional[list[MultiModalKwargsItem]] = None
299
+ sorted_mm_inputs: Optional[list[Optional[MultiModalKwargsItem]]] = None
300
sorted_mm_positions: Optional[list[PlaceholderRange]] = None
301
sorted_mm_hashes: Optional[list[str]] = None
302
if decoder_inputs["type"] == "multimodal":
@@ -308,7 +309,7 @@ class Processor:
309
# in the input sequence.
310
sorted_mm_idxs = argsort_mm_positions(decoder_mm_positions)
311
311
- sorted_mm_inputs = [
312
+ orig_sorted_mm_inputs = [
313
decoder_mm_inputs.get_item(modality, idx)
314
for modality, idx in sorted_mm_idxs
315
]
@@ -323,9 +324,12 @@ class Processor:
324
325
if sorted_mm_hashes is not None:
326
sorted_mm_inputs = self.mm_input_cache_client.get_and_update(
326
- sorted_mm_inputs,
327
+ orig_sorted_mm_inputs,
328
sorted_mm_hashes,
329
)
330
+ else:
331
+ assert is_list_of(orig_sorted_mm_inputs, MultiModalKwargsItem)
332
+ sorted_mm_inputs = orig_sorted_mm_inputs
333
334
return decoder_inputs.get("prompt"), EngineCoreRequest(
335
request_id=request_id,
vllm/v1/request.py
+5
-2
@@ -125,14 +125,17 @@ class Request:
125
block_hasher: Optional[Callable[["Request"], list["BlockHash"]]]
126
) -> "Request":
127
if request.mm_kwargs is not None:
128
- assert is_list_of(request.mm_kwargs, MultiModalKwargsItem), (
128
+ mm_kwargs_lst = list(request.mm_kwargs)
129
+ assert is_list_of(mm_kwargs_lst, MultiModalKwargsItem), (
130
"mm_kwargs was not updated in EngineCore.add_request")
131
+ else:
132
+ mm_kwargs_lst = None
133
134
return cls(
135
request_id=request.request_id,
136
client_index=request.client_index,
137
prompt_token_ids=request.prompt_token_ids,
135
- multi_modal_kwargs=request.mm_kwargs,
138
+ multi_modal_kwargs=mm_kwargs_lst,
139
multi_modal_hashes=request.mm_hashes,
140
multi_modal_placeholders=request.mm_placeholders,
141
sampling_params=request.sampling_params,
vllm/v1/worker/gpu_model_runner.py
+2
-2
@@ -500,8 +500,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
500
second_per_grid_ts = []
501
audio_feature_lengths = []
502
use_audio_in_video = False
503
- for item in self.requests[req_id].mm_kwargs:
504
- mm_input = item.require_data()
503
+ for mm_item in self.requests[req_id].mm_kwargs:
504
+ mm_input = mm_item.get_data()
505
if mm_input.get("image_grid_thw") is not None:
506
image_grid_thw.append(
507
mm_input["image_grid_thw"].tolist())