main
py 356 lines 13.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Sequence
5
6 import numpy as np
7 import torch
8
9 import vllm.envs as envs
10 from vllm.config import VllmConfig
11 from vllm.config.model import PROCESSED_LOGPROBS_MODES, LogprobsMode
12 from vllm.sampling_params import SamplingParams
13 from vllm.v1.sample.ops.topk_topp_sampler import (
14 apply_top_k_top_p,
15 flashinfer_sample,
16 flashinfer_sampler_supported,
17 xpu_sample,
18 xpu_sampler_supported,
19 )
20 from vllm.v1.worker.gpu.input_batch import InputBatch, get_num_sampled_and_rejected
21 from vllm.v1.worker.gpu.metrics.logits import get_num_nans
22 from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
23 from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
24 from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
25 from vllm.v1.worker.gpu.sample.logits_processor.interface import (
26 LogitsContext,
27 LogitsProcessor,
28 LogitsProcRequestState,
29 )
30 from vllm.v1.worker.gpu.sample.logprob import (
31 LogprobTokenIdsState,
32 compute_topk_scores,
33 )
34 from vllm.v1.worker.gpu.sample.output import SamplerOutput, SamplingMaskTensors
35 from vllm.v1.worker.gpu.sample.penalties import PenaltiesState
36 from vllm.v1.worker.gpu.sample.states import NO_LOGPROBS, SamplingStates
37 from vllm.v1.worker.gpu.sample.thinking_budget import ThinkingBudgetState
38 from vllm.v1.worker.gpu.sample.trace_replay import TraceReplayState
39 from vllm.v1.worker.gpu.states import RequestState
40
41
42 class Sampler:
43 def __init__(
44 self,
45 vllm_config: VllmConfig,
46 max_num_reqs: int,
47 vocab_size: int,
48 device: torch.device,
49 req_states: RequestState,
50 logprobs_mode: LogprobsMode = "raw_logprobs",
51 num_speculative_tokens: int = 1,
52 use_fp64_gumbel: bool = False,
53 enable_trace_replay: bool = False,
54 return_sampling_mask: bool = False,
55 custom_logits_processors: Sequence[LogitsProcessor] = (),
56 ):
57 self.logprobs_mode = logprobs_mode
58 self.compute_nans = envs.VLLM_COMPUTE_NANS_IN_LOGITS # False by default.
59 self.use_fp64_gumbel = use_fp64_gumbel
60
61 self.req_states = req_states
62 self.sampling_states = SamplingStates(max_num_reqs, vocab_size)
63
64 lp_req_state = LogitsProcRequestState.from_request_state(req_states)
65 self.penalties_state = PenaltiesState(vllm_config, lp_req_state)
66 logit_bias_state = LogitBiasState(vllm_config, lp_req_state)
67 bad_words_state = BadWordsState(vllm_config, lp_req_state)
68
69 # List order is pipeline order: bias adds, penalties scale, so the
70 # two do not commute.
71 self.logits_processors: list[LogitsProcessor] = [
72 logit_bias_state,
73 self.penalties_state,
74 bad_words_state,
75 *custom_logits_processors,
76 ]
77
78 self.logprob_token_ids_state = LogprobTokenIdsState(max_num_reqs, device)
79 self.thinking_budget_state = ThinkingBudgetState(
80 req_states, vllm_config.reasoning_config
81 )
82 self.trace_replay_state = (
83 TraceReplayState(req_states) if enable_trace_replay else None
84 )
85 self.needs_logits_processing = np.zeros(max_num_reqs, dtype=bool)
86 self.num_speculative_tokens = num_speculative_tokens
87 self.return_sampling_mask = return_sampling_mask
88 self.use_flashinfer = (
89 not return_sampling_mask and flashinfer_sampler_supported()
90 )
91 # The XPU kernel draws fp32 exponential noise, so it can't honor fp64.
92 self.use_xpu_sampler = (
93 not return_sampling_mask and not use_fp64_gumbel and xpu_sampler_supported()
94 )
95
96 def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
97 needs_processing = self.sampling_states.add_request(req_idx, sampling_params)
98 needs_processing |= self.thinking_budget_state.add_request(
99 req_idx, sampling_params
100 )
101 for processor in self.logits_processors:
102 needs_processing |= processor.add_request(req_idx, sampling_params)
103 self.needs_logits_processing[req_idx] = needs_processing
104
105 self.logprob_token_ids_state.add_request(req_idx, sampling_params)
106 if self.trace_replay_state is not None:
107 self.trace_replay_state.add_request(req_idx, sampling_params)
108
109 def apply_staged_writes(self) -> None:
110 self.sampling_states.apply_staged_writes()
111 for processor in self.logits_processors:
112 processor.apply_staged_writes()
113 self.thinking_budget_state.apply_staged_writes()
114 self.logprob_token_ids_state.apply_staged_writes()
115 if self.trace_replay_state is not None:
116 self.trace_replay_state.apply_staged_writes()
117
118 def get_logprobs_dims(
119 self, idx_mapping_np: np.ndarray, include_token_ids: bool = True
120 ) -> tuple[int, int] | None:
121 """(num_logprobs, max_per_req_token_ids) for the given requests, or
122 None when none of them want logprobs."""
123 max_num_logprobs = self.sampling_states.max_num_logprobs(idx_mapping_np)
124 max_token_ids = (
125 self.logprob_token_ids_state.max_num_token_ids(idx_mapping_np)
126 if include_token_ids
127 else 0
128 )
129 if max_num_logprobs == NO_LOGPROBS and max_token_ids == 0:
130 return None
131 num_logprobs = max_num_logprobs if max_num_logprobs != NO_LOGPROBS else 0
132 return num_logprobs, max_token_ids
133
134 def __call__(
135 self,
136 logits: torch.Tensor,
137 input_batch: InputBatch,
138 ) -> SamplerOutput:
139 expanded_idx_mapping = input_batch.expanded_idx_mapping
140 idx_mapping = input_batch.idx_mapping
141 idx_mapping_np = input_batch.idx_mapping_np
142 cu_num_logits_np = input_batch.cu_num_logits_np
143 expanded_local_pos = input_batch.expanded_local_pos
144 pos = input_batch.positions[input_batch.logits_indices]
145 input_ids = input_batch.input_ids[input_batch.logits_indices]
146 seq_lens_upper_bound_np = input_batch.seq_lens_cpu_upper_bound.numpy()
147
148 # NOTE(woosuk): We intentionally compute num_nans before sampling to make clear
149 # that num_nans is computed before applying penalties and temperature.
150 num_nans = get_num_nans(logits) if self.compute_nans else None
151
152 logprobs_dims = self.get_logprobs_dims(idx_mapping_np)
153
154 sampled, processed_logits = self.sample(
155 logits,
156 expanded_idx_mapping,
157 idx_mapping,
158 idx_mapping_np,
159 pos,
160 input_ids,
161 expanded_local_pos,
162 seq_lens_upper_bound_np,
163 return_logprobs=logprobs_dims is not None,
164 )
165
166 if self.trace_replay_state is not None:
167 # Overwrite sampled tokens with the replay trace up-front so that
168 # computed logprobs reflect the real distribution of the forced token.
169 self.trace_replay_state.apply_trace(sampled, idx_mapping)
170
171 if logprobs_dims is not None:
172 num_logprobs, max_per_req_token_ids = logprobs_dims
173 if self.logprobs_mode in PROCESSED_LOGPROBS_MODES:
174 logits = processed_logits
175 expanded_logits = logits.shape[0] != idx_mapping_np.shape[0]
176 cu_num_logits = cu_num_logits_np.tolist() if expanded_logits else None
177 logprobs_tensors = compute_topk_scores(
178 logits,
179 num_logprobs,
180 sampled,
181 cu_num_logits,
182 logprob_token_ids_state=self.logprob_token_ids_state,
183 expanded_idx_mapping=input_batch.expanded_idx_mapping,
184 max_per_req_token_ids=max_per_req_token_ids,
185 logits_mode=self.logprobs_mode in ("raw_logits", "processed_logits"),
186 )
187 else:
188 logprobs_tensors = None
189
190 # 1 sampled token per request, except chunked-prefill requests
191 # (seq_len < prefill_len) which aren't done prefilling and produce no
192 # output token. num_rejected is always 0 here (one logit per request).
193 num_sampled, num_rejected = get_num_sampled_and_rejected(
194 input_batch.seq_lens.new_ones(input_batch.num_reqs),
195 input_batch.seq_lens,
196 input_batch.cu_num_logits,
197 input_batch.idx_mapping,
198 self.req_states.prefill_len.gpu,
199 )
200
201 sampling_mask_tensors = None
202 if self.return_sampling_mask:
203 # Size by the validated top_k batch max; wider supports use the bitmask.
204 max_num_kept = int(np.max(self.sampling_states.top_k.np[idx_mapping_np]))
205 sampling_mask_tensors = SamplingMaskTensors.from_logits(
206 processed_logits,
207 input_batch.cu_num_logits,
208 num_sampled,
209 max_num_kept,
210 )
211
212 # These are GPU tensors.
213 sampler_output = SamplerOutput(
214 # The sampled tokens are expanded to 2D tensor with shape
215 # [num_requests, 1], where each row represents one generated
216 # token per request.
217 sampled_token_ids=sampled.view(-1, 1),
218 logprobs_tensors=logprobs_tensors,
219 num_nans=num_nans,
220 num_sampled=num_sampled,
221 num_rejected=num_rejected,
222 sampling_mask_tensors=sampling_mask_tensors,
223 )
224 return sampler_output
225
226 def apply_sampling_params(
227 self,
228 logits: torch.Tensor,
229 expanded_idx_mapping: torch.Tensor,
230 idx_mapping: torch.Tensor,
231 idx_mapping_np: np.ndarray,
232 pos: torch.Tensor,
233 input_ids: torch.Tensor,
234 expanded_local_pos: torch.Tensor,
235 seq_lens_upper_bound_np: np.ndarray,
236 skip_top_k_top_p: bool = False,
237 ) -> torch.Tensor:
238 if not np.any(self.needs_logits_processing[idx_mapping_np]):
239 return logits
240
241 # Copy logits to a new FP32 tensor.
242 logits = torch.empty_like(logits, dtype=torch.float32).copy_(logits)
243
244 ctx = LogitsContext(
245 expanded_idx_mapping=expanded_idx_mapping,
246 idx_mapping=idx_mapping,
247 idx_mapping_np=idx_mapping_np,
248 expanded_local_pos=expanded_local_pos,
249 input_ids=input_ids,
250 pos=pos,
251 seq_lens_upper_bound_np=seq_lens_upper_bound_np,
252 )
253
254 # Apply logits processors (native + any custom).
255 for processor in self.logits_processors:
256 logits = processor.apply(logits, ctx)
257
258 # Forcing runs last so no stage can overwrite the forced end marker
259 # or weaken it by scaling.
260 self.thinking_budget_state.apply(logits, ctx)
261
262 # Apply temperature in place.
263 self.sampling_states.apply_temperature(
264 logits, expanded_idx_mapping, idx_mapping_np
265 )
266
267 # Apply min_p in place.
268 self.sampling_states.apply_min_p(logits, expanded_idx_mapping, idx_mapping_np)
269
270 if skip_top_k_top_p:
271 return logits
272
273 # Apply top_k and/or top_p. This might or might not return a new tensor.
274 return self.sampling_states.apply_top_k_top_p(
275 logits, expanded_idx_mapping, idx_mapping_np
276 )
277
278 def sample(
279 self,
280 logits: torch.Tensor,
281 expanded_idx_mapping: torch.Tensor,
282 idx_mapping: torch.Tensor,
283 idx_mapping_np: np.ndarray,
284 pos: torch.Tensor,
285 input_ids: torch.Tensor,
286 expanded_local_pos: torch.Tensor,
287 seq_lens_upper_bound_np: np.ndarray,
288 return_logprobs: bool = False,
289 ) -> tuple[torch.Tensor, torch.Tensor]:
290 processed_logits = self.apply_sampling_params(
291 logits,
292 expanded_idx_mapping,
293 idx_mapping,
294 idx_mapping_np,
295 pos,
296 input_ids,
297 expanded_local_pos,
298 seq_lens_upper_bound_np,
299 skip_top_k_top_p=True,
300 )
301 top_k, top_p = self.sampling_states.get_top_k_top_p(
302 expanded_idx_mapping, idx_mapping_np
303 )
304 # Don't use a fused sampler if no requests use top_k/top_p, if there are
305 # any greedy requests or per-request seeds, or if post-processed
306 # logprobs need to be returned for any requests.
307 fused_sampler_eligible = not (
308 (top_k is None and top_p is None)
309 or (return_logprobs and self.logprobs_mode in PROCESSED_LOGPROBS_MODES)
310 or self.sampling_states.any_greedy(idx_mapping_np)
311 or self.sampling_states.any_explicit_seed(idx_mapping_np)
312 )
313 use_fused_sampler = (
314 self.use_flashinfer or self.use_xpu_sampler
315 ) and fused_sampler_eligible
316
317 return self._sample_random(
318 processed_logits,
319 expanded_idx_mapping,
320 idx_mapping_np,
321 pos,
322 top_k,
323 top_p,
324 use_fused_sampler,
325 )
326
327 def _sample_random(
328 self,
329 processed_logits: torch.Tensor,
330 expanded_idx_mapping: torch.Tensor,
331 idx_mapping_np: np.ndarray,
332 pos: torch.Tensor,
333 top_k: torch.Tensor | None,
334 top_p: torch.Tensor | None,
335 use_fused_sampler: bool,
336 ) -> tuple[torch.Tensor, torch.Tensor]:
337 if use_fused_sampler:
338 if self.use_flashinfer:
339 sampled = flashinfer_sample(processed_logits, top_k, top_p).to(
340 torch.int64
341 )
342 else: # Use XPU sampler
343 sampled, _ = xpu_sample(processed_logits, top_k, top_p)
344 else:
345 processed_logits = apply_top_k_top_p(processed_logits, top_k, top_p)
346 sampled = gumbel_sample(
347 processed_logits,
348 expanded_idx_mapping,
349 self.sampling_states.temperature.gpu,
350 self.sampling_states.seeds.gpu,
351 pos,
352 apply_temperature=False,
353 is_drafting=False,
354 use_fp64=self.use_fp64_gumbel,
355 )
356 return sampled, processed_logits