main
py 201 lines 6.23 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import os
5 import random
6 import sys
7 from typing import Any
8
9 import openai
10 import pytest
11 import pytest_asyncio
12
13 from tests.utils import RemoteOpenAIServerCustom
14 from tests.v1.logits_processors.utils import (
15 DUMMY_LOGITPROC_ARG,
16 DUMMY_LOGITPROC_FQCN,
17 MAX_TOKENS,
18 MODEL_NAME,
19 TEMP_GREEDY,
20 prompts,
21 setup_fake_entrypoint,
22 )
23
24
25 def _server_with_logitproc_entrypoint(
26 env_dict: dict[str, str] | None,
27 model: str,
28 vllm_serve_args: list[str],
29 ) -> None:
30 """Start vLLM server with dummy logitproc entrypoint."""
31 from vllm.entrypoints.cli import main
32
33 if env_dict is not None:
34 os.environ.update(env_dict)
35
36 # Emulate `vllm serve <model> <CLI args>`
37 sys.argv = ["vllm", "serve", model] + vllm_serve_args
38 main.main()
39
40
41 def _server_with_logitproc_fqcn(
42 env_dict: dict[str, str] | None,
43 model: str,
44 vllm_serve_args: list[str],
45 ) -> None:
46 """Start vLLM server with dummy logitproc specified by FQCN."""
47 from vllm.entrypoints.cli import main
48
49 if env_dict is not None:
50 os.environ.update(env_dict)
51
52 # Emulate `vllm serve <model> <CLI args>`
53 sys.argv = ["vllm", "serve", model] + vllm_serve_args
54 main.main()
55
56
57 @pytest.fixture(scope="module")
58 def default_server_args():
59 return [
60 # use half precision for speed and memory savings in CI environment
61 "--dtype",
62 "bfloat16",
63 "--max-model-len",
64 "2048",
65 "--max-num-seqs",
66 "128",
67 ]
68
69
70 @pytest.fixture(
71 scope="function", params=[[], ["--logits-processors", DUMMY_LOGITPROC_FQCN]]
72 )
73 def server(default_server_args, request, monkeypatch):
74 """Consider two server configurations:
75 (1) --logits-processors cli arg specifies dummy logits processor via fully-
76 qualified class name (FQCN)
77 (2) No --logits-processors cli arg; inject a dummy logits processor
78 entrypoint
79 """
80 # Test that logitproc info is passed to workers
81 monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
82 # These tests exercise the V1-interface logits processor; Model Runner V2
83 # rejects V1-interface processors at load time by design.
84 monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
85
86 if request.param:
87 # Launch server, append FQCN argument, inject dummy logitproc module
88 args = default_server_args + request.param
89 _server_fxn = _server_with_logitproc_fqcn
90 else:
91 # Launch server, inject dummy logitproc entrypoint
92 setup_fake_entrypoint(monkeypatch)
93 args = default_server_args
94 _server_fxn = _server_with_logitproc_entrypoint
95
96 with RemoteOpenAIServerCustom(MODEL_NAME, args, _server_fxn) as remote_server:
97 yield remote_server
98
99
100 @pytest_asyncio.fixture
101 async def client(server):
102 async with server.get_async_client() as async_client:
103 yield async_client
104
105
106 # General request argument values for these tests
107 api_keyword_args = {
108 # Greedy sampling ensures that requests which receive the `target_token`
109 # arg will decode it in every step
110 "temperature": TEMP_GREEDY,
111 # Since EOS will never be decoded (unless `target_token` is EOS)
112 "max_tokens": MAX_TOKENS,
113 # Return decoded token logprobs (as a way of getting token id)
114 "logprobs": 0,
115 }
116
117
118 @pytest.mark.parametrize(
119 "model_name",
120 [MODEL_NAME],
121 )
122 def test_custom_logitsprocs(server, model_name: str):
123 """Test custom logitsprocs when starting OpenAI server from CLI.
124
125 Launch vLLM OpenAI-compatible server, configured to load a custom logitproc
126 that has a well-defined behavior (mask out all tokens except one
127 `target_token`).
128
129 Pass in requests, 50% of which pass a `target_token` value
130 in through `extra_body["vllm_xargs"]`, 50% of which do not.
131
132 Validate that requests which activate the custom logitproc, repeat the same
133 token
134 """
135 import asyncio
136
137 async def _async_main(srv, mn):
138 async with srv.get_async_client() as client:
139 await _run(client)
140
141 async def _run(client):
142 use_dummy_logitproc = True
143 for prompt in prompts:
144 # Build request arguments
145 request_keyword_args: dict[str, Any] = {
146 **api_keyword_args,
147 }
148 if use_dummy_logitproc:
149 # 50% of requests pass target_token custom arg
150 target_token = random.choice([128, 67])
151 # For requests which activate the dummy logitproc, choose one of
152 # two `target_token` values which are known not to be EOS tokens
153 request_keyword_args["extra_body"] = {
154 "vllm_xargs": {DUMMY_LOGITPROC_ARG: target_token}
155 }
156 batch = await client.completions.create(
157 model=model_name,
158 prompt=prompt,
159 **request_keyword_args,
160 )
161
162 if use_dummy_logitproc:
163 # Only for requests which activate dummy logitproc - validate that
164 # output token is repeated
165 choices: openai.types.CompletionChoice = batch.choices
166 toks = choices[0].logprobs.tokens
167 if not all([x == toks[0] for x in toks]):
168 raise AssertionError(f"Generated {toks} should all be {toks[0]}")
169
170 # Alternate whether to activate dummy logitproc for each request
171 use_dummy_logitproc = not use_dummy_logitproc
172
173 asyncio.run(_async_main(server, model_name))
174
175
176 @pytest.mark.asyncio
177 @pytest.mark.parametrize(
178 "model_name",
179 [MODEL_NAME],
180 )
181 async def test_invalid_custom_logitsproc_arg(
182 client: openai.AsyncOpenAI, model_name: str
183 ):
184 """Test that request with invalid custom logitsproc is rejected."""
185 prompt = "Hello, my name is"
186 # Pass invalid (non-int) target_token value to dummy logits processor
187 request_keyword_args: dict[str, Any] = {
188 **api_keyword_args,
189 "extra_body": {
190 "vllm_xargs": {DUMMY_LOGITPROC_ARG: "invalid_target_token_value"}
191 },
192 }
193
194 with pytest.raises(openai.OpenAIError) as exc_info:
195 await client.completions.create(
196 model=model_name,
197 prompt=prompt,
198 **request_keyword_args,
199 )
200
201 assert "is not int" in str(exc_info.value)