feat: add XGrammar structured output with speculative decoding - #294
Open
magnetism87 wants to merge 2 commits into
Open
magnetism87 wants to merge 2 commits into
magnetism87 wants to merge 2 commits into
Conversation
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.
Problem and scope
Related Issue: #33, specifically its JSON Schema use case. This proposes an implementation of final-response constraints; it does not close the issue's broader GBNF and plain-text grammar requests. The specific XGrammar integration proposed here has not been pre-approved by the maintainer.
NInfer currently rejects JSON response formats, so applications cannot require parseable JSON or a schema-valid final response. This adds native token-constrained generation to the public Engine, CLI, and the three HTTP protocols:
response_formatwithjson_objectorjson_schema.text.format, with the selected format retained in the response object.output_config.format.--jsonand--json-schema FILE.For example, a request with a constant schema still returns the required JSON value when its prompt asks for unrelated prose. Invalid or unsupported constraints fail explicitly instead of being silently ignored.
Implementation
Grammar and request ownership
Vendor the native C++ portion of XGrammar v0.2.7 at
82505d0d987c36a4209fb3d8571cf6b0f28b5acd, with licenses and a target-scoped CMake build. No configure-time downloads, Python inference dependency, or model conversion are needed.third_party/xgrammar/NINFER.mddocuments three local correctness patches: JSON control-character exclusion in bounded strings, Unicode/escaped-key handling that prevents additional properties from overwriting constrained declared properties, and rejection of numeric intervals with no value at the supported decimal precision.The text layer owns compilation, the frontend lazily caches compiled grammars with a bounded cache, and each request owns its matcher state. Output preview uses a fork; only Engine publication commits the matcher. Grammar state is not restored from KV/prefix caches. Serving parses formats into public execution options and rejects unsupported combinations before generation. CLI and serving expose the requested format/schema as a leading instruction before preparation, preserving caller content and cache boundaries. This guides raw JSON generation when tools remain available; the grammar enforces validity independently.
The model frontend composes its reasoning delimiter and native Qwen tool envelope with the final schema into one matcher. A speculative block may cross the reasoning boundary without a separate phase switch. Forced thinking-budget closure advances the matcher through the same preview/commit transaction. Tool names are declared-only, and optional non-strict parameters follow the declaration order shown in the prompt.
Sampling and speculative execution
Apply token masks before penalties and target top-k/top-p/min-p filtering. Each speculative position uses the grammar state conditioned on its preceding draft prefix, including correction and bonus positions. Draft distributions remain unchanged; stochastic acceptance uses the constrained target probability in
min(1, p/q), with residual sampling from normalizedmax(p-q, 0). Rejected drafts do not advance committed grammar state.Program-owned persistent GPU masks and pinned staging buffers support compact batches, lane reuse, and CUDA graph replay. MTP uses host-known drafts; the shared DFlash/DFlash2 path captures a device-to-host draft copy, a host matcher node, and a host-to-device mask copy before verification. Callback errors surface after synchronization and before publishing output. Existing terminal-prefix commit and model-state handling remain in place.
Contract and tradeoffs
anyOf, and local recursive references. Unsupported keywords, includingmultipleOf, patterns/formats,oneOf/allOf, uniqueness, and conditionals, return HTTP 400. Exact restrictions are documented indocs/serving.md.Verification
Tested on an RTX 5090 with CUDA 13.1, a Release
sm_120abuild, and Python 3.11. Live inference used an existing Qwen3.8-27B NVFP4 Swift artifact containing MTP and DFlash2 weights.--jsoninvocation produced parseable JSON.git diff --checkpassed.2.6.1.dev65+g6be685f0e, TC-64 through TC-69, using Qwen3.8-27B with MTP-5, temperature 1.0, seed 42, top-p 0.95, top-k 20, thinking/preserve-thinking enabled, medium reasoning, and a 16,384-token limit: TC-64/65/66/67/69 passed 2/2 each. TC-68 failed 0/2; it deliberately omitsresponse_formatand tests prompt-only adherence. Total: 10/12 (83%). This is a six-scenario rerun, not a new full-benchmark score. All 224 installed benchmark Python files were verified unchanged.Build and focused-test reproduction:
Live-test reproduction, with Python 3.11 plus
requestsandjsonschema, and the explicit local artifact path substituted as needed:Unverified: end-to-end legacy DFlash inference, because no compatible artifact was available. Its shared mask-stage integration and operator-level acceptance/correction semantics are covered, but that is not a substitute for a real legacy DFlash run. No end-to-end performance comparison was conducted.
origin/devwas inspected and has no commits ahead of the targetmasterat submission.