Bound generation inputs before expensive work (#51447)

Signed-off-by: Clinton Thomas <1033162+KernelClint@users.noreply.github.com> Co-authored-by: Lucas Bourtoule <35483370+dhalf@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Clinton Thomas committed Aug 11, 2026 at 10:03 UTC b64a2708b06b6329420702bffce994543c1ec6d2
11 files changed +421 -37
rust/src/server/src/routes/openai/utils/types.rs
+47 -2
@@ -54,9 +54,23 @@ impl StringOrArray {
54 }
55 }
56
57 -/// Validates stop sequences (non-empty strings)
57 +fn max_stop_strings() -> usize {
58 + std::env::var("VLLM_MAX_STOP_STRINGS")
59 + .ok()
60 + .and_then(|value| value.parse().ok())
61 + .unwrap_or(4)
62 +}
63 +
64 +/// Validates stop sequences (at most the configured number of non-empty strings).
65 pub fn validate_stop(stop: &StringOrArray) -> Result<(), validator::ValidationError> {
59 - if stop.as_slice().iter().any(|s| s.is_empty()) {
66 + let stop = stop.as_slice();
67 + let max_stop_strings = max_stop_strings();
68 + if stop.len() > max_stop_strings {
69 + let mut error = validator::ValidationError::new("too_many_stop_strings");
70 + error.code = format!("stop strings must contain at most {max_stop_strings} items").into();
71 + return Err(error);
72 + }
73 + if stop.iter().any(|s| s.is_empty()) {
74 return Err(validator::ValidationError::new(
75 "stop strings cannot be empty",
76 ));
@@ -64,6 +78,37 @@ pub fn validate_stop(stop: &StringOrArray) -> Result<(), validator::ValidationEr
78 Ok(())
79 }
80
81 +#[cfg(test)]
82 +mod tests {
83 + use super::{StringOrArray, validate_stop};
84 +
85 + #[test]
86 + fn validate_stop_accepts_four_stop_strings() {
87 + let stop = StringOrArray::Array(
88 + ["one", "two", "three", "four"].into_iter().map(str::to_string).collect(),
89 + );
90 +
91 + validate_stop(&stop).expect("four stop strings should be accepted");
92 + }
93 +
94 + #[test]
95 + fn validate_stop_rejects_more_than_four_stop_strings() {
96 + let stop = StringOrArray::Array(
97 + ["one", "two", "three", "four", "five"]
98 + .into_iter()
99 + .map(str::to_string)
100 + .collect(),
101 + );
102 +
103 + let error = validate_stop(&stop).expect_err("five stop strings should be rejected");
104 +
105 + assert_eq!(
106 + error.code.as_ref(),
107 + "stop strings must contain at most 4 items"
108 + );
109 + }
110 +}
111 +
112 // ============================================================================
113 // Validation helpers
114 // ============================================================================
tests/test_request_input_bounds.py new
+273
@@ -0,0 +1,273 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Bounds on request-controlled inputs that would otherwise amplify work.
4 +
5 +Consolidates the regression tests for the request-input amplification fixes:
6 +stop-string caps, bad-words dedup/tokenization limit, stop-token-id dedup,
7 +beam-width/sequence caps, and the DeepSeek history-scan bound.
8 +"""
9 +
10 +import os
11 +import subprocess
12 +import sys
13 +from collections.abc import Callable
14 +from typing import Protocol
15 +
16 +import pytest
17 +from pydantic import ValidationError
18 +
19 +import vllm.envs as envs
20 +from vllm import SamplingParams
21 +from vllm.entrypoints.openai.chat_completion.protocol import (
22 + BatchChatCompletionRequest,
23 + ChatCompletionRequest,
24 +)
25 +from vllm.entrypoints.openai.completion.protocol import CompletionRequest
26 +from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
27 +from vllm.exceptions import VLLMValidationError
28 +from vllm.sampling_params import BeamSearchParams
29 +from vllm.tokenizers import deepseek_v4_encoding, deepseek_v32_encoding
30 +
31 +pytestmark = [pytest.mark.cpu_test, pytest.mark.skip_global_cleanup]
32 +
33 +
34 +# --- Stop strings: public requests cap the number of stop strings ---------
35 +
36 +
37 +class _StopRequest(Protocol):
38 + stop: str | list[str] | None
39 +
40 +
41 +def _completion_request(stop: list[str]) -> _StopRequest:
42 + return CompletionRequest(model="test-model", prompt="hello", stop=stop)
43 +
44 +
45 +def _chat_request(stop: list[str]) -> _StopRequest:
46 + return ChatCompletionRequest(
47 + model="test-model",
48 + messages=[{"role": "user", "content": "hello"}],
49 + stop=stop,
50 + )
51 +
52 +
53 +def _batch_chat_request(stop: list[str]) -> _StopRequest:
54 + return BatchChatCompletionRequest(
55 + model="test-model",
56 + messages=[[{"role": "user", "content": "hello"}]],
57 + stop=stop,
58 + )
59 +
60 +
61 +def _responses_request(stop: list[str]) -> _StopRequest:
62 + return ResponsesRequest(model="test-model", input="hello", stop=stop)
63 +
64 +
65 +REQUEST_BUILDERS: list[Callable[[list[str]], _StopRequest]] = [
66 + _completion_request,
67 + _chat_request,
68 + _batch_chat_request,
69 + _responses_request,
70 +]
71 +
72 +
73 +@pytest.mark.parametrize("build_request", REQUEST_BUILDERS)
74 +def test_public_requests_accept_four_stop_strings(
75 + build_request: Callable[[list[str]], _StopRequest],
76 +):
77 + stop = ["one", "two", "three", "four"]
78 +
79 + request = build_request(stop)
80 +
81 + assert request.stop == stop
82 +
83 +
84 +@pytest.mark.parametrize("build_request", REQUEST_BUILDERS)
85 +def test_public_requests_reject_more_than_four_stop_strings(
86 + build_request: Callable[[list[str]], _StopRequest],
87 +):
88 + with pytest.raises(ValidationError, match="at most 4"):
89 + build_request(["one", "two", "three", "four", "five"])
90 +
91 +
92 +def test_stop_string_limit_can_be_overridden():
93 + env = os.environ.copy()
94 + env["VLLM_MAX_STOP_STRINGS"] = "1"
95 + code = """
96 +from pydantic import ValidationError
97 +from vllm.entrypoints.openai.completion.protocol import CompletionRequest
98 +
99 +try:
100 + CompletionRequest(
101 + model="test-model",
102 + prompt="hello",
103 + stop=["one", "two"],
104 + )
105 +except ValidationError as error:
106 + assert "at most 1" in str(error)
107 +else:
108 + raise AssertionError("configured stop-string limit was not enforced")
109 +"""
110 +
111 + subprocess.run([sys.executable, "-c", code], check=True, env=env)
112 +
113 +
114 +# --- Stop token ids: duplicates are deduplicated in order ------------------
115 +
116 +
117 +def test_duplicate_stop_token_ids_are_deduplicated_in_order():
118 + params = SamplingParams(stop_token_ids=[42, 7, 42, 9, 7])
119 +
120 + assert params.stop_token_ids == [42, 7, 9]
121 + assert params.all_stop_token_ids == {7, 9, 42}
122 +
123 +
124 +# --- Bad words: dedup, and the tokenization pass is bounded ----------------
125 +
126 +
127 +class MockTokenizer:
128 + max_token_id = 1024
129 +
130 + def __init__(self):
131 + self.calls = 0
132 +
133 + def encode(self, text: str, add_special_tokens: bool = False) -> list[int]:
134 + self.calls += 1
135 + return [2] if text.startswith(" ") else [1]
136 +
137 +
138 +def test_duplicate_bad_words_are_deduplicated_in_order():
139 + params = SamplingParams(bad_words=["bad", "worse", "bad", "worst"])
140 +
141 + assert params.bad_words == ["bad", "worse", "worst"]
142 +
143 +
144 +def test_bad_word_tokenization_stops_at_worker_limit():
145 + params = SamplingParams(bad_words=[f"word-{i}" for i in range(65)])
146 + tokenizer = MockTokenizer()
147 +
148 + with pytest.raises(VLLMValidationError, match="Too many bad words"):
149 + params.update_from_tokenizer(tokenizer)
150 +
151 + assert tokenizer.calls == 129
152 +
153 +
154 +def test_bad_word_tokenization_limit_can_be_overridden(monkeypatch):
155 + monkeypatch.setenv("VLLM_MAX_NUM_BAD_WORDS", "2")
156 + params = SamplingParams(bad_words=["bad", "worse"])
157 + tokenizer = MockTokenizer()
158 +
159 + with pytest.raises(VLLMValidationError, match="The max number is 2"):
160 + params.update_from_tokenizer(tokenizer)
161 +
162 + assert tokenizer.calls == 3
163 +
164 +
165 +# --- Beam search: beam width / n honor the sequence cap --------------------
166 +
167 +
168 +def _set_max_n(monkeypatch: pytest.MonkeyPatch, value: int) -> None:
169 + monkeypatch.setenv("VLLM_MAX_N_SEQUENCES", str(value))
170 + if hasattr(envs.__getattr__, "cache_clear"):
171 + envs.__getattr__.cache_clear()
172 +
173 +
174 +def test_direct_beam_width_rejects_values_over_sequence_cap(
175 + monkeypatch: pytest.MonkeyPatch,
176 +):
177 + _set_max_n(monkeypatch, 4)
178 +
179 + with pytest.raises(VLLMValidationError, match="beam_width must be at most 4"):
180 + BeamSearchParams(beam_width=5, max_tokens=1)
181 +
182 +
183 +def test_chat_beam_conversion_rejects_n_before_stream_state_allocation(
184 + monkeypatch: pytest.MonkeyPatch,
185 +):
186 + _set_max_n(monkeypatch, 4)
187 + request = ChatCompletionRequest(
188 + model="test-model",
189 + messages=[{"role": "user", "content": "test"}],
190 + n=5,
191 + stream=True,
192 + use_beam_search=True,
193 + max_tokens=1,
194 + )
195 +
196 + with pytest.raises(VLLMValidationError, match="beam_width must be at most 4"):
197 + request.to_beam_search_params(max_tokens=1, default_sampling_params={})
198 +
199 +
200 +def test_chat_beam_conversion_accepts_n_at_sequence_cap(
201 + monkeypatch: pytest.MonkeyPatch,
202 +):
203 + _set_max_n(monkeypatch, 4)
204 + request = ChatCompletionRequest(
205 + model="test-model",
206 + messages=[{"role": "user", "content": "test"}],
207 + n=4,
208 + stream=True,
209 + use_beam_search=True,
210 + max_tokens=1,
211 + )
212 +
213 + params = request.to_beam_search_params(max_tokens=1, default_sampling_params={})
214 +
215 + assert params.beam_width == 4
216 +
217 +
218 +# --- DeepSeek encoders: the last-user scan runs once per conversation ------
219 +
220 +
221 +ENCODING_MODULES = [deepseek_v32_encoding, deepseek_v4_encoding]
222 +
223 +
224 +@pytest.mark.parametrize(
225 + "encoding_module",
226 + ENCODING_MODULES,
227 + ids=["deepseek_v32", "deepseek_v4"],
228 +)
229 +def test_encode_messages_scans_last_user_once_per_conversation(
230 + monkeypatch: pytest.MonkeyPatch,
231 + encoding_module,
232 +):
233 + calls = 0
234 + original_find_last_user_index = encoding_module.find_last_user_index
235 +
236 + def counted_find_last_user_index(messages):
237 + nonlocal calls
238 + calls += 1
239 + return original_find_last_user_index(messages)
240 +
241 + monkeypatch.setattr(
242 + encoding_module,
243 + "find_last_user_index",
244 + counted_find_last_user_index,
245 + )
246 +
247 + messages = [{"role": "user", "content": "Hello"}]
248 + messages.extend({"role": "assistant", "content": "Hi"} for _ in range(8))
249 +
250 + encoding_module.encode_messages(messages, thinking_mode="chat")
251 +
252 + assert calls == 1
253 +
254 +
255 +@pytest.mark.parametrize(
256 + "encoding_module",
257 + ENCODING_MODULES,
258 + ids=["deepseek_v32", "deepseek_v4"],
259 +)
260 +def test_encode_messages_preserves_small_chat_prompt(encoding_module):
261 + prompt = encoding_module.encode_messages(
262 + [
263 + {"role": "user", "content": "Hello"},
264 + {"role": "assistant", "content": "Hi"},
265 + {"role": "assistant", "content": "Again"},
266 + ],
267 + thinking_mode="chat",
268 + )
269 +
270 + assert prompt == (
271 + "<|begin▁of▁sentence|><|User|>Hello<|Assistant|></think>"
272 + "Hi<|end▁of▁sentence|>Again<|end▁of▁sentence|>"
273 + )
vllm/entrypoints/openai/chat_completion/protocol.py
+3 -2
@@ -30,6 +30,7 @@ from vllm.entrypoints.openai.engine.protocol import (
30 FunctionDefinition,
31 OpenAIBaseModel,
32 PerRequestTimingMetrics,
33 + StopParam,
34 StreamOptions,
35 ToolCall,
36 UsageInfo,
@@ -228,7 +229,7 @@ class ChatCompletionRequest(OpenAIBaseModel):
229 presence_penalty: float | None = 0.0
230 response_format: AnyResponseFormat | None = None
231 seed: int | None = Field(None, ge=_INT64_MIN, le=_INT64_MAX)
231 - stop: str | list[str] | None = []
232 + stop: StopParam = []
233 stream: bool | None = False
234 stream_options: StreamOptions | None = None
235 temperature: float | None = None
@@ -1077,7 +1078,7 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
1078 presence_penalty: float | None = 0.0
1079 response_format: Any | None = None
1080 seed: int | None = Field(None, ge=_INT64_MIN, le=_INT64_MAX)
1080 - stop: str | list[str] | None = Field(default_factory=list)
1081 + stop: StopParam = Field(default_factory=list)
1082 temperature: float | None = None
1083 top_p: float | None = None
1084 user: str | None = None
vllm/entrypoints/openai/completion/protocol.py
+2 -1
@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.engine.protocol import (
14 AnyResponseFormat,
15 OpenAIBaseModel,
16 PerRequestTimingMetrics,
17 + StopParam,
18 StreamOptions,
19 UsageInfo,
20 structured_outputs_from_response_format,
@@ -61,7 +62,7 @@ class CompletionRequest(OpenAIBaseModel):
62 n: int = 1
63 presence_penalty: float | None = 0.0
64 seed: int | None = Field(None, ge=_INT64_MIN, le=_INT64_MAX)
64 - stop: str | list[str] | None = []
65 + stop: StopParam = []
66 stream: bool | None = False
67 stream_options: StreamOptions | None = None
68 suffix: str | None = None
vllm/entrypoints/openai/engine/protocol.py
+6 -1
@@ -6,7 +6,7 @@
6 import json
7 import time
8 from http import HTTPStatus
9 -from typing import Any, ClassVar, Literal, TypeAlias
9 +from typing import Annotated, Any, ClassVar, Literal, TypeAlias
10
11 import regex as re
12 from pydantic import (
@@ -17,6 +17,7 @@ from pydantic import (
17 model_validator,
18 )
19
20 +import vllm.envs as envs
21 from vllm.config.utils import replace
22 from vllm.entrypoints.chat_utils import make_tool_call_id
23 from vllm.exceptions import VLLMServerError, VLLMValidationError
@@ -27,6 +28,10 @@ from vllm.utils.import_utils import resolve_obj_by_qualname
28
29 logger = init_logger(__name__)
30
31 +StopParam: TypeAlias = (
32 + str | Annotated[list[str], Field(max_length=envs.VLLM_MAX_STOP_STRINGS)] | None
33 +)
34 +
35
36 class OpenAIBaseModel(BaseModel):
37 # OpenAI API does allow extra fields
vllm/entrypoints/openai/responses/protocol.py
+2 -2
@@ -60,7 +60,7 @@ from vllm.entrypoints.chat_utils import (
60 ChatCompletionMessageParam,
61 ChatTemplateContentFormatOption,
62 )
63 -from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel
63 +from vllm.entrypoints.openai.engine.protocol import OpenAIBaseModel, StopParam
64 from vllm.exceptions import VLLMValidationError
65 from vllm.logger import init_logger
66 from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
@@ -281,7 +281,7 @@ class ResponsesRequest(OpenAIBaseModel):
281
282 repetition_penalty: float | None = None
283 seed: int | None = Field(None, ge=_INT64_MIN, le=_INT64_MAX)
284 - stop: str | list[str] | None = []
284 + stop: StopParam = []
285 ignore_eos: bool = False
286 vllm_xargs: dict[str, str | int | float | list[str | int | float]] | None = Field(
287 default=None,
vllm/envs.py
+14
@@ -108,6 +108,9 @@ if TYPE_CHECKING:
108 VLLM_HTTP_TIMEOUT_KEEP_ALIVE: int = 5 # seconds
109 VLLM_MAX_N_SEQUENCES: int = 16384
110 VLLM_MAX_COMPLETION_PROMPTS: int = 1024
111 + VLLM_MAX_STOP_STRINGS: int = 4
112 + VLLM_MAX_NUM_BAD_WORDS: int = 128
113 + VLLM_MAX_BAD_WORDS_TOTAL_TOKENS: int = 1024
114 VLLM_PLUGINS: list[str] | None = None
115 VLLM_LORA_RESOLVER_CACHE_DIR: str | None = None
116 VLLM_LORA_RESOLVER_HF_REPO_LIST: str | None = None
@@ -1114,6 +1117,17 @@ environment_variables: dict[str, Callable[[], Any]] = {
1117 "VLLM_MAX_COMPLETION_PROMPTS": lambda: int(
1118 os.environ.get("VLLM_MAX_COMPLETION_PROMPTS", "1024")
1119 ),
1120 + # Maximum number of stop strings allowed in a single request.
1121 + "VLLM_MAX_STOP_STRINGS": lambda: int(os.environ.get("VLLM_MAX_STOP_STRINGS", "4")),
1122 + # Maximum number of bad-word token sequences generated per request.
1123 + "VLLM_MAX_NUM_BAD_WORDS": lambda: int(
1124 + os.environ.get("VLLM_MAX_NUM_BAD_WORDS", "128")
1125 + ),
1126 + # Maximum total number of bad-word tokens (summed across all bad words)
1127 + # allowed per request. Bounds the per-request GPU buffer width.
1128 + "VLLM_MAX_BAD_WORDS_TOTAL_TOKENS": lambda: int(
1129 + os.environ.get("VLLM_MAX_BAD_WORDS_TOTAL_TOKENS", "1024")
1130 + ),
1131 # a list of plugin names to load, separated by commas.
1132 # if this is not set, it means all plugins will be loaded
1133 # if this is set to an empty string, no plugins will be loaded
vllm/sampling_params.py
+33 -13
@@ -32,6 +32,22 @@ MAX_LOGPROB_TOKEN_IDS = 128
32 the per-request row width allocated by the sampler's `LogprobTokenIdsState`."""
33
34
35 +def _verify_num_sequences(value: int, parameter_name: str) -> None:
36 + if not isinstance(value, int):
37 + raise VLLMValidationError(
38 + f"{parameter_name} must be an int, but is of type {type(value)}"
39 + )
40 + if value < 1:
41 + raise VLLMValidationError(f"{parameter_name} must be at least 1, got {value}.")
42 + max_n = envs.VLLM_MAX_N_SEQUENCES
43 + if value > max_n:
44 + raise VLLMValidationError(
45 + f"{parameter_name} must be at most {max_n}, got {value}. "
46 + "To increase this limit, set the VLLM_MAX_N_SEQUENCES "
47 + "environment variable."
48 + )
49 +
50 +
51 def validate_thinking_token_budget(value: int | float | bool | None) -> int | None:
52 """Validate ``thinking_token_budget``; return ``None`` if unset."""
53 if value is None:
@@ -479,9 +495,13 @@ class SamplingParams(
495
496 if self.stop_token_ids is None:
497 self.stop_token_ids = []
498 + else:
499 + self.stop_token_ids = list(dict.fromkeys(self.stop_token_ids))
500
501 if self.bad_words is None:
502 self.bad_words = []
503 + else:
504 + self.bad_words = list(dict.fromkeys(self.bad_words))
505
506 if self.logprobs is True:
507 self.logprobs = 1
@@ -513,19 +533,7 @@ class SamplingParams(
533 self.skip_reading_prefix_cache = self.prompt_logprobs is not None
534
535 def _verify_args(self) -> None:
516 - if not isinstance(self.n, int):
517 - raise VLLMValidationError(
518 - f"n must be an int, but is of type {type(self.n)}"
519 - )
520 - if self.n < 1:
521 - raise VLLMValidationError(f"n must be at least 1, got {self.n}.")
522 - max_n = envs.VLLM_MAX_N_SEQUENCES
523 - if self.n > max_n:
524 - raise VLLMValidationError(
525 - f"n must be at most {max_n}, got {self.n}. "
526 - "To increase this limit, set the VLLM_MAX_N_SEQUENCES "
527 - "environment variable."
528 - )
536 + _verify_num_sequences(self.n, "n")
537 if not -2.0 <= self.presence_penalty <= 2.0:
538 raise VLLMValidationError(
539 f"presence_penalty must be in [-2, 2], got {self.presence_penalty}."
@@ -677,6 +685,7 @@ class SamplingParams(
685 if not self.bad_words:
686 return
687 self._bad_words_token_ids = []
688 + max_num_bad_words = envs.VLLM_MAX_NUM_BAD_WORDS
689 for bad_word in self.bad_words:
690 # To prohibit words both at the beginning
691 # and in the middle of text
@@ -696,6 +705,14 @@ class SamplingParams(
705 and len(prompt_token_ids) == len(self._bad_words_token_ids[-1])
706 ):
707 self._bad_words_token_ids.append(prompt_token_ids)
708 + if len(self._bad_words_token_ids) > max_num_bad_words:
709 + raise VLLMValidationError(
710 + f"Too many bad words after tokenization: "
711 + f"{len(self._bad_words_token_ids)}. "
712 + f"The max number is {max_num_bad_words}.",
713 + parameter="bad_words",
714 + value=self.bad_words,
715 + )
716
717 invalid_token_ids = [
718 token_id
@@ -1147,3 +1164,6 @@ class BeamSearchParams(
1164 length_penalty: float = 1.0
1165 include_stop_str_in_output: bool = False
1166 structured_outputs: StructuredOutputsParams | None = None
1167 +
1168 + def __post_init__(self) -> None:
1169 + _verify_num_sequences(self.beam_width, "beam_width")
vllm/tokenizers/deepseek_v32_encoding.py
+13 -3
@@ -119,7 +119,10 @@ def find_last_user_index(messages: list[dict[str, Any]]) -> int:
119
120
121 def render_message(
122 - index: int, messages: list[dict[str, Any]], thinking_mode: str
122 + index: int,
123 + messages: list[dict[str, Any]],
124 + thinking_mode: str,
125 + last_user_idx: int | None = None,
126 ) -> str:
127 if not (0 <= index < len(messages)):
128 raise ValueError(
@@ -130,7 +133,9 @@ def render_message(
133
134 prompt = ""
135 msg = messages[index]
133 - last_user_idx = find_last_user_index(messages)
136 + last_user_idx = (
137 + find_last_user_index(messages) if last_user_idx is None else last_user_idx
138 + )
139
140 role = msg.get("role")
141 content = msg.get("content")
@@ -294,9 +299,14 @@ def encode_messages(
299 if thinking_mode == "thinking" and drop_thinking:
300 full_messages = drop_thinking_messages(full_messages)
301
302 + last_user_idx = find_last_user_index(full_messages)
303 +
304 for idx in range(len(messages)):
305 prompt += render_message(
299 - idx + len(context), full_messages, thinking_mode=thinking_mode
306 + idx + len(context),
307 + full_messages,
308 + thinking_mode=thinking_mode,
309 + last_user_idx=last_user_idx,
310 )
311
312 return prompt
vllm/tokenizers/deepseek_v4_encoding.py
+15 -2
@@ -199,7 +199,14 @@ def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
199 # Message Rendering
200 # ============================================================
201
202 -def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: str, drop_thinking: bool = True, reasoning_effort: Optional[str] = None) -> str:
202 +def render_message(
203 + index: int,
204 + messages: List[Dict[str, Any]],
205 + thinking_mode: str,
206 + drop_thinking: bool = True,
207 + reasoning_effort: Optional[str] = None,
208 + last_user_idx: Optional[int] = None,
209 +) -> str:
210 """
211 Render a single message at the given index into its encoded string form.
212
@@ -213,6 +220,7 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
220 drop_thinking: Whether to drop reasoning content from earlier turns.
221 reasoning_effort: Reasoning effort level, one of "low", "high", "max".
222 None is treated as "low".
223 + last_user_idx: Cached index of the last user/developer message.
224
225 Returns:
226 Encoded string for this message.
@@ -222,7 +230,9 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
230
231 prompt = ""
232 msg = messages[index]
225 - last_user_idx = find_last_user_index(messages)
233 + last_user_idx = (
234 + find_last_user_index(messages) if last_user_idx is None else last_user_idx
235 + )
236
237 role = msg.get("role")
238 content = msg.get("content")
@@ -544,6 +554,8 @@ def encode_messages(
554 num_to_render = len(messages)
555 context_len = len(context)
556
557 + last_user_idx = find_last_user_index(full_messages)
558 +
559 for idx in range(num_to_render):
560 prompt += render_message(
561 idx + context_len,
@@ -551,6 +563,7 @@ def encode_messages(
563 thinking_mode=thinking_mode,
564 drop_thinking=effective_drop_thinking,
565 reasoning_effort=reasoning_effort,
566 + last_user_idx=last_user_idx,
567 )
568
569 return prompt
vllm/v1/worker/gpu/sample/bad_words.py
+13 -11
@@ -3,14 +3,12 @@
3 import numpy as np
4 import torch
5
6 +import vllm.envs as envs
7 from vllm.sampling_params import SamplingParams
8 from vllm.triton_utils import tl, triton
9 from vllm.v1.worker.gpu.buffer_utils import StagedWriteTensor, UvaBackedTensor
10 from vllm.v1.worker.gpu.states import RequestState
11
11 -MAX_BAD_WORDS_TOTAL_TOKENS = 1024 # Max total tokens for all bad words per request
12 -MAX_NUM_BAD_WORDS = 128 # Max number of bad words per request
13 -
12
13 class BadWordsState:
14 def __init__(self, req_states: RequestState):
@@ -18,15 +16,17 @@ class BadWordsState:
16 self.max_num_reqs = req_states.max_num_reqs
17 self.device = req_states.device
18
21 - # flattened bad word tokens: [max_num_reqs, MAX_BAD_WORDS_TOTAL_TOKENS]
19 + max_total_tokens = envs.VLLM_MAX_BAD_WORDS_TOTAL_TOKENS
20 + max_num_bad_words = envs.VLLM_MAX_NUM_BAD_WORDS
21 + # flattened bad word tokens: [max_num_reqs, VLLM_MAX_BAD_WORDS_TOTAL_TOKENS]
22 self.bad_word_token_ids = StagedWriteTensor(
23 - (self.max_num_reqs, MAX_BAD_WORDS_TOTAL_TOKENS),
23 + (self.max_num_reqs, max_total_tokens),
24 dtype=torch.int32,
25 device=self.device,
26 )
27 - # cumulative offsets of bad words: [max_num_reqs, MAX_NUM_BAD_WORDS + 1]
27 + # cumulative offsets of bad words: [max_num_reqs, VLLM_MAX_NUM_BAD_WORDS + 1]
28 self.bad_word_offsets = StagedWriteTensor(
29 - (self.max_num_reqs, MAX_NUM_BAD_WORDS + 1),
29 + (self.max_num_reqs, max_num_bad_words + 1),
30 dtype=torch.int32,
31 device=self.device,
32 )
@@ -40,10 +40,11 @@ class BadWordsState:
40 return
41
42 num_bad_words = len(bad_words_token_ids)
43 - if num_bad_words > MAX_NUM_BAD_WORDS:
43 + max_num_bad_words = envs.VLLM_MAX_NUM_BAD_WORDS
44 + if num_bad_words > max_num_bad_words:
45 raise ValueError(
46 f"Too many bad words: {num_bad_words}. "
46 - f"The max number is {MAX_NUM_BAD_WORDS}."
47 + f"The max number is {max_num_bad_words}."
48 )
49
50 # Flatten bad words and compute offsets
@@ -53,10 +54,11 @@ class BadWordsState:
54 flattened_tokens.extend(bad_word)
55 offsets.append(len(flattened_tokens))
56
56 - if len(flattened_tokens) > MAX_BAD_WORDS_TOTAL_TOKENS:
57 + max_total_tokens = envs.VLLM_MAX_BAD_WORDS_TOTAL_TOKENS
58 + if len(flattened_tokens) > max_total_tokens:
59 raise ValueError(
60 f"Too many total bad word tokens: {len(flattened_tokens)}. "
59 - f"The max is {MAX_BAD_WORDS_TOTAL_TOKENS}."
61 + f"The max is {max_total_tokens}."
62 )
63
64 # Stage writes