@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 102bf967f0
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, 14:45 UTC

[Model] Add smolvlm support (#16017)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>

Chauncey committed Apr 9, 2025 at 10:12 UTC 102bf967f09d16df654aab68bc86969fc2f58fcc
14 files changed +219 -6
docs/source/models/supported_models.md
+7
@@ -990,6 +990,13 @@ See [this page](#generative-models) for more information on how to use generativ
990 *
991 * ✅︎
992 * ✅︎
993 +- * `SmolVLMForConditionalGeneration`
994 + * SmolVLM2
995 + * T + I
996 + * `SmolVLM2-2.2B-Instruct`
997 + *
998 + * ✅︎
999 + * ✅︎
1000 - * `UltravoxModel`
1001 * Ultravox
1002 * T + A<sup>E+</sup>
examples/offline_inference/vision_language.py
+29
@@ -298,6 +298,34 @@ def run_idefics3(questions: list[str], modality: str) -> ModelRequestData:
298 )
299
300
301 +# SmolVLM2-2.2B-Instruct
302 +def run_smolvlm(questions: list[str], modality: str) -> ModelRequestData:
303 + assert modality == "image"
304 + model_name = "HuggingFaceTB/SmolVLM2-2.2B-Instruct"
305 +
306 + engine_args = EngineArgs(
307 + model=model_name,
308 + max_model_len=8192,
309 + max_num_seqs=2,
310 + enforce_eager=True,
311 + mm_processor_kwargs={
312 + "max_image_size": {
313 + "longest_edge": 384
314 + },
315 + },
316 + disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
317 + )
318 + prompts = [
319 + (f"<|im_start|>User:<image>{question}<end_of_utterance>\nAssistant:")
320 + for question in questions
321 + ]
322 +
323 + return ModelRequestData(
324 + engine_args=engine_args,
325 + prompts=prompts,
326 + )
327 +
328 +
329 # InternVL
330 def run_internvl(questions: list[str], modality: str) -> ModelRequestData:
331 assert modality == "image"
@@ -955,6 +983,7 @@ model_example_map = {
983 "qwen2_vl": run_qwen2_vl,
984 "qwen2_5_vl": run_qwen2_5_vl,
985 "skywork_chat": run_skyworkr1v,
986 + "smolvlm": run_smolvlm,
987 }
988
989
examples/offline_inference/vision_language_multi_image.py
+28
@@ -217,6 +217,33 @@ def load_idefics3(question: str, image_urls: list[str]) -> ModelRequestData:
217 )
218
219
220 +def load_smolvlm(question: str, image_urls: list[str]) -> ModelRequestData:
221 + model_name = "HuggingFaceTB/SmolVLM2-2.2B-Instruct"
222 +
223 + # The configuration below has been confirmed to launch on a single L40 GPU.
224 + engine_args = EngineArgs(
225 + model=model_name,
226 + max_model_len=8192,
227 + max_num_seqs=16,
228 + enforce_eager=True,
229 + limit_mm_per_prompt={"image": len(image_urls)},
230 + mm_processor_kwargs={
231 + "max_image_size": {
232 + "longest_edge": 384
233 + },
234 + },
235 + )
236 +
237 + placeholders = "\n".join(f"Image-{i}: <image>\n"
238 + for i, _ in enumerate(image_urls, start=1))
239 + prompt = f"<|im_start|>User:{placeholders}\n{question}<end_of_utterance>\nAssistant:" # noqa: E501
240 + return ModelRequestData(
241 + engine_args=engine_args,
242 + prompt=prompt,
243 + image_data=[fetch_image(url) for url in image_urls],
244 + )
245 +
246 +
247 def load_internvl(question: str, image_urls: list[str]) -> ModelRequestData:
248 model_name = "OpenGVLab/InternVL2-2B"
249
@@ -614,6 +641,7 @@ model_example_map = {
641 "qwen_vl_chat": load_qwen_vl_chat,
642 "qwen2_vl": load_qwen2_vl,
643 "qwen2_5_vl": load_qwen2_5_vl,
644 + "smolvlm": load_smolvlm,
645 }
646
647
requirements/test.in
+1
@@ -28,6 +28,7 @@ torchvision==0.21.0
28 transformers_stream_generator # required for qwen-vl test
29 matplotlib # required for qwen-vl test
30 mistral_common[opencv] >= 1.5.4 # required for pixtral test
31 +num2words # required for smolvlm test
32 opencv-python-headless >= 4.11.0 # required for video test
33 datamodel_code_generator # required for minicpm3 test
34 lm-eval[api]==0.4.8 # required for model evaluation test
requirements/test.txt
+4
@@ -101,6 +101,8 @@ dill==0.3.8
101 # multiprocess
102 dnspython==2.7.0
103 # via email-validator
104 +docopt==0.6.2
105 + # via num2words
106 docutils==0.16
107 # via awscli
108 einops==0.8.0
@@ -263,6 +265,8 @@ networkx==3.2.1
265 # via torch
266 nltk==3.9.1
267 # via rouge-score
268 +num2words==0.5.14
269 + # via -r requirements/test.in
270 numba==0.61.0
271 # via
272 # -r requirements/test.in
tests/models/decoder_only/vision_language/test_models.py
+10
@@ -493,6 +493,16 @@ VLM_TEST_SETTINGS = {
493 patch_hf_runner=model_utils.skyworkr1v_patch_hf_runner,
494 marks=[large_gpu_mark(min_gb=80)],
495 ),
496 + "smolvlm": VLMTestInfo(
497 + models=["HuggingFaceTB/SmolVLM2-2.2B-Instruct"],
498 + test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
499 + prompt_formatter=lambda img_prompt:f"<|im_start|>User:{img_prompt}<end_of_utterance>\nAssistant:", # noqa: E501
500 + img_idx_to_prompt=lambda idx: "<image>",
501 + max_model_len=8192,
502 + max_num_seqs=2,
503 + auto_cls=AutoModelForImageTextToText,
504 + hf_output_post_proc=model_utils.smolvlm_trunc_hf_output,
505 + ),
506 ### Tensor parallel / multi-gpu broadcast tests
507 "chameleon-broadcast": VLMTestInfo(
508 models=["facebook/chameleon-7b"],
tests/models/decoder_only/vision_language/vlm_utils/model_utils.py
+6
@@ -204,6 +204,12 @@ def idefics3_trunc_hf_output(hf_output: RunnerOutput,
204 return output_ids, output_str, out_logprobs
205
206
207 +def smolvlm_trunc_hf_output(hf_output: RunnerOutput,
208 + model: str) -> RunnerOutput:
209 + # Based on Idefics3
210 + return idefics3_trunc_hf_output(hf_output, model)
211 +
212 +
213 def minicpmv_trunc_hf_output(hf_output: RunnerOutput,
214 model: str) -> RunnerOutput:
215 output_ids, output_str, out_logprobs = hf_output
tests/models/multimodal/processing/test_common.py
+1
@@ -257,6 +257,7 @@ def _test_processing_correctness_mistral(
257 "h2oai/h2ovl-mississippi-800m",
258 "OpenGVLab/InternVL2-1B",
259 "HuggingFaceM4/Idefics3-8B-Llama3",
260 + "HuggingFaceTB/SmolVLM2-2.2B-Instruct",
261 "meta-llama/Llama-4-Scout-17B-16E-Instruct",
262 "llava-hf/llava-1.5-7b-hf",
263 "llava-hf/llava-v1.6-mistral-7b-hf",
tests/models/multimodal/processing/test_smolvlm.py new
+65
@@ -0,0 +1,65 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +"""Tests for smolvlm's multimodal preprocessing kwargs."""
3 +import pytest
4 +from transformers import SmolVLMConfig
5 +
6 +from vllm.multimodal import MULTIMODAL_REGISTRY
7 +
8 +from ....conftest import _ImageAssets
9 +from ...utils import build_model_context
10 +
11 +
12 +@pytest.mark.parametrize("model_id", ["HuggingFaceTB/SmolVLM2-2.2B-Instruct"])
13 +# yapf: disable
14 +@pytest.mark.parametrize(
15 + ("mm_processor_kwargs", "expected_toks_per_img"),
16 + [
17 + ({"max_image_size": {"longest_edge": 384}}, 1377),
18 + ({"max_image_size": {"longest_edge": 768}}, 405),
19 + ])
20 +# yapf: enable
21 +@pytest.mark.parametrize("num_imgs", [1, 2])
22 +@pytest.mark.parametrize("kwargs_on_init", [True, False])
23 +def test_processor_override(
24 + image_assets: _ImageAssets,
25 + model_id: str,
26 + mm_processor_kwargs: dict[str, object],
27 + expected_toks_per_img: int,
28 + num_imgs: int,
29 + kwargs_on_init: bool,
30 +):
31 + """Ensure Idefics3MultiModalProcessor handles num_crops properly."""
32 + # Same as the previous test - don't initialize mm_processor_kwargs
33 + # in this test and assume that the kwargs will be correctly expanded by
34 + # the partial when calling the custom input processor.
35 + ctx = build_model_context(
36 + model_id,
37 + mm_processor_kwargs=mm_processor_kwargs if kwargs_on_init else None,
38 + limit_mm_per_prompt={"image": num_imgs},
39 + )
40 + processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config)
41 + hf_processor_mm_kwargs = {} if kwargs_on_init else mm_processor_kwargs
42 +
43 + # Build the image str / prompt based on the number of images we pass
44 + placeholders = "<image>" if num_imgs == 1 else "\n".join(
45 + f"Image-{i}: <image>\n" for i in range(1, num_imgs + 1))
46 + prompt = f"<|im_start|>User:{placeholders}\n<end_of_utterance>\nAssistant:" # noqa: E501
47 +
48 + # Build mm_data
49 + image_size = ctx.get_hf_config(SmolVLMConfig).vision_config.image_size
50 + dummy_image_size = (image_size * 4, image_size * 4)
51 + dummy_image = image_assets[0].pil_image.resize(dummy_image_size)
52 + mm_data = {"image": [dummy_image] * num_imgs}
53 +
54 + processed_inputs = processor.apply(prompt, mm_data, hf_processor_mm_kwargs)
55 +
56 + # Ensure the placeholders format are correct
57 + hf_processor = processor.info.get_hf_processor(**hf_processor_mm_kwargs)
58 + hf_processed_inputs = hf_processor(text=prompt, images=mm_data["image"])
59 + assert processed_inputs["prompt_token_ids"] == hf_processed_inputs[
60 + "input_ids"][0]
61 +
62 + # Ensure we have the right number of placeholders per num_crops size
63 + image_token_id = ctx.get_hf_config().image_token_id
64 + img_tok_count = processed_inputs["prompt_token_ids"].count(image_token_id)
65 + assert img_tok_count == expected_toks_per_img * num_imgs
tests/models/registry.py
+1
@@ -344,6 +344,7 @@ _MULTIMODAL_EXAMPLE_MODELS = {
344 "Qwen2_5_VLForConditionalGeneration": _HfExamplesInfo("Qwen/Qwen2.5-VL-3B-Instruct", # noqa: E501
345 min_transformers_version="4.49"), # noqa: E501
346 "SkyworkR1VChatModel": _HfExamplesInfo("Skywork/Skywork-R1V-38B"),
347 + "SmolVLMForConditionalGeneration": _HfExamplesInfo("HuggingFaceTB/SmolVLM2-2.2B-Instruct"), # noqa: E501
348 "UltravoxModel": _HfExamplesInfo("fixie-ai/ultravox-v0_5-llama-3_2-1b", # noqa: E501
349 trust_remote_code=True,
350 max_transformers_version="4.50"),
vllm/entrypoints/chat_utils.py
+1 -1
@@ -498,7 +498,7 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
498 hf_config.image_token_index)
499 if model_type in ("aya_vision", "chameleon", "deepseek_vl_v2",
500 "internvl_chat", "skywork_chat", "NVLM_D",
501 - "h2ovl_chat", "idefics3"):
501 + "h2ovl_chat", "idefics3", "smolvlm"):
502 return "<image>"
503 if model_type in ("mllama", "llama4"):
504 return "<|image|>"
vllm/model_executor/models/idefics3.py
+14 -5
@@ -206,6 +206,16 @@ class Idefics3ProcessingInfo(BaseProcessingInfo):
206
207 return grid_w * grid_h + 1
208
209 + def _get_image_token(
210 + self,
211 + processor: Optional[Idefics3Processor]) -> tuple[str, str, str]:
212 + if processor is None:
213 + processor = self.get_hf_processor()
214 + image_token = processor.image_token.content
215 + fake_image_token = processor.fake_image_token.content
216 + global_image_token = processor.global_image_tag
217 + return image_token, fake_image_token, global_image_token
218 +
219 def get_image_repl(
220 self,
221 *,
@@ -216,9 +226,8 @@ class Idefics3ProcessingInfo(BaseProcessingInfo):
226 if processor is None:
227 processor = self.get_hf_processor()
228
219 - image_token = processor.image_token.content
220 - fake_image_token = processor.fake_image_token.content
221 - global_img_token = processor.global_image_tag
229 + image_token, fake_image_token, global_img_token = self._get_image_token(
230 + processor)
231 image_seq_len = processor.image_seq_len
232 grid_placeholder = "<row_{n_h}_col_{n_w}>"
233
@@ -300,7 +309,7 @@ class Idefics3DummyInputsBuilder(BaseDummyInputsBuilder[Idefics3ProcessingInfo]
309 hf_processor = self.info.get_hf_processor()
310 image_processor: Idefics3ImageProcessor = hf_processor.image_processor
311 longest_edge = image_processor.max_image_size['longest_edge']
303 - image_token = hf_processor.image_token.content
312 + image_token, _, _ = self.info._get_image_token(hf_processor)
313
314 mm_data = {
315 "image":
@@ -382,7 +391,7 @@ class Idefics3MultiModalProcessor(
391 out_mm_kwargs: MultiModalKwargs,
392 ) -> Sequence[PromptUpdate]:
393 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
385 - image_token = hf_processor.image_token.content
394 + image_token, _, _ = self.info._get_image_token(hf_processor)
395
396 def get_replacement_idefics3(item_idx: int) -> PromptUpdateDetails:
397 images = mm_items.get_items("image", ImageProcessorItems)
vllm/model_executor/models/registry.py
+1
@@ -175,6 +175,7 @@ _MULTIMODAL_MODELS = {
175 "H2OVLChatModel": ("h2ovl", "H2OVLChatModel"),
176 "InternVLChatModel": ("internvl", "InternVLChatModel"),
177 "Idefics3ForConditionalGeneration":("idefics3","Idefics3ForConditionalGeneration"),
178 + "SmolVLMForConditionalGeneration": ("smolvlm","SmolVLMForConditionalGeneration"), # noqa: E501
179 "LlavaForConditionalGeneration": ("llava", "LlavaForConditionalGeneration"),
180 "LlavaNextForConditionalGeneration": ("llava_next", "LlavaNextForConditionalGeneration"), # noqa: E501
181 "LlavaNextVideoForConditionalGeneration": ("llava_next_video", "LlavaNextVideoForConditionalGeneration"), # noqa: E501
vllm/model_executor/models/smolvlm.py new
+51
@@ -0,0 +1,51 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +
3 +from typing import Dict, Optional
4 +
5 +from transformers import SmolVLMProcessor
6 +
7 +from vllm.config import VllmConfig
8 +from vllm.multimodal import MULTIMODAL_REGISTRY
9 +
10 +# yapf: disable
11 +from .idefics3 import Idefics3DummyInputsBuilder as SmolVLMDummyInputsBuilder
12 +from .idefics3 import Idefics3ForConditionalGeneration
13 +from .idefics3 import Idefics3MultiModalProcessor as SmolVLMMultiModalProcessor
14 +from .idefics3 import Idefics3ProcessingInfo
15 +
16 +# yapf: enable
17 +
18 +
19 +class SmolVLMProcessingInfo(Idefics3ProcessingInfo):
20 +
21 + def get_hf_processor(
22 + self,
23 + *,
24 + max_image_size: Optional[Dict[str, int]] = None,
25 + **kwargs: object,
26 + ) -> SmolVLMProcessor:
27 + if max_image_size is not None:
28 + kwargs["max_image_size"] = max_image_size
29 +
30 + return self.ctx.get_hf_processor(SmolVLMProcessor, **kwargs)
31 +
32 + def _get_image_token(
33 + self, processor: Optional[SmolVLMProcessor]) -> tuple[str, str]:
34 + if processor is None:
35 + processor = self.get_hf_processor()
36 + image_token = processor.image_token
37 + fake_image_token = processor.fake_image_token
38 + global_image_token = processor.global_image_token
39 + return image_token, fake_image_token, global_image_token
40 +
41 +
42 +@MULTIMODAL_REGISTRY.register_processor(SmolVLMMultiModalProcessor,
43 + info=SmolVLMProcessingInfo,
44 + dummy_inputs=SmolVLMDummyInputsBuilder)
45 +class SmolVLMForConditionalGeneration(Idefics3ForConditionalGeneration):
46 +
47 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
48 + super().__init__(
49 + vllm_config=vllm_config,
50 + prefix=prefix,
51 + )