Repository navigation
serve: structured decisions, one scored forward per request - #21
Merged
Merged
Conversation
A system message that is a JSON question schema turns a chat request into a Jev-style typed-decision call: the user message is the state, the reply is a probability distribution per question, and nothing is generated. The answer template is seeded into the canvas with each label slot as noise and the label distribution is read off the logits at that slot, so every question in a request costs the same forward, and each slot conditions on the whole template bidirectionally. Why a new pipeline op rather than a generate: the read must not commit, must run a fixed number of forwards, and must return raw logits, none of which the block loop exposes. `Score` prefills, seeds, runs, reads, and leaves the causal log at the prompt. The conversation is finalized with the schema prefix alone so every state on that schema routes as a continuation: the router only matches a recorded log that is a prefix of the new prompt. Labels are single tokens (yes/no, letters, digits) and the tokenizer is the authority: the template is encoded with each label substituted and a label that changes more than one token is refused. Measured findings, recorded in PLAN.md: a joint hill climb (write the top labels back, re-read from step 0) converges in one round and only ratifies the first read; the hole noise alone flips borderline answers across seeds, so `samples` averages reads and reports agreement; a 64-row canvas halves the forward; against generating the same JSON the read is one forward instead of 3 to 5 full-canvas steps. The scoring path builds its config without the empty-reply predicate, which loads the tokenizer from disk on every call and cost 300 ms per sample. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A structured request's state delta is ~50 tokens, and the plain-chunk prefill ran it as a full 256-row forward: 1.3 s of the 2.0 s request. The stages already take their row count from `forward_m` for a shrunk denoise canvas, and `kv_write_end` keeps pad rows out of the cache, so a tail chunk now dispatches at the narrowest 64-row multiple that holds it. The KV is bit-identical: `narrow_prefill_chunk_bit_identity` pins a 140+51 split against the whole prompt and against the full-width chunk, and the 8-case byte-identity gate passes, which covers the tails of the 3k, 4.6k and 13k fast prefills. The delta drops from 1.12 s to 0.51 s in the test and 1.3 s to 0.65 s served. `DGQ_PREFILL_NARROW=0` restores full width. Profiling under samply found no CPU-side cost left on the pipeline thread (under a second active over a session, the rest GPU wait), so the serve log now attributes each structured request's wall time to activate, prefill, forward and finalize. PLAN.md records the cold-schema prefill trade-off: the quantized prefill is 3 s against 9.4 s on the f32 engine, but a prefix prefilled that way flipped the borderline outage answers, so the engine stays the default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ARCHITECTURE said a cross-turn delta follows the prompt's length rule. It does not: the delta always takes the fast quantized resume unless it exceeds the trust cap, and the force flag never reaches it, so a short multi-turn prompt is mixed precision by default. Corrected. PLAN records the five-arm measurement that answers whether that matters for structured decisions: the schema prefix's precision shifts a borderline answer by a consistent 0.3 to 0.4 in probability across 16 seeds, the state's own precision is within noise, and the two fast arms are sample-for-sample identical. The mixed default keeps the f32 engine on the prefix that carries the question wording and pays it once per schema. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The empty-reply predicate is now built once per worker, so the scoring path no longer needs its own config builder to avoid the per-call tokenizer load. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README states the request and reply shape, the knobs, and the measured costs from one server process on the final tree; PLAN carries the same final head-to-head numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A single read is the denoiser's posterior given one noise draw in the slot, and it is sharper than the marginal over the noise: two reads at 0.9 for opposite labels are a 0.5, and single reads landed on either side of a borderline question across seeds. The mean over reads is the Monte Carlo estimate of the marginal, so it is now the default at 4 reads, with `stderr` (standard error of the top label's probability over the reads) beside `confidence` and `agreement`. `samples: 1` restores the single read. Cost at the default, one process: 5.8 s at 256 rows and 3.1 s at 64 for the next state on a warm schema; the single read stays 2.0 s and 1.3 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…emplate The answer is a fixed number of single-token labels, so its width is known before the forward, and a 64-row forward costs half a 256-row one. The worry was distribution shift: the model is trained at 256, and width does change behavior somewhere (the empty-reply attractor falls 72% to 3% with it). Measured at 32 reads per width on three tickets, the unambiguous answers are identical at 1.00 ± 0.00, and the borderline outage question reads tone 0.70 ± 0.05 against 0.68 ± 0.06 and "urgent" yes 0.56 ± 0.07 against yes 0.68 ± 0.06, within 1.3 standard errors with the same lean. MLX itself shrinks the canvas to max(remaining, 64), so 64 is the reference's own floor. `active` now defaults to the template's 64-row multiple; `active: 256` opts back. The default request drops from 5.8 s to 3.1 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Semicolons split into sentences, the field list is a list, and three sentences that performed or negated now state the fact: 0.5 is the marginal over the noise, no labelled-data calibration has been applied, and the narrow default reads the same as the full canvas within noise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mmastrac
added a commit
that referenced
this pull request
Sep 16, 2026
The held-out run caught every moving slot and flagged no stable one, so `samples: "auto"` (one read, up to four when a slot's first-read entropy is above 0.1 nats) is now the default. A count still fixes the reads. Also restores each read's row entropy in the sample diagnostics, which the squash of #21 predated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mmastrac
added a commit
that referenced
this pull request
Sep 16, 2026
* structured: opt-in auto sampling on first-read entropy, and pinned slots `samples: "auto"` takes one read and the remaining reads (up to `auto_max`, default 4) only when some slot's first-read row entropy is above `auto_threshold` (default 0.1 nats). The threshold was fixed before the held-out run: a separate agent wrote 20 tickets, choosing each one's intended labels first, and the text was never read by the person running the evaluation. Over 60 slots at 16 reads each, 5 moved across noise draws; the rule caught all 5, flagged none of the 55 stable slots, and would have stopped 15 of the 20 tickets at one read (1.3 s instead of 3.1 s). A 0.2 threshold would have missed one mover, so 0.1 stays. `fix_definite` pins the first-read-settled slots to their label for the later reads. On the 5 held-out movers it changed no label and moved agreement by at most 0.25. It estimates the slot conditional on the settled answers rather than the marginal over their noise, for no fewer reads, so it is opt-in. Both stay opt-in in this change; the default is still 4 fixed reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * structured: auto sampling is the default The held-out run caught every moving slot and flagged no stable one, so `samples: "auto"` (one read, up to four when a slot's first-read entropy is above 0.1 nats) is now the default. A count still fixes the reads. Also restores each read's row entropy in the sample diagnostics, which the squash of #21 predated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: this is for diffgemma only
Matching vLLM PR: vllm-project/vllm#57250
Overview
A system message that is a JSON question schema turns a
/v1/chat/completionsrequest into one scored denoise forward: the user message is the state, the reply is a probability distribution per question, and no text is generated. The answer template (id: labelper question) is seeded into the canvas with each label slot as noise, and the label distribution is read from the logits at that slot, so every question in a request costs the same forward and each slot conditions on the whole template bidirectionally. Modeled on TypeSafe's Jev API (choice / score / yes-no questions with probabilities and confidence).What is in it
structured.rs: schema parsing and detection, prompt and template rendering, tokenizer-verified single-token labels, reply JSON.PipelineOp::Score/score_canvas: prefill, seed, run N forwards, read logits rows. Commits nothing; the conversation is finalized with the schema prefix alone so every later state on the schema reuses it.samples,steps,active,hole,climb,climb_mode.narrow_prefill_chunk_bit_identitytest, golden 8/8); a 51-token delta drops from 1.3 s to 0.67 s.DGQ_PREFILL_NARROW=0restores full width.Numbers (M3 Pro, q4 pack, three questions, ~190-token prompt, one server process)
active: 256samples: 1samples: 1,active: 256Same labels from both paths on all three test tickets.
Findings recorded in PLAN.md
stderrandagreement; on that ticket "urgent" is yes 0.60 ± 0.16 at agreement 0.75, and the easy tickets stay unanimous at 1.00 ± 0.00.active: 256opts back).Gates
diffgemma golden: 8/8 byte-identical on the rebased tree.narrow_prefill_chunk_bit_identity: four fingerprints identical.origin/main(one bf16 ulp, from the RNE rounding change; CPU oracles still truncate). Tracked separately.🤖 Generated with Claude Code