From b0344a98959522f20acb638946c010d44d9c66f6 Mon Sep 17 00:00:00 2001 From: liuyanyi Date: Mon, 27 Apr 2026 02:57:09 +0000 Subject: [PATCH 1/3] feat(reasoning): implement reasoning boundary validation for speculative tokens Signed-off-by: liuyanyi Assisted-by: OpenAI Codex --- .../test_reasoning_boundary_index.py | 48 +++++ .../test_spec_decode_reasoning_boundary.py | 195 ++++++++++++++++++ vllm/envs.py | 6 + vllm/reasoning/abs_reasoning_parsers.py | 30 +++ vllm/reasoning/basic_parsers.py | 12 ++ vllm/v1/core/sched/scheduler.py | 75 ++++++- vllm/v1/structured_output/__init__.py | 39 ++++ 7 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 tests/reasoning/test_reasoning_boundary_index.py create mode 100644 tests/v1/structured_output/test_spec_decode_reasoning_boundary.py diff --git a/tests/reasoning/test_reasoning_boundary_index.py b/tests/reasoning/test_reasoning_boundary_index.py new file mode 100644 index 000000000000..6760a5d4755f --- /dev/null +++ b/tests/reasoning/test_reasoning_boundary_index.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from collections.abc import Sequence + +from vllm.reasoning.abs_reasoning_parsers import ReasoningParser +from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser + + +class DummyTokenizer: + def get_vocab(self) -> dict[str, int]: + return {"": 1, "": 99} + + +class DummyThinkingParser(BaseThinkingReasoningParser): + @property + def start_token(self) -> str: + return "" + + @property + def end_token(self) -> str: + return "" + + +class MultiTokenEndParser(ReasoningParser): + def is_reasoning_end(self, input_ids: Sequence[int]) -> bool: + return len(input_ids) >= 3 and list(input_ids[-3:]) == [7, 8, 9] + + def extract_content_ids(self, input_ids: list[int]) -> list[int]: + return input_ids + + +def test_single_token_end_marker_boundary_uses_delta_fast_path(): + parser = DummyThinkingParser(DummyTokenizer()) + + assert parser.find_reasoning_end_index([1, 2], [3, 99, 4]) == 1 + assert parser.find_reasoning_end_index([1, 2, 99], [3, 4]) is None + assert parser.may_have_reasoning_end_in_delta([3, 99, 4]) is True + assert parser.may_have_reasoning_end_in_delta([3, 4]) is False + + +def test_fallback_boundary_detection_crosses_prefix_and_delta(): + parser = MultiTokenEndParser(None) + + assert parser.find_reasoning_end_index([1, 7, 8], [9, 10]) == 0 + assert parser.find_reasoning_end_index([1, 7], [8, 10]) is None + assert parser.may_have_reasoning_end_in_delta([10]) is True + assert parser.may_have_reasoning_end_in_delta([]) is False diff --git a/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py b/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py new file mode 100644 index 000000000000..81f105579814 --- /dev/null +++ b/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py @@ -0,0 +1,195 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from vllm.reasoning import ReasoningParser +from vllm.sampling_params import SamplingParams +from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput +from vllm.v1.core.sched.scheduler import Scheduler +from vllm.v1.outputs import ModelRunnerOutput +from vllm.v1.request import Request, RequestStatus +from vllm.v1.structured_output import validate_spec_tokens_with_reasoning_boundary + + +def make_structured_request( + *, + reasoning_ended: bool | None = False, + valid_tokens: list[int] | None = None, +) -> SimpleNamespace: + grammar = Mock() + grammar.validate_tokens.return_value = valid_tokens or [] + grammar.accept_tokens.return_value = True + return SimpleNamespace(reasoning_ended=reasoning_ended, grammar=grammar) + + +def make_request(structured_req: SimpleNamespace) -> Mock: + request = Mock(spec=Request) + request.request_id = "req-0" + request.prompt_token_ids = [1, 2, 3] + request.all_token_ids = [1, 2, 3, 4, 5] + request.use_structured_output = True + request.structured_output_request = structured_req + return request + + +@pytest.mark.parametrize( + ( + "boundary_end", + "token_ids", + "valid_suffix", + "expected", + "validated", + "accepted", + ), + [ + (None, [9, 10], [], [9, 10], None, None), + (1, [9, 99], [], [9, 99], None, None), + (1, [9, 99, 11, 12], [11, 12], [9, 99, 11, 12], [11, 12], [11, 12]), + (1, [9, 99, 11, 13], [11], [9, 99, 11], [11, 13], [11]), + (1, [9, 99, 13], [], [9, 99], [13], None), + ], +) +def test_validate_spec_tokens_splits_reasoning_boundary_suffix( + boundary_end: int | None, + token_ids: list[int], + valid_suffix: list[int], + expected: list[int], + validated: list[int] | None, + accepted: list[int] | None, +): + structured_req = make_structured_request(valid_tokens=valid_suffix) + request = make_request(structured_req) + reasoner = Mock(spec=ReasoningParser) + reasoner.find_reasoning_end_index.return_value = boundary_end + + result = validate_spec_tokens_with_reasoning_boundary( + request, + token_ids=token_ids, + reasoner=reasoner, + ) + + assert result == expected + assert structured_req.reasoning_ended is (boundary_end is not None) + if validated is None: + structured_req.grammar.validate_tokens.assert_not_called() + else: + structured_req.grammar.validate_tokens.assert_called_once_with(validated) + if accepted is None: + structured_req.grammar.accept_tokens.assert_not_called() + else: + structured_req.grammar.accept_tokens.assert_called_once_with("req-0", accepted) + + +def make_scheduler_output(request_id: str) -> SchedulerOutput: + return SchedulerOutput( + scheduled_new_reqs=[], + scheduled_cached_reqs=CachedRequestData.make_empty(), + num_scheduled_tokens={request_id: 4}, + total_num_scheduled_tokens=4, + scheduled_spec_decode_tokens={request_id: [99, 11, 13]}, + scheduled_encoder_inputs={}, + num_common_prefix_blocks=[], + finished_req_ids=set(), + free_encoder_mm_hashes=[], + ) + + +def make_model_runner_output( + request_id: str, token_ids: list[int] +) -> ModelRunnerOutput: + return ModelRunnerOutput( + req_ids=[request_id], + req_id_to_index={request_id: 0}, + sampled_token_ids=[token_ids], + logprobs=None, + prompt_logprobs_dict={}, + pooler_output=[], + ) + + +def prepare_running_request( + reasoning_ended: bool | None, +) -> tuple[Scheduler, Request]: + scheduler = Scheduler.__new__(Scheduler) + scheduler.enable_spec_reasoning_boundary_validation = True + scheduler.log_stats = False + scheduler.perf_metrics = None + scheduler.max_model_len = 128 + scheduler.requests = {} + scheduler.running = [] + scheduler.finished_req_ids_dict = {} + scheduler.connector = None + scheduler.kv_cache_manager = Mock() + scheduler.kv_cache_manager.take_events.return_value = None + scheduler.make_stats = Mock(return_value=None) + scheduler.structured_output_manager = SimpleNamespace( + reasoner=None, + enable_in_reasoning=False, + should_advance=Mock( + side_effect=lambda req: ( + req.structured_output_request.reasoning_ended is True + ) + ), + ) + + request = Request( + request_id="req-0", + prompt_token_ids=[1, 2, 3], + sampling_params=SamplingParams(max_tokens=10, ignore_eos=True), + pooling_params=None, + ) + request.num_computed_tokens = request.num_tokens + 4 + request.num_output_placeholders = 4 + request.status = RequestStatus.RUNNING + request.structured_output_request = make_structured_request( + reasoning_ended=reasoning_ended, + valid_tokens=[11], + ) + scheduler.requests[request.request_id] = request + scheduler.running.append(request) + return scheduler, request + + +def test_scheduler_validates_and_truncates_post_boundary_spec_tokens(): + scheduler, request = prepare_running_request(reasoning_ended=False) + reasoner = Mock(spec=ReasoningParser) + reasoner.is_reasoning_end.return_value = False + reasoner.may_have_reasoning_end_in_delta.return_value = True + reasoner.find_reasoning_end_index.return_value = 1 + scheduler.structured_output_manager.reasoner = reasoner + + scheduler.update_from_output( + make_scheduler_output(request.request_id), + make_model_runner_output(request.request_id, [90, 99, 11, 13]), + ) + + grammar = request.structured_output_request.grammar + assert list(request.output_token_ids) == [90, 99, 11] + assert request.num_computed_tokens == len(request.all_token_ids) + assert request.num_output_placeholders == 3 + grammar.validate_tokens.assert_called_once_with([11, 13]) + grammar.accept_tokens.assert_called_once_with(request.request_id, [11]) + + +def test_scheduler_initializes_prompt_reasoning_state_before_boundary_path(): + scheduler, request = prepare_running_request(reasoning_ended=None) + reasoner = Mock(spec=ReasoningParser) + reasoner.is_reasoning_end.return_value = True + scheduler.structured_output_manager.reasoner = reasoner + + scheduler.update_from_output( + make_scheduler_output(request.request_id), + make_model_runner_output(request.request_id, [10, 11, 12]), + ) + + grammar = request.structured_output_request.grammar + assert request.structured_output_request.reasoning_ended is True + assert list(request.output_token_ids) == [10, 11, 12] + reasoner.is_reasoning_end.assert_called_once_with(request.prompt_token_ids) + reasoner.may_have_reasoning_end_in_delta.assert_not_called() + grammar.validate_tokens.assert_not_called() + grammar.accept_tokens.assert_called_once_with(request.request_id, [10, 11, 12]) diff --git a/vllm/envs.py b/vllm/envs.py index 806aed2a0414..bb55d6655407 100755 --- a/vllm/envs.py +++ b/vllm/envs.py @@ -1685,6 +1685,12 @@ def _get_or_set_default() -> str: "VLLM_DEBUG_MFU_METRICS": lambda: bool( int(os.getenv("VLLM_DEBUG_MFU_METRICS", "0")) ), + # Enable reasoning-boundary validation inside accepted speculative tokens. + # This is opt-in to avoid unexpected regressions for parsers that are not + # yet adapted to the validation path. + "VLLM_SPEC_REASONING_BOUNDARY_VALIDATION": lambda: bool( + int(os.getenv("VLLM_SPEC_REASONING_BOUNDARY_VALIDATION", "0")) + ), # Disable using pytorch's pin memory for CPU offloading. "VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY": lambda: bool( int(os.getenv("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY", "0")) diff --git a/vllm/reasoning/abs_reasoning_parsers.py b/vllm/reasoning/abs_reasoning_parsers.py index 8edbc5f82efd..531f1f2d079f 100644 --- a/vllm/reasoning/abs_reasoning_parsers.py +++ b/vllm/reasoning/abs_reasoning_parsers.py @@ -99,6 +99,36 @@ def is_reasoning_end_streaming( """ return self.is_reasoning_end(input_ids) + def find_reasoning_end_index( + self, prefix_ids: Sequence[int], delta_ids: Sequence[int] + ) -> int | None: + """Find where reasoning ends inside a streaming token delta. + + Args: + prefix_ids: Token ids accepted before this delta. + delta_ids: Newly accepted candidate token ids. + + Returns: + The index in ``delta_ids`` where the reasoning-end marker completes, + or ``None`` if the marker does not complete inside ``delta_ids``. + """ + current_input_ids = list(prefix_ids) + for end_index, token_id in enumerate(delta_ids): + current_input_ids.append(token_id) + if self.is_reasoning_end_streaming(current_input_ids, (token_id,)): + return end_index + return None + + def may_have_reasoning_end_in_delta(self, delta_ids: Sequence[int]) -> bool: + """Cheap precheck before running reasoning-boundary detection. + + Parsers with explicit single-token end markers should override this to + avoid expensive fallback checks on every speculative decode step. + The default is conservative for parsers that may use multi-token or + context-dependent reasoning-end markers. + """ + return bool(delta_ids) + @abstractmethod def extract_content_ids(self, input_ids: list[int]) -> list[int]: """ diff --git a/vllm/reasoning/basic_parsers.py b/vllm/reasoning/basic_parsers.py index 938b7f736b2c..ce44a41987e0 100644 --- a/vllm/reasoning/basic_parsers.py +++ b/vllm/reasoning/basic_parsers.py @@ -86,6 +86,18 @@ def is_reasoning_end_streaming( end_token_id = self.end_token_id return end_token_id in delta_ids + def find_reasoning_end_index( + self, prefix_ids: Sequence[int], delta_ids: Sequence[int] + ) -> int | None: + end_token_id = self.end_token_id + try: + return delta_ids.index(end_token_id) + except ValueError: + return None + + def may_have_reasoning_end_in_delta(self, delta_ids: Sequence[int]) -> bool: + return self.end_token_id in delta_ids + def extract_content_ids(self, input_ids: list[int]) -> list[int]: """ Extract the content after the end tokens diff --git a/vllm/v1/core/sched/scheduler.py b/vllm/v1/core/sched/scheduler.py index 2f22adf8a8e6..c7260fba9a51 100644 --- a/vllm/v1/core/sched/scheduler.py +++ b/vllm/v1/core/sched/scheduler.py @@ -58,7 +58,10 @@ from vllm.v1.outputs import DraftTokenIds, KVConnectorOutput, ModelRunnerOutput from vllm.v1.request import Request, RequestStatus, StreamingUpdate from vllm.v1.spec_decode.metrics import SpecDecodingStats -from vllm.v1.structured_output import StructuredOutputManager +from vllm.v1.structured_output import ( + StructuredOutputManager, + validate_spec_tokens_with_reasoning_boundary, +) from vllm.v1.utils import record_function_or_nullcontext logger = init_logger(__name__) @@ -259,6 +262,10 @@ def __init__( if self.log_stats and vllm_config.observability_config.enable_mfu_metrics: self.perf_metrics = ModelMetrics(vllm_config) + self.enable_spec_reasoning_boundary_validation = ( + envs.VLLM_SPEC_REASONING_BOUNDARY_VALIDATION + ) + if self.vllm_config.model_config.enable_return_routed_experts: assert self.dcp_world_size == 1 and self.pcp_world_size == 1, ( "enable_return_routed_experts does not support context parallelism " @@ -1366,6 +1373,8 @@ def update_from_output( scheduled_spec_token_ids = ( scheduler_output.scheduled_spec_decode_tokens.get(req_id) ) + num_draft_tokens = 0 + num_accepted = 0 if scheduled_spec_token_ids and generated_token_ids: num_draft_tokens = len(scheduled_spec_token_ids) num_accepted = len(generated_token_ids) - 1 @@ -1381,13 +1390,6 @@ def update_from_output( # the scheduled spec tokens count and so is similarly adjusted. if request.num_output_placeholders > 0: request.num_output_placeholders -= num_rejected - spec_decoding_stats = self.make_spec_decoding_stats( - spec_decoding_stats, - num_draft_tokens=num_draft_tokens, - num_accepted_tokens=num_accepted, - num_invalid_spec_tokens=scheduler_output.num_invalid_spec_tokens, - request_id=req_id, - ) # Free encoder inputs only after the step has actually executed. if request.has_encoder_inputs: @@ -1400,6 +1402,63 @@ def update_from_output( kv_transfer_params = None status_before_stop = request.status + # If reasoning ends inside accepted speculative tokens, tokens after + # the boundary belong to the answer phase and must be grammar-validated + # before they are appended to the request output. + structured_req = request.structured_output_request + validate_reasoning_boundary = ( + new_token_ids + and scheduled_spec_token_ids + and request.use_structured_output + and structured_req is not None + and self.structured_output_manager.reasoner is not None + and not self.structured_output_manager.enable_in_reasoning + and structured_req.reasoning_ended is not True + and self.enable_spec_reasoning_boundary_validation + ) + advanced_with_reasoning_boundary = False + if validate_reasoning_boundary: + reasoner = self.structured_output_manager.reasoner + if structured_req.reasoning_ended is None: + structured_req.reasoning_ended = reasoner.is_reasoning_end( + request.prompt_token_ids or [] + ) + + if structured_req.reasoning_ended is False: + may_have_reasoning_end = ( + reasoner.may_have_reasoning_end_in_delta(new_token_ids) + ) + + if may_have_reasoning_end: + num_new_token_ids = len(new_token_ids) + new_token_ids = validate_spec_tokens_with_reasoning_boundary( + request, + new_token_ids, + reasoner, + ) + advanced_with_reasoning_boundary = ( + structured_req.reasoning_ended is True + ) + num_rejected_by_grammar = num_new_token_ids - len(new_token_ids) + if num_rejected_by_grammar: + if request.num_computed_tokens > 0: + request.num_computed_tokens -= ( + num_rejected_by_grammar + ) + if request.num_output_placeholders > 0: + request.num_output_placeholders -= ( + num_rejected_by_grammar + ) + + if scheduled_spec_token_ids and generated_token_ids: + spec_decoding_stats = self.make_spec_decoding_stats( + spec_decoding_stats, + num_draft_tokens=num_draft_tokens, + num_accepted_tokens=min(len(new_token_ids), num_accepted), + num_invalid_spec_tokens=scheduler_output.num_invalid_spec_tokens, + request_id=req_id, + ) + # Check for stop and update request status. if new_token_ids: new_token_ids, stopped = self._update_request_with_output( diff --git a/vllm/v1/structured_output/__init__.py b/vllm/v1/structured_output/__init__.py index 213b49f28d91..84281430605b 100644 --- a/vllm/v1/structured_output/__init__.py +++ b/vllm/v1/structured_output/__init__.py @@ -32,6 +32,45 @@ logger = init_logger(__name__) +def validate_spec_tokens_with_reasoning_boundary( + request: "Request", + token_ids: list[int], + reasoner: "ReasoningParser", +) -> list[int]: + """Validate accepted speculative tokens across a reasoning boundary. + + Reasoning tokens are unconstrained. Once the reasoning-end marker is + accepted, only the post-boundary answer suffix is grammar-validated and + committed to the grammar state. + """ + structured_req = request.structured_output_request + assert token_ids + assert request.use_structured_output + assert structured_req is not None + assert structured_req.grammar is not None + assert structured_req.reasoning_ended is False + grammar = structured_req.grammar + + boundary_end = reasoner.find_reasoning_end_index( + request.all_token_ids, token_ids + ) + if boundary_end is None: + return token_ids + + keep = token_ids[: boundary_end + 1] + suffix = token_ids[boundary_end + 1 :] + + structured_req.reasoning_ended = True + + if not suffix: + return keep + + valid_suffix = grammar.validate_tokens(suffix) + if valid_suffix: + grammar.accept_tokens(request.request_id, valid_suffix) + return keep + valid_suffix + + class StructuredOutputManager: """Engine-level manager for structured output requests.""" From 3a2e43f46fc1c3129aa443082b15eeeb6bb5bf16 Mon Sep 17 00:00:00 2001 From: liuyanyi Date: Mon, 27 Apr 2026 03:00:19 +0000 Subject: [PATCH 2/3] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E5=A4=8D=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E5=99=A8=E5=9C=A8=E8=BE=B9=E7=95=8C=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=AD=E5=AF=B9=E6=8E=A8=E7=90=86=E7=8A=B6=E6=80=81=E7=9A=84?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: liuyanyi Assisted-by: OpenAI Codex --- .../test_spec_decode_reasoning_boundary.py | 7 +-- vllm/v1/core/sched/scheduler.py | 54 +++++++++---------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py b/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py index 81f105579814..1bbf288ebf38 100644 --- a/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py +++ b/tests/v1/structured_output/test_spec_decode_reasoning_boundary.py @@ -175,10 +175,9 @@ def test_scheduler_validates_and_truncates_post_boundary_spec_tokens(): grammar.accept_tokens.assert_called_once_with(request.request_id, [11]) -def test_scheduler_initializes_prompt_reasoning_state_before_boundary_path(): +def test_scheduler_boundary_path_requires_initialized_reasoning_state(): scheduler, request = prepare_running_request(reasoning_ended=None) reasoner = Mock(spec=ReasoningParser) - reasoner.is_reasoning_end.return_value = True scheduler.structured_output_manager.reasoner = reasoner scheduler.update_from_output( @@ -187,9 +186,7 @@ def test_scheduler_initializes_prompt_reasoning_state_before_boundary_path(): ) grammar = request.structured_output_request.grammar - assert request.structured_output_request.reasoning_ended is True assert list(request.output_token_ids) == [10, 11, 12] - reasoner.is_reasoning_end.assert_called_once_with(request.prompt_token_ids) reasoner.may_have_reasoning_end_in_delta.assert_not_called() grammar.validate_tokens.assert_not_called() - grammar.accept_tokens.assert_called_once_with(request.request_id, [10, 11, 12]) + grammar.accept_tokens.assert_not_called() diff --git a/vllm/v1/core/sched/scheduler.py b/vllm/v1/core/sched/scheduler.py index c7260fba9a51..3251ba504eb8 100644 --- a/vllm/v1/core/sched/scheduler.py +++ b/vllm/v1/core/sched/scheduler.py @@ -1413,42 +1413,32 @@ def update_from_output( and structured_req is not None and self.structured_output_manager.reasoner is not None and not self.structured_output_manager.enable_in_reasoning - and structured_req.reasoning_ended is not True + and structured_req.reasoning_ended is False and self.enable_spec_reasoning_boundary_validation ) advanced_with_reasoning_boundary = False if validate_reasoning_boundary: reasoner = self.structured_output_manager.reasoner - if structured_req.reasoning_ended is None: - structured_req.reasoning_ended = reasoner.is_reasoning_end( - request.prompt_token_ids or [] - ) + may_have_reasoning_end = reasoner.may_have_reasoning_end_in_delta( + new_token_ids + ) - if structured_req.reasoning_ended is False: - may_have_reasoning_end = ( - reasoner.may_have_reasoning_end_in_delta(new_token_ids) + if may_have_reasoning_end: + num_new_token_ids = len(new_token_ids) + new_token_ids = validate_spec_tokens_with_reasoning_boundary( + request, + new_token_ids, + reasoner, ) - - if may_have_reasoning_end: - num_new_token_ids = len(new_token_ids) - new_token_ids = validate_spec_tokens_with_reasoning_boundary( - request, - new_token_ids, - reasoner, - ) - advanced_with_reasoning_boundary = ( - structured_req.reasoning_ended is True - ) - num_rejected_by_grammar = num_new_token_ids - len(new_token_ids) - if num_rejected_by_grammar: - if request.num_computed_tokens > 0: - request.num_computed_tokens -= ( - num_rejected_by_grammar - ) - if request.num_output_placeholders > 0: - request.num_output_placeholders -= ( - num_rejected_by_grammar - ) + advanced_with_reasoning_boundary = ( + structured_req.reasoning_ended is True + ) + num_rejected_by_grammar = num_new_token_ids - len(new_token_ids) + if num_rejected_by_grammar: + if request.num_computed_tokens > 0: + request.num_computed_tokens -= num_rejected_by_grammar + if request.num_output_placeholders > 0: + request.num_output_placeholders -= num_rejected_by_grammar if scheduled_spec_token_ids and generated_token_ids: spec_decoding_stats = self.make_spec_decoding_stats( @@ -1469,7 +1459,11 @@ def update_from_output( request.status = RequestStatus.FINISHED_STOPPED stopped = True - if new_token_ids and self.structured_output_manager.should_advance(request): + if ( + new_token_ids + and not advanced_with_reasoning_boundary + and self.structured_output_manager.should_advance(request) + ): struct_output_request = request.structured_output_request assert struct_output_request is not None assert struct_output_request.grammar is not None From e8d8ac5d149e09a1ae6c060b6c1351ef6e41a93e Mon Sep 17 00:00:00 2001 From: liuyanyi Date: Mon, 27 Apr 2026 04:32:39 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(envs):=20=E7=A7=BB=E5=8A=A8=E6=8E=A8?= =?UTF-8?q?=E7=90=86=E8=BE=B9=E7=95=8C=E9=AA=8C=E8=AF=81=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=88=B0=E5=AD=97=E5=85=B8=E6=9C=AB=E5=B0=BE?= =?UTF-8?q?=E4=BB=A5=E6=8F=90=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: liuyanyi Assisted-by: OpenAI Codex --- vllm/envs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vllm/envs.py b/vllm/envs.py index bb55d6655407..8e9d78dba408 100755 --- a/vllm/envs.py +++ b/vllm/envs.py @@ -1685,12 +1685,6 @@ def _get_or_set_default() -> str: "VLLM_DEBUG_MFU_METRICS": lambda: bool( int(os.getenv("VLLM_DEBUG_MFU_METRICS", "0")) ), - # Enable reasoning-boundary validation inside accepted speculative tokens. - # This is opt-in to avoid unexpected regressions for parsers that are not - # yet adapted to the validation path. - "VLLM_SPEC_REASONING_BOUNDARY_VALIDATION": lambda: bool( - int(os.getenv("VLLM_SPEC_REASONING_BOUNDARY_VALIDATION", "0")) - ), # Disable using pytorch's pin memory for CPU offloading. "VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY": lambda: bool( int(os.getenv("VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY", "0")) @@ -1746,6 +1740,12 @@ def _get_or_set_default() -> str: "VLLM_LORA_ENABLE_DUAL_STREAM": lambda: bool( int(os.getenv("VLLM_LORA_ENABLE_DUAL_STREAM", "0")) ), + # Enable reasoning-boundary validation inside accepted speculative tokens. + # This is opt-in to avoid unexpected regressions for parsers that are not + # yet adapted to the validation path. + "VLLM_SPEC_REASONING_BOUNDARY_VALIDATION": lambda: bool( + int(os.getenv("VLLM_SPEC_REASONING_BOUNDARY_VALIDATION", "0")) + ), }