[Rust Frontend] Align sampling validation with Python (#47494)
Signed-off-by: reidliu41 <reid201711@gmail.com>
Reid committed
Jul 28, 2026 at 20:02 UTC
912d6b619de0f2a44df74704973cff90fa0fb4e9
6 files changed
+260
-2
rust/src/server/src/error.rs
+16
@@ -141,6 +141,22 @@ mod tests {
141
assert!(response.error.message.contains("max_tokens=4"));
142
}
143
144
+ #[test]
145
+ fn sampling_params_validation_maps_to_invalid_request() {
146
+ let api_error = text_submit_error(
147
+ "failed to submit completion request",
148
+ vllm_text::Error::SamplingParams(vllm_text::SamplingParamsError::OutOfRange {
149
+ parameter: "top_p",
150
+ value: 0.0,
151
+ expected: "(0, 1]",
152
+ }),
153
+ );
154
+ assert_eq!(api_error.status_code(), StatusCode::BAD_REQUEST);
155
+ let response = api_error.to_error_response();
156
+ assert_eq!(response.error.error_type, "invalid_request_error");
157
+ assert!(response.error.message.contains("top_p"));
158
+ }
159
+
160
#[test]
161
fn chat_wrapped_prompt_too_long_maps_to_invalid_request() {
162
let error = vllm_chat::Error::Text(vllm_text::Error::PromptTooLong {
rust/src/server/src/grpc/tests.rs
+29
@@ -648,6 +648,35 @@ async fn unary_generate_min_tokens_above_max_tokens_returns_invalid_argument() {
648
server_task.abort();
649
}
650
651
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
652
+#[serial]
653
+async fn unary_generate_invalid_sampling_params_returns_invalid_argument() {
654
+ let (mut client, server_task, _engine_task) = grpc_test_server(
655
+ b"engine-grpc-invalid-sampling",
656
+ default_stream_output_specs(),
657
+ )
658
+ .await;
659
+
660
+ let status = client
661
+ .generate(pb::GenerateRequest {
662
+ request_id: "test-invalid-sampling".to_string(),
663
+ model: "test-model".to_string(),
664
+ prompt: Some(pb::generate_request::Prompt::Text("hi".to_string())),
665
+ sampling: Some(pb::RandomSampling {
666
+ top_p: 2.0,
667
+ ..Default::default()
668
+ }),
669
+ ..Default::default()
670
+ })
671
+ .await
672
+ .expect_err("should fail when top_p is out of range");
673
+
674
+ assert_eq!(status.code(), tonic::Code::InvalidArgument);
675
+ assert!(status.message().contains("top_p"));
676
+
677
+ server_task.abort();
678
+}
679
+
680
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
681
#[serial]
682
async fn streaming_generate_yields_incremental_responses() {
rust/src/text/src/error.rs
+4
@@ -6,6 +6,7 @@ use vllm_engine_core_client::Error as EngineCoreError;
6
use vllm_llm::Error as LlmError;
7
8
pub use crate::lower::logprobs::LogprobsError;
9
+pub use crate::lower::sampling::SamplingParamsError;
10
pub use crate::lower::token_ids::TokenIdsError;
11
12
#[derive(Debug, Error)]
@@ -23,6 +24,8 @@ pub enum Error {
24
Logprobs(#[from] LogprobsError),
25
#[error(transparent)]
26
TokenIds(#[from] TokenIdsError),
27
+ #[error(transparent)]
28
+ SamplingParams(#[from] SamplingParamsError),
29
#[error(
30
"`min_tokens` must be less than or equal to `max_tokens`, \
31
got min_tokens={min_tokens}, max_tokens={max_tokens}"
@@ -50,6 +53,7 @@ impl Error {
53
| Self::EmptyPromptTokenIds { .. }
54
| Self::Logprobs(_)
55
| Self::TokenIds(_)
56
+ | Self::SamplingParams(_)
57
| Self::MinTokensExceedsMaxTokens { .. }
58
| Self::InvalidThinkingTokenBudget
59
| Self::InvalidRepetitionDetection { .. }
rust/src/text/src/lib.rs
+1
-1
@@ -10,7 +10,7 @@
10
use std::mem::take;
11
12
pub use backend::{DynTextBackend, SamplingHints, SamplingLimits, TextBackend};
13
-pub use error::{Error, LogprobsError, Result, TokenIdsError};
13
+pub use error::{Error, LogprobsError, Result, SamplingParamsError, TokenIdsError};
14
use futures::Stream;
15
pub use lower::{
16
PreparedTextRequest, lower_sampling_params, lower_text_request, resolve_max_tokens,
rust/src/text/src/lower.rs
+118
-1
@@ -4,9 +4,11 @@
4
use std::collections::BTreeSet;
5
6
pub(crate) mod logprobs;
7
+pub(crate) mod sampling;
8
pub(crate) mod token_ids;
9
10
use logprobs::validate_logprobs;
11
+use sampling::validate_resolved_sampling_params;
12
use token_ids::{validate_prompt_token_ids, validate_vocab_range};
13
use vllm_engine_core_client::protocol::sampling::{
14
EngineCoreSamplingParams, RepetitionDetectionParams,
@@ -186,6 +188,7 @@ pub fn lower_sampling_params(
188
skip_reading_prefix_cache,
189
extra_args: vllm_xargs,
190
};
191
+ validate_resolved_sampling_params(¶ms)?;
192
validate_vocab_range(¶ms, &sampling_limits)?;
193
Ok(params)
194
}
@@ -319,7 +322,7 @@ mod tests {
322
use super::*;
323
use crate::backend::hf::HfTextBackend;
324
use crate::backend::{SamplingHints, TextBackend as _};
322
- use crate::error::{LogprobsError, TokenIdsError};
325
+ use crate::error::{LogprobsError, SamplingParamsError, TokenIdsError};
326
use crate::request::{Prompt, TextRequest};
327
328
fn stub_tokenizer() -> TestTokenizer {
@@ -482,6 +485,120 @@ mod tests {
485
assert!(message.contains("min_count=1"));
486
}
487
488
+ #[test]
489
+ fn lower_sampling_params_rejects_invalid_sampling_ranges() {
490
+ let cases = [
491
+ (
492
+ "temperature",
493
+ SamplingParams {
494
+ temperature: Some(5.0),
495
+ ..SamplingParams::default()
496
+ },
497
+ ),
498
+ (
499
+ "top_p",
500
+ SamplingParams {
501
+ top_p: Some(0.0),
502
+ ..SamplingParams::default()
503
+ },
504
+ ),
505
+ (
506
+ "min_p",
507
+ SamplingParams {
508
+ min_p: Some(2.0),
509
+ ..SamplingParams::default()
510
+ },
511
+ ),
512
+ (
513
+ "repetition_penalty",
514
+ SamplingParams {
515
+ repetition_penalty: Some(0.0),
516
+ ..SamplingParams::default()
517
+ },
518
+ ),
519
+ (
520
+ "frequency_penalty",
521
+ SamplingParams {
522
+ frequency_penalty: Some(100.0),
523
+ ..SamplingParams::default()
524
+ },
525
+ ),
526
+ (
527
+ "presence_penalty",
528
+ SamplingParams {
529
+ presence_penalty: Some(100.0),
530
+ ..SamplingParams::default()
531
+ },
532
+ ),
533
+ ];
534
+
535
+ for (expected_parameter, sampling_params) in cases {
536
+ let error =
537
+ lower_sampling_params_with_limits(sampling_params, sample_sampling_limits())
538
+ .unwrap_err();
539
+
540
+ assert!(
541
+ matches!(
542
+ error,
543
+ Error::SamplingParams(SamplingParamsError::OutOfRange {
544
+ parameter,
545
+ ..
546
+ }) if parameter == expected_parameter
547
+ ),
548
+ "{expected_parameter} should be rejected"
549
+ );
550
+ }
551
+ }
552
+
553
+ #[test]
554
+ fn lower_sampling_params_rejects_non_finite_sampling_values() {
555
+ for (expected_parameter, sampling_params) in [
556
+ (
557
+ "temperature",
558
+ SamplingParams {
559
+ temperature: Some(f32::INFINITY),
560
+ ..SamplingParams::default()
561
+ },
562
+ ),
563
+ (
564
+ "repetition_penalty",
565
+ SamplingParams {
566
+ repetition_penalty: Some(f32::NAN),
567
+ ..SamplingParams::default()
568
+ },
569
+ ),
570
+ ] {
571
+ let error =
572
+ lower_sampling_params_with_limits(sampling_params, sample_sampling_limits())
573
+ .unwrap_err();
574
+
575
+ assert!(
576
+ matches!(
577
+ error,
578
+ Error::SamplingParams(SamplingParamsError::NotFinite {
579
+ parameter,
580
+ ..
581
+ }) if parameter == expected_parameter
582
+ ),
583
+ "{expected_parameter} should reject non-finite values"
584
+ );
585
+ }
586
+ }
587
+
588
+ #[test]
589
+ fn lower_sampling_params_accepts_python_compatible_repetition_penalty_above_two() {
590
+ let params = lower_sampling_params_with_limits(
591
+ SamplingParams {
592
+ repetition_penalty: Some(2.5),
593
+ ..SamplingParams::default()
594
+ },
595
+ sample_sampling_limits(),
596
+ )
597
+ .unwrap();
598
+
599
+ assert_eq!(params.repetition_penalty, 2.5);
600
+ }
601
+
602
#[test]
603
fn lower_text_request_applies_python_style_eos_hints() {
604
let prepared = lower_text_request(
rust/src/text/src/lower/sampling.rs
new
+92
@@ -0,0 +1,92 @@
1
+// SPDX-License-Identifier: Apache-2.0
2
+// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+use thiserror::Error;
5
+use vllm_engine_core_client::protocol::sampling::EngineCoreSamplingParams;
6
+
7
+#[derive(Debug, Error, PartialEq)]
8
+pub enum SamplingParamsError {
9
+ #[error("{parameter} must be a finite number, got {value}")]
10
+ NotFinite { parameter: &'static str, value: f32 },
11
+ #[error("{parameter} must be in {expected}, got {value}")]
12
+ OutOfRange {
13
+ parameter: &'static str,
14
+ value: f32,
15
+ expected: &'static str,
16
+ },
17
+}
18
+
19
+fn validate_frequency_penalty(value: f32) -> Result<(), SamplingParamsError> {
20
+ validate_closed_range("frequency_penalty", value, -2.0, 2.0, "[-2, 2]")
21
+}
22
+
23
+fn validate_presence_penalty(value: f32) -> Result<(), SamplingParamsError> {
24
+ validate_closed_range("presence_penalty", value, -2.0, 2.0, "[-2, 2]")
25
+}
26
+
27
+fn validate_temperature(value: f32) -> Result<(), SamplingParamsError> {
28
+ validate_finite("temperature", value)?;
29
+ validate_closed_range("temperature", value, 0.0, 2.0, "[0, 2]")
30
+}
31
+
32
+fn validate_top_p(value: f32) -> Result<(), SamplingParamsError> {
33
+ if value > 0.0 && value <= 1.0 {
34
+ return Ok(());
35
+ }
36
+ Err(SamplingParamsError::OutOfRange {
37
+ parameter: "top_p",
38
+ value,
39
+ expected: "(0, 1]",
40
+ })
41
+}
42
+
43
+fn validate_min_p(value: f32) -> Result<(), SamplingParamsError> {
44
+ validate_closed_range("min_p", value, 0.0, 1.0, "[0, 1]")
45
+}
46
+
47
+fn validate_repetition_penalty(value: f32) -> Result<(), SamplingParamsError> {
48
+ validate_finite("repetition_penalty", value)?;
49
+ if value > 0.0 {
50
+ return Ok(());
51
+ }
52
+ Err(SamplingParamsError::OutOfRange {
53
+ parameter: "repetition_penalty",
54
+ value,
55
+ expected: "(0, inf)",
56
+ })
57
+}
58
+
59
+pub(crate) fn validate_resolved_sampling_params(
60
+ params: &EngineCoreSamplingParams,
61
+) -> Result<(), SamplingParamsError> {
62
+ validate_temperature(params.temperature)?;
63
+ validate_top_p(params.top_p)?;
64
+ validate_min_p(params.min_p)?;
65
+ validate_frequency_penalty(params.frequency_penalty)?;
66
+ validate_presence_penalty(params.presence_penalty)?;
67
+ validate_repetition_penalty(params.repetition_penalty)
68
+}
69
+
70
+fn validate_finite(parameter: &'static str, value: f32) -> Result<(), SamplingParamsError> {
71
+ if value.is_finite() {
72
+ return Ok(());
73
+ }
74
+ Err(SamplingParamsError::NotFinite { parameter, value })
75
+}
76
+
77
+fn validate_closed_range(
78
+ parameter: &'static str,
79
+ value: f32,
80
+ min: f32,
81
+ max: f32,
82
+ expected: &'static str,
83
+) -> Result<(), SamplingParamsError> {
84
+ if value >= min && value <= max {
85
+ return Ok(());
86
+ }
87
+ Err(SamplingParamsError::OutOfRange {
88
+ parameter,
89
+ value,
90
+ expected,
91
+ })
92
+}