main
py 460 lines 14.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import requests
6 import torch
7 import torch.nn.functional as F
8
9 from tests.utils import RemoteOpenAIServer
10 from vllm.entrypoints.pooling.classify.protocol import ClassificationResponse
11 from vllm.entrypoints.pooling.pooling.protocol import PoolingResponse
12
13 MODEL_NAME = "jason9693/Qwen2.5-1.5B-apeach"
14 DTYPE = "float32" # Use float32 to avoid NaN issue
15 input_text = "This product was excellent and exceeded my expectations"
16 input_tokens = [1986, 1985, 572, 9073, 323, 33808, 847, 16665]
17
18
19 @pytest.fixture(scope="module")
20 def server():
21 args = [
22 "--enforce-eager",
23 "--max-model-len",
24 "512",
25 "--dtype",
26 DTYPE,
27 ]
28
29 with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
30 yield remote_server
31
32
33 @pytest.mark.parametrize("model_name", [MODEL_NAME])
34 def test_basic(server: RemoteOpenAIServer, model_name: str):
35 # test /v1/models
36 response = requests.get(server.url_for("/v1/models"))
37 served_model = response.json()["data"][0]["id"]
38 assert served_model == MODEL_NAME
39
40 # test /tokenize
41 response = requests.post(
42 server.url_for("/tokenize"),
43 json={"model": model_name, "prompt": input_text},
44 )
45 assert response.json()["tokens"] == input_tokens
46
47
48 @pytest.mark.parametrize("model_name", [MODEL_NAME])
49 def test_completion_request(server: RemoteOpenAIServer, model_name: str):
50 # test input: str
51 classification_response = requests.post(
52 server.url_for("classify"),
53 json={"model": model_name, "input": input_text},
54 )
55
56 classification_response.raise_for_status()
57 output = ClassificationResponse.model_validate(classification_response.json())
58
59 assert output.object == "list"
60 assert output.model == MODEL_NAME
61 assert len(output.data) == 1
62 assert hasattr(output.data[0], "label")
63 assert hasattr(output.data[0], "probs")
64
65 # test input: list[int]
66 classification_response = requests.post(
67 server.url_for("classify"),
68 json={"model": model_name, "input": input_tokens},
69 )
70
71 classification_response.raise_for_status()
72 output = ClassificationResponse.model_validate(classification_response.json())
73
74 assert output.object == "list"
75 assert output.model == MODEL_NAME
76 assert len(output.data) == 1
77 assert hasattr(output.data[0], "label")
78 assert hasattr(output.data[0], "probs")
79
80
81 @pytest.mark.parametrize("model_name", [MODEL_NAME])
82 def test_completion_request_batched(server: RemoteOpenAIServer, model_name: str):
83 N = 10
84
85 # test input: list[str]
86 classification_response = requests.post(
87 server.url_for("classify"),
88 json={"model": model_name, "input": [input_text] * N},
89 )
90 output = ClassificationResponse.model_validate(classification_response.json())
91
92 assert len(output.data) == N
93 for i, item in enumerate(output.data):
94 assert item.index == i
95 assert hasattr(item, "label")
96 assert hasattr(item, "probs")
97 assert len(item.probs) == item.num_classes
98 assert item.label in ["Default", "Spoiled"]
99
100 # test input: list[list[int]]
101 classification_response = requests.post(
102 server.url_for("classify"),
103 json={"model": model_name, "input": [input_tokens] * N},
104 )
105 output = ClassificationResponse.model_validate(classification_response.json())
106
107 assert len(output.data) == N
108 for i, item in enumerate(output.data):
109 assert item.index == i
110 assert hasattr(item, "label")
111 assert hasattr(item, "probs")
112 assert len(item.probs) == item.num_classes
113 assert item.label in ["Default", "Spoiled"]
114
115
116 @pytest.mark.parametrize("model_name", [MODEL_NAME])
117 def test_empty_input_error(server: RemoteOpenAIServer, model_name: str):
118 classification_response = requests.post(
119 server.url_for("classify"),
120 json={"model": model_name, "input": ""},
121 )
122
123 error = classification_response.json()
124 assert classification_response.status_code == 400
125 assert "error" in error
126
127 classification_response = requests.post(
128 server.url_for("classify"),
129 json={"model": model_name, "input": []},
130 )
131
132 error = classification_response.json()
133 assert classification_response.status_code == 400
134 assert "error" in error
135
136
137 @pytest.mark.parametrize("model_name", [MODEL_NAME])
138 def test_truncate_prompt_tokens(server: RemoteOpenAIServer, model_name: str):
139 long_text = "hello " * 600
140
141 classification_response = requests.post(
142 server.url_for("classify"),
143 json={"model": model_name, "input": long_text, "truncate_prompt_tokens": 5},
144 )
145
146 classification_response.raise_for_status()
147 output = ClassificationResponse.model_validate(classification_response.json())
148
149 assert len(output.data) == 1
150 assert output.data[0].index == 0
151 assert hasattr(output.data[0], "probs")
152 assert output.usage.prompt_tokens == 5
153 assert output.usage.total_tokens == 5
154
155 # invalid_truncate_prompt_tokens
156 classification_response = requests.post(
157 server.url_for("classify"),
158 json={"model": model_name, "input": "test", "truncate_prompt_tokens": 513},
159 )
160
161 error = classification_response.json()
162 assert classification_response.status_code == 400
163 assert "truncate_prompt_tokens" in error["error"]["message"]
164
165
166 @pytest.mark.parametrize("model_name", [MODEL_NAME])
167 def test_add_special_tokens(server: RemoteOpenAIServer, model_name: str):
168 # The add_special_tokens parameter doesn't seem to be working with this model.
169 # working with papluca/xlm-roberta-base-language-detection
170 response = requests.post(
171 server.url_for("classify"),
172 json={"model": model_name, "input": input_text, "add_special_tokens": False},
173 )
174 response.raise_for_status()
175 ClassificationResponse.model_validate(response.json())
176
177 response = requests.post(
178 server.url_for("classify"),
179 json={"model": model_name, "input": input_text, "add_special_tokens": True},
180 )
181 response.raise_for_status()
182 ClassificationResponse.model_validate(response.json())
183
184
185 @pytest.mark.asyncio
186 @pytest.mark.parametrize("model_name", [MODEL_NAME])
187 async def test_chat_request(server: RemoteOpenAIServer, model_name: str):
188 messages = [
189 {
190 "role": "user",
191 "content": "The cat sat on the mat.",
192 },
193 {
194 "role": "assistant",
195 "content": "A feline was resting on a rug.",
196 },
197 {
198 "role": "user",
199 "content": "Stars twinkle brightly in the night sky.",
200 },
201 ]
202
203 # test chat request basic usage
204 response = requests.post(
205 server.url_for("classify"),
206 json={"model": model_name, "messages": messages},
207 )
208
209 response.raise_for_status()
210 output = ClassificationResponse.model_validate(response.json())
211
212 assert output.object == "list"
213 assert output.model == MODEL_NAME
214 assert len(output.data) == 1
215 assert hasattr(output.data[0], "label")
216 assert hasattr(output.data[0], "probs")
217 assert output.usage.prompt_tokens == 51
218
219 # test add_generation_prompt
220 response = requests.post(
221 server.url_for("classify"),
222 json={"model": model_name, "messages": messages, "add_generation_prompt": True},
223 )
224
225 response.raise_for_status()
226 output = ClassificationResponse.model_validate(response.json())
227
228 assert output.object == "list"
229 assert output.model == MODEL_NAME
230 assert len(output.data) == 1
231 assert hasattr(output.data[0], "label")
232 assert hasattr(output.data[0], "probs")
233 assert output.usage.prompt_tokens == 54
234
235 # test continue_final_message
236 response = requests.post(
237 server.url_for("classify"),
238 json={
239 "model": model_name,
240 "messages": messages,
241 "continue_final_message": True,
242 },
243 )
244
245 response.raise_for_status()
246 output = ClassificationResponse.model_validate(response.json())
247
248 assert output.object == "list"
249 assert output.model == MODEL_NAME
250 assert len(output.data) == 1
251 assert hasattr(output.data[0], "label")
252 assert hasattr(output.data[0], "probs")
253 assert output.usage.prompt_tokens == 49
254
255 # test add_special_tokens
256 # The add_special_tokens parameter doesn't seem to be working with this model.
257 response = requests.post(
258 server.url_for("classify"),
259 json={"model": model_name, "messages": messages, "add_special_tokens": True},
260 )
261
262 response.raise_for_status()
263 output = ClassificationResponse.model_validate(response.json())
264
265 assert output.object == "list"
266 assert output.model == MODEL_NAME
267 assert len(output.data) == 1
268 assert hasattr(output.data[0], "label")
269 assert hasattr(output.data[0], "probs")
270 assert output.usage.prompt_tokens == 51
271
272 # test continue_final_message with add_generation_prompt
273 response = requests.post(
274 server.url_for("classify"),
275 json={
276 "model": model_name,
277 "messages": messages,
278 "continue_final_message": True,
279 "add_generation_prompt": True,
280 },
281 )
282 assert (
283 "Cannot set both `continue_final_message` and `add_generation_prompt` to True."
284 in response.json()["error"]["message"]
285 )
286
287
288 @pytest.mark.asyncio
289 async def test_invocations_completion_request(server: RemoteOpenAIServer):
290 request_args = {
291 "model": MODEL_NAME,
292 "input": input_text,
293 }
294
295 classification_response = requests.post(
296 server.url_for("classify"), json=request_args
297 )
298 classification_response.raise_for_status()
299
300 invocation_response = requests.post(
301 server.url_for("invocations"), json=request_args
302 )
303 invocation_response.raise_for_status()
304
305 classification_output = classification_response.json()
306 invocation_output = invocation_response.json()
307
308 assert classification_output.keys() == invocation_output.keys()
309 for classification_data, invocation_data in zip(
310 classification_output["data"], invocation_output["data"]
311 ):
312 assert classification_data.keys() == invocation_data.keys()
313 assert classification_data["probs"] == pytest.approx(
314 invocation_data["probs"], rel=0.01
315 )
316
317
318 @pytest.mark.asyncio
319 async def test_invocations_chat_request(server: RemoteOpenAIServer):
320 messages = [
321 {
322 "role": "user",
323 "content": "The cat sat on the mat.",
324 },
325 {
326 "role": "assistant",
327 "content": "A feline was resting on a rug.",
328 },
329 {
330 "role": "user",
331 "content": "Stars twinkle brightly in the night sky.",
332 },
333 ]
334
335 request_args = {"model": MODEL_NAME, "messages": messages}
336
337 classification_response = requests.post(
338 server.url_for("classify"), json=request_args
339 )
340 classification_response.raise_for_status()
341
342 invocation_response = requests.post(
343 server.url_for("invocations"), json=request_args
344 )
345 invocation_response.raise_for_status()
346
347 classification_output = classification_response.json()
348 invocation_output = invocation_response.json()
349
350 assert classification_output.keys() == invocation_output.keys()
351 for classification_data, invocation_data in zip(
352 classification_output["data"], invocation_output["data"]
353 ):
354 assert classification_data.keys() == invocation_data.keys()
355 assert classification_data["probs"] == pytest.approx(
356 invocation_data["probs"], rel=0.01
357 )
358
359
360 @pytest.mark.asyncio
361 @pytest.mark.parametrize("model_name", [MODEL_NAME])
362 async def test_use_activation(server: RemoteOpenAIServer, model_name: str):
363 async def get_outputs(use_activation):
364 response = requests.post(
365 server.url_for("classify"),
366 json={
367 "model": model_name,
368 "input": input_text,
369 "use_activation": use_activation,
370 },
371 )
372 outputs = response.json()
373 return torch.tensor([x["probs"] for x in outputs["data"]])
374
375 default = await get_outputs(use_activation=None)
376 w_activation = await get_outputs(use_activation=True)
377 wo_activation = await get_outputs(use_activation=False)
378
379 assert torch.allclose(default, w_activation, atol=1e-2), (
380 "Default should use activation."
381 )
382 assert not torch.allclose(w_activation, wo_activation, atol=1e-2), (
383 "wo_activation should not use activation."
384 )
385 assert torch.allclose(F.softmax(wo_activation, dim=-1), w_activation, atol=1e-2), (
386 "w_activation should be close to activation(wo_activation)."
387 )
388
389
390 @pytest.mark.asyncio
391 @pytest.mark.parametrize("model_name", [MODEL_NAME])
392 async def test_score(server: RemoteOpenAIServer, model_name: str):
393 # Scoring API is only enabled for num_labels == 1.
394 response = requests.post(
395 server.url_for("score"),
396 json={
397 "model": model_name,
398 "queries": "ping",
399 "documents": "pong",
400 },
401 )
402 assert response.json()["detail"] == "Not Found"
403
404
405 @pytest.mark.asyncio
406 @pytest.mark.parametrize("model_name", [MODEL_NAME])
407 async def test_rerank(server: RemoteOpenAIServer, model_name: str):
408 # Scoring API is only enabled for num_labels == 1.
409 response = requests.post(
410 server.url_for("rerank"),
411 json={
412 "model": model_name,
413 "query": "ping",
414 "documents": ["pong"],
415 },
416 )
417 assert response.json()["detail"] == "Not Found"
418
419
420 @pytest.mark.asyncio
421 @pytest.mark.parametrize("model_name", [MODEL_NAME])
422 async def test_pooling_classify(server: RemoteOpenAIServer, model_name: str):
423 response = requests.post(
424 server.url_for("pooling"),
425 json={
426 "model": model_name,
427 "input": input_text,
428 "encoding_format": "float",
429 "task": "classify",
430 },
431 )
432 poolings = PoolingResponse.model_validate(response.json())
433 assert len(poolings.data) == 1
434 assert len(poolings.data[0].data) == 2
435
436
437 @pytest.mark.asyncio
438 @pytest.mark.parametrize("model_name", [MODEL_NAME])
439 @pytest.mark.parametrize("task", ["embed", "token_embed", "token_classify", "plugin"])
440 async def test_pooling_not_supported(
441 server: RemoteOpenAIServer, model_name: str, task: str
442 ):
443 response = requests.post(
444 server.url_for("pooling"),
445 json={
446 "model": model_name,
447 "input": input_text,
448 "encoding_format": "float",
449 "task": task,
450 },
451 )
452 assert response.json()["error"]["type"] == "BadRequestError"
453
454 if task == "plugin":
455 err_msg = "No IOProcessor plugin installed."
456 elif task == "token_classify":
457 err_msg = "Try switching the model's pooling_task via"
458 else:
459 err_msg = f"Unsupported task: {task!r}"
460 assert response.json()["error"]["message"].startswith(err_msg)