@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a39203f99e
Fork 0

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, 12:15 UTC

[Bugfix] add qwen3 reasoning-parser fix content is None when disable … (#17369)

Signed-off-by: mofanke <mofanke@gmail.com>

mofanke committed Apr 30, 2025 at 00:32 UTC a39203f99ed426eef8b55927cb8f8668644d0a86
4 files changed +282
docs/source/features/reasoning_outputs.md
+1
@@ -15,6 +15,7 @@ vLLM currently supports the following reasoning models:
15 | [DeepSeek R1 series](https://huggingface.co/collections/deepseek-ai/deepseek-r1-678e1e131c0169c0bc89728d) | `deepseek_r1` | `guided_json`, `guided_regex` | ❌ |
16 | [QwQ-32B](https://huggingface.co/Qwen/QwQ-32B) | `deepseek_r1` | `guided_json`, `guided_regex` | ✅ |
17 | [IBM Granite 3.2 language models](https://huggingface.co/collections/ibm-granite/granite-32-language-models-67b3bc8c13508f6d064cff9a) | `granite` | ❌ | ❌ |
18 +| [Qwen3 series](https://huggingface.co/collections/Qwen/qwen3-67dd247413f0e2e4f653967f) | `qwen3` | `guided_json`, `guided_regex` | ✅ |
19
20 - IBM Granite 3.2 reasoning is disabled by default; to enable it, you must also pass `thinking=True` in your `chat_template_kwargs`.
21
tests/reasoning/test_qwen3_reasoning_parser.py new
+141
@@ -0,0 +1,141 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +
3 +import pytest
4 +from transformers import AutoTokenizer
5 +
6 +from tests.reasoning.utils import run_reasoning_extraction
7 +from vllm.reasoning import ReasoningParser, ReasoningParserManager
8 +
9 +parser_name = "qwen3"
10 +start_token = "<think>"
11 +end_token = "</think>"
12 +
13 +REASONING_MODEL_NAME = "Qwen/Qwen3-0.6B"
14 +
15 +
16 +@pytest.fixture(scope="module")
17 +def qwen3_tokenizer():
18 + return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
19 +
20 +
21 +# 带 <think></think>,非stream
22 +WITH_THINK = {
23 + "output": "<think>This is a reasoning section</think>This is the rest",
24 + "reasoning_content": "This is a reasoning section",
25 + "content": "This is the rest",
26 +}
27 +# 带 <think></think>,stream
28 +WITH_THINK_STREAM = {
29 + "output": "<think>This is a reasoning section</think>This is the rest",
30 + "reasoning_content": "This is a reasoning section",
31 + "content": "This is the rest",
32 +}
33 +# 不带 <think></think>,非stream
34 +WITHOUT_THINK = {
35 + "output": "This is the rest",
36 + "reasoning_content": None,
37 + "content": "This is the rest",
38 +}
39 +# 不带 <think></think>,stream
40 +WITHOUT_THINK_STREAM = {
41 + "output": "This is the rest",
42 + "reasoning_content": None,
43 + "content": "This is the rest",
44 +}
45 +
46 +COMPLETE_REASONING = {
47 + "output": "<think>This is a reasoning section</think>",
48 + "reasoning_content": "This is a reasoning section",
49 + "content": None,
50 +}
51 +MULTILINE_REASONING = {
52 + "output":
53 + "<think>This is a reasoning\nsection</think>This is the rest\nThat",
54 + "reasoning_content": "This is a reasoning\nsection",
55 + "content": "This is the rest\nThat",
56 +}
57 +ONLY_OPEN_TAG = {
58 + "output": "<think>This is a reasoning section",
59 + "reasoning_content": None,
60 + "content": "<think>This is a reasoning section",
61 +}
62 +
63 +ONLY_OPEN_TAG_STREAM = {
64 + "output": "<think>This is a reasoning section",
65 + "reasoning_content": "This is a reasoning section",
66 + "content": None,
67 +}
68 +
69 +TEST_CASES = [
70 + pytest.param(
71 + False,
72 + WITH_THINK,
73 + id="with_think",
74 + ),
75 + pytest.param(
76 + True,
77 + WITH_THINK_STREAM,
78 + id="with_think_stream",
79 + ),
80 + pytest.param(
81 + False,
82 + WITHOUT_THINK,
83 + id="without_think",
84 + ),
85 + pytest.param(
86 + True,
87 + WITHOUT_THINK_STREAM,
88 + id="without_think_stream",
89 + ),
90 + pytest.param(
91 + False,
92 + COMPLETE_REASONING,
93 + id="complete_reasoning",
94 + ),
95 + pytest.param(
96 + True,
97 + COMPLETE_REASONING,
98 + id="complete_reasoning_stream",
99 + ),
100 + pytest.param(
101 + False,
102 + MULTILINE_REASONING,
103 + id="multiline_reasoning",
104 + ),
105 + pytest.param(
106 + True,
107 + MULTILINE_REASONING,
108 + id="multiline_reasoning_stream",
109 + ),
110 + pytest.param(
111 + False,
112 + ONLY_OPEN_TAG,
113 + id="only_open_tag",
114 + ),
115 + pytest.param(
116 + True,
117 + ONLY_OPEN_TAG_STREAM,
118 + id="only_open_tag_stream",
119 + ),
120 +]
121 +
122 +
123 +@pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
124 +def test_reasoning(
125 + streaming: bool,
126 + param_dict: dict,
127 + qwen3_tokenizer,
128 +):
129 + output = qwen3_tokenizer.tokenize(param_dict["output"])
130 + output_tokens: list[str] = [
131 + qwen3_tokenizer.convert_tokens_to_string([token]) for token in output
132 + ]
133 + parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(
134 + parser_name)(qwen3_tokenizer)
135 +
136 + reasoning, content = run_reasoning_extraction(parser,
137 + output_tokens,
138 + streaming=streaming)
139 +
140 + assert reasoning == param_dict["reasoning_content"]
141 + assert content == param_dict["content"]
vllm/reasoning/__init__.py
+2
@@ -3,10 +3,12 @@
3 from .abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
4 from .deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
5 from .granite_reasoning_parser import GraniteReasoningParser
6 +from .qwen3_reasoning_parser import Qwen3ReasoningParser
7
8 __all__ = [
9 "ReasoningParser",
10 "ReasoningParserManager",
11 "DeepSeekR1ReasoningParser",
12 "GraniteReasoningParser",
13 + "Qwen3ReasoningParser",
14 ]
vllm/reasoning/qwen3_reasoning_parser.py new
+138
@@ -0,0 +1,138 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +
3 +import re
4 +from collections.abc import Sequence
5 +from typing import Optional, Union
6 +
7 +from transformers import PreTrainedTokenizerBase
8 +
9 +from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
10 + DeltaMessage)
11 +from vllm.logger import init_logger
12 +from vllm.reasoning import ReasoningParser, ReasoningParserManager
13 +
14 +logger = init_logger(__name__)
15 +
16 +
17 +@ReasoningParserManager.register_module("qwen3")
18 +class Qwen3ReasoningParser(ReasoningParser):
19 + """
20 + Reasoning parser for the Qwen3 model.
21 +
22 + The Qwen3 model uses <think>...</think> tokens to denote reasoning text
23 + within its output. The model provides a strict switch to disable reasoning
24 + output via the 'enable_thinking=False' parameter. This parser extracts the
25 + reasoning content enclosed by <think> and </think> tokens from the model's
26 + output.
27 + """
28 +
29 + def __init__(self, tokenizer: PreTrainedTokenizerBase):
30 + super().__init__(tokenizer)
31 + self.think_start_token = "<think>"
32 + self.think_end_token = "</think>"
33 +
34 + self.reasoning_regex = re.compile(
35 + rf"{self.think_start_token}(.*?){self.think_end_token}", re.DOTALL)
36 +
37 + if not self.model_tokenizer:
38 + raise ValueError(
39 + "The model tokenizer must be passed to the ReasoningParser "
40 + "constructor during construction.")
41 +
42 + self.think_start_token_id = self.vocab.get(self.think_start_token)
43 + self.think_end_token_id = self.vocab.get(self.think_end_token)
44 + if (self.think_start_token_id is None
45 + or self.think_end_token_id is None):
46 + raise RuntimeError(
47 + "Qwen3 reasoning parser could not locate think start/end "
48 + "tokens in the tokenizer!")
49 +
50 + def extract_reasoning_content_streaming(
51 + self,
52 + previous_text: str,
53 + current_text: str,
54 + delta_text: str,
55 + previous_token_ids: Sequence[int],
56 + current_token_ids: Sequence[int],
57 + delta_token_ids: Sequence[int],
58 + ) -> Union[DeltaMessage, None]:
59 + """
60 + Extract reasoning content from a delta message.
61 + Handles streaming output where previous + delta = current.
62 + Uses token IDs for faster processing.
63 + For text <think>abc</think>xyz:
64 + - 'abc' goes to reasoning_content
65 + - 'xyz' goes to content
66 + """
67 + # Skip single special tokens
68 + if len(delta_token_ids) == 1 and (delta_token_ids[0] in [
69 + self.think_start_token_id, self.think_end_token_id
70 + ]):
71 + return None
72 +
73 + if self.think_start_token_id in previous_token_ids:
74 + if self.think_end_token_id in delta_token_ids:
75 + # <think> in previous, </think> in delta,
76 + # extract reasoning content
77 + end_index = delta_text.find(self.think_end_token)
78 + reasoning_content = delta_text[:end_index]
79 + content = delta_text[end_index + len(self.think_end_token):]
80 + return DeltaMessage(reasoning_content=reasoning_content,
81 + content=content if content else None)
82 + elif self.think_end_token_id in previous_token_ids:
83 + # <think> in previous, </think> in previous,
84 + # reasoning content continues
85 + return DeltaMessage(content=delta_text)
86 + else:
87 + # <think> in previous, no </think> in previous or delta,
88 + # reasoning content continues
89 + return DeltaMessage(reasoning_content=delta_text)
90 + elif self.think_start_token_id in delta_token_ids:
91 + logger.info(delta_text)
92 + if self.think_end_token_id in delta_token_ids:
93 + # <think> in delta, </think> in delta, extract reasoning content
94 + start_index = delta_text.find(self.think_start_token)
95 + end_index = delta_text.find(self.think_end_token)
96 + reasoning_content = delta_text[start_index +
97 + len(self.think_start_token
98 + ):end_index]
99 + content = delta_text[end_index + len(self.think_end_token):]
100 + return DeltaMessage(reasoning_content=reasoning_content,
101 + content=content if content else None)
102 + else:
103 + # <think> in delta, no </think> in delta,
104 + # reasoning content continues
105 + return DeltaMessage(reasoning_content=delta_text)
106 + else:
107 + # thinking is disabled, just content
108 + return DeltaMessage(content=delta_text)
109 +
110 + def extract_reasoning_content(
111 + self, model_output: str, request: ChatCompletionRequest
112 + ) -> tuple[Optional[str], Optional[str]]:
113 +
114 + # Check if the model output contains the <think> tokens.
115 + if (self.think_start_token not in model_output
116 + or self.think_end_token not in model_output):
117 + return None, model_output
118 + else:
119 + # Use a regex to find the reasoning content
120 + reasoning_content = self.reasoning_regex.findall(model_output)[0]
121 +
122 + # Remove the reasoning content from the model output
123 + # Although <think> token is always at the
124 + # beginning of the line, we cannot guarantee that the
125 + # other models will follow this convention.
126 + # Therefore, we need to add :start_index.
127 + start_index = model_output.find(self.think_start_token)
128 + if start_index != -1:
129 + end_index = start_index + len(
130 + f"{self.think_start_token}{reasoning_content}{self.think_end_token}"
131 + )
132 + model_output = model_output[:start_index] + \
133 + model_output[end_index:]
134 +
135 + if len(model_output) == 0:
136 + return reasoning_content, None
137 +
138 + return reasoning_content, model_output