diff --git a/backend/python/sglang/backend.py b/backend/python/sglang/backend.py index 76d99a726aaf..12a5c6dfb45e 100644 --- a/backend/python/sglang/backend.py +++ b/backend/python/sglang/backend.py @@ -90,6 +90,14 @@ _ONE_DAY_IN_SECONDS = 60 * 60 * 24 + +# proto3 has no field presence, so an explicit 0 is indistinguishable from +# "unset" and the zero-filter below would drop it. These two fields have a +# meaningful zero a caller can actually intend: temperature 0 is greedy +# decoding, and 0 is a valid seed. Silently substituting a default for either +# turns a reproducible request into a random one. +_EXPLICIT_ZERO_FIELDS = ("Temperature", "Seed") + MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1')) @@ -323,7 +331,7 @@ def _build_sampling_params(self, request) -> dict: if not hasattr(request, proto_field): continue value = getattr(request, proto_field) - if proto_field != "Temperature" and value in (None, 0, 0.0, [], False, ""): + if proto_field not in _EXPLICIT_ZERO_FIELDS and value in (None, 0, 0.0, [], False, ""): continue # repeated fields come back as RepeatedScalarContainer — convert if hasattr(value, "__iter__") and not isinstance(value, (str, bytes)): diff --git a/backend/python/sglang/test.py b/backend/python/sglang/test.py index c50ed577f941..52b2d9b1f476 100644 --- a/backend/python/sglang/test.py +++ b/backend/python/sglang/test.py @@ -128,11 +128,14 @@ def kwargs_for(metadata): self.assertNotIn("enable_thinking", kwargs_for({})) self.assertIs(kwargs_for({"enable_thinking": "FALSE"})["enable_thinking"], False) - def test_explicit_zero_temperature_is_preserved(self): - """Temperature=0 is valid greedy decoding, not an unset value.""" + def test_explicit_zero_temperature_and_seed_are_preserved(self): + """Temperature=0 is greedy decoding and 0 is a valid seed — neither is + an unset value. A dropped seed turns a reproducible request random.""" from types import SimpleNamespace servicer = self._servicer() + import sys as _sys + _SEED_KEY_FOR_TEST = _sys.modules["backend"]._SEED_KEY request = SimpleNamespace( Temperature=0, N=0, @@ -154,8 +157,12 @@ def test_explicit_zero_temperature_is_preserved(self): params = servicer._build_sampling_params(request) self.assertEqual(params["temperature"], 0) - # Other protobuf-default scalar fields must remain filtered. + self.assertEqual(params[_SEED_KEY_FOR_TEST], 0) + # Other protobuf-default scalar fields must remain filtered. top_k=0 in + # particular is not a value sglang accepts (-1 disables it), so it must + # keep falling through to the engine default. self.assertNotIn("top_p", params) + self.assertNotIn("top_k", params) if __name__ == "__main__": diff --git a/backend/python/vllm/backend.py b/backend/python/vllm/backend.py index 7235c8e0737a..af072b49b1be 100644 --- a/backend/python/vllm/backend.py +++ b/backend/python/vllm/backend.py @@ -60,6 +60,12 @@ _ONE_DAY_IN_SECONDS = 60 * 60 * 24 +# proto3 has no field presence, so an explicit 0 is indistinguishable from +# "unset". These two fields have a meaningful zero a caller can intend: +# temperature 0 is greedy decoding, and 0 is a valid seed. +_EXPLICIT_ZERO_FIELDS = ("Temperature", "Seed") + + # If MAX_WORKERS are specified in the environment use it, otherwise default to 1 MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1')) @@ -553,7 +559,9 @@ def _build_sampling_params(self, request): for request_field, param_field in request_to_sampling_params.items(): if hasattr(request, request_field): value = getattr(request, request_field) - if request_field == "Temperature" or value not in (None, 0, [], False): + # 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) return sampling_params diff --git a/backend/python/vllm/test.py b/backend/python/vllm/test.py index a0679d4ff8e5..d2932648e255 100644 --- a/backend/python/vllm/test.py +++ b/backend/python/vllm/test.py @@ -121,16 +121,18 @@ def test_sampling_params(self): finally: self.tearDown() - def test_explicit_zero_temperature_is_preserved(self): - """Temperature=0 is valid greedy decoding, not an unset value.""" + def test_explicit_zero_temperature_and_seed_are_preserved(self): + """Temperature=0 is greedy decoding and 0 is a valid seed — neither is + an unset value. A dropped seed turns a reproducible request random.""" import sys, os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from backend import BackendServicer servicer = BackendServicer() - request = backend_pb2.PredictOptions(Prompt="hello", Temperature=0) + request = backend_pb2.PredictOptions(Prompt="hello", Temperature=0, Seed=0) 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. self.assertEqual(sampling_params.top_p, 0.9)