Compare commits

...
Author SHA1 Message Date
Bugen Zhao cf30ef60cf use newtype for logprobs count (-1 for all)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
2026-06-22 19:30:10 +08:00
21 changed files with 293 additions and 164 deletions
+4 -3
View File
@@ -16,7 +16,8 @@ use vllm_engine_core_client::protocol::logprobs::{
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
};
use vllm_engine_core_client::protocol::{
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, EngineCoreRequest, StopReason,
EngineCoreFinishReason, EngineCoreOutput, EngineCoreOutputs, EngineCoreRequest, LogprobsCount,
StopReason,
};
use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
use vllm_engine_core_client::{EngineCoreClient, EngineCoreClientConfig};
@@ -1387,8 +1388,8 @@ async fn chat_stream_and_collect_preserve_prompt_and_sample_logprobs() {
.await;
let mut request = sample_request("chat-logprobs");
request.sampling_params.logprobs = Some(1);
request.sampling_params.prompt_logprobs = Some(1);
request.sampling_params.logprobs = Some(LogprobsCount::Top(1));
request.sampling_params.prompt_logprobs = Some(LogprobsCount::Top(1));
let mut stream = chat.chat(request.clone()).await.unwrap();
match next_semantic(&mut stream).await.unwrap().unwrap() {
+8 -3
View File
@@ -20,6 +20,7 @@ use serde_with::{DefaultOnNull, OneOrMany, serde_as};
use thiserror_ext::AsReport as _;
use uuid::Uuid;
use vllm_engine_core_client::TransportMode;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_managed_engine::ManagedEngineConfig;
use vllm_managed_engine::cli::{ManagedEngineArgs, repartition_managed_engine_args};
use vllm_server::{
@@ -136,9 +137,9 @@ pub struct SharedRuntimeArgs {
pub max_model_len: Option<u32>,
/// Maximum number of log probabilities to return when `logprobs` is
/// specified in sampling parameters. `-1` means no cap.
#[arg(long, value_parser = clap::value_parser!(i32).range(-1..), allow_negative_numbers = true)]
#[arg(long, allow_negative_numbers = true)]
#[serde(default)]
pub max_logprobs: Option<i32>,
pub max_logprobs: Option<LogprobsCount>,
/// TCP port for the gRPC Generate service. When not set, no gRPC server is
/// started.
#[arg(long)]
@@ -529,7 +530,7 @@ impl ServeArgs {
self.managed_engine.clone().into_config(
self.runtime.model.clone(),
self.runtime.max_model_len,
self.runtime.max_logprobs,
self.runtime.max_logprobs.map(managed_max_logprobs_to_i32),
self.runtime.language_model_only,
self.runtime.disable_log_stats,
self.runtime.shutdown_timeout,
@@ -555,5 +556,9 @@ fn frontend_ipc_addresses() -> (String, String) {
)
}
fn managed_max_logprobs_to_i32(count: LogprobsCount) -> i32 {
i32::try_from(count).expect("max_logprobs is parsed through i32")
}
#[cfg(test)]
mod tests;
+4 -3
View File
@@ -1,5 +1,6 @@
use expect_test::expect;
use vllm_engine_core_client::TransportMode;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_server::{Config, HttpListenerMode, ParserSelection, RendererSelection};
use super::{Cli, Command};
@@ -165,10 +166,10 @@ fn serve_args_forward_max_logprobs_to_frontend_and_managed_engine() {
let Command::Serve(args) = cli.command else {
panic!("expected serve args");
};
assert_eq!(args.runtime.max_logprobs, Some(-1));
assert_eq!(args.runtime.max_logprobs, Some(LogprobsCount::All));
let frontend_config = args.to_frontend_config("tcp://127.0.0.1:62100".to_string());
assert_eq!(frontend_config.max_logprobs, Some(-1));
assert_eq!(frontend_config.max_logprobs, Some(LogprobsCount::All));
let engine_config = args.to_managed_engine_config(5555);
assert_eq!(engine_config.python_args, vec!["--max-logprobs", "-1"]);
@@ -529,7 +530,7 @@ fn frontend_args_json_accepts_supported_non_default_fields() {
assert_eq!(args.runtime.renderer, RendererSelection::DeepSeekV32);
assert!(args.runtime.language_model_only);
assert_eq!(args.runtime.max_model_len, Some(8192));
assert_eq!(args.runtime.max_logprobs, Some(-1));
assert_eq!(args.runtime.max_logprobs, Some(LogprobsCount::All));
assert_eq!(args.runtime.shutdown_timeout, 3);
}
@@ -6,7 +6,7 @@ use futures::StreamExt as _;
use tokio::time::timeout;
use tracing_subscriber::EnvFilter;
use vllm_engine_core_client::protocol::{
EngineCoreFinishReason, EngineCoreRequest, EngineCoreSamplingParams,
EngineCoreFinishReason, EngineCoreRequest, EngineCoreSamplingParams, LogprobsCount,
};
use vllm_engine_core_client::{
EngineCoreClient, EngineCoreClientConfig, EngineCoreStreamOutput, TransportMode,
@@ -33,10 +33,10 @@ struct Args {
output_timeout_secs: u64,
#[arg(long, default_value_t = 1)]
max_tokens: u32,
#[arg(long, default_value_t = 2)]
logprobs: i32,
#[arg(long, default_value_t = 1)]
prompt_logprobs: i32,
#[arg(long, default_value_t = LogprobsCount::Top(2), allow_negative_numbers = true)]
logprobs: LogprobsCount,
#[arg(long, default_value_t = LogprobsCount::Top(1), allow_negative_numbers = true)]
prompt_logprobs: LogprobsCount,
#[arg(long, default_value_t = 96)]
prompt_repeats: usize,
}
@@ -64,8 +64,8 @@ fn build_request(
request_id: String,
prompt_token_ids: Vec<u32>,
max_tokens: u32,
logprobs: i32,
prompt_logprobs: i32,
logprobs: LogprobsCount,
prompt_logprobs: LogprobsCount,
client_index: u32,
) -> EngineCoreRequest {
EngineCoreRequest {
@@ -0,0 +1,135 @@
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Number of log probabilities requested for a token position.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogprobsCount {
/// Return the full model vocabulary.
All,
/// Return the top-N tokens by probability.
Top(u32),
}
impl LogprobsCount {
/// Expands the count to the actual number of logprobs to return, given the vocabulary size.
pub fn expanded(self, vocab_size: usize) -> usize {
match self {
Self::All => vocab_size,
Self::Top(count) => count as usize,
}
}
}
impl TryFrom<i32> for LogprobsCount {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
-1 => Ok(Self::All),
value if value < -1 => Err(format!("must be non-negative or -1, got {value}")),
value => Ok(Self::Top(value as u32)),
}
}
}
impl TryFrom<LogprobsCount> for i32 {
type Error = String;
fn try_from(value: LogprobsCount) -> Result<Self, Self::Error> {
match value {
LogprobsCount::All => Ok(-1),
LogprobsCount::Top(count) => {
i32::try_from(count).map_err(|_| format!("must fit within i32, got {count}"))
}
}
}
}
impl FromStr for LogprobsCount {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = s
.parse::<i32>()
.map_err(|e| format!("must be an i32 integer, got {s:?}: {e}"))?;
Self::try_from(value)
}
}
impl fmt::Display for LogprobsCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::All => (-1).fmt(f),
Self::Top(count) => count.fmt(f),
}
}
}
impl Serialize for LogprobsCount {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value: i32 = (*self).try_into().map_err(serde::ser::Error::custom)?;
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for LogprobsCount {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = i32::deserialize(deserializer)?;
Self::try_from(value).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use rmpv::Value;
use super::*;
use crate::protocol::{decode_msgpack, encode_msgpack};
#[test]
fn logprobs_count_serializes_as_wire_integer() {
assert_eq!(serde_json::to_value(LogprobsCount::All).unwrap(), -1);
assert_eq!(serde_json::to_value(LogprobsCount::Top(3)).unwrap(), 3);
}
#[test]
fn logprobs_count_deserializes_wire_integer() {
assert_eq!(
serde_json::from_value::<LogprobsCount>(serde_json::json!(-1)).unwrap(),
LogprobsCount::All
);
assert_eq!(
serde_json::from_value::<LogprobsCount>(serde_json::json!(3)).unwrap(),
LogprobsCount::Top(3)
);
assert!(serde_json::from_value::<LogprobsCount>(serde_json::json!(-2)).is_err());
assert!(
serde_json::from_value::<LogprobsCount>(serde_json::json!(i64::from(i32::MAX) + 1))
.is_err()
);
}
#[test]
fn logprobs_count_decodes_msgpack_signed_and_unsigned() {
let mut encoded = Vec::new();
rmpv::encode::write_value(&mut encoded, &Value::from(-1)).unwrap();
assert_eq!(
decode_msgpack::<LogprobsCount>(&encoded).unwrap(),
LogprobsCount::All
);
let encoded = encode_msgpack(&LogprobsCount::Top(7)).unwrap();
assert_eq!(
decode_msgpack::<LogprobsCount>(&encoded).unwrap(),
LogprobsCount::Top(7)
);
}
}
@@ -56,6 +56,7 @@ mod classified_outputs;
pub mod dtype;
pub mod handshake;
pub mod logprobs;
mod logprobs_count;
pub mod lora;
pub mod multimodal;
pub mod stats;
@@ -66,6 +67,7 @@ pub use classified_outputs::{
};
pub use dtype::ModelDtype;
pub use logprobs::decode_engine_core_outputs;
pub use logprobs_count::LogprobsCount;
/// Request types are encoded as single-byte protocol constants so they can be
/// sent over the ZMQ socket without an extra encoding step.
@@ -285,12 +287,12 @@ pub struct EngineCoreSamplingParams {
pub thinking_token_budget: Option<u64>,
/// Number of log probabilities to return per generated token.
///
/// `None` disables sample logprobs. `-1` requests the full vocabulary.
pub logprobs: Option<i32>,
/// `None` disables sample logprobs.
pub logprobs: Option<LogprobsCount>,
/// Number of log probabilities to return per prompt token.
///
/// `None` disables prompt logprobs. `-1` requests the full vocabulary.
pub prompt_logprobs: Option<i32>,
/// `None` disables prompt logprobs.
pub prompt_logprobs: Option<LogprobsCount>,
/// Minimum probability threshold for token sampling.
pub min_p: f32,
/// Frequency penalty applied by the sampler.
+3 -11
View File
@@ -2,12 +2,13 @@ use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
use anyhow::{Result, bail};
use anyhow::Result;
use axum::http::{HeaderName, HeaderValue, Method};
use educe::Educe;
use serde::Serialize;
use serde_json::Value;
use vllm_chat::{ChatTemplateContentFormatOption, ParserSelection, RendererSelection};
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_engine_core_client::{CoordinatorMode as EngineCoreCoordinatorMode, TransportMode};
/// How the HTTP server obtains its listening socket.
@@ -133,7 +134,7 @@ pub struct Config {
pub chat_template_content_format: ChatTemplateContentFormatOption,
/// Optional maximum number of top log probabilities accepted by the
/// frontend. `None` delegates to the text layer default.
pub max_logprobs: Option<i32>,
pub max_logprobs: Option<LogprobsCount>,
/// HTTP/API-server behavior switches.
pub api_server_options: ApiServerOptions,
/// CORS settings applied to every HTTP response.
@@ -158,15 +159,6 @@ impl Config {
pub fn validate(&self) -> Result<()> {
vllm_chat::validate_parser_overrides(&self.tool_call_parser, &self.reasoning_parser)?;
self.cors.validate()?;
if let Some(max_logprobs) = self.max_logprobs
&& max_logprobs < -1
{
bail!(
"max_logprobs must be non-negative or -1, got {}",
max_logprobs
);
}
Ok(())
}
+13 -9
View File
@@ -3,7 +3,7 @@
use tonic::Status;
use uuid::Uuid;
use vllm_engine_core_client::protocol::{StopReason, StructuredOutputsParams};
use vllm_engine_core_client::protocol::{LogprobsCount, StopReason, StructuredOutputsParams};
use vllm_text::{
DecodedLogprobs, DecodedPromptLogprobs, FinishReason, Finished, Prompt, SamplingParams,
TextDecodeOptions, TextRequest,
@@ -202,18 +202,22 @@ fn build_sampling_params(
/// Map the proto `CandidateTokens` selector to a `(logprobs_count,
/// logprob_token_ids)` pair.
///
/// - `top_n(k)` → `(k, None)` — return top-k candidates by probability
/// - `all` → `(-1, None)` — return the full vocabulary
/// - `top_n(k)` → `(Top(k), None)` — return top-k candidates by probability
/// - `all` → `(All, None)` — return the full vocabulary
/// - `token_ids(n)` → `(1, Some(vec of n token ids))` — return logprobs for specific tokens (the
/// count `n` is stored in the proto as the number of token IDs that follow, but the actual IDs
/// are carried via `logprob_token_ids` on `SamplingParams`)
/// - absent → `(1, None)` — just the sampled/scored token
fn candidate_logprob_spec(candidates: Option<&pb::CandidateTokens>) -> (i32, Option<Vec<u32>>) {
/// - absent → `(Top(1), None)` — just the sampled/scored token
fn candidate_logprob_spec(
candidates: Option<&pb::CandidateTokens>,
) -> (LogprobsCount, Option<Vec<u32>>) {
match candidates.and_then(|c| c.select.as_ref()) {
Some(pb::candidate_tokens::Select::TopN(n)) => (*n as i32, None),
Some(pb::candidate_tokens::Select::All(true)) => (-1, None),
Some(pb::candidate_tokens::Select::TokenIds(ids)) => (1, Some(ids.ids.clone())),
_ => (1, None),
Some(pb::candidate_tokens::Select::TopN(n)) => (LogprobsCount::Top(*n), None),
Some(pb::candidate_tokens::Select::All(true)) => (LogprobsCount::All, None),
Some(pb::candidate_tokens::Select::TokenIds(ids)) => {
(LogprobsCount::Top(1), Some(ids.ids.clone()))
}
_ => (LogprobsCount::Top(1), None),
}
}
@@ -87,6 +87,7 @@ pub(super) fn prepare_generate_request(
#[cfg(test)]
mod tests {
use serde_json::json;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::Prompt;
use super::prepare_generate_request;
@@ -132,10 +133,13 @@ mod tests {
Prompt::TokenIds(vec![11, 22, 33])
);
assert_eq!(prepared.text_request.sampling_params.max_tokens, Some(7));
assert_eq!(prepared.text_request.sampling_params.logprobs, Some(2));
assert_eq!(
prepared.text_request.sampling_params.logprobs,
Some(LogprobsCount::Top(2))
);
assert_eq!(
prepared.text_request.sampling_params.prompt_logprobs,
Some(1)
Some(LogprobsCount::Top(1))
);
assert!(prepared.text_request.sampling_params.ignore_eos);
assert_eq!(prepared.text_request.priority, -3);
@@ -34,16 +34,6 @@ pub(super) fn validate_request_compat(
);
}
if let Some(prompt_logprobs) = request.sampling_params.prompt_logprobs
&& prompt_logprobs < 0
&& prompt_logprobs != -1
{
bail_invalid_request!(
param = "sampling_params",
"`prompt_logprobs` must be a non-negative value or -1."
);
}
Ok(())
}
@@ -4,6 +4,7 @@ use vllm_chat::{
ChatMessage as VllmChatMessage, ChatOptions, ChatRequest, ChatTool, ChatToolChoice,
GenerationPromptMode, SamplingParams,
};
use vllm_engine_core_client::protocol::LogprobsCount;
use super::types::ChatCompletionRequest;
use super::validate;
@@ -94,7 +95,7 @@ pub(super) fn prepare_chat_request(
// Auto-enable prompt logprobs for non-streaming echo, matching Python vLLM's
// behavior.
let top_logprobs = request.top_logprobs.unwrap_or(0);
let top_logprobs = request.top_logprobs.unwrap_or(LogprobsCount::Top(0));
let prompt_logprobs = request
.prompt_logprobs
.or((request.echo && !request.stream).then_some(top_logprobs));
@@ -378,6 +379,7 @@ mod tests {
ChatTool as VllmChatTool, ChatToolChoice, GenerationPromptMode,
SamplingParams as VllmSamplingParams,
};
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::output::TextDecodeOptions;
use super::prepare_chat_request;
@@ -967,7 +969,7 @@ mod tests {
let request = ChatCompletionRequest {
stream: false,
logprobs: true,
prompt_logprobs: Some(2),
prompt_logprobs: Some(LogprobsCount::Top(2)),
..base_request()
};
@@ -980,10 +982,13 @@ mod tests {
assert!(prepared.options.requested_logprobs);
assert!(prepared.options.include_prompt_logprobs);
assert_eq!(prepared.chat_request.sampling_params.logprobs, Some(0));
assert_eq!(
prepared.chat_request.sampling_params.logprobs,
Some(LogprobsCount::Top(0))
);
assert_eq!(
prepared.chat_request.sampling_params.prompt_logprobs,
Some(2)
Some(LogprobsCount::Top(2))
);
}
@@ -991,7 +996,7 @@ mod tests {
fn prepare_chat_request_keeps_prompt_logprobs_independent_from_echo() {
let request = ChatCompletionRequest {
logprobs: true,
top_logprobs: Some(3),
top_logprobs: Some(LogprobsCount::Top(3)),
echo: true,
..base_request()
};
@@ -1003,7 +1008,10 @@ mod tests {
)
.expect("request is valid");
assert_eq!(prepared.chat_request.sampling_params.logprobs, Some(3));
assert_eq!(
prepared.chat_request.sampling_params.logprobs,
Some(LogprobsCount::Top(3))
);
assert_eq!(prepared.chat_request.sampling_params.prompt_logprobs, None);
assert!(!prepared.options.include_prompt_logprobs);
}
@@ -6,6 +6,7 @@ use serde_json::Value;
use serde_with::SerializeDisplay;
use validator::Validate;
use vllm_chat::ReasoningEffort;
use vllm_engine_core_client::protocol::LogprobsCount;
use crate::routes::openai::utils::structured_outputs::ResponseFormat;
use crate::routes::openai::utils::types::{
@@ -44,10 +45,8 @@ pub struct ChatCompletionRequest {
#[serde(default)]
pub logprobs: bool,
/// An integer specifying the number of most likely tokens to return
/// -1 means return all
#[validate(range(min = -1))]
pub top_logprobs: Option<i32>,
/// Number of most likely tokens to return. `-1` means return full vocab.
pub top_logprobs: Option<LogprobsCount>,
/// Deprecated: Replaced by max_completion_tokens
#[deprecated(note = "Use max_completion_tokens instead")]
@@ -155,8 +154,8 @@ pub struct ChatCompletionRequest {
/// Truncate prompt tokens to this length
pub truncate_prompt_tokens: Option<i64>,
/// Number of prompt logprobs to return
pub prompt_logprobs: Option<i32>,
/// Number of prompt logprobs to return. `-1` means return full vocab.
pub prompt_logprobs: Option<LogprobsCount>,
/// Restrict output to these token IDs only
pub allowed_token_ids: Option<Vec<u32>>,
@@ -1,6 +1,7 @@
use super::types::ChatCompletionRequest;
use crate::error::{ApiError, bail_invalid_request};
use crate::routes::openai::utils::types::{ChatMessage, Tool, ToolChoice, ToolChoiceValue};
use vllm_engine_core_client::protocol::LogprobsCount;
/// Enforce the minimal compatibility contract for the Rust OpenAI server.
pub(super) fn validate_request_compat(
@@ -30,14 +31,12 @@ pub(super) fn validate_request_compat(
}
if let Some(prompt_logprobs) = request.prompt_logprobs {
if prompt_logprobs < 0 && prompt_logprobs != -1 {
bail_invalid_request!(
param = "prompt_logprobs",
"prompt_logprobs must be a non-negative value or -1."
);
}
if request.stream && (prompt_logprobs > 0 || prompt_logprobs == -1) {
if request.stream
&& matches!(
prompt_logprobs,
LogprobsCount::All | LogprobsCount::Top(1..)
)
{
bail_invalid_request!(
param = "prompt_logprobs",
"prompt_logprobs are not available when stream=true."
@@ -154,6 +153,7 @@ mod tests {
use serde_json::json;
use vllm_chat::ReasoningEffort;
use vllm_engine_core_client::protocol::LogprobsCount;
use super::validate_request_compat;
use crate::routes::openai::chat_completions::types::ChatCompletionRequest;
@@ -299,7 +299,7 @@ mod tests {
#[test]
fn validate_request_compat_rejects_top_logprobs_without_logprobs() {
let request = ChatCompletionRequest {
top_logprobs: Some(0),
top_logprobs: Some(LogprobsCount::Top(0)),
..base_request()
};
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
@@ -308,26 +308,26 @@ mod tests {
#[test]
fn validate_request_compat_rejects_streaming_prompt_logprobs_requests() {
let request = ChatCompletionRequest {
prompt_logprobs: Some(1),
prompt_logprobs: Some(LogprobsCount::Top(1)),
..base_request()
};
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
let request = ChatCompletionRequest {
prompt_logprobs: Some(-1),
prompt_logprobs: Some(LogprobsCount::All),
..base_request()
};
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
}
#[test]
fn validate_request_compat_rejects_invalid_prompt_logprobs_value() {
let request = ChatCompletionRequest {
stream: false,
prompt_logprobs: Some(-2),
..base_request()
};
assert!(validate_request_compat(&request, &served(&["Qwen/Qwen1.5-0.5B-Chat"])).is_err());
fn chat_request_deserialization_rejects_invalid_prompt_logprobs_value() {
let result = serde_json::from_value::<ChatCompletionRequest>(json!({
"model": "Qwen/Qwen1.5-0.5B-Chat",
"messages": [{"role": "user", "content": "hello"}],
"prompt_logprobs": -2
}));
assert!(result.is_err());
}
#[test]
@@ -1,3 +1,4 @@
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::{SamplingParams, TextDecodeOptions, TextRequest};
use super::types::CompletionRequest;
@@ -61,15 +62,7 @@ pub(super) fn prepare_completion_request(
.map(|request| request.lora_name.clone())
.unwrap_or_else(|| lora_resolution.model_names.first().cloned().unwrap_or_default());
let logprobs = match request.logprobs {
Some(logprobs) => Some(i32::try_from(logprobs).map_err(|_| {
ApiError::invalid_request(
"`logprobs` must fit within a signed 32-bit integer.".to_string(),
Some("logprobs"),
)
})?),
None => None,
};
let logprobs = request.logprobs.map(LogprobsCount::Top);
let prompt_only = request.echo && request.max_tokens == Some(0);
let prompt_logprobs =
request.prompt_logprobs.or(if request.echo && (!request.stream || prompt_only) {
@@ -163,6 +156,7 @@ pub(super) fn prepare_completion_request(
mod tests {
use axum::http::HeaderMap;
use serde_json::json;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::Prompt;
use super::prepare_completion_request;
@@ -247,7 +241,10 @@ mod tests {
Prompt::TokenIds(vec![11, 22, 33])
);
assert_eq!(prepared.text_request.sampling_params.max_tokens, Some(7));
assert_eq!(prepared.text_request.sampling_params.logprobs, Some(2));
assert_eq!(
prepared.text_request.sampling_params.logprobs,
Some(LogprobsCount::Top(2))
);
assert_eq!(prepared.text_request.sampling_params.top_p, Some(0.9));
assert_eq!(prepared.text_request.sampling_params.top_k, Some(42));
assert_eq!(prepared.text_request.sampling_params.min_p, Some(0.1));
@@ -410,10 +407,13 @@ mod tests {
.expect("prepare");
assert!(prepared.options.prompt_only);
assert_eq!(prepared.text_request.sampling_params.logprobs, Some(3));
assert_eq!(
prepared.text_request.sampling_params.logprobs,
Some(LogprobsCount::Top(3))
);
assert_eq!(
prepared.text_request.sampling_params.prompt_logprobs,
Some(3)
Some(LogprobsCount::Top(3))
);
}
@@ -435,10 +435,13 @@ mod tests {
)
.expect("prepare");
assert_eq!(prepared.text_request.sampling_params.logprobs, Some(3));
assert_eq!(
prepared.text_request.sampling_params.logprobs,
Some(LogprobsCount::Top(3))
);
assert_eq!(
prepared.text_request.sampling_params.prompt_logprobs,
Some(3)
Some(LogprobsCount::Top(3))
);
}
@@ -479,10 +482,13 @@ mod tests {
ResolvedRequestContext::default(),
)
.expect("prepare");
assert_eq!(prepared.text_request.sampling_params.logprobs, Some(1));
assert_eq!(
prepared.text_request.sampling_params.logprobs,
Some(LogprobsCount::Top(1))
);
assert_eq!(
prepared.text_request.sampling_params.prompt_logprobs,
Some(2)
Some(LogprobsCount::Top(2))
);
}
@@ -3,6 +3,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use validator::Validate;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::Prompt;
use crate::routes::openai::utils::types::{
@@ -131,8 +132,8 @@ pub struct CompletionRequest {
/// Restrict output to these token IDs only
pub allowed_token_ids: Option<Vec<u32>>,
/// Number of prompt logprobs to return
pub prompt_logprobs: Option<i32>,
/// Number of prompt logprobs to return. `-1` means return full vocab.
pub prompt_logprobs: Option<LogprobsCount>,
// -------- Extra vLLM Parameters --------
/// Whether to add special tokens (e.g. BOS) to the prompt
@@ -1,3 +1,4 @@
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_text::Prompt;
use super::types::CompletionRequest;
@@ -44,29 +45,18 @@ pub(super) fn validate_request_compat(
bail_invalid_request!(param = "suffix", "suffix is not supported.");
}
if let Some(logprobs) = request.logprobs
&& logprobs > i32::MAX as u32
{
bail_invalid_request!(
param = "logprobs",
"`logprobs` must fit within a signed 32-bit integer."
);
}
if let Some(prompt_logprobs) = request.prompt_logprobs {
if request.stream && (prompt_logprobs > 0 || prompt_logprobs == -1) {
if request.stream
&& matches!(
prompt_logprobs,
LogprobsCount::All | LogprobsCount::Top(1..)
)
{
bail_invalid_request!(
param = "prompt_logprobs",
"`prompt_logprobs` are not available when `stream=true`."
);
}
if prompt_logprobs < 0 && prompt_logprobs != -1 {
bail_invalid_request!(
param = "prompt_logprobs",
"`prompt_logprobs` must be a non-negative value or -1."
);
}
}
if request.use_beam_search {
@@ -101,6 +91,7 @@ pub(super) fn validate_request_compat(
#[cfg(test)]
mod tests {
use serde_json::json;
use vllm_engine_core_client::protocol::LogprobsCount;
use super::validate_request_compat;
use crate::routes::openai::completions::types::CompletionRequest;
@@ -150,7 +141,7 @@ mod tests {
#[test]
fn validate_request_compat_rejects_streaming_prompt_logprobs() {
let request = CompletionRequest {
prompt_logprobs: Some(1),
prompt_logprobs: Some(LogprobsCount::Top(1)),
..base_request()
};
assert!(
@@ -162,7 +153,7 @@ mod tests {
fn validate_request_compat_accepts_non_stream_prompt_logprobs() {
let request = CompletionRequest {
stream: false,
prompt_logprobs: Some(-1),
prompt_logprobs: Some(LogprobsCount::All),
..base_request()
};
assert!(
+3 -4
View File
@@ -2,6 +2,7 @@ pub mod hf;
use std::sync::Arc;
use vllm_engine_core_client::protocol::LogprobsCount;
use vllm_tokenizer::DynTokenizer;
use crate::error::Result;
@@ -26,9 +27,7 @@ pub struct SamplingLimits {
/// Runtime context window size reported by the engine startup handshake.
pub max_model_len: u32,
/// Maximum number of top log probabilities accepted by this frontend.
///
/// `-1` means allowing requests up to the model vocabulary size.
pub max_logprobs: i32,
pub max_logprobs: LogprobsCount,
/// Model vocabulary size from the model config, used to bound generated
/// token IDs and logits-domain sampling controls.
@@ -41,7 +40,7 @@ pub struct SamplingLimits {
impl SamplingLimits {
/// Original Python definition:
/// <https://github.com/vllm-project/vllm/blob/b5adb027ad03c29b46181752ba3b1cb84eff1dd4/vllm/config/model.py#L216-L220>
pub const DEFAULT_MAX_LOGPROBS: i32 = 20;
pub const DEFAULT_MAX_LOGPROBS: LogprobsCount = LogprobsCount::Top(20);
/// Original Python definition:
/// <https://github.com/vllm-project/vllm/blob/b5adb027ad03c29b46181752ba3b1cb84eff1dd4/vllm/sampling_params.py#L30-L32>
pub const MAX_LOGPROB_TOKEN_IDS: usize = 128;
+3 -2
View File
@@ -19,6 +19,7 @@ pub use output::{
pub use request::{Prompt, SamplingParams, TextRequest};
use trait_set::trait_set;
use vllm_engine_core_client::EngineCoreClient;
use vllm_engine_core_client::protocol::LogprobsCount;
pub use vllm_llm::FinishReason;
use vllm_llm::{GenerateOutputStream, Llm};
use vllm_tokenizer::DynTokenizer;
@@ -48,7 +49,7 @@ pub struct TextLlm {
/// Runtime context window size reported by the engine startup handshake.
max_model_len: u32,
/// Maximum number of top log probabilities accepted by this text facade.
max_logprobs: i32,
max_logprobs: LogprobsCount,
}
impl TextLlm {
@@ -68,7 +69,7 @@ impl TextLlm {
}
/// Override the maximum accepted logprobs count.
pub fn with_max_logprobs(mut self, max_logprobs: Option<i32>) -> Self {
pub fn with_max_logprobs(mut self, max_logprobs: Option<LogprobsCount>) -> Self {
if let Some(max_logprobs) = max_logprobs {
self.max_logprobs = max_logprobs;
}
+11 -10
View File
@@ -269,6 +269,7 @@ mod tests {
use std::collections::{BTreeSet, HashMap};
use serial_test::file_serial;
use vllm_engine_core_client::protocol::LogprobsCount;
use super::*;
use crate::backend::hf::HfTextBackend;
@@ -774,8 +775,8 @@ mod tests {
#[test]
fn lower_sampling_params_passes_logprobs_fields_through() {
let sampling_params = SamplingParams {
logprobs: Some(3),
prompt_logprobs: Some(-1),
logprobs: Some(LogprobsCount::Top(3)),
prompt_logprobs: Some(LogprobsCount::All),
..Default::default()
};
@@ -792,7 +793,7 @@ mod tests {
default_max_tokens: None,
},
SamplingLimits {
max_logprobs: -1,
max_logprobs: LogprobsCount::All,
..sample_sampling_limits()
},
3,
@@ -800,15 +801,15 @@ mod tests {
)
.unwrap();
assert_eq!(params.logprobs, Some(3));
assert_eq!(params.prompt_logprobs, Some(-1));
assert_eq!(params.logprobs, Some(LogprobsCount::Top(3)));
assert_eq!(params.prompt_logprobs, Some(LogprobsCount::All));
}
#[test]
fn lower_sampling_params_rejects_full_vocab_logprobs_over_default_cap() {
let error = lower_sampling_params_with_limits(
SamplingParams {
logprobs: Some(-1),
logprobs: Some(LogprobsCount::All),
..Default::default()
},
sample_sampling_limits(),
@@ -829,24 +830,24 @@ mod tests {
fn lower_sampling_params_expands_full_vocab_logprobs_from_model_vocab() {
let params = lower_sampling_params_with_limits(
SamplingParams {
logprobs: Some(-1),
logprobs: Some(LogprobsCount::All),
..Default::default()
},
SamplingLimits {
max_logprobs: 1500,
max_logprobs: LogprobsCount::Top(1500),
..sample_sampling_limits()
},
)
.unwrap();
assert_eq!(params.logprobs, Some(-1));
assert_eq!(params.logprobs, Some(LogprobsCount::All));
}
#[test]
fn lower_sampling_params_rejects_invalid_logprob_token_ids() {
let error = lower_sampling_params_with_limits(
SamplingParams {
logprobs: Some(1),
logprobs: Some(LogprobsCount::Top(1)),
logprob_token_ids: Some(vec![1000]),
..Default::default()
},
+13 -24
View File
@@ -1,15 +1,14 @@
//! Python-compatible validation for logprobs sampling params.
//!
//! `-1` is expanded only for bounds checks. The original request values are
//! `All` is expanded only for bounds checks. The original request values are
//! passed through to engine-core.
use crate::backend::SamplingLimits;
use thiserror::Error;
use vllm_engine_core_client::protocol::LogprobsCount;
#[derive(Debug, Error)]
pub enum LogprobsError {
#[error("{parameter} must be non-negative or -1, got {value}")]
InvalidCount { parameter: &'static str, value: i32 },
#[error(
"requested {parameter} of {requested}, which is greater than max allowed: {max_allowed}"
)]
@@ -30,19 +29,21 @@ pub enum LogprobsError {
"when both logprobs and logprob_token_ids are set, logprobs must equal \
len(logprob_token_ids). Got logprobs={logprobs}, len(logprob_token_ids)={num_token_ids}."
)]
TokenIdsMismatch { logprobs: i32, num_token_ids: usize },
TokenIdsMismatch {
logprobs: LogprobsCount,
num_token_ids: usize,
},
}
/// Validate logprobs count sampling parameters.
pub(super) fn validate_logprobs(
logprobs: Option<i32>,
prompt_logprobs: Option<i32>,
logprobs: Option<LogprobsCount>,
prompt_logprobs: Option<LogprobsCount>,
logprob_token_ids: Option<&[u32]>,
sampling_limits: SamplingLimits,
) -> Result<(), LogprobsError> {
let vocab_size = sampling_limits.model_vocab_size;
let max_logprobs =
normalize_logprobs_count(sampling_limits.max_logprobs, vocab_size, "max_logprobs")?;
let max_logprobs = sampling_limits.max_logprobs.expanded(vocab_size);
validate_logprobs_count(logprobs, max_logprobs, vocab_size, "logprobs")?;
validate_logprobs_count(prompt_logprobs, max_logprobs, vocab_size, "prompt_logprobs")?;
@@ -50,7 +51,7 @@ pub(super) fn validate_logprobs(
}
fn validate_logprobs_count(
requested: Option<i32>,
requested: Option<LogprobsCount>,
max_logprobs: usize,
vocab_size: usize,
parameter: &'static str,
@@ -59,7 +60,7 @@ fn validate_logprobs_count(
return Ok(());
};
let requested = normalize_logprobs_count(requested, vocab_size, parameter)?;
let requested = requested.expanded(vocab_size);
if requested > max_logprobs {
return Err(LogprobsError::TooManyCount {
parameter,
@@ -72,7 +73,7 @@ fn validate_logprobs_count(
}
pub(super) fn validate_logprob_token_ids(
logprobs: Option<i32>,
logprobs: Option<LogprobsCount>,
logprob_token_ids: Option<&[u32]>,
) -> Result<(), LogprobsError> {
let Some(logprob_token_ids) = logprob_token_ids else {
@@ -88,7 +89,7 @@ pub(super) fn validate_logprob_token_ids(
}
if let Some(logprobs) = logprobs
&& logprobs != n as i32
&& logprobs != LogprobsCount::Top(n as u32)
{
return Err(LogprobsError::TokenIdsMismatch {
logprobs,
@@ -98,15 +99,3 @@ pub(super) fn validate_logprob_token_ids(
Ok(())
}
fn normalize_logprobs_count(
value: i32,
vocab_size: usize,
parameter: &'static str,
) -> Result<usize, LogprobsError> {
match value {
-1 => Ok(vocab_size),
value if value < 0 => Err(LogprobsError::InvalidCount { parameter, value }),
value => Ok(value as usize),
}
}
+5 -5
View File
@@ -3,9 +3,9 @@ use std::collections::HashMap;
use enum_as_inner::EnumAsInner;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vllm_engine_core_client::protocol::StructuredOutputsParams;
use vllm_engine_core_client::protocol::lora::LoraRequest;
use vllm_engine_core_client::protocol::multimodal::MmFeatures;
use vllm_engine_core_client::protocol::{LogprobsCount, StructuredOutputsParams};
use crate::error::{Error, Result};
use crate::output::TextDecodeOptions;
@@ -64,12 +64,12 @@ pub struct SamplingParams {
pub thinking_token_budget: Option<i64>,
/// Number of log probabilities to return per generated token.
///
/// `None` disables sample logprobs. `-1` requests the full vocabulary.
pub logprobs: Option<i32>,
/// `None` disables sample logprobs.
pub logprobs: Option<LogprobsCount>,
/// Number of log probabilities to return per prompt token.
///
/// `None` disables prompt logprobs. `-1` requests the full vocabulary.
pub prompt_logprobs: Option<i32>,
/// `None` disables prompt logprobs.
pub prompt_logprobs: Option<LogprobsCount>,
/// Minimum probability threshold for token sampling. `None` means no
/// explicit user override.
pub min_p: Option<f32>,