Skip to content

[Optimization] refactor(chat_handler,completion_handler): extract base classes and use AsyncLLM - #5195

Merged
Jiang-Jia-Jun merged 4 commits into
PaddlePaddle:developfrom
memoryCoderC:asyncLLM
Dec 25, 2025
Merged

Jiang-Jia-Jun merged 4 commits into
PaddlePaddle:developfrom
memoryCoderC:asyncLLM

Conversation

@memoryCoderC

@memoryCoderC memoryCoderC commented Nov 24, 2025

Copy link
Copy Markdown
Collaborator

Motivation

重构api_server。

基于AsyncLLM与Engine进行交互

当使用AsyncLLM时,由EngineService启动work_process,架构变为:
(apiserver、 async_llm) → (EngineService)→(work_process)三个进程

Modifications

重构api_server。

基于AsyncLLM与Engine进行交互

Usage or Command

通过设置env FD_ENABLE_ASYNC_LLM = 1 开启

Accuracy Tests

通过单测,单测覆盖率已达到80%以上

Checklist

  • Add at least a tag in the PR title.
    • Tag list: [[FDConfig],[APIServer],[Engine], [Scheduler], [PD Disaggregation], [Executor], [Graph Optimization], [Speculative Decoding], [RL], [Models], [Quantization], [Loader], [OP], [KVCache], [DataProcessor], [BugFix], [Docs], [CI], [Optimization], [Feature], [Benchmark], [Others], [XPU], [HPU], [GCU], [DCU], [Iluvatar], [Metax]]
    • You can add new tags based on the PR content, but the semantics must be clear.
  • Format your code, run pre-commit before commit.
  • Add unit tests. Please write the reason in this PR if no unit tests.
  • Provide accuracy results.
  • If the current PR is submitting to the release branch, make sure the PR has been submitted to the develop branch, then cherry-pick it to the release branch with the [Cherry-Pick] PR tag.

@paddle-bot

paddle-bot Bot commented Nov 24, 2025

Copy link
Copy Markdown

Thanks for your contribution!

@paddle-bot paddle-bot Bot added the contributor External developers label Nov 24, 2025
@codecov-commenter

codecov-commenter commented Nov 24, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.96252% with 167 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (develop@c1aa66d). Learn more about missing BASE report.

Files with missing lines Patch % Lines
fastdeploy/entrypoints/openai/v1/serving_chat.py 68.65% 43 Missing and 20 partials ⚠️
...deploy/entrypoints/openai/v1/serving_completion.py 77.24% 28 Missing and 15 partials ⚠️
fastdeploy/entrypoints/openai/api_server.py 26.08% 14 Missing and 3 partials ⚠️
fastdeploy/entrypoints/openai/protocol.py 54.05% 10 Missing and 7 partials ⚠️
fastdeploy/entrypoints/openai/serving_engine.py 63.04% 16 Missing and 1 partial ⚠️
fastdeploy/entrypoints/openai/v1/serving_base.py 95.48% 5 Missing and 1 partial ⚠️
fastdeploy/engine/request.py 84.61% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #5195   +/-   ##
==========================================
  Coverage           ?   65.29%           
==========================================
  Files              ?      332           
  Lines              ?    42413           
  Branches           ?     6537           
==========================================
  Hits               ?    27694           
  Misses             ?    12643           
  Partials           ?     2076           
Flag Coverage Δ
GPU 65.29% <74.96%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@memoryCoderC
memoryCoderC force-pushed the asyncLLM branch 2 times, most recently from 5054e85 to 0396b26 Compare December 1, 2025 12:17
@memoryCoderC
memoryCoderC force-pushed the asyncLLM branch 24 times, most recently from 61252cb to 8bf1998 Compare December 10, 2025 03:05
@memoryCoderC
memoryCoderC force-pushed the asyncLLM branch 2 times, most recently from d31e772 to 3efcb2c Compare December 11, 2025 05:37
@CLAassistant

CLAassistant commented Dec 16, 2025

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@memoryCoderC
memoryCoderC force-pushed the asyncLLM branch 7 times, most recently from 3ec23b5 to abf0acf Compare December 16, 2025 13:55
sunlei1024
sunlei1024 previously approved these changes Dec 18, 2025
) -> AsyncGenerator:
pass

async def handleNonStream(self, ctx: ServeContext[ChatCompletionRequest | CompletionRequest]) -> Any:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

方法命名都是下划线格式

else:
return await self.handleNonStream(ctx)

async def handleStream(self, ctx: ServeContext) -> Union[AsyncGenerator, ErrorResponse]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

方法命名使用下划线格式

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the API server to use AsyncLLM for engine interaction, introducing a new v1 module with base classes for chat and completion handlers. The architecture becomes: (apiserver, async_llm) → (EngineService) → (work_process) when AsyncLLM is enabled via FD_ENABLE_ASYNC_LLM=1 environment variable.

Key Changes:

  • Introduces new v1 serving classes (OpenAiServingBase, OpenAIServingChat, OpenAIServingCompletion) that work with AsyncLLM
  • Refactors serving_engine.py to remove engine_client dependency from base class and add semaphore management
  • Adds accumulate method to RequestOutput for handling multi-part responses
  • Implements add methods in UsageInfo classes for accumulating token usage statistics
  • Adds comprehensive unit tests achieving 80%+ coverage

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/input/test_ernie4_5_processor.py Fixes test assertions to use delta_text and reasoning_content correctly
tests/entrypoints/openai/v1/test_serving_completion_v1.py New comprehensive test suite for OpenAIServingCompletion with AsyncLLM
tests/entrypoints/openai/v1/test_serving_chat_v1.py New test suite for OpenAIServingChat covering streaming and non-streaming cases
tests/entrypoints/openai/v1/test_serving_base_v1.py Tests for base class functionality including request handling and accumulation
tests/engine/test_request_output.py Tests for RequestOutput initialization, accumulation, and serialization
fastdeploy/input/ernie4_5_processor.py Adds reasoning_content and text field population for streaming responses
fastdeploy/envs.py Adds FD_ENABLE_ASYNC_LLM environment variable to toggle AsyncLLM usage
fastdeploy/entrypoints/openai/v1/serving_completion.py New completion handler implementation using AsyncLLM
fastdeploy/entrypoints/openai/v1/serving_chat.py New chat handler implementation with AsyncLLM support
fastdeploy/entrypoints/openai/v1/serving_base.py Base class providing common functionality for streaming and non-streaming requests
fastdeploy/entrypoints/openai/serving_reward.py Updates _build_response signature to accept request_output parameter
fastdeploy/entrypoints/openai/serving_engine.py Refactors to separate engine_client from base class, adds semaphore management
fastdeploy/entrypoints/openai/serving_embedding.py Updates _build_response signature for consistency
fastdeploy/entrypoints/openai/protocol.py Adds add methods to usage info classes and recover_stop finish reason
fastdeploy/entrypoints/openai/api_server.py Integrates AsyncLLM-based handlers conditionally based on environment variable
fastdeploy/engine/request.py Adds accumulate method and accumulate_tool_calls field to RequestOutput
fastdeploy/engine/async_llm.py Increases engine ready wait time from 180s to 500s

Comment thread fastdeploy/entrypoints/openai/api_server.py
def _wait_engine_ready(self) -> bool:
"""Wait for engine and workers to be fully ready"""
max_wait_time = 180 # seconds
max_wait_time = 500 # seconds

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increasing the max_wait_time from 180 seconds to 500 seconds (nearly 3x) is a significant change that appears unrelated to the main refactoring. This change:

  1. Could hide underlying initialization issues that should be fixed instead
  2. Lacks justification in the PR description
  3. May indicate a performance regression

If the engine initialization truly requires this much time, there should be detailed logging to explain why, or the initialization process should be optimized. Consider reverting this change or providing clear justification.

Copilot uses AI. Check for mistakes.
raise ValueError(f"Error processing response: {str(e)}")
finally:
await self.engine_client.connection_manager.cleanup_request(request_id)
self.engine_client.connection_manager.cleanup_request(request_id)

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of 'await' before 'self.engine_client.connection_manager.cleanup_request(request_id)' is incorrect. The cleanup_request method is defined as async (see fastdeploy/entrypoints/openai/utils.py line 190). Removing await means:

  1. The cleanup coroutine will not be executed
  2. The request won't be properly cleaned up from request_map and request_num
  3. This could lead to memory leaks as requests accumulate

This must be kept as 'await self.engine_client.connection_manager.cleanup_request(request_id)'.

Suggested change
self.engine_client.connection_manager.cleanup_request(request_id)
await self.engine_client.connection_manager.cleanup_request(request_id)

Copilot uses AI. Check for mistakes.
Comment on lines +383 to +390
response_dict["outputs"]["reasoning_content"] = reasoning_content
response_dict["outputs"]["text"] = (
reasoning_delta_message.content or ""
if reasoning_delta_message and hasattr(reasoning_delta_message, "content")
else ""
)
else:
response_dict["outputs"]["text"] = delta_text

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for setting response_dict["outputs"]["text"] has been moved inside the reasoning_content conditional block, but the else clause at line 389-390 only handles the non-reasoning case. However, the original code at line 407 set text unconditionally after all processing. This change means that when reasoning_content is present, text is set from reasoning_delta_message.content, which may not always match delta_text. Please verify this is the intended behavior and that all scenarios are covered:

  1. When reasoning_content exists and has content attribute
  2. When reasoning_content exists but doesn't have content attribute
  3. When reasoning_content doesn't exist (handled by else)

Consider whether the text assignment should remain after line 406 to handle edge cases.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +87
def add(self, other: CompletionTokenUsageInfo):
if self.reasoning_tokens and other.reasoning_tokens:
self.reasoning_tokens += other.reasoning_tokens
elif other.reasoning_tokens:
self.reasoning_tokens = other.reasoning_tokens
if self.image_tokens and other.image_tokens:
self.image_tokens += other.image_tokens
elif other.image_tokens:
self.image_tokens = other.image_tokens

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add method in CompletionTokenUsageInfo has inconsistent logic. When self.reasoning_tokens is None and other.reasoning_tokens exists, it correctly sets self.reasoning_tokens. However, when self.reasoning_tokens is 0 (which is falsy) and other.reasoning_tokens is non-zero, the addition won't happen because 'if self.reasoning_tokens' evaluates to False.

This should be:

if self.reasoning_tokens is not None and other.reasoning_tokens is not None:
    self.reasoning_tokens += other.reasoning_tokens
elif other.reasoning_tokens is not None:
    self.reasoning_tokens = other.reasoning_tokens

The same issue exists for image_tokens and applies to PromptTokenUsageInfo.add as well (lines 99-113).

Copilot uses AI. Check for mistakes.
def _check_master(self) -> bool:
"""Check if current node is master"""
return self.engine_client.is_master
return True

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _check_master method now always returns True, which changes the behavior for non-master nodes. In a distributed setup where multiple nodes are running, this could cause all nodes to think they are master. The previous implementation checked 'self.engine_client.is_master', which was more accurate.

Note that ZmqOpenAIServing overrides this at line 323 to preserve the original behavior, but the base class default should probably not be True for all cases. Consider if this is the intended behavior or if it should throw NotImplementedError to force subclasses to implement it.

Copilot uses AI. Check for mistakes.
else:
engine = LLMEngine.from_engine_args(engine_args)
started = False
if inspect.iscoroutinefunction(engine.start):

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using inspect.iscoroutinefunction to determine if engine.start needs to be awaited is fragile. This approach:

  1. Makes the code harder to understand and maintain
  2. Couples the API server to the implementation details of different engine types
  3. Could fail silently if a third engine type is added later

Consider using a more explicit pattern such as:

  • Define a common interface/protocol for engines
  • Use explicit type checking (isinstance) instead of runtime inspection
  • Have engines implement a standard async start method

This would make the code more maintainable and less prone to errors.

Suggested change
if inspect.iscoroutinefunction(engine.start):
if isinstance(engine, AsyncLLM):

Copilot uses AI. Check for mistakes.

MAX_CONCURRENT_CONNECTIONS = (args.max_concurrency + args.workers - 1) // args.workers
connection_semaphore = StatefulSemaphore(MAX_CONCURRENT_CONNECTIONS)

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new environment variable FD_ENABLE_ASYNC_LLM (line 98) should be documented. Add a comment explaining:

  • What it enables/disables
  • When to use it (e.g., "Set to 1 to use AsyncLLM-based architecture instead of traditional LLMEngine")
  • Any performance or compatibility implications
  • Default value (currently 0)

This helps future developers and operators understand the purpose of this flag.

Suggested change
# FD_ENABLE_ASYNC_LLM:
# - Set to "1" to enable the AsyncLLM-based engine architecture instead of the
# traditional LLMEngine used by default.
# - When enabled, `AsyncLLM` is used to start the engine, which may provide
# better concurrency characteristics but can have different performance
# and compatibility behavior compared to `LLMEngine`.
# - Set to "0" or leave unset to keep using the traditional `LLMEngine`.
# - Default: "0" (AsyncLLM disabled) if the environment variable is not set.

Copilot uses AI. Check for mistakes.
Comment thread fastdeploy/engine/request.py Outdated
Comment on lines +658 to +660
if next_output.prompt:
self.prompt = next_output.prompt
if next_output.prompt_token_ids:

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition 'if next_output.prompt:' on line 658 will skip updating self.prompt when next_output.prompt is an empty string, which is a valid value. This should check 'if next_output.prompt is not None:' instead. The same issue exists for prompt_token_ids on line 660.

Suggested change
if next_output.prompt:
self.prompt = next_output.prompt
if next_output.prompt_token_ids:
if next_output.prompt is not None:
self.prompt = next_output.prompt
if next_output.prompt_token_ids is not None:

Copilot uses AI. Check for mistakes.

send_idx = output.send_idx

max_streaming_response_tokens = max(1, max_streaming_response_tokens)

Copilot AI Dec 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable max_streaming_response_tokens is not used.

Copilot uses AI. Check for mistakes.
MAX_CONCURRENT_CONNECTIONS = (args.max_concurrency + args.workers - 1) // args.workers
connection_semaphore = StatefulSemaphore(MAX_CONCURRENT_CONNECTIONS)

enable_async_llm = environment_variables.get("FD_ENABLE_ASYNC_LLM")()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里按其它地方调用就直接是env.xxx即可,可以统一下调用方式

@Jiang-Jia-Jun
Jiang-Jia-Jun merged commit be3be49 into PaddlePaddle:develop Dec 25, 2025
14 of 18 checks passed
ckl117 pushed a commit to fxyfxy777/FastDeploy that referenced this pull request Dec 29, 2025
…e classes and use AsyncLLM (PaddlePaddle#5195)

* [Optimization] refactor(chat_handler,completion_handler): extract base classes and use AsyncLLM

* [Optimization] refactor(chat_handler,completion_handler): rename class
chang-wenbin pushed a commit to chang-wenbin/FastDeploy that referenced this pull request Mar 2, 2026
…e classes and use AsyncLLM (PaddlePaddle#5195)

* [Optimization] refactor(chat_handler,completion_handler): extract base classes and use AsyncLLM

* [Optimization] refactor(chat_handler,completion_handler): rename class
xiaoguoguo626807 pushed a commit to xiaoguoguo626807/FastDeploy that referenced this pull request May 7, 2026
…e classes and use AsyncLLM (PaddlePaddle#5195)

* [Optimization] refactor(chat_handler,completion_handler): extract base classes and use AsyncLLM

* [Optimization] refactor(chat_handler,completion_handler): rename class
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor External developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants