main
py 801 lines 25.5 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 openai
8 import pybase64 as base64
9 import pytest
10 import pytest_asyncio
11 import requests
12 import torch
13 import torch.nn.functional as F
14
15 from tests.models.language.pooling.embed_utils import (
16 float_embeddings,
17 run_embedding_correctness_test,
18 )
19 from tests.models.utils import check_embeddings_close
20 from tests.utils import RemoteOpenAIServer
21 from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
22 from vllm.entrypoints.pooling.embed.protocol import EmbeddingResponse
23 from vllm.entrypoints.pooling.pooling.protocol import PoolingResponse
24 from vllm.entrypoints.pooling.utils import (
25 MetadataItem,
26 build_metadata_items,
27 decode_pooling_output,
28 )
29 from vllm.platforms import current_platform
30 from vllm.tokenizers import get_tokenizer
31 from vllm.utils.serial_utils import EMBED_DTYPES, ENDIANNESS, binary2tensor
32
33 MODEL_NAME = "intfloat/multilingual-e5-small"
34 DUMMY_CHAT_TEMPLATE = """{% for message in messages %}{{message['role'] + ': ' + message['content'] + '\\n'}}{% endfor %}""" # noqa: E501
35 DTYPE = "bfloat16"
36 input_text = "The best thing about vLLM is that it supports many different models"
37 input_tokens = [
38 0,
39 581,
40 2965,
41 13580,
42 1672,
43 81,
44 23708,
45 594,
46 83,
47 450,
48 442,
49 8060,
50 7,
51 5941,
52 12921,
53 115774,
54 2,
55 ]
56
57 # On ROCm, floating-point reductions in attention and GEMM kernels are
58 # non-associative and sensitive to batch geometry. Force LLM instances
59 # into an identical, deterministic execution mode:
60 ROCM_DETERMINISM_ARGS: list[str] = (
61 ["--max-num-seqs", "1"] if current_platform.is_rocm() else []
62 )
63
64
65 @pytest.fixture(scope="module")
66 def server():
67 args = [
68 "--runner",
69 "pooling",
70 "--dtype",
71 DTYPE,
72 "--enforce-eager",
73 "--max-model-len",
74 "512",
75 "--chat-template",
76 DUMMY_CHAT_TEMPLATE,
77 *ROCM_DETERMINISM_ARGS,
78 ]
79
80 with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
81 yield remote_server
82
83
84 @pytest_asyncio.fixture
85 async def client(server):
86 async with server.get_async_client() as async_client:
87 yield async_client
88
89
90 @pytest.fixture(scope="module")
91 def hf_model(hf_runner):
92 with hf_runner(MODEL_NAME, dtype=DTYPE, is_sentence_transformer=True) as hf_model:
93 yield hf_model
94
95
96 @pytest.mark.asyncio
97 @pytest.mark.parametrize("model_name", [MODEL_NAME])
98 async def test_negative_token_ids(client: openai.AsyncOpenAI, model_name: str):
99 # A negative token id is out of vocabulary just like an over-large one, but
100 # is not caught by the upper-bound check. Unlike the OpenAI completion
101 # schema, the pooling schema does not constrain token ids to be
102 # non-negative, so the request reaches the shared engine-level validation.
103 with pytest.raises(openai.BadRequestError, match=".*out of vocabulary.*"):
104 await client.embeddings.create(model=model_name, input=[-1])
105
106
107 @pytest.mark.asyncio
108 @pytest.mark.parametrize("model_name", [MODEL_NAME])
109 async def test_basic(
110 server: RemoteOpenAIServer, client: openai.AsyncOpenAI, model_name: str
111 ):
112 # test /v1/models
113 response = requests.get(server.url_for("/v1/models"))
114 model = response.json()["data"][0]["id"]
115 assert model == MODEL_NAME
116
117 models = await client.models.list()
118 models = models.data
119 served_model = models[0]
120 assert served_model.id == MODEL_NAME
121
122 # test /tokenize
123 response = requests.post(
124 server.url_for("/tokenize"),
125 json={"model": model_name, "prompt": input_text},
126 )
127 assert response.json()["tokens"] == input_tokens
128
129
130 @pytest.mark.asyncio
131 @pytest.mark.parametrize("model_name", [MODEL_NAME])
132 async def test_completion_request(
133 client: openai.AsyncOpenAI, model_name: str, hf_model
134 ):
135 # test input: str
136 embedding_response = await client.embeddings.create(
137 model=model_name,
138 input=input_text,
139 encoding_format="float",
140 )
141 embeddings = EmbeddingResponse.model_validate(
142 embedding_response.model_dump(mode="json")
143 )
144
145 assert embeddings.id is not None
146 assert len(embeddings.data) == 1
147 assert len(embeddings.data[0].embedding) == 384
148 assert embeddings.usage.completion_tokens == 0
149 assert embeddings.usage.prompt_tokens == len(input_tokens)
150 assert embeddings.usage.total_tokens == len(input_tokens)
151
152 vllm_outputs = float_embeddings(embeddings)
153 run_embedding_correctness_test(hf_model, [input_text], vllm_outputs)
154
155 # test input: list[int]
156 embedding_response = await client.embeddings.create(
157 model=model_name,
158 input=input_tokens,
159 encoding_format="float",
160 )
161 embeddings = EmbeddingResponse.model_validate(
162 embedding_response.model_dump(mode="json")
163 )
164
165 assert embeddings.id is not None
166 assert len(embeddings.data) == 1
167 assert len(embeddings.data[0].embedding) == 384
168 assert embeddings.usage.completion_tokens == 0
169 assert embeddings.usage.prompt_tokens == len(input_tokens)
170 assert embeddings.usage.total_tokens == len(input_tokens)
171
172 vllm_outputs = float_embeddings(embeddings)
173 run_embedding_correctness_test(hf_model, [input_text], vllm_outputs)
174
175
176 @pytest.mark.asyncio
177 @pytest.mark.parametrize("model_name", [MODEL_NAME])
178 async def test_completion_request_batched(
179 client: openai.AsyncOpenAI, model_name: str, hf_model
180 ):
181 N = 10
182 input_texts = [input_text] * N
183
184 # test input: list[str]
185 embedding_response = await client.embeddings.create(
186 model=model_name,
187 input=input_texts,
188 encoding_format="float",
189 )
190 embeddings = EmbeddingResponse.model_validate(
191 embedding_response.model_dump(mode="json")
192 )
193
194 assert embeddings.id is not None
195 assert len(embeddings.data) == N
196 assert len(embeddings.data[0].embedding) == 384
197 assert embeddings.usage.completion_tokens == 0
198 assert embeddings.usage.prompt_tokens == len(input_tokens) * N
199 assert embeddings.usage.total_tokens == len(input_tokens) * N
200
201 vllm_outputs = float_embeddings(embeddings)
202 run_embedding_correctness_test(hf_model, input_texts, vllm_outputs)
203
204 # test list[list[int]]
205 embedding_response = await client.embeddings.create(
206 model=model_name,
207 input=[input_tokens] * N,
208 encoding_format="float",
209 )
210 embeddings = EmbeddingResponse.model_validate(
211 embedding_response.model_dump(mode="json")
212 )
213
214 assert embeddings.id is not None
215 assert len(embeddings.data) == N
216 assert len(embeddings.data[0].embedding) == 384
217 assert embeddings.usage.completion_tokens == 0
218 assert embeddings.usage.prompt_tokens == len(input_tokens) * N
219 assert embeddings.usage.total_tokens == len(input_tokens) * N
220
221 vllm_outputs = float_embeddings(embeddings)
222 run_embedding_correctness_test(hf_model, input_texts, vllm_outputs)
223
224
225 @pytest.mark.asyncio
226 @pytest.mark.parametrize("model_name", [MODEL_NAME])
227 async def test_truncate_prompt_tokens(client: openai.AsyncOpenAI, model_name: str):
228 input_texts = [
229 "Como o Brasil pode fomentar o desenvolvimento de modelos de IA?",
230 ]
231
232 # test single embedding
233 embedding_response = await client.embeddings.create(
234 model=model_name, input=input_texts, extra_body={"truncate_prompt_tokens": 10}
235 )
236 embeddings = EmbeddingResponse.model_validate(
237 embedding_response.model_dump(mode="json")
238 )
239
240 assert embeddings.id is not None
241 assert len(embeddings.data) == 1
242 assert len(embeddings.data[0].embedding) == 384
243 assert embeddings.usage.completion_tokens == 0
244 assert embeddings.usage.prompt_tokens == 10
245 assert embeddings.usage.total_tokens == 10
246
247 input_tokens = [
248 1,
249 24428,
250 289,
251 18341,
252 26165,
253 285,
254 19323,
255 283,
256 289,
257 26789,
258 3871,
259 28728,
260 9901,
261 340,
262 2229,
263 385,
264 340,
265 315,
266 28741,
267 28804,
268 2,
269 ]
270 embedding_response = await client.embeddings.create(
271 model=model_name, input=input_tokens, extra_body={"truncate_prompt_tokens": 10}
272 )
273 embeddings = EmbeddingResponse.model_validate(
274 embedding_response.model_dump(mode="json")
275 )
276
277 assert embeddings.id is not None
278 assert len(embeddings.data) == 1
279 assert len(embeddings.data[0].embedding) == 384
280 assert embeddings.usage.completion_tokens == 0
281 assert embeddings.usage.prompt_tokens == 10
282 assert embeddings.usage.total_tokens == 10
283
284 # invalid_truncate_prompt_tokens
285 input_texts = [
286 "Como o Brasil pode fomentar o desenvolvimento de modelos de IA?",
287 ]
288
289 with pytest.raises(openai.BadRequestError):
290 response = await client.embeddings.create(
291 model=model_name,
292 input=input_texts,
293 extra_body={"truncate_prompt_tokens": 8193},
294 )
295 assert "error" in response.object
296 assert (
297 "truncate_prompt_tokens value is greater than max_model_len. "
298 "Please request a smaller truncation size." in response.message
299 )
300
301
302 @pytest.mark.asyncio
303 @pytest.mark.parametrize("model_name", [MODEL_NAME])
304 async def test_padding(client: openai.AsyncOpenAI, model_name: str):
305 input_texts = ["Hello"]
306
307 async def prompt_tokens(**extra_body):
308 response = await client.embeddings.create(
309 model=model_name,
310 input=input_texts,
311 extra_body=extra_body,
312 )
313 embeddings = EmbeddingResponse.model_validate(response.model_dump(mode="json"))
314 assert len(embeddings.data) == 1
315 return embeddings.usage.prompt_tokens
316
317 unpadded = await prompt_tokens()
318 padded = await prompt_tokens(padding="max_length")
319
320 # Models trained with a fixed sequence length and no attention mask need
321 # this; without it their embeddings are not comparable.
322 assert padded > unpadded
323 assert await prompt_tokens(padding="do_not_pad") == unpadded
324
325 with pytest.raises(openai.BadRequestError):
326 await client.embeddings.create(
327 model=model_name,
328 input=input_texts,
329 extra_body={"padding": "longest"},
330 )
331
332
333 @pytest.mark.asyncio
334 @pytest.mark.parametrize("model_name", [MODEL_NAME])
335 async def test_chat_request(
336 server: RemoteOpenAIServer, client: openai.AsyncOpenAI, model_name: str
337 ):
338 messages: list[ChatCompletionMessageParam] = [
339 {
340 "role": "user",
341 "content": "The cat sat on the mat.",
342 },
343 {
344 "role": "assistant",
345 "content": "A feline was resting on a rug.",
346 },
347 {
348 "role": "user",
349 "content": "Stars twinkle brightly in the night sky.",
350 },
351 ]
352
353 # test chat request basic usage
354 chat_response = requests.post(
355 server.url_for("v1/embeddings"),
356 json={
357 "model": model_name,
358 "messages": messages,
359 "encoding_format": "float",
360 },
361 )
362 chat_response.raise_for_status()
363 chat_embeddings = EmbeddingResponse.model_validate(chat_response.json())
364
365 tokenizer = get_tokenizer(tokenizer_name=model_name)
366 prompt = tokenizer.apply_chat_template(
367 messages,
368 chat_template=DUMMY_CHAT_TEMPLATE,
369 add_generation_prompt=True,
370 continue_final_message=False,
371 tokenize=False,
372 )
373 completion_response = await client.embeddings.create(
374 model=model_name,
375 input=prompt,
376 encoding_format="float",
377 # To be consistent with chat
378 extra_body={"add_special_tokens": False},
379 )
380 completion_embeddings = EmbeddingResponse.model_validate(
381 completion_response.model_dump(mode="json")
382 )
383
384 assert chat_embeddings.id is not None
385 assert completion_embeddings.id is not None
386 assert chat_embeddings.created <= completion_embeddings.created
387 # Use tolerance-based comparison for embeddings
388 check_embeddings_close(
389 embeddings_0_lst=float_embeddings(chat_embeddings),
390 embeddings_1_lst=float_embeddings(completion_embeddings),
391 name_0="chat",
392 name_1="completion",
393 )
394 assert chat_embeddings.model_dump(exclude={"id", "created", "data"}) == (
395 completion_embeddings.model_dump(exclude={"id", "created", "data"})
396 )
397
398 # test add_generation_prompt
399 response = requests.post(
400 server.url_for("v1/embeddings"),
401 json={"model": model_name, "messages": messages, "add_generation_prompt": True},
402 )
403
404 response.raise_for_status()
405 output = EmbeddingResponse.model_validate(response.json())
406
407 assert output.object == "list"
408 assert len(output.data) == 1
409 assert output.model == MODEL_NAME
410 assert output.usage.prompt_tokens == 33
411
412 # test continue_final_message
413 response = requests.post(
414 server.url_for("v1/embeddings"),
415 json={
416 "model": model_name,
417 "messages": messages,
418 "continue_final_message": True,
419 },
420 )
421
422 response.raise_for_status()
423 output = EmbeddingResponse.model_validate(response.json())
424
425 assert output.object == "list"
426 assert len(output.data) == 1
427 assert output.model == MODEL_NAME
428 assert output.usage.prompt_tokens == 33
429
430 # test add_special_tokens
431 response = requests.post(
432 server.url_for("v1/embeddings"),
433 json={"model": model_name, "messages": messages, "add_special_tokens": True},
434 )
435
436 response.raise_for_status()
437 output = EmbeddingResponse.model_validate(response.json())
438
439 assert output.object == "list"
440 assert len(output.data) == 1
441 assert output.model == MODEL_NAME
442 assert output.usage.prompt_tokens == 35
443
444 # test continue_final_message with add_generation_prompt
445 response = requests.post(
446 server.url_for("v1/embeddings"),
447 json={
448 "model": model_name,
449 "messages": messages,
450 "continue_final_message": True,
451 "add_generation_prompt": True,
452 },
453 )
454 assert (
455 "Cannot set both `continue_final_message` and `add_generation_prompt` to True."
456 in response.json()["error"]["message"]
457 )
458
459
460 @pytest.mark.asyncio
461 async def test_invocations_completion_request(
462 server: RemoteOpenAIServer, client: openai.AsyncOpenAI
463 ):
464 request_args = {
465 "model": MODEL_NAME,
466 "input": input_text,
467 "encoding_format": "float",
468 }
469
470 completion_response = await client.embeddings.create(**request_args)
471
472 invocation_response = requests.post(
473 server.url_for("invocations"), json=request_args
474 )
475 invocation_response.raise_for_status()
476
477 completion_output = completion_response.model_dump()
478 invocation_output = invocation_response.json()
479
480 assert completion_output.keys() == invocation_output.keys()
481 for completion_data, invocation_data in zip(
482 completion_output["data"], invocation_output["data"]
483 ):
484 assert completion_data.keys() == invocation_data.keys()
485 check_embeddings_close(
486 embeddings_0_lst=[completion_data["embedding"]],
487 embeddings_1_lst=[invocation_data["embedding"]],
488 name_0="completion",
489 name_1="invocation",
490 )
491
492
493 @pytest.mark.asyncio
494 async def test_invocations_chat_request(server: RemoteOpenAIServer):
495 messages: list[ChatCompletionMessageParam] = [
496 {
497 "role": "user",
498 "content": "The cat sat on the mat.",
499 },
500 {
501 "role": "assistant",
502 "content": "A feline was resting on a rug.",
503 },
504 {
505 "role": "user",
506 "content": "Stars twinkle brightly in the night sky.",
507 },
508 ]
509
510 request_args = {
511 "model": MODEL_NAME,
512 "messages": messages,
513 "encoding_format": "float",
514 }
515
516 chat_response = requests.post(server.url_for("v1/embeddings"), json=request_args)
517 chat_response.raise_for_status()
518
519 invocation_response = requests.post(
520 server.url_for("invocations"), json=request_args
521 )
522 invocation_response.raise_for_status()
523
524 chat_output = chat_response.json()
525 invocation_output = invocation_response.json()
526
527 assert chat_output.keys() == invocation_output.keys()
528 for chat_data, invocation_data in zip(
529 chat_output["data"], invocation_output["data"]
530 ):
531 assert chat_data.keys() == invocation_data.keys()
532 check_embeddings_close(
533 embeddings_0_lst=[chat_data["embedding"]],
534 embeddings_1_lst=[invocation_data["embedding"]],
535 name_0="chat",
536 name_1="invocation",
537 )
538
539
540 @pytest.mark.asyncio
541 @pytest.mark.parametrize("model_name", [MODEL_NAME])
542 async def test_base64_embedding(hf_model, client: openai.AsyncOpenAI, model_name: str):
543 input_texts = [
544 "Hello my name is",
545 "The best thing about vLLM is that it supports many different models",
546 ]
547
548 def check_outputs(outputs: list[list[float]]) -> None:
549 # The embeddings endpoint executes list elements as separate scheduler
550 # requests, so use the same per-request geometry for the HF reference.
551 for input_text, output in zip(input_texts, outputs, strict=True):
552 run_embedding_correctness_test(hf_model, [input_text], [output])
553
554 responses_float = await client.embeddings.create(
555 input=input_texts, model=model_name, encoding_format="float"
556 )
557 float_data = [d.embedding for d in responses_float.data]
558 check_outputs(float_data)
559
560 responses_base64 = await client.embeddings.create(
561 input=input_texts, model=model_name, encoding_format="base64"
562 )
563 base64_data = []
564 for data in responses_base64.data:
565 base64_data.append(
566 np.frombuffer(base64.b64decode(data.embedding), dtype="float32").tolist()
567 )
568
569 check_outputs(base64_data)
570
571 # Default response is float32 decoded from base64 by OpenAI Client
572 responses_default = await client.embeddings.create(
573 input=input_texts, model=model_name
574 )
575 default_data = [d.embedding for d in responses_default.data]
576 check_outputs(default_data)
577
578
579 @pytest.mark.asyncio
580 @pytest.mark.parametrize("model_name", [MODEL_NAME])
581 async def test_base64_embed_dtype_and_endianness(
582 server: RemoteOpenAIServer, client: openai.AsyncOpenAI, model_name: str
583 ):
584 input_texts = [input_text] * 3
585 responses_float = await client.embeddings.create(
586 input=input_texts, model=model_name, encoding_format="float"
587 )
588 float_data = [d.embedding for d in responses_float.data]
589
590 for embed_dtype in EMBED_DTYPES:
591 for endianness in ENDIANNESS:
592 responses_base64 = requests.post(
593 server.url_for("/v1/embeddings"),
594 json={
595 "model": model_name,
596 "input": input_texts,
597 "encoding_format": "base64",
598 "embed_dtype": embed_dtype,
599 "endianness": endianness,
600 },
601 )
602
603 base64_data = []
604 for data in responses_base64.json()["data"]:
605 binary = base64.b64decode(data["embedding"])
606 tensor = binary2tensor(binary, (-1,), embed_dtype, endianness)
607 base64_data.append(tensor.to(torch.float32).tolist())
608
609 check_embeddings_close(
610 embeddings_0_lst=float_data,
611 embeddings_1_lst=base64_data,
612 name_0="float_data",
613 name_1="base64_data",
614 tol=1e-2,
615 )
616
617
618 @pytest.mark.asyncio
619 @pytest.mark.parametrize("model_name", [MODEL_NAME])
620 async def test_bytes_embed_dtype_and_endianness(
621 server: RemoteOpenAIServer, client: openai.AsyncOpenAI, model_name: str
622 ):
623 input_texts = [input_text] * 3
624 responses_float = await client.embeddings.create(
625 input=input_texts, model=model_name, encoding_format="float"
626 )
627 float_data = [d.embedding for d in responses_float.data]
628
629 for embed_dtype in EMBED_DTYPES:
630 for endianness in ENDIANNESS:
631 responses_bytes = requests.post(
632 server.url_for("/v1/embeddings"),
633 json={
634 "model": model_name,
635 "input": input_texts,
636 "encoding_format": "bytes",
637 "embed_dtype": embed_dtype,
638 "endianness": endianness,
639 },
640 )
641
642 metadata = json.loads(responses_bytes.headers["metadata"])
643 body = responses_bytes.content
644 items = [MetadataItem(**x) for x in metadata["data"]]
645
646 bytes_data = decode_pooling_output(items=items, body=body)
647 bytes_data = [x.to(torch.float32).tolist() for x in bytes_data]
648
649 check_embeddings_close(
650 embeddings_0_lst=float_data,
651 embeddings_1_lst=bytes_data,
652 name_0="float_data",
653 name_1="bytes_data",
654 tol=1e-2,
655 )
656
657
658 @pytest.mark.asyncio
659 @pytest.mark.parametrize("model_name", [MODEL_NAME])
660 async def test_bytes_only_embed_dtype_and_endianness(
661 server: RemoteOpenAIServer, client: openai.AsyncOpenAI, model_name: str
662 ):
663 input_texts = [
664 "The best thing about vLLM is that it supports many different models",
665 ] * 2
666
667 responses_float = await client.embeddings.create(
668 input=input_texts, model=model_name, encoding_format="float"
669 )
670 float_data = [d.embedding for d in responses_float.data]
671 embedding_size = len(float_data[0])
672
673 for embed_dtype in EMBED_DTYPES:
674 for endianness in ENDIANNESS:
675 responses_bytes = requests.post(
676 server.url_for("/v1/embeddings"),
677 json={
678 "model": model_name,
679 "input": input_texts,
680 "encoding_format": "bytes_only",
681 "embed_dtype": embed_dtype,
682 "endianness": endianness,
683 },
684 )
685
686 assert "metadata" not in responses_bytes.headers
687 body = responses_bytes.content
688 items = build_metadata_items(
689 embed_dtype=embed_dtype,
690 endianness=endianness,
691 shape=(embedding_size,),
692 n_request=len(input_texts),
693 )
694
695 bytes_data = decode_pooling_output(items=items, body=body)
696 bytes_data = [x.to(torch.float32).tolist() for x in bytes_data]
697
698 check_embeddings_close(
699 embeddings_0_lst=float_data,
700 embeddings_1_lst=bytes_data,
701 name_0="float_data",
702 name_1="bytes_data",
703 tol=1e-2,
704 )
705
706
707 @pytest.mark.asyncio
708 @pytest.mark.parametrize("model_name", [MODEL_NAME])
709 @pytest.mark.parametrize("param_name", ["encoding_format", "embed_dtype", "endianness"])
710 async def test_params_not_supported(
711 server: RemoteOpenAIServer, model_name: str, param_name: str
712 ):
713 responses_base64 = requests.post(
714 server.url_for("/v1/embeddings"),
715 json={
716 "model": model_name,
717 "input": input_text,
718 "encoding_format": "base64",
719 param_name: f"bad_{param_name}",
720 },
721 )
722
723 assert responses_base64.status_code == 400
724 assert "literal_error" in responses_base64.json()["error"]["message"]
725 assert f"bad_{param_name}" in responses_base64.json()["error"]["message"]
726
727
728 @pytest.mark.asyncio
729 @pytest.mark.parametrize("model_name", [MODEL_NAME])
730 async def test_use_activation(server: RemoteOpenAIServer, model_name: str):
731 async def get_outputs(use_activation):
732 request_args = {
733 "model": MODEL_NAME,
734 "input": input_text,
735 "encoding_format": "float",
736 "use_activation": use_activation,
737 }
738
739 response = requests.post(server.url_for("v1/embeddings"), json=request_args)
740 outputs = response.json()
741
742 return torch.tensor([x["embedding"] for x in outputs["data"]])
743
744 default = await get_outputs(use_activation=None)
745 w_normal = await get_outputs(use_activation=True)
746 wo_normal = await get_outputs(use_activation=False)
747
748 assert torch.allclose(default, w_normal, atol=1e-2), "Default should use normal."
749 assert not torch.allclose(w_normal, wo_normal, atol=1e-2), (
750 "wo_normal should not use normal."
751 )
752 assert torch.allclose(w_normal, F.normalize(wo_normal, p=2, dim=-1), atol=1e-2), (
753 "w_normal should be close to normal(wo_normal)."
754 )
755
756
757 @pytest.mark.asyncio
758 @pytest.mark.parametrize("model_name", [MODEL_NAME])
759 async def test_pooling_embed(server: RemoteOpenAIServer, model_name: str):
760 task = "embed"
761 response = requests.post(
762 server.url_for("pooling"),
763 json={
764 "model": model_name,
765 "input": input_text,
766 "encoding_format": "float",
767 "task": task,
768 },
769 )
770
771 poolings = PoolingResponse.model_validate(response.json())
772
773 assert len(poolings.data) == 1
774 assert len(poolings.data[0].data) == 384
775
776
777 @pytest.mark.asyncio
778 @pytest.mark.parametrize("model_name", [MODEL_NAME])
779 @pytest.mark.parametrize(
780 "task", ["classify", "token_classify", "token_embed", "plugin"]
781 )
782 async def test_pooling_not_supported(
783 server: RemoteOpenAIServer, model_name: str, task: str
784 ):
785 response = requests.post(
786 server.url_for("pooling"),
787 json={
788 "model": model_name,
789 "input": "test",
790 "encoding_format": "float",
791 "task": task,
792 },
793 )
794 assert response.json()["error"]["type"] == "BadRequestError"
795 if task == "plugin":
796 err_msg = "No IOProcessor plugin installed."
797 elif task == "token_embed":
798 err_msg = "Try switching the model's pooling_task via"
799 else:
800 err_msg = f"Unsupported task: {task!r}"
801 assert response.json()["error"]["message"].startswith(err_msg)