feat(chat-completions): server-side fan-out for n>1 choices - #4841
Open
lvhan028 wants to merge 3 commits into
Open
feat(chat-completions): server-side fan-out for n>1 choices#4841lvhan028 wants to merge 3 commits into
lvhan028 wants to merge 3 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends the OpenAI-compatible /v1/chat/completions endpoint to support n > 1 by performing server-side fan-out (N independent engine.generate() calls aggregated into one response), and also includes the chat-completions “package migration” refactor (protocol model relocation + top-level re-exports for backward compatibility).
Changes:
- Add server-side fan-out for
n > 1in chat completions, including concurrent collection and streaming interleaving with aggregated usage. - Introduce request validation specific to chat completions, including a cap for
nand non-negativeseedvalidation. - Add unit/integration tests covering
n > 1fan-out behavior plus migration/package invariants.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
lmdeploy/serve/openai/endpoints/chat_completions/serving.py |
New packaged handler implementation, including fan-out collect + stream logic and session cleanup behavior. |
lmdeploy/serve/openai/endpoints/chat_completions/validation.py |
New endpoint-specific request validation (fan-out n cap, seed validation, etc.). |
lmdeploy/serve/openai/endpoints/chat_completions/protocol.py |
New home for chat-specific Pydantic models (moved out of top-level protocol). |
lmdeploy/serve/openai/endpoints/chat_completions/logprobs.py |
New helper module for building chat logprobs structures. |
lmdeploy/serve/openai/endpoints/chat_completions/logits_processors.py |
New helper module for logit-bias processor construction. |
lmdeploy/serve/openai/endpoints/chat_completions/__init__.py |
Lazily exposes register to avoid circular imports during protocol re-export. |
lmdeploy/serve/openai/protocol.py |
Removes inlined chat models and re-exports them from the new chat_completions protocol module. |
lmdeploy/serve/openai/endpoints/chat_completions.py |
Deletes the old flat chat_completions module in favor of the package layout. |
lmdeploy/serve/openai/endpoints/__init__.py |
Lazily exposes create_openai_router to avoid circular imports with protocol re-exports. |
tests/test_lmdeploy/serve/openai/chat_completions/test_n_completions.py |
Adds fan-out aggregation + end-to-end handler tests for n > 1 (streaming and non-streaming). |
tests/test_lmdeploy/serve/openai/chat_completions/conftest.py |
Shared fake engine/session/context and endpoint fixture for chat handler tests. |
tests/test_chat_completions_package_migration.py |
Migration equivalence tests ensuring package structure + re-export invariants. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+700
to
+702
| response['remote_token_ids'] = [ | ||
| last.remote_token_ids[-1] | ||
| ] if last.remote_token_ids else [] |
Comment on lines
+41
to
+48
| # check sampling settings | ||
| if request.n <= 0: | ||
| return f'The n {request.n!r} must be a positive int.' | ||
| # n > 1 is implemented as server-side fan-out (N independent engine | ||
| # generate() calls). Cap it to prevent unbounded resource use. | ||
| if request.n > _MAX_FANOUT_N: | ||
| return (f'The n {request.n!r} exceeds the maximum supported ' | ||
| f'choices ({_MAX_FANOUT_N}).') |
Comment on lines
+350
to
+352
| - **n** (int): How many chat completion choices to generate for each input | ||
| message. **Only support one here**. | ||
| - **stream**: whether to stream the results or not. Default to false. |
Comment on lines
+74
to
+79
| the fan-out is therefore engine-agnostic and works for both pytorch and | ||
| turbomind. If any generator raises, the whole request fails (OpenAI-style: | ||
| a single n>1 request is all-or-nothing). ``completion_tokens`` is the sum | ||
| across choices; ``prompt_tokens`` is counted once (taken from | ||
| ``prompt_tokens`` if provided, else from the first choice's | ||
| ``input_token_len`` since all choices share the same prompt). |
lvhan028
force-pushed
the
feat/chat-n-completions
branch
2 times, most recently
from
August 10, 2026 02:02
5823cbf to
83f9d82
Compare
Aligns with the responses/ package layout. Splits the 633-line chat_completions.py into protocol/validation/logprobs/logits_processors/ serving modules. Chat-specific models move to endpoints/chat_completions/protocol.py; shared models stay in the top-level protocol.py with backward-compat re-exports. No behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
Fans a single n>1 request into N independent engine.generate() calls with distinct random_seeds, collating into N choices. Works for both pytorch and turbomind (engine-agnostic handler-layer approach). n==1 keeps the original single-generator fast path. Co-Authored-By: Claude <noreply@anthropic.com>
…ancellation Fix round 1 (code-review findings): - Non-streaming fan-out now wraps _fanout_nonstream in try/finally calling cleanup_result_generators so N fan-out sessions are removed on every exit path (success, parse-error, disconnect, generator-error). Previously leaked N sessions per non-streaming n>1 request. - Fan-out sub-sessions are auto-generated (create_session(None)) instead of reusing request.session_id N times, which collided in SessionManager.map_user_session_id on the 2nd call for explicit session_ids. - _fanout_generate_collect now runs _consume as explicit Tasks and cancels pending siblings on first exception (asyncio.gather does not cancel siblings by default), then awaits cancellations so engine generators close. - _consume wraps the generator in aclosing() for prompt closure on cancel. - Non-streaming fan-out now propagates with_cache cache_block_ids / remote_token_ids response fields (mirrors n==1 path). Tests: added explicit-session-id, sibling-cancellation, multi-chunk stream, and session-cleanup assertions. All 12 n_completions tests pass; 81 serve tests green. Co-Authored-By: Claude <noreply@anthropic.com>
lvhan028
force-pushed
the
feat/chat-n-completions
branch
from
August 10, 2026 02:40
83f9d82 to
ae43920
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Server-side fan-out for
n > 1in/v1/chat/completions: when a client requests multiple choices, the server issuesnindependentgenerate()calls (eachn=1) and aggregates them into one response, instead of relying on the engine's nativen. Derived per-choice seeds (seed + i), aggregatedusageacross choices, and error isolation (one choice failing does not abort the others).Task
Task 2 of the chat-completions feature plan.
Files
lmdeploy/serve/openai/endpoints/chat_completions/serving.py—_FanoutResultdataclass,_ClientDisconnected,_fanout_generate_collect(asyncio tasks + cancel pending siblings +aclosing),_fanout_generate_stream(asyncio.Queue interleave); handlerif request.n and request.n > 1:branch.lmdeploy/serve/openai/endpoints/chat_completions/validation.py—_MAX_FANOUT_N=128cap + non-negative seed validation.tests/test_lmdeploy/serve/openai/chat_completions/test_n_completions.py(+ conftest) — 12 tests.Tests
pytest tests/test_lmdeploy/serve/openai/chat_completions/test_n_completions.py -v→ 12 passed.Dependency
Depends on #4840 (
refactor/chat-completions-package). This branch is built on top of #4840's HEAD; merge #4840 first, then rebase this PR ontomainso the diff shrinks to only the Task 2 commits.Notes
gen_config.response_format(fromstructured_outputs/tools.strict/tool_choice='required') is set before the fan-out;deepcopy(gen_config)carries the constraint to each independent choice.--no-verifylocally (env lacks python3.10 for the docformatter pre-commit hook); CI runs the hook with the correct interpreter.🤖 Generated with Claude Code