@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7f280d69c9
Fork 0

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 15, 03:30 UTC

[Optimization] Cache sampled token ids in model runner (#20291)

Signed-off-by: Woosuk Kwon <woosuk.kwon@berkeley.edu>

Woosuk Kwon committed Jul 1, 2025 at 11:01 UTC 7f280d69c98e560427d2cbc9c3c3c13a83510dca
5 files changed +91 -45
tests/v1/worker/test_gpu_model_runner.py
+6 -6
@@ -172,7 +172,7 @@ def _is_req_state_block_table_match(model_runner, req_id: str) -> bool:
172 req_state.block_ids[0]).all()
173
174
175 -def test_update_states_new_request(model_runner):
175 +def test_update_states_new_request(model_runner, dist_init):
176 req_id = "req_0"
177
178 # new req
@@ -186,7 +186,7 @@ def test_update_states_new_request(model_runner):
186 assert _is_req_state_block_table_match(model_runner, req_id)
187
188
189 -def test_update_states_request_finished(model_runner):
189 +def test_update_states_request_finished(model_runner, dist_init):
190 req_id = "req_0"
191
192 # new req
@@ -218,7 +218,7 @@ def test_update_states_request_finished(model_runner):
218 assert not _is_req_scheduled(model_runner, req_id)
219
220
221 -def test_update_states_request_resumed(model_runner):
221 +def test_update_states_request_resumed(model_runner, dist_init):
222 req_id = "req_0"
223
224 # new req
@@ -278,7 +278,7 @@ def test_update_states_request_resumed(model_runner):
278 assert _is_req_state_block_table_match(model_runner, req_id)
279
280
281 -def test_get_nans_in_logits(model_runner):
281 +def test_get_nans_in_logits(model_runner, dist_init):
282 req_ids = ("req_0", "req_1")
283
284 scheduler_output = _schedule_new_request(*req_ids)
@@ -326,7 +326,7 @@ def test_get_nans_in_logits(model_runner):
326 assert result == {'req_0': 2, 'req_1': 0}
327
328
329 -def test_update_states_no_changes(model_runner):
329 +def test_update_states_no_changes(model_runner, dist_init):
330 req_id = "req_0"
331
332 # new req
@@ -359,7 +359,7 @@ def test_update_states_no_changes(model_runner):
359 assert _is_req_state_block_table_match(model_runner, req_id)
360
361
362 -def test_update_states_request_unscheduled(model_runner):
362 +def test_update_states_request_unscheduled(model_runner, dist_init):
363 req_ids = ("req_0", "req_1")
364
365 # new reqs
vllm/distributed/kv_transfer/kv_connector/v1/shared_storage_connector.py
+2 -2
@@ -307,7 +307,7 @@ class SharedStorageConnector(KVConnectorBase_V1):
307 cached_reqs = scheduler_output.scheduled_cached_reqs
308 for i, req_id in enumerate(cached_reqs.req_ids):
309 num_computed_tokens = cached_reqs.num_computed_tokens[i]
310 - new_token_ids = cached_reqs.new_token_ids[i]
310 + num_new_tokens = scheduler_output.num_scheduled_tokens[req_id]
311 new_block_ids = cached_reqs.new_block_ids[i]
312 resumed_from_preemption = cached_reqs.resumed_from_preemption[i]
313
@@ -320,7 +320,7 @@ class SharedStorageConnector(KVConnectorBase_V1):
320 # list of token ids (only new tokens). So we look it
321 # up in the actual request object.
322 request = self._requests_need_load[req_id]
323 - total_tokens = (len(new_token_ids) + num_computed_tokens)
323 + total_tokens = num_computed_tokens + num_new_tokens
324 token_ids = request.all_token_ids[:total_tokens]
325
326 # NOTE(rob): For resumed req, new_block_ids is all
vllm/v1/core/sched/output.py
+2
@@ -88,6 +88,8 @@ class CachedRequestData:
88 # the request's block IDs. If True, new_block_ids will be used as the
89 # request's block IDs instead of appending to the existing block IDs.
90 resumed_from_preemption: list[bool]
91 + # NOTE(woosuk): new_token_ids is only used for pipeline parallelism.
92 + # When PP is not used, new_token_ids will be empty.
93 new_token_ids: list[list[int]]
94 new_block_ids: list[tuple[list[int], ...]]
95 num_computed_tokens: list[int]
vllm/v1/core/sched/scheduler.py
+13 -5
@@ -55,6 +55,7 @@ class Scheduler(SchedulerInterface):
55 self.lora_config = vllm_config.lora_config
56 self.kv_cache_config = kv_cache_config
57 self.kv_events_config = vllm_config.kv_events_config
58 + self.parallel_config = vllm_config.parallel_config
59 self.log_stats = log_stats
60 self.structured_output_manager = structured_output_manager
61
@@ -87,7 +88,7 @@ class Scheduler(SchedulerInterface):
88
89 self.kv_event_publisher = EventPublisherFactory.create(
90 self.kv_events_config,
90 - vllm_config.parallel_config.data_parallel_rank,
91 + self.parallel_config.data_parallel_rank,
92 )
93
94 num_gpu_blocks = self.cache_config.num_gpu_blocks
@@ -159,6 +160,7 @@ class Scheduler(SchedulerInterface):
160 log_stats=self.log_stats,
161 enable_kv_cache_events=self.enable_kv_cache_events,
162 )
163 + self.use_pp = self.parallel_config.pipeline_parallel_size > 1
164
165 def schedule(self) -> SchedulerOutput:
166 # NOTE(woosuk) on the scheduling algorithm:
@@ -214,7 +216,7 @@ class Scheduler(SchedulerInterface):
216 # This is necessary when using spec decoding.
217 num_new_tokens = min(
218 num_new_tokens,
217 - self.max_model_len - request.num_computed_tokens)
219 + self.max_model_len - 1 - request.num_computed_tokens)
220
221 # Schedule encoder inputs.
222 encoder_inputs_to_schedule = None
@@ -624,9 +626,15 @@ class Scheduler(SchedulerInterface):
626 req_ids.append(req_id)
627 num_tokens = (num_scheduled_tokens[req_id] -
628 len(spec_decode_tokens.get(req_id, ())))
627 - token_ids = req.all_token_ids[req.num_computed_tokens:req.
628 - num_computed_tokens + num_tokens]
629 - new_token_ids.append(token_ids)
629 + if self.use_pp:
630 + # When using PP, the scheduler sends the sampled tokens back,
631 + # because there's no direct communication between the first-
632 + # stage worker and the last-stage worker. Otherwise, we don't
633 + # need to send the sampled tokens back because the model runner
634 + # will cache them.
635 + token_ids = req.all_token_ids[req.num_computed_tokens:req.
636 + num_computed_tokens + num_tokens]
637 + new_token_ids.append(token_ids)
638 new_block_ids.append(req_to_new_block_ids[req_id])
639 num_computed_tokens.append(req.num_computed_tokens)
640 # Because resumed_reqs is usually empty, it is more efficient to do
vllm/v1/worker/gpu_model_runner.py
+68 -32
@@ -470,26 +470,33 @@ class GPUModelRunner(LoRAModelRunnerMixin):
470 req_ids_to_add.append(req_id)
471
472 # Update the states of the running/resumed requests.
473 + is_last_rank = get_pp_group().is_last_rank
474 req_data = scheduler_output.scheduled_cached_reqs
475 for i, req_id in enumerate(req_data.req_ids):
476 req_state = self.requests[req_id]
477 num_computed_tokens = req_data.num_computed_tokens[i]
477 - new_token_ids = req_data.new_token_ids[i]
478 new_block_ids = req_data.new_block_ids[i]
479 resumed_from_preemption = req_data.resumed_from_preemption[i]
480
481 # Update the cached states.
482 req_state.num_computed_tokens = num_computed_tokens
483 - # Add the sampled token(s) from the previous step (if any).
484 - # This doesn't include "unverified" tokens like spec decode tokens.
485 - num_new_tokens = (num_computed_tokens + len(new_token_ids) -
486 - req_state.num_tokens)
487 - if num_new_tokens == 1:
488 - # Avoid slicing list in most common case.
489 - req_state.output_token_ids.append(new_token_ids[-1])
490 - elif num_new_tokens > 0:
491 - req_state.output_token_ids.extend(
492 - new_token_ids[-num_new_tokens:])
483 +
484 + if not is_last_rank:
485 + # When using PP, the scheduler sends the sampled tokens back,
486 + # because there's no direct communication between the first-
487 + # stage worker and the last-stage worker.
488 + new_token_ids = req_data.new_token_ids[i]
489 + # Add the sampled token(s) from the previous step (if any).
490 + # This doesn't include "unverified" tokens like spec tokens.
491 + num_new_tokens = (num_computed_tokens + len(new_token_ids) -
492 + req_state.num_tokens)
493 + if num_new_tokens == 1:
494 + # Avoid slicing list in most common case.
495 + req_state.output_token_ids.append(new_token_ids[-1])
496 + elif num_new_tokens > 0:
497 + req_state.output_token_ids.extend(
498 + new_token_ids[-num_new_tokens:])
499 +
500 # Update the block IDs.
501 if not resumed_from_preemption:
502 # Append the new blocks to the existing block IDs.
@@ -513,22 +520,30 @@ class GPUModelRunner(LoRAModelRunnerMixin):
520 self.input_batch.num_computed_tokens_cpu[req_index] = (
521 num_computed_tokens)
522 self.input_batch.block_table.append_row(new_block_ids, req_index)
516 - # Add new_token_ids to token_ids_cpu.
517 - start_token_index = num_computed_tokens
518 - end_token_index = num_computed_tokens + len(new_token_ids)
519 - self.input_batch.token_ids_cpu[
520 - req_index, start_token_index:end_token_index] = new_token_ids
521 - self.input_batch.num_tokens_no_spec[req_index] = end_token_index
522 - # Add spec_token_ids to token_ids_cpu.
523 - spec_token_ids = scheduler_output.scheduled_spec_decode_tokens.get(
524 - req_id, ())
525 - if spec_token_ids:
526 - start_index = end_token_index
527 - end_token_index += len(spec_token_ids)
523 +
524 + # For the last rank, we don't need to update the token_ids_cpu
525 + # because the sampled tokens are already cached.
526 + if not is_last_rank:
527 + # Add new_token_ids to token_ids_cpu.
528 + start_token_index = num_computed_tokens
529 + end_token_index = num_computed_tokens + len(new_token_ids)
530 self.input_batch.token_ids_cpu[
529 - req_index, start_index:end_token_index] = spec_token_ids
530 - # NOTE(woosuk): `num_tokens` here may include spec decode tokens.
531 - self.input_batch.num_tokens[req_index] = end_token_index
531 + req_index,
532 + start_token_index:end_token_index] = new_token_ids
533 + self.input_batch.num_tokens_no_spec[
534 + req_index] = end_token_index
535 + # Add spec_token_ids to token_ids_cpu.
536 + spec_token_ids = (
537 + scheduler_output.scheduled_spec_decode_tokens.get(
538 + req_id, ()))
539 + if spec_token_ids:
540 + start_index = end_token_index
541 + end_token_index += len(spec_token_ids)
542 + self.input_batch.token_ids_cpu[
543 + req_index,
544 + start_index:end_token_index] = spec_token_ids
545 + # NOTE(woosuk): `num_tokens` here may include spec tokens.
546 + self.input_batch.num_tokens[req_index] = end_token_index
547
548 # Check if the batch has changed. If not, we can skip copying the
549 # sampling metadata from CPU to GPU.
@@ -1509,6 +1524,30 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1524 for i in discard_sampled_tokens_req_indices:
1525 valid_sampled_token_ids[i].clear()
1526
1527 + # Cache the sampled tokens in the model runner, so that the scheduler
1528 + # doesn't need to send them back.
1529 + # NOTE(woosuk): As an exception, when using PP, the scheduler sends
1530 + # the sampled tokens back, because there's no direct communication
1531 + # between the first-stage worker and the last-stage worker.
1532 + for req_idx, sampled_ids in enumerate(valid_sampled_token_ids):
1533 + if not sampled_ids:
1534 + continue
1535 +
1536 + start_idx = self.input_batch.num_tokens_no_spec[req_idx]
1537 + end_idx = start_idx + len(sampled_ids)
1538 + assert end_idx <= self.max_model_len, (
1539 + "Sampled token IDs exceed the max model length. "
1540 + f"Total number of tokens: {end_idx} > max_model_len: "
1541 + f"{self.max_model_len}")
1542 +
1543 + self.input_batch.token_ids_cpu[req_idx,
1544 + start_idx:end_idx] = sampled_ids
1545 + self.input_batch.num_tokens_no_spec[req_idx] = end_idx
1546 + self.input_batch.num_tokens[req_idx] = end_idx
1547 + req_id = self.input_batch.req_ids[req_idx]
1548 + req_state = self.requests[req_id]
1549 + req_state.output_token_ids.extend(sampled_ids)
1550 +
1551 if not self.speculative_config:
1552 # Speculative decoding is not enabled.
1553 spec_token_ids = None
@@ -1730,17 +1769,14 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1769 draft_token_ids.append([])
1770 continue
1771
1733 - # Add sampled_token_ids to token_ids_cpu.
1734 - start_idx = self.input_batch.num_tokens_no_spec[i]
1735 - end_idx = start_idx + num_sampled_ids
1736 - if end_idx >= self.max_model_len:
1772 + num_tokens = self.input_batch.num_tokens_no_spec[i]
1773 + if num_tokens >= self.max_model_len:
1774 # Skip requests that have already reached the max model length.
1775 draft_token_ids.append([])
1776 continue
1777
1741 - self.input_batch.token_ids_cpu[i, start_idx:end_idx] = sampled_ids
1778 drafter_output = self.drafter.propose(
1743 - self.input_batch.token_ids_cpu[i, :end_idx])
1779 + self.input_batch.token_ids_cpu[i, :num_tokens])
1780 if drafter_output is None or len(drafter_output) == 0:
1781 draft_token_ids.append([])
1782 else: