@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 203a7f27da
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 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