diff --git a/tests/test_logprobs.py b/tests/test_logprobs.py index 75e9d337aa..48056d8ea2 100644 --- a/tests/test_logprobs.py +++ b/tests/test_logprobs.py @@ -208,3 +208,10 @@ def test_flat_logprobs_access() -> None: assert logprobs_last2.logprobs == [0.4, 0.5, 0.6, 0.1] assert logprobs_last2.ranks == [40, 50, 60, 10] assert logprobs_last2.decoded_tokens == ["40", "50", "60", "10"] + + for empty_slice in (slice(0, 0), slice(3, 3), slice(10, 10)): + empty = logprobs[empty_slice] + assert isinstance(empty, FlatLogprobs) + assert len(empty) == 0 + assert empty.start_indices == [] + assert empty.end_indices == [] diff --git a/tests/v1/engine/test_output_processor.py b/tests/v1/engine/test_output_processor.py index fa63b52200..f578aae7f0 100644 --- a/tests/v1/engine/test_output_processor.py +++ b/tests/v1/engine/test_output_processor.py @@ -3,6 +3,7 @@ import math import time +from unittest.mock import MagicMock import pytest @@ -14,7 +15,7 @@ from tests.v1.engine.utils import ( MockEngineCore, ) from vllm import PoolingParams -from vllm.logprobs import PromptLogprobs, SampleLogprobs +from vllm.logprobs import FlatLogprobs, Logprob, PromptLogprobs, SampleLogprobs from vllm.lora.request import LoRARequest from vllm.outputs import CompletionOutput, RequestOutput from vllm.sampling_params import RequestOutputKind, SamplingParams @@ -26,10 +27,39 @@ from vllm.v1.engine import ( EngineCoreRequest, FinishReason, ) -from vllm.v1.engine.output_processor import OutputProcessor, RequestOutputCollector +from vllm.v1.engine.output_processor import ( + OutputProcessor, + RequestOutputCollector, + RequestState, +) from vllm.v1.metrics.stats import IterationStats, SchedulerStats +@pytest.mark.parametrize("flat_logprobs", [False, True]) +def test_delta_output_without_new_tokens_returns_empty_logprobs( + flat_logprobs: bool, +) -> None: + accumulated = FlatLogprobs() if flat_logprobs else [] + accumulated.append({1: Logprob(logprob=-0.5, rank=1)}) + + state = RequestState.__new__(RequestState) + state.detokenizer = MagicMock() + state.detokenizer.get_next_output_text.return_value = "" + state.logprobs_processor = MagicMock() + state.logprobs_processor.logprobs = accumulated + state.logprobs_processor.cumulative_logprob = -0.5 + state.output_kind = RequestOutputKind.DELTA + state.request_index = 0 + state.sampling_mask_chunks = [] + state.routed_experts_chunks = [] + state.spec_decode_metrics = None + + output = state._new_completion_output([], None, None) + + assert isinstance(output.logprobs, FlatLogprobs if flat_logprobs else list) + assert len(output.logprobs) == 0 + + def _ref_convert_id_to_token( tokenizer: TokenizerLike, token_id: int, diff --git a/vllm/logprobs.py b/vllm/logprobs.py index cc77f5f7f1..a822199206 100644 --- a/vllm/logprobs.py +++ b/vllm/logprobs.py @@ -119,13 +119,18 @@ class FlatLogprobs(MutableSequence[LogprobsOnePosition | None]): for i in range(self.start_indices[index], self.end_indices[index]) } elif isinstance(index, slice): - min_index = self.start_indices[index][0] - max_index = self.end_indices[index][-1] + selected_starts = self.start_indices[index] + selected_ends = self.end_indices[index] + # Empty slices have no source offset to normalize. + if not selected_starts: + return FlatLogprobs() + min_index = selected_starts[0] + max_index = selected_ends[-1] return FlatLogprobs( # Shift updated start_indices and end_indices to # be 0-indexed - start_indices=[i - min_index for i in self.start_indices[index]], - end_indices=[i - min_index for i in self.end_indices[index]], + start_indices=[i - min_index for i in selected_starts], + end_indices=[i - min_index for i in selected_ends], token_ids=self.token_ids[min_index:max_index], logprobs=self.logprobs[min_index:max_index], ranks=self.ranks[min_index:max_index], diff --git a/vllm/v1/engine/output_processor.py b/vllm/v1/engine/output_processor.py index 055e3af574..6238fbc817 100644 --- a/vllm/v1/engine/output_processor.py +++ b/vllm/v1/engine/output_processor.py @@ -411,7 +411,11 @@ class RequestState: # Prepare logprobs, based on delta mode logprobs = self.logprobs_processor.logprobs if delta and logprobs: - logprobs = logprobs[-len(token_ids) :] + num_new_tokens = len(token_ids) + # Avoid [-0:], which returns the full accumulated history when a + # delta contains no new token IDs. [:0] preserves the concrete + # list or FlatLogprobs representation while returning no entries. + logprobs = logprobs[-num_new_tokens:] if num_new_tokens else logprobs[:0] sampling_mask = None if finished and self.sampling_mask_chunks: