Make the co-design architecture claim; ablations as evidence
Browse files
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.
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|