main
py 342 lines 10.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4 """This example shows how to use vLLM for running offline inference with
5 the correct prompt format on vision language models for multimodal embedding.
6
7 For most models, the prompt format should follow corresponding examples
8 on HuggingFace model repository.
9 """
10
11 import argparse
12 from pathlib import Path
13
14 from PIL.Image import Image
15
16 from vllm import LLM
17 from vllm.multimodal.utils import fetch_image
18 from vllm.utils.print_utils import print_embeddings
19
20 ROOT_DIR = Path(__file__).parent.parent.parent
21 EMBED_TEMPLATE_DIR = ROOT_DIR / "pooling/embed/template/"
22
23 image_url = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/cat_snow.jpg"
24 text = "A cat standing in the snow."
25 multi_modal_data = {"image": fetch_image(image_url)}
26
27
28 def run_clip(seed: int):
29 llm = LLM(
30 model="openai/clip-vit-base-patch32",
31 runner="pooling",
32 limit_mm_per_prompt={"image": 1},
33 seed=seed,
34 )
35
36 print("Text embedding output:")
37 outputs = llm.embed(text, use_tqdm=False)
38 print_embeddings(outputs[0].outputs.embedding)
39
40 print("Image embedding output:")
41 prompt = "" # For image input, make sure that the prompt text is empty
42 outputs = llm.embed(
43 {
44 "prompt": prompt,
45 "multi_modal_data": multi_modal_data,
46 },
47 use_tqdm=False,
48 )
49 print_embeddings(outputs[0].outputs.embedding)
50
51
52 def run_e5_v(seed: int):
53 llm = LLM(
54 model="royokong/e5-v",
55 runner="pooling",
56 max_model_len=4096,
57 limit_mm_per_prompt={"image": 1},
58 seed=seed,
59 )
60
61 llama3_template = "<|start_header_id|>user<|end_header_id|>\n\n{}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n \n" # noqa: E501
62
63 print("Text embedding output:")
64 prompt_text = llama3_template.format(
65 f"{text}\nSummary above sentence in one word: "
66 )
67 outputs = llm.embed(prompt_text, use_tqdm=False)
68 print_embeddings(outputs[0].outputs.embedding)
69
70 print("Image embedding output:")
71 prompt_image = llama3_template.format("<image>\nSummary above image in one word: ")
72 outputs = llm.embed(
73 {
74 "prompt": prompt_image,
75 "multi_modal_data": multi_modal_data,
76 },
77 use_tqdm=False,
78 )
79 print_embeddings(outputs[0].outputs.embedding)
80
81
82 def run_qwen3_vl(seed: int):
83 try:
84 from qwen_vl_utils import smart_resize
85 except ModuleNotFoundError:
86 print(
87 "WARNING: `qwen-vl-utils` not installed, input images will not "
88 "be automatically resized. This can cause different results "
89 "comparing with HF repo's example. "
90 "You can enable this functionality by `pip install qwen-vl-utils`."
91 )
92 smart_resize = None
93
94 if smart_resize is not None:
95
96 def post_process_image(image: Image) -> Image:
97 width, height = image.size
98 resized_height, resized_width = smart_resize(
99 height,
100 width,
101 factor=32,
102 )
103 return image.resize((resized_width, resized_height))
104
105 multi_modal_data["image"] = post_process_image(multi_modal_data["image"])
106
107 default_instruction = "Represent the user's input."
108 image_placeholder = "<|vision_start|><|image_pad|><|vision_end|>"
109 prompt_text = f"<|im_start|>system\n{default_instruction}<|im_end|>\n<|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n"
110 prompt_image = f"<|im_start|>system\n{default_instruction}<|im_end|>\n<|im_start|>user\n{image_placeholder}<|im_end|>\n<|im_start|>assistant\n"
111 prompt_image_text = f"<|im_start|>system\n{default_instruction}<|im_end|>\n<|im_start|>user\n{image_placeholder}{text}<|im_end|>\n<|im_start|>assistant\n"
112
113 llm = LLM(
114 model="Qwen/Qwen3-VL-Embedding-2B",
115 runner="pooling",
116 max_model_len=8192,
117 limit_mm_per_prompt={"image": 1},
118 mm_processor_kwargs={"do_resize": False} if smart_resize is not None else None,
119 seed=seed,
120 )
121
122 print("Text embedding output:")
123 outputs = llm.embed(prompt_text, use_tqdm=False)
124 print_embeddings(outputs[0].outputs.embedding)
125
126 print("Image embedding output:")
127 outputs = llm.embed(
128 {
129 "prompt": prompt_image,
130 "multi_modal_data": multi_modal_data,
131 },
132 use_tqdm=False,
133 )
134 print_embeddings(outputs[0].outputs.embedding)
135
136 print("Image+Text embedding output:")
137 outputs = llm.embed(
138 {
139 "prompt": prompt_image_text,
140 "multi_modal_data": multi_modal_data,
141 },
142 use_tqdm=False,
143 )
144 print_embeddings(outputs[0].outputs.embedding)
145
146
147 def run_siglip(seed: int):
148 llm = LLM(
149 model="google/siglip-base-patch16-224",
150 runner="pooling",
151 limit_mm_per_prompt={"image": 1},
152 seed=seed,
153 )
154
155 print("Text embedding output:")
156 # SigLIP is trained with padding to a fixed length and no attention mask, so
157 # text embeddings only line up with image embeddings when they are padded.
158 outputs = llm.embed(
159 text,
160 tokenization_kwargs={"padding": "max_length"},
161 use_tqdm=False,
162 )
163 print_embeddings(outputs[0].outputs.embedding)
164
165 print("Image embedding output:")
166 prompt = "" # For image input, make sure that the prompt text is empty
167 outputs = llm.embed(
168 {
169 "prompt": prompt,
170 "multi_modal_data": multi_modal_data,
171 },
172 use_tqdm=False,
173 )
174 print_embeddings(outputs[0].outputs.embedding)
175
176
177 def run_vlm2vec_phi3v(seed: int):
178 llm = LLM(
179 model="TIGER-Lab/VLM2Vec-Full",
180 runner="pooling",
181 max_model_len=4096,
182 trust_remote_code=True,
183 mm_processor_kwargs={"num_crops": 4},
184 limit_mm_per_prompt={"image": 1},
185 seed=seed,
186 )
187 image_token = "<|image_1|>"
188
189 print("Text embedding output:")
190 prompt_text = f"Find me an everyday image that matches the given caption: {text}"
191 outputs = llm.embed(prompt_text, use_tqdm=False)
192 print_embeddings(outputs[0].outputs.embedding)
193
194 print("Image embedding output:")
195 prompt_image = f"{image_token} Find a day-to-day image that looks similar to the provided image." # noqa: E501
196 outputs = llm.embed(
197 {
198 "prompt": prompt_image,
199 "multi_modal_data": multi_modal_data,
200 },
201 use_tqdm=False,
202 )
203 print_embeddings(outputs[0].outputs.embedding)
204
205 print("Image+Text embedding output:")
206 prompt_image_text = (
207 f"{image_token} Represent the given image with the following question: {text}" # noqa: E501
208 )
209 outputs = llm.embed(
210 {
211 "prompt": prompt_image_text,
212 "multi_modal_data": multi_modal_data,
213 },
214 use_tqdm=False,
215 )
216 print_embeddings(outputs[0].outputs.embedding)
217
218
219 def run_vlm2vec_qwen2vl(seed: int):
220 # vLLM does not support LoRA adapters on multi-modal encoder,
221 # so we merge the weights first
222 from huggingface_hub.constants import HF_HUB_CACHE
223 from peft import PeftConfig, PeftModel
224 from transformers import AutoModelForImageTextToText, AutoProcessor
225
226 from vllm.entrypoints.chat_utils import load_chat_template
227
228 model_id = "TIGER-Lab/VLM2Vec-Qwen2VL-2B"
229
230 base_model = AutoModelForImageTextToText.from_pretrained(model_id)
231 lora_model = PeftModel.from_pretrained(
232 base_model,
233 model_id,
234 config=PeftConfig.from_pretrained(model_id),
235 )
236 model = lora_model.merge_and_unload().to(dtype=base_model.dtype)
237 model._hf_peft_config_loaded = False # Needed to save the merged model
238
239 processor = AutoProcessor.from_pretrained(
240 model_id,
241 # `min_pixels` and `max_pixels` are deprecated for
242 # transformers `preprocessor_config.json`
243 size={"shortest_edge": 3136, "longest_edge": 12845056},
244 )
245 processor.chat_template = load_chat_template(
246 # The original chat template is not correct
247 EMBED_TEMPLATE_DIR / "vlm2vec_qwen2vl.jinja",
248 )
249
250 merged_path = str(
251 Path(HF_HUB_CACHE) / ("models--" + model_id.replace("/", "--") + "-vllm")
252 )
253 print(f"Saving merged model to {merged_path}...")
254 print(
255 "NOTE: This directory is not tracked by `huggingface_hub` "
256 "so you have to delete this manually if you don't want it anymore."
257 )
258 model.save_pretrained(merged_path)
259 processor.save_pretrained(merged_path)
260 print("Done!")
261
262 llm = LLM(
263 model=merged_path,
264 runner="pooling",
265 max_model_len=4096,
266 mm_processor_kwargs={
267 "min_pixels": 3136,
268 "max_pixels": 12845056,
269 },
270 limit_mm_per_prompt={"image": 1},
271 seed=seed,
272 )
273 image_token = "<|image_pad|>"
274
275 print("Text embedding output:")
276 prompt_text = f"Find me an everyday image that matches the given caption: {text}"
277 outputs = llm.embed(prompt_text, use_tqdm=False)
278 print_embeddings(outputs[0].outputs.embedding)
279
280 print("Image embedding output:")
281 prompt_image = f"{image_token} Find a day-to-day image that looks similar to the provided image." # noqa: E501
282 outputs = llm.embed(
283 {
284 "prompt": prompt_image,
285 "multi_modal_data": multi_modal_data,
286 },
287 use_tqdm=False,
288 )
289 print_embeddings(outputs[0].outputs.embedding)
290
291 print("Image+Text embedding output:")
292 prompt_image_text = (
293 f"{image_token} Represent the given image with the following question: {text}" # noqa: E501
294 )
295 outputs = llm.embed(
296 {
297 "prompt": prompt_image_text,
298 "multi_modal_data": multi_modal_data,
299 },
300 use_tqdm=False,
301 )
302 print_embeddings(outputs[0].outputs.embedding)
303
304
305 model_example_map = {
306 "clip": run_clip,
307 "e5_v": run_e5_v,
308 "qwen3_vl": run_qwen3_vl,
309 "siglip": run_siglip,
310 "vlm2vec_phi3v": run_vlm2vec_phi3v,
311 "vlm2vec_qwen2vl": run_vlm2vec_qwen2vl,
312 }
313
314
315 def parse_args():
316 parser = argparse.ArgumentParser(
317 "Script to run a specified VLM through vLLM offline api."
318 )
319 parser.add_argument(
320 "--model",
321 "-m",
322 type=str,
323 default="vlm2vec_phi3v",
324 choices=model_example_map.keys(),
325 help="The name of the embedding model.",
326 )
327 parser.add_argument(
328 "--seed",
329 type=int,
330 default=0,
331 help="Set the seed when initializing `vllm.LLM`.",
332 )
333 return parser.parse_args()
334
335
336 def main(args):
337 model_example_map[args.model](args.seed)
338
339
340 if __name__ == "__main__":
341 args = parse_args()
342 main(args)