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, 23:30 UTC
[V1][Spec decode] Move drafter to model runner (#13363)
Signed-off-by: Woosuk Kwon <woosuk.kwon@berkeley.edu>
Woosuk Kwon committed
Feb 17, 2025 at 15:40 UTC
cd4a72a28d32851c7d188ee4f0a0145f4250e2aa
9 files changed
+84
-57
tests/v1/core/test_scheduler.py
+7
@@ -203,6 +203,7 @@ def test_schedule_partial_requests():
203
req_ids=[request.request_id for request in requests],
204
req_id_to_index=req_to_index,
205
sampled_token_ids=[[0] for _ in range(len(requests))],
206
+ spec_token_ids=None,
207
logprobs=None,
208
prompt_logprobs_dict={},
209
)
@@ -259,6 +260,7 @@ def test_stop_via_update_from_output():
260
sampled_token_ids=[[EOS_TOKEN_ID],
261
[10,
262
11]], # First request hits EOS, second continues
263
+ spec_token_ids=None,
264
logprobs=None,
265
prompt_logprobs_dict={})
266
@@ -307,6 +309,7 @@ def test_stop_via_update_from_output():
309
},
310
sampled_token_ids=[[10, 42, 12],
311
[13, 14]], # First request hits stop token
312
+ spec_token_ids=None,
313
logprobs=None,
314
prompt_logprobs_dict={})
315
@@ -354,6 +357,7 @@ def test_stop_via_update_from_output():
357
},
358
sampled_token_ids=[[10, 11, 12],
359
[13]], # First request exceeds max_tokens
360
+ spec_token_ids=None,
361
logprobs=None,
362
prompt_logprobs_dict={})
363
@@ -394,6 +398,7 @@ def test_stop_via_update_from_output():
398
req_ids=[requests[0].request_id],
399
req_id_to_index={requests[0].request_id: 0},
400
sampled_token_ids=[[EOS_TOKEN_ID, 10, 11]],
401
+ spec_token_ids=None,
402
logprobs=None,
403
prompt_logprobs_dict={})
404
@@ -434,6 +439,7 @@ def test_schedule_concurrent_batches():
439
req_ids=[requests[0].request_id],
440
req_id_to_index={requests[0].request_id: 0},
441
sampled_token_ids=[[0]],
442
+ spec_token_ids=None,
443
logprobs=None,
444
prompt_logprobs_dict={},
445
)
@@ -450,6 +456,7 @@ def test_schedule_concurrent_batches():
456
req_ids=[requests[1].request_id],
457
req_id_to_index={requests[1].request_id: 0},
458
sampled_token_ids=[[0]],
459
+ spec_token_ids=None,
460
logprobs=None,
461
prompt_logprobs_dict={},
462
)
vllm/v1/core/scheduler.py
+4
-7
@@ -474,6 +474,7 @@ class Scheduler:
474
model_runner_output: "ModelRunnerOutput",
475
) -> EngineCoreOutputs:
476
sampled_token_ids = model_runner_output.sampled_token_ids
477
+ spec_token_ids = model_runner_output.spec_token_ids
478
logprobs = model_runner_output.logprobs
479
prompt_logprobs_dict = model_runner_output.prompt_logprobs_dict
480
num_scheduled_tokens = scheduler_output.num_scheduled_tokens
@@ -530,13 +531,9 @@ class Scheduler:
531
self.encoder_cache_manager.free_encoder_input(
532
request, input_id)
533
533
- if request.num_computed_tokens >= request.num_tokens:
534
- # Clear the spec tokens as the request has generated
535
- # a new token. Here, We assume all spec tokens are verified
536
- # if we perform speculative decoding for this request.
537
- # Therefore, we can clear all spec tokens after
538
- # the generation step.
539
- request.clear_spec_tokens()
534
+ # Add newly generated spec token ids to the request.
535
+ if spec_token_ids is not None:
536
+ request.spec_token_ids = spec_token_ids[req_index]
537
538
# Get prompt logprobs for this request.
539
prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id)
vllm/v1/engine/core.py
-30
@@ -27,7 +27,6 @@ from vllm.v1.executor.abstract import Executor
27
from vllm.v1.outputs import ModelRunnerOutput
28
from vllm.v1.request import Request, RequestStatus
29
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
30
-from vllm.v1.spec_decode.ngram_proposer import NgramProposer
30
from vllm.version import __version__ as VLLM_VERSION
31
32
logger = init_logger(__name__)
@@ -86,15 +85,6 @@ class EngineCore:
85
self.batch_queue_size)
86
self.batch_queue = queue.Queue(self.batch_queue_size)
87
89
- # Setup speculative decode.
90
- # TODO: find a better way to check if we are using ngram.
91
- self.use_spec_decode = False
92
- if self.scheduler.speculative_config:
93
- assert self.scheduler.speculative_config.ngram_prompt_lookup_min \
94
- , "Only ngram spec decode is supported in V1."
95
- self.proposer = NgramProposer()
96
- self.use_spec_decode = True
97
-
88
def _initialize_kv_caches(self,
89
vllm_config: VllmConfig) -> Tuple[int, int]:
90
start = time.time()
@@ -158,9 +148,6 @@ class EngineCore:
148
return EngineCoreOutputs(
149
outputs=[], scheduler_stats=self.scheduler.make_stats())
150
161
- if self.use_spec_decode:
162
- self.propose_tokens()
163
-
151
scheduler_output = self.scheduler.schedule()
152
output = self.model_executor.execute_model(scheduler_output)
153
engine_core_outputs = self.scheduler.update_from_output(
@@ -221,23 +208,6 @@ class EngineCore:
208
def profile(self, is_start: bool = True):
209
self.model_executor.profile(is_start)
210
224
- def propose_tokens(self):
225
- assert self.scheduler.speculative_config is not None
226
- for req in self.scheduler.running:
227
- # Ignore requests that are doing chunked prefill.
228
- if req.num_computed_tokens < req.num_tokens - 1:
229
- continue
230
- # Ignore requests that already have spec tokens.
231
- if req.spec_token_ids:
232
- continue
233
- spec_tokens = self.proposer.propose(
234
- req.all_token_ids,
235
- self.scheduler.speculative_config.ngram_prompt_lookup_min,
236
- self.scheduler.speculative_config.num_speculative_tokens,
237
- )
238
- if spec_tokens:
239
- req.append_spec_token_ids(spec_tokens)
240
-
211
def reset_prefix_cache(self):
212
self.scheduler.reset_prefix_cache()
213
vllm/v1/outputs.py
+3
@@ -67,6 +67,9 @@ class ModelRunnerOutput:
67
# each request due to speculative/jump decoding.
68
sampled_token_ids: List[List[int]]
69
70
+ # num_reqs x num_spec_tokens
71
+ spec_token_ids: Optional[List[List[int]]]
72
+
73
# [num_reqs, max_num_logprobs + 1]
74
# [num_reqs, max_num_logprobs + 1]
75
# [num_reqs]
vllm/v1/request.py
-12
@@ -104,18 +104,6 @@ class Request:
104
self._output_token_ids.extend(token_ids)
105
self._all_token_ids.extend(token_ids)
106
107
- def append_spec_token_ids(
108
- self,
109
- token_ids: Union[int, List[int]],
110
- ) -> None:
111
- if isinstance(token_ids, int):
112
- self.spec_token_ids.append(token_ids)
113
- else:
114
- self.spec_token_ids.extend(token_ids)
115
-
116
- def clear_spec_tokens(self) -> None:
117
- self.spec_token_ids.clear()
118
-
107
@property
108
def num_tokens(self) -> int:
109
return len(self._all_token_ids)
vllm/v1/spec_decode/ngram_proposer.py
+15
-8
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
from typing import List, Optional
3
4
-from vllm.v1.utils import ConstantList
4
+import numpy as np
5
6
7
class NgramProposer:
@@ -9,8 +9,12 @@ class NgramProposer:
9
def __init__(self):
10
pass
11
12
- def propose(self, context_token_ids: ConstantList[int], n: int,
13
- k: int) -> Optional[List[int]]:
12
+ def propose(
13
+ self,
14
+ context_token_ids: np.ndarray,
15
+ n: int,
16
+ k: int,
17
+ ) -> Optional[np.ndarray]:
18
"""Proposes the next sequence of tokens based on n-gram pattern
19
matching in the context. The function finds matches of the last n
20
tokens in the previous context, and returns k tokens that followed
@@ -25,8 +29,8 @@ class NgramProposer:
29
the maximum amount of tokens until the end.
30
31
Returns:
28
- List[int]: The sequence of tokens that followed
29
- the matched n-gram in the context.
32
+ np.ndarray: The sequence of tokens that followed
33
+ the matched n-gram in the context.
34
None: If no matching n-gram pattern is found.
35
36
Example:
@@ -66,9 +70,12 @@ class NgramProposer:
70
return lps
71
72
@staticmethod
69
- def _find_subarray_kmp(context_token_ids: ConstantList[int], n: int,
70
- k: int) -> Optional[List[int]]:
71
- context_len = len(context_token_ids)
73
+ def _find_subarray_kmp(
74
+ context_token_ids: np.ndarray,
75
+ n: int,
76
+ k: int,
77
+ ) -> Optional[np.ndarray]:
78
+ context_len = context_token_ids.shape[0]
79
assert n > 0
80
81
pattern = context_token_ids[-n:]
vllm/v1/worker/gpu_input_batch.py
+7
@@ -78,6 +78,7 @@ class InputBatch:
78
)
79
self.token_ids_cpu = self.token_ids_cpu_tensor.numpy()
80
self.num_tokens = np.zeros(max_num_reqs, dtype=np.int32)
81
+ self.num_tokens_no_spec = np.zeros(max_num_reqs, dtype=np.int32)
82
self.num_prompt_tokens = np.zeros(max_num_reqs, dtype=np.int32)
83
self.num_computed_tokens_cpu = np.empty(max_num_reqs, dtype=np.int32)
84
@@ -217,7 +218,11 @@ class InputBatch:
218
end_idx = start_idx + len(request.output_token_ids)
219
self.token_ids_cpu[req_index,
220
start_idx:end_idx] = request.output_token_ids
221
+ # Number of token ids in token_ids_cpu.
222
+ # NOTE(woosuk): This may include spec decode tokens.
223
self.num_tokens[req_index] = request.num_tokens
224
+ # Number of tokens without spec decode tokens.
225
+ self.num_tokens_no_spec[req_index] = request.num_tokens
226
227
self.num_computed_tokens_cpu[req_index] = request.num_computed_tokens
228
self.block_table.add_row(req_index, request.block_ids)
@@ -356,6 +361,8 @@ class InputBatch:
361
self.token_ids_cpu[empty_index, :num_tokens] = self.token_ids_cpu[
362
last_req_index, :num_tokens]
363
self.num_tokens[empty_index] = num_tokens
364
+ self.num_tokens_no_spec[empty_index] = self.num_tokens_no_spec[
365
+ last_req_index]
366
self.num_prompt_tokens[empty_index] = self.num_prompt_tokens[
367
last_req_index]
368
self.num_computed_tokens_cpu[
vllm/v1/worker/gpu_model_runner.py
+47
@@ -33,6 +33,7 @@ from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
33
from vllm.v1.outputs import LogprobsTensors, ModelRunnerOutput
34
from vllm.v1.sample.metadata import SamplingMetadata
35
from vllm.v1.sample.rejection_sampler import INVALID_TOKEN_ID
36
+from vllm.v1.spec_decode.ngram_proposer import NgramProposer
37
from vllm.v1.utils import bind_kv_cache
38
from vllm.v1.worker.gpu_input_batch import CachedRequestState, InputBatch
39
from vllm.v1.worker.lora_model_runner_mixin import LoRAModelRunnerMixin
@@ -117,6 +118,15 @@ class GPUModelRunner(LoRAModelRunnerMixin):
118
# req_id -> (input_id -> encoder_output)
119
self.encoder_cache: Dict[str, Dict[int, torch.Tensor]] = {}
120
121
+ # Set up speculative decoding.
122
+ self.use_spec_decode = False
123
+ if self.speculative_config:
124
+ # TODO: find a better way to check if we are using ngram.
125
+ assert self.speculative_config.ngram_prompt_lookup_min, \
126
+ "Currently, only ngram spec decode is supported in V1."
127
+ self.drafter = NgramProposer()
128
+ self.use_spec_decode = True
129
+
130
# Request states.
131
self.requests: Dict[str, CachedRequestState] = {}
132
# Persistent batch.
@@ -367,6 +377,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
377
self.input_batch.token_ids_cpu[
378
req_index,
379
start_token_index:end_token_index] = req_data.new_token_ids
380
+ self.input_batch.num_tokens_no_spec[req_index] = end_token_index
381
# Add spec_token_ids to token_ids_cpu.
382
spec_token_ids = scheduler_output.scheduled_spec_decode_tokens.get(
383
req_id, [])
@@ -1009,15 +1020,51 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1020
for seq in sampled_token_ids[valid_mask].split(gen_lens)
1021
]
1022
1023
+ if not self.use_spec_decode:
1024
+ spec_token_ids = None
1025
+ else:
1026
+ spec_token_ids = self.generate_draft_token_ids(
1027
+ valid_sampled_token_ids)
1028
+
1029
model_runner_output = ModelRunnerOutput(
1030
req_ids=req_ids,
1031
req_id_to_index=self.input_batch.req_id_to_index,
1032
sampled_token_ids=valid_sampled_token_ids,
1033
+ spec_token_ids=spec_token_ids,
1034
logprobs=logprobs_lists,
1035
prompt_logprobs_dict=prompt_logprobs_dict,
1036
)
1037
return model_runner_output
1038
1039
+ def generate_draft_token_ids(
1040
+ self,
1041
+ sampled_token_ids: List[List[int]],
1042
+ ) -> List[List[int]]:
1043
+ # TODO(woosuk): Optimize.
1044
+ num_reqs = len(sampled_token_ids)
1045
+ draft_token_ids: List[List[int]] = []
1046
+ for i in range(num_reqs):
1047
+ if len(sampled_token_ids[i]) == 0:
1048
+ # Skip speculative decoding.
1049
+ draft_token_ids.append([])
1050
+ continue
1051
+
1052
+ # Add sampled_token_ids to token_ids_cpu.
1053
+ start_idx = self.input_batch.num_tokens_no_spec[i]
1054
+ end_idx = start_idx + len(sampled_token_ids[i])
1055
+ self.input_batch.token_ids_cpu[
1056
+ i, start_idx:end_idx] = sampled_token_ids[i]
1057
+ drafter_output = self.drafter.propose(
1058
+ self.input_batch.token_ids_cpu[i, :end_idx],
1059
+ self.speculative_config.ngram_prompt_lookup_min,
1060
+ self.speculative_config.num_speculative_tokens,
1061
+ )
1062
+ if drafter_output is None or len(drafter_output) == 0:
1063
+ draft_token_ids.append([])
1064
+ else:
1065
+ draft_token_ids.append(drafter_output.tolist())
1066
+ return draft_token_ids
1067
+
1068
def load_model(self) -> None:
1069
logger.info("Starting to load model %s...", self.model_config.model)
1070
with DeviceMemoryProfiler() as m: # noqa: SIM117
vllm/v1/worker/tpu_model_runner.py
+1
@@ -696,6 +696,7 @@ class TPUModelRunner:
696
req_ids=all_req_ids,
697
req_id_to_index=self.input_batch.req_id_to_index,
698
sampled_token_ids=[[token_id] for token_id in sampled_token_ids],
699
+ spec_token_ids=None,
700
logprobs=None,
701
prompt_logprobs_dict=prompt_logprobs_dict, # type: ignore[arg-type]
702
)