Repository navigation
[Bugfix][Frontend] Honor token ID logprob overrides on derender paths - #63173
MuonChaser wants to merge 6 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. 🚀 |
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
|
Documentation preview: https://vllm--63173.org.readthedocs.build/en/63173/ |
|
cc @hickeyma |
hickeyma
left a comment
There was a problem hiding this comment.
Thanks @MuonChaser for pushing the fix. It looks good and covers everything in #63162. A few things to address:
test_render_generate_logprob_overrideasserts that derender withstream=truereturns a 400. #63103 removes that restriction, so this test breaks once that merges. It's not related to this bug anyway, so please drop it.- There seems like a lot of tests compared to the fix. Can you move the coverage into the existing suites instead of adding a new file with its own tokenizer, engine and app?
_resolve_logprobsplaceholders →test_generate_logprobs_conversion.py/derenderprecedence →test_derender.py/test_derender_stream.py_logprobs_tokenizerprecedence →test_serving_tokens.py(_build_serving_tokensalready takes the flag)
- Also drop the cases that pass whatever the flag is set to: the hidden reasoning ones, the missing
parse_context checkand the top-logprob count. - Both tests change settings on the module scoped
appfixture and the two derenderer references point at the same object. That only works because every test sets both values. Please usemonkeypatchor a functionscoped fixture ParseContext.to_chat_request()now passesreturn_tokens_as_token_idsthrough but nothing reads it from there. Either drop it or have_logprobs_tokenizerread it fromderender_requestso there's one place to look.- In
_resolve_logprobsthenot return_tokens_as_token_idsand checks before the U+FFFD repair can't trigger because atoken_id:Nstring never ends in U+FFFD. You can remove them. self._return_tokens_as_token_ids(...)gets called for every choice. Work it out once next totop_limit. That'll also shorten the five call sites.- Placeholder
bytescome back asnullon both derender paths. Coupled chat returns bytes there, though it's not consistent about it either. I'm fine withnullsince that matches generate's text mode but please add a comment in the parity test saying why the coupled bytes get blanked out. Right now it looks like the test is just hiding the difference.
|
@hickeyma Addressed in 08b40a0. Thanks for the detailed review. I removed the standalone route-test application and reused the existing conversion, derender and streaming suites. The real-model request-override regression now lives in I also removed the unused Validation: five representative new HTTP cases fail on the baseline for decoded-versus-placeholder mismatches; all 30 revised HTTP cases pass with the fix. The existing focused suites pass 101 tests, the real-model regression passes both server configurations, and staged pre-commit checks pass. |
hickeyma
left a comment
There was a problem hiding this comment.
Thanks @MuonChaser, LGTM.
A few optional follow ups, none of them blocking:
- The new test in
test_serving_tokens.pystarts its own server for each flag value. The moduleserverfixture already takes indirect params, so it could reuse that and cut most of the extra args. (My earlier pointer to_build_serving_tokenswas wrong, sorry about that. That helper is intest_generate_stream.py.) /rendernow always sendsreturn_tokens_as_token_idsinparse_context, even when it's null. That means an older generate node rejects every derender request, not just the ones that set it. The docs cover upgrade order but leaving the field out when it'sNonewould avoid the problem.- The streaming override test starts 6 render servers. Chat and completion could share one per flag value.
|
@claude review |
| ) = "none" | ||
| parallel_tool_calls: bool | None = True | ||
| include_reasoning: bool = True | ||
| return_tokens_as_token_ids: bool | None = None |
There was a problem hiding this comment.
🔴 During a render-first rolling upgrade, every output_mode: "derender" request fails with a 400 on older generate nodes, including requests that never set the flag. The new return_tokens_as_token_ids field defaults to None, but /render always serializes it. Older nodes reject any unknown parse_context key in derender mode, including a null value. Fix: omit the field from the /render output when it is None, for example by building ParseContext with only the explicitly set value. Only requests that actually set the override should then need an upgraded generate node.
Why this was flagged
ServingRender._build_parse_context (vllm/entrypoints/scale_out/render/serving.py:236) always sets return_tokens_as_token_ids=request.return_tokens_as_token_ids, which is usually None. The /render routes return the whole GenerateRequest through create_json_response (render/api_router.py:50, :71), so "return_tokens_as_token_ids": null appears in every parse_context. Older generate nodes parse ParseContext with extra="allow". Their _check_parse_context_fields (protocol.py:403) then fails any derender request that has an unknown key. As a result, every derender request routed from an upgraded render node to an older generate node gets a 400, not just ones that use the new override. On the base branch the field is absent and these requests succeed. The doc note in token_in_token_out.md says to upgrade generate nodes first and calls this accepted. It still means a render-first or mixed rollout breaks all derender traffic, and omitting the field when it is null would avoid that cost.
Verification: Trigger: render nodes are upgraded before generate nodes, and clients send output_mode: "derender". render/serving.py:236 always sets return_tokens_as_token_ids, so every parse_context carries "return_tokens_as_token_ids": null. _check_parse_context_fields (base protocol.py:402-411) raises, and that becomes a 400. Before this change those requests worked.
There was a problem hiding this comment.
Addressed in 4084e72.
Fixed by omitting only None at the ParseContext field's serialization boundary. Missing/null request values no longer add a key to /render responses; explicit false and true remain serialized and retain their precedence.
The new live render-server regression fails for the two default cases before the change and passes all four cases after it. I also checked the actual HTTP payloads against the generate protocol from b689feb: default requests are accepted, while explicit overrides still fail closed in derender mode. The broader routes (34 tests), focused suites (101 tests), real-model regression (2 tests) and staged pre-commit checks pass. Updated the rollout documentation accordingly.
|
This pull request has merge conflicts that must be resolved before it can be |
hickeyma
left a comment
There was a problem hiding this comment.
@MuonChaser #63103 merged and this needs a rebase. Some things to be handled also:
- In
serving.py, keep main's side and just passrequestto_logprobs_tokenizer. The PR'stext_modeline breaks streaming derender logprobs. test_derender_logprob_request_overrideneeds the Rust frontend skip because/renderreturns 501 there. It should also use the indirectserverparam becaue a second server will probably fail the free memory check while the module server holds the GPU. I said this was optional last time but it's required now.- Please add a mock test in
test_generate_stream.pyfor the override withstream: true.
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
4084e72 to
a0e3dde
Compare
|
@hickeyma Addressed in a0e3dde; rebased with #63103 included. I kept main's streaming setup and The model regression now uses the existing indirect module server/client fixtures with Qwen3-0.6B, plus the Rust frontend capability skip. Added six scripted-engine streaming override cases to Validation: 120 focused tests, 34 HTTP cases, both contributed real-model fixture cases, two additional real-model streaming parity cases, and all applicable pre-commit checks pass. The Rust selection check skips the two unsupported cases; no Rust E2E is claimed. |
hickeyma
left a comment
There was a problem hiding this comment.
Thanks @MuonChaser nearly there. One more change:
test_derender_logprob_request_override compares logprobs with == across separate requests. Exact floats could make this flaky. The server fixture's comment already mentions 1 ULP differences on ROCm and test_derender_parity.py only compares token and bytes. Please compare token and bytes exactly and use pytest.approx for logprob.
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
|
@hickeyma Addressed in 638fbfd. Sampled and top-candidate Validated the actual test coroutine with one-ULP differences and negative token/bytes/logprob mutations; six streaming cases and applicable pre-commit checks pass. This revision only changes test assertions; no new GPU run is claimed. |
hickeyma
left a comment
There was a problem hiding this comment.
Thanks @MuonChaser for the update. One last thing. The default pytest.approx tolerance is too tight here. With a bf16 model, a one step logit change moves the logprob by much more than 1e-6 relative and near certain tokens round to steps of about 1.2e-7. The default abs=1e-12 won't absorb this. Do you mind using pytest.approx(x, abs=1e-3) as test_vision.py does?
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: muonchaser <1507272310@qq.com>
|
@hickeyma Addressed in 9ca8dcf. Both sampled and top-candidate logprobs now use Offline assertion checks accept bf16-logit and near-zero differences while still rejecting incorrect tokens/bytes and out-of-tolerance logprobs. Applicable pre-commit checks pass. |
Overview
Fixes #63162: honor the tri-state
return_tokens_as_token_idsoverride on both disaggregated derender paths, including generate streaming after merged #63103.Claims
/renderomits the new context key when null, preserving legacy generate compatibility for default requests. Explicit overrides require an upgraded generate node.Validation
text_modemerge negative controlThe Rust frontend capability check skips the two unsupported render cases. No Rust server E2E, model-quality evaluation or performance benchmark is claimed. Existing entrypoints integration CI selects the changed suites; upstream CI has not run because the contributor pre-run gate remains unsatisfied.
Reproduction commands and before/after output
.venv/bin/python -m pytest \ tests/entrypoints/scale_out/token_in_token_out/test_generate_stream.py \ tests/entrypoints/scale_out/derender/test_generate_logprobs_conversion.py \ tests/entrypoints/scale_out/token_in_token_out/test_protocol.py \ tests/entrypoints/scale_out/token_in_token_out/test_tokens_logprobs.py -q .venv/bin/python -m pytest \ tests/entrypoints/scale_out/render/test_render.py \ tests/entrypoints/scale_out/derender/test_derender.py \ tests/entrypoints/scale_out/derender/test_derender_stream.py \ -k 'logprob_override or test_render_omits_null_token_id_override' -q .venv/bin/python -m pytest \ tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py \ -k test_derender_logprob_request_override -q .venv/bin/python -m pre_commit runFor the isolated streaming regression, keep the new tests/protocol field and substitute main
3b6eeb0606'sserving.py, then run:Before:
After restoring the fix:
GPU validation used the official CUDA wheel for
7ff608a03c(merged #63103), with the six patched Python modules overlaid and hash-matched to this rebased checkout. Serving/model Python paths are unchanged between that wheel base and the rebase base; unrelated newer Kimi kernel CMake pins were not rebuilt or validated. The Qwen checkpoint is revisionc1899de289a04d12100db370d81485cdf75e47ca, tested on one RTX 4090 with eager execution, prefix caching disabled and 30% GPU memory utilization.The human submitter confirmed line-by-line review of this rebased revision and personally running both the new streaming regression and the updated GPU fixture regression before publication.
Details
The request override was dropped at render serialization, generate used only its server flag, and separate derender always decoded. A nullable
ParseContextfield preserves explicit false; field-level null omission prevents adding an unknown key for older generate nodes. Each answering path resolves the override against its own server default. Placeholder formatting reuses the existing helper; decoded logprobs retain the existing decoder and UTF-8 fallback repair.The rebase keeps main's streaming processor setup,
_logprob_settingsandtext_modebehavior. The model regression reuses the module's indirect server fixture, avoiding a nested server while another fixture owns GPU memory. Its Rust capability skip matches the unsupported/renderroute.Duplicate searches found only this PR and merged #60082/#63103; neither merged feature includes this request-override fix. The affected logprobs docs explain precedence and rolling upgrade behavior. AI assistance was used for implementation, tests, documentation and review preparation.
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.