Skip to content

[Bugfix][Structured Output] Stop XGrammar token batches at termination - #52805

Merged
sfeng33 merged 3 commits into
vllm-project:mainfrom
sfeng33:xgrammar-fix
Aug 18, 2026
Merged

sfeng33 merged 3 commits into
vllm-project:mainfrom
sfeng33:xgrammar-fix

Conversation

@sfeng33

@sfeng33 sfeng33 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Prevent XGrammar from receiving additional tokens after accepting a terminating stop token within the same batch.
Fixes #52767.

This covers two related problems:

  1. accept_tokens() could return early after rejection without synchronizing LLM's cached _is_terminated state.
  2. Batched speculative acceptance and validation could continue past a terminating token, producing XGrammar warnings and possible FSM state inconsistencies.

Test Plan

Unit test:

pytest tests/v1/spec_decode/test_mtp_structured_output.py

Live model test:

vllm serve Qwen/Qwen3.8-27B     
--tensor-parallel-size 2     
--enable-auto-tool-choice     
--tool-call-parser qwen3_coder     
--reasoning-parser qwen3     
--mm-encoder-tp-mode data     
--speculative-config '{"method":"mtp","num_speculative_tokens":3}
Scenario How the bug was triggered Before fix After fix
Tokens after termination Aligned EOS with MTP draft slot 1 or 2, leaving additional draft tokens after EOS Matcher processed tokens after termination and emitted warnings 20/20 requests returned HTTP 200 without matcher errors
Second advance after termination Used strict JSON with ignore_eos=true to force another grammar advance after EOS HTTP 500 5/5 requests returned HTTP 200
Normal tool calling Sent strict-tool requests using tool_choice=auto and required Baseline behavior 10/10 requests returned correct tool calls

Signed-off-by: sfeng33 <4florafeng@gmail.com>
Signed-off-by: sfeng33 <4florafeng@gmail.com>
@sfeng33
sfeng33 marked this pull request as ready for review August 18, 2026 18:30

@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.

@russellb russellb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review

Reviewed with the patch applied locally. Verification: both new tests fail on main (accept_tokens('mtp_req', [50256, 198]) returns False; xgrammar logs trying to accept new token with id 198) and pass with the fix. With the patch, tests/v1/spec_decode/test_mtp_structured_output.py (18 passed) and tests/v1/structured_output/ (59 passed) are green. I also checked the assumption the new validate_tokens depends on: in xgrammar 0.2.1, rollback(1) correctly undoes a stop-token acceptance (is_terminated() goes True -> rollback -> False -> re-accept works), so the break-then-rollback is safe.

The approach looks right. It brings xgrammar in line with backend_guidance.py, which already returns True from accept_tokens and [] from validate_tokens once stopped. Moving the _is_terminated sync inside the loop makes the desync structurally impossible rather than patching it after the fact, and the reset() change is a real latent bug fix (today a reset matcher stays flagged as terminated forever).

One issue outside the diff:

The overlapping open PR is not disclosed. #37506 has been open since March, touches the same two files, and addresses the same desync (syncing _is_terminated on the failed-accept path rather than preventing the overshoot). On that thread @chubby-cutie proposed essentially this approach. AGENTS.md asks that you explain the difference when an open PR already covers the fix. This looks like the better fix and should supersede #37506, but saying so lets a maintainer close the other one.

Minor scope note: backend_outlines.py and backend_lm_format_enforcer.py have no equivalent guard, and #52767's reporters hit this across many models. Keeping the fix xgrammar-only is reasonable, just worth stating.

Three inline comments below.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread vllm/v1/structured_output/backend_xgrammar.py
Comment thread vllm/v1/structured_output/backend_xgrammar.py
Comment thread tests/v1/spec_decode/test_mtp_structured_output.py Outdated

@russellb russellb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

agent comments were very minor. I'm happy with this when you're ready to merge it

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

sfeng33 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #84456 for commit dd25c941229b.

@sfeng33
sfeng33 enabled auto-merge (squash) August 18, 2026 20:02
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 18, 2026
@sfeng33
sfeng33 merged commit 12f64b3 into vllm-project:main Aug 18, 2026
83 checks passed
@sfeng33
sfeng33 deleted the xgrammar-fix branch August 18, 2026 21:22
@gaby

gaby commented Aug 18, 2026

Copy link
Copy Markdown

@sfeng33 Thank you!

yzong-rh added a commit to yzong-rh/vllm that referenced this pull request Aug 19, 2026
Signed-off-by: Yifan Zong <yzong@redhat.com>
voipmonitor added a commit to voipmonitor/vllm that referenced this pull request Aug 20, 2026
Stop accepting speculative token batches when the grammar matcher reaches its terminal state. Preserve terminal-state tracking across validation and acceptance calls so tokens after a complete structured value cannot be committed.

This is the Infernal Invocation backport of vllm-project#52805 commits d8cde608cf1f3de406c75f081a76a0e6eb55a9cb, 1cf6f25351357354cf8c520c0b2976b029429668, and 1856abd22452c3da67364986ece7245fce52c950.

Signed-off-by: Martin Vit <martin@voipmonitor.org>
voipmonitor added a commit to voipmonitor/vllm that referenced this pull request Aug 20, 2026
Stop accepting speculative token batches when the grammar matcher reaches its terminal state. Preserve terminal-state tracking across validation and acceptance calls so tokens after a complete structured value cannot be committed.

This is the Infernal Invocation backport of vllm-project#52805 commits d8cde608cf1f3de406c75f081a76a0e6eb55a9cb, 1cf6f25351357354cf8c520c0b2976b029429668, and 1856abd22452c3da67364986ece7245fce52c950.

Signed-off-by: Martin Vit <martin@voipmonitor.org>
zyp2014 pushed a commit to zyp2014/vllm that referenced this pull request Aug 21, 2026
wyettzeng pushed a commit to wyettzeng/vllm that referenced this pull request Aug 21, 2026
vllm-project#52805)

Signed-off-by: sfeng33 <4florafeng@gmail.com>
Signed-off-by: Wyett <wyettzeng@gmail.com>
yzong-rh added a commit to yzong-rh/vllm that referenced this pull request Aug 21, 2026
Signed-off-by: Yifan Zong <yzong@redhat.com>
zufangzhu pushed a commit to zufangzhu/vllm that referenced this pull request Aug 24, 2026
vllm-project#52805)

Signed-off-by: sfeng33 <4florafeng@gmail.com>
Signed-off-by: Zhu, Zufang <zufang.zhu@intel.com>
kevinhirsch added a commit to kevinhirsch/vLLM-2080Ti-Definitive-Kevin that referenced this pull request Aug 24, 2026
…llm-project/vllm#52805)

XgrammarGrammar.accept_tokens fed every token in a batch to the FSM
matcher even after a terminating token (e.g. EOS landing in an MTP
draft slot, not necessarily the batch's last token) had already
terminated it -- producing "trying to accept new token" errors for the
post-termination tokens and returning False (a hard failure) once
_is_terminated was already set on a later call, instead of treating
post-termination tokens as a benign MTP artifact. validate_tokens had
the same gap (kept validating past a mid-batch termination instead of
stopping) and never short-circuited when the grammar was already
terminated. reset() also never cleared _is_terminated, so a request ID
reused for a fresh sequence could stay wedged "terminated".

Ported upstream's fix verbatim -- our backend_xgrammar.py's
XgrammarGrammar (accept_tokens/validate_tokens/reset) matches
upstream's pre-#52805 state closely enough for a direct line-level
port, no adaptation needed:
  * accept_tokens: return True (not False) once already terminated;
    move the is_terminated() check inside the per-token loop and break
    as soon as it fires, instead of running the whole batch through an
    already-terminated matcher.
  * validate_tokens: early-return [] if already terminated; break out
    of the validation loop as soon as the matcher terminates mid-batch.
  * reset(): also reset _is_terminated to False.

The PR author's own live-model test command matches this fork's
deployment shape almost exactly: Qwen3.8-27B, --tensor-parallel-size 2,
--enable-auto-tool-choice --tool-call-parser qwen3_coder
--reasoning-parser qwen3, MTP with num_speculative_tokens=3.

Upstream's own new tests live in
tests/v1/spec_decode/test_mtp_structured_output.py, which doesn't
exist in this tree (no prior xgrammar/structured-output test coverage
here at all) and depends on fixtures/helpers not present. Added
tests/v1/structured_output/test_xgrammar_termination.py instead:
exercises the real XgrammarGrammar dataclass directly (no CUDA
dependency in these methods) via a minimal duck-typed stand-in for
xgr.GrammarMatcher, rather than a real compiled grammar/tokenizer --
avoids depending on the installed xgrammar version's compile API while
still driving the actual production state-machine code. Verified both
new tests fail against the pre-fix code (num_processed_tokens == 2
instead of 1; validate_tokens returns [EOS, TRAILING] instead of
[EOS]) and pass after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
khushali9 pushed a commit to khushali9/vllm that referenced this pull request Aug 29, 2026
vllm-project#52805)

Signed-off-by: sfeng33 <4florafeng@gmail.com>
Signed-off-by: khushali9 <khushali.desai9@gmail.com>
am-cohere pushed a commit to am-cohere/vllm that referenced this pull request Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ready ONLY add when PR is ready to merge/full CI is needed speculative-decoding structured-output

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: MTP spec decode still advances the grammar matcher after termination when a structural tag is built (residual after #44297)

3 participants