Generating a Math Corpus from First Principles
Options considered, the setup, and the issues

Table of Contents

1. The problem

I have a 17.7M-parameter GPT-2-architecture model trained from scratch on 66.7M tokens of English composed before 1543. It writes creditable Middle English. It cannot do arithmetic, and I want to find out what it takes to change that without silencing the voice.

The corpus cut is De revolutionibus, 1543. Robert Recorde introduced the equals sign in The Whetstone of Witte in 1557. The model's world predates the equals sign by fourteen years — which is a cute way of saying the notation problem here is unusually total.

This note documents the options I considered for generating the training data, the constraints I measured before choosing, and the issues I expect to hit. It is written before building, so it can be wrong in recorded ways.

2. The setup, measured

Everything below was measured on the actual artifacts rather than assumed. Machine is an Apple M4, 16 GB.

2.1. The model

parameters (weight-tied) 17,665,792
token embedding 12,865,792 (73%)
transformer proper 4,800,000
layers / heads / width 6 / 8 / 256
context 256 tokens
vocabulary GPT-2 BPE, 50,257
best validation loss 3.4112 (perplexity 30.3)

The headline parameter count is misleading. Three quarters of it is a lookup table; the reasoning budget is 4.8M parameters.

2.2. What it has read

Three measurements over the training corpus, each ruling out a different explanation for the failure.

It has the numerals. A 60-text sample (38.5M characters) contains 95,135 integers — 2,468 per million characters. Every integer 0–99 is a single GPT-2 token and every one of those 100 tokens appears in the corpus. Those embedding rows are trained.

It has never seen an operation. The same 38.5M characters contain sixteen expressions matching \d+ \s* [+-×x] \s* \d+ \s* =. Sixteen. The model has read numerals as nouns — regnal years, chapter and verse, counts of ships — and has essentially never seen one used as an operand.

It has never seen the notation. Against the 2,543 distinct tokens in MATH-500's problems, 99 ids never occur in the corpus at all — 3.8% of the benchmark's problem tokens. They are not exotic vocabulary; they are the skeleton:

token occurrences in MATH-500
~ $\~ 222
frac 173
$, 166
$. 137
.$ 94
~ \\~ 83

Of 306 texts, exactly one is mathematical — Proclus's commentary on Euclid Book I. Philosophy about mathematics, containing no calculation.

So there are three separable deficits: numerals (present), operations (absent), notation (absent). They need different remedies, and conflating them is the first mistake available.

2.3. The tokenizer, which turns out to be the main antagonist

0–99       : 100% single-token
100–999    : 79.9% single-token, 20.1% split
1000–9999  :  1.0% single-token, 98.7% two-token, 0.3% three-token

The count is not the problem. The shape distribution is. Across all 9,000 four-digit numbers there are five distinct split shapes:

shape count
(2,2) 5807
(1,3) 2538
(3,1) 538
(4,) 88
(1,2,1) 29
1234 -> ['12','34']        5678 -> ['5','678']
1543 -> ['15','43']        2019 -> ['2019']      1024 -> ['1024']

Place value is not aligned across tokens, and it is not even consistent between two numbers of the same length. The single-token four-digit numbers are years and powers of two — corpus artifacts of whatever GPT-2's BPE was fit on. From the model's side, the tokenizer's treatment of numbers is arbitrary.

Also: '7' is token 22 and ' 7' is token 767. Different objects entirely.

And the LaTeX commands are multi-token sequences that cannot be decoded from parts — \frac is 2 tokens, \sqrt and \cdot and \boxed are 3 each. In literacy-curriculum terms these are sight words, and that framing falls out of the BPE rather than being imposed on it.

3. Options considered

3.1. 1. Download an existing dataset

Surveyed and verified live: deepmind/math_dataset (112M rows), AMPS via XinyaoHu/AMPS_mathematica (4.8M rows, genuine LaTeX, 137 sub-topics), math-ai/TemplateGSM, apple/GSM-Symbolic, nvidia/OpenMathInstruct-2 (14M rows), allenai/lila, meta-math/MetaMathQA, open-web-math, EleutherAI/proof-pile-2.

Rejected as the primary path. The register is wrong — every one of them is modern English, and fine-tuning a 4.8M-parameter transformer on modern mathematical prose will drag the pre-1543 voice with it. Some are also encumbered: apple/GSM-Symbolic is CC-BY-NC-ND-4.0, and "NoDerivatives" is a poor fit for training data.

3.2. 2. Generate with an LLM

Rejected on arithmetic. At ~62 tokens per problem a corpus of a few hundred thousand problems is hours of generation and hundreds of dollars, is not reproducible from a seed, and adds the circularity of teaching one language model from another's output. Volume is the one axis where programs beat models outright.

The one thing an LLM is worth using for is writing the grammar — perhaps 10,000 tokens of templates, once — rather than the corpus.

3.3. 3. Adapt deepmind/mathematics_dataset

This is the canonical prior art (Saxton et al., arXiv:1904.01557, ICLR 2019) and it is genuinely good: 56 train modules, 56 interpolate, 15 extrapolate; difficulty as a sampled entropy budget sliced into thirds rather than hand-tuned levels; SymPy load-bearing for construction; Apache-2.0.

Two practical problems and one philosophical one.

  • It emits plain ASCII, not LaTeX: "Find the second derivative of -191*q**4 - 6*q**3 + 5*q**2 + 13263054*q - 2." It has the architecture and not the surface form, and the surface form is the whole point here.
  • It calls ndarray.itemset, removed in NumPy 2.0. Running it means a separate venv pinned to numpy<2.
  • And the philosophical one, below.

3.4. 4. Naive f-string templates

Fast — 35M tokens/second — and wrong. My first prototype emitted \sqrt{12} unsimplified and computed a distance as a float. A template asserts its answer; nothing checks it. A corpus meant to teach mathematics cannot contain wrong mathematics.

3.5. 5. Hypothesis as the generator

Rejected, twice over, both measured:

  • 3,838 examples/second against a plain seeded RNG's 2,812,872 — 733× too slow.
  • Its distribution is adversarial by design. Drawing integers(1,30), the most frequent draws came back 1, 2, 30, 26, 3 — boundaries first, near-perfectly flat. Exactly right for finding bugs; exactly wrong for training data, where over-representing 1 and 30 teaches the model those numbers are special.

3.6. 6. SymPy construction + seeded RNG sampling — chosen

plain seeded RNG 2,812,872 draws/s
SymPy Rational + latex 67,606 problems/s
SymPy sqrt simplify 47,790 problems/s
SymPy solve() 1,739 problems/s

Fast enough that correctness is free, and the answer is derived rather than asserted:

naive template:  \sqrt{12}       SymPy:  2 \sqrt{3}
naive template:  \sqrt{50}       SymPy:  5 \sqrt{2}
                                 SymPy:  Solve $18x + 22 = 20$ -> $x = -\frac{1}{9}$

3.7. Where Hypothesis does belong

Not generating the corpus — auditing the generator. Run as an emitter it produced 3,000 verified examples in 1.7 s with zero property violations, and immediately surfaced this:

Solve $x = 1$ for $x$. The answer is $\boxed{1}$.
Evaluate $1^{2} - 1^{2}$. The answer is $\boxed{0}$.

Both technically valid, both worthless, and both of which a uniform sampler emits at a rate low enough to escape notice until they are sitting in the corpus. The emitted log is a review artifact; you read it, add the missing constraint, regenerate.

The property worth the most is the cumulative one: no unit may emit a token id outside its declared set. That is the machine-checkable form of the constraint that makes a scope-and-sequence a sequence rather than a list, and a plain unit test cannot falsify it.

One measured caveat: hypothesis.target() aimed at token coverage worked mechanically but reached only 5 of 99 missing ids, because those tokens are not in the strategies at all. Coverage is grammar-bound. No amount of search fixes a grammar that cannot express the token.

4. Why re-derive a solved problem

Saxton's architecture is better than what I will build first. I am building it anyway.

The purpose of this project is to understand a language model by constructing every part of it, and a dataset generator adopted wholesale is a part not understood. I would rather have a bad generator whose every failure I can explain than a good one I can only configure. The failures are the deliverable; this is the same reason the repository has six independent reimplementations of the same arithmetic rather than one good one.

The concrete plan is therefore: build it naively, measure how it fails, and then read Saxton to find out which of his decisions were forced. A design borrowed before the problem is felt is a design you cannot defend.

It is worth recording that the specific combination does not appear to exist. A literature sweep found the pieces separately — Saxton's module/entropy/split architecture, Lample & Charton's SymPy generation (arXiv:1912.01412), Lee et al.'s 10.6M-parameter scale, iGSM's operation-graded splits — but no work training a sub-20M model from scratch on a SymPy-generated, curriculum-structured LaTeX corpus with per-skill held-out evaluation. That gap looks real.

5. The issues

These are the things I expect to go wrong, recorded now so the record means something later.

5.1. The tokenizer may be fatal, and the literature says so

Nobody has demonstrated reliable small-model arithmetic on top of GPT-2 BPE. Lee et al. (arXiv:2307.03381) get ~100% on 3-digit addition from a 10.6M-parameter NanoGPT — but with a character-level tokenizer, vocabulary 80. Their GPT-2 experiments only work after inserting spaces between digits to force uniform tokenization. Nogueira et al. (arXiv:2102.13019) find models fail at 5-digit addition with subword tokenization and succeed to 60 digits with explicit position tokens. Wallace et al. (arXiv:1909.07940) find character-level embeddings the most numerate and subword the least.

A 4.8M-parameter transformer has no capacity to spend learning that ['12','34'] and ['5','678'] are the same kind of object. Mitigation is to space-separate digits in generated text; the cost is that the corpus then looks unlike MATH-500, which is what we wanted to evaluate against. This tension is unresolved and is the biggest open risk in the design.

5.2. Format dominates parameters, so my instincts about size were wrong

The same paper: plain A+B=C formatting plateaus at ~85% no matter how much data, while a detailed scratchpad reaches ~100% at 1,000 samples. Reverse-order answers phase-transition at ~2,500.

Two consequences. First, 17.7M parameters is above, not below, the demonstrated floor for arithmetic — the capability gate is data format, not model size, which reframes this whole project. Second, the generator should emit derivation steps, not just answers. SymPy gives intermediate expressions for free and not emitting them wastes its main advantage over scraped text.

5.3. I had the volume wrong by roughly 4×

My first plan was 20M synthetic tokens, reasoned from "30% of the existing corpus". But if the phase transition is 1,000–2,500 examples per skill, then ~35 skills is under 90,000 examples — about 5M tokens. Saxton's 2M per module was sized for 100M-parameter seq2seq models across 56 mixed modules.

The budget belongs in skill coverage and format, not per-skill volume. And because the accuracy curves are step functions, small eval sets will be very noisy near the transition.

5.4. Uniform sampling is a bug

Under uniform sampling of two operands in 0–999, the carry distribution comes out skewed and only 1.0% of draws are single-digit:

carries 0 1 2 3
share 16.6% 35.8% 34.0% 13.7%

Lee et al. instead force balanced coverage across digit counts and carry counts, and it improves accuracy in every slice. Charton (arXiv:2308.15594) shows the same lesson with a 2.4× effect: on learning GCDs, uniform operands yield 38 correct values ≤100, log-uniform operands 73, and log-uniform outcomes 91 — same model, same task, distribution alone.

The generalization is to balance over the answer distribution, not just the input distribution. This is cheap and I would not have thought of it.

5.5. Register drift

Fine-tuning on modern mathematical prose will pull the model's voice toward modern prose generally. The mitigation is to emit problems in two registers — modern LaTeX for benchmark compatibility, and period English modelled on Recorde's Grounde of Artes — plus a replay fraction of the original corpus.

Held-out validation loss on the original corpus, measured before and after, is the detector. If it rises materially the model is paying for mathematics with its voice.

5.6. The curriculum may not pay

Recent work (arXiv:2510.19099) finds no curriculum ordering that dominates universally across five difficulty definitions and three models. Skill-It (arXiv:2307.14430) reports large gains, but from online resampling driven by measured per-skill validation loss, not a fixed easy-to-hard sweep. Saxton's own headline numbers are the uniform mix.

So the curriculum is an ablation, not an assumption, and it ships with a uniform-mix control run. Without the control nothing is attributable.

6. What gets built

diagram-synth-pipeline.png

The split deserves a note. The obvious holdout — training data from seed A, evaluation from seed B — fails silently, because nothing stops seed B from drawing a pair seed A already drew, and over 0–99 there are only 10,000 addition pairs. Making the split a property of the value (md5(value) % 2) means a problem lands on the same side forever, regardless of generation order or machine, and regenerating the corpus cannot leak the eval set. This is Saxton's idea and I am taking it, because I felt the problem first.

7. Open questions

  1. Space-separated digits or not. Uniform tokenization is what makes small-model arithmetic work, and it makes the corpus look unlike the benchmark. There may be no way to have both without a custom vocabulary.
  2. Custom vocabulary. AlphaGeometry trains a 151M model on a 757-token vocabulary; Lee et al. use 80 characters. For 17.7M parameters with 73% in the embedding table, a small domain vocabulary is plausibly the highest-leverage change available — and it breaks compatibility with everything else in the repository.
  3. Does notation transfer without arithmetic? My prediction is yes: the model learns to render LaTeX and cannot compute. That is falsifiable in both directions and is the result I most expect to be interesting.

Measurements taken 2026-08-02 against run 20260801-205118 and MATH-500 (sha256 35dc4108…a06132). Literature verified by fetching sources; where a figure came from a rendered full text rather than an abstract, that is noted in the project's own notes rather than here.