| 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 |