Skip to content

Commit e261d37

Browse files
[Refactor] Lazy-loaded reasoning_parser (#28092)
Signed-off-by: chaunceyjiang <[email protected]>
1 parent b7cbc25 commit e261d37

21 files changed

+206
-99
lines changed

docs/features/reasoning_outputs.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ You can add a new `ReasoningParser` similar to [vllm/reasoning/deepseek_r1_reaso
219219
# define a reasoning parser and register it to vllm
220220
# the name list in register_module can be used
221221
# in --reasoning-parser.
222-
@ReasoningParserManager.register_module(["example"])
223222
class ExampleParser(ReasoningParser):
224223
def __init__(self, tokenizer: AnyTokenizer):
225224
super().__init__(tokenizer)
@@ -263,6 +262,12 @@ You can add a new `ReasoningParser` similar to [vllm/reasoning/deepseek_r1_reaso
263262
tuple[Optional[str], Optional[str]]
264263
A tuple containing the reasoning content and the content.
265264
"""
265+
# Register the reasoning parser
266+
ReasoningParserManager.register_lazy_module(
267+
name="example",
268+
module_path="vllm.reasoning.example_reasoning_parser",
269+
class_name="ExampleParser",
270+
)
266271
```
267272

268273
Additionally, to enable structured output, you'll need to create a new `Reasoner` similar to the one in [vllm/reasoning/deepseek_r1_reasoning_parser.py](../../vllm/reasoning/deepseek_r1_reasoning_parser.py).

tests/reasoning/test_deepseekv3_reasoning_parser.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
from transformers import AutoTokenizer
66

77
from vllm.entrypoints.openai.protocol import ChatCompletionRequest, DeltaMessage
8-
from vllm.reasoning import (
9-
DeepSeekR1ReasoningParser,
10-
DeepSeekV3ReasoningParser,
11-
IdentityReasoningParser,
12-
)
8+
from vllm.reasoning.deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
9+
from vllm.reasoning.deepseek_v3_reasoning_parser import DeepSeekV3ReasoningParser
10+
from vllm.reasoning.identity_reasoning_parser import IdentityReasoningParser
1311

1412
REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-V3.1"
1513

vllm/engine/arg_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ def add_cli_args(parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
708708
structured_outputs_group.add_argument(
709709
"--reasoning-parser",
710710
# This choice is a special case because it's not static
711-
choices=list(ReasoningParserManager.reasoning_parsers),
711+
choices=list(ReasoningParserManager.list_registered()),
712712
**structured_outputs_kwargs["reasoning_parser"],
713713
)
714714
# Deprecated guided decoding arguments

vllm/entrypoints/openai/api_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,7 +1944,7 @@ def validate_api_server_args(args):
19441944
f"(chose from {{ {','.join(valid_tool_parses)} }})"
19451945
)
19461946

1947-
valid_reasoning_parses = ReasoningParserManager.reasoning_parsers.keys()
1947+
valid_reasoning_parses = ReasoningParserManager.list_registered()
19481948
if (
19491949
reasoning_parser := args.structured_outputs_config.reasoning_parser
19501950
) and reasoning_parser not in valid_reasoning_parses:

vllm/entrypoints/openai/run_batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async def run_request(
334334

335335

336336
def validate_run_batch_args(args):
337-
valid_reasoning_parses = ReasoningParserManager.reasoning_parsers.keys()
337+
valid_reasoning_parses = ReasoningParserManager.list_registered()
338338
if (
339339
reasoning_parser := args.structured_outputs_config.reasoning_parser
340340
) and reasoning_parser not in valid_reasoning_parses:

vllm/entrypoints/openai/tool_parsers/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
),
7373
"llama4_json": (
7474
"llama_tool_parser",
75-
"Llama4JsonToolParser",
75+
"Llama3JsonToolParser",
7676
),
7777
"llama4_pythonic": (
7878
"llama4_pythonic_tool_parser",
@@ -116,11 +116,11 @@
116116
),
117117
"qwen3_xml": (
118118
"qwen3xml_tool_parser",
119-
"Qwen3XmlToolParser",
119+
"Qwen3XMLToolParser",
120120
),
121121
"seed_oss": (
122122
"seed_oss_tool_parser",
123-
"SeedOsSToolParser",
123+
"SeedOssToolParser",
124124
),
125125
"step3": (
126126
"step3_tool_parser",

vllm/reasoning/__init__.py

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,88 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4-
from .abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
5-
from .basic_parsers import BaseThinkingReasoningParser
6-
from .deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
7-
from .deepseek_v3_reasoning_parser import DeepSeekV3ReasoningParser
8-
from .ernie45_reasoning_parser import Ernie45ReasoningParser
9-
from .glm4_moe_reasoning_parser import Glm4MoeModelReasoningParser
10-
from .gptoss_reasoning_parser import GptOssReasoningParser
11-
from .granite_reasoning_parser import GraniteReasoningParser
12-
from .hunyuan_a13b_reasoning_parser import HunyuanA13BReasoningParser
13-
from .identity_reasoning_parser import IdentityReasoningParser
14-
from .minimax_m2_reasoning_parser import MiniMaxM2ReasoningParser
15-
from .mistral_reasoning_parser import MistralReasoningParser
16-
from .olmo3_reasoning_parser import Olmo3ReasoningParser
17-
from .qwen3_reasoning_parser import Qwen3ReasoningParser
18-
from .seedoss_reasoning_parser import SeedOSSReasoningParser
19-
from .step3_reasoning_parser import Step3ReasoningParser
4+
from vllm.reasoning.abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
205

216
__all__ = [
227
"ReasoningParser",
23-
"BaseThinkingReasoningParser",
248
"ReasoningParserManager",
25-
"DeepSeekR1ReasoningParser",
26-
"IdentityReasoningParser",
27-
"DeepSeekV3ReasoningParser",
28-
"Ernie45ReasoningParser",
29-
"GraniteReasoningParser",
30-
"HunyuanA13BReasoningParser",
31-
"Qwen3ReasoningParser",
32-
"Glm4MoeModelReasoningParser",
33-
"MistralReasoningParser",
34-
"Olmo3ReasoningParser",
35-
"Step3ReasoningParser",
36-
"GptOssReasoningParser",
37-
"SeedOSSReasoningParser",
38-
"MiniMaxM2ReasoningParser",
399
]
10+
"""
11+
Register a lazy module mapping.
12+
13+
Example:
14+
ReasoningParserManager.register_lazy_module(
15+
name="qwen3",
16+
module_path="vllm.reasoning.qwen3_reasoning_parser",
17+
class_name="Qwen3ReasoningParser",
18+
)
19+
"""
20+
21+
22+
_REASONING_PARSERS_TO_REGISTER = {
23+
"deepseek_r1": ( # name
24+
"deepseek_r1_reasoning_parser", # filename
25+
"DeepSeekR1ReasoningParser", # class_name
26+
),
27+
"deepseek_v3": (
28+
"deepseek_v3_reasoning_parser",
29+
"DeepSeekV3ReasoningParser",
30+
),
31+
"ernie45": (
32+
"ernie45_reasoning_parser",
33+
"Ernie45ReasoningParser",
34+
),
35+
"glm45": (
36+
"glm4_moe_reasoning_parser",
37+
"Glm4MoeModelReasoningParser",
38+
),
39+
"openai_gptoss": (
40+
"gptoss_reasoning_parser",
41+
"GptOssReasoningParser",
42+
),
43+
"granite": (
44+
"granite_reasoning_parser",
45+
"GraniteReasoningParser",
46+
),
47+
"hunyuan_a13b": (
48+
"hunyuan_a13b_reasoning_parser",
49+
"HunyuanA13BReasoningParser",
50+
),
51+
"minimax_m2": (
52+
"minimax_m2_reasoning_parser",
53+
"MiniMaxM2ReasoningParser",
54+
),
55+
"minimax_m2_append_think": (
56+
"minimax_m2_reasoning_parser",
57+
"MiniMaxM2AppendThinkReasoningParser",
58+
),
59+
"mistral": (
60+
"mistral_reasoning_parser",
61+
"MistralReasoningParser",
62+
),
63+
"olmo3": (
64+
"olmo3_reasoning_parser",
65+
"Olmo3ReasoningParser",
66+
),
67+
"qwen3": (
68+
"qwen3_reasoning_parser",
69+
"Qwen3ReasoningParser",
70+
),
71+
"seed_oss": (
72+
"seedoss_reasoning_parser",
73+
"SeedOSSReasoningParser",
74+
),
75+
"step3": (
76+
"step3_reasoning_parser",
77+
"Step3ReasoningParser",
78+
),
79+
}
80+
81+
82+
def register_lazy_reasoning_parsers():
83+
for name, (file_name, class_name) in _REASONING_PARSERS_TO_REGISTER.items():
84+
module_path = f"vllm.reasoning.{file_name}"
85+
ReasoningParserManager.register_lazy_module(name, module_path, class_name)
86+
87+
88+
register_lazy_reasoning_parsers()

vllm/reasoning/abs_reasoning_parsers.py

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4+
import importlib
45
import os
56
from abc import abstractmethod
67
from collections.abc import Callable, Sequence
@@ -129,50 +130,117 @@ def prepare_structured_tag(
129130

130131

131132
class ReasoningParserManager:
132-
reasoning_parsers: dict[str, type] = {}
133+
"""
134+
Central registry for ReasoningParser implementations.
135+
136+
Supports two registration modes:
137+
- Eager registration via `register_module`
138+
- Lazy registration via `register_lazy_module`
139+
140+
Each reasoning parser must inherit from `ReasoningParser`.
141+
"""
142+
143+
reasoning_parsers: dict[str, type[ReasoningParser]] = {}
144+
lazy_parsers: dict[str, tuple[str, str]] = {} # name -> (module_path, class_name)
133145

134146
@classmethod
135-
def get_reasoning_parser(cls, name: str | None) -> type[ReasoningParser]:
147+
def get_reasoning_parser(cls, name: str) -> type[ReasoningParser]:
136148
"""
137-
Get reasoning parser by name which is registered by `register_module`.
149+
Retrieve a registered or lazily registered ReasoningParser class.
150+
151+
If the parser is lazily registered, it will be imported and cached
152+
on first access.
138153
139-
Raise a KeyError exception if the name is not registered.
154+
Raises:
155+
KeyError: if no parser is found under the given name.
140156
"""
141157
if name in cls.reasoning_parsers:
142158
return cls.reasoning_parsers[name]
143159

144-
raise KeyError(f"reasoning helper: '{name}' not found in reasoning_parsers")
160+
if name in cls.lazy_parsers:
161+
return cls._load_lazy_parser(name)
162+
163+
raise KeyError(f"Reasoning parser '{name}' not found.")
164+
165+
@classmethod
166+
def list_registered(cls) -> list[str]:
167+
"""Return names of all eagerly and lazily registered reasoning parsers."""
168+
return sorted(set(cls.reasoning_parsers.keys()) | set(cls.lazy_parsers.keys()))
169+
170+
@classmethod
171+
def _load_lazy_parser(cls, name: str) -> type[ReasoningParser]:
172+
"""Import and register a lazily loaded reasoning parser."""
173+
module_path, class_name = cls.lazy_parsers[name]
174+
try:
175+
mod = importlib.import_module(module_path)
176+
parser_cls = getattr(mod, class_name)
177+
if not issubclass(parser_cls, ReasoningParser):
178+
raise TypeError(
179+
f"{class_name} in {module_path} is not a ReasoningParser subclass."
180+
)
181+
182+
cls.reasoning_parsers[name] = parser_cls # cache
183+
return parser_cls
184+
except Exception as e:
185+
logger.exception(
186+
"Failed to import lazy reasoning parser '%s' from %s: %s",
187+
name,
188+
module_path,
189+
e,
190+
)
191+
raise
145192

146193
@classmethod
147194
def _register_module(
148195
cls,
149-
module: type,
196+
module: type[ReasoningParser],
150197
module_name: str | list[str] | None = None,
151198
force: bool = True,
152199
) -> None:
200+
"""Register a ReasoningParser class immediately."""
153201
if not issubclass(module, ReasoningParser):
154202
raise TypeError(
155203
f"module must be subclass of ReasoningParser, but got {type(module)}"
156204
)
205+
157206
if module_name is None:
158-
module_name = module.__name__
159-
if isinstance(module_name, str):
160-
module_name = [module_name]
161-
for name in module_name:
207+
module_names = [module.__name__]
208+
elif isinstance(module_name, str):
209+
module_names = [module_name]
210+
elif is_list_of(module_name, str):
211+
module_names = module_name
212+
else:
213+
raise TypeError("module_name must be str, list[str], or None.")
214+
215+
for name in module_names:
162216
if not force and name in cls.reasoning_parsers:
163-
existed_module = cls.reasoning_parsers[name]
164-
raise KeyError(
165-
f"{name} is already registered at {existed_module.__module__}"
166-
)
217+
existed = cls.reasoning_parsers[name]
218+
raise KeyError(f"{name} is already registered at {existed.__module__}")
167219
cls.reasoning_parsers[name] = module
168220

221+
@classmethod
222+
def register_lazy_module(cls, name: str, module_path: str, class_name: str) -> None:
223+
"""
224+
Register a lazy module mapping for delayed import.
225+
226+
Example:
227+
ReasoningParserManager.register_lazy_module(
228+
name="qwen3",
229+
module_path="vllm.reasoning.parsers.qwen3_reasoning_parser",
230+
class_name="Qwen3ReasoningParser",
231+
)
232+
"""
233+
cls.lazy_parsers[name] = (module_path, class_name)
234+
169235
@classmethod
170236
def register_module(
171237
cls,
172238
name: str | list[str] | None = None,
173239
force: bool = True,
174-
module: type | None = None,
175-
) -> type | Callable:
240+
module: type[ReasoningParser] | None = None,
241+
) -> (
242+
type[ReasoningParser] | Callable[[type[ReasoningParser]], type[ReasoningParser]]
243+
):
176244
"""
177245
Register module with the given name or name list. it can be used as a
178246
decoder(with module as None) or normal function(with module as not
@@ -181,24 +249,29 @@ def register_module(
181249
if not isinstance(force, bool):
182250
raise TypeError(f"force must be a boolean, but got {type(force)}")
183251

184-
# raise the error ahead of time
185-
if not (name is None or isinstance(name, str) or is_list_of(name, str)):
186-
raise TypeError(
187-
"name must be None, an instance of str, or a sequence of str, "
188-
f"but got {type(name)}"
189-
)
190-
191-
# use it as a normal method: x.register_module(module=SomeClass)
252+
# Immediate registration (explicit call)
192253
if module is not None:
193254
cls._register_module(module=module, module_name=name, force=force)
194255
return module
195256

196-
# use it as a decorator: @x.register_module()
197-
def _register(module):
198-
cls._register_module(module=module, module_name=name, force=force)
199-
return module
257+
# Decorator usage
258+
def _decorator(obj: type[ReasoningParser]) -> type[ReasoningParser]:
259+
module_path = obj.__module__
260+
class_name = obj.__name__
261+
262+
if isinstance(name, str):
263+
names = [name]
264+
elif is_list_of(name, str):
265+
names = name
266+
else:
267+
names = [class_name]
268+
269+
for n in names:
270+
cls.lazy_parsers[n] = (module_path, class_name)
271+
272+
return obj
200273

201-
return _register
274+
return _decorator
202275

203276
@classmethod
204277
def import_reasoning_parser(cls, plugin_path: str) -> None:

0 commit comments

Comments
 (0)