@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / d8874c61a5
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 13, 02:00 UTC

[Core] Async Scheduling X Spec Decoding Compatibility (#24799)

Signed-off-by: Ronald1995 <ronaldautomobile@163.com> Signed-off-by: Nick Hill <nhill@redhat.com> Signed-off-by: Benjamin Chislett <chislett.ben@gmail.com> Co-authored-by: Nick Hill <nhill@redhat.com> Co-authored-by: Benjamin Chislett <chislett.ben@gmail.com>

Ronald committed Nov 18, 2025 at 04:16 UTC d8874c61a55e40db4ada047f1736c38c86439fff
11 files changed +314 -98
tests/v1/e2e/test_async_scheduling.py
+12 -26
@@ -15,7 +15,7 @@ from ...conftest import VllmRunner
15 from ...models.utils import check_outputs_equal
16
17 MODEL = "Qwen/Qwen3-0.6B"
18 -MTP_MODEL = "XiaomiMiMo/MiMo-7B-Base"
18 +MTP_MODEL = "meta-llama/Llama-3.2-1B-Instruct"
19
20
21 first_prompt = (
@@ -29,7 +29,8 @@ example_prompts = [first_prompt, "In one word, the capital of France is "] + [
29
30 default_params = dict(
31 temperature=0.0, # greedy
32 - max_tokens=20,
32 + max_tokens=23,
33 + min_tokens=18,
34 )
35
36
@@ -69,15 +70,9 @@ def test_without_spec_decoding(
70 (True, "uni", True, None, True),
71 ]
72
72 - run_tests(
73 - monkeypatch,
74 - MODEL,
75 - test_configs,
76 - test_sampling_params,
77 - )
73 + run_tests(monkeypatch, MODEL, test_configs, test_sampling_params)
74
75
80 -@pytest.mark.skip("MTP model too big to run in fp32 in CI")
76 def test_with_spec_decoding(monkeypatch: pytest.MonkeyPatch):
77 """Test consistency and acceptance rates with some different combos of
78 preemption, executor, async scheduling, prefill chunking,
@@ -85,8 +80,9 @@ def test_with_spec_decoding(monkeypatch: pytest.MonkeyPatch):
80 """
81
82 spec_config = {
88 - "method": "mtp",
83 + "method": "eagle3",
84 "num_speculative_tokens": 2,
85 + "model": "nm-testing/Llama3_2_1B_speculator.eagle3",
86 }
87 spec_config_short = spec_config | {"max_model_len": 50}
88
@@ -106,12 +102,7 @@ def test_with_spec_decoding(monkeypatch: pytest.MonkeyPatch):
102 (True, "uni", True, spec_config_short, True),
103 ]
104
109 - run_tests(
110 - monkeypatch,
111 - MTP_MODEL,
112 - test_configs,
113 - [{}],
114 - )
105 + run_tests(monkeypatch, MTP_MODEL, test_configs, [{}])
106
107
108 @dynamo_config.patch(cache_size_limit=16)
@@ -182,15 +173,13 @@ def run_tests(
173 and test_acceptance_rate is not None
174 ):
175 if "spec_mml=None" in test_config:
185 - # because the acceptance rate can vary, we use a looser
186 - # tolerance here.
176 assert (
177 pytest.approx(test_acceptance_rate, rel=5e-2)
178 == base_acceptance_rate
179 )
180 else:
181 # Currently the reported acceptance rate is expected to be
193 - # lower when we skip drafting altogether.
182 + # lower when we sometimes skip drafting altogether.
183 assert test_acceptance_rate > 0.05
184 print(
185 f"PASSED: config=[{test_config}], params={params}"
@@ -220,6 +209,7 @@ def run_test(
209 ):
210 spec_decoding = spec_config is not None
211 cache_arg: dict[str, Any] = (
212 + # Force preemptions
213 dict(num_gpu_blocks_override=32)
214 if test_preemption
215 else dict(gpu_memory_utilization=0.9)
@@ -238,6 +228,7 @@ def run_test(
228 model,
229 max_model_len=512,
230 enable_chunked_prefill=test_prefill_chunking,
231 + # Force prefill chunking
232 max_num_batched_tokens=48 if test_prefill_chunking else None,
233 # enforce_eager=True,
234 async_scheduling=async_scheduling,
@@ -255,10 +246,7 @@ def run_test(
246 results.append(
247 vllm_model.generate(
248 example_prompts,
258 - sampling_params=SamplingParams(
259 - **default_params,
260 - **override_params,
261 - ),
249 + sampling_params=SamplingParams(**default_params, **override_params),
250 return_logprobs=True,
251 )
252 )
@@ -270,9 +258,7 @@ def run_test(
258
259 if test_preemption:
260 preemptions = _get_count(
273 - metrics_before,
274 - metrics_after,
275 - "vllm:num_preemptions",
261 + metrics_before, metrics_after, "vllm:num_preemptions"
262 )
263 assert preemptions > 0, "preemption test had no preemptions"
264
vllm/config/speculative.py
+17 -21
@@ -3,7 +3,7 @@
3
4 import ast
5 import hashlib
6 -from typing import TYPE_CHECKING, Any, Literal
6 +from typing import TYPE_CHECKING, Any, Literal, get_args
7
8 from pydantic import Field, SkipValidation, model_validator
9 from pydantic.dataclasses import dataclass
@@ -29,31 +29,25 @@ else:
29
30 logger = init_logger(__name__)
31
32 -SpeculativeMethod = Literal[
33 - "ngram",
34 - "eagle",
35 - "eagle3",
36 - "medusa",
37 - "mlp_speculator",
38 - "draft_model",
39 - "deepseek_mtp",
40 - "ernie_mtp",
41 - "qwen3_next_mtp",
42 - "mimo_mtp",
43 - "longcat_flash_mtp",
44 - "pangu_ultra_moe_mtp",
45 - "mtp",
46 - "suffix",
47 -]
48 -MTP_MODEL_TYPES = (
32 +MTPModelTypes = Literal[
33 "deepseek_mtp",
34 "mimo_mtp",
35 "glm4_moe_mtp",
36 "ernie_mtp",
37 "qwen3_next_mtp",
38 "longcat_flash_mtp",
39 + "mtp",
40 "pangu_ultra_moe_mtp",
56 -)
41 +]
42 +EagleModelTypes = Literal["eagle", "eagle3", MTPModelTypes]
43 +SpeculativeMethod = Literal[
44 + "ngram",
45 + "medusa",
46 + "mlp_speculator",
47 + "draft_model",
48 + "suffix",
49 + EagleModelTypes,
50 +]
51
52
53 @config
@@ -244,7 +238,7 @@ class SpeculativeConfig:
238 # can not be detected, it will be considered as the "draft_model" by
239 # default.
240
247 - if self.method in MTP_MODEL_TYPES:
241 + if self.method in get_args(MTPModelTypes) and self.method != "mtp":
242 logger.warning(
243 "method `%s` is deprecated and replaced with mtp.", self.method
244 )
@@ -361,7 +355,9 @@ class SpeculativeConfig:
355 self.method = "medusa"
356 elif self.draft_model_config.hf_config.model_type == "mlp_speculator":
357 self.method = "mlp_speculator"
364 - elif self.draft_model_config.hf_config.model_type in MTP_MODEL_TYPES:
358 + elif self.draft_model_config.hf_config.model_type in get_args(
359 + MTPModelTypes
360 + ):
361 self.method = "mtp"
362 if self.num_speculative_tokens > 1:
363 logger.warning(
vllm/config/vllm.py
+17 -4
@@ -14,13 +14,14 @@ from dataclasses import replace
14 from datetime import datetime
15 from functools import lru_cache
16 from pathlib import Path
17 -from typing import TYPE_CHECKING, Any, TypeVar
17 +from typing import TYPE_CHECKING, Any, TypeVar, get_args
18
19 import torch
20 from pydantic import ConfigDict, Field, model_validator
21 from pydantic.dataclasses import dataclass
22
23 import vllm.envs as envs
24 +from vllm.config.speculative import EagleModelTypes
25 from vllm.logger import enable_trace_function_call, init_logger
26 from vllm.transformers_utils.runai_utils import is_runai_obj_uri
27 from vllm.utils import random_uuid
@@ -374,10 +375,22 @@ class VllmConfig:
375 "Async scheduling is not yet compatible with "
376 "pipeline_parallel_size > 1."
377 )
378 + # Currently, async scheduling only support eagle speculative
379 + # decoding.
380 if self.speculative_config is not None:
378 - raise ValueError(
379 - "Async scheduling is not yet compatible with speculative decoding."
380 - )
381 + if self.speculative_config.method not in get_args(EagleModelTypes):
382 + raise ValueError(
383 + "Currently, async scheduling is only supported "
384 + "with EAGLE/MTP kind of speculative decoding"
385 + )
386 + if self.speculative_config.disable_padded_drafter_batch:
387 + raise ValueError(
388 + "async scheduling for EAGLE/MTP kind of speculative "
389 + "decoding is enabled, but disable_padded_drafter_batch=True "
390 + "disable_padded_drafter_batch=True is not supported for "
391 + "this situation now. please set "
392 + "disable_padded_drafter_batch=Fasle"
393 + )
394 if not executor_supports_async_sched:
395 raise ValueError(
396 "Currently, async scheduling only supports `mp`, `uni`, or "
vllm/v1/core/sched/async_scheduler.py
+11 -4
@@ -16,18 +16,25 @@ class AsyncScheduler(Scheduler):
16 ) -> None:
17 super()._update_after_schedule(scheduler_output)
18 pending_structured_output_tokens = False
19 + spec_decode_tokens = scheduler_output.scheduled_spec_decode_tokens
20 for req_id in scheduler_output.num_scheduled_tokens:
21 request = self.requests[req_id]
22 pending_structured_output_tokens |= (
23 request.use_structured_output and request.num_output_placeholders > 0
24 )
25 + cur_num_spec_tokens = len(spec_decode_tokens.get(req_id, ()))
26 if (
27 request.num_computed_tokens
26 - == request.num_tokens + request.num_output_placeholders
28 + == request.num_tokens
29 + + request.num_output_placeholders
30 + + cur_num_spec_tokens
31 ):
28 - # The request will generate a new token in this scheduling step.
29 - # TODO(woosuk): Support speculative decoding.
30 - request.num_output_placeholders += 1
32 + # The request will generate a new token plus num_spec_tokens
33 + # in this scheduling step.
34 + request.num_output_placeholders += 1 + cur_num_spec_tokens
35 + # Add placeholders for the new tokens in spec_token_ids.
36 + # Wwe will update the actual spec token ids in the worker process.
37 + request.spec_token_ids = [-1] * self.num_spec_tokens
38
39 scheduler_output.pending_structured_output_tokens = (
40 pending_structured_output_tokens
vllm/v1/core/sched/scheduler.py
+10 -2
@@ -348,7 +348,10 @@ class Scheduler(SchedulerInterface):
348 # Speculative decode related.
349 if request.spec_token_ids:
350 num_scheduled_spec_tokens = (
351 - num_new_tokens + request.num_computed_tokens - request.num_tokens
351 + num_new_tokens
352 + + request.num_computed_tokens
353 + - request.num_tokens
354 + - request.num_output_placeholders
355 )
356 if num_scheduled_spec_tokens > 0:
357 # Trim spec_token_ids list to num_scheduled_spec_tokens.
@@ -1024,7 +1027,12 @@ class Scheduler(SchedulerInterface):
1027 # tokens and rejections. If some tokens are rejected,
1028 # num_computed_tokens is decreased by the number of rejected
1029 # tokens.
1027 - request.num_computed_tokens -= num_rejected
1030 + if request.num_computed_tokens > 0:
1031 + request.num_computed_tokens -= num_rejected
1032 + # If async scheduling, num_output_placeholders also includes
1033 + # the scheduled spec tokens count and so is similarly adjusted.
1034 + if request.num_output_placeholders > 0:
1035 + request.num_output_placeholders -= num_rejected
1036 spec_decoding_stats = self.make_spec_decoding_stats(
1037 spec_decoding_stats,
1038 num_draft_tokens=num_draft_tokens,
vllm/v1/engine/core.py
+5 -1
@@ -198,6 +198,7 @@ class EngineCore:
198 self.step_fn = (
199 self.step if self.batch_queue is None else self.step_with_batch_queue
200 )
201 + self.async_scheduling = vllm_config.scheduler_config.async_scheduling
202
203 # Mark the startup heap as static so that it's ignored by GC.
204 # Reduces pause times of oldest generation collections.
@@ -341,7 +342,10 @@ class EngineCore:
342 return engine_core_outputs, scheduler_output.total_num_scheduled_tokens > 0
343
344 def post_step(self, model_executed: bool) -> None:
344 - if self.use_spec_decode and model_executed:
345 + # When using async scheduling we can't get draft token ids in advance,
346 + # so we update draft token ids in the worker process and don't
347 + # need to update draft token ids here.
348 + if not self.async_scheduling and self.use_spec_decode and model_executed:
349 # Take the draft token ids.
350 draft_token_ids = self.model_executor.take_draft_token_ids()
351 if draft_token_ids is not None:
vllm/v1/engine/processor.py
+17
@@ -150,6 +150,23 @@ class Processor:
150 raise ValueError(
151 "vLLM V1 does not support per request user provided logits processors."
152 )
153 + # Async scheduling + spec decode currently incompatible with some
154 + # sampling parameters.
155 + if (
156 + self.vllm_config.speculative_config is not None
157 + and self.vllm_config.scheduler_config.async_scheduling
158 + and (
159 + params.frequency_penalty != 0.0
160 + or params.presence_penalty != 0.0
161 + or params.repetition_penalty != 1.0
162 + or params.bad_words_token_ids
163 + or params.structured_outputs
164 + )
165 + ):
166 + raise ValueError(
167 + "async scheduling with spec decoding doesn't yet support "
168 + "penalties, bad words or structured outputs in sampling parameters."
169 + )
170
171 def _validate_params(
172 self,
vllm/v1/sample/logits_processor/__init__.py
+1 -1
@@ -41,7 +41,7 @@ STR_POOLING_REJECTS_LOGITSPROCS = (
41 # Error message when the user tries to initialize vLLM with a speculative
42 # decoding enabled and custom logitsproces
43 STR_SPEC_DEC_REJECTS_LOGITSPROCS = (
44 - "Custom logits processors are not supportedwhen speculative decoding is enabled."
44 + "Custom logits processors are not supported when speculative decoding is enabled."
45 )
46
47 LOGITSPROCS_GROUP = "vllm.logits_processors"
vllm/v1/spec_decode/eagle.py
+5 -2
@@ -397,10 +397,13 @@ class EagleProposer:
397 positions += 1
398 exceeds_max_model_len = positions >= self.max_model_len
399 clamped_positions = torch.where(exceeds_max_model_len, 0, positions)
400 -
400 + # For data integrity when async scheduling, we shouldn't use in place
401 + # operations in case they are modified in next step's `prepare_input`
402 + # of main model.
403 # Increment the sequence lengths.
404 common_attn_metadata.seq_lens += 1
403 - common_attn_metadata.seq_lens_cpu += 1
405 + # This is an out-of-place operation to avoid modifying the original tensor.
406 + common_attn_metadata.seq_lens_cpu = common_attn_metadata.seq_lens_cpu + 1
407 # For the requests that exceed the max model length, we set the
408 # sequence length to 1 to minimize their overheads in attention.
409
vllm/v1/worker/gpu_input_batch.py
+3
@@ -46,6 +46,9 @@ class CachedRequestState:
46 lora_request: LoRARequest | None = None
47 prompt_embeds: torch.Tensor | None = None
48
49 + # Used when both async_scheduling and spec_decode are enabled.
50 + prev_num_draft_len: int = 0
51 +
52 def __post_init__(self):
53 self.num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
54 self.prompt_token_ids, self.prompt_embeds
vllm/v1/worker/gpu_model_runner.py
+216 -37
@@ -179,6 +179,7 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
179 logprobs_tensors: torch.Tensor | None,
180 invalid_req_indices: list[int],
181 async_output_copy_stream: torch.cuda.Stream,
182 + vocab_size: int,
183 ):
184 self._model_runner_output = model_runner_output
185 self._invalid_req_indices = invalid_req_indices
@@ -189,6 +190,7 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
190 # Keep a reference to the device tensor to avoid it being
191 # deallocated until we finish copying it to the host.
192 self._sampled_token_ids = sampled_token_ids
193 + self.vocab_size = vocab_size
194 self._logprobs_tensors = logprobs_tensors
195
196 # Initiate the copy on a separate stream, but do not synchronize it.
@@ -215,10 +217,16 @@ class AsyncGPUModelRunnerOutput(AsyncModelRunnerOutput):
217 # Release the device tensors once the copy has completed.
218 del self._logprobs_tensors
219 del self._sampled_token_ids
218 -
219 - valid_sampled_token_ids: list[np.ndarray] = [
220 - row for row in self.sampled_token_ids_cpu.numpy()
221 - ]
220 + max_gen_len = self.sampled_token_ids_cpu.shape[-1]
221 + if max_gen_len == 1:
222 + valid_sampled_token_ids: list[np.ndarray] = [
223 + row for row in self.sampled_token_ids_cpu.numpy()
224 + ]
225 + else:
226 + valid_sampled_token_ids = RejectionSampler.parse_output(
227 + self.sampled_token_ids_cpu,
228 + self.vocab_size,
229 + )
230 for i in self._invalid_req_indices:
231 valid_sampled_token_ids[i] = np.array([])
232
@@ -377,6 +385,10 @@ class GPUModelRunner(
385 )
386 self.rejection_sampler = RejectionSampler(self.sampler)
387
388 + self.num_spec_tokens = 0
389 + if self.speculative_config:
390 + self.num_spec_tokens = self.speculative_config.num_speculative_tokens
391 +
392 # Request states.
393 self.requests: dict[str, CachedRequestState] = {}
394 self.comm_stream = torch.cuda.Stream()
@@ -513,11 +525,7 @@ class GPUModelRunner(
525 self.max_num_tokens, dtype=torch.int32, device=self.device
526 )
527
516 - self.uniform_decode_query_len = (
517 - 1
518 - if not self.speculative_config
519 - else 1 + self.speculative_config.num_speculative_tokens
520 - )
528 + self.uniform_decode_query_len = 1 + self.num_spec_tokens
529
530 # Cudagraph dispatcher for runtime cudagraph dispatching.
531 self.cudagraph_dispatcher = CudagraphDispatcher(self.vllm_config)
@@ -549,6 +557,20 @@ class GPUModelRunner(
557 pin_memory=self.pin_memory,
558 )
559
560 + # Pre-allocated tensor for copying valid sampled token counts to CPU,
561 + # with dedicated stream for overlapping and event for coordination.
562 + self.valid_sampled_token_count_event: torch.cuda.Event | None = None
563 + self.valid_sampled_token_count_copy_stream: torch.cuda.Stream | None = None
564 + if self.use_async_scheduling and self.num_spec_tokens:
565 + self.valid_sampled_token_count_event = torch.cuda.Event()
566 + self.valid_sampled_token_count_copy_stream = torch.cuda.Stream()
567 + self.valid_sampled_token_count_cpu = torch.empty(
568 + self.max_num_reqs,
569 + dtype=torch.int64,
570 + device="cpu",
571 + pin_memory=self.pin_memory,
572 + )
573 +
574 # Ephemeral state transferred between execute_model() and sample_tokens().
575 self.execute_model_state: ExecuteModelState | None = None
576
@@ -736,17 +758,45 @@ class GPUModelRunner(
758 # Update the states of the running/resumed requests.
759 is_last_rank = get_pp_group().is_last_rank
760 req_data = scheduler_output.scheduled_cached_reqs
761 +
762 + # Wait until valid_sampled_tokens_count is copied to cpu,
763 + # then use it to update actual num_computed_tokens of each request.
764 + valid_sampled_token_count = self._get_valid_sampled_token_count()
765 +
766 for i, req_id in enumerate(req_data.req_ids):
767 req_state = self.requests[req_id]
768 num_computed_tokens = req_data.num_computed_tokens[i]
769 new_block_ids = req_data.new_block_ids[i]
770 resumed_from_preemption = req_id in req_data.resumed_req_ids
771 num_output_tokens = req_data.num_output_tokens[i]
772 + req_index = self.input_batch.req_id_to_index.get(req_id)
773
746 - # Update the cached states.
774 + # prev_num_draft_len is used in async scheduling mode with
775 + # spec decode. it indicates if need to update num_computed_tokens
776 + # of the request. for example:
777 + # fist step: num_computed_tokens = 0, spec_tokens = [],
778 + # prev_num_draft_len = 0.
779 + # second step: num_computed_tokens = 100(prompt lenth),
780 + # spec_tokens = [a,b], prev_num_draft_len = 0.
781 + # third step: num_computed_tokens = 100 + 2, spec_tokens = [c,d],
782 + # prev_num_draft_len = 2.
783 + # num_computed_tokens in first step and second step does't contain
784 + # the spec tokens length, but in third step it contains the
785 + # spec tokens length. we only need to update num_computed_tokens
786 + # when prev_num_draft_len > 0.
787 + if req_state.prev_num_draft_len:
788 + if req_index is None:
789 + req_state.prev_num_draft_len = 0
790 + else:
791 + assert self.input_batch.prev_req_id_to_index is not None
792 + prev_req_index = self.input_batch.prev_req_id_to_index[req_id]
793 + num_accepted = valid_sampled_token_count[prev_req_index] - 1
794 + num_rejected = req_state.prev_num_draft_len - num_accepted
795 + num_computed_tokens -= num_rejected
796 + req_state.output_token_ids.extend([-1] * num_accepted)
797
798 + # Update the cached states.
799 req_state.num_computed_tokens = num_computed_tokens
749 - req_index = self.input_batch.req_id_to_index.get(req_id)
800
801 if not is_last_rank:
802 # When using PP, the scheduler sends the sampled tokens back,
@@ -823,8 +873,11 @@ class GPUModelRunner(
873 spec_token_ids = scheduler_output.scheduled_spec_decode_tokens.get(
874 req_id, []
875 )
826 - if spec_token_ids:
827 - num_spec_tokens = len(spec_token_ids)
876 + num_spec_tokens = len(spec_token_ids)
877 + # For async scheduling, token_ids_cpu assigned from
878 + # spec_token_ids are placeholders and will be overwritten in
879 + # _prepare_input_ids.
880 + if num_spec_tokens:
881 start_index = self.input_batch.num_tokens_no_spec[req_index]
882 end_token_index = start_index + num_spec_tokens
883 self.input_batch.token_ids_cpu[
@@ -840,6 +893,15 @@ class GPUModelRunner(
893 # even when speculative decoding is enabled.
894 self.input_batch.spec_token_ids[req_index] = spec_token_ids
895
896 + # there are no draft tokens with async scheduling,
897 + # we clear the spec_decoding info in scheduler_output and
898 + # use normal sampling but rejection_sampling.
899 + if self.use_async_scheduling:
900 + req_state.prev_num_draft_len = num_spec_tokens
901 + if num_spec_tokens and self._draft_token_ids is None:
902 + scheduler_output.total_num_scheduled_tokens -= num_spec_tokens
903 + scheduler_output.num_scheduled_tokens[req_id] -= num_spec_tokens
904 + scheduler_output.scheduled_spec_decode_tokens.pop(req_id, None)
905 # Add the new or resumed requests to the persistent batch.
906 # The smaller empty indices are filled first.
907 for request in reqs_to_add:
@@ -959,7 +1021,10 @@ class GPUModelRunner(
1021 return cu_num_tokens, arange
1022
1023 def _prepare_input_ids(
962 - self, total_num_scheduled_tokens: int, cu_num_tokens: np.ndarray
1024 + self,
1025 + scheduler_output: "SchedulerOutput",
1026 + total_num_scheduled_tokens: int,
1027 + cu_num_tokens: np.ndarray,
1028 ) -> None:
1029 """Prepare the input IDs for the current batch.
1030
@@ -980,21 +1045,43 @@ class GPUModelRunner(
1045 # on the GPU from prev_sampled_token_ids.
1046 prev_req_id_to_index = self.input_batch.prev_req_id_to_index
1047 assert prev_req_id_to_index is not None
983 - flattened_indices = []
984 - prev_common_req_indices = []
1048 + sample_flattened_indices: list[int] = []
1049 + spec_flattened_indices: list[int] = []
1050 + prev_common_req_indices: list[int] = []
1051 + prev_draft_token_indices: list[int] = []
1052 indices_match = True
1053 max_flattened_index = -1
1054 + total_num_spec_tokens = 0
1055 + scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens
1056 +
1057 for req_id, cur_index in self.input_batch.req_id_to_index.items():
1058 if (prev_index := prev_req_id_to_index.get(req_id)) is not None:
1059 prev_common_req_indices.append(prev_index)
1060 # We need to compute the flattened input_ids index of the
1061 # last token in each common request.
1062 + draft_len = len(scheduled_spec_tokens.get(req_id, ()))
1063 + total_num_spec_tokens += draft_len
1064 flattened_index = cu_num_tokens[cur_index].item() - 1
993 - flattened_indices.append(flattened_index)
1065 + # example: cu_num_tokens = [2, 5, 8], draft_tokens = [1, 2, 2]
1066 + # sample_flattened_indices = [0, 2, 5]
1067 + # spec_flattened_indices = [1, 3, 4, 6, 7]
1068 + sample_flattened_indices.append(flattened_index - draft_len)
1069 + spec_flattened_indices.extend(
1070 + range(flattened_index - draft_len + 1, flattened_index + 1)
1071 + )
1072 + start = prev_index * self.num_spec_tokens
1073 + # prev_draft_token_indices is used to find which draft_tokens_id
1074 + # should be copied to input_ids
1075 + # example: prev draft_tokens_id [[1,2], [3,4], [5, 6]]
1076 + # flatten draft_tokens_id [1,2,3,4,5,6]
1077 + # draft_len of each request [1, 2, 1]
1078 + # then prev_draft_token_indices is [0, 2, 3, 4]
1079 + prev_draft_token_indices.extend(range(start, start + draft_len))
1080 indices_match &= prev_index == flattened_index
1081 max_flattened_index = max(max_flattened_index, flattened_index)
996 - num_commmon_tokens = len(flattened_indices)
997 - if num_commmon_tokens < total_num_scheduled_tokens:
1082 + num_commmon_tokens = len(sample_flattened_indices)
1083 + total_without_spec = total_num_scheduled_tokens - total_num_spec_tokens
1084 + if num_commmon_tokens < total_without_spec:
1085 # If not all requests are decodes from the last iteration,
1086 # We need to copy the input_ids_cpu to the GPU first.
1087 self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
@@ -1018,20 +1105,43 @@ class GPUModelRunner(
1105 self.is_token_ids.gpu[:num_commmon_tokens] = True
1106 return
1107 # Upload the index tensors asynchronously so the scatter can be non-blocking.
1021 - input_ids_index_tensor = torch.tensor(
1022 - flattened_indices, dtype=torch.int64, pin_memory=self.pin_memory
1108 + sampled_tokens_index_tensor = torch.tensor(
1109 + sample_flattened_indices, dtype=torch.int64, pin_memory=self.pin_memory
1110 ).to(self.device, non_blocking=True)
1111 prev_common_req_indices_tensor = torch.tensor(
1112 prev_common_req_indices, dtype=torch.int64, pin_memory=self.pin_memory
1113 ).to(self.device, non_blocking=True)
1114 self.input_ids.gpu.scatter_(
1115 dim=0,
1029 - index=input_ids_index_tensor,
1116 + index=sampled_tokens_index_tensor,
1117 src=self.input_batch.prev_sampled_token_ids[
1118 prev_common_req_indices_tensor, 0
1119 ],
1120 )
1121
1122 + # Scatter the draft tokens after the sampled tokens are scattered.
1123 + if self._draft_token_ids is None or not spec_flattened_indices:
1124 + return
1125 +
1126 + assert isinstance(self._draft_token_ids, torch.Tensor)
1127 + draft_tokens_index_tensor = torch.tensor(
1128 + spec_flattened_indices, dtype=torch.int64, pin_memory=self.pin_memory
1129 + ).to(self.device, non_blocking=True)
1130 + prev_draft_token_indices_tensor = torch.tensor(
1131 + prev_draft_token_indices, dtype=torch.int64, pin_memory=self.pin_memory
1132 + ).to(self.device, non_blocking=True)
1133 +
1134 + # because input_ids dtype is torch.int32,
1135 + # so convert draft_token_ids to torch.int32 here.
1136 + draft_token_ids = self._draft_token_ids.to(dtype=torch.int32)
1137 + self._draft_token_ids = None
1138 +
1139 + self.input_ids.gpu.scatter_(
1140 + dim=0,
1141 + index=draft_tokens_index_tensor,
1142 + src=draft_token_ids.flatten()[prev_draft_token_indices_tensor],
1143 + )
1144 +
1145 def _get_encoder_seq_lens(
1146 self,
1147 scheduled_encoder_inputs: dict[str, list[int]],
@@ -1218,7 +1328,11 @@ class GPUModelRunner(
1328 self.discard_request_indices.copy_to_gpu(self.num_discarded_requests)
1329
1330 # Copy the tensors to the GPU.
1221 - self._prepare_input_ids(total_num_scheduled_tokens, cu_num_tokens)
1331 + self._prepare_input_ids(
1332 + scheduler_output,
1333 + total_num_scheduled_tokens,
1334 + cu_num_tokens,
1335 + )
1336
1337 if self.uses_mrope:
1338 # Only relevant for models using M-RoPE (e.g, Qwen2-VL)
@@ -2377,12 +2491,14 @@ class GPUModelRunner(
2491 valid_sampled_token_ids = []
2492 invalid_req_indices = discard_sampled_tokens_req_indices.tolist()
2493 invalid_req_indices_set = set(invalid_req_indices)
2380 - assert sampled_token_ids.shape[-1] == 1
2494
2495 # Cache the sampled tokens on the GPU and avoid CPU sync.
2496 # These will be copied into input_ids in the next step
2497 # when preparing inputs.
2385 - self.input_batch.prev_sampled_token_ids = sampled_token_ids
2498 + # With spec decoding, this is done in propose_draft_token_ids().
2499 + if self.input_batch.prev_sampled_token_ids is None:
2500 + assert sampled_token_ids.shape[-1] == 1
2501 + self.input_batch.prev_sampled_token_ids = sampled_token_ids
2502 self.input_batch.prev_req_id_to_index = {
2503 req_id: i
2504 for i, req_id in enumerate(self.input_batch.req_ids)
@@ -2517,6 +2633,21 @@ class GPUModelRunner(
2633 "State error: sample_tokens() must be called "
2634 "after execute_model() returns None."
2635 )
2636 +
2637 + # self._draft_token_ids is None when `input_fits_in_drafter=False`
2638 + # and there is no draft tokens scheduled. so it need to update the
2639 + # spec_decoding info in scheduler_output with async_scheduling.
2640 + # use deepcopy to avoid the modification has influence on the
2641 + # scheduler_output in engine core process.
2642 + # TODO(Ronald1995): deepcopy is expensive when there is a large
2643 + # number of requests, optimize it later.
2644 + if (
2645 + self.use_async_scheduling
2646 + and self.num_spec_tokens
2647 + and self._draft_token_ids is None
2648 + ):
2649 + scheduler_output = deepcopy(scheduler_output)
2650 +
2651 num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
2652 with record_function_or_nullcontext("gpu_model_runner: preprocess"):
2653 with self.synchronize_input_prep():
@@ -2759,6 +2890,8 @@ class GPUModelRunner(
2890 with record_function_or_nullcontext("gpu_model_runner: sample"):
2891 sampler_output = self._sample(logits, spec_decode_metadata)
2892
2893 + self.input_batch.prev_sampled_token_ids = None
2894 +
2895 def propose_draft_token_ids(
2896 sampled_token_ids: torch.Tensor | list[np.ndarray],
2897 ) -> None:
@@ -2792,14 +2925,29 @@ class GPUModelRunner(
2925 self.speculative_config.draft_model_config.max_model_len
2926 )
2927 input_fits_in_drafter = spec_decode_common_attn_metadata and (
2795 - spec_decode_common_attn_metadata.max_seq_len
2796 - + self.speculative_config.num_speculative_tokens
2928 + spec_decode_common_attn_metadata.max_seq_len + self.num_spec_tokens
2929 <= effective_drafter_max_model_len
2930 )
2799 - if use_padded_batch_for_eagle and input_fits_in_drafter:
2800 - # EAGLE speculative decoding can use the GPU sampled tokens
2801 - # as inputs, and does not need to wait for bookkeeping to finish.
2802 - propose_draft_token_ids(sampler_output.sampled_token_ids)
2931 + if use_padded_batch_for_eagle:
2932 + sampled_token_ids = sampler_output.sampled_token_ids
2933 + if input_fits_in_drafter:
2934 + # EAGLE speculative decoding can use the GPU sampled tokens
2935 + # as inputs, and does not need to wait for bookkeeping to finish.
2936 + propose_draft_token_ids(sampled_token_ids)
2937 + elif self.valid_sampled_token_count_event is not None:
2938 + next_token_ids, valid_sampled_tokens_count = (
2939 + self.drafter.prepare_next_token_ids_padded(
2940 + spec_decode_common_attn_metadata,
2941 + sampled_token_ids,
2942 + self.requests,
2943 + self.input_batch,
2944 + self.discard_request_indices.gpu,
2945 + self.num_discarded_requests,
2946 + )
2947 + )
2948 + self._copy_valid_sampled_token_count(
2949 + next_token_ids, valid_sampled_tokens_count
2950 + )
2951
2952 with record_function_or_nullcontext("gpu_model_runner: bookkeep"):
2953 (
@@ -2856,6 +3004,7 @@ class GPUModelRunner(
3004 logprobs_tensors=sampler_output.logprobs_tensors,
3005 invalid_req_indices=invalid_req_indices,
3006 async_output_copy_stream=self.async_output_copy_stream,
3007 + vocab_size=self.input_batch.vocab_size,
3008 )
3009 with record_function_or_nullcontext(
3010 "gpu_model_runner: set_async_sampled_token_ids"
@@ -2880,6 +3029,37 @@ class GPUModelRunner(
3029 self._draft_token_ids = None
3030 return DraftTokenIds(req_ids, draft_token_ids)
3031
3032 + def _copy_valid_sampled_token_count(
3033 + self, next_token_ids: torch.Tensor, valid_sampled_tokens_count: torch.Tensor
3034 + ) -> None:
3035 + if self.valid_sampled_token_count_event is None:
3036 + return
3037 +
3038 + default_stream = torch.cuda.current_stream()
3039 + # Initialize a new stream to overlap the copy operation with
3040 + # prepare_input of draft model.
3041 + with torch.cuda.stream(self.valid_sampled_token_count_copy_stream):
3042 + self.valid_sampled_token_count_copy_stream.wait_stream(default_stream) # type: ignore
3043 + counts = valid_sampled_tokens_count
3044 + counts_cpu = self.valid_sampled_token_count_cpu
3045 + counts_cpu[: counts.shape[0]].copy_(counts, non_blocking=True)
3046 + self.valid_sampled_token_count_event.record()
3047 +
3048 + self.input_batch.prev_sampled_token_ids = next_token_ids.unsqueeze(1)
3049 +
3050 + def _get_valid_sampled_token_count(self) -> list[int]:
3051 + # Wait until valid_sampled_tokens_count is copied to cpu,
3052 + prev_sampled_token_ids = self.input_batch.prev_sampled_token_ids
3053 + if (
3054 + self.valid_sampled_token_count_event is None
3055 + or prev_sampled_token_ids is None
3056 + ):
3057 + return []
3058 +
3059 + counts_cpu = self.valid_sampled_token_count_cpu
3060 + self.valid_sampled_token_count_event.synchronize()
3061 + return counts_cpu[: prev_sampled_token_ids.shape[0]].tolist()
3062 +
3063 def propose_draft_token_ids(
3064 self,
3065 scheduler_output: "SchedulerOutput",
@@ -2967,6 +3147,9 @@ class GPUModelRunner(
3147 self.num_discarded_requests,
3148 )
3149 )
3150 + self._copy_valid_sampled_token_count(
3151 + next_token_ids, valid_sampled_tokens_count
3152 + )
3153
3154 if spec_decode_metadata is None:
3155 token_indices_to_sample = None
@@ -3532,7 +3715,7 @@ class GPUModelRunner(
3715 # TODO(luka) better system for describing dummy batches
3716 seq_lens = [1] * num_decode_tokens + [num_prefill_tokens + 1]
3717 else:
3535 - seq_lens = max_query_len
3718 + seq_lens = max_query_len # type: ignore[assignment]
3719 self.seq_lens.np[:num_reqs] = seq_lens
3720 self.seq_lens.np[num_reqs:] = 0
3721 self.seq_lens.copy_to_gpu()
@@ -4485,11 +4668,7 @@ class GPUModelRunner(
4668 logitsprocs=self.input_batch.logitsprocs,
4669 logitsprocs_need_output_token_ids=self.input_batch.logitsprocs_need_output_token_ids,
4670 is_pooling_model=self.is_pooling_model,
4488 - num_speculative_tokens=(
4489 - self.vllm_config.speculative_config.num_speculative_tokens
4490 - if self.vllm_config.speculative_config
4491 - else 0
4492 - ),
4671 + num_speculative_tokens=self.num_spec_tokens,
4672 )
4673
4674 def _allocate_kv_cache_tensors(