GLRMask
Constrained Decoding with Weighted Automata
Suppose you want to guarantee that an LLM’s output follows a grammar. How do we do this?
The dumb way is to sample repeatedly until we get a token the parser no longer rejects:
while generating:
logits = model()
token = sample(logits)
while not constraint.allows(token):
token = sample(logits)
constraint.commit(token)
model.commit(token)
The problem with rejection sampling is that it blocks the whole decoding pipeline. Nothing can continue until we’ve decided on a token that’s valid, and in the worst case could end up working through almost the entire vocabulary while the GPU sits idle.1
The alternative is to work out which tokens are legal up front, as a mask over the vocabulary, while the model is busy:
while generating:
parallel:
logits = model()
mask = constraint.get_mask()
logits[~mask] = -np.inf
token = sample(logits)
constraint.commit(token)
model.commit(token)
get_mask() runs in parallel with the forward pass to determine which model tokens are legal from the current state. The sampler picks one of the legal tokens, then commit(token) advances the lexer and parser using the token that was actually chosen.
If the mask is ready by the time the forward pass finishes, its cost is hidden. If not, it stalls generation. So get_mask has to be fast.
So masked generation works by restricting the sampler to grammatically valid tokens. It literally can’t go wrong2! But what does the mask actually compute?
Suppose the bytes generated so far are , the language we want to generate is (a language is just a set of strings; here, is the set allowed by the grammar), and model token corresponds to the byte string . The next-token mask is
Let’s think about what this means. A token is legal if appending its bytes still leaves some way to complete the output into a grammatically valid string. And we need to check that for every token in the vocabulary, at every generation step.
A bruteforce approach might look something like this:
- Build a mask over the entire LLM vocabulary.
- For each LLM token , check whether appending keeps the parse valid.
- If so, set its bit.
With a vocabulary of 100k+ tokens, this probably won’t go too well.
A straightforward way to generate the mask is to explore candidate token bytes online. In practice you put the vocabulary in a byte trie, share work between tokens with common prefixes, cut off branches as soon as they become impossible, and use further shortcuts to settle whole groups of tokens at once. This can already make mask generation very fast, particularly for JSON. llguidance is a strong implementation of this kind of approach.
But how much work remains depends on the grammar and the current parse state, and this can show up most clearly in tail latency (e.g. p99.9).
GLRMask tries to move as much of this work ahead of time as possible. Compilation is heavier, sometimes substantially so, but it only has to happen once, and the compiled constraint can then be reused for every subsequent request.
Weighted automata
To see where we’re headed, it might be useful to understand what we’re trying to build and why it should be possible, at least vaguely.
The central object in GLRMask is the Parser DWA. It eats the current parser stack from the top down, one symbol at a time, and returns the mask directly.
Okay, great: a magical automaton! But why should such a thing exist?
Here’s one way to think about it.
Fix a lexer state and a model token . Depending on how ‘s bytes are lexed, it may produce one or more terminal sequences. Each of those sequences, in turn, makes the parser perform some actions on the stack. For an LR parser, those actions are shifts, reduces, and gotos. But regardless of how the particular parsing paradigm formulates those actions, they must boil down to a sequence of reads and writes on the parser stack.
The lexer, parser, and vocabulary are known ahead of time. So for a given lexer state, by combining the terminal sequences that we know the lexer emits with the stack reads and writes we know the parser performs for them, we should be able to characterise ahead of time exactly how an LLM token acts on the parse state.
Viewed from before the token is consumed, when we just want to know whether or not this token will be accepted, this gives us a set of requirements on the old stack: which parser states need to be sitting at the top of the stack, and in what order. So for each , there is a set of parser-stack prefixes from which can be consumed legally.
For reasons we’ll come back to later, it is possible to ensure that an LR parser can produce only a bounded amount of parser activity while processing any one terminal. Furthermore, as long as each terminal consumes at least one byte, we know that each model token can produce only a bounded amount of lexer activity. Putting these together, a model token can induce only a bounded amount of parser activity, and therefore its acceptance depends on only a bounded prefix of the existing parser stack. Thus each induces a regular language over stack prefixes, recognizable by a finite automaton.
Now imagine one of these automata for every pair. An LLM vocabulary may contain a hundred thousand or more tokens, and a lexer can easily have tens of thousands of states, so there are a lot of them. But there is also a lot of shared structure among them.
GLRMask exploits this shared structure by storing the whole family in one weighted automaton. Common stack-prefix paths are represented only once, while the weights record which pairs use each part of the graph.
A weighted automaton is just an automaton whose transitions carry weights, with rules for how those weights combine. In GLRMask, a weight is a Boolean array
indexed by lexer states and model tokens. Concretely, means that the transition applies to the pair .
For the current lexer state , each transition’s Boolean weight narrows a running set of allowed tokens as it is taken.
To build the Parser DWA, we handle the lexer and parser separately:
- On the lexer side, the Terminal DWA records which terminal sequences a model token can produce.
- On the parser side, the stack-effect automata describe what each terminal can do to the parser stack.
Terminal DWA
The Terminal DWA records, for every lexer state and model token, the terminal sequences that token can produce.
Fix a lexer state and a model token , and let be the bytes of that token.
A single LLM token might:
- expand into multiple terminals (
"}\n"could lex asRBRACE NEWLINE), - end partway through a terminal (halfway through a string literal, halfway through a number, halfway through
true), - be lexically ambiguous,
- or produce no terminals yet (e.g. it adds bytes inside a string, but doesn’t close it).
So even for a fixed lexer state and model token, there may be more than one terminal sequence we need to consider.
We store these sequences in a deterministic weighted automaton, or DWA.
A path through the Terminal DWA represents a terminal sequence, and its weights record which lexer-state/model-token pairs can produce it.
Conceptually, for each pair , run through the lexer from , and add the terminal sequences it can produce as paths from the root, marking those paths with . If the token ends partway through a lexeme, we also need to account for its possible completions. The model token is only legal if at least one such completion can eventually be accepted by the parser. So branch the path once for each terminal that lexeme could still become, and end each branch with that terminal.
In practice, of course, we traverse a vocabulary trie rather than iterating through each full token individually. But you get the idea. Lots of tries! Just assume, from now on, that any time I mention iterating over the vocabulary, I really mean traversing it as a trie.
Stack effects
For an LR parser, consuming one terminal means performing zero or more reductions followed by a shift.
A shift pushes a state to the stack. Reductions are a bit more involved. But what matters really is that ultimately everything an LR parser does is a sequence of operations that read from (and consume) and write to the top of a stack.
GLRMask abstracts away parser behaviour as sequences of stack effects: for “read and remove parser state ,” and for “write parser state .”
A stack effect sequence such as
therefore says: the old stack must begin with ; consume it; then write and .
Different terminals can have different stack effects, and a terminal may have more than one possible effect. For each terminal , let denote the set of its possible stack-effect sequences.
Stack effects also compose nicely. Suppose one terminal has the effect
and the next has
Composing them gives
Let’s think about what this means. One terminal pushes states onto the stack, and the next terminal immediately reads back off. As far as the stack is concerned, those operations cancel:
The two-terminal sequence therefore only requires the original stack to begin with . And if it does, it simply replaces that with . Everything involving and happens internally between the two terminals.
So, the cancellation rule is simply
A mismatched write/read pair, on the other hand, makes the path invalid:
For any set of stack effects, repeatedly applying these cancellations takes every surviving effect to a normal form consisting of some reads from the original stack followed by some writes describing the new stack:
Finally, for each terminal , let be the set of stack effects by which can be consumed.
Rather than enumerate explicitly, we represent it with a small automaton over stack operations. Each path through the automaton corresponds to one stack effect in .
Parser DWA
Let’s recap. We’ve built two kinds of automata.
The Terminal DWA compactly represents the terminal sequences that model tokens can produce, with each path weighted by the lexer states and model tokens for which it applies.
Then for each terminal, we have a small automaton describing its effect on the parser stack.
So the Terminal DWA connects model tokens to terminals, while the stack-effect automata connect terminals to stack effects.
It’s time to cut out the middleman.
Wherever the Terminal DWA has an edge labelled , splice in the stack-effect automaton for . A path that previously represented a terminal sequence now expands into paths describing the corresponding parser-stack operations.
Writes from one terminal can run into reads from the next. Adjacent write/read symbols cancel when they match, and kill the path when they don’t. We keep cancelling until all of these internal write/read pairs are gone.
Any remaining writes sit at the end of the path, describing the net additions to the stack after the required stack prefix has been read.
For mask generation, the trailing writes aren’t needed. We could keep them and use the same automaton to update the parser state during commit, but that adds more complexity than it is worth. So instead we just drop them.
What we are left with is a weighted automaton over parser-stack symbols, whose paths read prefixes of the parser stack directly.
And that’s it: the Parser DWA!
I think it’s helpful to see the Parser DWA as basically a compressed representation of the family . For any particular , the corresponding read-only automaton for can in principle be recovered from the Parser DWA by keeping only transitions whose weights contain . So, effectively, slapping Boolean-array weights onto transitions lets us pack a whole family of automata into one, with the weights encoding the distinctions among them.
Finally, one nice property of the Parser DWA is that it only ever needs to read a bounded prefix of the parser stack. First, it should be fairly clear that each terminal sequence emitted by the lexer for a model token has bounded length. Every terminal emitted during the scan consumes at least one byte, and a model token contains only finitely many bytes. And because the vocabulary is finite, there is a fixed maximum token length. For each terminal, the stack effect is bounded too. After grammar normalization, the number of consecutive reductions between shifts is bounded by a constant depending only on the grammar [Aycock et al., 2001]. The Parser DWA is just the composition of these bounded terminal sequences and bounded stack effects. So it too must be bounded. Nice!
Still under construction. Mask generation, benchmarks and the rest of the implementation notes are coming back after another editing pass. Check back in a few days.
Appendix: The construction in equations
A model token may produce one or more terminal sequences. Each sequence induces a stack effect. The Parser DWA keeps only the read prefix of that effect: the part that must be present on the parser stack.
The weights are Boolean masks indexed by lexer state and model token:
Weights combine pointwise by union and intersection:
A deterministic weighted automaton is
If its run on is
its value is the intersection of the transition weights and the final weight:
The Terminal DWA stores which lexer-state/token pairs produce each grammar terminal sequence. If is the byte string of model token , then
Here is a sequence of grammar terminals.
Each terminal has a language of parser-stack effects. For parser stack alphabet , introduce a read and a write symbol for every parser state:
An effect means that consuming can replace the top prefix with :
Adjacent writes and reads cancel when their parser states agree. A disagreement kills the path:
Writing for prefix order, composition of two stack effects is
Composition extends from individual effects to their languages:
So the net effects of a terminal sequence are
For mask generation, only the read prefix matters: the part that had to exist before the token began. Project each net effect onto that prefix:
A terminal sequence is parseable from stack exactly when one of these read prefixes occurs at the top of :
A model token may produce several terminal sequences, so its read-prefix language is their union:
The Parser DWA stores all of these languages in one weighted automaton:
The Parser DWA therefore satisfies:
Ignoring longest-match exclusions for now, holds exactly when some top prefix of has Parser-DWA value containing :
Suppose one token emits the two terminals
x and ], with effects
Their internal parser work disappears:
The token may perform several reductions and shifts internally, but the mask query only has to check that the old stack begins with .
In one line:
Footnotes
-
Assuming sampling without replacement. ↩
-
Constrained decoding can hurt task performance even when it guarantees syntactic validity. Greedily masking and renormalizing over the tokens that are legal at the current step can distort the model’s original distribution, while token/grammar misalignment can force unnatural tokenizations and reduce accuracy. See Grammar-Aligned Decoding and DOMINO. ↩