Skip to content

Speculative decoding under a JSON schema: grammar-masked acceptance and grammar proposals - #391

Open
carinapeng wants to merge 1 commit into
apple:mainfrom
carinapeng:carina/spec-grammar-constrained
Open

carinapeng wants to merge 1 commit into
apple:mainfrom
carinapeng:carina/spec-grammar-constrained

Conversation

@carinapeng

Copy link
Copy Markdown
Contributor

Summary

llm-runner refused --json-schema with speculative decoding because the verifier had no notion of the grammar. Greedy speculative decoding now runs under a grammar:

  • Masked acceptance. The target's choice at each verified position is its argmax over the tokens the grammar allows, and the grammar advances by every emitted token. Output is schema-valid and token-for-token identical to grammar-constrained greedy decoding of the target alone. A drafted token the grammar forbids ends acceptance early; it is never emitted.
  • Grammar proposals. A span the grammar fully determines (keys, separators, literals) is verified as the proposal in place of drafted tokens. It is checked against the matcher and rolled back, so the grammar advances only when tokens are committed.
  • Early stop. If a run ends inside an accepted block, the grammar is rolled back to the emitted stream.
  • llm-runner. --json-schema is accepted with --draft-model and --prompt-lookup under greedy decoding. New --grammar-proposal-max (default 7, 0 disables, at most 63). Sampling with a schema is refused.

Verification scores full logits per slot; a top-k shortcut would need a new engine primitive and is not part of this change.

Testing

  • 9 new decoder tests and 2 option tests, including a differential test against a reference constrained-greedy loop (5 configurations × 12 seeds). Replacing the masked argmax, or removing the early-stop rollback, makes them fail.
  • swift test --filter "Speculative|Grammar|Constrained|StaticShape|PromptLookup": 102 tests pass. swift format lint clean.
  • iPhone 17 Pro, Gemma 4 E2B and E4B, four-field schema: output byte-identical to plain constrained decoding at proposal limits 0, 3 and 7, and valid JSON.

The gain is bounded by how much of the output the schema forces.

Greedy speculative decoding accepts a grammar. The target's choice at each
verified position is its argmax over the tokens the grammar allows, and the
grammar advances by every emitted token, so the output is schema-valid and
token-for-token identical to grammar-constrained greedy decoding of the target
alone.

A span the grammar fully determines (for example the key names of an object)
is verified as the proposal in place of drafted tokens, trimmed to leave the
last token to the target. The span is checked against the matcher and rolled
back, so the grammar advances only when the decoder commits tokens.

When a run stops before the end of an accepted block, the grammar is rolled
back to the emitted stream.

llm-runner accepts --json-schema with --draft-model and --prompt-lookup under
greedy decoding, and gains --grammar-proposal-max (0 disables grammar
proposals; default 7, at most 63, the grammar session's rollback budget less
the anchor). Sampling with a schema is refused.

Verification scores the full logits of each slot; a top-k shortcut is not part
of this change.
@carinapeng
carinapeng requested a review from a team as a code owner October 31, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant