Repository navigation
[Rust Frontend] Support n > 1 choices - #65625
Merged
Merged
Conversation
Chat and completions rejected `n > 1` with "Only n=1 is supported.".
Support it like Python's parallel sampling: render and tokenize the
prompt once, then submit one engine request per sample from `vllm-llm`
(child IDs `{index}_{request_id}`, `seed + index` when seeded, the
caller's external ID kept so an abort reaches every sample), observe
the request-level `n` and max-generation-tokens histograms once per
request, parse each sample with its own output processor, and merge
the per-choice responses in the server: choices carry their index,
usage counts the prompt once and sums completion and reasoning tokens,
and per-request metrics are reported only for `n = 1`.
`n` is checked like Python's `SamplingParams`: at least 1, at most
`VLLM_MAX_N_SEQUENCES` (16384), and 1 under greedy sampling. The render
routes forward `n` into the generate request, whose route still accepts
only `n = 1`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bind `VLLM_MAX_N_SEQUENCES` to a hidden `--max-n-sequences` arg (and the `--args-json` env fallbacks), parsed like Python's `int()`, instead of reading the env var on every request. The limit travels in `ApiServerOptions` and `RenderConfig`, and the chat, completions and render handlers check `n` against it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
njhill
approved these changes
Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCAeRMMqPf8kY3N54sP Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Member
Author
|
/ci run |
|
✅ Triggered Buildkite CI #100165 for commit ℹ️ This PR is 11 commits behind upstream |
6 tasks done
richard-for-bugen Bot
added a commit
to BugenZhao/vllm
that referenced
this pull request
Nov 5, 2026
…he Rust frontend vllm-project#65625 added n > 1 to the Rust frontend, so drop the test_parallel exclusion from test_completion.py. test_parallel_streaming counts chunks per sample as tokens; the Rust frontend may end a sample with a chunk that carries only the finish_reason, as test_completion_streaming already allows, so that chunk is not counted. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
richard-for-bugen Bot
added a commit
to BugenZhao/vllm
that referenced
this pull request
Nov 5, 2026
…ust frontend vllm-project#65625 (n > 1) fixed the last Rust gaps in test_chat.py, so run it in a new 35 GB step (zephyr-7b-beta, as in the Python chat_completion step), with three relaxations for differences both frontends accept: - the last streamed chunk may carry only the finish_reason, so test_chat_streaming no longer requires content on it (the joined text is still compared with the non-streaming output), and the continuous-usage check lets that chunk repeat the completion-token count, like the usage-only chunk; - the Rust frontend treats an all-null stream_options like an omitted one without streaming (decided on #61), so that one case is skipped under it; - test_long_seed also accepts the Rust deserialization message for an out-of-range seed (the 400 is still asserted). test_invocations is deselected: it calls the SageMaker /invocations route, which the Rust frontend does not serve. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
6 tasks done
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.
Overview
Support
n > 1on/v1/chat/completionsand/v1/completionsin the Rust frontend, following Python's parallel sampling: render and tokenize the prompt once, then submit one engine request per sample. Today the Rust frontend rejects anyn > 1with400 Only n=1 is supported.Supersedes inactive #58914, which fans out by calling
chat()/generate()ntimes in the server and so renders the prompt (multimodal processing included)ntimes.Claims
n > 1, streaming and non-streaming. Each choice carries itsindex; usage counts the prompt once and sums completion and reasoning tokens across choices.ParentRequest, the prompt is rendered once, child requests use IDs{index}_{request_id}andseed + indexwhen seeded, and the caller's external request ID stays on every child, so an abort or a client disconnect reaches every sample.nis validated like Python'sSamplingParams: at least 1, at mostVLLM_MAX_N_SEQUENCES(16384 by default; the chat DTO's old cap of 10 is gone), and 1 under greedy sampling, with Python's error messages.vllm:request_params_nandvllm:request_max_num_generation_tokensare observed once per request, the other request histograms per sample, and per-request metrics in the response only forn = 1.n = 1takes the same path and is unchanged on the wire./inference/v1/generateand gRPC still accept onlyn = 1; the render routes forwardninto the rendered generate request, son > 1keeps failing there explicitly instead of being dropped.Validation
Commands from
.buildkite/scripts/run-rust-frontend-cargo-ci.sh(style-clippyandtest), run from the repo root on this branch:The first commit also passes clippy and nextest on its own (2549 passed, 13 skipped).
New route tests against the mock engine (
rust/src/server/src/routes/tests.rs):chat_completions_n_returns_one_choice_per_seeded_sample: child IDs and seeds7/8under one external ID; choiceshi/stopandyo/lengthat indices 0/1; usage 22 + 5; the request-level histograms observed once (n = 2, max 3), the per-sample generation histogram twice.chat_completions_stream_n_indexes_chunks_and_sends_one_usage_chunkandcompletions_stream_n_indexes_chunks_and_sends_one_usage_chunk.completions_n_returns_one_choice_per_sample, withseed: -1: both children stay unseeded.n_is_checked_like_python_sampling_params: Python's messages forn = 0,n > 16384, and greedy sampling.n_limit_follows_max_n_sequences_option: the configured limit on chat, completions and both render routes; CLI tests cover the default, the env var,--args-jsonand an invalid value.render_forwards_n_to_the_generate_request_check: the render routes keepnand the generate route still rejectsn > 1.Red-green: on
main, every new test except the render one fails (n > 1is rejected); the render test passes onmaintoo and pins that removing the old check keeps the explicit error. With targeted reverts: droppingseed + indexfails the seeded test, offsetting-1fails the completions test, yielding every per-choice usage chunk fails both stream tests, dropping the greedy check fails the validation test, a< 1e-5greedy threshold fails the tiny-temperature cases, observing the request-level histograms per sample fails the metrics assertions, and droppingnin render fails the render test.Not run end to end on GPU in this PR.
test_completion.py::test_parallel_*and thencases intest_chat.pyremain deselected in the Rust frontend CI step;test_parallel_streamingasserts at mostmax_tokenschunks per choice, while Rust sends the finish reason in a separate final chunk, so a length-limited choice gets one extra chunk.Details
The fan-out happens where Python does it, and each layer above returns one stream per sample instead of pushing a choice index through every stream type:
vllm-llm:generate_n(request, n)is the required generation method of theLlmtrait, andgeneratea provided method callinggenerate_n(.., 1), since hownis carried out (child IDs, routing, request-level metrics) belongs to the implementation.EngineCoreLlm::generate_nsubmits thenchild engine requests likeParentRequest. Seeds wrap on overflow, and-1, Python's unseeded sentinel, stays-1on every child (Python normalizes it toNonebefore the fan-out). A sharedParallelSamplingStatsobserves the request-level histograms once, when the last child finishes (max over the children).n = 1submits the request unchanged.vllm-text:TextLlm::generate_nlowers once and decodes each child stream. It owns the greedy rule, checked against the resolved temperature (after generation-config defaults). Python first raises a positive temperature below 0.01 to 0.01, so onlytemperature = 0is greedy. Submission goes through the existingsubmitstep, which now takesn, so the--skip-tokenizer-initstructured-output check covers every sample.vllm-chat:ChatLlm::chat_nrenders once and builds one output processor per sample, since parsers are stateful; the output grammar is built once and applied to the shared request.vllm-server: the routes resolvenlike Python's_verify_num_sequences, run the existing per-choice chunk stream or collector for each sample, and merge. Streamed chunks interleave as the samples produce them, stamped with their choice index; the per-choice final usage chunks fold into one (Usage::add_choice); non-streaming responses append the choices in index order, with prompt-level fields from the first choice.VLLM_MAX_N_SEQUENCESis read once at startup through a hidden--max-n-sequencesargument bound to the env var (and to the--args-jsonenv fallbacks), parsed like Python'sint(), and carried inApiServerOptionsandRenderConfig. An invalid value fails startup, where Python fails each request.Ownership and layering: the fan-out (IDs, seeds, request-level metrics) sits in
vllm-llm'sEngineCoreLlm, theLlmimplementation that mirrorsAsyncLLM; the greedy rule invllm-text, which ownsSamplingParamssemantics; per-sample parsing invllm-chat; wire DTO checks and response merging invllm-server.nis passed as an argument rather than added to the shared request types, since only the fan-out and the greedy rule read it. Thenrange check stays in request lowering, which is configuration-free and shared with the render routes, separate from the configured upper limit.Behavior notes:
index, so this is not a wire contract.Not a duplicate: the only open PR implementing
nin the Rust frontend is #58914, inactive since 2026-10-12 and built on a different design (see Overview); searched open PRs forn choices,parallel sampling,n > 1andOnly n=1. The open beam-search PRs (#47705, #64862) cover a different feature. AI assistance (Claude Code) was used to prepare this change; the human submitter reviewed every changed line.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CJckCAkBsrWKV6peRwSdih