Skip to content

[Bugfix][Frontend] Return all choices from /inference/v1/generate when n > 1 - #52399

Merged
DarkLight1337 merged 5 commits into
vllm-project:mainfrom
qgallouedec:fix-scale-out-parallel-sampling
Aug 19, 2026
Merged

DarkLight1337 merged 5 commits into
vllm-project:mainfrom
qgallouedec:fix-scale-out-parallel-sampling

Conversation

@qgallouedec

@qgallouedec qgallouedec commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

closes #52398

serve_tokens sets sampling_params.output_kind only for streaming requests, so non-streaming ones keep the SamplingParams default, CUMULATIVE. With n > 1, serve_tokens_full_generator keeps only the last streamed RequestOutput and reads final_res.outputs, which under CUMULATIVE holds just the sequences updated in that step,
the other completions are silently dropped. No error is raised; the response is simply short.

CompletionRequest.to_sampling_params already uses FINAL_ONLY when not streaming, so this makes the
token-in-token-out path match it.

Test Plan

10 non-streaming requests per n (2, 4, 8) against vllm serve Qwen/Qwen2.5-1.5B on an H100, before and after the patch, counting the choices returned by /inference/v1/generate. Same check with multimodal input (/v1/chat/completions/render → token_ids + features, Qwen2.5-VL-3B). Streaming, logprobs, prompt_logprobs and n=1 re-checked after the patch. Adds a regression test asserting the response carries indices 0..n-1.

Test Result

Before:

n=2 -> {1: 6, 2: 4}
n=4 -> {2: 5, 3: 3, 4: 2}
n=8 -> {5: 2, 7: 5, 3: 1, 6: 2}
multimodal, n=4 -> 3 choices on 6/6 runs

After:

n=2 -> {2: 10}
n=4 -> {4: 10}
n=8 -> {8: 10}
multimodal, n=4 -> 4 choices on 6/6 runs

n=1, streaming, logprobs and prompt_logprobs behave as before. The measurements were taken on v0.27.1 with this
patch applied to the installed package; the added test follows the existing fixtures in tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py and will get its first full run in CI.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

…n n > 1

`serve_tokens` only set `sampling_params.output_kind` for streaming requests,
so non-streaming ones kept the `SamplingParams` default, `CUMULATIVE`. With
`n > 1`, `serve_tokens_full_generator` keeps just the last streamed
`RequestOutput` and reads `final_res.outputs`, which under `CUMULATIVE` holds
only the sequences updated in that step, so the other completions were silently
dropped: 10 runs of `n=4` returned 2, 3 or 4 choices, and with multimodal
features `n=4` returned 3 choices on 6 out of 6 runs. No error is raised, the
response is simply short.

Set `FINAL_ONLY` when not streaming, mirroring
`CompletionRequest.to_sampling_params`, and add a regression test.

Signed-off-by: Quentin Gallouédec <quentin.gallouedec@huggingface.co>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added frontend bug Something isn't working labels Aug 14, 2026
@DarkLight1337
DarkLight1337 enabled auto-merge (squash) August 15, 2026 03:01
@DarkLight1337

Copy link
Copy Markdown
Member

/ci run

@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 15, 2026
@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #84001 for commit fcbd83beada7.

qgallouedec added a commit to huggingface/trl that referenced this pull request Aug 15, 2026
`trl vllm-serve` ran a custom FastAPI app around `LLM`, with its own
data-parallel fan-out and a worker extension for weight sync. vLLM's server
covers all of it, so the app is deleted and the command becomes a deprecated
wrapper that translates its flags into `vllm serve`, prints the equivalent
command, and forwards anything it does not know about.

- generation goes to `/v1/completions` for token IDs. Multimodal prompts have
  no OpenAI-compatible endpoint that takes token IDs and images at once, so the
  images are processed on their own and the resulting features are paired with
  the trainer's token IDs on `/inference/v1/generate`, which keeps multi-turn
  generation working with images
- weight sync uses vLLM's NCCL weight-transfer engine: the tensors are
  announced once and streamed in a single packed broadcast, rather than one
  HTTP request and one broadcast per tensor
- teacher logprobs for distillation come from `prompt_logprobs`
- the docs teach `vllm serve` directly, and the web-server dependencies go

The request sets `output_kind` explicitly, as the server would otherwise drop
completions when `n > 1` (vllm-project/vllm#52399).
qgallouedec added a commit to huggingface/trl that referenced this pull request Aug 15, 2026
`trl vllm-serve` ran a custom FastAPI app around `LLM`, with its own
data-parallel fan-out and a worker extension for weight sync. vLLM's server
covers all of it, so the app is deleted and the command becomes a deprecated
wrapper that translates its flags into `vllm serve`, prints the equivalent
command, and forwards anything it does not know about.

- generation goes to `/v1/completions` for token IDs. Multimodal prompts have
  no OpenAI-compatible endpoint that takes token IDs and images at once, so the
  images are processed on their own and the resulting features are paired with
  the trainer's token IDs on `/inference/v1/generate`, which keeps multi-turn
  generation working with images
- weight sync uses vLLM's NCCL weight-transfer engine: the tensors are
  announced once and streamed in a single packed broadcast, rather than one
  HTTP request and one broadcast per tensor
- teacher logprobs for distillation come from `prompt_logprobs`
- the docs teach `vllm serve` directly, and the web-server dependencies go

The request sets `output_kind` explicitly, as the server would otherwise drop
completions when `n > 1` (vllm-project/vllm#52399).
qgallouedec added a commit to huggingface/trl that referenced this pull request Aug 15, 2026
`trl vllm-serve` ran a custom FastAPI app around `LLM`, with its own
data-parallel fan-out and a worker extension for weight sync. vLLM's server
covers all of it, so the app is deleted and the command becomes a deprecated
wrapper that translates its flags into `vllm serve`, prints the equivalent
command, and forwards anything it does not know about.

- generation goes to `/v1/completions` for token IDs. Multimodal prompts have
  no OpenAI-compatible endpoint that takes token IDs and images at once, so the
  images are processed on their own and the resulting features are paired with
  the trainer's token IDs on `/inference/v1/generate`, which keeps multi-turn
  generation working with images
- weight sync uses vLLM's NCCL weight-transfer engine: the tensors are
  announced once and streamed in a single packed broadcast, rather than one
  HTTP request and one broadcast per tensor
- teacher logprobs for distillation come from `prompt_logprobs`
- the docs teach `vllm serve` directly, and the web-server dependencies go

The request sets `output_kind` explicitly, as the server would otherwise drop
completions when `n > 1` (vllm-project/vllm#52399).
@DarkLight1337

Copy link
Copy Markdown
Member

@qgallouedec

Copy link
Copy Markdown
Contributor Author

Strange, I can't reproduce this. On main (0.27.2rc1.dev171+gc1e438728), with the test's payload and the fixture's server args, 5 trials each:

vllm serve Qwen/Qwen3-0.6B --dtype bfloat16 --max-model-len 1024 \
  --enforce-eager --no-enable-prefix-caching --port 8123

for i in 1 2 3 4 5; do
  curl -s localhost:8123/inference/v1/generate -H 'Content-Type: application/json' -d '{
    "model": "Qwen/Qwen3-0.6B", "token_ids": [1,2,3], "stream": false,
    "sampling_params": {"max_tokens": 5, "temperature": 1.0, "n": 4}}' \
  | python -c 'import json,sys; print(sorted(c["index"] for c in json.load(sys.stdin)["choices"]))'
done
with this patch:   [0,1,2,3] [0,1,2,3] [0,1,2,3] [0,1,2,3] [0,1,2,3]
without it:        [1,2,3]   [3]       [2,3]     [1,2,3]   [2,3]

So the bug reproduces on main and the patch fixes it. The CI result is what the unpatched path returns.

Could we re-run the job? If it fails again I'll log the SamplingParams the server actually receives (n, output_kind) to see whether the request reaches the engine with the patched values. Thanks

@DarkLight1337

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #84337 for commit e646b1d7db16.

auto-merge was automatically disabled August 18, 2026 23:58

Head branch was pushed to by a user without write access

@qgallouedec

qgallouedec commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor Author

ok I get it: the failing job runs this suite with VLLM_USE_RUST_FRONTEND=1, where /inference/v1/generate is served by the Rust frontend rather than serve_tokens, so the fix here doesn't apply.

The Rust frontend doesn't implement parallel sampling: its OpenAI routes reject n > 1 explicitly, and the raw generate route silently ignores n (the field isn't even part of its SamplingParams) and always returns a single choice.

Skipped the regression test under the Rust frontend, following the existing test_generate_sampling_mask precedent.

Follow-up PR: make the Rust generate route reject n > 1 loudly, matching its OpenAI routes. Opening a tracking issue for this: #52843

@DarkLight1337

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #84531 for commit 5aaed3d1e7f9.

@qgallouedec

Copy link
Copy Markdown
Contributor Author

Alright, it's green now

@DarkLight1337
DarkLight1337 merged commit 92bdee0 into vllm-project:main Aug 19, 2026
65 checks passed
zufangzhu pushed a commit to zufangzhu/vllm that referenced this pull request Aug 24, 2026
…n n > 1 (vllm-project#52399)

Signed-off-by: Quentin Gallouédec <quentin.gallouedec@huggingface.co>
Signed-off-by: Zhu, Zufang <zufang.zhu@intel.com>
am-cohere pushed a commit to am-cohere/vllm that referenced this pull request Sep 1, 2026
…n n > 1 (vllm-project#52399)

Signed-off-by: Quentin Gallouédec <quentin.gallouedec@huggingface.co>
michael-gpu pushed a commit to practical-inference/vllm-ampere-extended that referenced this pull request Oct 20, 2026
…n n > 1 (vllm-project#52399)

Signed-off-by: Quentin Gallouédec <quentin.gallouedec@huggingface.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working frontend ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: /inference/v1/generate silently drops choices when n > 1

2 participants