docs/marker-encoder.md
A bidirectional marker encoder, measured against the reference API
The short version
Most models answer by writing text. Needle reads the situation and the options you give it and, in one pass, says how likely each option is. This note is the record of building that model (a 395M-parameter encoder) and testing it against a commercial decision API on identical requests, stage by stage. Inside the kinds of decisions it was trained for it now beats the reference API by a wide margin and answers in about a third of the time. On general knowledge it loses badly, and on task families it has never seen it is still catching up.
What we built
- A bidirectional encoder (ModernBERT-large, 395M parameters). Each candidate option gets a marker token; the marker's final state scores that option. One forward pass scores every option in every question.
- No generation, no sampling. Identical prompts never flip the winner.
- Training runs as a curriculum of stages (A to L), each from the previous checkpoint, on synthetic data labelled by executable rules. The reference API's answers are never used as labels, and a frozen panel of 1,000 cached reference requests is excluded from every training file.
- From stage K on, about 1,700 whole task families are written by an LLM and labelled by running model-written code, so labels are program outputs.
How we measured
- Same requests to both models, constraints sent to neither, scored against oracle labels. Exact McNemar on disagreements, bootstrap for intervals.
- Reference API version 1.13.0, one call per request, no retries.
- Needle's latency is measured in-process on our GPU; the reference's includes HTTP to its provider. Not a fair latency comparison, and we say so.
- One seed per stage except K, which was re-run with a second seed and reproduces within noise (about plus or minus 4 points on the smallest panel).
What we found
- Blind test, 1,180 fields: stage E 89.7% vs 85.8% (95% interval +1.3 to +6.6 points); stage G 98.2%; stage K 97.0%.
- Reordering the answer choices flips the reference API's winner 8.3% of the time (15.6% when the evidence is thin). Stage G: 0%.
- Latency, policy-length prompts: 44/71 ms (p50/p95) vs 118/190 ms.
- New wording and layouts written after training: 97.9% vs 88.2%.
- Unseen task families: stage I 63.9%, stage K 80.4%, reference 96.1%. The 16-point jump came from LLM-synthesized families with executable labels; widening hand-written templates had moved it by nothing.
- General knowledge: MMLU 41.0% vs 88.7%, HellaSwag 82.8% vs 92.6%, WinoGrande 73.8% vs 92.6%. MMLU is bounded by what a 395M model knows; swapping in bigger untrained backbones moved it 17 to 29 points, more data moved it 1.6.
What did not work
- Stage K bought breadth and paid for it in order stability: the reordered choices panel fell from 97.8% to 82.9%. Stage L recovered order (90.0%) and gave back out-of-family accuracy (80.4% to 71.6%). At this data volume the two trade off.
- A 1.5B causal backbone with LoRA bought 12 MMLU points at twice the latency and was slightly behind on policy panels.
- Routing letter-choice questions to an untrained 7B model lifted MMLU by 27 points but lost HellaSwag and WinoGrande, where the fine-tuned encoder beats the untrained 7B.
- Probabilities on general-knowledge tasks are overconfident; the temperature is fitted on judgment-family data only.
What this does not show
- That Needle is a general reasoner or a replacement for the reference API.
- Fair latency, or calibration outside the trained families.
- More than one seed for most stages.
Full working record from the model repository, as written
Single runs, internal names and dead ends are left in. Commercial products are referred to as "the reference API".
MarkerEncoder packs one question, its candidates, and the state into a single
ModernBERT-large sequence. Each candidate is preceded by a [MASK] marker whose
final hidden state is the candidate feature; markers attend to the full state
in both directions. The existing scalar readout and constrained joint head are
reused. Inputs over the token limit raise rather than truncate.
ROCm attention gradients
On Atlas (ROCm, torch 2.8, transformers 5.17) ModernBERT's sdpa attention
gives a correct forward pass and a wrong backward pass: gradient norm 836,172
against 40.7 with eager on the same request, growing about 100,000× from the
last layer to layer 0. Training under sdpa stays at chance. The encoder loads
with attn_implementation="eager".
Training
experiments/train_curriculum.py: 16 requests per update, linear warmup and decay,
backbone learning rate 0.00001, head learning rate 0.001, seed 97. A cold start
on the prose corpus stays at chance, so stages run as a curriculum, each from
the previous checkpoint. Every file passes the frozen reference panel exclusion;
labels come from oracles only.
| Stage | Updates | Data |
|---|---|---|
| A | 400 | joint_fit_message_v1 truth tables |
| B | 1,500 | rlcd_synth_v3 + random-list and joint-fit replay |
| D | 2,000 | B's data, half of samples from reasoning_v1 and the benchmark-clean train cases of judgment_v1 (72) and support_v1 (80); label smoothing 0.1 |
One joint temperature is then fitted on 432 calibration-split requests
(reasoning_v1, judgment_v1, support_v1, semantic_v1) and stored in the
checkpoint. Stage D fits 0.858. An earlier unsmoothed stage C needed 4.30.
Atlas result
One seed. The cached panel is a development panel, not a blind test; reasoning test splits were not evaluated. No new reference API calls.
| cached reference panel, 2,600 fields | Marker D | Marker C + temperature | Qwen replay-plain | Reference API |
|---|---|---|---|---|
| Field accuracy | 96.54% | 91.92% | 49.71% | 92.50% |
| NLL | 0.157 | 0.250 | — | 0.219 |
| Brier | 0.068 | 0.128 | — | 0.119 |
| ECE, 10 bins | 0.014 | 0.029 | — | 0.019 |
| Complete requests correct | 944 | 865 | — | — |
| Latency p50 / p95 ms | 37 / 53 | 37 / 54 | 58 / 88 | 115 / 187 |
Marker D reaches 100% on policy development, paired counterfactual (640 requests
each, NLL 0.032) and random-list retention, against 80.6% policy development for
Qwen replay-plain. Reasoning calibration split: 99.70% field accuracy, NLL 0.039.
Remaining cached-panel errors are all in judgment_v1: action 94.2%, evidence
quality 95.2%, requirement match 92.7% (balanced 89.4%). Public validation
stays at chance (35.7% over hellaswag, mmlu, winogrande); this model is not a
general reasoner.
reference latency includes HTTP; local latency is serial in-process scoring.
Wording data and blind test
Stage D's calibration-split errors on judgment_v1 were polarity errors on unseen
sentences, mostly under a "must be false" requirement, not counting errors. The
existing banks hold 3 propositions with 2–4 train phrasings each, and every split
uses its own evidence layout. decisions/wording_data.py generates 3,312
train-only cases: 15 propositions (12 new domains), 6–8 new phrasings per
polarity, 5 layouts, 3 policy paraphrases, new injection strings, and
single-sentence assertion drills. Labels come from the judgment oracle. A test
asserts the bank shares no sentence with any existing train, calibration or test
phrase. Stage E is stage D's recipe plus this file; temperature 0.736.
| cached reference panel, 2,600 fields | Marker E | Marker D | Reference API |
|---|---|---|---|
| Field accuracy | 99.65% | 96.54% | 92.50% |
| NLL | 0.024 | 0.157 | 0.219 |
| Brier | 0.005 | 0.068 | 0.119 |
| ECE, 10 bins | 0.013 | 0.014 | 0.019 |
| Complete requests correct | 991 | 944 | — |
| Latency p50 / p95 ms | 41 / 57 | 37 / 53 | 115 / 187 |
Test splits (experiments/evaluate_blind.py, up to 256 requests per file, joint
accuracy). D was frozen before its single test run. E was selected on the cached
panel and calibration splits only, but its run is the second look at these files.
The reference API was not run on them.
| Test file | Marker E | Marker D | Qwen replay-plain |
|---|---|---|---|
judgment_v1 (192) |
95.3% | 83.9% | 27.6% |
reasoning_v1 |
97.3% | 98.4% | 18.8% |
reasoning_followup |
99.2% | 99.6% | 24.2% |
support_v1 (60) |
100% | 100% | 18.3% |
semantic_v1 (48) |
97.9% | 97.9% | 33.3% |
10 rlcd_synth_v3 policies |
100% each | 100% each | 48.0–100% |
E keeps 100% policy development, counterfactual and retention accuracy.
Matched reference comparison on new requests
599 new paid the reference API (v1.13.0) calls (no retries, none failed) on requests neither model
trained on, scored against oracle or official labels. Both models receive the
same payload; cross-field constraints are not sent to either, so local numbers
here are lower than the constrained evaluations above. Paired statistics come
from experiments/compare_cached_panel.py: exact McNemar on discordant fields and
a request-clustered bootstrap. Evidence.
| Panel | Fields | Reference acc. | Marker E acc. | Only the reference API / only E correct | McNemar p | Reference NLL | E NLL |
|---|---|---|---|---|---|---|---|
| Cached development | 2,600 | 92.5% | 99.7% | 9 / 195 | 1e-46 | 0.219 | 0.024 |
| Blind test, all | 1,180 | 85.8% | 89.7% | 71 / 117 | 1e-3 | 0.331 | 0.240 |
| — judgment, support, semantic, reasoning-followup tests | 484 | 91.7% | 99.8% | 1 / 40 | — | — | — |
— unseen_v1 (new propositions, layout, policy text) |
216 | 90.3% | 88.9% | 22 / 19 | — | 0.240 | 0.376 |
— policy condition (majority, override, veto) |
240 | 92.5% | 100% | 0 / 18 | — | — | — |
— policy action, unconstrained |
240 | 63.3% | 60.0% | 48 / 40 | — | 0.688 | 0.795 |
| Public validation (HellaSwag, MMLU, WinoGrande) | 99 | 96.0% | 37.4% | 59 / 1 | 1e-16 | 0.180 | 1.310 |
Blind-test accuracy difference, 95% interval: +1.3 to +6.6 points; complete requests correct 390 against 363 of 500. At 75% coverage the marker model is 98.8% accurate against the reference API's 93.2%. Latency p50 / p95: 41 / 63 ms local against 109 / 192 ms for the reference API over HTTP.
What this shows:
- Inside the trained task families the marker model beats the reference API on held-out wording, in accuracy and probability quality.
- On
unseen_v1the two are statistically tied on accuracy, and the reference API is better calibrated. The reference API is ahead onaction(91.7% against 81.9%, p = 0.17). - The policy
actionfield is only solved locally with the typed constraint that ties it tocondition; without it both models are near 60%. - On general knowledge and commonsense questions the marker model is at chance and the reference API is at 96%. This is a narrow model, not a the reference API replacement.
decisions/unseen_data.py generates unseen_v1 (test-only; a test asserts no
sentence or proposition is shared with any training generator). decisions.teacher
takes --split and --ungrouped; collecting a non-training split is an explicit
opt-in. The new collections are frozen as data/reference_blind_v1 and
data/reference_public_v1 and must never become training sources.
Stages F and G: public train splits, constraint dropout, policy paraphrases
Stage F continues E for 6,000 updates with three changes. decisions/public_train_data.py
samples 10,000 rows per task from the official HellaSwag, WinoGrande and MMLU
auxiliary train splits in the frozen typed format (validation rows and states over
1,500 characters excluded); these take about 45% of samples. --constraint-dropout 0.5
removes the typed constraints from half the requests, because the blind comparison
showed policy action was only solved through its constraint. wording_v2 widens
the bank to 31 propositions.
F regressed on unseen_v1 action (81.9% to 75.0%). Every error was a review case
(conflict, quorum shortfall, repeated source): the review rule was known only in
the wording of four policy texts. Stage G (2,500 updates from F) trains on
wording_v3: six more policy paraphrases plus "do kept observations disagree" and
"is it resolved" drills over all propositions. unseen_v1 and order_v1 were used
for that diagnosis and are development sets for G; unseen_v2 (new propositions,
XML layout, new policy text) was generated before G trained.
All panels below are paired with the reference API (v1.13.0) on identical requests, constraints sent to neither, G temperature 0.631. Evidence.
| Panel | Fields | Reference acc. | G acc. | Only the reference API / only G | Reference NLL | G NLL |
|---|---|---|---|---|---|---|
| Cached development | 2,600 | 92.5% | 99.8% | — | 0.219 | 0.012 |
| Policy development | 1,280 | 78.3% | 99.8% | 0 / 276 | 0.599 | 0.012 |
— action |
640 | 65.0% | 99.7% | 0 / 222 | 0.939 | 0.016 |
| Paired counterfactual | 1,280 | 76.7% | 99.9% | 0 / 297 | 0.585 | 0.014 |
| Random-list retention | 1,024 | 89.7% | 100% | 0 / 105 | 0.287 | 0.007 |
| Blind test | 1,180 | 85.8% | 98.2% | 18 / 164 | 0.331 | 0.073 |
— unseen_v1 (development for G) |
216 | 90.3% | 91.7% | 16 / 19 | 0.240 | 0.301 |
unseen_v2 |
432 | 88.2% | 97.9% | 9 / 51 | 0.355 | 0.053 |
| Order rotations (development for G) | 1,008 | 93.9% | 97.0% | 30 / 61 | 0.198 | 0.114 |
| HellaSwag validation | 256 | 92.6% | 82.8% | 37 / 12 | 0.387 | 0.695 |
| WinoGrande validation | 256 | 92.6% | 73.8% | 60 / 12 | 0.379 | 0.941 |
| MMLU validation | 256 | 88.7% | 41.0% | 127 / 5 | 0.823 | 2.311 |
Latency p50 / p95 ms, the reference API over HTTP against local: public 102 / 160 vs 20 / 35; policy 118 / 190 vs 44 / 71.
Choice-order and repeat stability (decisions/order_data.py, 96 unseen-proposition
cases × 3 rotations of the action choices, 48 identical repeats):
| Reference API | Marker E | Marker G | |
|---|---|---|---|
| Identical prompt, winner flips | 2.1% | 0% | 0% |
| Reordered choices, winner flips | 8.3% | 3.1% | 0% |
| — low evidence (absent, conflict, shortfall, repeated source) | 15.6% | 6.3% | 0% |
| — resolved evidence | 4.7% | 1.6% | 0% |
| Noul shift under reorder | 0.035 | 0.004 | 0.001 |
| First-listed option wins (unbiased 33.3%) | 32.3% | 33.0% | 33.3% |
The reference API's order effect concentrates where the state gives no dominant signal and is not a first-position prior, consistent with the evidence-alignment reading. Local inference is deterministic and training shuffles choice order every sample.
Remaining gaps: MMLU is bounded by what a 395M encoder knows; HellaSwag and
WinoGrande trail the reference API by 10 and 19 points after about 1.5 epochs of a 10,000-row
sample. Calibration on unseen_v1 is still worse than the reference API's. Public-task
probabilities are overconfident (the temperature is fitted on judgment-family
calibration splits only).
Why MMLU stays low: two probes
Stage G fits its own MMLU auxiliary training rows (93% on a 300-row sample) but scores 41.0% on validation: humanities and social 49%, STEM 39%, law and professional 29%, moral scenarios 24%. The format and optimisation work; the gap is generalisation. The auxiliary split is reading comprehension and grade-school science, not the recall MMLU tests.
Data probe: stage H continues G for 3,000 updates on closed-book questions
(decisions/knowledge_train_data.py: MedMCQA, SciQ, CommonsenseQA, plus auxiliary
rows of at most 400 characters; validation questions excluded). Headroom probe:
experiments/probe_zero_shot.py scores untrained Qwen2.5 Instruct backbones on the
same 256 validation rows per task.
| Model | MMLU | HellaSwag | WinoGrande |
|---|---|---|---|
| Marker G, 395M, fine-tuned | 41.0% | 82.8% | 73.8% |
| Marker H, knowledge data | 42.6% | 76.6% | 78.1% |
| Qwen2.5 0.5B, zero-shot | 39.8% | 36.7% | 48.4% |
| Qwen2.5 1.5B, zero-shot | 56.6% | 59.4% | 53.5% |
| Qwen2.5 7B, zero-shot | 68.4% | 77.3% | 68.4% |
| the reference API (v1.13.0) | 88.7% | 92.6% | 92.6% |
Better-matched data moves MMLU by 1.6 points, inside the ±3 point noise of 256 rows; backbone size moves it by 17 and 29 points with no training. MMLU is bounded by knowledge stored at pretraining. HellaSwag and WinoGrande are skills that fine-tuning teaches: the 395M encoder beats the untrained 7B model on both. Stage H is a probe, not a promoted checkpoint; its judgment and policy panels were not run.
Dense causal backbone: Qwen2.5-1.5B with LoRA
experiments/initialize_causal.py wraps Qwen2.5-1.5B-Instruct in the packed-prefix
candidate scorer and joint head. decisions/lora.py is a dependency-free LoRA
(rank 16 on every linear layer, merged into the weights on save; 6 GB while
training). Gradients are healthy under SDPA on this path: the truth-table stage
reaches 100% by update 120. One mixed stage follows: 3,000 updates of 16 requests,
learning rate 0.0001, stage G's data plus the knowledge sets, label smoothing 0.1,
constraint dropout 0.5; temperature 0.341. Same paired protocol as above.
| Panel | Fields | Reference API | Marker G (395M) | Qwen 1.5B LoRA |
|---|---|---|---|---|
| Cached development | 2,600 | 92.5% | 99.8% | 99.8% |
| Policy development | 1,280 | 78.3% | 99.8% | 97.0% |
| Paired counterfactual | 1,280 | 76.7% | 99.9% | 98.0% |
| Random-list retention | 1,024 | 89.7% | 100% | 100% |
| Blind test | 1,180 | 85.8% | 98.2% | 98.3% |
— unseen_v1 |
216 | 90.3% | 91.7% | 98.1% |
unseen_v2 |
432 | 88.2% | 97.9% | 94.0% |
| Order rotations | 1,008 | 93.9% | 97.0% | 98.1% |
| HellaSwag | 256 | 92.6% | 82.8% | 81.2% |
| WinoGrande | 256 | 92.6% | 73.8% | 70.7% |
| MMLU | 256 | 88.7% | 41.0% | 53.1% |
| Latency p50 / p95 ms, policy | 118 / 190 | 44 / 71 | 82 / 100 |
Order and repeat winner flips are 0% for both local models. The larger backbone
buys 12 MMLU points and better unseen_v1 transfer, at twice the latency, and is
slightly behind the marker encoder on policy panels and unseen_v2 after a fifth
of the marker's training updates. Its fine-tuned MMLU (53.1%) is below its own
zero-shot letter-token score (56.6%): scoring each option as a separate
"does this candidate answer" path is a worse fit for a causal model than reading
the letter directly, and LoRA on about 10,000 short MMLU-style rows adds no
knowledge.
Routing letter-choice questions to a zero-shot 7B model
decisions/router.py sends a request to a knowledge model only when it is one
unconstrained choice over option letters; everything else stays on the marker
encoder. The knowledge model is untrained Qwen2.5-7B-Instruct in bf16 reading the
letter from its language-model head (decisions.benchmark --knowledge-model).
Both fit in 24 GB. Paired with the reference API on the 768 public validation rows:
| Reference API | Marker G alone | Routed, Qwen 1.5B | Routed, Qwen 7B | |
|---|---|---|---|---|
| MMLU | 88.7% | 41.0% | 56.6% | 68.4% |
| HellaSwag | 92.6% | 82.8% | 59.4% | 77.3% |
| WinoGrande | 92.6% | 73.8% | 53.5% | 68.4% |
| NLL, all 768 | 0.530 | 1.316 | 2.021 | 2.833 |
| Latency p50 / p95 ms | 102 / 160 | 20 / 35 | 42 / 56 | 87 / 136 |
The blind panel routes nothing to the knowledge model and is unchanged (98.2%, 40 / 63 ms). On policy-length prompts (563 input tokens per request) the 7B model alone takes 162 / 250 ms against the reference API's 118 / 190 ms over HTTP, and is untrained there (59.2% field accuracy).
Routing by format lifts MMLU by 27 points but loses HellaSwag and WinoGrande, where the fine-tuned 395M encoder beats the untrained 7B model; the three tasks share one request shape, so format cannot separate them. Zero-shot probabilities are badly overconfident (ECE 0.26). The remaining levers are fine-tuning the 7B letter readout with LoRA on the HellaSwag and WinoGrande train splits and fitting a temperature for that route.
Out-of-family tasks and stage I
decisions/families_data.py is a test-only benchmark of four task families outside
every training schema (invoice matching, incident triage, refunds, access
requests): new field names, numeric thresholds, percent tolerances, rule precedence,
no typed constraints. 400 requests, 900 fields, paired with 400 reference API calls.
Stage I continues G for 12,000 updates with decisions/schema_data.py (20,000
train-only cases, each a random task: 12 domains disjoint from the held-out
families, random fields, thresholds, percent tolerances, code lists, 2–5 outcomes,
rule order, three rule phrasings, four layouts; labels from executing the rules)
and the full HellaSwag and WinoGrande train splits (about 39,000 rows each, 500 more
per task held out as calibration rows). Temperature 0.584.
| Panel | Fields | Reference API | Marker G | Marker I |
|---|---|---|---|---|
| Out-of-family, all | 900 | 94.1% | 50.2% | 66.6% |
| Out-of-family NLL | 0.149 | 1.675 | 1.472 | |
| HellaSwag | 256 | 92.6% | 82.8% | 86.7% |
| WinoGrande | 256 | 92.6% | 73.8% | 80.5% |
| MMLU (not trained; routed elsewhere) | 256 | 88.7% | 41.0% | 41.4% |
| Cached development | 2,600 | 92.5% | 99.8% | 99.8% |
| Policy development | 1,280 | 78.3% | 99.8% | 99.8% |
| Blind test | 1,180 | 85.8% | 98.2% | 98.6% |
unseen_v2 |
432 | 88.2% | 97.9% | 96.3% |
| Order rotations | 1,008 | 93.9% | 97.0% | 97.8% |
On 400 fresh tasks from its own generator stage I scores 96.5% on the outcome,
96.9% on condition checks and 90.9% on tallies, including thresholds (91–98%) and
percent tolerances (90%). Rule execution is learnable at this size; the
out-of-family gap is generator breadth. The weakest held-out fields need primitives
the generator lacks: a question derived from the outcome (page_oncall, 50%),
exact text equality between two fields plus a tolerance (disposition, 34%), and
counting factors named in the rules (risk, 37%). The benchmark was written before
the generator, so primitive choice is informed by it; vocabulary and domains are
disjoint (tested).
Stage J and the limit of hand-written task templates
decisions/families2_data.py is a second out-of-family holdout (expense claims,
service-credit tiers, age ratings, pharmacy refills; 675 fields, 300 reference API calls),
written before the generator changed and not used to tune it. schema-v2 then
added the primitives stage I lacked (consequences derived from the outcome,
same-person text matches, factor lists defined in the rules), two rule phrasings, a
sentence layout and six domains; stage J continues I for 6,000 updates on 30,000
such cases. Stage J is not promoted.
| Panel (field accuracy) | Reference API | Marker I | Marker J | Qwen 7B zero-shot | Qwen 1.5B zero-shot |
|---|---|---|---|---|---|
families_v2 (clean holdout) |
96.1% | 63.9% | 63.6% | 63.8% | 36.2% |
families_v1 (informed the generator) |
94.1% | 66.6% | 71.9% | 52.0% | 37.8% |
| Cached development | 92.5% | 99.8% | 99.1% | — | — |
| Blind test | 85.8% | 98.6% | 97.5% | — | — |
| HellaSwag / WinoGrande | 92.6 / 92.6% | 86.7 / 80.5% | 83.6 / 77.3% | 77.3 / 68.4% | — |
Widening hand-written templates lifts only the holdout that informed them: the
model learns the generator, not rule-reading in general. An untrained 7B model is
no better on unseen families than the trained 395M encoder, so the reference API's 94–96% is not
explained by size alone at the scale Atlas can run; it implies training on a far
broader task distribution. The scalable lever is task diversity from a generator
that does not share an author with the holdouts, for example LLM-written task
families with executable oracles through the existing decisions.llm_synth
admission pipeline.
Task-family synthesis with Bend oracles (synth/)
synth/ is a Rust pipeline that asks a writer model for whole task families and
labels every case by running model-written Bend 2 code. A family is a JSON spec:
fields (flags, categories, numbers with sampling boundaries; numbers may be
rules-text parameters so thresholds vary per case), outcomes, questions (one
outcome choice plus noul and score questions), prose rules with placeholders,
2-4 state layouts, and bend_defs. The harness wraps the defs in a State
record, argument parsing and printing, plus a helper library (if_str,
if_u32, abs_diff, within_pct, count3) so the model seldom needs Bend's
restricted match. Bend's checker gives purity, termination and no IO for free.
Per family: type-check and native compile (0.7 s), 600 sampled states for
admission (every outcome reachable at >=3%, no constant question, every field
changes some answer, deterministic), up to two repair calls that feed the
checker's message back, a reviewer call that applies each case's own filled
rules by hand to 12 cases (agreement is recorded; rendering skips families under
0.75), and a held-out-phrase filter (holdout_vocabulary.txt) so no synthesized
family reuses a benchmark family's domain. Every provider call is journaled
before it is sent with a worst-case cost reservation; nothing retries.
Pilots with GLM 5.3 Flash: the helper library lifted acceptance from 6/16 to
14/19; with per-case reviewer rules, 8/12 accepted and agreement 0.83-1.0.
Cost is about $0.002 per accepted family. render draws balanced cases per
family and holds out 5% of families by hash as a rotating out-of-family test set.
Stage K: training on synthesized families
The first synthesis run produced 1,870 families (69% of attempts, $5.13); 1,705 passed the 0.75 reviewer-agreement gate and rendered to 68,194 rows, with 96 families (3,840 rows) held out by hash. Stage K continues I for 12,000 updates with those rows at about 40% of samples. Paired with the reference API as before. Evidence.
| Panel (field accuracy) | Reference API | Marker I | Marker K |
|---|---|---|---|
families_v2 (clean holdout) |
96.1% | 63.9% | 80.4% |
families_v1 |
94.1% | 66.6% | 89.3% |
| Synthesized held-out families (256 requests) | — | — | 93.4% |
unseen_v2 |
88.2% | 96.3% | 98.1% |
| Cached development | 92.5% | 99.8% | 99.6% |
| Blind test | 85.8% | 98.6% | 97.0% |
| Order rotations | 93.9% | 97.8% | 82.9% |
| HellaSwag / WinoGrande | 92.6 / 92.6% | 86.7 / 80.5% | 84.4 / 77.7% |
Model-designed families move the clean out-of-family holdout by 16 points where
hand-written templates moved it by nothing. The remaining families_v2 errors
are concentrated in two fields whose rules derive a yes/no answer from the
outcome (money_paid_now 53%, concerns 55%). The order-rotation drop is not
instability (winner flips stay at 3%) but low-evidence judgment cases falling
to 48%: the "unresolved means review" rule was crowded out by the synthetic
weighting. Stage L continues K with judgment data reweighted up and synthetic
rows down.
Stage L (6,000 updates from K, judgment files at twice the weight, wording_v3 at
4, synthetic rows at about half K's share) recovers the order panel (82.9% to
90.0%, low-evidence action 48% to 79%) and the blind test (97.0% to 97.6%), but
gives back out-of-family accuracy: families_v2 80.4% to 71.6%, families_v1
89.3% to 88.2%, synthesized holdout unchanged at 93.5%. Reweighting trades one
capability for the other at this data volume; K stays the reference checkpoint,
and the lever for both is more synthesized families with judgment replay kept.
Throughput
The pipeline is bound by provider latency, not local work: a call takes about
22 s (800-1,600 completion tokens from GLM Flash) and a family needs about 2.4
sequential calls, against under 2 s of Bend checking, compiling and admission.
synth run is therefore one process with --in-flight worker threads (default 64,
at most 256) sharing one budget ledger that locks only for bookkeeping, never
across an HTTP call. Throughput scaled linearly up to 64 concurrent requests
with no rate limiting from the provider: 8 in flight gave 8 attempts per minute,
64 gave about 80. Reviewer replies were cut from 810 to 590 completion tokens
by asking for compact one-line JSON and 8 cases instead of 12; the writer's
1,500-token reply is the family itself and cannot shrink much. The admission
repair loop, boundary-biased resampling, literal-derived boundaries and the
narrower vocabulary filter raised acceptance from 69% to 92% of attempts.
Second seed and second synthesis run
The K recipe with seed 1997 reproduces seed 97 on every panel within noise except
the small clean holdout: families_v2 73.8% against 80.4% (675 fields), families_v1
89.9% against 89.3%, synthesized holdout 94.0% against 93.4%, order 83.6% against
82.9%, blind 97.1% against 97.0%, policy development 100% against 99.5%. Treat
families_v2 as accurate to about plus or minus 4 points per seed.
The second synthesis run (64 in flight, yield fixes) accepted 3,950 families from about 4,300 attempts (92%) for $11.17, mean reviewer agreement 0.94, 86 families under the 0.75 gate.
Stage M: both synthesized datasets
Stage M continues K for 8,000 updates over 176,000 synthesized rows (1,705 + 3,864 families) with judgment files at twice K's weight. Paired with the reference API as before.
| Panel (field accuracy) | Reference API | Marker K | Marker M |
|---|---|---|---|
families_v2 (clean holdout) |
96.1% | 80.4% | 79.7% |
families_v1 |
94.1% | 89.3% | 90.8% |
| Synthesized holdouts (run 1 / run 3) | — | 93.4% / — | 94.0% / 93.2% |
unseen_v2 |
88.2% | 98.1% | 98.4% |
| Blind test | 85.8% | 97.0% | 96.4% |
| Order panel accuracy | 93.9% | 82.9% | 80.1% |
| HellaSwag / WinoGrande | 92.6 / 92.6% | 84.4 / 77.7% | 84.4 / 80.5% |
Doubling the synthesized corpus did not move the clean holdout: the gain is in
specific fields (money_paid_now 53% to 89%, access:risk 73% to 80%) while
three primitives stay weak wherever they appear: counting factors that the rules
define (rating:concerns 53%), differences between two numbers in the state
compared against a step (uptime:credit 63%, invoice:amount_within_tolerance
63%), and a category label that changes which threshold applies (rating:label
69%). Breadth is no longer the lever; those primitives are underrepresented in
what the writer model produces unprompted, so the next batch requests them.
experiments/run_marker_encoder.sh