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 8, 06:15 UTC
[Renderer] Move InputPreprocessor into Renderer (1/2) (#34510)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk> Signed-off-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
Cyrus Leung committed
Feb 15, 2026 at 02:14 UTC
73391a1baa459e78be1ade466517c7206ab7dd7c
39 files changed
+456
-458
tests/entrypoints/openai/test_chat_error.py
+2
-1
@@ -54,6 +54,7 @@ class MockModelConfig:
54
media_io_kwargs: dict[str, dict[str, Any]] = field(default_factory=dict)
55
skip_tokenizer_init = False
56
is_encoder_decoder: bool = False
57
+ is_multimodal_model: bool = False
58
59
def get_diff_sampling_param(self):
60
return self.diff_sampling_param or {}
@@ -67,7 +68,7 @@ class MockVllmConfig:
68
def _build_renderer(model_config: MockModelConfig):
69
_, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
70
70
- return HfRenderer(
71
+ return HfRenderer.from_config(
72
MockVllmConfig(model_config),
73
tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
74
)
tests/entrypoints/openai/test_completion_error.py
+2
-1
@@ -53,6 +53,7 @@ class MockModelConfig:
53
media_io_kwargs: dict[str, dict[str, Any]] = field(default_factory=dict)
54
skip_tokenizer_init = False
55
is_encoder_decoder: bool = False
56
+ is_multimodal_model: bool = False
57
58
def get_diff_sampling_param(self):
59
return self.diff_sampling_param or {}
@@ -78,7 +79,7 @@ def _build_serving_completion(engine: AsyncLLM) -> OpenAIServingCompletion:
79
def _build_renderer(model_config: MockModelConfig):
80
_, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
81
81
- return HfRenderer(
82
+ return HfRenderer.from_config(
83
MockVllmConfig(model_config),
84
tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
85
)
tests/entrypoints/openai/test_lora_resolvers.py
+2
-1
@@ -52,6 +52,7 @@ class MockModelConfig:
52
generation_config: str = "auto"
53
skip_tokenizer_init: bool = False
54
is_encoder_decoder: bool = False
55
+ is_multimodal_model: bool = False
56
57
def get_diff_sampling_param(self):
58
return self.diff_sampling_param or {}
@@ -95,7 +96,7 @@ def register_mock_resolver():
96
def _build_renderer(model_config: MockModelConfig):
97
_, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
98
98
- return HfRenderer(
99
+ return HfRenderer.from_config(
100
MockVllmConfig(model_config),
101
tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
102
)
tests/entrypoints/openai/test_serving_chat.py
+4
-5
@@ -529,6 +529,7 @@ class MockModelConfig:
529
media_io_kwargs: dict[str, dict[str, Any]] = field(default_factory=dict)
530
skip_tokenizer_init: bool = False
531
is_encoder_decoder: bool = False
532
+ is_multimodal_model: bool = False
533
534
def get_diff_sampling_param(self):
535
return self.diff_sampling_param or {}
@@ -542,7 +543,7 @@ class MockVllmConfig:
543
def _build_renderer(model_config: MockModelConfig):
544
_, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
545
545
- return HfRenderer(
546
+ return HfRenderer.from_config(
547
MockVllmConfig(model_config),
548
tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
549
)
@@ -756,9 +757,8 @@ async def test_serving_chat_mistral_token_ids_prompt_is_validated():
757
mock_tokenizer = MagicMock(spec=MistralTokenizer)
758
mock_renderer = MistralRenderer(
759
MockVllmConfig(mock_engine.model_config),
759
- tokenizer_kwargs={},
760
+ tokenizer=mock_tokenizer,
761
)
761
- mock_renderer._tokenizer = mock_tokenizer
762
# Force the Mistral chat template renderer to return token IDs.
763
# Choose a prompt length that is < max_model_len, but large enough that
764
# adding max_tokens should exceed the model context window.
@@ -798,9 +798,8 @@ async def test_serving_chat_mistral_token_ids_prompt_too_long_is_rejected():
798
mock_tokenizer = MagicMock(spec=MistralTokenizer)
799
mock_renderer = MistralRenderer(
800
MockVllmConfig(mock_engine.model_config),
801
- tokenizer_kwargs={},
801
+ tokenizer=mock_tokenizer,
802
)
803
- mock_renderer._tokenizer = mock_tokenizer
803
# prompt_token_ids length == max_model_len should be rejected for
804
# completion-like requests (ChatCompletionRequest).
805
mock_renderer.render_messages_async = AsyncMock(
tests/renderers/test_completions.py
+12
-10
@@ -38,6 +38,7 @@ class MockModelConfig:
38
enable_prompt_embeds: bool = True
39
skip_tokenizer_init: bool = False
40
is_encoder_decoder: bool = False
41
+ is_multimodal_model: bool = False
42
43
44
@dataclass
@@ -78,15 +79,16 @@ def _build_renderer(
79
80
renderer = HfRenderer(
81
MockVllmConfig(model_config),
81
- tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
82
+ tokenizer=(
83
+ None
84
+ if model_config.skip_tokenizer_init
85
+ else DummyTokenizer(
86
+ truncation_side=truncation_side,
87
+ max_chars_per_token=max_chars_per_token,
88
+ )
89
+ ),
90
)
91
84
- if not model_config.skip_tokenizer_init:
85
- renderer._tokenizer = DummyTokenizer(
86
- truncation_side=truncation_side,
87
- max_chars_per_token=max_chars_per_token,
88
- )
89
-
92
return renderer
93
94
@@ -277,7 +279,7 @@ class TestRenderPrompt:
279
)
280
281
# Should not even attempt tokenization
280
- assert renderer._tokenizer._captured_encode_kwargs == {}
282
+ assert renderer.tokenizer._captured_encode_kwargs == {}
283
284
def test_text_max_length_exceeded_nonobvious(self):
285
renderer = _build_renderer(MockModelConfig(), max_chars_per_token=2)
@@ -298,8 +300,8 @@ class TestRenderPrompt:
300
)
301
302
# Should only tokenize the first max_total_tokens + 1 tokens
301
- assert renderer._tokenizer._captured_encode_kwargs["truncation"] is True
302
- assert renderer._tokenizer._captured_encode_kwargs["max_length"] == 101
303
+ assert renderer.tokenizer._captured_encode_kwargs["truncation"] is True
304
+ assert renderer.tokenizer._captured_encode_kwargs["max_length"] == 101
305
306
def test_token_max_length_exceeded(self):
307
renderer = _build_renderer(MockModelConfig())
tests/renderers/test_mistral.py
+2
-2
@@ -36,6 +36,7 @@ class MockModelConfig:
36
enable_prompt_embeds: bool = True
37
skip_tokenizer_init: bool = False
38
is_encoder_decoder: bool = False
39
+ is_multimodal_model: bool = False
40
41
42
@dataclass
@@ -57,9 +58,8 @@ async def test_async_mistral_tokenizer_does_not_block_event_loop():
58
mock_tokenizer.apply_chat_template = mocked_apply_chat_template
59
mock_renderer = MistralRenderer(
60
MockVllmConfig(mock_model_config),
60
- tokenizer_kwargs={},
61
+ tokenizer=mock_tokenizer,
62
)
62
- mock_renderer._tokenizer = mock_tokenizer
63
64
task = mock_renderer.render_messages_async([], ChatParams())
65
tests/v1/e2e/test_streaming_input.py
+1
-1
@@ -19,7 +19,7 @@ import pytest
19
import pytest_asyncio
20
21
from vllm import SamplingParams
22
-from vllm.inputs import StreamingInput
22
+from vllm.engine.protocol import StreamingInput
23
from vllm.outputs import RequestOutput
24
from vllm.platforms import current_platform
25
from vllm.sampling_params import RequestOutputKind
tests/v1/streaming_input/test_async_llm_streaming.py
+1
-1
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock
7
8
import pytest
9
10
-from vllm.inputs import StreamingInput
10
+from vllm.engine.protocol import StreamingInput
11
from vllm.outputs import RequestOutput
12
from vllm.sampling_params import RequestOutputKind, SamplingParams
13
from vllm.v1.engine.async_llm import AsyncLLM
vllm/benchmarks/mm_processor.py
+90
-6
@@ -18,7 +18,7 @@ import dataclasses
18
import json
19
import time
20
from datetime import datetime
21
-from typing import Any
21
+from typing import TYPE_CHECKING, Any
22
23
import numpy as np
24
@@ -28,9 +28,6 @@ from vllm.benchmarks.datasets import (
28
)
29
from vllm.benchmarks.throughput import get_requests
30
from vllm.engine.arg_utils import EngineArgs
31
-from vllm.multimodal.processing.context import (
32
- get_timing_stats_from_engine_client,
33
-)
31
from vllm.utils.gc_utils import freeze_gc_heap
32
from vllm.utils.import_utils import PlaceholderModule
33
@@ -39,16 +36,103 @@ try:
36
except ImportError:
37
pd = PlaceholderModule("pandas")
38
39
+if TYPE_CHECKING: # Avoid having to mock during docs build
40
+ from vllm.v1.engine.llm_engine import LLMEngine
41
+else:
42
+ LLMEngine = object
43
+
44
+
45
+def get_timing_stats_from_engine(llm_engine: LLMEngine) -> dict[str, dict[str, float]]:
46
+ """
47
+ Get all multimodal timing stats from the LLM engine.
48
+
49
+ Collects both preprocessing stats (HF processor, hashing, cache lookup,
50
+ prompt update) and encoder forward pass timing, merged by request_id.
51
+
52
+ Args:
53
+ llm_engine: The LLM engine (has input_processor and workers).
54
+
55
+ Returns:
56
+ Dictionary mapping request_id to merged stats dict containing
57
+ both preprocessing and encoder timing metrics.
58
+
59
+ Example:
60
+ {
61
+ 'request-123': {
62
+ 'hf_processor_time': 0.45,
63
+ 'hashing_time': 0.02,
64
+ 'cache_lookup_time': 0.01,
65
+ 'prompt_update_time': 0.03,
66
+ 'preprocessor_total_time': 0.51,
67
+ 'encoder_forward_time': 0.23,
68
+ 'num_encoder_calls': 1
69
+ }
70
+ }
71
+ """
72
+ observability_config = llm_engine.vllm_config.observability_config
73
+ if not observability_config or not observability_config.enable_mm_processor_stats:
74
+ return {}
75
+
76
+ renderer = llm_engine.renderer
77
+ mm_processor = renderer.get_mm_processor()
78
+ preprocessing_stats = mm_processor.info.ctx.get_all_timing_stats()
79
+
80
+ encoder_stats = dict[str, dict[str, float]]()
81
+ for worker_stats in llm_engine.collective_rpc("get_encoder_timing_stats"):
82
+ if not worker_stats:
83
+ continue
84
+
85
+ for request_id, stats_dict in worker_stats.items():
86
+ if request_id not in encoder_stats:
87
+ encoder_stats[request_id] = dict(stats_dict)
88
+ else:
89
+ # Aggregate timing metrics across workers
90
+ current_time = encoder_stats[request_id].get(
91
+ "encoder_forward_time", 0.0
92
+ )
93
+ new_time = stats_dict.get("encoder_forward_time", 0.0)
94
+ encoder_stats[request_id]["encoder_forward_time"] = max(
95
+ current_time, new_time
96
+ )
97
+
98
+ current_calls = encoder_stats[request_id].get("num_encoder_calls", 0)
99
+ new_calls = stats_dict.get("num_encoder_calls", 0)
100
+ encoder_stats[request_id]["num_encoder_calls"] = max(
101
+ current_calls, new_calls
102
+ )
103
+
104
+ merged_stats = dict[str, dict[str, float]]()
105
+
106
+ for request_id, prep_dict in preprocessing_stats.items():
107
+ merged_stats[request_id] = dict(prep_dict)
108
+
109
+ for request_id, enc_dict in encoder_stats.items():
110
+ if request_id in merged_stats:
111
+ merged_stats[request_id].update(enc_dict)
112
+ continue
113
+
114
+ # In V1 engine, the request_id in encoder_stats has a suffix
115
+ # appended to the original request_id (which is used in
116
+ # preprocessing_stats).
117
+ # We try to strip the suffix to find the matching request.
118
+ possible_original_id = request_id.rpartition("-")[0]
119
+ if possible_original_id and possible_original_id in merged_stats:
120
+ merged_stats[possible_original_id].update(enc_dict)
121
+ else:
122
+ merged_stats[request_id] = dict(enc_dict)
123
+
124
+ return merged_stats
125
+
126
127
def collect_mm_processor_stats(
44
- llm_engine: Any,
128
+ llm_engine: LLMEngine,
129
num_warmup_reqs: int = 0,
130
) -> dict[str, list[float]]:
131
"""
132
Collect multimodal processor timing stats.
133
Returns a dictionary mapping stage names to lists of timing values (in seconds).
134
"""
51
- all_stats = get_timing_stats_from_engine_client(llm_engine)
135
+ all_stats = get_timing_stats_from_engine(llm_engine)
136
137
stat_keys = [
138
"hf_processor_time",
vllm/engine/protocol.py
+14
-1
@@ -3,6 +3,7 @@
3
4
from abc import ABC, abstractmethod
5
from collections.abc import AsyncGenerator, Iterable, Mapping
6
+from dataclasses import dataclass
7
from typing import TYPE_CHECKING, Any
8
9
from vllm.config import ModelConfig, VllmConfig
@@ -10,7 +11,7 @@ from vllm.distributed.weight_transfer.base import (
11
WeightTransferInitRequest,
12
WeightTransferUpdateRequest,
13
)
13
-from vllm.inputs.data import PromptType, StreamingInput
14
+from vllm.inputs.data import PromptType
15
from vllm.lora.request import LoRARequest
16
from vllm.outputs import PoolingRequestOutput, RequestOutput
17
from vllm.plugins.io_processors import IOProcessor
@@ -26,6 +27,18 @@ if TYPE_CHECKING:
27
from vllm.v1.engine import PauseMode
28
29
30
+@dataclass
31
+class StreamingInput:
32
+ """Input data for a streaming generation request.
33
+
34
+ This is used with generate() to support multi-turn streaming sessions
35
+ where inputs are provided via an async generator.
36
+ """
37
+
38
+ prompt: PromptType
39
+ sampling_params: SamplingParams | None = None
40
+
41
+
42
class EngineClient(ABC):
43
"""Protocol class for Clients to Engine"""
44
vllm/entrypoints/llm.py
+16
-29
@@ -72,7 +72,7 @@ from vllm.outputs import (
72
)
73
from vllm.platforms import current_platform
74
from vllm.pooling_params import PoolingParams
75
-from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
75
+from vllm.renderers import ChatParams, merge_kwargs
76
from vllm.renderers.inputs import DictPrompt, TokPrompt
77
from vllm.renderers.inputs.preprocess import (
78
conversation_to_seq,
@@ -384,7 +384,7 @@ class LLM:
384
return parallel_config.world_size
385
386
def reset_mm_cache(self) -> None:
387
- self.input_processor.clear_mm_cache()
387
+ self.renderer.clear_mm_cache()
388
self.llm_engine.reset_mm_cache()
389
390
def get_default_sampling_params(self) -> SamplingParams:
@@ -876,19 +876,6 @@ class LLM:
876
877
return outputs
878
879
- def _get_cmpl_tok_params(self, tokenization_kwargs: dict[str, Any] | None):
880
- model_config = self.model_config
881
- encoder_config = model_config.encoder_config or {}
882
-
883
- return TokenizeParams(
884
- max_total_tokens=model_config.max_model_len,
885
- do_lower_case=encoder_config.get("do_lower_case", False),
886
- # For Whisper, special tokens should be provided by the user based
887
- # on the task and language of their request. Also needed to avoid
888
- # appending an EOS token to the prompt which disrupts generation.
889
- add_special_tokens=not model_config.is_encoder_decoder,
890
- ).with_kwargs(tokenization_kwargs)
891
-
879
def _preprocess_cmpl(
880
self,
881
prompts: Sequence[PromptType],
@@ -910,20 +897,12 @@ class LLM:
897
parsed_prompts = [
898
parse_model_prompt(model_config, prompt) for prompt in prompts
899
]
913
- tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
900
+ tok_params = renderer.default_cmpl_tok_params.with_kwargs(
901
+ **(tokenization_kwargs or {})
902
+ )
903
904
return renderer.render_cmpl(parsed_prompts, tok_params)
905
917
- def _get_chat_tok_params(self, tokenization_kwargs: dict[str, Any] | None):
918
- model_config = self.model_config
919
- encoder_config = model_config.encoder_config or {}
920
-
921
- return TokenizeParams(
922
- max_total_tokens=model_config.max_model_len,
923
- do_lower_case=encoder_config.get("do_lower_case", False),
924
- add_special_tokens=False,
925
- ).with_kwargs(tokenization_kwargs)
926
-
906
def _preprocess_chat(
907
self,
908
conversations: Sequence[list[ChatCompletionMessageParam]],
@@ -961,7 +940,9 @@ class LLM:
940
),
941
),
942
)
964
- tok_params = self._get_chat_tok_params(tokenization_kwargs)
943
+ tok_params = renderer.default_chat_tok_params.with_kwargs(
944
+ **(tokenization_kwargs or {})
945
+ )
946
947
_, engine_prompts = renderer.render_chat(
948
conversations,
@@ -1653,7 +1634,10 @@ class LLM:
1634
architecture=architecture,
1635
)
1636
1656
- tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
1637
+ renderer = self.renderer
1638
+ tok_params = renderer.default_cmpl_tok_params.with_kwargs(
1639
+ **(tokenization_kwargs or {})
1640
+ )
1641
encode_kwargs = tok_params.get_encode_kwargs()
1642
1643
if model_config.is_cross_encoder:
@@ -1970,7 +1954,10 @@ class LLM:
1954
dict(truncate_prompt_tokens=params.truncate_prompt_tokens),
1955
)
1956
1973
- tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
1957
+ renderer = self.renderer
1958
+ tok_params = renderer.default_cmpl_tok_params.with_kwargs(
1959
+ **(tokenization_kwargs or {})
1960
+ )
1961
1962
tokenization_kwargs = tok_params.get_encode_kwargs()
1963
engine_request = self.input_processor.process_inputs(
vllm/entrypoints/openai/realtime/serving.py
+2
-2
@@ -8,11 +8,11 @@ from typing import Literal, cast
8
9
import numpy as np
10
11
-from vllm.engine.protocol import EngineClient
11
+from vllm.engine.protocol import EngineClient, StreamingInput
12
from vllm.entrypoints.logger import RequestLogger
13
from vllm.entrypoints.openai.engine.serving import OpenAIServing
14
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
15
-from vllm.inputs.data import PromptType, StreamingInput
15
+from vllm.inputs.data import PromptType
16
from vllm.logger import init_logger
17
from vllm.model_executor.models.interfaces import SupportsRealtime
18
vllm/inputs/__init__.py
-2
@@ -12,7 +12,6 @@ from .data import (
12
PromptType,
13
SingletonInputs,
14
SingletonPrompt,
15
- StreamingInput,
15
TextPrompt,
16
TokenInputs,
17
TokensPrompt,
@@ -36,5 +35,4 @@ __all__ = [
35
"EncoderDecoderInputs",
36
"ProcessorInputs",
37
"SingletonInputs",
39
- "StreamingInput",
38
]
vllm/inputs/data.py
-15
@@ -1,13 +1,10 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-from dataclasses import dataclass
3
from typing import TYPE_CHECKING, Any, Literal, TypeAlias
4
5
import torch
6
from typing_extensions import NotRequired, TypedDict
7
9
-from vllm.sampling_params import SamplingParams
10
-
8
if TYPE_CHECKING:
9
from vllm.multimodal.inputs import (
10
MultiModalDataDict,
@@ -299,15 +296,3 @@ which can be passed to
296
297
SingletonInputs: TypeAlias = DecoderOnlyInputs | MultiModalEncDecInputs
298
"""The inputs for a single encoder/decoder prompt."""
302
-
303
-
304
-@dataclass
305
-class StreamingInput:
306
- """Input data for a streaming generation request.
307
-
308
- This is used with generate() to support multi-turn streaming sessions
309
- where inputs are provided via an async generator.
310
- """
311
-
312
- prompt: PromptType
313
- sampling_params: SamplingParams | None = None
vllm/inputs/preprocess.py
+23
-91
@@ -9,13 +9,11 @@ from typing_extensions import assert_never
9
from vllm.config import VllmConfig
10
from vllm.logger import init_logger
11
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
12
-from vllm.multimodal.cache import BaseMultiModalProcessorCache
12
from vllm.multimodal.inputs import (
13
MultiModalDataDict,
14
MultiModalInputs,
15
MultiModalUUIDDict,
16
)
18
-from vllm.multimodal.processing import BaseMultiModalProcessor
17
from vllm.renderers import BaseRenderer, renderer_from_config
18
from vllm.renderers.inputs import (
19
DecoderDictPrompt,
@@ -28,8 +26,6 @@ from vllm.renderers.inputs import (
26
)
27
from vllm.renderers.inputs.preprocess import parse_dec_only_prompt, parse_enc_dec_prompt
28
from vllm.tokenizers import TokenizerLike
31
-from vllm.utils.jsontree import json_iter_leaves
32
-from vllm.v1.metrics.stats import MultiModalCacheStats
29
30
from .data import (
31
DecoderInputs,
@@ -57,17 +53,12 @@ class InputPreprocessor:
53
vllm_config: VllmConfig,
54
renderer: BaseRenderer | None = None,
55
mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
60
- mm_processor_cache: BaseMultiModalProcessorCache | None = None,
56
) -> None:
57
super().__init__()
58
59
self.model_config = vllm_config.model_config
65
- self.observability_config = vllm_config.observability_config
60
self.renderer = renderer or renderer_from_config(vllm_config)
61
self.mm_registry = mm_registry
68
- self.mm_processor_cache = mm_processor_cache
69
-
70
- self.mm_cache_stats = MultiModalCacheStats() if mm_processor_cache else None
62
63
@property
64
def tokenizer(self) -> TokenizerLike | None:
@@ -124,23 +115,6 @@ class InputPreprocessor:
115
116
return decoder_input_ids
117
127
- def _get_tokenization_kw(
128
- self,
129
- overrides: dict[str, Any] | None = None,
130
- ) -> dict[str, Any]:
131
- kwargs = dict[str, Any]()
132
-
133
- if self.model_config.is_encoder_decoder:
134
- # For Whisper, special tokens should be provided by the user based
135
- # on the task and language of their request. Also needed to avoid
136
- # appending an EOS token to the prompt which disrupts generation.
137
- kwargs["add_special_tokens"] = False
138
-
139
- if overrides:
140
- kwargs.update(overrides)
141
-
142
- return kwargs
143
-
118
def _tokenize_prompt(
119
self,
120
prompt: str,
@@ -150,26 +124,18 @@ class InputPreprocessor:
124
Apply the model's tokenizer to a text prompt, returning the
125
corresponding token IDs.
126
"""
153
- tokenizer = self.get_tokenizer()
154
- tokenization_kwargs = self._get_tokenization_kw(tokenization_kwargs)
155
-
156
- encoder_config = self.model_config.encoder_config
127
+ renderer = self.renderer
128
158
- if encoder_config and encoder_config.get("do_lower_case", False):
159
- prompt = prompt.lower()
160
-
161
- return tokenizer.encode(prompt, **tokenization_kwargs)
129
+ tok_params = renderer.default_cmpl_tok_params.with_kwargs(
130
+ **(tokenization_kwargs or {})
131
+ )
132
163
- def _get_mm_processor(self) -> BaseMultiModalProcessor:
164
- if not hasattr(self, "_mm_processor"):
165
- self._mm_processor = self.mm_registry.create_processor(
166
- self.model_config,
167
- self.observability_config,
168
- tokenizer=self.tokenizer,
169
- cache=self.mm_processor_cache,
170
- )
133
+ tok_prompt = renderer.tokenize_prompt(
134
+ TextPrompt(prompt=prompt),
135
+ tok_params,
136
+ )
137
172
- return self._mm_processor
138
+ return tok_prompt["prompt_token_ids"]
139
140
def _process_multimodal(
141
self,
@@ -184,33 +150,20 @@ class InputPreprocessor:
150
Apply the model's multi-modal processor to a multi-modal prompt,
151
returning the corresponding token IDs and metadata.
152
"""
187
- mm_processor = self._get_mm_processor()
153
+ mm_processor = self.renderer.get_mm_processor()
154
155
if mm_processor_kwargs is None:
156
mm_processor_kwargs = {}
157
158
mm_items = mm_processor.info.parse_mm_data(mm_data)
193
- mm_input = mm_processor.apply(
159
+
160
+ return mm_processor.apply(
161
prompt,
162
mm_items,
163
hf_processor_mm_kwargs=mm_processor_kwargs,
164
tokenization_kwargs=tokenization_kwargs,
165
mm_uuids=mm_uuids,
166
)
200
- mm_hashes = mm_input["mm_hashes"]
201
-
202
- # Validate that all mm items have a string as their hash
203
- contains_only_strings = all(
204
- isinstance(leaf, str) for leaf in json_iter_leaves(mm_hashes)
205
- )
206
- if not contains_only_strings:
207
- raise ValueError(
208
- f"mm_hashes must contain only strings, got: {mm_hashes}. "
209
- "This is likely due to an incorrect custom implementation of "
210
- "MultiModalProcessor.apply method."
211
- )
212
-
213
- return mm_input
167
168
def _process_embeds(
169
self,
@@ -245,19 +198,18 @@ class InputPreprocessor:
198
def _truncate_inputs(
199
self, inputs: list[int], tokenization_kwargs: dict[str, Any] | None = None
200
) -> list[int]:
248
- if (
249
- not tokenization_kwargs
250
- or "truncation" not in tokenization_kwargs
251
- or self.tokenizer is None
252
- ):
253
- return inputs
201
+ renderer = self.renderer
202
255
- max_length = tokenization_kwargs["max_length"]
203
+ tok_params = renderer.default_cmpl_tok_params.with_kwargs(
204
+ **(tokenization_kwargs or {})
205
+ )
206
257
- if self.tokenizer.truncation_side == "left":
258
- return inputs[-max_length:]
259
- else:
260
- return inputs[:max_length]
207
+ tok_prompt = renderer.tokenize_prompt(
208
+ TokensPrompt(prompt_token_ids=inputs),
209
+ tok_params,
210
+ )
211
+
212
+ return tok_prompt["prompt_token_ids"]
213
214
def _process_tokens(
215
self,
@@ -539,26 +491,6 @@ class InputPreprocessor:
491
"""Preprocess the input prompt."""
492
res = self._preprocess(prompt, tokenization_kwargs, mm_uuids=mm_uuids)
493
542
- if self.mm_processor_cache and self.mm_cache_stats is not None:
543
- delta = self.mm_processor_cache.make_stats(delta=True)
544
- self.mm_cache_stats.requests += 1
545
- self.mm_cache_stats.queries += delta.total
546
- self.mm_cache_stats.hits += delta.hits
494
+ self.renderer.update_mm_cache_stats()
495
496
return res
549
-
550
- def stat_mm_cache(self) -> MultiModalCacheStats | None:
551
- mm_cache_stats = self.mm_cache_stats
552
- if mm_cache_stats is None:
553
- return None
554
-
555
- self.mm_cache_stats = MultiModalCacheStats()
556
-
557
- return mm_cache_stats
558
-
559
- def clear_mm_cache(self) -> None:
560
- if self.mm_processor_cache is not None:
561
- self.mm_processor_cache.clear_cache()
562
-
563
- if self.mm_cache_stats is not None:
564
- self.mm_cache_stats.reset = True
vllm/model_executor/models/clip.py
+16
-7
@@ -208,14 +208,23 @@ class CLIPMultiModalProcessor(BaseMultiModalProcessor[CLIPProcessingInfo]):
208
*,
209
mm_uuids: MultiModalUUIDDict | None = None,
210
) -> MultiModalInputs:
211
- if prompt and mm_items:
212
- raise ValueError(
213
- "CLIP accepts text-only or image-only inputs, not both! "
214
- "Image-only inputs means passing an image with an empty text "
215
- "prompt."
216
- )
217
-
211
if mm_items:
212
+ if isinstance(prompt, str):
213
+ if len(prompt) > 0:
214
+ raise ValueError(
215
+ "CLIP accepts text-only or image-only inputs, not both! "
216
+ "You must pass an image with an empty text prompt."
217
+ )
218
+ else:
219
+ special_tokens = self.info.get_tokenizer().all_special_ids
220
+ if all(tok in special_tokens for tok in prompt):
221
+ prompt = []
222
+ else:
223
+ raise ValueError(
224
+ "CLIP accepts text-only or image-only inputs, not both! "
225
+ "You must pass an image with an empty token prompt."
226
+ )
227
+
228
# For multi-modal data, the prompt after processing should
229
# only contain the dummy image tokens
230
tokenization_kwargs = {
vllm/model_executor/models/lfm2_vl.py
+4
@@ -42,6 +42,7 @@ from vllm.multimodal.processing import (
42
PromptReplacement,
43
PromptUpdateDetails,
44
)
45
+from vllm.renderers import TokenizeParams
46
from vllm.sequence import IntermediateTensors
47
from vllm.utils.tensor_schema import TensorSchema, TensorShape
48
@@ -90,6 +91,9 @@ class Lfm2VLProcessingInfo(BaseProcessingInfo):
91
def get_image_processor(self, **kwargs: object) -> Lfm2VlImageProcessorFast:
92
return self.get_hf_processor(**kwargs).image_processor
93
94
+ def get_default_tok_params(self) -> TokenizeParams:
95
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
96
+
97
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
98
return {"image": None}
99
vllm/model_executor/models/mllama4.py
+4
@@ -66,6 +66,7 @@ from vllm.multimodal.processing import (
66
PromptUpdate,
67
PromptUpdateDetails,
68
)
69
+from vllm.renderers import TokenizeParams
70
from vllm.sequence import IntermediateTensors
71
from vllm.utils.tensor_schema import TensorSchema, TensorShape
72
@@ -554,6 +555,9 @@ class Mllama4ProcessingInfo(BaseProcessingInfo):
555
Llama4Processor, use_fast=kwargs.pop("use_fast", True), **kwargs
556
)
557
558
+ def get_default_tok_params(self) -> TokenizeParams:
559
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
560
+
561
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
562
# Although vLLM can support more images from an infra capability
563
# perspective, we do not recommend using >10 images in practice.
vllm/model_executor/models/nano_nemotron_vl.py
+4
@@ -76,6 +76,7 @@ from vllm.multimodal.processing.processor import (
76
PromptUpdateDetails,
77
_seq2tokens,
78
)
79
+from vllm.renderers import TokenizeParams
80
from vllm.sequence import IntermediateTensors
81
from vllm.tokenizers import TokenizerLike, cached_tokenizer_from_config
82
from vllm.transformers_utils.configs.radio import RadioConfig
@@ -1093,6 +1094,9 @@ class BaseNanoNemotronVLProcessingInfo(BaseProcessingInfo):
1094
) -> BaseNanoNemotronVLProcessor:
1095
raise NotImplementedError
1096
1097
+ def get_default_tok_params(self) -> TokenizeParams:
1098
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
1099
+
1100
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
1101
return {"image": None}
1102
vllm/model_executor/models/nemotron_parse.py
+4
@@ -58,6 +58,7 @@ from vllm.multimodal.processing import (
58
PromptReplacement,
59
PromptUpdate,
60
)
61
+from vllm.renderers import TokenizeParams
62
from vllm.tokenizers import TokenizerLike
63
from vllm.transformers_utils.configs.radio import RadioConfig
64
from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -608,6 +609,9 @@ class NemotronParseProcessingInfo(BaseProcessingInfo):
609
**kwargs,
610
)
611
612
+ def get_default_tok_params(self) -> TokenizeParams:
613
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
614
+
615
@property
616
def skip_prompt_length_check(self) -> bool:
617
return True # Because the encoder prompt is padded
vllm/model_executor/models/ovis.py
+4
@@ -53,6 +53,7 @@ from vllm.multimodal.processing import (
53
BaseProcessingInfo,
54
PromptReplacement,
55
)
56
+from vllm.renderers import TokenizeParams
57
from vllm.sequence import IntermediateTensors
58
from vllm.transformers_utils.processors.ovis import OvisProcessor
59
from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -264,6 +265,9 @@ class OvisProcessingInfo(BaseProcessingInfo):
265
**kwargs,
266
)
267
268
+ def get_default_tok_params(self) -> TokenizeParams:
269
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
270
+
271
def get_image_segment_len(self) -> int:
272
visual_tokenizer_config = self.get_hf_config().visual_tokenizer_config
273
image_size = visual_tokenizer_config.backbone_config.image_size
vllm/model_executor/models/ovis2_5.py
+4
@@ -35,6 +35,7 @@ from vllm.multimodal.processing import (
35
BaseProcessingInfo,
36
PromptReplacement,
37
)
38
+from vllm.renderers import TokenizeParams
39
from vllm.sequence import IntermediateTensors
40
from vllm.transformers_utils.processors.ovis2_5 import Ovis2_5Processor
41
from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -183,6 +184,9 @@ class Ovis2_5ProcessingInfo(BaseProcessingInfo):
184
temporal_patch_size=vit_config.temporal_patch_size,
185
)
186
187
+ def get_default_tok_params(self) -> TokenizeParams:
188
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
189
+
190
def get_image_processor(self) -> BaseImageProcessor:
191
return self.get_hf_processor().image_processor # type: ignore
192
vllm/model_executor/models/paligemma.py
+4
@@ -32,6 +32,7 @@ from vllm.multimodal.processing import (
32
PromptUpdate,
33
PromptUpdateDetails,
34
)
35
+from vllm.renderers import TokenizeParams
36
from vllm.sequence import IntermediateTensors
37
from vllm.utils.tensor_schema import TensorSchema, TensorShape
38
@@ -102,6 +103,9 @@ class PaliGemmaProcessingInfo(BaseProcessingInfo):
103
def get_vision_encoder_info(self):
104
return get_vision_encoder_info(self.get_hf_config())
105
106
+ def get_default_tok_params(self) -> TokenizeParams:
107
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
108
+
109
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
110
return {"image": 1}
111
vllm/model_executor/models/siglip.py
+16
-7
@@ -194,14 +194,23 @@ class SiglipMultiModalProcessor(BaseMultiModalProcessor[SiglipProcessingInfo]):
194
*,
195
mm_uuids: MultiModalUUIDDict | None = None,
196
) -> MultiModalInputs:
197
- if prompt and mm_items:
198
- raise ValueError(
199
- "Siglip accepts text-only or image-only inputs, not both! "
200
- "Image-only inputs means passing an image with an empty text "
201
- "prompt."
202
- )
203
-
197
if mm_items:
198
+ if isinstance(prompt, str):
199
+ if len(prompt) > 0:
200
+ raise ValueError(
201
+ "SigLIP accepts text-only or image-only inputs, not both! "
202
+ "You must pass an image with an empty text prompt."
203
+ )
204
+ else:
205
+ special_tokens = self.info.get_tokenizer().all_special_ids
206
+ if all(tok in special_tokens for tok in prompt):
207
+ prompt = []
208
+ else:
209
+ raise ValueError(
210
+ "SigLIP accepts text-only or image-only inputs, not both! "
211
+ "You must pass an image with an empty token prompt."
212
+ )
213
+
214
# For multi-modal data, the prompt after processing should
215
# only contain the image token
216
tokenization_kwargs = {
vllm/model_executor/models/ultravox.py
+4
@@ -42,6 +42,7 @@ from vllm.multimodal.processing import (
42
PromptReplacement,
43
PromptUpdate,
44
)
45
+from vllm.renderers import TokenizeParams
46
from vllm.sequence import IntermediateTensors
47
from vllm.transformers_utils.configs.ultravox import UltravoxConfig
48
from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -133,6 +134,9 @@ class UltravoxProcessingInfo(BaseProcessingInfo):
134
assert isinstance(feature_extractor, WhisperFeatureExtractor)
135
return feature_extractor
136
137
+ def get_default_tok_params(self) -> TokenizeParams:
138
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
139
+
140
def get_data_parser(self):
141
feature_extractor = self.get_feature_extractor()
142
vllm/model_executor/models/voxtral_realtime.py
+2
-1
@@ -17,8 +17,9 @@ from mistral_common.tokens.tokenizers.audio import Audio, AudioConfig
17
18
from vllm.compilation.decorators import support_torch_compile
19
from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
20
+from vllm.engine.protocol import StreamingInput
21
from vllm.envs import VLLM_ENGINE_ITERATION_TIMEOUT_S
21
-from vllm.inputs.data import PromptType, StreamingInput, TokensPrompt
22
+from vllm.inputs.data import PromptType, TokensPrompt
23
from vllm.logger import init_logger
24
from vllm.model_executor.models.interfaces import MultiModalEmbeddings, SupportsRealtime
25
from vllm.model_executor.models.voxtral import (
vllm/model_executor/models/whisper.py
+7
@@ -55,6 +55,7 @@ from vllm.multimodal.processing import (
55
PromptReplacement,
56
PromptUpdate,
57
)
58
+from vllm.renderers import TokenizeParams
59
from vllm.transformers_utils.processor import cached_processor_from_config
60
from vllm.utils.jsontree import json_map_leaves
61
from vllm.utils.tensor_schema import TensorSchema, TensorShape
@@ -644,6 +645,12 @@ class WhisperProcessingInfo(BaseProcessingInfo):
645
def get_hf_config(self) -> WhisperConfig:
646
return self.ctx.get_hf_config(WhisperConfig)
647
648
+ def get_default_tok_params(self) -> TokenizeParams:
649
+ # Special tokens should be provided by the user based on the
650
+ # task and language of their request. Also needed to avoid
651
+ # appending an EOS token to the prompt which disrupts generation.
652
+ return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
653
+
654
def get_data_parser(self):
655
feature_extractor = self.get_feature_extractor()
656
vllm/multimodal/processing/context.py
+16
-104
@@ -21,6 +21,7 @@ from vllm.multimodal.parse import (
21
MultiModalDataItems,
22
MultiModalDataParser,
23
)
24
+from vllm.renderers import TokenizeParams
25
from vllm.tokenizers import TokenizerLike
26
from vllm.transformers_utils.processor import cached_processor_from_config
27
from vllm.utils.func_utils import get_allowed_kwarg_only_overrides
@@ -93,110 +94,6 @@ class MultiModalProcessorTimingStats:
94
}
95
96
96
-def get_timing_stats_from_engine_client(
97
- engine_client: Any,
98
-) -> dict[str, dict[str, float]]:
99
- """
100
- Get all multimodal timing stats from the engine client.
101
-
102
- Collects both preprocessing stats (HF processor, hashing, cache lookup,
103
- prompt update) and encoder forward pass timing, merged by request_id.
104
-
105
- Args:
106
- engine_client: The engine client (has input_processor and workers).
107
-
108
- Returns:
109
- Dictionary mapping request_id to merged stats dict containing
110
- both preprocessing and encoder timing metrics.
111
-
112
- Example:
113
- {
114
- 'request-123': {
115
- 'hf_processor_time': 0.45,
116
- 'hashing_time': 0.02,
117
- 'cache_lookup_time': 0.01,
118
- 'prompt_update_time': 0.03,
119
- 'preprocessor_total_time': 0.51,
120
- 'encoder_forward_time': 0.23,
121
- 'num_encoder_calls': 1
122
- }
123
- }
124
- """
125
- try:
126
- if not engine_client.vllm_config.observability_config.enable_mm_processor_stats:
127
- return {}
128
- except (AttributeError, RuntimeError):
129
- return {}
130
-
131
- preprocessing_stats = {}
132
- try:
133
- input_processor = engine_client.input_processor
134
- input_preprocessor = input_processor.input_preprocessor
135
-
136
- if hasattr(input_preprocessor, "_get_mm_processor"):
137
- mm_processor = input_preprocessor._get_mm_processor()
138
- if mm_processor is not None and hasattr(mm_processor, "info"):
139
- ctx = mm_processor.info.ctx
140
- preprocessing_stats = ctx.get_all_timing_stats()
141
- except (AttributeError, RuntimeError):
142
- pass
143
-
144
- encoder_stats = {}
145
- try:
146
- if hasattr(engine_client, "collective_rpc"):
147
- encoder_stats_results = engine_client.collective_rpc(
148
- "get_encoder_timing_stats"
149
- )
150
- if encoder_stats_results and len(encoder_stats_results) > 0:
151
- for worker_stats in encoder_stats_results:
152
- if not worker_stats:
153
- continue
154
- for request_id, stats_dict in worker_stats.items():
155
- if request_id not in encoder_stats:
156
- encoder_stats[request_id] = dict(stats_dict)
157
- else:
158
- # Aggregate timing metrics across workers
159
- current_time = encoder_stats[request_id].get(
160
- "encoder_forward_time", 0.0
161
- )
162
- new_time = stats_dict.get("encoder_forward_time", 0.0)
163
- encoder_stats[request_id]["encoder_forward_time"] = max(
164
- current_time, new_time
165
- )
166
-
167
- current_calls = encoder_stats[request_id].get(
168
- "num_encoder_calls", 0
169
- )
170
- new_calls = stats_dict.get("num_encoder_calls", 0)
171
- encoder_stats[request_id]["num_encoder_calls"] = max(
172
- current_calls, new_calls
173
- )
174
- except (AttributeError, RuntimeError):
175
- pass
176
-
177
- merged_stats = {}
178
-
179
- for request_id, prep_dict in preprocessing_stats.items():
180
- merged_stats[request_id] = dict(prep_dict)
181
-
182
- for request_id, enc_dict in encoder_stats.items():
183
- if request_id in merged_stats:
184
- merged_stats[request_id].update(enc_dict)
185
- continue
186
-
187
- # In V1 engine, the request_id in encoder_stats has a suffix
188
- # appended to the original request_id (which is used in
189
- # preprocessing_stats).
190
- # We try to strip the suffix to find the matching request.
191
- possible_original_id = request_id.rpartition("-")[0]
192
- if possible_original_id and possible_original_id in merged_stats:
193
- merged_stats[possible_original_id].update(enc_dict)
194
- else:
195
- merged_stats[request_id] = dict(enc_dict)
196
-
197
- return merged_stats
198
-
199
-
97
@contextmanager
98
def timed_preprocessor_operation(ctx: "InputProcessingContext", stage_name: str):
99
"""
@@ -576,6 +473,21 @@ class BaseProcessingInfo:
473
"""
474
return self.ctx.get_hf_processor(**kwargs)
475
476
+ def get_default_tok_params(self) -> TokenizeParams:
477
+ """Construct the default parameters for tokenization."""
478
+ model_config = self.ctx.model_config
479
+ encoder_config = model_config.encoder_config or {}
480
+
481
+ return TokenizeParams(
482
+ max_total_tokens=model_config.max_model_len,
483
+ do_lower_case=encoder_config.get("do_lower_case", False),
484
+ add_special_tokens=True,
485
+ )
486
+
487
+ @cached_property
488
+ def default_tok_params(self) -> TokenizeParams:
489
+ return self.get_default_tok_params()
490
+
491
def _get_expected_hidden_size(self) -> int | None:
492
"""
493
Get expected hidden size for embedding validation if `mm_embeds` are enabled.
vllm/renderers/base.py
+123
-22
@@ -3,12 +3,17 @@
3
import asyncio
4
from abc import ABC, abstractmethod
5
from collections.abc import Sequence
6
-from typing import TYPE_CHECKING, Any, overload
6
+from functools import cached_property
7
+from typing import TYPE_CHECKING, Any, Generic, overload
8
+
9
+from typing_extensions import TypeVar
10
11
from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
12
from vllm.logger import init_logger
13
from vllm.tokenizers import TokenizerLike
14
from vllm.utils.async_utils import AsyncMicrobatchTokenizer
15
+from vllm.utils.torch_utils import set_default_torch_num_threads
16
+from vllm.v1.metrics.stats import MultiModalCacheStats
17
18
from .embed_utils import safe_load_prompt_embeds
19
from .inputs import (
@@ -26,11 +31,16 @@ if TYPE_CHECKING:
31
ChatCompletionMessageParam,
32
ConversationMessage,
33
)
34
+ from vllm.multimodal.cache import BaseMultiModalProcessorCache
35
+ from vllm.multimodal.processing import BaseMultiModalProcessor
36
37
logger = init_logger(__name__)
38
39
33
-class BaseRenderer(ABC):
40
+_T = TypeVar("_T", bound=TokenizerLike, default=TokenizerLike)
41
+
42
+
43
+class BaseRenderer(ABC, Generic[_T]):
44
@classmethod
45
@abstractmethod
46
def from_config(
@@ -40,20 +50,36 @@ class BaseRenderer(ABC):
50
) -> "BaseRenderer":
51
raise NotImplementedError
52
43
- def __init__(self, config: "VllmConfig") -> None:
53
+ def __init__(self, config: "VllmConfig", tokenizer: _T | None) -> None:
54
super().__init__()
55
56
+ self.config = config
57
self.model_config = config.model_config
58
59
+ self.tokenizer = tokenizer
60
+
61
# Lazy initialization since offline LLM doesn't use async
62
self._async_tokenizer: AsyncMicrobatchTokenizer | None = None
63
51
- @property
52
- @abstractmethod
53
- def tokenizer(self) -> TokenizerLike | None:
54
- raise NotImplementedError
64
+ self.mm_processor: BaseMultiModalProcessor | None = None
65
+ self._mm_cache_stats: MultiModalCacheStats | None = None
66
+ if config.model_config.is_multimodal_model:
67
+ from vllm.multimodal import MULTIMODAL_REGISTRY as mm_registry
68
+
69
+ mm_processor_cache = mm_registry.processor_cache_from_config(config)
70
+
71
+ with set_default_torch_num_threads():
72
+ self.mm_processor = mm_registry.create_processor(
73
+ config.model_config,
74
+ config.observability_config,
75
+ tokenizer=tokenizer,
76
+ cache=mm_processor_cache,
77
+ )
78
+
79
+ if mm_processor_cache:
80
+ self._mm_cache_stats = MultiModalCacheStats()
81
56
- def get_tokenizer(self) -> TokenizerLike:
82
+ def get_tokenizer(self) -> _T:
83
tokenizer = self.tokenizer
84
if tokenizer is None:
85
raise ValueError("Tokenizer not available when `skip_tokenizer_init=True`")
@@ -66,6 +92,49 @@ class BaseRenderer(ABC):
92
93
return self._async_tokenizer
94
95
+ def get_mm_processor(self) -> "BaseMultiModalProcessor":
96
+ if self.mm_processor is None:
97
+ raise ValueError("Multi-modal processor not available for text-only models")
98
+
99
+ return self.mm_processor
100
+
101
+ @property
102
+ def mm_processor_cache(self) -> "BaseMultiModalProcessorCache | None":
103
+ if self.mm_processor is None:
104
+ return None
105
+
106
+ return self.mm_processor.cache
107
+
108
+ def stat_mm_cache(self) -> MultiModalCacheStats | None:
109
+ mm_cache_stats = self._mm_cache_stats
110
+ if mm_cache_stats is None:
111
+ return None
112
+
113
+ self._mm_cache_stats = MultiModalCacheStats()
114
+
115
+ return mm_cache_stats
116
+
117
+ def update_mm_cache_stats(self) -> None:
118
+ mm_processor_cache = self.mm_processor_cache
119
+ mm_cache_stats = self._mm_cache_stats
120
+
121
+ if mm_processor_cache and mm_cache_stats:
122
+ delta = mm_processor_cache.make_stats(delta=True)
123
+ mm_cache_stats.record(delta.total, delta.hits)
124
+
125
+ def clear_mm_cache(self) -> None:
126
+ mm_processor_cache = self.mm_processor_cache
127
+ if mm_processor_cache is not None:
128
+ mm_processor_cache.clear_cache()
129
+
130
+ if self._mm_cache_stats is not None:
131
+ self._mm_cache_stats.reset = True
132
+
133
+ def shutdown(self) -> None:
134
+ mm_processor_cache = self.mm_processor_cache
135
+ if mm_processor_cache is not None:
136
+ mm_processor_cache.close()
137
+
138
def get_bos_token_id(self) -> int | None:
139
if self.tokenizer is None:
140
logger.warning_once(
@@ -84,6 +153,36 @@ class BaseRenderer(ABC):
153
154
return self.tokenizer.eos_token_id
155
156
+ @cached_property
157
+ def default_cmpl_tok_params(self) -> TokenizeParams:
158
+ mm_processor = self.mm_processor
159
+ if mm_processor is not None:
160
+ return mm_processor.info.default_tok_params
161
+
162
+ model_config = self.model_config
163
+ encoder_config = model_config.encoder_config or {}
164
+
165
+ return TokenizeParams(
166
+ max_total_tokens=model_config.max_model_len,
167
+ do_lower_case=encoder_config.get("do_lower_case", False),
168
+ add_special_tokens=True,
169
+ )
170
+
171
+ @cached_property
172
+ def default_chat_tok_params(self) -> TokenizeParams:
173
+ mm_processor = self.mm_processor
174
+ if mm_processor is not None:
175
+ return mm_processor.info.default_tok_params
176
+
177
+ model_config = self.model_config
178
+ encoder_config = model_config.encoder_config or {}
179
+
180
+ return TokenizeParams(
181
+ max_total_tokens=model_config.max_model_len,
182
+ do_lower_case=encoder_config.get("do_lower_case", False),
183
+ add_special_tokens=False,
184
+ )
185
+
186
# Step 1: Convert raw inputs to prompts
187
def render_prompt(
188
self,
@@ -317,18 +416,14 @@ class BaseRenderer(ABC):
416
def render_cmpl(
417
self,
418
prompts: Sequence[DictPrompt | bytes],
320
- tok_params: TokenizeParams,
419
+ tok_params: TokenizeParams | None = None,
420
*,
421
prompt_extras: dict[str, Any] | None = None,
422
):
324
- dict_prompts = self.render_prompts(prompts)
325
-
326
- # NOTE: Some MM models have non-default `add_special_tokens`
327
- # so we handle tokenization in multi-modal processor
328
- if self.model_config.is_multimodal_model:
329
- self._apply_prompt_extras(dict_prompts, prompt_extras)
330
- return dict_prompts
423
+ if tok_params is None:
424
+ tok_params = self.default_cmpl_tok_params
425
426
+ dict_prompts = self.render_prompts(prompts)
427
tok_prompts = self.tokenize_prompts(dict_prompts, tok_params)
428
429
self._apply_prompt_extras(tok_prompts, prompt_extras)
@@ -339,14 +434,14 @@ class BaseRenderer(ABC):
434
async def render_cmpl_async(
435
self,
436
prompts: Sequence[DictPrompt | bytes],
342
- tok_params: TokenizeParams,
437
+ tok_params: TokenizeParams | None = None,
438
*,
439
prompt_extras: dict[str, Any] | None = None,
440
):
346
- dict_prompts = await self.render_prompts_async(prompts)
441
+ if tok_params is None:
442
+ tok_params = self.default_cmpl_tok_params
443
348
- # NOTE: MM data cannot be passed to online Completions API
349
- # so we don't have the special case that is in the offline version
444
+ dict_prompts = await self.render_prompts_async(prompts)
445
tok_prompts = await self.tokenize_prompts_async(dict_prompts, tok_params)
446
447
self._apply_prompt_extras(tok_prompts, prompt_extras)
@@ -358,10 +453,13 @@ class BaseRenderer(ABC):
453
self,
454
conversations: Sequence[list["ChatCompletionMessageParam"]],
455
chat_params: ChatParams,
361
- tok_params: TokenizeParams,
456
+ tok_params: TokenizeParams | None = None,
457
*,
458
prompt_extras: dict[str, Any] | None = None,
459
):
460
+ if tok_params is None:
461
+ tok_params = self.default_chat_tok_params
462
+
463
rendered = [
464
self.render_messages(conversation, chat_params)
465
for conversation in conversations
@@ -384,10 +482,13 @@ class BaseRenderer(ABC):
482
self,
483
conversations: Sequence[list["ChatCompletionMessageParam"]],
484
chat_params: ChatParams,
387
- tok_params: TokenizeParams,
485
+ tok_params: TokenizeParams | None = None,
486
*,
487
prompt_extras: dict[str, Any] | None = None,
488
):
489
+ if tok_params is None:
490
+ tok_params = self.default_chat_tok_params
491
+
492
rendered = [
493
self.render_messages_async(conversation, chat_params)
494
for conversation in conversations
vllm/renderers/deepseek_v32.py
+5
-26
@@ -13,7 +13,6 @@ from vllm.logger import init_logger
13
from vllm.tokenizers import cached_get_tokenizer
14
from vllm.tokenizers.deepseek_v32 import DeepseekV32Tokenizer
15
16
-from ..tokenizers.hf import HfTokenizer
16
from .base import BaseRenderer
17
from .inputs import DictPrompt
18
from .inputs.preprocess import parse_dec_only_prompt
@@ -22,23 +21,14 @@ from .params import ChatParams
21
logger = init_logger(__name__)
22
23
25
-class DeepseekV32Renderer(BaseRenderer):
24
+class DeepseekV32Renderer(BaseRenderer[DeepseekV32Tokenizer]):
25
@classmethod
27
- def from_config(
26
+ def from_config( # type: ignore[override]
27
cls,
28
config: VllmConfig,
29
tokenizer_kwargs: dict[str, Any],
31
- ) -> "BaseRenderer":
32
- return cls(config, tokenizer_kwargs)
33
-
34
- def __init__(
35
- self,
36
- config: VllmConfig,
37
- tokenizer_kwargs: dict[str, Any],
38
- ) -> None:
39
- super().__init__(config)
40
-
41
- model_config = self.model_config
30
+ ) -> "DeepseekV32Renderer":
31
+ model_config = config.model_config
32
if model_config.skip_tokenizer_init:
33
tokenizer = None
34
else:
@@ -47,18 +37,7 @@ class DeepseekV32Renderer(BaseRenderer):
37
**tokenizer_kwargs,
38
)
39
50
- self._tokenizer = tokenizer
51
-
52
- @property
53
- def tokenizer(self) -> HfTokenizer | None:
54
- return self._tokenizer
55
-
56
- def get_tokenizer(self) -> HfTokenizer:
57
- tokenizer = self.tokenizer
58
- if tokenizer is None:
59
- raise ValueError("Tokenizer not available when `skip_tokenizer_init=True`")
60
-
61
- return tokenizer
40
+ return cls(config, tokenizer)
41
42
def render_messages(
43
self,
vllm/renderers/grok2.py
+5
-25
@@ -21,23 +21,14 @@ from .params import ChatParams
21
logger = init_logger(__name__)
22
23
24
-class Grok2Renderer(BaseRenderer):
24
+class Grok2Renderer(BaseRenderer[Grok2Tokenizer]):
25
@classmethod
26
- def from_config(
26
+ def from_config( # type: ignore[override]
27
cls,
28
config: VllmConfig,
29
tokenizer_kwargs: dict[str, Any],
30
- ) -> "BaseRenderer":
31
- return cls(config, tokenizer_kwargs)
32
-
33
- def __init__(
34
- self,
35
- config: VllmConfig,
36
- tokenizer_kwargs: dict[str, Any],
37
- ) -> None:
38
- super().__init__(config)
39
-
40
- model_config = self.model_config
30
+ ) -> "Grok2Renderer":
31
+ model_config = config.model_config
32
if model_config.skip_tokenizer_init:
33
tokenizer = None
34
else:
@@ -46,18 +37,7 @@ class Grok2Renderer(BaseRenderer):
37
**tokenizer_kwargs,
38
)
39
49
- self._tokenizer = tokenizer
50
-
51
- @property
52
- def tokenizer(self) -> Grok2Tokenizer | None:
53
- return self._tokenizer
54
-
55
- def get_tokenizer(self) -> Grok2Tokenizer:
56
- tokenizer = self.tokenizer
57
- if tokenizer is None:
58
- raise ValueError("Tokenizer not available when `skip_tokenizer_init=True`")
59
-
60
- return tokenizer
40
+ return cls(config, tokenizer)
41
42
def render_messages(
43
self,
vllm/renderers/hf.py
+14
-27
@@ -585,27 +585,14 @@ def replace_vision_chunk_video_placeholder(
585
return prompt_raw
586
587
588
-class HfRenderer(BaseRenderer):
588
+class HfRenderer(BaseRenderer[HfTokenizer]):
589
@classmethod
590
- def from_config(
590
+ def from_config( # type: ignore[override]
591
cls,
592
config: VllmConfig,
593
tokenizer_kwargs: dict[str, Any],
594
- ) -> "BaseRenderer":
595
- return cls(config, tokenizer_kwargs)
596
-
597
- def __init__(
598
- self,
599
- config: VllmConfig,
600
- tokenizer_kwargs: dict[str, Any],
601
- ) -> None:
602
- super().__init__(config)
603
-
604
- model_config = self.model_config
605
- self.use_unified_vision_chunk = getattr(
606
- model_config.hf_config, "use_unified_vision_chunk", False
607
- )
608
-
594
+ ) -> "HfRenderer":
595
+ model_config = config.model_config
596
if model_config.skip_tokenizer_init:
597
tokenizer = None
598
else:
@@ -617,18 +604,18 @@ class HfRenderer(BaseRenderer):
604
),
605
)
606
620
- self._tokenizer = tokenizer
621
-
622
- @property
623
- def tokenizer(self) -> HfTokenizer | None:
624
- return self._tokenizer
607
+ return cls(config, tokenizer)
608
626
- def get_tokenizer(self) -> HfTokenizer:
627
- tokenizer = self.tokenizer
628
- if tokenizer is None:
629
- raise ValueError("Tokenizer not available when `skip_tokenizer_init=True`")
609
+ def __init__(
610
+ self,
611
+ config: VllmConfig,
612
+ tokenizer: HfTokenizer | None,
613
+ ) -> None:
614
+ super().__init__(config, tokenizer)
615
631
- return tokenizer
616
+ self.use_unified_vision_chunk = getattr(
617
+ config.model_config.hf_config, "use_unified_vision_chunk", False
618
+ )
619
620
def render_messages(
621
self,
vllm/renderers/mistral.py
+12
-25
@@ -50,23 +50,14 @@ def safe_apply_chat_template(
50
raise ValueError(str(e)) from e
51
52
53
-class MistralRenderer(BaseRenderer):
53
+class MistralRenderer(BaseRenderer[MistralTokenizer]):
54
@classmethod
55
- def from_config(
55
+ def from_config( # type: ignore[override]
56
cls,
57
config: VllmConfig,
58
tokenizer_kwargs: dict[str, Any],
59
- ) -> "BaseRenderer":
60
- return cls(config, tokenizer_kwargs)
61
-
62
- def __init__(
63
- self,
64
- config: VllmConfig,
65
- tokenizer_kwargs: dict[str, Any],
66
- ) -> None:
67
- super().__init__(config)
68
-
69
- model_config = self.model_config
59
+ ) -> "MistralRenderer":
60
+ model_config = config.model_config
61
if model_config.skip_tokenizer_init:
62
tokenizer = None
63
else:
@@ -75,24 +66,20 @@ class MistralRenderer(BaseRenderer):
66
**tokenizer_kwargs,
67
)
68
78
- self._tokenizer = tokenizer
69
+ return cls(config, tokenizer)
70
+
71
+ def __init__(
72
+ self,
73
+ config: VllmConfig,
74
+ tokenizer: MistralTokenizer | None,
75
+ ) -> None:
76
+ super().__init__(config, tokenizer)
77
78
self._apply_chat_template_executor = ThreadPoolExecutor(max_workers=1)
79
self._apply_chat_template_async = make_async(
80
safe_apply_chat_template, executor=self._apply_chat_template_executor
81
)
82
85
- @property
86
- def tokenizer(self) -> MistralTokenizer | None:
87
- return self._tokenizer
88
-
89
- def get_tokenizer(self) -> MistralTokenizer:
90
- tokenizer = self.tokenizer
91
- if tokenizer is None:
92
- raise ValueError("Tokenizer not available when `skip_tokenizer_init=True`")
93
-
94
- return tokenizer
95
-
83
def render_messages(
84
self,
85
messages: list[ChatCompletionMessageParam],
vllm/renderers/params.py
+6
-6
@@ -3,7 +3,6 @@
3
from dataclasses import dataclass, field
4
from typing import TYPE_CHECKING, Any, TypeVar
5
6
-from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
6
from vllm.exceptions import VLLMValidationError
7
from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
8
from vllm.logger import init_logger
@@ -12,9 +11,13 @@ from vllm.utils.import_utils import LazyLoader
11
12
if TYPE_CHECKING:
13
import torch
14
+
15
+ from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
16
else:
17
torch = LazyLoader("torch", globals(), "torch")
18
19
+ ChatTemplateContentFormatOption = object
20
+
21
logger = init_logger(__name__)
22
23
@@ -43,7 +46,7 @@ class ChatParams:
46
chat_template: str | None = None
47
"""The chat template to apply."""
48
46
- chat_template_content_format: ChatTemplateContentFormatOption = "auto"
49
+ chat_template_content_format: "ChatTemplateContentFormatOption" = "auto"
50
"""The format of the chat template."""
51
52
chat_template_kwargs: dict[str, Any] = field(default_factory=dict)
@@ -163,10 +166,7 @@ class TokenizeParams:
166
value=truncate_prompt_tokens,
167
)
168
166
- def with_kwargs(self, tokenization_kwargs: dict[str, Any] | None):
167
- if tokenization_kwargs is None:
168
- tokenization_kwargs = {}
169
-
169
+ def with_kwargs(self, **tokenization_kwargs: Any):
170
max_length = tokenization_kwargs.pop("max_length", self.max_input_tokens)
171
pad_prompt_tokens = tokenization_kwargs.pop(
172
"pad_prompt_tokens", self.pad_prompt_tokens
vllm/renderers/terratorch.py
+4
-15
@@ -10,7 +10,6 @@ from vllm.entrypoints.chat_utils import (
10
parse_chat_messages_async,
11
)
12
from vllm.logger import init_logger
13
-from vllm.tokenizers import TokenizerLike
13
14
from .base import BaseRenderer
15
from .inputs import DictPrompt
@@ -24,24 +23,14 @@ class TerratorchRenderer(BaseRenderer):
23
@classmethod
24
def from_config(
25
cls,
27
- config: VllmConfig,
26
+ config: VllmConfig, # type: ignore[override]
27
tokenizer_kwargs: dict[str, Any],
29
- ) -> "BaseRenderer":
30
- return cls(config)
31
-
32
- def __init__(self, config: VllmConfig) -> None:
33
- super().__init__(config)
34
-
35
- model_config = self.model_config
28
+ ) -> "TerratorchRenderer":
29
+ model_config = config.model_config
30
if not model_config.skip_tokenizer_init:
31
raise ValueError("Terratorch renderer requires `skip_tokenizer_init=True`")
32
39
- @property
40
- def tokenizer(self) -> TokenizerLike | None:
41
- return None
42
-
43
- def get_tokenizer(self) -> TokenizerLike:
44
- raise ValueError("Tokenizer not available for Terratorch renderer")
33
+ return cls(config, None)
34
35
def render_messages(
36
self,
vllm/v1/engine/async_llm.py
+8
-8
@@ -19,8 +19,8 @@ from vllm.distributed.weight_transfer.base import (
19
WeightTransferUpdateRequest,
20
)
21
from vllm.engine.arg_utils import AsyncEngineArgs
22
-from vllm.engine.protocol import EngineClient
23
-from vllm.inputs import PromptType, StreamingInput
22
+from vllm.engine.protocol import EngineClient, StreamingInput
23
+from vllm.inputs import PromptType
24
from vllm.logger import init_logger
25
from vllm.lora.request import LoRARequest
26
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
@@ -268,12 +268,12 @@ class AsyncLLM(EngineClient):
268
269
shutdown_prometheus()
270
271
+ if renderer := getattr(self, "renderer", None):
272
+ renderer.shutdown()
273
+
274
if engine_core := getattr(self, "engine_core", None):
275
engine_core.shutdown()
276
274
- if input_processor := getattr(self, "input_processor", None):
275
- input_processor.close()
276
-
277
handler = getattr(self, "output_handler", None)
278
if handler is not None:
279
cancel_task_threadsafe(handler)
@@ -654,7 +654,7 @@ class AsyncLLM(EngineClient):
654
output_processor = self.output_processor
655
log_stats = self.log_stats
656
logger_manager = self.logger_manager
657
- input_processor = self.input_processor
657
+ renderer = self.renderer
658
chunk_size = envs.VLLM_V1_OUTPUT_PROC_CHUNK_SIZE
659
660
async def output_handler():
@@ -702,7 +702,7 @@ class AsyncLLM(EngineClient):
702
engine_idx=outputs.engine_index,
703
scheduler_stats=outputs.scheduler_stats,
704
iteration_stats=iteration_stats,
705
- mm_cache_stats=input_processor.stat_mm_cache(),
705
+ mm_cache_stats=renderer.stat_mm_cache(),
706
)
707
except Exception as e:
708
logger.exception("AsyncLLM output_handler failed.")
@@ -881,7 +881,7 @@ class AsyncLLM(EngineClient):
881
await asyncio.gather(*coros)
882
883
async def reset_mm_cache(self) -> None:
884
- self.input_processor.clear_mm_cache()
884
+ self.renderer.clear_mm_cache()
885
await self.engine_core.reset_mm_cache_async()
886
887
async def reset_prefix_cache(
vllm/v1/engine/input_processor.py
+11
-15
@@ -33,9 +33,9 @@ from vllm.sampling_params import SamplingParams
33
from vllm.tasks import POOLING_TASKS, SupportedTask
34
from vllm.tokenizers import TokenizerLike
35
from vllm.utils import length_from_prompt_token_ids_or_embeds, random_uuid
36
+from vllm.utils.jsontree import json_iter_leaves
37
from vllm.utils.torch_utils import set_default_torch_num_threads
38
from vllm.v1.engine import EngineCoreRequest
38
-from vllm.v1.metrics.stats import MultiModalCacheStats
39
40
logger = init_logger(__name__)
41
@@ -60,8 +60,6 @@ class InputProcessor:
60
self.generation_config_fields = model_config.try_get_generation_config()
61
62
self.renderer = renderer or renderer_from_config(vllm_config)
63
- self.mm_registry = mm_registry
64
- self.mm_processor_cache = mm_registry.processor_cache_from_config(vllm_config)
63
64
self.supports_mm_inputs = mm_registry.supports_multimodal_inputs(model_config)
65
self.mm_encoder_cache_size = 0
@@ -78,7 +76,6 @@ class InputProcessor:
76
vllm_config,
77
renderer=renderer,
78
mm_registry=mm_registry,
81
- mm_processor_cache=self.mm_processor_cache,
79
)
80
81
@property
@@ -136,7 +133,7 @@ class InputProcessor:
133
)
134
135
def _parse_mm_items(self, mm_data: MultiModalDataDict) -> MultiModalDataItems:
139
- mm_processor = self.input_preprocessor._get_mm_processor()
136
+ mm_processor = self.renderer.get_mm_processor()
137
return mm_processor.info.parse_mm_data(mm_data)
138
139
def _validate_singleton_mm_uuids(self, prompt: SingletonPrompt) -> None:
@@ -415,6 +412,15 @@ class InputProcessor:
412
decoder_mm_positions = decoder_inputs["mm_placeholders"]
413
decoder_mm_hashes = decoder_inputs["mm_hashes"]
414
415
+ if not all(
416
+ isinstance(leaf, str) for leaf in json_iter_leaves(decoder_mm_hashes)
417
+ ):
418
+ raise ValueError(
419
+ f"mm_hashes must contain only strings, got: {decoder_mm_hashes}. "
420
+ "This is likely due to an incorrect custom implementation of "
421
+ "MultiModalProcessor.apply method."
422
+ )
423
+
424
# Merge and flatten multimodal placeholders, hashes and inputs
425
# from dictionaries to lists, and sort them by each item's position
426
# in the input sequence.
@@ -562,13 +568,3 @@ class InputProcessor:
568
self._validate_model_input(encoder_inputs, prompt_type="encoder")
569
570
self._validate_model_input(decoder_inputs, prompt_type="decoder")
565
-
566
- def stat_mm_cache(self) -> MultiModalCacheStats | None:
567
- return self.input_preprocessor.stat_mm_cache()
568
-
569
- def clear_mm_cache(self) -> None:
570
- self.input_preprocessor.clear_mm_cache()
571
-
572
- def close(self) -> None:
573
- if self.mm_processor_cache is not None:
574
- self.mm_processor_cache.close()
vllm/v1/engine/llm_engine.py
+2
-2
@@ -320,7 +320,7 @@ class LLMEngine:
320
self.logger_manager.record(
321
scheduler_stats=outputs.scheduler_stats,
322
iteration_stats=iteration_stats,
323
- mm_cache_stats=self.input_processor.stat_mm_cache(),
323
+ mm_cache_stats=self.renderer.stat_mm_cache(),
324
)
325
self.do_log_stats_with_interval()
326
@@ -333,7 +333,7 @@ class LLMEngine:
333
self.engine_core.profile(False)
334
335
def reset_mm_cache(self):
336
- self.input_processor.clear_mm_cache()
336
+ self.renderer.clear_mm_cache()
337
self.engine_core.reset_mm_cache()
338
339
def reset_prefix_cache(
vllm/v1/metrics/stats.py
+6
@@ -151,6 +151,12 @@ class MultiModalCacheStats(BaseCacheStats):
151
that were queried.
152
"""
153
154
+ def record(self, num_queries: int, num_hits: int) -> None:
155
+ """Aggregate request information into the stats."""
156
+ self.requests += 1
157
+ self.queries += num_queries
158
+ self.hits += num_hits
159
+
160
161
@dataclass
162
class KVCacheEvictionEvent: