main
py 238 lines 9.13 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import time
4 from concurrent.futures import Future
5
6 import pytest
7 from transformers import AutoTokenizer
8
9 from vllm.config import StructuredOutputsConfig, VllmConfig
10 from vllm.config.model import ModelConfig
11 from vllm.config.parallel import ParallelConfig
12 from vllm.config.speculative import SpeculativeConfig
13 from vllm.sampling_params import SamplingParams, StructuredOutputsParams
14 from vllm.tokenizers import get_tokenizer
15 from vllm.v1.request import Request
16 from vllm.v1.structured_output import StructuredOutputManager
17 from vllm.v1.structured_output.backend_guidance import GuidanceBackend
18 from vllm.v1.structured_output.backend_types import StructuredOutputOptions
19
20 TOKENIZER = "openai-community/gpt2"
21
22
23 @pytest.fixture(scope="module")
24 def mistral_tokenizer():
25 return get_tokenizer(
26 tokenizer_name="mistralai/Mistral-Small-3.2-24B-Instruct-2506",
27 tokenizer_mode="mistral",
28 )
29
30
31 def test_backend_guidance_rollback_terminated():
32 # Test that the backend guidance successfully rollbacks from a
33 # terminated state. This can happen with speculative decoding,
34 # where the draft model proposes EOS and it is verified by the
35 # guidance backend. In that case we are in a stopped state, but
36 # it should be reverted in case EOS is not accepted by the target
37 # model.
38 structured_outputs_config = StructuredOutputsConfig(backend="guidance")
39 vllm_config = VllmConfig(structured_outputs_config=structured_outputs_config)
40 tokenizer = AutoTokenizer.from_pretrained(TOKENIZER)
41
42 backend = GuidanceBackend(
43 vllm_config,
44 tokenizer=tokenizer,
45 vocab_size=50257,
46 )
47
48 grammar = backend.compile_grammar(
49 StructuredOutputOptions.JSON, '{"type": "object"}'
50 )
51
52 prompt = tokenizer.encode('{"a": "b"}')
53 assert len(prompt) > 1
54 dummy_wrong = tokenizer.encode('{"a"}')
55 for token in prompt:
56 assert grammar.accept_tokens("", [token])
57 assert not grammar.is_terminated()
58 assert grammar.accept_tokens("", [tokenizer.eos_token_id])
59 assert grammar.is_terminated()
60 # Giving any other token should also be accepted
61 assert grammar.accept_tokens("", dummy_wrong)
62 # Rollback is done from where state was terminated, so from '}' not EOS
63 grammar.rollback(len(prompt) - 1)
64 assert not grammar.is_terminated()
65 assert grammar.validate_tokens([tokenizer.eos_token_id]) == []
66 assert grammar.validate_tokens(dummy_wrong) != dummy_wrong
67 assert grammar.accept_tokens("", prompt[1:])
68 assert not grammar.is_terminated()
69 assert grammar.accept_tokens("", [tokenizer.eos_token_id])
70 assert grammar.is_terminated()
71 # Rollback of <= 0 should not change the terminated state
72 grammar.rollback(0)
73 assert grammar.is_terminated()
74 grammar.rollback(-1)
75 assert grammar.is_terminated()
76
77
78 def test_grammar_bitmask_with_specdec():
79 tokenizer = AutoTokenizer.from_pretrained(TOKENIZER)
80 prompt = tokenizer.encode('{"a": "b"}')
81 vllm_config = VllmConfig(
82 model_config=ModelConfig(tokenizer=TOKENIZER),
83 structured_outputs_config=StructuredOutputsConfig(backend="guidance"),
84 speculative_config=SpeculativeConfig(model="[ngram]", num_speculative_tokens=3),
85 )
86 structured_output_manager = StructuredOutputManager(vllm_config)
87
88 for i in range(1, 2):
89 sampling_params = SamplingParams(
90 structured_outputs=StructuredOutputsParams(
91 json='{"type": "object"}',
92 ),
93 )
94 sampling_params.structured_outputs._backend = "guidance" # type: ignore[union-attr]
95 sampling_params.update_from_generation_config({}, tokenizer.eos_token_id)
96
97 my_req_id = f"my_req_id_{i}"
98 request = Request(
99 my_req_id,
100 prompt_token_ids=prompt[:i],
101 sampling_params=sampling_params,
102 pooling_params=None,
103 )
104
105 structured_output_manager.grammar_init(request)
106
107 def grammar_bitmask(req: Request, tokens: list[int]) -> None:
108 structured_output_manager.grammar_bitmask(
109 requests={req.request_id: req},
110 structured_output_request_ids=[req.request_id],
111 scheduled_spec_decode_tokens={req.request_id: tokens},
112 )
113 # At this point, we rolled-back, so should not be terminated
114 assert req.structured_output_request is not None
115 grammar = req.structured_output_request.grammar
116 assert grammar is not None and not isinstance(grammar, Exception)
117 assert not grammar.is_terminated()
118
119 # The grammar might not yet be compiled, so we wait for it
120 assert request.structured_output_request is not None
121 while not request.structured_output_request._check_grammar_completion():
122 continue
123
124 grammar = request.structured_output_request.grammar
125 assert grammar is not None and not isinstance(grammar, Exception)
126 assert grammar.accept_tokens(request.request_id, prompt[:i])
127
128 grammar_bitmask(request, prompt[i:] + [tokenizer.eos_token_id])
129 grammar_bitmask(
130 request, prompt[i:] + [tokenizer.eos_token_id] + prompt
131 ) # EOS not the final token
132 grammar_bitmask(request, prompt[i:]) # EOS not present
133 grammar_bitmask(request, prompt[i:] + [tokenizer.eos_token_id])
134
135
136 @pytest.mark.parametrize("async_grammar", [True, False])
137 def test_grammar_init_async_and_sync(async_grammar):
138 """Test grammar initialization works correctly in both async and sync modes.
139
140 This test validates that the distributed_executor_backend config option
141 correctly controls whether grammar compilation happens asynchronously
142 (via executor.submit) or synchronously. When set to "external_launcher",
143 grammar compilation is synchronous to avoid deadlocks.
144 """
145 tokenizer = AutoTokenizer.from_pretrained(TOKENIZER)
146 prompt = tokenizer.encode('{"a": "b"}')
147
148 # Use "external_launcher" for sync mode, None for async mode
149 executor_backend = None if async_grammar else "external_launcher"
150 vllm_config = VllmConfig(
151 model_config=ModelConfig(tokenizer=TOKENIZER),
152 structured_outputs_config=StructuredOutputsConfig(backend="guidance"),
153 parallel_config=ParallelConfig(distributed_executor_backend=executor_backend),
154 )
155 structured_output_manager = StructuredOutputManager(vllm_config)
156
157 sampling_params = SamplingParams(
158 structured_outputs=StructuredOutputsParams(
159 json='{"type": "object"}',
160 ),
161 )
162 sampling_params.structured_outputs._backend = "guidance" # type: ignore[union-attr]
163 sampling_params.update_from_generation_config({}, tokenizer.eos_token_id)
164
165 request = Request(
166 "test_request",
167 prompt_token_ids=prompt,
168 sampling_params=sampling_params,
169 pooling_params=None,
170 )
171
172 structured_output_manager.grammar_init(request)
173
174 # Check the internal _grammar type immediately after init
175 # Before _check_grammar_completion is called, async mode should have a Future
176 assert request.structured_output_request is not None
177 raw_grammar = request.structured_output_request._grammar
178 if async_grammar:
179 assert isinstance(raw_grammar, Future), (
180 "Async mode should store a Future before completion"
181 )
182 else:
183 assert not isinstance(raw_grammar, Future), (
184 "Sync mode should store the grammar directly, not a Future"
185 )
186
187 # Wait for grammar to be ready (handles both async and sync cases)
188 start_time = time.time()
189 while not request.structured_output_request._check_grammar_completion():
190 if time.time() - start_time > 5: # 5-second timeout
191 pytest.fail("Grammar compilation timed out")
192 time.sleep(0.01)
193
194 # After completion, _grammar should no longer be a Future
195 assert not isinstance(request.structured_output_request._grammar, Future)
196
197 # Verify grammar is properly initialized and functional
198 grammar = request.structured_output_request.grammar
199 assert grammar is not None and not isinstance(grammar, Exception)
200 assert not grammar.is_terminated()
201
202 # Verify the grammar can accept valid tokens
203 assert grammar.accept_tokens(request.request_id, prompt)
204
205
206 @pytest.mark.parametrize(
207 "request_type,grammar_spec",
208 [
209 pytest.param(
210 StructuredOutputOptions.JSON,
211 '{"type": "object"}',
212 id="json",
213 ),
214 pytest.param(
215 StructuredOutputOptions.GRAMMAR,
216 'start: "hello" | "world"',
217 id="lark",
218 ),
219 ],
220 )
221 def test_mistral_tokenizer_compile_grammar(
222 mistral_tokenizer,
223 request_type: StructuredOutputOptions,
224 grammar_spec: str,
225 ) -> None:
226 vllm_config = VllmConfig(
227 structured_outputs_config=StructuredOutputsConfig(backend="guidance"),
228 )
229 backend = GuidanceBackend(
230 vllm_config,
231 tokenizer=mistral_tokenizer,
232 vocab_size=mistral_tokenizer.vocab_size,
233 )
234 assert backend.ll_tokenizer is mistral_tokenizer.llg_tokenizer
235
236 grammar = backend.compile_grammar(request_type, grammar_spec)
237 assert grammar is not None
238 assert not grammar.is_terminated()