@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / f0a1c8453a
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 9, 06:15 UTC

[Frontend] Use new Renderer for Completions and Tokenize API (#32863)

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

Cyrus Leung committed Jan 31, 2026 at 20:51 UTC f0a1c8453ad1c664c8a04c83fe545195fcd556eb
64 files changed +2117 -2004
examples/online_serving/prompt_embed_inference_with_openai_client.py
+1 -3
@@ -60,9 +60,7 @@ def main():
60
61 completion = client.completions.create(
62 model=model_name,
63 - # NOTE: The OpenAI client does not allow `None` as an input to
64 - # `prompt`. Use an empty string if you have no text prompts.
65 - prompt="",
63 + prompt=None,
64 max_tokens=5,
65 temperature=0.0,
66 # NOTE: The OpenAI client allows passing in extra JSON body via the
tests/engine/test_short_mm_context.py
+5 -1
@@ -22,7 +22,11 @@ def test_context_length_too_short(vllm_runner, image_assets, model):
22 with pytest.raises(ValueError, match="longer than the maximum model length"):
23 vllm_model = vllm_runner(
24 model,
25 - max_model_len=128, # LLaVA has a feature size of 576
25 + # LLaVA has a feature size of 576
26 + # For the HF processor to execute successfully but still
27 + # failing the overall context length check, we need the
28 + # max_model_len to at least contain all image tokens
29 + max_model_len=579,
30 enforce_eager=True,
31 load_format="dummy",
32 )
tests/entrypoints/llm/test_chat.py
+1 -1
@@ -205,7 +205,7 @@ def test_chat_batch_failure_cleanup(llm_for_failure_test):
205 valid_msg,
206 ]
207 sampling_params = SamplingParams(temperature=0, max_tokens=10)
208 - with pytest.raises(ValueError, match="longer than the maximum model length"):
208 + with pytest.raises(ValueError, match="context length is only"):
209 llm.chat(batch_1, sampling_params=sampling_params)
210 outputs_2 = llm.chat(batch_2, sampling_params=sampling_params)
211 assert len(outputs_2) == len(batch_2)
tests/entrypoints/openai/test_chat_error.py
+13 -16
@@ -15,7 +15,8 @@ from vllm.entrypoints.openai.engine.protocol import ErrorResponse
15 from vllm.entrypoints.openai.models.protocol import BaseModelPath
16 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
17 from vllm.outputs import CompletionOutput, RequestOutput
18 -from vllm.tokenizers import get_tokenizer
18 +from vllm.renderers.hf import HfRenderer
19 +from vllm.tokenizers.registry import tokenizer_args_from_config
20 from vllm.v1.engine.async_llm import AsyncLLM
21
22 MODEL_NAME = "openai-community/gpt2"
@@ -57,6 +58,15 @@ class MockModelConfig:
58 return self.diff_sampling_param or {}
59
60
61 +def _build_renderer(model_config: MockModelConfig):
62 + _, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
63 +
64 + return HfRenderer(
65 + model_config,
66 + tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
67 + )
68 +
69 +
70 def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
71 models = OpenAIServingModels(
72 engine_client=engine,
@@ -71,18 +81,6 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
81 chat_template_content_format="auto",
82 )
83
74 - async def _fake_process_inputs(
75 - request_id,
76 - engine_prompt,
77 - sampling_params,
78 - *,
79 - lora_request,
80 - trace_headers,
81 - priority,
82 - data_parallel_rank,
83 - ):
84 - return dict(engine_prompt), {}
85 -
84 async def _fake_preprocess_chat(*args, **kwargs):
85 # return conversation, engine_prompts
86 return (
@@ -90,7 +88,6 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
88 [{"prompt_token_ids": [1, 2, 3]}],
89 )
90
93 - serving_chat._process_inputs = AsyncMock(side_effect=_fake_process_inputs)
91 serving_chat._preprocess_chat = AsyncMock(side_effect=_fake_preprocess_chat)
92 return serving_chat
93
@@ -99,11 +96,11 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
96 async def test_chat_error_non_stream():
97 """test finish_reason='error' returns 500 InternalServerError (non-streaming)"""
98 mock_engine = MagicMock(spec=AsyncLLM)
102 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
99 mock_engine.errored = False
100 mock_engine.model_config = MockModelConfig()
101 mock_engine.input_processor = MagicMock()
102 mock_engine.io_processor = MagicMock()
103 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
104
105 serving_chat = _build_serving_chat(mock_engine)
106
@@ -153,11 +150,11 @@ async def test_chat_error_non_stream():
150 async def test_chat_error_stream():
151 """test finish_reason='error' returns 500 InternalServerError (streaming)"""
152 mock_engine = MagicMock(spec=AsyncLLM)
156 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
153 mock_engine.errored = False
154 mock_engine.model_config = MockModelConfig()
155 mock_engine.input_processor = MagicMock()
156 mock_engine.io_processor = MagicMock()
157 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
158
159 serving_chat = _build_serving_chat(mock_engine)
160
tests/entrypoints/openai/test_completion_error.py
+13 -18
@@ -4,7 +4,7 @@
4 from dataclasses import dataclass, field
5 from http import HTTPStatus
6 from typing import Any
7 -from unittest.mock import AsyncMock, MagicMock
7 +from unittest.mock import MagicMock
8
9 import pytest
10
@@ -15,7 +15,8 @@ from vllm.entrypoints.openai.engine.protocol import ErrorResponse
15 from vllm.entrypoints.openai.models.protocol import BaseModelPath
16 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
17 from vllm.outputs import CompletionOutput, RequestOutput
18 -from vllm.tokenizers import get_tokenizer
18 +from vllm.renderers.hf import HfRenderer
19 +from vllm.tokenizers.registry import tokenizer_args_from_config
20 from vllm.v1.engine.async_llm import AsyncLLM
21
22 MODEL_NAME = "openai-community/gpt2"
@@ -61,37 +62,31 @@ def _build_serving_completion(engine: AsyncLLM) -> OpenAIServingCompletion:
62 engine_client=engine,
63 base_model_paths=BASE_MODEL_PATHS,
64 )
64 - serving_completion = OpenAIServingCompletion(
65 + return OpenAIServingCompletion(
66 engine,
67 models,
68 request_logger=None,
69 )
70
70 - async def _fake_process_inputs(
71 - request_id,
72 - engine_prompt,
73 - sampling_params,
74 - *,
75 - lora_request,
76 - trace_headers,
77 - priority,
78 - data_parallel_rank,
79 - ):
80 - return dict(engine_prompt), {}
71
82 - serving_completion._process_inputs = AsyncMock(side_effect=_fake_process_inputs)
83 - return serving_completion
72 +def _build_renderer(model_config: MockModelConfig):
73 + _, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
74 +
75 + return HfRenderer(
76 + model_config,
77 + tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
78 + )
79
80
81 @pytest.mark.asyncio
82 async def test_completion_error_non_stream():
83 """test finish_reason='error' returns 500 InternalServerError (non-streaming)"""
84 mock_engine = MagicMock(spec=AsyncLLM)
90 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
85 mock_engine.errored = False
86 mock_engine.model_config = MockModelConfig()
87 mock_engine.input_processor = MagicMock()
88 mock_engine.io_processor = MagicMock()
89 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
90
91 serving_completion = _build_serving_completion(mock_engine)
92
@@ -141,11 +136,11 @@ async def test_completion_error_non_stream():
136 async def test_completion_error_stream():
137 """test finish_reason='error' returns 500 InternalServerError (streaming)"""
138 mock_engine = MagicMock(spec=AsyncLLM)
144 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
139 mock_engine.errored = False
140 mock_engine.model_config = MockModelConfig()
141 mock_engine.input_processor = MagicMock()
142 mock_engine.io_processor = MagicMock()
143 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
144
145 serving_completion = _build_serving_completion(mock_engine)
146
tests/entrypoints/openai/test_completion_with_prompt_embeds.py
+10 -10
@@ -110,7 +110,7 @@ async def test_completions_with_prompt_embeds(
110 # Test case: Single prompt embeds input
111 completion = await client_with_prompt_embeds.completions.create(
112 model=model_name,
113 - prompt="", # Add empty prompt as required parameter
113 + prompt=None,
114 max_tokens=5,
115 temperature=0.0,
116 extra_body={"prompt_embeds": encoded_embeds},
@@ -121,7 +121,7 @@ async def test_completions_with_prompt_embeds(
121 # Test case: batch completion with prompt_embeds
122 completion = await client_with_prompt_embeds.completions.create(
123 model=model_name,
124 - prompt="", # Add empty prompt as required parameter
124 + prompt=None,
125 max_tokens=5,
126 temperature=0.0,
127 extra_body={"prompt_embeds": [encoded_embeds, encoded_embeds2]},
@@ -133,7 +133,7 @@ async def test_completions_with_prompt_embeds(
133 # Test case: streaming with prompt_embeds
134 single_completion = await client_with_prompt_embeds.completions.create(
135 model=model_name,
136 - prompt="", # Add empty prompt as required parameter
136 + prompt=None,
137 max_tokens=5,
138 temperature=0.0,
139 extra_body={"prompt_embeds": encoded_embeds},
@@ -142,7 +142,7 @@ async def test_completions_with_prompt_embeds(
142
143 stream = await client_with_prompt_embeds.completions.create(
144 model=model_name,
145 - prompt="", # Add empty prompt as required parameter
145 + prompt=None,
146 max_tokens=5,
147 temperature=0.0,
148 stream=True,
@@ -162,7 +162,7 @@ async def test_completions_with_prompt_embeds(
162 # Test case: batch streaming with prompt_embeds
163 stream = await client_with_prompt_embeds.completions.create(
164 model=model_name,
165 - prompt="", # Add empty prompt as required parameter
165 + prompt=None,
166 max_tokens=5,
167 temperature=0.0,
168 stream=True,
@@ -197,7 +197,7 @@ async def test_completions_with_prompt_embeds(
197 )
198 completion_embeds_only = await client_with_prompt_embeds.completions.create(
199 model=model_name,
200 - prompt="",
200 + prompt=None,
201 max_tokens=5,
202 temperature=0.0,
203 extra_body={"prompt_embeds": encoded_embeds},
@@ -215,7 +215,7 @@ async def test_completions_errors_with_prompt_embeds(
215 # Test error case: invalid prompt_embeds
216 with pytest.raises(BadRequestError):
217 await client_with_prompt_embeds.completions.create(
218 - prompt="",
218 + prompt=None,
219 model=model_name,
220 max_tokens=5,
221 temperature=0.0,
@@ -237,7 +237,7 @@ async def test_completions_with_logprobs_and_prompt_embeds(
237 # Test case: Logprobs using prompt_embeds
238 completion = await client_with_prompt_embeds.completions.create(
239 model=model_name,
240 - prompt="", # Add empty prompt as required parameter
240 + prompt=None,
241 max_tokens=5,
242 temperature=0.0,
243 echo=False,
@@ -257,7 +257,7 @@ async def test_completions_with_logprobs_and_prompt_embeds(
257 # Test case: Log probs with batch completion and prompt_embeds
258 completion = await client_with_prompt_embeds.completions.create(
259 model=model_name,
260 - prompt="", # Add empty prompt as required parameter
260 + prompt=None,
261 max_tokens=5,
262 temperature=0.0,
263 echo=False,
@@ -287,7 +287,7 @@ async def test_prompt_logprobs_raises_error(
287 with pytest.raises(BadRequestError, match="not compatible"):
288 await client_with_prompt_embeds.completions.create(
289 model=MODEL_NAME,
290 - prompt="",
290 + prompt=None,
291 max_tokens=5,
292 temperature=0.0,
293 extra_body={"prompt_embeds": encoded_embeds, "prompt_logprobs": True},
tests/entrypoints/openai/test_embedding_shape_validation.py
+1 -1
@@ -7,7 +7,7 @@ Tests verify that embeddings with correct ndim but incorrect hidden_size
7 are rejected before they can cause crashes during model inference.
8
9 Validation is performed by the parser (MultiModalDataParser) and EmbeddingItems
10 -classes, not by CompletionRenderer or MediaIO classes.
10 +classes, not by MediaIO classes.
11 """
12
13 import pytest
tests/entrypoints/openai/test_lora_resolvers.py
+13 -8
@@ -16,7 +16,8 @@ from vllm.entrypoints.openai.models.protocol import BaseModelPath
16 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
17 from vllm.lora.request import LoRARequest
18 from vllm.lora.resolver import LoRAResolver, LoRAResolverRegistry
19 -from vllm.tokenizers import get_tokenizer
19 +from vllm.renderers.hf import HfRenderer
20 +from vllm.tokenizers.registry import tokenizer_args_from_config
21 from vllm.v1.engine.async_llm import AsyncLLM
22
23 MODEL_NAME = "openai-community/gpt2"
@@ -35,6 +36,7 @@ class MockModelConfig:
36 """Minimal mock ModelConfig for testing."""
37
38 model: str = MODEL_NAME
39 + runner_type = "generate"
40 tokenizer: str = MODEL_NAME
41 trust_remote_code: bool = False
42 tokenizer_mode: str = "auto"
@@ -85,15 +87,21 @@ def register_mock_resolver():
87 del LoRAResolverRegistry.resolvers[MOCK_RESOLVER_NAME]
88
89
90 +def _build_renderer(model_config: MockModelConfig):
91 + _, tokenizer_name, _, kwargs = tokenizer_args_from_config(model_config)
92 +
93 + return HfRenderer(
94 + model_config,
95 + tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
96 + )
97 +
98 +
99 @pytest.fixture
100 def mock_serving_setup():
101 """Provides a mocked engine and serving completion instance."""
102 mock_engine = MagicMock(spec=AsyncLLM)
103 mock_engine.errored = False
104
94 - tokenizer = get_tokenizer(MODEL_NAME)
95 - mock_engine.get_tokenizer = AsyncMock(return_value=tokenizer)
96 -
105 async def mock_add_lora_side_effect(lora_request: LoRARequest):
106 """Simulate engine behavior when adding LoRAs."""
107 if lora_request.lora_name == "test-lora":
@@ -118,6 +126,7 @@ def mock_serving_setup():
126 mock_engine.model_config = MockModelConfig()
127 mock_engine.input_processor = MagicMock()
128 mock_engine.io_processor = MagicMock()
129 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
130
131 models = OpenAIServingModels(
132 engine_client=mock_engine,
@@ -128,10 +137,6 @@ def mock_serving_setup():
137 mock_engine, models, request_logger=None
138 )
139
131 - serving_completion._process_inputs = AsyncMock(
132 - return_value=(MagicMock(name="engine_request"), {})
133 - )
134 -
140 return mock_engine, serving_completion
141
142
tests/entrypoints/openai/test_prompt_validation.py
+4 -8
@@ -12,7 +12,7 @@ import regex as re
12 import torch
13
14 from vllm.config import ModelConfig
15 -from vllm.entrypoints.renderer import CompletionRenderer
15 +from vllm.renderers.embed_utils import safe_load_prompt_embeds
16
17 from ...utils import RemoteOpenAIServer
18
@@ -30,7 +30,7 @@ async def test_empty_prompt():
30 ):
31 await client.completions.create(
32 model=model_name,
33 - prompt="",
33 + prompt=None,
34 max_tokens=5,
35 temperature=0.0,
36 extra_body={"prompt_embeds": []},
@@ -63,7 +63,6 @@ def test_load_prompt_embeds(
63 ):
64 model_config = Mock(spec=ModelConfig)
65 model_config.enable_prompt_embeds = True
66 - renderer = CompletionRenderer(model_config, tokenizer=None)
66
67 # construct arbitrary tensors of various dtypes, layouts, and sizes.
68 # We need to check against different layouts to make sure that if a user
@@ -89,9 +88,7 @@ def test_load_prompt_embeds(
88 buffer.seek(0)
89 encoded_tensor = pybase64.b64encode(buffer.getvalue())
90
92 - loaded_prompt_embeds = renderer.load_prompt_embeds(encoded_tensor)
93 - assert len(loaded_prompt_embeds) == 1
94 - loaded_tensor = loaded_prompt_embeds[0]["prompt_embeds"]
91 + loaded_tensor = safe_load_prompt_embeds(model_config, encoded_tensor)
92 assert loaded_tensor.device.type == "cpu"
93 assert loaded_tensor.layout == torch.strided
94 torch.testing.assert_close(
@@ -105,7 +102,6 @@ def test_load_prompt_embeds(
102 def test_disable_prompt_embeds(dtype: torch.dtype, seq_len: int, hidden_size: int):
103 model_config = Mock(spec=ModelConfig)
104 model_config.enable_prompt_embeds = False
108 - renderer = CompletionRenderer(model_config, tokenizer=None)
105
106 tensor = torch.randn((seq_len, hidden_size), dtype=dtype)
107
@@ -115,4 +111,4 @@ def test_disable_prompt_embeds(dtype: torch.dtype, seq_len: int, hidden_size: in
111 encoded_tensor = pybase64.b64encode(buffer.getvalue())
112
113 with pytest.raises(ValueError, match="--enable-prompt-embeds"):
118 - renderer.load_prompt_embeds(encoded_tensor)
114 + safe_load_prompt_embeds(model_config, encoded_tensor)
tests/entrypoints/openai/test_serving_chat.py
+26 -21
@@ -556,19 +556,6 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
556 request_logger=None,
557 )
558
559 - async def _fake_process_inputs(
560 - request_id,
561 - engine_prompt,
562 - sampling_params,
563 - *,
564 - lora_request,
565 - trace_headers,
566 - priority,
567 - data_parallel_rank,
568 - ):
569 - return dict(engine_prompt), {}
570 -
571 - serving_chat._process_inputs = AsyncMock(side_effect=_fake_process_inputs)
559 return serving_chat
560
561
@@ -784,7 +771,7 @@ async def test_serving_chat_mistral_token_ids_prompt_is_validated():
771
772 resp = await serving_chat.create_chat_completion(req)
773 assert isinstance(resp, ErrorResponse)
787 - assert "max_tokens" in resp.error.message
774 + assert "context length is only" in resp.error.message
775
776
777 @pytest.mark.asyncio
@@ -824,7 +811,7 @@ async def test_serving_chat_mistral_token_ids_prompt_too_long_is_rejected():
811
812 resp = await serving_chat.create_chat_completion(req)
813 assert isinstance(resp, ErrorResponse)
827 - assert "maximum context length" in resp.error.message
814 + assert "context length is only" in resp.error.message
815
816
817 @pytest.mark.asyncio
@@ -890,6 +877,20 @@ async def test_serving_chat_did_set_correct_cache_salt(model_type):
877
878 serving_chat = _build_serving_chat(mock_engine)
879
880 + orig_render_chat_request = serving_chat.render_chat_request
881 + captured_prompts = []
882 +
883 + async def render_chat_request(request):
884 + result = await orig_render_chat_request(request)
885 +
886 + assert isinstance(result, tuple)
887 + conversation, engine_prompts = result
888 + captured_prompts.extend(engine_prompts)
889 +
890 + return result
891 +
892 + serving_chat.render_chat_request = render_chat_request
893 +
894 # Test cache_salt
895 req = ChatCompletionRequest(
896 model=MODEL_NAME,
@@ -899,15 +900,19 @@ async def test_serving_chat_did_set_correct_cache_salt(model_type):
900 # By default, cache_salt in the engine prompt is not set
901 with suppress(Exception):
902 await serving_chat.create_chat_completion(req)
902 - engine_prompt = serving_chat._process_inputs.await_args_list[0].args[1]
903 - assert "cache_salt" not in engine_prompt
903 +
904 + assert len(captured_prompts) == 1
905 + assert "cache_salt" not in captured_prompts[0]
906 +
907 + captured_prompts.clear()
908
909 # Test with certain cache_salt
910 req.cache_salt = "test_salt"
911 with suppress(Exception):
912 await serving_chat.create_chat_completion(req)
909 - engine_prompt = serving_chat._process_inputs.await_args_list[1].args[1]
910 - assert engine_prompt.get("cache_salt") == "test_salt"
913 +
914 + assert len(captured_prompts) == 1
915 + assert captured_prompts[0]["cache_salt"] == "test_salt"
916
917
918 @pytest.mark.asyncio
@@ -1007,11 +1012,11 @@ class TestServingChatWithHarmony:
1012 @pytest.fixture()
1013 def mock_engine(self) -> AsyncLLM:
1014 mock_engine = MagicMock(spec=AsyncLLM)
1010 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
1015 mock_engine.errored = False
1016 mock_engine.model_config = MockModelConfig()
1017 mock_engine.input_processor = MagicMock()
1018 mock_engine.io_processor = MagicMock()
1019 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
1020 return mock_engine
1021
1022 @pytest.fixture()
@@ -1618,11 +1623,11 @@ async def test_tool_choice_validation_without_parser():
1623 """Test that tool_choice='required' or named tool without tool_parser
1624 returns an appropriate error message."""
1625 mock_engine = MagicMock(spec=AsyncLLM)
1621 - mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
1626 mock_engine.errored = False
1627 mock_engine.model_config = MockModelConfig()
1628 mock_engine.input_processor = MagicMock()
1629 mock_engine.io_processor = MagicMock()
1630 + mock_engine.renderer = _build_renderer(mock_engine.model_config)
1631
1632 models = OpenAIServingModels(
1633 engine_client=mock_engine,
tests/entrypoints/pooling/basic/test_truncation.py
-14
@@ -67,20 +67,6 @@ async def test_smaller_truncation_size(client: openai.AsyncOpenAI):
67 assert response["usage"]["prompt_tokens"] == truncation_size
68
69
70 -@pytest.mark.asyncio
71 -async def test_zero_truncation_size(client: openai.AsyncOpenAI):
72 - truncation_size = 0
73 - kwargs: dict[str, Any] = {
74 - "model": MODEL_NAME,
75 - "input": input,
76 - "truncate_prompt_tokens": truncation_size,
77 - }
78 -
79 - response = await client.post(path="embeddings", cast_to=object, body={**kwargs})
80 -
81 - assert response["usage"]["prompt_tokens"] == truncation_size
82 -
83 -
70 @pytest.mark.asyncio
71 async def test_bigger_truncation_size(client: openai.AsyncOpenAI):
72 truncation_size = max_model_len + 1
tests/entrypoints/pooling/classify/test_online.py
+3 -5
@@ -128,12 +128,10 @@ def test_empty_input_error(server: RemoteOpenAIServer, model_name: str):
128 server.url_for("classify"),
129 json={"model": model_name, "input": []},
130 )
131 - classification_response.raise_for_status()
132 - output = ClassificationResponse.model_validate(classification_response.json())
131
134 - assert output.object == "list"
135 - assert isinstance(output.data, list)
136 - assert len(output.data) == 0
132 + error = classification_response.json()
133 + assert classification_response.status_code == 400
134 + assert "error" in error
135
136
137 @pytest.mark.parametrize("model_name", [MODEL_NAME])
tests/entrypoints/pooling/score/test_online_score.py
+1 -1
@@ -247,7 +247,7 @@ class TestModel:
247 },
248 )
249 assert score_response.status_code == 400
250 - assert "Please, select a smaller truncation size." in score_response.text
250 + assert "Please request a smaller truncation size." in score_response.text
251
252 def test_invocations(self, server: RemoteOpenAIServer, model: dict[str, Any]):
253 queries = "What is the capital of France?"
tests/entrypoints/test_renderer.py deleted
-325
@@ -1,325 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -
4 -import io
5 -from dataclasses import dataclass
6 -from unittest.mock import AsyncMock, MagicMock
7 -
8 -import pybase64
9 -import pytest
10 -import torch
11 -
12 -from vllm.entrypoints.renderer import CompletionRenderer, RenderConfig
13 -from vllm.inputs.data import is_embeds_prompt
14 -
15 -
16 -@dataclass
17 -class MockModelConfig:
18 - max_model_len: int = 100
19 - encoder_config: dict | None = None
20 - enable_prompt_embeds: bool = True
21 -
22 -
23 -class MockTokenizerResult:
24 - def __init__(self, input_ids):
25 - self.input_ids = input_ids
26 -
27 -
28 -@pytest.fixture
29 -def mock_model_config():
30 - return MockModelConfig()
31 -
32 -
33 -@pytest.fixture
34 -def mock_tokenizer():
35 - tokenizer = MagicMock()
36 - return tokenizer
37 -
38 -
39 -@pytest.fixture
40 -def mock_async_tokenizer():
41 - async_tokenizer = AsyncMock()
42 - return async_tokenizer
43 -
44 -
45 -@pytest.fixture
46 -def renderer(mock_model_config, mock_tokenizer):
47 - return CompletionRenderer(
48 - model_config=mock_model_config,
49 - tokenizer=mock_tokenizer,
50 - async_tokenizer_pool={},
51 - )
52 -
53 -
54 -class TestRenderPrompt:
55 - """Test Category A: Basic Functionality Tests"""
56 -
57 - @pytest.mark.asyncio
58 - async def test_token_input(self, renderer):
59 - tokens = [101, 7592, 2088]
60 - results = await renderer.render_prompt(
61 - prompt_or_prompts=tokens, config=RenderConfig(max_length=100)
62 - )
63 -
64 - assert len(results) == 1
65 - assert results[0]["prompt_token_ids"] == tokens
66 -
67 - @pytest.mark.asyncio
68 - async def test_token_list_input(self, renderer):
69 - token_lists = [[101, 7592, 2088], [102, 1234, 5678, 9012], [103, 4567]]
70 - results = await renderer.render_prompt(
71 - prompt_or_prompts=token_lists, config=RenderConfig(max_length=100)
72 - )
73 -
74 - assert len(results) == 3
75 - assert results[0]["prompt_token_ids"] == [101, 7592, 2088]
76 - assert results[1]["prompt_token_ids"] == [102, 1234, 5678, 9012]
77 - assert results[2]["prompt_token_ids"] == [103, 4567]
78 -
79 - @pytest.mark.asyncio
80 - async def test_text_input(self, renderer, mock_async_tokenizer):
81 - mock_async_tokenizer.return_value = MockTokenizerResult([101, 7592, 2088])
82 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
83 -
84 - results = await renderer.render_prompt(
85 - prompt_or_prompts="Hello world", config=RenderConfig(max_length=100)
86 - )
87 -
88 - assert len(results) == 1
89 - assert results[0]["prompt_token_ids"] == [101, 7592, 2088]
90 - mock_async_tokenizer.assert_called_once()
91 -
92 - @pytest.mark.asyncio
93 - async def test_text_list_input(self, renderer, mock_async_tokenizer):
94 - mock_async_tokenizer.return_value = MockTokenizerResult([101, 7592, 2088])
95 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
96 -
97 - text_list_input = ["Hello world", "How are you?", "Good morning"]
98 - results = await renderer.render_prompt(
99 - prompt_or_prompts=text_list_input, config=RenderConfig(max_length=100)
100 - )
101 -
102 - assert len(results) == 3
103 - for result in results:
104 - assert result["prompt_token_ids"] == [101, 7592, 2088]
105 - assert mock_async_tokenizer.call_count == 3
106 -
107 - @pytest.mark.asyncio
108 - async def test_no_truncation(self, renderer, mock_async_tokenizer):
109 - mock_async_tokenizer.return_value = MockTokenizerResult([101, 7592, 2088])
110 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
111 -
112 - results = await renderer.render_prompt(
113 - prompt_or_prompts="Hello world", config=RenderConfig(max_length=100)
114 - )
115 -
116 - assert len(results) == 1
117 - call_args = mock_async_tokenizer.call_args
118 - assert (
119 - "truncation" not in call_args.kwargs
120 - or call_args.kwargs["truncation"] is False
121 - )
122 -
123 - @pytest.mark.asyncio
124 - async def test_truncation_positive(self, renderer, mock_async_tokenizer):
125 - mock_async_tokenizer.return_value = MockTokenizerResult(
126 - [101, 7592, 2088]
127 - ) # Truncated
128 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
129 -
130 - results = await renderer.render_prompt(
131 - prompt_or_prompts="Hello world",
132 - config=RenderConfig(max_length=100, truncate_prompt_tokens=50),
133 - )
134 -
135 - assert len(results) == 1
136 - call_args = mock_async_tokenizer.call_args
137 - assert call_args.kwargs["truncation"] is True
138 - assert call_args.kwargs["max_length"] == 50
139 -
140 - @pytest.mark.asyncio
141 - async def test_truncation_negative(self, renderer, mock_async_tokenizer):
142 - # Test that negative truncation uses model's max_model_len
143 - mock_async_tokenizer.return_value = MockTokenizerResult(
144 - [101, 7592, 2088]
145 - ) # Truncated to max_model_len
146 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
147 -
148 - results = await renderer.render_prompt(
149 - prompt_or_prompts="Hello world",
150 - config=RenderConfig(max_length=200, truncate_prompt_tokens=-1),
151 - )
152 -
153 - assert len(results) == 1
154 - call_args = mock_async_tokenizer.call_args
155 - assert call_args.kwargs["truncation"] is True
156 - assert call_args.kwargs["max_length"] == 100 # model's max_model_len
157 -
158 - @pytest.mark.asyncio
159 - async def test_token_truncation_last_elements(self, renderer):
160 - # Test that token truncation keeps the last N elements
161 - long_tokens = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109] # 10 tokens
162 - results = await renderer.render_prompt(
163 - prompt_or_prompts=long_tokens,
164 - config=RenderConfig(max_length=100, truncate_prompt_tokens=5),
165 - )
166 -
167 - assert len(results) == 1
168 - # Should keep the last 5 tokens: [105, 106, 107, 108, 109]
169 - assert results[0]["prompt_token_ids"] == [105, 106, 107, 108, 109]
170 -
171 - @pytest.mark.asyncio
172 - async def test_max_length_exceeded(self, renderer):
173 - long_tokens = list(range(150)) # Exceeds max_model_len=100
174 -
175 - with pytest.raises(ValueError, match="maximum context length"):
176 - await renderer.render_prompt(
177 - prompt_or_prompts=long_tokens, config=RenderConfig(max_length=100)
178 - )
179 -
180 - @pytest.mark.asyncio
181 - async def test_no_tokenizer_for_text(self, mock_model_config):
182 - renderer_no_tokenizer = CompletionRenderer(
183 - model_config=mock_model_config, tokenizer=None, async_tokenizer_pool={}
184 - )
185 -
186 - with pytest.raises(ValueError, match="No tokenizer available"):
187 - await renderer_no_tokenizer.render_prompt(
188 - prompt_or_prompts="Hello world", config=RenderConfig(max_length=100)
189 - )
190 -
191 - @pytest.mark.asyncio
192 - async def test_token_input_with_needs_detokenization(
193 - self, renderer, mock_async_tokenizer
194 - ):
195 - # When needs_detokenization=True for token inputs, renderer should
196 - # use the async tokenizer to decode and include the original text
197 - # in the returned prompt object.
198 - mock_async_tokenizer.decode = AsyncMock(return_value="decoded text")
199 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
200 -
201 - tokens = [1, 2, 3, 4]
202 - results = await renderer.render_prompt(
203 - prompt_or_prompts=tokens,
204 - config=RenderConfig(needs_detokenization=True),
205 - )
206 -
207 - assert len(results) == 1
208 - assert results[0]["prompt_token_ids"] == tokens
209 - assert results[0]["prompt"] == "decoded text"
210 - mock_async_tokenizer.decode.assert_awaited_once()
211 -
212 -
213 -class TestRenderEmbedPrompt:
214 - def _create_test_embed_bytes(self, tensor: torch.Tensor) -> bytes:
215 - """Helper to create base64-encoded tensor bytes"""
216 - buffer = io.BytesIO()
217 - torch.save(tensor, buffer)
218 - buffer.seek(0)
219 - return pybase64.b64encode(buffer.read())
220 -
221 - @pytest.mark.asyncio
222 - async def test_single_prompt_embed(self, renderer):
223 - # Create a test tensor
224 - test_tensor = torch.randn(10, 768, dtype=torch.float32)
225 - embed_bytes = self._create_test_embed_bytes(test_tensor)
226 -
227 - results = await renderer.render_prompt_and_embeds(
228 - prompt_embeds=embed_bytes,
229 - config=RenderConfig(cache_salt="test_salt"),
230 - )
231 -
232 - assert len(results) == 1
233 - assert is_embeds_prompt(results[0])
234 - assert torch.allclose(results[0]["prompt_embeds"], test_tensor)
235 - assert results[0]["cache_salt"] == "test_salt"
236 -
237 - @pytest.mark.asyncio
238 - async def test_multiple_prompt_embeds(self, renderer):
239 - # Create multiple test tensors
240 - test_tensors = [
241 - torch.randn(8, 512, dtype=torch.float32),
242 - torch.randn(12, 512, dtype=torch.float32),
243 - ]
244 - embed_bytes_list = [self._create_test_embed_bytes(t) for t in test_tensors]
245 -
246 - results = await renderer.render_prompt_and_embeds(
247 - prompt_embeds=embed_bytes_list,
248 - config=RenderConfig(),
249 - )
250 -
251 - assert len(results) == 2
252 - for i, result in enumerate(results):
253 - assert is_embeds_prompt(result)
254 - assert torch.allclose(result["prompt_embeds"], test_tensors[i])
255 -
256 - @pytest.mark.asyncio
257 - async def test_prompt_embed_truncation(self, renderer):
258 - # Create tensor with more tokens than truncation limit
259 - test_tensor = torch.randn(20, 768, dtype=torch.float32)
260 - embed_bytes = self._create_test_embed_bytes(test_tensor)
261 -
262 - results = await renderer.render_prompt_and_embeds(
263 - prompt_embeds=embed_bytes,
264 - config=RenderConfig(truncate_prompt_tokens=10),
265 - )
266 -
267 - assert len(results) == 1
268 - # Should keep last 10 tokens
269 - expected = test_tensor[-10:]
270 - assert torch.allclose(results[0]["prompt_embeds"], expected)
271 -
272 - @pytest.mark.asyncio
273 - async def test_prompt_embed_different_dtypes(self, renderer):
274 - # Test different supported dtypes
275 - dtypes = [torch.float32, torch.float16, torch.bfloat16]
276 -
277 - for dtype in dtypes:
278 - test_tensor = torch.randn(5, 256, dtype=dtype)
279 - embed_bytes = self._create_test_embed_bytes(test_tensor)
280 -
281 - results = await renderer.render_prompt_and_embeds(
282 - prompt_embeds=embed_bytes,
283 - config=RenderConfig(),
284 - )
285 -
286 - assert len(results) == 1
287 - assert results[0]["prompt_embeds"].dtype == dtype
288 -
289 - @pytest.mark.asyncio
290 - async def test_prompt_embed_squeeze_batch_dim(self, renderer):
291 - # Test tensor with batch dimension gets squeezed
292 - test_tensor = torch.randn(1, 10, 768, dtype=torch.float32)
293 - embed_bytes = self._create_test_embed_bytes(test_tensor)
294 -
295 - results = await renderer.render_prompt_and_embeds(
296 - prompt_embeds=embed_bytes,
297 - config=RenderConfig(),
298 - )
299 -
300 - assert len(results) == 1
301 - # Should be squeezed to 2D
302 - assert results[0]["prompt_embeds"].shape == (10, 768)
303 -
304 - @pytest.mark.asyncio
305 - async def test_both_prompts_and_embeds(self, renderer, mock_async_tokenizer):
306 - # Set up text tokenization
307 - mock_async_tokenizer.return_value = MockTokenizerResult([101, 102, 103])
308 - renderer.async_tokenizer_pool[renderer.tokenizer] = mock_async_tokenizer
309 -
310 - # Create embed
311 - test_tensor = torch.randn(5, 256, dtype=torch.float32)
312 - embed_bytes = self._create_test_embed_bytes(test_tensor)
313 -
314 - results = await renderer.render_prompt_and_embeds(
315 - prompt_or_prompts="Hello world",
316 - prompt_embeds=embed_bytes,
317 - config=RenderConfig(),
318 - )
319 -
320 - assert len(results) == 2
321 - # First should be embed prompt
322 - assert is_embeds_prompt(results[0])
323 - # Second should be tokens prompt
324 - assert "prompt_token_ids" in results[1]
325 - assert results[1]["prompt_token_ids"] == [101, 102, 103]
tests/models/language/pooling/test_mm_classifier_conversion.py
+1 -1
@@ -96,7 +96,7 @@ def test_gemma_multimodal(
96 dtype="bfloat16",
97 ) as vllm_model:
98 llm = vllm_model.get_llm()
99 - prompts = llm.preprocess_chat(messages)
99 + prompts = llm._preprocess_chat([messages])
100
101 result = llm.classify(prompts)
102 assert result[0].outputs.probs[0] > 0.95
tests/models/language/pooling/test_truncation_control.py
+6 -3
@@ -29,7 +29,8 @@ def test_smaller_truncation_size(
29 model_name, runner="pooling", max_model_len=max_model_len
30 ) as vllm_model:
31 vllm_output = vllm_model.llm.embed(
32 - input_str, truncate_prompt_tokens=truncate_prompt_tokens
32 + input_str,
33 + tokenization_kwargs=dict(truncate_prompt_tokens=truncate_prompt_tokens),
34 )
35
36 prompt_tokens = vllm_output[0].prompt_token_ids
@@ -44,7 +45,8 @@ def test_max_truncation_size(vllm_runner, model_name=MODEL_NAME, input_str=input
45 model_name, runner="pooling", max_model_len=max_model_len
46 ) as vllm_model:
47 vllm_output = vllm_model.llm.embed(
47 - input_str, truncate_prompt_tokens=truncate_prompt_tokens
48 + input_str,
49 + tokenization_kwargs=dict(truncate_prompt_tokens=truncate_prompt_tokens),
50 )
51
52 prompt_tokens = vllm_output[0].prompt_token_ids
@@ -64,7 +66,8 @@ def test_bigger_truncation_size(
66 ) as vllm_model,
67 ):
68 llm_output = vllm_model.llm.embed(
67 - input_str, truncate_prompt_tokens=truncate_prompt_tokens
69 + input_str,
70 + tokenization_kwargs=dict(truncate_prompt_tokens=truncate_prompt_tokens),
71 )
72
73 assert (
tests/models/language/pooling_mteb_test/mteb_embed_utils.py
+4 -1
@@ -187,7 +187,10 @@ def mteb_test_embed_models(
187 head_dtype = model_config.head_dtype
188
189 # Test embedding_size, isnan and whether to use normalize
190 - vllm_outputs = vllm_model.embed(example_prompts, truncate_prompt_tokens=-1)
190 + vllm_outputs = vllm_model.embed(
191 + example_prompts,
192 + tokenization_kwargs=dict(truncate_prompt_tokens=-1),
193 + )
194 outputs_tensor = torch.tensor(vllm_outputs)
195 assert not torch.any(torch.isnan(outputs_tensor))
196 embedding_size = model_config.embedding_size
tests/models/language/pooling_mteb_test/mteb_score_utils.py
+1 -1
@@ -79,9 +79,9 @@ class VllmMtebCrossEncoder(MtebCrossEncoderMixin):
79 outputs = self.llm.score(
80 queries,
81 corpus,
82 - truncate_prompt_tokens=-1,
82 use_tqdm=False,
83 chat_template=self.chat_template,
84 + tokenization_kwargs={"truncate_prompt_tokens": -1},
85 )
86 scores = np.array(outputs)
87 scores = scores[np.argsort(r)]
tests/renderers/test_completions.py new
+426
@@ -0,0 +1,426 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import io
5 +from dataclasses import dataclass
6 +from typing import Any
7 +from unittest.mock import AsyncMock
8 +
9 +import pybase64
10 +import pytest
11 +import torch
12 +
13 +from vllm.inputs.data import is_embeds_prompt
14 +from vllm.renderers import TokenizeParams
15 +from vllm.renderers.hf import HfRenderer
16 +from vllm.tokenizers.registry import tokenizer_args_from_config
17 +
18 +MODEL_NAME = "openai-community/gpt2"
19 +
20 +
21 +@dataclass
22 +class MockHFConfig:
23 + model_type: str = "any"
24 +
25 +
26 +@dataclass
27 +class MockModelConfig:
28 + runner_type = "generate"
29 + model: str = MODEL_NAME
30 + tokenizer: str = MODEL_NAME
31 + trust_remote_code: bool = False
32 + max_model_len: int = 100
33 + tokenizer_revision = None
34 + tokenizer_mode = "auto"
35 + hf_config = MockHFConfig()
36 + encoder_config: dict[str, Any] | None = None
37 + enable_prompt_embeds: bool = True
38 + skip_tokenizer_init: bool = False
39 +
40 +
41 +@pytest.fixture
42 +def mock_model_config():
43 + return MockModelConfig()
44 +
45 +
46 +@pytest.fixture
47 +def mock_async_tokenizer():
48 + return AsyncMock()
49 +
50 +
51 +@pytest.fixture
52 +def renderer(mock_model_config):
53 + _, tokenizer_name, _, kwargs = tokenizer_args_from_config(mock_model_config)
54 +
55 + return HfRenderer(
56 + mock_model_config,
57 + tokenizer_kwargs={**kwargs, "tokenizer_name": tokenizer_name},
58 + )
59 +
60 +
61 +class TestValidatePrompt:
62 + STRING_INPUTS = [
63 + "",
64 + "foo",
65 + "foo bar",
66 + "foo baz bar",
67 + "foo bar qux baz",
68 + ]
69 +
70 + TOKEN_INPUTS = [
71 + [-1],
72 + [1],
73 + [1, 2],
74 + [1, 3, 4],
75 + [1, 2, 4, 3],
76 + ]
77 +
78 + INPUTS_SLICES = [
79 + slice(None, None, -1),
80 + slice(None, None, 2),
81 + slice(None, None, -2),
82 + ]
83 +
84 + # Test that a nested mixed-type list of lists raises a TypeError.
85 + def test_empty_input(self, renderer):
86 + with pytest.raises(ValueError, match="at least one prompt"):
87 + renderer.render_completions([])
88 +
89 + def test_invalid_type(self, renderer):
90 + with pytest.raises(TypeError, match="string or an array of tokens"):
91 + renderer.render_completions([[1, 2], ["foo", "bar"]])
92 +
93 + @pytest.mark.parametrize("string_input", STRING_INPUTS)
94 + def test_string_consistent(self, renderer, string_input: str):
95 + assert renderer.render_completions(string_input) == renderer.render_completions(
96 + [string_input]
97 + )
98 +
99 + @pytest.mark.parametrize("token_input", TOKEN_INPUTS)
100 + def test_token_consistent(self, renderer, token_input: list[int]):
101 + assert renderer.render_completions(token_input) == renderer.render_completions(
102 + [token_input]
103 + )
104 +
105 + @pytest.mark.parametrize("inputs_slice", INPUTS_SLICES)
106 + def test_string_slice(self, renderer, inputs_slice: slice):
107 + assert renderer.render_completions(self.STRING_INPUTS)[
108 + inputs_slice
109 + ] == renderer.render_completions(self.STRING_INPUTS[inputs_slice])
110 +
111 +
112 +class TestRenderPrompt:
113 + @pytest.mark.asyncio
114 + async def test_token_input(self, renderer):
115 + tokens = [101, 7592, 2088]
116 + prompts = await renderer.render_completions_async(tokens)
117 + results = await renderer.tokenize_prompts_async(
118 + prompts,
119 + TokenizeParams(max_total_tokens=100),
120 + )
121 +
122 + assert len(results) == 1
123 + assert results[0]["prompt_token_ids"] == tokens
124 +
125 + @pytest.mark.asyncio
126 + async def test_token_list_input(self, renderer):
127 + token_lists = [[101, 7592, 2088], [102, 1234, 5678, 9012], [103, 4567]]
128 + prompts = await renderer.render_completions_async(token_lists)
129 + results = await renderer.tokenize_prompts_async(
130 + prompts,
131 + TokenizeParams(max_total_tokens=100),
132 + )
133 +
134 + assert len(results) == 3
135 + assert results[0]["prompt_token_ids"] == [101, 7592, 2088]
136 + assert results[1]["prompt_token_ids"] == [102, 1234, 5678, 9012]
137 + assert results[2]["prompt_token_ids"] == [103, 4567]
138 +
139 + @pytest.mark.asyncio
140 + async def test_text_input(self, renderer, mock_async_tokenizer):
141 + mock_async_tokenizer.encode.return_value = [101, 7592, 2088]
142 + renderer._async_tokenizer = mock_async_tokenizer
143 +
144 + prompts = await renderer.render_completions_async("Hello world")
145 + results = await renderer.tokenize_prompts_async(
146 + prompts,
147 + TokenizeParams(max_total_tokens=100),
148 + )
149 +
150 + assert len(results) == 1
151 + assert results[0]["prompt_token_ids"] == [101, 7592, 2088]
152 + mock_async_tokenizer.encode.assert_called_once()
153 +
154 + @pytest.mark.asyncio
155 + async def test_text_list_input(self, renderer, mock_async_tokenizer):
156 + mock_async_tokenizer.encode.return_value = [101, 7592, 2088]
157 + renderer._async_tokenizer = mock_async_tokenizer
158 +
159 + text_list_input = ["Hello world", "How are you?", "Good morning"]
160 + prompts = await renderer.render_completions_async(text_list_input)
161 + results = await renderer.tokenize_prompts_async(
162 + prompts,
163 + TokenizeParams(max_total_tokens=100),
164 + )
165 +
166 + assert len(results) == 3
167 + for result in results:
168 + assert result["prompt_token_ids"] == [101, 7592, 2088]
169 + assert mock_async_tokenizer.encode.call_count == 3
170 +
171 + @pytest.mark.asyncio
172 + async def test_no_truncation(self, renderer, mock_async_tokenizer):
173 + mock_async_tokenizer.encode.return_value = [101, 7592, 2088]
174 + renderer._async_tokenizer = mock_async_tokenizer
175 +
176 + prompts = await renderer.render_completions_async("Hello world")
177 + results = await renderer.tokenize_prompts_async(
178 + prompts,
179 + TokenizeParams(max_total_tokens=100),
180 + )
181 +
182 + assert len(results) == 1
183 + call_args = mock_async_tokenizer.encode.call_args
184 + assert (
185 + "truncation" not in call_args.kwargs
186 + or call_args.kwargs["truncation"] is False
187 + )
188 +
189 + @pytest.mark.asyncio
190 + async def test_truncation_positive(self, renderer, mock_async_tokenizer):
191 + mock_async_tokenizer.encode.return_value = [101, 7592, 2088] # Truncated
192 + renderer._async_tokenizer = mock_async_tokenizer
193 +
194 + prompts = await renderer.render_completions_async("Hello world")
195 + results = await renderer.tokenize_prompts_async(
196 + prompts,
197 + TokenizeParams(
198 + max_total_tokens=200,
199 + truncate_prompt_tokens=50,
200 + ),
201 + )
202 +
203 + assert len(results) == 1
204 + call_args = mock_async_tokenizer.encode.call_args
205 + assert call_args.kwargs["truncation"] is True
206 + assert call_args.kwargs["max_length"] == 50
207 +
208 + @pytest.mark.asyncio
209 + async def test_truncation_negative(self, renderer, mock_async_tokenizer):
210 + # Test that negative truncation uses model's max_model_len
211 + mock_async_tokenizer.encode.return_value = [
212 + 101,
213 + 7592,
214 + 2088,
215 + ] # Truncated to max_model_len
216 + renderer._async_tokenizer = mock_async_tokenizer
217 +
218 + prompts = await renderer.render_completions_async("Hello world")
219 + results = await renderer.tokenize_prompts_async(
220 + prompts,
221 + TokenizeParams(
222 + max_total_tokens=200,
223 + truncate_prompt_tokens=-1,
224 + ),
225 + )
226 +
227 + assert len(results) == 1
228 + call_args = mock_async_tokenizer.encode.call_args
229 + assert call_args.kwargs["truncation"] is True
230 + assert call_args.kwargs["max_length"] == 200
231 +
232 + @pytest.mark.asyncio
233 + async def test_token_truncation_last_elements(self, renderer):
234 + # Test that token truncation keeps the last N elements
235 + long_tokens = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109] # 10 tokens
236 + prompts = await renderer.render_completions_async(long_tokens)
237 + results = await renderer.tokenize_prompts_async(
238 + prompts,
239 + TokenizeParams(
240 + max_total_tokens=100,
241 + truncate_prompt_tokens=5,
242 + ),
243 + )
244 +
245 + assert len(results) == 1
246 + # Should keep the last 5 tokens: [105, 106, 107, 108, 109]
247 + assert results[0]["prompt_token_ids"] == [105, 106, 107, 108, 109]
248 +
249 + @pytest.mark.asyncio
250 + async def test_max_length_exceeded(self, renderer):
251 + long_tokens = list(range(150)) # Exceeds max_model_len=100
252 +
253 + prompts = await renderer.render_completions_async(long_tokens)
254 +
255 + with pytest.raises(ValueError, match="context length is only"):
256 + await renderer.tokenize_prompts_async(
257 + prompts,
258 + TokenizeParams(max_total_tokens=100),
259 + )
260 +
261 + @pytest.mark.asyncio
262 + async def test_no_tokenizer_for_text(self, renderer):
263 + renderer_no_tokenizer = HfRenderer.from_config(
264 + MockModelConfig(skip_tokenizer_init=True),
265 + tokenizer_kwargs={},
266 + )
267 +
268 + prompts = await renderer_no_tokenizer.render_completions_async("Hello world")
269 +
270 + with pytest.raises(ValueError, match="`skip_tokenizer_init=True`"):
271 + await renderer_no_tokenizer.tokenize_prompts_async(
272 + prompts,
273 + TokenizeParams(max_total_tokens=100),
274 + )
275 +
276 + @pytest.mark.asyncio
277 + async def test_token_input_with_needs_detokenization(
278 + self, renderer, mock_async_tokenizer
279 + ):
280 + # When needs_detokenization=True for token inputs, renderer should
281 + # use the async tokenizer to decode and include the original text
282 + # in the returned prompt object.
283 + mock_async_tokenizer.decode = AsyncMock(return_value="decoded text")
284 + renderer._async_tokenizer = mock_async_tokenizer
285 +
286 + tokens = [1, 2, 3, 4]
287 + prompts = await renderer.render_completions_async(tokens)
288 + results = await renderer.tokenize_prompts_async(
289 + prompts,
290 + TokenizeParams(
291 + max_total_tokens=renderer.config.max_model_len,
292 + needs_detokenization=True,
293 + ),
294 + )
295 +
296 + assert len(results) == 1
297 + assert results[0]["prompt_token_ids"] == tokens
298 + assert results[0]["prompt"] == "decoded text"
299 + mock_async_tokenizer.decode.assert_awaited_once()
300 +
301 +
302 +class TestRenderEmbedPrompt:
303 + def _create_test_embed_bytes(self, tensor: torch.Tensor) -> bytes:
304 + """Helper to create base64-encoded tensor bytes"""
305 + buffer = io.BytesIO()
306 + torch.save(tensor, buffer)
307 + buffer.seek(0)
308 + return pybase64.b64encode(buffer.read())
309 +
310 + @pytest.mark.asyncio
311 + async def test_single_prompt_embed(self, renderer):
312 + # Create a test tensor
313 + test_tensor = torch.randn(10, 768, dtype=torch.float32)
314 + embed_bytes = self._create_test_embed_bytes(test_tensor)
315 +
316 + prompts = await renderer.render_completions_async(prompt_embeds=embed_bytes)
317 + results = await renderer.tokenize_prompts_async(
318 + prompts,
319 + TokenizeParams(max_total_tokens=renderer.config.max_model_len),
320 + )
321 +
322 + assert len(results) == 1
323 + assert is_embeds_prompt(results[0])
324 + assert torch.allclose(results[0]["prompt_embeds"], test_tensor)
325 +
326 + @pytest.mark.asyncio
327 + async def test_multiple_prompt_embeds(self, renderer):
328 + # Create multiple test tensors
329 + test_tensors = [
330 + torch.randn(8, 512, dtype=torch.float32),
331 + torch.randn(12, 512, dtype=torch.float32),
332 + ]
333 + embed_bytes_list = [self._create_test_embed_bytes(t) for t in test_tensors]
334 +
335 + prompts = await renderer.render_completions_async(
336 + prompt_embeds=embed_bytes_list
337 + )
338 + results = await renderer.tokenize_prompts_async(
339 + prompts,
340 + TokenizeParams(max_total_tokens=renderer.config.max_model_len),
341 + )
342 +
343 + assert len(results) == 2
344 + for i, result in enumerate(results):
345 + assert is_embeds_prompt(result)
346 + assert torch.allclose(result["prompt_embeds"], test_tensors[i])
347 +
348 + @pytest.mark.asyncio
349 + async def test_prompt_embed_truncation(self, renderer):
350 + # Create tensor with more tokens than truncation limit
351 + test_tensor = torch.randn(20, 768, dtype=torch.float32)
352 + embed_bytes = self._create_test_embed_bytes(test_tensor)
353 +
354 + prompts = await renderer.render_completions_async(prompt_embeds=embed_bytes)
355 + results = await renderer.tokenize_prompts_async(
356 + prompts,
357 + TokenizeParams(
358 + max_total_tokens=renderer.config.max_model_len,
359 + truncate_prompt_tokens=10,
360 + ),
361 + )
362 +
363 + assert len(results) == 1
364 + # Should keep last 10 tokens
365 + expected = test_tensor[-10:]
366 + assert torch.allclose(results[0]["prompt_embeds"], expected)
367 +
368 + @pytest.mark.asyncio
369 + async def test_prompt_embed_different_dtypes(self, renderer):
370 + # Test different supported dtypes
371 + dtypes = [torch.float32, torch.float16, torch.bfloat16]
372 +
373 + for dtype in dtypes:
374 + test_tensor = torch.randn(5, 256, dtype=dtype)
375 + embed_bytes = self._create_test_embed_bytes(test_tensor)
376 +
377 + prompts = await renderer.render_completions_async(prompt_embeds=embed_bytes)
378 + results = await renderer.tokenize_prompts_async(
379 + prompts,
380 + TokenizeParams(max_total_tokens=renderer.config.max_model_len),
381 + )
382 +
383 + assert len(results) == 1
384 + assert results[0]["prompt_embeds"].dtype == dtype
385 +
386 + @pytest.mark.asyncio
387 + async def test_prompt_embed_squeeze_batch_dim(self, renderer):
388 + # Test tensor with batch dimension gets squeezed
389 + test_tensor = torch.randn(1, 10, 768, dtype=torch.float32)
390 + embed_bytes = self._create_test_embed_bytes(test_tensor)
391 +
392 + prompts = await renderer.render_completions_async(prompt_embeds=embed_bytes)
393 + results = await renderer.tokenize_prompts_async(
394 + prompts,
395 + TokenizeParams(max_total_tokens=renderer.config.max_model_len),
396 + )
397 +
398 + assert len(results) == 1
399 + # Should be squeezed to 2D
400 + assert results[0]["prompt_embeds"].shape == (10, 768)
401 +
402 + @pytest.mark.asyncio
403 + async def test_both_prompts_and_embeds(self, renderer, mock_async_tokenizer):
404 + # Set up text tokenization
405 + mock_async_tokenizer.encode.return_value = [101, 102, 103]
406 + renderer._async_tokenizer = mock_async_tokenizer
407 +
408 + # Create embed
409 + test_tensor = torch.randn(5, 256, dtype=torch.float32)
410 + embed_bytes = self._create_test_embed_bytes(test_tensor)
411 +
412 + prompts = await renderer.render_completions_async(
413 + "Hello world",
414 + prompt_embeds=embed_bytes,
415 + )
416 + results = await renderer.tokenize_prompts_async(
417 + prompts,
418 + TokenizeParams(max_total_tokens=renderer.config.max_model_len),
419 + )
420 +
421 + assert len(results) == 2
422 + # First should be embed prompt
423 + assert is_embeds_prompt(results[0])
424 + # Second should be tokens prompt
425 + assert "prompt_token_ids" in results[1]
426 + assert results[1]["prompt_token_ids"] == [101, 102, 103]
tests/renderers/test_mistral.py
+2 -1
@@ -9,6 +9,7 @@ import pytest
9 from mistral_common.tokens.tokenizers.base import SpecialTokenPolicy
10
11 from vllm.config import ModelConfig
12 +from vllm.renderers import ChatParams
13 from vllm.renderers.mistral import MistralRenderer, safe_apply_chat_template
14 from vllm.tokenizers.mistral import MistralTokenizer
15
@@ -27,7 +28,7 @@ async def test_async_mistral_tokenizer_does_not_block_event_loop():
28 mock_renderer = MistralRenderer(Mock(spec=ModelConfig), tokenizer_kwargs={})
29 mock_renderer._tokenizer = mock_tokenizer
30
30 - task = mock_renderer.render_messages_async([])
31 + task = mock_renderer.render_messages_async([], ChatParams())
32
33 # Ensure the event loop is not blocked
34 blocked_count = 0
tests/renderers/test_sparse_tensor_validation.py renamed
+23 -74
@@ -1,8 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """
4 -Sparse tensor validation in embedding APIs.
5 -
4 Tests verify that malicious sparse tensors are rejected before they can trigger
5 out-of-bounds memory writes during to_dense() operations.
6 """
@@ -13,8 +11,24 @@ import io
11 import pytest
12 import torch
13
16 -from vllm.entrypoints.renderer import CompletionRenderer
14 from vllm.multimodal.media import AudioEmbeddingMediaIO, ImageEmbeddingMediaIO
15 +from vllm.renderers.embed_utils import safe_load_prompt_embeds
16 +
17 +
18 +@pytest.fixture
19 +def model_config():
20 + """Mock ModelConfig for testing."""
21 + from vllm.config import ModelConfig
22 +
23 + return ModelConfig(
24 + model="facebook/opt-125m",
25 + tokenizer="facebook/opt-125m",
26 + tokenizer_mode="auto",
27 + trust_remote_code=False,
28 + dtype="float32",
29 + seed=0,
30 + enable_prompt_embeds=True, # Required for prompt embeds tests
31 + )
32
33
34 def _encode_tensor(tensor: torch.Tensor) -> bytes:
@@ -63,15 +77,12 @@ class TestPromptEmbedsValidation:
77
78 def test_valid_dense_tensor_accepted(self, model_config):
79 """Baseline: Valid dense tensors should work normally."""
66 - renderer = CompletionRenderer(model_config)
67 -
80 valid_tensor = _create_valid_dense_tensor()
81 encoded = _encode_tensor(valid_tensor)
82
83 # Should not raise any exception
72 - result = renderer.load_prompt_embeds(encoded)
73 - assert len(result) == 1
74 - assert result[0]["prompt_embeds"].shape == valid_tensor.shape
84 + result = safe_load_prompt_embeds(model_config, encoded)
85 + assert result.shape == valid_tensor.shape
86
87 def test_valid_sparse_tensor_accepted(self):
88 """Baseline: Valid sparse tensors should load successfully."""
@@ -86,14 +97,12 @@ class TestPromptEmbedsValidation:
97
98 def test_malicious_sparse_tensor_rejected(self, model_config):
99 """Security: Malicious sparse tensors should be rejected."""
89 - renderer = CompletionRenderer(model_config)
90 -
100 malicious_tensor = _create_malicious_sparse_tensor()
101 encoded = _encode_tensor(malicious_tensor)
102
103 # Should raise RuntimeError due to invalid sparse tensor
104 with pytest.raises((RuntimeError, ValueError)) as exc_info:
96 - renderer.load_prompt_embeds(encoded)
105 + safe_load_prompt_embeds(model_config, encoded)
106
107 # Error should indicate sparse tensor validation failure
108 error_msg = str(exc_info.value).lower()
@@ -101,8 +110,6 @@ class TestPromptEmbedsValidation:
110
111 def test_extremely_large_indices_rejected(self, model_config):
112 """Security: Sparse tensors with extremely large indices should be rejected."""
104 - renderer = CompletionRenderer(model_config)
105 -
113 # Create tensor with indices far beyond reasonable bounds
114 indices = torch.tensor([[999999], [999999]])
115 values = torch.tensor([1.0])
@@ -114,12 +121,10 @@ class TestPromptEmbedsValidation:
121 encoded = _encode_tensor(malicious_tensor)
122
123 with pytest.raises((RuntimeError, ValueError)):
117 - renderer.load_prompt_embeds(encoded)
124 + safe_load_prompt_embeds(model_config, encoded)
125
126 def test_negative_indices_rejected(self, model_config):
127 """Security: Sparse tensors with negative indices should be rejected."""
121 - renderer = CompletionRenderer(model_config)
122 -
128 # Create tensor with negative indices
129 indices = torch.tensor([[-1], [-1]])
130 values = torch.tensor([1.0])
@@ -131,7 +136,7 @@ class TestPromptEmbedsValidation:
136 encoded = _encode_tensor(malicious_tensor)
137
138 with pytest.raises((RuntimeError, ValueError)):
134 - renderer.load_prompt_embeds(encoded)
139 + safe_load_prompt_embeds(model_config, encoded)
140
141
142 class TestImageEmbedsValidation:
@@ -253,14 +258,12 @@ class TestSparseTensorValidationIntegration:
258 3. Sends to /v1/completions with prompt_embeds parameter
259 4. Server should reject before memory corruption occurs
260 """
256 - renderer = CompletionRenderer(model_config)
257 -
261 # Step 1-2: Attacker creates malicious payload
262 attack_payload = _encode_tensor(_create_malicious_sparse_tensor())
263
264 # Step 3-4: Server processes and should reject
265 with pytest.raises((RuntimeError, ValueError)):
263 - renderer.load_prompt_embeds(attack_payload)
266 + safe_load_prompt_embeds(model_config, attack_payload)
267
268 def test_attack_scenario_chat_api_image(self):
269 """
@@ -285,57 +288,3 @@ class TestSparseTensorValidationIntegration:
288
289 with pytest.raises((RuntimeError, ValueError)):
290 io_handler.load_base64("", attack_payload.decode("utf-8"))
288 -
289 - def test_multiple_valid_embeddings_in_batch(self, model_config):
290 - """
291 - Regression test: Multiple valid embeddings should still work.
292 -
293 - Ensures the fix doesn't break legitimate batch processing.
294 - """
295 - renderer = CompletionRenderer(model_config)
296 -
297 - valid_tensors = [
298 - _encode_tensor(_create_valid_dense_tensor()),
299 - _encode_tensor(_create_valid_dense_tensor()),
300 - _encode_tensor(_create_valid_dense_tensor()),
301 - ]
302 -
303 - # Should process all without error
304 - result = renderer.load_prompt_embeds(valid_tensors)
305 - assert len(result) == 3
306 -
307 - def test_mixed_valid_and_malicious_rejected(self, model_config):
308 - """
309 - Security: Batch with one malicious tensor should be rejected.
310 -
311 - Even if most tensors are valid, a single malicious one should
312 - cause rejection of the entire batch.
313 - """
314 - renderer = CompletionRenderer(model_config)
315 -
316 - mixed_batch = [
317 - _encode_tensor(_create_valid_dense_tensor()),
318 - _encode_tensor(_create_malicious_sparse_tensor()), # Malicious
319 - _encode_tensor(_create_valid_dense_tensor()),
320 - ]
321 -
322 - # Should fail on the malicious tensor
323 - with pytest.raises((RuntimeError, ValueError)):
324 - renderer.load_prompt_embeds(mixed_batch)
325 -
326 -
327 -# Pytest fixtures
328 -@pytest.fixture
329 -def model_config():
330 - """Mock ModelConfig for testing."""
331 - from vllm.config import ModelConfig
332 -
333 - return ModelConfig(
334 - model="facebook/opt-125m",
335 - tokenizer="facebook/opt-125m",
336 - tokenizer_mode="auto",
337 - trust_remote_code=False,
338 - dtype="float32",
339 - seed=0,
340 - enable_prompt_embeds=True, # Required for prompt embeds tests
341 - )
tests/test_inputs.py
-55
@@ -5,65 +5,10 @@ import pytest
5
6 from vllm.config import ModelConfig
7 from vllm.inputs import zip_enc_dec_prompts
8 -from vllm.inputs.parse import parse_raw_prompts
8 from vllm.inputs.preprocess import InputPreprocessor
9
10 pytestmark = pytest.mark.cpu_test
11
13 -STRING_INPUTS = [
14 - "",
15 - "foo",
16 - "foo bar",
17 - "foo baz bar",
18 - "foo bar qux baz",
19 -]
20 -
21 -TOKEN_INPUTS = [
22 - [-1],
23 - [1],
24 - [1, 2],
25 - [1, 3, 4],
26 - [1, 2, 4, 3],
27 -]
28 -
29 -INPUTS_SLICES = [
30 - slice(None, None, -1),
31 - slice(None, None, 2),
32 - slice(None, None, -2),
33 -]
34 -
35 -
36 -# Test that a nested mixed-type list of lists raises a TypeError.
37 -@pytest.mark.parametrize("invalid_input", [[[1, 2], ["foo", "bar"]]])
38 -def test_invalid_input_raise_type_error(invalid_input):
39 - with pytest.raises(TypeError):
40 - parse_raw_prompts(invalid_input)
41 -
42 -
43 -def test_parse_raw_single_batch_empty():
44 - with pytest.raises(ValueError, match="at least one prompt"):
45 - parse_raw_prompts([])
46 -
47 - with pytest.raises(ValueError, match="at least one prompt"):
48 - parse_raw_prompts([[]])
49 -
50 -
51 -@pytest.mark.parametrize("string_input", STRING_INPUTS)
52 -def test_parse_raw_single_batch_string_consistent(string_input: str):
53 - assert parse_raw_prompts(string_input) == parse_raw_prompts([string_input])
54 -
55 -
56 -@pytest.mark.parametrize("token_input", TOKEN_INPUTS)
57 -def test_parse_raw_single_batch_token_consistent(token_input: list[int]):
58 - assert parse_raw_prompts(token_input) == parse_raw_prompts([token_input])
59 -
60 -
61 -@pytest.mark.parametrize("inputs_slice", INPUTS_SLICES)
62 -def test_parse_raw_single_batch_string_slice(inputs_slice: slice):
63 - assert parse_raw_prompts(STRING_INPUTS)[inputs_slice] == parse_raw_prompts(
64 - STRING_INPUTS[inputs_slice]
65 - )
66 -
12
13 @pytest.mark.parametrize(
14 "mm_processor_kwargs,expected_mm_kwargs",
vllm/config/model.py
+2 -2
@@ -768,7 +768,7 @@ class ModelConfig:
768 )
769 self.tokenizer = object_storage_tokenizer.dir
770
771 - def _get_encoder_config(self):
771 + def _get_encoder_config(self) -> dict[str, Any] | None:
772 model = self.model
773 if is_remote_gguf(model):
774 model, _ = split_remote_gguf(model)
@@ -1918,7 +1918,7 @@ def _get_and_verify_max_len(
1918 disable_sliding_window: bool,
1919 sliding_window: int | None,
1920 spec_target_max_model_len: int | None = None,
1921 - encoder_config: Any | None = None,
1921 + encoder_config: dict[str, Any] | None = None,
1922 ) -> int:
1923 """Get and verify the model's maximum length."""
1924 (derived_max_model_len, max_len_key) = (
vllm/engine/protocol.py
+1 -6
@@ -72,14 +72,9 @@ class EngineClient(ABC):
72 lora_request: LoRARequest | None = None,
73 trace_headers: Mapping[str, str] | None = None,
74 priority: int = 0,
75 - truncate_prompt_tokens: int | None = None,
75 tokenization_kwargs: dict[str, Any] | None = None,
76 ) -> AsyncGenerator[PoolingRequestOutput, None]:
78 - """Generate outputs for a request from a pooling model.
79 -
80 - NOTE: truncate_prompt_tokens is deprecated in v0.14.
81 - TODO: Remove this argument in v0.15.
82 - """
77 + """Generate outputs for a request from a pooling model."""
78 ...
79
80 @abstractmethod
vllm/entrypoints/llm.py
+339 -199
@@ -2,8 +2,9 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 +import warnings
6 from collections.abc import Callable, Sequence
6 -from typing import TYPE_CHECKING, Any, cast
7 +from typing import TYPE_CHECKING, Any, TypeAlias, cast
8
9 import cloudpickle
10 import torch.nn as nn
@@ -46,15 +47,17 @@ from vllm.entrypoints.pooling.score.utils import (
47 compress_token_type_ids,
48 get_score_prompt,
49 )
49 -from vllm.entrypoints.utils import _validate_truncation_size, log_non_default_args
50 +from vllm.entrypoints.utils import log_non_default_args
51 from vllm.inputs import (
52 DataPrompt,
53 + EmbedsPrompt,
54 + ExplicitEncoderDecoderPrompt,
55 PromptType,
56 SingletonPrompt,
57 TextPrompt,
58 TokensPrompt,
59 )
57 -from vllm.inputs.parse import get_prompt_components
60 +from vllm.inputs.parse import get_prompt_components, is_explicit_encoder_decoder_prompt
61 from vllm.logger import init_logger
62 from vllm.lora.request import LoRARequest
63 from vllm.model_executor.layers.quantization import QuantizationMethods
@@ -67,6 +70,7 @@ from vllm.outputs import (
70 )
71 from vllm.platforms import current_platform
72 from vllm.pooling_params import PoolingParams
73 +from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
74 from vllm.sampling_params import BeamSearchParams, RequestOutputKind, SamplingParams
75 from vllm.tasks import PoolingTask
76 from vllm.tokenizers import TokenizerLike
@@ -74,7 +78,6 @@ from vllm.tokenizers.mistral import MistralTokenizer
78 from vllm.usage.usage_lib import UsageContext
79 from vllm.utils.collection_utils import as_iter, is_list_of
80 from vllm.utils.counter import Counter
77 -from vllm.v1.engine import EngineCoreRequest
81 from vllm.v1.engine.llm_engine import LLMEngine
82 from vllm.v1.sample.logits_processor import LogitsProcessor
83
@@ -85,6 +88,9 @@ logger = init_logger(__name__)
88
89 _R = TypeVar("_R", default=Any)
90
91 +EnginePrompt: TypeAlias = TextPrompt | TokensPrompt | EmbedsPrompt
92 +EngineEncDecPrompt: TypeAlias = ExplicitEncoderDecoderPrompt[EnginePrompt, EnginePrompt]
93 +
94
95 class LLM:
96 """An LLM for generating texts from given prompts and sampling parameters.
@@ -372,6 +378,7 @@ class LLM:
378 use_tqdm: bool | Callable[..., tqdm] = True,
379 lora_request: list[LoRARequest] | LoRARequest | None = None,
380 priority: list[int] | None = None,
381 + tokenization_kwargs: dict[str, Any] | None = None,
382 ) -> list[RequestOutput]:
383 """Generates the completions for the input prompts.
384
@@ -398,15 +405,11 @@ class LLM:
405 If provided, must be a list of integers matching the length
406 of `prompts`, where each priority value corresponds to the prompt
407 at the same index.
408 + tokenization_kwargs: Overrides for `tokenizer.encode`.
409
410 Returns:
411 A list of `RequestOutput` objects containing the
412 generated completions in the same order as the input prompts.
405 -
406 - Note:
407 - Using `prompts` and `prompt_token_ids` as keyword parameters is
408 - considered legacy and may be deprecated in the future. You should
409 - instead pass them via the `inputs` parameter.
413 """
414 model_config = self.model_config
415 runner_type = model_config.runner_type
@@ -418,17 +421,14 @@ class LLM:
421 )
422
423 if sampling_params is None:
421 - # Use default sampling params.
424 sampling_params = self.get_default_sampling_params()
425
424 - # Add any modality specific loras to the corresponding prompts
425 - lora_request = self._get_modality_specific_lora_reqs(prompts, lora_request)
426 -
426 self._validate_and_add_requests(
427 prompts=prompts,
428 params=sampling_params,
429 use_tqdm=use_tqdm,
431 - lora_request=lora_request,
430 + lora_request=self._get_modality_specific_lora_reqs(prompts, lora_request),
431 + tokenization_kwargs=tokenization_kwargs,
432 priority=priority,
433 )
434
@@ -771,65 +771,169 @@ class LLM:
771
772 return outputs
773
774 - def preprocess_chat(
774 + def _get_cmpl_tok_params(self, tokenization_kwargs: dict[str, Any] | None):
775 + model_config = self.model_config
776 + encoder_config = model_config.encoder_config or {}
777 +
778 + return TokenizeParams(
779 + max_total_tokens=model_config.max_model_len,
780 + do_lower_case=encoder_config.get("do_lower_case", False),
781 + # For Whisper, special tokens should be provided by the user based
782 + # on the task and language of their request. Also needed to avoid
783 + # appending an EOS token to the prompt which disrupts generation.
784 + add_special_tokens=not model_config.is_encoder_decoder,
785 + ).with_kwargs(tokenization_kwargs)
786 +
787 + def _normalize_prompts(
788 self,
776 - messages: list[ChatCompletionMessageParam]
789 + prompts: PromptType | Sequence[PromptType],
790 + ) -> list[EnginePrompt | EngineEncDecPrompt]:
791 + if isinstance(prompts, str):
792 + prompts = TextPrompt(prompt=prompts)
793 +
794 + return prompts if isinstance(prompts, Sequence) else [prompts] # type: ignore[return-value]
795 +
796 + def _preprocess_cmpl_singleton(
797 + self,
798 + prompt: SingletonPrompt,
799 + tok_params: TokenizeParams,
800 + *,
801 + tokenize: bool,
802 + ) -> EnginePrompt:
803 + renderer = self.llm_engine.renderer
804 +
805 + if not isinstance(prompt, dict):
806 + prompt = renderer.render_completion(prompt)
807 +
808 + return renderer.tokenize_prompt(prompt, tok_params) if tokenize else prompt
809 +
810 + def _preprocess_cmpl_enc_dec(
811 + self,
812 + prompt: ExplicitEncoderDecoderPrompt,
813 + tok_params: TokenizeParams,
814 + ) -> EngineEncDecPrompt:
815 + enc_prompt = prompt["encoder_prompt"]
816 + dec_prompt = prompt["decoder_prompt"]
817 +
818 + return EngineEncDecPrompt(
819 + encoder_prompt=self._preprocess_cmpl_singleton(
820 + enc_prompt,
821 + tok_params,
822 + # TODO: Move multi-modal processor into tokenization
823 + tokenize=not self.model_config.is_multimodal_model,
824 + ),
825 + decoder_prompt=(
826 + None
827 + if dec_prompt is None
828 + else self._preprocess_cmpl_singleton(
829 + dec_prompt,
830 + tok_params,
831 + # TODO: Move multi-modal processor into tokenization
832 + tokenize=not self.model_config.is_multimodal_model,
833 + )
834 + ),
835 + )
836 +
837 + def _preprocess_completion(
838 + self,
839 + prompts: PromptType | Sequence[PromptType],
840 + tokenization_kwargs: dict[str, Any] | None = None,
841 + ) -> list[EnginePrompt | EngineEncDecPrompt]:
842 + """
843 + Convert prompt inputs from LLM APIs (other than [LLM.chat][]) into
844 + a format that can be passed to `_add_request`.
845 +
846 + Refer to [LLM.generate][] for a complete description of the arguments.
847 +
848 + Returns:
849 + A list of `TokensPrompts` objects containing the tokenized prompt
850 + after chat template interpolation, and the raw multi-modal inputs.
851 + """
852 + tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
853 +
854 + engine_prompts = list[EnginePrompt | EngineEncDecPrompt]()
855 + for prompt in self._normalize_prompts(prompts):
856 + if is_explicit_encoder_decoder_prompt(prompt):
857 + engine_prompts.append(self._preprocess_cmpl_enc_dec(prompt, tok_params))
858 + else:
859 + # Some MM models have non-default `add_special_tokens`
860 + # TODO: Move multi-modal processor into tokenization
861 + engine_prompts.append(
862 + self._preprocess_cmpl_singleton(
863 + prompt,
864 + tok_params,
865 + tokenize=not self.model_config.is_multimodal_model,
866 + )
867 + )
868 +
869 + return engine_prompts
870 +
871 + def _normalize_conversations(
872 + self,
873 + conversations: list[ChatCompletionMessageParam]
874 + | list[list[ChatCompletionMessageParam]],
875 + ) -> list[list[ChatCompletionMessageParam]]:
876 + return conversations if is_list_of(conversations, list) else [conversations] # type: ignore[list-item,return-value]
877 +
878 + def _get_chat_tok_params(self, tokenization_kwargs: dict[str, Any] | None):
879 + model_config = self.model_config
880 + encoder_config = model_config.encoder_config or {}
881 +
882 + return TokenizeParams(
883 + max_total_tokens=model_config.max_model_len,
884 + do_lower_case=encoder_config.get("do_lower_case", False),
885 + add_special_tokens=False,
886 + ).with_kwargs(tokenization_kwargs)
887 +
888 + def _preprocess_chat(
889 + self,
890 + conversations: list[ChatCompletionMessageParam]
891 | list[list[ChatCompletionMessageParam]],
892 chat_template: str | None = None,
893 chat_template_content_format: ChatTemplateContentFormatOption = "auto",
894 + chat_template_kwargs: dict[str, Any] | None = None,
895 add_generation_prompt: bool = True,
896 continue_final_message: bool = False,
897 tools: list[dict[str, Any]] | None = None,
783 - chat_template_kwargs: dict[str, Any] | None = None,
898 + tokenization_kwargs: dict[str, Any] | None = None,
899 mm_processor_kwargs: dict[str, Any] | None = None,
785 - ) -> list[TextPrompt | TokensPrompt]:
900 + ) -> list[EnginePrompt]:
901 """
787 - Generate prompt for a chat conversation. The pre-processed
788 - prompt can then be used as input for the other LLM methods.
902 + Convert a list of conversations into prompts so that they can then
903 + be used as input for other LLM APIs.
904 +
905 + Refer to [LLM.chat][] for a complete description of the arguments.
906
790 - Refer to `chat` for a complete description of the arguments.
907 Returns:
792 - A list of `TokensPrompts` objects containing the tokenized
793 - prompt after chat template interpolation, and the
794 - pre-processed multi-modal inputs.
908 + A list of `TokensPrompts` objects containing the tokenized prompt
909 + after chat template interpolation, and the raw multi-modal inputs.
910 """
796 - list_of_messages: list[list[ChatCompletionMessageParam]]
797 -
798 - # Handle multi and single conversations
799 - if is_list_of(messages, list):
800 - # messages is list[list[...]]
801 - list_of_messages = cast(list[list[ChatCompletionMessageParam]], messages)
802 - else:
803 - # messages is list[...]
804 - list_of_messages = [cast(list[ChatCompletionMessageParam], messages)]
805 -
911 renderer = self.llm_engine.renderer
912
808 - chat_template_kwargs = {
809 - "chat_template": chat_template,
810 - "add_generation_prompt": add_generation_prompt,
811 - "continue_final_message": continue_final_message,
812 - "tools": tools,
813 - **(chat_template_kwargs or {}),
814 - }
815 -
816 - prompts = list[TextPrompt | TokensPrompt]()
817 -
818 - for msgs in list_of_messages:
819 - # NOTE: renderer.render_messages() currently doesn't
820 - # handle mm_processor_kwargs, since there is no implementation in
821 - # the chat message parsing for it.
822 - _, prompt = renderer.render_messages(
823 - msgs,
824 - chat_template_content_format=chat_template_content_format,
825 - **chat_template_kwargs,
826 - )
913 + chat_params = ChatParams(
914 + chat_template=chat_template,
915 + chat_template_content_format=chat_template_content_format,
916 + chat_template_kwargs=merge_kwargs(
917 + chat_template_kwargs,
918 + dict(
919 + add_generation_prompt=add_generation_prompt,
920 + continue_final_message=continue_final_message,
921 + tools=tools,
922 + tokenize=isinstance(renderer.tokenizer, MistralTokenizer),
923 + ),
924 + ),
925 + )
926 + tok_params = self._get_chat_tok_params(tokenization_kwargs)
927 +
928 + engine_prompts = list[EnginePrompt]()
929 + for conversation in self._normalize_conversations(conversations):
930 + _, in_prompt = renderer.render_messages(conversation, chat_params)
931 if mm_processor_kwargs is not None:
828 - prompt["mm_processor_kwargs"] = mm_processor_kwargs
932 + in_prompt["mm_processor_kwargs"] = mm_processor_kwargs
933
830 - prompts.append(prompt)
934 + engine_prompts.append(renderer.tokenize_prompt(in_prompt, tok_params))
935
832 - return prompts
936 + return engine_prompts
937
938 def chat(
939 self,
@@ -844,6 +948,7 @@ class LLM:
948 continue_final_message: bool = False,
949 tools: list[dict[str, Any]] | None = None,
950 chat_template_kwargs: dict[str, Any] | None = None,
951 + tokenization_kwargs: dict[str, Any] | None = None,
952 mm_processor_kwargs: dict[str, Any] | None = None,
953 ) -> list[RequestOutput]:
954 """
@@ -889,22 +994,22 @@ class LLM:
994 `True` if `add_generation_prompt` is also `True`.
995 chat_template_kwargs: Additional kwargs to pass to the chat
996 template.
892 - mm_processor_kwargs: Multimodal processor kwarg overrides for this
893 - chat request. Only used for offline requests.
997 + tokenization_kwargs: Overrides for `tokenizer.encode`.
998 + mm_processor_kwargs: Overrides for `processor.__call__`.
999
1000 Returns:
1001 A list of `RequestOutput` objects containing the generated
1002 responses in the same order as the input messages.
1003 """
899 -
900 - prompts = self.preprocess_chat(
901 - messages=messages,
1004 + prompts = self._preprocess_chat(
1005 + messages,
1006 chat_template=chat_template,
1007 chat_template_content_format=chat_template_content_format,
1008 + chat_template_kwargs=chat_template_kwargs,
1009 add_generation_prompt=add_generation_prompt,
1010 continue_final_message=continue_final_message,
1011 tools=tools,
907 - chat_template_kwargs=chat_template_kwargs,
1012 + tokenization_kwargs=tokenization_kwargs,
1013 mm_processor_kwargs=mm_processor_kwargs,
1014 )
1015
@@ -913,6 +1018,7 @@ class LLM:
1018 sampling_params=sampling_params,
1019 use_tqdm=use_tqdm,
1020 lora_request=lora_request,
1021 + tokenization_kwargs=tokenization_kwargs,
1022 )
1023
1024 def encode(
@@ -945,37 +1051,29 @@ class LLM:
1051 If `False`, no progress bar is created.
1052 lora_request: LoRA request to use for generation, if any.
1053 pooling_task: Override the pooling task to use.
948 - tokenization_kwargs: overrides tokenization_kwargs set in
949 - pooling_params
1054 + tokenization_kwargs: Overrides for `tokenizer.encode`.
1055
1056 Returns:
1057 A list of `PoolingRequestOutput` objects containing the
1058 pooled hidden states in the same order as the input prompts.
954 -
955 - Note:
956 - Using `prompts` and `prompt_token_ids` as keyword parameters is
957 - considered legacy and may be deprecated in the future. You should
958 - instead pass them via the `inputs` parameter.
1059 """
1060
961 - error_str = (
962 - "pooling_task required for `LLM.encode`\n"
963 - "Please use one of the more specific methods or set the "
964 - "pooling_task when using `LLM.encode`:\n"
965 - " - For embeddings, use `LLM.embed(...)` "
966 - 'or `pooling_task="embed"`.\n'
967 - " - For classification logits, use `LLM.classify(...)` "
968 - 'or `pooling_task="classify"`.\n'
969 - " - For similarity scores, use `LLM.score(...)`.\n"
970 - " - For rewards, use `LLM.reward(...)` "
971 - 'or `pooling_task="token_classify"`\n'
972 - " - For token classification, "
973 - 'use `pooling_task="token_classify"`\n'
974 - ' - For multi-vector retrieval, use `pooling_task="token_embed"`'
975 - )
976 -
1061 if pooling_task is None:
978 - raise ValueError(error_str)
1062 + raise ValueError(
1063 + "pooling_task required for `LLM.encode`\n"
1064 + "Please use one of the more specific methods or set the "
1065 + "pooling_task when using `LLM.encode`:\n"
1066 + " - For embeddings, use `LLM.embed(...)` "
1067 + 'or `pooling_task="embed"`.\n'
1068 + " - For classification logits, use `LLM.classify(...)` "
1069 + 'or `pooling_task="classify"`.\n'
1070 + " - For similarity scores, use `LLM.score(...)`.\n"
1071 + " - For rewards, use `LLM.reward(...)` "
1072 + 'or `pooling_task="token_classify"`\n'
1073 + " - For token classification, "
1074 + 'use `pooling_task="token_classify"`\n'
1075 + ' - For multi-vector retrieval, use `pooling_task="token_embed"`'
1076 + )
1077
1078 model_config = self.model_config
1079 runner_type = model_config.runner_type
@@ -986,6 +1084,20 @@ class LLM:
1084 "pooling model."
1085 )
1086
1087 + if truncate_prompt_tokens is not None:
1088 + warnings.warn(
1089 + "The `truncate_prompt_tokens` parameter in `LLM.encode()` "
1090 + "is deprecated and will be removed in v0.16. "
1091 + "Please pass it via `tokenization_kwargs` instead.",
1092 + DeprecationWarning,
1093 + stacklevel=2,
1094 + )
1095 +
1096 + tokenization_kwargs = merge_kwargs(
1097 + tokenization_kwargs,
1098 + dict(truncate_prompt_tokens=truncate_prompt_tokens),
1099 + )
1100 +
1101 io_processor_prompt = False
1102 if isinstance(prompts, dict) and "data" in prompts:
1103 io_processor_prompt = True
@@ -1017,19 +1129,16 @@ class LLM:
1129 pooling_params = self.io_processor.validate_or_generate_params(
1130 pooling_params
1131 )
1020 - else:
1021 - if pooling_params is None:
1022 - # Use default pooling params.
1023 - pooling_params = PoolingParams()
1132 +
1133 + if pooling_params is None:
1134 + # Use default pooling params.
1135 + pooling_params = PoolingParams()
1136
1137 if pooling_task not in self.supported_tasks:
1138 raise ValueError(f"pooling_task must be one of {self.supported_tasks}.")
1139
1140 for param in as_iter(pooling_params):
1141 param.verify(pooling_task, model_config)
1030 - # for backwards compatibility
1031 - if truncate_prompt_tokens is not None:
1032 - param.truncate_prompt_tokens = truncate_prompt_tokens
1142
1143 self._validate_and_add_requests(
1144 prompts=prompts,
@@ -1094,6 +1203,7 @@ class LLM:
1203 it is used to create the progress bar.
1204 If `False`, no progress bar is created.
1205 lora_request: LoRA request to use for generation, if any.
1206 + tokenization_kwargs: Overrides for `tokenizer.encode`.
1207
1208 Returns:
1209 A list of `EmbeddingRequestOutput` objects containing the
@@ -1105,9 +1215,14 @@ class LLM:
1215 "Try converting the model using `--convert embed`."
1216 )
1217
1218 + if truncate_prompt_tokens is not None:
1219 + tokenization_kwargs = merge_kwargs(
1220 + tokenization_kwargs,
1221 + dict(truncate_prompt_tokens=truncate_prompt_tokens),
1222 + )
1223 +
1224 items = self.encode(
1225 prompts,
1110 - truncate_prompt_tokens=truncate_prompt_tokens,
1226 use_tqdm=use_tqdm,
1227 pooling_params=pooling_params,
1228 lora_request=lora_request,
@@ -1121,8 +1236,8 @@ class LLM:
1236 self,
1237 prompts: PromptType | Sequence[PromptType],
1238 *,
1124 - use_tqdm: bool | Callable[..., tqdm] = True,
1239 pooling_params: PoolingParams | Sequence[PoolingParams] | None = None,
1240 + use_tqdm: bool | Callable[..., tqdm] = True,
1241 lora_request: list[LoRARequest] | LoRARequest | None = None,
1242 tokenization_kwargs: dict[str, Any] | None = None,
1243 ) -> list[ClassificationRequestOutput]:
@@ -1137,13 +1252,15 @@ class LLM:
1252 prompts: The prompts to the LLM. You may pass a sequence of prompts
1253 for batch inference. See [PromptType][vllm.inputs.PromptType]
1254 for more details about the format of each prompt.
1255 + pooling_params: The pooling parameters for pooling. If None, we
1256 + use the default pooling parameters.
1257 use_tqdm: If `True`, shows a tqdm progress bar.
1258 If a callable (e.g., `functools.partial(tqdm, leave=False)`),
1259 it is used to create the progress bar.
1260 If `False`, no progress bar is created.
1261 lora_request: LoRA request to use for generation, if any.
1145 - pooling_params: The pooling parameters for pooling. If None, we
1146 - use the default pooling parameters.
1262 + tokenization_kwargs: Overrides for `tokenizer.encode`.
1263 +
1264 Returns:
1265 A list of `ClassificationRequestOutput` objects containing the
1266 embedding vectors in the same order as the input prompts.
@@ -1170,9 +1287,9 @@ class LLM:
1287 prompts: PromptType | Sequence[PromptType],
1288 /,
1289 *,
1290 + pooling_params: PoolingParams | Sequence[PoolingParams] | None = None,
1291 truncate_prompt_tokens: int | None = None,
1292 use_tqdm: bool | Callable[..., tqdm] = True,
1175 - pooling_params: PoolingParams | Sequence[PoolingParams] | None = None,
1293 lora_request: list[LoRARequest] | LoRARequest | None = None,
1294 tokenization_kwargs: dict[str, Any] | None = None,
1295 ) -> list[PoolingRequestOutput]:
@@ -1183,13 +1300,15 @@ class LLM:
1300 prompts: The prompts to the LLM. You may pass a sequence of prompts
1301 for batch inference. See [PromptType][vllm.inputs.PromptType]
1302 for more details about the format of each prompt.
1303 + pooling_params: The pooling parameters for pooling. If None, we
1304 + use the default pooling parameters.
1305 use_tqdm: If `True`, shows a tqdm progress bar.
1306 If a callable (e.g., `functools.partial(tqdm, leave=False)`),
1307 it is used to create the progress bar.
1308 If `False`, no progress bar is created.
1309 lora_request: LoRA request to use for generation, if any.
1191 - pooling_params: The pooling parameters for pooling. If None, we
1192 - use the default pooling parameters.
1310 + tokenization_kwargs: Overrides for `tokenizer.encode`.
1311 +
1312 Returns:
1313 A list of `PoolingRequestOutput` objects containing the
1314 pooled hidden states in the same order as the input prompts.
@@ -1207,18 +1326,18 @@ class LLM:
1326
1327 def _embedding_score(
1328 self,
1210 - tokenizer: TokenizerLike,
1211 - text_1: list[str | TextPrompt | TokensPrompt],
1212 - text_2: list[str | TextPrompt | TokensPrompt],
1213 - truncate_prompt_tokens: int | None = None,
1214 - use_tqdm: bool | Callable[..., tqdm] = True,
1215 - pooling_params: PoolingParams | None = None,
1216 - lora_request: list[LoRARequest] | LoRARequest | None = None,
1217 - tokenization_kwargs: dict[str, Any] | None = None,
1329 + text_1: list[SingletonPrompt],
1330 + text_2: list[SingletonPrompt],
1331 + *,
1332 + use_tqdm: bool | Callable[..., tqdm],
1333 + pooling_params: PoolingParams | None,
1334 + lora_request: list[LoRARequest] | LoRARequest | None,
1335 + tokenization_kwargs: dict[str, Any],
1336 ) -> list[ScoringRequestOutput]:
1219 - encoded_output: list[PoolingRequestOutput] = self.encode(
1337 + tokenizer = self.get_tokenizer()
1338 +
1339 + encoded_output = self.encode(
1340 text_1 + text_2,
1221 - truncate_prompt_tokens=truncate_prompt_tokens,
1341 use_tqdm=use_tqdm,
1342 lora_request=lora_request,
1343 pooling_params=pooling_params,
@@ -1226,14 +1345,16 @@ class LLM:
1345 tokenization_kwargs=tokenization_kwargs,
1346 )
1347
1229 - encoded_output_1: list[PoolingRequestOutput] = encoded_output[0 : len(text_1)]
1230 - encoded_output_2: list[PoolingRequestOutput] = encoded_output[len(text_1) :]
1348 + encoded_output_1 = encoded_output[0 : len(text_1)]
1349 + encoded_output_2 = encoded_output[len(text_1) :]
1350
1351 if len(encoded_output_1) == 1:
1352 encoded_output_1 = encoded_output_1 * len(encoded_output_2)
1353
1354 scores = _cosine_similarity(
1236 - tokenizer=tokenizer, embed_1=encoded_output_1, embed_2=encoded_output_2
1355 + tokenizer=tokenizer,
1356 + embed_1=encoded_output_1,
1357 + embed_2=encoded_output_2,
1358 )
1359
1360 items = self.engine_class.validate_outputs(scores, PoolingRequestOutput)
@@ -1241,17 +1362,17 @@ class LLM:
1362
1363 def _cross_encoding_score(
1364 self,
1244 - tokenizer: TokenizerLike,
1365 data_1: list[str] | list[ScoreContentPartParam],
1366 data_2: list[str] | list[ScoreContentPartParam],
1247 - truncate_prompt_tokens: int | None = None,
1248 - use_tqdm: bool | Callable[..., tqdm] = True,
1249 - pooling_params: PoolingParams | None = None,
1250 - lora_request: list[LoRARequest] | LoRARequest | None = None,
1251 - tokenization_kwargs: dict[str, Any] | None = None,
1252 - score_template: str | None = None,
1367 + *,
1368 + use_tqdm: bool | Callable[..., tqdm],
1369 + pooling_params: PoolingParams | None,
1370 + lora_request: list[LoRARequest] | LoRARequest | None,
1371 + tokenization_kwargs: dict[str, Any],
1372 + score_template: str | None,
1373 ) -> list[ScoringRequestOutput]:
1374 model_config = self.model_config
1375 + tokenizer = self.get_tokenizer()
1376
1377 if isinstance(tokenizer, MistralTokenizer):
1378 raise ValueError("Score API is not supported for Mistral tokenizer")
@@ -1265,13 +1386,6 @@ class LLM:
1386 pooling_params.verify("score", model_config)
1387 pooling_params_list = list[PoolingParams]()
1388
1268 - local_kwargs = tokenization_kwargs or {}
1269 - tokenization_kwargs = local_kwargs.copy()
1270 -
1271 - _validate_truncation_size(
1272 - model_config.max_model_len, truncate_prompt_tokens, tokenization_kwargs
1273 - )
1274 -
1389 prompts = list[PromptType]()
1390
1391 input_pairs = [(t1, t2) for t1, t2 in zip(data_1, data_2)]
@@ -1314,10 +1428,10 @@ class LLM:
1428 data_2: SingletonPrompt | Sequence[SingletonPrompt] | ScoreMultiModalParam,
1429 /,
1430 *,
1317 - truncate_prompt_tokens: int | None = None,
1431 use_tqdm: bool | Callable[..., tqdm] = True,
1432 pooling_params: PoolingParams | None = None,
1433 lora_request: list[LoRARequest] | LoRARequest | None = None,
1434 + tokenization_kwargs: dict[str, Any] | None = None,
1435 chat_template: str | None = None,
1436 ) -> list[ScoringRequestOutput]:
1437 """Generate similarity scores for all pairs `<text,text_pair>` or
@@ -1344,20 +1458,22 @@ class LLM:
1458 the LLM. Can be text or multi-modal data. See [PromptType]
1459 [vllm.inputs.PromptType] for more details about the format of
1460 each prompt.
1461 + pooling_params: The pooling parameters for pooling. If None, we
1462 + use the default pooling parameters.
1463 use_tqdm: If `True`, shows a tqdm progress bar.
1464 If a callable (e.g., `functools.partial(tqdm, leave=False)`),
1465 it is used to create the progress bar.
1466 If `False`, no progress bar is created.
1467 lora_request: LoRA request to use for generation, if any.
1352 - pooling_params: The pooling parameters for pooling. If None, we
1353 - use the default pooling parameters.
1468 chat_template: The chat template to use for the scoring. If None, we
1469 use the model's default chat template.
1470 + tokenization_kwargs: Overrides for `tokenizer.encode`.
1471 Returns:
1472 A list of `ScoringRequestOutput` objects containing the
1473 generated scores in the same order as the input prompts.
1474 """
1475 model_config = self.model_config
1476 +
1477 runner_type = model_config.runner_type
1478 if runner_type != "pooling":
1479 raise ValueError(
@@ -1445,26 +1561,27 @@ class LLM:
1561
1562 _validate_score_input_lens(data_1, data_2) # type: ignore[arg-type]
1563
1564 + tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
1565 + encode_kwargs = tok_params.get_encode_kwargs()
1566 +
1567 if model_config.is_cross_encoder:
1568 return self._cross_encoding_score(
1450 - tokenizer,
1569 data_1, # type: ignore[arg-type]
1570 data_2, # type: ignore[arg-type]
1453 - truncate_prompt_tokens,
1454 - use_tqdm,
1455 - pooling_params,
1456 - lora_request,
1571 + use_tqdm=use_tqdm,
1572 + pooling_params=pooling_params,
1573 + lora_request=lora_request,
1574 + tokenization_kwargs=encode_kwargs,
1575 score_template=chat_template,
1576 )
1577 else:
1578 return self._embedding_score(
1461 - tokenizer,
1579 data_1, # type: ignore[arg-type]
1580 data_2, # type: ignore[arg-type]
1464 - truncate_prompt_tokens,
1465 - use_tqdm,
1466 - pooling_params,
1467 - lora_request,
1581 + use_tqdm=use_tqdm,
1582 + pooling_params=pooling_params,
1583 + lora_request=lora_request,
1584 + tokenization_kwargs=encode_kwargs,
1585 )
1586
1587 def start_profile(self) -> None:
@@ -1530,42 +1647,79 @@ class LLM:
1647
1648 def _validate_and_add_requests(
1649 self,
1533 - prompts: PromptType | Sequence[PromptType] | DataPrompt,
1650 + prompts: PromptType | Sequence[PromptType],
1651 params: SamplingParams
1652 | Sequence[SamplingParams]
1653 | PoolingParams
1654 | Sequence[PoolingParams],
1655 *,
1656 use_tqdm: bool | Callable[..., tqdm] = True,
1540 - lora_request: Sequence[LoRARequest] | LoRARequest | None,
1541 - priority: list[int] | None = None,
1657 + lora_request: Sequence[LoRARequest | None] | LoRARequest | None,
1658 tokenization_kwargs: dict[str, Any] | None = None,
1659 + priority: list[int] | None = None,
1660 ) -> None:
1544 - if isinstance(prompts, (str, dict)):
1545 - # Convert a single prompt to a list.
1546 - prompts = [prompts] # type: ignore[list-item]
1547 -
1548 - num_requests = len(prompts)
1549 - if isinstance(params, Sequence) and len(params) != num_requests:
1550 - raise ValueError("The lengths of prompts and params must be the same.")
1551 - if isinstance(lora_request, Sequence) and len(lora_request) != num_requests:
1552 - raise ValueError(
1553 - "The lengths of prompts and lora_request must be the same."
1554 - )
1555 - if priority is not None and len(priority) != num_requests:
1556 - raise ValueError(
1557 - "The lengths of prompts "
1558 - f"({num_requests}) and priority ({len(priority)}) "
1559 - "must be the same."
1661 + in_prompts = self._normalize_prompts(prompts)
1662 + num_requests = len(in_prompts)
1663 +
1664 + if isinstance(params, Sequence):
1665 + if len(params) != num_requests:
1666 + raise ValueError(
1667 + f"The lengths of prompts ({params}) "
1668 + f"and lora_request ({len(params)}) must be the same."
1669 + )
1670 +
1671 + engine_params = params
1672 + else:
1673 + engine_params = [params] * num_requests
1674 +
1675 + if isinstance(lora_request, Sequence):
1676 + if len(lora_request) != num_requests:
1677 + raise ValueError(
1678 + f"The lengths of prompts ({num_requests}) "
1679 + f"and lora_request ({len(lora_request)}) must be the same."
1680 + )
1681 +
1682 + engine_lora_requests: Sequence[LoRARequest | None] = lora_request
1683 + else:
1684 + engine_lora_requests = [lora_request] * num_requests
1685 +
1686 + if priority is not None:
1687 + if len(priority) != num_requests:
1688 + raise ValueError(
1689 + f"The lengths of prompts ({num_requests}) "
1690 + f"and priority ({len(priority)}) must be the same."
1691 + )
1692 + else:
1693 + priority = [0] * num_requests
1694 +
1695 + if any(param.truncate_prompt_tokens is not None for param in engine_params):
1696 + # TODO: Remove this after deprecating `param.truncate_prompt_tokens`
1697 + # Then, move the code from the `else` block to the top and let
1698 + # `self._preprocess_completion` handle prompt normalization
1699 + engine_prompts = [
1700 + engine_prompt
1701 + for in_prompt, param in zip(in_prompts, engine_params)
1702 + for engine_prompt in self._preprocess_completion(
1703 + [in_prompt],
1704 + tokenization_kwargs=merge_kwargs(
1705 + tokenization_kwargs,
1706 + dict(truncate_prompt_tokens=param.truncate_prompt_tokens),
1707 + ),
1708 + )
1709 + ]
1710 + else:
1711 + engine_prompts = self._preprocess_completion(
1712 + in_prompts,
1713 + tokenization_kwargs=tokenization_kwargs,
1714 )
1715
1562 - for sp in params if isinstance(params, Sequence) else (params,):
1716 + for sp in engine_params:
1717 if isinstance(sp, SamplingParams):
1718 # We only care about the final output
1719 sp.output_kind = RequestOutputKind.FINAL_ONLY
1720
1721 # Add requests to the engine.
1568 - it = prompts
1722 + it = engine_prompts
1723 if use_tqdm:
1724 tqdm_func = use_tqdm if callable(use_tqdm) else tqdm
1725 it = tqdm_func(it, desc="Adding requests")
@@ -1576,12 +1730,10 @@ class LLM:
1730 for i, prompt in enumerate(it):
1731 request_id = self._add_request(
1732 prompt,
1579 - params[i] if isinstance(params, Sequence) else params,
1580 - lora_request=lora_request[i]
1581 - if isinstance(lora_request, Sequence)
1582 - else lora_request,
1583 - priority=priority[i] if priority else 0,
1733 + engine_params[i],
1734 + lora_request=engine_lora_requests[i],
1735 tokenization_kwargs=tokenization_kwargs,
1736 + priority=priority[i],
1737 )
1738 added_request_ids.append(request_id)
1739 except Exception as e:
@@ -1589,54 +1741,42 @@ class LLM:
1741 self.llm_engine.abort_request(added_request_ids, internal=True)
1742 raise e
1743
1592 - def _process_inputs(
1593 - self,
1594 - request_id: str,
1595 - engine_prompt: PromptType,
1596 - params: SamplingParams | PoolingParams,
1597 - *,
1598 - lora_request: LoRARequest | None,
1599 - priority: int,
1600 - tokenization_kwargs: dict[str, Any] | None = None,
1601 - ) -> tuple[EngineCoreRequest, dict[str, Any]]:
1602 - """Use the Processor to process inputs for LLMEngine."""
1603 -
1604 - local_kwargs = tokenization_kwargs or {}
1605 - tokenization_kwargs = local_kwargs.copy()
1606 - _validate_truncation_size(
1607 - self.model_config.max_model_len,
1608 - params.truncate_prompt_tokens,
1609 - tokenization_kwargs,
1610 - )
1611 -
1612 - engine_request = self.input_processor.process_inputs(
1613 - request_id,
1614 - engine_prompt,
1615 - params,
1616 - lora_request=lora_request,
1617 - tokenization_kwargs=tokenization_kwargs,
1618 - priority=priority,
1619 - )
1620 - return engine_request, tokenization_kwargs
1621 -
1744 def _add_request(
1745 self,
1746 prompt: PromptType,
1747 params: SamplingParams | PoolingParams,
1748 lora_request: LoRARequest | None = None,
1627 - priority: int = 0,
1749 tokenization_kwargs: dict[str, Any] | None = None,
1750 + priority: int = 0,
1751 ) -> str:
1752 prompt_text, _, _ = get_prompt_components(prompt)
1753 request_id = str(next(self.request_counter))
1754
1633 - engine_request, tokenization_kwargs = self._process_inputs(
1755 + if params.truncate_prompt_tokens is not None:
1756 + params_type = type(params).__name__
1757 + warnings.warn(
1758 + f"The `truncate_prompt_tokens` parameter in `{params_type}` "
1759 + "is deprecated and will be removed in v0.16. "
1760 + "Please pass it via `tokenization_kwargs` instead.",
1761 + DeprecationWarning,
1762 + stacklevel=2,
1763 + )
1764 +
1765 + tokenization_kwargs = merge_kwargs(
1766 + tokenization_kwargs,
1767 + dict(truncate_prompt_tokens=params.truncate_prompt_tokens),
1768 + )
1769 +
1770 + tok_params = self._get_cmpl_tok_params(tokenization_kwargs)
1771 +
1772 + tokenization_kwargs = tok_params.get_encode_kwargs()
1773 + engine_request = self.input_processor.process_inputs(
1774 request_id,
1775 prompt,
1776 params,
1777 lora_request=lora_request,
1638 - priority=priority,
1778 tokenization_kwargs=tokenization_kwargs,
1779 + priority=priority,
1780 )
1781
1782 self.llm_engine.add_request(
vllm/entrypoints/openai/chat_completion/protocol.py
+44 -5
@@ -13,12 +13,13 @@ from openai.types.chat.chat_completion_audio import (
13 ChatCompletionAudio as OpenAIChatCompletionAudio,
14 )
15 from openai.types.chat.chat_completion_message import Annotation as OpenAIAnnotation
16 -from pydantic import (
17 - Field,
18 - model_validator,
19 -)
16 +from pydantic import Field, model_validator
17
21 -from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
18 +from vllm.config import ModelConfig
19 +from vllm.entrypoints.chat_utils import (
20 + ChatCompletionMessageParam,
21 + ChatTemplateContentFormatOption,
22 +)
23 from vllm.entrypoints.openai.engine.protocol import (
24 AnyResponseFormat,
25 DeltaMessage,
@@ -36,6 +37,7 @@ from vllm.entrypoints.openai.engine.protocol import (
37 from vllm.exceptions import VLLMValidationError
38 from vllm.logger import init_logger
39 from vllm.logprobs import Logprob
40 +from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
41 from vllm.sampling_params import (
42 BeamSearchParams,
43 RequestOutputKind,
@@ -348,6 +350,43 @@ class ChatCompletionRequest(OpenAIBaseModel):
350
351 # --8<-- [end:chat-completion-extra-params]
352
353 + def build_chat_params(
354 + self,
355 + default_template: str | None,
356 + default_template_content_format: ChatTemplateContentFormatOption,
357 + ) -> ChatParams:
358 + return ChatParams(
359 + chat_template=self.chat_template or default_template,
360 + chat_template_content_format=default_template_content_format,
361 + chat_template_kwargs=merge_kwargs(
362 + self.chat_template_kwargs,
363 + dict(
364 + add_generation_prompt=self.add_generation_prompt,
365 + continue_final_message=self.continue_final_message,
366 + documents=self.documents,
367 + reasoning_effort=self.reasoning_effort,
368 + ),
369 + ),
370 + )
371 +
372 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
373 + if self.max_completion_tokens is not None:
374 + max_output_tokens: int | None = self.max_completion_tokens
375 + max_output_tokens_param = "max_completion_tokens"
376 + else:
377 + max_output_tokens = self.max_tokens
378 + max_output_tokens_param = "max_tokens"
379 +
380 + return TokenizeParams(
381 + max_total_tokens=model_config.max_model_len,
382 + max_output_tokens=max_output_tokens or 0,
383 + truncate_prompt_tokens=self.truncate_prompt_tokens,
384 + add_special_tokens=self.add_special_tokens,
385 + needs_detokenization=bool(self.echo and not self.return_token_ids),
386 + max_total_tokens_param="max_model_len",
387 + max_output_tokens_param=max_output_tokens_param,
388 + )
389 +
390 # Default sampling parameters for chat completion requests
391 _DEFAULT_SAMPLING_PARAMS: dict = {
392 "repetition_penalty": 1.0,
vllm/entrypoints/openai/chat_completion/serving.py
+16 -28
@@ -67,7 +67,7 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
67 )
68 from vllm.entrypoints.openai.utils import maybe_filter_parallel_tool_calls
69 from vllm.entrypoints.utils import get_max_tokens, should_include_usage
70 -from vllm.inputs.data import TokensPrompt
70 +from vllm.inputs.data import EmbedsPrompt, TokensPrompt
71 from vllm.inputs.parse import get_prompt_components
72 from vllm.logger import init_logger
73 from vllm.logprobs import Logprob
@@ -185,8 +185,6 @@ class OpenAIServingChat(OpenAIServing):
185 start_time = time.perf_counter()
186
187 try:
188 - renderer = self.engine_client.renderer
189 -
188 # Create a minimal dummy request
189 dummy_request = ChatCompletionRequest(
190 messages=[{"role": "user", "content": "warmup"}],
@@ -201,18 +199,10 @@ class OpenAIServingChat(OpenAIServing):
199 # 3. Tokenizer initialization for chat
200 await self._preprocess_chat(
201 dummy_request,
204 - renderer,
202 dummy_request.messages,
206 - chat_template=self.chat_template,
207 - chat_template_content_format=self.chat_template_content_format,
208 - add_generation_prompt=True,
209 - continue_final_message=False,
210 - tool_dicts=None,
211 - documents=None,
212 - chat_template_kwargs=None,
213 - default_chat_template_kwargs=self.default_chat_template_kwargs,
214 - tool_parser=None,
215 - add_special_tokens=False,
203 + default_template=self.chat_template,
204 + default_template_content_format=self.chat_template_content_format,
205 + default_template_kwargs=self.default_chat_template_kwargs,
206 )
207
208 elapsed = (time.perf_counter() - start_time) * 1000
@@ -225,7 +215,10 @@ class OpenAIServingChat(OpenAIServing):
215 async def render_chat_request(
216 self,
217 request: ChatCompletionRequest,
228 - ) -> tuple[list[ConversationMessage], list[Any]] | ErrorResponse:
218 + ) -> (
219 + tuple[list[ConversationMessage], list[TokensPrompt | EmbedsPrompt]]
220 + | ErrorResponse
221 + ):
222 """
223 render chat request by validating and preprocessing inputs.
224
@@ -302,23 +295,14 @@ class OpenAIServingChat(OpenAIServing):
295 if error_check_ret is not None:
296 return error_check_ret
297
305 - chat_template_kwargs = request.chat_template_kwargs or {}
306 - chat_template_kwargs.update(reasoning_effort=request.reasoning_effort)
307 -
298 conversation, engine_prompts = await self._preprocess_chat(
299 request,
310 - renderer,
300 request.messages,
312 - chat_template=request.chat_template or self.chat_template,
313 - chat_template_content_format=self.chat_template_content_format,
314 - add_generation_prompt=request.add_generation_prompt,
315 - continue_final_message=request.continue_final_message,
301 + default_template=self.chat_template,
302 + default_template_content_format=self.chat_template_content_format,
303 + default_template_kwargs=self.default_chat_template_kwargs,
304 tool_dicts=tool_dicts,
317 - documents=request.documents,
318 - chat_template_kwargs=chat_template_kwargs,
319 - default_chat_template_kwargs=self.default_chat_template_kwargs,
305 tool_parser=tool_parser,
321 - add_special_tokens=request.add_special_tokens,
306 )
307 else:
308 # For GPT-OSS.
@@ -428,11 +412,15 @@ class OpenAIServingChat(OpenAIServing):
412 trace_headers=trace_headers,
413 )
414 else:
431 - engine_request, tokenization_kwargs = await self._process_inputs(
415 + tok_params = request.build_tok_params(self.model_config)
416 + tokenization_kwargs = tok_params.get_encode_kwargs()
417 +
418 + engine_request = self.input_processor.process_inputs(
419 sub_request_id,
420 engine_prompt,
421 sampling_params,
422 lora_request=lora_request,
423 + tokenization_kwargs=tokenization_kwargs,
424 trace_headers=trace_headers,
425 priority=request.priority,
426 data_parallel_rank=data_parallel_rank,
vllm/entrypoints/openai/completion/protocol.py
+14 -4
@@ -9,11 +9,9 @@ from dataclasses import replace
9 from typing import Annotated, Any, Literal
10
11 import torch
12 -from pydantic import (
13 - Field,
14 - model_validator,
15 -)
12 +from pydantic import Field, model_validator
13
14 +from vllm.config import ModelConfig
15 from vllm.entrypoints.openai.engine.protocol import (
16 AnyResponseFormat,
17 LegacyStructuralTagResponseFormat,
@@ -27,6 +25,7 @@ from vllm.entrypoints.openai.engine.protocol import (
25 from vllm.exceptions import VLLMValidationError
26 from vllm.logger import init_logger
27 from vllm.logprobs import Logprob
28 +from vllm.renderers import TokenizeParams
29 from vllm.sampling_params import (
30 BeamSearchParams,
31 RequestOutputKind,
@@ -178,6 +177,17 @@ class CompletionRequest(OpenAIBaseModel):
177
178 # --8<-- [end:completion-extra-params]
179
180 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
181 + return TokenizeParams(
182 + max_total_tokens=model_config.max_model_len,
183 + max_output_tokens=self.max_tokens or 0,
184 + truncate_prompt_tokens=self.truncate_prompt_tokens,
185 + add_special_tokens=self.add_special_tokens,
186 + needs_detokenization=bool(self.echo and not self.return_token_ids),
187 + max_total_tokens_param="max_model_len",
188 + max_output_tokens_param="max_tokens",
189 + )
190 +
191 # Default sampling parameters for completion requests
192 _DEFAULT_SAMPLING_PARAMS: dict = {
193 "repetition_penalty": 1.0,
vllm/entrypoints/openai/completion/serving.py
+8 -33
@@ -32,7 +32,6 @@ from vllm.entrypoints.openai.engine.serving import (
32 clamp_prompt_logprobs,
33 )
34 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
35 -from vllm.entrypoints.renderer import RenderConfig
35 from vllm.entrypoints.utils import get_max_tokens, should_include_usage
36 from vllm.exceptions import VLLMValidationError
37 from vllm.inputs.data import EmbedsPrompt, TokensPrompt, is_embeds_prompt
@@ -111,11 +110,10 @@ class OpenAIServingCompletion(OpenAIServing):
110 )
111
112 try:
114 - renderer = self._get_completion_renderer()
115 - engine_prompts = await renderer.render_prompt_and_embeds(
116 - prompt_or_prompts=request.prompt,
113 + engine_prompts = await self._preprocess_completion(
114 + request,
115 + prompt_input=request.prompt,
116 prompt_embeds=request.prompt_embeds,
118 - config=self._build_render_config(request),
117 )
118 except (ValueError, TypeError, RuntimeError, jinja2.TemplateError) as e:
119 logger.exception("Error in preprocessing prompt inputs")
@@ -203,10 +201,6 @@ class OpenAIServingCompletion(OpenAIServing):
201 else await self._get_trace_headers(raw_request.headers)
202 )
203
206 - # Mypy inconsistently requires this second cast in different
207 - # environments. It shouldn't be necessary (redundant from above)
208 - # but pre-commit in CI fails without it.
209 - engine_prompt = cast(EmbedsPrompt | TokensPrompt, engine_prompt)
204 if isinstance(sampling_params, BeamSearchParams):
205 generator = self.beam_search(
206 prompt=engine_prompt,
@@ -216,11 +210,15 @@ class OpenAIServingCompletion(OpenAIServing):
210 trace_headers=trace_headers,
211 )
212 else:
219 - engine_request, tokenization_kwargs = await self._process_inputs(
213 + tok_params = request.build_tok_params(self.model_config)
214 + tokenization_kwargs = tok_params.get_encode_kwargs()
215 +
216 + engine_request = self.input_processor.process_inputs(
217 request_id_item,
218 engine_prompt,
219 sampling_params,
220 lora_request=lora_request,
221 + tokenization_kwargs=tokenization_kwargs,
222 trace_headers=trace_headers,
223 priority=request.priority,
224 data_parallel_rank=data_parallel_rank,
@@ -709,26 +707,3 @@ class OpenAIServingCompletion(OpenAIServing):
707 tokens=out_tokens,
708 top_logprobs=out_top_logprobs,
709 )
712 -
713 - def _build_render_config(
714 - self,
715 - request: CompletionRequest,
716 - max_input_length: int | None = None,
717 - ) -> RenderConfig:
718 - # Validate max_tokens before using it
719 - if request.max_tokens is not None and request.max_tokens > self.max_model_len:
720 - raise VLLMValidationError(
721 - f"'max_tokens' ({request.max_tokens}) cannot be greater than "
722 - f"the model's maximum context length ({self.max_model_len}).",
723 - parameter="max_tokens",
724 - value=request.max_tokens,
725 - )
726 -
727 - max_input_tokens_len = self.max_model_len - (request.max_tokens or 0)
728 - return RenderConfig(
729 - max_length=max_input_tokens_len,
730 - truncate_prompt_tokens=request.truncate_prompt_tokens,
731 - add_special_tokens=request.add_special_tokens,
732 - cache_salt=request.cache_salt,
733 - needs_detokenization=bool(request.echo and not request.return_token_ids),
734 - )
vllm/entrypoints/openai/engine/protocol.py
+1 -3
@@ -16,9 +16,7 @@ from pydantic import (
16
17 from vllm.entrypoints.chat_utils import make_tool_call_id
18 from vllm.logger import init_logger
19 -from vllm.sampling_params import (
20 - SamplingParams,
21 -)
19 +from vllm.sampling_params import SamplingParams
20 from vllm.utils import random_uuid
21 from vllm.utils.import_utils import resolve_obj_by_qualname
22
vllm/entrypoints/openai/engine/serving.py
+99 -267
@@ -5,10 +5,10 @@ import json
5 import sys
6 import time
7 import traceback
8 -from collections.abc import AsyncGenerator, Callable, Iterable, Mapping
8 +from collections.abc import AsyncGenerator, Callable, Mapping
9 from dataclasses import dataclass, field
10 from http import HTTPStatus
11 -from typing import Any, ClassVar, Generic, TypeAlias, TypeVar, cast
11 +from typing import Any, ClassVar, Generic, Protocol, TypeAlias, TypeVar
12
13 import numpy as np
14 from fastapi import Request
@@ -20,6 +20,7 @@ from starlette.datastructures import Headers
20
21 import vllm.envs as envs
22 from vllm.beam_search import BeamSearchSequence, create_sort_beams_key_function
23 +from vllm.config import ModelConfig
24 from vllm.engine.protocol import EngineClient
25 from vllm.entrypoints.chat_utils import (
26 ChatCompletionMessageParam,
@@ -86,7 +87,6 @@ from vllm.entrypoints.pooling.score.protocol import (
87 ScoreResponse,
88 ScoreTextRequest,
89 )
89 -from vllm.entrypoints.renderer import BaseRenderer, CompletionRenderer, RenderConfig
90 from vllm.entrypoints.serve.disagg.protocol import GenerateRequest, GenerateResponse
91 from vllm.entrypoints.serve.tokenize.protocol import (
92 DetokenizeRequest,
@@ -94,13 +94,9 @@ from vllm.entrypoints.serve.tokenize.protocol import (
94 TokenizeCompletionRequest,
95 TokenizeResponse,
96 )
97 -from vllm.entrypoints.utils import (
98 - _validate_truncation_size,
99 - get_max_tokens,
100 - sanitize_message,
101 -)
97 +from vllm.entrypoints.utils import get_max_tokens, sanitize_message
98 from vllm.exceptions import VLLMValidationError
103 -from vllm.inputs.data import PromptType, TokensPrompt
99 +from vllm.inputs.data import EmbedsPrompt, PromptType, TokensPrompt
100 from vllm.inputs.parse import (
101 get_prompt_components,
102 is_explicit_encoder_decoder_prompt,
@@ -112,7 +108,7 @@ from vllm.multimodal import MultiModalDataDict
108 from vllm.outputs import CompletionOutput, PoolingRequestOutput, RequestOutput
109 from vllm.pooling_params import PoolingParams
110 from vllm.reasoning import ReasoningParser, ReasoningParserManager
115 -from vllm.renderers import RendererLike
111 +from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
112 from vllm.sampling_params import BeamSearchParams, SamplingParams
113 from vllm.tokenizers import TokenizerLike
114 from vllm.tool_parsers import ToolParser, ToolParserManager
@@ -123,11 +119,9 @@ from vllm.tracing import (
119 )
120 from vllm.utils import random_uuid
121 from vllm.utils.async_utils import (
126 - AsyncMicrobatchTokenizer,
122 collect_from_async_generator,
123 merge_async_iterators,
124 )
130 -from vllm.v1.engine import EngineCoreRequest
125
126
127 class GenerationError(Exception):
@@ -140,6 +134,21 @@ class GenerationError(Exception):
134
135 logger = init_logger(__name__)
136
137 +
138 +class RendererRequest(Protocol):
139 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
140 + raise NotImplementedError
141 +
142 +
143 +class RendererChatRequest(RendererRequest, Protocol):
144 + def build_chat_params(
145 + self,
146 + default_template: str | None,
147 + default_template_content_format: ChatTemplateContentFormatOption,
148 + ) -> ChatParams:
149 + raise NotImplementedError
150 +
151 +
152 CompletionLikeRequest: TypeAlias = (
153 CompletionRequest
154 | TokenizeCompletionRequest
@@ -158,7 +167,9 @@ ChatLikeRequest: TypeAlias = (
167 | ClassificationChatRequest
168 | PoolingChatRequest
169 )
170 +
171 SpeechToTextRequest: TypeAlias = TranscriptionRequest | TranslationRequest
172 +
173 AnyRequest: TypeAlias = (
174 CompletionLikeRequest
175 | ChatLikeRequest
@@ -193,7 +204,7 @@ class ServeContext(Generic[RequestT]):
204 request_id: str
205 created_time: int = field(default_factory=lambda: int(time.time()))
206 lora_request: LoRARequest | None = None
196 - engine_prompts: list[TokensPrompt] | None = None
207 + engine_prompts: list[TokensPrompt | EmbedsPrompt] | None = None
208
209 result_generator: AsyncGenerator[tuple[int, PoolingRequestOutput], None] | None = (
210 None
@@ -227,7 +238,6 @@ class OpenAIServing:
238 self.request_logger = request_logger
239 self.return_tokens_as_token_ids = return_tokens_as_token_ids
240
230 - self._async_tokenizer_pool: dict[TokenizerLike, AsyncMicrobatchTokenizer] = {}
241 self.log_error_stack = log_error_stack
242
243 self.input_processor = self.models.input_processor
@@ -519,41 +529,6 @@ class OpenAIServing:
529 prompt_logprobs=None,
530 )
531
522 - def _get_completion_renderer(self) -> BaseRenderer:
523 - """
524 - Get a Renderer instance with the provided tokenizer.
525 - Uses shared async tokenizer pool for efficiency.
526 - """
527 - return CompletionRenderer(
528 - model_config=self.model_config,
529 - tokenizer=self.renderer.tokenizer,
530 - async_tokenizer_pool=self._async_tokenizer_pool,
531 - )
532 -
533 - def _build_render_config(
534 - self,
535 - request: Any,
536 - ) -> RenderConfig:
537 - """
538 - Build and return a `RenderConfig` for an endpoint.
539 -
540 - Used by the renderer to control how prompts are prepared
541 - (e.g., tokenization and length handling). Endpoints should
542 - implement this with logic appropriate to their request type.
543 - """
544 - raise NotImplementedError
545 -
546 - def _get_async_tokenizer(self, tokenizer) -> AsyncMicrobatchTokenizer:
547 - """
548 - Return (and cache) an `AsyncMicrobatchTokenizer` bound to the
549 - given tokenizer.
550 - """
551 - async_tokenizer = self._async_tokenizer_pool.get(tokenizer)
552 - if async_tokenizer is None:
553 - async_tokenizer = AsyncMicrobatchTokenizer(tokenizer)
554 - self._async_tokenizer_pool[tokenizer] = async_tokenizer
555 - return async_tokenizer
556 -
532 async def _preprocess(
533 self,
534 ctx: ServeContext,
@@ -912,71 +887,6 @@ class OpenAIServing:
887 message_types.add(content_dict["type"].split("_")[0])
888 return message_types
889
915 - async def _normalize_prompt_text_to_input(
916 - self,
917 - request: AnyRequest,
918 - prompt: str,
919 - tokenizer: TokenizerLike,
920 - add_special_tokens: bool,
921 - ) -> TokensPrompt:
922 - async_tokenizer = self._get_async_tokenizer(tokenizer)
923 -
924 - if (
925 - self.model_config.encoder_config is not None
926 - and self.model_config.encoder_config.get("do_lower_case", False)
927 - ):
928 - prompt = prompt.lower()
929 -
930 - truncate_prompt_tokens = getattr(request, "truncate_prompt_tokens", None)
931 -
932 - if truncate_prompt_tokens is None:
933 - encoded = await async_tokenizer(
934 - prompt, add_special_tokens=add_special_tokens
935 - )
936 - elif truncate_prompt_tokens < 0:
937 - # Negative means we cap at the model's max length
938 - encoded = await async_tokenizer(
939 - prompt,
940 - add_special_tokens=add_special_tokens,
941 - truncation=True,
942 - max_length=self.max_model_len,
943 - )
944 - else:
945 - encoded = await async_tokenizer(
946 - prompt,
947 - add_special_tokens=add_special_tokens,
948 - truncation=True,
949 - max_length=truncate_prompt_tokens,
950 - )
951 -
952 - input_ids = encoded.input_ids
953 - input_text = prompt
954 -
955 - return self._validate_input(request, input_ids, input_text)
956 -
957 - async def _normalize_prompt_tokens_to_input(
958 - self,
959 - request: AnyRequest,
960 - prompt_ids: list[int],
961 - tokenizer: TokenizerLike | None,
962 - ) -> TokensPrompt:
963 - truncate_prompt_tokens = getattr(request, "truncate_prompt_tokens", None)
964 -
965 - if truncate_prompt_tokens is None:
966 - input_ids = prompt_ids
967 - elif truncate_prompt_tokens < 0:
968 - input_ids = prompt_ids[-self.max_model_len :]
969 - else:
970 - input_ids = prompt_ids[-truncate_prompt_tokens:]
971 -
972 - if tokenizer is None:
973 - input_text = ""
974 - else:
975 - async_tokenizer = self._get_async_tokenizer(tokenizer)
976 - input_text = await async_tokenizer.decode(input_ids)
977 -
978 - return self._validate_input(request, input_ids, input_text)
979 -
890 def _validate_input(
891 self,
892 request: object,
@@ -1061,50 +971,6 @@ class OpenAIServing:
971
972 return TokensPrompt(prompt=input_text, prompt_token_ids=input_ids)
973
1064 - async def _tokenize_prompt_input_async(
1065 - self,
1066 - request: AnyRequest,
1067 - tokenizer: TokenizerLike,
1068 - prompt_input: str | list[int],
1069 - add_special_tokens: bool = True,
1070 - ) -> TokensPrompt:
1071 - """
1072 - A simpler implementation that tokenizes a single prompt input.
1073 - """
1074 - async for result in self._tokenize_prompt_inputs_async(
1075 - request,
1076 - tokenizer,
1077 - [prompt_input],
1078 - add_special_tokens=add_special_tokens,
1079 - ):
1080 - return result
1081 - raise ValueError("No results yielded from tokenization")
1082 -
1083 - async def _tokenize_prompt_inputs_async(
1084 - self,
1085 - request: AnyRequest,
1086 - tokenizer: TokenizerLike,
1087 - prompt_inputs: Iterable[str | list[int]],
1088 - add_special_tokens: bool = True,
1089 - ) -> AsyncGenerator[TokensPrompt, None]:
1090 - """
1091 - A simpler implementation that tokenizes multiple prompt inputs.
1092 - """
1093 - for prompt in prompt_inputs:
1094 - if isinstance(prompt, str):
1095 - yield await self._normalize_prompt_text_to_input(
1096 - request,
1097 - prompt=prompt,
1098 - tokenizer=tokenizer,
1099 - add_special_tokens=add_special_tokens,
1100 - )
1101 - else:
1102 - yield await self._normalize_prompt_tokens_to_input(
1103 - request,
1104 - prompt_ids=prompt,
1105 - tokenizer=tokenizer,
1106 - )
1107 -
974 def _validate_chat_template(
975 self,
976 request_chat_template: str | None,
@@ -1137,131 +1003,94 @@ class OpenAIServing:
1003 # Apply server defaults first, then request kwargs override.
1004 return default_chat_template_kwargs | request_chat_template_kwargs
1005
1006 + async def _preprocess_completion(
1007 + self,
1008 + request: RendererRequest,
1009 + prompt_input: str | list[str] | list[int] | list[list[int]] | None,
1010 + prompt_embeds: bytes | list[bytes] | None,
1011 + ) -> list[TokensPrompt | EmbedsPrompt]:
1012 + renderer = self.renderer
1013 + tok_params = request.build_tok_params(self.model_config)
1014 +
1015 + in_prompts = await renderer.render_completions_async(
1016 + prompt_input, prompt_embeds
1017 + )
1018 + engine_prompts = await renderer.tokenize_prompts_async(in_prompts, tok_params)
1019 +
1020 + extra_items = {
1021 + k: v
1022 + for k in ("mm_processor_kwargs", "cache_salt")
1023 + if (v := getattr(request, k, None)) is not None
1024 + }
1025 + for prompt in engine_prompts:
1026 + prompt.update(extra_items) # type: ignore
1027 +
1028 + return engine_prompts
1029 +
1030 async def _preprocess_chat(
1031 self,
1142 - request: ChatLikeRequest | ResponsesRequest,
1143 - renderer: RendererLike,
1032 + request: RendererChatRequest,
1033 messages: list[ChatCompletionMessageParam],
1145 - chat_template: str | None,
1146 - chat_template_content_format: ChatTemplateContentFormatOption,
1147 - add_generation_prompt: bool = True,
1148 - continue_final_message: bool = False,
1034 + default_template: str | None,
1035 + default_template_content_format: ChatTemplateContentFormatOption,
1036 + default_template_kwargs: dict[str, Any] | None,
1037 tool_dicts: list[dict[str, Any]] | None = None,
1150 - documents: list[dict[str, str]] | None = None,
1151 - chat_template_kwargs: dict[str, Any] | None = None,
1152 - default_chat_template_kwargs: dict[str, Any] | None = None,
1038 tool_parser: Callable[[TokenizerLike], ToolParser] | None = None,
1154 - add_special_tokens: bool = False,
1155 - ) -> tuple[list[ConversationMessage], list[TokensPrompt]]:
1156 - chat_template_kwargs = {
1157 - "chat_template": chat_template,
1158 - "add_generation_prompt": add_generation_prompt,
1159 - "continue_final_message": continue_final_message,
1160 - "tools": tool_dicts,
1161 - "documents": documents,
1162 - **(chat_template_kwargs or {}),
1163 - }
1164 - chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
1165 - chat_template_kwargs,
1166 - default_chat_template_kwargs,
1167 - )
1168 -
1169 - # Use the async tokenizer in `OpenAIServing` if possible.
1170 - # Later we can move it into the renderer so that we can return both
1171 - # text and token IDs in the same prompt from `render_messages_async`
1172 - # which is used for logging and `enable_response_messages`.
1039 + ) -> tuple[list[ConversationMessage], list[TokensPrompt | EmbedsPrompt]]:
1040 from vllm.tokenizers.mistral import MistralTokenizer
1041
1175 - conversation, engine_prompt = await renderer.render_messages_async(
1176 - messages,
1177 - chat_template_content_format=chat_template_content_format,
1178 - tokenize=(
1179 - chat_template_kwargs.pop("tokenize", False)
1180 - or isinstance(renderer.tokenizer, MistralTokenizer)
1042 + renderer = self.renderer
1043 +
1044 + default_template_kwargs = merge_kwargs(
1045 + default_template_kwargs,
1046 + dict(
1047 + tools=tool_dicts,
1048 + tokenize=isinstance(renderer.tokenizer, MistralTokenizer),
1049 ),
1182 - **chat_template_kwargs,
1050 )
1051
1185 - if "prompt_token_ids" not in engine_prompt:
1186 - extra_data = engine_prompt
1187 - engine_prompt = await self._tokenize_prompt_input_async(
1188 - request,
1189 - renderer.get_tokenizer(),
1190 - engine_prompt["prompt"],
1191 - add_special_tokens=add_special_tokens,
1192 - )
1193 - # Fill in other keys like MM data
1194 - engine_prompt.update(extra_data) # type: ignore
1195 - else:
1196 - self._validate_input(
1197 - request=request,
1198 - input_ids=engine_prompt["prompt_token_ids"], # type: ignore
1199 - input_text="",
1200 - )
1052 + tok_params = request.build_tok_params(self.model_config)
1053 + chat_params = request.build_chat_params(
1054 + default_template, default_template_content_format
1055 + ).with_defaults(default_template_kwargs)
1056
1202 - engine_prompt = cast(TokensPrompt, engine_prompt)
1057 + conversation, prompt = await renderer.render_messages_async(
1058 + messages, chat_params
1059 + )
1060 + engine_prompt = await renderer.tokenize_prompt_async(prompt, tok_params)
1061
1204 - if request.mm_processor_kwargs is not None:
1205 - engine_prompt["mm_processor_kwargs"] = request.mm_processor_kwargs
1206 - if (cache_salt := getattr(request, "cache_salt", None)) is not None:
1207 - engine_prompt["cache_salt"] = cache_salt
1062 + extra_items = {
1063 + k: v
1064 + for k in ("mm_processor_kwargs", "cache_salt")
1065 + if (v := getattr(request, k, None)) is not None
1066 + }
1067 + engine_prompt.update(extra_items) # type: ignore
1068
1069 # tool parsing is done only if a tool_parser has been set and if
1070 # tool_choice is not "none" (if tool_choice is "none" but a tool_parser
1071 # is set, we want to prevent parsing a tool_call hallucinated by the LLM
1212 - should_parse_tools = tool_parser is not None and (
1213 - hasattr(request, "tool_choice") and request.tool_choice != "none"
1214 - )
1215 -
1216 - if should_parse_tools:
1217 - if not isinstance(request, ChatCompletionRequest | ResponsesRequest):
1218 - msg = (
1219 - "Tool usage is only supported for Chat Completions API "
1220 - "or Responses API requests."
1221 - )
1222 - raise NotImplementedError(msg)
1072 + if tool_parser is not None:
1073 + tool_choice = getattr(request, "tool_choice", "none")
1074 + if tool_choice != "none":
1075 + if not isinstance(request, ChatCompletionRequest | ResponsesRequest):
1076 + msg = (
1077 + "Tool usage is only supported for Chat Completions API "
1078 + "or Responses API requests."
1079 + )
1080 + raise NotImplementedError(msg)
1081
1224 - tokenizer = renderer.get_tokenizer()
1225 - request = tool_parser(tokenizer).adjust_request(request=request) # type: ignore
1082 + # TODO: Update adjust_request to accept ResponsesRequest
1083 + tokenizer = renderer.get_tokenizer()
1084 + request = tool_parser(tokenizer).adjust_request(request=request) # type: ignore[arg-type]
1085
1086 return conversation, [engine_prompt]
1087
1229 - async def _process_inputs(
1230 - self,
1231 - request_id: str,
1232 - engine_prompt: PromptType,
1233 - params: SamplingParams | PoolingParams,
1234 - *,
1235 - lora_request: LoRARequest | None,
1236 - trace_headers: Mapping[str, str] | None,
1237 - priority: int,
1238 - data_parallel_rank: int | None = None,
1239 - ) -> tuple[EngineCoreRequest, dict[str, Any]]:
1240 - """Use the Processor to process inputs for AsyncLLM."""
1241 - tokenization_kwargs: dict[str, Any] = {}
1242 - _validate_truncation_size(
1243 - self.max_model_len, params.truncate_prompt_tokens, tokenization_kwargs
1244 - )
1245 -
1246 - engine_request = self.input_processor.process_inputs(
1247 - request_id,
1248 - engine_prompt,
1249 - params,
1250 - lora_request=lora_request,
1251 - tokenization_kwargs=tokenization_kwargs,
1252 - trace_headers=trace_headers,
1253 - priority=priority,
1254 - data_parallel_rank=data_parallel_rank,
1255 - )
1256 - return engine_request, tokenization_kwargs
1257 -
1088 async def _render_next_turn(
1089 self,
1090 request: ResponsesRequest,
1261 - renderer: RendererLike,
1091 messages: list[ResponseInputOutputItem],
1092 tool_dicts: list[dict[str, Any]] | None,
1264 - tool_parser,
1093 + tool_parser: Callable[[TokenizerLike], ToolParser] | None,
1094 chat_template: str | None,
1095 chat_template_content_format: ChatTemplateContentFormatOption,
1096 ):
@@ -1271,24 +1100,25 @@ class OpenAIServing:
1100
1101 _, engine_prompts = await self._preprocess_chat(
1102 request,
1274 - renderer,
1103 new_messages,
1104 + default_template=chat_template,
1105 + default_template_content_format=chat_template_content_format,
1106 + default_template_kwargs=None,
1107 tool_dicts=tool_dicts,
1108 tool_parser=tool_parser,
1278 - chat_template=chat_template,
1279 - chat_template_content_format=chat_template_content_format,
1109 )
1110 return engine_prompts
1111
1112 async def _generate_with_builtin_tools(
1113 self,
1114 request_id: str,
1286 - engine_prompt: TokensPrompt,
1115 + engine_prompt: TokensPrompt | EmbedsPrompt,
1116 sampling_params: SamplingParams,
1117 + tok_params: TokenizeParams,
1118 context: ConversationContext,
1119 lora_request: LoRARequest | None = None,
1120 priority: int = 0,
1291 - **kwargs,
1121 + trace_headers: Mapping[str, str] | None = None,
1122 ):
1123 prompt_text, _, _ = get_prompt_components(engine_prompt)
1124
@@ -1297,18 +1127,21 @@ class OpenAIServing:
1127 while True:
1128 # Ensure that each sub-request has a unique request id.
1129 sub_request_id = f"{request_id}_{sub_request}"
1130 +
1131 self._log_inputs(
1132 sub_request_id,
1133 engine_prompt,
1134 params=sampling_params,
1135 lora_request=lora_request,
1136 )
1306 - trace_headers = kwargs.get("trace_headers")
1307 - engine_request, tokenization_kwargs = await self._process_inputs(
1137 +
1138 + tokenization_kwargs = tok_params.get_encode_kwargs()
1139 + engine_request = self.input_processor.process_inputs(
1140 sub_request_id,
1141 engine_prompt,
1142 sampling_params,
1143 lora_request=lora_request,
1144 + tokenization_kwargs=tokenization_kwargs,
1145 trace_headers=trace_headers,
1146 priority=priority,
1147 )
@@ -1318,10 +1151,10 @@ class OpenAIServing:
1151 sampling_params,
1152 sub_request_id,
1153 lora_request=lora_request,
1154 + trace_headers=trace_headers,
1155 priority=priority,
1156 prompt_text=prompt_text,
1157 tokenization_kwargs=tokenization_kwargs,
1324 - **kwargs,
1158 )
1159
1160 async for res in generator:
@@ -1350,7 +1183,6 @@ class OpenAIServing:
1183 elif isinstance(context, ParsableContext):
1184 engine_prompts = await self._render_next_turn(
1185 context.request,
1353 - context.renderer,
1186 context.parser.response_messages,
1187 context.tool_dicts,
1188 context.tool_parser_cls,
vllm/entrypoints/openai/responses/context.py
+1 -5
@@ -43,7 +43,6 @@ from vllm.entrypoints.openai.responses.protocol import (
43 from vllm.entrypoints.openai.responses.utils import construct_tool_dicts
44 from vllm.outputs import RequestOutput
45 from vllm.reasoning.abs_reasoning_parsers import ReasoningParser
46 -from vllm.renderers import RendererLike
46 from vllm.tokenizers import TokenizerLike
47 from vllm.tool_parsers.abstract_tool_parser import ToolParser
48 from vllm.utils import random_uuid
@@ -261,7 +260,7 @@ class ParsableContext(ConversationContext):
260 self,
261 *,
262 response_messages: list[ResponseInputOutputItem],
264 - renderer: RendererLike,
263 + tokenizer: TokenizerLike,
264 reasoning_parser_cls: Callable[[TokenizerLike], ReasoningParser] | None,
265 request: ResponsesRequest,
266 available_tools: list[str] | None,
@@ -280,7 +279,6 @@ class ParsableContext(ConversationContext):
279 if reasoning_parser_cls is None:
280 raise ValueError("reasoning_parser_cls must be provided.")
281
283 - tokenizer = renderer.get_tokenizer()
282 self.parser = get_responses_parser_for_simple_context(
283 tokenizer=tokenizer,
284 reasoning_parser_cls=reasoning_parser_cls,
@@ -290,8 +288,6 @@ class ParsableContext(ConversationContext):
288 )
289 self.tool_parser_cls = tool_parser_cls
290 self.request = request
293 - self.renderer = renderer
294 - self.tokenizer = tokenizer
291
292 self.available_tools = available_tools or []
293 self._tool_sessions: dict[str, ClientSession | Tool] = {}
vllm/entrypoints/openai/responses/protocol.py
+42 -3
@@ -59,12 +59,15 @@ from pydantic import (
59 model_validator,
60 )
61
62 -from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
63 -from vllm.entrypoints.openai.engine.protocol import (
64 - OpenAIBaseModel,
62 +from vllm.config import ModelConfig
63 +from vllm.entrypoints.chat_utils import (
64 + ChatCompletionMessageParam,
65 + ChatTemplateContentFormatOption,
66 )
67 +from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel
68 from vllm.exceptions import VLLMValidationError
69 from vllm.logger import init_logger
70 +from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
71 from vllm.sampling_params import (
72 RequestOutputKind,
73 SamplingParams,
@@ -230,6 +233,42 @@ class ResponsesRequest(OpenAIBaseModel):
233 previous_input_messages: list[OpenAIHarmonyMessage | dict] | None = None
234 # --8<-- [end:responses-extra-params]
235
236 + def build_chat_params(
237 + self,
238 + default_template: str | None,
239 + default_template_content_format: ChatTemplateContentFormatOption,
240 + ) -> ChatParams:
241 + from .utils import should_continue_final_message
242 +
243 + # Check if we should continue the final message (partial completion)
244 + # This enables Anthropic-style partial message completion where the
245 + # user provides an incomplete assistant message to continue from.
246 + continue_final = should_continue_final_message(self.input)
247 +
248 + reasoning = self.reasoning
249 +
250 + return ChatParams(
251 + chat_template=default_template,
252 + chat_template_content_format=default_template_content_format,
253 + chat_template_kwargs=merge_kwargs( # To remove unset values
254 + {},
255 + dict(
256 + add_generation_prompt=not continue_final,
257 + continue_final_message=continue_final,
258 + reasoning_effort=None if reasoning is None else reasoning.effort,
259 + ),
260 + ),
261 + )
262 +
263 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
264 + return TokenizeParams(
265 + max_total_tokens=model_config.max_model_len,
266 + max_output_tokens=self.max_output_tokens or 0,
267 + truncate_prompt_tokens=-1 if self.truncation != "disabled" else None,
268 + max_total_tokens_param="max_model_len",
269 + max_output_tokens_param="max_output_tokens",
270 + )
271 +
272 _DEFAULT_SAMPLING_PARAMS = {
273 "temperature": 1.0,
274 "top_p": 1.0,
vllm/entrypoints/openai/responses/serving.py
+18 -30
@@ -114,16 +114,15 @@ from vllm.entrypoints.openai.responses.utils import (
114 construct_input_messages,
115 construct_tool_dicts,
116 extract_tool_types,
117 - should_continue_final_message,
117 )
118 from vllm.entrypoints.utils import get_max_tokens
119 from vllm.exceptions import VLLMValidationError
121 -from vllm.inputs.data import TokensPrompt
120 +from vllm.inputs.data import EmbedsPrompt, TokensPrompt
121 +from vllm.inputs.parse import get_prompt_len
122 from vllm.logger import init_logger
123 from vllm.logprobs import Logprob as SampleLogprob
124 from vllm.logprobs import SampleLogprobs
125 from vllm.outputs import CompletionOutput
126 -from vllm.renderers import RendererLike
126 from vllm.sampling_params import SamplingParams, StructuredOutputsParams
127 from vllm.tokenizers import TokenizerLike
128 from vllm.utils import random_uuid
@@ -291,13 +290,14 @@ class OpenAIServingResponses(OpenAIServing):
290 self.tool_server = tool_server
291
292 def _validate_generator_input(
294 - self, engine_prompt: TokensPrompt
293 + self,
294 + engine_prompt: TokensPrompt | EmbedsPrompt,
295 ) -> ErrorResponse | None:
296 """Add validations to the input to the generator here."""
297 - if self.max_model_len <= len(engine_prompt["prompt_token_ids"]):
297 + prompt_len = get_prompt_len(engine_prompt)
298 + if self.max_model_len <= prompt_len:
299 error_message = (
299 - "The engine prompt length"
300 - f" {len(engine_prompt['prompt_token_ids'])} "
300 + f"The engine prompt length {prompt_len} "
301 f"exceeds the max_model_len {self.max_model_len}. "
302 "Please reduce prompt."
303 )
@@ -307,6 +307,7 @@ class OpenAIServingResponses(OpenAIServing):
307 status_code=HTTPStatus.BAD_REQUEST,
308 param="input",
309 )
310 +
311 return None
312
313 def _validate_create_responses_input(
@@ -387,8 +388,6 @@ class OpenAIServingResponses(OpenAIServing):
388 try:
389 lora_request = self._maybe_get_adapters(request)
390 model_name = self.models.model_name(lora_request)
390 - renderer = self.engine_client.renderer
391 - tokenizer = renderer.get_tokenizer()
391
392 if self.use_harmony:
393 messages, engine_prompts = self._make_request_with_harmony(
@@ -396,7 +395,7 @@ class OpenAIServingResponses(OpenAIServing):
395 )
396 else:
397 messages, engine_prompts = await self._make_request(
399 - request, prev_response, renderer
398 + request, prev_response
399 )
400
401 except (
@@ -431,6 +430,9 @@ class OpenAIServingResponses(OpenAIServing):
430 assert len(builtin_tool_list) == 0
431 available_tools = []
432 try:
433 + renderer = self.engine_client.renderer
434 + tokenizer = renderer.get_tokenizer()
435 +
436 for engine_prompt in engine_prompts:
437 maybe_error = self._validate_generator_input(engine_prompt)
438 if maybe_error is not None:
@@ -446,6 +448,7 @@ class OpenAIServingResponses(OpenAIServing):
448 sampling_params = request.to_sampling_params(
449 default_max_tokens, self.default_sampling_params
450 )
451 + tok_params = request.build_tok_params(self.model_config)
452
453 trace_headers = (
454 None
@@ -465,7 +468,7 @@ class OpenAIServingResponses(OpenAIServing):
468 # tokens during generation instead of at the end
469 context = ParsableContext(
470 response_messages=messages,
468 - renderer=renderer,
471 + tokenizer=tokenizer,
472 reasoning_parser_cls=self.reasoning_parser,
473 request=request,
474 tool_parser_cls=self.tool_parser,
@@ -495,6 +498,7 @@ class OpenAIServingResponses(OpenAIServing):
498 request_id=request.request_id,
499 engine_prompt=engine_prompt,
500 sampling_params=sampling_params,
501 + tok_params=tok_params,
502 context=context,
503 lora_request=lora_request,
504 priority=request.priority,
@@ -596,7 +600,6 @@ class OpenAIServingResponses(OpenAIServing):
600 self,
601 request: ResponsesRequest,
602 prev_response: ResponsesResponse | None,
599 - renderer: RendererLike,
603 ):
604 tool_dicts = construct_tool_dicts(request.tools, request.tool_choice)
605 # Construct the input messages.
@@ -606,30 +609,15 @@ class OpenAIServingResponses(OpenAIServing):
609 prev_msg=self.msg_store.get(prev_response.id) if prev_response else None,
610 prev_response_output=prev_response.output if prev_response else None,
611 )
609 - # Check if we should continue the final message (partial completion)
610 - # This enables Anthropic-style partial message completion where the
611 - # user provides an incomplete assistant message to continue from.
612 - continue_final = should_continue_final_message(request.input)
613 - chat_template_kwargs = dict(
614 - reasoning_effort=None
615 - if request.reasoning is None
616 - else request.reasoning.effort
617 - )
612
613 _, engine_prompts = await self._preprocess_chat(
614 request,
621 - renderer,
615 messages,
616 + default_template=self.chat_template,
617 + default_template_content_format=self.chat_template_content_format,
618 + default_template_kwargs=None,
619 tool_dicts=tool_dicts,
620 tool_parser=self.tool_parser,
625 - chat_template=self.chat_template,
626 - chat_template_content_format=self.chat_template_content_format,
627 - # When continuing a partial message, we set continue_final_message=True
628 - # and add_generation_prompt=False so the model continues the message
629 - # rather than starting a new one.
630 - add_generation_prompt=not continue_final,
631 - continue_final_message=continue_final,
632 - chat_template_kwargs=chat_template_kwargs,
621 )
622 return messages, engine_prompts
623
vllm/entrypoints/pooling/base/protocol.py
+22 -1
@@ -8,8 +8,12 @@ from pydantic import Field, model_validator
8
9 from vllm import PoolingParams
10 from vllm.config.pooler import get_use_activation
11 -from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
11 +from vllm.entrypoints.chat_utils import (
12 + ChatCompletionMessageParam,
13 + ChatTemplateContentFormatOption,
14 +)
15 from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel
16 +from vllm.renderers import ChatParams, merge_kwargs
17 from vllm.utils import random_uuid
18 from vllm.utils.serial_utils import EmbedDType, EncodingFormat, Endianness
19
@@ -119,6 +123,23 @@ class ChatRequestMixin(OpenAIBaseModel):
123 )
124 return data
125
126 + def build_chat_params(
127 + self,
128 + default_template: str | None,
129 + default_template_content_format: ChatTemplateContentFormatOption,
130 + ) -> ChatParams:
131 + return ChatParams(
132 + chat_template=self.chat_template or default_template,
133 + chat_template_content_format=default_template_content_format,
134 + chat_template_kwargs=merge_kwargs(
135 + self.chat_template_kwargs,
136 + dict(
137 + add_generation_prompt=self.add_generation_prompt,
138 + continue_final_message=self.continue_final_message,
139 + ),
140 + ),
141 + )
142 +
143
144 class EncodingRequestMixin(OpenAIBaseModel):
145 # --8<-- [start:encoding-params]
vllm/entrypoints/pooling/classify/protocol.py
+26 -4
@@ -4,10 +4,9 @@
4 import time
5 from typing import Any, TypeAlias
6
7 -from pydantic import (
8 - Field,
9 -)
7 +from pydantic import Field
8
9 +from vllm.config import ModelConfig
10 from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, UsageInfo
11 from vllm.entrypoints.pooling.base.protocol import (
12 ChatRequestMixin,
@@ -15,13 +14,24 @@ from vllm.entrypoints.pooling.base.protocol import (
14 CompletionRequestMixin,
15 PoolingBasicRequestMixin,
16 )
17 +from vllm.renderers import TokenizeParams
18 from vllm.utils import random_uuid
19
20
21 class ClassificationCompletionRequest(
22 PoolingBasicRequestMixin, CompletionRequestMixin, ClassifyRequestMixin
23 ):
24 - pass
24 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
25 + encoder_config = model_config.encoder_config or {}
26 +
27 + return TokenizeParams(
28 + max_total_tokens=model_config.max_model_len,
29 + max_output_tokens=0,
30 + truncate_prompt_tokens=self.truncate_prompt_tokens,
31 + do_lower_case=encoder_config.get("do_lower_case", False),
32 + add_special_tokens=self.add_special_tokens,
33 + max_total_tokens_param="max_model_len",
34 + )
35
36
37 class ClassificationChatRequest(
@@ -33,6 +43,18 @@ class ClassificationChatRequest(
43 description=("Additional kwargs to pass to the HF processor."),
44 )
45
46 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
47 + encoder_config = model_config.encoder_config or {}
48 +
49 + return TokenizeParams(
50 + max_total_tokens=model_config.max_model_len,
51 + max_output_tokens=0,
52 + truncate_prompt_tokens=self.truncate_prompt_tokens,
53 + do_lower_case=encoder_config.get("do_lower_case", False),
54 + add_special_tokens=self.add_special_tokens,
55 + max_total_tokens_param="max_model_len",
56 + )
57 +
58
59 ClassificationRequest: TypeAlias = (
60 ClassificationCompletionRequest | ClassificationChatRequest
vllm/entrypoints/pooling/classify/serving.py
+12 -37
@@ -1,8 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from http import HTTPStatus
5 -from typing import Final, cast
4 +from typing import Final, TypeAlias
5
6 import jinja2
7 import numpy as np
@@ -21,15 +20,14 @@ from vllm.entrypoints.pooling.classify.protocol import (
20 ClassificationRequest,
21 ClassificationResponse,
22 )
24 -from vllm.entrypoints.renderer import RenderConfig
23 from vllm.logger import init_logger
26 -from vllm.outputs import ClassificationOutput, PoolingRequestOutput
24 +from vllm.outputs import ClassificationOutput
25 from vllm.pooling_params import PoolingParams
26
27 logger = init_logger(__name__)
28
29
32 -ClassificationServeContext = ServeContext[ClassificationRequest]
30 +ClassificationServeContext: TypeAlias = ServeContext[ClassificationRequest]
31
32
33 class ServingClassification(OpenAIServing):
@@ -77,34 +75,18 @@ class ServingClassification(OpenAIServing):
75 if error_check_ret:
76 return error_check_ret
77
80 - _, engine_prompts = await self._preprocess_chat(
78 + _, ctx.engine_prompts = await self._preprocess_chat(
79 ctx.request,
82 - self.renderer,
80 ctx.request.messages,
84 - chat_template=ctx.request.chat_template or self.chat_template,
85 - chat_template_content_format=self.chat_template_content_format,
86 - add_generation_prompt=ctx.request.add_generation_prompt,
87 - continue_final_message=ctx.request.continue_final_message,
88 - add_special_tokens=ctx.request.add_special_tokens,
81 + default_template=self.chat_template,
82 + default_template_content_format=self.chat_template_content_format,
83 + default_template_kwargs=None,
84 )
90 - ctx.engine_prompts = engine_prompts
91 -
85 elif isinstance(ctx.request, ClassificationCompletionRequest):
93 - input_data = ctx.request.input
94 - if input_data in (None, ""):
95 - return self.create_error_response(
96 - "Input or messages must be provided",
97 - status_code=HTTPStatus.BAD_REQUEST,
98 - )
99 - if isinstance(input_data, list) and not input_data:
100 - ctx.engine_prompts = []
101 - return None
102 -
103 - renderer = self._get_completion_renderer()
104 - prompt_input = cast(str | list[str], input_data)
105 - ctx.engine_prompts = await renderer.render_prompt(
106 - prompt_or_prompts=prompt_input,
107 - config=self._build_render_config(ctx.request),
86 + ctx.engine_prompts = await self._preprocess_completion(
87 + ctx.request,
88 + prompt_input=ctx.request.input,
89 + prompt_embeds=None,
90 )
91 else:
92 return self.create_error_response("Invalid classification request type")
@@ -128,7 +110,7 @@ class ServingClassification(OpenAIServing):
110 items: list[ClassificationData] = []
111 num_prompt_tokens = 0
112
131 - final_res_batch_checked = cast(list[PoolingRequestOutput], ctx.final_res_batch)
113 + final_res_batch_checked = ctx.final_res_batch
114
115 for idx, final_res in enumerate(final_res_batch_checked):
116 classify_res = ClassificationOutput.from_base(final_res.outputs)
@@ -161,13 +143,6 @@ class ServingClassification(OpenAIServing):
143 usage=usage,
144 )
145
164 - def _build_render_config(self, request: ClassificationRequest) -> RenderConfig:
165 - return RenderConfig(
166 - max_length=self.max_model_len,
167 - truncate_prompt_tokens=request.truncate_prompt_tokens,
168 - add_special_tokens=request.add_special_tokens,
169 - )
170 -
146 async def create_classify(
147 self,
148 request: ClassificationRequest,
vllm/entrypoints/pooling/embed/protocol.py
+55 -6
@@ -3,10 +3,9 @@
3 import time
4 from typing import Any, TypeAlias
5
6 -from pydantic import (
7 - Field,
8 -)
6 +from pydantic import Field
7
8 +from vllm.config import ModelConfig
9 from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, UsageInfo
10 from vllm.entrypoints.pooling.base.protocol import (
11 ChatRequestMixin,
@@ -14,15 +13,47 @@ from vllm.entrypoints.pooling.base.protocol import (
13 EmbedRequestMixin,
14 PoolingBasicRequestMixin,
15 )
16 +from vllm.renderers import TokenizeParams
17 from vllm.utils import random_uuid
18
19
20 +def _get_max_total_output_tokens(
21 + model_config: ModelConfig,
22 +) -> tuple[int | None, int]:
23 + max_total_tokens = model_config.max_model_len
24 + pooler_config = model_config.pooler_config
25 +
26 + if pooler_config is None:
27 + return max_total_tokens, 0
28 +
29 + if pooler_config.enable_chunked_processing:
30 + return None, 0
31 +
32 + max_embed_len = pooler_config.max_embed_len or max_total_tokens
33 + max_output_tokens = max_total_tokens - max_embed_len
34 + return max_total_tokens, max_output_tokens
35 +
36 +
37 class EmbeddingCompletionRequest(
38 PoolingBasicRequestMixin, CompletionRequestMixin, EmbedRequestMixin
39 ):
23 - # Ordered by official OpenAI API documentation
24 - # https://platform.openai.com/docs/api-reference/embeddings
25 - pass
40 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
41 + encoder_config = model_config.encoder_config or {}
42 +
43 + (
44 + max_total_tokens,
45 + max_output_tokens,
46 + ) = _get_max_total_output_tokens(model_config)
47 +
48 + return TokenizeParams(
49 + max_total_tokens=max_total_tokens,
50 + max_output_tokens=max_output_tokens,
51 + truncate_prompt_tokens=self.truncate_prompt_tokens,
52 + do_lower_case=encoder_config.get("do_lower_case", False),
53 + add_special_tokens=self.add_special_tokens,
54 + max_total_tokens_param="max_model_len",
55 + max_output_tokens_param="max_model_len - max_embed_len",
56 + )
57
58
59 class EmbeddingChatRequest(
@@ -33,6 +64,24 @@ class EmbeddingChatRequest(
64 description=("Additional kwargs to pass to the HF processor."),
65 )
66
67 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
68 + encoder_config = model_config.encoder_config or {}
69 +
70 + (
71 + max_total_tokens,
72 + max_output_tokens,
73 + ) = _get_max_total_output_tokens(model_config)
74 +
75 + return TokenizeParams(
76 + max_total_tokens=max_total_tokens,
77 + max_output_tokens=max_output_tokens,
78 + truncate_prompt_tokens=self.truncate_prompt_tokens,
79 + do_lower_case=encoder_config.get("do_lower_case", False),
80 + add_special_tokens=self.add_special_tokens,
81 + max_total_tokens_param="max_model_len",
82 + max_output_tokens_param="max_model_len - max_embed_len",
83 + )
84 +
85
86 EmbeddingRequest: TypeAlias = EmbeddingCompletionRequest | EmbeddingChatRequest
87
vllm/entrypoints/pooling/embed/serving.py
+25 -35
@@ -2,7 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import json
4 from collections.abc import AsyncGenerator, Mapping
5 -from typing import Any, Final, cast
5 +from typing import Any, Final, TypeAlias
6
7 import torch
8 from fastapi import Request
@@ -22,8 +22,7 @@ from vllm.entrypoints.pooling.embed.protocol import (
22 EmbeddingResponse,
23 EmbeddingResponseData,
24 )
25 -from vllm.entrypoints.renderer import RenderConfig
26 -from vllm.inputs.data import TokensPrompt
25 +from vllm.inputs.data import EmbedsPrompt, TokensPrompt
26 from vllm.logger import init_logger
27 from vllm.outputs import PoolingOutput, PoolingRequestOutput
28 from vllm.pooling_params import PoolingParams
@@ -37,7 +36,7 @@ from vllm.utils.serial_utils import (
36 logger = init_logger(__name__)
37
38
40 -EmbeddingServeContext = ServeContext[EmbeddingRequest]
39 +EmbeddingServeContext: TypeAlias = ServeContext[EmbeddingRequest]
40
41
42 class OpenAIServingEmbedding(OpenAIServing):
@@ -95,19 +94,16 @@ class OpenAIServingEmbedding(OpenAIServing):
94
95 _, ctx.engine_prompts = await self._preprocess_chat(
96 ctx.request,
98 - self.renderer,
97 ctx.request.messages,
100 - chat_template=ctx.request.chat_template or self.chat_template,
101 - chat_template_content_format=self.chat_template_content_format,
102 - add_generation_prompt=ctx.request.add_generation_prompt,
103 - continue_final_message=ctx.request.continue_final_message,
104 - add_special_tokens=ctx.request.add_special_tokens,
98 + default_template=self.chat_template,
99 + default_template_content_format=self.chat_template_content_format,
100 + default_template_kwargs=None,
101 )
102 elif isinstance(ctx.request, EmbeddingCompletionRequest):
107 - renderer = self._get_completion_renderer()
108 - ctx.engine_prompts = await renderer.render_prompt(
109 - prompt_or_prompts=ctx.request.input,
110 - config=self._build_render_config(ctx.request),
103 + ctx.engine_prompts = await self._preprocess_completion(
104 + ctx.request,
105 + prompt_input=ctx.request.input,
106 + prompt_embeds=None,
107 )
108 else:
109 return self.create_error_response("Invalid classification request type")
@@ -117,19 +113,6 @@ class OpenAIServingEmbedding(OpenAIServing):
113 logger.exception("Error in preprocessing prompt inputs")
114 return self.create_error_response(str(e))
115
120 - def _build_render_config(self, request: EmbeddingCompletionRequest) -> RenderConfig:
121 - # Set max_length based on chunked processing capability
122 - if self._should_use_chunked_processing(request):
123 - max_length = None
124 - else:
125 - max_length = self.max_embed_len or self.max_model_len
126 -
127 - return RenderConfig(
128 - max_length=max_length,
129 - truncate_prompt_tokens=request.truncate_prompt_tokens,
130 - add_special_tokens=request.add_special_tokens,
131 - )
132 -
116 def _build_response(
117 self,
118 ctx: EmbeddingServeContext,
@@ -246,14 +229,18 @@ class OpenAIServingEmbedding(OpenAIServing):
229 lora_request=ctx.lora_request,
230 )
231
232 + tok_params = ctx.request.build_tok_params(self.model_config)
233 + tokenization_kwargs = tok_params.get_encode_kwargs()
234 +
235 # Create generator for this chunk and wrap it to return indices
236 original_generator = self.engine_client.encode(
237 chunk_engine_prompt,
238 pooling_params,
239 chunk_request_id,
240 lora_request=ctx.lora_request,
241 + tokenization_kwargs=tokenization_kwargs,
242 trace_headers=trace_headers,
256 - priority=getattr(ctx.request, "priority", 0),
243 + priority=ctx.request.priority,
244 )
245
246 generators.append(original_generator)
@@ -338,7 +325,7 @@ class OpenAIServingEmbedding(OpenAIServing):
325 async def _create_single_prompt_generator(
326 self,
327 ctx: EmbeddingServeContext,
341 - engine_prompt: TokensPrompt,
328 + engine_prompt: TokensPrompt | EmbedsPrompt,
329 pooling_params: PoolingParams,
330 trace_headers: Mapping[str, str] | None,
331 prompt_index: int,
@@ -353,23 +340,25 @@ class OpenAIServingEmbedding(OpenAIServing):
340 lora_request=ctx.lora_request,
341 )
342
343 + tok_params = ctx.request.build_tok_params(self.model_config)
344 + tokenization_kwargs = tok_params.get_encode_kwargs()
345 +
346 # Return the original generator without wrapping
347 return self.engine_client.encode(
348 engine_prompt,
349 pooling_params,
350 request_id_item,
351 lora_request=ctx.lora_request,
352 + tokenization_kwargs=tokenization_kwargs,
353 trace_headers=trace_headers,
363 - priority=getattr(ctx.request, "priority", 0),
354 + priority=ctx.request.priority,
355 )
356
357 async def _prepare_generators(
358 self,
368 - ctx: ServeContext,
359 + ctx: EmbeddingServeContext,
360 ) -> ErrorResponse | None:
361 """Override to support chunked processing."""
371 - ctx = cast(EmbeddingServeContext, ctx)
372 -
362 # Check if we should use chunked processing
363 use_chunked = self._should_use_chunked_processing(ctx.request)
364
@@ -405,7 +394,8 @@ class OpenAIServingEmbedding(OpenAIServing):
394 for i, engine_prompt in enumerate(ctx.engine_prompts):
395 # Check if this specific prompt needs chunked processing
396 if "prompt_token_ids" in engine_prompt:
408 - prompt_token_ids = engine_prompt["prompt_token_ids"]
397 + prompt_token_ids = engine_prompt["prompt_token_ids"] # type: ignore[typeddict-item]
398 +
399 if len(prompt_token_ids) > max_pos_embeddings:
400 # Use chunked processing for this prompt
401 chunk_generators = await self._process_chunked_request(
@@ -573,7 +563,7 @@ class OpenAIServingEmbedding(OpenAIServing):
563 "token IDs"
564 )
565
576 - original_token_ids = original_prompt["prompt_token_ids"]
566 + original_token_ids = original_prompt["prompt_token_ids"] # type: ignore[typeddict-item]
567
568 pooling_request_output = PoolingRequestOutput(
569 request_id=aggregator["request_id"],
vllm/entrypoints/pooling/pooling/protocol.py
+27 -3
@@ -3,11 +3,10 @@
3 import time
4 from typing import Any, Generic, TypeAlias, TypeVar
5
6 -from pydantic import (
7 - Field,
8 -)
6 +from pydantic import Field
7
8 from vllm import PoolingParams
9 +from vllm.config import ModelConfig
10 from vllm.config.pooler import get_use_activation
11 from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, UsageInfo
12 from vllm.entrypoints.pooling.base.protocol import (
@@ -18,6 +17,7 @@ from vllm.entrypoints.pooling.base.protocol import (
17 EncodingRequestMixin,
18 PoolingBasicRequestMixin,
19 )
20 +from vllm.renderers import TokenizeParams
21 from vllm.tasks import PoolingTask
22 from vllm.utils import random_uuid
23
@@ -30,6 +30,18 @@ class PoolingCompletionRequest(
30 ):
31 task: PoolingTask | None = None
32
33 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
34 + encoder_config = model_config.encoder_config or {}
35 +
36 + return TokenizeParams(
37 + max_total_tokens=model_config.max_model_len,
38 + max_output_tokens=0,
39 + truncate_prompt_tokens=self.truncate_prompt_tokens,
40 + do_lower_case=encoder_config.get("do_lower_case", False),
41 + add_special_tokens=self.add_special_tokens,
42 + max_total_tokens_param="max_model_len",
43 + )
44 +
45 def to_pooling_params(self):
46 return PoolingParams(
47 truncate_prompt_tokens=self.truncate_prompt_tokens,
@@ -48,6 +60,18 @@ class PoolingChatRequest(
60 description=("Additional kwargs to pass to the HF processor."),
61 )
62
63 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
64 + encoder_config = model_config.encoder_config or {}
65 +
66 + return TokenizeParams(
67 + max_total_tokens=model_config.max_model_len,
68 + max_output_tokens=0,
69 + truncate_prompt_tokens=self.truncate_prompt_tokens,
70 + do_lower_case=encoder_config.get("do_lower_case", False),
71 + add_special_tokens=self.add_special_tokens,
72 + max_total_tokens_param="max_model_len",
73 + )
74 +
75 def to_pooling_params(self):
76 return PoolingParams(
77 truncate_prompt_tokens=self.truncate_prompt_tokens,
vllm/entrypoints/pooling/pooling/serving.py
+16 -29
@@ -5,7 +5,7 @@ import asyncio
5 import json
6 import time
7 from collections.abc import AsyncGenerator, Sequence
8 -from typing import Final, cast
8 +from typing import Any, Final, cast
9
10 import jinja2
11 from fastapi import Request
@@ -14,10 +14,7 @@ from typing_extensions import assert_never
14 from vllm.engine.protocol import EngineClient
15 from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
16 from vllm.entrypoints.logger import RequestLogger
17 -from vllm.entrypoints.openai.engine.protocol import (
18 - ErrorResponse,
19 - UsageInfo,
20 -)
17 +from vllm.entrypoints.openai.engine.protocol import ErrorResponse, UsageInfo
18 from vllm.entrypoints.openai.engine.serving import OpenAIServing
19 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
20 from vllm.entrypoints.pooling.pooling.protocol import (
@@ -30,8 +27,6 @@ from vllm.entrypoints.pooling.pooling.protocol import (
27 PoolingResponse,
28 PoolingResponseData,
29 )
33 -from vllm.entrypoints.renderer import RenderConfig
34 -from vllm.entrypoints.utils import _validate_truncation_size
30 from vllm.logger import init_logger
31 from vllm.outputs import PoolingRequestOutput
32 from vllm.tasks import PoolingTask, SupportedTask
@@ -99,11 +94,6 @@ class OpenAIServingPooling(OpenAIServing):
94 "dimensions is currently not supported"
95 )
96
102 - truncate_prompt_tokens = getattr(request, "truncate_prompt_tokens", None)
103 - truncate_prompt_tokens = _validate_truncation_size(
104 - self.max_model_len, truncate_prompt_tokens
105 - )
106 -
97 if is_io_processor_request:
98 if self.io_processor is None:
99 raise ValueError(
@@ -134,19 +124,16 @@ class OpenAIServingPooling(OpenAIServing):
124
125 _, engine_prompts = await self._preprocess_chat(
126 request,
137 - self.renderer,
127 request.messages,
139 - chat_template=request.chat_template or self.chat_template,
140 - chat_template_content_format=self.chat_template_content_format,
141 - add_generation_prompt=request.add_generation_prompt,
142 - continue_final_message=request.continue_final_message,
143 - add_special_tokens=request.add_special_tokens,
128 + default_template=self.chat_template,
129 + default_template_content_format=self.chat_template_content_format,
130 + default_template_kwargs=None,
131 )
132 elif isinstance(request, PoolingCompletionRequest):
146 - renderer = self._get_completion_renderer()
147 - engine_prompts = await renderer.render_prompt(
148 - prompt_or_prompts=request.input,
149 - config=self._build_render_config(request),
133 + engine_prompts = await self._preprocess_completion(
134 + request,
135 + prompt_input=request.input,
136 + prompt_embeds=None,
137 )
138 else:
139 raise ValueError(f"Unsupported request of type {type(request)}")
@@ -207,11 +194,18 @@ class OpenAIServingPooling(OpenAIServing):
194 else await self._get_trace_headers(raw_request.headers)
195 )
196
197 + if is_io_processor_request:
198 + tokenization_kwargs: dict[str, Any] = {}
199 + else:
200 + tok_params = request.build_tok_params(self.model_config) # type: ignore
201 + tokenization_kwargs = tok_params.get_encode_kwargs()
202 +
203 generator = self.engine_client.encode(
204 engine_prompt,
205 pooling_params,
206 request_id_item,
207 lora_request=lora_request,
208 + tokenization_kwargs=tokenization_kwargs,
209 trace_headers=trace_headers,
210 priority=request.priority,
211 )
@@ -338,10 +332,3 @@ class OpenAIServingPooling(OpenAIServing):
332 return encode_bytes(bytes_only=encoding_format == "bytes_only")
333 else:
334 assert_never(encoding_format)
341 -
342 - def _build_render_config(self, request: PoolingCompletionRequest) -> RenderConfig:
343 - return RenderConfig(
344 - max_length=self.max_model_len,
345 - truncate_prompt_tokens=request.truncate_prompt_tokens,
346 - add_special_tokens=request.add_special_tokens,
347 - )
vllm/entrypoints/pooling/score/protocol.py
+25 -4
@@ -3,12 +3,10 @@
3 import time
4 from typing import Any, TypeAlias
5
6 -from pydantic import (
7 - BaseModel,
8 - Field,
9 -)
6 +from pydantic import BaseModel, Field
7
8 from vllm import PoolingParams
9 +from vllm.config import ModelConfig
10 from vllm.config.pooler import get_use_activation
11 from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, UsageInfo
12 from vllm.entrypoints.pooling.base.protocol import (
@@ -19,6 +17,7 @@ from vllm.entrypoints.pooling.score.utils import (
17 ScoreContentPartParam,
18 ScoreMultiModalParam,
19 )
20 +from vllm.renderers import TokenizeParams
21 from vllm.utils import random_uuid
22
23
@@ -30,6 +29,17 @@ class ScoreRequestMixin(PoolingBasicRequestMixin, ClassifyRequestMixin):
29 )
30 # --8<-- [end:score-extra-params]
31
32 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
33 + encoder_config = model_config.encoder_config or {}
34 +
35 + return TokenizeParams(
36 + max_total_tokens=model_config.max_model_len,
37 + max_output_tokens=0,
38 + truncate_prompt_tokens=self.truncate_prompt_tokens,
39 + do_lower_case=encoder_config.get("do_lower_case", False),
40 + max_total_tokens_param="max_model_len",
41 + )
42 +
43 def to_pooling_params(self):
44 return PoolingParams(
45 truncate_prompt_tokens=self.truncate_prompt_tokens,
@@ -85,6 +95,17 @@ class RerankRequest(PoolingBasicRequestMixin, ClassifyRequestMixin):
95 )
96 # --8<-- [end:rerank-extra-params]
97
98 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
99 + encoder_config = model_config.encoder_config or {}
100 +
101 + return TokenizeParams(
102 + max_total_tokens=model_config.max_model_len,
103 + max_output_tokens=0,
104 + truncate_prompt_tokens=self.truncate_prompt_tokens,
105 + do_lower_case=encoder_config.get("do_lower_case", False),
106 + max_total_tokens_param="max_model_len",
107 + )
108 +
109
110 class RerankDocument(BaseModel):
111 text: str | None = None
vllm/entrypoints/pooling/score/serving.py
+18 -29
@@ -34,7 +34,6 @@ from vllm.entrypoints.pooling.score.utils import (
34 compress_token_type_ids,
35 get_score_prompt,
36 )
37 -from vllm.entrypoints.utils import _validate_truncation_size
37 from vllm.inputs.data import TokensPrompt
38 from vllm.logger import init_logger
39 from vllm.lora.request import LoRARequest
@@ -68,31 +67,31 @@ class ServingScores(OpenAIServing):
67
68 async def _embedding_score(
69 self,
71 - tokenizer: TokenizerLike,
70 data_1: list[str],
71 data_2: list[str],
72 request: RerankRequest | ScoreRequest,
73 request_id: str,
76 - tokenization_kwargs: dict[str, Any] | None = None,
74 lora_request: LoRARequest | None | None = None,
75 trace_headers: Mapping[str, str] | None = None,
76 ) -> list[PoolingRequestOutput] | ErrorResponse:
80 - input_texts = data_1 + data_2
77 + model_config = self.model_config
78 + tokenizer = self.renderer.get_tokenizer()
79
82 - engine_prompts: list[TokensPrompt] = []
83 - tokenize_async = make_async(
84 - tokenizer.__call__, executor=self._tokenizer_executor
80 + encode_async = make_async(
81 + tokenizer.encode,
82 + executor=self._tokenizer_executor,
83 )
84
87 - tokenization_kwargs = tokenization_kwargs or {}
85 + input_texts = data_1 + data_2
86 +
87 + tokenization_kwargs = request.build_tok_params(model_config).get_encode_kwargs()
88 tokenized_prompts = await asyncio.gather(
89 - *(tokenize_async(t, **tokenization_kwargs) for t in input_texts)
89 + *(encode_async(t, **tokenization_kwargs) for t in input_texts)
90 )
91
92 + engine_prompts: list[TokensPrompt] = []
93 for tok_result, input_text in zip(tokenized_prompts, input_texts):
93 - text_token_prompt = self._validate_input(
94 - request, tok_result["input_ids"], input_text
95 - )
94 + text_token_prompt = self._validate_input(request, tok_result, input_text)
95
96 engine_prompts.append(
97 TokensPrompt(prompt_token_ids=text_token_prompt["prompt_token_ids"])
@@ -184,15 +183,16 @@ class ServingScores(OpenAIServing):
183
184 async def _cross_encoding_score(
185 self,
187 - tokenizer: TokenizerLike,
186 data_1: list[str] | list[ScoreContentPartParam],
187 data_2: list[str] | list[ScoreContentPartParam],
188 request: RerankRequest | ScoreRequest,
189 request_id: str,
192 - tokenization_kwargs: dict[str, Any] | None = None,
190 lora_request: LoRARequest | None | None = None,
191 trace_headers: Mapping[str, str] | None = None,
192 ) -> list[PoolingRequestOutput] | ErrorResponse:
193 + model_config = self.model_config
194 + tokenizer = self.renderer.get_tokenizer()
195 +
196 request_prompts: list[str] = []
197 engine_prompts: list[TokensPrompt] = []
198
@@ -202,12 +202,13 @@ class ServingScores(OpenAIServing):
202 if isinstance(tokenizer, MistralTokenizer):
203 raise ValueError("MistralTokenizer not supported for cross-encoding")
204
205 - tokenization_kwargs = tokenization_kwargs or {}
205 + tok_kwargs = request.build_tok_params(model_config).get_encode_kwargs()
206
207 input_pairs = [(t1, t2) for t1, t2 in zip(data_1, data_2)]
208
209 preprocess_async = make_async(
210 - self._preprocess_score, executor=self._tokenizer_executor
210 + self._preprocess_score,
211 + executor=self._tokenizer_executor,
212 )
213
214 preprocessed_prompts = await asyncio.gather(
@@ -215,7 +216,7 @@ class ServingScores(OpenAIServing):
216 preprocess_async(
217 request=request,
218 tokenizer=tokenizer,
218 - tokenization_kwargs=tokenization_kwargs,
219 + tokenization_kwargs=tok_kwargs,
220 data_1=t1,
221 data_2=t2,
222 )
@@ -286,14 +287,6 @@ class ServingScores(OpenAIServing):
287 raw_request: Request | None = None,
288 ) -> list[PoolingRequestOutput] | ErrorResponse:
289 lora_request = self._maybe_get_adapters(request)
289 - tokenizer = self.renderer.get_tokenizer()
290 -
291 - truncate_prompt_tokens = getattr(request, "truncate_prompt_tokens", None)
292 -
293 - tokenization_kwargs: dict[str, Any] = {}
294 - _validate_truncation_size(
295 - self.max_model_len, truncate_prompt_tokens, tokenization_kwargs
296 - )
290
291 trace_headers = (
292 None
@@ -322,24 +315,20 @@ class ServingScores(OpenAIServing):
315
316 if self.model_config.is_cross_encoder:
317 return await self._cross_encoding_score(
325 - tokenizer=tokenizer,
318 data_1=data_1, # type: ignore[arg-type]
319 data_2=data_2, # type: ignore[arg-type]
320 request=request,
321 request_id=request_id,
330 - tokenization_kwargs=tokenization_kwargs,
322 lora_request=lora_request,
323 trace_headers=trace_headers,
324 )
325
326 else:
327 return await self._embedding_score(
337 - tokenizer=tokenizer,
328 data_1=data_1, # type: ignore[arg-type]
329 data_2=data_2, # type: ignore[arg-type]
330 request=request,
331 request_id=request_id,
342 - tokenization_kwargs=tokenization_kwargs,
332 lora_request=lora_request,
333 trace_headers=trace_headers,
334 )
vllm/entrypoints/renderer.py deleted
-411
@@ -1,411 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -
4 -import asyncio
5 -import io
6 -from abc import ABC, abstractmethod
7 -from dataclasses import dataclass
8 -from typing import Annotated
9 -
10 -import pybase64
11 -import torch
12 -from pydantic import Field
13 -
14 -from vllm.config import ModelConfig
15 -from vllm.exceptions import VLLMValidationError
16 -from vllm.inputs.data import EmbedsPrompt, TextPrompt, TokensPrompt
17 -from vllm.inputs.parse import get_prompt_components, parse_raw_prompts
18 -from vllm.tokenizers import TokenizerLike
19 -from vllm.utils.async_utils import AsyncMicrobatchTokenizer
20 -
21 -
22 -@dataclass(frozen=True)
23 -class RenderConfig:
24 - """Configuration to control how prompts are prepared."""
25 -
26 - max_length: int | None = None
27 - """Maximum allowable total input token length. If provided,
28 - token inputs longer than this raise `ValueError`."""
29 -
30 - truncate_prompt_tokens: int | None = None
31 - """Number of tokens to keep. `None` means no truncation.
32 - `0` yields an empty list (and skips embeds).
33 - `-1` maps to `model_config.max_model_len`."""
34 -
35 - add_special_tokens: bool = True
36 - """Whether to add model-specific special tokens during tokenization."""
37 -
38 - cache_salt: str | None = None
39 - """String to disambiguate prefix cache entries."""
40 -
41 - needs_detokenization: bool | None = False
42 - """If True, detokenize IDs back to text for inclusion in outputs."""
43 -
44 - def verify_truncate_prompt_tokens(self, model_config: ModelConfig) -> int | None:
45 - """Validate and normalize `truncate_prompt_tokens` parameter."""
46 - truncate_prompt_tokens = self.truncate_prompt_tokens
47 - if truncate_prompt_tokens is None or truncate_prompt_tokens == 0:
48 - return truncate_prompt_tokens
49 -
50 - if truncate_prompt_tokens < 0:
51 - truncate_prompt_tokens = model_config.max_model_len
52 -
53 - max_length = self.max_length
54 - if max_length is not None and truncate_prompt_tokens > max_length: # type: ignore[operator]
55 - raise ValueError(
56 - f"{truncate_prompt_tokens=} cannot be greater than "
57 - f"{max_length=}. Please select a smaller truncation size."
58 - )
59 -
60 - return truncate_prompt_tokens
61 -
62 -
63 -class BaseRenderer(ABC):
64 - """
65 - Base class for unified input processing and rendering.
66 -
67 - The Renderer serves as a unified input processor that consolidates
68 - tokenization, chat template formatting, and multimodal input handling
69 - into a single component.
70 - It converts high-level API requests (OpenAI-style JSON) into token IDs and
71 - multimodal features ready for engine consumption.
72 -
73 - Key responsibilities:
74 - - Convert text prompts to token sequences with proper special tokens
75 - - Apply chat templates and format conversations
76 - - Handle multimodal inputs (images, audio, etc.) when applicable
77 - - Manage prompt truncation and length validation
78 - - Provide clean separation between API layer and engine core
79 - """
80 -
81 - def __init__(
82 - self,
83 - model_config: ModelConfig,
84 - tokenizer: TokenizerLike | None = None,
85 - ):
86 - super().__init__()
87 - self.model_config = model_config
88 - self.tokenizer = tokenizer
89 -
90 - @abstractmethod
91 - async def render_prompt(
92 - self,
93 - *,
94 - prompt_or_prompts: str | list[str] | list[int] | list[list[int]],
95 - config: RenderConfig,
96 - ) -> list[TokensPrompt]:
97 - """
98 - Convert text or token inputs into engine-ready TokensPrompt objects.
99 -
100 - This method accepts text or token inputs and produces a
101 - list of [`TokensPrompt`][vllm.inputs.data.TokensPrompt] objects
102 - for the engine.
103 -
104 - Args:
105 - prompt_or_prompts: One of:
106 - - `str`: Single text prompt.
107 - - `list[str]`: Batch of text prompts.
108 - - `list[int]`: Single pre-tokenized sequence.
109 - - `list[list[int]]`: Batch of pre-tokenized sequences.
110 - config: Render configuration controlling how prompts are prepared
111 - (e.g., tokenization and length handling).
112 -
113 - Returns:
114 - list[TokensPrompt]: Engine-ready token prompts.
115 -
116 - Raises:
117 - ValueError: If input formats are invalid or length limits exceeded.
118 - """
119 - raise NotImplementedError
120 -
121 - @abstractmethod
122 - async def render_prompt_and_embeds(
123 - self,
124 - *,
125 - prompt_or_prompts: str | list[str] | list[int] | list[list[int]] | None = None,
126 - prompt_embeds: bytes | list[bytes] | None = None,
127 - config: RenderConfig,
128 - ) -> list[TokensPrompt | EmbedsPrompt]:
129 - """
130 - Convert text/token and/or base64-encoded embeddings inputs into
131 - engine-ready prompt objects using a unified RenderConfig.
132 -
133 - At least one of `prompt_or_prompts` or `prompt_embeds` must be
134 - provided and non-empty. If both are omitted or empty (e.g., empty
135 - string and empty list), a `ValueError` is raised.
136 -
137 - Args:
138 - prompt_or_prompts: Text or token inputs to include.
139 - prompt_embeds: Base64-encoded bytes (or list thereof) containing a
140 - torch-saved tensor to be used as prompt embeddings.
141 - config: Render configuration controlling how prompts are prepared
142 - (e.g., tokenization and length handling).
143 -
144 - Returns:
145 - list[Union[TokensPrompt, EmbedsPrompt]]:
146 - Engine-ready prompt objects.
147 -
148 - Raises:
149 - ValueError: If both `prompt_or_prompts` and `prompt_embeds`
150 - are omitted or empty (decoder prompt cannot be empty), or if
151 - length limits are exceeded.
152 - """
153 - raise NotImplementedError
154 -
155 - def load_prompt_embeds(
156 - self,
157 - prompt_embeds: bytes | list[bytes],
158 - truncate_prompt_tokens: Annotated[int, Field(ge=0)] | None = None,
159 - cache_salt: str | None = None,
160 - ) -> list[EmbedsPrompt]:
161 - """Load and validate base64-encoded embeddings into prompt objects."""
162 - if not self.model_config.enable_prompt_embeds:
163 - raise VLLMValidationError(
164 - "You must set `--enable-prompt-embeds` to input `prompt_embeds`.",
165 - parameter="prompt_embeds",
166 - )
167 -
168 - def _load_and_validate_embed(embed: bytes) -> EmbedsPrompt:
169 - # Enable sparse tensor integrity checks to prevent out-of-bounds
170 - # writes from maliciously crafted tensors
171 - with torch.sparse.check_sparse_tensor_invariants():
172 - tensor = torch.load(
173 - io.BytesIO(pybase64.b64decode(embed, validate=True)),
174 - weights_only=True,
175 - map_location=torch.device("cpu"),
176 - )
177 - assert isinstance(tensor, torch.Tensor) and tensor.dtype in (
178 - torch.float32,
179 - torch.bfloat16,
180 - torch.float16,
181 - )
182 - tensor = tensor.to_dense()
183 - if tensor.dim() > 2:
184 - tensor = tensor.squeeze(0)
185 - assert tensor.dim() == 2
186 - if truncate_prompt_tokens is not None:
187 - tensor = tensor[-truncate_prompt_tokens:]
188 - embeds_prompt = EmbedsPrompt(prompt_embeds=tensor)
189 - if cache_salt is not None:
190 - embeds_prompt["cache_salt"] = cache_salt
191 - return embeds_prompt
192 -
193 - if isinstance(prompt_embeds, list):
194 - return [_load_and_validate_embed(embed) for embed in prompt_embeds]
195 -
196 - return [_load_and_validate_embed(prompt_embeds)]
197 -
198 -
199 -class CompletionRenderer(BaseRenderer):
200 - def __init__(
201 - self,
202 - model_config: ModelConfig,
203 - tokenizer: TokenizerLike | None = None,
204 - async_tokenizer_pool: dict[TokenizerLike, AsyncMicrobatchTokenizer]
205 - | None = None,
206 - ):
207 - super().__init__(model_config, tokenizer)
208 - self.async_tokenizer_pool = async_tokenizer_pool
209 - self.async_tokenizer: AsyncMicrobatchTokenizer | None = None
210 -
211 - async def render_prompt(
212 - self,
213 - *,
214 - prompt_or_prompts: str | list[str] | list[int] | list[list[int]],
215 - config: RenderConfig,
216 - ) -> list[TokensPrompt]:
217 - """Implementation of prompt rendering for completion-style requests.
218 -
219 - Uses async tokenizer pooling for improved performance. See base class
220 - for detailed parameter documentation.
221 - """
222 - truncate_prompt_tokens = config.verify_truncate_prompt_tokens(self.model_config)
223 - if truncate_prompt_tokens == 0:
224 - return []
225 -
226 - tasks = (
227 - self._create_prompt(
228 - prompt_input,
229 - config=config,
230 - truncate_prompt_tokens=truncate_prompt_tokens,
231 - )
232 - for prompt_input in parse_raw_prompts(prompt_or_prompts)
233 - )
234 -
235 - return await asyncio.gather(*tasks)
236 -
237 - async def render_prompt_and_embeds(
238 - self,
239 - *,
240 - prompt_or_prompts: str | list[str] | list[int] | list[list[int]] | None = None,
241 - prompt_embeds: bytes | list[bytes] | None = None,
242 - config: RenderConfig,
243 - ) -> list[TokensPrompt | EmbedsPrompt]:
244 - """
245 - Render text/token prompts and/or precomputed embedding prompts. At
246 - least one of `prompt_or_prompts` or `prompt_embeds` must be provided.
247 - """
248 - truncate_prompt_tokens = config.verify_truncate_prompt_tokens(self.model_config)
249 - if truncate_prompt_tokens == 0:
250 - return []
251 -
252 - rendered: list[TokensPrompt | EmbedsPrompt] = []
253 -
254 - if prompt_embeds is not None:
255 - rendered.extend(
256 - self.load_prompt_embeds(
257 - prompt_embeds, truncate_prompt_tokens, config.cache_salt
258 - )
259 - )
260 - if prompt_or_prompts is None or prompt_or_prompts == "":
261 - return rendered
262 -
263 - token_prompts = await self.render_prompt(
264 - prompt_or_prompts=prompt_or_prompts,
265 - config=config,
266 - )
267 - rendered.extend(token_prompts)
268 -
269 - return rendered
270 -
271 - def _maybe_apply_truncation(
272 - self, token_ids: list[int], truncate_prompt_tokens: int | None
273 - ) -> list[int]:
274 - """Apply truncation to token sequence."""
275 - if truncate_prompt_tokens is None:
276 - return token_ids
277 - if truncate_prompt_tokens >= len(token_ids):
278 - return token_ids
279 -
280 - return token_ids[-truncate_prompt_tokens:]
281 -
282 - async def _create_prompt(
283 - self,
284 - prompt_input: TextPrompt | TokensPrompt,
285 - config: RenderConfig,
286 - truncate_prompt_tokens: int | None,
287 - ) -> TokensPrompt:
288 - prompt, prompt_token_ids, _ = get_prompt_components(prompt_input)
289 -
290 - if prompt_token_ids is not None:
291 - # NOTE: detokenization is needed when echo is enabled,
292 - # where the input token IDs are decoded back to text.
293 - return await self._create_prompt_from_token_ids(
294 - prompt_token_ids,
295 - config.max_length,
296 - truncate_prompt_tokens,
297 - config.cache_salt,
298 - config.needs_detokenization,
299 - )
300 -
301 - if prompt is not None:
302 - return await self._create_prompt_from_text(
303 - prompt,
304 - config.max_length,
305 - truncate_prompt_tokens,
306 - config.add_special_tokens,
307 - config.cache_salt,
308 - )
309 -
310 - # TODO: Also handle embeds prompt using this method
311 - raise NotImplementedError
312 -
313 - async def _create_prompt_from_text(
314 - self,
315 - text: str,
316 - max_length: int | None,
317 - truncate_prompt_tokens: int | None,
318 - add_special_tokens: bool,
319 - cache_salt: str | None,
320 - ) -> TokensPrompt:
321 - """Tokenize text input asynchronously."""
322 - async_tokenizer = self._get_async_tokenizer()
323 -
324 - # Handle encoder-specific preprocessing
325 - if (
326 - self.model_config.encoder_config is not None
327 - and self.model_config.encoder_config.get("do_lower_case", False)
328 - ):
329 - text = text.lower()
330 -
331 - # Tokenize texts
332 - if truncate_prompt_tokens is None:
333 - encoded = await async_tokenizer(text, add_special_tokens=add_special_tokens)
334 - else:
335 - encoded = await async_tokenizer(
336 - text,
337 - add_special_tokens=add_special_tokens,
338 - truncation=True,
339 - max_length=truncate_prompt_tokens,
340 - )
341 -
342 - return self._create_tokens_prompt(
343 - encoded.input_ids, max_length, cache_salt, text
344 - )
345 -
346 - async def _create_prompt_from_token_ids(
347 - self,
348 - token_ids: list[int],
349 - max_length: int | None,
350 - truncate_prompt_tokens: int | None,
351 - cache_salt: str | None,
352 - needs_detokenization: bool | None = False,
353 - ) -> TokensPrompt:
354 - """Optionally detokenize token IDs and build a tokens prompt."""
355 - token_ids = self._maybe_apply_truncation(token_ids, truncate_prompt_tokens)
356 -
357 - prompt = None
358 - if needs_detokenization:
359 - async_tokenizer = self._get_async_tokenizer()
360 - prompt = await async_tokenizer.decode(token_ids)
361 -
362 - return self._create_tokens_prompt(
363 - token_ids=token_ids,
364 - max_length=max_length,
365 - cache_salt=cache_salt,
366 - prompt=prompt,
367 - )
368 -
369 - def _get_async_tokenizer(self) -> AsyncMicrobatchTokenizer:
370 - """Get or create async tokenizer using shared pool."""
371 - async_tokenizer = self.async_tokenizer
372 - if async_tokenizer is not None:
373 - return async_tokenizer
374 -
375 - tokenizer = self.tokenizer
376 - if tokenizer is None:
377 - raise ValueError("No tokenizer available for text input processing")
378 -
379 - if self.async_tokenizer_pool is None:
380 - async_tokenizer = AsyncMicrobatchTokenizer(tokenizer)
381 - else:
382 - async_tokenizer = self.async_tokenizer_pool.get(tokenizer)
383 - if async_tokenizer is None:
384 - async_tokenizer = AsyncMicrobatchTokenizer(tokenizer)
385 - self.async_tokenizer_pool[tokenizer] = async_tokenizer
386 - self.async_tokenizer = async_tokenizer
387 - return async_tokenizer
388 -
389 - def _create_tokens_prompt(
390 - self,
391 - token_ids: list[int],
392 - max_length: int | None = None,
393 - cache_salt: str | None = None,
394 - prompt: str | None = None,
395 - ) -> TokensPrompt:
396 - """Create validated TokensPrompt."""
397 - if max_length is not None and len(token_ids) > max_length:
398 - raise VLLMValidationError(
399 - f"This model's maximum context length is {max_length} tokens. "
400 - f"However, your request has {len(token_ids)} input tokens. "
401 - "Please reduce the length of the input messages.",
402 - parameter="input_tokens",
403 - value=len(token_ids),
404 - )
405 -
406 - tokens_prompt = TokensPrompt(prompt_token_ids=token_ids)
407 - if cache_salt is not None:
408 - tokens_prompt["cache_salt"] = cache_salt
409 - if prompt is not None:
410 - tokens_prompt["prompt"] = prompt
411 - return tokens_prompt
vllm/entrypoints/serve/disagg/protocol.py
+8
@@ -4,12 +4,14 @@ from typing import Any
4
5 from pydantic import BaseModel, Field
6
7 +from vllm.config import ModelConfig
8 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionLogProbs
9 from vllm.entrypoints.openai.engine.protocol import (
10 SamplingParams,
11 StreamOptions,
12 )
13 from vllm.logprobs import Logprob
14 +from vllm.renderers import TokenizeParams
15 from vllm.utils import random_uuid
16
17
@@ -62,6 +64,12 @@ class GenerateRequest(BaseModel):
64 description="KVTransfer parameters used for disaggregated serving.",
65 )
66
67 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
68 + return TokenizeParams(
69 + max_total_tokens=None,
70 + max_output_tokens=0,
71 + )
72 +
73
74 class GenerateResponseChoice(BaseModel):
75 index: int
vllm/entrypoints/serve/disagg/serving.py
+11 -6
@@ -101,12 +101,13 @@ class ServingTokens(OpenAIServing):
101
102 # TODO(NickLucche): Change to EngineCoreRequest once Renderer work is
103 # completed
104 - engine_prompt = TokensPrompt(prompt_token_ids=request.token_ids)
105 - if request.features is not None:
106 - engine_prompt["multi_modal_data"] = None
107 -
108 - if hasattr(request, "cache_salt") and request.cache_salt is not None:
109 - engine_prompt["cache_salt"] = request.cache_salt
104 + engine_prompts = await self._preprocess_completion(
105 + request,
106 + prompt_input=request.token_ids,
107 + prompt_embeds=None,
108 + )
109 + assert len(engine_prompts) == 1
110 + engine_prompt = engine_prompts[0]
111
112 # Schedule the request and get the result generator.
113 result_generator: AsyncGenerator[RequestOutput, None] | None = None
@@ -128,11 +129,15 @@ class ServingTokens(OpenAIServing):
129 else await self._get_trace_headers(raw_request.headers)
130 )
131
132 + tok_params = request.build_tok_params(self.model_config)
133 + tokenization_kwargs = tok_params.get_encode_kwargs()
134 +
135 result_generator = self.engine_client.generate(
136 engine_prompt,
137 sampling_params,
138 request_id,
139 lora_request=lora_request,
140 + tokenization_kwargs=tokenization_kwargs,
141 trace_headers=trace_headers,
142 priority=request.priority,
143 )
vllm/entrypoints/serve/tokenize/protocol.py
+41
@@ -6,8 +6,10 @@ from typing import Any, TypeAlias
6
7 from pydantic import ConfigDict, Field, model_validator
8
9 +from vllm.config import ModelConfig
10 from vllm.entrypoints.chat_utils import (
11 ChatCompletionMessageParam,
12 + ChatTemplateContentFormatOption,
13 )
14 from vllm.entrypoints.openai.chat_completion.protocol import (
15 ChatCompletionToolsParam,
@@ -15,6 +17,7 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
17 from vllm.entrypoints.openai.engine.protocol import (
18 OpenAIBaseModel,
19 )
20 +from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
21
22
23 class TokenizeCompletionRequest(OpenAIBaseModel):
@@ -35,6 +38,13 @@ class TokenizeCompletionRequest(OpenAIBaseModel):
38 ),
39 )
40
41 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
42 + return TokenizeParams(
43 + max_total_tokens=None,
44 + max_output_tokens=0,
45 + add_special_tokens=self.add_special_tokens,
46 + )
47 +
48
49 class TokenizeChatRequest(OpenAIBaseModel):
50 model: str | None = None
@@ -109,6 +119,30 @@ class TokenizeChatRequest(OpenAIBaseModel):
119 )
120 return data
121
122 + def build_chat_params(
123 + self,
124 + default_template: str | None,
125 + default_template_content_format: ChatTemplateContentFormatOption,
126 + ) -> ChatParams:
127 + return ChatParams(
128 + chat_template=self.chat_template or default_template,
129 + chat_template_content_format=default_template_content_format,
130 + chat_template_kwargs=merge_kwargs(
131 + self.chat_template_kwargs,
132 + dict(
133 + add_generation_prompt=self.add_generation_prompt,
134 + continue_final_message=self.continue_final_message,
135 + ),
136 + ),
137 + )
138 +
139 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
140 + return TokenizeParams(
141 + max_total_tokens=None,
142 + max_output_tokens=0,
143 + add_special_tokens=self.add_special_tokens,
144 + )
145 +
146
147 TokenizeRequest: TypeAlias = TokenizeCompletionRequest | TokenizeChatRequest
148
@@ -124,6 +158,13 @@ class DetokenizeRequest(OpenAIBaseModel):
158 model: str | None = None
159 tokens: list[int]
160
161 + def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
162 + return TokenizeParams(
163 + max_total_tokens=None,
164 + max_output_tokens=0,
165 + needs_detokenization=True,
166 + )
167 +
168
169 class DetokenizeResponse(OpenAIBaseModel):
170 prompt: str
vllm/entrypoints/serve/tokenize/serving.py
+19 -28
@@ -9,12 +9,9 @@ from fastapi import Request
9 from vllm.engine.protocol import EngineClient
10 from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
11 from vllm.entrypoints.logger import RequestLogger
12 -from vllm.entrypoints.openai.engine.protocol import (
13 - ErrorResponse,
14 -)
12 +from vllm.entrypoints.openai.engine.protocol import ErrorResponse
13 from vllm.entrypoints.openai.engine.serving import OpenAIServing
14 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
17 -from vllm.entrypoints.renderer import RenderConfig
15 from vllm.entrypoints.serve.tokenize.protocol import (
16 DetokenizeRequest,
17 DetokenizeResponse,
@@ -83,21 +80,17 @@ class OpenAIServingTokenization(OpenAIServing):
80
81 _, engine_prompts = await self._preprocess_chat(
82 request,
86 - self.renderer,
83 request.messages,
84 + default_template=self.chat_template,
85 + default_template_content_format=self.chat_template_content_format,
86 + default_template_kwargs=None,
87 tool_dicts=tool_dicts,
89 - chat_template=request.chat_template or self.chat_template,
90 - chat_template_content_format=self.chat_template_content_format,
91 - add_generation_prompt=request.add_generation_prompt,
92 - continue_final_message=request.continue_final_message,
93 - chat_template_kwargs=request.chat_template_kwargs,
94 - add_special_tokens=request.add_special_tokens,
88 )
89 else:
97 - renderer = self._get_completion_renderer()
98 - engine_prompts = await renderer.render_prompt(
99 - prompt_or_prompts=request.prompt,
100 - config=self._build_render_config(request),
90 + engine_prompts = await self._preprocess_completion(
91 + request,
92 + prompt_input=request.prompt,
93 + prompt_embeds=None,
94 )
95 except (ValueError, TypeError, jinja2.TemplateError) as e:
96 logger.exception("Error in preprocessing prompt inputs")
@@ -106,11 +99,14 @@ class OpenAIServingTokenization(OpenAIServing):
99 input_ids: list[int] = []
100 for engine_prompt in engine_prompts:
101 self._log_inputs(
109 - request_id, engine_prompt, params=None, lora_request=lora_request
102 + request_id,
103 + engine_prompt,
104 + params=None,
105 + lora_request=lora_request,
106 )
107
112 - if isinstance(engine_prompt, dict) and "prompt_token_ids" in engine_prompt:
113 - input_ids.extend(engine_prompt["prompt_token_ids"])
108 + if "prompt_token_ids" in engine_prompt:
109 + input_ids.extend(engine_prompt["prompt_token_ids"]) # type: ignore[typeddict-item]
110
111 token_strs = None
112 if request.return_token_strs:
@@ -136,7 +132,6 @@ class OpenAIServingTokenization(OpenAIServing):
132 request_id = f"tokenize-{self._base_request_id(raw_request)}"
133
134 lora_request = self._maybe_get_adapters(request)
139 - tokenizer = self.renderer.get_tokenizer()
135
136 self._log_inputs(
137 request_id,
@@ -145,14 +140,13 @@ class OpenAIServingTokenization(OpenAIServing):
140 lora_request=lora_request,
141 )
142
148 - prompt_input = await self._tokenize_prompt_input_async(
149 - request,
150 - tokenizer,
151 - request.tokens,
143 + engine_prompt = await self.renderer.tokenize_prompt_async(
144 + TokensPrompt(prompt_token_ids=request.tokens),
145 + request.build_tok_params(self.model_config),
146 )
153 - input_text = prompt_input["prompt"]
147 + prompt_text = engine_prompt["prompt"] # type: ignore[typeddict-item]
148
155 - return DetokenizeResponse(prompt=input_text)
149 + return DetokenizeResponse(prompt=prompt_text)
150
151 async def get_tokenizer_info(
152 self,
@@ -165,9 +159,6 @@ class OpenAIServingTokenization(OpenAIServing):
159 except Exception as e:
160 return self.create_error_response(f"Failed to get tokenizer info: {str(e)}")
161
168 - def _build_render_config(self, request: TokenizeRequest) -> RenderConfig:
169 - return RenderConfig(add_special_tokens=request.add_special_tokens)
170 -
162
163 @dataclass
164 class TokenizerInfo:
vllm/entrypoints/utils.py
+4 -36
@@ -8,7 +8,7 @@ import os
8 from argparse import Namespace
9 from logging import Logger
10 from string import Template
11 -from typing import TYPE_CHECKING, Any
11 +from typing import TYPE_CHECKING
12
13 import regex as re
14 from fastapi import Request
@@ -18,9 +18,9 @@ from starlette.background import BackgroundTask, BackgroundTasks
18 from vllm import envs
19 from vllm.engine.arg_utils import EngineArgs
20 from vllm.inputs import EmbedsPrompt, TokensPrompt
21 +from vllm.inputs.parse import get_prompt_len
22 from vllm.logger import current_formatter_type, init_logger
23 from vllm.platforms import current_platform
23 -from vllm.utils import length_from_prompt_token_ids_or_embeds
24 from vllm.utils.argparse_utils import FlexibleArgumentParser
25
26 if TYPE_CHECKING:
@@ -34,9 +34,7 @@ if TYPE_CHECKING:
34 StreamOptions,
35 )
36 from vllm.entrypoints.openai.models.protocol import LoRAModulePath
37 - from vllm.entrypoints.openai.responses.protocol import (
38 - ResponsesRequest,
39 - )
37 + from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
38 else:
39 ChatCompletionRequest = object
40 CompletionRequest = object
@@ -188,33 +186,6 @@ def cli_env_setup():
186 os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
187
188
191 -def _validate_truncation_size(
192 - max_model_len: int,
193 - truncate_prompt_tokens: int | None,
194 - tokenization_kwargs: dict[str, Any] | None = None,
195 -) -> int | None:
196 - if truncate_prompt_tokens is not None:
197 - if truncate_prompt_tokens <= -1:
198 - truncate_prompt_tokens = max_model_len
199 -
200 - if truncate_prompt_tokens > max_model_len:
201 - raise ValueError(
202 - f"truncate_prompt_tokens value ({truncate_prompt_tokens}) "
203 - f"is greater than max_model_len ({max_model_len})."
204 - f" Please, select a smaller truncation size."
205 - )
206 -
207 - if tokenization_kwargs is not None:
208 - tokenization_kwargs["truncation"] = True
209 - tokenization_kwargs["max_length"] = truncate_prompt_tokens
210 -
211 - else:
212 - if tokenization_kwargs is not None:
213 - tokenization_kwargs["truncation"] = False
214 -
215 - return truncate_prompt_tokens
216 -
217 -
189 def get_max_tokens(
190 max_model_len: int,
191 request: "CompletionRequest | ChatCompletionRequest | ResponsesRequest",
@@ -233,10 +204,7 @@ def get_max_tokens(
204 # CompletionRequest (also a fallback for ChatCompletionRequest)
205 max_tokens = getattr(request, "max_tokens", None)
206
236 - input_length = length_from_prompt_token_ids_or_embeds(
237 - prompt.get("prompt_token_ids"), # type: ignore[arg-type]
238 - prompt.get("prompt_embeds"), # type: ignore[arg-type]
239 - )
207 + input_length = get_prompt_len(prompt)
208 default_max_tokens = max_model_len - input_length
209 max_output_tokens = current_platform.get_max_output_tokens(input_length)
210
vllm/inputs/data.py
+12 -42
@@ -21,12 +21,7 @@ else:
21 MultiModalUUIDDict = object
22
23
24 -class TextPrompt(TypedDict):
25 - """Schema for a text prompt."""
26 -
27 - prompt: str
28 - """The input text to be tokenized before passing to the model."""
29 -
24 +class _CommonKeys(TypedDict):
25 multi_modal_data: NotRequired[MultiModalDataDict | None]
26 """
27 Optional multi-modal data to pass to the model,
@@ -56,7 +51,14 @@ class TextPrompt(TypedDict):
51 """
52
53
59 -class TokensPrompt(TypedDict):
54 +class TextPrompt(_CommonKeys):
55 + """Schema for a text prompt."""
56 +
57 + prompt: str
58 + """The input text to be tokenized before passing to the model."""
59 +
60 +
61 +class TokensPrompt(_CommonKeys):
62 """Schema for a tokenized prompt."""
63
64 prompt_token_ids: list[int]
@@ -68,47 +70,15 @@ class TokensPrompt(TypedDict):
70 token_type_ids: NotRequired[list[int]]
71 """A list of token type IDs to pass to the cross encoder model."""
72
71 - multi_modal_data: NotRequired[MultiModalDataDict | None]
72 - """
73 - Optional multi-modal data to pass to the model,
74 - if the model supports it.
75 - """
76 -
77 - mm_processor_kwargs: NotRequired[dict[str, Any] | None]
78 - """
79 - Optional multi-modal processor kwargs to be forwarded to the
80 - multimodal input mapper & processor. Note that if multiple modalities
81 - have registered mappers etc for the model being considered, we attempt
82 - to pass the mm_processor_kwargs to each of them.
83 - """
84 -
85 - multi_modal_uuids: NotRequired[MultiModalUUIDDict]
86 - """
87 - Optional user-specified UUIDs for multimodal items, mapped by modality.
88 - Lists must match the number of items per modality and may contain `None`.
89 - For `None` entries, the hasher will compute IDs automatically; non-None
90 - entries override the default hashes for caching.
91 - """
73
93 - cache_salt: NotRequired[str]
94 - """
95 - Optional cache salt to be used for prefix caching.
96 - """
97 -
98 -
99 -class EmbedsPrompt(TypedDict):
74 +class EmbedsPrompt(_CommonKeys):
75 """Schema for a prompt provided via token embeddings."""
76
77 prompt_embeds: torch.Tensor
78 """The embeddings of the prompt."""
79
105 - cache_salt: NotRequired[str]
106 - """
107 - Optional cache salt to be used for prefix caching.
108 - """
109 -
80
111 -class DataPrompt(TypedDict):
81 +class DataPrompt(_CommonKeys):
82 """Represents generic inputs handled by IO processor plugins."""
83
84 data: Any
@@ -197,7 +167,7 @@ class ExplicitEncoderDecoderPrompt(TypedDict, Generic[_T1_co, _T2_co]):
167 mm_processor_kwargs: NotRequired[dict[str, Any]]
168
169
200 -PromptType: TypeAlias = SingletonPrompt | ExplicitEncoderDecoderPrompt
170 +PromptType: TypeAlias = SingletonPrompt | ExplicitEncoderDecoderPrompt[Any, Any]
171 """
172 Set of possible schemas for an LLM input, including
173 both decoder-only and encoder/decoder input types:
vllm/inputs/parse.py
+9 -47
@@ -1,11 +1,10 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -from collections.abc import Sequence
4 -from typing import TYPE_CHECKING, Literal, NamedTuple, TypeAlias, TypedDict, cast
3 +from typing import TYPE_CHECKING, Literal, NamedTuple, TypeAlias, TypedDict
4
5 from typing_extensions import TypeIs
6
8 -from vllm.utils.collection_utils import is_list_of
7 +from vllm.utils import length_from_prompt_token_ids_or_embeds
8
9 from .data import (
10 EmbedsPrompt,
@@ -22,50 +21,6 @@ if TYPE_CHECKING:
21 import torch
22
23
25 -def parse_raw_prompts(
26 - prompt: str | list[str] | list[int] | list[list[int]],
27 -) -> Sequence[TextPrompt] | Sequence[TokensPrompt]:
28 - if isinstance(prompt, str):
29 - # case 1: a string
30 - return [TextPrompt(prompt=prompt)]
31 -
32 - if isinstance(prompt, list):
33 - if len(prompt) == 0:
34 - raise ValueError("please provide at least one prompt")
35 -
36 - # case 2: array of strings
37 - if is_list_of(prompt, str):
38 - prompt = cast(list[str], prompt)
39 - return [TextPrompt(prompt=elem) for elem in prompt]
40 -
41 - # case 3: array of tokens
42 - if is_list_of(prompt, int):
43 - prompt = cast(list[int], prompt)
44 - return [TokensPrompt(prompt_token_ids=prompt)]
45 -
46 - # case 4: array of token arrays
47 - if is_list_of(prompt, list):
48 - if len(prompt) == 1 and isinstance(prompt[0], list) and len(prompt[0]) == 0:
49 - raise ValueError("please provide at least one prompt")
50 - for elem in prompt:
51 - if not isinstance(elem, list):
52 - raise TypeError(
53 - "prompt must be a list of lists, but found a non-list element."
54 - )
55 - if not is_list_of(elem, int):
56 - raise TypeError(
57 - "Nested lists of tokens must contain only integers."
58 - )
59 -
60 - prompt = cast(list[list[int]], prompt)
61 - return [TokensPrompt(prompt_token_ids=elem) for elem in prompt]
62 -
63 - raise TypeError(
64 - "prompt must be a string, array of strings, "
65 - "array of tokens, or array of token arrays"
66 - )
67 -
68 -
24 class ParsedStrPrompt(TypedDict):
25 type: Literal["str"]
26 content: str
@@ -145,3 +100,10 @@ def get_prompt_components(prompt: PromptType) -> PromptComponents:
100 token_ids=prompt.get("prompt_token_ids"), # type: ignore[arg-type]
101 embeds=prompt.get("prompt_embeds"),
102 )
103 +
104 +
105 +def get_prompt_len(prompt: TokensPrompt | EmbedsPrompt):
106 + return length_from_prompt_token_ids_or_embeds(
107 + prompt.get("prompt_token_ids"), # type: ignore[arg-type]
108 + prompt.get("prompt_embeds"), # type: ignore[arg-type]
109 + )
vllm/model_executor/models/ultravox.py
+1
@@ -209,6 +209,7 @@ class UltravoxMultiModalProcessor(BaseMultiModalProcessor[UltravoxProcessingInfo
209 item_processor_data = dict(**mm_data, audios=audios)
210
211 # some tokenizer kwargs are incompatible with UltravoxProcessor
212 + tok_kwargs.pop("add_special_tokens", None)
213 tok_kwargs.pop("padding", None)
214 tok_kwargs.pop("truncation", None)
215
vllm/renderers/__init__.py
+9 -1
@@ -1,7 +1,15 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from .params import ChatParams, TokenizeParams, merge_kwargs
5 from .protocol import RendererLike
6 from .registry import RendererRegistry, renderer_from_config
7
7 -__all__ = ["RendererLike", "RendererRegistry", "renderer_from_config"]
8 +__all__ = [
9 + "RendererLike",
10 + "RendererRegistry",
11 + "renderer_from_config",
12 + "ChatParams",
13 + "TokenizeParams",
14 + "merge_kwargs",
15 +]
vllm/renderers/deepseek_v32.py
+12 -19
@@ -9,11 +9,12 @@ from vllm.entrypoints.chat_utils import (
9 parse_chat_messages,
10 parse_chat_messages_async,
11 )
12 -from vllm.inputs import TextPrompt, TokensPrompt
12 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
13 from vllm.logger import init_logger
14 from vllm.tokenizers import cached_get_tokenizer
15 from vllm.tokenizers.deepseek_v32 import DeepseekV32Tokenizer
16
17 +from .params import ChatParams
18 from .protocol import RendererLike
19
20 logger = init_logger(__name__)
@@ -61,8 +62,8 @@ class DeepseekV32Renderer(RendererLike):
62 def render_messages(
63 self,
64 messages: list[ChatCompletionMessageParam],
64 - **kwargs,
65 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
65 + params: ChatParams,
66 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
67 tokenizer = self.get_tokenizer()
68 conversation, mm_data, mm_uuids = parse_chat_messages(
69 messages,
@@ -74,26 +75,22 @@ class DeepseekV32Renderer(RendererLike):
75 prompt_raw = tokenizer.apply_chat_template(
76 conversation=conversation,
77 messages=messages,
77 - **kwargs,
78 + **params.get_apply_chat_template_kwargs(),
79 )
80
80 - prompt = (
81 - TextPrompt(prompt=prompt_raw)
82 - if isinstance(prompt_raw, str)
83 - else TokensPrompt(prompt_token_ids=prompt_raw)
84 - )
81 + prompt = self.render_completion(prompt_raw)
82 if mm_data is not None:
83 prompt["multi_modal_data"] = mm_data
84 if mm_uuids is not None:
85 prompt["multi_modal_uuids"] = mm_uuids
86
90 - return conversation, prompt # type: ignore[return-value]
87 + return conversation, prompt
88
89 async def render_messages_async(
90 self,
91 messages: list[ChatCompletionMessageParam],
95 - **kwargs,
96 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
92 + params: ChatParams,
93 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
94 tokenizer = self.get_tokenizer()
95 conversation, mm_data, mm_uuids = await parse_chat_messages_async(
96 messages,
@@ -105,17 +102,13 @@ class DeepseekV32Renderer(RendererLike):
102 prompt_raw = tokenizer.apply_chat_template(
103 conversation=conversation,
104 messages=messages,
108 - **kwargs,
105 + **params.get_apply_chat_template_kwargs(),
106 )
107
111 - prompt = (
112 - TextPrompt(prompt=prompt_raw)
113 - if isinstance(prompt_raw, str)
114 - else TokensPrompt(prompt_token_ids=prompt_raw)
115 - )
108 + prompt = self.render_completion(prompt_raw)
109 if mm_data is not None:
110 prompt["multi_modal_data"] = mm_data
111 if mm_uuids is not None:
112 prompt["multi_modal_uuids"] = mm_uuids
113
121 - return conversation, prompt # type: ignore[return-value]
114 + return conversation, prompt
vllm/renderers/embed_utils.py new
+44
@@ -0,0 +1,44 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from io import BytesIO
4 +from typing import TYPE_CHECKING
5 +
6 +import pybase64
7 +import torch
8 +
9 +from vllm.exceptions import VLLMValidationError
10 +
11 +if TYPE_CHECKING:
12 + from vllm.config import ModelConfig
13 +
14 +
15 +def safe_load_prompt_embeds(
16 + model_config: "ModelConfig",
17 + embed: bytes,
18 +) -> torch.Tensor:
19 + if not model_config.enable_prompt_embeds:
20 + raise VLLMValidationError(
21 + "You must set `--enable-prompt-embeds` to input `prompt_embeds`.",
22 + parameter="prompt_embeds",
23 + )
24 +
25 + # Enable sparse tensor integrity checks to prevent out-of-bounds
26 + # writes from maliciously crafted tensors
27 + with torch.sparse.check_sparse_tensor_invariants():
28 + tensor = torch.load(
29 + BytesIO(pybase64.b64decode(embed, validate=True)),
30 + weights_only=True,
31 + map_location=torch.device("cpu"),
32 + )
33 + assert isinstance(tensor, torch.Tensor) and tensor.dtype in (
34 + torch.float32,
35 + torch.bfloat16,
36 + torch.float16,
37 + )
38 + tensor = tensor.to_dense()
39 +
40 + if tensor.dim() > 2:
41 + tensor = tensor.squeeze(0)
42 + assert tensor.dim() == 2
43 +
44 + return tensor
vllm/renderers/grok2.py
+12 -19
@@ -9,11 +9,12 @@ from vllm.entrypoints.chat_utils import (
9 parse_chat_messages,
10 parse_chat_messages_async,
11 )
12 -from vllm.inputs import TextPrompt, TokensPrompt
12 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
13 from vllm.logger import init_logger
14 from vllm.tokenizers import cached_get_tokenizer
15 from vllm.tokenizers.grok2 import Grok2Tokenizer
16
17 +from .params import ChatParams
18 from .protocol import RendererLike
19
20 logger = init_logger(__name__)
@@ -61,8 +62,8 @@ class Grok2Renderer(RendererLike):
62 def render_messages(
63 self,
64 messages: list[ChatCompletionMessageParam],
64 - **kwargs,
65 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
65 + params: ChatParams,
66 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
67 tokenizer = self.get_tokenizer()
68 conversation, mm_data, mm_uuids = parse_chat_messages(
69 messages,
@@ -74,26 +75,22 @@ class Grok2Renderer(RendererLike):
75 prompt_raw = tokenizer.apply_chat_template(
76 conversation=conversation,
77 messages=messages,
77 - **kwargs,
78 + **params.get_apply_chat_template_kwargs(),
79 )
80
80 - prompt = (
81 - TextPrompt(prompt=prompt_raw)
82 - if isinstance(prompt_raw, str)
83 - else TokensPrompt(prompt_token_ids=prompt_raw)
84 - )
81 + prompt = self.render_completion(prompt_raw)
82 if mm_data is not None:
83 prompt["multi_modal_data"] = mm_data
84 if mm_uuids is not None:
85 prompt["multi_modal_uuids"] = mm_uuids
86
90 - return conversation, prompt # type: ignore[return-value]
87 + return conversation, prompt
88
89 async def render_messages_async(
90 self,
91 messages: list[ChatCompletionMessageParam],
95 - **kwargs,
96 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
92 + params: ChatParams,
93 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
94 tokenizer = self.get_tokenizer()
95 conversation, mm_data, mm_uuids = await parse_chat_messages_async(
96 messages,
@@ -105,17 +102,13 @@ class Grok2Renderer(RendererLike):
102 prompt_raw = tokenizer.apply_chat_template(
103 conversation=conversation,
104 messages=messages,
108 - **kwargs,
105 + **params.get_apply_chat_template_kwargs(),
106 )
107
111 - prompt = (
112 - TextPrompt(prompt=prompt_raw)
113 - if isinstance(prompt_raw, str)
114 - else TokensPrompt(prompt_token_ids=prompt_raw)
115 - )
108 + prompt = self.render_completion(prompt_raw)
109 if mm_data is not None:
110 prompt["multi_modal_data"] = mm_data
111 if mm_uuids is not None:
112 prompt["multi_modal_uuids"] = mm_uuids
113
121 - return conversation, prompt # type: ignore[return-value]
114 + return conversation, prompt
vllm/renderers/hf.py
+20 -31
@@ -25,7 +25,7 @@ from vllm.entrypoints.chat_utils import (
25 parse_chat_messages,
26 parse_chat_messages_async,
27 )
28 -from vllm.inputs import TextPrompt, TokensPrompt
28 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
29 from vllm.logger import init_logger
30 from vllm.tokenizers import cached_get_tokenizer
31 from vllm.tokenizers.hf import CachedHfTokenizer, HfTokenizer
@@ -33,6 +33,7 @@ from vllm.transformers_utils.chat_templates import get_chat_template_fallback_pa
33 from vllm.transformers_utils.processor import cached_get_processor
34 from vllm.utils.func_utils import supports_kw
35
36 +from .params import ChatParams
37 from .protocol import RendererLike
38
39 if TYPE_CHECKING:
@@ -632,9 +633,8 @@ class HfRenderer(RendererLike):
633 def render_messages(
634 self,
635 messages: list[ChatCompletionMessageParam],
635 - chat_template_content_format: ChatTemplateContentFormatOption = "auto",
636 - **kwargs,
637 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
636 + params: ChatParams,
637 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
638 model_config = self.config
639 tokenizer = self.get_tokenizer()
640
@@ -642,9 +642,9 @@ class HfRenderer(RendererLike):
642 messages,
643 model_config,
644 content_format=resolve_chat_template_content_format(
645 - chat_template=kwargs.get("chat_template"),
646 - tools=kwargs.get("tools"),
647 - given_format=chat_template_content_format,
645 + chat_template=params.chat_template,
646 + tools=params.chat_template_kwargs.get("tools"),
647 + given_format=params.chat_template_content_format,
648 tokenizer=tokenizer,
649 model_config=model_config,
650 ),
@@ -654,7 +654,7 @@ class HfRenderer(RendererLike):
654 model_config,
655 tokenizer,
656 conversation,
657 - **kwargs,
657 + **params.get_apply_chat_template_kwargs(),
658 )
659
660 # NOTE: use_unified_vision_chunk is currently specific to Kimi-K2.5
@@ -666,7 +666,7 @@ class HfRenderer(RendererLike):
666 ):
667 mm_uuids = rebuild_mm_uuids_from_mm_data(mm_uuids, mm_data)
668
669 - # get video placehoder, replace it with runtime video-chunk prompts
669 + # get video placeholder, replace it with runtime video-chunk prompts
670 video_placeholder = getattr(
671 model_config.hf_config, "video_placeholder", None
672 )
@@ -676,24 +676,19 @@ class HfRenderer(RendererLike):
676 video_placeholder,
677 )
678
679 - prompt = (
680 - TextPrompt(prompt=prompt_raw)
681 - if isinstance(prompt_raw, str)
682 - else TokensPrompt(prompt_token_ids=prompt_raw)
683 - )
679 + prompt = self.render_completion(prompt_raw)
680 if mm_data is not None:
681 prompt["multi_modal_data"] = mm_data
682 if mm_uuids is not None:
683 prompt["multi_modal_uuids"] = mm_uuids
684
689 - return conversation, prompt # type: ignore[return-value]
685 + return conversation, prompt
686
687 async def render_messages_async(
688 self,
689 messages: list[ChatCompletionMessageParam],
694 - chat_template_content_format: ChatTemplateContentFormatOption = "auto",
695 - **kwargs,
696 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
690 + params: ChatParams,
691 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
692 model_config = self.config
693 tokenizer = self.get_tokenizer()
694
@@ -701,9 +696,9 @@ class HfRenderer(RendererLike):
696 messages,
697 model_config,
698 content_format=resolve_chat_template_content_format(
704 - chat_template=kwargs.get("chat_template"),
705 - tools=kwargs.get("tools"),
706 - given_format=chat_template_content_format,
699 + chat_template=params.chat_template,
700 + tools=params.chat_template_kwargs.get("tools"),
701 + given_format=params.chat_template_content_format,
702 tokenizer=tokenizer,
703 model_config=model_config,
704 ),
@@ -713,7 +708,7 @@ class HfRenderer(RendererLike):
708 model_config,
709 tokenizer,
710 conversation,
716 - **kwargs,
711 + **params.get_apply_chat_template_kwargs(),
712 )
713
714 # NOTE: use_unified_vision_chunk is currently specific to Kimi-K2.5
@@ -723,9 +718,7 @@ class HfRenderer(RendererLike):
718 and mm_uuids is not None
719 and mm_data is not None
720 ):
726 - mm_uuids = rebuild_mm_uuids_from_mm_data(mm_uuids, mm_data)
727 -
728 - # get video placehoder, replace it with runtime video-chunk prompts
721 + # get video placeholder, replace it with runtime video-chunk prompts
722 video_placeholder = getattr(
723 model_config.hf_config, "video_placeholder", None
724 )
@@ -735,14 +728,10 @@ class HfRenderer(RendererLike):
728 video_placeholder,
729 )
730
738 - prompt = (
739 - TextPrompt(prompt=prompt_raw)
740 - if isinstance(prompt_raw, str)
741 - else TokensPrompt(prompt_token_ids=prompt_raw)
742 - )
731 + prompt = self.render_completion(prompt_raw)
732 if mm_data is not None:
733 prompt["multi_modal_data"] = mm_data
734 if mm_uuids is not None:
735 prompt["multi_modal_uuids"] = mm_uuids
736
748 - return conversation, prompt # type: ignore[return-value]
737 + return conversation, prompt
vllm/renderers/mistral.py
+18 -19
@@ -10,12 +10,13 @@ from vllm.entrypoints.chat_utils import (
10 parse_chat_messages,
11 parse_chat_messages_async,
12 )
13 -from vllm.inputs import TextPrompt, TokensPrompt
13 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
14 from vllm.logger import init_logger
15 from vllm.tokenizers import cached_get_tokenizer
16 from vllm.tokenizers.mistral import MistralTokenizer
17 from vllm.utils.async_utils import make_async
18
19 +from .params import ChatParams
20 from .protocol import RendererLike
21
22 logger = init_logger(__name__)
@@ -95,8 +96,8 @@ class MistralRenderer(RendererLike):
96 def render_messages(
97 self,
98 messages: list[ChatCompletionMessageParam],
98 - **kwargs,
99 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
99 + params: ChatParams,
100 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
101 tokenizer = self.get_tokenizer()
102 conversation, mm_data, mm_uuids = parse_chat_messages(
103 messages,
@@ -104,25 +105,25 @@ class MistralRenderer(RendererLike):
105 content_format="string",
106 )
107
107 - prompt_raw = safe_apply_chat_template(tokenizer, messages, **kwargs)
108 -
109 - prompt = (
110 - TextPrompt(prompt=prompt_raw)
111 - if isinstance(prompt_raw, str)
112 - else TokensPrompt(prompt_token_ids=prompt_raw)
108 + prompt_raw = safe_apply_chat_template(
109 + tokenizer,
110 + messages,
111 + **params.get_apply_chat_template_kwargs(),
112 )
113 +
114 + prompt = self.render_completion(prompt_raw)
115 if mm_data is not None:
116 prompt["multi_modal_data"] = mm_data
117 if mm_uuids is not None:
118 prompt["multi_modal_uuids"] = mm_uuids
119
119 - return conversation, prompt # type: ignore[return-value]
120 + return conversation, prompt
121
122 async def render_messages_async(
123 self,
124 messages: list[ChatCompletionMessageParam],
124 - **kwargs,
125 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
125 + params: ChatParams,
126 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
127 tokenizer = self.get_tokenizer()
128 conversation, mm_data, mm_uuids = await parse_chat_messages_async(
129 messages,
@@ -131,17 +132,15 @@ class MistralRenderer(RendererLike):
132 )
133
134 prompt_raw = await self._apply_chat_template_async(
134 - tokenizer, messages, **kwargs
135 + tokenizer,
136 + messages,
137 + **params.get_apply_chat_template_kwargs(),
138 )
139
137 - prompt = (
138 - TextPrompt(prompt=prompt_raw)
139 - if isinstance(prompt_raw, str)
140 - else TokensPrompt(prompt_token_ids=prompt_raw)
141 - )
140 + prompt = self.render_completion(prompt_raw)
141 if mm_data is not None:
142 prompt["multi_modal_data"] = mm_data
143 if mm_uuids is not None:
144 prompt["multi_modal_uuids"] = mm_uuids
145
147 - return conversation, prompt # type: ignore[return-value]
146 + return conversation, prompt
vllm/renderers/params.py new
+351
@@ -0,0 +1,351 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from dataclasses import dataclass, field
4 +from typing import TYPE_CHECKING, Any, TypeVar
5 +
6 +from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
7 +from vllm.exceptions import VLLMValidationError
8 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
9 +from vllm.logger import init_logger
10 +from vllm.tokenizers import TokenizerLike
11 +from vllm.utils.import_utils import LazyLoader
12 +
13 +if TYPE_CHECKING:
14 + import torch
15 +else:
16 + torch = LazyLoader("torch", globals(), "torch")
17 +
18 +logger = init_logger(__name__)
19 +
20 +
21 +_S = TypeVar("_S", list[int], "torch.Tensor")
22 +
23 +
24 +def merge_kwargs(
25 + defaults: dict[str, Any] | None,
26 + overrides: dict[str, Any] | None,
27 + /,
28 + *,
29 + unset_values: tuple[object, ...] = (None, "auto"),
30 +) -> dict[str, Any]:
31 + if defaults is None:
32 + defaults = {}
33 + if overrides is None:
34 + overrides = {}
35 +
36 + return defaults | {k: v for k, v in overrides.items() if v not in unset_values}
37 +
38 +
39 +@dataclass(frozen=True)
40 +class ChatParams:
41 + """Configuration to control how to parse chat messages."""
42 +
43 + chat_template: str | None = None
44 + """The chat template to apply."""
45 +
46 + chat_template_content_format: ChatTemplateContentFormatOption = "auto"
47 + """The format of the chat template."""
48 +
49 + chat_template_kwargs: dict[str, Any] = field(default_factory=dict)
50 + """The kwargs to pass to the chat template."""
51 +
52 + def with_defaults(self, default_chat_template_kwargs: dict[str, Any] | None):
53 + if not default_chat_template_kwargs:
54 + return self
55 +
56 + return ChatParams(
57 + chat_template=self.chat_template,
58 + chat_template_content_format=self.chat_template_content_format,
59 + chat_template_kwargs=merge_kwargs(
60 + default_chat_template_kwargs,
61 + self.chat_template_kwargs,
62 + ),
63 + )
64 +
65 + def get_apply_chat_template_kwargs(self) -> dict[str, Any]:
66 + """The arguments to pass to `tokenizer.apply_chat_template`."""
67 + return merge_kwargs(
68 + self.chat_template_kwargs,
69 + dict(chat_template=self.chat_template),
70 + )
71 +
72 +
73 +@dataclass(frozen=True)
74 +class TokenizeParams:
75 + """Configuration to control how prompts are tokenized."""
76 +
77 + max_total_tokens: int | None
78 + """
79 + Maximum allowed number of input + output tokens.
80 +
81 + Usually, this refers to the model's context length.
82 + """
83 +
84 + max_output_tokens: int = 0
85 + """Maximum requested number of output tokens."""
86 +
87 + pad_prompt_tokens: int | None = None
88 + """
89 + Number of tokens to pad to:
90 + - `None` means no padding.
91 + - `-1` maps to `max_input_tokens`.
92 + """
93 +
94 + truncate_prompt_tokens: int | None = None
95 + """
96 + Number of tokens to keep:
97 + - `None` means no truncation.
98 + - `-1` maps to `max_input_tokens`.
99 + """
100 +
101 + do_lower_case: bool = False
102 + """Whether to normalize text to lower case before tokenization."""
103 +
104 + add_special_tokens: bool = True
105 + """Whether to add special tokens."""
106 +
107 + needs_detokenization: bool = False
108 + """
109 + Whether the tokenized prompt needs to contain the original text.
110 +
111 + Not to be confused with `SamplingParams.detokenize` which deals
112 + with the output generated by the model.
113 + """
114 +
115 + max_total_tokens_param: str = "max_total_tokens"
116 + """Override this to edit the message for validation errors."""
117 +
118 + max_output_tokens_param: str = "max_output_tokens"
119 + """Override this to edit the message for validation errors."""
120 +
121 + truncate_prompt_tokens_param: str = "truncate_prompt_tokens"
122 + """Override this to edit the message for validation errors."""
123 +
124 + @property
125 + def max_input_tokens(self) -> int | None:
126 + """Maximum allowed number of input tokens."""
127 + if self.max_total_tokens is None:
128 + return None
129 +
130 + return self.max_total_tokens - self.max_output_tokens
131 +
132 + def __post_init__(self) -> None:
133 + max_total_tokens = self.max_total_tokens
134 + max_output_tokens = self.max_output_tokens
135 + max_input_tokens = self.max_input_tokens
136 + truncate_prompt_tokens = self.truncate_prompt_tokens
137 +
138 + if (
139 + max_output_tokens is not None
140 + and max_total_tokens is not None
141 + and max_output_tokens > max_total_tokens
142 + ):
143 + raise VLLMValidationError(
144 + f"{self.max_output_tokens_param}={max_output_tokens}"
145 + f"cannot be greater than "
146 + f"{self.max_total_tokens_param}={max_total_tokens=}. "
147 + f"Please request fewer output tokens.",
148 + parameter=self.max_output_tokens_param,
149 + value=max_output_tokens,
150 + )
151 +
152 + if (
153 + max_input_tokens is not None
154 + and truncate_prompt_tokens is not None
155 + and truncate_prompt_tokens > max_input_tokens
156 + ):
157 + raise VLLMValidationError(
158 + f"{self.truncate_prompt_tokens_param}={truncate_prompt_tokens} "
159 + f"cannot be greater than {self.max_total_tokens_param} - "
160 + f"{self.max_output_tokens_param} = {max_input_tokens}. "
161 + f"Please request a smaller truncation size.",
162 + parameter=self.truncate_prompt_tokens_param,
163 + value=truncate_prompt_tokens,
164 + )
165 +
166 + def with_kwargs(self, tokenization_kwargs: dict[str, Any] | None):
167 + if tokenization_kwargs is None:
168 + tokenization_kwargs = {}
169 +
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
173 + )
174 + truncate_prompt_tokens = tokenization_kwargs.pop(
175 + "truncate_prompt_tokens", self.truncate_prompt_tokens
176 + )
177 + do_lower_case = tokenization_kwargs.pop("do_lower_case", self.do_lower_case)
178 + add_special_tokens = tokenization_kwargs.pop(
179 + "add_special_tokens", self.add_special_tokens
180 + )
181 + needs_detokenization = tokenization_kwargs.pop(
182 + "needs_detokenization", self.needs_detokenization
183 + )
184 +
185 + # https://huggingface.co/docs/transformers/en/pad_truncation
186 + if padding := tokenization_kwargs.pop("padding", None):
187 + if padding == "max_length":
188 + pad_prompt_tokens = max_length
189 + elif padding in (False, "do_not_pad"):
190 + pad_prompt_tokens = None
191 + else:
192 + # To emit the below warning
193 + tokenization_kwargs["padding"] = padding
194 +
195 + if truncation := tokenization_kwargs.pop("truncation", None):
196 + if truncation in (True, "longest_first"):
197 + truncate_prompt_tokens = max_length
198 + elif truncation in (False, "do_not_truncate"):
199 + truncate_prompt_tokens = None
200 + else:
201 + # To emit the below warning
202 + tokenization_kwargs["truncation"] = truncation
203 +
204 + if tokenization_kwargs:
205 + logger.warning(
206 + "The following tokenization arguments are not supported "
207 + "by vLLM Renderer and will be ignored: %s",
208 + tokenization_kwargs,
209 + )
210 +
211 + max_total_tokens = self.max_total_tokens
212 +
213 + return TokenizeParams(
214 + max_total_tokens=max_total_tokens,
215 + max_output_tokens=(
216 + 0
217 + if max_total_tokens is None or max_length is None
218 + else max_total_tokens - max_length
219 + ),
220 + pad_prompt_tokens=pad_prompt_tokens,
221 + truncate_prompt_tokens=truncate_prompt_tokens,
222 + do_lower_case=do_lower_case,
223 + add_special_tokens=add_special_tokens,
224 + needs_detokenization=needs_detokenization,
225 + )
226 +
227 + def get_encode_kwargs(self) -> dict[str, Any]:
228 + """The arguments to pass to `tokenizer.encode`."""
229 + max_length = self.truncate_prompt_tokens
230 + if max_length is not None and max_length < 0:
231 + max_length = self.max_input_tokens
232 +
233 + return dict(
234 + truncation=self.truncate_prompt_tokens is not None,
235 + max_length=max_length,
236 + add_special_tokens=self.add_special_tokens,
237 + )
238 +
239 + def _apply_lowercase(self, tokenizer: TokenizerLike | None, text: str) -> str:
240 + if self.do_lower_case:
241 + text = text.lower()
242 +
243 + return text
244 +
245 + def _validate_text(self, tokenizer: TokenizerLike | None, text: str) -> str:
246 + """Apply all validators to prompt text."""
247 + # TODO: Implement https://github.com/vllm-project/vllm/pull/31366
248 + for validator in (self._apply_lowercase,):
249 + text = validator(tokenizer, text)
250 +
251 + return text
252 +
253 + def apply_pre_tokenization(
254 + self,
255 + tokenizer: TokenizerLike | None,
256 + prompt: TextPrompt,
257 + ) -> TextPrompt:
258 + """
259 + Ensure that the prompt meets the requirements set out by this config.
260 + If that is not possible, raise a `VLLMValidationError`.
261 +
262 + This method is run before tokenization occurs.
263 + """
264 + prompt["prompt"] = self._validate_text(tokenizer, prompt["prompt"])
265 +
266 + return prompt
267 +
268 + def _apply_padding(self, tokenizer: TokenizerLike | None, tokens: _S) -> _S:
269 + """Apply padding to a token sequence."""
270 + pad_length = self.pad_prompt_tokens
271 + if pad_length is not None and pad_length < 0:
272 + pad_length = self.max_input_tokens
273 +
274 + if pad_length is None or pad_length <= len(tokens):
275 + return tokens
276 +
277 + if tokenizer is None:
278 + raise ValueError("Cannot pad tokens when `skip_tokenizer_init=True`")
279 + if not isinstance(tokens, list):
280 + raise ValueError("Cannot pad tokens for embedding inputs")
281 +
282 + return tokens + [tokenizer.pad_token_id] * (pad_length - len(tokens))
283 +
284 + def _apply_truncation(self, tokenizer: TokenizerLike | None, tokens: _S) -> _S:
285 + """Apply truncation to a token sequence."""
286 + max_length = self.truncate_prompt_tokens
287 + if max_length is not None and max_length < 0:
288 + max_length = self.max_input_tokens
289 +
290 + if max_length is None or max_length >= len(tokens):
291 + return tokens
292 + if max_length == 0:
293 + return tokens[:0]
294 +
295 + if getattr(tokenizer, "truncation_side", "left") == "left":
296 + return tokens[-max_length:]
297 +
298 + return tokens[:max_length]
299 +
300 + def _apply_length_check(self, tokenizer: TokenizerLike | None, tokens: _S) -> _S:
301 + """Apply length checks to a token sequence."""
302 + max_input_tokens = self.max_input_tokens
303 +
304 + if max_input_tokens is not None and len(tokens) > max_input_tokens:
305 + raise VLLMValidationError(
306 + f"You passed {len(tokens)} input tokens and "
307 + f"requested {self.max_output_tokens} output tokens. "
308 + f"However, the model's context length is only "
309 + f"{self.max_total_tokens}, resulting in a maximum "
310 + f"input length of {max_input_tokens}. "
311 + f"Please reduce the length of the input messages.",
312 + parameter="input_tokens",
313 + value=len(tokens),
314 + )
315 +
316 + return tokens
317 +
318 + def _validate_tokens(self, tokenizer: TokenizerLike | None, tokens: _S) -> _S:
319 + """Apply all validators to a token sequence."""
320 + for validator in (
321 + self._apply_padding,
322 + self._apply_truncation,
323 + self._apply_length_check,
324 + ):
325 + tokens = validator(tokenizer, tokens)
326 +
327 + return tokens
328 +
329 + def apply_post_tokenization(
330 + self,
331 + tokenizer: TokenizerLike | None,
332 + prompt: TokensPrompt | EmbedsPrompt,
333 + ) -> TokensPrompt | EmbedsPrompt:
334 + """
335 + Ensure that the prompt meets the requirements set out by this config.
336 + If that is not possible, raise a `VLLMValidationError`.
337 +
338 + This method is run after tokenization occurs.
339 + """
340 + if "prompt_token_ids" in prompt:
341 + prompt["prompt_token_ids"] = self._validate_tokens( # type: ignore[typeddict-unknown-key]
342 + tokenizer,
343 + prompt["prompt_token_ids"], # type: ignore[typeddict-item]
344 + )
345 + if "prompt_embeds" in prompt:
346 + prompt["prompt_embeds"] = self._validate_tokens( # type: ignore[typeddict-unknown-key]
347 + tokenizer,
348 + prompt["prompt_embeds"], # type: ignore[typeddict-item]
349 + )
350 +
351 + return prompt
vllm/renderers/protocol.py
+146 -6
@@ -1,9 +1,15 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import asyncio
4 from typing import TYPE_CHECKING, Any, Protocol
5
5 -from vllm.inputs import TextPrompt, TokensPrompt
6 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
7 from vllm.tokenizers import TokenizerLike
8 +from vllm.utils.async_utils import AsyncMicrobatchTokenizer
9 +from vllm.utils.collection_utils import is_list_of
10 +
11 +from .embed_utils import safe_load_prompt_embeds
12 +from .params import ChatParams, TokenizeParams
13
14 if TYPE_CHECKING:
15 from vllm.config import ModelConfig
@@ -14,6 +20,9 @@ if TYPE_CHECKING:
20
21
22 class RendererLike(Protocol):
23 + config: "ModelConfig"
24 + _async_tokenizer: AsyncMicrobatchTokenizer
25 +
26 @classmethod
27 def from_config(
28 cls,
@@ -33,16 +42,147 @@ class RendererLike(Protocol):
42
43 return tokenizer
44
45 + def get_async_tokenizer(self) -> AsyncMicrobatchTokenizer:
46 + # Lazy initialization since offline LLM doesn't use async
47 + if not hasattr(self, "_async_tokenizer"):
48 + self._async_tokenizer = AsyncMicrobatchTokenizer(self.get_tokenizer())
49 +
50 + return self._async_tokenizer
51 +
52 + # Step 1: Convert raw inputs to prompts
53 + def render_completion(
54 + self,
55 + prompt_raw: str | list[int] | bytes,
56 + ) -> TextPrompt | TokensPrompt | EmbedsPrompt:
57 + error_msg = "Each prompt must be a string or an array of tokens"
58 +
59 + if isinstance(prompt_raw, str):
60 + return TextPrompt(prompt=prompt_raw)
61 +
62 + if isinstance(prompt_raw, list):
63 + if not is_list_of(prompt_raw, int):
64 + raise TypeError(error_msg)
65 +
66 + return TokensPrompt(prompt_token_ids=prompt_raw)
67 +
68 + if isinstance(prompt_raw, bytes):
69 + embeds = safe_load_prompt_embeds(self.config, prompt_raw)
70 + return EmbedsPrompt(prompt_embeds=embeds)
71 +
72 + raise TypeError(error_msg)
73 +
74 + def render_completions(
75 + self,
76 + prompt_input: str | list[str] | list[int] | list[list[int]] | None = None,
77 + prompt_embeds: bytes | list[bytes] | None = None,
78 + ) -> list[TextPrompt | TokensPrompt | EmbedsPrompt]:
79 + prompts_raw = list[str | list[int] | bytes]()
80 +
81 + if prompt_embeds is not None: # embeds take higher priority
82 + if isinstance(prompt_embeds, bytes):
83 + prompts_raw.append(prompt_embeds)
84 + else:
85 + prompts_raw.extend(prompt_embeds)
86 +
87 + if prompt_input is not None:
88 + if isinstance(prompt_input, str) or (
89 + len(prompt_input) > 0 and is_list_of(prompt_input, int)
90 + ):
91 + prompts_raw.append(prompt_input) # type: ignore[arg-type]
92 + else:
93 + prompts_raw.extend(prompt_input) # type: ignore[arg-type]
94 +
95 + if len(prompts_raw) == 0:
96 + raise ValueError("You must pass at least one prompt")
97 +
98 + return [self.render_completion(prompt) for prompt in prompts_raw]
99 +
100 + async def render_completions_async(
101 + self,
102 + prompt_input: str | list[str] | list[int] | list[list[int]] | None = None,
103 + prompt_embeds: bytes | list[bytes] | None = None,
104 + ) -> list[TextPrompt | TokensPrompt | EmbedsPrompt]:
105 + return self.render_completions(prompt_input, prompt_embeds)
106 +
107 def render_messages(
108 self,
109 messages: list["ChatCompletionMessageParam"],
39 - **kwargs,
40 - ) -> tuple[list["ConversationMessage"], TextPrompt | TokensPrompt]:
110 + params: ChatParams,
111 + ) -> tuple[list["ConversationMessage"], TextPrompt | TokensPrompt | EmbedsPrompt]:
112 raise NotImplementedError
113
114 async def render_messages_async(
115 self,
116 messages: list["ChatCompletionMessageParam"],
46 - **kwargs,
47 - ) -> tuple[list["ConversationMessage"], TextPrompt | TokensPrompt]:
48 - return self.render_messages(messages, **kwargs)
117 + params: ChatParams,
118 + ) -> tuple[list["ConversationMessage"], TextPrompt | TokensPrompt | EmbedsPrompt]:
119 + return self.render_messages(messages, params)
120 +
121 + # Step 2: Tokenize prompts if necessary
122 + def tokenize_prompt(
123 + self,
124 + prompt: TextPrompt | TokensPrompt | EmbedsPrompt,
125 + params: TokenizeParams,
126 + ) -> TokensPrompt | EmbedsPrompt:
127 + if "prompt_token_ids" not in prompt and "prompt_embeds" not in prompt:
128 + prompt = params.apply_pre_tokenization(self.tokenizer, prompt)
129 +
130 + tokenizer = self.get_tokenizer()
131 + prompt_token_ids = tokenizer.encode(
132 + prompt["prompt"],
133 + **params.get_encode_kwargs(),
134 + )
135 +
136 + prompt = TokensPrompt(prompt_token_ids=prompt_token_ids, **prompt)
137 +
138 + if params.needs_detokenization and "prompt" not in prompt:
139 + if "prompt_token_ids" not in prompt:
140 + raise RuntimeError("Cannot run detokenization on embeddings")
141 +
142 + tokenizer = self.get_tokenizer()
143 + prompt_text = tokenizer.decode(prompt["prompt_token_ids"]) # type: ignore[typeddict-item]
144 + prompt["prompt"] = prompt_text # type: ignore[typeddict-unknown-key]
145 +
146 + return params.apply_post_tokenization(self.tokenizer, prompt) # type: ignore[arg-type]
147 +
148 + def tokenize_prompts(
149 + self,
150 + prompts: list[TextPrompt | TokensPrompt | EmbedsPrompt],
151 + params: TokenizeParams,
152 + ) -> list[TokensPrompt | EmbedsPrompt]:
153 + return [self.tokenize_prompt(prompt, params) for prompt in prompts]
154 +
155 + async def tokenize_prompt_async(
156 + self,
157 + prompt: TextPrompt | TokensPrompt | EmbedsPrompt,
158 + params: TokenizeParams,
159 + ) -> TokensPrompt | EmbedsPrompt:
160 + if "prompt_token_ids" not in prompt and "prompt_embeds" not in prompt:
161 + prompt = params.apply_pre_tokenization(self.tokenizer, prompt)
162 +
163 + tokenizer = self.get_async_tokenizer()
164 + prompt_token_ids = await tokenizer.encode(
165 + prompt["prompt"],
166 + **params.get_encode_kwargs(),
167 + )
168 +
169 + prompt = TokensPrompt(prompt_token_ids=prompt_token_ids, **prompt)
170 +
171 + if params.needs_detokenization and "prompt" not in prompt:
172 + if "prompt_token_ids" not in prompt:
173 + raise RuntimeError("Cannot run detokenization on embeddings")
174 +
175 + tokenizer = self.get_async_tokenizer()
176 + prompt_text = await tokenizer.decode(prompt["prompt_token_ids"]) # type: ignore[typeddict-item]
177 + prompt["prompt"] = prompt_text # type: ignore[typeddict-unknown-key]
178 +
179 + return params.apply_post_tokenization(self.tokenizer, prompt) # type: ignore[arg-type]
180 +
181 + async def tokenize_prompts_async(
182 + self,
183 + prompts: list[TextPrompt | TokensPrompt | EmbedsPrompt],
184 + params: TokenizeParams,
185 + ) -> list[TokensPrompt | EmbedsPrompt]:
186 + return await asyncio.gather(
187 + *(self.tokenize_prompt_async(prompt, params) for prompt in prompts)
188 + )
vllm/renderers/terratorch.py
+8 -7
@@ -9,10 +9,11 @@ from vllm.entrypoints.chat_utils import (
9 parse_chat_messages,
10 parse_chat_messages_async,
11 )
12 -from vllm.inputs import TextPrompt, TokensPrompt
12 +from vllm.inputs import EmbedsPrompt, TextPrompt, TokensPrompt
13 from vllm.logger import init_logger
14 from vllm.tokenizers import TokenizerLike
15
16 +from .params import ChatParams
17 from .protocol import RendererLike
18
19 logger = init_logger(__name__)
@@ -45,8 +46,8 @@ class TerratorchRenderer(RendererLike):
46 def render_messages(
47 self,
48 messages: list[ChatCompletionMessageParam],
48 - **kwargs,
49 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
49 + params: ChatParams,
50 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
51 model_config = self.config
52
53 conversation, mm_data, mm_uuids = parse_chat_messages(
@@ -55,7 +56,7 @@ class TerratorchRenderer(RendererLike):
56 content_format="string",
57 )
58
58 - prompt = TokensPrompt(prompt_token_ids=[1])
59 + prompt = self.render_completion([1]) # Dummy token IDs
60 if mm_data is not None:
61 prompt["multi_modal_data"] = mm_data
62 if mm_uuids is not None:
@@ -66,8 +67,8 @@ class TerratorchRenderer(RendererLike):
67 async def render_messages_async(
68 self,
69 messages: list[ChatCompletionMessageParam],
69 - **kwargs,
70 - ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt]:
70 + params: ChatParams,
71 + ) -> tuple[list[ConversationMessage], TextPrompt | TokensPrompt | EmbedsPrompt]:
72 model_config = self.config
73
74 conversation, mm_data, mm_uuids = await parse_chat_messages_async(
@@ -76,7 +77,7 @@ class TerratorchRenderer(RendererLike):
77 content_format="string",
78 )
79
79 - prompt = TokensPrompt(prompt_token_ids=[1]) # Dummy token IDs
80 + prompt = self.render_completion([1]) # Dummy token IDs
81 if mm_data is not None:
82 prompt["multi_modal_data"] = mm_data
83 if mm_uuids is not None:
vllm/transformers_utils/config.py
+1 -1
@@ -834,7 +834,7 @@ def parse_pooling_type(pooling_name: str):
834 @cache
835 def get_sentence_transformer_tokenizer_config(
836 model: str | Path, revision: str | None = "main"
837 -):
837 +) -> dict[str, Any] | None:
838 """
839 Returns the tokenization configuration dictionary for a
840 given Sentence Transformer BERT model.
vllm/utils/async_utils.py
+5 -2
@@ -50,14 +50,17 @@ class AsyncMicrobatchTokenizer:
50 self._executor = ThreadPoolExecutor(max_workers=1)
51
52 # === Public async API ===
53 - async def __call__(self, prompt, **kwargs):
53 + async def __call__(self, prompt, **kwargs) -> BatchEncoding:
54 result_future: Future = self._loop.create_future()
55 key = self._queue_key("encode", kwargs)
56 queue = self._get_queue(self._loop, key)
57 await queue.put((prompt, kwargs, result_future))
58 return await result_future
59
60 - async def decode(self, token_ids, **kwargs):
60 + async def encode(self, prompt, **kwargs) -> list[int]:
61 + return (await self(prompt, **kwargs)).input_ids
62 +
63 + async def decode(self, token_ids, **kwargs) -> str:
64 result_future: Future = self._loop.create_future()
65 key = self._queue_key("decode", kwargs)
66 queue = self._get_queue(self._loop, key)
vllm/v1/engine/async_llm.py
+21 -28
@@ -16,7 +16,6 @@ from vllm import TokensPrompt
16 from vllm.config import VllmConfig
17 from vllm.engine.arg_utils import AsyncEngineArgs
18 from vllm.engine.protocol import EngineClient
19 -from vllm.entrypoints.utils import _validate_truncation_size
19 from vllm.inputs import PromptType
20 from vllm.inputs.data import StreamingInput
21 from vllm.logger import init_logger
@@ -25,7 +24,7 @@ from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
24 from vllm.outputs import STREAM_FINISHED, PoolingRequestOutput, RequestOutput
25 from vllm.plugins.io_processors import get_io_processor
26 from vllm.pooling_params import PoolingParams
28 -from vllm.renderers import RendererLike
27 +from vllm.renderers import RendererLike, merge_kwargs
28 from vllm.sampling_params import RequestOutputKind, SamplingParams
29 from vllm.tasks import SupportedTask
30 from vllm.tokenizers import TokenizerLike
@@ -304,13 +303,20 @@ class AsyncLLM(EngineClient):
303 "prompt logprobs"
304 )
305
307 - if tokenization_kwargs is None:
308 - tokenization_kwargs = {}
309 - _validate_truncation_size(
310 - self.model_config.max_model_len,
311 - params.truncate_prompt_tokens,
312 - tokenization_kwargs,
313 - )
306 + if params.truncate_prompt_tokens is not None:
307 + params_type = type(params).__name__
308 + warnings.warn(
309 + f"The `truncate_prompt_tokens` parameter in `{params_type}` "
310 + "is deprecated and will be removed in v0.16. "
311 + "Please pass it via `tokenization_kwargs` instead.",
312 + DeprecationWarning,
313 + stacklevel=2,
314 + )
315 +
316 + tokenization_kwargs = merge_kwargs(
317 + tokenization_kwargs,
318 + dict(truncate_prompt_tokens=params.truncate_prompt_tokens),
319 + )
320
321 if isinstance(prompt, AsyncGenerator):
322 # Streaming input case.
@@ -344,12 +350,12 @@ class AsyncLLM(EngineClient):
350 request_id,
351 prompt,
352 params,
347 - arrival_time,
348 - lora_request,
349 - tokenization_kwargs,
350 - trace_headers,
351 - priority,
352 - data_parallel_rank,
353 + arrival_time=arrival_time,
354 + lora_request=lora_request,
355 + tokenization_kwargs=tokenization_kwargs,
356 + trace_headers=trace_headers,
357 + priority=priority,
358 + data_parallel_rank=data_parallel_rank,
359 )
360 prompt_text = get_prompt_text(prompt)
361
@@ -757,7 +763,6 @@ class AsyncLLM(EngineClient):
763 lora_request: LoRARequest | None = None,
764 trace_headers: Mapping[str, str] | None = None,
765 priority: int = 0,
760 - truncate_prompt_tokens: int | None = None,
766 tokenization_kwargs: dict[str, Any] | None = None,
767 ) -> AsyncGenerator[PoolingRequestOutput, None]:
768 """
@@ -772,22 +777,10 @@ class AsyncLLM(EngineClient):
777
778 The caller of generate() iterates the returned AsyncGenerator,
779 returning the RequestOutput back to the caller.
775 -
776 - NOTE: truncate_prompt_tokens is deprecated in v0.14.
777 - TODO: Remove truncate_prompt_tokens in v0.15.
780 """
781
782 q: RequestOutputCollector | None = None
783 try:
782 - if truncate_prompt_tokens is not None:
783 - warnings.warn(
784 - "The `truncate_prompt_tokens` parameter in `AsyncLLM.encode()` "
785 - "is deprecated and will be removed in v0.15. "
786 - "Please use `pooling_params.truncate_prompt_tokens` instead.",
787 - DeprecationWarning,
788 - stacklevel=2,
789 - )
790 -
784 q = await self.add_request(
785 request_id,
786 prompt,