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 5, 08:30 UTC
[Feature] Add auto-detection for reasoning_config when only reasoning_parser is set (#38214)
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
Chauncey committed
Apr 10, 2026 at 09:36 UTC
ecbfbb8d61cbe1e2dfd63c5a39490e59922fb1b4
10 files changed
+110
-16
docs/features/reasoning_outputs.md
+1
-1
@@ -249,7 +249,7 @@ Token counting starts from `reasoning_start_str`. Once the reasoning token count
249
To use this feature:
250
251
- `--reasoning-parser` enables reasoning extraction.
252
-- `--reasoning-config` defines the reasoning boundary tokens (e.g., `reasoning_start_str`, `reasoning_end_str`).
252
+- `--reasoning-config` defines the reasoning boundary tokens (e.g., `reasoning_start_str`, `reasoning_end_str`). If not set, vLLM will attempt to automatically initialize these tokens from the reasoning parser.
253
- `thinking_token_budget` (a sampling parameter) sets the per-request reasoning token limit.
254
255
If `thinking_token_budget` is not specified, no explicit reasoning limit is applied beyond normal generation constraints such as `max_tokens`.
tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py
renamed
+28
-3
@@ -24,6 +24,24 @@ def server():
24
"--max-model-len",
25
"2048",
26
"--enforce-eager",
27
+ "--gpu-memory-utilization",
28
+ "0.4",
29
+ "--no-async-scheduling",
30
+ ]
31
+ with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
32
+ yield remote_server
33
+
34
+
35
+@pytest.fixture(scope="module")
36
+def server_with_auto_reasoning_config():
37
+ args = [
38
+ "--reasoning-parser",
39
+ "qwen3",
40
+ "--max-model-len",
41
+ "2048",
42
+ "--enforce-eager",
43
+ "--gpu-memory-utilization",
44
+ "0.4",
45
"--no-async-scheduling",
46
]
47
with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
@@ -31,12 +49,18 @@ def server():
49
50
51
@pytest_asyncio.fixture
34
-async def client(server):
35
- async with server.get_async_client() as async_client:
52
+async def client(request, server, server_with_auto_reasoning_config):
53
+ server_map = {
54
+ "default": server,
55
+ "auto_config": server_with_auto_reasoning_config,
56
+ }
57
+ target_server = server_map[request.param]
58
+ async with target_server.get_async_client() as async_client:
59
yield async_client
60
61
62
@pytest.mark.asyncio
63
+@pytest.mark.parametrize("client", ["default", "auto_config"], indirect=True)
64
async def test_thinking_token_budget_mixed_requests(client: openai.AsyncOpenAI):
65
"""Test that mixed requests (some with thinking_token_budget, some without)
66
complete successfully without errors."""
@@ -61,6 +85,7 @@ async def test_thinking_token_budget_mixed_requests(client: openai.AsyncOpenAI):
85
86
87
@pytest.mark.asyncio
88
+@pytest.mark.parametrize("client", ["default", "auto_config"], indirect=True)
89
async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI):
90
"""Test that thinking_token_budget limits the number of reasoning tokens.
91
@@ -82,6 +107,6 @@ async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI
107
reasoning_token_count += 1
108
109
assert reasoning_token_count == THINK_BUDGET, (
85
- f"reasoning tokens ({reasoning_token_count}) != "
110
+ f"reasoning tokens ({reasoning_token_count}) exceeded "
111
f"thinking_token_budget ({THINK_BUDGET})"
112
)
tests/v1/logits_processors/test_correctness.py
+1
@@ -106,6 +106,7 @@ class MockReasoningConfig:
106
107
reasoning_start_token_ids = [THINK_START_TOKEN_ID]
108
reasoning_end_token_ids = [THINK_END_TOKEN_ID]
109
+ enabled = True
110
111
112
def _generate_fake_sampling_metadata(
vllm/config/reasoning.py
+40
-7
@@ -5,6 +5,7 @@ from dataclasses import field
5
6
from vllm.config.model import ModelConfig
7
from vllm.config.utils import config
8
+from vllm.reasoning import ReasoningParserManager
9
from vllm.tokenizers import cached_tokenizer_from_config
10
11
@@ -18,11 +19,11 @@ class ReasoningConfig:
19
`initialize_token_ids` and are not intended to be set directly.
20
"""
21
21
- # NOTE: These parameters are temporary, the intent is to derive them
22
- # automatically from the reasoning parser in a future version.
23
- reasoning_start_str: str = "<think>"
22
+ reasoning_parser: str = ""
23
+ """The name of the ReasoningParser to use for this model."""
24
+ reasoning_start_str: str = ""
25
"""String that indicates the start of reasoning."""
25
- reasoning_end_str: str = "</think>"
26
+ reasoning_end_str: str = ""
27
"""String that indicates the end of reasoning content."""
28
29
_reasoning_start_token_ids: list[int] | None = field(
@@ -36,6 +37,16 @@ class ReasoningConfig:
37
"""Private backing field for `reasoning_end_token_ids`. Set by
38
`initialize_token_ids`. Not intended to be configured directly."""
39
40
+ _enabled: bool = field(default=False, init=False, repr=False)
41
+ """Private field indicating whether reasoning token IDs have been initialized.
42
+ Set to True by `initialize_token_ids` once token IDs are initialized."""
43
+
44
+ @property
45
+ def enabled(self) -> bool:
46
+ """Returns True if reasoning is enabled (i.e. if token IDs have been
47
+ initialized), False otherwise."""
48
+ return self._enabled
49
+
50
@property
51
def reasoning_start_token_ids(self) -> list[int] | None:
52
"""Token IDs derived from `reasoning_start_str`. Set automatically by
@@ -54,15 +65,36 @@ class ReasoningConfig:
65
self._reasoning_start_token_ids is not None
66
and self._reasoning_end_token_ids is not None
67
):
57
- return
68
+ self._enabled = True
69
+ return # Already initialized
70
71
tokenizer = cached_tokenizer_from_config(model_config=model_config)
72
+ reasoning_start_str = self.reasoning_start_str
73
+ reasoning_end_str = self.reasoning_end_str
74
+ if self.reasoning_parser is not None and (
75
+ not reasoning_start_str or not reasoning_end_str
76
+ ):
77
+ parser_cls = ReasoningParserManager.get_reasoning_parser(
78
+ self.reasoning_parser
79
+ )
80
+ reasoning_parser = parser_cls(tokenizer)
81
+ start_token = reasoning_parser.reasoning_start_str
82
+ if start_token and not reasoning_start_str:
83
+ reasoning_start_str = start_token
84
85
+ end_token = reasoning_parser.reasoning_end_str
86
+ if end_token and not reasoning_end_str:
87
+ reasoning_end_str = end_token
88
+
89
+ if not reasoning_start_str or not reasoning_end_str:
90
+ # If we don't have valid strings to tokenize,
91
+ # we can't initialize the token IDs.
92
+ return
93
self._reasoning_start_token_ids = tokenizer.encode(
62
- self.reasoning_start_str, add_special_tokens=False
94
+ reasoning_start_str, add_special_tokens=False
95
)
96
self._reasoning_end_token_ids = tokenizer.encode(
65
- self.reasoning_end_str, add_special_tokens=False
97
+ reasoning_end_str, add_special_tokens=False
98
)
99
100
if not self._reasoning_start_token_ids or not self._reasoning_end_token_ids:
@@ -72,3 +104,4 @@ class ReasoningConfig:
104
f"reasoning_end_str='{self.reasoning_end_str}'. "
105
"Ensure the strings are valid tokens in the model's vocabulary."
106
)
107
+ self._enabled = True
vllm/config/vllm.py
+6
@@ -1210,6 +1210,12 @@ class VllmConfig:
1210
1211
if self.reasoning_config is not None and self.model_config is not None:
1212
self.reasoning_config.initialize_token_ids(self.model_config)
1213
+ if not self.reasoning_config.enabled:
1214
+ logger.warning_once(
1215
+ "Auto-initialization of reasoning token IDs failed. "
1216
+ "Please check whether your reasoning parser has implemented "
1217
+ "the `reasoning_start_str` and `reasoning_end_str`."
1218
+ )
1219
1220
# Hybrid KV cache manager (HMA) runtime rules:
1221
# - Explicit enable (--no-disable-kv-cache-manager): error if runtime
vllm/engine/arg_utils.py
+8
-1
@@ -1591,7 +1591,7 @@ class EngineArgs:
1591
self._set_default_max_num_seqs_and_batched_tokens_args(
1592
usage_context, model_config
1593
)
1594
-
1594
+ self._set_default_reasoning_config_args()
1595
sliding_window: int | None = None
1596
if not is_interleaved(model_config.hf_text_config):
1597
# Only set CacheConfig.sliding_window if the model is all sliding
@@ -2233,6 +2233,13 @@ class EngineArgs:
2233
)
2234
self.enable_prefix_caching = False
2235
2236
+ def _set_default_reasoning_config_args(self):
2237
+ if not self.reasoning_parser:
2238
+ return
2239
+ if self.reasoning_config is None:
2240
+ self.reasoning_config = ReasoningConfig()
2241
+ self.reasoning_config.reasoning_parser = self.reasoning_parser
2242
+
2243
def _set_default_max_num_seqs_and_batched_tokens_args(
2244
self,
2245
usage_context: UsageContext | None,
vllm/reasoning/abs_reasoning_parsers.py
+14
@@ -39,6 +39,20 @@ class ReasoningParser:
39
# whereas all tokenizers have .get_vocab()
40
return self.model_tokenizer.get_vocab()
41
42
+ @property
43
+ def reasoning_start_str(self) -> str | None:
44
+ """Set `reasoning_start_str` to the strings that delimit
45
+ the reasoning block (e.g. `""<seed:think>""` and `"<think>"`).
46
+ """
47
+ return None
48
+
49
+ @property
50
+ def reasoning_end_str(self) -> str | None:
51
+ """Set `reasoning_end_str` to the strings that delimit
52
+ the reasoning block (e.g. `""</seed:think>""` and `"</think>"`).
53
+ """
54
+ return None
55
+
56
@abstractmethod
57
def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
58
"""
vllm/reasoning/basic_parsers.py
+8
@@ -39,6 +39,14 @@ class BaseThinkingReasoningParser(ReasoningParser):
39
"""The token that ends reasoning content."""
40
raise NotImplementedError
41
42
+ @property
43
+ def reasoning_start_str(self) -> str:
44
+ return self.start_token
45
+
46
+ @property
47
+ def reasoning_end_str(self) -> str:
48
+ return self.end_token
49
+
50
def __init__(self, tokenizer: TokenizerLike, *args, **kwargs):
51
super().__init__(tokenizer, *args, **kwargs)
52
vllm/v1/engine/input_processor.py
+3
-3
@@ -98,9 +98,9 @@ class InputProcessor:
98
self.tokenizer,
99
)
100
101
- if (
102
- params.thinking_token_budget is not None
103
- and self.vllm_config.reasoning_config is None
101
+ if params.thinking_token_budget is not None and (
102
+ self.vllm_config.reasoning_config is None
103
+ or not self.vllm_config.reasoning_config.enabled
104
):
105
raise ValueError(
106
"thinking_token_budget is set but reasoning_config is "
vllm/v1/sample/logits_processor/builtin.py
+1
-1
@@ -301,7 +301,7 @@ class ThinkingTokenBudgetLogitsProcessor(LogitsProcessor):
301
max_num_reqs = vllm_config.scheduler_config.max_num_seqs
302
303
# Check if thinking is enabled
304
- self.is_enabled = reasoning_config is not None
304
+ self.is_enabled = reasoning_config is not None and reasoning_config.enabled
305
306
self.reasoning_start_token_ids = getattr(
307
reasoning_config, "reasoning_start_token_ids", []