[Bugfix][Rust Frontend] Fix LogprobsTensors wire schema mismatch (#53939)

Signed-off-by: jungjiyu <libraryofjiyu@gmail.com>
Signed-off-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
Co-authored-by: Tyler Michael Smith <tlrmchlsmth@gmail.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Jung Jiyu
2026-08-26 16:16:43 -07:00
committed by GitHub
co-authored by Tyler Michael Smith OpenAI Codex
parent a27e88c862
commit 3fd72ffd7b
4 changed files with 56 additions and 3 deletions
@@ -222,6 +222,7 @@ impl WireLogprobs {
data: WireArrayData::RawView(token_ranks.into()),
},
cu_num_generated_tokens: None,
cu_num_generated_tokens_tensor: None,
})
}
@@ -239,6 +240,15 @@ impl WireLogprobs {
);
}
// Unlike the sibling check above, don't Debug-print the payload:
// an opaque non-None value here may embed a full tensor blob.
if self.cu_num_generated_tokens_tensor.is_some() {
bail_ext_value_decode!(
"{field_prefix}.cu_num_generated_tokens_tensor: \
expected None for per-request engine-core logprobs payload"
);
}
let token_ids = array::decode_array2_u32(
self.logprob_token_ids,
&format!("{field_prefix}.logprob_token_ids"),
@@ -87,6 +87,7 @@ fn inline_prompt_logprobs_value() -> Value {
ndarray_value("float32", &[2, 3], probs),
ndarray_value("int64", &[2], ranks),
Value::Nil,
Value::Nil,
])
}
@@ -240,6 +241,29 @@ fn decodes_inline_prompt_logprobs() {
assert_eq!(logprobs, expected_prompt_logprobs());
}
#[test]
fn rejects_non_none_cu_num_generated_tokens_tensor() {
let Value::Array(mut fields) = inline_prompt_logprobs_value() else {
panic!("inline_prompt_logprobs_value must be an array");
};
fields[4] = Value::from(42);
let frames = vec![Bytes::from(encode_value(&output_wire_with_custom_fields(
None,
Some(Value::Array(fields)),
)))];
let error = decode_engine_core_outputs(&frames).unwrap_err();
let crate::error::Error::ExtValueDecode { message } = &error else {
panic!("expected ValueDecodeExt");
};
assert_eq!(
message,
"new_prompt_logprobs_tensors.cu_num_generated_tokens_tensor: \
expected None for per-request engine-core logprobs payload"
);
}
#[test]
fn decodes_big_endian_payloads() {
let frames = vec![Bytes::from(encode_value(&output_wire_with_custom_fields(
@@ -3,6 +3,7 @@
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use crate::protocol::OpaqueValue;
use crate::protocol::tensor::WireNdArray;
/// Python wire representation of `LogprobsLists` / `LogprobsTensors` before
@@ -13,7 +14,15 @@ use crate::protocol::tensor::WireNdArray;
/// into semantic per-position logprobs records.
///
/// Original Python definition:
/// <https://github.com/vllm-project/vllm/blob/f22d6e026798a74e6542a52ef776c054f2de572a/vllm/v1/outputs.py#L23-L56>
/// <https://github.com/vllm-project/vllm/blob/d5cadcee8641a9fcec15facb5a9157d157daa207/vllm/v1/outputs.py#L30-L106>
///
/// # Wire compatibility
///
/// Python's `LogprobsLists` / `LogprobsTensors` are `NamedTuple`s: every
/// field is serialized positionally, and `Deserialize_tuple` here hard-fails
/// on payloads longer than this struct. To preserve positional compatibility,
/// append new Python fields and add matching trailing `#[serde(default)]`
/// fields here in the same change.
#[derive(Debug, Clone, PartialEq, Serialize_tuple, Deserialize_tuple)]
pub struct WireLogprobs {
/// Wire array with shape `[num_positions, max_num_logprobs + 1]`.
@@ -31,4 +40,12 @@ pub struct WireLogprobs {
/// the semantic Rust decoder rejects any other value.
#[serde(default)]
pub cu_num_generated_tokens: Option<Vec<usize>>,
/// Device-only generation boundaries added to `LogprobsTensors` in
/// vllm-project/vllm#52242. Preserved only for wire compatibility:
/// NamedTuple encoding always emits the slot even when it is `None`,
/// so this field must exist for the 5-element payload to decode.
/// Scheduler-sliced per-request outputs should emit `None` here, and
/// the semantic Rust decoder rejects any other value.
#[serde(default)]
pub cu_num_generated_tokens_tensor: Option<OpaqueValue>,
}
@@ -353,7 +353,8 @@ inline_prompt_logprobs = engine_outputs_wire(
).tobytes(),
),
("int64", [2], np.array([3, 4], dtype=np.int64).tobytes()),
None,
None, # cu_num_generated_tokens
None, # cu_num_generated_tokens_tensor
),
)
)
@@ -376,7 +377,8 @@ multipart_prompt_logprobs = engine_outputs_wire(
).tobytes(),
),
("int64", [2], np.array([3, 4], dtype=np.int64).tobytes()),
None,
None, # cu_num_generated_tokens
None, # cu_num_generated_tokens_tensor
),
)
)