@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / fe3398fab2
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 12, 08:30 UTC

[Chore] Enable passing `tokenizer=None` into MM processor (#29724)

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

Cyrus Leung committed Nov 29, 2025 at 22:25 UTC fe3398fab2b11f92b4ada209a85558710c446a36
8 files changed +68 -91
tests/multimodal/test_processing.py
+8 -33
@@ -3,7 +3,6 @@
3
4 import time
5 from contextlib import nullcontext
6 -from typing import cast
6
7 import numpy as np
8 import pytest
@@ -24,7 +23,6 @@ from vllm.multimodal.processing import (
23 replace_token_matches,
24 )
25 from vllm.multimodal.profiling import MultiModalProfiler
27 -from vllm.tokenizers import TokenizerLike
26
27 from .utils import random_image
28
@@ -238,15 +236,12 @@ def test_find_token_matches(
236 expected_by_key,
237 update_type,
238 ):
241 - # Should not be used since there is nothing to convert to token IDs
242 - mock_tokenizer = cast(TokenizerLike, object())
243 -
239 prompt_updates = {
240 key: update_type(key, target, []).resolve(0)
241 for key, target in target_by_key.items()
242 }
243 result = {
249 - key: list(update.iter_token_matches(prompt, mock_tokenizer))
244 + key: list(update.iter_token_matches(prompt, tokenizer=None))
245 for key, update in prompt_updates.items()
246 }
247
@@ -385,15 +380,12 @@ def test_find_text_matches(
380 expected_by_key,
381 update_type,
382 ):
388 - # Should not be used since there is nothing to convert to text
389 - mock_tokenizer = cast(TokenizerLike, object())
390 -
383 prompt_updates = {
384 key: update_type(key, target, []).resolve(0)
385 for key, target in target_by_key.items()
386 }
387 result = {
396 - key: list(update.iter_text_matches(prompt, mock_tokenizer))
388 + key: list(update.iter_text_matches(prompt, tokenizer=None))
389 for key, update in prompt_updates.items()
390 }
391
@@ -545,9 +537,6 @@ def test_find_update_text(
537 repl_by_key,
538 expected_by_update_type_mm_count,
539 ):
548 - # Should not be used since there is nothing to convert to text
549 - mock_tokenizer = cast(TokenizerLike, object())
550 -
540 for (
541 update_type,
542 expected_by_mm_count,
@@ -564,7 +553,7 @@ def test_find_update_text(
553 new_prompt, result = apply_text_matches(
554 prompt,
555 mm_prompt_updates,
567 - mock_tokenizer,
556 + tokenizer=None,
557 )
558
559 # Only displayed on error
@@ -750,9 +739,6 @@ def test_find_update_tokens(
739 repl_by_key,
740 expected_by_update_type_mm_count,
741 ):
753 - # Should not be used since there is nothing to convert to tokens
754 - mock_tokenizer = cast(TokenizerLike, object())
755 -
742 for (
743 update_type,
744 expected_by_mm_count,
@@ -769,7 +755,7 @@ def test_find_update_tokens(
755 new_prompt, result = apply_token_matches(
756 prompt,
757 mm_prompt_updates,
772 - mock_tokenizer,
758 + tokenizer=None,
759 )
760
761 # Only displayed on error
@@ -900,15 +886,12 @@ def test_find_mm_placeholders(
886 expected,
887 update_type,
888 ):
903 - # Should not be used since there is nothing to convert to tokens
904 - mock_tokenizer = cast(TokenizerLike, object())
905 -
889 mm_prompt_updates = {
890 key: [[update_type(key, [], repl).resolve(i)] for i in range(3)]
891 for key, repl in repl_by_key.items()
892 }
893
911 - result = find_mm_placeholders(prompt, mm_prompt_updates, mock_tokenizer)
894 + result = find_mm_placeholders(prompt, mm_prompt_updates, tokenizer=None)
895
896 # Only displayed on error
897 print("result:", result)
@@ -1029,12 +1012,9 @@ def test_hf_processor_init_kwargs(
1012 inference_kwargs,
1013 expected_kwargs,
1014 ):
1032 - # Should not be used since there is nothing to convert to tokens
1033 - mock_tokenizer = cast(TokenizerLike, object())
1034 -
1015 ctx = InputProcessingContext(
1016 model_config=ModelConfig(model_id, mm_processor_kwargs=config_kwargs),
1037 - tokenizer=mock_tokenizer,
1017 + tokenizer=None,
1018 )
1019
1020 processor = ctx.get_hf_processor(
@@ -1065,12 +1045,9 @@ def test_hf_processor_call_kwargs(
1045 inference_kwargs,
1046 expected_kwargs,
1047 ):
1068 - # Should not be used since there is nothing to convert to tokens
1069 - mock_tokenizer = cast(TokenizerLike, object())
1070 -
1048 ctx = InputProcessingContext(
1049 model_config=ModelConfig(model_id, mm_processor_kwargs=config_kwargs),
1073 - tokenizer=mock_tokenizer,
1050 + tokenizer=None,
1051 )
1052
1053 processor = ctx.get_hf_processor(DummyProcessor) # type: ignore[arg-type]
@@ -1089,8 +1066,6 @@ def test_apply_matches_no_match_exits_quickly():
1066
1067 With the fix, it should exit immediately when no match is found.
1068 """
1092 - mock_tokenizer = cast(TokenizerLike, object())
1093 -
1069 # Create a long prompt with no placeholder
1070 long_prompt = "x" * 10000
1071
@@ -1103,7 +1078,7 @@ def test_apply_matches_no_match_exits_quickly():
1078 result, _ = _apply_matches(
1079 long_prompt,
1080 mm_prompt_updates,
1106 - mock_tokenizer,
1081 + tokenizer=None,
1082 )
1083 elapsed = time.perf_counter() - start
1084
vllm/entrypoints/openai/serving_engine.py
+1 -1
@@ -337,7 +337,7 @@ class OpenAIServing:
337 tokenizer = input_processor.tokenizer
338 if tokenizer is None:
339 raise ValueError(
340 - "You cannot use beam search when `skip_tokenizer_init` is True"
340 + "You cannot use beam search when `skip_tokenizer_init=True`"
341 )
342
343 eos_token_id: int = tokenizer.eos_token_id # type: ignore
vllm/inputs/preprocess.py
+2 -13
@@ -62,7 +62,7 @@ class InputPreprocessor:
62 def get_tokenizer(self) -> TokenizerLike:
63 if self.tokenizer is None:
64 raise ValueError(
65 - "You cannot pass text prompts when `skip_tokenizer_init` is True"
65 + "You cannot pass text prompts when `skip_tokenizer_init=True`"
66 )
67
68 return self.tokenizer
@@ -228,22 +228,11 @@ class InputPreprocessor:
228
229 return tokenizer.encode(prompt, **tokenization_kwargs)
230
231 - def _get_mm_tokenizer(self) -> TokenizerLike:
232 - # PrithviGeoSpatialMAE needs to be initialized without a tokenizer
233 - # while using also multi-modal input
234 - if not self.tokenizer:
235 - return cast(TokenizerLike, object()) # Dummy
236 -
237 - tokenizer = self.get_tokenizer()
238 - return tokenizer
239 -
231 def _get_mm_processor(self) -> BaseMultiModalProcessor:
232 if not hasattr(self, "_mm_processor"):
242 - tokenizer = self._get_mm_tokenizer()
243 -
233 self._mm_processor = self.mm_registry.create_processor(
234 self.model_config,
246 - tokenizer=tokenizer,
235 + tokenizer=self.tokenizer,
236 cache=self.mm_processor_cache,
237 )
238
vllm/model_executor/models/glm4_1v.py
-6
@@ -866,12 +866,6 @@ class Glm4vVisionTransformer(nn.Module):
866
867
868 class Glm4vProcessingInfo(BaseProcessingInfo):
869 - def get_hf_config(self):
870 - return self.ctx.get_hf_config()
871 -
872 - def get_tokenizer(self):
873 - return self.ctx.tokenizer
874 -
869 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
870 return {"image": None, "video": 1}
871
vllm/model_executor/models/qwen3_vl.py
-3
@@ -615,9 +615,6 @@ class Qwen3VLProcessingInfo(Qwen2VLProcessingInfo):
615 **kwargs,
616 )
617
618 - def get_tokenizer(self):
619 - return self.ctx.tokenizer
620 -
618 def get_image_processor(self, **kwargs: object) -> Qwen2VLImageProcessorFast:
619 return self.get_hf_processor(**kwargs).image_processor
620
vllm/model_executor/models/qwen_vl.py
+1 -1
@@ -555,7 +555,7 @@ class QwenVLProcessor:
555
556 class QwenVLProcessingInfo(BaseProcessingInfo):
557 def get_tokenizer(self) -> PreTrainedTokenizer:
558 - tokenizer = self.ctx.tokenizer
558 + tokenizer = self.ctx.get_tokenizer()
559 assert isinstance(tokenizer, PreTrainedTokenizer)
560
561 return _get_tokenizer_without_image_pad(tokenizer)
vllm/multimodal/processing.py
+55 -31
@@ -97,15 +97,37 @@ def _cached_decode(
97 )
98
99
100 -def _seq2text(tokenizer: TokenizerLike, seq: PromptSeq) -> str:
100 +def _seq2text(
101 + tokenizer: TokenizerLike | None,
102 + seq: PromptSeq,
103 + *,
104 + use_cache: bool = True,
105 +) -> str:
106 if isinstance(seq, str):
107 return seq
108
109 + if tokenizer is None:
110 + raise ValueError("You cannot decode tokens when `skip_tokenizer_init=True`")
111 +
112 + if not use_cache:
113 + return decode_tokens(tokenizer, seq)
114 +
115 return _cached_decode(tokenizer, tuple(seq))
116
117
107 -def _seq2tokens(tokenizer: TokenizerLike, seq: PromptSeq) -> list[int]:
118 +def _seq2tokens(
119 + tokenizer: TokenizerLike | None,
120 + seq: PromptSeq,
121 + *,
122 + use_cache: bool = True,
123 +) -> list[int]:
124 if isinstance(seq, str):
125 + if tokenizer is None:
126 + raise ValueError("You cannot encode text when `skip_tokenizer_init=True`")
127 +
128 + if not use_cache:
129 + return encode_tokens(tokenizer, seq, add_special_tokens=False)
130 +
131 return _cached_encode(tokenizer, seq, add_special_tokens=False)
132
133 return seq
@@ -114,7 +136,7 @@ def _seq2tokens(tokenizer: TokenizerLike, seq: PromptSeq) -> list[int]:
136 class _GetMatchIndex(Protocol):
137 def __call__(
138 self,
117 - tokenizer: TokenizerLike,
139 + tokenizer: TokenizerLike | None,
140 prompt: PromptSeq,
141 start_idx: int = 0,
142 ) -> int | None: ...
@@ -144,7 +166,7 @@ class PromptIndexTargets:
166 """
167
168 def get_match_index(
147 - tokenizer: TokenizerLike,
169 + tokenizer: TokenizerLike | None,
170 prompt: PromptSeq,
171 start_idx: int = 0,
172 ) -> int | None:
@@ -154,13 +176,11 @@ class PromptIndexTargets:
176 prefix = seq
177
178 if isinstance(prompt, str):
157 - if not isinstance(prefix, str):
158 - # Make both `str`
159 - prefix = decode_tokens(tokenizer, prefix)
179 + # Make both `str`
180 + prefix = _seq2text(tokenizer, prefix, use_cache=False)
181 else:
161 - if isinstance(prefix, str):
162 - # Make both `list[int]`
163 - prefix = encode_tokens(tokenizer, prefix, add_special_tokens=False)
182 + # Make both `list[int]`
183 + prefix = _seq2tokens(tokenizer, prefix, use_cache=False)
184
185 match_idx = len(prefix)
186 return match_idx if prompt[:match_idx] == prefix else None
@@ -200,7 +220,7 @@ class PromptUpdateDetails(Generic[_S]):
220 full: _S
221 """The full content."""
222
203 - is_embed: Callable[[TokenizerLike, PromptSeq], torch.Tensor] | None = None
223 + is_embed: Callable[[TokenizerLike | None, PromptSeq], torch.Tensor] | None = None
224 """
225 Given [`full`][vllm.multimodal.processing.PromptUpdateDetails.full],
226 return a boolean mask of shape `(len(full),)` indicating which positions
@@ -221,8 +241,8 @@ class PromptUpdateDetails(Generic[_S]):
241 seq: _S,
242 embed_text: str,
243 ) -> "PromptUpdateDetails[_S]":
224 - def is_embed(tokenizer: TokenizerLike, full: PromptSeq) -> torch.Tensor:
225 - embed_token_ids = encode_tokens(tokenizer, embed_text)
244 + def is_embed(tokenizer: TokenizerLike | None, full: PromptSeq) -> torch.Tensor:
245 + embed_token_ids = _seq2tokens(tokenizer, embed_text, use_cache=False)
246 token_ids = _seq2tokens(tokenizer, full)
247
248 return torch.isin(
@@ -237,7 +257,7 @@ class PromptUpdateDetails(Generic[_S]):
257 seq: _S,
258 embed_token_id: int,
259 ) -> "PromptUpdateDetails[_S]":
240 - def is_embed(tokenizer: TokenizerLike, full: PromptSeq) -> torch.Tensor:
260 + def is_embed(tokenizer: TokenizerLike | None, full: PromptSeq) -> torch.Tensor:
261 token_ids = _seq2tokens(tokenizer, full)
262
263 return torch.tensor(token_ids) == embed_token_id
@@ -523,7 +543,7 @@ class ResolvedPromptUpdate:
543 def iter_token_matches(
544 self,
545 prompt: list[int],
526 - tokenizer: TokenizerLike,
546 + tokenizer: TokenizerLike | None,
547 *,
548 start_idx: int = 0,
549 ) -> Generator[PromptTargetMatch]:
@@ -545,7 +565,7 @@ class ResolvedPromptUpdate:
565 def iter_text_matches(
566 self,
567 prompt: str,
548 - tokenizer: TokenizerLike,
568 + tokenizer: TokenizerLike | None,
569 *,
570 start_idx: int = 0,
571 ) -> Generator[PromptTargetMatch]:
@@ -567,7 +587,7 @@ class ResolvedPromptUpdate:
587 def iter_matches(
588 self,
589 prompt: list[int] | str,
570 - tokenizer: TokenizerLike,
590 + tokenizer: TokenizerLike | None,
591 *,
592 start_idx: int = 0,
593 ) -> Generator[PromptTargetMatch]:
@@ -676,7 +696,7 @@ _MatchToApply = tuple[tuple[str, int], tuple[PromptTargetMatch, int]]
696 def _find_matches(
697 prompt: _S,
698 mm_prompt_updates: "MultiModalPromptUpdates",
679 - tokenizer: TokenizerLike,
699 + tokenizer: TokenizerLike | None,
700 *,
701 prev_end_idx: int = 0,
702 current_result: "MultiModalPromptUpdatesApplyResult",
@@ -741,7 +761,7 @@ def _all_items_found(
761 def _apply_matches(
762 prompt: _S,
763 mm_prompt_updates: "MultiModalPromptUpdates",
744 - tokenizer: TokenizerLike,
764 + tokenizer: TokenizerLike | None,
765 ) -> tuple[list[_S], "MultiModalPromptUpdatesApplyResult"]:
766 mm_item_counts = {m: len(items) for m, items in mm_prompt_updates.items()}
767
@@ -807,7 +827,7 @@ def _apply_matches(
827 def apply_token_matches(
828 prompt: list[int],
829 mm_prompt_updates: "MultiModalPromptUpdates",
810 - tokenizer: TokenizerLike,
830 + tokenizer: TokenizerLike | None,
831 ) -> tuple[list[int], "MultiModalPromptUpdatesApplyResult"]:
832 """
833 Apply the updates in `mm_prompt_updates` to `prompt`.
@@ -824,7 +844,7 @@ def apply_token_matches(
844 def apply_text_matches(
845 prompt: str,
846 mm_prompt_updates: "MultiModalPromptUpdates",
827 - tokenizer: TokenizerLike,
847 + tokenizer: TokenizerLike | None,
848 ) -> tuple[str, "MultiModalPromptUpdatesApplyResult"]:
849 """
850 Apply the updates in `mm_prompt_updates` to `prompt`.
@@ -841,7 +861,7 @@ def apply_text_matches(
861 def _iter_placeholders(
862 prompt: list[int],
863 mm_prompt_updates: "MultiModalPromptUpdates",
844 - tokenizer: TokenizerLike,
864 + tokenizer: TokenizerLike | None,
865 ) -> Iterable[PlaceholderFeaturesInfo]:
866 """
867 Yield each set of placeholder tokens found in `prompt`.
@@ -910,7 +930,7 @@ def _iter_placeholders(
930 def find_mm_placeholders(
931 prompt: list[int],
932 mm_prompt_updates: "MultiModalPromptUpdates",
913 - tokenizer: TokenizerLike,
933 + tokenizer: TokenizerLike | None,
934 ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
935 it = _iter_placeholders(prompt, mm_prompt_updates, tokenizer)
936 return dict(full_groupby_modality(it))
@@ -931,9 +951,17 @@ class InputProcessingContext:
951 model_config: ModelConfig
952 """The configuration of the model."""
953
934 - tokenizer: TokenizerLike
954 + tokenizer: TokenizerLike | None
955 """The tokenizer used to tokenize the inputs."""
956
957 + def get_tokenizer(self) -> TokenizerLike:
958 + if self.tokenizer is None:
959 + raise ValueError(
960 + "You cannot pass text prompts when `skip_tokenizer_init=True`"
961 + )
962 +
963 + return self.tokenizer
964 +
965 @overload
966 def get_hf_config(self, /) -> PretrainedConfig: ...
967
@@ -1148,7 +1176,7 @@ class BaseProcessingInfo:
1176 return self.ctx.model_config.model
1177
1178 def get_tokenizer(self) -> TokenizerLike:
1151 - return self.ctx.tokenizer
1179 + return self.ctx.get_tokenizer()
1180
1181 def get_hf_config(self) -> PretrainedConfig:
1182 return self.ctx.get_hf_config()
@@ -1960,15 +1988,11 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1988 for update_idxs in match_result.values()
1989 ):
1990 new_text, match_result = self._apply_text_matches(
1963 - decode_tokens(tokenizer, token_ids),
1991 + _seq2text(tokenizer, token_ids, use_cache=False),
1992 mm_prompt_updates,
1993 )
1994
1967 - new_token_ids = encode_tokens(
1968 - tokenizer,
1969 - new_text,
1970 - add_special_tokens=False,
1971 - )
1995 + new_token_ids = _seq2tokens(tokenizer, new_text, use_cache=False)
1996
1997 matched_updates = defaultdict[str, list[Sequence[ResolvedPromptUpdate]]](list)
1998 for modality, update_idxs in match_result.items():
vllm/multimodal/registry.py
+1 -3
@@ -234,9 +234,7 @@ class MultiModalRegistry:
234 model_config: "ModelConfig",
235 tokenizer: TokenizerLike | None = None,
236 ) -> InputProcessingContext:
237 - if model_config.skip_tokenizer_init:
238 - tokenizer = cast(TokenizerLike, object())
239 - elif tokenizer is None:
237 + if tokenizer is None and not model_config.skip_tokenizer_init:
238 tokenizer = cached_tokenizer_from_config(model_config)
239
240 return InputProcessingContext(model_config, tokenizer)