mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
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:
1 parent
3934ee8e34
commit
48d11fcf74
2 files changed
+80
-5
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user