@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 6abb0454ad
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 8, 21:30 UTC

[Perf] Optimize the performance of structured output + reasoning (#33557)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>

Chauncey committed Feb 5, 2026 at 15:45 UTC 6abb0454adb531de0b081bbf65ccf907e4bd560d
4 files changed +51 -61
vllm/entrypoints/openai/chat_completion/serving.py
+41 -60
@@ -72,6 +72,7 @@ from vllm.logger import init_logger
72 from vllm.logprobs import Logprob
73 from vllm.outputs import CompletionOutput, RequestOutput
74 from vllm.parser import ParserManager
75 +from vllm.reasoning import ReasoningParser
76 from vllm.sampling_params import BeamSearchParams, SamplingParams
77 from vllm.tokenizers import TokenizerLike
78 from vllm.tokenizers.mistral import (
@@ -132,7 +133,7 @@ class OpenAIServingChat(OpenAIServing):
133 self.logits_processors = self.model_config.logits_processors
134
135 # set up reasoning parser
135 - self.reasoning_parser = ParserManager.get_reasoning_parser(
136 + self.reasoning_parser_cls = ParserManager.get_reasoning_parser(
137 reasoning_parser_name=reasoning_parser
138 )
139 # set up tool use
@@ -330,6 +331,24 @@ class OpenAIServingChat(OpenAIServing):
331 for the API specification. This API mimics the OpenAI
332 Chat Completion API.
333 """
334 + # Streaming response
335 + tokenizer = self.renderer.tokenizer
336 + assert tokenizer is not None
337 + reasoning_parser: ReasoningParser | None = None
338 + try:
339 + if self.reasoning_parser_cls:
340 + # Pass the same chat template kwargs as used in tokenization
341 + chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
342 + request.chat_template_kwargs,
343 + self.default_chat_template_kwargs,
344 + )
345 + reasoning_parser = self.reasoning_parser_cls(
346 + tokenizer,
347 + chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg]
348 + )
349 + except RuntimeError as e:
350 + logger.exception("Error in reasoning parser creation.")
351 + return self.create_error_response(str(e))
352 result = await self.render_chat_request(request)
353 if isinstance(result, ErrorResponse):
354 return result
@@ -427,7 +446,12 @@ class OpenAIServingChat(OpenAIServing):
446 priority=request.priority,
447 data_parallel_rank=data_parallel_rank,
448 )
430 -
449 + reasoning_ended = None
450 + if reasoning_parser:
451 + reasoning_ended = reasoning_parser.is_reasoning_end(
452 + engine_request.prompt_token_ids or [] # type: ignore[attr-defined]
453 + )
454 + engine_request.reasoning_ended = reasoning_ended
455 generator = self.engine_client.generate(
456 engine_request,
457 sampling_params,
@@ -447,10 +471,6 @@ class OpenAIServingChat(OpenAIServing):
471 assert len(generators) == 1
472 (result_generator,) = generators
473
450 - # Streaming response
451 - tokenizer = self.renderer.tokenizer
452 - assert tokenizer is not None
453 -
474 if request.stream:
475 return self.chat_completion_stream_generator(
476 request,
@@ -460,6 +480,7 @@ class OpenAIServingChat(OpenAIServing):
480 conversation,
481 tokenizer,
482 request_metadata,
483 + reasoning_parser,
484 )
485
486 try:
@@ -471,6 +492,7 @@ class OpenAIServingChat(OpenAIServing):
492 conversation,
493 tokenizer,
494 request_metadata,
495 + reasoning_parser,
496 )
497 except GenerationError as e:
498 return self._convert_generation_error_to_response(e)
@@ -630,6 +652,7 @@ class OpenAIServingChat(OpenAIServing):
652 conversation: list[ConversationMessage],
653 tokenizer: TokenizerLike,
654 request_metadata: RequestResponseMetadata,
655 + reasoning_parser: ReasoningParser | None = None,
656 ) -> AsyncGenerator[str, None]:
657 from vllm.tokenizers.mistral import MistralTokenizer
658
@@ -673,7 +696,7 @@ class OpenAIServingChat(OpenAIServing):
696
697 # Only one of these will be used, thus previous_texts and
698 # all_previous_token_ids will not be used twice in the same iteration.
676 - if tool_choice_auto or self.reasoning_parser:
699 + if tool_choice_auto or reasoning_parser:
700 # These are only required in "auto" tool choice case
701 all_previous_token_ids = [[]] * num_choices
702 # For reasoning parser and tool call all enabled
@@ -683,28 +706,6 @@ class OpenAIServingChat(OpenAIServing):
706 else:
707 all_previous_token_ids = None
708
686 - try:
687 - if self.reasoning_parser:
688 - if tokenizer is None:
689 - raise ValueError(
690 - "Tokenizer not available when `skip_tokenizer_init=True`"
691 - )
692 -
693 - # Pass the same chat template kwargs as used in tokenization
694 - chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
695 - request.chat_template_kwargs,
696 - self.default_chat_template_kwargs,
697 - )
698 - reasoning_parser = self.reasoning_parser(
699 - tokenizer,
700 - chat_template_kwargs=chat_template_kwargs or {}, # type: ignore[call-arg]
701 - )
702 - except RuntimeError as e:
703 - logger.exception("Error in reasoning parser creation.")
704 - data = self.create_streaming_error_response(str(e))
705 - yield f"data: {data}\n\n"
706 - yield "data: [DONE]\n\n"
707 - return
709 # Prepare the tool parser if it's needed
710 try:
711 if tool_choice_auto and self.tool_parser:
@@ -826,7 +827,7 @@ class OpenAIServingChat(OpenAIServing):
827 tool_parser = tool_parsers[i]
828
829 if (
829 - self.reasoning_parser
830 + reasoning_parser
831 and res.prompt_token_ids
832 and prompt_is_reasoning_end_arr[i] is None
833 ):
@@ -888,7 +889,7 @@ class OpenAIServingChat(OpenAIServing):
889 delta_message: DeltaMessage | None
890
891 # just update previous_texts and previous_token_ids
891 - if tool_choice_auto or self.reasoning_parser:
892 + if tool_choice_auto or reasoning_parser:
893 assert previous_texts is not None
894 assert all_previous_token_ids is not None
895 previous_text = previous_texts[i]
@@ -915,7 +916,7 @@ class OpenAIServingChat(OpenAIServing):
916 # handle streaming deltas for tools with named tool_choice
917 elif tool_choice_function_name:
918 if (
918 - self.reasoning_parser
919 + reasoning_parser
920 and not reasoning_end_arr[i]
921 and not reasoning_parser.is_reasoning_end(
922 previous_token_ids
@@ -952,7 +953,7 @@ class OpenAIServingChat(OpenAIServing):
953 current_text = ""
954 else:
955 # Just to add remaining `content`
955 - if self.reasoning_parser:
956 + if reasoning_parser:
957 delta_text = previous_text + delta_text
958 current_text = ""
959
@@ -998,13 +999,13 @@ class OpenAIServingChat(OpenAIServing):
999 output_token_ids = as_list(output.token_ids)
1000
1001 if (
1001 - self.reasoning_parser is not None
1002 + reasoning_parser is not None
1003 and not reasoning_end_arr[i]
1004 and prompt_is_reasoning_end_arr[i]
1005 ):
1006 reasoning_end_arr[i] = True
1007
1007 - if self.reasoning_parser and not reasoning_end_arr[i]:
1008 + if reasoning_parser and not reasoning_end_arr[i]:
1009 delta_message = (
1010 reasoning_parser.extract_reasoning_streaming(
1011 previous_text,
@@ -1047,9 +1048,8 @@ class OpenAIServingChat(OpenAIServing):
1048
1049 # handle streaming deltas for tools with "auto" tool choice
1050 # and reasoning parser
1050 - elif tool_choice_auto and self.reasoning_parser:
1051 + elif tool_choice_auto and reasoning_parser:
1052 assert tool_parser is not None
1052 - assert reasoning_parser is not None
1053 assert added_content_delta_arr is not None
1054 assert reasoning_end_arr is not None
1055 output_token_ids = as_list(output.token_ids)
@@ -1130,7 +1130,7 @@ class OpenAIServingChat(OpenAIServing):
1130 tools_streamed[i] = True
1131
1132 # when only reasoning
1133 - elif self.reasoning_parser:
1133 + elif reasoning_parser:
1134 delta_message = reasoning_parser.extract_reasoning_streaming(
1135 previous_text,
1136 current_text,
@@ -1144,9 +1144,7 @@ class OpenAIServingChat(OpenAIServing):
1144 delta_message = DeltaMessage(content=delta_text)
1145
1146 # update the previous values for the next iteration
1147 - if (
1148 - tool_choice_auto or self.reasoning_parser
1149 - ) and not self.use_harmony:
1147 + if (tool_choice_auto or reasoning_parser) and not self.use_harmony:
1148 assert previous_texts is not None
1149 assert all_previous_token_ids is not None
1150 previous_texts[i] = current_text
@@ -1400,6 +1398,7 @@ class OpenAIServingChat(OpenAIServing):
1398 conversation: list[ConversationMessage],
1399 tokenizer: TokenizerLike,
1400 request_metadata: RequestResponseMetadata,
1401 + reasoning_parser: ReasoningParser | None = None,
1402 ) -> ErrorResponse | ChatCompletionResponse:
1403 from vllm.tokenizers.mistral import MistralTokenizer
1404
@@ -1494,25 +1493,7 @@ class OpenAIServingChat(OpenAIServing):
1493 choices.append(choice_data)
1494 continue
1495
1497 - if self.reasoning_parser:
1498 - try:
1499 - if tokenizer is None:
1500 - raise ValueError(
1501 - "Tokenizer not available when `skip_tokenizer_init=True`"
1502 - )
1503 -
1504 - # Pass the same chat template kwargs as used in tokenization
1505 - chat_template_kwargs = self._prepare_extra_chat_template_kwargs(
1506 - request.chat_template_kwargs,
1507 - self.default_chat_template_kwargs,
1508 - )
1509 - reasoning_parser = self.reasoning_parser(
1510 - tokenizer,
1511 - chat_template_kwargs=chat_template_kwargs, # type: ignore[call-arg]
1512 - )
1513 - except RuntimeError as e:
1514 - logger.exception("Error in reasoning parser creation.")
1515 - return self.create_error_response(str(e))
1496 + if reasoning_parser:
1497 # If the reasoning parser is enabled,
1498 # tool calls are extracted exclusively from the content.
1499 reasoning, content = reasoning_parser.extract_reasoning(
vllm/v1/engine/__init__.py
+2
@@ -83,6 +83,8 @@ class EngineCoreRequest(
83 # Used in outputs and to support abort(req_id, internal=False).
84 external_req_id: str | None = None
85
86 + reasoning_ended: bool | None = None
87 +
88 @property
89 def params(self) -> SamplingParams | PoolingParams:
90 """Return the processed params (sampling or pooling)."""
vllm/v1/request.py
+4
@@ -74,6 +74,7 @@ class Request:
74 trace_headers: Mapping[str, str] | None = None,
75 block_hasher: Callable[["Request"], list["BlockHash"]] | None = None,
76 resumable: bool = False,
77 + reasoning_ended: bool | None = None,
78 ) -> None:
79 self.request_id = request_id
80 self.client_index = client_index
@@ -86,6 +87,8 @@ class Request:
87 self.structured_output_request = StructuredOutputRequest.from_sampling_params(
88 sampling_params
89 )
90 + if self.structured_output_request is not None:
91 + self.structured_output_request.reasoning_ended = reasoning_ended
92 self.arrival_time = arrival_time if arrival_time is not None else time.time()
93
94 self.status = RequestStatus.WAITING
@@ -195,6 +198,7 @@ class Request:
198 trace_headers=request.trace_headers,
199 block_hasher=block_hasher,
200 resumable=request.resumable,
201 + reasoning_ended=request.reasoning_ended,
202 )
203
204 def append_output_token_ids(
vllm/v1/structured_output/__init__.py
+4 -1
@@ -284,12 +284,15 @@ class StructuredOutputManager:
284 # NOTE (Hanchen) if enable_in_reasoning is True, it means that
285 # the model needs to be constrained in reasoning. So we should always
286 # enable the bitmask filling.
287 -
287 if self.reasoner is not None:
288 if self.enable_in_reasoning:
289 return True
290 assert request.structured_output_request is not None
291 if request.structured_output_request.reasoning_ended is None:
292 + # This should be removed here, but since `openai_gptoss`
293 + # is an independent code path, it is kept for now.
294 + # After unifying the `openai_gptoss` and non-`openai_gptoss` styles,
295 + # it can be removed.
296 request.structured_output_request.reasoning_ended = (
297 self.reasoner.is_reasoning_end(request.prompt_token_ids or [])
298 )