[Bugfix][V1/V2] Fix prompt_logprobs to respect logprobs_mode (#47680)

Signed-off-by: Wojciech Wais <wojciech.wais@gmail.com> Signed-off-by: Federico Kamelhar <209537060+fede-kamel@users.noreply.github.com> Signed-off-by: Allen Shen <aoshen@inferact.ai> Co-authored-by: Wojciech Wais <wojciech.wais@gmail.com> Co-authored-by: Federico Kamelhar <209537060+fede-kamel@users.noreply.github.com>

aoshen02 committed Jul 18, 2026 at 04:58 UTC 41ea2dd44a3a20c46ebeb985de0022c7673fb953
11 files changed +106 -40
tests/v1/sample/test_logprobs.py
+38
@@ -564,6 +564,44 @@ def test_logprobs_mode(logprobs_mode: LogprobsMode):
564 cleanup_dist_env_and_memory()
565
566
567 +def test_prompt_logprobs_mode():
568 + """prompt_logprobs must respect logprobs_mode: *_logits and *_logprobs
569 + must return different values. Prompt tokens skip sampling processors,
570 + so processed_* == raw_* on the prompt side."""
571 + from vllm import LLM
572 +
573 + values: dict[str, float] = {}
574 + for mode in get_args(LogprobsMode):
575 + llm = LLM(
576 + "facebook/opt-125m",
577 + enable_prefix_caching=False,
578 + gpu_memory_utilization=0.05,
579 + max_model_len=16,
580 + logprobs_mode=mode,
581 + )
582 + try:
583 + results = llm.generate(
584 + ["Hello world"],
585 + sampling_params=SamplingParams(
586 + max_tokens=1, prompt_logprobs=0, temperature=0
587 + ),
588 + )
589 + assert results[0].prompt_logprobs is not None
590 + assert results[0].prompt_logprobs[1] is not None
591 + tok_id = results[0].prompt_token_ids[1]
592 + values[mode] = results[0].prompt_logprobs[1][tok_id].logprob
593 + finally:
594 + del llm
595 + torch.accelerator.empty_cache()
596 + cleanup_dist_env_and_memory()
597 +
598 + assert values["raw_logprobs"] <= 0
599 + assert values["processed_logprobs"] <= 0
600 + assert values["raw_logits"] != values["raw_logprobs"]
601 + assert values["processed_logits"] == values["raw_logits"]
602 + assert values["processed_logprobs"] == values["raw_logprobs"]
603 +
604 +
605 class TestCorrectDecodedToken:
606 """Unit tests for _correct_decoded_token method in LogprobsProcessor.
607
vllm/config/model.py
+2
@@ -233,6 +233,8 @@ class ModelConfig:
233 Raw means the values before applying any logit processors, like bad words.
234 Processed means the values after applying all processors, including
235 temperature and top_k/top_p.
236 + Note: for prompt_logprobs, processed_* and raw_* yield identical results
237 + because prompt tokens do not go through sampling processors.
238 """
239 use_fp64_gumbel: bool = False
240 """Whether to use FP64 (instead of FP32) random noise for Gumbel-max and
vllm/config/vllm.py
-7
@@ -2202,13 +2202,6 @@ class VllmConfig:
2202 if model_config is not None and model_config.enable_prompt_embeds:
2203 unsupported.append("prompt embeds")
2204
2205 - if (
2206 - model_config is not None
2207 - and model_config.runner_type == "generate"
2208 - and model_config.logprobs_mode in ("raw_logits", "processed_logits")
2209 - ):
2210 - unsupported.append(f"logprobs mode '{model_config.logprobs_mode}'")
2211 -
2205 if self.cache_config.kv_sharing_fast_prefill:
2206 # Will be added by https://github.com/vllm-project/vllm/pull/35045
2207 unsupported.append("KV sharing fast prefill")
vllm/model_executor/models/diffusion_gemma.py
+4 -2
@@ -53,7 +53,7 @@ from vllm.v1.worker.gpu.attn_utils import build_attn_metadata
53 from vllm.v1.worker.gpu.buffer_utils import UvaBackedTensor, async_copy_to_gpu
54 from vllm.v1.worker.gpu.input_batch import InputBatch
55 from vllm.v1.worker.gpu.model_states.interface import ModelState
56 -from vllm.v1.worker.gpu.sample.logprob import compute_topk_logprobs
56 +from vllm.v1.worker.gpu.sample.logprob import compute_topk_scores
57 from vllm.v1.worker.gpu.sample.output import SamplerOutput
58 from vllm.v1.worker.gpu.sample.penalties import use_penalty
59 from vllm.v1.worker.gpu.states import RequestState
@@ -1064,6 +1064,7 @@ class DiffusionSampler:
1064 ):
1065 self.sampling_states = sampler.sampling_states
1066 self.req_states = sampler.req_states
1067 + self.logits_mode = sampler.logprobs_mode in ("raw_logits", "processed_logits")
1068 # Self-conditioning soft embed = probs @ embed_weight * normalizer,
1069 # computed in the sampler (see _compiled_sample_step). ``embed_weight``
1070 # is the vocab-parallel shard; [sc_vocab_start, sc_vocab_end) is this
@@ -1359,10 +1360,11 @@ class DiffusionSampler:
1360 # positions are never emitted.
1361 k_i = int(valid_canvas_len_np[start_req + li])
1362 pos = li * CL
1362 - self._pending_logprobs[slot.item()] = compute_topk_logprobs(
1363 + self._pending_logprobs[slot.item()] = compute_topk_scores(
1364 flat_logits[pos : pos + k_i],
1365 max_num_logprobs,
1366 argmax_tokens[local_idx][:k_i],
1367 + logits_mode=self.logits_mode,
1368 )
1369
1370 # Commit steps: is_committing was True at entry. Reassemble previously
vllm/v1/sample/rejection_sampler.py
+8 -2
@@ -67,8 +67,14 @@ class RejectionSampler(nn.Module):
67 self.sampler = sampler
68 self.use_fp64_gumbel = getattr(sampler, "use_fp64_gumbel", False)
69 logprobs_mode = self.sampler.logprobs_mode
70 - self.is_processed_logprobs_mode = logprobs_mode.startswith("processed")
71 - self.is_logits_logprobs_mode = logprobs_mode.endswith("logits")
70 + self.is_processed_logprobs_mode = logprobs_mode in (
71 + "processed_logprobs",
72 + "processed_logits",
73 + )
74 + self.is_logits_logprobs_mode = logprobs_mode in (
75 + "raw_logits",
76 + "processed_logits",
77 + )
78
79 self.synthetic_conditional_rates: torch.Tensor | None = None
80 if (
vllm/v1/worker/gpu/model_runner.py
+4 -1
@@ -341,7 +341,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
341 self.speculative_config,
342 self.device,
343 )
344 - self.prompt_logprobs_worker = PromptLogprobsWorker(self.max_num_reqs)
344 + self.prompt_logprobs_worker = PromptLogprobsWorker(
345 + self.max_num_reqs,
346 + logprobs_mode=self.model_config.logprobs_mode,
347 + )
348 self.structured_outputs_worker = StructuredOutputsWorker(
349 max_num_logits=self.max_num_reqs * self.decode_query_len,
350 vocab_size=self.vocab_size,
vllm/v1/worker/gpu/sample/logprob.py
+12 -5
@@ -106,7 +106,7 @@ def compute_token_logprobs(
106 return logprobs
107
108
109 -def compute_topk_logprobs(
109 +def compute_topk_scores(
110 logits: torch.Tensor,
111 num_logprobs: int,
112 sampled_token_ids: torch.Tensor,
@@ -114,6 +114,7 @@ def compute_topk_logprobs(
114 logprob_token_ids_state: "LogprobTokenIdsState | None" = None,
115 expanded_idx_mapping: torch.Tensor | None = None,
116 max_per_req_token_ids: int = 0,
117 + logits_mode: bool = False,
118 ) -> LogprobsTensors:
119 assert num_logprobs >= 0
120 batch_size, vocab_size = logits.shape
@@ -124,7 +125,10 @@ def compute_topk_logprobs(
125 if num_logprobs > 0:
126 topk_indices = torch.topk(logits, num_logprobs, dim=-1).indices
127 logprob_token_ids = torch.cat((logprob_token_ids, topk_indices), dim=1)
127 - logprobs = compute_token_logprobs(logits, logprob_token_ids)
128 + if logits_mode:
129 + scores = logits.gather(-1, logprob_token_ids).to(torch.float32)
130 + else:
131 + scores = compute_token_logprobs(logits, logprob_token_ids)
132 else:
133 # Some requests specified logprob_token_ids. Build the [batch_size,
134 # 1 + max_cols] token_ids matrix and validity mask on the GPU via a
@@ -158,8 +162,11 @@ def compute_topk_logprobs(
162 NUM_TOPK=num_logprobs,
163 PADDED_COLS=triton.next_power_of_2(num_cols),
164 )
161 - logprobs = compute_token_logprobs(logits, logprob_token_ids)
162 - logprobs = logprobs.masked_fill(~valid_mask, float("-inf"))
165 + if logits_mode:
166 + scores = logits.gather(-1, logprob_token_ids).to(torch.float32)
167 + else:
168 + scores = compute_token_logprobs(logits, logprob_token_ids)
169 + scores = scores.masked_fill(~valid_mask, float("-inf"))
170
171 token_ranks = torch.empty(batch_size, dtype=torch.int64, device=logits.device)
172 _ranks_kernel[(batch_size,)](
@@ -172,7 +179,7 @@ def compute_topk_logprobs(
179 )
180 return LogprobsTensors(
181 logprob_token_ids=logprob_token_ids,
175 - logprobs=logprobs,
182 + logprobs=scores,
183 selected_token_ranks=token_ranks,
184 cu_num_generated_tokens=cu_num_logits,
185 )
vllm/v1/worker/gpu/sample/prompt_logprob.py
+17 -11
@@ -5,16 +5,18 @@ from collections.abc import Callable
5 import numpy as np
6 import torch
7
8 +from vllm.config.model import LogprobsMode
9 from vllm.sampling_params import SamplingParams
10 from vllm.triton_utils import tl, triton
11 from vllm.v1.outputs import LogprobsTensors
12 from vllm.v1.worker.gpu.input_batch import InputBatch
12 -from vllm.v1.worker.gpu.sample.logprob import compute_topk_logprobs
13 +from vllm.v1.worker.gpu.sample.logprob import compute_topk_scores
14
15
16 class PromptLogprobsWorker:
16 - def __init__(self, max_num_reqs: int):
17 + def __init__(self, max_num_reqs: int, logprobs_mode: LogprobsMode = "raw_logprobs"):
18 self.max_num_reqs = max_num_reqs
19 + self.logprobs_mode = logprobs_mode
20
21 self.uses_prompt_logprobs = np.zeros(self.max_num_reqs, dtype=bool)
22 self.num_prompt_logprobs = np.zeros(self.max_num_reqs, dtype=np.int32)
@@ -82,6 +84,7 @@ class PromptLogprobsWorker:
84 hidden_states[: input_batch.num_tokens],
85 logits_fn,
86 max_num_prompt_logprobs,
87 + self.logprobs_mode,
88 )
89 )
90
@@ -206,33 +209,36 @@ def compute_prompt_logprobs_with_chunking(
209 prompt_hidden_states: torch.Tensor,
210 logits_fn: Callable[[torch.Tensor], torch.Tensor],
211 num_prompt_logprobs: int,
212 + logprobs_mode: LogprobsMode = "raw_logprobs",
213 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
214 # Since materializing the full prompt logits can take too much memory,
215 # we compute it in chunks.
216 CHUNK_SIZE = 1024
217 token_ids = []
214 - logprobs = []
218 + scores = []
219 ranks = []
220 + logits_mode = logprobs_mode in ("raw_logits", "processed_logits")
221 prompt_token_ids = prompt_token_ids.to(torch.int64)
222 for start_idx in range(0, prompt_token_ids.shape[0], CHUNK_SIZE):
223 end_idx = start_idx + CHUNK_SIZE
224 # NOTE(woosuk): logits_fn can be slow because it involves all-gather.
225 prompt_logits = logits_fn(prompt_hidden_states[start_idx:end_idx])
221 - requested_num_prompt_logprobs = (
226 + requested_num = (
227 prompt_logits.shape[-1]
228 if num_prompt_logprobs == -1
229 else num_prompt_logprobs
230 )
226 - prompt_logprobs = compute_topk_logprobs(
231 + result = compute_topk_scores(
232 prompt_logits,
228 - requested_num_prompt_logprobs,
233 + requested_num,
234 prompt_token_ids[start_idx:end_idx],
235 + logits_mode=logits_mode,
236 )
231 - token_ids.append(prompt_logprobs.logprob_token_ids)
232 - logprobs.append(prompt_logprobs.logprobs)
233 - ranks.append(prompt_logprobs.selected_token_ranks)
237 + token_ids.append(result.logprob_token_ids)
238 + scores.append(result.logprobs)
239 + ranks.append(result.selected_token_ranks)
240
241 token_ids = torch.cat(token_ids, dim=0) if len(token_ids) > 1 else token_ids[0]
236 - logprobs = torch.cat(logprobs, dim=0) if len(logprobs) > 1 else logprobs[0]
242 + scores = torch.cat(scores, dim=0) if len(scores) > 1 else scores[0]
243 ranks = torch.cat(ranks, dim=0) if len(ranks) > 1 else ranks[0]
238 - return token_ids, logprobs, ranks
244 + return token_ids, scores, ranks
vllm/v1/worker/gpu/sample/sampler.py
+8 -6
@@ -19,7 +19,7 @@ from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
19 from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
20 from vllm.v1.worker.gpu.sample.logprob import (
21 LogprobTokenIdsState,
22 - compute_topk_logprobs,
22 + compute_topk_scores,
23 )
24 from vllm.v1.worker.gpu.sample.output import SamplerOutput
25 from vllm.v1.worker.gpu.sample.penalties import PenaltiesState
@@ -38,8 +38,6 @@ class Sampler:
38 num_speculative_tokens: int = 1,
39 use_fp64_gumbel: bool = False,
40 ):
41 - if logprobs_mode not in ("processed_logprobs", "raw_logprobs"):
42 - raise NotImplementedError(f"Unsupported logprobs_mode: {logprobs_mode}")
41 self.logprobs_mode = logprobs_mode
42 self.compute_nans = envs.VLLM_COMPUTE_NANS_IN_LOGITS # False by default.
43 self.use_fp64_gumbel = use_fp64_gumbel
@@ -102,12 +100,12 @@ class Sampler:
100 )
101
102 if return_logprobs:
105 - if self.logprobs_mode == "processed_logprobs":
103 + if self.logprobs_mode in ("processed_logprobs", "processed_logits"):
104 logits = processed_logits
105 expanded_logits = logits.shape[0] != idx_mapping_np.shape[0]
106 cu_num_logits = cu_num_logits_np.tolist() if expanded_logits else None
107 num_logprobs = max_num_logprobs if max_num_logprobs != NO_LOGPROBS else 0
110 - logprobs_tensors = compute_topk_logprobs(
108 + logprobs_tensors = compute_topk_scores(
109 logits,
110 num_logprobs,
111 sampled,
@@ -115,6 +113,7 @@ class Sampler:
113 logprob_token_ids_state=self.logprob_token_ids_state,
114 expanded_idx_mapping=input_batch.expanded_idx_mapping,
115 max_per_req_token_ids=max_per_req_token_ids,
116 + logits_mode=self.logprobs_mode in ("raw_logits", "processed_logits"),
117 )
118 else:
119 logprobs_tensors = None
@@ -222,7 +221,10 @@ class Sampler:
221 # any greedy requests or per-request seeds, or if post-processed
222 # logprobs need to be returned for any requests.
223 (top_k is None and top_p is None)
225 - or (return_logprobs and self.logprobs_mode == "processed_logprobs")
224 + or (
225 + return_logprobs
226 + and self.logprobs_mode in ("processed_logprobs", "processed_logits")
227 + )
228 or self.sampling_states.any_greedy(idx_mapping_np)
229 or self.sampling_states.any_explicit_seed(idx_mapping_np)
230 )
vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
+5 -3
@@ -11,7 +11,7 @@ from vllm.v1.worker.gpu.input_batch import (
11 get_num_sampled_and_rejected,
12 )
13 from vllm.v1.worker.gpu.metrics.logits import get_num_nans
14 -from vllm.v1.worker.gpu.sample.logprob import compute_topk_logprobs
14 +from vllm.v1.worker.gpu.sample.logprob import compute_topk_scores
15 from vllm.v1.worker.gpu.sample.output import SamplerOutput
16 from vllm.v1.worker.gpu.sample.sampler import Sampler
17 from vllm.v1.worker.gpu.sample.states import NO_LOGPROBS
@@ -91,11 +91,13 @@ class RejectionSampler:
91 num_warps=1,
92 )
93 expanded_logits = num_logits != input_batch.idx_mapping.shape[0]
94 - return compute_topk_logprobs(
94 + return compute_topk_scores(
95 logits,
96 max_num_logprobs,
97 flat_sampled,
98 input_batch.cu_num_logits_np.tolist() if expanded_logits else None,
99 + logits_mode=self.sampler.logprobs_mode
100 + in ("raw_logits", "processed_logits"),
101 )
102
103 def __call__(
@@ -139,7 +141,7 @@ class RejectionSampler:
141 sampled,
142 num_sampled,
143 processed_logits
142 - if self.sampler.logprobs_mode == "processed_logprobs"
144 + if self.sampler.logprobs_mode in ("processed_logprobs", "processed_logits")
145 else logits,
146 )
147
vllm/v1/worker/gpu_model_runner.py
+8 -3
@@ -5635,10 +5635,15 @@ class GPUModelRunner(
5635 # to gather the logprob for.
5636 tgt_token_ids = prompt_token_ids[start_tok : start_tok + num_logits]
5637
5638 - # Compute prompt logprobs.
5639 - logprobs = self.sampler.compute_logprobs(logits)
5638 + # Compute prompt scores respecting logprobs_mode.
5639 + # NOTE: prompt tokens skip sampling processors, so
5640 + # processed_* and raw_* yield the same scores here.
5641 + if self.model_config.logprobs_mode in ("raw_logits", "processed_logits"):
5642 + scores = logits.to(torch.float32)
5643 + else:
5644 + scores = self.sampler.compute_logprobs(logits)
5645 token_ids, logprobs, ranks, _ = self.sampler.gather_logprobs(
5641 - logprobs, num_prompt_logprobs, tgt_token_ids
5646 + scores, num_prompt_logprobs, tgt_token_ids
5647 )
5648
5649 # Transfer GPU->CPU async.