main
py 410 lines 11.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4 """Example Python client for multimodal embedding API using vLLM API server.
5
6 Refer to each `run_*` function for the command to run the server for that model.
7 """
8
9 import argparse
10 import io
11 from typing import Literal
12
13 import pybase64 as base64
14 from openai import OpenAI
15 from openai._types import NOT_GIVEN, NotGiven
16 from openai.types.chat import ChatCompletionMessageParam
17 from openai.types.create_embedding_response import CreateEmbeddingResponse
18 from PIL import Image
19
20 from vllm.utils.print_utils import print_embeddings
21
22 # Modify OpenAI's API key and API base to use vLLM's API server.
23 openai_api_key = "EMPTY"
24 openai_api_base = "http://localhost:8000/v1"
25
26 image_url = "https://vllm-public-assets.s3.us-west-2.amazonaws.com/multimodal_asset/cat_snow.jpg"
27 text = "A cat standing in the snow."
28
29
30 def create_chat_embeddings(
31 client: OpenAI,
32 *,
33 messages: list[ChatCompletionMessageParam],
34 model: str,
35 encoding_format: Literal["base64", "float"] | NotGiven = NOT_GIVEN,
36 continue_final_message: bool = False,
37 add_special_tokens: bool = False,
38 padding: str | NotGiven = NOT_GIVEN,
39 ) -> CreateEmbeddingResponse:
40 """Convenience function for accessing vLLM's Chat Embeddings API,
41 which is an extension of OpenAI's existing Embeddings API.
42 """
43 return client.post(
44 "/embeddings",
45 cast_to=CreateEmbeddingResponse,
46 body={
47 "messages": messages,
48 "model": model,
49 "encoding_format": encoding_format,
50 "continue_final_message": continue_final_message,
51 "add_special_tokens": add_special_tokens,
52 "padding": padding,
53 },
54 )
55
56
57 def run_clip(client: OpenAI, model: str):
58 """Start the server using:
59
60 vllm serve openai/clip-vit-base-patch32 \
61 --runner pooling
62 """
63 response = create_chat_embeddings(
64 client,
65 messages=[
66 {
67 "role": "user",
68 "content": [
69 {"type": "image_url", "image_url": {"url": image_url}},
70 ],
71 }
72 ],
73 model=model,
74 encoding_format="float",
75 )
76
77 print("Image embedding output:", response.data[0].embedding)
78
79 response = create_chat_embeddings(
80 client,
81 messages=[
82 {
83 "role": "user",
84 "content": [
85 {"type": "text", "text": "a photo of a cat"},
86 ],
87 }
88 ],
89 model=model,
90 encoding_format="float",
91 )
92
93 print("Text embedding output:", response.data[0].embedding)
94
95
96 def run_dse_qwen2_vl(client: OpenAI, model: str):
97 """Start the server using:
98
99 vllm serve MrLight/dse-qwen2-2b-mrl-v1 \
100 --runner pooling \
101 --trust-remote-code \
102 --max-model-len 8192 \
103 --chat-template examples/pooling/embed/template/dse_qwen2_vl.jinja
104 """
105 response = create_chat_embeddings(
106 client,
107 messages=[
108 {
109 "role": "user",
110 "content": [
111 {
112 "type": "image_url",
113 "image_url": {
114 "url": image_url,
115 },
116 },
117 {"type": "text", "text": "What is shown in this image?"},
118 ],
119 }
120 ],
121 model=model,
122 encoding_format="float",
123 )
124
125 print("Image embedding output:", response.data[0].embedding)
126
127 # MrLight/dse-qwen2-2b-mrl-v1 requires a placeholder image
128 # of the minimum input size
129 buffer = io.BytesIO()
130 image_placeholder = Image.new("RGB", (56, 56))
131 image_placeholder.save(buffer, "png")
132 buffer.seek(0)
133 image_placeholder = base64.b64encode(buffer.read()).decode("utf-8")
134 response = create_chat_embeddings(
135 client,
136 messages=[
137 {
138 "role": "user",
139 "content": [
140 {
141 "type": "image_url",
142 "image_url": {
143 "url": f"data:image/jpeg;base64,{image_placeholder}",
144 },
145 },
146 {"type": "text", "text": "Query: What is the weather like today?"},
147 ],
148 }
149 ],
150 model=model,
151 encoding_format="float",
152 )
153
154 print("Text embedding output:", response.data[0].embedding)
155
156
157 def run_qwen3_vl(client: OpenAI, model: str):
158 """Start the server using:
159
160 vllm serve Qwen/Qwen3-VL-Embedding-2B \
161 --runner pooling \
162 --max-model-len 8192
163 """
164 default_instruction = "Represent the user's input."
165
166 print("Text embedding output:")
167 response = create_chat_embeddings(
168 client,
169 messages=[
170 {
171 "role": "system",
172 "content": [
173 {"type": "text", "text": default_instruction},
174 ],
175 },
176 {
177 "role": "user",
178 "content": [
179 {"type": "text", "text": text},
180 ],
181 },
182 {
183 "role": "assistant",
184 "content": [
185 {"type": "text", "text": ""},
186 ],
187 },
188 ],
189 model=model,
190 encoding_format="float",
191 continue_final_message=True,
192 add_special_tokens=True,
193 )
194 print_embeddings(response.data[0].embedding)
195
196 print("Image embedding output:")
197 response = create_chat_embeddings(
198 client,
199 messages=[
200 {
201 "role": "system",
202 "content": [
203 {"type": "text", "text": default_instruction},
204 ],
205 },
206 {
207 "role": "user",
208 "content": [
209 {"type": "image_url", "image_url": {"url": image_url}},
210 {"type": "text", "text": ""},
211 ],
212 },
213 {
214 "role": "assistant",
215 "content": [
216 {"type": "text", "text": ""},
217 ],
218 },
219 ],
220 model=model,
221 encoding_format="float",
222 continue_final_message=True,
223 add_special_tokens=True,
224 )
225 print_embeddings(response.data[0].embedding)
226
227 print("Image+Text embedding output:")
228 response = create_chat_embeddings(
229 client,
230 messages=[
231 {
232 "role": "system",
233 "content": [
234 {"type": "text", "text": default_instruction},
235 ],
236 },
237 {
238 "role": "user",
239 "content": [
240 {"type": "image_url", "image_url": {"url": image_url}},
241 {
242 "type": "text",
243 "text": f"{text}",
244 },
245 ],
246 },
247 {
248 "role": "assistant",
249 "content": [
250 {"type": "text", "text": ""},
251 ],
252 },
253 ],
254 model=model,
255 encoding_format="float",
256 continue_final_message=True,
257 add_special_tokens=True,
258 )
259 print_embeddings(response.data[0].embedding)
260
261
262 def run_siglip(client: OpenAI, model: str):
263 """Start the server using:
264
265 vllm serve google/siglip-base-patch16-224 \
266 --runner pooling \
267 --chat-template template_basic.jinja
268 """
269 response = create_chat_embeddings(
270 client,
271 messages=[
272 {
273 "role": "user",
274 "content": [
275 {"type": "image_url", "image_url": {"url": image_url}},
276 ],
277 }
278 ],
279 model=model,
280 encoding_format="float",
281 )
282
283 print("Image embedding output:", response.data[0].embedding)
284
285 # SigLIP is trained with padding to a fixed length and no attention mask, so
286 # text embeddings only line up with image embeddings when they are padded.
287 response = create_chat_embeddings(
288 client,
289 messages=[
290 {
291 "role": "user",
292 "content": [
293 {"type": "text", "text": "a photo of a cat"},
294 ],
295 }
296 ],
297 model=model,
298 encoding_format="float",
299 padding="max_length",
300 )
301
302 print("Text embedding output:", response.data[0].embedding)
303
304
305 def run_vlm2vec(client: OpenAI, model: str):
306 """Start the server using:
307
308 vllm serve TIGER-Lab/VLM2Vec-Full \
309 --runner pooling \
310 --trust-remote-code \
311 --max-model-len 4096 \
312 --chat-template examples/pooling/embed/template/vlm2vec_phi3v.jinja
313 """
314 response = create_chat_embeddings(
315 client,
316 messages=[
317 {
318 "role": "user",
319 "content": [
320 {"type": "image_url", "image_url": {"url": image_url}},
321 {"type": "text", "text": "Represent the given image."},
322 ],
323 }
324 ],
325 model=model,
326 encoding_format="float",
327 )
328
329 print("Image embedding output:")
330 print_embeddings(response.data[0].embedding)
331
332 response = create_chat_embeddings(
333 client,
334 messages=[
335 {
336 "role": "user",
337 "content": [
338 {"type": "image_url", "image_url": {"url": image_url}},
339 {
340 "type": "text",
341 "text": "Represent the given image with the following question: What is in the image.",
342 },
343 ],
344 }
345 ],
346 model=model,
347 encoding_format="float",
348 )
349
350 print("Image+Text embedding output:")
351 print_embeddings(response.data[0].embedding)
352
353 response = create_chat_embeddings(
354 client,
355 messages=[
356 {
357 "role": "user",
358 "content": [
359 {"type": "text", "text": "A cat and a dog"},
360 ],
361 }
362 ],
363 model=model,
364 encoding_format="float",
365 )
366
367 print("Text embedding output:")
368 print_embeddings(response.data[0].embedding)
369
370
371 model_example_map = {
372 "clip": run_clip,
373 "qwen3_vl": run_qwen3_vl,
374 "dse_qwen2_vl": run_dse_qwen2_vl,
375 "siglip": run_siglip,
376 "vlm2vec": run_vlm2vec,
377 }
378
379
380 def parse_args():
381 parser = argparse.ArgumentParser(
382 "Script to call a specified VLM through the API. Make sure to serve "
383 "the model with `--runner pooling` before running this."
384 )
385 parser.add_argument(
386 "--model",
387 type=str,
388 choices=model_example_map.keys(),
389 required=True,
390 help="The name of the embedding model.",
391 )
392 return parser.parse_args()
393
394
395 def main(args):
396 client = OpenAI(
397 # defaults to os.environ.get("OPENAI_API_KEY")
398 api_key=openai_api_key,
399 base_url=openai_api_base,
400 )
401
402 models = client.models.list()
403 model_id = models.data[0].id
404
405 model_example_map[args.model](client, model_id)
406
407
408 if __name__ == "__main__":
409 args = parse_args()
410 main(args)