Structured outputs: JSON schemas, grammars and token bitmasks in vLLM
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
vLLM compiles a request's constraint into a grammar in the engine core. Every step then masks the logits, so only allowed tokens can be sampled.
The idea#
Structured outputs force the answer into a format. Examples are valid JSON for a schema, a string that matches a regex, one of a list of choices, or text of a grammar. The method is a token mask. At each step, a grammar state machine says which tokens may come next, and the sampler sees -inf for all others (Structured outputs: token masks). This page shows how vLLM splits that work between its processes.
The API server turns the request fields into one constraint and picks a structured output backend for it. The engine core compiles the constraint into a grammar matcher, in a thread pool, while the request waits. In every step, the engine core fills a grammar bitmask row for each constrained request; the worker applies it to the logits on the GPU. After the step, the engine core feeds the sampled token to the matcher.
An everyday comparison. Think of a form with boxes that accept only digits. Before the clerk starts, someone prints the form (compile). For every character, the form shows which keys still work (the bitmask), and the clerk can press only those. The comparison stops being true in one place: vLLM's clerk may write freely for a while first. While a reasoning model thinks, the mask allows every token; the form starts only after the thinking ends.
The picture#
Read the panels from top to bottom; each panel runs in a different place. Blue is the API server, violet and teal are the engine core process, amber is the worker on the GPU. The parameter strip shows the bitmask size for a 151,936-token vocabulary: 4,748 int32 words, 18,992 bytes per row.
In the code#
Step 1: the request fields#
Three fields lead to a constraint. All end in one StructuredOutputsParams inside SamplingParams:
response_format(OpenAI).json_objectbecomesjson_object=True;json_schemabecomesjson=<schema>;structural_tagstays a structural tag.textmeans no constraint (vllm/entrypoints/generate/base/protocol.py:181-211). Ajson_schemawithout the schema gives HTTP 400 (vllm/entrypoints/openai/chat_completion/protocol.py:827-838).structured_outputs(vLLM extra field) with exactly one ofjson,regex,choice,grammar,json_object,structural_tag(vllm/sampling_params.py:102-141).response_formatoverrides the matching fields when both are set (vllm/entrypoints/generate/base/protocol.py:208-211).tool_choice"required"or a named function: the tool parser writes a constraint intostructured_outputsduring rendering (Tool and reasoning parsers: from model text to tool_calls and reasoning).
The sampling parameters are made with structured_outputs=self.extract_structured_outputs() for chat and for completions (vllm/entrypoints/openai/chat_completion/protocol.py:801, vllm/entrypoints/openai/completion/protocol.py:427).
Step 2: StructuredOutputsConfig and the backend choice#
StructuredOutputsBackend = Literal[
"auto", "xgrammar", "guidance", "outlines", "lm-format-enforcer"
]
@config
class StructuredOutputsConfig:
"""Dataclass which contains structured outputs config for the engine."""
backend: StructuredOutputsBackend = "auto"
"""Which engine will be used for structured outputs (e.g. JSON schema,
regex, etc) by default. With "auto", we will make opinionated choices
based on request contents and what the backend libraries currently support,
so the behavior is subject to change in each release."""
disable_any_whitespace: bool = False
"""If `True`, json output will always be compact without any whitespace.
If `False`, the model may generate whitespace between JSON fields,
which is still valid JSON. This is only supported for xgrammar
and guidance backends."""
disable_additional_properties: bool = False
"""If `True`, the `guidance` backend will not use `additionalProperties`
in the JSON schema. This is only supported for the `guidance` backend and
is used to better align its behaviour with `outlines` and `xgrammar`."""
reasoning_parser: str = ""
"""Select the reasoning parser depending on the model that you're using.
This is used to parse the reasoning content into OpenAI API format."""
reasoning_parser_plugin: str = ""
"""Path to a dynamically reasoning parser plugin that can be dynamically
loaded and registered."""
enable_in_reasoning: bool = False
"""Whether to use structured input for reasoning."""The backend is set with --structured-outputs-config.backend; the default is auto. A search of vllm/ finds no other assignment to this field; EngineArgs merges only the reasoning parser into this config (vllm/engine/arg_utils.py:2187-2204). The choice happens per request, when InputProcessor validates the sampling parameters inside AsyncLLM.add_request (vllm/v1/engine/input_processor.py:157-162):
else:
# NOTE: backend must be "auto" here, because we have
# checked supported_backends above.
# In this mode, we set opinionated defaults based on what we think
# will satisfy the most use cases without having to worry about
# this setting. We include fallback behavior here, but not with any
# other setting where a specific backend was specified.
try:
validate_xgrammar_grammar(self)
self.structured_outputs._backend = "xgrammar"
except VLLMValidationError:
# The request either failed validation
# or includes some jsonschema feature(s) that
# are not supported in xgrammar.
skip_guidance = _is_non_tekken_mistral(tokenizer)
# Check if schema has features unsupported by guidance
so_params = self.structured_outputs
if not skip_guidance and so_params.json:
if isinstance(so_params.json, str):
try:
schema = json_mod.loads(so_params.json)
except json_mod.JSONDecodeError as e:
raise VLLMValidationError(
"Invalid JSON grammar specification."
) from e
else:
schema = so_params.json
skip_guidance = has_guidance_unsupported_json_features(schema)
if skip_guidance:
# Fall back to outlines for non-Tekken Mistral tokenizers or
# schemas containing features unsupported by guidance.
validate_structured_output_request_outlines(self)
self.structured_outputs._backend = "outlines"
else:
# Fall back to guidance by default.
if not GuidanceBackend.is_tokenizer_supported(tokenizer):
raise VLLMValidationError(
"No compatible structured output backend was found for "
"this request. XGrammar rejected the constraint, and "
"Guidance does not support this tokenizer. Configure a "
"fast tokenizer or select a compatible backend explicitly."
) from None
validate_guidance_grammar(
self,
tokenizer=_get_llg_tokenizer(tokenizer),
)
self.structured_outputs._backend = "guidance"
# Remember that this backend was set automatically
self.structured_outputs._backend_was_auto = True- Lines 1397-1399:
autotries xgrammar first. If xgrammar accepts the constraint, the request uses xgrammar. - Lines 1405-1425: else outlines, for a non-Tekken Mistral tokenizer or a JSON schema with features that guidance does not support.
- Lines 1427-1439: else guidance, if the tokenizer is a fast Hugging Face tokenizer; else HTTP 400.
- Line 1441:
_backend_was_autoremembers that the choice was automatic.
A Tekken Mistral tokenizer with a Lark grammar goes to guidance directly (vllm/sampling_params.py:1374-1389). A fixed backend (not auto) validates with that backend only and has no fallback (vllm/sampling_params.py:1336-1373).
Step 3: compile in the engine core, wait in the scheduler#
The engine core's input thread turns the EngineCoreRequest into a Request and starts the grammar at once (vllm/v1/engine/core.py:1128-1135). A request with a constraint starts in the status WAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR, not WAITING (vllm/v1/request.py:112-117).
request_backend = structured_outputs._backend
assert request_backend is not None
if self.backend_name is not None and request_backend != self.backend_name:
raise VLLMValidationError(
"Structured output processing only supports one backend per engine. "
f"The engine is already using '{self.backend_name}', but this "
f"request resolved to '{request_backend}'. Configure "
"`structured_outputs_config.backend` explicitly or use schemas "
"supported by the initialized backend."
)
# Initialize the backend the first time it is needed.
#
# NOTE: We only support a single backend. We do NOT support different
# backends on a per-request basis in V1 (for now, anyway...).
# _backend is set in Processor._validate_structured_output
if self.backend is None:
vocab_size = self.vllm_config.model_config.get_vocab_size()
if request_backend == "xgrammar":
self.backend = XgrammarBackend(
self.vllm_config,
tokenizer=self.tokenizer,
vocab_size=vocab_size,
)
elif request_backend == "guidance":
self.backend = GuidanceBackend(
self.vllm_config,
tokenizer=self.tokenizer,
vocab_size=vocab_size,
)
elif request_backend == "outlines":
from vllm.v1.structured_output.backend_outlines import OutlinesBackend
self.backend = OutlinesBackend(
self.vllm_config,
tokenizer=self.tokenizer,
vocab_size=vocab_size,
)
elif request_backend == "lm-format-enforcer":
from vllm.v1.structured_output.backend_lm_format_enforcer import ( # noqa: E501
LMFormatEnforcerBackend,
)
self.backend = LMFormatEnforcerBackend(
self.vllm_config,
tokenizer=self.tokenizer,
vocab_size=vocab_size,
)
else:
raise ValueError(
f"Unsupported structured output backend: {request_backend}"
)
self.backend_name = request_backend
grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar
if self._use_async_grammar_compilation:
grammar = self.executor.submit(self._create_grammar, request)
else:
try:
grammar = self._create_grammar(request)
except Exception as e:
grammar = Future()
grammar.set_exception(e)
request.structured_output_request.grammar = grammar- Lines 143-150: the engine has one backend. A request with another backend than the one already built gets
VLLMValidationError. The input thread finishes the request with a validation error, which the client sees as HTTP 400, or as an error chunk in a stream (vllm/v1/engine/core.py:1999-2005,vllm/v1/engine/core.py:2158-2167). - Lines 157-193: the first constrained request builds the backend.
- Lines 196-197: the compile runs in a
ThreadPoolExecutorwith(CPU count + 1) // 2workers (vllm/v1/structured_output/__init__.py:95-96); the request keeps aFuture. With theexternal_launcherexecutor the compile runs at once instead, so that every rank switches state at the same time (vllm/v1/structured_output/__init__.py:65-74).
The scheduler's waiting pass skips the request until the Future is done (vllm/v1/core/sched/scheduler.py:3291-3303). Then the status becomes WAITING, and the request is scheduled like any other. A compile error finishes the request with FINISHED_ERROR and a validation_error, which the front end turns into VLLMValidationError (vllm/v1/core/sched/scheduler.py:2399-2425, vllm/outputs.py:30-35).
The xgrammar backend keeps two compilers with a cache. One is for JSON, regex, choice and EBNF grammars; it never allows special tokens. The other is for structural tags and Lark grammars, which may need special tokens (vllm/v1/structured_output/backend_xgrammar.py:84-112). The cache is limited by VLLM_XGRAMMAR_CACHE_MB, 512 MB by default (vllm/v1/structured_output/backend_xgrammar.py:215-224, vllm/envs.py:1689). So the same schema compiles fast the second time (cache inside xgrammar; library behaviour).
Step 4: the bitmask of each step#
In the default engine loop, the engine core schedules a step, starts the forward pass, and then asks for the bitmask while the GPU runs (vllm/v1/engine/core.py:724-745):
def get_grammar_bitmask(
self, scheduler_output: SchedulerOutput
) -> GrammarOutput | None:
# Collect list of scheduled request ids that use structured output.
# The corresponding rows of the bitmask will be in this order.
if not scheduler_output.has_structured_output_requests:
return None
structured_output_request_ids = [
req_id
for req_id in scheduler_output.num_scheduled_tokens
if (req := self.requests.get(req_id))
and (req.use_structured_output and not req.is_prefill_chunk)
]
if not structured_output_request_ids:
return None
bitmask = self.structured_output_manager.grammar_bitmask(
self.requests,
structured_output_request_ids,
scheduler_output.scheduled_spec_decode_tokens,
)
spec_tokens = scheduler_output.scheduled_spec_decode_tokens
num_acceptable_drafts = [
len(strip_speculative_padding(spec_tokens.get(req_id, [])))
for req_id in structured_output_request_ids
]
return GrammarOutput(
structured_output_request_ids, bitmask, num_acceptable_drafts
)Only requests that sample in this step get a row. A chunk of a long prefill that is not the last one gets none (line 2098). grammar_bitmask fills the rows (vllm/v1/structured_output/__init__.py:382-531):
- The bitmask has
max_num_seqs × (1 + num_speculative_tokens)rows, allocated once (lines 397-406). Each structured request ownsmask_striderows: one per draft position plus one for the bonus token (line 395). - A request that is still thinking gets a full row of ones: no constraint (lines 241-247 and 466-469).
- A request in its format gets the matcher's mask for the next token (line 242).
- Above 128 structured requests and without speculative decoding, up to 8 threads fill the rows in batches of 16 (lines 79-87 and 416-448).
The result goes to the worker as a GrammarOutput with a numpy array (vllm/v1/structured_output/__init__.py:523-531).
Step 5: masking on the worker#
Model Runner V2 applies the mask after compute_logits and before the sampler (The model runner (Model Runner V2)):
sample_hidden_states = hidden_states[input_batch.logits_indices]
logits = self.model.compute_logits(sample_hidden_states)
invalid_drafts = None
# A diffusion prefill has no logit rows even when a bitmask row
# arrived for it.
if grammar_output is not None and logits.shape[0] > 0:
# Apply grammar bitmask to the logits in-place.
assert self.structured_outputs_worker is not None
self.structured_outputs_worker.apply_grammar_bitmask(
logits,
input_batch,
grammar_output.structured_output_request_ids,
grammar_output.grammar_bitmask,
)
invalid_drafts = grammar_invalid_drafts(
input_batch,
grammar_output.structured_output_request_ids,
grammar_output.num_acceptable_drafts,
)
sampler_output: SamplerOutput | None
if input_batch.num_reqs == 0:
# This rank owns no requests this step. It contributes an
# all-padding block to the gather below.
sampler_output = None
elif input_batch.num_draft_tokens == 0 or self.rejection_sampler is None:
assert self.sampler is not None
sampler_output = self.sampler(logits, input_batch)StructuredOutputsWorker keeps an int32 [max_num_logits, cdiv(vocab_size, 32)] buffer on the GPU (vllm/v1/worker/gpu/structured_outputs.py:60-68). It copies the rows on a side CUDA stream, then a Triton kernel writes -inf into each logit whose bit is 0 (vllm/v1/worker/gpu/structured_outputs.py:80-124, vllm/v1/worker/gpu/structured_outputs.py:162-171). The sampler then sees only allowed tokens.
Step 6: after the step#
update_from_output feeds the new tokens to the matcher (vllm/v1/core/sched/scheduler.py:2279-2290):
def accept_tokens(self, request: "Request", new_token_ids: list[int]) -> bool:
"""Advance grammar with accepted tokens. Returns False on rejection."""
if not request.use_structured_output or not new_token_ids:
return True
structured_req = request.structured_output_request
if TYPE_CHECKING:
assert structured_req is not None
grammar = structured_req.grammar
if TYPE_CHECKING:
assert isinstance(grammar, StructuredOutputGrammar)
grammar_start, _ = self._get_constraint_bounds(
request, new_token_ids, spec_tokens_committed=True
)
# Early return only when no reasoning-end token is present.
# Otherwise, latch structured_req.reasoning_ended.
if grammar_start > len(new_token_ids):
return True
structured_req.reasoning_ended = True
return grammar.accept_tokens(request.request_id, new_token_ids[grammar_start:])Tokens before the end of the thinking are not fed. When the end token is among the new tokens, reasoning_ended is latched, and the grammar starts after it (lines 545-552). If the matcher rejects a token, the request finishes with an error; the log says "Unexpected", because the mask should make this impossible (vllm/v1/core/sched/scheduler.py:2282-2290). The matcher is built with the request's stop tokens (vllm/v1/structured_output/__init__.py:216-223, vllm/v1/structured_output/backend_xgrammar.py:198-203). Two parts are library behaviour of xgrammar. At the end of the format, the mask allows a stop token. After the matcher accepts a stop token, it reports is_terminated(). vLLM then stops feeding the matcher, and any later row would be a full row (vllm/v1/structured_output/backend_xgrammar.py:271-286, vllm/v1/structured_output/__init__.py:241-247).
Step 7: async scheduling, speculative decoding and reasoning#
- Async scheduling. The bitmask for step k+1 needs the token of step k, but with async scheduling that token may still be on the GPU. The scheduler sets
pending_structured_output_tokensfor such a step (vllm/v1/core/sched/async_scheduler.py:37-39). The engine core still starts the forward pass of step k+1. But it callssample_tokensonly after it has processed the output of step k (vllm/v1/engine/core.py:737-749,vllm/v1/engine/core.py:789-810). The flag is for the whole batch. So one structured request in decode delays the sampling of every request in that step (derived from line 37). See Async scheduling: overlap the CPU and the GPU. - Speculative decoding. Draft tokens are checked against the grammar before they are scheduled; the longest valid prefix stays, and the rest becomes
-1padding (vllm/v1/core/sched/scheduler.py:2663-2666,vllm/v1/core/sched/scheduler.py:2691-2699). The bitmask has one row per draft position; the manager advances the matcher through the drafts to fill the rows and rolls it back afterwards (vllm/v1/structured_output/__init__.py:476-521). The matcher allows a rollback ofnum_speculative_tokenstokens (vllm/v1/structured_output/backend_xgrammar.py:198-203). - Reasoning. With a reasoning parser, the grammar starts only after the thinking ends, unless
enable_in_reasoningis set (vllm/v1/structured_output/__init__.py:300-319). The front end tells the engine core whether the prompt already ended the thinking. Withreasoning_endedunset, the engine core checks the prompt with its own parser (lines 312-319). Some engine-based parsers end the thinking with a content token, for example a tool-call start. Such a token stays masked during the thinking if the grammar cannot start with it (vllm/v1/structured_output/__init__.py:258-281).
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: one JSON-schema request, step by step
Server: vllm serve Qwen/Qwen2.5-0.5B-Instruct on one GPU, default flags (max_num_seqs from the GPU size, async scheduling on, no reasoning parser). Request (example):
{"messages": [{"role": "user", "content": "Give me a city as JSON."}],
"response_format": {"type": "json_schema",
"json_schema": {"name": "city", "schema": {"type": "object",
"properties": {"name": {"type": "string"}}, "required": ["name"]}}}}
- API server.
to_sampling_paramsmakesStructuredOutputsParams(json={...schema...})(vllm/entrypoints/generate/base/protocol.py:192-195). Inadd_request, the input processor validates with xgrammar; it accepts, so_backend = "xgrammar"and_backend_was_auto = True. - Input thread.
Requeststarts asWAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR. This is the first constrained request, so the manager buildsXgrammarBackend: two compilers, each withmax_threads=8(vllm/v1/structured_output/backend_xgrammar.py:215-224). The compile job goes to the pool. - Scheduler. While the
Futureruns, each waiting pass skips the request. When it is done, the request becomesWAITINGand is admitted. - Prefill step. The whole prompt fits into one step, so the request is not a partial prefill chunk; it gets bitmask row 0. The matcher allows only tokens that can start a JSON object of the schema; every other token gets
-inf. - Size of the mask. The Qwen2.5 vocabulary has 151,936 entries (inferred from its
config.json). One row iscdiv(151936, 32)= 4,748 int32 words = 18,992 bytes. With 3 structured requests in a step, the array isint32 [3, 4748], about 57 KB, copied to the GPU once per step. - Decode steps. With async scheduling, the next step is scheduled while the token of this step is in flight, so
pending_structured_output_tokensis true. The engine core starts the forward pass and processes the previous output, which feeds the token to the matcher. Only then does it fill the row and sample. - End. After
{"name": "Paris"}, the format is complete. The mask then allows a stop token of the request (here<|im_end|>), and the request finishes withfinish_reason: "stop"when the model samples it.
Deep diveCosts: where structured outputs spend time
| Cost | Where | When | Lines |
|---|---|---|---|
| validation of the constraint | API server, in add_request | every request | vllm/sampling_params.py:1336-1441 |
| grammar compile | engine core thread pool | per request; repeat schemas hit the xgrammar cache | vllm/v1/structured_output/__init__.py:195-204 |
| wait for the compile | scheduler | until the Future is done: adds to TTFT | vllm/v1/core/sched/scheduler.py:3291-3295 |
| fill the bitmask | engine core main thread (threads above 128 requests) | every step with constrained requests | vllm/v1/structured_output/__init__.py:416-521 |
| copy and apply the mask | worker, GPU | every step | vllm/v1/worker/gpu/structured_outputs.py:70-128 |
| deferred sampling | engine core loop | async scheduling with a constrained request in decode | vllm/v1/engine/core.py:737-749 |
| advance the matcher | engine core, update_from_output | every new token | vllm/v1/structured_output/__init__.py:533-553 |
The code gives no measured numbers for these costs. Measure with and without the constraint on your own workload (vllm bench serve, Benchmarking vLLM: vllm bench serve, throughput, latency and startup).
Deep diveErrors: what fails where
| Situation | Where | Result |
|---|---|---|
two of json, regex and choice in one request | request validator | 400 (vllm/entrypoints/openai/chat_completion/protocol.py:931-945) |
a constraint and a named tool_choice | request validator | 400 (vllm/entrypoints/openai/chat_completion/protocol.py:946-955) |
| empty choice list, empty grammar or schema, NUL in a regex | input processor | 400 (vllm/sampling_params.py:1266-1308) |
| no backend accepts the constraint (auto) | input processor | 400 (vllm/sampling_params.py:1428-1434) |
a model loaded without a tokenizer (--skip-tokenizer-init) | input processor | 400 (vllm/sampling_params.py:1242-1245) |
| request resolves to another backend than the engine's | engine core input thread | validation error: 400, or an error chunk in a stream (vllm/v1/structured_output/__init__.py:143-150) |
| compile fails | engine core thread pool | request finished with a validation error (vllm/v1/core/sched/scheduler.py:3296-3301) |
| the matcher rejects a sampled token | update_from_output | request finished with an error, logged as unexpected (vllm/v1/core/sched/scheduler.py:2279-2290) |
old guided_* field | pydantic model | no error, no constraint, a warning in the log |
Deep diveFlags and environment variables
| Setting | Default | Effect |
|---|---|---|
--structured-outputs-config.backend | auto (vllm/config/structured_outputs.py:20) | Fix one backend; no fallback then. |
--structured-outputs-config.disable_any_whitespace | false (vllm/config/structured_outputs.py:25) | Compact JSON; only xgrammar and guidance, else ValueError at start-up (vllm/config/structured_outputs.py:62-66). |
--structured-outputs-config.disable_additional_properties | false | Guidance only. |
--structured-outputs-config.enable_in_reasoning | false (vllm/config/structured_outputs.py:40) | Constrain the thinking too. |
--reasoning-parser | "" | Lets the engine core find the end of the thinking. |
VLLM_XGRAMMAR_CACHE_MB | 512 (vllm/envs.py:1689) | Size limit of the xgrammar compile cache. The type stub at vllm/envs.py:226 says 0; the reader returns 512. |
What can go wrong#
- Unconstrained output with HTTP 200. The request uses
guided_jsonor another removed field (Step 1). - JSON appears only after a long thinking text. With a reasoning parser, the grammar starts after the thinking (Step 7). Set
enable_in_reasoningor turn the thinking off. - The JSON is cut off.
max_tokensended the request before the matcher reached a final state; checkfinish_reason: "length". - Slow first token for new schemas. The request waits for its compile (Step 3). Repeat schemas hit the cache.
- Lower throughput with async scheduling. One constrained decode request defers the sampling of the whole step (Step 7).
Why it matters. Structured outputs make agent and tool pipelines reliable, but they add a compile wait, a per-step CPU job and, with async scheduling, less overlap. Knowing where each part runs tells you what to tune.
Check yourself#
- 1Which field of the OpenAI API asks for JSON that matches a schema, and what does vLLM turn it into?easy
Show answer
response_formatwithtype: "json_schema". vLLM turns it intoStructuredOutputsParams(json=<schema>)(vllm/entrypoints/generate/base/protocol.py:192-195). - 2In which process and on which thread does the grammar compile, and what does the request do meanwhile?medium
Show answer
In the engine core process, in the
ThreadPoolExecutorof the structured output manager; the input thread submits the job (vllm/v1/engine/core.py:1128-1135,vllm/v1/structured_output/__init__.py:196-197). The request waits in the statusWAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR, and the scheduler skips it until theFutureis done (vllm/v1/core/sched/scheduler.py:3291-3303). - 3How large is one bitmask row for a vocabulary of 151,936 tokens? How many rows does a structured request own with 3 speculative tokens?medium
Show answer
cdiv(151936, 32)= 4,748 int32 words, 18,992 bytes (vllm/v1/worker/gpu/structured_outputs.py:63-65). With 3 speculative tokens,mask_stride= 3 + 1 = 4 rows per request (vllm/v1/structured_output/__init__.py:393-395). - 4The server uses async scheduling. A batch has 200 normal requests and 1 JSON-schema request in decode. What happens to the sampling of that step, and why?hard
Show answer
The step is scheduled while the previous token of the JSON request is still in flight, so the scheduler sets
pending_structured_output_tokensfor the step (vllm/v1/core/sched/async_scheduler.py:37-39). The engine core starts the forward pass but deferssample_tokensfor the whole batch. It first processes the previous step's output, then fills the bitmask and samples (vllm/v1/engine/core.py:737-749,vllm/v1/engine/core.py:789-810). The flag is per batch, so all 201 requests wait (derived). - 5A reasoning model withhard
--reasoning-parser deepseek_r1gets a JSON-schema request. Which tokens does the grammar see, and what is the bitmask while the model thinks?Show answer
While the thinking is not ended, the constraint bounds start after the thinking, so the rows are full rows: every token is allowed (
vllm/v1/structured_output/__init__.py:300-319,vllm/v1/structured_output/__init__.py:241-247).accept_tokensignores the tokens before the end; when</think>arrives, it latchesreasoning_endedand feeds only the tokens after it to the matcher (vllm/v1/structured_output/__init__.py:545-553).