Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Grammar-forced speculative drafts (#48): forced spans in constrained JSON output are free, guaranteed-accepted drafts - #70

Merged
JustVugg merged 1 commit into
JustVugg:mainfrom
fabio-rovai:grammar-draft-48
Jul 11, 2026
Merged

Grammar-forced speculative drafts (#48): forced spans in constrained JSON output are free, guaranteed-accepted drafts#70
JustVugg merged 1 commit into
JustVugg:mainfrom
fabio-rovai:grammar-draft-48

Conversation

@fabio-rovai

@fabio-rovai fabio-rovai commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Implements #48 — the grammar itself as a third draft source, next to MTP and n-gram lookup.

What it does

For constrained-output workloads (JSON/NDJSON, function calling, structured extraction), wherever the grammar admits exactly one legal byte — braces, quotes, key names, separators, enum bodies — that forced span is tokenized and injected as drafts that the existing batch-union forward verifies. Properties, as discussed in the issue:

  • ~1.0 acceptance on forced spans — correct by construction, no head, no lookup table, no acceptance gamble.
  • Engages even with the int4 MTP head from MTP: int4 head unusable (int8 fixes acceptance); speculation still a net loss on CPU streaming-MoE — measured. #8 — grammar drafting needs no model head at all, and it drafts even when DRAFT=0.
  • Composes with MTP/n-gram — grammar is tried first each step; where it branches (free text), the existing sources take over untouched.
  • Never constrains sampling. The grammar only proposes; verification accepts or rejects exactly as for MTP drafts (greedy match, or Leviathan rejection sampling under temperature). A wrong, incomplete, or out-of-sync grammar cannot change the output — worst case is rejected drafts, and an adaptive guard (same pattern as the MTP auto-off) disables the source below 50% acceptance.

Usage

GRAMMAR=schema.gbnf coli run ...     # or serve; GRAMMAR_DRAFT=n caps the span (default 24)

Grammar format: byte-level GBNF subset (llama.cpp style) — literals with escapes, char classes incl. negation, rule refs, groups, | ? * +, comments, multi-line rules. Root rule is root. Left recursion is detected and fails safe (walker off, generation proceeds undrafted).

How

  • c/grammar.h (new, header-only like tok.h/st.h): GBNF parser + PDA walker with a set of stacks in normal form (top of every stack is a terminal byte-class, or the stack is empty = parse completable). gr_forced() extends the forced span while exactly one byte is legal and the parse cannot terminate there (where it can, the model may legitimately emit EOS, so we don't force).
  • c/glm.c: the walker follows every emitted token (gr_feed), arming lazily at the first byte the root admits (preambles are skipped) and re-arming after a desync. spec_decode asks the grammar first; forced bytes are encoded with the engine's own tokenizer — the tokenization boundary is not guaranteed to match the model's, which is fine: verification absorbs it (that's what the acceptance stat measures). Separate grammatica: acceptance stat; grammar-accepted tokens no longer pollute the MTP acceptance counter.
  • c/tests/test_grammar.c: 13 scenarios (forcing, branching, enums, ? * + incl. multi-byte-literal repetition, negated classes, desync, hex escapes, parse errors, left recursion, and the NDJSON shape from the issue), wired into make check.

Measured (Apple M3 Max, 128 GB, same box as #47)

Real-model A/B on the int8-MTP container (GLM-5.2-colibri-int4-with-int8-mtp, 357 GB), greedy TEMP=0, MTP=0 DRAFT=0 (grammar is the only draft source), NGEN=130, RAM_GB=60, NDJSON classification task with the grammar below, fresh process per run:

config tok/s wall (130 tok) tok/forward draft acceptance expert hit
baseline 0.37 351.1 s 1.00 46.7%
GRAMMAR=fit.gbnf 0.50 260.7 s 1.60 100% (48/48) 45.1%
  • Output byte-identical to baseline — the lossless property, confirmed end-to-end on the 744B model.
  • 100% acceptance on all 48 forced drafts — the tokenization-boundary worry didn't bite once on this run (the engine's own BPE and the model agree on structural JSON spans).
  • 1.60 tokens/forward lands inside the 1.3–1.5 estimate from the issue (slightly above, since this run's output was punctuation-dense) — and this prompt spent its first ~30 tokens on free-text preamble, where the walker correctly stayed disarmed. On pure NDJSON output the fraction is higher.
  • tok/forward gain (1.60×) exceeds the wall-clock gain (1.35×) because verified drafts still route to extra experts on a cold cache — same effect you documented for MTP in MTP: int4 head unusable (int8 fixes acceptance); speculation still a net loss on CPU streaming-MoE — measured. #8; the adaptive guard governs this source identically.

Grammar used:

root ::= riga+
riga ::= "{\"id\":\"" id "\",\"fit_category\":\"" cat "\",\"fit_score\":" num ",\"reason\":\"" testo "\"}" "\n"
id    ::= [a-z0-9-]+
cat   ::= "no_fit" | "partial_fit" | "good_fit"
num   ::= [0-9]+
testo ::= [^"\x00-\x1f]*

Happy to adjust naming/placement to taste — e.g. if you'd rather the serve/API layer compile JSON schemas to GBNF (the OpenAI response_format seam you mentioned), that composes naturally on top of this engine-side mechanism and I can follow up with it.

🤖 Generated with Claude Code

…trained JSON output are free, guaranteed-accepted drafts

New byte-level GBNF-subset engine (c/grammar.h: parser + set-of-stacks PDA
walker) wired into spec_decode as a third draft source ("metodo F"), tried
before MTP/n-gram. Wherever the grammar admits exactly one legal byte, the
forced span is tokenized and injected as drafts; the existing batch-union
verification confirms them, so a wrong or out-of-sync grammar can never
change the output. Lazy arming skips preambles; adaptive guard (same
pattern as MTP) disables the source below 50% acceptance; grammar-accepted
tokens no longer pollute the MTP acceptance counter.

GRAMMAR=file.gbnf enables it in run and serve modes (also with DRAFT=0 and
with the int4 MTP head from JustVugg#8); GRAMMAR_DRAFT=n caps the span (default 24).

Measured on M3 Max / int8-MTP container, greedy, MTP=0 DRAFT=0, NDJSON
classification: 0.37 -> 0.50 tok/s (1.60 tok/forward, 81 fw per 130 tok),
100% acceptance (48/48), output byte-identical to baseline.

Co-Authored-By: Claude Fable 5 <[email protected]>

@JustVugg JustVugg left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautifully done — this is exactly the design the idea called for, and it's implemented with the right safety guarantees. Validated here:

  • Build clean (0 warnings), oracle unchanged with no grammar (32/32 TF, 20/20 greedy).
  • test_grammar passes.
  • Lossless proof: greedy generation with a grammar loaded vs without produces byte-identical output on the oracle (20/20 either way) — confirming the grammar only proposes drafts that normal verification accepts or rejects; a wrong or out-of-sync grammar cannot change the result.

The three properties that make this special all hold: ~1.0 acceptance on forced spans (correct by construction), engages even with the int4 MTP head from #8 (no model head needed), and composes with MTP/n-gram (grammar tried first, existing sources take over at branches). The self-contained grammar.h (byte-level GBNF subset, left-recursion detected and fails safe) keeps it clean, and the adaptive guard below 50% acceptance matches the MTP auto-off pattern.

This is the highest-leverage speculation source for structured-output/API workloads — on JSON-heavy generation the forced-token fraction is large and free. Great work turning #48 into reality. Merging.

@JustVugg
JustVugg merged commit cec7d6b into JustVugg:main Jul 11, 2026
fabio-rovai added a commit to fabio-rovai/colibri that referenced this pull request Jul 13, 2026
…rafts (JustVugg#48/JustVugg#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>
fabio-rovai added a commit to fabio-rovai/colibri that referenced this pull request Jul 13, 2026
…rafts (JustVugg#48/JustVugg#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>
fabio-rovai added a commit to fabio-rovai/colibri that referenced this pull request Jul 13, 2026
…rafts (JustVugg#48/JustVugg#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>
JustVugg added a commit that referenced this pull request Jul 14, 2026
…rafts (#148)

* SCHEMA=<file.json>: JSON-Schema -> GBNF compiler for grammar-forced drafts (#48/#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>

* schema_gbnf: whitespace-tolerant emission (jws at separators)

Measured on GLM-5.2 current main (#146): the greedy continuation writes sloppy
JSON (spaces after colons, fences, long free text) and a compact-only grammar
desyncs at the first stray space, forfeiting every span after it. jws points are
not forced themselves (two legal bytes) but the multi-byte spans around them
keep drafting and the walker survives non-compact output - strictly
acceptance-positive for a verified draft source. Tests re-derived for the new
span boundaries + a sloppy-instance walk.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: JustVugg <[email protected]>
fabio-rovai added a commit to fabio-rovai/colibri that referenced this pull request Jul 14, 2026
…rafts (JustVugg#48/JustVugg#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>
ErikTromp pushed a commit to SensAI-PT/aviary that referenced this pull request Aug 9, 2026
…ce, guaranteed-accepted forced spans, lossless + opt-in (JustVugg#48, JustVugg#70)

New byte-level GBNF-subset engine (c/grammar.h: parser + set-of-stacks PDA
walker) wired into spec_decode as a third draft source ("metodo F"), tried
before MTP/n-gram. Wherever the grammar admits exactly one legal byte, the
forced span is tokenized and injected as drafts; the existing batch-union
verification confirms them, so a wrong or out-of-sync grammar can never
change the output. Lazy arming skips preambles; adaptive guard (same
pattern as MTP) disables the source below 50% acceptance; grammar-accepted
tokens no longer pollute the MTP acceptance counter.

GRAMMAR=file.gbnf enables it in run and serve modes (also with DRAFT=0 and
with the int4 MTP head from JustVugg#8); GRAMMAR_DRAFT=n caps the span (default 24).

Measured on M3 Max / int8-MTP container, greedy, MTP=0 DRAFT=0, NDJSON
classification: 0.37 -> 0.50 tok/s (1.60 tok/forward, 81 fw per 130 tok),
100% acceptance (48/48), output byte-identical to baseline.

Co-authored-by: Claude Fable 5 <[email protected]>
ErikTromp pushed a commit to SensAI-PT/aviary that referenced this pull request Aug 9, 2026
…rafts (JustVugg#148)

* SCHEMA=<file.json>: JSON-Schema -> GBNF compiler for grammar-forced drafts (JustVugg#48/JustVugg#70 follow-up)

schema_gbnf.h compiles a practical JSON-Schema subset (strict objects, string/
number/integer/boolean/null, enum/const, arrays with items, nesting) into the
byte-level GBNF subset grammar.h parses, so structured-output workloads get
grammar-forced drafts without hand-writing GBNF. Unsupported keywords fail soft:
the engine runs without a grammar and output is unchanged (drafts are verified,
never constraints - a wrong compile can only cost acceptance, not correctness).

grammar_setup: GRAMMAR= (raw GBNF) keeps precedence; SCHEMA= feeds the compiler
into the same gr_parse path. 8 test groups in tests/test_schema_gbnf.c walk
compiled grammars end-to-end through the PDA (forced spans, enum disambiguation,
nested instances, escapes, leading-zero rejection, fail-closed fallbacks).

Co-Authored-By: Claude Fable 5 <[email protected]>

* schema_gbnf: whitespace-tolerant emission (jws at separators)

Measured on GLM-5.2 current main (JustVugg#146): the greedy continuation writes sloppy
JSON (spaces after colons, fences, long free text) and a compact-only grammar
desyncs at the first stray space, forfeiting every span after it. jws points are
not forced themselves (two legal bytes) but the multi-byte spans around them
keep drafting and the walker survives non-compact output - strictly
acceptance-positive for a verified draft source. Tests re-derived for the new
span boundaries + a sloppy-instance walk.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: JustVugg <[email protected]>
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.

2 participants