diff --git a/docs/features/reasoning_outputs.md b/docs/features/reasoning_outputs.md index 9d41185f1ae..5c99eb20bc7 100644 --- a/docs/features/reasoning_outputs.md +++ b/docs/features/reasoning_outputs.md @@ -249,7 +249,7 @@ Token counting starts from `reasoning_start_str`. Once the reasoning token count To use this feature: - `--reasoning-parser` enables reasoning extraction. -- `--reasoning-config` defines the reasoning boundary tokens (e.g., `reasoning_start_str`, `reasoning_end_str`). +- `--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. - `thinking_token_budget` (a sampling parameter) sets the per-request reasoning token limit. If `thinking_token_budget` is not specified, no explicit reasoning limit is applied beyond normal generation constraints such as `max_tokens`. diff --git a/tests/v1/entrypoints/openai/test_thinking_token_budget.py b/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py similarity index 72% rename from tests/v1/entrypoints/openai/test_thinking_token_budget.py rename to tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py index b3f6d53ab1d..d2db50082a5 100644 --- a/tests/v1/entrypoints/openai/test_thinking_token_budget.py +++ b/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py @@ -24,6 +24,24 @@ def server(): "--max-model-len", "2048", "--enforce-eager", + "--gpu-memory-utilization", + "0.4", + "--no-async-scheduling", + ] + with RemoteOpenAIServer(MODEL_NAME, args) as remote_server: + yield remote_server + + +@pytest.fixture(scope="module") +def server_with_auto_reasoning_config(): + args = [ + "--reasoning-parser", + "qwen3", + "--max-model-len", + "2048", + "--enforce-eager", + "--gpu-memory-utilization", + "0.4", "--no-async-scheduling", ] with RemoteOpenAIServer(MODEL_NAME, args) as remote_server: @@ -31,12 +49,18 @@ def server(): @pytest_asyncio.fixture -async def client(server): - async with server.get_async_client() as async_client: +async def client(request, server, server_with_auto_reasoning_config): + server_map = { + "default": server, + "auto_config": server_with_auto_reasoning_config, + } + target_server = server_map[request.param] + async with target_server.get_async_client() as async_client: yield async_client @pytest.mark.asyncio +@pytest.mark.parametrize("client", ["default", "auto_config"], indirect=True) async def test_thinking_token_budget_mixed_requests(client: openai.AsyncOpenAI): """Test that mixed requests (some with thinking_token_budget, some without) complete successfully without errors.""" @@ -61,6 +85,7 @@ async def test_thinking_token_budget_mixed_requests(client: openai.AsyncOpenAI): @pytest.mark.asyncio +@pytest.mark.parametrize("client", ["default", "auto_config"], indirect=True) async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI): """Test that thinking_token_budget limits the number of reasoning tokens. @@ -82,6 +107,6 @@ async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI reasoning_token_count += 1 assert reasoning_token_count == THINK_BUDGET, ( - f"reasoning tokens ({reasoning_token_count}) != " + f"reasoning tokens ({reasoning_token_count}) exceeded " f"thinking_token_budget ({THINK_BUDGET})" ) diff --git a/tests/v1/logits_processors/test_correctness.py b/tests/v1/logits_processors/test_correctness.py index bf29793710a..9ee6a70abe4 100644 --- a/tests/v1/logits_processors/test_correctness.py +++ b/tests/v1/logits_processors/test_correctness.py @@ -106,6 +106,7 @@ class MockReasoningConfig: reasoning_start_token_ids = [THINK_START_TOKEN_ID] reasoning_end_token_ids = [THINK_END_TOKEN_ID] + enabled = True def _generate_fake_sampling_metadata( diff --git a/vllm/config/reasoning.py b/vllm/config/reasoning.py index be1e2b6da58..ff5546e05eb 100644 --- a/vllm/config/reasoning.py +++ b/vllm/config/reasoning.py @@ -5,6 +5,7 @@ from dataclasses import field from vllm.config.model import ModelConfig from vllm.config.utils import config +from vllm.reasoning import ReasoningParserManager from vllm.tokenizers import cached_tokenizer_from_config @@ -18,11 +19,11 @@ class ReasoningConfig: `initialize_token_ids` and are not intended to be set directly. """ - # NOTE: These parameters are temporary, the intent is to derive them - # automatically from the reasoning parser in a future version. - reasoning_start_str: str = "" + reasoning_parser: str = "" + """The name of the ReasoningParser to use for this model.""" + reasoning_start_str: str = "" """String that indicates the start of reasoning.""" - reasoning_end_str: str = "" + reasoning_end_str: str = "" """String that indicates the end of reasoning content.""" _reasoning_start_token_ids: list[int] | None = field( @@ -36,6 +37,16 @@ class ReasoningConfig: """Private backing field for `reasoning_end_token_ids`. Set by `initialize_token_ids`. Not intended to be configured directly.""" + _enabled: bool = field(default=False, init=False, repr=False) + """Private field indicating whether reasoning token IDs have been initialized. + Set to True by `initialize_token_ids` once token IDs are initialized.""" + + @property + def enabled(self) -> bool: + """Returns True if reasoning is enabled (i.e. if token IDs have been + initialized), False otherwise.""" + return self._enabled + @property def reasoning_start_token_ids(self) -> list[int] | None: """Token IDs derived from `reasoning_start_str`. Set automatically by @@ -54,15 +65,36 @@ class ReasoningConfig: self._reasoning_start_token_ids is not None and self._reasoning_end_token_ids is not None ): - return + self._enabled = True + return # Already initialized tokenizer = cached_tokenizer_from_config(model_config=model_config) + reasoning_start_str = self.reasoning_start_str + reasoning_end_str = self.reasoning_end_str + if self.reasoning_parser is not None and ( + not reasoning_start_str or not reasoning_end_str + ): + parser_cls = ReasoningParserManager.get_reasoning_parser( + self.reasoning_parser + ) + reasoning_parser = parser_cls(tokenizer) + start_token = reasoning_parser.reasoning_start_str + if start_token and not reasoning_start_str: + reasoning_start_str = start_token + end_token = reasoning_parser.reasoning_end_str + if end_token and not reasoning_end_str: + reasoning_end_str = end_token + + if not reasoning_start_str or not reasoning_end_str: + # If we don't have valid strings to tokenize, + # we can't initialize the token IDs. + return self._reasoning_start_token_ids = tokenizer.encode( - self.reasoning_start_str, add_special_tokens=False + reasoning_start_str, add_special_tokens=False ) self._reasoning_end_token_ids = tokenizer.encode( - self.reasoning_end_str, add_special_tokens=False + reasoning_end_str, add_special_tokens=False ) if not self._reasoning_start_token_ids or not self._reasoning_end_token_ids: @@ -72,3 +104,4 @@ class ReasoningConfig: f"reasoning_end_str='{self.reasoning_end_str}'. " "Ensure the strings are valid tokens in the model's vocabulary." ) + self._enabled = True diff --git a/vllm/config/vllm.py b/vllm/config/vllm.py index 6229b44d52a..3b8431e9530 100644 --- a/vllm/config/vllm.py +++ b/vllm/config/vllm.py @@ -1210,6 +1210,12 @@ class VllmConfig: if self.reasoning_config is not None and self.model_config is not None: self.reasoning_config.initialize_token_ids(self.model_config) + if not self.reasoning_config.enabled: + logger.warning_once( + "Auto-initialization of reasoning token IDs failed. " + "Please check whether your reasoning parser has implemented " + "the `reasoning_start_str` and `reasoning_end_str`." + ) # Hybrid KV cache manager (HMA) runtime rules: # - Explicit enable (--no-disable-kv-cache-manager): error if runtime diff --git a/vllm/engine/arg_utils.py b/vllm/engine/arg_utils.py index 55c87bf356c..c9b90848ff0 100644 --- a/vllm/engine/arg_utils.py +++ b/vllm/engine/arg_utils.py @@ -1591,7 +1591,7 @@ class EngineArgs: self._set_default_max_num_seqs_and_batched_tokens_args( usage_context, model_config ) - + self._set_default_reasoning_config_args() sliding_window: int | None = None if not is_interleaved(model_config.hf_text_config): # Only set CacheConfig.sliding_window if the model is all sliding @@ -2233,6 +2233,13 @@ class EngineArgs: ) self.enable_prefix_caching = False + def _set_default_reasoning_config_args(self): + if not self.reasoning_parser: + return + if self.reasoning_config is None: + self.reasoning_config = ReasoningConfig() + self.reasoning_config.reasoning_parser = self.reasoning_parser + def _set_default_max_num_seqs_and_batched_tokens_args( self, usage_context: UsageContext | None, diff --git a/vllm/reasoning/abs_reasoning_parsers.py b/vllm/reasoning/abs_reasoning_parsers.py index 7244b9fac84..2ff72ea2f5e 100644 --- a/vllm/reasoning/abs_reasoning_parsers.py +++ b/vllm/reasoning/abs_reasoning_parsers.py @@ -39,6 +39,20 @@ class ReasoningParser: # whereas all tokenizers have .get_vocab() return self.model_tokenizer.get_vocab() + @property + def reasoning_start_str(self) -> str | None: + """Set `reasoning_start_str` to the strings that delimit + the reasoning block (e.g. `""""` and `""`). + """ + return None + + @property + def reasoning_end_str(self) -> str | None: + """Set `reasoning_end_str` to the strings that delimit + the reasoning block (e.g. `""""` and `""`). + """ + return None + @abstractmethod def is_reasoning_end(self, input_ids: Sequence[int]) -> bool: """ diff --git a/vllm/reasoning/basic_parsers.py b/vllm/reasoning/basic_parsers.py index a8bb33d2c9c..938b7f736b2 100644 --- a/vllm/reasoning/basic_parsers.py +++ b/vllm/reasoning/basic_parsers.py @@ -39,6 +39,14 @@ class BaseThinkingReasoningParser(ReasoningParser): """The token that ends reasoning content.""" raise NotImplementedError + @property + def reasoning_start_str(self) -> str: + return self.start_token + + @property + def reasoning_end_str(self) -> str: + return self.end_token + def __init__(self, tokenizer: TokenizerLike, *args, **kwargs): super().__init__(tokenizer, *args, **kwargs) diff --git a/vllm/v1/engine/input_processor.py b/vllm/v1/engine/input_processor.py index 323862d77ba..1aab90c1458 100644 --- a/vllm/v1/engine/input_processor.py +++ b/vllm/v1/engine/input_processor.py @@ -98,9 +98,9 @@ class InputProcessor: self.tokenizer, ) - if ( - params.thinking_token_budget is not None - and self.vllm_config.reasoning_config is None + if params.thinking_token_budget is not None and ( + self.vllm_config.reasoning_config is None + or not self.vllm_config.reasoning_config.enabled ): raise ValueError( "thinking_token_budget is set but reasoning_config is " diff --git a/vllm/v1/sample/logits_processor/builtin.py b/vllm/v1/sample/logits_processor/builtin.py index 0d9b67017fd..1739452b44a 100644 --- a/vllm/v1/sample/logits_processor/builtin.py +++ b/vllm/v1/sample/logits_processor/builtin.py @@ -301,7 +301,7 @@ class ThinkingTokenBudgetLogitsProcessor(LogitsProcessor): max_num_reqs = vllm_config.scheduler_config.max_num_seqs # Check if thinking is enabled - self.is_enabled = reasoning_config is not None + self.is_enabled = reasoning_config is not None and reasoning_config.enabled self.reasoning_start_token_ids = getattr( reasoning_config, "reasoning_start_token_ids", []