A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 6, 20:30 UTC
[Model Runner V2] Add probabilistic rejection sampling for spec decoding (#35461)
Signed-off-by: Giancarlo Delfin <gdelfin@inferact.ai>
Giancarlo Delfin committed
Mar 11, 2026 at 14:04 UTC
c77181e534597f7347fc03b7d26600fb3cea9981
9 files changed
+494
-112
vllm/config/speculative.py
+10
@@ -57,6 +57,10 @@ SpeculativeMethod = Literal[
57
EagleModelTypes,
58
NgramGPUTypes,
59
]
60
+RejectionSampleMethod = Literal[
61
+ "strict",
62
+ "probabilistic",
63
+]
64
65
66
@config
@@ -171,6 +175,12 @@ class SpeculativeConfig:
175
"""Load config for the draft model. If not specified, will use the load
176
config from the target model."""
177
178
+ rejection_sample_method: RejectionSampleMethod = "strict"
179
+ """Whether to use strict (target and draft sampled tokens match exactly)
180
+ or probabilistic rejection sampling. Both respect the target model
181
+ distribution, but the latter yields a higher acceptance rate at the cost
182
+ of more memory to cache draft logits."""
183
+
184
def compute_hash(self) -> str:
185
"""
186
WARNING: Whenever a new field is added to this config,
vllm/v1/worker/gpu/model_runner.py
+30
-34
@@ -90,7 +90,7 @@ from vllm.v1.worker.gpu.spec_decode import init_speculator
90
from vllm.v1.worker.gpu.spec_decode.eagle.eagle3_utils import (
91
set_eagle3_aux_hidden_state_layers,
92
)
93
-from vllm.v1.worker.gpu.spec_decode.rejection_sample import rejection_sample
93
+from vllm.v1.worker.gpu.spec_decode.rejection_sampler import RejectionSampler
94
from vllm.v1.worker.gpu.spec_decode.utils import DraftTokensHandler
95
from vllm.v1.worker.gpu.states import RequestState
96
from vllm.v1.worker.gpu.structured_outputs import StructuredOutputsWorker
@@ -162,6 +162,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
162
self.speculator = None
163
self.num_speculative_steps = 0
164
self.use_aux_hidden_state_outputs = False
165
+ use_strict_rejection_sampling = False
166
if self.speculative_config is not None:
167
self.num_speculative_steps = self.speculative_config.num_speculative_tokens
168
if self.is_last_pp_rank:
@@ -172,6 +173,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
173
self.use_aux_hidden_state_outputs = True
174
if self.pp_size > 1:
175
raise ValueError("EAGLE3 with pipeline parallel is not supported.")
176
+ use_strict_rejection_sampling = (
177
+ self.speculative_config.rejection_sample_method == "strict"
178
+ )
179
180
# Draft tokens propagation - for spec-dec + struct outputs.
181
self.draft_tokens_handler = DraftTokensHandler(self.device)
@@ -183,6 +187,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
187
num_speculative_steps=self.num_speculative_steps,
188
vocab_size=self.vocab_size,
189
device=self.device,
190
+ model_dtype=self.dtype,
191
+ cache_draft_logits=not use_strict_rejection_sampling,
192
)
193
self.input_buffers = InputBuffers(
194
max_num_reqs=self.max_num_reqs,
@@ -197,6 +203,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
203
logprobs_mode=self.model_config.logprobs_mode,
204
num_speculative_tokens=self.num_speculative_steps + 1,
205
)
206
+ self.rejection_sampler = RejectionSampler(
207
+ self.sampler,
208
+ num_speculative_steps=self.num_speculative_steps,
209
+ use_strict_rejection_sampling=use_strict_rejection_sampling,
210
+ )
211
self.prompt_logprobs_worker = PromptLogprobsWorker(self.max_num_reqs)
212
213
# CUDA graphs.
@@ -412,6 +423,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
423
next_prefill_tokens=self.req_states.next_prefill_tokens,
424
temperature=self.sampler.sampling_states.temperature.gpu,
425
seeds=self.sampler.sampling_states.seeds.gpu,
426
+ draft_logits_out=self.req_states.draft_logits,
427
num_tokens_across_dp=num_tokens_across_dp,
428
dummy_run=True,
429
skip_attn_for_dummy_run=skip_attn,
@@ -425,24 +437,16 @@ class GPUModelRunner(LoRAModelRunnerMixin):
437
def _dummy_sampler_run(self, hidden_states: torch.Tensor) -> None:
438
num_reqs = hidden_states.shape[0]
439
logits = self.model.compute_logits(hidden_states)
428
- idx_mapping = torch.arange(num_reqs, dtype=torch.int32, device=self.device)
429
- idx_mapping_np = np.arange(num_reqs, dtype=np.int32)
430
- pos = torch.zeros(num_reqs, dtype=torch.int64, device=self.device)
431
- dummy_input_ids = torch.zeros(num_reqs, dtype=torch.int32, device=self.device)
432
- expanded_local_pos = torch.zeros(
433
- num_reqs, dtype=torch.int32, device=self.device
440
+ dummy_input_batch = InputBatch.make_dummy(
441
+ num_reqs, num_reqs, self.input_buffers
442
)
443
+
444
# NOTE(woosuk): During the initial memory profiling, the sampler may skip
445
# top_k, top_p, and logprobs, using less GPU memory than what is possible
446
# during actual execution.
447
self.sampler(
448
logits,
440
- idx_mapping,
441
- idx_mapping_np,
442
- idx_mapping_np,
443
- pos,
444
- dummy_input_ids,
445
- expanded_local_pos,
449
+ dummy_input_batch,
450
)
451
452
@torch.inference_mode()
@@ -768,8 +772,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
772
grammar_output: GrammarOutput | None,
773
) -> tuple[SamplerOutput, torch.Tensor, torch.Tensor]:
774
sample_hidden_states = hidden_states[input_batch.logits_indices]
771
- sample_pos = input_batch.positions[input_batch.logits_indices]
772
- input_ids = input_batch.input_ids[input_batch.logits_indices]
775
logits = self.model.compute_logits(sample_hidden_states)
776
if grammar_output is not None:
777
# Apply grammar bitmask to the logits in-place.
@@ -780,34 +782,27 @@ class GPUModelRunner(LoRAModelRunnerMixin):
782
grammar_output.grammar_bitmask,
783
)
784
783
- # Sample tokens and compute logprobs (if needed).
784
- sampler_output = self.sampler(
785
- logits,
786
- input_batch.expanded_idx_mapping,
787
- input_batch.idx_mapping_np,
788
- input_batch.cu_num_logits_np,
789
- sample_pos,
790
- input_ids,
791
- input_batch.expanded_local_pos,
792
- )
793
-
785
if input_batch.num_draft_tokens == 0:
786
# No draft tokens (common case).
796
- num_sampled = input_batch.seq_lens.new_ones(input_batch.num_reqs)
787
+ sampler_output = self.sampler(
788
+ logits,
789
+ input_batch,
790
+ )
791
else:
792
# Rejection sampling for spec decoding.
799
- sampled_tokens, num_sampled = rejection_sample(
800
- sampler_output.sampled_token_ids,
801
- input_ids,
802
- input_batch.cu_num_logits,
803
- self.num_speculative_steps,
793
+ sampler_output = self.rejection_sampler(
794
+ logits,
795
+ input_batch,
796
+ # Draft logits are needed for probabilistic rejection sampling.
797
+ self.req_states.draft_logits[input_batch.idx_mapping]
798
+ if self.req_states.draft_logits is not None
799
+ else None,
800
)
805
- sampler_output.sampled_token_ids = sampled_tokens
801
802
# Get the number of sampled and rejected tokens.
803
# For chunked prefills, num_sampled and num_rejected are both 0.
804
num_sampled, num_rejected = get_num_sampled_and_rejected(
810
- num_sampled,
805
+ sampler_output.num_sampled,
806
input_batch.seq_lens,
807
input_batch.cu_num_logits,
808
input_batch.idx_mapping,
@@ -1105,6 +1100,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1100
self.req_states.next_prefill_tokens,
1101
self.sampler.sampling_states.temperature.gpu,
1102
self.sampler.sampling_states.seeds.gpu,
1103
+ self.req_states.draft_logits,
1104
num_tokens_across_dp=num_tokens_across_dp,
1105
)
1106
self.req_states.draft_tokens[input_batch.idx_mapping] = draft_tokens
vllm/v1/worker/gpu/sample/gumbel.py
+19
-6
@@ -55,6 +55,8 @@ def _gumbel_sample_kernel(
55
local_argmax_stride,
56
local_max_ptr,
57
local_max_stride,
58
+ processed_logits_ptr,
59
+ processed_logits_stride,
60
logits_ptr,
61
logits_stride,
62
expanded_idx_mapping_ptr,
@@ -79,6 +81,20 @@ def _gumbel_sample_kernel(
81
logits = logits.to(tl.float32)
82
83
temp = tl.load(temp_ptr + req_state_idx).to(tl.float32)
84
+ if (temp != 0.0) and APPLY_TEMPERATURE:
85
+ # Apply temperature.
86
+ # NOTE(woosuk): Match the behavior of _temperature_kernel.
87
+ # E.g., if the kernel uses tl.div_rn, we should use tl.div_rn here too.
88
+ logits = logits / temp
89
+
90
+ # Store the temperature-applied logits.
91
+ if processed_logits_ptr is not None:
92
+ tl.store(
93
+ processed_logits_ptr + req_state_idx * processed_logits_stride + block,
94
+ logits,
95
+ mask=mask,
96
+ )
97
+
98
if temp != 0.0:
99
# Calculate the seed for gumbel noise.
100
seed = tl.load(seeds_ptr + req_state_idx)
@@ -90,12 +106,6 @@ def _gumbel_sample_kernel(
106
u = tl.maximum(u, 1e-7)
107
gumbel_noise = -tl.log(-tl.log(u))
108
93
- # Apply temperature.
94
- if APPLY_TEMPERATURE:
95
- # NOTE(woosuk): Match the behavior of _temperature_kernel.
96
- # E.g., if the kernel uses tl.div_rn, we should use tl.div_rn here too.
97
- logits = logits / temp
98
-
109
# Apply gumbel noise.
110
logits = tl.where(mask, logits + gumbel_noise, float("-inf"))
111
@@ -112,6 +122,7 @@ def gumbel_sample(
122
seed: torch.Tensor, # [max_num_reqs]
123
pos: torch.Tensor, # [num_tokens]
124
apply_temperature: bool,
125
+ processed_logits_out: torch.Tensor | None = None, # [num_reqs, vocab_size]
126
) -> torch.Tensor:
127
num_tokens, vocab_size = logits.shape
128
BLOCK_SIZE = 1024
@@ -133,6 +144,8 @@ def gumbel_sample(
144
local_argmax.stride(0),
145
local_max,
146
local_max.stride(0),
147
+ processed_logits_out,
148
+ processed_logits_out.stride(0) if processed_logits_out is not None else 0,
149
logits,
150
logits.stride(0),
151
expanded_idx_mapping,
vllm/v1/worker/gpu/sample/output.py
+1
@@ -12,3 +12,4 @@ class SamplerOutput:
12
sampled_token_ids: torch.Tensor
13
logprobs_tensors: LogprobsTensors | None
14
num_nans: torch.Tensor | None
15
+ num_sampled: torch.Tensor | None
vllm/v1/worker/gpu/sample/sampler.py
+32
-10
@@ -7,6 +7,7 @@ import torch
7
import vllm.envs as envs
8
from vllm.config.model import LogprobsMode
9
from vllm.sampling_params import SamplingParams
10
+from vllm.v1.worker.gpu.input_batch import InputBatch
11
from vllm.v1.worker.gpu.metrics.logits import get_num_nans
12
from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
13
from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
@@ -56,13 +57,15 @@ class Sampler:
57
def __call__(
58
self,
59
logits: torch.Tensor,
59
- expanded_idx_mapping: torch.Tensor,
60
- idx_mapping_np: np.ndarray,
61
- cu_num_logits_np: np.ndarray,
62
- pos: torch.Tensor,
63
- input_ids: torch.Tensor,
64
- expanded_local_pos: torch.Tensor,
60
+ input_batch: InputBatch,
61
) -> SamplerOutput:
62
+ expanded_idx_mapping = input_batch.expanded_idx_mapping
63
+ idx_mapping_np = input_batch.idx_mapping_np
64
+ cu_num_logits_np = input_batch.cu_num_logits_np
65
+ expanded_local_pos = input_batch.expanded_local_pos
66
+ pos = input_batch.positions[input_batch.logits_indices]
67
+ input_ids = input_batch.input_ids[input_batch.logits_indices]
68
+
69
# NOTE(woosuk): We intentionally compute num_nans before sampling to make clear
70
# that num_nans is computed before applying penalties and temperature.
71
num_nans = get_num_nans(logits) if self.compute_nans else None
@@ -95,10 +98,11 @@ class Sampler:
98
sampled_token_ids=sampled.view(-1, 1),
99
logprobs_tensors=logprobs_tensors,
100
num_nans=num_nans,
101
+ num_sampled=input_batch.seq_lens.new_ones(input_batch.num_reqs),
102
)
103
return sampler_output
104
101
- def sample(
105
+ def apply_sampling_params(
106
self,
107
logits: torch.Tensor,
108
expanded_idx_mapping: torch.Tensor,
@@ -106,7 +110,7 @@ class Sampler:
110
pos: torch.Tensor,
111
input_ids: torch.Tensor,
112
expanded_local_pos: torch.Tensor,
109
- ) -> tuple[torch.Tensor, torch.Tensor]:
113
+ ) -> torch.Tensor:
114
# Copy logits to a new FP32 tensor.
115
logits = torch.empty_like(logits, dtype=torch.float32).copy_(logits)
116
@@ -143,13 +147,31 @@ class Sampler:
147
self.sampling_states.apply_min_p(logits, expanded_idx_mapping, idx_mapping_np)
148
149
# Apply top_k and/or top_p. This might or might not return a new tensor.
146
- logits = self.sampling_states.apply_top_k_top_p(
150
+ return self.sampling_states.apply_top_k_top_p(
151
logits, expanded_idx_mapping, idx_mapping_np
152
)
153
154
+ def sample(
155
+ self,
156
+ logits: torch.Tensor,
157
+ expanded_idx_mapping: torch.Tensor,
158
+ idx_mapping_np: np.ndarray,
159
+ pos: torch.Tensor,
160
+ input_ids: torch.Tensor,
161
+ expanded_local_pos: torch.Tensor,
162
+ ) -> tuple[torch.Tensor, torch.Tensor]:
163
+ processed_logits = self.apply_sampling_params(
164
+ logits,
165
+ expanded_idx_mapping,
166
+ idx_mapping_np,
167
+ pos,
168
+ input_ids,
169
+ expanded_local_pos,
170
+ )
171
+
172
# Sample the next token.
173
sampled = gumbel_sample(
152
- logits,
174
+ processed_logits,
175
expanded_idx_mapping,
176
self.sampling_states.temperature.gpu,
177
self.sampling_states.seeds.gpu,
vllm/v1/worker/gpu/spec_decode/eagle/speculator.py
+12
@@ -140,6 +140,7 @@ class EagleSpeculator:
140
slot_mappings: dict[str, torch.Tensor] | None,
141
num_tokens_across_dp: torch.Tensor | None,
142
cudagraph_runtime_mode: CUDAGraphMode = CUDAGraphMode.NONE,
143
+ draft_logits_out: torch.Tensor | None = None,
144
) -> None:
145
pos = self.input_buffers.positions[:num_reqs]
146
query_start_loc = self.input_buffers.query_start_loc[: num_reqs + 1]
@@ -166,6 +167,9 @@ class EagleSpeculator:
167
self.seeds,
168
pos + 1,
169
apply_temperature=True,
170
+ processed_logits_out=draft_logits_out[:, step]
171
+ if draft_logits_out is not None
172
+ else None,
173
)
174
self.draft_tokens[:num_reqs, step] = draft_tokens
175
@@ -219,6 +223,8 @@ class EagleSpeculator:
223
temperature: torch.Tensor,
224
# [max_num_reqs]
225
seeds: torch.Tensor,
226
+ # [max_num_reqs, num_speculative_steps, vocab_size]
227
+ draft_logits_out: torch.Tensor | None,
228
num_tokens_across_dp: torch.Tensor | None = None,
229
dummy_run: bool = False,
230
skip_attn_for_dummy_run: bool = False,
@@ -271,6 +277,7 @@ class EagleSpeculator:
277
idx_mapping.copy_(input_batch.idx_mapping)
278
self.temperature.copy_(temperature)
279
self.seeds.copy_(seeds)
280
+
281
# Gather the values and copy them to the pre-allocated buffers.
282
pos = self.input_buffers.positions[:num_reqs]
283
torch.gather(input_batch.positions, 0, last_token_indices, out=pos)
@@ -283,7 +290,11 @@ class EagleSpeculator:
290
self.seeds,
291
pos + 1,
292
apply_temperature=True,
293
+ processed_logits_out=draft_logits_out[:, 0]
294
+ if draft_logits_out is not None
295
+ else None,
296
)
297
+
298
if self.num_speculative_steps == 1:
299
# Early exit.
300
return draft_tokens.view(-1, 1)
@@ -365,6 +376,7 @@ class EagleSpeculator:
376
slot_mappings_updated,
377
num_tokens_across_dp=num_tokens_across_dp,
378
cudagraph_runtime_mode=batch_desc.cg_mode,
379
+ draft_logits_out=draft_logits_out,
380
)
381
return self.draft_tokens[:num_reqs]
382
vllm/v1/worker/gpu/spec_decode/rejection_sample.py
deleted
-62
@@ -1,62 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-import torch
4
-
5
-from vllm.triton_utils import tl, triton
6
-
7
-
8
-@triton.jit
9
-def _rejection_sample_kernel(
10
- sampled_ptr, # [num_reqs, num_speculative_steps + 1]
11
- sampled_stride,
12
- num_sampled_ptr, # [num_reqs]
13
- target_sampled_ptr, # [num_draft_tokens + num_reqs]
14
- input_ids_ptr, # [num_draft_tokens + num_reqs]
15
- cu_num_logits_ptr, # [num_reqs + 1]
16
-):
17
- req_idx = tl.program_id(0)
18
- start_idx = tl.load(cu_num_logits_ptr + req_idx)
19
- end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
20
- num_tokens = end_idx - start_idx
21
-
22
- num_sampled = 0
23
- rejected = False
24
- for i in range(num_tokens - 1):
25
- if not rejected:
26
- target_sampled = tl.load(target_sampled_ptr + start_idx + i)
27
- draft_sampled = tl.load(input_ids_ptr + start_idx + i + 1)
28
- tl.store(sampled_ptr + req_idx * sampled_stride + i, target_sampled)
29
- num_sampled += 1
30
- if target_sampled != draft_sampled:
31
- rejected = True
32
- if not rejected:
33
- target_sampled = tl.load(target_sampled_ptr + start_idx + num_tokens - 1)
34
- tl.store(
35
- sampled_ptr + req_idx * sampled_stride + num_tokens - 1, target_sampled
36
- )
37
- num_sampled += 1
38
- tl.store(num_sampled_ptr + req_idx, num_sampled)
39
-
40
-
41
-def rejection_sample(
42
- # [num_draft_tokens + num_reqs]
43
- target_sampled: torch.Tensor,
44
- # [num_draft_tokens + num_reqs]
45
- input_ids: torch.Tensor,
46
- # [num_reqs + 1]
47
- cu_num_logits: torch.Tensor,
48
- num_speculative_steps: int,
49
-) -> tuple[torch.Tensor, torch.Tensor]:
50
- num_reqs = cu_num_logits.shape[0] - 1
51
- sampled = target_sampled.new_empty(num_reqs, num_speculative_steps + 1)
52
- num_sampled = cu_num_logits.new_empty(num_reqs)
53
- _rejection_sample_kernel[(num_reqs,)](
54
- sampled,
55
- sampled.stride(0),
56
- num_sampled,
57
- target_sampled,
58
- input_ids,
59
- cu_num_logits,
60
- num_warps=1,
61
- )
62
- return sampled, num_sampled
vllm/v1/worker/gpu/spec_decode/rejection_sampler.py
new
+375
@@ -0,0 +1,375 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import torch
4
+
5
+from vllm.triton_utils import tl, triton
6
+from vllm.v1.worker.gpu.input_batch import InputBatch
7
+from vllm.v1.worker.gpu.metrics.logits import get_num_nans
8
+from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
9
+from vllm.v1.worker.gpu.sample.output import SamplerOutput
10
+from vllm.v1.worker.gpu.sample.sampler import Sampler
11
+
12
+
13
+@triton.jit
14
+def _strict_rejection_sample_kernel(
15
+ sampled_ptr, # [num_reqs, num_speculative_steps + 1]
16
+ sampled_stride,
17
+ num_sampled_ptr, # [num_reqs]
18
+ target_sampled_ptr, # [num_draft_tokens + num_reqs]
19
+ input_ids_ptr, # [num_draft_tokens + num_reqs]
20
+ cu_num_logits_ptr, # [num_reqs + 1]
21
+):
22
+ req_idx = tl.program_id(0)
23
+ start_idx = tl.load(cu_num_logits_ptr + req_idx)
24
+ end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
25
+ num_tokens = end_idx - start_idx
26
+
27
+ num_sampled = 0
28
+ rejected = False
29
+ for i in range(num_tokens - 1):
30
+ if not rejected:
31
+ target_sampled = tl.load(target_sampled_ptr + start_idx + i)
32
+ draft_sampled = tl.load(input_ids_ptr + start_idx + i + 1)
33
+ tl.store(sampled_ptr + req_idx * sampled_stride + i, target_sampled)
34
+ num_sampled += 1
35
+ if target_sampled != draft_sampled:
36
+ rejected = True
37
+ if not rejected:
38
+ target_sampled = tl.load(target_sampled_ptr + start_idx + num_tokens - 1)
39
+ tl.store(
40
+ sampled_ptr + req_idx * sampled_stride + num_tokens - 1, target_sampled
41
+ )
42
+ num_sampled += 1
43
+ tl.store(num_sampled_ptr + req_idx, num_sampled)
44
+
45
+
46
+def strict_rejection_sample(
47
+ # [num_draft_tokens + num_reqs]
48
+ target_sampled: torch.Tensor,
49
+ # [num_draft_tokens + num_reqs]
50
+ draft_sampled: torch.Tensor,
51
+ # [num_reqs + 1]
52
+ cu_num_logits: torch.Tensor,
53
+ num_speculative_steps,
54
+) -> tuple[torch.Tensor, torch.Tensor]:
55
+ num_reqs = cu_num_logits.shape[0] - 1
56
+ sampled = torch.empty(
57
+ num_reqs,
58
+ num_speculative_steps + 1,
59
+ dtype=target_sampled.dtype,
60
+ device=target_sampled.device,
61
+ )
62
+ num_sampled = torch.empty(
63
+ num_reqs,
64
+ dtype=torch.int32,
65
+ device=target_sampled.device,
66
+ )
67
+ _strict_rejection_sample_kernel[(num_reqs,)](
68
+ sampled,
69
+ sampled.stride(0),
70
+ num_sampled,
71
+ target_sampled,
72
+ draft_sampled,
73
+ cu_num_logits,
74
+ num_warps=1,
75
+ )
76
+ return sampled, num_sampled
77
+
78
+
79
+@triton.jit
80
+def _probabilistic_rejection_sample_kernel(
81
+ # [num_reqs, num_speculative_steps + 1]
82
+ sampled_ptr,
83
+ sampled_stride,
84
+ # [num_reqs]
85
+ rejected_steps_ptr,
86
+ # [num_logits]
87
+ draft_sampled_ptr,
88
+ # [num_logits, V]
89
+ target_probs_ptr,
90
+ target_probs_stride,
91
+ # [num_reqs, num_speculative_steps, V]
92
+ draft_probs_ptr,
93
+ draft_probs_stride_0,
94
+ draft_probs_stride_1,
95
+ # [num_reqs + 1]
96
+ cu_num_logits_ptr,
97
+ # [num_logits]
98
+ pos_ptr,
99
+ # [num_reqs]
100
+ idx_mapping_ptr,
101
+ # [num_reqs]
102
+ seeds_ptr,
103
+):
104
+ req_idx = tl.program_id(0)
105
+ start_idx = tl.load(cu_num_logits_ptr + req_idx)
106
+ num_tokens = tl.load(cu_num_logits_ptr + req_idx + 1) - start_idx
107
+ seed = tl.load(seeds_ptr + tl.load(idx_mapping_ptr + req_idx))
108
+
109
+ rejected_step = 0
110
+ accepted = True
111
+ for i in range(num_tokens - 1):
112
+ if accepted:
113
+ draft_sampled = tl.load(draft_sampled_ptr + start_idx + i + 1)
114
+ target_prob = tl.load(
115
+ target_probs_ptr + (start_idx + i) * target_probs_stride + draft_sampled
116
+ )
117
+ draft_prob = tl.load(
118
+ draft_probs_ptr
119
+ + req_idx * draft_probs_stride_0
120
+ + i * draft_probs_stride_1
121
+ + draft_sampled
122
+ )
123
+ pos = tl.load(pos_ptr + start_idx + i)
124
+ u = tl.sum(tl.rand(seed, pos + tl.arange(0, 1)))
125
+ accepted &= target_prob > u * draft_prob
126
+ tl.store(sampled_ptr + req_idx * sampled_stride + i, draft_sampled)
127
+ rejected_step += accepted
128
+ tl.store(rejected_steps_ptr + req_idx, rejected_step)
129
+
130
+
131
+@triton.jit
132
+def _compute_residual_logits_kernel(
133
+ # [num_reqs, V]
134
+ residual_logits_ptr,
135
+ residual_logits_stride,
136
+ # [num_reqs]
137
+ residual_pos_ptr,
138
+ # [num_logits, V]
139
+ target_logits_ptr,
140
+ target_logits_stride,
141
+ # [num_logits, V]
142
+ target_probs_ptr,
143
+ target_probs_stride,
144
+ # [num_reqs, num_speculative_steps, V]
145
+ draft_probs_ptr,
146
+ draft_probs_stride_0,
147
+ draft_probs_stride_1,
148
+ # [num_reqs]
149
+ rejected_step_ptr,
150
+ # [num_reqs + 1]
151
+ cu_num_logits_ptr,
152
+ # [num_logits]
153
+ pos_ptr,
154
+ vocab_size,
155
+ BLOCK_SIZE: tl.constexpr,
156
+):
157
+ req_idx = tl.program_id(0)
158
+ block_idx = tl.program_id(1)
159
+
160
+ start_idx = tl.load(cu_num_logits_ptr + req_idx)
161
+ end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
162
+ rejected_draft_step = tl.load(rejected_step_ptr + req_idx)
163
+ rejected_logit_idx = start_idx + rejected_draft_step
164
+
165
+ block_offsets = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
166
+ mask = block_offsets < vocab_size
167
+
168
+ if rejected_logit_idx < end_idx - 1:
169
+ target_probs = tl.load(
170
+ target_probs_ptr + rejected_logit_idx * target_probs_stride + block_offsets,
171
+ mask=mask,
172
+ other=0.0,
173
+ )
174
+ draft_probs = tl.load(
175
+ draft_probs_ptr
176
+ + req_idx * draft_probs_stride_0
177
+ + rejected_draft_step * draft_probs_stride_1
178
+ + block_offsets,
179
+ mask=mask,
180
+ other=0.0,
181
+ )
182
+ residual_probs = tl.maximum(target_probs - draft_probs, 0.0)
183
+ residual_logits = tl.log(residual_probs)
184
+ else:
185
+ # This is a bonus token. Directly return the target logits.
186
+ residual_logits = tl.load(
187
+ target_logits_ptr
188
+ + rejected_logit_idx * target_logits_stride
189
+ + block_offsets,
190
+ mask=mask,
191
+ other=0.0,
192
+ )
193
+
194
+ tl.store(
195
+ residual_logits_ptr + req_idx * residual_logits_stride + block_offsets,
196
+ residual_logits,
197
+ mask=mask,
198
+ )
199
+
200
+ # First block computes the residual logit positions.
201
+ if block_idx == 0:
202
+ pos_val = tl.load(pos_ptr + rejected_logit_idx)
203
+ tl.store(residual_pos_ptr + req_idx, pos_val)
204
+
205
+
206
+def probabilistic_rejection_sample(
207
+ # [num_draft_tokens + num_reqs, V]
208
+ target_logits: torch.Tensor,
209
+ # [num_reqs, num_speculative_steps, V]
210
+ draft_logits: torch.Tensor,
211
+ # [num_draft_tokens + num_reqs]
212
+ draft_sampled: torch.Tensor,
213
+ # [num_reqs + 1]
214
+ cu_num_logits: torch.Tensor,
215
+ # [num_logits]
216
+ pos: torch.Tensor,
217
+ # [num_reqs]
218
+ idx_mapping: torch.Tensor,
219
+ temperature,
220
+ seeds,
221
+ num_speculative_steps,
222
+) -> tuple[torch.Tensor, torch.Tensor]:
223
+ num_reqs = cu_num_logits.shape[0] - 1
224
+ device = target_logits.device
225
+ vocab_size = target_logits.shape[-1]
226
+
227
+ # Compute target and draft probs.
228
+ target_probs = torch.softmax(target_logits, dim=-1)
229
+ draft_probs = torch.softmax(draft_logits, dim=-1)
230
+
231
+ # Rejection sample.
232
+ # [num_reqs, num_speculative_steps + 1]
233
+ sampled = torch.empty(
234
+ num_reqs,
235
+ num_speculative_steps + 1,
236
+ dtype=torch.int64,
237
+ device=device,
238
+ )
239
+ # [num_reqs]
240
+ rejected_steps = torch.empty(
241
+ num_reqs,
242
+ dtype=torch.int64,
243
+ device=device,
244
+ )
245
+ _probabilistic_rejection_sample_kernel[(num_reqs,)](
246
+ sampled,
247
+ sampled.stride(0),
248
+ rejected_steps,
249
+ draft_sampled,
250
+ target_probs,
251
+ target_probs.stride(0),
252
+ draft_probs,
253
+ draft_probs.stride(0),
254
+ draft_probs.stride(1),
255
+ cu_num_logits,
256
+ pos,
257
+ idx_mapping,
258
+ seeds,
259
+ num_warps=1,
260
+ )
261
+
262
+ # Compute the logits and positions to resample the rejected/bonus
263
+ # tokens from.
264
+ # [num_reqs, vocab_size]
265
+ residual_logits = torch.empty(
266
+ num_reqs,
267
+ vocab_size,
268
+ dtype=target_logits.dtype,
269
+ device=device,
270
+ )
271
+ # [num_reqs]
272
+ residual_pos = torch.empty(
273
+ num_reqs,
274
+ dtype=pos.dtype,
275
+ device=device,
276
+ )
277
+ BLOCK_SIZE = 1024
278
+ num_blocks = triton.cdiv(vocab_size, BLOCK_SIZE)
279
+ _compute_residual_logits_kernel[(num_reqs, num_blocks)](
280
+ residual_logits,
281
+ residual_logits.stride(0),
282
+ residual_pos,
283
+ target_logits,
284
+ target_logits.stride(0),
285
+ target_probs,
286
+ target_probs.stride(0),
287
+ draft_probs,
288
+ draft_probs.stride(0),
289
+ draft_probs.stride(1),
290
+ rejected_steps,
291
+ cu_num_logits,
292
+ pos,
293
+ vocab_size,
294
+ BLOCK_SIZE=BLOCK_SIZE,
295
+ )
296
+
297
+ # Gumbel sample tokens from the residual distribution.
298
+ resampled = gumbel_sample(
299
+ residual_logits,
300
+ idx_mapping,
301
+ temperature,
302
+ seeds,
303
+ residual_pos,
304
+ apply_temperature=False,
305
+ )
306
+ sampled.scatter_(1, rejected_steps.unsqueeze(1), resampled.unsqueeze(1))
307
+
308
+ return sampled, rejected_steps + 1
309
+
310
+
311
+class RejectionSampler:
312
+ def __init__(
313
+ self,
314
+ sampler: Sampler,
315
+ num_speculative_steps,
316
+ use_strict_rejection_sampling: bool = True,
317
+ ):
318
+ self.sampler = sampler
319
+ self.num_speculative_steps = num_speculative_steps
320
+ self.use_strict_rejection_sampling = use_strict_rejection_sampling
321
+
322
+ def __call__(
323
+ self,
324
+ logits: torch.Tensor,
325
+ input_batch: InputBatch,
326
+ draft_logits: torch.Tensor | None = None,
327
+ ) -> SamplerOutput:
328
+ draft_sampled = input_batch.input_ids[input_batch.logits_indices]
329
+ # NOTE(woosuk): We intentionally compute num_nans before sampling to make clear
330
+ # that num_nans is computed before applying penalties and temperature.
331
+ num_nans = get_num_nans(logits) if self.sampler.compute_nans else None
332
+
333
+ if self.use_strict_rejection_sampling:
334
+ sampler_output = self.sampler(
335
+ logits,
336
+ input_batch,
337
+ )
338
+ logprobs_tensors = sampler_output.logprobs_tensors
339
+ sampled, num_sampled = strict_rejection_sample(
340
+ sampler_output.sampled_token_ids.view(-1),
341
+ draft_sampled,
342
+ input_batch.cu_num_logits,
343
+ self.num_speculative_steps,
344
+ )
345
+ else:
346
+ assert draft_logits is not None
347
+ pos = input_batch.positions[input_batch.logits_indices]
348
+ processed_logits = self.sampler.apply_sampling_params(
349
+ logits,
350
+ input_batch.expanded_idx_mapping,
351
+ input_batch.idx_mapping_np,
352
+ pos,
353
+ draft_sampled,
354
+ input_batch.expanded_local_pos,
355
+ )
356
+ # TODO (TheEpicDolphin): Return logprobs for sampled token ids.
357
+ logprobs_tensors = None
358
+ sampled, num_sampled = probabilistic_rejection_sample(
359
+ processed_logits,
360
+ draft_logits,
361
+ draft_sampled,
362
+ input_batch.cu_num_logits,
363
+ pos,
364
+ input_batch.idx_mapping,
365
+ self.sampler.sampling_states.temperature.gpu,
366
+ self.sampler.sampling_states.seeds.gpu,
367
+ self.num_speculative_steps,
368
+ )
369
+
370
+ return SamplerOutput(
371
+ sampled_token_ids=sampled,
372
+ logprobs_tensors=logprobs_tensors,
373
+ num_nans=num_nans,
374
+ num_sampled=num_sampled,
375
+ )
vllm/v1/worker/gpu/states.py
+15
@@ -15,6 +15,8 @@ class RequestState:
15
num_speculative_steps: int,
16
vocab_size: int,
17
device: torch.device,
18
+ model_dtype: torch.dtype,
19
+ cache_draft_logits: bool,
20
):
21
self.max_num_reqs = max_num_reqs
22
self.max_model_len = max_model_len
@@ -70,6 +72,19 @@ class RequestState:
72
dtype=torch.int64,
73
device=device,
74
)
75
+ # Draft token logits.
76
+ # NOTE: This tensor maintains the "processed" logits after applying temperature,
77
+ # top-p, etc.
78
+ self.draft_logits: torch.Tensor | None = None
79
+ if cache_draft_logits:
80
+ self.draft_logits = torch.zeros(
81
+ self.max_num_reqs,
82
+ self.num_speculative_steps,
83
+ self.vocab_size,
84
+ dtype=model_dtype,
85
+ device=device,
86
+ )
87
+
88
self.next_prefill_tokens = torch.zeros(
89
self.max_num_reqs, dtype=torch.int32, device=device
90
)