<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://rishipadhye.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rishipadhye.github.io/" rel="alternate" type="text/html" /><updated>2026-07-21T20:03:19+00:00</updated><id>https://rishipadhye.github.io/feed.xml</id><title type="html">Rishi Padhye</title><subtitle>Notes and write-ups on machine learning — building transformers and training language models from the ground up, and the experiments along the way.</subtitle><author><name>Rishi Padhye</name><email>rishipadhye@gmail.com</email></author><entry><title type="html">What a 30M-Parameter Language Model Taught Me About Training LLMs</title><link href="https://rishipadhye.github.io/tinystories-llm/" rel="alternate" type="text/html" title="What a 30M-Parameter Language Model Taught Me About Training LLMs" /><published>2026-07-21T00:00:00+00:00</published><updated>2026-07-21T00:00:00+00:00</updated><id>https://rishipadhye.github.io/tinystories-llm</id><content type="html" xml:base="https://rishipadhye.github.io/tinystories-llm/"><![CDATA[<p><em>I built a decoder-only transformer from scratch and trained it on TinyStories
using a single free GPU. The interesting part wasn’t the model — it was
discovering that most of the training machinery people obsess over does nothing
at this scale, that an apparent “scaling floor” was a mirage, and that a
1.8M-parameter model quietly rediscovers the same circuits found in GPT.</em></p>

<hr />

<h2 id="tldr">TL;DR</h2>

<p>I trained a family of small language models (1.8M–56.6M non-embedding params) from
scratch on <a href="https://arxiv.org/abs/2305.07759">TinyStories</a>, on Kaggle’s free T4
GPU, with no high-level modelling libraries. Running controlled experiments, I
found:</p>

<ul>
  <li><strong>Most “standard” training tricks are no-ops at this scale.</strong> Warmup length and
LayerNorm-vs-RMSNorm made no measurable difference to final loss. The one lever
that mattered was <strong>peak learning rate</strong> — the textbook default was simply too
conservative.</li>
  <li><strong>A clean scaling law that turned out to be a trap.</strong> Loss followed a tidy
power law with an apparent floor at ~1.16 — but doubling the compute budget on
the largest model punched straight through it. The floor was
<em>compute</em>-limited, not a data ceiling. <strong>Lesson: don’t read an irreducible
floor off an undertrained sweep.</strong></li>
  <li><strong>A tiny model rediscovers big-model circuits.</strong> Visualizing attention, the
1.8M model had crisp <em>previous-token heads</em> and <em>attention sinks</em> — the same
interpretable structures documented in GPT-scale models.</li>
  <li><strong>Specialization beats scale on-distribution.</strong> Judged by an LLM on story
quality, my 56.6M model beat GPT-2 (124M) at the children’s-story task, because
GPT-2 — though a stronger language model — can’t stay in the genre.</li>
</ul>

<p>Full code and lab notebook: <strong><a href="https://github.com/rishipadhye/my-LLM">github.com/rishipadhye/my-LLM</a></strong>.</p>

<hr />

<h2 id="why-build-this">Why build this</h2>

<p>I wanted to actually <em>understand</em> transformers, not import one. So everything —
the BPE tokenizer, multi-head attention, the training loop with AMP and cosine
scheduling, checkpointing — is hand-written. The goal was never a state-of-the-art
model; it was to run <strong>controlled experiments</strong> on a system small enough to
iterate on in an hour, and to see how much of the received wisdom from
GPT-scale training actually transfers to the tiny regime.</p>

<p>The base model: a ~30M-parameter decoder-only transformer (512-wide, 8 layers, 8
heads, 8k-token vocab), trained on TinyStories to <strong>val loss 1.401</strong>. It writes
fluent, coherent little stories. That was the starting point, not the point.</p>

<h2 id="finding-1--most-training-tricks-do-nothing-here">Finding 1 — Most training tricks do nothing here</h2>

<p>I ran three ablations expecting to learn which knobs matter. The answer, three
times running, was “not this one”:</p>

<table>
  <thead>
    <tr>
      <th>Ablation</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LayerNorm vs RMSNorm</td>
      <td>Loss tie (1.601 vs 1.603); LayerNorm ~33% faster (fused kernel)</td>
    </tr>
    <tr>
      <td>Warmup length (50 / 500 / 2000)</td>
      <td>Null (1.590 / 1.599 / 1.601) — within noise</td>
    </tr>
    <tr>
      <td>Warmup under stress (10× LR, no clipping)</td>
      <td><em>Still</em> null; neither run diverged</td>
    </tr>
  </tbody>
</table>

<p>The warmup result surprised me most. I removed gradient clipping and cranked the
LR 10× specifically to <em>make</em> warmup matter — and it still didn’t. The reason is
the optimizer: <strong>AdamW already normalizes each parameter’s update by a running
estimate of its own gradient magnitude.</strong> That adaptive step-sizing does the very
job warmup and clipping were invented for, so at this scale they’re redundant. A
“null result,” but a mechanistic one — and a reminder that a lot of training
folklore is really <em>“things that matter for plain SGD at billion-parameter
scale.”</em></p>

<h2 id="finding-2--learning-rate-was-the-only-real-lever">Finding 2 — Learning rate was the only real lever</h2>

<p>After three ties, the LR sweep was the first experiment to actually show a difference:</p>

<table>
  <thead>
    <tr>
      <th>Peak LR</th>
      <th>val loss</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3e-4 (the default I started with)</td>
      <td>1.601</td>
    </tr>
    <tr>
      <td><strong>1e-3</strong></td>
      <td><strong>1.559</strong></td>
    </tr>
    <tr>
      <td>3e-3</td>
      <td>1.557</td>
    </tr>
  </tbody>
</table>

<p>Tripling the LR cut loss by 0.042 — ~20× the run-to-run noise floor — then
plateaued. My original 3e-4 baseline was just too timid. Promoting 1e-3 to the
full 80k-step run carried the gain all the way through (1.43 → <strong>1.401</strong>). The
takeaway I’ll keep: <strong>tune the learning rate before touching architecture.</strong></p>

<h2 id="finding-3--a-scaling-law-and-the-floor-that-wasnt">Finding 3 — A scaling law, and the floor that wasn’t</h2>

<p>I trained a 5-point ladder (1.8M → 56.6M non-embedding params) at a fixed token
budget and fit loss against parameters. It’s a clean, gently bending power law:</p>

<blockquote>
  <p><strong>L ≈ 1.16 + 0.78 · N^−0.276</strong>   (R² = 0.9975)</p>
</blockquote>

<p><img src="/assets/scaling_loglog.png" alt="Scaling curve" /></p>

<p>The bend toward ~1.16 looked exactly like an irreducible loss floor — the point
where more parameters stop helping. It’s tempting to stop there and write
“TinyStories saturates at 1.16.”</p>

<p>So I tested it. I retrained the largest model for <strong>double the budget</strong>. If 1.16
were a real data ceiling, more compute shouldn’t help. Instead loss dropped from
1.415 to <strong>1.325</strong> — straight through the “floor,” and past even the
fully-trained 30M reference. <strong>The floor was an artifact of the training budget,
not the data.</strong> The real lesson generalizes well beyond this project: <em>a scaling
curve fit to undertrained points will show you a floor that isn’t there.</em> That
single control run changed the conclusion — which is exactly why I ran it.</p>

<h2 id="finding-4--a-tiny-model-rediscovers-gpts-circuits">Finding 4 — A tiny model rediscovers GPT’s circuits</h2>

<p>Loss curves say how well a model does, not what it learned. I added a one-line
hook to capture attention weights and plotted them. Even the <strong>smallest</strong> model
(1.8M params, 4 layers) had learned recognizable, interpretable structure:</p>

<p><img src="/assets/attn_xs_l0_h0.png" alt="A previous-token head" /></p>

<p>That bright sub-diagonal is a <strong>previous-token head</strong> — every token attending to
the one before it. Plotting all heads revealed a familiar taxonomy: previous-token
heads, attend-to-self heads, and <strong>attention-sink heads</strong> (nearly every token
dumping attention onto the first token) — the same circuits documented in
GPT-scale interpretability work, reinvented by a model trained only on children’s
stories.</p>

<p>I also probed for <strong>induction heads</strong> (the copy/pattern-completion circuit) by
scoring every head on a repeated prompt. Both model sizes showed only a <em>weak,
above-chance</em> induction bias (top head ~2× chance) — no dedicated induction
circuit at either scale. That’s an honest negative: TinyStories rarely rewards
long-range verbatim copying, so there’s little pressure to build one.</p>

<h2 id="finding-5--specialization-beats-scale-on-distribution">Finding 5 — Specialization beats scale, on-distribution</h2>

<p>Finally: are the stories any <em>good</em>? I had every model continue 10 prompts, and
scored the continuations 1–5 on six dimensions with an LLM judge
(Llama-3.3-70B), instructed to grade <em>fitness for a young child’s story</em>.</p>

<p><img src="/assets/eval_scores.png" alt="Per-dimension judge scores" /></p>

<p>My 56.6M model (overall <strong>4.02</strong>) beat <strong>GPT-2 (124M, overall 1.60)</strong> — a general
model twice its size — at the children’s-story task. GPT-2 collapses on the
<em>story</em> dimensions (coherence, staying on-prompt, plot) because it drifts
off-genre into police stations and inconsistent characters. A model specialized
for a narrow distribution beats a bigger general one <em>at that distribution</em>.</p>

<p><strong>An honest caveat</strong>, because it matters: GPT-2’s low grammar/fluency scores
almost certainly <em>understate</em> its real linguistic quality — my task-framed judge
let the genre-drift bleed into the language scores (a halo effect). The
defensible claim is narrower: GPT-2’s weakness here is <em>task-fit</em>, not language.
Flagging where my own evaluation is soft is part of doing it honestly.</p>

<h2 id="what-id-do-differently">What I’d do differently</h2>

<ul>
  <li><strong>Compute-optimal points.</strong> The scaling ladder is near-converged, not
Chinchilla-optimal — fine for showing the <em>shape</em>, not for a precise exponent.</li>
  <li><strong>A more rigorous eval.</strong> One generation per prompt, ten prompts, a single
judge. Multiple samples, more prompts, and multiple judges would tighten it.</li>
  <li><strong>A fairer prompt for GPT-2.</strong> I fed GPT-2 the same bare story-starters my model
saw, but GPT-2 was never told it was writing for young children — so some of its
genre-drift is a prompting artifact, not a capability gap. A system/instruction
prefix (“Write a simple children’s story:”) would give it a fair shot at staying
on-genre and make the comparison cleaner.</li>
  <li><strong>A dedicated induction probe.</strong> Averaging over many random repeated sequences
would turn the weak induction signal into a firm result either way.</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<p>The thread running through all of it: <strong>at 30M parameters, most of what people
tune is noise, and the things that actually move the needle are unglamorous</strong> —
the learning rate, the training budget, and the match between model and data. The
most valuable habit this project reinforced wasn’t any single result; it was
running the control that could <em>disprove</em> the tidy story — the 80k diagnostic
that killed the “data floor,” the caveat that keeps the eval honest.</p>

<p><em>Built with PyTorch on a single Kaggle T4. Code, configs, and the full lab
notebook (every experiment, including the null results): <a href="https://github.com/rishipadhye/my-LLM">github.com/rishipadhye/my-LLM</a>.</em></p>]]></content><author><name>Rishi Padhye</name><email>rishipadhye@gmail.com</email></author><summary type="html"><![CDATA[I built a decoder-only transformer from scratch and trained it on TinyStories on a single free GPU. The interesting part wasn't the model — it was discovering that most of the training machinery people obsess over does nothing at this scale, that an apparent "scaling floor" was a mirage, and that a 1.8M-parameter model quietly rediscovers the same circuits found in GPT.]]></summary></entry></feed>