main
py 344 lines 10.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import pytest
4 from transformers import AutoTokenizer
5
6 from tests.reasoning.utils import DeltaMessage, run_reasoning_extraction
7 from vllm.reasoning import ReasoningParser, ReasoningParserManager
8
9 parser_name = "granite"
10 START_REASONING = "Here is my thought process:"
11 START_RESPONSE = "Here is my response:"
12
13 SIMPLE_REASONING = {
14 "output": f"{START_REASONING}This is a reasoning section{START_RESPONSE}This is the rest", # noqa: E501
15 "reasoning": "This is a reasoning section",
16 "content": "This is the rest",
17 }
18 COMPLETE_REASONING = {
19 "output": f"{START_REASONING}This is a reasoning section{START_RESPONSE}",
20 "reasoning": "This is a reasoning section",
21 "content": None,
22 }
23 NO_REASONING = {
24 "output": "This is content",
25 "reasoning": None,
26 "content": "This is content",
27 }
28 MULTIPLE_LINES = {
29 "output": f"{START_REASONING}This\nThat{START_RESPONSE}This is the rest\nThat",
30 "reasoning": "This\nThat",
31 "content": "This is the rest\nThat",
32 }
33 REASONING_WITH_THINK = {
34 "output": f"{START_REASONING}This is a reasoning section{START_RESPONSE}This is the rest", # noqa: E501
35 "reasoning": "This is a reasoning section",
36 "content": "This is the rest",
37 }
38 COMPLETE_REASONING_WITH_THINK = {
39 "output": f"{START_REASONING}This is a reasoning section{START_RESPONSE}",
40 "reasoning": "This is a reasoning section",
41 "content": None,
42 }
43 MULTIPLE_LINES_WITH_THINK = {
44 "output": f"{START_REASONING}This\nThat{START_RESPONSE}This is the rest\nThat",
45 "reasoning": "This\nThat",
46 "content": "This is the rest\nThat",
47 }
48
49 TEST_CASES = [
50 pytest.param(
51 False,
52 SIMPLE_REASONING,
53 id="simple_reasoning",
54 ),
55 pytest.param(
56 False,
57 COMPLETE_REASONING,
58 id="complete_reasoning",
59 ),
60 pytest.param(
61 False,
62 NO_REASONING,
63 id="no_reasoning",
64 ),
65 pytest.param(
66 False,
67 MULTIPLE_LINES,
68 id="multiple_lines",
69 ),
70 pytest.param(
71 False,
72 REASONING_WITH_THINK,
73 id="reasoning_with_think",
74 ),
75 pytest.param(
76 False,
77 COMPLETE_REASONING_WITH_THINK,
78 id="complete_reasoning_with_think",
79 ),
80 pytest.param(
81 False,
82 MULTIPLE_LINES_WITH_THINK,
83 id="multiple_lines_with_think",
84 ),
85 pytest.param(
86 True,
87 SIMPLE_REASONING,
88 id="simple_reasoning_streaming",
89 ),
90 pytest.param(
91 True,
92 COMPLETE_REASONING,
93 id="complete_reasoning_streaming",
94 ),
95 pytest.param(
96 True,
97 NO_REASONING,
98 id="no_reasoning_streaming",
99 ),
100 pytest.param(
101 True,
102 MULTIPLE_LINES,
103 id="multiple_lines_streaming",
104 ),
105 pytest.param(
106 True,
107 REASONING_WITH_THINK,
108 id="reasoning_with_think_streaming",
109 ),
110 pytest.param(
111 True,
112 COMPLETE_REASONING_WITH_THINK,
113 id="complete_reasoning_with_think_streaming",
114 ),
115 pytest.param(
116 True,
117 MULTIPLE_LINES_WITH_THINK,
118 id="multiple_lines_with_think_streaming",
119 ),
120 ]
121
122 # Global tokenizer initialization to avoid repeated loading
123 tokenizer = AutoTokenizer.from_pretrained("facebook/opt-125m")
124
125
126 @pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
127 def test_reasoning(
128 streaming: bool,
129 param_dict: dict,
130 ):
131 output = tokenizer.tokenize(param_dict["output"])
132 # decode everything to tokens
133 output_tokens: list[str] = [
134 tokenizer.convert_tokens_to_string([token]) for token in output
135 ]
136 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
137 tokenizer
138 )
139
140 reasoning, content = run_reasoning_extraction(
141 parser, output_tokens, streaming=streaming
142 )
143
144 assert reasoning == param_dict["reasoning"]
145 assert content == param_dict["content"]
146
147
148 # Additional tests for verifying the correctness of granite streaming; this
149 # is complicated because granite uses multiple tokens to indicate when thinking
150 # is starting / when it's starting its response, so skipping special tokens
151 # is awkward.
152
153 ### Handling the start of reasoning
154 STREAMING_1 = {
155 "previous_text": None,
156 "current_text": "Here",
157 "delta_text": "Here",
158 "reasoning": None,
159 "content": None,
160 }
161 # When we fail, we should give what was previously being silenced first
162 STREAMING_2 = {
163 "previous_text": "Here is my thought",
164 "current_text": "Here is my thought failure",
165 "delta_text": " failure",
166 "reasoning": None,
167 "content": "Here is my thought failure",
168 }
169 # But then after the first one, we should only add the delta text to content
170 STREAMING_3 = {
171 "previous_text": "Here wrong",
172 "current_text": " words",
173 "delta_text": " Here wrong words",
174 "reasoning": None,
175 "content": " words",
176 }
177 # But then after the first one, we should only add the delta text to content
178 STREAMING_4 = {
179 "previous_text": "Here is my thought",
180 "current_text": "Here is my thought process:",
181 "delta_text": " process:",
182 "reasoning": None,
183 "content": None,
184 }
185 # Reasoning started successfully; parse reasoning content
186 STREAMING_5 = {
187 "previous_text": "Here is my thought process:",
188 "current_text": "Here is my thought process: foo",
189 "delta_text": " foo",
190 "reasoning": " foo",
191 "content": None,
192 }
193 # Response special sequence has started, but not finished.
194 STREAMING_6 = {
195 "previous_text": "Here is my thought process: foo",
196 "current_text": "Here is my thought process: foo Here is",
197 "delta_text": " Here is",
198 "reasoning": " ",
199 "content": None,
200 }
201 # Response special sequence started, but was broken; the reasoning
202 # content should be the content that was previously unused.
203 STREAMING_7 = {
204 "previous_text": "Here is my thought process: foo Here is",
205 "current_text": "Here is my thought process: foo Here is Here",
206 "delta_text": " Here",
207 "reasoning": "Here is ",
208 "content": None,
209 }
210 # Response special sequence is ongoing
211 STREAMING_8 = {
212 "previous_text": "Here is my thought process: foo Here is my response:",
213 "current_text": "Here is my thought process: foo Here is my response: bar",
214 "delta_text": " bar",
215 "reasoning": None,
216 "content": " bar",
217 }
218 # The delta text has everything; we should be able to correctly parse both
219 STREAMING_9 = {
220 "previous_text": None,
221 "current_text": "Here is my thought process: foo Here is my response: bar",
222 "delta_text": "Here is my thought process: foo Here is my response: bar",
223 "reasoning": " foo ",
224 "content": " bar",
225 }
226 ## The Response is ongoing, and the delta mixes reasoning content / content
227 STREAMING_10 = {
228 "previous_text": "Here is my thought process: foo",
229 "current_text": "Here is my thought process: foo bar Here is my response: baz",
230 "delta_text": " bar Here is my response: baz",
231 "reasoning": " bar ",
232 "content": " baz",
233 }
234 # The delta text starts a new substring that might be a response special seq
235 STREAMING_11 = {
236 "previous_text": "Here is my thought process: This is a reasoning section ",
237 "current_text": "Here is my thought process: This is a reasoning section Here",
238 "delta_text": "Here",
239 "reasoning": None,
240 "content": None,
241 }
242 # The delta text is finishing the response special seq
243 STREAMING_12 = {
244 "previous_text": "Here is my thought process: foo Here is my response",
245 "current_text": "Here is my thought process: foo Here is my response:",
246 "delta_text": ":",
247 "reasoning": None,
248 "content": None,
249 }
250 STREAMING_13 = {
251 "previous_text": "Here is my thought process: foo Here",
252 "current_text": "Here is my thought process: foo Here was",
253 "delta_text": " was",
254 "reasoning": "Here was",
255 "content": None,
256 }
257
258 STREAMING_SUBCASES = [
259 pytest.param(
260 STREAMING_1,
261 id="Starting reasoning special sequence",
262 ),
263 pytest.param(
264 STREAMING_2,
265 id="Unexpected start reasoning sequence",
266 ),
267 pytest.param(
268 STREAMING_3,
269 id="Continuing unexpected start reasoning sequence",
270 ),
271 pytest.param(
272 STREAMING_4,
273 id="Only start reasoning sequence and nothing else",
274 ),
275 pytest.param(
276 STREAMING_5,
277 id="Reasoning content has started",
278 ),
279 pytest.param(
280 STREAMING_6,
281 id="Response special sequence has started",
282 ),
283 pytest.param(
284 STREAMING_7,
285 id="Response special sequence reset",
286 ),
287 pytest.param(
288 STREAMING_8,
289 id="Response text has started",
290 ),
291 pytest.param(
292 STREAMING_9,
293 id="Delta contains everything",
294 ),
295 pytest.param(
296 STREAMING_10,
297 id="Delta contains some reasoning and response",
298 ),
299 pytest.param(
300 STREAMING_11,
301 id="Delta starts response sequence",
302 ),
303 pytest.param(
304 STREAMING_12,
305 id="Delta finishes response sequence",
306 ),
307 pytest.param(
308 STREAMING_13,
309 id="Delta breaks potential responise sequence",
310 ),
311 ]
312
313
314 @pytest.mark.parametrize("param_dict", STREAMING_SUBCASES)
315 def test_streaming_subcases(param_dict):
316 # Get all of the token IDs
317 previous_token_ids = (
318 tokenizer.encode(param_dict["previous_text"])
319 if param_dict["previous_text"] is not None
320 else []
321 )
322 current_token_ids = tokenizer.encode(param_dict["current_text"])
323 delta_token_ids = tokenizer.encode(param_dict["delta_text"])
324
325 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
326 tokenizer
327 )
328
329 response = parser.extract_reasoning_streaming(
330 previous_text=param_dict["previous_text"],
331 current_text=param_dict["current_text"],
332 delta_text=param_dict["delta_text"],
333 previous_token_ids=previous_token_ids,
334 current_token_ids=current_token_ids,
335 delta_token_ids=delta_token_ids,
336 )
337 # Streaming currently expects at least one of reasoning content / content,
338 # so the response should return None in that case.
339 if param_dict["reasoning"] is None and param_dict["content"] is None:
340 assert response is None
341 else:
342 assert isinstance(response, DeltaMessage)
343 assert param_dict["reasoning"] == response.reasoning
344 assert param_dict["content"] == response.content