[Perf] Skip detokenization in online beam search - #46422
Merged
vllm-bot merged 3 commits intoJun 23, 2026
Merged
Conversation
The online beam_search per-step SamplingParams requests 2*beam_width logprobs, and the engine detokenizes every one of those logprob token ids into strings on every decode step. Beam search ranks candidates by logprob and detokenizes only the final sequences itself (via tokenizer.decode), so the per-step detokenization is pure overhead. Set detokenize=False on the internal SamplingParams to skip it; final outputs are unchanged. Benchmark (vllm bench serve, Qwen3-1.7B, RTX PRO 6000, random 512-in/32-out, 20 prompts, openai-chat): mean E2EL bw20 1543->855 ms (1.80x), bw60 6612->2615 ms (2.53x); output throughput ~1.8-2.5x. nsys shows identical total GPU kernel time (~3.31s) with ~half the wall clock, and GPU SM utilization roughly doubles -- the GPU was being starved by CPU-side detokenization. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Guy Stone <guys@spotify.com>
mgoin
approved these changes
Jun 22, 2026
mgoin
left a comment
Member
There was a problem hiding this comment.
Thanks for catching this forgotten bit
njhill
reviewed
Jun 23, 2026
Co-authored-by: Guy Stone <guystone3@gmail.com> Signed-off-by: Guy Stone <guystone3@gmail.com>
GuyStone
commented
Jun 23, 2026
Signed-off-by: Guy Stone <guystone3@gmail.com>
mgoin
enabled auto-merge (squash)
June 23, 2026 00:50
njhill
approved these changes
Jun 23, 2026
nkzhenhua
pushed a commit
to nkzhenhua/vllm
that referenced
this pull request
Jun 24, 2026
Signed-off-by: Guy Stone <guys@spotify.com> Signed-off-by: Guy Stone <guystone3@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
wincent8
pushed a commit
to wincent8/vllm
that referenced
this pull request
Jun 29, 2026
Signed-off-by: Guy Stone <guys@spotify.com> Signed-off-by: Guy Stone <guystone3@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
efschu
pushed a commit
to efschu/shvllm
that referenced
this pull request
Jul 18, 2026
Signed-off-by: Guy Stone <guys@spotify.com> Signed-off-by: Guy Stone <guystone3@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
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 & why
Online beam search builds an internal per-step
SamplingParamsrequestinglogprobs = 2 * beam_width. The engine then detokenizes every one of those2 * beam_widthlogprob token ids into strings on every decode step(
LogprobsProcessor→tokenizer.decode). Beam search never uses thosestrings — it ranks candidates by
cum_logproband detokenizes only the finalselected sequences itself (
tokenizer.decode(tokens)near the end ofbeam_search).vllm/vllm/entrypoints/generate/beam_search/online.py
Line 208 in 837d5b3
The per-step detokenization is therefore pure overhead that
grows with beam width, and it starves the GPU between decode steps.
This sets
detokenize=Falseon the internal per-stepSamplingParamsinvllm/entrypoints/generate/beam_search/online.py. Final output text isunchanged (still produced by the explicit
tokenizer.decodeon the winningbeams).
detokenize=Falseis only invalid alongsidestopstrings, and theseper-step params set none, so it is safe here.
logprobs_num = 2 * beam_width sampling_params = SamplingParams( logprobs=logprobs_num, max_tokens=1, temperature=temperature, + detokenize=False, )Scope is the online path only. The offline path
(
beam_search/offline.py) has the identical pattern and would benefit from thesame one-liner as a follow-up.
Benchmark — before / after
vllm bench serve, Qwen3-1.7B, single RTX PRO 6000 Blackwell,--backend openai-chat, random dataset 512-in / 32-out, 20 prompts,--max-concurrency 4,--ignore-eos, fixed--seed 12345. Beam search triggered via--extra-body '{"use_beam_search": true, "n": <bw>, "temperature": 1.0}'.Beam search is non-streaming, so TTFT ≈ E2EL and TPOT/ITL are ~0 — the
meaningful metrics are end-to-end latency and throughput.
The win grows with beam width, as expected for an O(beam_width) per-step cost.
GPU utilization (nvidia-smi dmon, during the runs)
The GPU was being starved by CPU-side detokenization; removing it roughly
doubles SM utilization:
nsys profiling (before / after)
Nsight Systems trace bracketing an identical focused bw60 run (12 prompts,
conc 2, seed 777, captured via
nsys start/stop):cuda_gpu_kern_sum)The total GPU kernel time is identical before and after (~3.31 s) — the fix
removes no GPU work, only CPU work. Same compute in ~half the wall clock, so the
GPU busy fraction doubles (cross-validates the dmon numbers). Host-side CUDA API
time stays dominated by
cudaEventSynchronize(the engine waiting on the GPU),consistent with the bottleneck being host-side detok between GPU bursts.
(CPU IP/backtrace sampling was unavailable in this environment, so the trace
attributes the saved time to reduced wall clock / higher GPU duty cycle rather
than naming
tokenizers.decodedirectly.)Correctness
Identical deterministic beam-search request (
n=4,temperature=0) against theserver before and after the change returned byte-identical output for all
beams — confirming
detokenize=Falsedoes not alter results.Not a duplicate
No open PR addresses this. Upstream PR #33563 implemented the
same idea and was approved by a maintainer ("a good and safe optimization")
but was closed by its author over commit-history issues, deferring to
#29133, which has since also closed. Both touched the older
file layout (
entrypoints/llm.py,entrypoints/openai/engine/serving.py); thecurrent
entrypoints/generate/beam_search/online.pystill lacks theoptimization. This PR applies it to the current code path.
Test commands run
Lint:
ruff checkandruff format --checkpass on the changed file.AI assistance
This change was developed with AI assistance (Claude Code). The human submitter
reviewed every changed line and ran the benchmarks and correctness check above.