main
py 326 lines 12.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import importlib.metadata
4 from typing import Any
5
6 import pytest
7
8 import tests.v1.logits_processors.utils as logitproc_test_utils
9 from tests.utils import create_new_process_for_each_test, set_random_seed
10 from tests.v1.logits_processors.utils import (
11 DUMMY_LOGITPROC_ARG,
12 DUMMY_LOGITPROC_FQCN,
13 MAX_TOKENS,
14 MODEL_NAME,
15 POOLING_MODEL_NAME,
16 TEMP_GREEDY,
17 CustomLogitprocSource,
18 DummyLogitsProcessor,
19 WrappedPerReqLogitsProcessor,
20 prompts,
21 setup_fake_entrypoint,
22 )
23 from vllm import LLM, SamplingParams
24 from vllm.v1.sample.logits_processor import (
25 STR_POOLING_REJECTS_LOGITSPROCS,
26 STR_SPEC_DEC_REJECTS_LOGITSPROCS,
27 LogitsProcessor,
28 )
29
30 # Create a mixture of requests which do and don't utilize the dummy logitproc
31 sampling_params_list = [
32 SamplingParams(
33 temperature=TEMP_GREEDY,
34 max_tokens=MAX_TOKENS,
35 extra_args={DUMMY_LOGITPROC_ARG: 128},
36 ),
37 SamplingParams(temperature=TEMP_GREEDY, max_tokens=MAX_TOKENS),
38 SamplingParams(
39 temperature=TEMP_GREEDY,
40 max_tokens=MAX_TOKENS,
41 extra_args={DUMMY_LOGITPROC_ARG: 67},
42 ),
43 SamplingParams(temperature=TEMP_GREEDY, max_tokens=MAX_TOKENS),
44 ]
45
46
47 def test_fake_entrypoint_preserves_other_groups(monkeypatch):
48 other_group_entrypoints = object()
49 monkeypatch.setattr(
50 importlib.metadata,
51 "entry_points",
52 lambda **kwargs: other_group_entrypoints,
53 )
54 monkeypatch.setattr(
55 logitproc_test_utils, "requires_spawn_multiprocessing", lambda: False
56 )
57
58 setup_fake_entrypoint(monkeypatch)
59
60 logitproc_entrypoints = importlib.metadata.entry_points(
61 group=logitproc_test_utils.LOGITSPROCS_GROUP
62 )
63 assert logitproc_entrypoints.names == [
64 logitproc_test_utils.DUMMY_LOGITPROC_ENTRYPOINT
65 ]
66 assert (
67 importlib.metadata.entry_points(group="another.entrypoint.group")
68 is other_group_entrypoints
69 )
70 assert importlib.metadata.entry_points() is other_group_entrypoints
71
72
73 def _run_test(kwargs: dict, logitproc_loaded: bool) -> None:
74 """Compare `LLM` instance initialized with specified `kwargs` against
75 reference `LLM` instance.
76
77 Two scenarios:
78 1. Server has loaded dummy logitproc; test that requests which specify
79 dummy logitproc arg value behave as if logitproc is operating (output
80 token value should repeat), while requests that don't specify dummy
81 logitproc arg value should match reference `LLM` output.
82 2. Server has *not* loaded dummy logitproc; test that all requests
83 behave as if logitproc is *not* operating (output matches reference
84 `LLM` output.)
85
86 Args:
87 kwargs: `LLM` constructor kwargs
88 logitproc_loaded: server has loaded dummy logitproc if True
89
90 """
91 # Create a vLLM instance and load custom logitproc
92 llm_logitproc = LLM(
93 model=MODEL_NAME,
94 gpu_memory_utilization=0.1,
95 **kwargs,
96 )
97
98 # Create a reference vLLM instance without custom logitproc
99 llm_ref = LLM(model=MODEL_NAME, gpu_memory_utilization=0.1)
100
101 # Run inference with logitproc loaded
102 outputs_logitproc = llm_logitproc.generate(prompts, sampling_params_list)
103
104 # Reference run
105 outputs_ref = llm_ref.generate(prompts, sampling_params_list)
106
107 # Validate outputs
108 for bdx, (out_lp, out_ref, params) in enumerate(
109 zip(outputs_logitproc, outputs_ref, sampling_params_list)
110 ):
111 lp_toks = out_lp.outputs[0].token_ids
112 if logitproc_loaded and params.extra_args:
113 # This request exercises custom logitproc; validate that logitproc
114 # forces `target_token` to be decoded in each step
115 target_token = params.extra_args[DUMMY_LOGITPROC_ARG]
116 if not all(x == target_token for x in lp_toks):
117 raise AssertionError(
118 f"Request {bdx} generated {lp_toks}, should all be {target_token}"
119 )
120 else:
121 # This request does not exercise custom logitproc (or custom
122 # logitproc is not enabled on this server); validate against
123 # reference result
124 ref_toks = out_ref.outputs[0].token_ids
125 if lp_toks != ref_toks:
126 raise AssertionError(
127 f"Request {bdx} generated {lp_toks}, should match {ref_toks}"
128 )
129
130
131 @create_new_process_for_each_test()
132 @pytest.mark.parametrize("logitproc_source", list(CustomLogitprocSource))
133 def test_custom_logitsprocs(monkeypatch, logitproc_source: CustomLogitprocSource):
134 """Test offline Python interface for passing custom logitsprocs.
135
136 Construct an `LLM` instance which loads a custom logitproc that has a
137 well-defined behavior (mask out all tokens except one `target_token`)
138
139 Construct a reference `LLM` instance with no custom logitproc
140
141 Pass in a batch of requests, 50% of which pass a `target_token` value
142 in through `SamplingParams.extra_args`, 50% of which do not.
143
144 Validate that
145 * Requests which do not activate the custom logitproc, yield the same
146 results for both `LLM` instances
147 * Requests which activate the custom logitproc, only output `target_token`
148
149 Test four scenarios, corresponding to `logitproc_source` value
150 * No logitsprocs loaded - test that generated tokens match reference `LLM`
151 instance output
152 * Logitproc passed in via {entrypoint, class object, fully-qualified class
153 name (FQCN)} - test that dummy logitproc is utilized correctly when
154 provided via any of these three possible sources
155
156 Args:
157 monkeypatch: for setting env vars
158 logitproc_source: what source (entrypoint, fully-qualified class name
159 (FQCN), class object, or None) the user pulls the
160 logitproc from
161
162 """
163 # Test that logitproc info is passed to workers
164 monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
165 # These tests exercise the V1-interface logits processor; Model Runner V2
166 # rejects V1-interface processors at load time by design.
167 monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
168 set_random_seed(40)
169
170 # Choose LLM args based on logitproc source
171 if logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_NONE:
172 # Scenario: the server does not load any custom logitproc
173 # Every other scenario is a different way of loading a custom logitproc
174 _run_test({}, logitproc_loaded=False)
175 return
176
177 if logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_ENTRYPOINT:
178 # Scenario: vLLM loads a logitproc from a preconfigured entrypoint
179 # To that end, register a real dist-info package so spawned
180 # workers can discover the entrypoint via PYTHONPATH
181 setup_fake_entrypoint(monkeypatch)
182 _run_test({}, logitproc_loaded=True)
183 return
184
185 kwargs: dict[str, list[str | type[LogitsProcessor]]] = {}
186 if logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_FQCN:
187 # Scenario: load logitproc based on fully-qualified class name (FQCN)
188 kwargs["logits_processors"] = [DUMMY_LOGITPROC_FQCN]
189 elif logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_CLASS:
190 # Scenario: load logitproc from provided class object
191 kwargs["logits_processors"] = [DummyLogitsProcessor]
192
193 _run_test(kwargs, logitproc_loaded=True)
194
195
196 @create_new_process_for_each_test()
197 def test_custom_logitsprocs_req(monkeypatch):
198 """Test passing request-level logits processor to offline Python interface.
199
200 Wrap a request-level logits processor to create a batch level logits
201 processor that has a well-defined behavior (mask out all tokens except one
202 `target_token`)
203
204 Construct an `LLM` instance which loads the wrapped logits processor. Pass
205 the custom logitproc as a class object.
206
207 Construct a reference `LLM` instance with no custom logitproc
208
209 Pass in a batch of requests, 50% of which pass a `target_token` value
210 in through `SamplingParams.extra_args`, 50% of which do not.
211
212 Validate that
213 * Requests which do not activate the custom logitproc, yield the same
214 results for both `LLM` instances
215 * Requests which activate the custom logitproc, only output `target_token`
216
217 Args:
218 monkeypatch: for setting env vars
219
220 """
221 # Test that logitproc info is passed to workers
222 monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
223 # These tests exercise the V1-interface logits processor; Model Runner V2
224 # rejects V1-interface processors at load time by design.
225 monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
226 set_random_seed(40)
227 _run_test(
228 {"logits_processors": [WrappedPerReqLogitsProcessor]}, logitproc_loaded=True
229 )
230
231
232 @create_new_process_for_each_test()
233 @pytest.mark.parametrize("model_scenario", ["pooling", "spec_dec"])
234 @pytest.mark.parametrize(
235 "logitproc_source",
236 [
237 CustomLogitprocSource.LOGITPROC_SOURCE_ENTRYPOINT,
238 CustomLogitprocSource.LOGITPROC_SOURCE_FQCN,
239 CustomLogitprocSource.LOGITPROC_SOURCE_CLASS,
240 ],
241 )
242 def test_rejects_custom_logitsprocs(
243 monkeypatch, model_scenario: str, logitproc_source: CustomLogitprocSource
244 ):
245 """Validate that vLLM engine initialization properly rejects custom
246 logitsprocs when the model is a pooling model or speculative decoding
247 enabled.
248
249 Use `LLM` entrypoint. We expect `LLM` initialization to fail before the
250 logitproc is actually loaded.
251
252 Scenario 1:
253 * Mock a logitproc entrypoint
254 * Validate that `LLM` does not load the logitproc
255
256 Scenario 2:
257 * Pass custom logitproc to `LLM` constructor
258 * Scenario 2a: via FQCN
259 * Scenario 2b: via class object
260 * Validate that initialization fails with appropriate exception
261
262 Args:
263 monkeypatch: used to set environment variables
264 logitproc_source: what source (entrypoint, fully-qualified class name
265 (FQCN), or class object) the user pulls the
266 logitproc from
267
268 """
269 monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
270 # These tests exercise V1-interface rejection and V1 runner internals;
271 # Model Runner V2 rejects V1-interface processors at load time by design.
272 monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
273 set_random_seed(40)
274
275 test_params: dict[str, dict[str, Any]] = {
276 "pooling": {
277 "runner": "pooling",
278 "model": POOLING_MODEL_NAME,
279 "error_message": STR_POOLING_REJECTS_LOGITSPROCS,
280 "speculative_config": None,
281 },
282 "spec_dec": {
283 "runner": "auto",
284 "model": MODEL_NAME,
285 "error_message": STR_SPEC_DEC_REJECTS_LOGITSPROCS,
286 "speculative_config": {"model": "ngram", "num_speculative_tokens": 1},
287 },
288 }
289
290 config = test_params[model_scenario]
291
292 llm_kwargs: dict[str, Any] = {
293 "runner": config["runner"],
294 "model": config["model"],
295 "gpu_memory_utilization": 0.1,
296 "speculative_config": config["speculative_config"],
297 }
298
299 if logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_ENTRYPOINT:
300 # Scenario: vLLM loads a model and ignores a logitproc that is
301 # available at a preconfigured entrypoint
302
303 # Register real dist-info package so spawned workers can
304 # discover the entrypoint via PYTHONPATH (spawn-compatible)
305 setup_fake_entrypoint(monkeypatch)
306
307 llm = LLM(**llm_kwargs)
308 # Require that no custom logitsprocs have been loaded
309 # (built-in processors may exist: MinTokensLogitsProcessor,
310 # LogitBiasLogitsProcessor, MinPLogitsProcessor)
311 worker = llm.llm_engine.model_executor.driver_worker.worker
312 for proc in worker.model_runner.input_batch.logitsprocs.all:
313 assert not isinstance(proc, DummyLogitsProcessor)
314 return
315
316 if logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_FQCN:
317 # Scenario: load logitproc based on fully-qualified class name (FQCN)
318 llm_kwargs["logits_processors"] = [DUMMY_LOGITPROC_FQCN]
319 elif logitproc_source == CustomLogitprocSource.LOGITPROC_SOURCE_CLASS:
320 # Scenario: load logitproc from provided class object
321 llm_kwargs["logits_processors"] = [DummyLogitsProcessor]
322
323 with pytest.raises(ValueError, match=config["error_message"]):
324 # Require that loading a model alongside the logitproc raises
325 # the appropriate exception.
326 LLM(**llm_kwargs)