251 lines
6.6 KiB
Python
251 lines
6.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
from collections.abc import Callable, Sequence
|
|
from dataclasses import dataclass
|
|
from functools import lru_cache, partial
|
|
from typing import Any, Literal, cast
|
|
|
|
import pybase64
|
|
import torch
|
|
from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
|
from vllm.config import ModelConfig
|
|
from vllm.entrypoints.openai.engine.protocol import UsageInfo
|
|
from vllm.logger import init_logger
|
|
from vllm.outputs import PoolingRequestOutput
|
|
from vllm.tasks import SupportedTask
|
|
from vllm.utils.serial_utils import (
|
|
EMBED_DTYPES,
|
|
EmbedDType,
|
|
Endianness,
|
|
binary2tensor,
|
|
tensor2binary,
|
|
)
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
JsonEncodingFormat = Literal["float", "base64"]
|
|
BytesEncodingFormat = Literal["bytes", "bytes_only"]
|
|
FloatEncodedPoolingOutput = list[float] | list[list[float]]
|
|
JsonEncodedPoolingOutput = FloatEncodedPoolingOutput | str
|
|
|
|
|
|
@dataclass
|
|
class MetadataItem:
|
|
index: int
|
|
embed_dtype: EmbedDType
|
|
endianness: Endianness
|
|
start: int
|
|
end: int
|
|
shape: tuple[int, ...]
|
|
|
|
|
|
def build_metadata_items(
|
|
embed_dtype: EmbedDType,
|
|
endianness: Endianness,
|
|
shape: tuple[int, ...],
|
|
n_request: int,
|
|
) -> list[MetadataItem]:
|
|
n_bytes = EMBED_DTYPES[embed_dtype].nbytes
|
|
size = math.prod(shape)
|
|
|
|
return [
|
|
MetadataItem(
|
|
index=i,
|
|
embed_dtype=embed_dtype,
|
|
endianness=endianness,
|
|
start=i * size * n_bytes,
|
|
end=(i + 1) * size * n_bytes,
|
|
shape=shape,
|
|
)
|
|
for i in range(n_request)
|
|
]
|
|
|
|
|
|
def encode_pooling_output_float(
|
|
output: PoolingRequestOutput,
|
|
) -> FloatEncodedPoolingOutput:
|
|
return output.outputs.data.tolist()
|
|
|
|
|
|
def encode_pooling_output_float_or_ndarray(output: PoolingRequestOutput) -> Any:
|
|
"""Return an ndarray when the response renderer can serialize NumPy."""
|
|
try:
|
|
data = output.outputs.data
|
|
if not data.is_contiguous():
|
|
data = data.contiguous()
|
|
return data.numpy()
|
|
except (RuntimeError, TypeError):
|
|
return output.outputs.data.tolist()
|
|
|
|
|
|
def encode_pooling_output_base64(
|
|
output: PoolingRequestOutput,
|
|
embed_dtype: EmbedDType,
|
|
endianness: Endianness,
|
|
) -> str:
|
|
embedding_bytes = tensor2binary(output.outputs.data, embed_dtype, endianness)
|
|
return pybase64.b64encode(embedding_bytes).decode("utf-8")
|
|
|
|
|
|
def encode_pooling_bytes(
|
|
pooling_outputs: list[PoolingRequestOutput],
|
|
embed_dtype: EmbedDType,
|
|
endianness: Endianness,
|
|
) -> tuple[list[bytes], list[dict[str, Any]]]:
|
|
items: list[dict[str, Any]] = []
|
|
body: list[bytes] = []
|
|
offset = 0
|
|
for idx, output in enumerate(pooling_outputs):
|
|
binary = tensor2binary(
|
|
tensor=output.outputs.data,
|
|
embed_dtype=embed_dtype,
|
|
endianness=endianness,
|
|
)
|
|
size = len(binary)
|
|
|
|
# Dictionary form of MetadataItem
|
|
item = dict(
|
|
index=idx,
|
|
embed_dtype=embed_dtype,
|
|
endianness=endianness,
|
|
start=offset,
|
|
end=offset + size,
|
|
shape=output.outputs.data.shape,
|
|
)
|
|
|
|
body.append(binary)
|
|
items.append(item)
|
|
offset += size
|
|
|
|
return body, items
|
|
|
|
|
|
def get_pooling_output_encoder(
|
|
encoding_format: JsonEncodingFormat,
|
|
embed_dtype: EmbedDType,
|
|
endianness: Endianness,
|
|
) -> Callable[[PoolingRequestOutput], JsonEncodedPoolingOutput]:
|
|
return cast(
|
|
Callable[[PoolingRequestOutput], JsonEncodedPoolingOutput],
|
|
(
|
|
encode_pooling_output_float
|
|
if encoding_format == "float"
|
|
else partial(
|
|
encode_pooling_output_base64,
|
|
embed_dtype=embed_dtype,
|
|
endianness=endianness,
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
def get_pooling_usage(
|
|
pooling_outputs: Sequence[PoolingRequestOutput],
|
|
) -> UsageInfo:
|
|
num_prompt_tokens = sum(
|
|
len(output.prompt_token_ids) if output.prompt_token_ids is not None else 0
|
|
for output in pooling_outputs
|
|
)
|
|
return UsageInfo(
|
|
prompt_tokens=num_prompt_tokens,
|
|
total_tokens=num_prompt_tokens,
|
|
)
|
|
|
|
|
|
def get_pooling_usage_payload(
|
|
pooling_outputs: Sequence[PoolingRequestOutput],
|
|
) -> dict[str, int]:
|
|
usage = get_pooling_usage(pooling_outputs)
|
|
return {
|
|
"prompt_tokens": usage.prompt_tokens,
|
|
"total_tokens": usage.total_tokens,
|
|
}
|
|
|
|
|
|
def build_pooling_bytes_streaming_response(
|
|
pooling_outputs: list[PoolingRequestOutput],
|
|
request_id: str,
|
|
created_time: int,
|
|
model_name: str,
|
|
encoding_format: BytesEncodingFormat,
|
|
embed_dtype: EmbedDType,
|
|
endianness: Endianness,
|
|
) -> StreamingResponse:
|
|
content, items = encode_pooling_bytes(
|
|
pooling_outputs=pooling_outputs,
|
|
embed_dtype=embed_dtype,
|
|
endianness=endianness,
|
|
)
|
|
|
|
headers = (
|
|
None
|
|
if encoding_format == "bytes_only"
|
|
else {
|
|
"metadata": json.dumps(
|
|
{
|
|
"id": request_id,
|
|
"created": created_time,
|
|
"model": model_name,
|
|
"data": items,
|
|
"usage": get_pooling_usage_payload(pooling_outputs),
|
|
}
|
|
)
|
|
}
|
|
)
|
|
|
|
return StreamingResponse(
|
|
content=content,
|
|
headers=headers,
|
|
media_type="application/octet-stream",
|
|
)
|
|
|
|
|
|
def decode_pooling_output(items: list[MetadataItem], body: bytes) -> list[torch.Tensor]:
|
|
return [
|
|
binary2tensor(
|
|
body[item.start : item.end],
|
|
item.shape,
|
|
item.embed_dtype,
|
|
item.endianness,
|
|
)
|
|
for item in sorted(items, key=lambda x: x.index)
|
|
]
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_json_response_cls() -> type[JSONResponse]:
|
|
if importlib.util.find_spec("orjson") is not None:
|
|
from fastapi.responses import ORJSONResponse
|
|
|
|
return ORJSONResponse
|
|
logger.warning_once(
|
|
"To make v1/embeddings API fast, please install orjson by `pip install orjson`"
|
|
)
|
|
return JSONResponse
|
|
|
|
|
|
def enable_scoring_api(
|
|
supported_tasks: tuple["SupportedTask", ...],
|
|
model_config: ModelConfig | None = None,
|
|
) -> bool:
|
|
if model_config is None:
|
|
return False
|
|
|
|
pooling_task = model_config.get_pooling_task(supported_tasks)
|
|
if pooling_task in ("embed", "token_embed"):
|
|
return True
|
|
|
|
if pooling_task == "classify":
|
|
num_labels = getattr(model_config.hf_config, "num_labels", 0)
|
|
if num_labels != 1:
|
|
logger.debug_once("Scoring API is only enabled for num_labels == 1.")
|
|
return False
|
|
return True
|
|
|
|
return False
|