fix(vllm): build SamplingParams once so stop reaches the engine

_build_sampling_params constructed SamplingParams and then assigned the
request values with setattr. protobuf repeated fields (StopPrompts) stay
RepeatedScalarContainer that way, and __post_init__ never sees them:

- vLLM's argument checks assert `isinstance(self.stop, list)` and the
  msgpack encoder that ships the params to the engine core cannot
  serialise the container, so every request with `stop` failed
  (HTTP 500, EngineGenerateError), streaming or not.
- the stop-string hold-back buffer (output_text_buffer_length) was never
  set, so the start of a multi-character stop string could be streamed;
  seed -1 was not mapped to None.

Collect the values, turn repeated fields into lists and construct
SamplingParams once, as vLLM's OpenAI server does. Values vLLM rejects
(e.g. a penalty out of range, an empty stop string) now fail with vLLM's
validation error instead of being set unchecked.

Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
This commit is contained in:
pos-ei-don committed 2026-10-24 00:03:34 +02:00
1 parent 3934ee8e34
commit 48d11fcf74
2 files changed
+80 -5

No files matched your search

+11 -4
View File
@@ -555,17 +555,24 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
"TruncatePromptTokens": "truncate_prompt_tokens",
}
sampling_params = SamplingParams(top_p=0.9, max_tokens=200)
# Construct SamplingParams from the collected values, as vLLM's own
# OpenAI server does. Assigning them with setattr after construction
# skipped __post_init__ and left protobuf repeated fields (StopPrompts)
# as RepeatedScalarContainer, which vLLM's argument checks and the
# msgpack encoder to the engine core reject: every request with `stop`
# failed. Invalid values now raise vLLM's validation error here.
kwargs = {"top_p": 0.9, "max_tokens": 200}
for request_field, param_field in request_to_sampling_params.items():
if hasattr(request, request_field):
value = getattr(request, request_field)
# See _EXPLICIT_ZERO_FIELDS: temperature 0 is greedy decoding
# and 0 is a valid seed, so neither may be filtered out.
if request_field in _EXPLICIT_ZERO_FIELDS or value not in (None, 0, [], False):
setattr(sampling_params, param_field, value)
if not isinstance(value, (str, bytes, int, float, bool)):
value = list(value) # protobuf repeated field -> list
kwargs[param_field] = value
return sampling_params
return SamplingParams(**kwargs)
def _new_reasoning_parser(self, chat_template_kwargs):
"""Build the reasoning parser, telling it whether thinking is on.
+69 -1
View File
@@ -133,8 +133,13 @@ class TestBackendServicer(unittest.TestCase):
sampling_params = servicer._build_sampling_params(request)
self.assertEqual(sampling_params.temperature, 0)
self.assertEqual(sampling_params.seed, 0)
# Other protobuf-default scalar fields must remain filtered.
# Other protobuf-default scalar fields must remain filtered. (With
# temperature 0, vLLM's __post_init__ switches to greedy and sets
# top_p to 1.0, so check the default on a sampled request.)
request = backend_pb2.PredictOptions(Prompt="hello", Temperature=0.7, Seed=0)
sampling_params = servicer._build_sampling_params(request)
self.assertEqual(sampling_params.top_p, 0.9)
self.assertEqual(sampling_params.seed, 0)
def test_messages_to_dicts(self):
@@ -296,6 +301,69 @@ class TestBackendServicer(unittest.TestCase):
self.tearDown()
class TestSamplingParamsConstruction(unittest.TestCase):
"""Server-less tests for BackendServicer._build_sampling_params.
protobuf repeated fields (StopPrompts) are RepeatedScalarContainer, not
list. Assigned with setattr after construction they reached the engine
as-is: vLLM's argument checks assert a list, and the msgpack encoder that
ships the params to the engine core cannot serialise them, so every
request with `stop` failed.
"""
def _build(self, **fields):
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from backend import BackendServicer
servicer = BackendServicer.__new__(BackendServicer)
return servicer._build_sampling_params(backend_pb2.PredictOptions(**fields))
@staticmethod
def _engine_round_trip(sp):
"""What happens to the params on their way to the engine core."""
from vllm.v1.serial_utils import MsgpackEncoder
sp._verify_args()
sp.clone()
MsgpackEncoder().encode(sp)
def test_stop_is_a_list_and_reaches_the_engine(self):
sp = self._build(Prompt="x", StopPrompts=["</answer>", "##"], Tokens=50, Temperature=0.7)
self.assertIs(type(sp.stop), list)
self.assertEqual(sp.stop, ["</answer>", "##"])
self._engine_round_trip(sp)
def test_stop_string_hold_back_buffer_is_set(self):
"""Without it the start of a multi-character stop string is streamed
before the match completes."""
sp = self._build(Prompt="x", StopPrompts=["</answer>"], Tokens=50, Temperature=0.7)
self.assertEqual(sp.output_text_buffer_length, len("</answer>") - 1)
def test_request_values_are_applied(self):
sp = self._build(Prompt="x", Tokens=77, Temperature=0.6, TopP=0.95, TopK=20)
self.assertEqual(sp.max_tokens, 77)
self.assertEqual(sp.top_k, 20)
self.assertAlmostEqual(sp.top_p, 0.95, places=6)
self.assertEqual(sp.stop, [])
self._engine_round_trip(sp)
def test_seed_minus_one_means_random(self):
sp = self._build(Prompt="x", Seed=-1, Temperature=0.7)
self.assertIsNone(sp.seed)
def test_invalid_values_are_rejected(self):
"""Out-of-range values fail with vLLM's validation error instead of
reaching the engine."""
try:
from vllm.exceptions import VLLMValidationError as rejected
except ImportError: # older vLLM raises ValueError
rejected = ValueError
for fields in ({"PresencePenalty": 5.0, "Temperature": 0.7},
{"StopPrompts": [""], "Temperature": 0.7}):
with self.subTest(fields=fields):
with self.assertRaises(rejected):
self._build(Prompt="x", Tokens=10, **fields)
class TestStreamingToolParser(unittest.TestCase):
"""
Server-less unit tests for the streaming + tool-parser machinery in