Repository navigation
[Rust Frontend] Select one structured outputs backend for every route - #62456
Conversation
Every Rust route now compiles structured outputs with one server-selected backend, `--structured-outputs-config.backend` (default xgrammar; `auto` selects xgrammar). Previously gRPC and parser-built output grammars used xgrammar while other HTTP constraints used guidance, and EngineCore keeps the backend of the first structured request it sees, so mixed traffic failed with `KeyError: 'triggers'` once guidance was chosen first. - The backend lives only on the EngineCore request type, `EngineCoreStructuredOutputsParams`; text lowering selects the configured backend and rejects constraints it cannot enforce, as Python's per-backend validation does. - Chat builds parser output grammars (xgrammar structural tags) only with xgrammar; with another backend the request keeps its own constraint. - `--tool-strict-level function|parameter` requires xgrammar. - `vllm-rs serve` forwards `--structured-outputs-config` to the engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
…naged engine" This reverts commit 1d003fe. vllm-project#62456 parses --structured-outputs-config in the Rust frontend and forwards it to the managed engine itself, so this PR no longer touches the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
|
This pull request has merge conflicts that must be resolved before it can be |
Signed-off-by: Bugen Zhao <i@bugenzhao.com> # Conflicts: # rust/src/engine-core-client/src/protocol/structured_outputs.rs # rust/src/text/src/lower.rs
|
This pull request has merge conflicts that must be resolved before it can be |
|
We hit this on DeepSeek-V4.1-Flash (4×H200, TP4, Rust frontend, This PR (
An older build, for context (
On that build the json_schema request reaches the engine as a guidance constraint and pins the engine backend to guidance. The engine error in the first row is then |
Pass the structured outputs backend to lower_sampling_params in the bad-words test added on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mh7QwxnkeqG3LX7ngPhdyy Signed-off-by: Bugen Zhao <i@bugenzhao.com>
|
/ci run |
|
✅ Triggered Buildkite CI #97196 for commit |
…vllm-project#62456) Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
…vllm-project#62456) Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Overview
EngineCore compiles every structured-output request with the backend of the first one it sees. The Rust frontend sends gRPC constraints and parser-built output grammars as xgrammar, but every other HTTP constraint as guidance (#44729), so one server that takes both kinds of traffic is order-dependent: after a native constraint reaches the engine first, every later strict,
required, or named tool call and everyresponse_formatcomposed with a reasoning parser fails withKeyError: 'triggers'(HTTP 500). This PR makes every Rust route use one backend, chosen by the operator with--structured-outputs-config.backendand defaulting to xgrammar, as Python'sautodoes for most requests. It is the follow-up named in #61892.Claims
--structured-outputs-config.backend, defaultxgrammar.autoselects xgrammar;guidance,outlines, andlm-format-enforcerare accepted. A server no longer depends on which request arrives first.backend=xgrammar.--tool-strict-level function|parameterfails startup unless the backend is xgrammar.format-shape structural tags on guidance, and the constraint kinds outlines and LM Format Enforcer reject.vllm-rs serveforwards--structured-outputs-configto the managed engine, so engine-side options such asdisable_any_whitespacetake effect.Validation
GPU smoke on GB200, Qwen3-8B with
--reasoning-parser qwen3 --tool-call-parser hermesviaVLLM_USE_RUST_FRONTEND=1 vllm serve. Same request sequence per server, starting with a native/v1/completionsconstraint; the completions prompts close the think block, since the engine's reasoning gate otherwise waits for</think>before applying a raw completions constraint.mainbackend=guidancechoicetool_choice=requiredKeyError: 'triggers')get_weathercallget_weathercallresponse_formatjson_schematool_choice=autoget_weathercallget_weathercallregexmultipleOfmultipleOf)Rust tests on the branch tip:
cargo nextest run -p vllm-engine-core-client -p vllm-text -p vllm-chat -p vllm-server -p vllm-cmd -p vllm-managed-engine -p vllm-parser(1806 passed),cargo clippy --workspace --all-targets,cargo fmt --check. New tests:chat_structured_outputs_use_configured_backend(routes): a chat request with a strict tool andresponse_formatthrough a mock engine; the engine request carries the configured backend, a structural tag with xgrammar and the request's own JSON schema with guidance. With the chat-side backend check removed, the guidance case fails (lowering rejects theformat-shape tag with 400).lower_sampling_params_validates_against_configured_backend,backend_rejects_constraint_kinds_it_cannot_compile, andengine_structured_outputs_serializes_through_raw_shape(msgpack round trip).vllm-rs serveand the--args-jsonbootstrap path,auto, forwarding to the managed engine, and the--tool-strict-levelstartup check.Details
Root cause.
StructuredOutputManager.grammar_initcreates one backend from the first structured request and keeps it until shutdown. Python chooses_backendinSamplingParamsvalidation, which the Rust frontend skips, so #44729 defaulted Rust to guidance; guidance handledchoice, which xgrammar could not compile then. Parser-built output grammars (#46057) and gRPC (#61892) later moved to xgrammar, and guidance reads only the legacytriggers/structuresstructural tag.Where the backend lives. The backend is now a field of the EngineCore request type only (
EngineCoreStructuredOutputsParams).StructuredOutputsParams, the type the HTTP/gRPC entry points and chat build, carries no backend, so nothing picks one implicitly.TextRequestProcessorholds the configured backend, and text lowering, which HTTP and gRPC share, attaches it and validates the final constraint against it. gRPC no longer selects a backend itself.Why one backend per server, not per request. The engine still holds one backend, so a per-request choice would bring the order dependence back. Python's
autofallback has the same limit in a mixed workload. Engine-side multi-backend support (#62246, draft) would allow request-level selection later; a single configured backend maps onto a one-element list then.Tradeoffs.
multipleOf,uniqueItems/contains, unsupported stringformats,patternwith length bounds, ...) return 400 instead of being enforced by guidance;--structured-outputs-config '{"backend": "guidance"}'restores that. llguidance does not implementuniqueItemseither: Pythonautorejects it in guidance validation, while the Rust frontend onmainreturns an empty 200 when the matcher errors in EngineCore.backend=guidance.vllm-rs servereads only the JSON form of--structured-outputs-config; the dotted form (--structured-outputs-config.backend guidance) goes to the Python engine only, like other dotted flags on that path. Thevllm servepath reads both.Not a duplicate.
vllm servefrontend flags explicit #62431 (open) no longer touches--structured-outputs-config; the two PRs are independent.RequestError(code="validation_error"). Onmainthe Rust frontend's mixed traffic now hits that rejection instead ofKeyError: 'triggers'(still a 500 in Rust, which does not map that code yet); with this PR every Rust request names the same backend, so the rejection is not reached. [Bugfix][Structured Output] Honor resolved backends in mixed requests #62246 (draft) would let EngineCore hold several backends. This PR changes only what the Rust frontend sends.format-shape structural tags; with it, more guidance requests would pass the new check, which can be relaxed then.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) was used to investigate, write, and test this change. I reviewed every changed line and ran the tests and GPU smoke above.
🤖 Generated with Claude Code