[Bugfix] Handle empty FlatLogprobs slices and delta output (#53704)

Signed-off-by: tony <864832769@qq.com>
This commit is contained in:
Tony
2026-08-26 21:00:54 +08:00
committed by GitHub
parent 6a5e8f5979
commit 903a02192f
4 changed files with 53 additions and 7 deletions
+7
View File
@@ -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 == []
+32 -2
View File
@@ -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,
+9 -4
View File
@@ -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],
+5 -1
View File
@@ -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: