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 14, 05:30 UTC
[CORE] Prompt Embeddings Support for v1 Engine (#24278)
Signed-off-by: Andrew Sansom <andrew@protopia.ai> Signed-off-by: Andrew Sansom <qthequartermasterman@gmail.com> Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
Andrew Sansom committed
Sep 18, 2025 at 19:03 UTC
9a4600e4dcbbd13988c31d5198d3ab8b4172ecca
20 files changed
+304
-75
tests/basic_correctness/test_basic_correctness.py
-10
@@ -76,11 +76,6 @@ def test_models(
76
model_executor: str,
77
enable_prompt_embeds: bool,
78
) -> None:
79
-
80
- if enable_prompt_embeds and envs.is_set(
81
- "VLLM_USE_V1") and envs.VLLM_USE_V1:
82
- pytest.skip("enable_prompt_embeds is not supported in v1.")
83
-
79
if not envs.VLLM_USE_V1:
80
if async_scheduling:
81
pytest.skip("async_scheduling only supported in v1.")
@@ -164,11 +159,6 @@ def test_models_distributed(
159
extra_env: dict[str, str],
160
enable_prompt_embeds: bool,
161
) -> None:
167
-
168
- if enable_prompt_embeds and envs.is_set(
169
- "VLLM_USE_V1") and envs.VLLM_USE_V1:
170
- pytest.skip("enable_prompt_embeds is not supported in v1.")
171
-
162
if test_suite != TARGET_TEST_SUITE:
163
pytest.skip(f"Skip test for {test_suite}")
164
tests/entrypoints/openai/test_completion_with_prompt_embeds.py
-1
@@ -36,7 +36,6 @@ def default_server_args() -> list[str]:
36
"--enforce-eager",
37
# Prompt Embeds server args
38
"--enable-prompt-embeds",
39
- "--no-enable-chunked-prefill",
39
]
40
41
tests/models/language/generation/test_common.py
-6
@@ -125,12 +125,6 @@ def test_models(hf_runner, vllm_runner, example_prompts, model: str,
125
# in parts of the operators
126
pytest.skip(f"Skipping '{model}' model test with AITER kernel.")
127
128
- # Note: can be removed when
129
- # https://github.com/vllm-project/vllm/pull/24278 finished
130
- if current_platform.is_cpu() and use_prompt_embeds:
131
- pytest.skip("Skipping use_prompt_embeds=True with "
132
- "V1-only CPU backend.")
133
-
128
with hf_runner(model) as hf_model:
129
hf_outputs = hf_model.generate_greedy_logprobs_limit(
130
example_prompts, max_tokens, num_logprobs)
vllm/engine/arg_utils.py
+18
-6
@@ -1513,12 +1513,6 @@ class EngineArgs:
1513
recommend_to_remove=False)
1514
return False
1515
1516
- # No text embedding inputs so far.
1517
- if self.enable_prompt_embeds:
1518
- _raise_or_fallback(feature_name="--enable-prompt-embeds",
1519
- recommend_to_remove=False)
1520
- return False
1521
-
1516
# No Mamba or Encoder-Decoder so far.
1517
if not model_config.is_v1_compatible:
1518
_raise_or_fallback(feature_name=model_config.architectures,
@@ -1651,6 +1645,13 @@ class EngineArgs:
1645
"models in V0 and has been disabled.")
1646
self.enable_prefix_caching = False
1647
1648
+ if self.enable_prompt_embeds:
1649
+ logger.warning(
1650
+ "--enable-prompt-embeds and --enable-prefix-caching "
1651
+ "are not supported together in V0. Prefix caching has "
1652
+ "been disabled.")
1653
+ self.enable_prefix_caching = False
1654
+
1655
# Set max_num_seqs to 256 for VLLM_V0.
1656
if self.max_num_seqs is None:
1657
self.max_num_seqs = 256
@@ -1664,6 +1665,17 @@ class EngineArgs:
1665
# For pooling tasks the default is False
1666
if model_config.runner_type != "pooling":
1667
self.enable_chunked_prefill = True
1668
+
1669
+ # TODO: When prefix caching supports prompt embeds inputs, this
1670
+ # check can be removed.
1671
+ if (self.enable_prompt_embeds
1672
+ and self.enable_prefix_caching is not False):
1673
+ logger.warning(
1674
+ "--enable-prompt-embeds and --enable-prefix-caching "
1675
+ "are not supported together in V1. Prefix caching has "
1676
+ "been disabled.")
1677
+ self.enable_prefix_caching = False
1678
+
1679
if self.enable_prefix_caching is None:
1680
self.enable_prefix_caching = True
1681
else:
vllm/entrypoints/openai/protocol.py
+1
-1
@@ -973,7 +973,6 @@ class CompletionRequest(OpenAIBaseModel):
973
# https://platform.openai.com/docs/api-reference/completions/create
974
model: Optional[str] = None
975
prompt: Optional[Union[list[int], list[list[int]], str, list[str]]] = None
976
- prompt_embeds: Optional[Union[bytes, list[bytes]]] = None
976
best_of: Optional[int] = None
977
echo: Optional[bool] = False
978
frequency_penalty: Optional[float] = 0.0
@@ -1009,6 +1008,7 @@ class CompletionRequest(OpenAIBaseModel):
1008
# --8<-- [end:completion-sampling-params]
1009
1010
# --8<-- [start:completion-extra-params]
1011
+ prompt_embeds: Optional[Union[bytes, list[bytes]]] = None
1012
add_special_tokens: bool = Field(
1013
default=True,
1014
description=(
vllm/utils/__init__.py
+27
@@ -3443,3 +3443,30 @@ def decorate_logs(process_name: Optional[str] = None) -> None:
3443
pid = os.getpid()
3444
_add_prefix(sys.stdout, process_name, pid)
3445
_add_prefix(sys.stderr, process_name, pid)
3446
+
3447
+
3448
+def length_from_prompt_token_ids_or_embeds(
3449
+ prompt_token_ids: Optional[list[int]],
3450
+ prompt_embeds: Optional[torch.Tensor],
3451
+) -> int:
3452
+ """Calculate the request length (in number of tokens) give either
3453
+ prompt_token_ids or prompt_embeds.
3454
+ """
3455
+ prompt_token_len = None if prompt_token_ids is None else len(
3456
+ prompt_token_ids)
3457
+ prompt_embeds_len = \
3458
+ None if prompt_embeds is None else len(prompt_embeds)
3459
+
3460
+ if prompt_token_len is None:
3461
+ if prompt_embeds_len is None:
3462
+ raise ValueError(
3463
+ "Neither prompt_token_ids nor prompt_embeds were defined.")
3464
+ return prompt_embeds_len
3465
+ else:
3466
+ if (prompt_embeds_len is not None
3467
+ and prompt_embeds_len != prompt_token_len):
3468
+ raise ValueError(
3469
+ "Prompt token ids and prompt embeds had different lengths"
3470
+ f" prompt_token_ids={prompt_token_len}"
3471
+ f" prompt_embeds={prompt_embeds_len}")
3472
+ return prompt_token_len
vllm/v1/core/sched/output.py
+18
-6
@@ -11,6 +11,7 @@ from vllm._bc_linter import bc_linter_include
11
if TYPE_CHECKING:
12
import numpy as np
13
import numpy.typing as npt
14
+ import torch
15
16
from vllm.distributed.kv_transfer.kv_connector.v1.base import (
17
KVConnectorMetadata)
@@ -26,13 +27,14 @@ if TYPE_CHECKING:
27
class NewRequestData:
28
29
req_id: str
29
- prompt_token_ids: list[int]
30
+ prompt_token_ids: Optional[list[int]]
31
mm_features: list[MultiModalFeatureSpec]
32
sampling_params: Optional[SamplingParams]
33
pooling_params: Optional[PoolingParams]
34
block_ids: tuple[list[int], ...]
35
num_computed_tokens: int
36
lora_request: Optional[LoRARequest]
37
+ prompt_embeds: Optional[torch.Tensor] = None
38
39
@classmethod
40
def from_request(
@@ -49,9 +51,12 @@ class NewRequestData:
51
block_ids=block_ids,
52
num_computed_tokens=request.num_computed_tokens,
53
lora_request=request.lora_request,
54
+ prompt_embeds=request.prompt_embeds,
55
)
56
54
- def __repr__(self):
57
+ def __repr__(self) -> str:
58
+ prompt_embeds_shape = (self.prompt_embeds.shape
59
+ if self.prompt_embeds else None)
60
return (f"NewRequestData("
61
f"req_id={self.req_id},"
62
f"prompt_token_ids={self.prompt_token_ids},"
@@ -59,19 +64,26 @@ class NewRequestData:
64
f"sampling_params={self.sampling_params},"
65
f"block_ids={self.block_ids},"
66
f"num_computed_tokens={self.num_computed_tokens},"
62
- f"lora_request={self.lora_request}"
67
+ f"lora_request={self.lora_request},"
68
+ f"prompt_embeds_shape={prompt_embeds_shape}"
69
")")
70
71
# Version of __repr__ with the prompt data obfuscated
66
- def anon_repr(self):
72
+ def anon_repr(self) -> str:
73
+ prompt_token_ids_len = len(
74
+ self.prompt_token_ids
75
+ ) if self.prompt_token_ids is not None else None
76
+ prompt_embeds_shape = (self.prompt_embeds.shape
77
+ if self.prompt_embeds else None)
78
return (f"NewRequestData("
79
f"req_id={self.req_id},"
69
- f"prompt_token_ids_len={len(self.prompt_token_ids)},"
80
+ f"prompt_token_ids_len={prompt_token_ids_len},"
81
f"mm_features={self.mm_features},"
82
f"sampling_params={self.sampling_params},"
83
f"block_ids={self.block_ids},"
84
f"num_computed_tokens={self.num_computed_tokens},"
74
- f"lora_request={self.lora_request}"
85
+ f"lora_request={self.lora_request},"
86
+ f"prompt_embeds_shape={prompt_embeds_shape}"
87
")")
88
89
vllm/v1/engine/__init__.py
+2
-1
@@ -47,7 +47,7 @@ class EngineCoreRequest(
47
gc=False): # type: ignore[call-arg]
48
49
request_id: str
50
- prompt_token_ids: list[int]
50
+ prompt_token_ids: Optional[list[int]]
51
mm_features: Optional[list[MultiModalFeatureSpec]]
52
sampling_params: Optional[SamplingParams]
53
pooling_params: Optional[PoolingParams]
@@ -56,6 +56,7 @@ class EngineCoreRequest(
56
lora_request: Optional[LoRARequest]
57
cache_salt: Optional[str]
58
data_parallel_rank: Optional[int]
59
+ prompt_embeds: Optional[torch.Tensor] = None
60
61
# Index of the client, used to ensure outputs are sent back to the same
62
# client for this request when scaling out the front-end.
vllm/v1/engine/detokenizer.py
+21
-10
@@ -13,6 +13,7 @@ from vllm.engine.output_processor.stop_checker import StopChecker
13
from vllm.logger import init_logger
14
from vllm.transformers_utils.detokenizer_utils import (
15
AnyTokenizer, convert_prompt_ids_to_tokens, detokenize_incrementally)
16
+from vllm.utils import length_from_prompt_token_ids_or_embeds
17
from vllm.v1.engine import EngineCoreRequest
18
19
logger = init_logger(__name__)
@@ -179,11 +180,12 @@ class FastIncrementalDetokenizer(BaseIncrementalDetokenizer):
180
self.tokenizer: Tokenizer = tokenizer._tokenizer
181
182
# Find a safe place to start.
182
- prompt_suffix = request.prompt_token_ids
183
+ prompt_token_ids = request.prompt_token_ids or []
184
+ prompt_suffix = prompt_token_ids
185
prompt_len = len(prompt_suffix)
186
if prompt_len > 4:
187
for i in range(4, min(prompt_len + 1, 24)):
186
- suffix = request.prompt_token_ids[-i:]
188
+ suffix = prompt_token_ids[-i:]
189
if '�' not in self.tokenizer.decode(suffix):
190
prompt_suffix = suffix
191
break
@@ -260,16 +262,25 @@ class SlowIncrementalDetokenizer(BaseIncrementalDetokenizer):
262
params = request.sampling_params
263
assert params is not None
264
265
+ self.prompt_len = length_from_prompt_token_ids_or_embeds(
266
+ request.prompt_token_ids, request.prompt_embeds)
267
+
268
# Metadata for incremental detokenization.
264
- self.tokens, self.prefix_offset, self.read_offset = (
265
- convert_prompt_ids_to_tokens(
266
- tokenizer=tokenizer,
267
- prompt_ids=request.prompt_token_ids,
268
- skip_special_tokens=params.skip_special_tokens,
269
- ))
269
+ if request.prompt_token_ids is not None:
270
+ self.tokens, self.prefix_offset, self.read_offset = (
271
+ convert_prompt_ids_to_tokens(
272
+ tokenizer=tokenizer,
273
+ prompt_ids=request.prompt_token_ids,
274
+ skip_special_tokens=params.skip_special_tokens,
275
+ ))
276
+ else:
277
+ # Prompt embedding requests cannot be detokenized, in general.
278
+ self.tokens = [""] * self.prompt_len
279
+ self.prefix_offset = 0
280
+ self.read_offest = 0
281
271
- self.token_ids.extend(request.prompt_token_ids)
272
- self.prompt_len = len(request.prompt_token_ids)
282
+ self.token_ids.extend(request.prompt_token_ids
283
+ or [0] * self.prompt_len)
284
285
self.skip_special_tokens = params.skip_special_tokens
286
self.spaces_between_special_tokens = (
vllm/v1/engine/output_processor.py
+20
-5
@@ -14,6 +14,7 @@ from vllm.sampling_params import RequestOutputKind
14
from vllm.tracing import (SpanAttributes, SpanKind, Tracer,
15
extract_trace_context)
16
from vllm.transformers_utils.tokenizer import AnyTokenizer
17
+from vllm.utils import length_from_prompt_token_ids_or_embeds
18
from vllm.v1.engine import EngineCoreOutput, EngineCoreRequest, FinishReason
19
from vllm.v1.engine.detokenizer import IncrementalDetokenizer
20
from vllm.v1.engine.logprobs import LogprobsProcessor
@@ -86,7 +87,8 @@ class RequestState:
87
lora_name: Optional[str],
88
output_kind: RequestOutputKind,
89
prompt: Optional[str],
89
- prompt_token_ids: list[int],
90
+ prompt_token_ids: Optional[list[int]],
91
+ prompt_embeds: Optional[torch.Tensor],
92
logprobs_processor: Optional[LogprobsProcessor],
93
detokenizer: Optional[IncrementalDetokenizer],
94
max_tokens_param: Optional[int],
@@ -104,7 +106,9 @@ class RequestState:
106
self.output_kind = output_kind
107
self.prompt = prompt
108
self.prompt_token_ids = prompt_token_ids
107
- self.prompt_len = len(prompt_token_ids)
109
+ self.prompt_embeds = prompt_embeds
110
+ self.prompt_len = length_from_prompt_token_ids_or_embeds(
111
+ self.prompt_token_ids, self.prompt_embeds)
112
self.logprobs_processor = logprobs_processor
113
self.detokenizer = detokenizer
114
self.max_tokens_param = max_tokens_param
@@ -165,6 +169,7 @@ class RequestState:
169
output_kind=output_kind,
170
prompt=prompt,
171
prompt_token_ids=request.prompt_token_ids,
172
+ prompt_embeds=request.prompt_embeds,
173
logprobs_processor=logprobs_processor,
174
detokenizer=detokenizer,
175
max_tokens_param=max_tokens_param,
@@ -223,6 +228,8 @@ class RequestState:
228
first_output = outputs[0]
229
if isinstance(first_output, PoolingOutput):
230
assert len(outputs) == 1
231
+ # Prompt embeddings are currently not supported by pooling requests.
232
+ assert self.prompt_token_ids is not None
233
return PoolingRequestOutput(
234
request_id=request_id,
235
outputs=first_output,
@@ -236,10 +243,15 @@ class RequestState:
243
else:
244
prompt_logprobs = self.logprobs_processor.prompt_logprobs
245
246
+ # If prompt embeds were used, put placeholder prompt token ids
247
+ prompt_token_ids = self.prompt_token_ids
248
+ if prompt_token_ids is None and self.prompt_embeds is not None:
249
+ prompt_token_ids = [0] * len(self.prompt_embeds)
250
+
251
return RequestOutput(
252
request_id=request_id,
253
prompt=self.prompt,
242
- prompt_token_ids=self.prompt_token_ids,
254
+ prompt_token_ids=prompt_token_ids,
255
prompt_logprobs=prompt_logprobs,
256
outputs=cast(list[CompletionOutput], outputs),
257
finished=finished,
@@ -469,6 +481,8 @@ class OutputProcessor:
481
482
arrival_time_nano_seconds = int(req_state.stats.arrival_time * 1e9)
483
trace_context = extract_trace_context(engine_core_output.trace_headers)
484
+ prompt_length = length_from_prompt_token_ids_or_embeds(
485
+ req_state.prompt_token_ids, req_state.prompt_embeds)
486
with (self.tracer.start_as_current_span(
487
"llm_request",
488
kind=SpanKind.SERVER,
@@ -488,7 +502,7 @@ class OutputProcessor:
502
span.set_attribute(SpanAttributes.GEN_AI_LATENCY_TIME_IN_QUEUE,
503
queued_time)
504
span.set_attribute(SpanAttributes.GEN_AI_USAGE_PROMPT_TOKENS,
491
- len(req_state.prompt_token_ids))
505
+ prompt_length)
506
span.set_attribute(SpanAttributes.GEN_AI_USAGE_COMPLETION_TOKENS,
507
metrics.num_generation_tokens)
508
span.set_attribute(
@@ -544,7 +558,8 @@ class OutputProcessor:
558
assert req_state.stats is not None
559
iteration_stats.update_from_finished_request(
560
finish_reason=finish_reason,
547
- num_prompt_tokens=len(req_state.prompt_token_ids),
561
+ num_prompt_tokens=length_from_prompt_token_ids_or_embeds(
562
+ req_state.prompt_token_ids, req_state.prompt_embeds),
563
max_tokens_param=req_state.max_tokens_param,
564
req_stats=req_state.stats)
565
self.lora_states.finish_request(req_state)
vllm/v1/engine/processor.py
+29
-9
@@ -19,6 +19,7 @@ from vllm.multimodal.utils import argsort_mm_positions
19
from vllm.pooling_params import PoolingParams
20
from vllm.sampling_params import SamplingParams
21
from vllm.transformers_utils.tokenizer import AnyTokenizer
22
+from vllm.utils import length_from_prompt_token_ids_or_embeds
23
from vllm.v1.engine import EngineCoreRequest
24
from vllm.v1.structured_output.backend_guidance import (
25
validate_guidance_grammar)
@@ -390,6 +391,16 @@ class Processor:
391
self._validate_model_inputs(processed_inputs)
392
393
encoder_inputs, decoder_inputs = split_enc_dec_inputs(processed_inputs)
394
+ # Mypy does not always properly infer the types of some elements of
395
+ # discriminated unions of TypedDicts, because of how it handles
396
+ # inheritance of TypedDict. If we explicitly extract the items we want
397
+ # we can avoid type errors from using `dict.get` later in the method.
398
+ prompt_str: Optional[str] = None if decoder_inputs[
399
+ "type"] == "embeds" else decoder_inputs.get("prompt")
400
+ prompt_token_ids = decoder_inputs[
401
+ "prompt_token_ids"] if decoder_inputs["type"] != "embeds" else None
402
+ prompt_embeds = decoder_inputs["prompt_embeds"] if decoder_inputs[
403
+ "type"] == "embeds" else None
404
405
sampling_params = None
406
pooling_params = None
@@ -398,9 +409,10 @@ class Processor:
409
sampling_params = params.clone()
410
# If unset max tokens, then generate up to the max_model_len.
411
if sampling_params.max_tokens is None:
401
- sampling_params.max_tokens = (
402
- self.model_config.max_model_len -
403
- len(decoder_inputs["prompt_token_ids"]))
412
+ seq_len = length_from_prompt_token_ids_or_embeds(
413
+ prompt_token_ids, prompt_embeds)
414
+ sampling_params.max_tokens = \
415
+ self.model_config.max_model_len - seq_len
416
sampling_params.update_from_generation_config(
417
self.generation_config_fields, eos_token_id)
418
if self.tokenizer is not None:
@@ -430,9 +442,10 @@ class Processor:
442
identifier=decoder_mm_hashes[modality][idx],
443
mm_position=decoder_mm_positions[modality][idx]))
444
433
- return decoder_inputs.get("prompt"), EngineCoreRequest(
445
+ return prompt_str, EngineCoreRequest(
446
request_id=request_id,
435
- prompt_token_ids=decoder_inputs["prompt_token_ids"],
447
+ prompt_token_ids=prompt_token_ids,
448
+ prompt_embeds=prompt_embeds,
449
mm_features=mm_features,
450
sampling_params=sampling_params,
451
pooling_params=pooling_params,
@@ -461,10 +474,17 @@ class Processor:
474
):
475
model_config = self.model_config
476
464
- prompt_ids = prompt_inputs["prompt_token_ids"]
477
+ prompt_ids = None if prompt_inputs[
478
+ "type"] == "embeds" else prompt_inputs["prompt_token_ids"]
479
+ prompt_embeds = prompt_inputs["prompt_embeds"] if prompt_inputs[
480
+ "type"] == "embeds" else None
481
+ prompt_len = length_from_prompt_token_ids_or_embeds(
482
+ prompt_ids, prompt_embeds)
483
if not prompt_ids:
484
if prompt_type == "encoder" and model_config.is_multimodal_model:
485
pass # Mllama may have empty encoder inputs for text-only data
486
+ elif prompt_inputs["type"] == "embeds":
487
+ pass # Prompt embeds should not have prompt_ids.
488
else:
489
raise ValueError(f"The {prompt_type} prompt cannot be empty")
490
@@ -472,7 +492,7 @@ class Processor:
492
tokenizer = None
493
else:
494
tokenizer = self.tokenizer
475
- max_input_id = max(prompt_ids, default=0)
495
+ max_input_id = max(prompt_ids or [], default=0)
496
497
# NOTE: tokenizer.max_token_id is the tokenizer’s vocab size while
498
# self.model_config.get_vocab_size() is the model’s vocab size.
@@ -490,7 +510,7 @@ class Processor:
510
f"Token id {max_input_id} is out of vocabulary")
511
512
max_prompt_len = self.model_config.max_model_len
493
- if len(prompt_ids) > max_prompt_len:
513
+ if prompt_len > max_prompt_len:
514
if prompt_type == "encoder" and model_config.is_multimodal_model:
515
mm_registry = self.input_preprocessor.mm_registry
516
mm_processor = mm_registry.create_processor(
@@ -514,7 +534,7 @@ class Processor:
534
"number of text tokens.")
535
536
raise ValueError(
517
- f"The {prompt_type} prompt (length {len(prompt_ids)}) is "
537
+ f"The {prompt_type} prompt (length {prompt_len}) is "
538
f"longer than the maximum model length of {max_prompt_len}. "
539
f"{suggestion}")
540
vllm/v1/request.py
+12
-3
@@ -7,9 +7,12 @@ from collections.abc import Mapping
7
from functools import partial
8
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
9
10
+import torch
11
+
12
from vllm.multimodal.inputs import MultiModalFeatureSpec
13
from vllm.pooling_params import PoolingParams
14
from vllm.sampling_params import SamplingParams
15
+from vllm.utils import length_from_prompt_token_ids_or_embeds
16
from vllm.v1.engine import (EngineCoreEvent, EngineCoreEventType,
17
EngineCoreRequest, FinishReason)
18
from vllm.v1.structured_output.request import StructuredOutputRequest
@@ -25,12 +28,13 @@ class Request:
28
def __init__(
29
self,
30
request_id: str,
28
- prompt_token_ids: list[int],
31
+ prompt_token_ids: Optional[list[int]],
32
sampling_params: Optional[SamplingParams],
33
pooling_params: Optional[PoolingParams],
34
eos_token_id: Optional[int],
35
client_index: int = 0,
36
arrival_time: Optional[float] = None,
37
+ prompt_embeds: Optional[torch.Tensor] = None,
38
mm_features: Optional[list[MultiModalFeatureSpec]] = None,
39
lora_request: Optional["LoRARequest"] = None,
40
structured_output_request: Optional["StructuredOutputRequest"] = None,
@@ -79,9 +83,13 @@ class Request:
83
"sampling_params and pooling_params can't both be unset")
84
85
self.prompt_token_ids = prompt_token_ids
82
- self.num_prompt_tokens = len(self.prompt_token_ids)
86
+ self.prompt_embeds = prompt_embeds
87
+ self.num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
88
+ prompt_token_ids, prompt_embeds)
89
self._output_token_ids: list[int] = []
84
- self._all_token_ids: list[int] = self.prompt_token_ids.copy()
90
+ self._all_token_ids: list[int] = self.prompt_token_ids.copy(
91
+ ) if self.prompt_token_ids is not None else [0
92
+ ] * self.num_prompt_tokens
93
self.num_output_placeholders = 0 # Used in async scheduling.
94
self.spec_token_ids: list[int] = []
95
self.num_computed_tokens = 0
@@ -123,6 +131,7 @@ class Request:
131
request_id=request.request_id,
132
client_index=request.client_index,
133
prompt_token_ids=request.prompt_token_ids,
134
+ prompt_embeds=request.prompt_embeds,
135
mm_features=request.mm_features,
136
sampling_params=request.sampling_params,
137
pooling_params=request.pooling_params,
vllm/v1/sample/logits_processor/__init__.py
+1
-1
@@ -243,7 +243,7 @@ class AdapterLogitsProcessor(LogitsProcessor):
243
def _new_state(
244
self,
245
params: SamplingParams,
246
- prompt_ids: list[int],
246
+ prompt_ids: Optional[list[int]],
247
output_ids: list[int],
248
) -> Optional[partial[torch.Tensor]]:
249
"""Return state representation for new request
vllm/v1/sample/logits_processor/builtin.py
+4
-2
@@ -187,7 +187,8 @@ class MinTokensLogitsProcessor(LogitsProcessor):
187
188
@staticmethod
189
def add_request(
190
- params: SamplingParams, _: list[int], output_tok_ids: list[int]
190
+ params: SamplingParams, _: Optional[list[int]],
191
+ output_tok_ids: list[int]
192
) -> Optional[tuple[int, Sequence[int], set[int]]]:
193
min_tokens = params.min_tokens
194
if not min_tokens or len(output_tok_ids) >= min_tokens:
@@ -234,7 +235,8 @@ class MinTokensLogitsProcessor(LogitsProcessor):
235
236
def process_dict_updates(
237
req_entries: dict[int, T], batch_update: Optional[BatchUpdate],
237
- new_state: Callable[[SamplingParams, list[int], list[int]], Optional[T]]
238
+ new_state: Callable[[SamplingParams, Optional[list[int]], list[int]],
239
+ Optional[T]]
240
) -> bool:
241
"""Utility function to update dict state for sparse LogitsProcessors."""
242
vllm/v1/sample/logits_processor/interface.py
+1
-1
@@ -26,7 +26,7 @@ RemovedRequest = int
26
27
# (index, params, prompt_tok_ids, output_tok_ids) tuples for new
28
# requests added to the batch.
29
-AddedRequest = tuple[int, SamplingParams, list[int], list[int]]
29
+AddedRequest = tuple[int, SamplingParams, Optional[list[int]], list[int]]
30
31
# (index 1, index 2, directionality) tuples representing
32
# one-way moves or two-way swaps of requests in batch
vllm/v1/serial_utils.py
+1
-1
@@ -174,7 +174,7 @@ class MsgpackEncoder:
174
) -> tuple[str, tuple[int, ...], Union[int, memoryview]]:
175
assert self.aux_buffers is not None
176
# view the tensor as a contiguous 1D array of bytes
177
- arr = obj.flatten().contiguous().view(torch.uint8).numpy()
177
+ arr = obj.flatten().contiguous().cpu().view(torch.uint8).numpy()
178
if obj.nbytes < self.size_threshold:
179
# Smaller tensors are encoded inline, just like ndarrays.
180
data = msgpack.Ext(CUSTOM_TYPE_RAW_VIEW, arr.data)
vllm/v1/worker/gpu_input_batch.py
+48
-7
@@ -13,7 +13,7 @@ from vllm.lora.request import LoRARequest
13
from vllm.multimodal.inputs import MultiModalFeatureSpec, MultiModalKwargsItems
14
from vllm.pooling_params import PoolingParams
15
from vllm.sampling_params import SamplingParams, SamplingType
16
-from vllm.utils import swap_dict_values
16
+from vllm.utils import length_from_prompt_token_ids_or_embeds, swap_dict_values
17
from vllm.v1.outputs import LogprobsTensors
18
from vllm.v1.pool.metadata import PoolingMetadata
19
from vllm.v1.sample.logits_processor import (BatchUpdateBuilder,
@@ -29,7 +29,7 @@ from vllm.v1.worker.block_table import MultiGroupBlockTable
29
class CachedRequestState:
30
31
req_id: str
32
- prompt_token_ids: list[int]
32
+ prompt_token_ids: Optional[list[int]]
33
mm_features: list[MultiModalFeatureSpec]
34
sampling_params: Optional[SamplingParams]
35
pooling_params: Optional[PoolingParams]
@@ -43,9 +43,11 @@ class CachedRequestState:
43
mrope_position_delta: Optional[int] = None
44
45
lora_request: Optional[LoRARequest] = None
46
+ prompt_embeds: Optional[torch.Tensor] = None
47
48
def __post_init__(self):
48
- self.num_prompt_tokens = len(self.prompt_token_ids)
49
+ self.num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
50
+ self.prompt_token_ids, self.prompt_embeds)
51
52
@property
53
def num_tokens(self) -> int:
@@ -63,6 +65,10 @@ class CachedRequestState:
65
66
def get_token_id(self, idx: int) -> int:
67
if idx < self.num_prompt_tokens:
68
+ if self.prompt_token_ids is None:
69
+ raise ValueError(
70
+ f"Tried to access token index {idx}, but that token was "
71
+ "provided via prompt_embeds, and its ID is unknown.")
72
return self.prompt_token_ids[idx]
73
elif idx - self.num_prompt_tokens < len(self.output_token_ids):
74
return self.output_token_ids[idx - self.num_prompt_tokens]
@@ -109,6 +115,14 @@ class InputBatch:
115
pin_memory=False,
116
)
117
self.token_ids_cpu = self.token_ids_cpu_tensor.numpy()
118
+ self.is_token_ids = torch.zeros((max_num_reqs, max_model_len),
119
+ device="cpu",
120
+ dtype=bool,
121
+ pin_memory=False)
122
+ # Store prompt embeddings per request to avoid OOM from large upfront
123
+ # allocation if max_model_len is big.
124
+ # Maps req_index -> tensor of shape (num_prompt_tokens, hidden_size)
125
+ self.req_prompt_embeds: dict[int, torch.Tensor] = {}
126
self.num_tokens = np.zeros(max_num_reqs, dtype=np.int32)
127
self.num_tokens_no_spec = np.zeros(max_num_reqs, dtype=np.int32)
128
self.num_prompt_tokens = np.zeros(max_num_reqs, dtype=np.int32)
@@ -310,15 +324,23 @@ class InputBatch:
324
self.req_id_to_index[req_id] = req_index
325
326
# Copy the prompt token ids and output token ids.
313
- num_prompt_tokens = len(request.prompt_token_ids)
327
+ num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
328
+ request.prompt_token_ids, request.prompt_embeds)
329
self.num_prompt_tokens[req_index] = num_prompt_tokens
315
- self.token_ids_cpu[
316
- req_index, :num_prompt_tokens] = request.prompt_token_ids
330
start_idx = num_prompt_tokens
331
end_idx = start_idx + len(request.output_token_ids)
332
+ if request.prompt_token_ids is not None:
333
+ self.token_ids_cpu[
334
+ req_index, :num_prompt_tokens] = request.prompt_token_ids
335
+ self.is_token_ids[req_index, :num_prompt_tokens] = True
336
+ else:
337
+ self.is_token_ids[req_index, :num_prompt_tokens] = False
338
+ if request.prompt_embeds is not None:
339
+ self.req_prompt_embeds[req_index] = request.prompt_embeds
340
self.token_ids_cpu[req_index,
341
start_idx:end_idx] = request.output_token_ids
321
- # Number of token ids in token_ids_cpu.
342
+ self.is_token_ids[req_index, start_idx:end_idx] = True
343
+ # Number of token ids in prompt (token_ids_cpu or prompt_embeds).
344
# NOTE(woosuk): This may include spec decode tokens.
345
self.num_tokens[req_index] = request.num_tokens
346
# Number of tokens without spec decode tokens.
@@ -503,6 +525,20 @@ class InputBatch:
525
self.token_ids_cpu[i1, ...] = self.token_ids_cpu[i2, ...]
526
self.token_ids_cpu[i2, ...] = tmp
527
528
+ self.is_token_ids[[i1, i2], ...] = self.is_token_ids[[i2, i1], ...]
529
+
530
+ # Swap prompt embeddings if they exist
531
+ embeds_i1 = self.req_prompt_embeds.get(i1)
532
+ embeds_i2 = self.req_prompt_embeds.get(i2)
533
+ if embeds_i1 is not None:
534
+ self.req_prompt_embeds[i2] = embeds_i1
535
+ else:
536
+ self.req_prompt_embeds.pop(i2, None)
537
+ if embeds_i2 is not None:
538
+ self.req_prompt_embeds[i1] = embeds_i2
539
+ else:
540
+ self.req_prompt_embeds.pop(i1, None)
541
+
542
self.block_table.swap_row(i1, i2)
543
544
self.request_lora_mapping[i1], self.request_lora_mapping[i2] = \
@@ -592,6 +628,11 @@ class InputBatch:
628
num_tokens = self.num_tokens[last_req_index]
629
self.token_ids_cpu[empty_index, :num_tokens] = self.token_ids_cpu[
630
last_req_index, :num_tokens]
631
+ self.is_token_ids[empty_index, :num_tokens] = self.is_token_ids[
632
+ last_req_index, :num_tokens]
633
+ if last_req_index in self.req_prompt_embeds:
634
+ self.req_prompt_embeds[
635
+ empty_index] = self.req_prompt_embeds.pop(last_req_index)
636
self.num_tokens[empty_index] = num_tokens
637
self.num_tokens_no_spec[empty_index] = self.num_tokens_no_spec[
638
last_req_index]
vllm/v1/worker/gpu_model_runner.py
+96
-3
@@ -56,7 +56,9 @@ from vllm.sequence import IntermediateTensors, PoolerOutput
56
from vllm.tasks import GenerationTask, PoolingTask, SupportedTask
57
from vllm.utils import (STR_DTYPE_TO_TORCH_DTYPE, DeviceMemoryProfiler,
58
GiB_bytes, check_use_alibi, get_dtype_size,
59
- is_pin_memory_available, round_up, supports_dynamo)
59
+ is_pin_memory_available,
60
+ length_from_prompt_token_ids_or_embeds, round_up,
61
+ supports_dynamo)
62
from vllm.v1.attention.backends.flash_attn import AttentionMetadata
63
from vllm.v1.attention.backends.gdn_attn import GDNAttentionMetadataBuilder
64
from vllm.v1.attention.backends.utils import (
@@ -197,6 +199,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
199
cache_config.cache_dtype]
200
201
self.is_pooling_model = (model_config.runner_type == 'pooling')
202
+ self.enable_prompt_embeds = model_config.enable_prompt_embeds
203
self.is_multimodal_raw_input_only_model = (
204
model_config.is_multimodal_raw_input_only_model)
205
@@ -342,6 +345,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
345
self.hidden_size,
346
dtype=self.dtype,
347
numpy=False)
348
+ self.is_token_ids = self._make_buffer(self.max_num_tokens,
349
+ dtype=torch.bool)
350
self.discard_request_indices = self._make_buffer(self.max_num_reqs,
351
dtype=torch.int64)
352
self.num_discarded_requests = 0
@@ -574,6 +579,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
579
req_state = CachedRequestState(
580
req_id=req_id,
581
prompt_token_ids=new_req_data.prompt_token_ids,
582
+ prompt_embeds=new_req_data.prompt_embeds,
583
mm_features=new_req_data.mm_features,
584
sampling_params=sampling_params,
585
pooling_params=pooling_params,
@@ -819,6 +825,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
825
if self.input_batch.prev_sampled_token_ids is None:
826
# Normal scheduling case
827
self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
828
+ self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens)
829
+ self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)
830
return
831
832
# Async scheduling case, where some decode requests from the previous
@@ -844,6 +852,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
852
# If not all requests are decodes from the last iteration,
853
# We need to copy the input_ids_cpu to the GPU first.
854
self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
855
+ self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens)
856
+ self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)
857
if num_commmon_tokens == 0:
858
# No requests in common with the previous iteration
859
# So input_ids_cpu will have all the input ids.
@@ -857,6 +867,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
867
self.input_batch.prev_sampled_token_ids[:num_commmon_tokens,
868
0],
869
non_blocking=True)
870
+ self.is_token_ids.gpu[:num_commmon_tokens] = True
871
return
872
# Upload the index tensors asynchronously
873
# so the scatter can be non-blocking.
@@ -947,14 +958,60 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
958
# where M is the max_model_len.
959
token_indices = (positions_np +
960
req_indices * self.input_batch.token_ids_cpu.shape[1])
961
+ token_indices_tensor = torch.from_numpy(token_indices)
962
963
# NOTE(woosuk): We use torch.index_select instead of np.take here
964
# because torch.index_select is much faster than np.take for large
965
# tensors.
966
torch.index_select(self.input_batch.token_ids_cpu_tensor.flatten(),
967
0,
956
- torch.from_numpy(token_indices),
968
+ token_indices_tensor,
969
out=self.input_ids.cpu[:total_num_scheduled_tokens])
970
+ is_token_ids = self.input_batch.is_token_ids.flatten()
971
+ torch.index_select(
972
+ is_token_ids,
973
+ 0,
974
+ token_indices_tensor,
975
+ out=self.is_token_ids.cpu[:total_num_scheduled_tokens])
976
+
977
+ # Because we did not pre-allocate a massive prompt_embeds CPU tensor on
978
+ # the InputBatch, we need to fill in the prompt embeds into the expected
979
+ # spots in the GpuModelRunner's pre-allocated prompt_embeds tensor.
980
+ if self.input_batch.req_prompt_embeds:
981
+ output_idx = 0
982
+ for req_idx in range(num_reqs):
983
+ num_sched = num_scheduled_tokens[req_idx]
984
+
985
+ # Skip if this request doesn't have embeddings
986
+ if req_idx not in self.input_batch.req_prompt_embeds:
987
+ output_idx += num_sched
988
+ continue
989
+
990
+ # Skip if no tokens scheduled
991
+ if num_sched <= 0:
992
+ output_idx += num_sched
993
+ continue
994
+
995
+ req_embeds = self.input_batch.req_prompt_embeds[req_idx]
996
+ start_pos = self.input_batch.num_computed_tokens_cpu[req_idx]
997
+
998
+ # Skip if trying to read beyond available embeddings
999
+ if start_pos >= req_embeds.shape[0]:
1000
+ output_idx += num_sched
1001
+ continue
1002
+
1003
+ # Copy available embeddings
1004
+ end_pos = start_pos + num_sched
1005
+ actual_end = min(end_pos, req_embeds.shape[0])
1006
+ actual_num_sched = actual_end - start_pos
1007
+
1008
+ if actual_num_sched > 0:
1009
+ self.inputs_embeds.cpu[output_idx:output_idx +
1010
+ actual_num_sched].copy_(
1011
+ req_embeds[start_pos:actual_end]
1012
+ )
1013
+
1014
+ output_idx += num_sched
1015
1016
self.input_batch.block_table.compute_slot_mapping(
1017
req_indices, positions_np)
@@ -1279,7 +1336,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1336
self.input_batch.num_computed_tokens_cpu[index]
1337
num_scheduled_tokens = \
1338
scheduler_output.num_scheduled_tokens[req_id]
1282
- num_prompt_tokens = len(req.prompt_token_ids)
1339
+ num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
1340
+ req.prompt_token_ids, req.prompt_embeds)
1341
1342
if num_computed_tokens + num_scheduled_tokens > num_prompt_tokens:
1343
prompt_part_len = max(0,
@@ -1845,6 +1903,32 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1903
**self._init_model_kwargs(num_scheduled_tokens),
1904
**self._extract_mm_kwargs(scheduler_output),
1905
}
1906
+ elif (self.enable_prompt_embeds and get_pp_group().is_first_rank):
1907
+ # Get the input embeddings for the tokens that are not input embeds,
1908
+ # then put them into the appropriate positions.
1909
+ # TODO(qthequartermasterman): Since even when prompt embeds are
1910
+ # enabled, (a) not all requests will use prompt embeds, and (b)
1911
+ # after the initial prompt is processed, the rest of the generated
1912
+ # tokens will be token ids, it is not desirable to have the
1913
+ # embedding layer outside of the CUDA graph all the time. The v0
1914
+ # engine avoids this by "double compiling" the CUDA graph, once
1915
+ # with input_ids and again with inputs_embeds, for all num_tokens.
1916
+ # If a batch only has token ids, then including the embedding layer
1917
+ # in the CUDA graph will be more performant (like in the else case
1918
+ # below).
1919
+ token_ids_idx = self.is_token_ids.gpu[:num_scheduled_tokens] \
1920
+ .nonzero(as_tuple=False) \
1921
+ .squeeze(1)
1922
+ # Some tokens ids may need to become embeds
1923
+ if token_ids_idx.numel() > 0:
1924
+ token_ids = self.input_ids.gpu[token_ids_idx]
1925
+ tokens_to_embeds = self.model.get_input_embeddings(
1926
+ input_ids=token_ids)
1927
+ self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds
1928
+
1929
+ inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
1930
+ model_kwargs = self._init_model_kwargs(num_input_tokens)
1931
+ input_ids = None
1932
else:
1933
# For text-only models, we use token ids as input.
1934
# While it is possible to use embeddings as input just like the
@@ -2023,6 +2107,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2107
2108
self.input_batch.token_ids_cpu[req_idx,
2109
start_idx:end_idx] = sampled_ids
2110
+ self.input_batch.is_token_ids[req_idx, start_idx:end_idx] = True
2111
self.input_batch.num_tokens_no_spec[req_idx] = end_idx
2112
self.input_batch.num_tokens[req_idx] = end_idx
2113
@@ -2570,6 +2655,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2655
2656
# Get metadata for this request.
2657
request = self.requests[req_id]
2658
+ if request.prompt_token_ids is None:
2659
+ # Prompt logprobs is incompatible with prompt embeddings
2660
+ continue
2661
+
2662
num_prompt_tokens = len(request.prompt_token_ids)
2663
prompt_token_ids = torch.tensor(request.prompt_token_ids).to(
2664
self.device, non_blocking=True)
@@ -2922,6 +3011,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3011
**model_kwargs,
3012
**self._dummy_mm_kwargs(num_reqs),
3013
}
3014
+ elif self.enable_prompt_embeds:
3015
+ input_ids = None
3016
+ inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
3017
+ model_kwargs = self._init_model_kwargs(num_tokens)
3018
else:
3019
input_ids = self.input_ids.gpu[:num_tokens]
3020
inputs_embeds = None
vllm/v1/worker/tpu_input_batch.py
+4
-2
@@ -9,7 +9,7 @@ import torch
9
10
from vllm.lora.request import LoRARequest
11
from vllm.sampling_params import SamplingType
12
-from vllm.utils import swap_dict_values
12
+from vllm.utils import length_from_prompt_token_ids_or_embeds, swap_dict_values
13
from vllm.v1.outputs import LogprobsTensors
14
from vllm.v1.worker.block_table import MultiGroupBlockTable
15
from vllm.v1.worker.gpu_input_batch import CachedRequestState
@@ -213,7 +213,9 @@ class InputBatch:
213
self.req_id_to_index[req_id] = req_index
214
215
# Copy the prompt token ids and output token ids.
216
- num_prompt_tokens = len(request.prompt_token_ids)
216
+ num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
217
+ request.prompt_token_ids, request.prompt_embeds)
218
+ # TODO: copy prompt_embeds
219
self.num_prompt_tokens[req_index] = num_prompt_tokens
220
self.token_ids_cpu[
221
req_index, :num_prompt_tokens] = request.prompt_token_ids
vllm/v1/worker/tpu_model_runner.py
+1
@@ -387,6 +387,7 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
387
self.requests[req_id] = CachedRequestState(
388
req_id=req_id,
389
prompt_token_ids=new_req_data.prompt_token_ids,
390
+ prompt_embeds=new_req_data.prompt_embeds,
391
mm_features=new_req_data.mm_features,
392
sampling_params=sampling_params,
393
pooling_params=None,