flashvenom commited on
Commit
81c08e1
Β·
verified Β·
1 Parent(s): 76af275

Make the co-design architecture claim; ablations as evidence

Browse files
Files changed (1) hide show
  1. README.md +100 -30
README.md CHANGED
@@ -76,9 +76,13 @@ out, at 48M parameters.
76
 
77
  It does not converse, reason, or write prose β€” it was never trained to. It reads
78
  a catalog of typed functions and a request, and returns the calls to make or an
79
- empty list when nothing fits. That narrowness is the point: the whole job fits in
80
- 48M parameters, small enough that specializing it to one API surface is routine
81
- rather than a project.
 
 
 
 
82
 
83
  [**GitHub β€” code, adaptation loop, full experimental record**](https://github.com/nikshepsvn/thimble) Β· MIT Β· 48.12M params Β· 768-token context
84
 
@@ -179,37 +183,103 @@ Two things about that recipe are load-bearing, both measured rather than assumed
179
  scratch and 33.1 annealed into the LR-decay phase. **Keep the guard data** β€”
180
  annealing purely on your catalog trades away the competence you are building on.
181
 
 
 
 
182
  `adapt.py` wires together exactly the machinery that produced the v6 result, but
183
  no third-party catalog has been adapted and published yet. The recipe is
184
  measured; the ergonomics are new.
185
 
186
- ## How it was built
187
-
188
- **1. Tool calling is five decisions, not a generation problem.** A grammar
189
- compiled from the tool schemas force-feeds all JSON structure β€” braces, quotes,
190
- and every argument key. The model is consulted at exactly five choice points:
191
- *refuse or call Β· which tool Β· include this optional? Β· what value Β· stop or
192
- continue*. Malformed JSON, hallucinated parameter names, and calls to
193
- nonexistent tools are **unreachable, not unlikely**. At 48M parameters, capacity
194
- spent learning that `{` follows `[` is capacity wasted.
195
-
196
- Measured honestly, the grammar is a *reliability* mechanism rather than an
197
- accuracy one β€” on Mobile Actions, free generation and constrained decoding agree
198
- on 150 of 150 rows. What it buys is that the worst failure modes cannot be
199
- expressed at all.
200
-
201
- **2. Every training example earns its place.** Row accuracy factors as
202
- `P(name sequence) Γ— pⁿ`. Each version measured which factor was binding and
203
- attacked only that. The final data round was synthesized directly against the
204
- previous model's diagnosed failure buckets β€” spurious optional arguments,
205
- wrong-slot entity binding, date canonicalization β€” with a mid-training causal
206
- check (+3.3 points at constant LR, attributable to the corrective data alone).
207
-
208
- **3. Anneal, don't retrain.** A controlled twin experiment: the corrective
209
- corpus fed from scratch *diluted* (28.4); the same corpus **annealed into the
210
- learning-rate decay phase** of a continued run *concentrated* (33.1). The decay
211
- phase is where a WSD-trained model crystallizes β€” that is where the good data
212
- belongs. This is probably the most portable result in the project.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  ## What didn't work (measured, not guessed)
215
 
 
76
 
77
  It does not converse, reason, or write prose β€” it was never trained to. It reads
78
  a catalog of typed functions and a request, and returns the calls to make or an
79
+ empty list when nothing fits.
80
+
81
+ That narrowness is the design, not a limitation of it. The tokenizer, the
82
+ training loss, and the decoder are all built around the same five decisions, so
83
+ the model is never asked to spend capacity on JSON it will never emit. The whole
84
+ job then fits in 48M parameters β€” small enough that specializing it to one API
85
+ surface is routine rather than a project.
86
 
87
  [**GitHub β€” code, adaptation loop, full experimental record**](https://github.com/nikshepsvn/thimble) Β· MIT Β· 48.12M params Β· 768-token context
88
 
 
183
  scratch and 33.1 annealed into the LR-decay phase. **Keep the guard data** β€”
184
  annealing purely on your catalog trades away the competence you are building on.
185
 
186
+ Needs `OPENROUTER_API_KEY` for synthesis and a GPU to train. For scale, the v6
187
+ cycle synthesized 74,250 validated rows for $56.
188
+
189
  `adapt.py` wires together exactly the machinery that produced the v6 result, but
190
  no third-party catalog has been adapted and published yet. The recipe is
191
  measured; the ergonomics are new.
192
 
193
+ ## How it works
194
+
195
+ Most constrained-decoding systems bolt a grammar onto a model trained to generate
196
+ free text, then manage the mismatch. Here the **tokenizer, the training loss, and
197
+ the decoder are one design**, built around the same five decision points:
198
+ refuse-or-call, which tool, include this optional, what value, stop or continue.
199
+
200
+ **The tokenizer is built for the grammar.** JSON structural characters β€” and
201
+ digits β€” are singleton tokens. Structure can therefore be force-fed *exactly*,
202
+ with no token-healing and no ambiguity about where a constraint lands. The usual
203
+ arrangement masks logits over a vocabulary that merged `",` into a single token
204
+ and papers over the seam. Digits never merge either, so a copied number tokenizes
205
+ the same way every time; the rebuild was verified by a fragmentation gate
206
+ (word-value fragmentation 2.72 β†’ 2.34 tokens/word, digits lengthening by design).
207
+ It shipped as part of the v4 β†’ v5 bundle that took name-sequence accuracy from
208
+ 80.4% to 91.5% β€” that bundle also added 350k corpus rows and reweighted the mix,
209
+ so the tokenizer's own share of the gain was never isolated.
210
+
211
+ **The loss is weighted by those same five decisions** β€” structure 1x, keys 1.5x,
212
+ names 2x, values 4x, stop-decision 6x β€” matched to the measured error
213
+ distribution. The model is optimized for the choices it will be asked to make,
214
+ not for tokens it will never emit.
215
+
216
+ **The decoder consults the model only at those points.** Everything else is
217
+ determined before it runs, which is where the contract above comes
218
+ from.
219
+
220
+ ### Evidence the co-design works
221
+
222
+ Two measurements that look like caveats in isolation are the proof in context.
223
+
224
+ **There is no projection tax.** The same rows decoded with the grammar and with
225
+ no grammar at all (`scripts/draft_vs_constrained.py`):
226
+
227
+ | suite | free generation | grammar-constrained |
228
+ |---|---|---|
229
+ | Mobile Actions (150) | 78.7 | 78.7 |
230
+ | Seal-Tools in (150) | 26.7 | 28.0 |
231
+
232
+ On Mobile Actions the two agree on **150 of 150 rows**. The grammar is not
233
+ overriding the model β€” the model already wants what the grammar enforces. A
234
+ bolted-on grammar produces disagreement and a tax to recover; this is why
235
+ draft-then-constrain (DCCD) had nothing to recover here and was abandoned.
236
+
237
+ Stated plainly, because the distinction matters: the grammar buys *reliability*,
238
+ not accuracy. "Constrained decoding makes the model correct" would be a different
239
+ claim and not one this data supports. What it buys is that the worst failure
240
+ modes cannot be expressed, plus parseability on the ~11% of Seal rows where free
241
+ generation emits invalid JSON.
242
+
243
+ **And the co-design is load-bearing, not decorative.** Down-weighting the
244
+ grammar-forced tokens in the loss β€” on the theory that the model need not learn
245
+ what the decoder will supply β€” cost **12 points** in a controlled twin run. Those
246
+ tokens carry the call-sequencing signal: the model learns *when a call ends*
247
+ through structure it never has to emit. Remove them and it breaks.
248
+
249
+ ### The rest of the stack
250
+
251
+ - **Retriever** β€” `retrieve(query, tools, emitted=...)`, a DTDR-style
252
+ (arXiv 2512.17052) refresh conditioned on the *partial plan*, so the candidate
253
+ set is recomputed after each emitted call rather than once per request.
254
+ - **Name head** β€” a bilinear readout scoring candidate tool-name spans in the
255
+ prompt against the hidden state at the decision position. Selection is treated
256
+ as pointing at the prompt, not generating from a vocabulary, following "Looking
257
+ Is Not Picking" (arXiv 2606.16364): mis-selection is a readout failure, not a
258
+ perception one. Its only positive result was on *unfamiliar* catalogs (+2.2),
259
+ which is why it is on by default for your own tools.
260
+ - **Trunk** β€” deep-thin and gated: d=448, 20 layers, GQA 8/4, SwiGLU x2.0,
261
+ QK-norm, sandwich RMSNorm, tied embeddings, Muon on 2D weights and AdamW on
262
+ embeddings, norms and heads. This part is standard modern practice and is not
263
+ where the advantage is; a controlled study from the Needle authors
264
+ (arXiv 2607.18363) finds architecture choices at this scale worth hundredths of
265
+ a nat at matched parameters. The co-design above is the part that matters.
266
+
267
+ ### How the model was built
268
+
269
+ Row accuracy factors as `P(name sequence) x p^n`, where `p` is per-call argument
270
+ accuracy. Each version measured which factor was binding and attacked only that:
271
+
272
+ | version | name seq | p | Seal-in | what changed |
273
+ |---|---|---|---|---|
274
+ | v4 | 80.4% | 0.593 | 24.3 | baseline |
275
+ | v5 | 91.5% | 0.60 | 31.4 | 16k digit-singleton tokenizer, +350k corpus rows, seal_train x6, dev-selected EMA |
276
+ | **v6** | ~92% | ~0.66 | **33.1** | error-driven synth against three measured failure buckets, annealed into the decay phase |
277
+
278
+ The v6 data round came straight from the v5 diagnostic: of 193 failing calls, 66
279
+ added exactly one unmentioned optional, ~35 bound the wrong entity, ~30 missed
280
+ canonical date forms, ~29 were unwinnable noise in the gold. Mid-run causal
281
+ check: **+3.3 points at constant LR** from the corrective corpus alone. That loop
282
+ is what `adapt.py` automates for your catalog.
283
 
284
  ## What didn't work (measured, not guessed)
285