Repository navigation
fix(grammar): bound JSON whitespace at llama.cpp's limit on both runners - #470
Conversation
The longest whitespace run a structured-output grammar accepts between two JSON tokens is now llama.cpp's own limit
for a JSON schema (json-schema-to-grammar's SPACE_RULE): nothing, one space, or one or two newlines and up to 20
spaces or tabs.
- mlxrunner: max_whitespace_cnt 32 -> 22. muse-glimmer on MLX in single pass (OLLAMA_FORMAT_TWO_PASS=0), answering
after its message header, padded the first and last slot of every object to the bound ('{' + 33 spaces); stock
0.40.2 has no bound and filled all 16384 tokens with whitespace. 22 still fits a newline and ten levels of
two-space indent.
- llm: format "json" on GGUF uses the fork's grammarJSON, whose ws rule was unbounded ("([ \t\n] ws)?"); a schema
was already bounded by llama-server's converter. It is now llama.cpp's rule as an optional group, placement
unchanged (after literal characters only).
- Tests: TestRequestGrammarWhitespaceRunMatchesLlamaCpp and TestGrammarJSONBoundsWhitespace drive each grammar
through xgrammar one byte at a time (a plain format, after thinking, and "json" after a header); both fail on
the old grammars and skip where the MLX payload is not built. The wire-tag tests move to 22.
- ADR 0035 amended: the new limit, the GGUF "json" gap, why patching xgrammar to llama.cpp's exact rule was not
taken (mlc-ai/xgrammar#837), and the upstream state (ollama#18567 closed, ollama#18569 closed untested).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CUDA host review, at Checked against llama.cpp b11351: Tests:
Through xgrammar, The two runners agree on length only: MLX accepts 22 spaces in one gap, llama.cpp at most one. The test name Wrong upstream reference: mlc-ai/xgrammar#837 bounds empty XML tool-call whitespace. It does not add a whitespace-rule option. Before a CUDA deploy: From |
Review on the PR found grammarJSON still allowed a 44-character run in three
places: "{" ws (...)? ws "}", the same for arrays, and a literal's trailing ws
before ws "}". Through xgrammar, "{" + 2x("\n\n" + 20 spaces) + "}", a literal
followed by two such gaps, and "{ }" were all accepted.
- llm: grammarJSON now has the shape of llama.cpp's grammars/json.gbnf: every
value ends in ws, and nothing puts ws before "}" or "]", so each gap is one
ws and no run is longer than 22. "{ }" is refused, as json.gbnf refuses it.
Kept from the old grammar: the root object ends at its "}" (no trailing
whitespace) and there is none between a key and its ":". New: whitespace
after a string, number, object or array value before a ",", which json.gbnf
allows and the old grammar allowed only after true/false/null.
- Tests: TestGrammarJSONBoundsWhitespace now runs a reference GBNF recognizer
in plain Go (llm/gbnf_reference_test.go), so it runs without the MLX payload;
TestGrammarJSONBoundsWhitespaceXGrammar drives the same cases through
xgrammar. The two-gap cases and "{ }" fail on the previous grammar.
- mlxrunner: TestRequestGrammarWhitespaceRunMatchesLlamaCpp is renamed
TestRequestGrammarBoundsWhitespaceRunLength. The runners agree on the run's
length only: MLX accepts 22 spaces in one gap, llama.cpp at most one.
- ADR 0035 and the README row say so, and the mlc-ai/xgrammar#837 reference
is removed: that PR bounds empty XML tool-call whitespace, it does not add a
whitespace-rule option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, all three points are fixed in
How it is tested here:
Not run here: the xgrammar tests,
From |
|
The xgrammar half, run at Setup:
Positive control: with On a CUDA arm: yes. The #477 run is on the tag. Once it has finished, I can run the think-off GGUF and MLX-CUDA cells on this head as a separate arm, so the format-acceptance change shows up against the tag's cells on the same build. From |
…s 1-5, the fixes it produced, and the follow-ups Phases 0-5 ran on all eight tags. The record's outcome section lists the five fixes now in v0.40.2-dynres.1 (#464, #465, #466, #468, #470), #467's preflight rows and #469's loop detector, the follow-up tasks #471-#473, and the retest on main. Every results table is generator output pasted verbatim, with the build each cell ran on; the release think-on cells are labelled as not a measurement of the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Structured output now bounds the whitespace between JSON tokens at llama.cpp's own limit, on both runners. That limit is nothing, one space, or one or two newlines and up to 20 spaces or tabs: a run of at most 22 characters. GGUF
format: "json"now uses that rule itself. MLX bounds the run at the same length but not the same shape (any 22 whitespace characters).Why
max_whitespace_cnt32 → 22. muse-glimmer on MLX in single pass (OLLAMA_FORMAT_TWO_PASS=0), answering after its message header (fix(glimmer): a format with thinking off waits for the message header, in single pass and two-pass alike #464), padded the first and last slot of every object to the bound.format_schemaon30b-nvfp4returned{+ 33 spaces, the value, about 23 spaces and}.format: "json". A schema goes to llama-server's converter, which is bounded.format: "json", though, uses the fork's owngrammarJSON, and itsws ::= ([ \t\n] ws)?was unbounded. A model that degenerates into whitespace ran tonum_predictor to llama-server's 100-repeat abort, an HTTP 500 with no output.ws ::= (" " | "\n"{1,2} [ \t]{0,20})?. The grammar also takes the shape of llama.cpp'sgrammars/json.gbnf, so no twowsrules sit side by side and no gap runs past 22. The first version allowed 44 in three places, and{ }(147fd742d).{,[,:,,and every value, and never comes before}or], as in json.gbnf. Two differences from json.gbnf are kept from the old grammar: no whitespace after the root's}, and none between a key and its:. One broadening: whitespace after a string, number, object or array value, before a,, is newly accepted, as json.gbnf accepts it.Not taken
Patching xgrammar to llama.cpp's exact rule. On MLX it would cut glimmer's padding to one space per slot. It costs a native patch on every xgrammar bump, and it moves every MLX format request's mask. xgrammar v0.2.7 hardcodes
any_whitespace=truefor structural-tagjson_schemaelements, so only the count can be set from the tag. ADR 0035 records this, and says to revisit it if xgrammar adds a whitespace-rule option.Upstream
Tests
TestRequestGrammarBoundsWhitespaceRunLengthdrivesrequestGrammar's tag through xgrammar one byte at a time. It proves the run's length, not llama.cpp's shape. It covers three request shapes: a plain format, a format after thinking, and"json"after a header.TestGrammarJSONBoundsWhitespacechecksgrammarJSONwith a reference GBNF recognizer in plain Go, so it runs without the MLX payload. It covers two gaps at the bound side by side in an empty object, in an empty array and after a literal, and{ }.TestGrammarJSONBoundsWhitespaceXGrammardrives the same cases through xgrammar, since llama.cpp's engine has no Go entry point.Both fail on the old grammars. The xgrammar tests skip where the MLX payload is not built. The wire-tag tests move to 22.
go test ./mlxrunner/ ./mlxrunner/xgrammar/ ./llm/ ./server/passes.Real-model check: queued behind the regression A/B on the Metal host. It covers:
format_schemain both flows on glimmer MLX and GGUF;gemma4:31b-nvfp4andgemma4:31b-it-q4_K_Magainst the release build, on the same store.The results will be added here.
ADR 0035 is amended, and the README row is updated. #461 carries the same README row; whichever merges second updates it.
macbook-pro-m5-max-128GB/mlx-metal🤖 Generated with Claude Code