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] Do not initialize sampler for non-last PP ranks (#36824)
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Woosuk Kwon committed
Mar 11, 2026 at 20:55 UTC
2f8b4ce0c0ece2dfc3a5c61c51631606ee879ec0
3 files changed
+75
-50
vllm/v1/worker/gpu/input_batch.py
+11
-8
@@ -438,17 +438,20 @@ def _post_update_kernel(
438
439
for i in range(num_sampled):
440
token_id = tl.load(sampled_tokens_ptr + req_id * sampled_tokens_stride + i)
441
- token_ptr = (
442
- output_bin_counts_ptr + req_state_idx * output_bin_counts_stride + token_id
443
- )
444
- count = tl.load(token_ptr)
445
- count += 1
446
- tl.store(token_ptr, count)
441
tl.store(
442
all_token_ids_ptr + req_state_idx * all_token_ids_stride + total_len + i,
443
token_id,
444
)
445
446
+ if output_bin_counts_ptr is not None:
447
+ token_ptr = (
448
+ output_bin_counts_ptr
449
+ + req_state_idx * output_bin_counts_stride
450
+ + token_id
451
+ )
452
+ count = tl.load(token_ptr)
453
+ tl.store(token_ptr, count + 1)
454
+
455
query_start = tl.load(query_start_loc_ptr + req_id)
456
query_end = tl.load(query_start_loc_ptr + req_id + 1)
457
query_len = query_end - query_start
@@ -467,7 +470,7 @@ def post_update(
470
# [max_num_reqs]
471
last_sampled_tokens: torch.Tensor,
472
# [max_num_reqs, vocab_size]
470
- output_bin_counts: torch.Tensor,
473
+ output_bin_counts: torch.Tensor | None,
474
# [num_reqs, num_speculative_steps + 1]
475
sampled_tokens: torch.Tensor,
476
# [num_reqs]
@@ -487,7 +490,7 @@ def post_update(
490
num_computed_tokens,
491
last_sampled_tokens,
492
output_bin_counts,
490
- output_bin_counts.stride(0),
493
+ output_bin_counts.stride(0) if output_bin_counts is not None else 0,
494
sampled_tokens,
495
sampled_tokens.stride(0),
496
num_sampled,
vllm/v1/worker/gpu/model_runner.py
+60
-39
@@ -183,6 +183,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
183
# Draft tokens propagation - for spec-dec + struct outputs.
184
self.draft_tokens_handler = DraftTokensHandler(self.device)
185
186
+ # Pooling models.
187
+ self.is_pooling_model = self.model_config.runner_type == "pooling"
188
+ self.pooling_runner: PoolingRunner | None = None
189
+
190
# General request states.
191
self.req_states = RequestState(
192
max_num_reqs=self.max_num_reqs,
@@ -199,20 +203,34 @@ class GPUModelRunner(LoRAModelRunnerMixin):
203
max_num_tokens=self.max_num_tokens,
204
device=self.device,
205
)
202
- self.sampler = Sampler(
203
- max_num_reqs=self.max_num_reqs,
204
- vocab_size=self.vocab_size,
205
- device=self.device,
206
- req_states=self.req_states,
207
- logprobs_mode=self.model_config.logprobs_mode,
208
- num_speculative_tokens=self.num_speculative_steps + 1,
209
- )
210
- self.rejection_sampler = RejectionSampler(
211
- self.sampler,
212
- num_speculative_steps=self.num_speculative_steps,
213
- use_strict_rejection_sampling=use_strict_rejection_sampling,
214
- )
215
- self.prompt_logprobs_worker = PromptLogprobsWorker(self.max_num_reqs)
206
+
207
+ self.sampler: Sampler | None = None
208
+ self.rejection_sampler: RejectionSampler | None = None
209
+ self.prompt_logprobs_worker: PromptLogprobsWorker | None = None
210
+ self.structured_outputs_worker: StructuredOutputsWorker | None = None
211
+ if self.is_last_pp_rank and not self.is_pooling_model:
212
+ # Initialize sampling-related workers.
213
+ # These components are only set up on the last PP rank and
214
+ # for generative (non-pooling) models.
215
+ self.sampler = Sampler(
216
+ max_num_reqs=self.max_num_reqs,
217
+ vocab_size=self.vocab_size,
218
+ device=self.device,
219
+ req_states=self.req_states,
220
+ logprobs_mode=self.model_config.logprobs_mode,
221
+ num_speculative_tokens=self.num_speculative_steps + 1,
222
+ )
223
+ self.rejection_sampler = RejectionSampler(
224
+ self.sampler,
225
+ num_speculative_steps=self.num_speculative_steps,
226
+ use_strict_rejection_sampling=use_strict_rejection_sampling,
227
+ )
228
+ self.prompt_logprobs_worker = PromptLogprobsWorker(self.max_num_reqs)
229
+ self.structured_outputs_worker = StructuredOutputsWorker(
230
+ max_num_logits=self.max_num_reqs * (self.num_speculative_steps + 1),
231
+ vocab_size=self.vocab_size,
232
+ device=self.device,
233
+ )
234
235
# CUDA graphs.
236
self.decode_query_len = self.num_speculative_steps + 1
@@ -222,21 +240,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
240
self.compilation_config.cudagraph_mode,
241
decode_query_len=self.decode_query_len,
242
)
225
- # Structured outputs worker.
226
- self.structured_outputs_worker = StructuredOutputsWorker(
227
- max_num_logits=self.max_num_reqs * (self.num_speculative_steps + 1),
228
- vocab_size=self.vocab_size,
229
- device=self.device,
230
- )
243
# LoRA-related workers.
244
self.lora_state = LoraState(max_num_reqs=self.max_num_reqs)
245
# KV Connector if configured.
246
self.kv_connector: KVConnector = NO_OP_KV_CONNECTOR
247
236
- # Pooling models.
237
- self.is_pooling_model = self.model_config.runner_type == "pooling"
238
- self.pooling_runner: PoolingRunner | None = None
239
-
248
# For transferring state from execute_model to subsequent sample_tokens call.
249
self.execute_model_state: ExecuteModelState | None = None
250
@@ -248,8 +256,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
256
tasks: list[SupportedTask] = []
257
if self.model_config.runner_type == "generate":
258
tasks.extend(self.model_state.get_supported_generation_tasks())
251
- if self.pooling_runner is not None:
252
- tasks.extend(self.pooling_runner.get_supported_pooling_tasks())
259
+ if self.is_pooling_model:
260
+ # Do not rely on pooling_runner here, since this information is needed
261
+ # on the first PP rank, while pooling_runner is only initialized
262
+ # on the last PP rank.
263
+ tasks.extend(PoolingRunner.get_supported_tasks(self.model))
264
return tuple(tasks)
265
266
def load_model(self, *args, **kwargs) -> None:
@@ -289,7 +300,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
300
self.model_state = init_model_state(
301
self.vllm_config, self.model, self.encoder_cache, self.device
302
)
292
- if self.is_pooling_model:
303
+ if self.is_pooling_model and self.is_last_pp_rank:
304
self.pooling_runner = PoolingRunner(self.model)
305
306
def get_model(self) -> nn.Module:
@@ -420,6 +431,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
431
432
# dummy run the eagle speculator's propose to ensure DP/EP sync.
433
if self.speculator is not None:
434
+ assert self.sampler is not None
435
self.speculator.propose(
436
input_batch=input_batch,
437
attn_metadata=attn_metadata,
@@ -457,10 +469,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
469
# NOTE(woosuk): During the initial memory profiling, the sampler may skip
470
# top_k, top_p, and logprobs, using less GPU memory than what is possible
471
# during actual execution.
460
- self.sampler(
461
- logits,
462
- dummy_input_batch,
463
- )
472
+ assert self.sampler is not None
473
+ self.sampler(logits, dummy_input_batch)
474
475
@torch.inference_mode()
476
def _dummy_pooler_run(self, hidden_states: torch.Tensor) -> None:
@@ -558,7 +568,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
568
self.req_states.remove_request(req_id)
569
if self.encoder_cache is not None:
570
self.encoder_cache.remove_request(req_id)
561
- self.prompt_logprobs_worker.remove_request(req_id)
571
+ if self.prompt_logprobs_worker is not None:
572
+ self.prompt_logprobs_worker.remove_request(req_id)
573
self.lora_state.remove_request(req_id)
574
575
def free_states(self, scheduler_output: SchedulerOutput) -> None:
@@ -589,18 +600,21 @@ class GPUModelRunner(LoRAModelRunnerMixin):
600
)
601
self.lora_state.add_request(req_id, req_index, new_req_data.lora_request)
602
592
- if new_req_data.sampling_params is not None:
603
+ if self.is_last_pp_rank and new_req_data.sampling_params is not None:
604
+ assert self.sampler is not None
605
self.sampler.add_request(
606
req_index, prompt_len, new_req_data.sampling_params
607
)
608
+ assert self.prompt_logprobs_worker is not None
609
self.prompt_logprobs_worker.add_request(
610
req_id, req_index, new_req_data.sampling_params
611
)
612
613
if scheduler_output.scheduled_new_reqs:
614
self.req_states.apply_staged_writes()
602
- self.sampler.apply_staged_writes()
615
self.model_state.apply_staged_writes()
616
+ if self.sampler is not None:
617
+ self.sampler.apply_staged_writes()
618
619
def update_requests(self, scheduler_output: SchedulerOutput) -> None:
620
# Add new blocks for the existing requests.
@@ -788,6 +802,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
802
logits = self.model.compute_logits(sample_hidden_states)
803
if grammar_output is not None:
804
# Apply grammar bitmask to the logits in-place.
805
+ assert self.structured_outputs_worker is not None
806
self.structured_outputs_worker.apply_grammar_bitmask(
807
logits,
808
input_batch,
@@ -797,12 +812,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
812
813
if input_batch.num_draft_tokens == 0:
814
# No draft tokens (common case).
800
- sampler_output = self.sampler(
801
- logits,
802
- input_batch,
803
- )
815
+ assert self.sampler is not None
816
+ sampler_output = self.sampler(logits, input_batch)
817
else:
818
# Rejection sampling for spec decoding.
819
+ assert self.rejection_sampler is not None
820
sampler_output = self.rejection_sampler(
821
logits,
822
input_batch,
@@ -831,11 +845,16 @@ class GPUModelRunner(LoRAModelRunnerMixin):
845
num_rejected: torch.Tensor,
846
) -> None:
847
# Update the number of computed tokens.
848
+ if self.is_last_pp_rank:
849
+ assert self.sampler is not None
850
+ output_bin_counts = self.sampler.penalties_state.output_bin_counts
851
+ else:
852
+ output_bin_counts = None
853
post_update(
854
input_batch.idx_mapping,
855
self.req_states.num_computed_tokens.gpu,
856
self.req_states.last_sampled_tokens,
838
- self.sampler.penalties_state.output_bin_counts,
857
+ output_bin_counts,
858
sampled_tokens,
859
num_sampled,
860
num_rejected,
@@ -1076,6 +1095,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1095
# Broadcast to non-last PP ranks (handles spec decode multi-token).
1096
pp_broadcast(sampler_output.sampled_token_ids, num_sampled, num_rejected)
1097
1098
+ assert self.prompt_logprobs_worker is not None
1099
prompt_logprobs_dict = self.prompt_logprobs_worker.compute_prompt_logprobs(
1100
self.model.compute_logits,
1101
hidden_states,
@@ -1115,6 +1135,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1135
input_batch, sampler_output.sampled_token_ids, num_sampled, num_rejected
1136
)
1137
if self.speculator is not None:
1138
+ assert self.sampler is not None
1139
draft_tokens = self.speculator.propose(
1140
input_batch,
1141
attn_metadata,
vllm/v1/worker/gpu/pool/pooling_runner.py
+4
-3
@@ -19,10 +19,11 @@ class PoolingRunner:
19
def __init__(self, model: nn.Module):
20
self.model = cast(VllmModelForPooling, model)
21
22
- def get_supported_pooling_tasks(self) -> list[PoolingTask]:
23
- if not is_pooling_model(self.model):
22
+ @staticmethod
23
+ def get_supported_tasks(model: nn.Module) -> list[PoolingTask]:
24
+ if not is_pooling_model(model):
25
return []
25
- assert "embed" in self.model.pooler.get_supported_tasks()
26
+ assert "embed" in model.pooler.get_supported_tasks()
27
return ["embed"]
28
29
def pool(