main
py 288 lines 7.49 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 from transformers import AutoTokenizer, PreTrainedTokenizerBase
6
7 from tests.reasoning.utils import run_reasoning_extraction
8 from vllm.reasoning import ReasoningParser, ReasoningParserManager
9
10 parser_name = "deepseek_r1"
11 start_token = "<think>"
12 end_token = "</think>"
13
14 REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
15
16
17 @pytest.fixture(scope="module")
18 def deepseek_r1_qwen_tokenizer() -> PreTrainedTokenizerBase:
19 return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
20
21
22 SIMPLE_REASONING = {
23 "output": "This is a reasoning section</think>This is the rest",
24 "reasoning": "This is a reasoning section",
25 "content": "This is the rest",
26 "is_reasoning_end": True,
27 }
28 COMPLETE_REASONING = {
29 "output": "This is a reasoning section</think>",
30 "reasoning": "This is a reasoning section",
31 "content": None,
32 "is_reasoning_end": True,
33 }
34 NO_CONTENT = {
35 "output": "This is content",
36 "reasoning": "This is content",
37 "content": None,
38 "is_reasoning_end": False,
39 }
40 NO_REASONING_STREAMING = {
41 "output": "This is a reasoning section",
42 "reasoning": "This is a reasoning section",
43 "content": None,
44 "is_reasoning_end": False,
45 }
46 MULTIPLE_LINES = {
47 "output": "This\nThat</think>This is the rest\nThat",
48 "reasoning": "This\nThat",
49 "content": "This is the rest\nThat",
50 "is_reasoning_end": True,
51 }
52 SHORTEST_REASONING_NO_STREAMING = {
53 "output": "</think>This is the rest",
54 "reasoning": "",
55 "content": "This is the rest",
56 "is_reasoning_end": True,
57 }
58 SHORTEST_REASONING = {
59 "output": "</think>This is the rest",
60 "reasoning": None,
61 "content": "This is the rest",
62 "is_reasoning_end": True,
63 }
64 REASONING_WITH_THINK = {
65 "output": "<think>This is a reasoning section</think>This is the rest",
66 "reasoning": "This is a reasoning section",
67 "content": "This is the rest",
68 "is_reasoning_end": True,
69 }
70 COMPLETE_REASONING_WITH_THINK = {
71 "output": "<think>This is a reasoning section</think>",
72 "reasoning": "This is a reasoning section",
73 "content": None,
74 "is_reasoning_end": True,
75 }
76 MULTIPLE_LINES_WITH_THINK = {
77 "output": "<think>This\nThat</think>This is the rest\nThat",
78 "reasoning": "This\nThat",
79 "content": "This is the rest\nThat",
80 "is_reasoning_end": True,
81 }
82 SHORTEST_REASONING_NO_STREAMING_WITH_THINK = {
83 "output": "</think>This is the rest",
84 "reasoning": "",
85 "content": "This is the rest",
86 "is_reasoning_end": True,
87 }
88 SHORTEST_REASONING_WITH_THINK = {
89 "output": "</think>This is the rest",
90 "reasoning": None,
91 "content": "This is the rest",
92 "is_reasoning_end": True,
93 }
94 THINK_NO_END = {
95 "output": "<think>This is a reasoning section",
96 "reasoning": "This is a reasoning section",
97 "content": None,
98 "is_reasoning_end": False,
99 }
100 EMPTY = {
101 "output": "",
102 "reasoning": "",
103 "content": None,
104 "is_reasoning_end": False,
105 }
106 EMPTY_STREAMING = {
107 "output": "",
108 "reasoning": None,
109 "content": None,
110 "is_reasoning_end": False,
111 }
112 NEW_LINE = {
113 "output": "\n<think>This is a reasoning section</think>\nThis is the rest",
114 "reasoning": "This is a reasoning section",
115 "content": "\nThis is the rest",
116 "is_reasoning_end": True,
117 }
118 # Streaming cannot handle new lines at the beginning of the output
119 # because we need to support <think>...</think> and </think>...
120 # We cannot know if the text before <think> is reasoning content
121 # or not.
122 NEW_LINE_STREAMING = {
123 "output": "\n<think>This is a reasoning section</think>\nThis is the rest",
124 "reasoning": "\nThis is a reasoning section",
125 "content": "\nThis is the rest",
126 "is_reasoning_end": True,
127 }
128
129 TEST_CASES = [
130 pytest.param(
131 False,
132 SIMPLE_REASONING,
133 id="simple_reasoning",
134 ),
135 pytest.param(
136 True,
137 SIMPLE_REASONING,
138 id="simple_reasoning_streaming",
139 ),
140 pytest.param(
141 False,
142 COMPLETE_REASONING,
143 id="complete_reasoning",
144 ),
145 pytest.param(
146 True,
147 COMPLETE_REASONING,
148 id="complete_reasoning_streaming",
149 ),
150 pytest.param(
151 False,
152 NO_CONTENT,
153 id="no_content_token",
154 ),
155 pytest.param(
156 True,
157 NO_REASONING_STREAMING,
158 id="no_reasoning_token_streaming",
159 ),
160 pytest.param(
161 False,
162 MULTIPLE_LINES,
163 id="multiple_lines",
164 ),
165 pytest.param(
166 True,
167 MULTIPLE_LINES,
168 id="multiple_lines_streaming",
169 ),
170 pytest.param(
171 True,
172 SHORTEST_REASONING,
173 id="shortest",
174 ),
175 pytest.param(
176 False,
177 SHORTEST_REASONING_NO_STREAMING,
178 id="shortest_streaming",
179 ),
180 pytest.param(
181 False,
182 REASONING_WITH_THINK,
183 id="reasoning_with_think",
184 ),
185 pytest.param(
186 True,
187 REASONING_WITH_THINK,
188 id="reasoning_with_think_streaming",
189 ),
190 pytest.param(
191 False,
192 COMPLETE_REASONING_WITH_THINK,
193 id="complete_reasoning_with_think",
194 ),
195 pytest.param(
196 True,
197 COMPLETE_REASONING_WITH_THINK,
198 id="complete_reasoning_with_think_streaming",
199 ),
200 pytest.param(
201 False,
202 MULTIPLE_LINES_WITH_THINK,
203 id="multiple_lines_with_think",
204 ),
205 pytest.param(
206 True,
207 MULTIPLE_LINES_WITH_THINK,
208 id="multiple_lines_with_think_streaming",
209 ),
210 pytest.param(
211 False,
212 SHORTEST_REASONING_NO_STREAMING_WITH_THINK,
213 id="shortest_with_think",
214 ),
215 pytest.param(
216 True,
217 SHORTEST_REASONING_WITH_THINK,
218 id="shortest_with_think_streaming",
219 ),
220 pytest.param(
221 False,
222 THINK_NO_END,
223 id="think_no_end",
224 ),
225 pytest.param(
226 True,
227 THINK_NO_END,
228 id="think_no_end_streaming",
229 ),
230 pytest.param(
231 False,
232 EMPTY,
233 id="empty",
234 ),
235 pytest.param(
236 True,
237 EMPTY_STREAMING,
238 id="empty_streaming",
239 ),
240 pytest.param(
241 False,
242 NEW_LINE,
243 id="new_line",
244 ),
245 pytest.param(
246 True,
247 NEW_LINE_STREAMING,
248 id="new_line_streaming",
249 ),
250 ]
251
252
253 @pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
254 def test_reasoning(
255 streaming: bool,
256 param_dict: dict,
257 deepseek_r1_qwen_tokenizer,
258 ):
259 output = deepseek_r1_qwen_tokenizer.tokenize(param_dict["output"])
260 # decode everything to tokens
261 output_tokens: list[str] = [
262 deepseek_r1_qwen_tokenizer.convert_tokens_to_string([token]) for token in output
263 ]
264 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
265 deepseek_r1_qwen_tokenizer
266 )
267
268 reasoning, content = run_reasoning_extraction(
269 parser, output_tokens, streaming=streaming
270 )
271
272 assert reasoning == param_dict["reasoning"]
273 assert content == param_dict["content"]
274
275 # Test is_reasoning_end
276 output_ids = deepseek_r1_qwen_tokenizer.convert_tokens_to_ids(output)
277 is_reasoning_end = parser.is_reasoning_end(output_ids)
278 assert is_reasoning_end == param_dict["is_reasoning_end"]
279
280 # Test extract_content
281 if param_dict["content"] is not None:
282 content_ids = parser.extract_content_ids(output_ids)
283 assert content_ids == deepseek_r1_qwen_tokenizer.convert_tokens_to_ids(
284 deepseek_r1_qwen_tokenizer.tokenize(param_dict["content"])
285 )
286 else:
287 content_ids = parser.extract_content_ids(output)
288 assert content_ids == []