A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 17:15 UTC
[Bugfix][Frontend] Eliminate regex based check in reasoning full generator (#14821)
Signed-off-by: Ce Gao <cegao@tensorchord.ai>
Ce Gao committed
Mar 28, 2025 at 19:20 UTC
3bbaacbe15c90ac5339aa46f481311a80038d3a9
2 files changed
+89
-18
tests/reasoning/test_deepseekr1_reasoning_parser.py
+64
@@ -90,6 +90,40 @@ SHORTEST_REASONING_WITH_THINK = {
90
"content": "This is the rest",
91
"is_reasoning_end": True,
92
}
93
+THINK_NO_END = {
94
+ "output": "<think>This is a reasoning section",
95
+ "reasoning_content": "This is a reasoning section",
96
+ "content": None,
97
+ "is_reasoning_end": False,
98
+}
99
+EMPTY = {
100
+ "output": "",
101
+ "reasoning_content": "",
102
+ "content": None,
103
+ "is_reasoning_end": False,
104
+}
105
+EMPTY_STREAMING = {
106
+ "output": "",
107
+ "reasoning_content": None,
108
+ "content": None,
109
+ "is_reasoning_end": False,
110
+}
111
+NEW_LINE = {
112
+ "output": "\n<think>This is a reasoning section</think>\nThis is the rest",
113
+ "reasoning_content": "This is a reasoning section",
114
+ "content": "\nThis is the rest",
115
+ "is_reasoning_end": True,
116
+}
117
+# Streaming cannot handle new lines at the beginning of the output
118
+# because we need to support <think>...</think> and </think>...
119
+# We cannot know if the text before <think> is reasoning content
120
+# or not.
121
+NEW_LINE_STREAMING = {
122
+ "output": "\n<think>This is a reasoning section</think>\nThis is the rest",
123
+ "reasoning_content": "\nThis is a reasoning section",
124
+ "content": "\nThis is the rest",
125
+ "is_reasoning_end": True,
126
+}
127
128
TEST_CASES = [
129
pytest.param(
@@ -182,6 +216,36 @@ TEST_CASES = [
216
SHORTEST_REASONING_WITH_THINK,
217
id="shortest_with_think_streaming",
218
),
219
+ pytest.param(
220
+ False,
221
+ THINK_NO_END,
222
+ id="think_no_end",
223
+ ),
224
+ pytest.param(
225
+ True,
226
+ THINK_NO_END,
227
+ id="think_no_end_streaming",
228
+ ),
229
+ pytest.param(
230
+ False,
231
+ EMPTY,
232
+ id="empty",
233
+ ),
234
+ pytest.param(
235
+ True,
236
+ EMPTY_STREAMING,
237
+ id="empty_streaming",
238
+ ),
239
+ pytest.param(
240
+ False,
241
+ NEW_LINE,
242
+ id="new_line",
243
+ ),
244
+ pytest.param(
245
+ True,
246
+ NEW_LINE_STREAMING,
247
+ id="new_line_streaming",
248
+ ),
249
]
250
251
vllm/reasoning/deepseek_r1_reasoning_parser.py
+25
-18
@@ -1,6 +1,5 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
-import re
3
from collections.abc import Sequence
4
from typing import Optional, Union
5
@@ -32,9 +31,6 @@ class DeepSeekR1ReasoningParser(ReasoningParser):
31
def __init__(self, tokenizer: PreTrainedTokenizerBase):
32
super().__init__(tokenizer)
33
35
- self.reasoning_regex = re.compile(
36
- rf"{self.start_token}(.*?){self.end_token}", re.DOTALL)
37
-
34
if not self.model_tokenizer:
35
raise ValueError(
36
"The model tokenizer must be passed to the ReasoningParser "
@@ -143,23 +139,34 @@ class DeepSeekR1ReasoningParser(ReasoningParser):
139
def extract_reasoning_content(
140
self, model_output: str, request: ChatCompletionRequest
141
) -> tuple[Optional[str], Optional[str]]:
142
+ """
143
+ Extract reasoning content from the model output.
144
+
145
+ For text <think>abc</think>xyz:
146
+ - 'abc' goes to reasoning_content
147
+ - 'xyz' goes to content
148
+
149
+ Returns:
150
+ tuple[Optional[str], Optional[str]]: reasoning content and content
151
+ """
152
+
153
+ # Check if the start token is present in the model output, remove it
154
+ # if it is present.
155
+ model_output_parts = model_output.partition(self.start_token)
156
+ model_output = model_output_parts[2] if model_output_parts[
157
+ 1] else model_output_parts[0]
158
+
159
# DeepSeek R1 doesn't generate <think> now.
160
# Thus we assume the reasoning content is always at the start.
161
# Ref https://huggingface.co/deepseek-ai/DeepSeek-R1/commit/8a58a132790c9935686eb97f042afa8013451c9f
162
if self.end_token not in model_output:
163
return model_output, None
164
else:
152
- # Add a start token if it's missing to keep compatibility.
153
- if self.start_token not in model_output:
154
- model_output = f"{self.start_token}{model_output}"
155
- # Use a regex to find the reasoning content
156
- reasoning_content = self.reasoning_regex.findall(model_output)[0]
157
-
158
- end_index = len(
159
- f"{self.start_token}{reasoning_content}{self.end_token}")
160
- final_output = model_output[end_index:]
161
-
162
- if len(final_output) == 0:
163
- return reasoning_content, None
164
-
165
- return reasoning_content, final_output
165
+ reasoning_content, _, content = model_output.partition(
166
+ self.end_token)
167
+ # If the end token is not found, return the model output as is.
168
+ # It should not happen since we already checked for the presence
169
+ # of the end token.
170
+ # If generation stops right after end-of-think, return null content
171
+ final_content = content or None
172
+ return reasoning_content, final_content