rejection_sampler.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from collections.abc import Iterable, Iterator
  4. from typing import Any
  5. import numpy as np
  6. import torch
  7. from vllm.v1.worker.gpu.spec_decode import draft_trunc
  8. from vllm.config import SpeculativeConfig
  9. from vllm.config.model import PROCESSED_LOGPROBS_MODES
  10. from vllm.triton_utils import tl, triton
  11. from vllm.v1.outputs import LogprobsTensors
  12. from vllm.v1.spec_decode.utils import unconditional_to_conditional_rates
  13. from vllm.v1.watermarking.gpu_sampler import GPUWatermarkSampler
  14. from vllm.v1.worker.gpu.input_batch import (
  15. InputBatch,
  16. get_num_sampled_and_rejected,
  17. )
  18. from vllm.v1.worker.gpu.metrics.logits import get_num_nans
  19. from vllm.v1.worker.gpu.sample.logprob import compute_topk_scores
  20. from vllm.v1.worker.gpu.sample.output import SamplerOutput
  21. from vllm.v1.worker.gpu.sample.sampler import Sampler
  22. from vllm.v1.worker.gpu.sample.states import NO_LOGPROBS
  23. from vllm.v1.worker.gpu.spec_decode.rejection_sampler_utils import (
  24. rejection_sample,
  25. )
  26. # Cap on the FP32 target-logits buffer materialized by apply_sampling_params.
  27. # TODO(mgoin): Chunking is a workaround. The rejection kernels already upcast
  28. # per vocab block on load and apply ops like temperature and gumbel, so folding
  29. # sampling-param application into those kernels would remove this buffer and
  30. # its traffic entirely.
  31. MAX_CHUNK_BYTES = 2**30 # 1GB
  32. _FP32_BYTES = 4
  33. def get_max_chunk_logits(vocab_size: int) -> int:
  34. """Largest number of logits rows one verification chunk may hold."""
  35. return max(1, MAX_CHUNK_BYTES // (vocab_size * _FP32_BYTES))
  36. def _iter_request_chunks(
  37. cu_num_logits: np.ndarray, max_chunk_logits: int
  38. ) -> Iterator[tuple[int, int]]:
  39. """Yield maximally packed request ranges without splitting requests."""
  40. assert max_chunk_logits > 0
  41. num_reqs = cu_num_logits.size - 1
  42. start = 0
  43. while start < num_reqs:
  44. max_logit = int(cu_num_logits[start]) + max_chunk_logits
  45. end = int(np.searchsorted(cu_num_logits, max_logit, side="right") - 1)
  46. end = min(num_reqs, max(start + 1, end))
  47. yield start, end
  48. start = end
  49. @triton.jit
  50. def _flatten_sampled_kernel(
  51. # [num_logits]
  52. flat_sampled_ptr,
  53. # [num_reqs, num_speculative_steps + 1]
  54. sampled_ptr,
  55. sampled_stride,
  56. # [num_reqs]
  57. num_sampled_ptr,
  58. # [num_reqs + 1]
  59. cu_num_logits_ptr,
  60. ):
  61. req_idx = tl.program_id(0)
  62. start_idx = tl.load(cu_num_logits_ptr + req_idx)
  63. num_sampled = tl.load(num_sampled_ptr + req_idx)
  64. for i in range(num_sampled):
  65. token_id = tl.load(sampled_ptr + req_idx * sampled_stride + i)
  66. tl.store(flat_sampled_ptr + start_idx + i, token_id)
  67. @triton.jit
  68. def _gather_draft_sampled_kernel(
  69. # [num_logits]
  70. draft_sampled_ptr,
  71. # [num_logits]
  72. pos_ptr,
  73. # [num_tokens]
  74. input_ids_ptr,
  75. # [num_tokens]
  76. positions_ptr,
  77. # [num_logits]
  78. logits_indices_ptr,
  79. # [num_logits]
  80. expanded_idx_mapping_ptr,
  81. # [num_logits]
  82. expanded_local_pos_ptr,
  83. # [max_num_reqs]
  84. prefill_len_ptr,
  85. num_logits,
  86. BLOCK_SIZE: tl.constexpr,
  87. ):
  88. block = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  89. mask = block < num_logits
  90. token_idx = tl.load(logits_indices_ptr + block, mask=mask)
  91. token_id = tl.load(input_ids_ptr + token_idx, mask=mask)
  92. pos = tl.load(positions_ptr + token_idx, mask=mask)
  93. req_state_idx = tl.load(expanded_idx_mapping_ptr + block, mask=mask)
  94. local_pos = tl.load(expanded_local_pos_ptr + block, mask=mask)
  95. prefill_len = tl.load(prefill_len_ptr + req_state_idx, mask=mask)
  96. # Drafts are only proposed after sampling past the prefill, so any draft
  97. # slots in a step starting within the prefill are placeholders (e.g. the
  98. # padded first step after a P/D remote KV load).
  99. is_placeholder = (local_pos > 0) & (pos - local_pos < prefill_len)
  100. token_id = tl.where(is_placeholder, -1, token_id)
  101. tl.store(draft_sampled_ptr + block, token_id, mask=mask)
  102. tl.store(pos_ptr + block, pos, mask=mask)
  103. def gather_draft_sampled(
  104. input_ids: torch.Tensor,
  105. positions: torch.Tensor,
  106. logits_indices: torch.Tensor,
  107. expanded_idx_mapping: torch.Tensor,
  108. expanded_local_pos: torch.Tensor,
  109. prefill_len: torch.Tensor,
  110. ) -> tuple[torch.Tensor, torch.Tensor]:
  111. """Gather the input token and position of each logits row.
  112. Draft rows of requests that have not yet sampled past their prefill are
  113. set to -1 so that the rejection kernels reject them.
  114. """
  115. num_logits = logits_indices.shape[0]
  116. draft_sampled = input_ids.new_empty(num_logits)
  117. pos = positions.new_empty(num_logits)
  118. BLOCK_SIZE = 1024
  119. _gather_draft_sampled_kernel[(triton.cdiv(num_logits, BLOCK_SIZE),)](
  120. draft_sampled,
  121. pos,
  122. input_ids,
  123. positions,
  124. logits_indices,
  125. expanded_idx_mapping,
  126. expanded_local_pos,
  127. prefill_len,
  128. num_logits,
  129. BLOCK_SIZE=BLOCK_SIZE,
  130. )
  131. return draft_sampled, pos
  132. class RejectionSampler:
  133. def __init__(
  134. self,
  135. sampler: Sampler,
  136. spec_config: SpeculativeConfig,
  137. device: torch.device,
  138. *,
  139. watermark_key: int | None = None,
  140. ):
  141. self.sampler = sampler
  142. self.watermark_key = watermark_key
  143. if watermark_key is not None:
  144. assert isinstance(sampler, GPUWatermarkSampler)
  145. self.num_speculative_steps = spec_config.num_speculative_tokens
  146. self.enable_adaptive_verification = spec_config.enable_adaptive_verification
  147. rejection_sample_method = spec_config.rejection_sample_method
  148. self.use_block_verification: bool = False
  149. self.synthetic_conditional_rates: torch.Tensor | None = None
  150. if rejection_sample_method == "synthetic":
  151. assert spec_config.synthetic_acceptance_rates is not None
  152. self.synthetic_conditional_rates = torch.tensor(
  153. unconditional_to_conditional_rates(
  154. spec_config.synthetic_acceptance_rates
  155. ),
  156. dtype=torch.float32,
  157. device=device,
  158. )
  159. elif rejection_sample_method == "block":
  160. self.use_block_verification = True
  161. def _get_logprobs_tensors(
  162. self,
  163. sampled: torch.Tensor,
  164. num_sampled: torch.Tensor,
  165. logits: torch.Tensor,
  166. cu_num_logits: torch.Tensor,
  167. cu_num_logits_np: np.ndarray,
  168. max_num_logprobs: int,
  169. ) -> LogprobsTensors | None:
  170. if max_num_logprobs == NO_LOGPROBS:
  171. return None
  172. num_reqs = cu_num_logits.shape[0] - 1
  173. num_logits = logits.shape[0]
  174. flat_sampled = torch.zeros(
  175. num_logits, dtype=sampled.dtype, device=sampled.device
  176. )
  177. _flatten_sampled_kernel[(num_reqs,)](
  178. flat_sampled,
  179. sampled,
  180. sampled.stride(0),
  181. num_sampled,
  182. cu_num_logits,
  183. num_warps=1,
  184. )
  185. expanded_logits = num_logits != num_reqs
  186. cu_num_generated_tokens: list[int] | torch.Tensor | None = None
  187. if expanded_logits:
  188. if self.enable_adaptive_verification:
  189. # Adaptive verification keeps the true per-request boundaries
  190. # on device only; cu_num_logits_np holds the pre-compacted layout.
  191. cu_num_generated_tokens = cu_num_logits.clone()
  192. else:
  193. cu_num_generated_tokens = cu_num_logits_np.tolist()
  194. return compute_topk_scores(
  195. logits,
  196. max_num_logprobs,
  197. flat_sampled,
  198. cu_num_generated_tokens,
  199. logits_mode=self.sampler.logprobs_mode
  200. in ("raw_logits", "processed_logits"),
  201. )
  202. def _watermarking_kwargs(
  203. self,
  204. draft_sampled: torch.Tensor,
  205. expanded_idx_mapping: torch.Tensor,
  206. expanded_local_pos: torch.Tensor,
  207. ) -> dict[str, Any]:
  208. if self.watermark_key is None:
  209. return {}
  210. assert isinstance(self.sampler, GPUWatermarkSampler)
  211. return {
  212. "contexts": self.sampler._get_contexts(
  213. expanded_idx_mapping, expanded_local_pos, draft_sampled
  214. ),
  215. "watermarking": self.sampler.watermarking.gpu,
  216. "watermark_key": self.watermark_key,
  217. }
  218. def _verify(
  219. self,
  220. logits: torch.Tensor,
  221. draft_logits: torch.Tensor | None,
  222. draft_sampled: torch.Tensor,
  223. accept_drafts: torch.Tensor,
  224. pos: torch.Tensor,
  225. cu_num_logits: torch.Tensor,
  226. idx_mapping: torch.Tensor,
  227. idx_mapping_np: np.ndarray,
  228. expanded_idx_mapping: torch.Tensor,
  229. expanded_local_pos: torch.Tensor,
  230. seq_lens_upper_bound_np: np.ndarray,
  231. ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
  232. processed_logits = self.sampler.apply_sampling_params(
  233. logits,
  234. expanded_idx_mapping,
  235. idx_mapping,
  236. idx_mapping_np,
  237. pos,
  238. draft_sampled,
  239. expanded_local_pos,
  240. seq_lens_upper_bound_np,
  241. )
  242. sampled, num_sampled = rejection_sample(
  243. processed_logits,
  244. draft_logits,
  245. accept_drafts,
  246. cu_num_logits,
  247. pos,
  248. idx_mapping,
  249. expanded_idx_mapping,
  250. expanded_local_pos,
  251. self.sampler.sampling_states.temperature.gpu,
  252. self.sampler.sampling_states.seeds.gpu,
  253. self.num_speculative_steps,
  254. self.synthetic_conditional_rates,
  255. use_fp64=self.sampler.use_fp64_gumbel,
  256. use_block_verification=self.use_block_verification,
  257. **self._watermarking_kwargs(
  258. draft_sampled, expanded_idx_mapping, expanded_local_pos
  259. ),
  260. )
  261. return processed_logits, sampled, num_sampled
  262. def _verify_in_chunks(
  263. self,
  264. logits: torch.Tensor,
  265. input_batch: InputBatch,
  266. draft_logits: torch.Tensor | None,
  267. draft_sampled: torch.Tensor,
  268. pos: torch.Tensor,
  269. max_chunk_logits: int,
  270. max_num_logprobs: int,
  271. accept_drafts: torch.Tensor | None = None,
  272. ) -> tuple[torch.Tensor, torch.Tensor, LogprobsTensors | None]:
  273. cu_num_logits_np = input_batch.cu_num_logits_np
  274. use_processed_logits = self.sampler.logprobs_mode in PROCESSED_LOGPROBS_MODES
  275. num_reqs = input_batch.num_reqs
  276. if logits.shape[0] <= max_chunk_logits:
  277. # One chunk covers the batch. Adaptive verification compacts the logits
  278. # without updating cu_num_logits_np (it keeps the pre-compacted layout),
  279. # so the stale sums must not pick chunk boundaries; its budget cap
  280. # guarantees the compacted batch always lands here.
  281. request_chunks: Iterable[tuple[int, int]] = ((0, num_reqs),)
  282. else:
  283. assert not self.enable_adaptive_verification
  284. request_chunks = _iter_request_chunks(cu_num_logits_np, max_chunk_logits)
  285. sampled_chunks: list[torch.Tensor] = []
  286. num_sampled_chunks: list[torch.Tensor] = []
  287. logprobs_chunks: list[LogprobsTensors] = []
  288. for start, end in request_chunks:
  289. lo = int(cu_num_logits_np[start])
  290. hi = int(cu_num_logits_np[end])
  291. chunk_cu_num_logits_np = cu_num_logits_np[start : end + 1] - lo
  292. chunk_cu_num_logits = input_batch.cu_num_logits[start : end + 1] - lo
  293. # draft_logits uses persistent request-state indices and stays global.
  294. processed_logits, sampled, num_sampled = self._verify(
  295. logits[lo:hi],
  296. draft_logits,
  297. draft_sampled[lo:hi],
  298. (draft_sampled if accept_drafts is None else accept_drafts)[lo:hi],
  299. pos[lo:hi],
  300. chunk_cu_num_logits,
  301. input_batch.idx_mapping[start:end],
  302. input_batch.idx_mapping_np[start:end],
  303. input_batch.expanded_idx_mapping[lo:hi],
  304. input_batch.expanded_local_pos[lo:hi],
  305. input_batch.seq_lens_cpu_upper_bound.numpy()[start:end],
  306. )
  307. chunk_logprobs = self._get_logprobs_tensors(
  308. sampled,
  309. num_sampled,
  310. processed_logits if use_processed_logits else logits[lo:hi],
  311. chunk_cu_num_logits,
  312. chunk_cu_num_logits_np,
  313. max_num_logprobs,
  314. )
  315. if chunk_logprobs is not None:
  316. logprobs_chunks.append(chunk_logprobs)
  317. del processed_logits
  318. sampled_chunks.append(sampled)
  319. num_sampled_chunks.append(num_sampled)
  320. if len(sampled_chunks) == 1:
  321. logprobs_tensors = logprobs_chunks[0] if logprobs_chunks else None
  322. return sampled_chunks[0], num_sampled_chunks[0], logprobs_tensors
  323. logprobs_tensors = None
  324. if logprobs_chunks:
  325. expanded_logits = logits.shape[0] != input_batch.num_reqs
  326. logprobs_tensors = LogprobsTensors.cat(
  327. logprobs_chunks,
  328. cu_num_generated_tokens=(
  329. cu_num_logits_np.tolist() if expanded_logits else None
  330. ),
  331. )
  332. sampled = torch.cat(sampled_chunks)
  333. num_sampled = torch.cat(num_sampled_chunks)
  334. return sampled, num_sampled, logprobs_tensors
  335. def __call__(
  336. self,
  337. logits: torch.Tensor,
  338. input_batch: InputBatch,
  339. draft_logits: torch.Tensor | None = None,
  340. ) -> SamplerOutput:
  341. # NOTE(woosuk): We intentionally compute num_nans before sampling to make clear
  342. # that num_nans is computed before applying penalties and temperature.
  343. num_nans = get_num_nans(logits) if self.sampler.compute_nans else None
  344. draft_sampled, pos = gather_draft_sampled(
  345. input_batch.input_ids,
  346. input_batch.positions,
  347. input_batch.logits_indices,
  348. input_batch.expanded_idx_mapping,
  349. input_batch.expanded_local_pos,
  350. self.sampler.req_states.prefill_len.gpu,
  351. )
  352. # A dead draft (draft_trunc) is -1 for acceptance only, so it never matches.
  353. accept_drafts = draft_sampled
  354. if draft_trunc.ENABLED:
  355. accept_drafts = draft_sampled.masked_fill(input_batch.is_padding[input_batch.logits_indices], -1)
  356. max_num_logprobs = self.sampler.sampling_states.max_num_logprobs(
  357. input_batch.idx_mapping_np
  358. )
  359. chunk_logit_limit = get_max_chunk_logits(logits.shape[1])
  360. sampled, num_sampled, logprobs_tensors = self._verify_in_chunks(
  361. logits,
  362. input_batch,
  363. draft_logits,
  364. draft_sampled,
  365. pos,
  366. chunk_logit_limit,
  367. max_num_logprobs,
  368. accept_drafts=accept_drafts,
  369. )
  370. num_sampled, num_rejected = get_num_sampled_and_rejected(
  371. num_sampled,
  372. input_batch.seq_lens,
  373. input_batch.cu_num_logits,
  374. input_batch.idx_mapping,
  375. self.sampler.req_states.prefill_len.gpu,
  376. )
  377. return SamplerOutput(
  378. sampled_token_ids=sampled,
  379. logprobs_tensors=logprobs_tensors,
  380. num_nans=num_nans,
  381. num_sampled=num_sampled,
  382. num_rejected=num_rejected,
  383. )