@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / cbcdf2c609
Fork 0

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, 17:15 UTC

[Bugfix] Fix chat template loading (#15143)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk> Signed-off-by: Roger Wang <ywang@roblox.com> Co-authored-by: chaunceyjiang <chaunceyjiang@gmail.com> Co-authored-by: Roger Wang <ywang@roblox.com>

Cyrus Leung committed Mar 24, 2025 at 21:50 UTC cbcdf2c6091b162b94cbecf41312270e4b5d6ff2
7 files changed +187 -47
tests/entrypoints/openai/test_chat_template.py
+2
@@ -107,8 +107,10 @@ def test_get_gen_prompt(model, template, add_generation_prompt,
107 # Call the function and get the result
108 result = apply_hf_chat_template(
109 tokenizer,
110 + trust_remote_code=True,
111 conversation=mock_request.messages,
112 chat_template=mock_request.chat_template or template_content,
113 + tools=None,
114 add_generation_prompt=mock_request.add_generation_prompt,
115 continue_final_message=mock_request.continue_final_message,
116 )
tests/entrypoints/openai/test_video.py
+2 -2
@@ -87,7 +87,7 @@ async def test_single_chat_session_video(client: openai.AsyncOpenAI,
87 choice = chat_completion.choices[0]
88 assert choice.finish_reason == "length"
89 assert chat_completion.usage == openai.types.CompletionUsage(
90 - completion_tokens=10, prompt_tokens=6299, total_tokens=6309)
90 + completion_tokens=10, prompt_tokens=6287, total_tokens=6297)
91
92 message = choice.message
93 message = chat_completion.choices[0].message
@@ -180,7 +180,7 @@ async def test_single_chat_session_video_base64encoded(
180 choice = chat_completion.choices[0]
181 assert choice.finish_reason == "length"
182 assert chat_completion.usage == openai.types.CompletionUsage(
183 - completion_tokens=10, prompt_tokens=6299, total_tokens=6309)
183 + completion_tokens=10, prompt_tokens=6287, total_tokens=6297)
184
185 message = choice.message
186 message = chat_completion.choices[0].message
tests/entrypoints/test_chat_utils.py
+62 -2
@@ -4,10 +4,13 @@ import warnings
4 from typing import Optional
5
6 import pytest
7 +from packaging.version import Version
8 +from transformers import __version__ as TRANSFORMERS_VERSION
9
10 from vllm.assets.image import ImageAsset
11 from vllm.config import ModelConfig
10 -from vllm.entrypoints.chat_utils import (_try_extract_ast, load_chat_template,
12 +from vllm.entrypoints.chat_utils import (_resolve_hf_chat_template,
13 + _try_extract_ast, load_chat_template,
14 parse_chat_messages,
15 parse_chat_messages_futures,
16 resolve_chat_template_content_format)
@@ -23,8 +26,10 @@ EXAMPLES_DIR = VLLM_PATH / "examples"
26 PHI3V_MODEL_ID = "microsoft/Phi-3.5-vision-instruct"
27 ULTRAVOX_MODEL_ID = "fixie-ai/ultravox-v0_5-llama-3_2-1b"
28 QWEN2VL_MODEL_ID = "Qwen/Qwen2-VL-2B-Instruct"
29 +QWEN25VL_MODEL_ID = "Qwen/Qwen2.5-VL-3B-Instruct"
30 MLLAMA_MODEL_ID = "meta-llama/Llama-3.2-11B-Vision-Instruct"
31 LLAMA_GUARD_MODEL_ID = "meta-llama/Llama-Guard-3-1B"
32 +HERMES_MODEL_ID = "NousResearch/Hermes-3-Llama-3.1-8B"
33
34
35 @pytest.fixture(scope="function")
@@ -703,25 +708,70 @@ def test_multimodal_image_parsing_matches_hf(model, image_url):
708
709 vllm_result = apply_hf_chat_template(
710 tokenizer,
711 + trust_remote_code=model_config.trust_remote_code,
712 conversation=conversation,
713 chat_template=None,
714 + tools=None,
715 add_generation_prompt=True,
716 )
717
718 assert hf_result == vllm_result
719
720
721 +@pytest.mark.parametrize(
722 + "model",
723 + [
724 + QWEN2VL_MODEL_ID, # tokenizer.chat_template is of type str
725 + HERMES_MODEL_ID, # tokenizer.chat_template is of type dict
726 + ])
727 +@pytest.mark.parametrize("use_tools", [True, False])
728 +def test_resolve_hf_chat_template(sample_json_schema, model, use_tools):
729 + """checks that chat_template is a dict type for HF models."""
730 +
731 + # Build the tokenizer group and grab the underlying tokenizer
732 + tokenizer_group = TokenizerGroup(
733 + model,
734 + enable_lora=False,
735 + max_num_seqs=5,
736 + max_input_length=None,
737 + )
738 + tokenizer = tokenizer_group.tokenizer
739 +
740 + tools = [{
741 + "type": "function",
742 + "function": {
743 + "name": "dummy_function_name",
744 + "description": "This is a dummy function",
745 + "parameters": sample_json_schema
746 + }
747 + }] if use_tools else None
748 +
749 + # Test detecting the tokenizer's chat_template
750 + chat_template = _resolve_hf_chat_template(
751 + tokenizer,
752 + chat_template=None,
753 + tools=tools,
754 + trust_remote_code=True,
755 + )
756 + assert isinstance(chat_template, str)
757 +
758 +
759 # yapf: disable
760 @pytest.mark.parametrize(
761 ("model", "expected_format"),
762 [(PHI3V_MODEL_ID, "string"),
763 (QWEN2VL_MODEL_ID, "openai"),
764 + (QWEN25VL_MODEL_ID, "openai"),
765 (ULTRAVOX_MODEL_ID, "string"),
766 (MLLAMA_MODEL_ID, "openai"),
767 (LLAMA_GUARD_MODEL_ID, "openai")],
768 )
769 # yapf: enable
770 def test_resolve_content_format_hf_defined(model, expected_format):
771 + if model == QWEN25VL_MODEL_ID and Version(TRANSFORMERS_VERSION) < Version(
772 + "4.49.0"):
773 + pytest.skip("Qwen2.5-VL requires transformers>=4.49.0")
774 +
775 tokenizer_group = TokenizerGroup(
776 model,
777 enable_lora=False,
@@ -730,7 +780,13 @@ def test_resolve_content_format_hf_defined(model, expected_format):
780 )
781 tokenizer = tokenizer_group.tokenizer
782
733 - chat_template = tokenizer.chat_template
783 + # Test detecting the tokenizer's chat_template
784 + chat_template = _resolve_hf_chat_template(
785 + tokenizer,
786 + chat_template=None,
787 + tools=None,
788 + trust_remote_code=True,
789 + )
790 assert isinstance(chat_template, str)
791
792 print("[TEXT]")
@@ -740,8 +796,10 @@ def test_resolve_content_format_hf_defined(model, expected_format):
796
797 resolved_format = resolve_chat_template_content_format(
798 None, # Test detecting the tokenizer's chat_template
799 + None,
800 "auto",
801 tokenizer,
802 + trust_remote_code=True,
803 )
804
805 assert resolved_format == expected_format
@@ -791,8 +849,10 @@ def test_resolve_content_format_examples(template_path, expected_format):
849
850 resolved_format = resolve_chat_template_content_format(
851 chat_template,
852 + None,
853 "auto",
854 dummy_tokenizer,
855 + trust_remote_code=True,
856 )
857
858 assert resolved_format == expected_format
tests/tool_use/utils.py
+4 -1
@@ -39,7 +39,10 @@ def ensure_system_prompt(messages: list[dict[str, Any]],
39
40 # universal args for all models go here. also good if you need to test locally
41 # and change type or KV cache quantization or something.
42 -ARGS: list[str] = ["--enable-auto-tool-choice", "--max-model-len", "1024"]
42 +ARGS: list[str] = [
43 + "--enable-auto-tool-choice", "--max-model-len", "1024", "--max-num-seqs",
44 + "256"
45 +]
46
47 CONFIGS: dict[str, ServerConfig] = {
48 "hermes": {
vllm/entrypoints/chat_utils.py
+106 -39
@@ -1,7 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2
3 import asyncio
4 -import codecs
4 import json
5 from abc import ABC, abstractmethod
6 from collections import defaultdict, deque
@@ -30,7 +29,8 @@ from openai.types.chat.chat_completion_content_part_input_audio_param import (
29 InputAudio)
30 # yapf: enable
31 # pydantic needs the TypedDict from typing_extensions
33 -from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast
32 +from transformers import (PreTrainedTokenizer, PreTrainedTokenizerFast,
33 + ProcessorMixin)
34 from typing_extensions import Required, TypeAlias, TypedDict
35
36 from vllm.config import ModelConfig
@@ -306,24 +306,63 @@ def _detect_content_format(
306 return "openai"
307
308
309 +def _resolve_hf_chat_template(
310 + tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
311 + chat_template: Optional[str],
312 + tools: Optional[list[dict[str, Any]]],
313 + *,
314 + trust_remote_code: bool,
315 +) -> Optional[str]:
316 + # 1st priority: The given chat template
317 + if chat_template is not None:
318 + return chat_template
319 +
320 + # 2nd priority: AutoProcessor chat template, unless tool calling is enabled
321 + if tools is None:
322 + try:
323 + processor = cached_get_processor(
324 + tokenizer.name_or_path,
325 + processor_cls=(PreTrainedTokenizer, PreTrainedTokenizerFast,
326 + ProcessorMixin),
327 + trust_remote_code=trust_remote_code,
328 + )
329 + if isinstance(processor, ProcessorMixin) and \
330 + processor.chat_template is not None:
331 + return processor.chat_template
332 + except Exception:
333 + logger.debug("Failed to load AutoProcessor chat template for %s",
334 + tokenizer.name_or_path, exc_info=True)
335 +
336 + # 3rd priority: AutoTokenizer chat template
337 + try:
338 + return tokenizer.get_chat_template(chat_template, tools=tools)
339 + except Exception:
340 + logger.debug("Failed to load AutoTokenizer chat template for %s",
341 + tokenizer.name_or_path, exc_info=True)
342 +
343 + return None
344 +
345 +
346 def _resolve_chat_template_content_format(
347 chat_template: Optional[str],
348 + tools: Optional[list[dict[str, Any]]],
349 given_format: ChatTemplateContentFormatOption,
350 tokenizer: AnyTokenizer,
351 + *,
352 + trust_remote_code: bool,
353 ) -> _ChatTemplateContentFormat:
354 if isinstance(tokenizer, (PreTrainedTokenizer, PreTrainedTokenizerFast)):
315 - tokenizer_chat_template = tokenizer.chat_template
316 - else:
317 - tokenizer_chat_template = None
318 -
319 - jinja_text: Optional[str]
320 - if isinstance(tokenizer_chat_template, str) and chat_template is None:
321 - jinja_text = tokenizer_chat_template
322 - elif (isinstance(tokenizer_chat_template, dict)
323 - and chat_template in tokenizer_chat_template):
324 - jinja_text = tokenizer_chat_template[chat_template]
355 + hf_chat_template = _resolve_hf_chat_template(
356 + tokenizer,
357 + chat_template=chat_template,
358 + trust_remote_code=trust_remote_code,
359 + tools=tools,
360 + )
361 else:
326 - jinja_text = load_chat_template(chat_template, is_literal=True)
362 + hf_chat_template = None
363 +
364 + jinja_text = (hf_chat_template if isinstance(hf_chat_template, str)
365 + else load_chat_template(chat_template, is_literal=True))
366
367 detected_format = ("string" if jinja_text is None else
368 _detect_content_format(jinja_text, default="string"))
@@ -332,17 +371,11 @@ def _resolve_chat_template_content_format(
371
372
373 @lru_cache
335 -def resolve_chat_template_content_format(
374 +def _log_chat_template_content_format(
375 chat_template: Optional[str],
376 given_format: ChatTemplateContentFormatOption,
338 - tokenizer: AnyTokenizer,
339 -) -> _ChatTemplateContentFormat:
340 - detected_format = _resolve_chat_template_content_format(
341 - chat_template,
342 - given_format,
343 - tokenizer,
344 - )
345 -
377 + detected_format: ChatTemplateContentFormatOption,
378 +):
379 logger.info(
380 "Detected the chat template content format to be '%s'. "
381 "You can set `--chat-template-content-format` to override this.",
@@ -360,6 +393,29 @@ def resolve_chat_template_content_format(
393 detected_format,
394 )
395
396 +
397 +def resolve_chat_template_content_format(
398 + chat_template: Optional[str],
399 + tools: Optional[list[dict[str, Any]]],
400 + given_format: ChatTemplateContentFormatOption,
401 + tokenizer: AnyTokenizer,
402 + *,
403 + trust_remote_code: bool = False,
404 +) -> _ChatTemplateContentFormat:
405 + detected_format = _resolve_chat_template_content_format(
406 + chat_template,
407 + tools,
408 + given_format,
409 + tokenizer,
410 + trust_remote_code=trust_remote_code,
411 + )
412 +
413 + _log_chat_template_content_format(
414 + chat_template,
415 + given_format=given_format,
416 + detected_format=detected_format,
417 + )
418 +
419 return detected_format
420
421
@@ -711,7 +767,7 @@ def validate_chat_template(chat_template: Optional[Union[Path, str]]):
767 f"{type(chat_template)} is not a valid chat template type")
768
769
714 -def load_chat_template(
770 +def _load_chat_template(
771 chat_template: Optional[Union[Path, str]],
772 *,
773 is_literal: bool = False,
@@ -724,7 +780,7 @@ def load_chat_template(
780 raise TypeError("chat_template is expected to be read directly "
781 "from its value")
782
727 - return codecs.decode(chat_template, "unicode_escape")
783 + return chat_template
784
785 try:
786 with open(chat_template) as f:
@@ -742,7 +798,18 @@ def load_chat_template(
798
799 # If opening a file fails, set chat template to be args to
800 # ensure we decode so our escape are interpreted correctly
745 - return load_chat_template(chat_template, is_literal=True)
801 + return _load_chat_template(chat_template, is_literal=True)
802 +
803 +
804 +_cached_load_chat_template = lru_cache(_load_chat_template)
805 +
806 +
807 +def load_chat_template(
808 + chat_template: Optional[Union[Path, str]],
809 + *,
810 + is_literal: bool = False,
811 +) -> Optional[str]:
812 + return _cached_load_chat_template(chat_template, is_literal=is_literal)
813
814
815 # TODO: Let user specify how to insert multimodal tokens into prompt
@@ -1067,23 +1134,20 @@ def apply_hf_chat_template(
1134 tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
1135 conversation: list[ConversationMessage],
1136 chat_template: Optional[str],
1137 + tools: Optional[list[dict[str, Any]]],
1138 *,
1139 + trust_remote_code: bool = False,
1140 tokenize: bool = False, # Different from HF's default
1141 **kwargs: Any,
1142 ) -> str:
1074 - if chat_template is None:
1075 - chat_template = tokenizer.chat_template
1076 -
1077 - # FIXME: Temporary workaround for
1078 - # https://huggingface.co/mistral-community/pixtral-12b/discussions/31
1079 - if chat_template is None:
1080 - try:
1081 - processor = cached_get_processor(tokenizer.name_or_path)
1082 - chat_template = processor.chat_template
1083 - except Exception:
1084 - pass
1143 + hf_chat_template = _resolve_hf_chat_template(
1144 + tokenizer,
1145 + chat_template=chat_template,
1146 + tools=tools,
1147 + trust_remote_code=trust_remote_code,
1148 + )
1149
1086 - if chat_template is None:
1150 + if hf_chat_template is None:
1151 raise ValueError(
1152 "As of transformers v4.44, default chat template is no longer "
1153 "allowed, so you must provide a chat template if the tokenizer "
@@ -1091,7 +1155,8 @@ def apply_hf_chat_template(
1155
1156 return tokenizer.apply_chat_template(
1157 conversation=conversation, # type: ignore[arg-type]
1094 - chat_template=chat_template,
1158 + tools=tools, # type: ignore[arg-type]
1159 + chat_template=hf_chat_template,
1160 tokenize=tokenize,
1161 **kwargs,
1162 )
@@ -1100,7 +1165,8 @@ def apply_hf_chat_template(
1165 def apply_mistral_chat_template(
1166 tokenizer: MistralTokenizer,
1167 messages: list[ChatCompletionMessageParam],
1103 - chat_template: Optional[str] = None,
1168 + chat_template: Optional[str],
1169 + tools: Optional[list[dict[str, Any]]],
1170 **kwargs: Any,
1171 ) -> list[int]:
1172 if chat_template is not None:
@@ -1117,5 +1183,6 @@ def apply_mistral_chat_template(
1183
1184 return tokenizer.apply_chat_template(
1185 messages=messages,
1186 + tools=tools,
1187 **kwargs,
1188 )
vllm/entrypoints/llm.py
+5 -2
@@ -690,8 +690,10 @@ class LLM:
690 model_config = self.llm_engine.get_model_config()
691 resolved_content_format = resolve_chat_template_content_format(
692 chat_template,
693 + tools,
694 chat_template_content_format,
695 tokenizer,
696 + trust_remote_code=model_config.trust_remote_code,
697 )
698
699 prompts: list[Union[TokensPrompt, TextPrompt]] = []
@@ -713,18 +715,19 @@ class LLM:
715 tokenizer,
716 messages=msgs,
717 chat_template=chat_template,
718 + tools=tools,
719 add_generation_prompt=add_generation_prompt,
720 continue_final_message=continue_final_message,
718 - tools=tools,
721 )
722 else:
723 prompt_data = apply_hf_chat_template(
724 tokenizer,
725 + trust_remote_code=model_config.trust_remote_code,
726 conversation=conversation,
727 chat_template=chat_template,
728 + tools=tools,
729 add_generation_prompt=add_generation_prompt,
730 continue_final_message=continue_final_message,
727 - tools=tools,
731 )
732
733 prompt: Union[TokensPrompt, TextPrompt]
vllm/entrypoints/openai/serving_engine.py
+6 -1
@@ -379,14 +379,18 @@ class OpenAIServing:
379 add_special_tokens: bool = False,
380 ) -> tuple[list[ConversationMessage], Sequence[RequestPrompt],
381 list[TokensPrompt]]:
382 + model_config = self.model_config
383 +
384 resolved_content_format = resolve_chat_template_content_format(
385 chat_template,
386 + tool_dicts,
387 chat_template_content_format,
388 tokenizer,
389 + trust_remote_code=model_config.trust_remote_code,
390 )
391 conversation, mm_data_future = parse_chat_messages_futures(
392 messages,
389 - self.model_config,
393 + model_config,
394 tokenizer,
395 content_format=resolved_content_format,
396 )
@@ -410,6 +414,7 @@ class OpenAIServing:
414 else:
415 request_prompt = apply_hf_chat_template(
416 tokenizer,
417 + trust_remote_code=model_config.trust_remote_code,
418 conversation=conversation,
419 **_chat_template_kwargs,
420 )