main
py 570 lines 18.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import json
5
6 import numpy as np
7 import pybase64 as base64
8 import pytest
9 import requests
10 import torch
11
12 from tests.models.utils import check_embeddings_close
13 from tests.utils import RemoteOpenAIServer
14 from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
15 from vllm.entrypoints.pooling.pooling.protocol import PoolingResponse
16 from vllm.entrypoints.pooling.utils import (
17 MetadataItem,
18 build_metadata_items,
19 decode_pooling_output,
20 )
21 from vllm.tokenizers import get_tokenizer
22 from vllm.utils.serial_utils import EMBED_DTYPES, ENDIANNESS, binary2tensor
23
24 MODEL_NAME = "internlm/internlm2-1_8b-reward"
25 DUMMY_CHAT_TEMPLATE = """{% for message in messages %}{{message['role'] + ': ' + message['content'] + '\\n'}}{% endfor %}""" # noqa: E501
26 input_text = "The chef prepared a delicious meal."
27 input_tokens = [1, 918, 29981, 10166, 395, 18067, 15265, 281]
28
29
30 @pytest.fixture(scope="module")
31 def server():
32 args = [
33 "--runner",
34 "pooling",
35 # use half precision for speed and memory savings in CI environment
36 "--dtype",
37 "bfloat16",
38 "--enforce-eager",
39 "--max-model-len",
40 "512",
41 "--chat-template",
42 DUMMY_CHAT_TEMPLATE,
43 "--trust-remote-code",
44 ]
45
46 with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
47 yield remote_server
48
49
50 @pytest.mark.parametrize("model_name", [MODEL_NAME])
51 def test_basic(server: RemoteOpenAIServer, model_name: str):
52 # test /v1/models
53 response = requests.get(server.url_for("/v1/models"))
54 served_model = response.json()["data"][0]["id"]
55 assert served_model == MODEL_NAME
56
57 # test /tokenize
58 response = requests.post(
59 server.url_for("/tokenize"),
60 json={"model": model_name, "prompt": input_text},
61 )
62 assert response.json()["tokens"] == input_tokens
63
64
65 @pytest.mark.asyncio
66 @pytest.mark.parametrize("model_name", [MODEL_NAME])
67 def test_completion_request(server: RemoteOpenAIServer, model_name: str):
68 # test input: str
69 response = requests.post(
70 server.url_for("pooling"),
71 json={"model": model_name, "input": input_text, "encoding_format": "float"},
72 )
73 response.raise_for_status()
74 poolings = PoolingResponse.model_validate(response.json())
75
76 assert poolings.id is not None
77 assert len(poolings.data) == 1
78 assert len(poolings.data[0].data) == len(input_tokens)
79 assert poolings.usage.completion_tokens == 0
80 assert poolings.usage.prompt_tokens == len(input_tokens)
81 assert poolings.usage.total_tokens == len(input_tokens)
82
83 # test input: list[int]
84 response = requests.post(
85 server.url_for("pooling"),
86 json={"model": model_name, "input": input_tokens, "encoding_format": "float"},
87 )
88 response.raise_for_status()
89 poolings = PoolingResponse.model_validate(response.json())
90
91 assert poolings.id is not None
92 assert len(poolings.data) == 1
93 assert len(poolings.data[0].data) == len(input_tokens)
94 assert poolings.usage.completion_tokens == 0
95 assert poolings.usage.prompt_tokens == len(input_tokens)
96 assert poolings.usage.total_tokens == len(input_tokens)
97
98
99 @pytest.mark.parametrize("model_name", [MODEL_NAME])
100 def test_completion_request_batched(server: RemoteOpenAIServer, model_name: str):
101 N = 10
102 input_texts = [input_text] * N
103
104 response = requests.post(
105 server.url_for("pooling"),
106 json={"model": model_name, "input": input_texts, "encoding_format": "float"},
107 )
108 response.raise_for_status()
109 poolings = PoolingResponse.model_validate(response.json())
110
111 assert poolings.id is not None
112 assert len(poolings.data) == N
113 assert len(poolings.data[0].data) == len(input_tokens)
114 assert poolings.usage.completion_tokens == 0
115 assert poolings.usage.prompt_tokens == len(input_tokens) * N
116 assert poolings.usage.total_tokens == len(input_tokens) * N
117
118 # test list[list[int]]
119 response = requests.post(
120 server.url_for("pooling"),
121 json={
122 "model": model_name,
123 "input": [input_tokens] * N,
124 "encoding_format": "float",
125 },
126 )
127 response.raise_for_status()
128 poolings = PoolingResponse.model_validate(response.json())
129
130 assert poolings.id is not None
131 assert len(poolings.data) == N
132 assert len(poolings.data[0].data) == len(input_tokens)
133 assert poolings.usage.completion_tokens == 0
134 assert poolings.usage.prompt_tokens == len(input_tokens) * N
135 assert poolings.usage.total_tokens == len(input_tokens) * N
136
137
138 @pytest.mark.asyncio
139 @pytest.mark.parametrize("model_name", [MODEL_NAME])
140 async def test_chat_request(server: RemoteOpenAIServer, model_name: str):
141 messages: list[ChatCompletionMessageParam] = [
142 {
143 "role": "user",
144 "content": "The cat sat on the mat.",
145 },
146 {
147 "role": "assistant",
148 "content": "A feline was resting on a rug.",
149 },
150 {
151 "role": "user",
152 "content": "Stars twinkle brightly in the night sky.",
153 },
154 ]
155
156 # test chat request basic usage
157 chat_response = requests.post(
158 server.url_for("pooling"),
159 json={
160 "model": model_name,
161 "messages": messages,
162 "encoding_format": "float",
163 },
164 )
165 chat_response.raise_for_status()
166 chat_poolings = PoolingResponse.model_validate(chat_response.json())
167
168 tokenizer = get_tokenizer(tokenizer_name=model_name, trust_remote_code=True)
169 prompt = tokenizer.apply_chat_template(
170 messages,
171 chat_template=DUMMY_CHAT_TEMPLATE,
172 add_generation_prompt=True,
173 continue_final_message=False,
174 tokenize=False,
175 )
176 completions_response = requests.post(
177 server.url_for("pooling"),
178 json={
179 "model": model_name,
180 "input": prompt,
181 "encoding_format": "float",
182 # To be consistent with chat
183 "add_special_tokens": False,
184 },
185 )
186 completions_response.raise_for_status()
187 completion_poolings = PoolingResponse.model_validate(completions_response.json())
188
189 assert chat_poolings.id is not None
190 assert completion_poolings.id is not None
191 assert chat_poolings.created <= completion_poolings.created
192 assert chat_poolings.model_dump(exclude={"id", "created"}) == (
193 completion_poolings.model_dump(exclude={"id", "created"})
194 )
195
196 # test add_generation_prompt
197 response = requests.post(
198 server.url_for("pooling"),
199 json={"model": model_name, "messages": messages, "add_generation_prompt": True},
200 )
201
202 response.raise_for_status()
203 output = PoolingResponse.model_validate(response.json())
204
205 assert output.object == "list"
206 assert len(output.data) == 1
207 assert output.model == MODEL_NAME
208 assert output.usage.prompt_tokens == 33
209
210 # test continue_final_message
211 # The continue_final_message parameter doesn't seem to be working with this model.
212 response = requests.post(
213 server.url_for("pooling"),
214 json={
215 "model": model_name,
216 "messages": messages,
217 "continue_final_message": True,
218 },
219 )
220
221 response.raise_for_status()
222 output = PoolingResponse.model_validate(response.json())
223
224 assert output.object == "list"
225 assert len(output.data) == 1
226 assert output.model == MODEL_NAME
227 assert output.usage.prompt_tokens == 33
228
229 # test add_special_tokens
230 response = requests.post(
231 server.url_for("pooling"),
232 json={"model": model_name, "messages": messages, "add_special_tokens": True},
233 )
234
235 response.raise_for_status()
236 output = PoolingResponse.model_validate(response.json())
237
238 assert output.object == "list"
239 assert len(output.data) == 1
240 assert output.model == MODEL_NAME
241 assert output.usage.prompt_tokens == 34
242
243 # test continue_final_message with add_generation_prompt
244 response = requests.post(
245 server.url_for("pooling"),
246 json={
247 "model": model_name,
248 "messages": messages,
249 "continue_final_message": True,
250 "add_generation_prompt": True,
251 },
252 )
253 assert (
254 "Cannot set both `continue_final_message` and `add_generation_prompt` to True."
255 in response.json()["error"]["message"]
256 )
257
258
259 @pytest.mark.asyncio
260 @pytest.mark.parametrize("model_name", [MODEL_NAME])
261 async def test_batch_base64_pooling(server: RemoteOpenAIServer, model_name: str):
262 input_texts = [
263 "Hello my name is",
264 "The best thing about vLLM is that it supports many different models",
265 ]
266
267 float_response = requests.post(
268 server.url_for("pooling"),
269 json={
270 "input": input_texts,
271 "model": model_name,
272 "encoding_format": "float",
273 },
274 )
275 float_response.raise_for_status()
276 responses_float = PoolingResponse.model_validate(float_response.json())
277 float_data = [np.array(d.data).squeeze(-1).tolist() for d in responses_float.data]
278
279 base64_response = requests.post(
280 server.url_for("pooling"),
281 json={
282 "input": input_texts,
283 "model": model_name,
284 "encoding_format": "base64",
285 },
286 )
287 base64_response.raise_for_status()
288 responses_base64 = PoolingResponse.model_validate(base64_response.json())
289
290 decoded_responses_base64_data = []
291 for data in responses_base64.data:
292 decoded_responses_base64_data.append(
293 np.frombuffer(base64.b64decode(data.data), dtype="float32").tolist()
294 )
295
296 check_embeddings_close(
297 embeddings_0_lst=float_data,
298 embeddings_1_lst=decoded_responses_base64_data,
299 name_0="float32",
300 name_1="base64",
301 )
302
303 # Default response is float32 decoded from base64 by OpenAI Client
304 default_response = requests.post(
305 server.url_for("pooling"),
306 json={
307 "input": input_texts,
308 "model": model_name,
309 },
310 )
311 default_response.raise_for_status()
312 responses_default = PoolingResponse.model_validate(default_response.json())
313 default_data = [
314 np.array(d.data).squeeze(-1).tolist() for d in responses_default.data
315 ]
316
317 check_embeddings_close(
318 embeddings_0_lst=float_data,
319 embeddings_1_lst=default_data,
320 name_0="float32",
321 name_1="default",
322 )
323
324
325 @pytest.mark.asyncio
326 @pytest.mark.parametrize("model_name", [MODEL_NAME])
327 async def test_base64_embed_dtype_and_endianness(
328 server: RemoteOpenAIServer, model_name: str
329 ):
330 input_texts = [input_text] * 3
331
332 url = server.url_for("pooling")
333 float_response = requests.post(
334 url,
335 json={
336 "model": model_name,
337 "input": input_texts,
338 "encoding_format": "float",
339 },
340 )
341 responses_float = PoolingResponse.model_validate(float_response.json())
342 float_data = [np.array(d.data).squeeze(-1).tolist() for d in responses_float.data]
343
344 for embed_dtype in EMBED_DTYPES:
345 for endianness in ENDIANNESS:
346 responses_base64 = requests.post(
347 url,
348 json={
349 "model": model_name,
350 "input": input_texts,
351 "encoding_format": "base64",
352 "embed_dtype": embed_dtype,
353 "endianness": endianness,
354 },
355 )
356
357 base64_data = []
358 for data in responses_base64.json()["data"]:
359 binary = base64.b64decode(data["data"])
360 tensor = binary2tensor(binary, (-1,), embed_dtype, endianness)
361 base64_data.append(tensor.to(torch.float32).tolist())
362
363 check_embeddings_close(
364 embeddings_0_lst=float_data,
365 embeddings_1_lst=base64_data,
366 name_0="float_data",
367 name_1="base64_data",
368 tol=1e-2,
369 )
370
371
372 @pytest.mark.asyncio
373 @pytest.mark.parametrize("model_name", [MODEL_NAME])
374 async def test_bytes_embed_dtype_and_endianness(
375 server: RemoteOpenAIServer, model_name: str
376 ):
377 input_texts = [input_text] * 3
378
379 url = server.url_for("pooling")
380 float_response = requests.post(
381 url,
382 json={
383 "model": model_name,
384 "input": input_texts,
385 "encoding_format": "float",
386 },
387 )
388 responses_float = PoolingResponse.model_validate(float_response.json())
389 float_data = [np.array(d.data).squeeze(-1).tolist() for d in responses_float.data]
390
391 for embed_dtype in EMBED_DTYPES:
392 for endianness in ENDIANNESS:
393 responses_bytes = requests.post(
394 url,
395 json={
396 "model": model_name,
397 "input": input_texts,
398 "encoding_format": "bytes",
399 "embed_dtype": embed_dtype,
400 "endianness": endianness,
401 },
402 )
403
404 metadata = json.loads(responses_bytes.headers["metadata"])
405 body = responses_bytes.content
406 items = [MetadataItem(**x) for x in metadata["data"]]
407
408 bytes_data = decode_pooling_output(items=items, body=body)
409 bytes_data = [x.to(torch.float32).view(-1).tolist() for x in bytes_data]
410
411 check_embeddings_close(
412 embeddings_0_lst=float_data,
413 embeddings_1_lst=bytes_data,
414 name_0="float_data",
415 name_1="bytes_data",
416 tol=1e-2,
417 )
418
419
420 @pytest.mark.asyncio
421 @pytest.mark.parametrize("model_name", [MODEL_NAME])
422 async def test_bytes_only_embed_dtype_and_endianness(
423 server: RemoteOpenAIServer, model_name: str
424 ):
425 input_texts = [input_text] * 3
426
427 url = server.url_for("pooling")
428 float_response = requests.post(
429 url,
430 json={
431 "model": model_name,
432 "input": input_texts,
433 "encoding_format": "float",
434 },
435 )
436 responses_float = PoolingResponse.model_validate(float_response.json())
437 float_data = [np.array(d.data).squeeze(-1).tolist() for d in responses_float.data]
438 n_tokens = responses_float.usage.prompt_tokens // len(input_texts)
439
440 for embed_dtype in EMBED_DTYPES:
441 for endianness in ENDIANNESS:
442 responses_bytes = requests.post(
443 url,
444 json={
445 "model": model_name,
446 "input": input_texts,
447 "encoding_format": "bytes_only",
448 "embed_dtype": embed_dtype,
449 "endianness": endianness,
450 },
451 )
452
453 assert "metadata" not in responses_bytes.headers
454 body = responses_bytes.content
455 items = build_metadata_items(
456 embed_dtype=embed_dtype,
457 endianness=endianness,
458 shape=(n_tokens, 1),
459 n_request=len(input_texts),
460 )
461 bytes_data = decode_pooling_output(items=items, body=body)
462 bytes_data = [x.to(torch.float32).view(-1).tolist() for x in bytes_data]
463
464 check_embeddings_close(
465 embeddings_0_lst=float_data,
466 embeddings_1_lst=bytes_data,
467 name_0="float_data",
468 name_1="bytes_data",
469 tol=1e-2,
470 )
471
472
473 @pytest.mark.asyncio
474 @pytest.mark.parametrize("model_name", [MODEL_NAME])
475 @pytest.mark.parametrize("param_name", ["encoding_format", "embed_dtype", "endianness"])
476 async def test_params_not_supported(
477 server: RemoteOpenAIServer, model_name: str, param_name: str
478 ):
479 responses_base64 = requests.post(
480 server.url_for("pooling"),
481 json={
482 "model": model_name,
483 "input": input_text,
484 "encoding_format": "base64",
485 param_name: f"bad_{param_name}",
486 },
487 )
488
489 assert responses_base64.status_code == 400
490 assert "literal_error" in responses_base64.json()["error"]["message"]
491 assert f"bad_{param_name}" in responses_base64.json()["error"]["message"]
492
493
494 @pytest.mark.asyncio
495 async def test_invocations_chat_request(server: RemoteOpenAIServer):
496 request_args = {
497 "model": MODEL_NAME,
498 "input": input_text,
499 "encoding_format": "float",
500 }
501
502 completion_response = requests.post(server.url_for("pooling"), json=request_args)
503 completion_response.raise_for_status()
504
505 invocation_response = requests.post(
506 server.url_for("invocations"), json=request_args
507 )
508 invocation_response.raise_for_status()
509
510 completion_output = completion_response.json()
511 invocation_output = invocation_response.json()
512
513 assert completion_output.keys() == invocation_output.keys()
514 for completion_data, invocation_data in zip(
515 completion_output["data"], invocation_output["data"]
516 ):
517 assert completion_data.keys() == invocation_data.keys()
518 check_embeddings_close(
519 embeddings_0_lst=completion_data["data"],
520 embeddings_1_lst=invocation_data["data"],
521 name_0="completion",
522 name_1="invocation",
523 )
524
525
526 @pytest.mark.asyncio
527 async def test_invocations_conversation_chat_request(server: RemoteOpenAIServer):
528 messages: list[ChatCompletionMessageParam] = [
529 {
530 "role": "user",
531 "content": "The cat sat on the mat.",
532 },
533 {
534 "role": "assistant",
535 "content": "A feline was resting on a rug.",
536 },
537 {
538 "role": "user",
539 "content": "Stars twinkle brightly in the night sky.",
540 },
541 ]
542
543 request_args = {
544 "model": MODEL_NAME,
545 "messages": messages,
546 "encoding_format": "float",
547 }
548
549 chat_response = requests.post(server.url_for("pooling"), json=request_args)
550 chat_response.raise_for_status()
551
552 invocation_response = requests.post(
553 server.url_for("invocations"), json=request_args
554 )
555 invocation_response.raise_for_status()
556
557 chat_output = chat_response.json()
558 invocation_output = invocation_response.json()
559
560 assert chat_output.keys() == invocation_output.keys()
561 for chat_data, invocation_data in zip(
562 chat_output["data"], invocation_output["data"]
563 ):
564 assert chat_data.keys() == invocation_data.keys()
565 check_embeddings_close(
566 embeddings_0_lst=chat_data["data"],
567 embeddings_1_lst=invocation_data["data"],
568 name_0="chat",
569 name_1="invocation",
570 )