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 8, 17:45 UTC
[Model Runner V2] support apply penalty for spec decode (#33251)
Signed-off-by: zhuhaoran <zhuhaoran.zhr@alibaba-inc.com>
zhrrr committed
Feb 7, 2026 at 02:56 UTC
16786da7357327f44f3a8f23d17e3c84235d2952
4 files changed
+91
-14
vllm/v1/worker/gpu/input_batch.py
+12
-2
@@ -40,6 +40,8 @@ class InputBatch:
40
idx_mapping_np: np.ndarray
41
# Identical to idx_mapping except for spec decoding.
42
expanded_idx_mapping: torch.Tensor
43
+ # [total_num_logits] position within request for each logit
44
+ expanded_local_pos: torch.Tensor
45
46
# [num_reqs]
47
# batch_idx -> num_scheduled_tokens
@@ -91,6 +93,7 @@ class InputBatch:
93
idx_mapping_np = np.arange(num_reqs, dtype=np.int32)
94
idx_mapping = torch.arange(num_reqs, dtype=torch.int32, device=device)
95
expanded_idx_mapping = idx_mapping
96
+ expanded_local_pos = torch.zeros(num_reqs, dtype=torch.int32, device=device)
97
num_scheduled_tokens = np.full(num_reqs, num_tokens // num_reqs, dtype=np.int32)
98
num_scheduled_tokens[-1] += num_tokens % num_reqs
99
assert int(num_scheduled_tokens.sum()) == num_tokens
@@ -126,6 +129,7 @@ class InputBatch:
129
idx_mapping=idx_mapping,
130
idx_mapping_np=idx_mapping_np,
131
expanded_idx_mapping=expanded_idx_mapping,
132
+ expanded_local_pos=expanded_local_pos,
133
num_scheduled_tokens=num_scheduled_tokens,
134
num_tokens=num_tokens,
135
num_tokens_after_padding=num_tokens,
@@ -487,6 +491,7 @@ def post_update(
491
def _expand_idx_mapping_kernel(
492
idx_mapping_ptr,
493
expanded_idx_mapping_ptr,
494
+ expanded_local_pos_ptr,
495
cu_num_logits_ptr,
496
BLOCK_SIZE: tl.constexpr,
497
):
@@ -499,6 +504,7 @@ def _expand_idx_mapping_kernel(
504
mask = block < num_tokens
505
req_state_idx = tl.load(idx_mapping_ptr + req_idx)
506
tl.store(expanded_idx_mapping_ptr + start_idx + block, req_state_idx, mask=mask)
507
+ tl.store(expanded_local_pos_ptr + start_idx + block, block, mask=mask)
508
509
510
def expand_idx_mapping(
@@ -506,13 +512,17 @@ def expand_idx_mapping(
512
total_num_logits: int,
513
cu_num_logits: torch.Tensor,
514
max_expand_len: int,
509
-) -> torch.Tensor:
515
+) -> tuple[torch.Tensor, torch.Tensor]:
516
num_reqs = idx_mapping.shape[0]
517
expanded_idx_mapping = idx_mapping.new_empty(total_num_logits)
518
+ expanded_local_pos = torch.empty(
519
+ total_num_logits, dtype=torch.int32, device=idx_mapping.device
520
+ )
521
_expand_idx_mapping_kernel[(num_reqs,)](
522
idx_mapping,
523
expanded_idx_mapping,
524
+ expanded_local_pos,
525
cu_num_logits,
526
BLOCK_SIZE=triton.next_power_of_2(max_expand_len),
527
)
518
- return expanded_idx_mapping
528
+ return expanded_idx_mapping, expanded_local_pos
vllm/v1/worker/gpu/model_runner.py
+22
-3
@@ -152,6 +152,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
152
vocab_size=self.vocab_size,
153
device=self.device,
154
logprobs_mode=self.model_config.logprobs_mode,
155
+ num_speculative_tokens=self.num_speculative_steps + 1,
156
)
157
self.prompt_logprobs_worker = PromptLogprobsWorker(self.max_num_reqs)
158
@@ -318,10 +319,22 @@ class GPUModelRunner(LoRAModelRunnerMixin):
319
idx_mapping = torch.arange(num_reqs, dtype=torch.int32, device=self.device)
320
idx_mapping_np = np.arange(num_reqs, dtype=np.int32)
321
pos = torch.zeros(num_reqs, dtype=torch.int64, device=self.device)
322
+ dummy_input_ids = torch.zeros(num_reqs, dtype=torch.int32, device=self.device)
323
+ expanded_local_pos = torch.zeros(
324
+ num_reqs, dtype=torch.int32, device=self.device
325
+ )
326
# NOTE(woosuk): During the initial memory profiling, the sampler may skip
327
# top_k, top_p, and logprobs, using less GPU memory than what is possible
328
# during actual execution.
324
- self.sampler(logits, idx_mapping, idx_mapping_np, idx_mapping_np, pos)
329
+ self.sampler(
330
+ logits,
331
+ idx_mapping,
332
+ idx_mapping_np,
333
+ idx_mapping_np,
334
+ pos,
335
+ dummy_input_ids,
336
+ expanded_local_pos,
337
+ )
338
339
@torch.inference_mode()
340
def profile_run(self) -> None:
@@ -511,6 +524,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
524
num_reqs + 1, device=self.device, dtype=torch.int32
525
)
526
expanded_idx_mapping = idx_mapping
527
+ expanded_local_pos = torch.zeros(
528
+ num_reqs, dtype=torch.int32, device=self.device
529
+ )
530
else:
531
num_draft_tokens = np.array(
532
[len(draft_tokens.get(req_id, ())) for req_id in req_ids],
@@ -526,7 +542,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
542
cu_num_logits = async_copy_to_gpu(cu_num_logits_np, device=self.device)
543
544
max_expand_len = self.num_speculative_steps + 1
529
- expanded_idx_mapping = expand_idx_mapping(
545
+ expanded_idx_mapping, expanded_local_pos = expand_idx_mapping(
546
idx_mapping, total_num_logits, cu_num_logits, max_expand_len
547
)
548
@@ -627,6 +643,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
643
idx_mapping=idx_mapping,
644
idx_mapping_np=idx_mapping_np,
645
expanded_idx_mapping=expanded_idx_mapping,
646
+ expanded_local_pos=expanded_local_pos,
647
num_scheduled_tokens=num_scheduled_tokens,
648
num_tokens=num_tokens,
649
num_tokens_after_padding=num_tokens_after_padding,
@@ -674,6 +691,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
691
) -> tuple[SamplerOutput, torch.Tensor, torch.Tensor]:
692
sample_hidden_states = hidden_states[input_batch.logits_indices]
693
sample_pos = input_batch.positions[input_batch.logits_indices]
694
+ input_ids = input_batch.input_ids[input_batch.logits_indices]
695
logits = self.model.compute_logits(sample_hidden_states)
696
if grammar_output is not None:
697
# Apply grammar bitmask to the logits in-place.
@@ -691,6 +709,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
709
input_batch.idx_mapping_np,
710
input_batch.cu_num_logits_np,
711
sample_pos,
712
+ input_ids,
713
+ input_batch.expanded_local_pos,
714
)
715
716
if input_batch.num_draft_tokens == 0:
@@ -700,7 +720,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
720
)
721
else:
722
# Rejection sampling for spec decoding.
703
- input_ids = input_batch.input_ids[input_batch.logits_indices]
723
sampled_tokens, num_sampled = rejection_sample(
724
sampler_output.sampled_token_ids,
725
input_ids,
vllm/v1/worker/gpu/sample/penalties.py
+37
-7
@@ -75,6 +75,9 @@ class PenaltiesState:
75
logits: torch.Tensor,
76
idx_mapping: torch.Tensor,
77
idx_mapping_np: np.ndarray,
78
+ input_ids: torch.Tensor,
79
+ expanded_local_pos: torch.Tensor,
80
+ num_speculative_tokens: int,
81
) -> None:
82
if not np.any(self.use_penalty[idx_mapping_np]):
83
# No request uses penalties. Skip the kernel launch.
@@ -83,11 +86,14 @@ class PenaltiesState:
86
apply_penalties(
87
logits,
88
idx_mapping,
89
+ input_ids,
90
+ expanded_local_pos,
91
self.repetition_penalty.gpu,
92
self.frequency_penalty.gpu,
93
self.presence_penalty.gpu,
94
self.prompt_bin_mask,
95
self.output_bin_counts,
96
+ num_speculative_tokens,
97
)
98
99
@@ -96,6 +102,8 @@ def _penalties_kernel(
102
logits_ptr,
103
logits_stride,
104
idx_mapping_ptr,
105
+ token_ids_ptr,
106
+ expanded_local_pos_ptr,
107
repetition_penalty_ptr,
108
frequency_penalty_ptr,
109
presence_penalty_ptr,
@@ -105,9 +113,10 @@ def _penalties_kernel(
113
output_bin_counts_stride,
114
vocab_size,
115
BLOCK_SIZE: tl.constexpr,
116
+ MAX_SPEC_LEN: tl.constexpr,
117
):
109
- batch_idx = tl.program_id(0)
110
- req_state_idx = tl.load(idx_mapping_ptr + batch_idx)
118
+ token_idx = tl.program_id(0)
119
+ req_state_idx = tl.load(idx_mapping_ptr + token_idx)
120
rep_penalty = tl.load(repetition_penalty_ptr + req_state_idx)
121
freq_penalty = tl.load(frequency_penalty_ptr + req_state_idx)
122
pres_penalty = tl.load(presence_penalty_ptr + req_state_idx)
@@ -123,13 +132,27 @@ def _penalties_kernel(
132
block_idx = tl.program_id(1)
133
block = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
134
mask = block < vocab_size
126
- logits = tl.load(logits_ptr + batch_idx * logits_stride + block, mask=mask)
135
+ logits = tl.load(logits_ptr + token_idx * logits_stride + block, mask=mask)
136
logits = logits.to(tl.float32)
137
129
- output_bin_counts = tl.load(
138
+ base_output_counts = tl.load(
139
output_bin_counts_ptr + req_state_idx * output_bin_counts_stride + block,
140
mask=mask,
141
+ other=0,
142
)
143
+
144
+ # Compute cumulative draft_counts from previous positions in this request
145
+ pos = tl.load(expanded_local_pos_ptr + token_idx)
146
+ start_idx = token_idx - pos
147
+ draft_counts = tl.zeros((BLOCK_SIZE,), dtype=tl.int32)
148
+ for prev_pos in tl.static_range(MAX_SPEC_LEN):
149
+ if prev_pos < pos:
150
+ prev_token = tl.load(token_ids_ptr + start_idx + prev_pos + 1)
151
+ token_match = block == prev_token
152
+ draft_counts = draft_counts + token_match.to(tl.int32)
153
+
154
+ # Total counts = base output counts + cumulative draft counts
155
+ output_bin_counts = base_output_counts + draft_counts
156
output_bin_mask = output_bin_counts > 0
157
158
# Apply repetition penalties.
@@ -138,6 +161,7 @@ def _penalties_kernel(
161
packed_mask = tl.load(
162
prompt_bin_mask_ptr + req_state_idx * prompt_bin_mask_stride + packed_block,
163
mask=packed_block < tl.cdiv(vocab_size, 32),
164
+ other=0,
165
)
166
prompt_bin_mask = (packed_mask[:, None] >> (tl.arange(0, 32)[None, :])) & 1
167
prompt_bin_mask = prompt_bin_mask.to(tl.int1)
@@ -153,25 +177,30 @@ def _penalties_kernel(
177
# Apply presence penalties.
178
logits -= pres_penalty * output_bin_mask
179
# Store back to logits.
156
- tl.store(logits_ptr + batch_idx * logits_stride + block, logits, mask=mask)
180
+ tl.store(logits_ptr + token_idx * logits_stride + block, logits, mask=mask)
181
182
183
def apply_penalties(
184
logits: torch.Tensor,
185
idx_mapping: torch.Tensor,
186
+ token_ids: torch.Tensor,
187
+ expanded_local_pos: torch.Tensor,
188
repetition_penalty: torch.Tensor,
189
frequency_penalty: torch.Tensor,
190
presence_penalty: torch.Tensor,
191
prompt_bin_mask: torch.Tensor,
192
output_bin_counts: torch.Tensor,
193
+ num_speculative_tokens: int,
194
) -> None:
168
- num_reqs, vocab_size = logits.shape
195
+ num_tokens, vocab_size = logits.shape
196
BLOCK_SIZE = 8192
197
num_blocks = triton.cdiv(vocab_size, BLOCK_SIZE)
171
- _penalties_kernel[(num_reqs, num_blocks)](
198
+ _penalties_kernel[(num_tokens, num_blocks)](
199
logits,
200
logits.stride(0),
201
idx_mapping,
202
+ token_ids,
203
+ expanded_local_pos,
204
repetition_penalty,
205
frequency_penalty,
206
presence_penalty,
@@ -181,6 +210,7 @@ def apply_penalties(
210
output_bin_counts.stride(0),
211
vocab_size,
212
BLOCK_SIZE=BLOCK_SIZE,
213
+ MAX_SPEC_LEN=num_speculative_tokens,
214
)
215
216
vllm/v1/worker/gpu/sample/sampler.py
+20
-2
@@ -25,6 +25,7 @@ class Sampler:
25
vocab_size: int,
26
device: torch.device,
27
logprobs_mode: LogprobsMode = "raw_logprobs",
28
+ num_speculative_tokens: int = 1,
29
):
30
if logprobs_mode not in ("processed_logprobs", "raw_logprobs"):
31
raise NotImplementedError(f"Unsupported logprobs_mode: {logprobs_mode}")
@@ -34,6 +35,7 @@ class Sampler:
35
self.sampling_states = SamplingStates(max_num_reqs, vocab_size)
36
self.penalties_state = PenaltiesState(max_num_reqs, vocab_size, device)
37
self.logit_bias_state = LogitBiasState(max_num_reqs, device)
38
+ self.num_speculative_tokens = num_speculative_tokens
39
40
def add_request(
41
self, req_idx: int, prompt_len: int, sampling_params: SamplingParams
@@ -61,12 +63,19 @@ class Sampler:
63
idx_mapping_np: np.ndarray,
64
cu_num_logits_np: np.ndarray,
65
pos: torch.Tensor,
66
+ input_ids: torch.Tensor,
67
+ expanded_local_pos: torch.Tensor,
68
) -> SamplerOutput:
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
72
sampled, processed_logits = self.sample(
69
- logits, idx_mapping, idx_mapping_np, pos
73
+ logits,
74
+ idx_mapping,
75
+ idx_mapping_np,
76
+ pos,
77
+ input_ids,
78
+ expanded_local_pos,
79
)
80
81
max_num_logprobs = self.sampling_states.max_num_logprobs(idx_mapping_np)
@@ -98,6 +107,8 @@ class Sampler:
107
idx_mapping: torch.Tensor,
108
idx_mapping_np: np.ndarray,
109
pos: torch.Tensor,
110
+ input_ids: torch.Tensor,
111
+ expanded_local_pos: torch.Tensor,
112
) -> tuple[torch.Tensor, torch.Tensor]:
113
# Copy logits to a new FP32 tensor.
114
logits = torch.empty_like(logits, dtype=torch.float32).copy_(logits)
@@ -106,7 +117,14 @@ class Sampler:
117
self.logit_bias_state.apply_logit_bias(logits, idx_mapping, idx_mapping_np, pos)
118
119
# Apply penalties in place.
109
- self.penalties_state.apply_penalties(logits, idx_mapping, idx_mapping_np)
120
+ self.penalties_state.apply_penalties(
121
+ logits,
122
+ idx_mapping,
123
+ idx_mapping_np,
124
+ input_ids,
125
+ expanded_local_pos,
126
+ self.num_speculative_tokens,
127
+ )
128
129
# Apply temperature in place.
130
apply_temperature(logits, idx_mapping, self.sampling_states.temperature.gpu)