tiny-math-1m / STATUS.md
Atharva1232's picture
tokenizer ablation: digit-split pre-tok = 0.2% -> 99.7% on TinyStories-1M; smallest arithmetic LLM now 3.75M
f117e6b verified
|
Raw History Blame Contribute Delete
14.5 kB

tiny-math β€” overnight run status log

Goal

Find the smallest transformer that can do 4-digit addition. Sizes {1M, 5M, 8M}, from-scratch GPT, digit-level vocab (16 tokens), SFT on generated data, GRPO polish only if SFT plateaus. Headline number + phase diagram + honest model card.

Key facts

  • Kaggle quota: 30h/week (checked at start: 0h used). Kernels run serially.
  • Baselines (local, n=5000, 4-digit l2r): bigram = 0.0%, untrained 1M = 0.0%. Floor is zero β€” any accuracy is real learning.
  • Local CPU speed: ~3100 tok/s (1M model). Kaggle T4 expected ~100-200x faster.
  • Data: train/eval contamination prevented BY CONSTRUCTION β€” both eval sets built upfront, stream rejects any (a,b) in either. (Bug found + fixed after local run crashed on the overlap assert; bakeoff v1 was deleted pre-run.)

Formats (Phase 0b bakeoff picks winner)

l2r = "47+68=\n115\n" | rtl = "47+68=\n511\n" (answer reversed) | cot = "47+68=\n7+8=15,4+6+1=11,115\n" (digit steps + answer, comma-sep) Paper (arXiv 2307.03381) predicts rtl and cot >> l2r for addition.

Decision rules (pre-committed, no drift overnight)

  1. Format winner = highest final acc at 1M/4-digit/10M tokens. Tie β†’ prefer cot (most interesting), then rtl.
  2. GRPO phase triggers ONLY if best SFT (8m, 50M tokens, 4-digit) < 90%. GRPO reuses the 150-line trainer pattern from grpo-gsm8k (shaped reward with format floor β€” zero-variance groups give zero advantage).
  3. If bakeoff shows ALL formats ~0% at 1M/10M β†’ raise 1M config to d=192 L=6 before sweeping (under-parameterized, don't burn sweep quota on a dead cell).
  4. Extrapolation kernel runs after sweep; it mounts sweep outputs as a dataset.

SWEEP RESULTS (n=5000, cot format, all 11 runs complete)

4-digit addition, exact-match on held-out problems:

model 71k probs 357k probs 1.78M probs
1M 0.0% 0.2% 100.0%
5M 0.0% 95.7% 100.0%
8M 0.0% 98.8% 100.0%
2-digit ceiling: 1M and 8M both 100% at 1.78M probs.
  • Headline: 1M params does 4-digit addition perfectly (0 errors / 5000).
  • Phase transition visible in the 1M row: 0.2% -> 100.0% between 357k and 1.78M probs.
  • Per rule 2: SFT hit 100% -> NO GRPO needed for 4-digit in-distribution.
  • Wall time: whole sweep β‰ˆ 50 min on one T4, ~0.7h quota. Quota check needed before extrapol push.

MISSION UPDATE (user clarification)

Part 1 (from-scratch 1M calculator) = good reference, KEEP AND SHIP IT, but the REAL target: the smallest real LLM (pretrained, knows language) that is good at arithmetic. Plan:

  • Stage A: zero-shot+1-shot quiz of real LLMs {TinyStories-1M/3M/33M, pythia-14m/70m, SmolLM2-135M} on the same 4-digit task β€” kernel READY (kernels/llmquiz/script.py, needs internet=true to pull HF weights).

  • Stage B: SFT ladder β€” fine-tune the promising ones on the same arithmetic lessons, find smallest to clear >=95%.

  • Stage C: verdict number + language-retention probe (does it still speak English after arithmetic training?) + final report. Open question worth measuring: BPE tokenizers split numbers badly, so a 14M LLM might LOSE to our 1M calculator β€” that contrast IS the story.

  • 01:0x extrapol v1 CANCELLED at 59min β€” MY push timeout (-t 3600) was the kill (cancel_acknowledged = Kaggle honoring the timeout). No outputs survived (jsonl written only at end). FIXES: (a) -t 7200 now, (b) incremental extrapolation.jsonl append after EVERY eval row β€” a kill can no longer eat results, (c) trunc_frac metric added β€” 5-6 digit cot needs >64 tokens, and truncation would otherwise masquerade as model failure, (d) make_eval_set attempts-guarded (1-digit space is only 100 pairs; unbounded while-loop would hang forever).

  • 01:3x extrapol v2 PUSHED + RUNNING, watcher armed (140min window).

STAGE A RESULTS (zero/1-shot, n=2000, 1 worked cot example, complete)

All 6 real LLMs: strict 0.000, lenient 0.000. From 3.7M to 134.5M params. Failure modes by family:

  • TinyStories (1M/3M/33M): ignore the task, TELL STORIES ("One day, a 3 year old boy was walking in the park...")
  • pythia-14m: attempts cot format, loops ("3+9=9,9+4=12,9+4=12,...")
  • pythia-70m: emits plausible cot chains with wrong digits, then repeats the worked example back
  • SmolLM2-135M: closest but loops / collapses to "100+100+100..." Reference: from-scratch 1M (trained) = 1.000. Zero-shot β‰  trained; the LLM advantage (language) buys nothing on pure arithmetic below 135M. Saved: results/stage_a_results.jsonl.

SHIPPED (part 1)

  • HF: Atharva1232/tiny-math-1m (model + card + all results + chart), public
  • GitHub: Its-Atharva-Gupta/tiny-math (public), pushed clean, token never touched git config (one-shot GIT_ASKPASS helper)
  • MODEL_CARD.md includes: 100% headline, phase-cliff finding, extrapolation 0%-everywhere table, bigram/untrained baselines at 0%, contamination-by- construction note, positional-pattern failure analysis with examples.

STAGE B ROUND 1 (500k problems, complete) β€” NULL RESULT + DIAGNOSIS

All LLMs ~0% after fine-tuning (TinyStories-1M: 0.001 strict = 2/2000 luck; others 0.000). But curves reveal THREE distinct bugs, not one failure:

  • pythia-14m/70m: loss NaN at step 1 (fp16 autocast instability) β€” models were dead on arrival, hence empty generations. NOT a real result.
  • TinyStories-1M/3M: loss fell properly (2.83->1.65, 2.30->1.05) β€” they learned the text but eval 0%: (a) BPE splits numbers inconsistently, no stable digit mapping; (b) my eval prompt had a worked example models never saw in training (format mismatch, my bug).
  • 33M/135M: OOM at batch 64 (50k-vocab logits vs 16GB) β€” never trained.
  • Also found: round-1 final eval was same-seed superset of tuning eval -> biased. Fixed in round 2 (independent seeds 7781/7782 + overlap assert).
  • Side finding: fine-tuning DESTROYS TinyStories language ("Once upon a time" -> digit chains). Language-retention probe kept for round 2.

STAGE B ROUND 2 (RUNNING, watcher proc_9c5edaeb2e6f, -t 18000)

Pre-registered configs:

  • TinyStories-1M: 1.78M problems (escalation: smallest LLM candidate gets same budget our from-scratch 1M model got)
  • pythia-14m/70m: 500k, pure fp32 (NaN fix)
  • TinyStories-33M: 500k, batch 16 (OOM fix)
  • SmolLM2-135M: 500k, batch 8 (OOM fix)
  • All: eval prompt = "a+b=\n" matching training format exactly. NaN guard aborts a model at first non-finite loss.

STAGE B ROUND 2 RESULTS (complete, 142min) β€” BREAKTHROUGH

LLM params problems strict
TinyStories-33M 68.5M 500k 98.5% PASSES
SmolLM2-135M 134.5M 500k 76.9%
TinyStories-1M 3.7M 1.78M 0.2% (loss 1.27->0.74, eval flat 0: BPE wall confirmed at the budget our from-scratch 1M hit 100%)
pythia-14m 14.1M 500k NaN @ step 449 even in fp32 β€” GPT-NeoX instability, not fp16
pythia-70m 70.4M 500k NaN @ step 899 even in fp32
  • TS-33M wrong answers: gets the sum RIGHT then hallucinates more problems ("...15989\n8984+8984=...") β€” strict parses first line => 98.5%. lenient parses last number => 0. Disclosed, not hidden.
  • Language: TS-33M lost English ("Once upon a time" -> digit chains); SmolLM2-135M KEPT it ("...a little girl named Lily..."). Capacity helps retention. TS-1M also lost English.
  • Headline so far: smallest passing real LLM = 68.5M (vs 1.07M from scratch).

STAGE B ROUND 3 (RUNNING, watcher proc pending, -t 10800)

Two candidates that never got a fair shot:

  • TinyStories-3M (8.3M): round-1 0% was under broken eval prompt; now round-1-surviving recipe + fixed eval, 500k problems.
  • pythia-14m (14.1M): anti-NaN recipe v3 β€” LR 2e-5, embedding LR x0.1 (embedding = 46% of its params, spike suspect), no wd on norms/biases, 10% warmup, NaN auto-retry ladder [5e-6].

ROUND 3 RESULTS (partial β€” kernel crashed after TS-3M)

  • TinyStories-3M (8.3M) @ 500k: strict 0.000 β€” half-learned carry pattern but BPE garbage ("cartridge1", "8irs") swallows digits. Below the floor.
  • pythia-14m: NaN'd at 2e-5 AND at the 5e-6 retry, then MY retry handler crashed (off-by-one: NAN_RETRIES[attempt] with attempt==len). Handler bug fixed by design in round 4 (no retry ladder there).
  • Verdict math: candidates below 68.5M are now 3.7M (0.2%), 8.3M (0%), 14.1M (untrainable so far). pythia-14m is the LAST one that could beat the 68.5M headline -> round 4.

ROUND 4 (RUNNING, watcher armed, -t 7200)

Recipe v5 for pythia-14m: freeze embed_in+embed_out (91% of its params, the NaN suspect β€” 50277-vocab matrices under full-param AdamW), train only the ~1.2M transformer blocks at lr 1e-4, grad-norm logged pre-clipping. If this NaNs too -> documented stability blocker, verdict stands at 68.5M.

ROUND 4 + LOCAL NaN AUTOPSY (diagnosis in progress)

  • Round 4 (frozen "embed*" only): NaN at STEP 2, grad_norm=nan. Log revealed my filter froze only embed_in (6.44M) β€” lm_head (also 6.44M) kept the name "lm_head", not caught by "embed" substring.

  • Local autopsy (nan_autopsy.py, pythia-14m on CPU, exact Kaggle recipe): forward clean (loss 2.42, max|logit| 23), embedding rows used by cot data fine (max|w| 0.135), step-1 gradients ALL FINITE (no NaN/inf, total norm 60.09, top grad 28) -> then ONE AdamW step turns 46/76 tensors NaN. Finite grads + finite clip + one optimizer step = NaN weights means an optimizer-internals bug (suspect: foreach implementation), not data/LR.

  • LR-vs-time pattern across 5 recipes: 1e-4β†’step2, 6e-5 fp16β†’step1, 6e-5 fp32β†’step449, 2e-5β†’later, 5e-6β†’later. Consistent with slow weight drift into an overflow threshold, OR two separate mechanisms.

  • nan_bisect.py running locally: 4 one-step variants (foreach default / foreach=False / wd=0 / SGD) to name the culprit. If foreach=False is clean -> that's the Kaggle round-5 fix (one flag).

  • nan_bisect RESULT β€” CASE CLOSED: param dtype = float16! transformers 5.19 loads pythia-14m in its native fp16; Kaggle's transformers 4.x loaded fp32. fp16 + AdamW explodes because eps=1e-8 is UNREPRESENTABLE in fp16 (min normal 6e-5): underflowed second-moment rows divide by ~0 -> inf -> NaN weights in ONE step. All 4 bisect variants: AdamW (any wd/foreach) = 51 NaN tensors; SGD = clean (no division) β€” confirms mechanism exactly. This explains every instant NaN (rounds 1/3-retry/4 locally). The SLOW Kaggle NaNs (step 449 @ 6e-5 fp32) are a separate real instability β€” round 5 recipe addresses both: explicit fp32 + blocks-only + lr 3e-4.

  • Skill patched: rlvr-finetuning now carries the fp16/AdamW eps lesson and the clip-can't-save-NaN-grads lesson.

  • round5_preflight.py running locally: 10-step fp32 blocks-only test; if CLEAN -> Kaggle round 5 with same recipe at 500k problems.

ROUND 5 RESULT (complete, 10min) β€” NaN SAGA ENDS, REAL LEARNING

pythia-14m blocks-only fp32: strict 0.642 best / 0.6255 final @ 500k problems. Stable the whole run (grad_norm_max 56.7, zero NaN). Same hallucinate-more-problems failure as TS-33M (gets 8857+7132 right -> 15989 then invents more problems; lenient=0 because last number is hallucination).

  • Sub-68.5M ladder now: 3.7M=0.2%, 8.3M=0%, 14.1M=64.2% (blocks-only, 1.19M trainable), 68.5M=98.5% PASSES, 134.5M=76.9%.
  • Per pre-committed escalation rule (10%<=acc<95% -> 1.78M problems): ROUND 6 launched β€” pythia-14m @ 1.78M, same proven recipe. If >=95%: headline drops 68.5M -> 14.1M. Else: verdict closes at 68.5M.
  • SmolLM2-135M also in the escalation band (76.9%) but ABOVE the 68.5M headline β€” escalating it cannot change the "smallest" answer, skipped (quota discipline, reason recorded).

TOKENIZER ABLATION (user request, RUNNING β€” kernel tiny-math-tokablation)

One-variable experiment: TinyStories-1M, native BPE scored strict 0.002 @ 1.78M problems (control r2_ts1m_1780k). Treatment = SMALLEST tokenizer change: number runs split PER DIGIT (pre-tok ?\\p{N}+ -> ?\\d), same 50,257 vocab, SAME embedding matrix, no new tokens, no resize. Everything else frozen: same checkpoint, same 1.78M problems, same fp16/lr 6e-5/ batch-32 recipe, same eval sets (tune 7781 / final 7782).

  • Local validation done: OLD chops 1842 -> ['18','42'], 9920 -> ['99','20'] (inconsistent boundaries β€” the mechanism we suspected); NEW gives one token per digit; 500-string round-trip check passed; alphabet closed, no spaces (chunk-interaction assumption holds).
  • Compute: digit-split = 1.23x tokens/problem (36.9 -> 45.4), disclosed as a consequence of the treatment (same problems, ~1.23x steps).
  • PRE-REGISTERED PREDICTIONS: BPE-chunking was THE wall -> big jump (>>10%, plausibly 90%+; from-scratch 806K hit 100% on same data); tokenization was not it -> <5% (wall = pretraining/capacity).
  • Implementation: per-char id dict == patched-pretokenizer for this corpus (single-char chunk = BPE identity), asserted in-kernel at startup.

Timeline

  • 23:0x Phase 0 done: train.py + baselines.py + unit tests all pass. Local 2-digit 2M-token run proved learning (loss 2.08β†’1.34, acc 0β†’~2% on l2r; crashed only at final-eval overlap assert β€” the bug since fixed).
  • 23:1x bakeoff v1+v2 FAILED: Kaggle runs kernels as notebooks; sibling import from train import train not resolvable (ModuleNotFoundError at ~10s). sys.path patch also failed. FIX: single-file merged kernels via make_kernel.py (train.py core + driver concatenated, syntax-gated). No more sibling imports.
  • 23:5x bakeoff v3 COMPLETE (single-file, ~3 min, 6 configs): 2-digit: cot 16.6% > l2r 15.0% > rtl 8.7% | 4-digit: cot 0.5%, others 0. FORMAT = cot (pre-committed rule 1). 1M@10M tok is below phase transition for 4-digit β€” model learns procedure format (see wrong-examples: valid digit step chains with wrong sums) but not yet the arithmetic.
  • 00:0x sweep PUSHED + RUNNING (11 configs: 3 sizes Γ— {2M,10M,50M} @ 4d cot
    • 2 ceiling 2d @ 50M). Watcher proc_069900fd4eb2 armed.
  • Also fixed: extrapol CHECKPOINTS tags now match sweep tag scheme (sw_1m_4d_50000k etc, format not in tag); extrapol metadata kernel_sources (was dataset_sources β€” would have failed at push).
  • Next: sweep results β†’ extrapol β†’ decide GRPO (rule 2: only if best SFT <90%).

Kernel IDs

  • tiny-math-bakeoff (DONE v3): format winner = cot
  • tiny-math-sweep (RUNNING): {1m,5m,8m} Γ— {2M,10M,50M} tok @ 4d + 2d ceiling
  • tiny-math-extrapol (pending, rebuilt single-file): ckpt eval at 1..6 digits