Repository navigation
[Rust Frontend] Support beam search (use_beam_search, length_penalty) - #64862
aaroncheng00 wants to merge 3 commits into
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
| finish_reason, | ||
| } = beam; | ||
| // Like Python: one position per returned token, `requested` top-k each. | ||
| logprobs.truncate(tokens.len()); |
There was a problem hiding this comment.
An EOS-only beam causes a 500 with logprobs=true: the chat collector converts empty logprobs to None. Please keep empty logprobs valid.
There was a problem hiding this comment.
Thanks, good catch. Fixed in 3566d2e. Updated chat collector so empty logprobs are preserved instead of mapped to None for logprobs=true and an EOS-only beam. Added beam_search_eos_only_beam_has_empty_logprobs test which verifies EOS-only beam for both chat and completion endpoint. Confirmed the test returned 500 on chat before the fix.
3566d2e to
2308f8f
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
Split `ChatRequestProcessor::prepare` into `prepare_for_outputs`, which builds one independent output processor per output sequence generated from the same rendered prompt. `prepare` keeps its behavior and asks for one. Beam search will use this to parse each returned beam separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Cheng <aircondi5@gmail.com>
Port Python's online beam search (`BeamSearchOnlineMixin.beam_search`) as `TextLlm::beam_search`. Each step submits one `max_tokens=1` engine request per live beam asking for `2 * beam_width` logprobs, EOS candidates finish their beam, and the best `beam_width` extensions stay live. Finished and live beams are ranked by cumulative logprob divided by the generated length (including a terminating EOS) to the power of `length_penalty`, matching vllm-project#62928. Each returned beam is presented as a finished decoded text stream, so callers reuse the regular detokenization and response paths. A failed step returns a single stream that finishes with the error, like Python. Dropping the search drops the in-flight step streams, which aborts them in the engine. Sampling fields beam search does not apply are rejected when set to non-default values instead of being ignored as in Python. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Cheng <aircondi5@gmail.com>
Accept `use_beam_search` with `n` as the beam width and `length_penalty` on `/v1/chat/completions` and `/v1/completions`, replacing the previous 400s. The routes run `beam_search` and collect each returned beam through the regular non-streaming response path, merging them into one response with a choice per beam, best first. Chat parses each beam with its own output processor, so reasoning and tool calls work as usual. Like Python, reject streaming on completions, `logprob_token_ids`, and completions `echo` with `logprobs`. Also reject chat streaming, a tool choice that implies an output grammar, and prompt-only completions (`max_tokens=0`), which Python handles differently. `n > 1` and a non-default `length_penalty` stay rejected without beam search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Cheng <aircondi5@gmail.com>
2308f8f to
86b2966
Compare
|
@BugenZhao do you mind taking a pass on this PR when you get the chance? Regarding testing, |
|
This pull request has merge conflicts that must be resolved before it can be |
Overview
Implements beam search in the Rust frontend for
/v1/completionsand/v1/chat/completions(use_beam_search, beam widthn,length_penalty), mirroring Python's online beam search. Part of the Rust frontend parity roadmap #44280 ("Beam search via the existinguse_beam_searchrequest parameter, includinglength_penalty").nchoices #58914 (general OpenAInchoices). This PR does not depend on it; beam output assembles its own choices, so it can be rebased onto [Rust Frontend] Support OpenAInchoices #58914's merging later.main; I'm happy to close it if @yangyang-cs95 prefers to continue there.Claims
use_beam_search=truewithn≥ 1 andlength_penaltynow works on both endpoints, replacing the previoususe_beam_search is not supported./length_penalty is not supported.400s.finish_reason,stop_reason, usage) on Qwen2.5-0.5B-Instruct.Validation
Model comparison, Python vs Rust frontend. Qwen/Qwen2.5-0.5B-Instruct, float32, CPU (Apple M2, 16 GB),
n=3,temperature=0,max_tokens=24. Each frontend served the same model in turn; responses were reduced toindex, text/content,finish_reason,stop_reasonandusage, then diffed.length_penalty/v1/completions/v1/completions/v1/chat/completions/v1/chat/completionsThe only diff was
usage.prompt_tokens_details: nullfrom Rust, which #65374 has since removed onmain.Reproduction
VLLM_CPU_KVCACHE_SPACE=2 vllm serve Qwen/Qwen2.5-0.5B-Instruct --dtype float32 --max-model-len 2048 --port 8000 # then, with VLLM_USE_RUST_FRONTEND=1 for the Rust runEdge cases, Python vs Rust (same setup, full responses diffed):
n=1;max_tokens=1;ignore_eos;echo;temperature=0.7;length_penalty0, −1, 2include_stop_str_in_output;skip_special_tokens=falseannotations: nullin this run was removed onmainby #65374)logprobs=2; chattop_logprobs=2n=11(logprobs=22> default--max-logprobs 20)logprobsonmainn=0beam_width must be at least 1, got 0.in bothstop,top_pstreamRejections on the live Rust server:
streamStreaming is not currently supported with beam search(Python's message)top_p: 0.5`top_p` is not supported with beam search.(param: top_p)n: 2without beam searchOnly n=1 is supported without use_beam_search.streamStreaming is not supported with beam search.(param: stream)Tests:
The pre-commit hooks passed on each commit, except
actionlint, which was skipped locally because it needs Go >= 1.25.Unit tests (
vllm-text,beam_search.rs) cover:include_stop_str_in_outputandignore_eosEnd-to-end tests (
test_beam_search_completionincompletion/test_completion.py,test_beam_search_chat_completioninchat_completion/test_chat_completion.py) checkndistinct beams by token IDs, choice indices, and usage counting the prompt once. Both the Python entrypoints CI and the Rust frontend GPU CI (rust_frontend.yaml) already select these files, so they run on GPU with both frontends. Locally, both pass with both frontends on CPU (Apple M2), using each file's CI model and server flags:Route tests (
routes/tests.rs, mock engine) cover:tool_choiceauto/nonevsrequired, and a strict tool underautoDetails
Design. Same algorithm as
vllm/entrypoints/generate/beam_search/online.py:max_tokens=1andlogprobs=2·n.stop_reason=<eos id>.cum_logprob / max(len,1)**length_penaltyand the topnkept.lencounts generated tokens, including EOS ([Bugfix][Frontend] Normalize beam search scores by generated length #62928).nreturned.Ownership by crate:
vllm-text:TextLlm::beam_searchruns the steps and builds one synthetic terminal output per beam. That output goes through the existingdecoded_text_event_stream, so detokenization, stop handling and logprobs reuse the normal path.vllm-chat:ChatLlm::beam_searchpairs each beam with its own chat output processor, so reasoning and tool parsing run per choice. Commit 1 is a mechanical refactor that letspreparecreate several processors.vllm-server: validation, and assembling the multi-choice response with merged usage.Deviations from Python (per
rust/AGENTS.md, unimplemented behavior is rejected instead of silently ignored):top_p,top_k,min_p,seed,min_tokens, the penalties,stop,stop_token_ids,logit_bias,bad_words,structured_outputs, prompt logprobs,kv_transfer_params,ec_transfer_paramsand similar fields. Rust returns 400 for any non-default value. This is the main behavior change to review.param: stream). Python finishes the search and then streams every beam at once as SSE chunks; no tokens stream during the search. The Rust chat SSE path currently writes a single choice (index: 0), so this needs multi-choice streaming. A possible follow-up builds on [Rust Frontend] Support OpenAInchoices #58914'sn > 1streaming:ChatLlm::beam_searchalready returns one finishedChatEventStreamper beam, which can feed that writer, and then the 400 can be removed. Completions streaming is rejected in both frontends.allowed_token_idsis rejected (TODO). Python scores allowlisted beams with exact-ID logprobs, which needs Model Runner V2 support.tool_choicerequiredor a named function, strict tools or--tool-strict-level, or a tool parser that always constrains output. Python's online beam search ignores the grammar. Its offlineLLM.beam_searchdoes enforce it per beam, by running a structured output backend in the frontend process and replaying each beam's tokens. The engine only advances a grammar over tokens it generates itself, so Rust would need its own grammar matcher or an engine protocol change.autoandnonework unless strict tools or the model's tool parser impose a grammar.max_tokens=0is rejected (TODO). Python returns the bare prompt as a single choice.kcandidates, deduplicated, as Rust's non-beam responses do. Python's beam search reports the first entries of the step's2·ncandidate dict instead. So Python can returnk+1alternatives, can leave out the chosen token, and lists the candidates in rank order rather than chosen-first. The token and logprob of each position are identical.<parent>-<uuid>-beam-<i>), so/abort_requestswith the parent ID does not reach in-flight steps. Client disconnect does abort them.metricson the response isnull, because there's no single engine request to report.Shared limit:
n > 10needs--max-logprobs≥2·n, because each step asks for2·nlogprobs. Both frontends reject it with the default of 20.Not validated locally:
rust_frontend.yamlalso updates a comment that said the Rust frontend has no beam search.test_batch_completionsstays excluded, because Rust still rejects batched prompts.Docs: the rendered docs have no Rust frontend parameter page, and recent
[Rust Frontend]parity PRs haven't toucheddocs/. I'm happy to add a short note under "Extra Parameters" indocs/serving/online_serving/openai_compatible_server.mdif reviewers want one.Size: about 800 implementation lines and 870 test lines across 20 files. Most of the implementation is in
rust/src/text/src/beam_search.rs.Updates after review:
logprobsno longer returns 500 on chat; it returns empty logprobs, like Python.--tool-strict-levelnow name those causes instead oftool_choice.kv_transfer_params/ec_transfer_paramsare rejected under their own names instead ofvllm_xargs.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.
AI assistance (Claude Code) was used to write this change. I reviewed every changed line and ran the tests and model comparison above.
🤖 Generated with Claude Code
BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)