[Rust Frontend] Align sampling validation with Python (#47494)

Signed-off-by: reidliu41 <reid201711@gmail.com>
This commit is contained in:
Reid
2026-07-28 12:02:00 +00:00
committed by GitHub
parent bf9f23003c
commit 912d6b619d
6 changed files with 260 additions and 2 deletions
+16
View File
@@ -141,6 +141,22 @@ mod tests {
assert!(response.error.message.contains("max_tokens=4"));
}
#[test]
fn sampling_params_validation_maps_to_invalid_request() {
let api_error = text_submit_error(
"failed to submit completion request",
vllm_text::Error::SamplingParams(vllm_text::SamplingParamsError::OutOfRange {
parameter: "top_p",
value: 0.0,
expected: "(0, 1]",
}),
);
assert_eq!(api_error.status_code(), StatusCode::BAD_REQUEST);
let response = api_error.to_error_response();
assert_eq!(response.error.error_type, "invalid_request_error");
assert!(response.error.message.contains("top_p"));
}
#[test]
fn chat_wrapped_prompt_too_long_maps_to_invalid_request() {
let error = vllm_chat::Error::Text(vllm_text::Error::PromptTooLong {
+29
View File
@@ -648,6 +648,35 @@ async fn unary_generate_min_tokens_above_max_tokens_returns_invalid_argument() {
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn unary_generate_invalid_sampling_params_returns_invalid_argument() {
let (mut client, server_task, _engine_task) = grpc_test_server(
b"engine-grpc-invalid-sampling",
default_stream_output_specs(),
)
.await;
let status = client
.generate(pb::GenerateRequest {
request_id: "test-invalid-sampling".to_string(),
model: "test-model".to_string(),
prompt: Some(pb::generate_request::Prompt::Text("hi".to_string())),
sampling: Some(pb::RandomSampling {
top_p: 2.0,
..Default::default()
}),
..Default::default()
})
.await
.expect_err("should fail when top_p is out of range");
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(status.message().contains("top_p"));
server_task.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn streaming_generate_yields_incremental_responses() {
+4
View File
@@ -6,6 +6,7 @@ use vllm_engine_core_client::Error as EngineCoreError;
use vllm_llm::Error as LlmError;
pub use crate::lower::logprobs::LogprobsError;
pub use crate::lower::sampling::SamplingParamsError;
pub use crate::lower::token_ids::TokenIdsError;
#[derive(Debug, Error)]
@@ -23,6 +24,8 @@ pub enum Error {
Logprobs(#[from] LogprobsError),
#[error(transparent)]
TokenIds(#[from] TokenIdsError),
#[error(transparent)]
SamplingParams(#[from] SamplingParamsError),
#[error(
"`min_tokens` must be less than or equal to `max_tokens`, \
got min_tokens={min_tokens}, max_tokens={max_tokens}"
@@ -50,6 +53,7 @@ impl Error {
| Self::EmptyPromptTokenIds { .. }
| Self::Logprobs(_)
| Self::TokenIds(_)
| Self::SamplingParams(_)
| Self::MinTokensExceedsMaxTokens { .. }
| Self::InvalidThinkingTokenBudget
| Self::InvalidRepetitionDetection { .. }
+1 -1
View File
@@ -10,7 +10,7 @@
use std::mem::take;
pub use backend::{DynTextBackend, SamplingHints, SamplingLimits, TextBackend};
pub use error::{Error, LogprobsError, Result, TokenIdsError};
pub use error::{Error, LogprobsError, Result, SamplingParamsError, TokenIdsError};
use futures::Stream;
pub use lower::{
PreparedTextRequest, lower_sampling_params, lower_text_request, resolve_max_tokens,
+118 -1
View File
@@ -4,9 +4,11 @@
use std::collections::BTreeSet;
pub(crate) mod logprobs;
pub(crate) mod sampling;
pub(crate) mod token_ids;
use logprobs::validate_logprobs;
use sampling::validate_resolved_sampling_params;
use token_ids::{validate_prompt_token_ids, validate_vocab_range};
use vllm_engine_core_client::protocol::sampling::{
EngineCoreSamplingParams, RepetitionDetectionParams,
@@ -186,6 +188,7 @@ pub fn lower_sampling_params(
skip_reading_prefix_cache,
extra_args: vllm_xargs,
};
validate_resolved_sampling_params(&params)?;
validate_vocab_range(&params, &sampling_limits)?;
Ok(params)
}
@@ -319,7 +322,7 @@ mod tests {
use super::*;
use crate::backend::hf::HfTextBackend;
use crate::backend::{SamplingHints, TextBackend as _};
use crate::error::{LogprobsError, TokenIdsError};
use crate::error::{LogprobsError, SamplingParamsError, TokenIdsError};
use crate::request::{Prompt, TextRequest};
fn stub_tokenizer() -> TestTokenizer {
@@ -482,6 +485,120 @@ mod tests {
assert!(message.contains("min_count=1"));
}
#[test]
fn lower_sampling_params_rejects_invalid_sampling_ranges() {
let cases = [
(
"temperature",
SamplingParams {
temperature: Some(5.0),
..SamplingParams::default()
},
),
(
"top_p",
SamplingParams {
top_p: Some(0.0),
..SamplingParams::default()
},
),
(
"min_p",
SamplingParams {
min_p: Some(2.0),
..SamplingParams::default()
},
),
(
"repetition_penalty",
SamplingParams {
repetition_penalty: Some(0.0),
..SamplingParams::default()
},
),
(
"frequency_penalty",
SamplingParams {
frequency_penalty: Some(100.0),
..SamplingParams::default()
},
),
(
"presence_penalty",
SamplingParams {
presence_penalty: Some(100.0),
..SamplingParams::default()
},
),
];
for (expected_parameter, sampling_params) in cases {
let error =
lower_sampling_params_with_limits(sampling_params, sample_sampling_limits())
.unwrap_err();
assert!(
matches!(
error,
Error::SamplingParams(SamplingParamsError::OutOfRange {
parameter,
..
}) if parameter == expected_parameter
),
"{expected_parameter} should be rejected"
);
}
}
#[test]
fn lower_sampling_params_rejects_non_finite_sampling_values() {
for (expected_parameter, sampling_params) in [
(
"temperature",
SamplingParams {
temperature: Some(f32::INFINITY),
..SamplingParams::default()
},
),
(
"repetition_penalty",
SamplingParams {
repetition_penalty: Some(f32::NAN),
..SamplingParams::default()
},
),
] {
let error =
lower_sampling_params_with_limits(sampling_params, sample_sampling_limits())
.unwrap_err();
assert!(
matches!(
error,
Error::SamplingParams(SamplingParamsError::NotFinite {
parameter,
..
}) if parameter == expected_parameter
),
"{expected_parameter} should reject non-finite values"
);
}
}
#[test]
fn lower_sampling_params_accepts_python_compatible_repetition_penalty_above_two() {
let params = lower_sampling_params_with_limits(
SamplingParams {
repetition_penalty: Some(2.5),
..SamplingParams::default()
},
sample_sampling_limits(),
)
.unwrap();
assert_eq!(params.repetition_penalty, 2.5);
}
#[test]
fn lower_text_request_applies_python_style_eos_hints() {
let prepared = lower_text_request(
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
use thiserror::Error;
use vllm_engine_core_client::protocol::sampling::EngineCoreSamplingParams;
#[derive(Debug, Error, PartialEq)]
pub enum SamplingParamsError {
#[error("{parameter} must be a finite number, got {value}")]
NotFinite { parameter: &'static str, value: f32 },
#[error("{parameter} must be in {expected}, got {value}")]
OutOfRange {
parameter: &'static str,
value: f32,
expected: &'static str,
},
}
fn validate_frequency_penalty(value: f32) -> Result<(), SamplingParamsError> {
validate_closed_range("frequency_penalty", value, -2.0, 2.0, "[-2, 2]")
}
fn validate_presence_penalty(value: f32) -> Result<(), SamplingParamsError> {
validate_closed_range("presence_penalty", value, -2.0, 2.0, "[-2, 2]")
}
fn validate_temperature(value: f32) -> Result<(), SamplingParamsError> {
validate_finite("temperature", value)?;
validate_closed_range("temperature", value, 0.0, 2.0, "[0, 2]")
}
fn validate_top_p(value: f32) -> Result<(), SamplingParamsError> {
if value > 0.0 && value <= 1.0 {
return Ok(());
}
Err(SamplingParamsError::OutOfRange {
parameter: "top_p",
value,
expected: "(0, 1]",
})
}
fn validate_min_p(value: f32) -> Result<(), SamplingParamsError> {
validate_closed_range("min_p", value, 0.0, 1.0, "[0, 1]")
}
fn validate_repetition_penalty(value: f32) -> Result<(), SamplingParamsError> {
validate_finite("repetition_penalty", value)?;
if value > 0.0 {
return Ok(());
}
Err(SamplingParamsError::OutOfRange {
parameter: "repetition_penalty",
value,
expected: "(0, inf)",
})
}
pub(crate) fn validate_resolved_sampling_params(
params: &EngineCoreSamplingParams,
) -> Result<(), SamplingParamsError> {
validate_temperature(params.temperature)?;
validate_top_p(params.top_p)?;
validate_min_p(params.min_p)?;
validate_frequency_penalty(params.frequency_penalty)?;
validate_presence_penalty(params.presence_penalty)?;
validate_repetition_penalty(params.repetition_penalty)
}
fn validate_finite(parameter: &'static str, value: f32) -> Result<(), SamplingParamsError> {
if value.is_finite() {
return Ok(());
}
Err(SamplingParamsError::NotFinite { parameter, value })
}
fn validate_closed_range(
parameter: &'static str,
value: f32,
min: f32,
max: f32,
expected: &'static str,
) -> Result<(), SamplingParamsError> {
if value >= min && value <= max {
return Ok(());
}
Err(SamplingParamsError::OutOfRange {
parameter,
value,
expected,
})
}