main
py 444 lines 16.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import MutableSequence
5 from collections.abc import Sequence as GenericSequence
6 from dataclasses import dataclass
7 from typing import Any, Generic
8
9 import numpy as np
10 import torch
11 from typing_extensions import TypeVar
12
13 from vllm.logger import init_logger
14 from vllm.logprobs import PromptLogprobs, SampleLogprobs
15 from vllm.lora.request import LoRARequest
16 from vllm.v1.metrics.stats import RequestSpecDecodeMetrics, RequestStateStats
17
18 logger = init_logger(__name__)
19
20
21 @dataclass(frozen=True)
22 class RequestError:
23 """A request-level error that does not mark the engine as unhealthy."""
24
25 code: str
26 message: str
27 retryable: bool = False
28
29
30 @dataclass
31 class SamplingMask:
32 """Per-token sampling support sets aligned with completion token IDs.
33
34 Each inner list contains the vocabulary token IDs that survived
35 top-k / top-p / min-p filtering for the corresponding generated token.
36 """
37
38 token_ids: list[list[int]]
39
40
41 @dataclass
42 class CompletionOutput:
43 """The output data of one completion output of a request.
44
45 Args:
46 index: The index of the output in the request.
47 text: The generated output text.
48 token_ids: The token IDs of the generated output text.
49 cumulative_logprob: The cumulative log probability of the generated
50 output text.
51 logprobs: The log probabilities of the top probability words at each
52 position if the logprobs are requested.
53 sampling_mask: The post-processing token support set for each generated
54 token, if requested.
55 finish_reason: The reason why the sequence is finished.
56 stop_reason: The stop string or token id that caused the completion
57 to stop, None if the completion finished for some other reason
58 including encountering the EOS token.
59 lora_request: The LoRA request that was used to generate the output.
60 spec_decode_metrics: Per-sequence speculative-decoding acceptance metrics,
61 populated on finish when speculative decoding ran and
62 ``--per-request-spec-decode-metrics`` is enabled; None otherwise.
63 Surfaced in the response as ``metrics.speculative_decoding`` for
64 single-sequence (``n == 1``) requests.
65
66 """
67
68 index: int
69 text: str
70 token_ids: GenericSequence[int]
71 cumulative_logprob: float | None
72 logprobs: SampleLogprobs | None
73 routed_experts: np.ndarray | None = None # [seq_len,layer_num,topk]
74 finish_reason: str | None = None
75 stop_reason: int | str | None = None
76 lora_request: LoRARequest | None = None
77 sampling_mask: SamplingMask | None = None
78 spec_decode_metrics: RequestSpecDecodeMetrics | None = None
79
80 def finished(self) -> bool:
81 return self.finish_reason is not None
82
83 def __repr__(self) -> str:
84 return (
85 f"CompletionOutput(index={self.index}, "
86 f"text={self.text!r}, "
87 f"token_ids={self.token_ids}, "
88 f"routed_experts={self.routed_experts}, "
89 f"sampling_mask={self.sampling_mask}, "
90 f"cumulative_logprob={self.cumulative_logprob}, "
91 f"logprobs={self.logprobs}, "
92 f"finish_reason={self.finish_reason}, "
93 f"stop_reason={self.stop_reason})"
94 )
95
96
97 @dataclass
98 class PoolingOutput:
99 """The output data of one pooling output of a request.
100
101 Args:
102 data: The extracted hidden states.
103
104 """
105
106 data: torch.Tensor
107
108 def __repr__(self) -> str:
109 return f"PoolingOutput(data={self.data})"
110
111 def __eq__(self, other: object) -> bool:
112 return isinstance(other, self.__class__) and bool(
113 (self.data == other.data).all()
114 )
115
116
117 class RequestOutput:
118 """The output data of a completion request to the LLM.
119
120 Args:
121 request_id: The unique ID of the request.
122 prompt: The prompt string of the request.
123 For encoder/decoder models, this is the
124 decoder input prompt.
125 prompt_token_ids: The token IDs of the prompt.
126 For encoder/decoder models, this is the
127 decoder input prompt token ids.
128 prompt_logprobs: The log probabilities to return per prompt token.
129 prompt_token_id_logprobs: Logprobs of prompt_logprob_token_ids, shaped
130 [max(prompt_len - 1 - prompt_logprob_start, 0),
131 len(prompt_logprob_token_ids)]; row i scores them as predictions of
132 prompt token prompt_logprob_start + i + 1.
133 outputs: The output sequences of the request.
134 finished: Whether the whole request is finished.
135 metrics: Metrics associated with the request.
136 lora_request: The LoRA request that was used to generate the output.
137 encoder_prompt: The encoder prompt string of the request.
138 None if decoder-only.
139 encoder_prompt_token_ids: The token IDs of the encoder prompt.
140 None if decoder-only.
141 num_cached_tokens: The number of tokens with prefix cache hit.
142 num_cache_creation_tokens: Prompt tokens currently counted as local
143 prefix-cache writes for this request.
144 kv_transfer_params: The params for remote K/V transfer.
145 ec_transfer_params: The params for remote encoder-cache transfer.
146
147 """
148
149 def __init__(
150 self,
151 request_id: str,
152 prompt: str | None,
153 prompt_token_ids: list[int] | None,
154 prompt_logprobs: PromptLogprobs | None,
155 outputs: list[CompletionOutput],
156 finished: bool,
157 metrics: RequestStateStats | None = None,
158 lora_request: LoRARequest | None = None,
159 encoder_prompt: str | None = None,
160 encoder_prompt_token_ids: list[int] | None = None,
161 num_cached_tokens: int | None = None,
162 num_cache_creation_tokens: int | None = None,
163 *,
164 prompt_token_id_logprobs: np.ndarray | None = None,
165 kv_transfer_params: dict[str, Any] | None = None,
166 ec_transfer_params: dict[str, Any] | None = None,
167 # Forward compatibility, code that uses args added in new release can
168 # still run with older versions of vLLM without breaking.
169 **kwargs: Any,
170 ) -> None:
171 if kwargs:
172 logger.warning_once(
173 "RequestOutput: Ignoring extra arguments: %s", str(kwargs)
174 )
175 self.request_id = request_id
176 self.prompt = prompt
177 self.prompt_token_ids = prompt_token_ids
178 self.prompt_logprobs = prompt_logprobs
179 self.prompt_token_id_logprobs = prompt_token_id_logprobs
180 self.outputs = outputs
181 self.finished = finished
182 self.metrics = metrics
183 self.lora_request = lora_request
184 self.encoder_prompt = encoder_prompt
185 self.encoder_prompt_token_ids = encoder_prompt_token_ids
186 self.num_cached_tokens = num_cached_tokens
187 self.num_cache_creation_tokens = num_cache_creation_tokens
188 self.kv_transfer_params = kv_transfer_params
189 self.ec_transfer_params = ec_transfer_params
190
191 def add(self, next_output: "RequestOutput", aggregate: bool) -> None:
192 """Merge subsequent RequestOutput into this one"""
193 self.finished |= next_output.finished
194 self.kv_transfer_params = next_output.kv_transfer_params
195 self.ec_transfer_params = next_output.ec_transfer_params
196
197 for next_completion in next_output.outputs:
198 for i, completion in enumerate(self.outputs):
199 if completion.index == next_completion.index:
200 if aggregate:
201 # Merge outputs with same index
202 completion.text += next_completion.text
203 if not isinstance(completion.token_ids, MutableSequence):
204 completion.token_ids = list(completion.token_ids)
205 completion.token_ids.extend(next_completion.token_ids)
206 if next_completion.logprobs:
207 assert completion.logprobs is not None
208 completion.logprobs.extend(next_completion.logprobs) # type: ignore[arg-type]
209 if next_completion.sampling_mask is not None:
210 if completion.sampling_mask is None:
211 completion.sampling_mask = SamplingMask([])
212 completion.sampling_mask.token_ids.extend(
213 next_completion.sampling_mask.token_ids
214 )
215 completion.cumulative_logprob = (
216 next_completion.cumulative_logprob
217 )
218 # R3 is returned on the terminal output and must survive
219 # aggregation with earlier chunks that have no R3.
220 if next_completion.routed_experts is not None:
221 completion.routed_experts = next_completion.routed_experts
222 completion.finish_reason = next_completion.finish_reason
223 completion.stop_reason = next_completion.stop_reason
224 else:
225 # Replace the output with the new one
226 self.outputs[i] = next_completion
227 break
228 else:
229 self.outputs.append(next_completion)
230
231 def __repr__(self) -> str:
232 return (
233 f"RequestOutput(request_id={self.request_id}, "
234 f"prompt={self.prompt!r}, "
235 f"prompt_token_ids={self.prompt_token_ids}, "
236 f"encoder_prompt={self.encoder_prompt!r}, "
237 f"encoder_prompt_token_ids={self.encoder_prompt_token_ids}, "
238 f"prompt_logprobs={self.prompt_logprobs}, "
239 f"prompt_token_id_logprobs={self.prompt_token_id_logprobs}, "
240 f"outputs={self.outputs}, "
241 f"finished={self.finished}, "
242 f"metrics={self.metrics}, "
243 f"lora_request={self.lora_request}, "
244 f"num_cached_tokens={self.num_cached_tokens}, "
245 f"num_cache_creation_tokens={self.num_cache_creation_tokens})"
246 )
247
248
249 # Sentinel to indicate request is finished, used with streaming inputs.
250 STREAM_FINISHED = RequestOutput(
251 request_id="",
252 prompt=None,
253 prompt_token_ids=None,
254 prompt_logprobs=None,
255 outputs=[],
256 finished=True,
257 )
258
259 _O = TypeVar("_O", default=PoolingOutput)
260
261
262 class PoolingRequestOutput(Generic[_O]):
263 """The output data of a pooling request to the LLM.
264
265 Args:
266 request_id (str): A unique identifier for the pooling request.
267 outputs (PoolingOutput): The pooling results for the given input.
268 prompt_token_ids (list[int]): A list of token IDs used in the prompt.
269 num_cached_tokens: The number of tokens with prefix cache hit.
270 finished (bool): A flag indicating whether the pooling is completed.
271 error: Structured request-level error information, if the request failed.
272
273 """
274
275 def __init__(
276 self,
277 request_id: str,
278 outputs: _O,
279 prompt_token_ids: list[int],
280 num_cached_tokens: int,
281 finished: bool,
282 *,
283 error: RequestError | None = None,
284 ):
285 self.request_id = request_id
286 self.prompt_token_ids = prompt_token_ids
287 self.num_cached_tokens = num_cached_tokens
288 self.finished = finished
289 self.outputs = outputs
290 self.error = error
291
292 def __repr__(self) -> str:
293 error = f", error={self.error!r}" if self.error is not None else ""
294 return (
295 f"{type(self).__name__}(request_id={self.request_id!r}, "
296 f"outputs={self.outputs!r}, "
297 f"prompt_token_ids={self.prompt_token_ids}, "
298 f"num_cached_tokens={self.num_cached_tokens}, "
299 f"finished={self.finished}"
300 f"{error})"
301 )
302
303
304 @dataclass
305 class EmbeddingOutput:
306 """The output data of one embedding output of a request.
307
308 Args:
309 embedding: The embedding vector, which is a list of floats.
310 Its length depends on the hidden dimension of the model.
311
312 """
313
314 embedding: list[float]
315
316 @staticmethod
317 def from_base(pooling_output: PoolingOutput) -> "EmbeddingOutput":
318 pooled_data = pooling_output.data
319 if pooled_data.ndim != 1:
320 raise ValueError("pooled_data should be a 1-D embedding vector")
321
322 return EmbeddingOutput(pooled_data.tolist())
323
324 @property
325 def hidden_size(self) -> int:
326 return len(self.embedding)
327
328 def __repr__(self) -> str:
329 return f"EmbeddingOutput(hidden_size={self.hidden_size})"
330
331
332 class EmbeddingRequestOutput(PoolingRequestOutput[EmbeddingOutput]):
333 @staticmethod
334 def from_base(
335 request_output: PoolingRequestOutput,
336 ) -> "EmbeddingRequestOutput":
337 outputs = (
338 EmbeddingOutput([])
339 if request_output.error is not None
340 else EmbeddingOutput.from_base(request_output.outputs)
341 )
342 return EmbeddingRequestOutput(
343 request_id=request_output.request_id,
344 outputs=outputs,
345 prompt_token_ids=request_output.prompt_token_ids,
346 num_cached_tokens=request_output.num_cached_tokens,
347 finished=request_output.finished,
348 error=request_output.error,
349 )
350
351
352 @dataclass
353 class ClassificationOutput:
354 """The output data of one classification output of a request.
355
356 Args:
357 probs: The probability vector, which is a list of floats.
358 Its length depends on the number of classes.
359
360 """
361
362 probs: list[float]
363
364 @staticmethod
365 def from_base(pooling_output: PoolingOutput) -> "ClassificationOutput":
366 # pooling_output shape: (num_classes)
367 pooled_data = pooling_output.data
368 if pooled_data.ndim != 1:
369 raise ValueError("pooled_data should be a 1-D probability vector")
370
371 return ClassificationOutput(pooled_data.tolist())
372
373 @property
374 def num_classes(self) -> int:
375 return len(self.probs)
376
377 def __repr__(self) -> str:
378 return f"ClassificationOutput(num_classes={self.num_classes})"
379
380
381 class ClassificationRequestOutput(PoolingRequestOutput[ClassificationOutput]):
382 @staticmethod
383 def from_base(
384 request_output: PoolingRequestOutput,
385 ) -> "ClassificationRequestOutput":
386 outputs = (
387 ClassificationOutput([])
388 if request_output.error is not None
389 else ClassificationOutput.from_base(request_output.outputs)
390 )
391 return ClassificationRequestOutput(
392 request_id=request_output.request_id,
393 outputs=outputs,
394 prompt_token_ids=request_output.prompt_token_ids,
395 num_cached_tokens=request_output.num_cached_tokens,
396 finished=request_output.finished,
397 error=request_output.error,
398 )
399
400
401 @dataclass
402 class ScoringOutput:
403 """The output data of one scoring output of a request.
404
405 Args:
406 score: The similarity score, which is a scalar value.
407
408 """
409
410 score: float
411
412 @staticmethod
413 def from_base(pooling_output: PoolingOutput) -> "ScoringOutput":
414 # pooling_output shape:
415 # classify task: (num_classes) num_classes == 1
416 # embed task: a scalar value
417 pooled_data = pooling_output.data.squeeze()
418 if pooled_data.ndim != 0:
419 raise ValueError("pooled_data should be a scalar score")
420
421 return ScoringOutput(pooled_data.item())
422
423 def __repr__(self) -> str:
424 return f"ScoringOutput(score={self.score})"
425
426
427 class ScoringRequestOutput(PoolingRequestOutput[ScoringOutput]):
428 @staticmethod
429 def from_base(
430 request_output: PoolingRequestOutput,
431 ) -> "ScoringRequestOutput":
432 outputs = (
433 ScoringOutput(0.0)
434 if request_output.error is not None
435 else ScoringOutput.from_base(request_output.outputs)
436 )
437 return ScoringRequestOutput(
438 request_id=request_output.request_id,
439 outputs=outputs,
440 prompt_token_ids=request_output.prompt_token_ids,
441 num_cached_tokens=request_output.num_cached_tokens,
442 finished=request_output.finished,
443 error=request_output.error,
444 )