07 — Structured / Guided Decoding¶
Constraining LLM output to a grammar (JSON schema, regex, EBNF) via per-step logit masking. Powers tool calling, function calling, JSON mode, and any “output must parse” workflow — a large fraction of your agentic day job traffic.
The problem¶
An LLM’s next-token distribution over the ~<phone_number_or_numberic_id_or_random_id_37>k vocab includes many
tokens that would be syntactically invalid given the current output
prefix. If we’re mid-way through a JSON object and just emitted "user":,
legal next tokens are things like ", {, [, 42, null — not
random prose tokens. Yet the raw model has nonzero probability on those
invalid tokens, and greedy sampling will occasionally pick them, breaking
your parser.
Solution: mask illegal tokens’ logits to −∞ before sampling. The model then samples only from legal continuations. Provably valid output, zero retry loops, zero “clean up the JSON” post-processing.
This is called guided decoding, structured output, or constrained decoding. All the same thing.
The three libraries¶
1. Outlines — Rémi Louf et al.¶
https://github.com/dottxt-ai/outlines. First mover in the modern era.
Mechanism: compile the grammar to a finite-state machine (FSM). States are grammar positions; transitions are token IDs. On each step, look up the current FSM state → set of allowed token IDs → mask.
Precomputation cost: compiling a JSON schema FSM can take seconds (indexes must be built over the full vocab per state). Runtime cost: O(1) per step after compilation.
Strength: elegant, well-tested, extensive schema support. Weakness: compilation time hurt cold start; couldn’t handle context-free grammars (only regular).
2. XGrammar — Yixin Dong et al.¶
arXiv: <phone_number_or_numberic_id_or_random_id_37>. Repo: https://github.com/mlc-ai/xgrammar. Current leader; used by SGLang and integrated in vLLM.
Two key innovations over Outlines:
a. Vocabulary is partitioned into:
Context-independent tokens — whose validity doesn’t depend on the full parse state (just on the last token or so). Precomputed once per grammar. This is the ~80-90% of the vocab.
Context-dependent tokens — whose validity requires runtime PDA (pushdown automaton) traversal. Only ~10-20% of the vocab.
At each step, XGrammar first applies the precomputed context-independent mask (cheap), then runs the PDA only over the context-dependent subset (also cheap because subset is small). Result: per-step overhead falls to ~1-10 μs — essentially free.
b. Adaptive Token Mask Cache — memoize the (grammar_state, recent_context) → mask mapping. Big win for grammars with repetitive states (JSON objects have many).
Supports context-free grammars (JSON schemas naturally become CFGs for nested structure), so it handles more expressive constraints than Outlines could originally.
XGrammar 2 (arXiv <phone_number_or_numberic_id_or_random_id_38>) adds JIT compilation, an Earley parser for dynamic grammars, and TagDispatch for agentic tool calling where the legal grammar depends on which tool was invoked (grammar switches mid-generation).
3. llguidance — Microsoft, used by Guidance library¶
https://github.com/microsoft/llguidance. Rust-based grammar engine with focus on low latency and lark-syntax grammars. Used inside Microsoft’s Guidance framework. Competitive with XGrammar on many benchmarks.
Comparison for your practical use¶
Feature |
Outlines |
XGrammar |
llguidance |
|---|---|---|---|
JSON schemas |
✓ |
✓ (fastest) |
✓ |
Regex |
✓ |
✓ |
✓ |
Context-free grammar |
Partial |
✓ |
✓ |
Per-step overhead |
~100 μs |
~1-10 μs |
~10-30 μs |
Compilation time |
Seconds |
Milliseconds |
Milliseconds |
In vLLM V1 |
✓ |
✓ (default) |
✓ |
In SGLang |
Partial |
✓ (native) |
Partial |
Dynamic grammar (mid-gen switch) |
No |
✓ (XGrammar 2) |
Limited |
In 2026, XGrammar is the default. Use it unless you have a specific reason not to.
How the mask actually applies (mechanism level)¶
In vLLM’s pipeline, structured decoding is a logit processor: after the model produces logits for the next token, but before sampling, apply:
def apply_grammar_mask(logits: Tensor, grammar_state: GrammarState) -> Tensor:
mask = grammar_state.get_allowed_token_mask(vocab_size) # bool tensor
logits.masked_fill_(~mask, -float("inf"))
return logits
Then normal sampling (temperature, top-p, etc.) runs on the masked logits. Because illegal tokens have logit −∞, softmax gives them probability 0. The sample is guaranteed legal.
After sampling, the grammar state advances by the chosen token:
grammar_state.advance(sampled_token)
That’s the entire runtime loop. All the complexity is in get_allowed_ token_mask and advance, which is where XGrammar’s context-
independent/dependent split does its magic.
FSM compilation: a JSON example¶
Say the schema is {"name": string, "age": integer}. Compile to an
FSM (simplified):
[start] --'{'--> [expect_key]
[expect_key] --'"name"'--> [expect_colon_name]
[expect_colon_name] --':'--> [expect_string_value]
[expect_string_value] --'"..."'--> [expect_comma_or_close]
[expect_comma_or_close] --','--> [expect_next_key]
[expect_next_key] --'"age"'--> [expect_colon_age]
[expect_colon_age] --':'--> [expect_int_value]
[expect_int_value] --int--> [expect_close]
[expect_close] --'}'--> [DONE]
At each state, the mask over the vocab is derived: which token IDs
start with a byte sequence that could match a legal edge out of this
state? (Handling BPE tokens crossing byte boundaries is the messy part
— a token like "na might be legal here but "n alone isn’t; the
FSM must handle token-level transitions, not character-level.)
This token-boundary problem is where Outlines and XGrammar do the heavy engineering. Handling it correctly for tens of thousands of token IDs against a schema is why the compilation used to take seconds — and why XGrammar’s precomputed context-independent mask is such a win.
Integration in vLLM V1¶
vLLM V1 supports structured output via the OpenAI-compatible API:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="...")
response = client.chat.completions.create(
model="...",
messages=[{"role": "user", "content": "Give me a person."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "Person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
}
}
)
Internally: vLLM parses the schema, compiles with XGrammar, wires the
resulting logit processor into the sampling pipeline. Also supports
"type": "json_object" (any valid JSON), "type": "regex",
"type": "grammar" (raw EBNF).
For tool calling the model card’s tool schema is converted to a union-typed JSON schema per tool + a discriminator on the tool name. XGrammar 2’s TagDispatch handles the mid-generation switch when the model picks a specific tool.
Overhead reality¶
On XGrammar with a moderate JSON schema on Llama-3-8B on H100:
Compilation: ~5 ms (JIT-compiled on first request; cached thereafter)
Per-step masking: ~2-5 μs (essentially noise compared to the ~10-30 ms forward pass)
Token generation slowdown: <3% end-to-end
Compare to Outlines’ original numbers (~5-15% overhead, seconds of compilation): XGrammar collapsed the cost by ~10×.
Actionable: in 2026 there is essentially no performance reason not to turn on structured output for any parser-consuming endpoint.
Failure modes to know¶
Grammar unsatisfiable given prompt. Model is prompted “write a poem” but the response_format demands JSON. Legal tokens still exist, but the model is fighting them → outputs something like
{}quickly or hits max_tokens. Quality suffers because the sampling space excluded what the model wanted to say.Token-boundary ambiguity in edge grammars. A grammar that requires exact byte-level constraints (e.g., specific whitespace) can occasionally lack any legal token from certain states. Modern libraries detect this and emit a
<phone_number_or_numberic_id_or_random_id_39>byte-level fallback token; older ones threw.JSON with unbounded strings + max_tokens. Model can start a string and never close it before running out of tokens → output truncates mid-string, unparseable. Fix: cap string length in the schema (
"maxLength": <phone_number_or_numberic_id_or_random_id_40>).Nested schemas exploding compilation. Deeply recursive schemas (linked list of arbitrary depth) blow up FSM state count. Fix: bound recursion depth in the schema.
Reading exercise¶
Skim XGrammar §3 (the algorithm section). Understand the context-independent / context-dependent split.
Look at
python/sglang/srt/constrained/xgrammar_backend.py(or equivalent). Trace one call: prompt arrives with a JSON schema → compilation → per-step masking.Try to find a corner case: JSON with a
patternregex that XGrammar handles slowly. Reproduce it. This is a plausible OSS contribution area.
The intuition to internalize¶
Structured decoding is a logit processor. That’s it. All the libraries are competing on one narrow question: “how fast can I compute the legal-token mask for the current grammar state?” The mechanism is simple; the engineering is in the constant factors, and XGrammar currently owns them.
For your agentic day job: turn it on. Every tool call gets a schema. Every schema goes through XGrammar. You never write JSON repair code again. Zero retries from malformed output. If your platform doesn’t use structured decoding yet, that’s a Phase 4 quick win alongside prefix caching (file 05).