TinyTitan · GPT from scratch
decoder-only LM · JAX / Flax NNX

A 51M-parameter GPT,
trained from scratch.

A small GPT built from scratch — embeddings, attention, training loop, mixed precision, all by hand — pretrained on 3.5B tokens, then fine-tuned into a science-Q&A chat model.

51.4M params 8 layers 1024 context 3.5B tokens ~3h · single A100
01 / Architecture

The model, end to end.

A decoder-only transformer — nanoGPT modernized with ideas from Gemma: a token embedding, eight pre-norm blocks, a final norm, and an output projection that reuses the embedding.

input token ids  (B, T)
Token Embedding  50257 × 512
× 8
RMSNorm
Causal Self-Attention ⊕ residual
8 heads · RoPE · QK-norm
RMSNorm
SwiGLU MLP ⊕ residual
512 → 1408 → 512
Final RMSNorm
Output projection → logits  → 50257
02 / Specification
51.4M
parameters
8
layers
512
model dim
8
attn heads
1408
FFN (SwiGLU)
1024
context window
50,257
vocab · GPT-2 BPE
bf16
precision
03 / The layers

What each piece does.

04 / Training

From raw text to a chat model.

Two phases — pretrain, then fine-tune — on the same machinery; only the data and objective change. The shared choices first, then each phase.

Shared foundations

1 · Pretrain

done
  • Corpus
    70% FineWeb-Edu · 20% Cosmopedia · 10% FineMath, streamed and packed into one uint16 stream with no padding.
  • Budget
    3.5B tokens ≈ 13.3k steps at 256k tokens/step (~one epoch). That's ~136 tokens/param — roughly 7× past Chinchilla-optimal, the over-training strategy small models like SmolLM use. ~3h on one A100.
  • Settings
    Context 1024, cosine LR 6e-4 → 6e-5, 2k-step warmup. Objective: next-token prediction over the whole sequence.
Pre-training train and validation loss over ~13,000 steps, falling from ~5.8 to ~3.2.
Train and validation loss over ~13.3k steps. Both fall from ~5.8 to a final val loss ≈ 3.17 and track each other closely — no overfitting, and still inching down at the budget.

2 · SFT

done
  • Corpus
    Science Q&A — SciQ + OpenBookQA + ARC (~22k) — plus a small smol-smoltalk chat subset (~10k), all rendered to one question → answer shape in a chat template.
  • Template
    ChatML markers as plain text; each assistant turn ends with <|endoftext|> — the token the base already knows as a stop — so the vocabulary stays fixed: no new tokens, no checkpoint surgery.
  • Budget
    A few epochs over the small set — a few hundred optimizer steps, not billions of tokens. Minutes on one GPU.
  • Settings
    Initialized from the base weights with a fresh optimizer, cosine LR ~5e-5 (far gentler than pretrain), loss on assistant tokens only — so it learns to answer, not echo the question. The lowest-val checkpoint is kept.
SFT masked-loss curve (placeholder — pretrain plot shown until the SFT plot is added).
Placeholder — SFT loss plot coming. Masked loss (assistant tokens only) over the fine-tuning run: it drops fast, and the best-val checkpoint is taken before it starts to overfit the small dataset.
05 / Evaluation

How we know it learned.

Scored on the SciQ test set: science questions, each with one correct answer and three distractors. The chat model only — the base can't do the task.

Result

47.3%
SciQ test accuracy
25%
random baseline
~1.9×
above chance
1,000
test questions

Length-normalized accuracy on the held-out split (raw-sum agrees at 47.8%, so length isn't driving it) — nearly twice chance for a 51M model from scratch. Free-form generation is rougher: it lands the right topic, not always the exact word.

06 / Inference

How it picks each word.

Generation is autoregressive: feed the context, get a probability for every token, pick one, append, repeat. These knobs shape that pick — and a KV cache makes the repeating fast.

07 / Reflections

What 51M can and can't do.

It clears the bar it was built for — 47% on SciQ vs 25% chance. Using it also makes the capacity ceiling concrete, and it's worth being honest about where that line falls.

08 / References & inspiration

Built on prior work.