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 7, 00:15 UTC
add nemotron v3 reasoning parser (#36393)
Signed-off-by: Shaun Kotek - Nvidia <skotek@nvidia.com> Co-authored-by: root <root@gpu-259.slurm-workers-slurm.slurm.svc.cluster.local>
Shaun Kotek committed
Mar 10, 2026 at 00:11 UTC
203a7f27dac2197ddcf5bb1cfd105596a19ea990
3 files changed
+186
tests/reasoning/test_nemotron_v3_reasoning_parser.py
new
+150
@@ -0,0 +1,150 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import TypedDict
5
+
6
+import pytest
7
+import regex as re
8
+
9
+from tests.reasoning.utils import run_reasoning_extraction
10
+from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
11
+from vllm.reasoning import ReasoningParser, ReasoningParserManager
12
+
13
+parser_name = "nemotron_v3"
14
+
15
+
16
+class ReasoningCase(TypedDict):
17
+ output: str
18
+ reasoning: str | None
19
+ content: str | None
20
+
21
+
22
+class FakeNemotronTokenizer:
23
+ def __init__(self):
24
+ self._vocab = {
25
+ "<think>": 1,
26
+ "</think>": 2,
27
+ }
28
+ self._pattern = re.compile(r"(<think>|</think>)")
29
+
30
+ def get_vocab(self) -> dict[str, int]:
31
+ return self._vocab
32
+
33
+ def tokenize(self, text: str) -> list[str]:
34
+ tokens: list[str] = []
35
+ for part in self._pattern.split(text):
36
+ if part:
37
+ tokens.append(part)
38
+ return tokens
39
+
40
+ def convert_tokens_to_string(self, tokens: list[str]) -> str:
41
+ return "".join(tokens)
42
+
43
+
44
+@pytest.fixture
45
+def tokenizer():
46
+ return FakeNemotronTokenizer()
47
+
48
+
49
+@pytest.mark.parametrize(
50
+ "streaming,param_dict",
51
+ [
52
+ pytest.param(
53
+ False,
54
+ {
55
+ "output": "This is a reasoning section</think>This is the rest",
56
+ "reasoning": "This is a reasoning section",
57
+ "content": "This is the rest",
58
+ },
59
+ id="without_start_token",
60
+ ),
61
+ pytest.param(
62
+ True,
63
+ {
64
+ "output": "This is a reasoning section</think>This is the rest",
65
+ "reasoning": "This is a reasoning section",
66
+ "content": "This is the rest",
67
+ },
68
+ id="without_start_token_streaming",
69
+ ),
70
+ pytest.param(
71
+ False,
72
+ {
73
+ "output": "<think>This is a reasoning section</think>This is the rest",
74
+ "reasoning": "This is a reasoning section",
75
+ "content": "This is the rest",
76
+ },
77
+ id="with_start_token",
78
+ ),
79
+ pytest.param(
80
+ True,
81
+ {
82
+ "output": "<think>This is a reasoning section</think>This is the rest",
83
+ "reasoning": "This is a reasoning section",
84
+ "content": "This is the rest",
85
+ },
86
+ id="with_start_token_streaming",
87
+ ),
88
+ ],
89
+)
90
+def test_nemotron_v3_reasoning(
91
+ tokenizer: FakeNemotronTokenizer,
92
+ streaming: bool,
93
+ param_dict: ReasoningCase,
94
+):
95
+ output = tokenizer.tokenize(param_dict["output"])
96
+ model_output = [tokenizer.convert_tokens_to_string([token]) for token in output]
97
+ parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
98
+ tokenizer
99
+ )
100
+
101
+ reasoning, content = run_reasoning_extraction(
102
+ parser, model_output, streaming=streaming
103
+ )
104
+
105
+ assert reasoning == param_dict["reasoning"]
106
+ assert content == param_dict["content"]
107
+
108
+
109
+def test_nemotron_v3_without_thinking_returns_content(
110
+ tokenizer: FakeNemotronTokenizer,
111
+):
112
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
113
+ parser = parser_cls(tokenizer)
114
+ request = ChatCompletionRequest(
115
+ model="test-model",
116
+ messages=[],
117
+ chat_template_kwargs={"enable_thinking": False},
118
+ )
119
+
120
+ reasoning, content = run_reasoning_extraction(
121
+ parser,
122
+ ["This is plain content"],
123
+ request=request,
124
+ streaming=False,
125
+ )
126
+
127
+ assert reasoning is None
128
+ assert content == "This is plain content"
129
+
130
+
131
+def test_nemotron_v3_with_thinking_keeps_truncated_reasoning(
132
+ tokenizer: FakeNemotronTokenizer,
133
+):
134
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
135
+ parser = parser_cls(tokenizer)
136
+ request = ChatCompletionRequest(
137
+ model="test-model",
138
+ messages=[],
139
+ chat_template_kwargs={"enable_thinking": True},
140
+ )
141
+
142
+ reasoning, content = run_reasoning_extraction(
143
+ parser,
144
+ ["This is truncated reasoning"],
145
+ request=request,
146
+ streaming=False,
147
+ )
148
+
149
+ assert reasoning == "This is truncated reasoning"
150
+ assert content is None
vllm/reasoning/__init__.py
+4
@@ -68,6 +68,10 @@ _REASONING_PARSERS_TO_REGISTER = {
68
"mistral_reasoning_parser",
69
"MistralReasoningParser",
70
),
71
+ "nemotron_v3": (
72
+ "nemotron_v3_reasoning_parser",
73
+ "NemotronV3ReasoningParser",
74
+ ),
75
"olmo3": (
76
"olmo3_reasoning_parser",
77
"Olmo3ReasoningParser",
vllm/reasoning/nemotron_v3_reasoning_parser.py
new
+32
@@ -0,0 +1,32 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from vllm.entrypoints.openai.chat_completion.protocol import (
4
+ ChatCompletionRequest,
5
+)
6
+from vllm.entrypoints.openai.responses.protocol import (
7
+ ResponsesRequest,
8
+)
9
+from vllm.reasoning.deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
10
+
11
+
12
+class NemotronV3ReasoningParser(DeepSeekR1ReasoningParser):
13
+ """
14
+ Reasoning parser for Nemotron V3 models.
15
+ """
16
+
17
+ def extract_reasoning(
18
+ self, model_output: str, request: ChatCompletionRequest | ResponsesRequest
19
+ ) -> tuple[str | None, str | None]:
20
+ reasoning_content, final_content = super().extract_reasoning(
21
+ model_output, request
22
+ )
23
+ chat_template_kwargs = getattr(request, "chat_template_kwargs", None)
24
+
25
+ if (
26
+ chat_template_kwargs
27
+ and chat_template_kwargs.get("enable_thinking") is False
28
+ and final_content is None
29
+ ):
30
+ reasoning_content, final_content = final_content, reasoning_content
31
+
32
+ return reasoning_content, final_content