From 548188888d2d3207c2879ce85b28577972087b58 Mon Sep 17 00:00:00 2001 From: Yigtwxx Date: Sat, 1 Aug 2026 13:09:35 +0300 Subject: [PATCH 1/2] [#17158][fix] Reject NaN top_p, min_p and temperature in SamplingParams The range checks for these three parameters were written in positive form, as `value < low or value > high`. Every comparison against NaN is False, so NaN passed validation and reached the sampling backend, which applies no guard of its own. Rewrite the three checks as negated range checks, which rejects NaN while leaving all other values unchanged. This is the form the neighbouring top_p_decay and top_p_min checks already use. Signed-off-by: Yigtwxx --- tensorrt_llm/sampling_params.py | 11 ++++-- tests/unittest/llmapi/test_sampling_params.py | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tensorrt_llm/sampling_params.py b/tensorrt_llm/sampling_params.py index 5a1c74fa4d1a..34c92979fe77 100644 --- a/tensorrt_llm/sampling_params.py +++ b/tensorrt_llm/sampling_params.py @@ -372,13 +372,18 @@ def _validate(self): For instance, while the greedy decoding with n > 1 is capable in the Executor class of C++ runtime, the LLM API disallows such combination. """ - if self.top_p is not None and (self.top_p < 0 or self.top_p > 1): + # These bounds are written as negated range checks rather than as + # `value < low or value > high`, so that NaN is rejected too: every + # comparison against NaN is False, which lets it slip through the + # positive form. The top_p_decay / top_p_min checks below already use + # this form. + if self.top_p is not None and not 0 <= self.top_p <= 1: raise ValueError(f"require 0 <= top_p <= 1, got top_p={self.top_p}") if self.top_k is not None and self.top_k < 0: raise ValueError(f"require top_k >= 0, got top_k={self.top_k}") - if self.min_p is not None and (self.min_p < 0 or self.min_p > 1): + if self.min_p is not None and not 0 <= self.min_p <= 1: raise ValueError(f"require 0 <= min_p <= 1, got min_p={self.min_p}") - if self.temperature is not None and self.temperature < 0: + if self.temperature is not None and not self.temperature >= 0: raise ValueError(f"require temperature >= 0, got temperature={self.temperature}") # Top-p decay param ranges mirror the hard checks in the diff --git a/tests/unittest/llmapi/test_sampling_params.py b/tests/unittest/llmapi/test_sampling_params.py index 8c5d2a7b5d3f..75d2a6c45e4d 100644 --- a/tests/unittest/llmapi/test_sampling_params.py +++ b/tests/unittest/llmapi/test_sampling_params.py @@ -117,6 +117,44 @@ def test_completion_logprobs_assignment_revalidates(): request.to_sampling_params(backend="pytorch") +@pytest.mark.parametrize("field", ["top_p", "min_p", "temperature"]) +def test_sampling_params_rejects_nan(field): + with pytest.raises(ValueError, match=field): + SamplingParams(**{field: float("nan")}) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("top_p", -0.1), + ("top_p", 1.1), + ("min_p", -0.1), + ("min_p", 1.1), + ("temperature", -1.0), + ], +) +def test_sampling_params_rejects_out_of_range(field, value): + with pytest.raises(ValueError, match=field): + SamplingParams(**{field: value}) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("top_p", 0.0), + ("top_p", 0.9), + ("top_p", 1.0), + ("min_p", 0.0), + ("min_p", 0.5), + ("min_p", 1.0), + ("temperature", 0.0), + ("temperature", 1.0), + ], +) +def test_sampling_params_accepts_in_range_values(field, value): + assert getattr(SamplingParams(**{field: value}), field) == value + + @pytest.mark.parametrize("value", [None, -1]) def test_thinking_token_budget_unlimited_values(value): assert SamplingParams(thinking_token_budget=value).thinking_token_budget is None From 30897c3068f5436a530d95e0f3adbbdc1fa613b1 Mon Sep 17 00:00:00 2001 From: Yigtwxx Date: Mon, 3 Aug 2026 08:51:35 +0300 Subject: [PATCH 2/2] [#17158][test] Register test_sampling_params.py in the L0 CPU pre-merge list The module was absent from every test list, so none of its cases -- including the NaN regression tests added in this branch -- ran in CI. Add it to the llmapi group of the CPU pre-merge stage, alongside the other CPU-only llmapi unit tests such as test_reasoning_parser.py. The tests need no GPU: they only construct SamplingParams and assert on validation. Signed-off-by: Yigtwxx --- tests/integration/test_lists/test-db/l0_cpu.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_lists/test-db/l0_cpu.yml b/tests/integration/test_lists/test-db/l0_cpu.yml index 3bb62ffd3b89..5a1a08f2e19b 100644 --- a/tests/integration/test_lists/test-db/l0_cpu.yml +++ b/tests/integration/test_lists/test-db/l0_cpu.yml @@ -41,6 +41,7 @@ l0_cpu: - unittest/llmapi/test_mpi_session.py ISOLATION - unittest/llmapi/test_reasoning_parser.py - unittest/llmapi/test_request_priority.py + - unittest/llmapi/test_sampling_params.py - unittest/llmapi/test_serialization.py - unittest/llmapi/test_utils.py - unittest/others/test_http_utils_fail_fast.py