The Nightly Build.
The Build · Engineering

A grammar masks all but seven of GPT-2's 50,257 tokens to force valid output

A finite-state machine marks the legal next tokens at each step, and every rival token's logit is set to negative infinity before the model samples.

The guarantee holds because the tokens are gone

A model decoding one token at a time produces a distribution over its whole vocabulary at each step. Constrained decoding adds a grammar that tracks which tokens could legally come next, and sets the logits of every other token to negative infinity before the softmax, so the sampler can only draw a token that keeps the output valid.1 That one layer, added to the distribution and nothing to the model, is what lets OpenAI's Structured Outputs promise it will "always generate responses that adhere to your supplied JSON Schema."2 The word "always" is not a claim about the prompt. It is enforced by deletion.

Compile a grammar that permits only the JSON value true or false, run it against the real 50,257-token GPT-2 vocabulary, and at the first step exactly seven tokens survive. The other 50,250 have their probability driven to zero, no matter what the model wanted. Building that machine from the grammar up, in runnable Python, exposes the three bills the naive version hides: the index it must precompute, the gap between a grammar that counts characters and a model that emits tokens, and the places where forcing structure changes what the model says.

Constrained decoding Restricting generation to a formal language by masking, at each step, every token whose logit the grammar cannot currently allow, so the emitted sequence is valid by construction rather than by post-hoc parsing.1

A grammar is a machine you can walk

Willard and Louf's contribution, the method behind the Outlines library, is to reduce guided generation to walking a finite-state machine: the grammar becomes states and transitions, and generating a valid string is nothing more than following transitions that exist.1 For a regular language the machine is small enough to write by hand. Take the grammar true | false. Ten states, one transition per character, two accepting states where a complete word ends.

constrain.pyPYTHON
# grammar as a DFA over characters: the JSON value  true | false
# state -1 is the implicit dead state; accepting states are 5 and 9.
DELTA = {
    (0, "t"): 1, (1, "r"): 3, (3, "u"): 4, (4, "e"): 5,
    (0, "f"): 2, (2, "a"): 6, (6, "l"): 7, (7, "s"): 8, (8, "e"): 9,
}
ACCEPT, START = {5, 9}, 0

def step(state, ch):
    return DELTA.get((state, ch), -1)

def walk(state, s):
    for ch in s:
        state = step(state, ch)
        if state == -1:
            return -1
    return state
Fig. 1 · The grammar as a deterministic machine. walk consumes a whole string and returns the state it lands in, or the dead state if the string ever leaves the machine.1

The reference implementation, outlines-core, exposes exactly this shape around a real tokenizer: build a Vocabulary from a pretrained model, combine it with a regular expression into an Index, and read off allowed_tokens(state) and next_state(state, token) at each step.3 The hand-written machine above is the toy version of that Index. What is missing is the bridge from character transitions to whole tokens, and building that bridge is the first real cost.

The cost hides in the one-time compile

The masking step runs once per generated token, so it has to be cheap. The naive way to enforce a grammar is, at each step, to loop over the whole vocabulary and test each token against the machine. Willard and Louf call this out as "a fixed O(N) cost for each token generated, where N is the size of the LLM's vocabulary."1 Their fix is to precompute, for every state, the set of tokens that keep the output legal from that state. With that map in hand the per-step work is a single lookup: "using a hash-map for σ can make the m step in Algorithm 2 cost only O(1) on average."1

constrain.pyPYTHON
from tokenizers import Tokenizer

tok = Tokenizer.from_pretrained("gpt2")           # the real GPT-2 tokenizer
V = tok.get_vocab_size()                          # 50257
STRINGS = [tok.decode([i]) for i in range(V)]     # every token's literal text

def build_index():
    # for each reachable state, the token ids whose whole string stays legal.
    states = {START} | set(DELTA.values())
    return {q: {i for i in range(V) if STRINGS[i] and walk(q, STRINGS[i]) != -1}
            for q in states}

index = build_index()
print(len(index[START]), "of", V, "tokens legal at the start state")
Fig. 2 · Compiling the grammar against a real vocabulary. The comprehension is the whole index-construction algorithm; it runs the O(N) scan once, up front, so the per-step mask can be a set lookup.1
outputCONSOLE
7 of 50257 tokens legal at the start state
Fig. 3 · The compiled index over GPT-2. Seven tokens can legally begin a true or false value; the machine has already deleted the rest before the model runs.

The O(1)-versus-O(N) trade is bought with a one-time preprocessing pass, and that pass is where the bill lands. Willard and Louf describe their construction, Algorithm 4, as building "a map from FSM states to subsets of V" whose "run-time cost is effectively irrelevant, although it theoretically requires memory equal to the number of states in the FSM."1 For a ten-state boolean the map is tiny. For a real JSON Schema it is not free, and the independent JSONSchemaBench measured the gap: compiling a schema into an index took Outlines about 3.5 seconds, against roughly 0 milliseconds for Guidance's incremental approach on the same schemas.4 Hosted systems pay the same cost and hide it behind a cache. OpenAI states plainly that "the first request you make with any schema will have additional latency as our API processes the schema, but subsequent requests with the same schema will not."2 The mask is cheap; the compile that makes it cheap is what you schedule around.

The grammar counts characters, the model emits tokens

Here is the joint where it is tempting to wave a hand, so slow down and ask the plain question. The machine has transitions on single characters: t, then r, then u. The model does not emit characters. It emits tokens, and a token is a string of one or more characters chosen by the tokenizer. So which states does a single token move the machine through, and how does the index know?

constrain.pyPYTHON
for s in ["true", "tr", "t", " true"]:
    tid = tok.encode(s, add_special_tokens=False).ids[0]
    print(f"{s!r:>7}  id {tid:>5}  start state to state {walk(START, s)}")
Fig. 4 · One token, several transitions. Each string is a single GPT-2 token; walk reports where it lands the machine from the start.1
outputCONSOLE
 'true'  id  7942  start state to state 5
   'tr'  id  2213  start state to state 3
    't'  id    83  start state to state 1
' true'  id  2081  start state to state -1
Fig. 5 · The token 'true' crosses four transitions in one step and lands in an accepting state. The leading-space token ' true' is a different id and leaves the machine at once.

The answer to the childlike question is that the index does not map characters to states. It maps each state to the tokens whose entire string can be consumed from there. Willard and Louf's Algorithm 3 finds, for a token string, "the states through which the FSM would traverses [sic] when accepting the provided string," and Algorithm 4 stores that per state.1 So true is a legal one-step move, while  true, with a leading space, is a different id and dies at once, because the machine has no space transition. That is the alignment tax in miniature: the mask has to be computed in the model's token alphabet, not the grammar's character alphabet, and the two do not line up.

The tax is small but not zero, and it does not fully vanish into the precomputed index. XGrammar measured it on a production vocabulary: for Llama-3.1's 128k tokens under a JSON grammar, the tokens whose legality depends on runtime context "account for only a minor proportion, amounting to less than 1% (1134 out of 128k)."5 Those 1,134 tokens are the ones a static state-to-token map cannot fully settle in advance, and they have to be rechecked as generation proceeds.

Read the change off one step

With the index built, the per-step mask is four lines: copy the logits, set every token not in the current state's allowed set to negative infinity, and let the softmax do the rest. The claim to check is that the mask alone changes the distribution. Take one concrete step and print the vector.

constrain.pyPYTHON
import numpy as np

def mask(logits, state):
    out = logits.copy()
    out[[i for i in range(len(logits)) if i not in index[state]]] = -np.inf
    return out

def softmax(x):
    x = x - x.max(); e = np.exp(x); return e / e.sum()

# one concrete step: the model, asked for a boolean, scores these tokens.
# " maybe" is its favourite, but the grammar forbids it here.
scores = {" maybe": 8.2, "true": 7.9, " yes": 6.4,
          "false": 6.1, "tr": 3.0, " I": 2.5}
logits = np.full(V, -30.0)
for s, v in scores.items():
    logits[tok.encode(s, add_special_tokens=False).ids[0]] = v

before, after = softmax(logits), softmax(mask(logits, START))
for s in scores:
    i = tok.encode(s, add_special_tokens=False).ids[0]
    lg = "-inf" if i not in index[START] else f"{logits[i]:.1f}"
    print(f"{s!r:>8}  logit {lg:>4}  p_before {before[i]:.4f}  p_after {after[i]:.4f}")
Fig. 6 · The whole mechanism, minus the network. The input scores stand in for a model's logits at a step where it wants to hedge; everything else is the real index, the real tokenizer, and a real softmax.1
outputCONSOLE
' maybe'  logit -inf  p_before 0.4908  p_after 0.0000
  'true'  logit  7.9  p_before 0.3636  p_after 0.8527
  ' yes'  logit -inf  p_before 0.0811  p_after 0.0000
 'false'  logit  6.1  p_before 0.0601  p_after 0.1410
    'tr'  logit  3.0  p_before 0.0027  p_after 0.0063
    ' I'  logit -inf  p_before 0.0016  p_after 0.0000
Fig. 7 · The receipt. The model's favourite token, ' maybe' at probability 0.49, is illegal at this state and goes to zero. The mass it held is redistributed across the legal tokens; 'true' rises from 0.36 to 0.85.

Read the first line. Before the mask, the model's most likely next token is  maybe at probability 0.49, the kind of hedge that would break a boolean field. It is not in the seven legal tokens, its logit becomes negative infinity, and after the softmax its probability is 0.0000. The token true, second before the mask at 0.36, absorbs most of the freed mass and comes out at 0.85. The mask never reorders the legal tokens, so true stays ahead of false; it only removes options.

Fig. 8 · The step's distribution over the six scored tokens, before and after the mask. Illegal tokens (maybe, yes, I) collapse to zero and their mass moves to the legal ones. Data from the run in Fig. 7; mechanism from Willard & Louf.

This runs in production, not only in a paper. llama.cpp ships grammar-constrained sampling in its GBNF format, "a format for defining formal grammars to constrain model outputs," applying the same per-step mask inside a local inference engine.6 The per-step cost, done well, is nearly invisible. XGrammar reports masking latency "less than 40us" per token and up to a 100x reduction against Outlines v1.0 and llama.cpp, and its end-to-end overhead against unconstrained generation is close to nothing: per-output-token latency of 6.2 milliseconds without constraint versus 6.3 with it at batch size one, 9.0 versus 9.1 at batch size 16.5 The mask itself is cheap. What is not free is what the mask can do to the content underneath the shape.

When the shape bends the answer

The mask guarantees shape. The uncomfortable question is whether forcing the shape changes what the model would have said, and here the evidence genuinely conflicts. Tam and colleagues, in "Let Me Speak Freely?", measured "a significant decline in LLMs' reasoning abilities under format restrictions."7 On GSM8K grade-school math with GPT-3.5-turbo, accuracy fell from 75.99% in free-form natural language to 49.25% under a constrained JSON mode. On a last-letter-concatenation task, LLaMA-3-8B dropped from 70.07% to 28.00%.7 Taken alone, that is a case against ever reaching for the mask on a task that requires thinking.

It should not be taken alone. Two independent re-runs, holding the prompt fixed, found the opposite. The Outlines vendor dottxt, a party with a stake, argued the original study "uses different prompts for structured generation and unstructured generation" and conflated true masked decoding with a fine-tuned "JSON-mode" that "has no guarantees about the returned value." With matched prompts their re-run showed structured generation edging ahead, GSM8K moving from 0.77 to 0.78.8 The neutral referee agrees. JSONSchemaBench, built by neither the Outlines nor the OpenAI team, reports that "the constrained decoding, regardless of the framework, achieves higher performance than the unconstrained setting," with Guidance at 83.8% on GSM8K against an unconstrained 80.1%.4

The two results reconcile, and the reconciliation is the practical rule. The mask changes content only when the legal set at a step excludes the token the model needed to reason well. Force a schema that puts the answer field before any room to work, and the model must commit before it has thought, which is the failure Tam's constrained runs mostly measure. Give it a schema with a place to reason first, or match the prompt, and the measured harm disappears. The degradation tracks the schema and the prompt, not the logit mask.

One more honesty check sits underneath the word "guarantee." The guarantee is validity against the grammar the engine actually compiled, which is not always the schema you wrote. OpenAI concedes that "while Structured Outputs supports much of JSON Schema, some features are unavailable either for performance or technical reasons."2 JSONSchemaBench makes the limit quantitative: on a set of hard real-world GitHub schemas, empirical coverage fell to 41% for Guidance, 39% for llama.cpp, 28% for XGrammar, and 3% for Outlines, the last mostly to timeouts.4 A schema the engine cannot compile is either reduced to one it can or refused; the mask keeps its promise only over the grammar it built.

A grammar becomes a machine, the machine becomes a per-state token index, and the index becomes a mask that deletes the illegal options before the model draws. It buys guaranteed structure for a one-time compile, a one-percent alignment tax handled at runtime, and the discipline to write a schema that lets the model reason before it commits. Reach for it when a downstream parser must never fail. Design the schema, not just the type, when the answer needs thought before shape.1

Sources

  1. arXiv · Willard & Louf, "Efficient Guided Generation for Large Language Models" (the FSM-index method: O(N)-per-token naive cost, the O(1)-average masking after a precomputed hash-map, Algorithm 3 for token sub-paths, and Algorithm 4 for the state-to-token map)
  2. OpenAI · "Structured Outputs" API guide ("always generate responses that adhere to your supplied JSON Schema"; first-request schema latency then cached; "some features are unavailable")
  3. dottxt-ai · outlines-core README (the reference implementation: Vocabulary from a pretrained tokenizer plus a regex into an Index, with allowed_tokens and next_state per step)
  4. arXiv · Geng et al., "Generating Structured Outputs from Language Models: Benchmark and Studies" (JSONSchemaBench: constrained decoding "achieves higher performance than the unconstrained setting"; GSM8K 83.8% vs 80.1%; hard-schema coverage 3-41%; Outlines ~3.5s vs Guidance ~0ms compile)
  5. arXiv · Dong et al., "XGrammar: Flexible and Efficient Structured Generation Engine for LLMs" (per-token mask "less than 40us", up to 100x vs Outlines v1.0 and llama.cpp; TPOT 6.2 vs 6.3 ms at batch 1; context-dependent tokens 1,134 of 128k)
  6. llama.cpp · GBNF grammar documentation ("a format for defining formal grammars to constrain model outputs"; grammar-constrained sampling in a production local-inference engine, with its performance caveats)
  7. arXiv · Tam et al., "Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of LLMs" ("a significant decline in LLMs' reasoning abilities under format restrictions"; GSM8K 75.99% to 49.25%; LLaMA-3-8B last-letter 70.07% to 28.00%)
  8. dottxt · "Say What You Mean: A Response to 'Let Me Speak Freely'" (matched-prompt re-run: structured generation edges unstructured, GSM8K 0.77 to 0.78; the critique that JSON-mode is not masked structured generation)