"vllm/v1/executor/ray_executor.py" did not exist on "8936316d587ca0afb5ef058584c407d404c0ffb0"
chat_utils.py 9.76 KB
Newer Older
1
import codecs
2
from dataclasses import dataclass
3
from functools import lru_cache
4
from pathlib import Path
5
from typing import (Any, Awaitable, Iterable, List, Literal, Optional, Tuple,
6
                    Union)
7

8
9
10
11
12
13
14
15
16
17
# yapf conflicts with isort for this block
# yapf: disable
from openai.types.chat import ChatCompletionContentPartImageParam
from openai.types.chat import (
    ChatCompletionContentPartParam as OpenAIChatCompletionContentPartParam)
from openai.types.chat import ChatCompletionContentPartTextParam
from openai.types.chat import (
    ChatCompletionMessageParam as OpenAIChatCompletionMessageParam)
# yapf: enable
# pydantic needs the TypedDict from typing_extensions
18
19
from pydantic import ConfigDict, TypeAdapter
from typing_extensions import Required, TypeAlias, TypedDict
20

21
from vllm.config import ModelConfig
22
23
from vllm.logger import init_logger
from vllm.multimodal import MultiModalDataDict
24
25
from vllm.multimodal.utils import (async_get_and_parse_audio,
                                   async_get_and_parse_image)
26
from vllm.transformers_utils.tokenizer import AnyTokenizer
27
28
29
30

logger = init_logger(__name__)


31
32
33
34
35
36
37
38
39
40
41
42
43
44
class AudioURL(TypedDict, total=False):
    url: Required[str]
    """
    Either a URL of the audio or a data URL with base64 encoded audio data.
    """


class ChatCompletionContentPartAudioParam(TypedDict, total=False):
    audio_url: Required[AudioURL]

    type: Required[Literal["audio_url"]]
    """The type of the content part."""


45
46
47
48
49
50
51
class CustomChatCompletionContentPartParam(TypedDict, total=False):
    __pydantic_config__ = ConfigDict(extra="allow")  # type: ignore

    type: Required[str]
    """The type of the content part."""


52
53
54
ChatCompletionContentPartParam: TypeAlias = Union[
    OpenAIChatCompletionContentPartParam, ChatCompletionContentPartAudioParam,
    CustomChatCompletionContentPartParam, ]
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76


class CustomChatCompletionMessageParam(TypedDict, total=False):
    """Enables custom roles in the Chat Completion API."""
    role: Required[str]
    """The role of the message's author."""

    content: Union[str, List[ChatCompletionContentPartParam]]
    """The contents of the message."""

    name: str
    """An optional name for the participant.

    Provides the model information to differentiate between participants of the
    same role.
    """


ChatCompletionMessageParam = Union[OpenAIChatCompletionMessageParam,
                                   CustomChatCompletionMessageParam]


77
# TODO: Make fields ReadOnly once mypy supports it
78
79
80
81
82
83
84
85
class ConversationMessage(TypedDict):
    role: str
    content: str


@dataclass(frozen=True)
class ChatMessageParseResult:
    messages: List[ConversationMessage]
86
    mm_futures: List[Awaitable[MultiModalDataDict]]
87
88


89
90
def load_chat_template(
        chat_template: Optional[Union[Path, str]]) -> Optional[str]:
91
92
93
94
95
96
    if chat_template is None:
        return None
    try:
        with open(chat_template, "r") as f:
            resolved_chat_template = f.read()
    except OSError as e:
97
98
99
        if isinstance(chat_template, Path):
            raise

100
101
102
103
104
105
        JINJA_CHARS = "{}\n"
        if not any(c in chat_template for c in JINJA_CHARS):
            msg = (f"The supplied chat template ({chat_template}) "
                   f"looks like a file path, but it failed to be "
                   f"opened. Reason: {e}")
            raise ValueError(msg) from e
106

107
108
109
        # If opening a file fails, set chat template to be args to
        # ensure we decode so our escape are interpreted correctly
        resolved_chat_template = codecs.decode(chat_template, "unicode_escape")
110

111
112
    logger.info("Using supplied chat template:\n%s", resolved_chat_template)
    return resolved_chat_template
113
114
115


@lru_cache(maxsize=None)
116
def _mm_token_str(model_config: ModelConfig, tokenizer: AnyTokenizer,
117
                  modality: Literal["image", "audio"]) -> Optional[str]:
118
119
    # TODO: Let user specify how to insert image tokens into prompt
    # (similar to chat template)
120
    model_type = model_config.hf_config.model_type
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
    if modality == "image":
        if model_type == "phi3_v":
            # Workaround since this token is not defined in the tokenizer
            return "<|image_1|>"
        if model_type == "minicpmv":
            return "(<image>./</image>)"
        if model_type in ("blip-2", "chatglm", "fuyu", "paligemma"):
            # These models do not use image tokens in the prompt
            return None
        if model_type.startswith("llava"):
            return tokenizer.decode(model_config.hf_config.image_token_index)
        if model_type in ("chameleon", "internvl_chat"):
            return "<image>"

        raise TypeError(f"Unknown model type: {model_type}")
    elif modality == "audio":
137
138
139
        if model_type == "ultravox":
            return "<|reserved_special_token_0|>"
        raise TypeError(f"Unknown model type: {model_type}")
140
141
142
143
144
    else:
        raise TypeError(f"Unknown modality: {modality}")


# TODO: Let user specify how to insert multimodal tokens into prompt
145
# (similar to chat template)
146
147
148
def _get_full_multimodal_text_prompt(placeholder_token_str: str,
                                     text_prompt: str) -> str:
    """Combine multimodal prompts for a multimodal language model"""
149
150

    # NOTE: For now we assume all model architectures use the same
151
152
    # placeholder + text prompt format. This may change in the future.
    return f"{placeholder_token_str}\n{text_prompt}"
153
154


155
156
157
158
159
_TextParser = TypeAdapter(ChatCompletionContentPartTextParam)
_ImageParser = TypeAdapter(ChatCompletionContentPartImageParam)
_AudioParser = TypeAdapter(ChatCompletionContentPartAudioParam)


160
161
162
def _parse_chat_message_content_parts(
    role: str,
    parts: Iterable[ChatCompletionContentPartParam],
163
    model_config: ModelConfig,
164
    tokenizer: AnyTokenizer,
165
166
167
) -> ChatMessageParseResult:
    texts: List[str] = []
    mm_futures: List[Awaitable[MultiModalDataDict]] = []
168
    modality: Literal["image", "audio"] = "image"
169
170
171
172

    for part in parts:
        part_type = part["type"]
        if part_type == "text":
173
            text = _TextParser.validate_python(part)["text"]
174
175
            texts.append(text)
        elif part_type == "image_url":
176
            modality = "image"
177
178
            if len(mm_futures) > 0:
                raise NotImplementedError(
179
                    "Multiple multimodal inputs is currently not supported.")
180

181
            image_url = _ImageParser.validate_python(part)["image_url"]
182
183
184
185
186
187
188
189

            if image_url.get("detail", "auto") != "auto":
                logger.warning(
                    "'image_url.detail' is currently not supported and "
                    "will be ignored.")

            image_future = async_get_and_parse_image(image_url["url"])
            mm_futures.append(image_future)
190
191
192
193
194
195
        elif part_type == "audio_url":
            modality = "audio"
            if len(mm_futures) > 0:
                raise NotImplementedError(
                    "Multiple multimodal inputs is currently not supported.")

196
            audio_url = _AudioParser.validate_python(part)["audio_url"]
197
198
            audio_future = async_get_and_parse_audio(audio_url["url"])
            mm_futures.append(audio_future)
199
200
201
202
203
204
        else:
            raise NotImplementedError(f"Unknown part type: {part_type}")

    text_prompt = "\n".join(texts)

    if mm_futures:
205
206
207
208
        placeholder_token_str = _mm_token_str(model_config, tokenizer,
                                              modality)
        if placeholder_token_str is not None:
            if placeholder_token_str in text_prompt:
209
                logger.warning(
210
                    "Detected multi-modal token string in the text prompt. "
211
212
                    "Skipping prompt formatting.")
            else:
213
214
                text_prompt = _get_full_multimodal_text_prompt(
                    placeholder_token_str=placeholder_token_str,
215
216
217
218
219
220
221
222
                    text_prompt=text_prompt,
                )

    messages = [ConversationMessage(role=role, content=text_prompt)]

    return ChatMessageParseResult(messages=messages, mm_futures=mm_futures)


223
def _parse_chat_message_content(
224
    message: ChatCompletionMessageParam,
225
    model_config: ModelConfig,
226
    tokenizer: AnyTokenizer,
227
228
229
230
231
232
233
234
235
236
) -> ChatMessageParseResult:
    role = message["role"]
    content = message.get("content")

    if content is None:
        return ChatMessageParseResult(messages=[], mm_futures=[])
    if isinstance(content, str):
        messages = [ConversationMessage(role=role, content=content)]
        return ChatMessageParseResult(messages=messages, mm_futures=[])

237
238
239
240
241
242
    return _parse_chat_message_content_parts(
        role,
        content,  # type: ignore
        model_config,
        tokenizer,
    )
243
244
245
246
247


def parse_chat_messages(
    messages: List[ChatCompletionMessageParam],
    model_config: ModelConfig,
248
    tokenizer: AnyTokenizer,
249
250
251
252
253
254
255
256
257
258
259
260
) -> Tuple[List[ConversationMessage], List[Awaitable[MultiModalDataDict]]]:
    conversation: List[ConversationMessage] = []
    mm_futures: List[Awaitable[MultiModalDataDict]] = []

    for msg in messages:
        parse_result = _parse_chat_message_content(msg, model_config,
                                                   tokenizer)

        conversation.extend(parse_result.messages)
        mm_futures.extend(parse_result.mm_futures)

    return conversation, mm_futures
261
262
263
264
265
266
267
268
269


def apply_chat_template(
    tokenizer: AnyTokenizer,
    conversation: List[ConversationMessage],
    chat_template: Optional[str],
    *,
    tokenize: bool = False,  # Different from HF's default
    **kwargs: Any,
270
) -> Union[str, List[int]]:
271
272
273
274
275
276
277
278
279
280
281
282
283
    if chat_template is None and tokenizer.chat_template is None:
        raise ValueError(
            "As of transformers v4.44, default chat template is no longer "
            "allowed, so you must provide a chat template if the tokenizer "
            "does not define one.")

    prompt = tokenizer.apply_chat_template(
        conversation=conversation,
        chat_template=chat_template,
        tokenize=tokenize,
        **kwargs,
    )
    return prompt