[Bugfix] Handle empty FlatLogprobs slices and delta output (#53704)
Signed-off-by: tony <864832769@qq.com>
Tony committed
Aug 26, 2026 at 21:00 UTC
903a02192fca19e4c89705af7017c0f24971ea4f
4 files changed
+53
-7
tests/test_logprobs.py
+7
@@ -208,3 +208,10 @@ def test_flat_logprobs_access() -> None:
208
assert logprobs_last2.logprobs == [0.4, 0.5, 0.6, 0.1]
209
assert logprobs_last2.ranks == [40, 50, 60, 10]
210
assert logprobs_last2.decoded_tokens == ["40", "50", "60", "10"]
211
+
212
+ for empty_slice in (slice(0, 0), slice(3, 3), slice(10, 10)):
213
+ empty = logprobs[empty_slice]
214
+ assert isinstance(empty, FlatLogprobs)
215
+ assert len(empty) == 0
216
+ assert empty.start_indices == []
217
+ assert empty.end_indices == []
tests/v1/engine/test_output_processor.py
+32
-2
@@ -3,6 +3,7 @@
3
4
import math
5
import time
6
+from unittest.mock import MagicMock
7
8
import pytest
9
@@ -14,7 +15,7 @@ from tests.v1.engine.utils import (
15
MockEngineCore,
16
)
17
from vllm import PoolingParams
17
-from vllm.logprobs import PromptLogprobs, SampleLogprobs
18
+from vllm.logprobs import FlatLogprobs, Logprob, PromptLogprobs, SampleLogprobs
19
from vllm.lora.request import LoRARequest
20
from vllm.outputs import CompletionOutput, RequestOutput
21
from vllm.sampling_params import RequestOutputKind, SamplingParams
@@ -26,10 +27,39 @@ from vllm.v1.engine import (
27
EngineCoreRequest,
28
FinishReason,
29
)
29
-from vllm.v1.engine.output_processor import OutputProcessor, RequestOutputCollector
30
+from vllm.v1.engine.output_processor import (
31
+ OutputProcessor,
32
+ RequestOutputCollector,
33
+ RequestState,
34
+)
35
from vllm.v1.metrics.stats import IterationStats, SchedulerStats
36
37
38
+@pytest.mark.parametrize("flat_logprobs", [False, True])
39
+def test_delta_output_without_new_tokens_returns_empty_logprobs(
40
+ flat_logprobs: bool,
41
+) -> None:
42
+ accumulated = FlatLogprobs() if flat_logprobs else []
43
+ accumulated.append({1: Logprob(logprob=-0.5, rank=1)})
44
+
45
+ state = RequestState.__new__(RequestState)
46
+ state.detokenizer = MagicMock()
47
+ state.detokenizer.get_next_output_text.return_value = ""
48
+ state.logprobs_processor = MagicMock()
49
+ state.logprobs_processor.logprobs = accumulated
50
+ state.logprobs_processor.cumulative_logprob = -0.5
51
+ state.output_kind = RequestOutputKind.DELTA
52
+ state.request_index = 0
53
+ state.sampling_mask_chunks = []
54
+ state.routed_experts_chunks = []
55
+ state.spec_decode_metrics = None
56
+
57
+ output = state._new_completion_output([], None, None)
58
+
59
+ assert isinstance(output.logprobs, FlatLogprobs if flat_logprobs else list)
60
+ assert len(output.logprobs) == 0
61
+
62
+
63
def _ref_convert_id_to_token(
64
tokenizer: TokenizerLike,
65
token_id: int,
vllm/logprobs.py
+9
-4
@@ -119,13 +119,18 @@ class FlatLogprobs(MutableSequence[LogprobsOnePosition | None]):
119
for i in range(self.start_indices[index], self.end_indices[index])
120
}
121
elif isinstance(index, slice):
122
- min_index = self.start_indices[index][0]
123
- max_index = self.end_indices[index][-1]
122
+ selected_starts = self.start_indices[index]
123
+ selected_ends = self.end_indices[index]
124
+ # Empty slices have no source offset to normalize.
125
+ if not selected_starts:
126
+ return FlatLogprobs()
127
+ min_index = selected_starts[0]
128
+ max_index = selected_ends[-1]
129
return FlatLogprobs(
130
# Shift updated start_indices and end_indices to
131
# be 0-indexed
127
- start_indices=[i - min_index for i in self.start_indices[index]],
128
- end_indices=[i - min_index for i in self.end_indices[index]],
132
+ start_indices=[i - min_index for i in selected_starts],
133
+ end_indices=[i - min_index for i in selected_ends],
134
token_ids=self.token_ids[min_index:max_index],
135
logprobs=self.logprobs[min_index:max_index],
136
ranks=self.ranks[min_index:max_index],
vllm/v1/engine/output_processor.py
+5
-1
@@ -411,7 +411,11 @@ class RequestState:
411
# Prepare logprobs, based on delta mode
412
logprobs = self.logprobs_processor.logprobs
413
if delta and logprobs:
414
- logprobs = logprobs[-len(token_ids) :]
414
+ num_new_tokens = len(token_ids)
415
+ # Avoid [-0:], which returns the full accumulated history when a
416
+ # delta contains no new token IDs. [:0] preserves the concrete
417
+ # list or FlatLogprobs representation while returning no entries.
418
+ logprobs = logprobs[-num_new_tokens:] if num_new_tokens else logprobs[:0]
419
420
sampling_mask = None
421
if finished and self.sampling_mask_chunks: