Skip to content

[Bugfix][Structured Output] Mask request stop tokens in xgrammar until grammar terminates - #49227

Merged
sfeng33 merged 2 commits into
vllm-project:mainfrom
sfeng33:fix-stop
Aug 10, 2026
Merged

[Bugfix][Structured Output] Mask request stop tokens in xgrammar until grammar terminates#49227
sfeng33 merged 2 commits into
vllm-project:mainfrom
sfeng33:fix-stop

Conversation

@sfeng33

@sfeng33 sfeng33 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Fixes #42403.

With structured outputs (e.g. JSON schema), a model can sample a stop token while the grammar FSM is still mid-object, truncating generation into invalid JSON. On Gemma this is <end_of_turn> (id 106).

Root cause: xgrammar only knows the tokenizer's single eos, but a request's real stop set is generation_config's eos list plus any user stop_token_ids. On Gemma-4 that gap is concrete — generation_config eos is [1, 106, 50] while the tokenizer eos is just <eos> (1). Tokens 106/50 are invisible to the grammar, so they escape the bitmask in non-terminal states.

Fix

Forward the request's sampling_params.all_stop_token_ids through compile_grammar(...) into xgrammar's native
GrammarMatcher(override_stop_tokens=...). xgrammar then gates every one of the request's stop tokens to the grammar-terminal state — masked mid-object, allowed only once the schema is satisfied.

Test Plan

 vllm serve google/gemma-4-31B-it \
  --served-model-name gemma4 \
  --structured-outputs-config '{"backend":"xgrammar"}' 

Force <end_of_turn> (id 106) via logit_bias + greedy:

from openai import OpenAI
c = OpenAI(base_url="http://localhost:8000/v1", api_key="x")
schema = {"type":"object","properties":{"name":{"type":"string"},
          "bio":{"type":"string"}},"required":["name","bio"]}
r = c.chat.completions.create(
    model="gemma4", temperature=0, max_tokens=128, logit_bias={"106": 100},
    messages=[{"role":"user","content":"Return a person as JSON: name and a one-line bio."}],
    extra_body={"structured_outputs": {"json": schema}})
print(r.choices[0].message.content, "| stop_reason:", r.choices[0].stop_reason)

Test Result

Output valid JSON
before {"name": " (truncated, stop_reason=106)
after {"name": "Elara Vance", "bio": "..."} (stop_reason=106)

@mergify mergify Bot added structured-output v1 bug Something isn't working labels Jul 20, 2026
@sfeng33
sfeng33 marked this pull request as ready for review July 20, 2026 16:51

@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 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @sfeng33.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 23, 2026

@yzong-rh yzong-rh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Reproduces cleanly, test passes.

The issue occur in guidance as well, but it doesn't seem mask out stop tokens even if we override eos_token in llguidance_hf.from_tokenizer. Separate fix required.

Signed-off-by: sfeng33 <4florafeng@gmail.com>
Signed-off-by: sfeng33 <4florafeng@gmail.com>
@sfeng33

sfeng33 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #83175 for commit c2b5c3268ca4.

@bbrowning bbrowning left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pulled locally, reviewed, and confirmed the new test properly catches this regression. Looks good!

@sfeng33
sfeng33 merged commit 05f0a80 into vllm-project:main Aug 10, 2026
81 checks passed
@sfeng33
sfeng33 deleted the fix-stop branch August 10, 2026 18:29
bbrowning added a commit to bbrowning/upstream-city that referenced this pull request Aug 10, 2026
…e README path

Backlog note in the pack README ("Growing up (later)"): let `gc dev-pack review`
(and materialize) accept a rig-carrying PR spec so --rig isn't needed —
`vllm#49227` (<rig>#<PR> split) and a full GitHub PR URL
(vllm-project/vllm#49227 → map owner/repo to the local rig
by matching each rig's origin remote). Bare-number + --rig stays as the override.

Also fix a stale post-rename path in the "More reviewers" bullet
(agents/reviewer/ -> agents/pr-reviewer/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: (Gemma 4) xgrammar bitmask lets <end_of_turn> escape during structured outputs, terminating generation mid-JSON

3 participants