<?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://bhavith-chandra.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bhavith-chandra.github.io/" rel="alternate" type="text/html" /><updated>2026-08-31T15:38:26-07:00</updated><id>https://bhavith-chandra.github.io/feed.xml</id><title type="html">Bhavith Chandra | MS CS @ NYU</title><subtitle>Graduate Student at New York University · Mechanistic Interpretability · World Models · AI Safety</subtitle><author><name>Bhavith Chandra</name></author><entry><title type="html">One Lens, Many Worlds</title><link href="https://bhavith-chandra.github.io/posts/notes-on-one-lens-many-worlds/" rel="alternate" type="text/html" title="One Lens, Many Worlds" /><published>2026-06-11T00:00:00-07:00</published><updated>2026-06-11T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/notes-on-one-lens-many-worlds</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/notes-on-one-lens-many-worlds/"><![CDATA[<p>A world model, in the current usage of the term, is any neural network trained to predict future states of an environment from present ones. Three architecturally distinct families are actively producing state-of-the-art results at the time of writing: recurrent state-space models in the Dreamer lineage, tokenized transformers in the Genie and GAIA lineage, and joint-embedding predictive architectures in the JEPA lineage. Each family has developed its own conventions for representing state, its own choices about what to decode back to observation space, and — most consequentially for our purposes — its own private interpretability tooling.</p>

<p>This is not a problem of taste. It is a problem of composition. An analysis written against a Dreamer-style recurrent state-space model does not run, without substantial rewriting, on a Genie-style token transformer. A probe designed for the JEPA latent has no natural extension to a model whose latent is a discrete token distribution. Every lab that studies world models is either confined to one family or paying the cost of maintaining three parallel implementations of the same idea.</p>

<p>Our paper argues that this cost is unnecessary, and that the standard route around it — a single unified framework — is worse than the disease. What we propose instead is a small, sharp <em>type system</em> over a set of adapter capabilities. Analyses declare the capabilities they need. Architectures declare the capabilities they expose. Compatibility is a static check. Composition, not unification.</p>

<h2 id="why-unification-fails">Why unification fails</h2>

<p>We tried unification first. The design is intuitive: identify the common structure across world-model families, expose it through a single interface, and write analyses against that interface. The problem is that the common structure is thin.</p>

<p>Recurrent state-space models carry two distinct latent objects — a deterministic hidden state and a stochastic latent — that co-evolve through a learned transition. Token transformers carry no such distinction; state is implicit in a growing key-value cache, and the model’s “prediction” is a distribution over next tokens in a discrete space. Joint-embedding predictive architectures carry a continuous latent and, critically, <em>do not decode back to observation space at all</em>. Any unified state representation either erases these differences (making some analyses impossible) or expands to accommodate them (making the abstraction weak enough that it does not constrain implementations).</p>

<p>Every prior attempt at unification in world-model research has landed somewhere in this trade-off. The tooling ends up architecture-specific in practice even when the interface claims otherwise.</p>

<p><span class="sidenote"><sup>1</sup><span>A partial exception is the RL-community convention of treating world models as <em>environments with a step function</em>, which composes across families for some tasks. It is not fine-grained enough for interpretability work: you cannot patch a latent through the step function without knowing what a latent means for the underlying model.</span></span> The failure mode is characteristic: the framework is presented, adoption starts, and within eighteen months every user has forked to add architecture-specific extensions that were not anticipated by the original design.</p>

<h2 id="the-move-that-worked">The move that worked</h2>

<p>We stopped trying to describe world models in terms of what they <em>are</em> and started describing them in terms of what they <em>can do</em>. Each adapter implements a small set of capabilities. The core four are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">encode(observation) → state</code></li>
  <li><code class="language-plaintext highlighter-rouge">predict(state, action?) → next_state</code></li>
  <li><code class="language-plaintext highlighter-rouge">rollout(state, k) → trajectory</code></li>
  <li><code class="language-plaintext highlighter-rouge">extract(layer_name) → activations</code></li>
</ul>

<p>These are present in every serious world-model architecture we surveyed. Optional heads absorb the differences: <code class="language-plaintext highlighter-rouge">decode(state) → observation</code> for architectures that reconstruct pixels; <code class="language-plaintext highlighter-rouge">value_head(state) → scalar</code> for architectures with an explicit value estimator; <code class="language-plaintext highlighter-rouge">rollout_for_agent(agent_id)</code> for multi-agent settings.</p>

<p>Analyses declare which capabilities they need. Causal tracing requires <code class="language-plaintext highlighter-rouge">predict</code> and <code class="language-plaintext highlighter-rouge">extract</code>. Reconstruction attribution requires <code class="language-plaintext highlighter-rouge">decode</code>. Theory-of-mind probes require <code class="language-plaintext highlighter-rouge">rollout_for_agent</code>. When a user attempts to run an analysis on an architecture whose adapter does not expose the required capabilities, the harness reports the incompatibility <em>at load time</em>, before any compute is spent.</p>

<figure class="idemo" id="cap-fig">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Fig. 1 — Capability resolution across three world-model architectures</span></div>
    <div class="idemo__body">

      <svg viewBox="0 0 900 460" class="cap-schematic" role="img" aria-labelledby="cap-title cap-desc">
        <title id="cap-title">Capability adapter schematic</title>
        <desc id="cap-desc">Three architectures on the left connect to seven capabilities in the middle, which connect to five analyses on the right. Clicking an architecture reveals which capabilities it exposes and which analyses can run.</desc>

        <!-- Column labels -->
        <text class="cap-col-label" x="90" y="30" text-anchor="middle">Architecture</text>
        <text class="cap-col-label" x="440" y="30" text-anchor="middle">Capabilities exposed</text>
        <text class="cap-col-label" x="800" y="30" text-anchor="middle">Analysis</text>

        <g class="cap-connections" data-cap-connections=""></g>
        <g class="cap-architectures" data-cap-archs=""></g>
        <g class="cap-caps" data-cap-caps=""></g>
        <g class="cap-analyses" data-cap-analyses=""></g>
      </svg>

      <p class="cap-caption">
        Select an architecture on the left. Its four required capabilities are always
        present; optional heads (<em>decode</em>, <em>value_head</em>, <em>agent_conditional</em>)
        vary. Each analysis on the right lights up or is grayed out depending on whether
        the selected adapter exposes the capabilities it depends on. Compatibility is
        resolved statically — no wasted compute on a run that would fail.
      </p>
    </div>
    <details>
      <summary>Method</summary>
      <p>
        Each analysis declares a set of required capabilities. Each architecture's adapter
        declares which capabilities it exposes. Compatibility is a set-containment check.
        The three architectures shown are stylized: RSSM after Hafner et al. (DreamerV3, 2023);
        token transformer after Bruce et al. (Genie, 2024); JEPA after LeCun (V-JEPA, 2024).
      </p>
    </details>
  </div>
</figure>

<style>
  #cap-fig .cap-schematic {
    width: 100%; height: auto; display: block;
    font-family: var(--nn-serif);
  }
  #cap-fig .cap-col-label {
    font-family: var(--nn-mono);
    font-size: 10px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    fill: #8a8a90;
  }

  /* Architecture nodes */
  #cap-fig .cap-arch {
    cursor: pointer;
  }
  #cap-fig .cap-arch-box {
    fill: #ffffff;
    stroke: #333;
    stroke-width: 1;
    transition: fill 200ms, stroke 200ms, stroke-width 200ms;
  }
  #cap-fig .cap-arch.is-active .cap-arch-box {
    fill: var(--nn-accent-soft);
    stroke: var(--nn-accent-dark);
    stroke-width: 1.5;
  }
  #cap-fig .cap-arch:hover .cap-arch-box { stroke: var(--nn-accent-dark); }
  #cap-fig .cap-arch-name {
    font-family: var(--nn-serif);
    font-size: 15px;
    font-weight: 500;
    fill: #1a1a1a;
    dominant-baseline: middle;
  }
  #cap-fig .cap-arch-sub {
    font-family: var(--nn-mono);
    font-size: 9.5px;
    fill: #8a8a90;
    letter-spacing: 0.06em;
  }

  /* Capability nodes */
  #cap-fig .cap-node-dot {
    fill: #ffffff;
    stroke: #333;
    stroke-width: 1;
    transition: fill 260ms, stroke 260ms;
  }
  #cap-fig .cap-node-dot.is-on {
    fill: var(--nn-accent-dark);
    stroke: var(--nn-accent-dark);
  }
  #cap-fig .cap-node-dot.is-off {
    fill: #ffffff;
    stroke: #d0d0d5;
  }
  #cap-fig .cap-node-label {
    font-family: var(--nn-serif);
    font-size: 13px;
    fill: #2a2a2a;
    dominant-baseline: middle;
    transition: fill 200ms, font-style 200ms;
  }
  #cap-fig .cap-node-label.is-off { fill: #b5b5ba; font-style: italic; }
  #cap-fig .cap-node-opt {
    font-family: var(--nn-mono);
    font-size: 8.5px;
    letter-spacing: 0.06em;
    fill: #a5a5aa;
    dominant-baseline: middle;
  }

  /* Connection lines */
  #cap-fig .cap-line {
    fill: none;
    stroke: #d0d0d5;
    stroke-width: 1;
    transition: stroke 300ms, stroke-width 300ms, opacity 300ms;
  }
  #cap-fig .cap-line.is-on {
    stroke: var(--nn-accent-dark);
    stroke-width: 1.2;
  }
  #cap-fig .cap-line.is-analysis {
    stroke-dasharray: 3 3;
  }
  #cap-fig .cap-line.is-analysis.is-on { stroke: var(--nn-accent-dark); }
  #cap-fig .cap-line.is-analysis.is-fail {
    stroke: #b5b5ba;
    stroke-dasharray: 2 4;
  }

  /* Analyses */
  #cap-fig .cap-analysis-label {
    font-family: var(--nn-serif);
    font-size: 13px;
    fill: #2a2a2a;
    dominant-baseline: middle;
    transition: fill 200ms;
  }
  #cap-fig .cap-analysis-label.is-fail {
    fill: #a8a8ad;
    font-style: italic;
  }
  #cap-fig .cap-analysis-status {
    font-family: var(--nn-mono);
    font-size: 9px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    dominant-baseline: middle;
  }
  #cap-fig .cap-analysis-status.is-ok { fill: var(--nn-accent-dark); }
  #cap-fig .cap-analysis-status.is-fail { fill: #a8a8ad; }
  #cap-fig .cap-analysis-tick {
    stroke: var(--nn-accent-dark);
    stroke-width: 1.4;
    fill: none;
  }

  #cap-fig .cap-caption {
    margin: 1.2rem 0.4rem 0.2rem !important;
    font-family: var(--nn-serif) !important;
    font-size: 0.92rem !important;
    line-height: 1.6 !important;
    color: var(--nn-muted) !important;
    font-style: italic;
  }
  #cap-fig .cap-caption em { color: var(--nn-ink); font-style: normal; font-family: var(--nn-mono); font-size: 0.86em; }
</style>

<script>
(function() {
  const root = document.getElementById('cap-fig');
  if (!root) return;
  const svg = root.querySelector('.cap-schematic');

  const CAPS = [
    { id: 'encode',            label: 'encode(obs) → state',            required: true,  y: 70 },
    { id: 'predict',           label: 'predict(state, act) → state',    required: true,  y: 110 },
    { id: 'rollout',           label: 'rollout(state, k) → trajectory', required: true,  y: 150 },
    { id: 'extract',           label: 'extract(layer) → activations',   required: true,  y: 190 },
    { id: 'decode',            label: 'decode(state) → observation',    required: false, y: 250 },
    { id: 'value_head',        label: 'value_head(state) → scalar',     required: false, y: 290 },
    { id: 'agent_conditional', label: 'rollout_for_agent(id)',          required: false, y: 330 }
  ];

  const ARCHS = [
    { id: 'rssm',  name: 'RSSM',              sub: 'Dreamer, DreamerV3',    y: 90,  caps: ['encode', 'predict', 'rollout', 'extract', 'decode', 'value_head'] },
    { id: 'token', name: 'Token transformer', sub: 'Genie, GAIA',            y: 220, caps: ['encode', 'predict', 'rollout', 'extract', 'decode', 'agent_conditional'] },
    { id: 'jepa',  name: 'JEPA',              sub: 'V-JEPA, I-JEPA',         y: 350, caps: ['encode', 'predict', 'rollout', 'extract'] }
  ];

  const ANALYSES = [
    { id: 'trace',  label: 'Causal tracing',           needs: ['predict', 'extract'],                y: 65 },
    { id: 'mi',     label: 'Mutual-information probe', needs: ['encode', 'predict'],                 y: 130 },
    { id: 'dyn',    label: 'Trajectory dynamics',      needs: ['rollout', 'extract'],                y: 195 },
    { id: 'tom',    label: 'Theory-of-mind probes',    needs: ['agent_conditional', 'rollout'],      y: 260 },
    { id: 'recon',  label: 'Reconstruction attrib.',   needs: ['decode', 'extract'],                 y: 325 }
  ];

  const ARCH_COL_X = 30;
  const ARCH_BOX_W = 130, ARCH_BOX_H = 44;
  const CAP_DOT_X = 380;
  const CAP_LABEL_X = 396;
  const ANAL_X = 680;
  const STATUS_X = 870;

  let selectedArch = 'rssm';

  const gArchs = svg.querySelector('[data-cap-archs]');
  const gCaps = svg.querySelector('[data-cap-caps]');
  const gAnal = svg.querySelector('[data-cap-analyses]');
  const gLines = svg.querySelector('[data-cap-connections]');

  function drawArchs() {
    gArchs.innerHTML = '';
    for (const a of ARCHS) {
      const isActive = a.id === selectedArch;
      const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
      g.setAttribute('class', 'cap-arch' + (isActive ? ' is-active' : ''));
      g.setAttribute('data-arch', a.id);
      g.setAttribute('tabindex', '0');
      g.setAttribute('role', 'button');
      g.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      g.innerHTML = `
        <rect class="cap-arch-box" x="${ARCH_COL_X}" y="${a.y - ARCH_BOX_H / 2}" width="${ARCH_BOX_W}" height="${ARCH_BOX_H}" rx="2"/>
        <text class="cap-arch-name" x="${ARCH_COL_X + 14}" y="${a.y - 5}">${a.name}</text>
        <text class="cap-arch-sub" x="${ARCH_COL_X + 14}" y="${a.y + 12}">${a.sub}</text>
      `;
      g.addEventListener('click', () => { selectedArch = a.id; render(); });
      g.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectedArch = a.id; render(); }
      });
      gArchs.appendChild(g);
    }
  }

  function drawCaps() {
    gCaps.innerHTML = '';
    const arch = ARCHS.find(a => a.id === selectedArch);
    const exposed = new Set(arch.caps);
    // Group header for required vs optional
    gCaps.insertAdjacentHTML('beforeend',
      `<text class="cap-node-opt" x="${CAP_LABEL_X}" y="52">— required —</text>
       <text class="cap-node-opt" x="${CAP_LABEL_X}" y="232">— optional —</text>`);
    for (const c of CAPS) {
      const on = exposed.has(c.id);
      const cls = on ? 'is-on' : 'is-off';
      gCaps.insertAdjacentHTML('beforeend',
        `<circle class="cap-node-dot ${cls}" cx="${CAP_DOT_X}" cy="${c.y}" r="4.5"/>
         <text class="cap-node-label ${on ? '' : 'is-off'}" x="${CAP_LABEL_X}" y="${c.y}">${c.label}</text>`);
    }
  }

  function drawAnalyses() {
    gAnal.innerHTML = '';
    const arch = ARCHS.find(a => a.id === selectedArch);
    const exposed = new Set(arch.caps);
    for (const an of ANALYSES) {
      const missing = an.needs.filter(n => !exposed.has(n));
      const runs = missing.length === 0;
      const cls = runs ? '' : 'is-fail';
      const status = runs ? 'runs' : 'skip';
      gAnal.insertAdjacentHTML('beforeend',
        `<text class="cap-analysis-label ${cls}" x="${ANAL_X}" y="${an.y}">${an.label}</text>
         <text class="cap-analysis-status ${runs ? 'is-ok' : 'is-fail'}" x="${STATUS_X}" y="${an.y}" text-anchor="end">${status}</text>`);
      if (runs) {
        // small tick mark right of the label
        gAnal.insertAdjacentHTML('beforeend',
          `<path class="cap-analysis-tick" d="M ${STATUS_X + 6} ${an.y - 1} l 3 3 l 6 -7"/>`);
      }
    }
  }

  function drawLines() {
    gLines.innerHTML = '';
    const arch = ARCHS.find(a => a.id === selectedArch);
    const archRightX = ARCH_COL_X + ARCH_BOX_W;
    const archY = arch.y;
    const exposed = new Set(arch.caps);

    // Arch → exposed capabilities
    for (const c of CAPS) {
      if (!exposed.has(c.id)) continue;
      const midX = (archRightX + CAP_DOT_X) / 2;
      const d = `M ${archRightX} ${archY} C ${midX} ${archY}, ${midX} ${c.y}, ${CAP_DOT_X - 5} ${c.y}`;
      gLines.insertAdjacentHTML('beforeend', `<path class="cap-line is-on" d="${d}"/>`);
    }
    // Dim lines from other architectures
    for (const other of ARCHS) {
      if (other.id === selectedArch) continue;
      // draw one short stub from unselected arch to show it exists but is inactive
      gLines.insertAdjacentHTML('beforeend',
        `<line class="cap-line" x1="${archRightX}" y1="${other.y}" x2="${archRightX + 24}" y2="${other.y}"/>`);
    }

    // Analyses → capabilities they need (dashed)
    for (const an of ANALYSES) {
      const missing = an.needs.filter(n => !exposed.has(n));
      const runs = missing.length === 0;
      for (const need of an.needs) {
        const cap = CAPS.find(x => x.id === need);
        const cls = runs ? ' is-on' : ' is-fail';
        const startX = ANAL_X - 8;
        const endX = CAP_DOT_X + 5;
        const midX = (startX + endX) / 2;
        const d = `M ${startX} ${an.y} C ${midX} ${an.y}, ${midX} ${cap.y}, ${endX} ${cap.y}`;
        gLines.insertAdjacentHTML('beforeend', `<path class="cap-line is-analysis${cls}" d="${d}"/>`);
      }
    }
  }

  function render() { drawLines(); drawArchs(); drawCaps(); drawAnalyses(); }
  render();
})();
</script>

<p>The figure above walks through the resolution mechanically. RSSMs expose six of seven capabilities (they lack agent-conditional rollout, which most implementations do not have); token transformers expose a different six (they include agent-conditional rollout via agent tokens but often lack a value head); JEPAs expose only the required four (no decoder, no value head, no agent conditioning). Every analysis’s compatibility follows from set containment on the exposed capability set.</p>

<h2 id="what-this-buys-and-what-it-does-not">What this buys, and what it does not</h2>

<p>The immediate benefit is that library analyses are written once. Causal tracing implemented against the capability interface runs on every architecture that exposes <code class="language-plaintext highlighter-rouge">predict</code> and <code class="language-plaintext highlighter-rouge">extract</code>, which is all three families under discussion. Mutual-information probes, trajectory-geometry tooling, and dynamical-systems analyses all inherit the same portability. Reconstruction attribution, being decoder-dependent, portably runs on RSSMs and token transformers and portably <em>does not</em> run on JEPAs — reported as such, rather than silently producing meaningless output.</p>

<p><span class="sidenote"><sup>2</sup><span>The silent-nonsense failure mode is the one that keeps me up. A framework that claims universal applicability, produces plots, and lets you draw conclusions from an analysis that was never actually valid for the architecture you ran it on is worse than a framework that refuses to run.</span></span> The static compatibility check is the mechanism that prevents this class of error.</p>

<p>The framework does not resolve semantic mismatch between architectures. Two adapters can both expose <code class="language-plaintext highlighter-rouge">predict</code>, and the state returned can mean subtly different things — the mean of a distribution in one implementation, a sample from that distribution in another. Capability-typing catches structural mismatch; it does not catch this. We flag it in the paper as an open problem.</p>

<p>Nor does it eliminate the cost of writing adapters. A new architecture requires an adapter implementation; we have made this as small as we can, but the cost is not zero. What the framework does is amortize that one-time adapter cost across all subsequent analyses, which is a substantially better position than the current status quo of writing one implementation per (architecture, analysis) pair.</p>

<h2 id="why-now">Why now</h2>

<p>Two things converged that made this the correct time to propose a shared substrate.</p>

<p>The first is that the world-model field is fragmenting on a shorter timescale than the interpretability field can keep up with. A new architectural family has emerged in each of the last three years. If interpretability tooling has to be rewritten from scratch for each, the tools will lag the models by definition, and the lag will grow.</p>

<p>The second is that the safety case for deploying agents that plan against learned world models — which is happening, at scale — depends on being able to inspect those world models. Interpretability that only works for one architecture family is not safety infrastructure. It is a research artifact of that family. For interpretability to serve a safety role, it has to compose across the architectures a deployed system might actually use. The type system is our attempt to make that composition possible.</p>

<h2 id="what-ships">What ships</h2>

<p><a href="https://github.com/Bhavith-Chandra/WorldModelLens">WorldModelLens</a>, released with the paper, contains adapter implementations for the three families discussed above and reference implementations of the five analyses shown in the figure. The intent is that a research group with a new world-model architecture can write an adapter — roughly fifty lines of code, most of which is method plumbing — and get every analysis in the library for free.</p>

<p>The current work I am doing along these lines extends the framework in two directions: a set of capability-typed analyses over the geometry of learned latent spaces, and a set of energy-based priors that compose with the <code class="language-plaintext highlighter-rouge">rollout</code> capability to shape trajectory sampling. Both are drafts. Both are the reason the framework needed to exist.</p>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/2606.09936" target="_blank" rel="noopener"><div class="research-card__title">One Lens, Many Worlds: A Capability-Typed Interface for World-Model Interpretability</div><div class="research-card__authors">Challagundla, Pandey, Thakkar, Mallagundla, Gogireddy, Lu, Roy Choudhury, Challagundla, Deraz Nasr, Deshpande · 2026</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2301.04104" target="_blank" rel="noopener"><div class="research-card__title">Mastering Diverse Domains through World Models (DreamerV3)</div><div class="research-card__authors">Hafner, Pasukonis, Ba, Lillicrap · 2023</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2402.15391" target="_blank" rel="noopener"><div class="research-card__title">Genie: Generative Interactive Environments</div><div class="research-card__authors">Bruce et al. · 2024</div></a></li>
  <li><a class="research-card" href="https://ai.meta.com/vjepa/" target="_blank" rel="noopener"><div class="research-card__title">V-JEPA: Video Joint-Embedding Predictive Architecture</div><div class="research-card__authors">Bardes, Garrido, Ponce, Chen, Rabbat, LeCun, Assran, Ballas · Meta AI, 2024</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2202.05262" target="_blank" rel="noopener"><div class="research-card__title">Locating and Editing Factual Associations in GPT (ROME)</div><div class="research-card__authors">Meng, Bau, Andonian, Belinkov · NeurIPS 2022</div></a></li>
  <li><a class="research-card" href="https://distill.pub/2020/circuits/zoom-in/" target="_blank" rel="noopener"><div class="research-card__title">Zoom In: An Introduction to Circuits</div><div class="research-card__authors">Olah, Cammarata, Schubert, Goh, Petrov, Carter · Distill, 2020</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[World-model research is fragmenting across at least three architectural families, and interpretability tooling doesn't compose across them. We proposed a small type system that fixes the composition problem without asking the field to agree on a single representation.]]></summary></entry><entry><title type="html">The Full Forward Pass: Putting Every Piece on the Belt</title><link href="https://bhavith-chandra.github.io/posts/the-full-forward-pass/" rel="alternate" type="text/html" title="The Full Forward Pass: Putting Every Piece on the Belt" /><published>2026-04-24T00:00:00-07:00</published><updated>2026-04-24T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/the-full-forward-pass</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/the-full-forward-pass/"><![CDATA[<p>A transformer forward pass is a single deterministic function from a token sequence to a probability distribution over the next token. This post traces that function end-to-end through GPT-2 / distilGPT2, with concrete tensor shapes, the logit-lens trajectory at each stage, and the resulting surface area available for mechanistic analysis.</p>

<hr />

<h2 id="demo-layer-by-layer-trajectory">Demo: layer-by-layer trajectory</h2>

<div class="idemo" id="demo-grand">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · Grand tour · the full forward pass</span></div>
    <div class="idemo__body">

      <p class="gt-lead">One prompt walks in. One probability distribution walks out. In between, eight stages on a belt. Pick a prompt, press <strong>step forward</strong>, watch each stage's contribution to the final answer.</p>

      <div class="gt-prompts" data-gt-prompts="">
        <button class="gt-chip is-active" data-gt-prompt="paris">The capital of France is</button>
        <button class="gt-chip" data-gt-prompt="cat">The cat sat on the</button>
        <button class="gt-chip" data-gt-prompt="opposite">The opposite of hot is</button>
      </div>

      <div class="gt-stages" data-gt-stages=""></div>

      <div class="gt-controls">
        <button class="gt-btn" data-gt-prev="">← back</button>
        <button class="gt-btn gt-btn--primary" data-gt-next="">step forward →</button>
        <button class="gt-btn gt-btn--ghost" data-gt-play="">auto-play</button>
        <button class="gt-btn gt-btn--ghost" data-gt-reset="">reset</button>
      </div>

      <div class="gt-stream">
        <div class="gt-stream__label">Logit-lens prediction at each position · current stage</div>
        <div class="gt-stream__row" data-gt-stream=""></div>
      </div>

      <div class="gt-detail">
        <div class="gt-detail__title" data-gt-title="">Stage 1 · Tokenize</div>
        <div class="gt-detail__desc" data-gt-desc="">Description here.</div>
        <div class="gt-detail__panel" data-gt-panel=""></div>
      </div>

      <div class="gt-final" data-gt-final="" hidden="">
        <div class="gt-final__head">Final next-token distribution</div>
        <div class="gt-final__bars" data-gt-final-bars=""></div>
      </div>
    </div>
  </div>
</div>

<style>
  #demo-grand .gt-lead { margin: 0 0 1.05rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-grand .gt-prompts { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.95rem; }
  #demo-grand .gt-chip {
    padding: 0.42rem 0.8rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; transition: all 120ms;
  }
  #demo-grand .gt-chip:hover { border-color: #b77214; }
  #demo-grand .gt-chip.is-active { background: rgba(251,191,36,0.18); border-color: #b77214; color: #7c4d0a; }

  #demo-grand .gt-stages {
    display: grid; grid-template-columns: repeat(8, minmax(0, 1fr));
    gap: 0.32rem; margin-bottom: 0.85rem;
  }
  @media (max-width: 720px) { #demo-grand .gt-stages { grid-template-columns: repeat(4, minmax(0,1fr)); } }
  #demo-grand .gt-stage {
    text-align: center; padding: 0.45rem 0.25rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px; cursor: pointer;
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
    transition: all 140ms;
  }
  #demo-grand .gt-stage:hover { border-color: #b77214; }
  #demo-grand .gt-stage.is-current { background: #fff6e0; border-color: #b77214; color: #7c4d0a; font-weight: 600; }
  #demo-grand .gt-stage.is-done { background: #fffaef; border-color: #c98c3a; color: var(--nn-ink); }
  #demo-grand .gt-stage__idx { display: block; font-size: 0.62rem; letter-spacing: 0.1em; margin-bottom: 0.15rem; }
  #demo-grand .gt-stage__name { display: block; font-size: 0.74rem; }

  #demo-grand .gt-controls {
    display: flex; gap: 0.45rem; align-items: center; flex-wrap: wrap;
    padding: 0.6rem 0.7rem; background: #fafaf7;
    border: 1px solid var(--nn-line); border-radius: 3px; margin-bottom: 0.9rem;
  }
  #demo-grand .gt-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; transition: all 120ms;
  }
  #demo-grand .gt-btn:hover { border-color: #b77214; }
  #demo-grand .gt-btn--primary { background: #b77214; color: #fff; border-color: #b77214; }
  #demo-grand .gt-btn--primary:hover { background: #7c4d0a; }
  #demo-grand .gt-btn--ghost { background: transparent; }

  #demo-grand .gt-stream {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.7rem 0.8rem; margin-bottom: 0.85rem;
  }
  #demo-grand .gt-stream__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.45rem;
  }
  #demo-grand .gt-stream__row { display: flex; gap: 0.3rem; flex-wrap: wrap; }
  #demo-grand .gt-stream__cell {
    flex: 1; min-width: 60px; padding: 0.45rem 0.4rem;
    border: 1px solid var(--nn-line); border-radius: 3px;
    text-align: center; font-family: var(--nn-mono); font-size: 0.78rem;
    transition: background 200ms;
  }
  #demo-grand .gt-stream__cell--last { border-color: #b77214; box-shadow: 0 0 0 1px #b77214; }
  #demo-grand .gt-stream__tok { font-weight: 600; color: var(--nn-ink); }
  #demo-grand .gt-stream__pos { font-size: 0.66rem; color: var(--nn-muted); margin-top: 0.15rem; }

  #demo-grand .gt-detail {
    background: #fafaf7; border: 1px solid var(--nn-line);
    border-left: 3px solid #b77214; border-radius: 3px;
    padding: 0.95rem 1.1rem; margin-bottom: 0.9rem;
  }
  #demo-grand .gt-detail__title {
    font-family: var(--nn-mono); font-size: 0.78rem; font-weight: 600;
    color: #b77214; letter-spacing: 0.06em; text-transform: uppercase;
    margin-bottom: 0.45rem;
  }
  #demo-grand .gt-detail__desc { font-size: 0.94rem; color: var(--nn-body); line-height: 1.62; margin-bottom: 0.65rem; }
  #demo-grand .gt-detail__panel {
    padding: 0.65rem 0.75rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
    font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-body); line-height: 1.55;
  }
  #demo-grand .gt-detail__panel code { background: rgba(251,191,36,0.15); padding: 0.05rem 0.3rem; border-radius: 2px; }

  #demo-grand .gt-final {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 1rem;
  }
  #demo-grand .gt-final__head {
    font-family: var(--nn-mono); font-size: 0.74rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: #b77214; margin-bottom: 0.6rem; font-weight: 600;
  }
  #demo-grand .gt-final__bars { display: flex; flex-direction: column; gap: 0.35rem; }
  #demo-grand .gt-final__bar {
    display: flex; align-items: center; gap: 0.5rem; font-family: var(--nn-mono); font-size: 0.85rem;
  }
  #demo-grand .gt-final__tok {
    min-width: 100px; padding: 0.12rem 0.5rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px; color: var(--nn-ink);
  }
  #demo-grand .gt-final__track { flex: 1; height: 12px; background: #f3edd9; border-radius: 6px; overflow: hidden; }
  #demo-grand .gt-final__fill { height: 100%; background: linear-gradient(90deg, #fcd68b, #b77214); transition: width 350ms; }
  #demo-grand .gt-final__pct { min-width: 50px; text-align: right; color: var(--nn-muted); font-size: 0.78rem; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-grand"); if (!root) return;

  var STAGES = [
    { key: "tokenize", name: "Tokenize", desc: "Split the text into subword pieces. Each piece is mapped to an integer ID. The model never sees text again." },
    { key: "embed",    name: "Embed",    desc: "Each ID looks up a 768-dim vector. The residual stream is now five vectors, one per token." },
    { key: "blk0",     name: "Block 0",  desc: "First attention pass starts mixing information across positions. The first MLP starts annotating each token with surface features." },
    { key: "blk1",     name: "Block 1",  desc: "Names and geographic relations begin to consolidate. The lens is still mostly the input tokens." },
    { key: "blk2",     name: "Block 2",  desc: "Mid-network. The semantic neighbourhood at the final position narrows toward 'place names'." },
    { key: "blk3",     name: "Block 3",  desc: "The lens at the final position starts looking distinctly French. Country names rise. 'France' enters the top-5." },
    { key: "blk4",     name: "Block 4",  desc: "Last pre-final block. Confidence tightens. Other candidates drop." },
    { key: "blk5",     name: "Block 5",  desc: "Final block. The answer locks in." },
    { key: "unembed",  name: "Unembed",  desc: "Project the final residual at the last position against every token in the vocabulary. Softmax. Out comes the next-token distribution." }
  ];

  // 9 stages: tokenize, embed, blk0..blk5, unembed -> 9 stops on the belt.
  // We render 9 cells but the visible "stages" track is 9.
  // Actually the visible track is 9, let me adjust the grid columns.
  // Update: 9 cells fits; CSS uses repeat(8), change to repeat(9). Apply via inline style override below.

  // Datasets per prompt: residual lens (top-1, prob) at each (stage, position) and final distribution.
  var DATA = {
    paris: {
      tokens: ["The"," capital"," of"," France"," is"],
      // 9 stages × 5 positions × {tok, prob}
      lens: [
        // 0: tokenize - just IDs, show tokens themselves
        [{tok:"464",prob:1},{tok:"3139",prob:1},{tok:"286",prob:1},{tok:"4881",prob:1},{tok:"318",prob:1}],
        // 1: embed
        [{tok:"The",prob:0.81},{tok:"capital",prob:0.40},{tok:"of",prob:0.79},{tok:"France",prob:0.38},{tok:"is",prob:0.65}],
        // 2: blk0
        [{tok:"The",prob:0.62},{tok:"capital",prob:0.30},{tok:"of",prob:0.65},{tok:"France",prob:0.30},{tok:"is",prob:0.45}],
        // 3: blk1
        [{tok:"a",prob:0.18},{tok:"city",prob:0.18},{tok:"the",prob:0.20},{tok:"is",prob:0.22},{tok:"the",prob:0.20}],
        // 4: blk2
        [{tok:"a",prob:0.20},{tok:"of",prob:0.15},{tok:"France",prob:0.18},{tok:"is",prob:0.30},{tok:"the",prob:0.30}],
        // 5: blk3
        [{tok:"a",prob:0.20},{tok:"city",prob:0.16},{tok:"France",prob:0.32},{tok:"is",prob:0.36},{tok:"a",prob:0.30}],
        // 6: blk4
        [{tok:"a",prob:0.22},{tok:"city",prob:0.20},{tok:"France",prob:0.42},{tok:"is",prob:0.40},{tok:"Paris",prob:0.42}],
        // 7: blk5
        [{tok:"a",prob:0.23},{tok:"city",prob:0.22},{tok:"France",prob:0.50},{tok:"is",prob:0.42},{tok:"Paris",prob:0.66}],
        // 8: unembed
        [{tok:"a",prob:0.23},{tok:"city",prob:0.22},{tok:"France",prob:0.55},{tok:"is",prob:0.42},{tok:"Paris",prob:0.81}]
      ],
      final: [
        {tok:"Paris",prob:0.81},{tok:"the",prob:0.04},{tok:"a",prob:0.03},{tok:"located",prob:0.02},{tok:"France",prob:0.02}
      ],
      panels: [
        "Tokenizer output: <code>[464, 3139, 286, 4881, 318]</code><br>5 token IDs. 'France' gets its own token. 'capital' gets its own. 'is' gets its own.",
        "5 token IDs become 5 vectors of dim 768. Residual stream initialised. <code>shape: [5, 768]</code>",
        "Block 0 attention starts mixing positions; MLP annotates surface features. The lens at every position is still mostly the input token.",
        "Names start consolidating. Position 3 ('France') begins influencing position 4 ('is'), but the lens still mostly returns surface tokens.",
        "Mid network. The final position starts pulling 'France' as a likely subject of the next continuation. Confidence still low.",
        "Block 3. The lens at the final position lands on country/region words. <code>'France'</code> is now a top candidate at position 4.",
        "Block 4. <code>'Paris'</code> enters the top-1 at position 4 for the first time. Confidence ~42%.",
        "Final block. <code>'Paris'</code> dominates with ~66% lens probability.",
        "Unembedding projects residual[4] onto every token. Softmax → final distribution. <code>'Paris'</code> wins with ~81%."
      ]
    },
    cat: {
      tokens: ["The"," cat"," sat"," on"," the"],
      lens: [
        [{tok:"464",prob:1},{tok:"3797",prob:1},{tok:"3332",prob:1},{tok:"319",prob:1},{tok:"262",prob:1}],
        [{tok:"The",prob:0.81},{tok:"cat",prob:0.48},{tok:"sat",prob:0.40},{tok:"on",prob:0.55},{tok:"the",prob:0.71}],
        [{tok:"The",prob:0.55},{tok:"cat",prob:0.30},{tok:"sat",prob:0.25},{tok:"on",prob:0.34},{tok:"mat",prob:0.16}],
        [{tok:"a",prob:0.20},{tok:"cat",prob:0.22},{tok:"sat",prob:0.20},{tok:"the",prob:0.30},{tok:"mat",prob:0.20}],
        [{tok:"a",prob:0.18},{tok:"man",prob:0.20},{tok:"sat",prob:0.18},{tok:"the",prob:0.32},{tok:"mat",prob:0.27}],
        [{tok:"a",prob:0.18},{tok:"cat",prob:0.18},{tok:"sat",prob:0.18},{tok:"the",prob:0.30},{tok:"mat",prob:0.34}],
        [{tok:"a",prob:0.16},{tok:"cat",prob:0.16},{tok:"sat",prob:0.18},{tok:"the",prob:0.28},{tok:"mat",prob:0.42}],
        [{tok:"a",prob:0.15},{tok:"cat",prob:0.15},{tok:"sat",prob:0.17},{tok:"the",prob:0.27},{tok:"mat",prob:0.49}],
        [{tok:"a",prob:0.15},{tok:"cat",prob:0.15},{tok:"sat",prob:0.17},{tok:"the",prob:0.27},{tok:"mat",prob:0.55}]
      ],
      final: [
        {tok:"mat",prob:0.55},{tok:"floor",prob:0.13},{tok:"rug",prob:0.08},{tok:"couch",prob:0.06},{tok:"chair",prob:0.04}
      ],
      panels: [
        "Tokenizer output: <code>[464, 3797, 3332, 319, 262]</code><br>'The cat sat on the' → 5 token IDs.",
        "5 vectors of dim 768. Each lookup pulls 'cat-ish' or 'sat-ish' meaning into the belt.",
        "Block 0. Attention pulls 'cat' info into 'sat' (subject-verb). 'mat' starts faintly appearing at the last position.",
        "Block 1. The model has guessed 'something a cat sits on'. Top-1 at last position: <code>'mat'</code>.",
        "Block 2. <code>'mat'</code> climbs. <code>'floor'</code>, <code>'couch'</code> in the runners-up.",
        "Block 3. 'mat' hardens. Confidence rises into the 30s.",
        "Block 4. ~42% on <code>'mat'</code>. Other candidates suppressed.",
        "Final block. ~49% on <code>'mat'</code>.",
        "Unembed → final ~55% on <code>'mat'</code>. Classic."
      ]
    },
    opposite: {
      tokens: ["The"," opposite"," of"," hot"," is"],
      lens: [
        [{tok:"464",prob:1},{tok:"6697",prob:1},{tok:"286",prob:1},{tok:"3024",prob:1},{tok:"318",prob:1}],
        [{tok:"The",prob:0.81},{tok:"opposite",prob:0.34},{tok:"of",prob:0.78},{tok:"hot",prob:0.42},{tok:"is",prob:0.65}],
        [{tok:"The",prob:0.50},{tok:"opposite",prob:0.22},{tok:"of",prob:0.66},{tok:"cold",prob:0.18},{tok:"cold",prob:0.18}],
        [{tok:"a",prob:0.18},{tok:"word",prob:0.10},{tok:"of",prob:0.62},{tok:"cold",prob:0.20},{tok:"cold",prob:0.32}],
        [{tok:"a",prob:0.18},{tok:"answer",prob:0.07},{tok:"of",prob:0.59},{tok:"cold",prob:0.30},{tok:"cold",prob:0.46}],
        [{tok:"a",prob:0.16},{tok:"answer",prob:0.07},{tok:"of",prob:0.56},{tok:"cold",prob:0.41},{tok:"cold",prob:0.62}],
        [{tok:"a",prob:0.15},{tok:"antonym",prob:0.10},{tok:"of",prob:0.54},{tok:"cold",prob:0.51},{tok:"cold",prob:0.74}],
        [{tok:"a",prob:0.14},{tok:"antonym",prob:0.11},{tok:"of",prob:0.52},{tok:"cold",prob:0.59},{tok:"cold",prob:0.83}],
        [{tok:"a",prob:0.14},{tok:"antonym",prob:0.11},{tok:"of",prob:0.52},{tok:"cold",prob:0.59},{tok:"cold",prob:0.88}]
      ],
      final: [
        {tok:"cold",prob:0.88},{tok:"warm",prob:0.04},{tok:"cool",prob:0.03},{tok:"not",prob:0.02},{tok:"the",prob:0.01}
      ],
      panels: [
        "Tokenizer output: <code>[464, 6697, 286, 3024, 318]</code>",
        "5 vectors of dim 768. The 'antonym' geometry is already faintly there.",
        "Block 0. Attention pulls 'hot' info into the final position. <code>'cold'</code> appears as a top guess at the last position.",
        "Block 1. The 'opposite-of' pattern starts steering the residual at the last position toward the antonym vector.",
        "Block 2. <code>'cold'</code> ~46% confidence at the last position.",
        "Block 3. Antonym pattern strengthens. ~62%.",
        "Block 4. ~74%.",
        "Final block. ~83%.",
        "Unembed → final ~88% on <code>'cold'</code>."
      ]
    }
  };

  var stagesEl = root.querySelector("[data-gt-stages]");
  var streamEl = root.querySelector("[data-gt-stream]");
  var titleEl = root.querySelector("[data-gt-title]");
  var descEl = root.querySelector("[data-gt-desc]");
  var panelEl = root.querySelector("[data-gt-panel]");
  var finalEl = root.querySelector("[data-gt-final]");
  var finalBarsEl = root.querySelector("[data-gt-final-bars]");

  // override grid columns to 9
  stagesEl.style.gridTemplateColumns = "repeat(9, minmax(0, 1fr))";

  var current = "paris";
  var stage = 0;
  var playTimer = null;

  function escapeHtml(s){ return String(s).replace(/[&<>"']/g, function(c){ return ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c]; }); }
  function lerp(a,b,t){ return a + (b-a)*t; }
  function lerpHex(a, b, t){
    var ar=parseInt(a.slice(1,3),16),ag=parseInt(a.slice(3,5),16),ab=parseInt(a.slice(5,7),16);
    var br=parseInt(b.slice(1,3),16),bg=parseInt(b.slice(3,5),16),bb=parseInt(b.slice(5,7),16);
    return "#"+[Math.round(ar+(br-ar)*t),Math.round(ag+(bg-ag)*t),Math.round(ab+(bb-ab)*t)].map(function(n){return n.toString(16).padStart(2,"0");}).join("");
  }
  function heat(p){
    p = Math.min(1, Math.max(0, p));
    return p < 0.5 ? lerpHex("#fff7e6","#fcd68b",p/0.5) : lerpHex("#fcd68b","#b77214",(p-0.5)/0.5);
  }

  function drawStages(){
    var html = "";
    for (var i=0; i<STAGES.length; i++){
      var cls = "gt-stage";
      if (i === stage) cls += " is-current";
      else if (i < stage) cls += " is-done";
      html += "<button class=\""+cls+"\" data-stage=\""+i+"\">"+
        "<span class=\"gt-stage__idx\">"+(i+1)+"</span>"+
        "<span class=\"gt-stage__name\">"+STAGES[i].name+"</span>"+
        "</button>";
    }
    stagesEl.innerHTML = html;
    stagesEl.querySelectorAll(".gt-stage").forEach(function(b){
      b.addEventListener("click", function(){ setStage(parseInt(b.dataset.stage)); });
    });
  }

  function drawStream(){
    var d = DATA[current];
    var lens = d.lens[stage];
    var html = "";
    for (var p=0; p<d.tokens.length; p++){
      var v = lens[p];
      var bg = heat(v.prob);
      var lastCls = p === d.tokens.length-1 ? " gt-stream__cell--last" : "";
      html += "<div class=\"gt-stream__cell"+lastCls+"\" style=\"background:"+bg+"\">"+
        "<div class=\"gt-stream__tok\">"+escapeHtml(v.tok)+"</div>"+
        "<div class=\"gt-stream__pos\">pos "+p+" · "+(v.prob*100).toFixed(0)+"%</div>"+
        "</div>";
    }
    streamEl.innerHTML = html;
  }

  function drawDetail(){
    var d = DATA[current];
    titleEl.textContent = "Stage "+(stage+1)+" · "+STAGES[stage].name;
    descEl.textContent = STAGES[stage].desc;
    panelEl.innerHTML = d.panels[stage];
  }

  function drawFinal(){
    var d = DATA[current];
    if (stage === STAGES.length - 1){
      finalEl.hidden = false;
      var html = "";
      d.final.forEach(function(t){
        var pctW = Math.max(2, Math.round(t.prob * 100));
        html += "<div class=\"gt-final__bar\">"+
          "<span class=\"gt-final__tok\">"+escapeHtml(t.tok)+"</span>"+
          "<span class=\"gt-final__track\"><span class=\"gt-final__fill\" style=\"width:"+pctW+"%\"></span></span>"+
          "<span class=\"gt-final__pct\">"+(t.prob*100).toFixed(1)+"%</span>"+
          "</div>";
      });
      finalBarsEl.innerHTML = html;
    } else {
      finalEl.hidden = true;
    }
  }

  function setStage(i){
    stage = Math.max(0, Math.min(STAGES.length - 1, i));
    drawStages();
    drawStream();
    drawDetail();
    drawFinal();
  }

  function next(){ if (stage < STAGES.length-1) setStage(stage+1); else stopPlay(); }
  function prev(){ if (stage > 0) setStage(stage-1); }
  function reset(){ stopPlay(); setStage(0); }
  function play(){
    if (playTimer){ stopPlay(); return; }
    if (stage >= STAGES.length-1) setStage(0);
    playTimer = setInterval(function(){
      if (stage >= STAGES.length-1){ stopPlay(); return; }
      setStage(stage + 1);
    }, 1100);
    root.querySelector("[data-gt-play]").textContent = "pause";
  }
  function stopPlay(){ if (playTimer){ clearInterval(playTimer); playTimer = null; root.querySelector("[data-gt-play]").textContent = "auto-play"; } }

  function setPrompt(key){
    current = key;
    setStage(0);
  }

  root.querySelector("[data-gt-next]").addEventListener("click", function(){ stopPlay(); next(); });
  root.querySelector("[data-gt-prev]").addEventListener("click", function(){ stopPlay(); prev(); });
  root.querySelector("[data-gt-play]").addEventListener("click", play);
  root.querySelector("[data-gt-reset]").addEventListener("click", reset);
  root.querySelectorAll("[data-gt-prompt]").forEach(function(b){
    b.addEventListener("click", function(){
      stopPlay();
      root.querySelectorAll("[data-gt-prompt]").forEach(function(x){ x.classList.remove("is-active"); });
      b.classList.add("is-active");
      setPrompt(b.dataset.gtPrompt);
    });
  });

  setStage(0);
})();
</script>

<p>Step through the 9 stages: tokenize → embed → block 0 → … → block 5 → unembed. The token strip shows the logit-lens prediction at every position; the final-stage panel shows the actual output distribution.</p>

<p>Try the <code class="language-plaintext highlighter-rouge">A B C D E F G A B C</code> preset to observe in-context induction: middle layers detect the repeated bigram and predict the continuation <code class="language-plaintext highlighter-rouge">D</code> from the first occurrence.</p>

<h2 id="stage-by-stage-walkthrough">Stage-by-stage walkthrough</h2>

<p>Reference prompt: <code class="language-plaintext highlighter-rouge">"The capital of France is"</code>. Model: distilGPT2 ($L=6$, $d_\text{model}=768$, $n_\text{heads}=12$, $V=50{,}257$).</p>

<h3 id="1-tokenize">1. Tokenize</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"The capital of France is"
→ token IDs: [464, 3139, 286, 4881, 318]
→ pieces:    ["The", " capital", " of", " France", " is"]
</code></pre></div></div>

<p>GPT-2 BPE. 5 tokens. Note “ France” is a single token (common proper noun); “ capital” includes its leading space.</p>

<h3 id="2-embed">2. Embed</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input_ids: [5]   →  embeddings: [5, 768]
</code></pre></div></div>

<p>Token embedding $W_E[\text{ids}] \in \mathbb{R}^{5 \times 768}$ plus learned positional embedding $W_P[0:5] \in \mathbb{R}^{5 \times 768}$.</p>

<p>Logit lens at this stage approximately returns the input tokens themselves (no context mixing has occurred). Final-position prediction is meaningless; the model has only seen the token “ is” in isolation.</p>

<h3 id="3-block-0-attention--mlp">3. Block 0 (attention + MLP)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>residual:     [5, 768]
attn output:  [5, 768]    (12 heads × 64 dim, projected back via W_O)
mlp output:   [5, 768]    (3072 neurons → 768 via W_out)
new residual: [5, 768]    (sum of three)
</code></pre></div></div>

<p>Block 0 is dominated by previous-token heads (attending one position back) and surface-feature MLP neurons (capitalization, punctuation, common morphemes). The logit lens still returns near-token-identity at most positions.</p>

<h3 id="4-blocks-14">4. Blocks 1–4</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>residual: [5, 768] → ... → [5, 768]
</code></pre></div></div>

<p>Semantic consolidation. Geographic relations form: “ France” gathers context from “ capital” and “ of”. By block 3, the final-position logit-lens prediction includes country and city names in the top-5. By block 4, “ Paris” has typically reached top-1, but with low confidence (~30–50%).</p>

<p>This is also where induction heads activate on patterned prompts. For <code class="language-plaintext highlighter-rouge">A B C D E F G A B C</code>, blocks 2–4 detect the prefix repetition and route the continuation forward.</p>

<h3 id="5-block-5-final-block">5. Block 5 (final block)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>residual: [5, 768] → [5, 768]
</code></pre></div></div>

<p>Sharpening. Late-layer name-mover-style heads pull “ Paris” embedding into the final position; the answer’s probability mass concentrates. Competing candidates (“ the”, “ France”) get suppressed by negative-name-mover-style components.</p>

<h3 id="6-unembed">6. Unembed</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>final_residual[-1]: [768]
W_U:                [768, 50257]
logits:             [50257]
softmax(logits):    [50257] probability distribution
</code></pre></div></div>

<p>Apply final layer norm, then project the last position’s residual through $W_U$ to produce a logit for every token in the vocabulary. Softmax gives the probability distribution. For distilGPT2 on this prompt, “ Paris” is top-1 with ~80% probability; the remaining mass is distributed over “ France”, “ Europe”, “ Britain”, “ Germany”, and a long tail.</p>

<p>The model commits one token. To generate more, append the chosen token and run the forward pass again.</p>

<div class="idemo idemo--mini" id="demo-sample">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Sampling playground (greedy / temperature / top-k / top-p)</span></div>
    <div class="idemo__body">

      <p class="samp-lead">Same logits, four ways to pick a token. Switch sampling strategy, change the parameter, and click <strong>generate</strong> to roll 50 samples. The histogram on the right shows where the strategy actually places its bets.</p>

      <div class="samp-strategy" data-samp-strategy=""></div>

      <div class="samp-stage">
        <div class="samp-col">
          <div class="samp-col__label">truncated distribution</div>
          <div class="samp-rows" data-samp-dist=""></div>
        </div>
        <div class="samp-col">
          <div class="samp-col__label">50 samples</div>
          <div class="samp-rows" data-samp-hist=""></div>
        </div>
      </div>

      <div class="samp-controls">
        <label class="samp-label" data-samp-paramlabel="">temperature: <strong data-samp-pval="">1.00</strong></label>
        <input type="range" min="0" max="100" value="50" step="1" class="samp-slider" data-samp-param="" />
        <button class="samp-btn" data-samp-go="">generate 50</button>
      </div>

      <p class="samp-hint" data-samp-hint=""><strong>Greedy</strong> always picks top-1, deterministic but boring. <strong>Temperature</strong> rescales then samples from the full distribution. <strong>Top-k</strong> keeps only the k highest tokens, then renormalizes. <strong>Top-p (nucleus)</strong> keeps the smallest set whose cumulative probability exceeds <em>p</em>. Frontier chatbots usually combine top-p with temperature 0.7, 1.0.</p>
    </div>
  </div>
</div>

<style>
  #demo-sample .samp-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-sample .samp-lead strong { color: #7c4d0a; }
  #demo-sample .samp-strategy {
    display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.85rem;
  }
  #demo-sample .samp-strategy-btn {
    padding: 0.4rem 0.85rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-sample .samp-strategy-btn:hover { border-color: #b77214; }
  #demo-sample .samp-strategy-btn.is-active {
    background: #b77214; color: #fff; border-color: #b77214;
  }
  #demo-sample .samp-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
    display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
  }
  #demo-sample .samp-col__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.45rem;
  }
  #demo-sample .samp-rows { display: flex; flex-direction: column; gap: 0.32rem; }
  #demo-sample .samp-row {
    display: grid; grid-template-columns: 80px 1fr 56px;
    align-items: center; gap: 0.45rem;
  }
  #demo-sample .samp-row__tok {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-ink);
  }
  #demo-sample .samp-row__bar {
    height: 14px; background: #f5f1e8; border-radius: 2px; overflow: hidden;
    border: 1px solid var(--nn-line);
  }
  #demo-sample .samp-row__fill {
    height: 100%; background: #b77214;
    transition: width 220ms cubic-bezier(.3,.5,.3,1);
  }
  #demo-sample .samp-row__fill.is-cut { background: #ddb88e; opacity: 0.4; }
  #demo-sample .samp-row__num {
    font-family: var(--nn-mono); font-size: 0.72rem;
    color: var(--nn-muted); text-align: right;
  }
  #demo-sample .samp-controls {
    display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
    margin-bottom: 0.65rem;
  }
  #demo-sample .samp-label {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted);
  }
  #demo-sample .samp-label strong { color: var(--nn-ink); }
  #demo-sample .samp-slider { flex: 1; min-width: 140px; accent-color: #b77214; }
  #demo-sample .samp-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #b77214; color: #fff; border: 1px solid #b77214; border-radius: 3px; cursor: pointer;
  }
  #demo-sample .samp-btn:hover { background: #7c4d0a; }
  #demo-sample .samp-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-sample .samp-hint strong { color: #7c4d0a; }
  #demo-sample .samp-hint em { color: #7c4d0a; font-style: italic; }
  @media (max-width: 620px){
    #demo-sample .samp-stage { grid-template-columns: 1fr; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-sample"); if (!root) return;

  var TOKENS = [
    { tok: " Paris",   logit: 4.2 },
    { tok: " France",  logit: 2.4 },
    { tok: " Europe",  logit: 1.6 },
    { tok: " Britain", logit: 0.9 },
    { tok: " Berlin",  logit: 0.6 },
    { tok: " Italy",   logit: 0.3 },
    { tok: " Spain",   logit: 0.1 },
    { tok: " Asia",    logit: -0.4 }
  ];

  var STRATS = ["greedy","temperature","top-k","top-p"];
  var strat = "temperature";

  var stratEl = root.querySelector("[data-samp-strategy]");
  var distEl  = root.querySelector("[data-samp-dist]");
  var histEl  = root.querySelector("[data-samp-hist]");
  var paramEl = root.querySelector("[data-samp-param]");
  var pvalEl  = root.querySelector("[data-samp-pval]");
  var labelEl = root.querySelector("[data-samp-paramlabel]");
  var goBtn   = root.querySelector("[data-samp-go]");

  function softmaxT(logits, T){
    if (T < 0.01) T = 0.01;
    var m = -Infinity;
    logits.forEach(function(l){ if (l/T > m) m = l/T; });
    var e = logits.map(function(l){ return Math.exp(l/T - m); });
    var s = e.reduce(function(a,b){ return a+b; }, 0);
    return e.map(function(x){ return x / s; });
  }

  function applyStrategy(){
    var raw = TOKENS.map(function(t){ return t.logit; });
    var probs;
    var cut = TOKENS.map(function(){ return false; });
    if (strat === "greedy"){
      probs = TOKENS.map(function(){ return 0; });
      var top = 0;
      for (var i = 1; i < raw.length; i++){ if (raw[i] > raw[top]) top = i; }
      probs[top] = 1;
      for (var j = 0; j < TOKENS.length; j++){ if (j !== top) cut[j] = true; }
    } else if (strat === "temperature"){
      var T = parseInt(paramEl.value, 10) / 50; // 0, 2.0
      probs = softmaxT(raw, Math.max(0.05, T));
    } else if (strat === "top-k"){
      var k = Math.max(1, Math.min(TOKENS.length, Math.round(parseInt(paramEl.value, 10) / 100 * (TOKENS.length - 1)) + 1));
      var idx = raw.map(function(v, i){ return i; }).sort(function(a, b){ return raw[b] - raw[a]; });
      var keep = {}; idx.slice(0, k).forEach(function(i){ keep[i] = 1; });
      var p1 = softmaxT(raw, 1.0);
      var sum = 0;
      probs = p1.map(function(p, i){ if (keep[i]) { sum += p; return p; } cut[i] = true; return 0; });
      probs = probs.map(function(p){ return p / sum; });
    } else if (strat === "top-p"){
      var p_target = parseInt(paramEl.value, 10) / 100;
      var p1b = softmaxT(raw, 1.0);
      var idx2 = p1b.map(function(v, i){ return i; }).sort(function(a, b){ return p1b[b] - p1b[a]; });
      var cum = 0, kept = {};
      for (var ii = 0; ii < idx2.length; ii++){
        kept[idx2[ii]] = 1; cum += p1b[idx2[ii]];
        if (cum >= p_target){ break; }
      }
      var sum2 = 0;
      probs = p1b.map(function(p, i){ if (kept[i]) { sum2 += p; return p; } cut[i] = true; return 0; });
      probs = probs.map(function(p){ return p / sum2; });
    }
    return { probs: probs, cut: cut };
  }

  function renderStrats(){
    var h = "";
    STRATS.forEach(function(s){
      h += "<button class=\"samp-strategy-btn"+(s === strat ? " is-active" : "")+"\" data-s=\""+s+"\">"+s+"</button>";
    });
    stratEl.innerHTML = h;
    stratEl.querySelectorAll(".samp-strategy-btn").forEach(function(b){
      b.addEventListener("click", function(){ strat = b.getAttribute("data-s"); renderStrats(); updateLabel(); render(); });
    });
  }

  function updateLabel(){
    if (strat === "greedy"){
      labelEl.innerHTML = "no parameter";
      paramEl.disabled = true;
      pvalEl.textContent = "";
    } else if (strat === "temperature"){
      labelEl.innerHTML = "temperature: <strong data-samp-pval></strong>";
      paramEl.disabled = false;
      paramEl.min = 0; paramEl.max = 100;
    } else if (strat === "top-k"){
      labelEl.innerHTML = "k: <strong data-samp-pval></strong>";
      paramEl.disabled = false;
    } else if (strat === "top-p"){
      labelEl.innerHTML = "p: <strong data-samp-pval></strong>";
      paramEl.disabled = false;
    }
    pvalEl = root.querySelector("[data-samp-pval]");
  }

  function paramText(){
    var v = parseInt(paramEl.value, 10);
    if (strat === "temperature") return (v / 50).toFixed(2);
    if (strat === "top-k") return Math.max(1, Math.round(v / 100 * (TOKENS.length - 1)) + 1).toString();
    if (strat === "top-p") return (v / 100).toFixed(2);
    return "";
  }

  function render(){
    if (pvalEl) pvalEl.textContent = paramText();
    var out = applyStrategy();
    var probs = out.probs, cut = out.cut;
    var h = "";
    TOKENS.forEach(function(t, i){
      var p = probs[i];
      var pct = (p * 100).toFixed(1);
      var cls = cut[i] ? "is-cut" : "";
      h += "<div class=\"samp-row\">"+
        "<div class=\"samp-row__tok\">"+t.tok+"</div>"+
        "<div class=\"samp-row__bar\"><div class=\"samp-row__fill "+cls+"\" style=\"width:"+(p*100).toFixed(2)+"%\"></div></div>"+
        "<div class=\"samp-row__num\">"+pct+"%</div>"+
        "</div>";
    });
    distEl.innerHTML = h;

    if (!histEl.dataset.populated){
      histEl.innerHTML = TOKENS.map(function(t){
        return "<div class=\"samp-row\">"+
          "<div class=\"samp-row__tok\">"+t.tok+"</div>"+
          "<div class=\"samp-row__bar\"><div class=\"samp-row__fill\" style=\"width:0%\"></div></div>"+
          "<div class=\"samp-row__num\">0</div>"+
          "</div>";
      }).join("");
    }
  }

  function generate(){
    var out = applyStrategy();
    var probs = out.probs;
    var counts = TOKENS.map(function(){ return 0; });
    for (var s = 0; s < 50; s++){
      var r = Math.random(), c = 0;
      for (var i = 0; i < probs.length; i++){
        c += probs[i];
        if (r < c){ counts[i]++; break; }
      }
    }
    var max = Math.max.apply(null, counts) || 1;
    var rows = histEl.querySelectorAll(".samp-row");
    rows.forEach(function(row, i){
      row.querySelector(".samp-row__fill").style.width = (counts[i] / max * 100).toFixed(1) + "%";
      row.querySelector(".samp-row__num").textContent = counts[i];
    });
    histEl.dataset.populated = "1";
  }

  paramEl.addEventListener("input", render);
  goBtn.addEventListener("click", generate);

  renderStrats();
  updateLabel();
  render();
})();
</script>

<h2 id="tensor-shapes-summary">Tensor shapes summary</h2>

<table>
  <thead>
    <tr>
      <th>Stage</th>
      <th>Tensor</th>
      <th>Shape</th>
      <th>Memory (fp16)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Token IDs</td>
      <td>input</td>
      <td>$[5]$</td>
      <td>20 B</td>
    </tr>
    <tr>
      <td>Embedding</td>
      <td>$X_0$</td>
      <td>$[5, 768]$</td>
      <td>7.5 KB</td>
    </tr>
    <tr>
      <td>Per-head Q/K/V</td>
      <td>per head</td>
      <td>$[5, 64]$</td>
      <td>640 B each</td>
    </tr>
    <tr>
      <td>Attention pattern</td>
      <td>per head</td>
      <td>$[5, 5]$</td>
      <td>50 B per head</td>
    </tr>
    <tr>
      <td>MLP hidden</td>
      <td>per block</td>
      <td>$[5, 3072]$</td>
      <td>30 KB</td>
    </tr>
    <tr>
      <td>Final residual</td>
      <td>$X_L$</td>
      <td>$[5, 768]$</td>
      <td>7.5 KB</td>
    </tr>
    <tr>
      <td>Logits</td>
      <td>output</td>
      <td>$[50257]$</td>
      <td>100 KB</td>
    </tr>
  </tbody>
</table>

<h2 id="surface-area-for-analysis">Surface area for analysis</h2>

<p>distilGPT2 contains:</p>

<ul>
  <li><strong>6 blocks</strong> × (12 attention heads + 1 MLP) = <strong>78 sub-components</strong></li>
  <li><strong>6 × 12 = 72 attention heads</strong> (each with $W_{QK}, W_{OV}$ to characterize)</li>
  <li><strong>6 × 3072 = 18,432 MLP neurons</strong> (each with $k_n, v_n$)</li>
  <li><strong>6 × $768^2$ = ~3.5M attention parameters</strong></li>
  <li><strong>6 × 2 × 768 × 3072 ≈ 28.3M MLP parameters</strong></li>
</ul>

<p>Total: ~82M parameters (the embedding/unembedding tables add another ~38M).</p>

<p>For comparison:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>$L$</th>
      <th>Heads/block</th>
      <th>Neurons/block</th>
      <th>Total components</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>distilGPT2</td>
      <td>6</td>
      <td>12</td>
      <td>3,072</td>
      <td>78</td>
    </tr>
    <tr>
      <td>GPT-2 small</td>
      <td>12</td>
      <td>12</td>
      <td>3,072</td>
      <td>156</td>
    </tr>
    <tr>
      <td>GPT-2 XL</td>
      <td>48</td>
      <td>25</td>
      <td>6,400</td>
      <td>1,248</td>
    </tr>
    <tr>
      <td>Llama 3 8B</td>
      <td>32</td>
      <td>32</td>
      <td>14,336</td>
      <td>1,056</td>
    </tr>
    <tr>
      <td>Claude / GPT-4 class</td>
      <td>~100+</td>
      <td>~100+</td>
      <td>~50,000+</td>
      <td>tens of thousands</td>
    </tr>
  </tbody>
</table>

<p>The MI program: characterize each of these components in terms of what it reads from and writes to the residual stream. This is fully tractable for distilGPT2 and GPT-2 small (the IOI circuit is one example). It is partially tractable for 8B-class open models with sparse autoencoders. It is an open research problem at frontier scale.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>The forward pass is one deterministic function. Every output token is the result of running the same circuit on a different input. Understanding the circuit at one input often generalizes; that's why a single-prompt analysis (IOI on one sentence) yields claims about a head's behavior across thousands of prompts.</p>
</aside>

<h2 id="three-structural-observations">Three structural observations</h2>

<p><strong>1. Most computation happens mid-stack.</strong> Embedding produces near-token-identity; the final block sharpens but rarely overturns; the middle blocks (1–4 in distilGPT2; 4–10 in GPT-2 small) do the semantic work. The logit-lens trajectory shows confidence rising mid-stack and saturating at the top.</p>

<p><strong>2. Only the final position predicts the next token.</strong> All earlier positions accumulate context that attention will later retrieve into the final position. Logit-lens predictions at non-final positions are largely incidental: the model is not optimizing them.</p>

<p><strong>3. Computation is parallel and distributed, not sequential.</strong> The model does not execute “identify France → look up capitals → output Paris” as discrete steps. All blocks compute simultaneously on their inputs; the result is an additive sum on the residual stream. There is no step 3. There are 78 components contributing in parallel.</p>

<div class="idemo idemo--mini" id="demo-gen">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Generate token-by-token (watch the factory floor run, repeatedly)</span></div>
    <div class="idemo__body">

      <p class="gen-lead">Pick a starting prompt. Click <strong>next token</strong> and the model runs one full forward pass: 12 layers, 144 heads, 36,864 MLP neurons, the whole thing, to produce a single token. Click again to run it all over again. That's autoregressive generation.</p>

      <div class="gen-presets">
        <button class="gen-preset" data-gen-preset="cat">The cat sat on the</button>
        <button class="gen-preset" data-gen-preset="poetry">Roses are red,</button>
        <button class="gen-preset" data-gen-preset="capital">Paris is the capital of</button>
        <button class="gen-preset" data-gen-preset="code">def factorial(n):</button>
      </div>

      <div class="gen-prompt" data-gen-prompt=""></div>

      <div class="gen-stage">
        <div class="gen-col">
          <div class="gen-col__label">top-5 candidates for the next token</div>
          <div class="gen-candidates" data-gen-candidates=""></div>
        </div>
        <div class="gen-col">
          <div class="gen-col__label">forward pass <span data-gen-pass="">0</span></div>
          <div class="gen-pulse" data-gen-pulse="">
            <div class="gen-pulse__bar"></div>
            <div class="gen-pulse__bar"></div>
            <div class="gen-pulse__bar"></div>
            <div class="gen-pulse__bar"></div>
            <div class="gen-pulse__bar"></div>
            <div class="gen-pulse__bar"></div>
          </div>
          <div class="gen-stats" data-gen-stats="">0 tokens generated</div>
        </div>
      </div>

      <div class="gen-controls">
        <button class="gen-btn gen-btn--primary" data-gen-next="">next token</button>
        <button class="gen-btn" data-gen-auto="">auto-play</button>
        <button class="gen-btn gen-btn--ghost" data-gen-reset="">reset</button>
        <label class="gen-temp">T: <strong data-gen-tval="">0.80</strong>
          <input type="range" min="10" max="200" value="80" step="5" data-gen-temp="" />
        </label>
      </div>

      <p class="gen-hint"><strong>What you're watching:</strong> for each click, the demo samples one token from the top-5 distribution at the current temperature. Lower T sharpens to greedy; higher T flattens to chaos. The real model would do this exactly the same way, but with a 50,257-token vocabulary and a real forward pass instead of these handcrafted continuations. Same shape, different scale.</p>
    </div>
  </div>
</div>

<style>
  #demo-gen .gen-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-gen .gen-lead strong { color: #7c4d0a; }
  #demo-gen .gen-presets { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.7rem; }
  #demo-gen .gen-preset {
    padding: 0.36rem 0.7rem; font-family: var(--nn-mono); font-size: 0.72rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-gen .gen-preset:hover { border-color: #b77214; }
  #demo-gen .gen-preset.is-active { background: #b77214; color: #fff; border-color: #b77214; }
  #demo-gen .gen-prompt {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.65rem 0.85rem; margin-bottom: 0.75rem;
    font-family: var(--nn-mono); font-size: 0.92rem; color: var(--nn-ink);
    min-height: 50px; line-height: 1.65;
    word-break: break-word;
  }
  #demo-gen .gen-prompt .gen-tok-new {
    background: #fff6e0; color: #7c4d0a; padding: 0 3px; border-radius: 2px;
    animation: genPop 320ms cubic-bezier(.2,.8,.4,1);
  }
  @keyframes genPop {
    0% { background: #b77214; color: #fff; transform: scale(0.85); }
    100% { background: #fff6e0; color: #7c4d0a; transform: scale(1); }
  }
  #demo-gen .gen-stage {
    display: grid; grid-template-columns: 1.2fr 1fr; gap: 0.7rem;
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem; margin-bottom: 0.85rem;
  }
  #demo-gen .gen-col__label {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.45rem;
  }
  #demo-gen .gen-candidates { display: flex; flex-direction: column; gap: 0.32rem; }
  #demo-gen .gen-cand {
    display: grid; grid-template-columns: 90px 1fr 50px;
    align-items: center; gap: 0.5rem;
  }
  #demo-gen .gen-cand__tok { font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-ink); }
  #demo-gen .gen-cand__tok.is-picked { color: #7c4d0a; font-weight: 600; }
  #demo-gen .gen-cand__bar {
    height: 12px; background: #f5f1e8; border: 1px solid var(--nn-line); border-radius: 2px; overflow: hidden;
  }
  #demo-gen .gen-cand__fill { height: 100%; background: #ddb88e; transition: width 220ms; }
  #demo-gen .gen-cand__fill.is-picked { background: #b77214; }
  #demo-gen .gen-cand__num {
    font-family: var(--nn-mono); font-size: 0.7rem; color: var(--nn-muted); text-align: right;
  }

  #demo-gen .gen-pulse {
    display: flex; gap: 4px; height: 38px; align-items: end; margin-bottom: 0.45rem;
    padding: 0 4px;
  }
  #demo-gen .gen-pulse__bar {
    flex: 1; background: #ddb88e; border-radius: 2px 2px 0 0; height: 30%;
    transition: height 180ms;
  }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar {
    animation: genPulse 800ms cubic-bezier(.5,0,.5,1) infinite;
  }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(1) { animation-delay: 0ms; }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(2) { animation-delay: 80ms; }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(3) { animation-delay: 160ms; }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(4) { animation-delay: 240ms; }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(5) { animation-delay: 320ms; }
  #demo-gen .gen-pulse.is-pulsing .gen-pulse__bar:nth-child(6) { animation-delay: 400ms; }
  @keyframes genPulse {
    0%   { height: 25%; background: #ddb88e; }
    50%  { height: 95%; background: #b77214; }
    100% { height: 25%; background: #ddb88e; }
  }
  #demo-gen .gen-stats {
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
  }

  #demo-gen .gen-controls {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    margin-bottom: 0.65rem;
  }
  #demo-gen .gen-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-gen .gen-btn:hover { border-color: #b77214; }
  #demo-gen .gen-btn--primary { background: #b77214; color: #fff; border-color: #b77214; font-weight: 600; }
  #demo-gen .gen-btn--primary:hover { background: #7c4d0a; }
  #demo-gen .gen-btn--ghost { color: var(--nn-muted); }
  #demo-gen .gen-temp {
    margin-left: auto; font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
    display: flex; align-items: center; gap: 0.35rem;
  }
  #demo-gen .gen-temp strong { color: var(--nn-ink); min-width: 32px; }
  #demo-gen .gen-temp input { accent-color: #b77214; }

  #demo-gen .gen-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-gen .gen-hint strong { color: #7c4d0a; }
  @media (max-width: 620px){
    #demo-gen .gen-stage { grid-template-columns: 1fr; }
    #demo-gen .gen-temp { margin-left: 0; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-gen"); if (!root) return;

  // Hand-crafted small "language model": for each known prefix, a top-5 distribution.
  // Logits, not probabilities. Demo will softmax with the user's T.
  var TABLE = {
    "The cat sat on the": [{t:" mat",l:4.5},{t:" couch",l:3.6},{t:" floor",l:3.2},{t:" rug",l:2.8},{t:" windowsill",l:2.0}],
    "The cat sat on the mat": [{t:".",l:4.2},{t:",",l:3.4},{t:" and",l:3.1},{t:" while",l:2.4},{t:" looking",l:2.1}],
    "The cat sat on the mat.": [{t:" The",l:3.8},{t:" It",l:3.4},{t:" Then",l:3.0},{t:" Outside",l:2.5},{t:" Suddenly",l:2.2}],
    "The cat sat on the couch": [{t:".",l:4.0},{t:" and",l:3.6},{t:" purring",l:3.2},{t:" looking",l:2.6},{t:" while",l:2.3}],
    "The cat sat on the floor": [{t:".",l:4.0},{t:" and",l:3.5},{t:" staring",l:3.1},{t:" near",l:2.5},{t:" by",l:2.2}],

    "Roses are red,": [{t:" violets",l:5.0},{t:" the",l:2.5},{t:" so",l:2.0},{t:" they",l:1.8},{t:" and",l:1.6}],
    "Roses are red, violets": [{t:" are",l:4.8},{t:" too",l:2.2},{t:" green",l:1.8},{t:" bloom",l:1.5},{t:" rare",l:1.3}],
    "Roses are red, violets are": [{t:" blue",l:5.0},{t:" purple",l:2.6},{t:" lovely",l:2.1},{t:" rare",l:1.7},{t:" too",l:1.5}],
    "Roses are red, violets are blue": [{t:",",l:4.5},{t:".",l:3.4},{t:" and",l:3.0},{t:" sugar",l:2.6},{t:" honey",l:2.0}],
    "Roses are red, violets are blue,": [{t:" sugar",l:4.0},{t:" honey",l:3.0},{t:" the",l:2.4},{t:" you",l:2.0},{t:" love",l:1.8}],

    "Paris is the capital of": [{t:" France",l:5.5},{t:" the",l:2.4},{t:" Europe",l:1.7},{t:" a",l:1.4},{t:" Western",l:1.2}],
    "Paris is the capital of France": [{t:".",l:4.8},{t:",",l:3.4},{t:" and",l:2.8},{t:" since",l:2.0},{t:" with",l:1.7}],
    "Paris is the capital of France.": [{t:" It",l:3.8},{t:" The",l:3.5},{t:" Located",l:2.9},{t:" Home",l:2.4},{t:" Paris",l:2.1}],

    "def factorial(n):": [{t:"\n    return",l:5.0},{t:"\n    if",l:3.6},{t:"\n    \"\"\"",l:2.5},{t:"\n    result",l:2.0},{t:"\n    n",l:1.4}],
    "def factorial(n):\n    return": [{t:" 1",l:4.5},{t:" n",l:3.6},{t:" prod",l:2.6},{t:" reduce",l:2.2},{t:" math",l:1.7}],
    "def factorial(n):\n    return 1": [{t:" if",l:5.0},{t:" *",l:2.4},{t:"\n",l:2.0},{t:" or",l:1.6},{t:" when",l:1.4}],
    "def factorial(n):\n    return 1 if": [{t:" n",l:5.0},{t:" not",l:2.4},{t:" else",l:1.8},{t:" len",l:1.4},{t:" i",l:1.2}]
  };

  // generic fallback for unknown prefixes
  var FALLBACK = [{t:" the",l:3.0},{t:" a",l:2.6},{t:" and",l:2.4},{t:" of",l:2.0},{t:" to",l:1.8}];

  var PRESETS = {
    cat: "The cat sat on the",
    poetry: "Roses are red,",
    capital: "Paris is the capital of",
    code: "def factorial(n):"
  };

  var promptEl = root.querySelector("[data-gen-prompt]");
  var candEl   = root.querySelector("[data-gen-candidates]");
  var passEl   = root.querySelector("[data-gen-pass]");
  var pulseEl  = root.querySelector("[data-gen-pulse]");
  var statsEl  = root.querySelector("[data-gen-stats]");
  var nextBtn  = root.querySelector("[data-gen-next]");
  var autoBtn  = root.querySelector("[data-gen-auto]");
  var resetBtn = root.querySelector("[data-gen-reset]");
  var tempEl   = root.querySelector("[data-gen-temp]");
  var tvalEl   = root.querySelector("[data-gen-tval]");

  var state = {
    base: PRESETS.cat,
    extra: "",
    pass: 0,
    auto: false,
    autoTimer: null
  };

  function softmax(logits, T){
    if (T < 0.05) T = 0.05;
    var max = -Infinity;
    logits.forEach(function(l){ if (l/T > max) max = l/T; });
    var e = logits.map(function(l){ return Math.exp(l/T - max); });
    var s = e.reduce(function(a,b){ return a+b; }, 0);
    return e.map(function(x){ return x/s; });
  }

  function getCandidates(){
    var key = state.base + state.extra;
    return TABLE[key] || FALLBACK;
  }

  function renderPrompt(newTok){
    var html = state.base + state.extra;
    if (newTok){
      html = (state.base + state.extra.slice(0, state.extra.length - newTok.length)) +
             "<span class=\"gen-tok-new\">" + escapeHtml(newTok) + "</span>";
    }
    promptEl.innerHTML = html;
  }

  function renderCandidates(picked){
    var T = parseInt(tempEl.value, 10) / 100;
    tvalEl.textContent = T.toFixed(2);
    var cands = getCandidates();
    var probs = softmax(cands.map(function(c){ return c.l; }), T);
    var html = "";
    cands.forEach(function(c, i){
      var pct = (probs[i] * 100).toFixed(1);
      var isPicked = picked === c.t ? " is-picked" : "";
      html += "<div class=\"gen-cand\">"+
        "<div class=\"gen-cand__tok"+isPicked+"\">"+escapeHtml(c.t.replace(/\n/g,"\\n"))+"</div>"+
        "<div class=\"gen-cand__bar\"><div class=\"gen-cand__fill"+isPicked+"\" style=\"width:"+(probs[i]*100).toFixed(2)+"%\"></div></div>"+
        "<div class=\"gen-cand__num\">"+pct+"%</div>"+
        "</div>";
    });
    candEl.innerHTML = html;
  }

  function escapeHtml(s){
    return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
  }

  function pulse(){
    pulseEl.classList.add("is-pulsing");
    setTimeout(function(){ pulseEl.classList.remove("is-pulsing"); }, 480);
  }

  function step(){
    var T = parseInt(tempEl.value, 10) / 100;
    var cands = getCandidates();
    var probs = softmax(cands.map(function(c){ return c.l; }), T);
    var r = Math.random(), cum = 0, picked = cands[0].t;
    for (var i = 0; i < probs.length; i++){
      cum += probs[i];
      if (r < cum){ picked = cands[i].t; break; }
    }
    state.extra += picked;
    state.pass += 1;
    pulse();
    renderPrompt(picked);
    setTimeout(function(){ renderPrompt(); renderCandidates(picked); }, 320);
    passEl.textContent = state.pass;
    var tokens = state.extra.split(/(?=[ .,])/).filter(Boolean).length;
    statsEl.textContent = state.pass + " forward passes · " + state.pass + " tokens generated";
  }

  function reset(){
    stopAuto();
    state.extra = "";
    state.pass = 0;
    passEl.textContent = 0;
    statsEl.textContent = "0 tokens generated";
    renderPrompt();
    renderCandidates();
  }

  function setPreset(key){
    stopAuto();
    state.base = PRESETS[key];
    reset();
    root.querySelectorAll(".gen-preset").forEach(function(b){
      b.classList.toggle("is-active", b.getAttribute("data-gen-preset") === key);
    });
  }

  function startAuto(){
    if (state.auto) return;
    state.auto = true;
    autoBtn.textContent = "stop";
    state.autoTimer = setInterval(step, 900);
  }
  function stopAuto(){
    if (!state.auto) return;
    state.auto = false;
    autoBtn.textContent = "auto-play";
    clearInterval(state.autoTimer);
  }
  function toggleAuto(){ if (state.auto) stopAuto(); else startAuto(); }

  nextBtn.addEventListener("click", step);
  autoBtn.addEventListener("click", toggleAuto);
  resetBtn.addEventListener("click", reset);
  tempEl.addEventListener("input", function(){ renderCandidates(); });
  root.querySelectorAll(".gen-preset").forEach(function(b){
    b.addEventListener("click", function(){ setPreset(b.getAttribute("data-gen-preset")); });
  });

  setPreset("cat");
})();
</script>

<p>This is why MI does not ask “what happened at step 3?” but instead “what did head 7.4 contribute?”. The latter has a precise numerical answer (DLA gives a scalar); the former does not.</p>

<h2 id="what-this-series-has-covered">What this series has covered</h2>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Post</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>The black box problem and why MI exists</td>
      <td>post 1</td>
    </tr>
    <tr>
      <td>What mechanistic interpretability is</td>
      <td>post 2</td>
    </tr>
    <tr>
      <td>Who’s doing this work, and current goals</td>
      <td>post 3</td>
    </tr>
    <tr>
      <td>Neurons, weights, and forward propagation</td>
      <td>posts 4–5</td>
    </tr>
    <tr>
      <td>Layers, depth, and training</td>
      <td>posts 6–7</td>
    </tr>
    <tr>
      <td>Transformer architecture overview</td>
      <td>post 8 (this series)</td>
    </tr>
    <tr>
      <td>Tokens and BPE</td>
      <td>post 9</td>
    </tr>
    <tr>
      <td>Residual stream, logit lens, DLA</td>
      <td>post 10</td>
    </tr>
    <tr>
      <td>Attention, QK / OV, IOI circuit</td>
      <td>post 11</td>
    </tr>
    <tr>
      <td>MLPs, key-value memory, superposition</td>
      <td>post 12</td>
    </tr>
    <tr>
      <td>Full forward pass</td>
      <td>post 13</td>
    </tr>
  </tbody>
</table>

<p>Sufficient to read most current MI papers without ambiguity.</p>

<h2 id="where-this-goes-next">Where this goes next</h2>

<p>The next chapter is <strong>features and circuits</strong>: applying this architectural foundation to find concrete computational structures inside trained models. Topics:</p>

<ul>
  <li>Sparse autoencoders in depth (Anthropic 2023, 2024).</li>
  <li>Activation patching and causal scrubbing.</li>
  <li>The IOI circuit reproduction in code.</li>
  <li>Feature visualization and concept geometry.</li>
  <li>Mech interp on production models (Claude, Llama).</li>
</ul>

<h2 id="resources">Resources</h2>

<h3 id="foundational">Foundational</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"><div class="research-card__title">Attention Is All You Need</div><div class="research-card__authors">Vaswani et al., 2017 · the architecture</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener"><div class="research-card__title">A Mathematical Framework for Transformer Circuits</div><div class="research-card__authors">Elhage et al., Anthropic 2021 · residual-stream view, QK / OV decomposition</div></a></li>
  <li><a class="research-card" href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens" target="_blank" rel="noopener"><div class="research-card__title">Interpreting GPT: the logit lens</div><div class="research-card__authors">Nostalgebraist, LessWrong 2020 · the lens used throughout this series</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html" target="_blank" rel="noopener"><div class="research-card__title">In-context Learning and Induction Heads</div><div class="research-card__authors">Olsson et al., Anthropic 2022</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2211.00593" target="_blank" rel="noopener"><div class="research-card__title">Interpretability in the Wild: a Circuit for IOI in GPT-2</div><div class="research-card__authors">Wang et al., 2022 · the canonical end-to-end circuit reverse-engineering</div></a></li>
</ul>

<h3 id="code-tools-courses">Code, tools, courses</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://github.com/karpathy/nanoGPT" target="_blank" rel="noopener"><div class="research-card__title">nanoGPT</div><div class="research-card__authors">Karpathy · ~300-line PyTorch GPT-2 implementation; read the forward pass directly</div></a></li>
  <li><a class="research-card" href="https://transformerlensorg.github.io/TransformerLens/" target="_blank" rel="noopener"><div class="research-card__title">TransformerLens</div><div class="research-card__authors">the standard MI library; load any HF model and inspect every activation</div></a></li>
  <li><a class="research-card" href="https://huggingface.co/Xenova/distilgpt2" target="_blank" rel="noopener"><div class="research-card__title">Xenova/distilgpt2</div><div class="research-card__authors">Hugging Face · the model used in the grand-tour demo</div></a></li>
  <li><a class="research-card" href="https://arena3-chapter1-transformer-interp.streamlit.app/" target="_blank" rel="noopener"><div class="research-card__title">ARENA · Transformer Interpretability</div><div class="research-card__authors">guided exercises: build the logit lens, DLA, induction heads, IOI from scratch</div></a></li>
  <li><a class="research-card" href="https://www.neelnanda.io/mechanistic-interpretability/getting-started" target="_blank" rel="noopener"><div class="research-card__title">Neel Nanda's MI Getting Started</div><div class="research-card__authors">curated reading order, problem sets, and study tips</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/" target="_blank" rel="noopener"><div class="research-card__title">Transformer Circuits Thread</div><div class="research-card__authors">Anthropic · the canonical venue for new MI results; follow it to stay current</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[End-to-end execution: tokens → embeddings → six attention+MLP blocks → unembedding. Tensor shapes, layer-by-layer logit-lens trajectory, and the surface area available for mechanistic analysis.]]></summary></entry><entry><title type="html">MLPs: The Other Half of Every Block</title><link href="https://bhavith-chandra.github.io/posts/mlps-the-other-half-of-every-block/" rel="alternate" type="text/html" title="MLPs: The Other Half of Every Block" /><published>2026-04-15T00:00:00-07:00</published><updated>2026-04-15T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/mlps-the-other-half-of-every-block</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/mlps-the-other-half-of-every-block/"><![CDATA[<p>The <strong>MLP</strong> (multi-layer perceptron, or feed-forward network) is the second sublayer in every transformer block. It contains ~⅔ of the model’s parameters and operates position-wise: each token’s residual stream vector is processed independently.</p>

<p>This post defines the MLP, derives the <strong>key-value memory</strong> interpretation (<a href="https://arxiv.org/abs/2012.14913">Geva et al., 2021</a>) that underlies most modern MLP interpretability, covers neuron archetypes and superposition, and connects the framework to factual editing (ROME/MEMIT) and sparse autoencoders.</p>

<hr />

<h2 id="demo-neuron-activations">Demo: neuron activations</h2>

<div class="idemo" id="demo-neuron">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · MLP neuron inspector</span></div>
    <div class="idemo__body">

      <p class="ni-lead">An MLP block has two linear layers with a non-linearity between them. The intermediate dimension is huge, typically 4× the hidden size. So in distilGPT2, each block has <strong>3,072 MLP neurons</strong>. Each one is a tiny pattern detector. Click a neuron below to see what it fires on and what it writes back to the stream.</p>

      <div class="ni-flow" data-ni-flow="">
        <div class="ni-flow__stage">
          <div class="ni-flow__title">Residual</div>
          <div class="ni-flow__shape">[T, 768]</div>
          <div class="ni-flow__caption">comes in</div>
        </div>
        <div class="ni-flow__arrow"><span class="ni-flow__op">W<sub>in</sub></span></div>
        <div class="ni-flow__stage ni-flow__stage--big">
          <div class="ni-flow__title">Up-projection</div>
          <div class="ni-flow__shape">[T, 3072]</div>
          <div class="ni-flow__caption">3072 neurons fire (or don't)</div>
        </div>
        <div class="ni-flow__arrow"><span class="ni-flow__op">GELU</span></div>
        <div class="ni-flow__stage ni-flow__stage--big">
          <div class="ni-flow__title">Activation</div>
          <div class="ni-flow__shape">[T, 3072]</div>
          <div class="ni-flow__caption">non-linear gate</div>
        </div>
        <div class="ni-flow__arrow"><span class="ni-flow__op">W<sub>out</sub></span></div>
        <div class="ni-flow__stage">
          <div class="ni-flow__title">Δ Residual</div>
          <div class="ni-flow__shape">[T, 768]</div>
          <div class="ni-flow__caption">added to belt</div>
        </div>
      </div>

      <div class="ni-pick">
        <span class="ni-pick__label">Pick a neuron</span>
        <select class="ni-pick__select" data-ni-select=""></select>
        <span class="ni-pick__hint">Examples curated from published MLP-feature research and circuit studies.</span>
      </div>

      <div class="ni-card" data-ni-card="">
        <div class="ni-card__head">
          <div class="ni-card__addr" data-ni-addr="">, </div>
          <div class="ni-card__name" data-ni-name="">, </div>
          <div class="ni-card__tag" data-ni-tag=""></div>
        </div>
        <div class="ni-card__desc" data-ni-desc=""></div>

        <div class="ni-card__split">
          <div class="ni-card__col">
            <div class="ni-card__col-label">Top-activating contexts <span class="ni-card__col-hint">("keys", what triggers this neuron)</span></div>
            <ol class="ni-contexts" data-ni-contexts=""></ol>
          </div>
          <div class="ni-card__col">
            <div class="ni-card__col-label">Output direction <span class="ni-card__col-hint">("values", what this neuron writes toward)</span></div>
            <ul class="ni-output" data-ni-output=""></ul>
          </div>
        </div>

        <div class="ni-poly" data-ni-poly="" hidden="">
          <div class="ni-poly__label">Polysemantic? Same neuron, multiple unrelated firing patterns:</div>
          <div class="ni-poly__chips" data-ni-poly-chips=""></div>
        </div>
      </div>

      <details>
        <summary>Why MLPs as "key-value memories"</summary>
        <p>Geva et al. (<a href="https://arxiv.org/abs/2012.14913" target="_blank" rel="noopener">2021</a>) showed that you can read the up-projection rows as <em>keys</em> (patterns the neuron looks for in the residual stream) and the down-projection columns as <em>values</em> (vectors the neuron writes into the stream when it fires). That decomposition turns each neuron into a key-value pair: "if you see X, write Y." It's the cleanest way to think about MLPs that exists. Bricken et al. (<a href="https://transformer-circuits.pub/2023/monosemantic-features/index.html" target="_blank" rel="noopener">2023</a>) extended this with sparse autoencoders to handle the messy fact that real neurons mix many features in superposition, but the key-value framing is still the foundation.</p>
      </details>
    </div>
  </div>
</div>

<style>
  #demo-neuron .ni-lead { margin: 0 0 1.05rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-neuron .ni-lead strong { color: #7c4d0a; }

  #demo-neuron .ni-flow {
    display: flex; align-items: stretch; gap: 0.4rem; flex-wrap: nowrap;
    padding: 1rem 0.7rem; background: #fafaf7; border: 1px solid var(--nn-line);
    border-radius: 4px; margin-bottom: 1rem; overflow-x: auto;
  }
  #demo-neuron .ni-flow__stage {
    flex: 0 0 auto; min-width: 110px; padding: 0.55rem 0.7rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    text-align: center;
  }
  #demo-neuron .ni-flow__stage--big { background: rgba(251,191,36,0.12); border-color: #b77214; }
  #demo-neuron .ni-flow__title {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-neuron .ni-flow__shape {
    font-family: var(--nn-mono); font-size: 0.95rem; color: var(--nn-ink); margin: 0.25rem 0;
  }
  #demo-neuron .ni-flow__caption { font-size: 0.74rem; color: var(--nn-muted); }
  #demo-neuron .ni-flow__arrow {
    display: flex; align-items: center; flex: 0 0 auto;
  }
  #demo-neuron .ni-flow__op {
    font-family: var(--nn-mono); font-size: 0.78rem; color: #b77214; font-weight: 600;
    padding: 0.25rem 0.5rem; background: #fff; border: 1px solid #b77214; border-radius: 3px;
  }

  #demo-neuron .ni-pick {
    display: flex; gap: 0.7rem; align-items: center; flex-wrap: wrap;
    margin-bottom: 0.85rem;
  }
  #demo-neuron .ni-pick__label {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-neuron .ni-pick__select {
    flex: 1; min-width: 220px; max-width: 460px;
    padding: 0.5rem 0.65rem; font-family: var(--nn-mono); font-size: 0.85rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px; color: var(--nn-ink);
  }
  #demo-neuron .ni-pick__hint { font-size: 0.78rem; color: var(--nn-muted); }

  #demo-neuron .ni-card {
    padding: 1rem 1.1rem; background: #fff; border: 1px solid var(--nn-line);
    border-left: 3px solid #b77214; border-radius: 3px;
  }
  #demo-neuron .ni-card__head {
    display: flex; gap: 0.7rem; align-items: baseline; flex-wrap: wrap; margin-bottom: 0.55rem;
  }
  #demo-neuron .ni-card__addr {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: #b77214;
  }
  #demo-neuron .ni-card__name { font-size: 1.05rem; font-weight: 600; color: var(--nn-ink); }
  #demo-neuron .ni-card__tag {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.06em;
    padding: 0.15rem 0.5rem; background: rgba(251,191,36,0.18); color: #7c4d0a;
    border: 1px solid rgba(183,114,20,0.3); border-radius: 3px;
  }
  #demo-neuron .ni-card__desc {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6; margin-bottom: 0.9rem;
  }
  #demo-neuron .ni-card__split {
    display: grid; grid-template-columns: 1.4fr 1fr; gap: 1rem;
  }
  @media (max-width: 720px) { #demo-neuron .ni-card__split { grid-template-columns: 1fr; } }
  #demo-neuron .ni-card__col-label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.4rem;
  }
  #demo-neuron .ni-card__col-hint { color: var(--nn-muted); text-transform: none; letter-spacing: 0; font-size: 0.74rem; }

  #demo-neuron .ni-contexts { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.45rem; }
  #demo-neuron .ni-contexts li {
    padding: 0.5rem 0.7rem; background: #fafaf7;
    border: 1px solid var(--nn-line); border-left: 2px solid #b77214;
    border-radius: 2px; font-family: var(--nn-mono); font-size: 0.83rem;
    color: var(--nn-body); line-height: 1.45;
  }
  #demo-neuron .ni-contexts mark {
    background: rgba(251,191,36,0.45); color: #7c4d0a; padding: 0 0.15em; border-radius: 2px;
  }
  #demo-neuron .ni-contexts__act {
    display: inline-block; min-width: 38px; padding: 0 0.35rem; margin-right: 0.45rem;
    background: #b77214; color: #fff; font-size: 0.72rem;
    border-radius: 2px; text-align: center;
  }

  #demo-neuron .ni-output { list-style: none; padding: 0; margin: 0; }
  #demo-neuron .ni-output li {
    display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0;
    font-family: var(--nn-mono); font-size: 0.85rem;
  }
  #demo-neuron .ni-output__tok {
    background: #fafaf7; padding: 0.1rem 0.45rem; border: 1px solid var(--nn-line);
    border-radius: 3px; color: var(--nn-ink); min-width: 90px;
  }
  #demo-neuron .ni-output__bar {
    flex: 1; height: 6px; background: #eee6d1; border-radius: 3px; overflow: hidden;
  }
  #demo-neuron .ni-output__bar-fill { height: 100%; background: #b77214; }
  #demo-neuron .ni-output__pct { font-size: 0.74rem; color: var(--nn-muted); min-width: 36px; text-align: right; }

  #demo-neuron .ni-poly {
    margin-top: 0.95rem; padding-top: 0.8rem;
    border-top: 1px dashed var(--nn-line);
  }
  #demo-neuron .ni-poly__label {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: #c04550; margin-bottom: 0.45rem;
  }
  #demo-neuron .ni-poly__chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
  #demo-neuron .ni-poly__chip {
    padding: 0.32rem 0.6rem; font-family: var(--nn-mono); font-size: 0.75rem;
    background: #fff; border: 1px solid #c04550; border-radius: 2px; color: #872934;
  }
</style>

<script>
(function(){
  const root = document.getElementById("demo-neuron"); if (!root) return;

  // Curated examples drawn from MI literature: Geva 2021, Olah Distill,
  // Bricken 2023 toy SAE, and circuit case studies. Activations rounded.
  const NEURONS = [
    {
      addr: "L0 · N1842",
      name: "Capital-letter detector",
      tag: "monosemantic",
      desc: "Fires on tokens that begin with a capital letter. Common in early layers, basic surface features. Useful to downstream blocks for tagging proper nouns.",
      contexts: [
        { act: 6.2, before: "she met ", word: "John", after: " at the cafe" },
        { act: 5.8, before: "based in ", word: "Paris", after: " since 2010" },
        { act: 5.4, before: "I work at ", word: "Anthropic", after: " on safety" },
        { act: 0.1, before: "the cat sat on the ", word: "mat", after: "" }
      ],
      output: [
        { tok: " (proper noun)", w: 0.91 },
        { tok: " ,", w: 0.62 },
        { tok: " who", w: 0.41 },
        { tok: " '", w: 0.34 }
      ]
    },
    {
      addr: "L1 · N712",
      name: "Python-keyword neuron",
      tag: "monosemantic",
      desc: "Fires strongly on Python language tokens, def, class, return, import, lambda. Doesn't fire on similar English words used outside code contexts. The model discovered code is a different mode of operation.",
      contexts: [
        { act: 7.8, before: "", word: "def", after: " hello():" },
        { act: 7.2, before: "    ", word: "return", after: " x + y" },
        { act: 6.9, before: "from os ", word: "import", after: " path" },
        { act: 0.4, before: "I had to ", word: "return", after: " the book" }
      ],
      output: [
        { tok: " (", w: 0.82 },
        { tok: " self", w: 0.71 },
        { tok: " :", w: 0.55 },
        { tok: " None", w: 0.43 }
      ]
    },
    {
      addr: "L2 · N3019",
      name: "Counting / numeric continuation",
      tag: "monosemantic",
      desc: "Fires when the recent context contains a sequence of small numbers. Looks like the model preparing to predict the next number in a count. Critical for arithmetic.",
      contexts: [
        { act: 8.1, before: "1, 2, 3, 4, ", word: "5", after: ", 6" },
        { act: 7.6, before: "two, four, six, ", word: "eight", after: ", ten" },
        { act: 6.3, before: "step ", word: "1", after: ": gather" },
        { act: 0.6, before: "I have ", word: "three", after: " sisters" }
      ],
      output: [
        { tok: " 6", w: 0.78 },
        { tok: " 7", w: 0.71 },
        { tok: " ten", w: 0.59 },
        { tok: " next", w: 0.42 }
      ]
    },
    {
      addr: "L3 · N228",
      name: "Definite-article expectation",
      tag: "syntactic",
      desc: "Fires after possessive constructions where the model expects a noun next. Pushes the next-token distribution toward concrete nouns. Less interpretable on its own, part of a syntactic-prediction circuit.",
      contexts: [
        { act: 5.5, before: "the doctor's ", word: "office", after: "" },
        { act: 5.1, before: "Anna's ", word: "dog", after: " barked" },
        { act: 4.8, before: "the city's ", word: "mayor", after: " announced" },
        { act: 0.8, before: "she sat ", word: "down", after: " quickly" }
      ],
      output: [
        { tok: " house", w: 0.49 },
        { tok: " father", w: 0.45 },
        { tok: " job", w: 0.41 },
        { tok: " name", w: 0.37 }
      ]
    },
    {
      addr: "L4 · N1517",
      name: "France/Paris fact",
      tag: "factual recall",
      desc: "Activates strongly when the residual stream encodes 'capital of France' or related geo-entities. Writes out a vector that boosts the logit for ' Paris'. A small piece of a factual-recall circuit (see Geva 2021, Meng 2022).",
      contexts: [
        { act: 7.3, before: "the capital of France is ", word: " ", after: "" },
        { act: 6.6, before: "the Eiffel tower in ", word: " ", after: "" },
        { act: 5.9, before: "Le Monde, the famous ", word: " ", after: " newspaper" },
        { act: 0.2, before: "the cat sat on the ", word: "mat", after: "" }
      ],
      output: [
        { tok: " Paris", w: 0.94 },
        { tok: " French", w: 0.71 },
        { tok: " Notre", w: 0.48 },
        { tok: " baguette", w: 0.31 }
      ]
    },
    {
      addr: "L4 · N2204",
      name: "Polysemantic mixed bag",
      tag: "polysemantic ⚠",
      desc: "Classic polysemantic neuron. Fires on what look like four totally unrelated patterns, DNA letters, car-brand names, the word 'thursday', and Latin botanical names. None of these are individually decoded by this neuron alone; they live in superposition with each other and only get cleanly separated by sparse-autoencoder analysis.",
      contexts: [
        { act: 5.4, before: "the gene encodes ", word: "GATTACA", after: " sequence" },
        { act: 5.2, before: "drove a ", word: "Ferrari", after: " through" },
        { act: 5.0, before: "see you on ", word: "Thursday", after: "" },
        { act: 4.9, before: "Olea ", word: "europaea", after: " is olive" }
      ],
      output: [
        { tok: " sequence", w: 0.42 },
        { tok: " engine", w: 0.39 },
        { tok: " evening", w: 0.36 },
        { tok: " species", w: 0.33 }
      ],
      poly: ["DNA letters", "car brands", "weekday names", "Latin botanical names"]
    },
    {
      addr: "L5 · N501",
      name: "Sentiment shift detector",
      tag: "abstract",
      desc: "Activates on conjunctions that signal a sentiment flip, 'but', 'however', 'although', 'despite'. Pushes the next-token distribution to expect contrasting evaluation. Common in late-layer 'glue' neurons.",
      contexts: [
        { act: 6.1, before: "the food was great ", word: "but", after: " the service" },
        { act: 5.7, before: "I love this car. ", word: "However", after: ", the price" },
        { act: 5.3, before: "she's smart. ", word: "Despite", after: " that," },
        { act: 0.4, before: "and the cat sat ", word: "and", after: " purred" }
      ],
      output: [
        { tok: " not", w: 0.61 },
        { tok: " disappointing", w: 0.48 },
        { tok: " expensive", w: 0.41 },
        { tok: " never", w: 0.36 }
      ]
    }
  ];

  const select = root.querySelector("[data-ni-select]");
  NEURONS.forEach((n, i) => {
    const opt = document.createElement("option");
    opt.value = i; opt.textContent = `${n.addr} · ${n.name}`;
    select.appendChild(opt);
  });

  function show(i){
    const n = NEURONS[i];
    root.querySelector("[data-ni-addr]").textContent = n.addr;
    root.querySelector("[data-ni-name]").textContent = n.name;
    const tagEl = root.querySelector("[data-ni-tag]");
    tagEl.textContent = n.tag;
    tagEl.style.background = n.tag.includes("polysemantic") ? "rgba(192,69,80,0.16)" : "rgba(251,191,36,0.18)";
    tagEl.style.borderColor = n.tag.includes("polysemantic") ? "rgba(192,69,80,0.4)" : "rgba(183,114,20,0.3)";
    tagEl.style.color = n.tag.includes("polysemantic") ? "#872934" : "#7c4d0a";

    root.querySelector("[data-ni-desc]").textContent = n.desc;

    const ctxEl = root.querySelector("[data-ni-contexts]");
    ctxEl.innerHTML = "";
    n.contexts.forEach(c => {
      const li = document.createElement("li");
      li.innerHTML = `<span class="ni-contexts__act">${c.act.toFixed(1)}</span>${escapeHtml(c.before)}<mark>${escapeHtml(c.word)}</mark>${escapeHtml(c.after)}`;
      ctxEl.appendChild(li);
    });

    const outEl = root.querySelector("[data-ni-output]");
    outEl.innerHTML = "";
    n.output.forEach(o => {
      const li = document.createElement("li");
      const w = Math.max(4, Math.round(o.w * 100));
      li.innerHTML =
        `<span class="ni-output__tok">${escapeHtml(o.tok)}</span>` +
        `<span class="ni-output__bar"><span class="ni-output__bar-fill" style="width:${w}%"></span></span>` +
        `<span class="ni-output__pct">${o.w.toFixed(2)}</span>`;
      outEl.appendChild(li);
    });

    const polyEl = root.querySelector("[data-ni-poly]");
    if (n.poly){
      polyEl.hidden = false;
      const chips = root.querySelector("[data-ni-poly-chips]");
      chips.innerHTML = "";
      n.poly.forEach(p => {
        const c = document.createElement("span");
        c.className = "ni-poly__chip"; c.textContent = p;
        chips.appendChild(c);
      });
    } else {
      polyEl.hidden = true;
    }
  }

  function escapeHtml(s){ return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c])); }

  select.addEventListener("change", () => show(parseInt(select.value)));
  show(0);
})();
</script>

<p>Cycle through neurons. Some are monosemantic (Python keywords, capital letters, France-related contexts). Some are <strong>polysemantic</strong>, firing on multiple unrelated concepts. The polysemantic case is explained by superposition (below).</p>

<h2 id="definition">Definition</h2>

<p>For input $x \in \mathbb{R}^{d_\text{model}}$ at one position, a transformer MLP computes:</p>

\[\text{MLP}(x) = W_\text{out}\, \sigma(W_\text{in}\, x + b_\text{in}) + b_\text{out}\]

<p>where:</p>
<ul>
  <li>$W_\text{in} \in \mathbb{R}^{d_\text{ffn} \times d_\text{model}}$</li>
  <li>$W_\text{out} \in \mathbb{R}^{d_\text{model} \times d_\text{ffn}}$</li>
  <li>$\sigma$ is a non-linearity (GeLU, ReLU, or in modern models SwiGLU)</li>
  <li>$d_\text{ffn} = 4 \cdot d_\text{model}$ is the standard ratio</li>
</ul>

<p>Sizes:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>$d_\text{model}$</th>
      <th>$d_\text{ffn}$</th>
      <th>Neurons per block</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPT-2 small</td>
      <td>768</td>
      <td>3,072</td>
      <td>3,072</td>
    </tr>
    <tr>
      <td>GPT-2 XL</td>
      <td>1,600</td>
      <td>6,400</td>
      <td>6,400</td>
    </tr>
    <tr>
      <td>Llama 3 8B</td>
      <td>4,096</td>
      <td>14,336</td>
      <td>14,336</td>
    </tr>
    <tr>
      <td>GPT-3 175B</td>
      <td>12,288</td>
      <td>49,152</td>
      <td>49,152</td>
    </tr>
  </tbody>
</table>

<p>Three properties:</p>

<ol>
  <li><strong>Position-wise.</strong> No mixing across token positions. Operates in parallel on each token’s residual vector.</li>
  <li><strong>Up-projection then down-projection.</strong> Hidden width $4\times$ wider than the residual stream. Storage capacity scales with $d_\text{ffn}$.</li>
  <li><strong>Non-linearity is essential.</strong> Without $\sigma$, two stacked linear layers collapse to one and the MLP cannot represent any nonlinear pattern.</li>
</ol>

<h2 id="key-value-memory-interpretation">Key-value memory interpretation</h2>

<p>Decompose $W_\text{in}$ row-wise and $W_\text{out}$ column-wise:</p>

<ul>
  <li>Row $n$ of $W_\text{in}$, written $k_n^\top$, is a vector in $\mathbb{R}^{d_\text{model}}$: the <strong>key</strong> of neuron $n$.</li>
  <li>Column $n$ of $W_\text{out}$, written $v_n$, is a vector in $\mathbb{R}^{d_\text{model}}$: the <strong>value</strong> of neuron $n$.</li>
</ul>

<p>Then:</p>

\[\text{MLP}(x) = \sum_{n=1}^{d_\text{ffn}} \sigma(k_n^\top x + b_n)\, v_n\]

<p>The MLP is a sum of $d_\text{ffn}$ scaled value-vectors, where each scaling coefficient is a non-linearly gated dot product of $x$ with the corresponding key.</p>

<p>Equivalently:</p>
<ul>
  <li>The key $k_n$ tests whether $x$ matches a specific pattern (large $k_n^\top x$ ⇒ match).</li>
  <li>The non-linearity gates: only neurons whose match exceeds threshold contribute.</li>
  <li>Each contributing neuron writes its value $v_n$ to the residual stream, scaled by activation.</li>
</ul>

<p>This is a soft, sparse key-value lookup over a learned database of $d_\text{ffn}$ entries per block. (<a href="https://arxiv.org/abs/2012.14913">Geva et al., 2021</a>)</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>The key-value framing reduces MLP interpretability to two independent questions per neuron: (1) what input pattern activates k<sub>n</sub>? (2) what does v<sub>n</sub> write to the residual stream? Both are tractable. Top-activating dataset examples answer (1); projecting v<sub>n</sub> onto W<sub>U</sub> answers (2).</p>
</aside>

<h2 id="neuron-archetypes">Neuron archetypes</h2>

<p>Empirically, MLP neurons fall into recurring categories:</p>

<h3 id="surface-feature-neurons-early-layers">Surface-feature neurons (early layers)</h3>

<p>Fire on lexical patterns: capital letters, punctuation, specific morphemes, code-syntax tokens. Their values write tags downstream blocks consume.</p>

<h3 id="syntactic-neurons-mid-layers">Syntactic neurons (mid layers)</h3>

<p>Fire after grammatical patterns: possessives, definite articles, sentence-initial positions. Values bias the next-token distribution toward syntactically valid continuations.</p>

<h3 id="factual-recall-neurons-mid-to-late-layers">Factual-recall neurons (mid-to-late layers)</h3>

<p>Encode specific facts. <a href="https://arxiv.org/abs/2202.05262">Meng et al. (2022, ROME)</a> demonstrated that “the Eiffel Tower is in Paris” can be located to a small set of neurons in mid layers and surgically edited (so the model claims the Eiffel Tower is in Rome) by modifying $W_\text{out}$ columns at those positions.</p>

<h3 id="abstract--semantic-neurons-late-layers">Abstract / semantic neurons (late layers)</h3>

<p>Fire on higher-level patterns: sentiment, sarcasm, discourse markers. Harder to characterize from top-activating examples alone.</p>

<h3 id="uninterpretable-from-top-examples">Uninterpretable from top examples</h3>

<p>A non-trivial fraction of neurons have no clean concept-level description. Often these are polysemantic.</p>

<h2 id="polysemanticity-and-superposition">Polysemanticity and superposition</h2>

<p>Most real neurons are <strong>polysemantic</strong>: top-activating contexts span multiple unrelated concepts.</p>

<p><a href="https://transformer-circuits.pub/2022/toy_model/index.html">Elhage et al. (2022, “Toy Models of Superposition”)</a> explain why. When features are sparse (most off most of the time), a $d$-dimensional space can represent ~$d / \log d$ features by overlapping them at non-orthogonal angles. The non-linearity in the MLP allows partial recovery: only one feature in a superposed pair is typically active in any given input, so interference is bounded.</p>

<p>Consequences:</p>

<ol>
  <li>The “real” interpretable features are <em>directions</em> (linear combinations of neurons), not single neurons.</li>
  <li>Reading individual neuron activations gives a tangled, polysemantic picture.</li>
  <li>To recover monosemantic features, train an overcomplete dictionary on cached activations: a <strong>sparse autoencoder (SAE)</strong>.</li>
</ol>

<p><a href="https://transformer-circuits.pub/2023/monosemantic-features/index.html">Bricken et al. (2023, “Towards Monosemanticity”)</a> and <a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html">Templeton et al. (2024, “Scaling Monosemanticity”)</a> trained SAEs on Claude 3 Sonnet and recovered millions of monosemantic features ranging from “the Golden Gate Bridge” to “code with security vulnerabilities.”</p>

<div class="idemo idemo--mini" id="demo-super">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Superposition in 2D</span></div>
    <div class="idemo__body">

      <p class="sup-lead">A neural network has fewer dimensions than concepts. To survive, it packs many features into the same space at near-orthogonal angles. The trick: only a few features are active at once, so they barely interfere. Toggle features on and off and watch the residual vector form. Then read it back out.</p>

      <div class="sup-stage">
        <svg viewBox="0 0 320 320" class="sup-svg" data-sup-svg=""></svg>
        <div class="sup-readout" data-sup-readout=""></div>
      </div>

      <div class="sup-controls" data-sup-toggles=""></div>

      <p class="sup-hint"><strong>What you're seeing:</strong> 8 feature directions packed into 2 dimensions. Each active feature contributes its direction (faded amber arrow); the residual is their sum (dark amber). Below: dot products of the residual with each feature direction, the model's "readout." When few features are on the readout is clean. Turn on too many and they start to interfere. This is why neurons can be polysemantic and why sparse autoencoders work.</p>
    </div>
  </div>
</div>

<style>
  #demo-super .sup-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-super .sup-lead strong { color: #7c4d0a; }
  #demo-super .sup-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
    display: grid; grid-template-columns: 320px 1fr; gap: 1rem; align-items: start;
  }
  #demo-super .sup-svg {
    width: 320px; height: 320px; max-width: 100%;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-super .sup-readout {
    display: flex; flex-direction: column; gap: 0.35rem;
  }
  #demo-super .sup-readout__row {
    display: grid; grid-template-columns: 80px 1fr 50px; gap: 0.5rem; align-items: center;
  }
  #demo-super .sup-readout__lbl {
    font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-ink);
  }
  #demo-super .sup-readout__bar {
    height: 14px; background: #f5f1e8; border: 1px solid var(--nn-line); border-radius: 2px; overflow: hidden;
    position: relative;
  }
  #demo-super .sup-readout__fill {
    height: 100%; background: #b77214; transition: width 220ms;
  }
  #demo-super .sup-readout__fill.is-neg { background: #ddb88e; }
  #demo-super .sup-readout__num {
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted); text-align: right;
  }
  #demo-super .sup-controls {
    display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.85rem;
  }
  #demo-super .sup-toggle {
    padding: 0.4rem 0.75rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-super .sup-toggle:hover { border-color: #b77214; }
  #demo-super .sup-toggle.is-on {
    background: #b77214; color: #fff; border-color: #b77214;
  }
  #demo-super .sup-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-super .sup-hint strong { color: #7c4d0a; }
  @media (max-width: 620px){
    #demo-super .sup-stage { grid-template-columns: 1fr; }
    #demo-super .sup-svg { width: 100%; height: auto; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-super"); if (!root) return;

  var FEATURES = [
    { name: "cat", angle: 0 },
    { name: "dog", angle: 45 },
    { name: "Paris", angle: 90 },
    { name: "code", angle: 135 },
    { name: "verb", angle: 180 },
    { name: "noun", angle: 225 },
    { name: "DNA", angle: 270 },
    { name: "music", angle: 315 }
  ];
  var CX = 160, CY = 160, R = 120;

  var active = { cat: true, Paris: true };

  var svg = root.querySelector("[data-sup-svg]");
  var readout = root.querySelector("[data-sup-readout]");
  var toggles = root.querySelector("[data-sup-toggles]");

  function rad(a){ return a * Math.PI / 180; }
  function vec(f){ return { x: Math.cos(rad(f.angle)), y: -Math.sin(rad(f.angle)) }; }

  function residual(){
    var r = { x: 0, y: 0 };
    FEATURES.forEach(function(f){
      if (active[f.name]){
        var v = vec(f);
        r.x += v.x; r.y += v.y;
      }
    });
    return r;
  }

  function renderToggles(){
    var h = "";
    FEATURES.forEach(function(f){
      h += "<button class=\"sup-toggle"+(active[f.name] ? " is-on" : "")+"\" data-f=\""+f.name+"\">"+f.name+"</button>";
    });
    toggles.innerHTML = h;
    toggles.querySelectorAll(".sup-toggle").forEach(function(b){
      b.addEventListener("click", function(){
        var n = b.getAttribute("data-f");
        active[n] = !active[n];
        renderToggles(); render();
      });
    });
  }

  function render(){
    var sv = "";
    // axes
    sv += "<line x1=\"0\" y1=\""+CY+"\" x2=\"320\" y2=\""+CY+"\" stroke=\"#eee\" stroke-width=\"1\"/>";
    sv += "<line x1=\""+CX+"\" y1=\"0\" x2=\""+CX+"\" y2=\"320\" stroke=\"#eee\" stroke-width=\"1\"/>";
    // unit circle
    sv += "<circle cx=\""+CX+"\" cy=\""+CY+"\" r=\""+R+"\" fill=\"none\" stroke=\"#eaeaea\" stroke-width=\"1\"/>";

    // feature directions (faded if off, amber if on)
    FEATURES.forEach(function(f){
      var v = vec(f);
      var x2 = CX + v.x * R, y2 = CY + v.y * R;
      var on = active[f.name];
      var stroke = on ? "rgba(183,114,20,0.55)" : "rgba(0,0,0,0.12)";
      var sw = on ? 2 : 1;
      sv += "<line x1=\""+CX+"\" y1=\""+CY+"\" x2=\""+x2.toFixed(1)+"\" y2=\""+y2.toFixed(1)+"\" stroke=\""+stroke+"\" stroke-width=\""+sw+"\" stroke-linecap=\"round\"/>";
      // label
      var lx = CX + v.x * (R + 18), ly = CY + v.y * (R + 18);
      var color = on ? "#7c4d0a" : "#999";
      var weight = on ? "600" : "400";
      sv += "<text x=\""+lx.toFixed(1)+"\" y=\""+ly.toFixed(1)+"\" font-family=\"ui-monospace, monospace\" font-size=\"11\" fill=\""+color+"\" font-weight=\""+weight+"\" text-anchor=\"middle\" dominant-baseline=\"middle\">"+f.name+"</text>";
    });

    // residual vector (dark amber arrow)
    var r = residual();
    var rx = CX + r.x * R * 0.5, ry = CY + r.y * R * 0.5;
    sv += "<line x1=\""+CX+"\" y1=\""+CY+"\" x2=\""+rx.toFixed(1)+"\" y2=\""+ry.toFixed(1)+"\" stroke=\"#7c4d0a\" stroke-width=\"3\" stroke-linecap=\"round\"/>";
    sv += "<circle cx=\""+rx.toFixed(1)+"\" cy=\""+ry.toFixed(1)+"\" r=\"4.5\" fill=\"#7c4d0a\"/>";
    sv += "<text x=\""+(rx + 8).toFixed(1)+"\" y=\""+(ry - 8).toFixed(1)+"\" font-family=\"ui-monospace, monospace\" font-size=\"10\" fill=\"#7c4d0a\" font-weight=\"600\">residual</text>";

    svg.innerHTML = sv;

    // readout = dot product residual · feature_direction
    var rh = "<div style=\"font-family: var(--nn-mono); font-size: 0.7rem; color: var(--nn-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.3rem;\">readout (residual · feature)</div>";
    var maxAbs = 0;
    FEATURES.forEach(function(f){
      var v = vec(f);
      var dot = r.x * v.x + r.y * v.y;
      if (Math.abs(dot) > maxAbs) maxAbs = Math.abs(dot);
    });
    if (maxAbs < 0.01) maxAbs = 1;
    FEATURES.forEach(function(f){
      var v = vec(f);
      var dot = r.x * v.x + r.y * v.y;
      var pct = Math.min(100, Math.abs(dot) / maxAbs * 100);
      var cls = dot < 0 ? "is-neg" : "";
      rh += "<div class=\"sup-readout__row\">"+
        "<div class=\"sup-readout__lbl\">"+f.name+"</div>"+
        "<div class=\"sup-readout__bar\"><div class=\"sup-readout__fill "+cls+"\" style=\"width:"+pct.toFixed(1)+"%\"></div></div>"+
        "<div class=\"sup-readout__num\">"+dot.toFixed(2)+"</div>"+
        "</div>";
    });
    readout.innerHTML = rh;
  }

  renderToggles();
  render();
})();
</script>

<h2 id="direct-logit-attribution-for-mlps">Direct logit attribution for MLPs</h2>

<p>Because each neuron’s contribution to the residual stream is $\sigma(k_n^\top x) v_n$, its contribution to the final logit of token $w$ is:</p>

\[\Delta\text{logit}_n(w) = \sigma(k_n^\top x)\, v_n^\top W_U[:, w]\]

<table>
  <tbody>
    <tr>
      <td>Sort neurons by $</td>
      <td>\Delta\text{logit}_n(w)</td>
      <td>$ to identify which neurons drove the prediction. This is <strong>MLP-level DLA</strong>.</td>
    </tr>
  </tbody>
</table>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in TransformerLens
</span><span class="n">mlp_act</span> <span class="o">=</span> <span class="n">cache</span><span class="p">[</span><span class="s">"post"</span><span class="p">,</span> <span class="n">layer</span><span class="p">,</span> <span class="s">"mlp"</span><span class="p">]</span>      <span class="c1"># [seq, d_ffn]
</span><span class="n">W_out</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">W_out</span><span class="p">[</span><span class="n">layer</span><span class="p">]</span>                 <span class="c1"># [d_ffn, d_model]
</span><span class="n">W_U</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">W_U</span><span class="p">[:,</span> <span class="n">answer_id</span><span class="p">]</span>              <span class="c1"># [d_model]
</span><span class="n">neuron_dla</span> <span class="o">=</span> <span class="n">mlp_act</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="n">W_out</span> <span class="o">@</span> <span class="n">W_U</span><span class="p">)</span>   <span class="c1"># [d_ffn]
</span><span class="n">top_neurons</span> <span class="o">=</span> <span class="n">neuron_dla</span><span class="p">.</span><span class="n">argsort</span><span class="p">(</span><span class="n">descending</span><span class="o">=</span><span class="bp">True</span><span class="p">)[:</span><span class="mi">10</span><span class="p">]</span>
</code></pre></div></div>

<h2 id="why-mlps-hold-the-knowledge">Why MLPs hold the knowledge</h2>

<p>Three lines of evidence support the claim that factual knowledge lives in MLPs:</p>

<ol>
  <li><strong>Parameter share.</strong> MLPs are ~⅔ of total parameters. Most learned content is statistically there.</li>
  <li><strong>Editing.</strong> ROME and <a href="https://memit.baulab.info/">MEMIT</a> edit specific facts by modifying MLP weights at specific layers (typically mid-layers, around layer 5–8 in GPT-2 medium). Editing attention weights does not produce the same effect.</li>
  <li><strong>Causal tracing.</strong> <a href="https://arxiv.org/abs/2202.05262">Meng et al. (2022)</a> corrupt subject tokens, then restore individual layers’ activations one at a time and measure which restoration recovers the correct prediction. The signal localizes to mid-layer MLPs.</li>
</ol>

<p>A clean operational summary: <strong>attention moves information; MLPs add new information.</strong> Both contribute additively to the residual stream. Their roles are complementary.</p>

<div class="idemo idemo--mini" id="demo-rome">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Edit a fact in the model (ROME-style)</span></div>
    <div class="idemo__body">

      <p class="rome-lead">Pick a fact. Pick the new answer you want the model to insist on. Drag the edit slider from 0 to 1 to apply a rank-one update to one MLP layer's weights, simulating what the ROME paper does. Watch the prediction shift, and watch a related question to see if your edit generalizes correctly.</p>

      <div class="rome-pick">
        <div class="rome-pick__label">fact:</div>
        <div class="rome-pick__row">
          <span class="rome-prompt" data-rome-prompt="">The Eiffel Tower is in</span>
          <select class="rome-select" data-rome-fact="">
            <option value="eiffel">The Eiffel Tower is in ___</option>
            <option value="capital">The capital of France is ___</option>
            <option value="lisa">The Mona Lisa was painted by ___</option>
            <option value="space">Neil Armstrong walked on ___</option>
          </select>
        </div>
        <div class="rome-pick__label">new answer:</div>
        <select class="rome-select" data-rome-target=""></select>
      </div>

      <div class="rome-stage">
        <div class="rome-result">
          <div class="rome-result__row">
            <span class="rome-result__name" data-rome-truth="">Paris</span>
            <div class="rome-result__bar"><div class="rome-result__fill rome-result__fill--truth" data-rome-truth-bar=""></div></div>
            <span class="rome-result__num" data-rome-truth-num="">91%</span>
          </div>
          <div class="rome-result__row">
            <span class="rome-result__name" data-rome-target-name="">Rome</span>
            <div class="rome-result__bar"><div class="rome-result__fill rome-result__fill--new" data-rome-new-bar=""></div></div>
            <span class="rome-result__num" data-rome-new-num="">0%</span>
          </div>
        </div>

        <div class="rome-related">
          <div class="rome-related__lbl">consistency check</div>
          <div class="rome-related__q" data-rome-related-q=""></div>
          <div class="rome-related__a" data-rome-related-a=""></div>
        </div>
      </div>

      <div class="rome-controls">
        <label class="rome-label">edit strength: <strong data-rome-tval="">0.00</strong></label>
        <input type="range" min="0" max="100" value="0" step="1" class="rome-slider" data-rome-strength="" />
      </div>

      <p class="rome-hint"><strong>What ROME actually does:</strong> the model encodes facts as (key, value) pairs in mid-layer MLPs. ROME locates the layer where the subject's representation peaks, then computes a rank-one update to <code>W_out</code> that swaps the value while preserving the key. The edit propagates to paraphrases of the same fact (the consistency check) without destroying unrelated knowledge. Numbers shown are illustrative; real ROME edits typically achieve 95%+ post-edit probability.</p>
    </div>
  </div>
</div>

<style>
  #demo-rome .rome-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-rome .rome-pick {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.7rem 0.85rem; margin-bottom: 0.55rem;
    display: flex; flex-direction: column; gap: 0.4rem;
  }
  #demo-rome .rome-pick__label {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
  }
  #demo-rome .rome-pick__row {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  }
  #demo-rome .rome-prompt {
    font-family: var(--nn-mono); font-size: 0.84rem; color: var(--nn-ink);
  }
  #demo-rome .rome-select {
    font-family: var(--nn-mono); font-size: 0.78rem;
    padding: 0.32rem 0.5rem; border: 1px solid var(--nn-line); border-radius: 3px;
    background: #fff; color: var(--nn-ink);
  }
  #demo-rome .rome-stage {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem;
    margin-bottom: 0.85rem;
  }
  #demo-rome .rome-result {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.7rem 0.85rem;
    display: flex; flex-direction: column; gap: 0.5rem;
  }
  #demo-rome .rome-result__row {
    display: grid; grid-template-columns: 90px 1fr 50px;
    align-items: center; gap: 0.55rem;
  }
  #demo-rome .rome-result__name {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-ink);
  }
  #demo-rome .rome-result__bar {
    height: 14px; background: #f5f1e8; border: 1px solid var(--nn-line); border-radius: 2px; overflow: hidden;
  }
  #demo-rome .rome-result__fill { height: 100%; transition: width 280ms cubic-bezier(.3,.5,.3,1); }
  #demo-rome .rome-result__fill--truth { background: #ddb88e; }
  #demo-rome .rome-result__fill--new { background: #b77214; }
  #demo-rome .rome-result__num {
    font-family: var(--nn-mono); font-size: 0.74rem; text-align: right; color: var(--nn-muted);
  }

  #demo-rome .rome-related {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.7rem 0.85rem;
    display: flex; flex-direction: column; gap: 0.3rem;
  }
  #demo-rome .rome-related__lbl {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
  }
  #demo-rome .rome-related__q {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-ink);
  }
  #demo-rome .rome-related__a {
    font-family: var(--nn-mono); font-size: 0.78rem; color: #7c4d0a; font-weight: 600;
    transition: color 280ms;
  }
  #demo-rome .rome-controls {
    display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
    margin-bottom: 0.65rem;
  }
  #demo-rome .rome-label { font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted); }
  #demo-rome .rome-label strong { color: var(--nn-ink); }
  #demo-rome .rome-slider { flex: 1; min-width: 140px; accent-color: #b77214; }

  #demo-rome .rome-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-rome .rome-hint strong { color: #7c4d0a; }
  #demo-rome .rome-hint code {
    font-family: var(--nn-mono); font-size: 0.84em;
    background: #f5f1e8; padding: 1px 6px; border-radius: 3px;
  }
  @media (max-width: 620px){
    #demo-rome .rome-stage { grid-template-columns: 1fr; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-rome"); if (!root) return;

  var FACTS = {
    eiffel: {
      prompt: "The Eiffel Tower is in",
      truth: "Paris",
      options: ["Rome", "London", "Berlin", "Tokyo"],
      relatedQ: "What language do you hear near the Eiffel Tower?",
      relatedA: { Paris: "French", Rome: "Italian", London: "English", Berlin: "German", Tokyo: "Japanese" }
    },
    capital: {
      prompt: "The capital of France is",
      truth: "Paris",
      options: ["Madrid", "Berlin", "Rome", "Lyon"],
      relatedQ: "Tourists in the capital of France ride...",
      relatedA: { Paris: "the Métro", Madrid: "the Madrid Metro", Berlin: "the U-Bahn", Rome: "the Roma metro", Lyon: "the Lyon metro" }
    },
    lisa: {
      prompt: "The Mona Lisa was painted by",
      truth: "da Vinci",
      options: ["Picasso", "Van Gogh", "Monet", "Rembrandt"],
      relatedQ: "The Mona Lisa was painted in the style of...",
      relatedA: { "da Vinci": "the Italian Renaissance", Picasso: "Cubism", "Van Gogh": "Post-Impressionism", Monet: "Impressionism", Rembrandt: "the Dutch Golden Age" }
    },
    space: {
      prompt: "Neil Armstrong walked on",
      truth: "the Moon",
      options: ["Mars", "Venus", "Jupiter", "Saturn"],
      relatedQ: "The first object Armstrong stepped onto was made of...",
      relatedA: { "the Moon": "lunar regolith", Mars: "Martian dust", Venus: "Venusian rock", Jupiter: "gas (impossible)", Saturn: "gas (impossible)" }
    }
  };

  var promptEl = root.querySelector("[data-rome-prompt]");
  var factSel  = root.querySelector("[data-rome-fact]");
  var targetSel= root.querySelector("[data-rome-target]");
  var truthEl  = root.querySelector("[data-rome-truth]");
  var truthNum = root.querySelector("[data-rome-truth-num]");
  var truthBar = root.querySelector("[data-rome-truth-bar]");
  var newBar   = root.querySelector("[data-rome-new-bar]");
  var newNum   = root.querySelector("[data-rome-new-num]");
  var newName  = root.querySelector("[data-rome-target-name]");
  var relatedQ = root.querySelector("[data-rome-related-q]");
  var relatedA = root.querySelector("[data-rome-related-a]");
  var slider   = root.querySelector("[data-rome-strength]");
  var tval     = root.querySelector("[data-rome-tval]");

  function populateTargets(){
    var f = FACTS[factSel.value];
    promptEl.textContent = f.prompt;
    truthEl.textContent = f.truth;
    targetSel.innerHTML = f.options.map(function(o){ return "<option value=\""+o+"\">"+o+"</option>"; }).join("");
    render();
  }

  function render(){
    var f = FACTS[factSel.value];
    var target = targetSel.value;
    var t = parseInt(slider.value, 10) / 100;
    tval.textContent = t.toFixed(2);

    var pTruth = 0.91 * (1 - t) + 0.04 * t;
    var pNew   = 0.01 * (1 - t) + 0.93 * t;

    truthEl.textContent = f.truth;
    truthNum.textContent = (pTruth * 100).toFixed(0) + "%";
    truthBar.style.width = (pTruth * 100) + "%";

    newName.textContent = target;
    newNum.textContent = (pNew * 100).toFixed(0) + "%";
    newBar.style.width = (pNew * 100) + "%";

    relatedQ.textContent = f.relatedQ;
    var ans = (t > 0.5) ? f.relatedA[target] : f.relatedA[f.truth];
    relatedA.textContent = ans || "—";
    // Tint the related answer amber while edit is partial
    var alpha = Math.min(1, Math.max(0.4, 0.4 + Math.abs(t - 0.5) * 1.2));
    relatedA.style.color = "rgba(124, 77, 10, "+alpha.toFixed(2)+")";
  }

  factSel.addEventListener("change", populateTargets);
  targetSel.addEventListener("change", render);
  slider.addEventListener("input", render);

  populateTargets();
})();
</script>

<h2 id="activation-functions-in-modern-models">Activation functions in modern models</h2>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Non-linearity</th>
      <th>Form</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPT-2 / GPT-3</td>
      <td>GeLU</td>
      <td>$x \cdot \Phi(x)$</td>
    </tr>
    <tr>
      <td>Original Transformer</td>
      <td>ReLU</td>
      <td>$\max(0, x)$</td>
    </tr>
    <tr>
      <td>PaLM, Llama, Mistral</td>
      <td>SwiGLU</td>
      <td>$\text{Swish}(W_g x) \odot (W_\text{in} x)$</td>
    </tr>
  </tbody>
</table>

<p>SwiGLU adds a gating branch:</p>

\[\text{MLP}_\text{SwiGLU}(x) = W_\text{out}\, (\text{Swish}(W_g x) \odot W_\text{in} x)\]

<p>This requires three matrices instead of two, but the key-value interpretation extends: each neuron’s “key” is now a (gate, input) pair, and the value is still the corresponding $W_\text{out}$ column. Most MLP interpretability tooling generalizes with minor modification.</p>

<h2 id="what-we-have-so-far">What we have so far</h2>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Role</th>
      <th>Reads</th>
      <th>Writes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Embedding</td>
      <td>Token → vector</td>
      <td>token IDs</td>
      <td>residual stream</td>
    </tr>
    <tr>
      <td>Attention</td>
      <td>Cross-position routing</td>
      <td>residual stream (all positions)</td>
      <td>residual stream (current position)</td>
    </tr>
    <tr>
      <td>MLP</td>
      <td>Stored knowledge / transforms</td>
      <td>residual stream (current position)</td>
      <td>residual stream (current position)</td>
    </tr>
    <tr>
      <td>Unembedding</td>
      <td>Vector → logits</td>
      <td>residual stream (last position)</td>
      <td>output distribution</td>
    </tr>
  </tbody>
</table>

<p>All four components communicate exclusively via the residual stream. Every interpretability tool in this series operates on that interface.</p>

<p>The next post runs a full forward pass through GPT-2 small, end-to-end, with concrete numbers at every stage.</p>

<h2 id="resources">Resources</h2>

<h3 id="foundational-papers">Foundational papers</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/2012.14913" target="_blank" rel="noopener"><div class="research-card__title">Transformer Feed-Forward Layers Are Key-Value Memories</div><div class="research-card__authors">Geva et al., 2021 · the key-value framing</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2202.05262" target="_blank" rel="noopener"><div class="research-card__title">Locating and Editing Factual Associations in GPT (ROME)</div><div class="research-card__authors">Meng et al., 2022 · causal tracing + rank-one MLP edits</div></a></li>
  <li><a class="research-card" href="https://memit.baulab.info/" target="_blank" rel="noopener"><div class="research-card__title">Mass-Editing Memory in a Transformer (MEMIT)</div><div class="research-card__authors">Meng et al., 2023 · scaling ROME to thousands of edits</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2022/toy_model/index.html" target="_blank" rel="noopener"><div class="research-card__title">Toy Models of Superposition</div><div class="research-card__authors">Elhage et al., Anthropic 2022 · why polysemanticity is rational</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2023/monosemantic-features/index.html" target="_blank" rel="noopener"><div class="research-card__title">Towards Monosemanticity</div><div class="research-card__authors">Bricken et al., Anthropic 2023 · SAEs on a 1-layer transformer</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html" target="_blank" rel="noopener"><div class="research-card__title">Scaling Monosemanticity</div><div class="research-card__authors">Templeton et al., Anthropic 2024 · SAEs on Claude 3 Sonnet, millions of features</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2002.05202" target="_blank" rel="noopener"><div class="research-card__title">GLU Variants Improve Transformer</div><div class="research-card__authors">Shazeer, 2020 · why SwiGLU replaced GeLU in modern LLMs</div></a></li>
</ul>

<h3 id="tools-and-code">Tools and code</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://neuronpedia.org/" target="_blank" rel="noopener"><div class="research-card__title">Neuronpedia</div><div class="research-card__authors">browse top-activating contexts for MLP neurons and SAE features across many models</div></a></li>
  <li><a class="research-card" href="https://github.com/jbloomAus/SAELens" target="_blank" rel="noopener"><div class="research-card__title">SAELens</div><div class="research-card__authors">train and analyze sparse autoencoders on any HF transformer</div></a></li>
  <li><a class="research-card" href="https://rome.baulab.info/" target="_blank" rel="noopener"><div class="research-card__title">ROME · code &amp; demo</div><div class="research-card__authors">Bau Lab · reproduce factual editing in GPT-2 / GPT-J</div></a></li>
  <li><a class="research-card" href="https://transformerlensorg.github.io/TransformerLens/generated/demos/Main_Demo.html#MLP-Layers" target="_blank" rel="noopener"><div class="research-card__title">TransformerLens · MLP analysis</div><div class="research-card__authors">cache MLP activations, decompose neuron contributions to logits</div></a></li>
  <li><a class="research-card" href="https://distill.pub/2020/circuits/zoom-in/" target="_blank" rel="noopener"><div class="research-card__title">Zoom In: An Introduction to Circuits</div><div class="research-card__authors">Olah et al., Distill 2020 · the original feature-and-circuit framing (vision)</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[MLPs hold ~⅔ of a transformer's parameters and act as key-value memories: each neuron is a learned (key, value) pair that adds to the residual stream when the key matches. Where most factual knowledge lives.]]></summary></entry><entry><title type="html">Attention: How Every Position Decides Who to Listen To</title><link href="https://bhavith-chandra.github.io/posts/attention-how-every-position-decides-who-to-listen-to/" rel="alternate" type="text/html" title="Attention: How Every Position Decides Who to Listen To" /><published>2026-04-06T00:00:00-07:00</published><updated>2026-04-06T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/attention-how-every-position-decides-who-to-listen-to</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/attention-how-every-position-decides-who-to-listen-to/"><![CDATA[<p><strong>Self-attention</strong> is the mechanism that lets each position in a sequence read from every other (causally) position. A single attention head consists of three learned linear maps and a softmax. A multi-head layer runs $n_\text{heads}$ such heads in parallel.</p>

<p>This post defines the operation, derives the <strong>QK / OV decomposition</strong> that underlies head-level interpretability, and walks through four head archetypes plus the indirect-object identification circuit.</p>

<hr />

<h2 id="demo-72-real-attention-heads">Demo: 72 real attention heads</h2>

<div class="idemo" id="demo-attn">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · Attention head explorer</span></div>
    <div class="idemo__body">

      <p class="att-lead">Every attention head asks a single question: <em>"for each position, which other positions should I look at?"</em> Different heads learn wildly different rules. Pick a head from the gallery and watch what it actually pays attention to.</p>

      <div class="att-prompt-row">
        <span class="att-prompt-row__label">Prompt</span>
        <div class="att-prompt-row__chips" data-att-prompts="">
          <button class="att-chip is-active" data-att-prompt="ioi">When John and Mary went to the store, John gave a drink to</button>
          <button class="att-chip" data-att-prompt="abab">A B A B A B A</button>
          <button class="att-chip" data-att-prompt="paris">Paris is the capital of France .</button>
        </div>
      </div>

      <div class="att-gallery" data-att-gallery="">
        <button class="att-head is-active" data-att-head="prev">
          <span class="att-head__addr">L1 · H4</span>
          <span class="att-head__name">Previous-token head</span>
          <span class="att-head__hint">Each token attends to the one right before it.</span>
        </button>
        <button class="att-head" data-att-head="bos">
          <span class="att-head__addr">L0 · H3</span>
          <span class="att-head__name">BOS-sink head</span>
          <span class="att-head__hint">"I have nothing to say." Dumps attention on the first token.</span>
        </button>
        <button class="att-head" data-att-head="dup">
          <span class="att-head__addr">L2 · H7</span>
          <span class="att-head__name">Duplicate-token head</span>
          <span class="att-head__hint">Notices when a word repeats earlier in the sequence.</span>
        </button>
        <button class="att-head" data-att-head="ind">
          <span class="att-head__addr">L4 · H10</span>
          <span class="att-head__name">Induction head</span>
          <span class="att-head__hint">"I saw A B before. Now I see A. Look at B." The pattern-completion machine.</span>
        </button>
        <button class="att-head" data-att-head="name">
          <span class="att-head__addr">L5 · H6</span>
          <span class="att-head__name">Name-mover head</span>
          <span class="att-head__hint">Copies a name token from earlier in the sequence into the current position.</span>
        </button>
        <button class="att-head" data-att-head="self">
          <span class="att-head__addr">L3 · H1</span>
          <span class="att-head__name">Self-attending head</span>
          <span class="att-head__hint">Mostly attends to its own position. A null op, or close to it.</span>
        </button>
      </div>

      <div class="att-grid">
        <div class="att-grid__matrix">
          <div class="att-panel-label">Attention matrix · row = query position · column = key position · darker = stronger</div>
          <svg class="att-svg" data-att-svg="" viewBox="0 0 380 380" preserveAspectRatio="xMidYMid meet"></svg>
        </div>
        <div class="att-grid__flow">
          <div class="att-panel-label">Flow view · pick a query (row), see where it looks</div>
          <div class="att-flow" data-att-flow=""></div>
          <div class="att-diag">
            <span class="att-diag__label">Pattern</span>
            <span class="att-diag__value" data-att-diag="">previous-token</span>
          </div>
        </div>
      </div>

      <details>
        <summary>What "head" means and why we care</summary>
        <p>Each transformer block has multiple parallel attention heads (12 in distilGPT2, 96+ in larger models). Each head computes its own query/key/value matrices and produces its own attention pattern. The block sums all heads' contributions back into the residual stream. So the model has hundreds or thousands of <em>tiny independent attention circuits</em>, and a huge amount of MI work is just figuring out what each one does. The patterns shown here are textbook archetypes, observed in real GPT-2 small. See <a href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener">Elhage et al. 2021</a> and <a href="https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html" target="_blank" rel="noopener">Olsson et al. 2022</a>.</p>
      </details>
    </div>
  </div>
</div>

<style>
  #demo-attn .att-lead { margin: 0 0 1.05rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-attn .att-lead em { color: #7c4d0a; font-style: italic; }

  #demo-attn .att-prompt-row { display: flex; gap: 0.6rem; margin-bottom: 0.9rem; flex-wrap: wrap; align-items: center; }
  #demo-attn .att-prompt-row__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-attn .att-prompt-row__chips { display: flex; gap: 0.35rem; flex-wrap: wrap; flex: 1; }
  #demo-attn .att-chip {
    padding: 0.3rem 0.65rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-attn .att-chip:hover { border-color: #b77214; }
  #demo-attn .att-chip.is-active { background: rgba(251,191,36,0.18); border-color: #b77214; color: #7c4d0a; }

  #demo-attn .att-gallery {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 0.55rem; margin-bottom: 1rem;
  }
  #demo-attn .att-head {
    text-align: left; cursor: pointer;
    padding: 0.65rem 0.75rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 4px; color: var(--nn-ink);
    display: flex; flex-direction: column; gap: 0.15rem;
    transition: border-color 120ms, background 120ms;
  }
  #demo-attn .att-head:hover { border-color: #b77214; }
  #demo-attn .att-head.is-active { background: #fffaef; border-color: #b77214; }
  #demo-attn .att-head__addr {
    font-family: var(--nn-mono); font-size: 0.66rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #b77214;
  }
  #demo-attn .att-head__name { font-size: 0.92rem; font-weight: 600; color: var(--nn-ink); }
  #demo-attn .att-head__hint { font-size: 0.78rem; color: var(--nn-muted); line-height: 1.4; }

  #demo-attn .att-grid {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
    gap: 0.9rem; margin-bottom: 0.9rem;
  }
  @media (max-width: 720px) { #demo-attn .att-grid { grid-template-columns: 1fr; } }

  #demo-attn .att-panel-label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.4rem;
  }
  #demo-attn .att-grid__matrix, #demo-attn .att-grid__flow {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.7rem 0.8rem;
  }
  #demo-attn .att-svg { display: block; width: 100%; height: auto; }
  #demo-attn .att-svg .cell { stroke: rgba(255,255,255,0.6); stroke-width: 0.5; cursor: pointer; transition: stroke 100ms; }
  #demo-attn .att-svg .cell:hover { stroke: #b77214; stroke-width: 2; }
  #demo-attn .att-svg .cell.is-active { stroke: #b77214; stroke-width: 2.5; }
  #demo-attn .att-svg .label-row, #demo-attn .att-svg .label-col {
    font-family: var(--nn-mono); font-size: 9px; fill: var(--nn-muted);
  }
  #demo-attn .att-svg .label-col { text-anchor: middle; }
  #demo-attn .att-svg .label-row { text-anchor: end; }

  #demo-attn .att-flow {
    display: flex; flex-direction: column; gap: 0.4rem; min-height: 200px;
  }
  #demo-attn .att-flow__row {
    display: flex; align-items: center; gap: 0.45rem;
    padding: 0.32rem 0.5rem; border-radius: 3px; transition: background 120ms;
    cursor: pointer;
  }
  #demo-attn .att-flow__row:hover { background: #fffaef; }
  #demo-attn .att-flow__row.is-active { background: #fff6e0; box-shadow: inset 0 0 0 1px #b77214; }
  #demo-attn .att-flow__qbadge {
    font-family: var(--nn-mono); font-size: 0.7rem; padding: 0.1rem 0.4rem;
    background: rgba(251,191,36,0.18); color: #7c4d0a; border-radius: 3px;
    min-width: 32px; text-align: center;
  }
  #demo-attn .att-flow__tok {
    font-family: var(--nn-mono); font-size: 0.84rem; min-width: 70px; color: var(--nn-ink);
  }
  #demo-attn .att-flow__bar { flex: 1; display: flex; gap: 1px; height: 16px; }
  #demo-attn .att-flow__seg { height: 100%; transition: opacity 120ms; }

  #demo-attn .att-diag {
    margin-top: 0.6rem; padding-top: 0.55rem; border-top: 1px dashed var(--nn-line);
    display: flex; gap: 0.5rem; align-items: baseline;
  }
  #demo-attn .att-diag__label {
    font-family: var(--nn-mono); font-size: 0.68rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-attn .att-diag__value {
    font-family: var(--nn-mono); font-size: 0.88rem; color: #b77214; font-weight: 600;
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-attn"); if (!root) return;

  var PROMPTS = {
    ioi: ["When"," John"," and"," Mary"," went"," to"," the"," store",","," John"," gave"," a"," drink"," to"],
    abab: ["A"," B"," A"," B"," A"," B"," A"],
    paris: ["Paris"," is"," the"," capital"," of"," France"," ."]
  };

  function softmaxRow(row){
    var max = -Infinity, i;
    for (i=0; i<row.length; i++) if (row[i] > max) max = row[i];
    var s = 0;
    for (i=0; i<row.length; i++){ row[i] = Math.exp(row[i] - max); s += row[i]; }
    for (i=0; i<row.length; i++) row[i] /= s;
    return row;
  }
  function makeMatrix(n, scoreFn){
    var M = [];
    for (var q=0; q<n; q++){
      var row = new Array(n).fill(-1e9);
      for (var k=0; k<=q; k++) row[k] = scoreFn(q, k);
      M.push(softmaxRow(row));
    }
    return M;
  }

  var PATTERNS = {
    prev: function(tokens){
      return makeMatrix(tokens.length, function(q,k){
        if (q === 0) return 0;
        if (k === q-1) return 4.5;
        if (k === q)   return 0.5;
        return -1.5 + (k/tokens.length)*0.3;
      });
    },
    bos: function(tokens){
      return makeMatrix(tokens.length, function(q,k){
        if (k === 0) return 4.0;
        if (k === q) return 0.5;
        return -1 - (q-k)*0.05;
      });
    },
    self: function(tokens){
      return makeMatrix(tokens.length, function(q,k){
        if (k === q) return 4.0;
        return -2;
      });
    },
    dup: function(tokens){
      return makeMatrix(tokens.length, function(q,k){
        if (k === q) return 0.2;
        if (tokens[k] === tokens[q] && k < q) return 4.0;
        return -1.5;
      });
    },
    ind: function(tokens){
      return makeMatrix(tokens.length, function(q,k){
        if (q === 0 || k === 0) return k === q ? 0.5 : -2;
        if (tokens[k-1] === tokens[q-1] && k < q) return 4.5;
        if (k === q) return 0.3;
        return -2;
      });
    },
    name: function(tokens){
      function isName(t){ var s = t.trim(); return s.length > 0 && /[A-Z]/.test(s[0]); }
      return makeMatrix(tokens.length, function(q,k){
        if (k === q) return 0.4;
        if (isName(tokens[k]) && k < q && k > 0) return 4.0 - (q-k)*0.05;
        if (k === 0) return 0.6;
        return -1.5;
      });
    }
  };

  var DIAGNOSES = {
    prev: "previous-token",
    bos: "BOS-sink (uninformative)",
    self: "self-attending (near no-op)",
    dup: "duplicate-token detector",
    ind: "induction (pattern completion)",
    name: "name-mover (copies earliest name)"
  };

  var svg = root.querySelector("[data-att-svg]");
  var flowEl = root.querySelector("[data-att-flow]");
  var diagEl = root.querySelector("[data-att-diag]");

  var currentPrompt = "ioi";
  var currentHead = "prev";
  var matrix = null;
  var tokens = null;

  function escapeHtml(s){ return String(s).replace(/[&<>"']/g, function(c){ return ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c]; }); }
  function lerpHex(a,b,t){
    var ar=parseInt(a.slice(1,3),16),ag=parseInt(a.slice(3,5),16),ab=parseInt(a.slice(5,7),16);
    var br=parseInt(b.slice(1,3),16),bg=parseInt(b.slice(3,5),16),bb=parseInt(b.slice(5,7),16);
    return "#"+[Math.round(ar+(br-ar)*t),Math.round(ag+(bg-ag)*t),Math.round(ab+(bb-ab)*t)].map(function(n){return n.toString(16).padStart(2,"0");}).join("");
  }
  function heat(p){
    p = Math.min(1, Math.max(0, p));
    if (p < 0.5) return lerpHex("#fff7e6","#fcd68b",p/0.5);
    return lerpHex("#fcd68b","#7c4d0a",(p-0.5)/0.5);
  }

  function renderMatrix(){
    var n = tokens.length;
    var SIZE = 380;
    var pad = 56;
    var cellSize = (SIZE - pad - 8) / n;
    var html = "";
    for (var k=0; k<n; k++){
      var cx = pad + cellSize * k + cellSize / 2;
      html += "<text class=\"label-col\" x=\""+cx+"\" y=\""+(pad-6)+"\" transform=\"rotate(-45 "+cx+" "+(pad-6)+")\">"+escapeHtml(tokens[k].trim() || "·")+"</text>";
    }
    for (var q=0; q<n; q++){
      var cy = pad + cellSize * q + cellSize/2 + 3;
      html += "<text class=\"label-row\" x=\""+(pad-4)+"\" y=\""+cy+"\">"+escapeHtml(tokens[q].trim() || "·")+"</text>";
    }
    for (var qq=0; qq<n; qq++){
      for (var kk=0; kk<=qq; kk++){
        var x = pad + cellSize * kk;
        var y = pad + cellSize * qq;
        var v = matrix[qq][kk];
        html += "<rect class=\"cell\" data-q=\""+qq+"\" data-k=\""+kk+"\" x=\""+x+"\" y=\""+y+"\" width=\""+cellSize+"\" height=\""+cellSize+"\" fill=\""+heat(v)+"\"><title>q="+escapeHtml(tokens[qq])+" k="+escapeHtml(tokens[kk])+" w="+(v*100).toFixed(1)+"%</title></rect>";
      }
    }
    svg.setAttribute("viewBox", "0 0 "+SIZE+" "+SIZE);
    svg.innerHTML = html;
    svg.querySelectorAll(".cell").forEach(function(c){
      c.addEventListener("click", function(){ setActiveQ(parseInt(c.dataset.q)); });
    });
  }

  function renderFlow(){
    var html = "";
    for (var q=0; q<tokens.length; q++){
      var row = matrix[q];
      var segs = "";
      for (var k=0; k<tokens.length; k++){
        var w = (k <= q) ? row[k] : 0;
        var color = w > 0.001 ? heat(w) : "#f5f1e8";
        var op = w > 0.001 ? 1 : 0.4;
        segs += "<span class=\"att-flow__seg\" style=\"flex:1;background:"+color+";opacity:"+op+"\" title=\""+escapeHtml(tokens[k])+" "+(w*100).toFixed(1)+"%\"></span>";
      }
      html += "<div class=\"att-flow__row\" data-q=\""+q+"\"><span class=\"att-flow__qbadge\">q"+q+"</span><span class=\"att-flow__tok\">"+escapeHtml(tokens[q].trim() || "·")+"</span><span class=\"att-flow__bar\">"+segs+"</span></div>";
    }
    flowEl.innerHTML = html;
    flowEl.querySelectorAll(".att-flow__row").forEach(function(r){
      r.addEventListener("click", function(){ setActiveQ(parseInt(r.dataset.q)); });
    });
  }

  function setActiveQ(q){
    svg.querySelectorAll(".cell.is-active").forEach(function(c){ c.classList.remove("is-active"); });
    svg.querySelectorAll(".cell[data-q=\""+q+"\"]").forEach(function(c){ c.classList.add("is-active"); });
    flowEl.querySelectorAll(".att-flow__row").forEach(function(r){
      r.classList.toggle("is-active", parseInt(r.dataset.q) === q);
    });
  }

  function update(){
    tokens = PROMPTS[currentPrompt];
    matrix = PATTERNS[currentHead](tokens);
    renderMatrix();
    renderFlow();
    diagEl.textContent = DIAGNOSES[currentHead];
    setActiveQ(tokens.length - 1);
  }

  root.querySelectorAll("[data-att-prompt]").forEach(function(b){
    b.addEventListener("click", function(){
      root.querySelectorAll("[data-att-prompt]").forEach(function(x){ x.classList.remove("is-active"); });
      b.classList.add("is-active");
      currentPrompt = b.dataset.attPrompt;
      update();
    });
  });
  root.querySelectorAll("[data-att-head]").forEach(function(b){
    b.addEventListener("click", function(){
      root.querySelectorAll("[data-att-head]").forEach(function(x){ x.classList.remove("is-active"); });
      b.classList.add("is-active");
      currentHead = b.dataset.attHead;
      update();
    });
  });

  update();
})();
</script>

<p>3 prompts × 6 routing-pattern reproductions × matrix and flow views. The patterns shown (“previous-token”, “BOS sink”, “induction”, “duplicate-token”, “name-mover”, “self”) are reproductions of the canonical patterns observed in real GPT-2 small heads.</p>

<h2 id="definition">Definition</h2>

<p>Each attention head has three weight matrices:</p>

\[W_Q, W_K, W_V \in \mathbb{R}^{d_\text{model} \times d_\text{head}}\]

<p>Typical sizes: GPT-2 small has $d_\text{model} = 768$, $n_\text{heads} = 12$, $d_\text{head} = 64$ per head ($d_\text{head} = d_\text{model} / n_\text{heads}$).</p>

<p>For input $X \in \mathbb{R}^{T \times d_\text{model}}$:</p>

\[Q = XW_Q,\quad K = XW_K,\quad V = XW_V \quad \in \mathbb{R}^{T \times d_\text{head}}\]

<p><strong>Attention scores</strong> (causal, scaled):</p>

\[A = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_\text{head}}} + M\right)\]

<p>where $M_{ij} = -\infty$ for $j &gt; i$ (causal mask), 0 otherwise. $A \in \mathbb{R}^{T \times T}$ is the attention pattern.</p>

<p><strong>Output:</strong></p>

\[Z = AV \quad \in \mathbb{R}^{T \times d_\text{head}}\]

<p>Multi-head: concatenate $n_\text{heads}$ outputs $[Z^{(1)}, \ldots, Z^{(h)}]$ and project through $W_O \in \mathbb{R}^{(n_\text{heads} \cdot d_\text{head}) \times d_\text{model}}$ to write back to the residual stream.</p>

<p>The scaling factor $\sqrt{d_\text{head}}$ keeps the dot products in a numerically stable range (<a href="https://arxiv.org/abs/1706.03762">Vaswani et al., 2017</a>, §3.2.1).</p>

<h2 id="qk-and-ov-two-circuits-per-head">QK and OV: two circuits per head</h2>

<p>Each head can be analyzed as two independent linear maps composed by softmax + sum.</p>

<h3 id="qk-circuit-where-to-attend">QK circuit (where to attend)</h3>

<p>The attention score is bilinear in the inputs:</p>

\[Q_i K_j^\top = (X_i W_Q)(X_j W_K)^\top = X_i (W_Q W_K^\top) X_j^\top\]

<p>The product $W_{QK} := W_Q W_K^\top \in \mathbb{R}^{d_\text{model} \times d_\text{model}}$ is the <strong>QK matrix</strong>. It maps pairs (query position content, key position content) → score. Eigendecomposing or projecting $W_{QK}$ onto interpretable subspaces reveals the routing rule.</p>

<h3 id="ov-circuit-what-to-write">OV circuit (what to write)</h3>

<p>The output written back to the residual stream from source position $j$, weighted by $A_{ij}$, is:</p>

\[\Delta_i = \sum_j A_{ij}\, X_j W_V W_O^{(h)}\]

<p>The product $W_{OV} := W_V W_O^{(h)} \in \mathbb{R}^{d_\text{model} \times d_\text{model}}$ is the <strong>OV matrix</strong>. It maps (source residual content) → (write contribution). Reading the eigenstructure of $W_{OV}$ describes what kind of information the head copies.</p>

<p><strong>The two are independent.</strong> Routing (QK) and payload (OV) are trained jointly but are mathematically separate objects. Most interpretability claims about a head reduce to characterizing $W_{QK}$ and $W_{OV}$ separately. (<a href="https://transformer-circuits.pub/2021/framework/index.html">Elhage et al., 2021</a>)</p>

<div class="idemo idemo--mini" id="demo-qkov">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · QK / OV, the two halves of an attention head</span></div>
    <div class="idemo__body">

      <p class="qkov-lead">Every attention head does two separate things. <strong>QK</strong> decides where to look; <strong>OV</strong> decides what to copy. Pick a head archetype, then toggle between the two views to see the same head from both angles.</p>

      <div class="qkov-tabs">
        <div class="qkov-tabs__group" data-qkov-heads=""></div>
        <div class="qkov-tabs__group" data-qkov-modes=""></div>
      </div>

      <div class="qkov-stage">
        <div class="qkov-stage__row">
          <div class="qkov-stage__label">tokens</div>
          <div class="qkov-stage__tokens" data-qkov-tokens=""></div>
        </div>
        <div class="qkov-stage__row">
          <div class="qkov-stage__label" data-qkov-rowlabel="">QK · attention pattern</div>
          <div class="qkov-stage__matrix" data-qkov-matrix=""></div>
        </div>
      </div>

      <div class="qkov-explain" data-qkov-explain=""></div>
    </div>
  </div>
</div>

<style>
  #demo-qkov .qkov-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-qkov .qkov-lead strong { color: #7c4d0a; }
  #demo-qkov .qkov-tabs {
    display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.85rem;
    align-items: center;
  }
  #demo-qkov .qkov-tabs__group { display: flex; gap: 0.4rem; flex-wrap: wrap; }
  #demo-qkov .qkov-btn {
    padding: 0.4rem 0.75rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-qkov .qkov-btn:hover { border-color: #b77214; }
  #demo-qkov .qkov-btn.is-active {
    background: #b77214; color: #fff; border-color: #b77214;
  }
  #demo-qkov .qkov-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
    display: flex; flex-direction: column; gap: 0.75rem;
  }
  #demo-qkov .qkov-stage__row { display: grid; grid-template-columns: 110px 1fr; gap: 0.7rem; align-items: start; }
  #demo-qkov .qkov-stage__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--nn-muted); padding-top: 0.25rem;
  }
  #demo-qkov .qkov-stage__tokens {
    display: flex; gap: 4px; flex-wrap: wrap;
  }
  #demo-qkov .qkov-tok {
    font-family: var(--nn-mono); font-size: 0.78rem; padding: 3px 8px;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink);
  }
  #demo-qkov .qkov-stage__matrix {
    display: grid; gap: 2px; max-width: 360px;
  }
  #demo-qkov .qkov-cell {
    aspect-ratio: 1 / 1; border-radius: 2px;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--nn-mono); font-size: 0.6rem; color: rgba(124,77,10,0.55);
    transition: background 220ms;
  }
  #demo-qkov .qkov-cell.is-mask {
    background: #f0ece3; color: rgba(0,0,0,0.15);
  }
  #demo-qkov .qkov-explain {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-qkov .qkov-explain strong { color: #7c4d0a; }
  #demo-qkov .qkov-explain code {
    font-family: var(--nn-mono); font-size: 0.84em;
    background: #f5f1e8; padding: 1px 6px; border-radius: 3px;
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-qkov"); if (!root) return;

  var TOKENS = ["When","John","and","Mary","went","to","the","store",",","John","gave","a","drink","to"];
  var T = TOKENS.length;

  function tri(scoreFn){
    var rows = [];
    for (var i = 0; i < T; i++){
      var row = [], sum = 0;
      for (var j = 0; j < T; j++){
        if (j > i){ row.push(null); continue; }
        var s = Math.exp(scoreFn(i, j));
        row.push(s); sum += s;
      }
      for (var j2 = 0; j2 < T; j2++){
        if (row[j2] !== null) row[j2] = row[j2] / sum;
      }
      rows.push(row);
    }
    return rows;
  }

  function ov(payloadFn){
    // OV view: at each (i, j) cell, show "amount of source-j content that gets written to position i".
    // We display = attention weight * payload_strength(j).
    var rows = [];
    for (var i = 0; i < T; i++){
      var row = [];
      for (var j = 0; j < T; j++){
        if (j > i) row.push(null);
        else row.push(payloadFn(i, j));
      }
      rows.push(row);
    }
    return rows;
  }

  var HEADS = {
    prev: {
      name: "previous-token head",
      qk: tri(function(i, j){ return j === i - 1 ? 4 : 0; }),
      ov: ov(function(i, j){ return j === i - 1 ? 1 : 0; }),
      explain: "<strong>QK</strong>: position <em>i</em> attends almost entirely to <em>i, 1</em>. The dot product fires when the key encodes \"position one before query.\" <strong>OV</strong>: copies the source token's embedding into the destination, building a \"what came right before?\" feature for downstream heads."
    },
    bos: {
      name: "BOS-sink head",
      qk: tri(function(i, j){ return j === 0 ? 3 : 0; }),
      ov: ov(function(i, j){ return j === 0 ? 1 : 0; }),
      explain: "<strong>QK</strong>: most queries match the key at position 0 (the BOS slot). <strong>OV</strong>: the source content at BOS is near-constant, so the head writes near, nothing. This is a head \"resting\" because softmax forces it to attend somewhere even when no content key matches."
    },
    induction: {
      name: "induction head",
      qk: (function(){
        return tri(function(i, j){
          // For each i where TOKENS[i] previously appeared, attend to (prev_occurrence + 1).
          for (var k = 0; k < i; k++){
            if (TOKENS[k] === TOKENS[i] && j === k + 1) return 4;
          }
          return 0;
        });
      })(),
      ov: (function(){
        return ov(function(i, j){
          for (var k = 0; k < i; k++){
            if (TOKENS[k] === TOKENS[i] && j === k + 1) return 1;
          }
          return 0;
        });
      })(),
      explain: "<strong>QK</strong>: when the current token equals an earlier token <code>A</code>, attend to whatever came right after that earlier <code>A</code>. <strong>OV</strong>: copy that next-token forward. Result: in-context bigram completion. This is the head that lets the model continue patterns it has never been trained on."
    },
    namemover: {
      name: "name-mover head",
      qk: tri(function(i, j){
        if (i !== T - 1) return 0;
        if (TOKENS[j] === "John") return 3.5;
        if (TOKENS[j] === "Mary") return 4;
        return 0;
      }),
      ov: ov(function(i, j){
        if (i !== T - 1) return 0;
        if (TOKENS[j] === "John") return 0.6;
        if (TOKENS[j] === "Mary") return 1;
        return 0;
      }),
      explain: "<strong>QK</strong>: the final position attends to all earlier name tokens. <strong>OV</strong>: writes those name embeddings into the final residual, raising their logits. Because earlier S-inhibition heads have suppressed the John direction, the OV write toward Mary dominates and the model predicts <code>Mary</code>. This is the output stage of the IOI circuit."
    }
  };

  var headsEl = root.querySelector("[data-qkov-heads]");
  var modesEl = root.querySelector("[data-qkov-modes]");
  var tokensEl= root.querySelector("[data-qkov-tokens]");
  var matrixEl= root.querySelector("[data-qkov-matrix]");
  var rowLabel= root.querySelector("[data-qkov-rowlabel]");
  var explainEl= root.querySelector("[data-qkov-explain]");

  var head = "prev";
  var mode = "qk";

  function renderTabs(){
    var hh = "";
    Object.keys(HEADS).forEach(function(k){
      hh += "<button class=\"qkov-btn"+(k === head ? " is-active" : "")+"\" data-h=\""+k+"\">"+HEADS[k].name+"</button>";
    });
    headsEl.innerHTML = hh;
    headsEl.querySelectorAll(".qkov-btn").forEach(function(b){
      b.addEventListener("click", function(){ head = b.getAttribute("data-h"); renderTabs(); render(); });
    });

    var mh = "";
    [["qk","QK · where to look"], ["ov","OV · what to copy"]].forEach(function(p){
      mh += "<button class=\"qkov-btn"+(p[0] === mode ? " is-active" : "")+"\" data-m=\""+p[0]+"\">"+p[1]+"</button>";
    });
    modesEl.innerHTML = mh;
    modesEl.querySelectorAll(".qkov-btn").forEach(function(b){
      b.addEventListener("click", function(){ mode = b.getAttribute("data-m"); renderTabs(); render(); });
    });
  }

  function render(){
    var th = "";
    TOKENS.forEach(function(t){ th += "<span class=\"qkov-tok\">"+t.replace(/</g,"&lt;")+"</span>"; });
    tokensEl.innerHTML = th;

    var data = HEADS[head][mode];
    rowLabel.textContent = mode === "qk" ? "QK · attention pattern" : "OV · written content";
    matrixEl.style.gridTemplateColumns = "repeat("+T+", 1fr)";

    var html = "";
    for (var i = 0; i < T; i++){
      for (var j = 0; j < T; j++){
        var v = data[i][j];
        if (v === null){
          html += "<div class=\"qkov-cell is-mask\"></div>";
          continue;
        }
        var alpha = Math.min(1, v * 1.0);
        var bg = "rgba(183, 114, 20, "+alpha.toFixed(3)+")";
        html += "<div class=\"qkov-cell\" style=\"background:"+bg+"\"></div>";
      }
    }
    matrixEl.innerHTML = html;

    explainEl.innerHTML = HEADS[head].explain;
  }

  renderTabs();
  render();
})();
</script>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>"What does this head do?" decomposes into two questions: "What does QK select for?" and "What does OV copy?" Almost every head archetype in the literature (induction, copy, name-mover, S-inhibition) is named after its OV behavior with a description of QK as the routing condition.</p>
</aside>

<h2 id="four-head-archetypes">Four head archetypes</h2>

<h3 id="1-previous-token-heads">1. Previous-token heads</h3>

<ul>
  <li><strong>QK</strong>: position $i$ attends primarily to $i-1$. Often pure positional (the QK matrix is approximately a shift operator after positional encoding).</li>
  <li><strong>OV</strong>: copies the source token’s embedding into the destination.</li>
  <li><strong>Where</strong>: layer 0–2 in GPT-2 small.</li>
  <li><strong>Use</strong>: feeds shifted-token information into later heads. A prerequisite for induction.</li>
</ul>

<h3 id="2-induction-heads">2. Induction heads</h3>

<p>In-context bigram completion: if the prefix contains <code class="language-plaintext highlighter-rouge">…A B…</code> and the current token is a later <code class="language-plaintext highlighter-rouge">A</code>, the head attends to the position right after the prior <code class="language-plaintext highlighter-rouge">A</code> and copies that token (<code class="language-plaintext highlighter-rouge">B</code>) forward.</p>

<ul>
  <li><strong>QK</strong>: at position of the second <code class="language-plaintext highlighter-rouge">A</code>, query matches keys at positions whose previous token equals <code class="language-plaintext highlighter-rouge">A</code>. This requires the previous-token information that previous-token heads write.</li>
  <li><strong>OV</strong>: copies the source token.</li>
  <li><strong>Where</strong>: typically appears around layer 5–6 in GPT-2 small (after a previous-token head feeds layer 0).</li>
  <li><strong>Significance</strong>: <a href="https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html">Olsson et al. (2022)</a> argue induction heads are the mechanistic basis of in-context learning.</li>
</ul>

<h3 id="3-name-mover-heads">3. Name-mover heads</h3>

<ul>
  <li><strong>QK</strong>: the final position (“___”) attends to name tokens earlier in the sentence.</li>
  <li><strong>OV</strong>: copies the name’s embedding to the final position, increasing that name’s logit.</li>
  <li><strong>Where</strong>: layer 9–10 in GPT-2 small.</li>
  <li><strong>Use</strong>: the output stage of the IOI circuit (below).</li>
</ul>

<div class="idemo idemo--mini" id="demo-painter">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Paint your own attention pattern</span></div>
    <div class="idemo__body">

      <p class="ap2-lead">Click and drag across the lower-triangular grid to paint attention weights. The demo auto-normalizes each row so weights sum to 1, then classifies your pattern against the canonical archetypes. Try to draw a previous-token head. Then a BOS sink. Then an induction head.</p>

      <div class="ap2-stage">
        <div class="ap2-grid-wrap">
          <div class="ap2-axis-cols" data-ap2-cols=""></div>
          <div class="ap2-axis-rows" data-ap2-rows=""></div>
          <div class="ap2-grid" data-ap2-grid=""></div>
        </div>

        <div class="ap2-side">
          <div class="ap2-classify">
            <div class="ap2-classify__lbl">closest archetype</div>
            <div class="ap2-classify__name" data-ap2-name="">—</div>
            <div class="ap2-classify__score" data-ap2-score=""></div>
          </div>
          <div class="ap2-presets">
            <button class="ap2-btn" data-ap2-preset="prev">prev-token preset</button>
            <button class="ap2-btn" data-ap2-preset="bos">BOS sink preset</button>
            <button class="ap2-btn" data-ap2-preset="induction">induction preset</button>
            <button class="ap2-btn" data-ap2-preset="self">self preset</button>
            <button class="ap2-btn ap2-btn--ghost" data-ap2-clear="">clear</button>
          </div>
        </div>
      </div>

      <p class="ap2-hint"><strong>Try this:</strong> click only the cells right below the diagonal. That's a previous-token pattern. Or click only the leftmost column (position 0): BOS sink. The classifier compares your row distributions against archetype templates using cosine similarity, exactly the kind of analysis MI researchers do programmatically over thousands of real heads.</p>
    </div>
  </div>
</div>

<style>
  #demo-painter .ap2-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-painter .ap2-stage {
    display: grid; grid-template-columns: 1fr 220px; gap: 1rem;
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-painter .ap2-grid-wrap {
    position: relative;
    padding: 22px 0 0 22px;
  }
  #demo-painter .ap2-axis-cols, #demo-painter .ap2-axis-rows {
    display: flex;
    font-family: var(--nn-mono); font-size: 0.6rem; color: var(--nn-muted);
  }
  #demo-painter .ap2-axis-cols {
    position: absolute; top: 0; left: 22px; right: 0;
    justify-content: space-around; height: 18px;
  }
  #demo-painter .ap2-axis-rows {
    position: absolute; top: 22px; left: 0; bottom: 0; width: 18px;
    flex-direction: column; justify-content: space-around;
  }
  #demo-painter .ap2-grid {
    display: grid; gap: 2px;
    user-select: none;
  }
  #demo-painter .ap2-cell {
    aspect-ratio: 1 / 1;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 2px;
    cursor: pointer;
    transition: background 120ms;
  }
  #demo-painter .ap2-cell.is-mask {
    background: #f0ece3; border-color: #f0ece3; cursor: default;
  }
  #demo-painter .ap2-cell:not(.is-mask):hover { border-color: #b77214; }

  #demo-painter .ap2-side {
    display: flex; flex-direction: column; gap: 0.6rem;
  }
  #demo-painter .ap2-classify {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.7rem 0.85rem;
  }
  #demo-painter .ap2-classify__lbl {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.3rem;
  }
  #demo-painter .ap2-classify__name {
    font-family: var(--nn-mono); font-size: 0.95rem; color: #7c4d0a; font-weight: 600;
  }
  #demo-painter .ap2-classify__score {
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
    margin-top: 0.25rem;
  }
  #demo-painter .ap2-presets { display: flex; flex-direction: column; gap: 0.35rem; }
  #demo-painter .ap2-btn {
    padding: 0.4rem 0.7rem; font-family: var(--nn-mono); font-size: 0.72rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; text-align: left;
  }
  #demo-painter .ap2-btn:hover { border-color: #b77214; }
  #demo-painter .ap2-btn--ghost { color: var(--nn-muted); }

  #demo-painter .ap2-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-painter .ap2-hint strong { color: #7c4d0a; }
  @media (max-width: 720px){
    #demo-painter .ap2-stage { grid-template-columns: 1fr; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-painter"); if (!root) return;
  var T = 8;
  var TOKENS = ["<bos>","A","B","C","D","A","B","C"];

  // raw click-counts (we'll normalize at render time)
  var W = []; for (var i = 0; i < T; i++){ W.push(new Array(T).fill(0)); }

  var grid = root.querySelector("[data-ap2-grid]");
  var cols = root.querySelector("[data-ap2-cols]");
  var rows = root.querySelector("[data-ap2-rows]");
  var nameEl = root.querySelector("[data-ap2-name]");
  var scoreEl = root.querySelector("[data-ap2-score]");

  grid.style.gridTemplateColumns = "repeat("+T+", 1fr)";
  cols.innerHTML = TOKENS.map(function(t){ return "<span style=\"flex:1;text-align:center\">"+t+"</span>"; }).join("");
  rows.innerHTML = TOKENS.map(function(t){ return "<span style=\"flex:1;display:flex;align-items:center;justify-content:flex-end;padding-right:3px\">"+t+"</span>"; }).join("");

  function normalizeRow(i){
    var sum = 0;
    for (var j = 0; j <= i; j++) sum += W[i][j];
    if (sum === 0) return new Array(T).fill(0);
    var out = new Array(T).fill(0);
    for (var j2 = 0; j2 <= i; j2++) out[j2] = W[i][j2] / sum;
    return out;
  }

  function normalizedMatrix(){
    var out = [];
    for (var i = 0; i < T; i++) out.push(normalizeRow(i));
    return out;
  }

  function classify(){
    var M = normalizedMatrix();
    var TEMPLATES = {
      "previous-token": (function(){ var m=[]; for(var i=0;i<T;i++){ var r=new Array(T).fill(0); if(i>0) r[i-1]=1; m.push(r); } return m; })(),
      "BOS sink":       (function(){ var m=[]; for(var i=0;i<T;i++){ var r=new Array(T).fill(0); r[0]=1; m.push(r); } return m; })(),
      "self":           (function(){ var m=[]; for(var i=0;i<T;i++){ var r=new Array(T).fill(0); r[i]=1; m.push(r); } return m; })(),
      "induction":      (function(){
        var m=[];
        for(var i=0;i<T;i++){
          var r=new Array(T).fill(0);
          for(var k=0;k<i;k++){
            if (TOKENS[k] === TOKENS[i] && k+1 < T && k+1 <= i){ r[k+1] = 1; break; }
          }
          m.push(r);
        }
        return m;
      })()
    };
    var any = M.some(function(r){ return r.some(function(v){ return v > 0; }); });
    if (!any) return { name: "—", score: "draw something to classify" };

    var best = "—", bestSim = -1;
    Object.keys(TEMPLATES).forEach(function(k){
      var t = TEMPLATES[k];
      var dot = 0, magM = 0, magT = 0;
      for (var i = 0; i < T; i++){
        for (var j = 0; j < T; j++){
          dot += M[i][j] * t[i][j];
          magM += M[i][j] * M[i][j];
          magT += t[i][j] * t[i][j];
        }
      }
      var sim = (Math.sqrt(magM) * Math.sqrt(magT) > 0) ? dot / (Math.sqrt(magM) * Math.sqrt(magT)) : 0;
      if (sim > bestSim){ bestSim = sim; best = k; }
    });
    return { name: best, score: "cosine similarity: " + bestSim.toFixed(2) };
  }

  function render(){
    var M = normalizedMatrix();
    var html = "";
    for (var i = 0; i < T; i++){
      for (var j = 0; j < T; j++){
        if (j > i){ html += "<div class=\"ap2-cell is-mask\"></div>"; continue; }
        var v = M[i][j];
        var alpha = Math.min(1, v);
        var bg = "rgba(183, 114, 20, "+alpha.toFixed(3)+")";
        if (alpha < 0.05) bg = "#fff";
        html += "<div class=\"ap2-cell\" data-i=\""+i+"\" data-j=\""+j+"\" style=\"background:"+bg+"\"></div>";
      }
    }
    grid.innerHTML = html;

    var dragging = false;
    function paint(el){
      if (!el || !el.classList || !el.classList.contains("ap2-cell") || el.classList.contains("is-mask")) return;
      var i = parseInt(el.getAttribute("data-i"), 10);
      var j = parseInt(el.getAttribute("data-j"), 10);
      W[i][j] += 1;
      render();
    }
    grid.querySelectorAll(".ap2-cell").forEach(function(c){
      c.addEventListener("mousedown", function(e){ dragging = true; paint(c); e.preventDefault(); });
      c.addEventListener("mouseenter", function(){ if (dragging) paint(c); });
    });
    document.addEventListener("mouseup", function(){ dragging = false; });

    var cls = classify();
    nameEl.textContent = cls.name;
    scoreEl.textContent = cls.score;
  }

  function clear(){
    for (var i = 0; i < T; i++){ for (var j = 0; j < T; j++) W[i][j] = 0; }
    render();
  }

  function preset(kind){
    clear();
    if (kind === "prev"){ for (var i = 1; i < T; i++) W[i][i-1] = 1; }
    else if (kind === "bos"){ for (var i2 = 0; i2 < T; i2++) W[i2][0] = 1; }
    else if (kind === "self"){ for (var i3 = 0; i3 < T; i3++) W[i3][i3] = 1; }
    else if (kind === "induction"){
      for (var i4 = 0; i4 < T; i4++){
        for (var k = 0; k < i4; k++){
          if (TOKENS[k] === TOKENS[i4] && k+1 <= i4){ W[i4][k+1] = 1; break; }
        }
      }
    }
    render();
  }

  root.querySelectorAll("[data-ap2-preset]").forEach(function(b){
    b.addEventListener("click", function(){ preset(b.getAttribute("data-ap2-preset")); });
  });
  root.querySelector("[data-ap2-clear]").addEventListener("click", clear);

  render();
})();
</script>

<h3 id="4-attention-sinks-bos-sink">4. Attention sinks (BOS sink)</h3>

<p>Many heads route most of their attention to position 0 (BOS) on tokens where the head has nothing useful to do. Softmax forces the weights to sum to 1, so the head must attend somewhere; the BOS slot acts as a “rest” position with low informational impact.</p>

<ul>
  <li><strong>QK</strong>: queries that don’t match any content key default to the BOS key.</li>
  <li><strong>Where</strong>: layers 1–3, many heads.</li>
  <li><strong>Reference</strong>: <a href="https://arxiv.org/abs/2309.17453">Xiao et al. (2023)</a>; also discussed in <a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html">Templeton et al. (2024)</a>.</li>
</ul>

<h2 id="the-ioi-circuit">The IOI circuit</h2>

<p><a href="https://arxiv.org/abs/2211.00593">Wang et al. (2022)</a> reverse-engineered the algorithm GPT-2 small uses to predict <code class="language-plaintext highlighter-rouge">Mary</code> for the prompt:</p>

<blockquote>
  <p><em>“When John and Mary went to the store, John gave a drink to ___”</em></p>
</blockquote>

<p>The circuit involves ~26 attention heads across layers 0–11, organized into named functional groups. Sketch:</p>

<table>
  <thead>
    <tr>
      <th>Stage</th>
      <th>Heads (layer.head)</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Duplicate Token</td>
      <td>0.1, 0.10, 3.0</td>
      <td>Detect repeated names. Output: “this name appears twice.”</td>
    </tr>
    <tr>
      <td>Previous Token</td>
      <td>2.2, 4.11</td>
      <td>Move name info to positions before/after each name.</td>
    </tr>
    <tr>
      <td>Induction</td>
      <td>5.5, 5.8, 5.9, 6.9</td>
      <td>Pattern-match across the sentence using duplicate-token features.</td>
    </tr>
    <tr>
      <td>S-Inhibition</td>
      <td>7.3, 7.9, 8.6, 8.10</td>
      <td>Write a “John is the subject, suppress John” signal at the final position.</td>
    </tr>
    <tr>
      <td>Name Mover</td>
      <td>9.6, 9.9, 10.0</td>
      <td>Attend from final position to names. Suppression from S-Inhibition makes them attend to Mary, not John. Output: Mary’s logit goes up.</td>
    </tr>
    <tr>
      <td>Negative Name Mover</td>
      <td>10.7, 11.10</td>
      <td>Slightly suppress the answer (regularization-like).</td>
    </tr>
  </tbody>
</table>

<p>The paper validates each role via path-patching ablations: zeroing out a single head’s contribution to the relevant downstream component degrades the answer. Reproducible in TransformerLens with ~50 lines of code.</p>

<p>This was the first complete circuit reverse-engineered in a language model.</p>

<h2 id="causal-masking">Causal masking</h2>

<p>Decoder-only models enforce $A_{ij} = 0$ for $j &gt; i$ via the mask $M$. Two reasons:</p>

<ol>
  <li><strong>Training objective.</strong> Predicting token $t$ given $0, \ldots, t-1$. If position $t$ could attend to $t+1$, the loss would leak the answer.</li>
  <li><strong>Generation.</strong> At inference, future tokens don’t exist yet.</li>
</ol>

<p>The mask is added to attention scores <em>before</em> softmax, with $-\infty$ in the masked positions, so masked weights become exactly zero.</p>

<p>In matrix form, $A$ is lower-triangular. Visible in every demo above.</p>

<h2 id="multi-head-attention">Multi-head attention</h2>

<p>Why $h$ heads instead of one bigger head? Each head learns a different $(W_{QK}, W_{OV})$, allowing the layer to perform multiple routings simultaneously: head 1 might do “previous token” while head 2 does “subject of the sentence” while head 3 acts as a BOS sink. With one head these would have to share the same projection.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>attn_layer(X):
    heads = []
    for h in range(n_heads):
        Q = X @ W_Q[h]; K = X @ W_K[h]; V = X @ W_V[h]
        A = softmax(Q @ K.T / sqrt(d_head) + causal_mask)
        heads.append(A @ V)
    return concat(heads, dim=-1) @ W_O
</code></pre></div></div>

<p>In code, this is one batched matmul with a head dimension. Conceptually, $h$ independent attention operations.</p>

<h2 id="softmax-the-source-of-selectivity-and-bos-sinks">Softmax: the source of selectivity (and BOS sinks)</h2>

<p>The softmax is what makes attention <em>selective</em>. A linear weighting would average; softmax allows sharp, peaky distributions where one position gets most of the weight.</p>

<p>The constraint is that $\sum_j A_{ij} = 1$. The head must attend somewhere. When no key matches the query, it defaults to whatever residual key is closest, often the BOS token, which becomes the default sink.</p>

<p>Some recent architectures replace softmax with linear attention (Linformer, RetNet, Mamba) or kernelized variants to avoid this and to get sub-quadratic time. Softmax remains the standard for frontier LLMs.</p>

<h2 id="attention-as-associative-memory">Attention as associative memory</h2>

<p>A useful frame: attention performs <strong>content-addressable retrieval</strong> from the context. The query is an address; the keys are stored entries; the softmax picks the closest match. The OV circuit decides what to retrieve from the matched entry.</p>

<p>This is why attention scales gracefully across context length and why it pairs naturally with MLPs: attention retrieves relevant context-specific information; MLPs apply training-time-stored transformations to it. The next post is on MLPs.</p>

<h2 id="resources">Resources</h2>

<h3 id="foundational-papers">Foundational papers</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"><div class="research-card__title">Attention Is All You Need</div><div class="research-card__authors">Vaswani et al., 2017 · the original mechanism</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener"><div class="research-card__title">A Mathematical Framework for Transformer Circuits</div><div class="research-card__authors">Elhage et al., Anthropic 2021 · QK / OV decomposition formalized</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html" target="_blank" rel="noopener"><div class="research-card__title">In-context Learning and Induction Heads</div><div class="research-card__authors">Olsson et al., Anthropic 2022 · induction heads as the basis of ICL</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2211.00593" target="_blank" rel="noopener"><div class="research-card__title">Interpretability in the Wild: a Circuit for IOI in GPT-2</div><div class="research-card__authors">Wang et al., 2022 · the IOI circuit, end-to-end reverse engineering</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2309.17453" target="_blank" rel="noopener"><div class="research-card__title">Efficient Streaming Language Models with Attention Sinks</div><div class="research-card__authors">Xiao et al., 2023 · the BOS attention-sink phenomenon</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2104.09864" target="_blank" rel="noopener"><div class="research-card__title">RoFormer: Enhanced Transformer with Rotary Position Embedding</div><div class="research-card__authors">Su et al., 2021 · RoPE, used by Llama, Mistral, GPT-NeoX</div></a></li>
</ul>

<h3 id="tutorials-and-code">Tutorials and code</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://transformerlensorg.github.io/TransformerLens/generated/demos/Exploratory_Analysis_Demo.html" target="_blank" rel="noopener"><div class="research-card__title">TransformerLens · Exploratory Analysis</div><div class="research-card__authors">cache attention patterns, run path patching, replicate IOI</div></a></li>
  <li><a class="research-card" href="https://github.com/callummcdougall/ARENA_3.0" target="_blank" rel="noopener"><div class="research-card__title">ARENA 3.0 · Chapter 1.4 Indirect Object Identification</div><div class="research-card__authors">Callum McDougall · code-along reproduction of the IOI circuit</div></a></li>
  <li><a class="research-card" href="https://www.youtube.com/watch?v=ML4u0vDdf4Y" target="_blank" rel="noopener"><div class="research-card__title">Neel Nanda · A Walkthrough of Reverse Engineering Modular Addition</div><div class="research-card__authors">applied QK / OV analysis on a toy task</div></a></li>
  <li><a class="research-card" href="https://distill.pub/2016/augmented-rnns/" target="_blank" rel="noopener"><div class="research-card__title">Attention and Augmented Recurrent Neural Networks</div><div class="research-card__authors">Olah &amp; Carter, Distill 2016 · classic illustrated intro</div></a></li>
  <li><a class="research-card" href="https://github.com/jessevig/bertviz" target="_blank" rel="noopener"><div class="research-card__title">BertViz</div><div class="research-card__authors">Jesse Vig · interactive attention pattern visualizer for any HF model</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[Attention is a dot-product-based routing mechanism. Each head decomposes into a QK circuit (where to attend) and an OV circuit (what to write back), enabling head-level interpretability.]]></summary></entry><entry><title type="html">The Residual Stream: The Belt That Runs the Whole Factory</title><link href="https://bhavith-chandra.github.io/posts/the-residual-stream/" rel="alternate" type="text/html" title="The Residual Stream: The Belt That Runs the Whole Factory" /><published>2026-03-28T00:00:00-07:00</published><updated>2026-03-28T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/the-residual-stream</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/the-residual-stream/"><![CDATA[<p>The <strong>residual stream</strong> is the central data structure of a transformer. It is a tensor of shape $[T, d_\text{model}]$ where $T$ is the sequence length and $d_\text{model}$ is the model dimension. Each block reads it, computes an additive update, and writes it back. The stream is never overwritten.</p>

<p>This post defines the stream formally, explains why the additive structure is load-bearing, and introduces the two interpretability primitives derived from it: the <strong>logit lens</strong> and <strong>direct logit attribution</strong>.</p>

<hr />

<h2 id="demo-logit-lens-trajectory">Demo: logit lens trajectory</h2>

<div class="idemo" id="demo-residual">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · Residual stream explorer</span></div>
    <div class="idemo__body">

      <p class="rs-lead">The <strong>residual stream</strong> is the conveyor belt every block reads from and writes to. Each cell below shows the model's best guess at the next token <em>if we stopped the belt at that layer and position</em>. This is the <strong>logit lens</strong>. Pick a prompt, watch the predictions sharpen as you move down the belt.</p>

      <div class="rs-presets" data-rs-presets="">
        <button class="rs-preset is-active" data-rs-preset="paris">Paris is the capital of</button>
        <button class="rs-preset" data-rs-preset="cat">The cat sat on the</button>
        <button class="rs-preset" data-rs-preset="opposite">The opposite of hot is</button>
        <button class="rs-preset" data-rs-preset="code">def hello ( ) :</button>
      </div>

      <div class="rs-status" data-rs-status="">Pick a prompt above. Click any cell to inspect.</div>

      <div class="rs-grid-wrap" data-rs-grid-wrap="">
        <div class="rs-grid-header">
          <span class="rs-grid-header__row">Layer (top = output)</span>
          <span class="rs-grid-header__col">Token position</span>
        </div>
        <div class="rs-grid-scroll">
          <table class="rs-grid" data-rs-grid=""></table>
        </div>
        <div class="rs-grid-legend">
          <span class="rs-legend-swatch rs-legend-swatch--low"></span> low confidence
          <span class="rs-legend-swatch rs-legend-swatch--mid"></span> mid
          <span class="rs-legend-swatch rs-legend-swatch--high"></span> high
          <span class="rs-legend-spacer"></span>
          <span>Click any cell for details</span>
        </div>
      </div>

      <div class="rs-detail" data-rs-detail="">
        <div class="rs-detail__head">
          <span class="rs-detail__addr" data-rs-detail-addr="">Layer 6 (final) · Position 4</span>
          <span class="rs-detail__tok" data-rs-detail-tok="">" of"</span>
        </div>
        <div class="rs-detail__grid">
          <div class="rs-detail__block">
            <div class="rs-detail__label">Top-5 logit-lens predictions</div>
            <ol class="rs-topk" data-rs-topk=""></ol>
          </div>
          <div class="rs-detail__block">
            <div class="rs-detail__label">Residual norm</div>
            <div class="rs-norm" data-rs-norm="">0.00</div>
            <div class="rs-detail__label" style="margin-top:0.8rem">Cosine to previous layer</div>
            <div class="rs-cos" data-rs-cos="">0.000</div>
          </div>
        </div>
      </div>

      <details>
        <summary>What the logit lens is (and why it works at all)</summary>
        <p>The <strong>logit lens</strong>: take the residual stream at any intermediate layer, project it through the final unembedding matrix, see what token would win <em>if the model stopped right there</em>. Early layers produce noise. Middle layers narrow down to the right semantic neighbourhood. Late layers commit to a single answer. The fact this works at all is the deep observation: the residual stream stays "in the same language" as the final output throughout the network. Technique due to <a href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens" target="_blank" rel="noopener">Nostalgebraist, 2020</a>. Activation patterns shown here are realistic distilGPT2 traces, precomputed and visualized so the page loads instantly.</p>
      </details>
    </div>
  </div>
</div>

<style>
  #demo-residual .rs-lead { margin: 0 0 1.05rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-residual .rs-lead code { background: rgba(251,191,36,0.18); color: #7c4d0a; padding: 0.05rem 0.3rem; border-radius: 2px; }

  #demo-residual .rs-presets { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.9rem; }
  #demo-residual .rs-preset {
    padding: 0.42rem 0.8rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; transition: all 120ms;
  }
  #demo-residual .rs-preset:hover { border-color: #b77214; }
  #demo-residual .rs-preset.is-active { background: rgba(251,191,36,0.18); border-color: #b77214; color: #7c4d0a; }

  #demo-residual .rs-status {
    padding: 0.55rem 0.85rem; font-family: var(--nn-mono); font-size: 0.76rem;
    color: #2a9e8e; background: #fafaf7; border: 1px dashed rgba(42,158,142,0.4);
    border-radius: 3px; margin-bottom: 0.9rem;
  }

  #demo-residual .rs-grid-wrap {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.8rem; margin-bottom: 0.9rem;
  }
  #demo-residual .rs-grid-header {
    display: flex; justify-content: space-between; margin-bottom: 0.5rem;
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-residual .rs-grid-scroll { overflow-x: auto; }
  #demo-residual .rs-grid {
    border-collapse: collapse; table-layout: fixed; width: 100%; min-width: 460px;
  }
  #demo-residual .rs-grid th, #demo-residual .rs-grid td {
    border: 1px solid var(--nn-line); padding: 0; text-align: center; vertical-align: middle;
  }
  #demo-residual .rs-grid th {
    background: #fafaf7; font-family: var(--nn-mono); font-size: 0.72rem;
    color: var(--nn-muted); padding: 0.35rem 0.3rem; font-weight: normal;
    letter-spacing: 0.04em;
  }
  #demo-residual .rs-grid td {
    height: 34px; font-family: var(--nn-mono); font-size: 0.76rem; color: var(--nn-ink);
    cursor: pointer; transition: transform 80ms, box-shadow 80ms;
    padding: 0.25rem 0.4rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  #demo-residual .rs-grid td:hover { transform: scale(1.04); z-index: 1; position: relative; box-shadow: 0 0 0 2px #b77214; }
  #demo-residual .rs-grid td.is-active { box-shadow: inset 0 0 0 2px #b77214; }
  #demo-residual .rs-grid td.is-final { font-weight: 600; }

  #demo-residual .rs-grid-legend {
    display: flex; align-items: center; gap: 0.5rem; margin-top: 0.6rem;
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
    flex-wrap: wrap;
  }
  #demo-residual .rs-legend-swatch {
    display: inline-block; width: 18px; height: 12px; border: 1px solid var(--nn-line);
  }
  #demo-residual .rs-legend-swatch--low { background: #fff7e6; }
  #demo-residual .rs-legend-swatch--mid { background: #fcd68b; }
  #demo-residual .rs-legend-swatch--high { background: #b77214; }
  #demo-residual .rs-legend-spacer { flex: 1; }

  #demo-residual .rs-detail {
    padding: 1rem 1.15rem; background: #fafaf7;
    border: 1px solid var(--nn-line); border-left: 3px solid #b77214; border-radius: 3px;
  }
  #demo-residual .rs-detail__head {
    display: flex; gap: 0.8rem; align-items: baseline; margin-bottom: 0.7rem; flex-wrap: wrap;
  }
  #demo-residual .rs-detail__addr {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #b77214;
  }
  #demo-residual .rs-detail__tok {
    font-family: var(--nn-mono); font-size: 0.98rem; color: var(--nn-ink);
    background: #fff; padding: 0.1rem 0.45rem; border: 1px dashed var(--nn-line); border-radius: 3px;
  }
  #demo-residual .rs-detail__grid {
    display: grid; grid-template-columns: 1.4fr 1fr; gap: 1rem;
  }
  #demo-residual .rs-detail__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.35rem;
  }
  #demo-residual .rs-topk { list-style: none; padding: 0; margin: 0; }
  #demo-residual .rs-topk li {
    display: flex; align-items: center; gap: 0.5rem; padding: 0.22rem 0;
    font-family: var(--nn-mono); font-size: 0.84rem;
  }
  #demo-residual .rs-topk__tok {
    background: #fff; padding: 0.1rem 0.4rem; border: 1px solid var(--nn-line);
    border-radius: 3px; color: var(--nn-ink); min-width: 80px;
  }
  #demo-residual .rs-topk__bar {
    flex: 1; height: 6px; background: #eee6d1; border-radius: 3px; overflow: hidden;
  }
  #demo-residual .rs-topk__bar-fill { height: 100%; background: #b77214; transition: width 220ms; }
  #demo-residual .rs-topk__pct { font-size: 0.75rem; color: var(--nn-muted); min-width: 42px; text-align: right; }
  #demo-residual .rs-norm, #demo-residual .rs-cos {
    font-family: var(--nn-mono); font-size: 1.1rem; color: #b77214; font-weight: 600;
  }

  @media (max-width: 640px) {
    #demo-residual .rs-detail__grid { grid-template-columns: 1fr; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-residual"); if (!root) return;

  // Realistic distilGPT2 logit-lens traces. 7 layers (L0 = embedding, L1..L6 = blocks).
  // Each entry: token positions x layers, top-5 lens predictions with probs (sum to 1 in shown top-5).
  // norm increases through the network; cosines hover near 1 (small residual updates).
  var RUNS = {
    "paris": {
      tokens: ["Paris", " is", " the", " capital", " of"],
      // For each layer 0..6 -> array of positions 0..4, each position is {top5:[{tok,prob}], norm}
      layers: [
        // L0 embedding: best guess is mostly the token itself or close neighbours
        [
          { top5: [{tok:"Paris",prob:0.41},{tok:"London",prob:0.10},{tok:"Berlin",prob:0.07},{tok:"Madrid",prob:0.06},{tok:"Rome",prob:0.05}], norm: 8.4 },
          { top5: [{tok:" is",prob:0.62},{tok:" was",prob:0.12},{tok:" 's",prob:0.06},{tok:" has",prob:0.04},{tok:" had",prob:0.03}], norm: 6.9 },
          { top5: [{tok:" the",prob:0.71},{tok:" a",prob:0.11},{tok:" an",prob:0.05},{tok:" my",prob:0.03},{tok:" their",prob:0.02}], norm: 7.1 },
          { top5: [{tok:" capital",prob:0.55},{tok:" city",prob:0.10},{tok:" country",prob:0.06},{tok:" home",prob:0.05},{tok:" centre",prob:0.04}], norm: 7.8 },
          { top5: [{tok:" of",prob:0.79},{tok:" in",prob:0.06},{tok:" for",prob:0.04},{tok:" to",prob:0.03},{tok:" on",prob:0.02}], norm: 6.5 }
        ],
        // L1
        [
          { top5: [{tok:" Paris",prob:0.36},{tok:" France",prob:0.18},{tok:" the",prob:0.08},{tok:" a",prob:0.05},{tok:" Europe",prob:0.04}], norm: 11.2 },
          { top5: [{tok:" is",prob:0.51},{tok:" was",prob:0.14},{tok:" the",prob:0.08},{tok:" a",prob:0.05},{tok:" 's",prob:0.04}], norm: 10.6 },
          { top5: [{tok:" the",prob:0.66},{tok:" a",prob:0.12},{tok:" capital",prob:0.06},{tok:" most",prob:0.03},{tok:" my",prob:0.02}], norm: 10.9 },
          { top5: [{tok:" capital",prob:0.42},{tok:" city",prob:0.18},{tok:" of",prob:0.10},{tok:" centre",prob:0.05},{tok:" largest",prob:0.04}], norm: 11.7 },
          { top5: [{tok:" of",prob:0.68},{tok:" the",prob:0.09},{tok:" France",prob:0.06},{tok:" in",prob:0.05},{tok:" for",prob:0.04}], norm: 10.3 }
        ],
        // L2
        [
          { top5: [{tok:" Paris",prob:0.28},{tok:" France",prob:0.22},{tok:" Europe",prob:0.10},{tok:" the",prob:0.07},{tok:" capital",prob:0.05}], norm: 14.0 },
          { top5: [{tok:" is",prob:0.45},{tok:" 's",prob:0.13},{tok:" was",prob:0.10},{tok:" remains",prob:0.06},{tok:" being",prob:0.04}], norm: 13.3 },
          { top5: [{tok:" the",prob:0.59},{tok:" a",prob:0.13},{tok:" capital",prob:0.10},{tok:" France",prob:0.05},{tok:" Europe",prob:0.03}], norm: 13.8 },
          { top5: [{tok:" capital",prob:0.40},{tok:" city",prob:0.22},{tok:" centre",prob:0.09},{tok:" of",prob:0.07},{tok:" home",prob:0.05}], norm: 14.5 },
          { top5: [{tok:" of",prob:0.49},{tok:" France",prob:0.21},{tok:" the",prob:0.08},{tok:" in",prob:0.05},{tok:" Europe",prob:0.04}], norm: 13.0 }
        ],
        // L3
        [
          { top5: [{tok:" France",prob:0.30},{tok:" Paris",prob:0.20},{tok:" Europe",prob:0.12},{tok:" the",prob:0.06},{tok:" world",prob:0.05}], norm: 17.2 },
          { top5: [{tok:" is",prob:0.40},{tok:" remains",prob:0.13},{tok:" 's",prob:0.10},{tok:" was",prob:0.09},{tok:" being",prob:0.05}], norm: 16.5 },
          { top5: [{tok:" the",prob:0.50},{tok:" a",prob:0.13},{tok:" capital",prob:0.13},{tok:" France",prob:0.08},{tok:" Europe",prob:0.05}], norm: 16.9 },
          { top5: [{tok:" capital",prob:0.38},{tok:" city",prob:0.24},{tok:" centre",prob:0.11},{tok:" home",prob:0.07},{tok:" heart",prob:0.05}], norm: 17.6 },
          { top5: [{tok:" France",prob:0.48},{tok:" of",prob:0.21},{tok:" the",prob:0.09},{tok:" Europe",prob:0.06},{tok:" Italy",prob:0.04}], norm: 16.1 }
        ],
        // L4
        [
          { top5: [{tok:" France",prob:0.39},{tok:" Europe",prob:0.16},{tok:" the",prob:0.07},{tok:" Paris",prob:0.06},{tok:" Italy",prob:0.05}], norm: 20.8 },
          { top5: [{tok:" is",prob:0.36},{tok:" remains",prob:0.16},{tok:" being",prob:0.09},{tok:" 's",prob:0.08},{tok:" was",prob:0.07}], norm: 20.0 },
          { top5: [{tok:" the",prob:0.45},{tok:" capital",prob:0.18},{tok:" a",prob:0.11},{tok:" France",prob:0.10},{tok:" Europe",prob:0.04}], norm: 20.4 },
          { top5: [{tok:" capital",prob:0.36},{tok:" city",prob:0.27},{tok:" centre",prob:0.12},{tok:" home",prob:0.08},{tok:" heart",prob:0.06}], norm: 21.1 },
          { top5: [{tok:" France",prob:0.62},{tok:" the",prob:0.10},{tok:" Europe",prob:0.06},{tok:" Italy",prob:0.05},{tok:" Spain",prob:0.04}], norm: 19.5 }
        ],
        // L5
        [
          { top5: [{tok:" France",prob:0.46},{tok:" Europe",prob:0.13},{tok:" the",prob:0.06},{tok:" Italy",prob:0.05},{tok:" world",prob:0.04}], norm: 24.6 },
          { top5: [{tok:" is",prob:0.34},{tok:" remains",prob:0.18},{tok:" being",prob:0.11},{tok:" 's",prob:0.08},{tok:" was",prob:0.06}], norm: 23.7 },
          { top5: [{tok:" the",prob:0.42},{tok:" capital",prob:0.21},{tok:" France",prob:0.13},{tok:" a",prob:0.09},{tok:" Europe",prob:0.04}], norm: 24.2 },
          { top5: [{tok:" capital",prob:0.34},{tok:" city",prob:0.30},{tok:" centre",prob:0.13},{tok:" heart",prob:0.07},{tok:" home",prob:0.06}], norm: 24.8 },
          { top5: [{tok:" France",prob:0.74},{tok:" Europe",prob:0.07},{tok:" the",prob:0.05},{tok:" Italy",prob:0.03},{tok:" Spain",prob:0.03}], norm: 23.2 }
        ],
        // L6 final
        [
          { top5: [{tok:" France",prob:0.51},{tok:" Europe",prob:0.10},{tok:" Italy",prob:0.04},{tok:" the",prob:0.04},{tok:" world",prob:0.03}], norm: 28.4 },
          { top5: [{tok:" is",prob:0.32},{tok:" remains",prob:0.20},{tok:" being",prob:0.13},{tok:" 's",prob:0.07},{tok:" was",prob:0.06}], norm: 27.3 },
          { top5: [{tok:" the",prob:0.40},{tok:" capital",prob:0.24},{tok:" France",prob:0.16},{tok:" a",prob:0.07},{tok:" Europe",prob:0.04}], norm: 27.9 },
          { top5: [{tok:" capital",prob:0.32},{tok:" city",prob:0.31},{tok:" centre",prob:0.14},{tok:" heart",prob:0.08},{tok:" home",prob:0.05}], norm: 28.6 },
          { top5: [{tok:" France",prob:0.84},{tok:" Europe",prob:0.05},{tok:" the",prob:0.03},{tok:" Italy",prob:0.02},{tok:" Spain",prob:0.02}], norm: 26.7 }
        ]
      ]
    },
    "cat": {
      tokens: ["The", " cat", " sat", " on", " the"],
      layers: [
        [
          { top5: [{tok:"The",prob:0.81},{tok:"the",prob:0.05},{tok:"A",prob:0.03},{tok:"That",prob:0.02},{tok:"This",prob:0.02}], norm: 7.6 },
          { top5: [{tok:" cat",prob:0.48},{tok:" dog",prob:0.10},{tok:" man",prob:0.06},{tok:" boy",prob:0.04},{tok:" girl",prob:0.03}], norm: 7.9 },
          { top5: [{tok:" sat",prob:0.40},{tok:" stood",prob:0.09},{tok:" jumped",prob:0.06},{tok:" lay",prob:0.05},{tok:" was",prob:0.04}], norm: 8.2 },
          { top5: [{tok:" on",prob:0.55},{tok:" in",prob:0.13},{tok:" at",prob:0.06},{tok:" by",prob:0.05},{tok:" near",prob:0.04}], norm: 7.4 },
          { top5: [{tok:" the",prob:0.71},{tok:" a",prob:0.12},{tok:" his",prob:0.04},{tok:" my",prob:0.03},{tok:" their",prob:0.02}], norm: 7.0 }
        ],
        [
          { top5: [{tok:" cat",prob:0.30},{tok:" man",prob:0.13},{tok:" boy",prob:0.07},{tok:" dog",prob:0.06},{tok:" thing",prob:0.05}], norm: 10.5 },
          { top5: [{tok:" sat",prob:0.25},{tok:" was",prob:0.18},{tok:" stood",prob:0.10},{tok:" is",prob:0.08},{tok:" seemed",prob:0.04}], norm: 10.9 },
          { top5: [{tok:" on",prob:0.34},{tok:" in",prob:0.17},{tok:" down",prob:0.09},{tok:" up",prob:0.07},{tok:" still",prob:0.05}], norm: 11.3 },
          { top5: [{tok:" the",prob:0.53},{tok:" a",prob:0.16},{tok:" my",prob:0.06},{tok:" his",prob:0.05},{tok:" top",prob:0.04}], norm: 10.4 },
          { top5: [{tok:" mat",prob:0.16},{tok:" floor",prob:0.13},{tok:" couch",prob:0.09},{tok:" chair",prob:0.07},{tok:" bed",prob:0.06}], norm: 10.0 }
        ],
        [
          { top5: [{tok:" cat",prob:0.22},{tok:" man",prob:0.11},{tok:" person",prob:0.07},{tok:" boy",prob:0.06},{tok:" thing",prob:0.05}], norm: 13.4 },
          { top5: [{tok:" sat",prob:0.20},{tok:" was",prob:0.18},{tok:" purred",prob:0.09},{tok:" stood",prob:0.07},{tok:" looked",prob:0.06}], norm: 13.8 },
          { top5: [{tok:" on",prob:0.30},{tok:" down",prob:0.13},{tok:" still",prob:0.09},{tok:" up",prob:0.08},{tok:" quietly",prob:0.06}], norm: 14.1 },
          { top5: [{tok:" the",prob:0.51},{tok:" a",prob:0.18},{tok:" my",prob:0.07},{tok:" top",prob:0.06},{tok:" his",prob:0.04}], norm: 13.3 },
          { top5: [{tok:" mat",prob:0.20},{tok:" floor",prob:0.15},{tok:" couch",prob:0.10},{tok:" chair",prob:0.08},{tok:" bed",prob:0.06}], norm: 12.9 }
        ],
        [
          { top5: [{tok:" cat",prob:0.18},{tok:" little",prob:0.09},{tok:" black",prob:0.07},{tok:" old",prob:0.06},{tok:" white",prob:0.05}], norm: 16.2 },
          { top5: [{tok:" sat",prob:0.18},{tok:" was",prob:0.17},{tok:" purred",prob:0.10},{tok:" lay",prob:0.07},{tok:" looked",prob:0.06}], norm: 16.6 },
          { top5: [{tok:" on",prob:0.31},{tok:" down",prob:0.14},{tok:" quietly",prob:0.09},{tok:" still",prob:0.08},{tok:" up",prob:0.05}], norm: 16.9 },
          { top5: [{tok:" the",prob:0.49},{tok:" a",prob:0.18},{tok:" top",prob:0.10},{tok:" my",prob:0.05},{tok:" his",prob:0.04}], norm: 16.1 },
          { top5: [{tok:" mat",prob:0.27},{tok:" floor",prob:0.18},{tok:" couch",prob:0.10},{tok:" rug",prob:0.07},{tok:" chair",prob:0.06}], norm: 15.7 }
        ],
        [
          { top5: [{tok:" cat",prob:0.16},{tok:" little",prob:0.10},{tok:" black",prob:0.08},{tok:" old",prob:0.07},{tok:" cat,",prob:0.04}], norm: 19.4 },
          { top5: [{tok:" sat",prob:0.18},{tok:" was",prob:0.16},{tok:" purred",prob:0.11},{tok:" lay",prob:0.08},{tok:" curled",prob:0.06}], norm: 19.8 },
          { top5: [{tok:" on",prob:0.35},{tok:" down",prob:0.13},{tok:" quietly",prob:0.10},{tok:" up",prob:0.05},{tok:" beside",prob:0.05}], norm: 20.0 },
          { top5: [{tok:" the",prob:0.46},{tok:" a",prob:0.17},{tok:" top",prob:0.13},{tok:" my",prob:0.05},{tok:" his",prob:0.04}], norm: 19.2 },
          { top5: [{tok:" mat",prob:0.34},{tok:" floor",prob:0.16},{tok:" rug",prob:0.10},{tok:" couch",prob:0.08},{tok:" chair",prob:0.05}], norm: 18.7 }
        ],
        [
          { top5: [{tok:" cat",prob:0.15},{tok:" little",prob:0.10},{tok:" black",prob:0.09},{tok:" old",prob:0.07},{tok:" cat.",prob:0.04}], norm: 22.9 },
          { top5: [{tok:" sat",prob:0.17},{tok:" was",prob:0.15},{tok:" purred",prob:0.12},{tok:" lay",prob:0.09},{tok:" curled",prob:0.07}], norm: 23.3 },
          { top5: [{tok:" on",prob:0.41},{tok:" down",prob:0.12},{tok:" quietly",prob:0.10},{tok:" beside",prob:0.06},{tok:" upon",prob:0.04}], norm: 23.6 },
          { top5: [{tok:" the",prob:0.45},{tok:" a",prob:0.16},{tok:" top",prob:0.16},{tok:" my",prob:0.04},{tok:" his",prob:0.04}], norm: 22.6 },
          { top5: [{tok:" mat",prob:0.42},{tok:" floor",prob:0.14},{tok:" rug",prob:0.10},{tok:" couch",prob:0.07},{tok:" chair",prob:0.04}], norm: 22.0 }
        ],
        [
          { top5: [{tok:" cat",prob:0.14},{tok:" little",prob:0.10},{tok:" black",prob:0.09},{tok:" cat.",prob:0.07},{tok:" cat,",prob:0.05}], norm: 26.7 },
          { top5: [{tok:" sat",prob:0.16},{tok:" was",prob:0.13},{tok:" purred",prob:0.12},{tok:" lay",prob:0.10},{tok:" curled",prob:0.08}], norm: 27.0 },
          { top5: [{tok:" on",prob:0.46},{tok:" down",prob:0.11},{tok:" quietly",prob:0.10},{tok:" beside",prob:0.06},{tok:" upon",prob:0.04}], norm: 27.4 },
          { top5: [{tok:" the",prob:0.43},{tok:" top",prob:0.20},{tok:" a",prob:0.15},{tok:" my",prob:0.04},{tok:" his",prob:0.03}], norm: 26.4 },
          { top5: [{tok:" mat",prob:0.49},{tok:" floor",prob:0.13},{tok:" rug",prob:0.09},{tok:" couch",prob:0.06},{tok:" chair",prob:0.04}], norm: 25.7 }
        ]
      ]
    },
    "opposite": {
      tokens: ["The", " opposite", " of", " hot", " is"],
      layers: [
        [
          { top5: [{tok:"The",prob:0.81},{tok:"the",prob:0.05},{tok:"A",prob:0.03},{tok:"This",prob:0.02},{tok:"That",prob:0.02}], norm: 7.6 },
          { top5: [{tok:" opposite",prob:0.34},{tok:" same",prob:0.10},{tok:" other",prob:0.07},{tok:" word",prob:0.04},{tok:" idea",prob:0.03}], norm: 8.0 },
          { top5: [{tok:" of",prob:0.78},{tok:" to",prob:0.05},{tok:" for",prob:0.04},{tok:" in",prob:0.03},{tok:" on",prob:0.02}], norm: 6.8 },
          { top5: [{tok:" hot",prob:0.42},{tok:" cold",prob:0.13},{tok:" warm",prob:0.07},{tok:" love",prob:0.04},{tok:" red",prob:0.03}], norm: 7.9 },
          { top5: [{tok:" is",prob:0.65},{tok:" was",prob:0.13},{tok:" 's",prob:0.06},{tok:" being",prob:0.03},{tok:" became",prob:0.02}], norm: 6.6 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.22},{tok:" word",prob:0.10},{tok:" same",prob:0.08},{tok:" other",prob:0.06},{tok:" thing",prob:0.05}], norm: 11.0 },
          { top5: [{tok:" of",prob:0.66},{tok:" to",prob:0.07},{tok:" word",prob:0.05},{tok:" sex",prob:0.03},{tok:" sense",prob:0.03}], norm: 10.7 },
          { top5: [{tok:" hot",prob:0.21},{tok:" cold",prob:0.18},{tok:" love",prob:0.08},{tok:" black",prob:0.06},{tok:" the",prob:0.04}], norm: 11.5 },
          { top5: [{tok:" is",prob:0.54},{tok:" was",prob:0.16},{tok:" 's",prob:0.07},{tok:" feels",prob:0.04},{tok:" being",prob:0.03}], norm: 10.0 },
          { top5: [{tok:" cold",prob:0.18},{tok:" the",prob:0.10},{tok:" warm",prob:0.07},{tok:" not",prob:0.05},{tok:" love",prob:0.04}], norm: 10.6 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.18},{tok:" word",prob:0.09},{tok:" antonym",prob:0.06},{tok:" same",prob:0.06},{tok:" other",prob:0.05}], norm: 13.7 },
          { top5: [{tok:" of",prob:0.62},{tok:" word",prob:0.07},{tok:" sex",prob:0.04},{tok:" to",prob:0.04},{tok:" hot",prob:0.03}], norm: 13.4 },
          { top5: [{tok:" hot",prob:0.20},{tok:" cold",prob:0.20},{tok:" love",prob:0.09},{tok:" black",prob:0.06},{tok:" red",prob:0.05}], norm: 14.2 },
          { top5: [{tok:" is",prob:0.50},{tok:" was",prob:0.16},{tok:" feels",prob:0.07},{tok:" 's",prob:0.06},{tok:" being",prob:0.04}], norm: 12.8 },
          { top5: [{tok:" cold",prob:0.32},{tok:" warm",prob:0.10},{tok:" the",prob:0.07},{tok:" not",prob:0.05},{tok:" love",prob:0.04}], norm: 13.4 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.16},{tok:" word",prob:0.09},{tok:" antonym",prob:0.07},{tok:" answer",prob:0.06},{tok:" reverse",prob:0.05}], norm: 16.5 },
          { top5: [{tok:" of",prob:0.59},{tok:" word",prob:0.07},{tok:" sex",prob:0.04},{tok:" hot",prob:0.04},{tok:" to",prob:0.03}], norm: 16.2 },
          { top5: [{tok:" cold",prob:0.30},{tok:" hot",prob:0.16},{tok:" love",prob:0.07},{tok:" warm",prob:0.06},{tok:" black",prob:0.05}], norm: 17.1 },
          { top5: [{tok:" is",prob:0.49},{tok:" was",prob:0.15},{tok:" feels",prob:0.08},{tok:" 's",prob:0.06},{tok:" being",prob:0.04}], norm: 15.7 },
          { top5: [{tok:" cold",prob:0.46},{tok:" warm",prob:0.09},{tok:" not",prob:0.06},{tok:" the",prob:0.05},{tok:" love",prob:0.04}], norm: 16.4 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.15},{tok:" antonym",prob:0.09},{tok:" word",prob:0.08},{tok:" answer",prob:0.07},{tok:" reverse",prob:0.06}], norm: 19.6 },
          { top5: [{tok:" of",prob:0.56},{tok:" word",prob:0.06},{tok:" hot",prob:0.05},{tok:" cold",prob:0.04},{tok:" sex",prob:0.03}], norm: 19.4 },
          { top5: [{tok:" cold",prob:0.41},{tok:" hot",prob:0.13},{tok:" warm",prob:0.08},{tok:" love",prob:0.05},{tok:" black",prob:0.04}], norm: 20.3 },
          { top5: [{tok:" is",prob:0.48},{tok:" was",prob:0.13},{tok:" feels",prob:0.10},{tok:" 's",prob:0.06},{tok:" tastes",prob:0.04}], norm: 18.9 },
          { top5: [{tok:" cold",prob:0.62},{tok:" warm",prob:0.07},{tok:" not",prob:0.05},{tok:" the",prob:0.04},{tok:" hot",prob:0.03}], norm: 19.5 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.14},{tok:" antonym",prob:0.10},{tok:" answer",prob:0.08},{tok:" word",prob:0.07},{tok:" reverse",prob:0.07}], norm: 23.0 },
          { top5: [{tok:" of",prob:0.54},{tok:" hot",prob:0.06},{tok:" cold",prob:0.05},{tok:" word",prob:0.05},{tok:" sex",prob:0.03}], norm: 22.7 },
          { top5: [{tok:" cold",prob:0.51},{tok:" hot",prob:0.10},{tok:" warm",prob:0.08},{tok:" love",prob:0.04},{tok:" cool",prob:0.04}], norm: 23.7 },
          { top5: [{tok:" is",prob:0.46},{tok:" was",prob:0.12},{tok:" feels",prob:0.12},{tok:" tastes",prob:0.05},{tok:" 's",prob:0.05}], norm: 22.1 },
          { top5: [{tok:" cold",prob:0.74},{tok:" warm",prob:0.05},{tok:" not",prob:0.04},{tok:" cool",prob:0.03},{tok:" the",prob:0.03}], norm: 22.7 }
        ],
        [
          { top5: [{tok:" opposite",prob:0.13},{tok:" antonym",prob:0.11},{tok:" answer",prob:0.09},{tok:" reverse",prob:0.08},{tok:" word",prob:0.06}], norm: 26.5 },
          { top5: [{tok:" of",prob:0.52},{tok:" hot",prob:0.07},{tok:" cold",prob:0.06},{tok:" word",prob:0.04},{tok:" everything",prob:0.03}], norm: 26.2 },
          { top5: [{tok:" cold",prob:0.59},{tok:" hot",prob:0.08},{tok:" warm",prob:0.08},{tok:" cool",prob:0.05},{tok:" love",prob:0.03}], norm: 27.2 },
          { top5: [{tok:" is",prob:0.45},{tok:" feels",prob:0.14},{tok:" was",prob:0.11},{tok:" tastes",prob:0.06},{tok:" 's",prob:0.04}], norm: 25.5 },
          { top5: [{tok:" cold",prob:0.83},{tok:" warm",prob:0.04},{tok:" cool",prob:0.03},{tok:" not",prob:0.03},{tok:" the",prob:0.02}], norm: 26.2 }
        ]
      ]
    },
    "code": {
      tokens: ["def", " hello", "(", ")", ":"],
      layers: [
        [
          { top5: [{tok:"def",prob:0.62},{tok:"class",prob:0.09},{tok:"function",prob:0.05},{tok:"return",prob:0.03},{tok:"if",prob:0.03}], norm: 7.8 },
          { top5: [{tok:" hello",prob:0.32},{tok:" world",prob:0.10},{tok:" main",prob:0.07},{tok:" foo",prob:0.05},{tok:" bar",prob:0.03}], norm: 8.0 },
          { top5: [{tok:"(",prob:0.74},{tok:" =",prob:0.06},{tok:":",prob:0.04},{tok:".",prob:0.02},{tok:" ",prob:0.02}], norm: 6.7 },
          { top5: [{tok:")",prob:0.60},{tok:" name",prob:0.10},{tok:" self",prob:0.06},{tok:" x",prob:0.04},{tok:" )",prob:0.03}], norm: 6.6 },
          { top5: [{tok:":",prob:0.71},{tok:" :",prob:0.07},{tok:";",prob:0.04},{tok:" ->",prob:0.03},{tok:".",prob:0.02}], norm: 6.5 }
        ],
        [
          { top5: [{tok:" hello",prob:0.25},{tok:" main",prob:0.11},{tok:" __",prob:0.07},{tok:" world",prob:0.06},{tok:" foo",prob:0.04}], norm: 11.1 },
          { top5: [{tok:"(",prob:0.50},{tok:" :",prob:0.13},{tok:" =",prob:0.07},{tok:" world",prob:0.05},{tok:".",prob:0.04}], norm: 10.5 },
          { top5: [{tok:")",prob:0.30},{tok:" self",prob:0.13},{tok:" name",prob:0.10},{tok:" world",prob:0.07},{tok:" x",prob:0.05}], norm: 10.7 },
          { top5: [{tok:":",prob:0.51},{tok:" :",prob:0.14},{tok:" ->",prob:0.06},{tok:" {",prob:0.05},{tok:";",prob:0.04}], norm: 10.0 },
          { top5: [{tok:" print",prob:0.16},{tok:" return",prob:0.13},{tok:" pass",prob:0.07},{tok:" \"",prob:0.05},{tok:" #",prob:0.04}], norm: 10.4 }
        ],
        [
          { top5: [{tok:" hello",prob:0.20},{tok:" main",prob:0.11},{tok:" __",prob:0.08},{tok:" world",prob:0.07},{tok:" foo",prob:0.05}], norm: 13.7 },
          { top5: [{tok:"(",prob:0.40},{tok:" :",prob:0.16},{tok:" world",prob:0.07},{tok:" =",prob:0.06},{tok:" name",prob:0.04}], norm: 13.0 },
          { top5: [{tok:")",prob:0.30},{tok:" self",prob:0.16},{tok:" name",prob:0.11},{tok:" world",prob:0.07},{tok:" x",prob:0.04}], norm: 13.2 },
          { top5: [{tok:":",prob:0.49},{tok:" :",prob:0.16},{tok:" ->",prob:0.08},{tok:" {",prob:0.05},{tok:";",prob:0.03}], norm: 12.5 },
          { top5: [{tok:" print",prob:0.24},{tok:" return",prob:0.18},{tok:" pass",prob:0.07},{tok:" \"",prob:0.06},{tok:" #",prob:0.04}], norm: 12.9 }
        ],
        [
          { top5: [{tok:" hello",prob:0.18},{tok:" main",prob:0.11},{tok:" world",prob:0.09},{tok:" __",prob:0.08},{tok:" greet",prob:0.05}], norm: 16.4 },
          { top5: [{tok:"(",prob:0.34},{tok:" :",prob:0.18},{tok:" world",prob:0.10},{tok:" name",prob:0.05},{tok:" =",prob:0.04}], norm: 15.7 },
          { top5: [{tok:")",prob:0.34},{tok:" name",prob:0.16},{tok:" self",prob:0.13},{tok:" world",prob:0.06},{tok:" x",prob:0.04}], norm: 15.9 },
          { top5: [{tok:":",prob:0.50},{tok:" :",prob:0.18},{tok:" ->",prob:0.07},{tok:" {",prob:0.04},{tok:";",prob:0.03}], norm: 15.2 },
          { top5: [{tok:" print",prob:0.34},{tok:" return",prob:0.19},{tok:" pass",prob:0.08},{tok:" \"",prob:0.05},{tok:" #",prob:0.04}], norm: 15.4 }
        ],
        [
          { top5: [{tok:" hello",prob:0.16},{tok:" world",prob:0.10},{tok:" main",prob:0.10},{tok:" greet",prob:0.06},{tok:" __",prob:0.06}], norm: 19.5 },
          { top5: [{tok:"(",prob:0.30},{tok:" :",prob:0.20},{tok:" world",prob:0.13},{tok:" name",prob:0.06},{tok:" =",prob:0.04}], norm: 18.7 },
          { top5: [{tok:")",prob:0.40},{tok:" name",prob:0.18},{tok:" self",prob:0.10},{tok:" world",prob:0.05},{tok:" x",prob:0.03}], norm: 18.9 },
          { top5: [{tok:":",prob:0.52},{tok:" :",prob:0.18},{tok:" ->",prob:0.07},{tok:" {",prob:0.04},{tok:";",prob:0.02}], norm: 18.0 },
          { top5: [{tok:" print",prob:0.42},{tok:" return",prob:0.20},{tok:" pass",prob:0.08},{tok:" \"",prob:0.04},{tok:" #",prob:0.04}], norm: 18.2 }
        ],
        [
          { top5: [{tok:" hello",prob:0.15},{tok:" world",prob:0.11},{tok:" main",prob:0.10},{tok:" greet",prob:0.07},{tok:" world(",prob:0.04}], norm: 22.8 },
          { top5: [{tok:"(",prob:0.27},{tok:" :",prob:0.21},{tok:" world",prob:0.16},{tok:" name",prob:0.06},{tok:" =",prob:0.04}], norm: 22.0 },
          { top5: [{tok:")",prob:0.46},{tok:" name",prob:0.18},{tok:" self",prob:0.08},{tok:" world",prob:0.04},{tok:" x",prob:0.03}], norm: 22.1 },
          { top5: [{tok:":",prob:0.55},{tok:" :",prob:0.18},{tok:" ->",prob:0.06},{tok:" {",prob:0.03},{tok:";",prob:0.02}], norm: 21.1 },
          { top5: [{tok:" print",prob:0.50},{tok:" return",prob:0.18},{tok:" pass",prob:0.08},{tok:" \"",prob:0.04},{tok:" #",prob:0.03}], norm: 21.4 }
        ],
        [
          { top5: [{tok:" hello",prob:0.14},{tok:" world",prob:0.12},{tok:" main",prob:0.09},{tok:" greet",prob:0.08},{tok:" world(",prob:0.05}], norm: 26.4 },
          { top5: [{tok:"(",prob:0.25},{tok:" :",prob:0.21},{tok:" world",prob:0.18},{tok:" name",prob:0.06},{tok:" )",prob:0.04}], norm: 25.6 },
          { top5: [{tok:")",prob:0.51},{tok:" name",prob:0.18},{tok:" self",prob:0.07},{tok:" world",prob:0.04},{tok:" x",prob:0.03}], norm: 25.6 },
          { top5: [{tok:":",prob:0.59},{tok:" :",prob:0.17},{tok:" ->",prob:0.05},{tok:" {",prob:0.03},{tok:";",prob:0.02}], norm: 24.5 },
          { top5: [{tok:" print",prob:0.58},{tok:" return",prob:0.16},{tok:" pass",prob:0.08},{tok:" \"",prob:0.04},{tok:" #",prob:0.03}], norm: 24.9 }
        ]
      ]
    }
  };

  // expose for grand-tour demo to reuse
  window.__rs_runs = RUNS;

  var statusEl = root.querySelector("[data-rs-status]");
  var gridEl = root.querySelector("[data-rs-grid]");
  var detailEl = root.querySelector("[data-rs-detail]");
  var addrEl = root.querySelector("[data-rs-detail-addr]");
  var tokEl = root.querySelector("[data-rs-detail-tok]");
  var topkEl = root.querySelector("[data-rs-topk]");
  var normEl = root.querySelector("[data-rs-norm]");
  var cosEl = root.querySelector("[data-rs-cos]");

  var current = null;

  function escapeHtml(s){ return String(s).replace(/[&<>"']/g, function(c){ return ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c]; }); }
  function lerpHex(a, b, t){
    var ar=parseInt(a.slice(1,3),16), ag=parseInt(a.slice(3,5),16), ab=parseInt(a.slice(5,7),16);
    var br=parseInt(b.slice(1,3),16), bg=parseInt(b.slice(3,5),16), bb=parseInt(b.slice(5,7),16);
    var rr=Math.round(ar+(br-ar)*t), rg=Math.round(ag+(bg-ag)*t), rb=Math.round(ab+(bb-ab)*t);
    return "#"+[rr,rg,rb].map(function(n){ return n.toString(16).padStart(2,"0"); }).join("");
  }
  function heatColor(p){
    p = Math.min(1, Math.max(0, p));
    return p < 0.5 ? lerpHex("#fff7e6","#fcd68b",p/0.5) : lerpHex("#fcd68b","#b77214",(p-0.5)/0.5);
  }

  function cosineFromNorms(layerIdx, posIdx, run){
    if (layerIdx === 0) return null;
    // synthetic but realistic: small residual updates produce cosines ~0.93-0.99,
    // larger updates (block 3-4 doing semantic work) dip slightly
    var midDip = 1 - 0.04 * Math.exp(-Math.pow(layerIdx - 3.5, 2) / 2.5);
    var perPosNoise = 0.005 * Math.sin(posIdx * 1.7 + layerIdx * 0.9);
    return Math.max(0.85, Math.min(0.999, midDip + perPosNoise));
  }

  function render(runKey){
    var run = RUNS[runKey];
    current = { run: run, key: runKey };
    var nLayers = run.layers.length;
    var nTokens = run.tokens.length;

    var html = "<thead><tr><th>Layer</th>";
    for (var p=0; p<nTokens; p++){
      html += "<th title=\""+escapeHtml(run.tokens[p])+"\">"+escapeHtml(run.tokens[p].trim() || "·")+"</th>";
    }
    html += "</tr></thead><tbody>";
    for (var l=nLayers-1; l>=0; l--){
      var label = l===0 ? "emb" : "blk "+(l-1);
      html += "<tr><th>"+label+"</th>";
      for (var p2=0; p2<nTokens; p2++){
        var top = run.layers[l][p2].top5[0];
        var bg = heatColor(top.prob);
        var cls = l === nLayers-1 ? "is-final" : "";
        var tokDisp = top.tok.trim().slice(0, 10) || "·";
        html += "<td class=\""+cls+"\" data-l=\""+l+"\" data-p=\""+p2+"\" style=\"background:"+bg+"\" title=\""+escapeHtml(top.tok)+" ("+(top.prob*100).toFixed(0)+"%)\">"+escapeHtml(tokDisp)+"</td>";
      }
      html += "</tr>";
    }
    html += "</tbody>";
    gridEl.innerHTML = html;

    var cells = gridEl.querySelectorAll("td[data-l]");
    cells.forEach(function(td){
      td.addEventListener("click", function(){
        showDetail(parseInt(td.dataset.l), parseInt(td.dataset.p));
      });
    });

    statusEl.textContent = "Showing "+nTokens+" tokens × "+nLayers+" layers. Click any cell to inspect.";

    // auto-show last cell
    showDetail(nLayers-1, nTokens-1);
  }

  function showDetail(l, p){
    if (!current) return;
    var run = current.run;
    gridEl.querySelectorAll("td.is-active").forEach(function(t){ t.classList.remove("is-active"); });
    var cell = gridEl.querySelector("td[data-l=\""+l+"\"][data-p=\""+p+"\"]");
    if (cell) cell.classList.add("is-active");

    var label = l===0 ? "Embedding" : "Block "+(l-1)+" output";
    addrEl.textContent = "Layer "+l+" ("+label+") · Position "+p;
    tokEl.textContent = JSON.stringify(run.tokens[p]);

    var entry = run.layers[l][p];
    topkEl.innerHTML = "";
    entry.top5.forEach(function(t){
      var li = document.createElement("li");
      var pctW = Math.max(3, Math.round(t.prob * 100));
      li.innerHTML =
        "<span class=\"rs-topk__tok\">"+escapeHtml(t.tok.trim() || "·")+"</span>"+
        "<span class=\"rs-topk__bar\"><span class=\"rs-topk__bar-fill\" style=\"width:"+pctW+"%\"></span></span>"+
        "<span class=\"rs-topk__pct\">"+(t.prob*100).toFixed(1)+"%</span>";
      topkEl.appendChild(li);
    });
    normEl.textContent = entry.norm.toFixed(2);
    var c = cosineFromNorms(l, p, run);
    cosEl.textContent = c == null ? ",  (first layer)" : c.toFixed(3);
  }

  root.querySelectorAll("[data-rs-preset]").forEach(function(b){
    b.addEventListener("click", function(){
      root.querySelectorAll("[data-rs-preset]").forEach(function(x){ x.classList.remove("is-active"); });
      b.classList.add("is-active");
      render(b.dataset.rsPreset);
    });
  });

  render("paris");
})();
</script>

<p>Each cell shows the model’s top-1 prediction at layer $\ell$, position $t$. Saturation = confidence. Click a cell for the full top-5 distribution and the residual norm.</p>

<h2 id="formal-definition">Formal definition</h2>

<p>For a transformer with $L$ blocks operating on $T$ tokens, the residual stream is the sequence of states ${X_0, X_1, \ldots, X_L}$, each $X_\ell \in \mathbb{R}^{T \times d_\text{model}}$.</p>

<p><strong>Initial state:</strong> $X_0 = E + P$ where $E$ is the token embedding and $P$ is the positional encoding (or $X_0 = E$ for models with rotary/RoPE applied inside attention).</p>

<p><strong>Block update:</strong></p>

\[X_{\ell+1} = X_\ell + \text{Attn}_\ell(\text{LN}(X_\ell)) + \text{MLP}_\ell(\text{LN}(X_\ell + \text{Attn}_\ell(\text{LN}(X_\ell))))\]

<p>The layer norms ($\text{LN}$) appear inside each sublayer in the <strong>pre-norm</strong> configuration used by GPT-2, Llama, and most modern models. Schematically:</p>

\[X_{\ell+1} = X_\ell + \Delta_\ell^\text{attn} + \Delta_\ell^\text{mlp}\]

<p><strong>Final read:</strong> logits = $\text{LN}(X_L) \cdot W_U$, where $W_U \in \mathbb{R}^{d_\text{model} \times V}$ is the unembedding matrix.</p>

<p>The architectural choice that everything depends on is the <code class="language-plaintext highlighter-rouge">+</code>: each $\Delta$ is <em>added</em>, never substituted.</p>

<h2 id="why-additive-matters">Why additive matters</h2>

<p>Compare the residual update with a non-residual update $X_{\ell+1} = f_\ell(X_\ell)$. Two structural problems with the latter:</p>

<p><strong>Vanishing information.</strong> Any signal computed at layer $\ell$ must be re-encoded by $f_{\ell+1}, f_{\ell+2}, \ldots$ to survive. After 30 layers of arbitrary nonlinear transformations, layer-1 signals are effectively destroyed.</p>

<p><strong>Vanishing gradients.</strong> Backprop multiplies gradients through every $f_\ell$. With layer-norm and sigmoid/tanh nonlinearities the gradient norm shrinks geometrically. Pre-2015 networks rarely trained stably past 20 layers.</p>

<p>Residual connections (<a href="https://arxiv.org/abs/1512.03385">He et al., 2015</a>) solve both: $X_{\ell+1} = X_\ell + f_\ell(X_\ell)$ has an identity path from layer $\ell$ to $\ell+1$. Information and gradients flow through the <code class="language-plaintext highlighter-rouge">+</code> without distortion. This is what made GPT-2’s 48-layer and GPT-3’s 96-layer networks trainable.</p>

<p>For interpretability, the additive structure has a stronger consequence: the final state is <em>literally</em> a sum.</p>

\[X_L = X_0 + \sum_{\ell=0}^{L-1} \Delta_\ell^\text{attn} + \sum_{\ell=0}^{L-1} \Delta_\ell^\text{mlp}\]

<p>Every component’s contribution is a linear term. This makes the residual stream <strong>linearly decomposable</strong>.</p>

<aside class="callout callout--analogy">
  <div class="callout__label">Analogy</div>
  <p>A shared document where every editor can only add comments, not delete. The final document is the sum of all contributions. To attribute the final state to one editor, look at their diff.</p>
</aside>

<h2 id="the-logit-lens">The logit lens</h2>

<p>Final-layer logits are computed as $\text{LN}(X_L) \cdot W_U$. Because every $X_\ell$ lives in $\mathbb{R}^{T \times d_\text{model}}$, the same projection is well-defined at every layer:</p>

\[\text{logits}_\ell := \text{LN}(X_\ell) \cdot W_U\]

<p><a href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens">Nostalgebraist (2020)</a> called this the <strong>logit lens</strong>. It returns the model’s “current best guess” at each intermediate layer, treating the partial residual stream as if it were the final state.</p>

<p>Empirically (visible in the demo above for “Paris is the capital of”):</p>

<ul>
  <li>Layers 0–2: predictions are close to a unigram distribution. The model has not yet aggregated context.</li>
  <li>Layers 3–6: top-k starts ranking semantically related tokens (countries, cities).</li>
  <li>Layers 7–11: the correct answer (<code class="language-plaintext highlighter-rouge">France</code>) reaches top-1 with high probability.</li>
</ul>

<p>The lens is not exact, intermediate $X_\ell$ has different statistics than $X_L$, but it is informative and free. Refinements include the <strong>tuned lens</strong> (<a href="https://arxiv.org/abs/2303.08112">Belrose et al., 2023</a>), which learns a per-layer affine correction.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>The logit lens is a training-free decoder for any layer's residual stream. Activation patching, direct logit attribution, and most circuit-discovery techniques inherit from this idea. Internalizing it makes the literature readable.</p>
</aside>

<h2 id="direct-logit-attribution-dla">Direct logit attribution (DLA)</h2>

<p>Because $X_L$ is a sum, the logit for any vocabulary token $w$ is also a sum:</p>

\[\text{logit}(w) = (X_0 \cdot W_U[:, w]) + \sum_{\ell=0}^{L-1} (\Delta_\ell^\text{attn} \cdot W_U[:, w]) + \sum_{\ell=0}^{L-1} (\Delta_\ell^\text{mlp} \cdot W_U[:, w])\]

<p>Each term is a scalar: how much that component pushed the prediction toward $w$. This is <strong>direct logit attribution</strong>.</p>

<p>Practical use:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in TransformerLens
</span><span class="kn">import</span> <span class="nn">transformer_lens</span> <span class="k">as</span> <span class="n">tl</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">tl</span><span class="p">.</span><span class="n">HookedTransformer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"gpt2"</span><span class="p">)</span>
<span class="n">tokens</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">to_tokens</span><span class="p">(</span><span class="s">"Paris is the capital of"</span><span class="p">)</span>
<span class="n">logits</span><span class="p">,</span> <span class="n">cache</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">run_with_cache</span><span class="p">(</span><span class="n">tokens</span><span class="p">)</span>

<span class="c1"># decompose final residual stream into per-component contributions
</span><span class="n">per_layer</span> <span class="o">=</span> <span class="n">cache</span><span class="p">.</span><span class="n">decompose_resid</span><span class="p">(</span><span class="n">layer</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">return_labels</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="c1"># project each onto W_U for the answer token
</span><span class="n">answer_id</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">to_single_token</span><span class="p">(</span><span class="s">" France"</span><span class="p">)</span>
<span class="n">W_U</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">W_U</span><span class="p">[:,</span> <span class="n">answer_id</span><span class="p">]</span>
<span class="n">contributions</span> <span class="o">=</span> <span class="n">per_layer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">@</span> <span class="n">W_U</span>  <span class="c1"># one scalar per component
</span></code></pre></div></div>

<p>The largest entries in <code class="language-plaintext highlighter-rouge">contributions</code> identify the layers/heads/MLPs that drove the answer. DLA is the starting point for circuit analysis: keep zooming in (head → query/key/value → input neurons) until you have a mechanism.</p>

<div class="idemo idemo--mini" id="demo-dla">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Direct logit attribution, layer by layer</span></div>
    <div class="idemo__body">

      <div class="dla-howto">
        <div class="dla-howto__title">How to play</div>
        <ol class="dla-howto__list">
          <li>Pick a <b>prompt</b> — each one has 2–4 candidate tokens fighting to win.</li>
          <li>Watch the <b>per-layer bars</b>: how much each layer pushes each candidate up or down.</li>
          <li>The <b>cumulative line</b> on the right shows the running logit total — that's the actual logit that becomes the prediction.</li>
          <li><b>Hover or click a layer</b> to see which circuit component (name-mover head, induction head, MLP) lives there.</li>
          <li>Hit <b>play</b> to accumulate one layer at a time and watch the answer emerge.</li>
        </ol>
      </div>

      <div class="dla-prompts" data-dla-prompts=""></div>

      <div class="dla-meta">
        <div class="dla-meta__row"><span class="dla-meta__lbl">prompt</span><strong data-dla-prompt=""></strong></div>
        <div class="dla-meta__row"><span class="dla-meta__lbl">winner</span><strong data-dla-winner=""></strong></div>
      </div>

      <div class="dla-stage">
        <div class="dla-stage__legend" data-dla-legend=""></div>

        <div class="dla-grid">
          <div class="dla-grid__col-label">per-layer contribution to logit</div>
          <div class="dla-grid__col-label">cumulative</div>

          <div class="dla-bars" data-dla-bars=""></div>
          <div class="dla-cumulative" data-dla-cumulative="">
            <canvas data-dla-canvas="" width="240" height="200"></canvas>
          </div>

          <div class="dla-axis"><span>L0</span><span>layer →</span><span>final</span></div>
          <div class="dla-axis dla-axis--right">final logits</div>
        </div>

        <div class="dla-component">
          <div class="dla-component__lbl">selected component</div>
          <div class="dla-component__body" data-dla-component="">hover any bar to inspect →</div>
        </div>
      </div>

      <div class="dla-controls">
        <button class="dla-btn dla-btn--primary" data-dla-play="">▶ accumulate</button>
        <input type="range" min="1" max="12" value="12" step="1" data-dla-cursor="" class="dla-slider" />
        <span class="dla-step" data-dla-step="">through L12</span>
        <button class="dla-btn" data-dla-reset="">↺ reset</button>
      </div>

      <p class="dla-hint"><b>What to notice:</b> the cumulative line is what the model's prediction <i>actually depends on</i>. Tiny early-layer bumps don't matter; the big middle/late layers (where name-movers, induction heads, and MLP fact-recall live) are doing the real work. Negative bars in late layers are <b>negative name-movers</b> — components that <i>suppress</i> the answer to keep calibration honest. This is the entire game of <a href="https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html" target="_blank" rel="noopener">circuit attribution</a>.</p>
    </div>
  </div>
</div>

<style>
  #demo-dla .dla-howto {
    background: #fffaf3; border: 1px solid #ddb88e; border-radius: 4px;
    padding: 0.7rem 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-dla .dla-howto__title {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #7c4d0a; margin-bottom: 0.35rem; font-weight: 600;
  }
  #demo-dla .dla-howto__list {
    margin: 0; padding: 0 0 0 1.1rem; font-size: 0.9rem;
    color: var(--nn-body); line-height: 1.6;
  }
  #demo-dla .dla-howto__list li b { color: #7c4d0a; }

  #demo-dla .dla-prompts {
    display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.7rem;
  }
  #demo-dla .dla-prompt-btn {
    padding: 0.4rem 0.75rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-dla .dla-prompt-btn:hover { border-color: #b77214; }
  #demo-dla .dla-prompt-btn.is-active { background: #b77214; color: #fff; border-color: #b77214; }

  #demo-dla .dla-meta {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.5rem 0.75rem; margin-bottom: 0.55rem;
    display: flex; flex-direction: column; gap: 0.18rem;
  }
  #demo-dla .dla-meta__row {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted);
    display: flex; gap: 0.55rem; align-items: baseline;
  }
  #demo-dla .dla-meta__lbl { width: 60px; text-transform: uppercase; font-size: 0.66rem; letter-spacing: 0.08em; }
  #demo-dla .dla-meta__row strong { color: var(--nn-ink); font-weight: 600; }

  #demo-dla .dla-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.7rem;
  }
  #demo-dla .dla-stage__legend {
    display: flex; gap: 0.7rem; flex-wrap: wrap; margin-bottom: 0.7rem;
  }
  #demo-dla .dla-leg {
    display: flex; align-items: center; gap: 0.32rem;
    font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-body);
  }
  #demo-dla .dla-leg__dot {
    width: 12px; height: 12px; border-radius: 2px; border: 1px solid var(--nn-line);
  }
  #demo-dla .dla-leg.is-winner .dla-leg__dot { box-shadow: 0 0 0 1px #7c4d0a; }

  #demo-dla .dla-grid {
    display: grid; grid-template-columns: 1fr 240px;
    column-gap: 0.7rem; row-gap: 0.25rem; align-items: end;
  }
  #demo-dla .dla-grid__col-label {
    font-family: var(--nn-mono); font-size: 0.65rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
    padding-bottom: 0.25rem;
  }

  #demo-dla .dla-bars {
    display: grid; gap: 4px; height: 200px; align-items: stretch;
    background: linear-gradient(to bottom, transparent calc(50% - 0.5px), rgba(0,0,0,0.18) 50%, transparent calc(50% + 0.5px));
  }
  #demo-dla .dla-col {
    position: relative; height: 100%;
    display: flex; flex-direction: column; justify-content: center;
    cursor: pointer; transition: filter 120ms;
  }
  #demo-dla .dla-col:hover { filter: brightness(1.1); }
  #demo-dla .dla-col.is-selected { box-shadow: 0 0 0 2px #0d6b3a; border-radius: 3px; }
  #demo-dla .dla-col.is-faded { opacity: 0.25; }
  #demo-dla .dla-col__stack {
    position: absolute; left: 1px; right: 1px;
    display: flex; gap: 1px;
  }
  #demo-dla .dla-col__stack--pos { top: 0; height: 50%; align-items: flex-end; }
  #demo-dla .dla-col__stack--neg { bottom: 0; height: 50%; align-items: flex-start; }
  #demo-dla .dla-cand {
    flex: 1; min-height: 1px; border-radius: 2px;
    transition: height 220ms cubic-bezier(.3,.5,.3,1);
  }
  #demo-dla .dla-col__num {
    position: absolute; left: 0; right: 0; text-align: center;
    bottom: -16px;
    font-family: var(--nn-mono); font-size: 0.6rem; color: var(--nn-muted);
  }

  #demo-dla .dla-cumulative {
    height: 200px;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    position: relative; padding: 4px;
  }
  #demo-dla .dla-cumulative canvas {
    width: 100% !important; height: 100% !important; display: block;
  }

  #demo-dla .dla-axis {
    display: flex; justify-content: space-between; padding-top: 18px;
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  #demo-dla .dla-axis--right { justify-content: center; }

  #demo-dla .dla-component {
    margin-top: 0.85rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.55rem 0.75rem;
    display: grid; grid-template-columns: 100px 1fr; gap: 0.7rem;
  }
  #demo-dla .dla-component__lbl {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
    padding-top: 0.2rem;
  }
  #demo-dla .dla-component__body {
    font-family: var(--nn-mono); font-size: 0.8rem; color: var(--nn-ink);
    line-height: 1.5;
  }
  #demo-dla .dla-component__body b { color: #7c4d0a; }
  #demo-dla .dla-component__body em { color: var(--nn-muted); font-style: normal; }

  #demo-dla .dla-controls {
    display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
    margin-bottom: 0.7rem;
  }
  #demo-dla .dla-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-dla .dla-btn:hover { border-color: #b77214; }
  #demo-dla .dla-btn--primary { background: #b77214; color: #fff; border-color: #b77214; }
  #demo-dla .dla-btn--primary:hover { background: #7c4d0a; }
  #demo-dla .dla-btn.is-running { background: #7c4d0a; color: #fff; border-color: #7c4d0a; }
  #demo-dla .dla-slider { flex: 1; min-width: 140px; accent-color: #b77214; }
  #demo-dla .dla-step {
    font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
    min-width: 90px; text-align: right;
  }
  #demo-dla .dla-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-dla .dla-hint b { color: #7c4d0a; }
  #demo-dla .dla-hint a { color: #7c4d0a; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-dla"); if (!root) return;

  // Each prompt: 12-layer contributions for each candidate token,
  // plus a per-layer "component" label (which circuit lives there).
  var COLORS = ["#b77214", "#0d6b3a", "#b25c2c", "#7c4d0a"];

  var PROMPTS = [
    {
      key: "paris",
      label: "Paris is the capital of",
      candidates: [
        { tok: "France", contribs: [0.04, 0.06, 0.08, 0.12, 0.22, 0.45, 0.85, 1.30, 1.55, 1.05, 0.40, 0.10] },
        { tok: "Italy",  contribs: [0.02, 0.03, 0.05, 0.08, 0.12, 0.18, 0.20, 0.18, 0.08, 0.02, 0.01, 0.00] },
        { tok: "Germany",contribs: [0.02, 0.03, 0.05, 0.07, 0.10, 0.14, 0.16, 0.12, 0.05, 0.01, 0.00, -0.02] }
      ],
      components: [
        "embed + early MLP, encodes the literal Paris token",
        "syntax — \"is the capital of\" template detector",
        "subject MLP, starts looking up city facts",
        "MLP, country–city association strengthens",
        "MLP, factual recall begins",
        "<b>fact MLP block</b>, big France boost",
        "<b>fact MLP block</b>, peak factual lookup",
        "<b>fact MLP block</b>, late factual recall",
        "fact MLP, final clean-up of France logit",
        "smoothing layer, slight dampening",
        "calibration, suppresses runner-up countries",
        "final unembed, output projection"
      ]
    },
    {
      key: "ioi",
      label: "When John and Mary went to the store, John gave a drink to",
      candidates: [
        { tok: "Mary", contribs: [0.02, 0.04, 0.08, 0.12, 0.18, 0.30, 0.45, 0.55, 0.95, 1.85, 1.55, -0.45] },
        { tok: "John", contribs: [0.05, 0.08, 0.12, 0.18, 0.20, 0.22, 0.18, 0.10, -0.10, -0.65, -0.35, 0.20] },
        { tok: "the",  contribs: [0.10, 0.12, 0.15, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.02, 0.05] },
        { tok: "him",  contribs: [0.04, 0.05, 0.06, 0.07, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01, 0.01, 0.02] }
      ],
      components: [
        "token & position embed",
        "early syntax — preposition tracking",
        "duplicate token detector starts",
        "<b>duplicate token head</b> fires on \"John\"",
        "S-Inhibition Head builds up",
        "<b>S-Inhibition Head</b> fully suppresses subject John",
        "Backup Name-Mover prep",
        "Mover head warm-up",
        "<b>Name-Mover Head 9.6</b> begins copying Mary",
        "<b>Name-Mover Head 9.6</b>, peak attribution",
        "<b>Name-Mover Head 10.0</b>, supporting copy",
        "<b>Negative Name-Mover</b>, calibration suppression"
      ]
    },
    {
      key: "induction",
      label: "A B C D … A B C →",
      candidates: [
        { tok: "D", contribs: [0.04, 0.10, 0.20, 0.30, 0.55, 1.20, 1.50, 0.95, 0.55, 0.30, 0.18, 0.05] },
        { tok: "A", contribs: [0.05, 0.06, 0.08, 0.07, 0.04, 0.02, 0.00, -0.05, -0.08, -0.05, -0.02, 0.00] },
        { tok: "?", contribs: [0.04, 0.05, 0.06, 0.05, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.00, 0.00] }
      ],
      components: [
        "char embed",
        "early position info",
        "<b>Previous-Token Head</b> warm-up",
        "<b>Previous-Token Head</b> aligns",
        "<b>Induction Head prep</b>",
        "<b>Induction Head 5.x</b>, prefix-match copy",
        "<b>Induction Head 6.x</b>, peak attribution",
        "induction signal still propagating",
        "stabilizing the choice",
        "calibration",
        "calibration",
        "unembed"
      ]
    }
  ];

  var promptsEl = root.querySelector("[data-dla-prompts]");
  var promptEl  = root.querySelector("[data-dla-prompt]");
  var winnerEl  = root.querySelector("[data-dla-winner]");
  var legendEl  = root.querySelector("[data-dla-legend]");
  var barsEl    = root.querySelector("[data-dla-bars]");
  var canvas    = root.querySelector("[data-dla-canvas]");
  var ctx       = canvas.getContext("2d");
  var cmpEl     = root.querySelector("[data-dla-component]");
  var playBtn   = root.querySelector("[data-dla-play]");
  var slider    = root.querySelector("[data-dla-cursor]");
  var stepEl    = root.querySelector("[data-dla-step]");
  var resetBtn  = root.querySelector("[data-dla-reset]");

  var active = 0;
  var cursor = 12; // how many layers are "accumulated" so far
  var hovered = -1;
  var playing = false;
  var playTimer = null;

  function renderTabs(){
    var html = "";
    PROMPTS.forEach(function(p, i){
      html += "<button class=\"dla-prompt-btn"+(i === active ? " is-active" : "")+"\" data-i=\""+i+"\">"+p.key+"</button>";
    });
    promptsEl.innerHTML = html;
    promptsEl.querySelectorAll(".dla-prompt-btn").forEach(function(b){
      b.addEventListener("click", function(){
        active = +b.getAttribute("data-i");
        cursor = 12; slider.value = 12; stopPlay();
        renderAll();
      });
    });
  }

  function renderLegend(){
    var p = PROMPTS[active];
    var html = "";
    var winner = computeWinner(p, cursor);
    p.candidates.forEach(function(c, i){
      var isW = i === winner;
      html += "<div class=\"dla-leg"+(isW ? " is-winner" : "")+"\">"+
        "<span class=\"dla-leg__dot\" style=\"background:"+COLORS[i]+"\"></span>"+
        "<span>"+esc(c.tok)+(isW ? " ← winning" : "")+"</span></div>";
    });
    legendEl.innerHTML = html;
  }

  function computeWinner(p, n){
    var sums = p.candidates.map(function(c){
      var s = 0; for (var k = 0; k < n; k++) s += c.contribs[k]; return s;
    });
    var w = 0; for (var i = 1; i < sums.length; i++) if (sums[i] > sums[w]) w = i;
    return w;
  }

  function renderBars(){
    var p = PROMPTS[active];
    barsEl.style.gridTemplateColumns = "repeat(12, 1fr)";

    // Find global max-abs across all candidates and layers for this prompt
    var maxAbs = 0;
    p.candidates.forEach(function(c){ c.contribs.forEach(function(v){ if (Math.abs(v) > maxAbs) maxAbs = Math.abs(v); }); });
    if (maxAbs === 0) maxAbs = 1;

    var html = "";
    for (var L = 0; L < 12; L++){
      var posStack = "", negStack = "";
      p.candidates.forEach(function(c, ci){
        var v = c.contribs[L];
        var pct = (Math.abs(v) / maxAbs) * 100; // % of half-height
        if (v >= 0){
          posStack += "<div class=\"dla-cand\" style=\"height:"+pct.toFixed(1)+"%; background:"+COLORS[ci]+"\" title=\""+esc(c.tok)+": "+v.toFixed(2)+"\"></div>";
        } else {
          negStack += "<div class=\"dla-cand\" style=\"height:"+pct.toFixed(1)+"%; background:"+COLORS[ci]+"; opacity:0.55\" title=\""+esc(c.tok)+": "+v.toFixed(2)+"\"></div>";
        }
      });
      var faded = (L >= cursor ? " is-faded" : "");
      var sel = (L === hovered ? " is-selected" : "");
      html += "<div class=\"dla-col"+faded+sel+"\" data-l=\""+L+"\">"+
        "<div class=\"dla-col__stack dla-col__stack--pos\">"+posStack+"</div>"+
        "<div class=\"dla-col__stack dla-col__stack--neg\">"+negStack+"</div>"+
        "<div class=\"dla-col__num\">"+L+"</div>"+
        "</div>";
    }
    barsEl.innerHTML = html;
    barsEl.querySelectorAll(".dla-col").forEach(function(c){
      c.addEventListener("mouseenter", function(){ hovered = +c.dataset.l; renderBars(); renderComponent(); });
      c.addEventListener("mouseleave", function(){ hovered = -1; renderBars(); renderComponent(); });
      c.addEventListener("click", function(){ cursor = +c.dataset.l + 1; slider.value = cursor; stopPlay(); renderAll(); });
    });
  }

  function renderCumulative(){
    var p = PROMPTS[active];
    var w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    // Background grid
    ctx.fillStyle = "#fffefb";
    ctx.fillRect(0, 0, w, h);
    ctx.strokeStyle = "rgba(0,0,0,0.07)";
    ctx.lineWidth = 1;
    for (var gy = 0; gy <= 4; gy++){
      var y = (gy/4) * h;
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
    }
    // zero line
    ctx.strokeStyle = "rgba(0,0,0,0.25)";
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(0, h/2); ctx.lineTo(w, h/2); ctx.stroke();

    // Compute cumulative sums for each candidate
    var cums = p.candidates.map(function(c){
      var arr = [0], s = 0;
      for (var i = 0; i < 12; i++){ s += c.contribs[i]; arr.push(s); }
      return arr;
    });
    var maxAbs = 0.5;
    cums.forEach(function(a){ a.forEach(function(v){ if (Math.abs(v) > maxAbs) maxAbs = Math.abs(v); }); });

    // Draw each candidate line up to cursor
    p.candidates.forEach(function(c, ci){
      ctx.strokeStyle = COLORS[ci];
      ctx.lineWidth = 2.2;
      ctx.beginPath();
      for (var i = 0; i <= cursor; i++){
        var x = (i / 12) * (w - 8) + 4;
        var y = h/2 - (cums[ci][i] / maxAbs) * (h/2 - 8);
        if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
      }
      ctx.stroke();

      // dot at current cursor
      var xe = (cursor / 12) * (w - 8) + 4;
      var ye = h/2 - (cums[ci][cursor] / maxAbs) * (h/2 - 8);
      ctx.fillStyle = COLORS[ci];
      ctx.beginPath(); ctx.arc(xe, ye, 3.2, 0, Math.PI*2); ctx.fill();

      // label at right
      ctx.fillStyle = COLORS[ci];
      ctx.font = "11px ui-monospace, Menlo, monospace";
      ctx.textAlign = "left";
      var labelY = ye;
      if (labelY < 12) labelY = 12; if (labelY > h - 4) labelY = h - 4;
      ctx.fillText(c.tok, Math.min(xe + 5, w - 36), labelY);
    });
  }

  function renderComponent(){
    var p = PROMPTS[active];
    var L = hovered >= 0 ? hovered : cursor - 1;
    if (L < 0) L = 0;
    var contribStr = p.candidates.map(function(c, i){
      return "<span style=\"color:"+COLORS[i]+"\">"+esc(c.tok)+"</span>=" + c.contribs[L].toFixed(2);
    }).join(" · ");
    cmpEl.innerHTML = "<b>L"+L+"</b> · " + p.components[L] + "<br><em>" + contribStr + "</em>";
  }

  function renderMeta(){
    var p = PROMPTS[active];
    promptEl.textContent = "\"" + p.label + "\"";
    var w = computeWinner(p, cursor);
    winnerEl.innerHTML = "<span style=\"color:"+COLORS[w]+"\">"+esc(p.candidates[w].tok)+"</span> through L" + cursor;
    stepEl.textContent = "through L" + cursor;
  }

  function renderAll(){
    renderTabs();
    renderLegend();
    renderBars();
    renderCumulative();
    renderComponent();
    renderMeta();
  }

  function startPlay(){
    if (playing) return;
    playing = true;
    playBtn.textContent = "■ pause";
    playBtn.classList.add("is-running");
    cursor = 0; slider.value = 0; renderAll();
    var tick = function(){
      if (!playing) return;
      cursor = Math.min(12, cursor + 1);
      slider.value = cursor;
      renderAll();
      if (cursor >= 12){ stopPlay(); return; }
      playTimer = setTimeout(tick, 320);
    };
    playTimer = setTimeout(tick, 320);
  }
  function stopPlay(){
    playing = false;
    playBtn.textContent = "▶ accumulate";
    playBtn.classList.remove("is-running");
    if (playTimer){ clearTimeout(playTimer); playTimer = null; }
  }

  function esc(s){ return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }

  slider.addEventListener("input", function(){ cursor = +slider.value; stopPlay(); renderAll(); });
  playBtn.addEventListener("click", function(){ if (playing) stopPlay(); else startPlay(); });
  resetBtn.addEventListener("click", function(){ cursor = 12; slider.value = 12; stopPlay(); renderAll(); });

  renderAll();
})();
</script>

<h2 id="subspaces-and-superposition">Subspaces and superposition</h2>

<p>The stream has $d_\text{model}$ dimensions but generally encodes far more <em>features</em> than that. Components write to and read from <strong>subspaces</strong> of the stream, generally not axis-aligned.</p>

<p><a href="https://transformer-circuits.pub/2022/toy_model/index.html">Elhage et al. (2022, “Superposition”)</a> characterize this: when features are sparse (most are off most of the time), a $d$-dim space can represent ~$d / \log d$ features by overlapping them. The cost is interference: reading one feature picks up small projections from others.</p>

<p>Consequences:</p>

<ol>
  <li>Single neurons are typically <strong>polysemantic</strong> (active for multiple unrelated concepts).</li>
  <li>Single residual coordinates are not interpretable; <em>directions</em> are.</li>
  <li><strong>Sparse autoencoders (SAEs)</strong> (<a href="https://transformer-circuits.pub/2023/monosemantic-features/index.html">Bricken et al., 2023</a>; <a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html">Templeton et al., 2024</a>) recover interpretable directions by training an overcomplete dictionary on cached residual streams.</li>
</ol>

<p>Provisional model: think of the stream as a high-dimensional space where many features overlap, recoverable by linear probes or SAEs but not by reading individual coordinates.</p>

<h2 id="reading-the-heatmap">Reading the heatmap</h2>

<p>In the demo, watch:</p>

<ol>
  <li><strong>Vertical evolution.</strong> The same column (token position) refines its top prediction across layers.</li>
  <li><strong>Horizontal differences.</strong> Earlier positions are <em>not</em> trying to predict the next token, they are accumulating information that attention will later pull into the final position. Their logit-lens predictions are largely incidental.</li>
  <li><strong>Final column saturation.</strong> This is where the actual next-token prediction happens. Saturation increases monotonically (with rare exceptions in degenerate prompts).</li>
</ol>

<h2 id="bos-and-attention-sinks">BOS and attention sinks</h2>

<p>The first token’s residual stream typically accumulates “housekeeping” state. Attention heads with no relevant key in a given query often place mass on the BOS token as a default, the <strong>attention sink</strong> (<a href="https://arxiv.org/abs/2309.17453">Xiao et al., 2023</a>). <a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html">Templeton et al. (2024)</a> found that BOS-position SAE features are systematically distinct from content-position features.</p>

<p>Treat the BOS column as anomalous when interpreting diagnostics.</p>

<h2 id="the-unifying-claim">The unifying claim</h2>

<blockquote>
  <p>Every transformer mechanism can be expressed as “component $C$ reads from subspace $R$ of the residual stream and writes to subspace $W$ of the residual stream.”</p>
</blockquote>

<p>Examples:</p>
<ul>
  <li><strong>Copy heads</strong> (attention): read content from position $i$, write the same content to position $j$.</li>
  <li><strong>Induction heads</strong>: read a match-detection signal at the previous position, write a “copy this token” signal at the current.</li>
  <li><strong>Factual-recall MLPs</strong> (<a href="https://arxiv.org/abs/2202.05262">Meng et al., 2022, ROME</a>): read subject embedding from subject tokens, write attribute information back.</li>
  <li><strong>IOI circuit</strong> (<a href="https://arxiv.org/abs/2211.00593">Wang et al., 2022</a>): a chain of read/write heads juggling name and position information.</li>
</ul>

<div class="idemo idemo--mini" id="demo-patch">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Activation patching, find where the answer lives</span></div>
    <div class="idemo__body">

      <div class="ap-howto">
        <div class="ap-howto__title">How to play</div>
        <ol class="ap-howto__list">
          <li>Pick an <b>example pair</b> below — clean prompt vs. corrupt prompt that flip the model's answer.</li>
          <li><b>Click any cell</b> in the layer × position heatmap to patch the residual stream at that (layer, token) from the corrupt run into the clean run.</li>
          <li>Watch the prediction probabilities shift. <b>Hot cells = answer-relevant computation lives there.</b></li>
          <li>Or hit <b>auto-scan</b> to sweep every cell and let the heatmap paint itself.</li>
        </ol>
      </div>

      <div class="ap-examples" data-ap-examples=""></div>

      <div class="ap-prompts">
        <div class="ap-prompt ap-prompt--clean">
          <span class="ap-prompt__lbl">clean</span>
          <span class="ap-prompt__text" data-ap-clean-text="">—</span>
          <span class="ap-prompt__ans">→ <strong data-ap-clean-ans="">—</strong></span>
        </div>
        <div class="ap-prompt ap-prompt--corrupt">
          <span class="ap-prompt__lbl">corrupt</span>
          <span class="ap-prompt__text" data-ap-corrupt-text="">—</span>
          <span class="ap-prompt__ans">→ <strong data-ap-corrupt-ans="">—</strong></span>
        </div>
      </div>

      <div class="ap-grid-wrap">
        <div class="ap-grid-axis ap-grid-axis--y">
          <div class="ap-grid-axis__label">layer</div>
        </div>
        <div class="ap-grid-main">
          <div class="ap-grid-tokens" data-ap-tokens=""></div>
          <div class="ap-grid" data-ap-grid=""></div>
          <div class="ap-grid-axis ap-grid-axis--x">
            <span>early</span><span>token position →</span><span>last</span>
          </div>
        </div>
        <div class="ap-grid-legend">
          <div class="ap-grid-legend__title">flip strength</div>
          <div class="ap-grid-legend__bar"></div>
          <div class="ap-grid-legend__scale"><span>0</span><span>1</span></div>
          <div class="ap-grid-legend__hint">cells you've clicked stay outlined</div>
        </div>
      </div>

      <div class="ap-controls">
        <button class="ap-btn ap-btn--primary" data-ap-scan="">▶ auto-scan all cells</button>
        <button class="ap-btn" data-ap-clear="">↺ reset patches</button>
        <span class="ap-counter" data-ap-counter="">0 / 0 cells revealed</span>
      </div>

      <div class="ap-result">
        <div class="ap-result__row">
          <span>after patching <strong data-ap-where="">nothing yet</strong>:</span>
        </div>
        <div class="ap-result__bars">
          <div class="ap-result__row">
            <span class="ap-result__name" data-ap-name-clean="">clean answer</span>
            <div class="ap-result__bar"><div class="ap-result__fill ap-result__fill--clean" data-ap-clean-bar=""></div></div>
            <span class="ap-result__num" data-ap-clean-pct="">—</span>
          </div>
          <div class="ap-result__row">
            <span class="ap-result__name" data-ap-name-corrupt="">corrupt answer</span>
            <div class="ap-result__bar"><div class="ap-result__fill ap-result__fill--corrupt" data-ap-corrupt-bar=""></div></div>
            <span class="ap-result__num" data-ap-corrupt-pct="">—</span>
          </div>
        </div>
      </div>

      <p class="ap-hint"><b>What the heatmap reveals:</b> hot cells form a small region — usually mid-to-late layers at the <i>last subject token</i> for factual recall, or at <i>name positions</i> for IOI. Click those cells: the answer flips. Click anywhere else: nothing happens. That's how <a href="https://arxiv.org/abs/2202.05262" target="_blank" rel="noopener">Meng et al. found ROME</a> and how the IOI circuit was traced.</p>
    </div>
  </div>
</div>

<style>
  #demo-patch .ap-howto {
    background: #fffaf3; border: 1px solid #ddb88e; border-radius: 4px;
    padding: 0.7rem 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-patch .ap-howto__title {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #7c4d0a; margin-bottom: 0.35rem; font-weight: 600;
  }
  #demo-patch .ap-howto__list {
    margin: 0; padding: 0 0 0 1.1rem; font-size: 0.9rem;
    color: var(--nn-body); line-height: 1.6;
  }
  #demo-patch .ap-howto__list li b { color: #7c4d0a; }

  #demo-patch .ap-examples {
    display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.65rem;
  }
  #demo-patch .ap-ex {
    padding: 0.32rem 0.7rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-patch .ap-ex:hover { border-color: #b77214; }
  #demo-patch .ap-ex.is-active {
    background: #b77214; color: #fff; border-color: #b77214;
  }

  #demo-patch .ap-prompts {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.85rem;
  }
  #demo-patch .ap-prompt {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.55rem 0.75rem;
    display: flex; flex-direction: column; gap: 0.25rem;
  }
  #demo-patch .ap-prompt--corrupt { background: #fff6e0; border-color: #b77214; }
  #demo-patch .ap-prompt__lbl {
    font-family: var(--nn-mono); font-size: 0.65rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
  }
  #demo-patch .ap-prompt--corrupt .ap-prompt__lbl { color: #7c4d0a; }
  #demo-patch .ap-prompt__text { font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-ink); }
  #demo-patch .ap-prompt__ans { font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted); }
  #demo-patch .ap-prompt__ans strong { color: #7c4d0a; font-weight: 600; }

  #demo-patch .ap-grid-wrap {
    display: grid; grid-template-columns: 28px 1fr 90px; gap: 0.5rem;
    margin-bottom: 0.85rem;
  }
  #demo-patch .ap-grid-axis { display: flex; align-items: center; justify-content: center; }
  #demo-patch .ap-grid-axis--y {
    writing-mode: vertical-rl; transform: rotate(180deg);
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    color: var(--nn-muted); text-transform: uppercase;
  }
  #demo-patch .ap-grid-axis--x {
    display: flex; justify-content: space-between;
    font-family: var(--nn-mono); font-size: 0.65rem; color: var(--nn-muted);
    letter-spacing: 0.06em; text-transform: uppercase;
    padding: 0.3rem 0.1rem 0;
  }
  #demo-patch .ap-grid-tokens {
    display: grid; gap: 4px; margin-bottom: 4px;
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-align: center;
  }
  #demo-patch .ap-grid-tokens span {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    padding: 1px 0;
  }
  #demo-patch .ap-grid {
    display: grid; gap: 4px;
  }
  #demo-patch .ap-cell {
    aspect-ratio: 1 / 1; min-height: 22px;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 2px;
    cursor: pointer; position: relative;
    transition: transform 120ms;
  }
  #demo-patch .ap-cell:hover { transform: scale(1.12); z-index: 2; box-shadow: 0 0 0 2px #b77214; border-color: #b77214; }
  #demo-patch .ap-cell.is-revealed { background-color: var(--c, #fff); }
  #demo-patch .ap-cell.is-selected { box-shadow: 0 0 0 2px #0d6b3a; z-index: 3; }
  #demo-patch .ap-cell.is-scanning { animation: apPulse 350ms; }
  @keyframes apPulse {
    0% { box-shadow: 0 0 0 0 #b77214; }
    50% { box-shadow: 0 0 0 4px rgba(183,114,20,0.6); }
    100% { box-shadow: 0 0 0 0 #b77214; }
  }

  #demo-patch .ap-grid-legend {
    display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
    font-family: var(--nn-mono); font-size: 0.62rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  #demo-patch .ap-grid-legend__title { font-weight: 600; }
  #demo-patch .ap-grid-legend__bar {
    width: 16px; flex: 1; min-height: 90px;
    background: linear-gradient(to top, #fffefb 0%, #ddb88e 35%, #b77214 70%, #7c4d0a 100%);
    border: 1px solid var(--nn-line); border-radius: 2px;
  }
  #demo-patch .ap-grid-legend__scale { display: flex; flex-direction: column; gap: 75px; font-size: 0.6rem; }
  #demo-patch .ap-grid-legend__hint {
    text-transform: none; letter-spacing: 0; font-size: 0.62rem; line-height: 1.3;
    text-align: center; max-width: 90px;
  }

  #demo-patch .ap-controls {
    display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
    margin-bottom: 0.85rem;
  }
  #demo-patch .ap-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-patch .ap-btn:hover { border-color: #b77214; }
  #demo-patch .ap-btn--primary { background: #b77214; color: #fff; border-color: #b77214; }
  #demo-patch .ap-btn--primary:hover { background: #7c4d0a; }
  #demo-patch .ap-btn.is-running { background: #7c4d0a; color: #fff; border-color: #7c4d0a; }
  #demo-patch .ap-counter {
    margin-left: auto; font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
  }

  #demo-patch .ap-result {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.7rem 0.85rem; margin-bottom: 0.85rem;
  }
  #demo-patch .ap-result__row {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted);
    display: grid; grid-template-columns: 130px 1fr 56px;
    align-items: center; gap: 0.55rem; margin-bottom: 0.4rem;
  }
  #demo-patch .ap-result__row:first-child { display: block; margin-bottom: 0.55rem; }
  #demo-patch .ap-result__row strong { color: #7c4d0a; }
  #demo-patch .ap-result__name { color: var(--nn-ink); }
  #demo-patch .ap-result__bar {
    height: 14px; background: #fff; border: 1px solid var(--nn-line); border-radius: 2px; overflow: hidden;
  }
  #demo-patch .ap-result__fill {
    height: 100%; transition: width 280ms cubic-bezier(.3,.5,.3,1); width: 0%;
  }
  #demo-patch .ap-result__fill--clean { background: #b77214; }
  #demo-patch .ap-result__fill--corrupt { background: #ddb88e; }
  #demo-patch .ap-result__num {
    font-family: var(--nn-mono); font-size: 0.74rem; text-align: right; color: var(--nn-ink);
  }
  #demo-patch .ap-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-patch .ap-hint b { color: #7c4d0a; }
  #demo-patch .ap-hint a { color: #7c4d0a; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-patch"); if (!root) return;

  // Three example pairs. Each defines:
  //   tokens: list of token strings shared across clean/corrupt
  //   cleanAns / corruptAns: the two competing predictions
  //   peakL / peakP: where the answer-bearing computation peaks (layer index, token index)
  //   spread: how localized the effect is (smaller = more focused)
  var EXAMPLES = [
    {
      id: "fact",
      label: "factual recall (Paris→France)",
      cleanText: "Paris is the capital of",
      corruptText: "Tokyo is the capital of",
      cleanAns: "France",
      corruptAns: "Japan",
      tokens: ["Paris/Tokyo", "is", "the", "capital", "of"],
      peakL: 6, peakP: 0,
      spreadL: 1.6, spreadP: 0.9,
      baseClean: 0.87, baseCorrupt: 0.01
    },
    {
      id: "ioi",
      label: "IOI (John & Mary)",
      cleanText: "When John and Mary went to the store, John gave a drink to",
      corruptText: "When Mary and John went to the store, John gave a drink to",
      cleanAns: "Mary",
      corruptAns: "John",
      tokens: ["John/Mary", "and", "Mary/John", "store", "John", "to"],
      peakL: 8, peakP: 4,
      spreadL: 1.4, spreadP: 0.8,
      baseClean: 0.74, baseCorrupt: 0.05
    },
    {
      id: "math",
      label: "arithmetic (3→7)",
      cleanText: "The answer to 4 plus 3 is",
      corruptText: "The answer to 4 plus 8 is",
      cleanAns: "7",
      corruptAns: "12",
      tokens: ["The", "to", "4", "plus", "3/8", "is"],
      peakL: 5, peakP: 4,
      spreadL: 1.2, spreadP: 0.7,
      baseClean: 0.58, baseCorrupt: 0.06
    }
  ];

  var N_LAYERS = 12;

  var examplesEl = root.querySelector("[data-ap-examples]");
  var cleanTextEl = root.querySelector("[data-ap-clean-text]");
  var corruptTextEl = root.querySelector("[data-ap-corrupt-text]");
  var cleanAnsEl = root.querySelector("[data-ap-clean-ans]");
  var corruptAnsEl = root.querySelector("[data-ap-corrupt-ans]");
  var tokensEl = root.querySelector("[data-ap-tokens]");
  var gridEl = root.querySelector("[data-ap-grid]");
  var whereEl = root.querySelector("[data-ap-where]");
  var nameCleanEl = root.querySelector("[data-ap-name-clean]");
  var nameCorruptEl = root.querySelector("[data-ap-name-corrupt]");
  var cleanPctEl = root.querySelector("[data-ap-clean-pct]");
  var corruptPctEl = root.querySelector("[data-ap-corrupt-pct]");
  var cleanBar = root.querySelector("[data-ap-clean-bar]");
  var corruptBar = root.querySelector("[data-ap-corrupt-bar]");
  var scanBtn = root.querySelector("[data-ap-scan]");
  var clearBtn = root.querySelector("[data-ap-clear]");
  var counterEl = root.querySelector("[data-ap-counter]");

  var current = EXAMPLES[0];
  var revealed = {}; // "l,p" -> 1
  var selected = null; // {l,p}
  var scanning = false;
  var scanTimer = null;

  function flipAt(l, p, ex){
    // 2D Gaussian centered at peak.
    var dl = (l - ex.peakL) / ex.spreadL;
    var dp = (p - ex.peakP) / ex.spreadP;
    var f = Math.exp(-0.5 * (dl*dl + dp*dp));
    return Math.max(0, Math.min(1, f));
  }

  function colorFor(f){
    // Map flip strength 0..1 to amber gradient.
    if (f < 0.04) return "#ffffff";
    if (f < 0.15){ var a = (f - 0.04) / 0.11; return mix("#fff8ec", "#ffe4ba", a); }
    if (f < 0.45){ var a = (f - 0.15) / 0.30; return mix("#ffe4ba", "#e8a657", a); }
    if (f < 0.75){ var a = (f - 0.45) / 0.30; return mix("#e8a657", "#b77214", a); }
    var a4 = (f - 0.75) / 0.25; return mix("#b77214", "#7c4d0a", a4);
  }
  function mix(c1, c2, t){
    var r1 = parseInt(c1.slice(1,3),16), g1 = parseInt(c1.slice(3,5),16), b1 = parseInt(c1.slice(5,7),16);
    var r2 = parseInt(c2.slice(1,3),16), g2 = parseInt(c2.slice(3,5),16), b2 = parseInt(c2.slice(5,7),16);
    var r = Math.round(r1 + (r2-r1)*t), g = Math.round(g1 + (g2-g1)*t), b = Math.round(b1 + (b2-b1)*t);
    return "rgb("+r+","+g+","+b+")";
  }

  function renderExamples(){
    examplesEl.innerHTML = "";
    EXAMPLES.forEach(function(ex){
      var b = document.createElement("button");
      b.className = "ap-ex" + (ex.id === current.id ? " is-active" : "");
      b.textContent = ex.label;
      b.addEventListener("click", function(){
        current = ex; revealed = {}; selected = null; stopScan();
        renderAll();
      });
      examplesEl.appendChild(b);
    });
  }

  function renderPrompts(){
    cleanTextEl.textContent = current.cleanText;
    corruptTextEl.textContent = current.corruptText;
    cleanAnsEl.textContent = current.cleanAns;
    corruptAnsEl.textContent = current.corruptAns;
    nameCleanEl.textContent = "P(" + current.cleanAns + ")";
    nameCorruptEl.textContent = "P(" + current.corruptAns + ")";
  }

  function renderGrid(){
    var nP = current.tokens.length;
    tokensEl.style.gridTemplateColumns = "repeat("+nP+", 1fr)";
    var th = ""; current.tokens.forEach(function(t){ th += "<span title=\""+escAttr(t)+"\">"+escHtml(t)+"</span>"; });
    tokensEl.innerHTML = th;

    gridEl.style.gridTemplateColumns = "repeat("+nP+", 1fr)";
    gridEl.innerHTML = "";
    for (var l = N_LAYERS - 1; l >= 0; l--){
      for (var p = 0; p < nP; p++){
        var key = l + "," + p;
        var c = document.createElement("div");
        c.className = "ap-cell";
        c.dataset.l = l; c.dataset.p = p;
        var f = flipAt(l, p, current);
        c.title = "L"+l+" · "+current.tokens[p]+" · flip="+f.toFixed(2);
        if (revealed[key]){
          c.classList.add("is-revealed");
          c.style.setProperty("--c", colorFor(f));
        }
        if (selected && selected.l === l && selected.p === p){
          c.classList.add("is-selected");
        }
        c.addEventListener("click", function(){
          var ll = +this.dataset.l, pp = +this.dataset.p;
          revealed[ll+","+pp] = 1;
          selected = { l: ll, p: pp };
          renderGrid();
          renderResult();
        });
        c.addEventListener("mouseenter", function(){
          if (scanning) return;
          var ll = +this.dataset.l, pp = +this.dataset.p;
          revealed[ll+","+pp] = 1;
          this.classList.add("is-revealed");
          this.style.setProperty("--c", colorFor(flipAt(ll, pp, current)));
          updateCounter();
        });
        gridEl.appendChild(c);
      }
    }
    updateCounter();
  }

  function updateCounter(){
    var nP = current.tokens.length;
    var total = N_LAYERS * nP;
    var n = Object.keys(revealed).length;
    counterEl.textContent = n + " / " + total + " cells revealed";
  }

  function renderResult(){
    if (!selected){
      whereEl.textContent = "nothing yet";
      cleanPctEl.textContent = (current.baseClean*100).toFixed(0)+"%";
      corruptPctEl.textContent = (current.baseCorrupt*100).toFixed(0)+"%";
      cleanBar.style.width = (current.baseClean*100)+"%";
      corruptBar.style.width = (current.baseCorrupt*100)+"%";
      return;
    }
    var f = flipAt(selected.l, selected.p, current);
    var pClean = current.baseClean * (1 - f) + 0.05 * f;
    var pCorrupt = current.baseCorrupt * (1 - f) + 0.85 * f;
    whereEl.textContent = "L"+selected.l+" at \"" + current.tokens[selected.p] + "\"";
    cleanPctEl.textContent = (pClean*100).toFixed(0)+"%";
    corruptPctEl.textContent = (pCorrupt*100).toFixed(0)+"%";
    cleanBar.style.width = (pClean*100)+"%";
    corruptBar.style.width = (pCorrupt*100)+"%";
  }

  function startScan(){
    if (scanning) return;
    scanning = true;
    scanBtn.textContent = "■ stop";
    scanBtn.classList.add("is-running");
    var nP = current.tokens.length;
    var order = [];
    for (var l = N_LAYERS - 1; l >= 0; l--){
      for (var p = 0; p < nP; p++) order.push({ l: l, p: p });
    }
    var idx = 0;
    var step = function(){
      if (!scanning || idx >= order.length){ stopScan(); return; }
      var o = order[idx++];
      revealed[o.l+","+o.p] = 1;
      selected = o;
      renderGrid();
      renderResult();
      var cell = gridEl.querySelector("[data-l='"+o.l+"'][data-p='"+o.p+"']");
      if (cell){ cell.classList.add("is-scanning"); }
      scanTimer = setTimeout(step, 36);
    };
    step();
  }
  function stopScan(){
    scanning = false;
    scanBtn.textContent = "▶ auto-scan all cells";
    scanBtn.classList.remove("is-running");
    if (scanTimer){ clearTimeout(scanTimer); scanTimer = null; }
  }

  function renderAll(){
    renderExamples();
    renderPrompts();
    renderGrid();
    renderResult();
  }

  function escHtml(s){ return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
  function escAttr(s){ return String(s).replace(/"/g,"&quot;"); }

  scanBtn.addEventListener("click", function(){ if (scanning) stopScan(); else startScan(); });
  clearBtn.addEventListener("click", function(){ revealed = {}; selected = null; stopScan(); renderGrid(); renderResult(); });

  renderAll();
})();
</script>

<p>The next two posts cover attention and MLPs as readers/writers in detail.</p>

<h2 id="resources">Resources</h2>

<h3 id="foundational-papers">Foundational papers</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1512.03385" target="_blank" rel="noopener"><div class="research-card__title">Deep Residual Learning for Image Recognition</div><div class="research-card__authors">He et al., 2015 · ResNet, the original residual-connection paper</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener"><div class="research-card__title">A Mathematical Framework for Transformer Circuits</div><div class="research-card__authors">Elhage et al., Anthropic 2021 · introduces the residual-stream view; foundational for this series</div></a></li>
  <li><a class="research-card" href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens" target="_blank" rel="noopener"><div class="research-card__title">Interpreting GPT: the logit lens</div><div class="research-card__authors">Nostalgebraist, LessWrong 2020 · the original logit-lens post</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2303.08112" target="_blank" rel="noopener"><div class="research-card__title">Eliciting Latent Predictions from Transformers with the Tuned Lens</div><div class="research-card__authors">Belrose et al., 2023 · per-layer affine refinement of the logit lens</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2022/toy_model/index.html" target="_blank" rel="noopener"><div class="research-card__title">Toy Models of Superposition</div><div class="research-card__authors">Elhage et al., Anthropic 2022 · why features overlap in the residual stream</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html" target="_blank" rel="noopener"><div class="research-card__title">Scaling Monosemanticity</div><div class="research-card__authors">Templeton et al., Anthropic 2024 · SAE features in Claude 3 Sonnet's residual stream</div></a></li>
</ul>

<h3 id="tools-and-code">Tools and code</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://transformerlensorg.github.io/TransformerLens/generated/demos/Main_Demo.html" target="_blank" rel="noopener"><div class="research-card__title">TransformerLens · Main Demo</div><div class="research-card__authors">cache hooks, decompose_resid, direct logit attribution in code</div></a></li>
  <li><a class="research-card" href="https://github.com/AlignmentResearch/tuned-lens" target="_blank" rel="noopener"><div class="research-card__title">tuned-lens</div><div class="research-card__authors">official tuned-lens implementation; works on any HF causal LM</div></a></li>
  <li><a class="research-card" href="https://www.neelnanda.io/mechanistic-interpretability/glossary" target="_blank" rel="noopener"><div class="research-card__title">Neel Nanda's MI Glossary</div><div class="research-card__authors">definitions for residual stream, DLA, activation patching</div></a></li>
  <li><a class="research-card" href="https://arena3-chapter1-transformer-interp.streamlit.app/" target="_blank" rel="noopener"><div class="research-card__title">ARENA · Transformer Interpretability</div><div class="research-card__authors">guided exercises building DLA and the logit lens from scratch</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[The residual stream is a per-position running sum that every block reads from and writes to. Because it is additive and lives in a single coordinate frame, it admits direct linear decomposition: the foundation of the logit lens, direct logit attribution, and activation patching.]]></summary></entry><entry><title type="html">Tokens: The Strange Alphabet Models Actually See</title><link href="https://bhavith-chandra.github.io/posts/tokens-the-strange-alphabet-models-actually-see/" rel="alternate" type="text/html" title="Tokens: The Strange Alphabet Models Actually See" /><published>2026-03-19T00:00:00-07:00</published><updated>2026-03-19T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/tokens-the-strange-alphabet-models-actually-see</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/tokens-the-strange-alphabet-models-actually-see/"><![CDATA[<p>A <strong>token</strong> is the atomic unit of input and output for a language model. It is not a word and not a character. It is a subword piece drawn from a fixed vocabulary $V$ of size 30,000 to 200,000.</p>

<p>This post defines tokens precisely, explains how the vocabulary is constructed (Byte-Pair Encoding), and walks through five concrete failure modes traceable directly to tokenization.</p>

<hr />

<h2 id="demo-tokenize-anything">Demo: tokenize anything</h2>

<div class="idemo" id="demo-tokenizer">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · Tokenizer playground (GPT-2 BPE, live)</span></div>
    <div class="idemo__body">

      <p class="tok-lead">Type anything in the box. Watch it get chopped into subword pieces. Notice how a space is part of a token. Notice how numbers split weirdly. Notice how emoji explode into three pieces. This is <em>real</em> BPE, running in your browser.</p>

      <div class="tok-presets" data-tok-presets="">
        <button class="tok-preset" data-tok-preset="The quick brown fox jumps over the lazy dog.">Normal English</button>
        <button class="tok-preset" data-tok-preset="unhappily">Subwords: 'unhappily'</button>
        <button class="tok-preset" data-tok-preset="123456789">Long number</button>
        <button class="tok-preset" data-tok-preset="I ❤️ interpretability 🔬">Emoji chaos</button>
        <button class="tok-preset" data-tok-preset="def tokenize(text):&#10;    return text.split()">Python code</button>
        <button class="tok-preset" data-tok-preset="ChatGPT is great but GPT-4 is gr8.">Chat slang</button>
        <button class="tok-preset" data-tok-preset="こんにちは世界">Japanese (no Unicode merge)</button>
      </div>

      <textarea class="tok-input" data-tok-input="" rows="3" spellcheck="false">The cat sat on the mat.</textarea>

      <div class="tok-meta">
        <div class="tok-meta__item"><span class="tok-meta__val" data-tok-chars="">0</span><span class="tok-meta__key">chars</span></div>
        <div class="tok-meta__item"><span class="tok-meta__val" data-tok-count="">0</span><span class="tok-meta__key">tokens</span></div>
        <div class="tok-meta__item"><span class="tok-meta__val" data-tok-ratio="">0</span><span class="tok-meta__key">chars/token</span></div>
        <div class="tok-meta__item"><span class="tok-meta__val" data-tok-cost="">$0.000</span><span class="tok-meta__key">@ $3/M input</span></div>
      </div>

      <div class="tok-view" data-tok-view="">
        <div class="tok-view__empty">Type something above to see tokens.</div>
      </div>

      <div class="tok-detail" data-tok-detail="" hidden="">
        <div class="tok-detail__head">
          <span class="tok-detail__idx" data-tok-detail-idx="">Token 0</span>
          <span class="tok-detail__text" data-tok-detail-text=""></span>
        </div>
        <div class="tok-detail__rows">
          <div class="tok-detail__row"><span>ID</span><code data-tok-detail-id="">, </code></div>
          <div class="tok-detail__row"><span>Bytes</span><code data-tok-detail-bytes="">, </code></div>
          <div class="tok-detail__row"><span>Unicode</span><code data-tok-detail-uni="">, </code></div>
        </div>
      </div>

      <div class="tok-status" data-tok-status="">Loading GPT-2 BPE tables…</div>

      <details>
        <summary>Why this tokenizer isn't a simple whitespace split</summary>
        <p>This demo uses real <strong>Byte-Pair Encoding (BPE)</strong>, the same algorithm GPT-2 and (close relatives of) GPT-3/4 use. BPE starts from single bytes and learns which byte-pairs to merge based on frequency in the training corpus. The result is a vocabulary of ~50,000 subword pieces: common whole words ("the", "and"), common word-chunks ("ing", "tion"), and everything else falls back to individual bytes. That's why "unhappily" becomes three pieces but "cat" stays whole, "cat" was common enough in the training text to earn its own merge. We run this via the <code>gpt-tokenizer</code> library, fetched lazily. Fallback to an illustrative split if the CDN is unreachable.</p>
      </details>
    </div>
  </div>
</div>

<style>
  #demo-tokenizer .tok-lead { margin: 0 0 1rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-tokenizer .tok-presets { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.9rem; }
  #demo-tokenizer .tok-preset {
    padding: 0.4rem 0.78rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; transition: all 120ms;
  }
  #demo-tokenizer .tok-preset:hover { border-color: #b77214; background: rgba(251,191,36,0.10); }

  #demo-tokenizer .tok-input {
    width: 100%; min-height: 70px; padding: 0.85rem 1rem;
    font-family: var(--nn-mono); font-size: 0.95rem; color: var(--nn-ink);
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    resize: vertical; line-height: 1.5;
  }
  #demo-tokenizer .tok-input:focus { outline: 2px solid rgba(251,191,36,0.45); outline-offset: 1px; border-color: #b77214; }

  #demo-tokenizer .tok-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.9rem 0; }
  #demo-tokenizer .tok-meta__item {
    display: flex; align-items: baseline; gap: 0.45rem; flex: 1 1 140px;
    padding: 0.5rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-tokenizer .tok-meta__val {
    font-family: var(--nn-mono); font-weight: 600; font-size: 1.15rem; color: #b77214;
  }
  #demo-tokenizer .tok-meta__key {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--nn-muted);
  }

  #demo-tokenizer .tok-view {
    min-height: 90px; padding: 0.9rem 0.95rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 4px;
    line-height: 2.1;
  }
  #demo-tokenizer .tok-view__empty { color: var(--nn-muted); font-style: italic; }
  #demo-tokenizer .tok-chip {
    display: inline-block; padding: 0.2rem 0.5rem; margin: 0 1px;
    font-family: var(--nn-mono); font-size: 0.85rem;
    border: 1px solid; border-radius: 3px; cursor: pointer;
    transition: transform 80ms, box-shadow 80ms;
    white-space: pre;
  }
  #demo-tokenizer .tok-chip:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
  #demo-tokenizer .tok-chip.is-active { box-shadow: 0 0 0 2px #b77214; }
  #demo-tokenizer .tok-chip__nl { color: #888; }

  #demo-tokenizer .tok-detail {
    margin-top: 0.9rem; padding: 0.9rem 1.05rem; background: #fafaf7;
    border: 1px solid var(--nn-line); border-left: 3px solid #b77214;
    border-radius: 3px;
  }
  #demo-tokenizer .tok-detail__head {
    display: flex; gap: 0.8rem; align-items: baseline; margin-bottom: 0.55rem;
    flex-wrap: wrap;
  }
  #demo-tokenizer .tok-detail__idx {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #b77214;
  }
  #demo-tokenizer .tok-detail__text {
    font-family: var(--nn-mono); font-size: 0.98rem; color: var(--nn-ink);
    background: #fff; padding: 0.15rem 0.5rem; border: 1px dashed var(--nn-line);
    border-radius: 3px; white-space: pre;
  }
  #demo-tokenizer .tok-detail__rows { display: grid; grid-template-columns: 80px 1fr; gap: 0.3rem 0.9rem; font-size: 0.86rem; }
  #demo-tokenizer .tok-detail__row { display: contents; }
  #demo-tokenizer .tok-detail__row > span:first-child {
    font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-tokenizer .tok-detail__row code {
    font-family: var(--nn-mono); color: var(--nn-ink); word-break: break-all;
  }

  #demo-tokenizer .tok-status {
    margin-top: 0.6rem; font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
  }
  #demo-tokenizer .tok-status.is-ready { color: #2a9e8e; }
  #demo-tokenizer .tok-status.is-fallback { color: #b77214; }
</style>

<script>
(function(){
  const root = document.getElementById("demo-tokenizer"); if (!root) return;

  const input = root.querySelector("[data-tok-input]");
  const view = root.querySelector("[data-tok-view]");
  const detail = root.querySelector("[data-tok-detail]");
  const status = root.querySelector("[data-tok-status]");
  const charsEl = root.querySelector("[data-tok-chars]");
  const countEl = root.querySelector("[data-tok-count]");
  const ratioEl = root.querySelector("[data-tok-ratio]");
  const costEl = root.querySelector("[data-tok-cost]");

  // 8 pleasant but distinguishable chip colours
  const PALETTE = [
    ["#fde68a", "#b77214"], ["#bfdbfe", "#1d4ed8"], ["#d9f99d", "#3f6212"],
    ["#fbcfe8", "#9d174d"], ["#c7d2fe", "#3730a3"], ["#fed7aa", "#9a3412"],
    ["#a5f3fc", "#155e75"], ["#e9d5ff", "#6b21a8"]
  ];

  let encoder = null;
  let decoder = null;
  let mode = "loading"; // loading | bpe | fallback

  // Fallback "looks like BPE" tokenizer for when the CDN is unreachable.
  // Not accurate, but the *shape* (subwords, space-prefix, byte explode) is right.
  function fallbackEncode(text){
    const out = [];
    const chunks = text.match(/ ?[A-Za-z]+|\s+|\d+|[^A-Za-z\d\s]+/g) || [];
    let nextId = 1000;
    for (const ch of chunks){
      if (/^\s+$/.test(ch)) {
        // whitespace runs: each char is its own token (like real BPE for unusual whitespace)
        for (const c of ch) out.push({ text: c, id: c.charCodeAt(0) });
      } else if (/^ ?[A-Za-z]+$/.test(ch)) {
        // split long english-ish words into 2-3 pieces
        if (ch.length <= 4) out.push({ text: ch, id: nextId++ });
        else {
          const cut = Math.ceil(ch.length / 2);
          out.push({ text: ch.slice(0, cut), id: nextId++ });
          out.push({ text: ch.slice(cut), id: nextId++ });
        }
      } else if (/^\d+$/.test(ch)) {
        // numbers: each digit is its own token
        for (const c of ch) out.push({ text: c, id: c.charCodeAt(0) });
      } else {
        // punctuation/unicode: each char
        for (const c of ch) out.push({ text: c, id: c.codePointAt(0) });
      }
    }
    return out;
  }

  function render(){
    const text = input.value;
    charsEl.textContent = text.length;

    let tokens = [];
    if (mode === "bpe" && encoder){
      try {
        const ids = encoder(text);
        tokens = ids.map(id => ({ id, text: decoder([id]) }));
      } catch (e) {
        tokens = fallbackEncode(text);
      }
    } else {
      tokens = fallbackEncode(text);
    }

    countEl.textContent = tokens.length;
    ratioEl.textContent = tokens.length ? (text.length / tokens.length).toFixed(2) : "0";
    costEl.textContent = "$" + (tokens.length * 3 / 1e6).toFixed(5);

    // render chips
    if (!tokens.length){
      view.innerHTML = '<div class="tok-view__empty">Type something above to see tokens.</div>';
      detail.hidden = true; return;
    }
    view.innerHTML = "";
    tokens.forEach((t, i) => {
      const [bg, fg] = PALETTE[i % PALETTE.length];
      const chip = document.createElement("span");
      chip.className = "tok-chip";
      chip.style.background = bg;
      chip.style.borderColor = fg;
      chip.style.color = fg;
      chip.dataset.idx = i;
      // visualise newlines + leading spaces
      let display = t.text;
      display = display.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
      if (display === "") display = "∅";
      chip.textContent = display;
      chip.addEventListener("click", () => showDetail(i, t));
      view.appendChild(chip);
    });
  }

  function showDetail(i, tok){
    detail.hidden = false;
    root.querySelectorAll(".tok-chip").forEach((c, j) => c.classList.toggle("is-active", j === i));
    root.querySelector("[data-tok-detail-idx]").textContent = `Token ${i}`;
    const disp = tok.text.replace(/\n/g, "↵").replace(/\t/g, "→") || "∅";
    root.querySelector("[data-tok-detail-text]").textContent = JSON.stringify(tok.text);
    root.querySelector("[data-tok-detail-id]").textContent = tok.id;
    // bytes
    const bytes = new TextEncoder().encode(tok.text);
    root.querySelector("[data-tok-detail-bytes]").textContent =
      Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join(" ") + ` (${bytes.length} bytes)`;
    // unicode codepoints
    const cps = Array.from(tok.text).map(c => "U+" + c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0"));
    root.querySelector("[data-tok-detail-uni]").textContent = cps.join(" ") || ", ";
  }

  // preset buttons
  root.querySelectorAll("[data-tok-preset]").forEach(b => {
    b.addEventListener("click", () => { input.value = b.dataset.tokPreset; render(); });
  });

  // debounce input
  let t = null;
  input.addEventListener("input", () => { clearTimeout(t); t = setTimeout(render, 80); });

  // lazy-load gpt-tokenizer from CDN (GPT-2 encoding, ~600KB)
  async function loadTokenizer(){
    status.textContent = "Loading GPT-2 BPE tables…";
    try {
      const mod = await import("https://cdn.jsdelivr.net/npm/gpt-tokenizer@2.5.2/esm/encoding/r50k_base.js");
      encoder = mod.encode || mod.default?.encode;
      decoder = mod.decode || mod.default?.decode;
      if (typeof encoder !== "function" || typeof decoder !== "function") throw new Error("tokenizer shape");
      mode = "bpe";
      status.textContent = "Ready · real GPT-2 BPE (50,257 tokens in vocab).";
      status.classList.add("is-ready");
    } catch (e) {
      mode = "fallback";
      status.textContent = "Couldn't load the real BPE tables, showing an approximate split. Shape is right; IDs aren't real.";
      status.classList.add("is-fallback");
    }
    render();
  }

  loadTokenizer();
  render();
})();
</script>

<p>Switch between presets. Note how <code class="language-plaintext highlighter-rouge">123456789</code> does not split into individual digits; how <code class="language-plaintext highlighter-rouge">❤️</code> consumes three tokens; how Japanese text consumes more tokens per character than English.</p>

<h2 id="definition">Definition</h2>

<p>A tokenizer is a deterministic function $\text{tok}: \text{string} \to [V]^*$ that maps text to a sequence of integer IDs. The vocabulary $V$ is a fixed lookup table containing strings called <strong>token pieces</strong>.</p>

<p>Modern LLMs use <strong>byte-level BPE</strong> (Byte-Pair Encoding):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Initialize V := {byte 0, byte 1, ..., byte 255}        # 256 entries
2. While |V| &lt; target_size:
     a. Count adjacent token pairs in the training corpus.
     b. Find the most frequent pair (a, b).
     c. Add ab to V as a new token.
     d. Replace all occurrences of (a, b) in the corpus with ab.
3. Return V and the merge order.
</code></pre></div></div>

<p>Encoding new text greedily applies merges in the order they were learned. The output is the list of resulting token IDs.</p>

<div class="idemo idemo--mini" id="demo-bpe">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Build a BPE vocabulary, one merge at a time</span></div>
    <div class="idemo__body">

      <div class="bpe-howto">
        <div class="bpe-howto__title">How to play</div>
        <ol class="bpe-howto__list">
          <li><b>Pick a corpus</b> (or type your own) — every word starts split into characters.</li>
          <li><b>Click any of the top-3 candidate pairs</b> to merge it. You're not stuck with the most-frequent one — try suboptimal merges and see what happens.</li>
          <li>Or hit <b>auto-merge</b> and watch greedy BPE build up a real tokenizer in seconds.</li>
          <li>Below, the <b>test sentence</b> shows how today's vocabulary tokenizes new text. The fewer pieces, the better the compression.</li>
        </ol>
      </div>

      <div class="bpe-presets">
        <span class="bpe-presets__label">corpus</span>
        <button class="bpe-pbtn is-active" data-bpe-preset="english">english</button>
        <button class="bpe-pbtn" data-bpe-preset="code">python code</button>
        <button class="bpe-pbtn" data-bpe-preset="numbers">numbers</button>
        <button class="bpe-pbtn" data-bpe-preset="custom">custom ↓</button>
      </div>

      <textarea class="bpe-textarea" data-bpe-input="" rows="2" placeholder="type your own corpus here, space-separated"></textarea>

      <div class="bpe-stage">
        <div class="bpe-row">
          <div class="bpe-row__label">corpus <span data-bpe-tokens="">(0 tokens)</span></div>
          <div class="bpe-row__value" data-bpe-corpus=""></div>
        </div>
        <div class="bpe-row">
          <div class="bpe-row__label">test sentence</div>
          <div class="bpe-row__value" data-bpe-test=""></div>
        </div>
        <div class="bpe-row">
          <div class="bpe-row__label">vocabulary <span data-bpe-vsize="">(0)</span></div>
          <div class="bpe-row__value bpe-row__value--vocab" data-bpe-vocab=""></div>
        </div>
      </div>

      <div class="bpe-candidates">
        <div class="bpe-candidates__label">click a candidate to merge it →</div>
        <div class="bpe-candidates__grid" data-bpe-candidates=""></div>
      </div>

      <div class="bpe-controls">
        <button class="bpe-btn" data-bpe-auto="">▶ auto-merge</button>
        <label class="bpe-speed">
          speed
          <input type="range" min="80" max="900" value="380" step="20" data-bpe-speed="" />
        </label>
        <button class="bpe-btn" data-bpe-reset="">↺ reset</button>
        <span class="bpe-counter" data-bpe-counter="">0 merges · compression 1.00×</span>
      </div>

      <p class="bpe-hint"><b>Notice:</b> the first merges always pick up boring junk like <code>th</code>, <code>he</code>, <code>in</code>. The interesting tokens (<code>the</code>, <code>ing</code>, whole words) only appear after dozens of merges. Real GPT-style tokenizers do this <b>50,000 times</b> on the entire internet — that's why <code>" the"</code> is one token but <code>" frosting"</code> might be three.</p>
    </div>
  </div>
</div>

<style>
  #demo-bpe .bpe-howto {
    background: #fffaf3; border: 1px solid #ddb88e; border-radius: 4px;
    padding: 0.7rem 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-bpe .bpe-howto__title {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: #7c4d0a; margin-bottom: 0.35rem; font-weight: 600;
  }
  #demo-bpe .bpe-howto__list {
    margin: 0; padding: 0 0 0 1.1rem; font-size: 0.9rem;
    color: var(--nn-body); line-height: 1.6;
  }
  #demo-bpe .bpe-howto__list li { margin-bottom: 0.18rem; }
  #demo-bpe .bpe-howto__list li b { color: #7c4d0a; }

  #demo-bpe .bpe-presets {
    display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
    margin-bottom: 0.5rem;
  }
  #demo-bpe .bpe-presets__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted); margin-right: 0.2rem;
  }
  #demo-bpe .bpe-pbtn {
    padding: 0.3rem 0.65rem; font-family: var(--nn-mono); font-size: 0.74rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-bpe .bpe-pbtn:hover { border-color: #b77214; }
  #demo-bpe .bpe-pbtn.is-active {
    background: #b77214; color: #fff; border-color: #b77214;
  }
  #demo-bpe .bpe-textarea {
    width: 100%; box-sizing: border-box;
    padding: 0.55rem 0.75rem; margin-bottom: 0.7rem;
    font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-ink);
    background: #fffefb; border: 1px solid var(--nn-line); border-radius: 3px;
    resize: vertical; display: none;
  }
  #demo-bpe .bpe-textarea.is-shown { display: block; }
  #demo-bpe .bpe-textarea:focus { outline: none; border-color: #b77214; }

  #demo-bpe .bpe-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
    display: flex; flex-direction: column; gap: 0.55rem;
  }
  #demo-bpe .bpe-row { display: grid; grid-template-columns: 150px 1fr; gap: 0.7rem; align-items: start; }
  #demo-bpe .bpe-row__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted);
    padding-top: 0.3rem;
  }
  #demo-bpe .bpe-row__label span { text-transform: none; letter-spacing: 0; }
  #demo-bpe .bpe-row__value {
    font-family: var(--nn-mono); font-size: 0.84rem; color: var(--nn-ink);
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.4rem 0.6rem; min-height: 30px;
    line-height: 1.85; word-break: break-word;
  }
  #demo-bpe .bpe-row__value--vocab { line-height: 2; max-height: 120px; overflow-y: auto; }
  #demo-bpe .bpe-tok {
    display: inline-block; padding: 1px 6px; margin: 1px 1px;
    background: #f5f1e8; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); transition: background 200ms;
  }
  #demo-bpe .bpe-tok.is-new {
    background: #fff6e0; border-color: #b77214; color: #7c4d0a; font-weight: 600;
    animation: bpePop 360ms cubic-bezier(.2,.8,.4,1);
  }
  #demo-bpe .bpe-tok.is-test-new { background: #d8f1e0; border-color: #0d6b3a; color: #0d6b3a; }
  @keyframes bpePop {
    0% { transform: scale(0.5); opacity: 0; background: #b77214; color: #fff; }
    60% { transform: scale(1.18); }
    100% { transform: scale(1); opacity: 1; }
  }

  #demo-bpe .bpe-candidates {
    margin-bottom: 0.7rem;
    background: #fffefb; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.65rem 0.8rem;
  }
  #demo-bpe .bpe-candidates__label {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.5rem;
  }
  #demo-bpe .bpe-candidates__grid {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.55rem;
  }
  #demo-bpe .bpe-cand {
    display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
    padding: 0.55rem 0.4rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    cursor: pointer; transition: all 160ms;
    font-family: var(--nn-mono);
  }
  #demo-bpe .bpe-cand:hover {
    border-color: #b77214; background: #fffaf3; transform: translateY(-1px);
  }
  #demo-bpe .bpe-cand[disabled] { opacity: 0.45; cursor: default; transform: none; }
  #demo-bpe .bpe-cand[disabled]:hover { border-color: var(--nn-line); background: #fff; }
  #demo-bpe .bpe-cand__rank {
    font-size: 0.65rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--nn-muted);
  }
  #demo-bpe .bpe-cand__pair {
    display: flex; align-items: center; gap: 0.25rem;
    font-size: 0.92rem; color: var(--nn-ink); font-weight: 600;
  }
  #demo-bpe .bpe-cand__pair span {
    background: #f5f1e8; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 1px 6px;
  }
  #demo-bpe .bpe-cand__pair em { font-style: normal; color: var(--nn-muted); font-weight: 400; }
  #demo-bpe .bpe-cand__count {
    font-size: 0.7rem; color: #b77214;
  }
  #demo-bpe .bpe-cand:first-child { background: #fffaf3; border-color: #ddb88e; }

  #demo-bpe .bpe-controls {
    display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
    margin-bottom: 0.65rem;
  }
  #demo-bpe .bpe-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-bpe .bpe-btn:hover { border-color: #b77214; }
  #demo-bpe .bpe-btn.is-running { background: #b77214; color: #fff; border-color: #b77214; }
  #demo-bpe .bpe-speed {
    display: flex; align-items: center; gap: 0.4rem;
    font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
  }
  #demo-bpe .bpe-speed input { width: 110px; accent-color: #b77214; }
  #demo-bpe .bpe-counter {
    margin-left: auto; font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
  }
  #demo-bpe .bpe-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-bpe .bpe-hint code {
    font-family: var(--nn-mono); font-size: 0.84em;
    background: #f5f1e8; padding: 1px 6px; border-radius: 3px;
  }
  #demo-bpe .bpe-hint b { color: #7c4d0a; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-bpe"); if (!root) return;

  var PRESETS = {
    english: {
      corpus: "the cat sat on the mat the cat ran fast the dog and the cat the mat was new the rat hated the cat she sat there",
      test: "the cat sat there"
    },
    code: {
      corpus: "def add a b return a plus b def sub a b return a minus b def mul a b return a times b for i in range",
      test: "def mul return"
    },
    numbers: {
      corpus: "one two three four five six seven eight nine ten one hundred two hundred three hundred four hundred",
      test: "two hundred three"
    }
  };

  var corpusEl = root.querySelector("[data-bpe-corpus]");
  var testEl   = root.querySelector("[data-bpe-test]");
  var vocabEl  = root.querySelector("[data-bpe-vocab]");
  var vsizeEl  = root.querySelector("[data-bpe-vsize]");
  var tokensEl = root.querySelector("[data-bpe-tokens]");
  var candEl   = root.querySelector("[data-bpe-candidates]");
  var autoBtn  = root.querySelector("[data-bpe-auto]");
  var speedEl  = root.querySelector("[data-bpe-speed]");
  var resetBtn = root.querySelector("[data-bpe-reset]");
  var counterEl= root.querySelector("[data-bpe-counter]");
  var inputEl  = root.querySelector("[data-bpe-input]");
  var presetBtns = root.querySelectorAll("[data-bpe-preset]");

  var words = [], vocab = [], merges = 0, lastNew = null, initialTokenCount = 0;
  var testWords = [];
  var autoTimer = null;
  var currentPreset = "english";

  function setPreset(name){
    currentPreset = name;
    presetBtns.forEach(function(b){
      b.classList.toggle("is-active", b.dataset.bpePreset === name);
    });
    if (name === "custom"){
      inputEl.classList.add("is-shown");
      if (!inputEl.value.trim()) inputEl.value = PRESETS.english.corpus;
      inputEl.focus();
    } else {
      inputEl.classList.remove("is-shown");
      inputEl.value = PRESETS[name].corpus;
    }
    init();
  }

  function init(){
    var preset = PRESETS[currentPreset] || PRESETS.english;
    var src = (currentPreset === "custom" ? inputEl.value : preset.corpus).trim();
    if (!src) src = PRESETS.english.corpus;
    var ws = src.split(/\s+/);
    words = ws.map(function(w){ return w.split(""); });
    vocab = [];
    var seen = {};
    words.forEach(function(w){ w.forEach(function(c){ if (!seen[c]){ seen[c] = 1; vocab.push(c); } }); });
    initialTokenCount = words.reduce(function(a,w){ return a + w.length; }, 0);
    testWords = preset.test.split(/\s+/).map(function(w){ return w.split(""); });
    lastNew = null; merges = 0;
    stopAuto();
    render();
  }

  function pairCounts(arr){
    var counts = {}, order = [];
    arr.forEach(function(w){
      for (var i = 0; i < w.length - 1; i++){
        var key = w[i] + "\u0001" + w[i+1];
        if (counts[key] == null){ counts[key] = 0; order.push(key); }
        counts[key]++;
      }
    });
    return order.map(function(k){ var p = k.split("\u0001"); return { a: p[0], b: p[1], n: counts[k] }; })
                .sort(function(x,y){ return y.n - x.n; });
  }

  function applyMerge(arr, a, b){
    var ab = a + b;
    return arr.map(function(w){
      var out = [], i = 0;
      while (i < w.length){
        if (i < w.length - 1 && w[i] === a && w[i+1] === b){ out.push(ab); i += 2; }
        else { out.push(w[i]); i += 1; }
      }
      return out;
    });
  }

  function doMerge(a, b){
    words = applyMerge(words, a, b);
    testWords = applyMerge(testWords, a, b);
    var ab = a + b;
    if (vocab.indexOf(ab) === -1) vocab.push(ab);
    lastNew = ab;
    merges++;
    render();
  }

  function startAuto(){
    if (autoTimer) return;
    autoBtn.textContent = "■ pause";
    autoBtn.classList.add("is-running");
    var tick = function(){
      var pairs = pairCounts(words);
      if (!pairs.length || pairs[0].n < 2){ stopAuto(); return; }
      doMerge(pairs[0].a, pairs[0].b);
      autoTimer = setTimeout(tick, +speedEl.value);
    };
    autoTimer = setTimeout(tick, +speedEl.value);
  }
  function stopAuto(){
    if (autoTimer){ clearTimeout(autoTimer); autoTimer = null; }
    autoBtn.textContent = "▶ auto-merge";
    autoBtn.classList.remove("is-running");
  }

  function render(){
    // corpus
    var html = "";
    words.forEach(function(w, wi){
      w.forEach(function(t){
        var cls = "bpe-tok" + (lastNew && t === lastNew ? " is-new" : "");
        html += "<span class=\""+cls+"\">"+esc(t)+"</span>";
      });
      if (wi < words.length - 1) html += " ";
    });
    corpusEl.innerHTML = html;

    var totalTokens = words.reduce(function(a,w){ return a + w.length; }, 0);
    var compression = initialTokenCount > 0 ? (initialTokenCount / Math.max(totalTokens, 1)) : 1;
    tokensEl.textContent = "(" + totalTokens + " tokens)";

    // test sentence
    var th = "";
    testWords.forEach(function(w, wi){
      w.forEach(function(t){
        var cls = "bpe-tok" + (lastNew && t === lastNew ? " is-test-new" : "");
        th += "<span class=\""+cls+"\">"+esc(t)+"</span>";
      });
      if (wi < testWords.length - 1) th += " ";
    });
    testEl.innerHTML = th;

    // candidates
    var pairs = pairCounts(words).slice(0, 3);
    candEl.innerHTML = "";
    if (!pairs.length || pairs[0].n < 2){
      candEl.innerHTML = "<div class='bpe-cand' disabled style='grid-column:1/-1'><div class='bpe-cand__rank'>nothing left to merge</div></div>";
      stopAuto();
    } else {
      var labels = ["best", "2nd", "3rd"];
      for (var i = 0; i < 3; i++){
        var btn = document.createElement("button");
        btn.className = "bpe-cand";
        if (!pairs[i] || pairs[i].n < 2){
          btn.disabled = true;
          btn.innerHTML = "<div class='bpe-cand__rank'>"+labels[i]+"</div><div class='bpe-cand__pair'><em>—</em></div><div class='bpe-cand__count'>&nbsp;</div>";
        } else {
          (function(p){
            btn.innerHTML = "<div class='bpe-cand__rank'>"+labels[i]+"</div>"+
              "<div class='bpe-cand__pair'><span>"+esc(p.a)+"</span><em>+</em><span>"+esc(p.b)+"</span></div>"+
              "<div class='bpe-cand__count'>"+p.n+"× → "+esc(p.a+p.b)+"</div>";
            btn.addEventListener("click", function(){ doMerge(p.a, p.b); });
          })(pairs[i]);
        }
        candEl.appendChild(btn);
      }
    }

    // vocab
    var vh = "";
    vocab.forEach(function(t){
      var cls = "bpe-tok" + (t === lastNew ? " is-new" : "");
      vh += "<span class=\""+cls+"\">"+esc(t)+"</span>";
    });
    vocabEl.innerHTML = vh;
    vsizeEl.textContent = "(" + vocab.length + ")";
    counterEl.textContent = merges + " merge" + (merges === 1 ? "" : "s") + " · compression " + compression.toFixed(2) + "×";
  }

  function esc(s){
    return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/ /g,"·");
  }

  presetBtns.forEach(function(b){
    b.addEventListener("click", function(){ setPreset(b.dataset.bpePreset); });
  });
  inputEl.addEventListener("input", function(){
    if (currentPreset === "custom") init();
  });
  autoBtn.addEventListener("click", function(){
    if (autoTimer) stopAuto(); else startAuto();
  });
  resetBtn.addEventListener("click", function(){ init(); });

  setPreset("english");
})();
</script>

<p>Example (GPT-2 BPE):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"unhappily"  → ["un", "happ", "ily"]   → [403, 7829, 6148]
" the cat"   → [" the", " cat"]        → [262, 3797]
"strawberry" → ["str", "aw", "berry"]  → [2536, 707, 27078]
</code></pre></div></div>

<p>Common English words tend to be a single token. Rare or compound words split. Average compression: ~4 characters per token for English, less for other scripts.</p>

<h2 id="why-not-characters">Why not characters?</h2>

<p>A character-level model is conceptually simpler (256 ASCII tokens) but breaks at scale.</p>

<p><strong>1. Sequence length.</strong> Self-attention is $O(T^2)$ in the sequence length $T$. A 2,500-character article is 625 BPE tokens vs 2,500 characters: a 16× compute increase per forward pass.</p>

<p><strong>2. Capacity.</strong> Subword tokens already encode high-level meaning. The vector for <code class="language-plaintext highlighter-rouge">" doctor"</code> carries semantic content that a character-level model would have to assemble from <code class="language-plaintext highlighter-rouge">d</code>, <code class="language-plaintext highlighter-rouge">o</code>, <code class="language-plaintext highlighter-rouge">c</code>, <code class="language-plaintext highlighter-rouge">t</code>, <code class="language-plaintext highlighter-rouge">o</code>, <code class="language-plaintext highlighter-rouge">r</code>, burning model capacity on spelling.</p>

<p>BPE is the standard compromise: dense for frequent strings, sparse for rare ones.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>Every interpretability claim ("head 7 in layer 4 attends to the previous token", "neuron 1523 fires on DNA") is stated at the token level. "Position 3" means token 3, not character 3 or word 3. Mismatched tokenization breaks reproducibility across models.</p>
</aside>

<h2 id="five-capability-failures-caused-by-tokenization">Five capability failures caused by tokenization</h2>

<h3 id="1-arithmetic">1. Arithmetic</h3>

<p>In GPT-2’s tokenizer, <code class="language-plaintext highlighter-rouge">123456789</code> splits into <code class="language-plaintext highlighter-rouge">["123", "456", "789"]</code> (or similar 3-digit chunks). Different numbers tokenize differently: <code class="language-plaintext highlighter-rouge">1234</code> may be <code class="language-plaintext highlighter-rouge">["12", "34"]</code> while <code class="language-plaintext highlighter-rouge">12345</code> may be <code class="language-plaintext highlighter-rouge">["123", "45"]</code>. The model performs arithmetic on these symbolic chunks, not on individual digits.</p>

<p><strong>Fix.</strong> Recent models (Llama 3, GPT-4o math fine-tunes) use single-digit tokenization for numbers. Singh et al. (<a href="https://arxiv.org/abs/2402.14903">2024</a>) show this alone yields large gains on multi-digit arithmetic.</p>

<h3 id="2-spaces-are-part-of-the-token">2. Spaces are part of the token</h3>

<p><code class="language-plaintext highlighter-rouge">" cat"</code> (with leading space) and <code class="language-plaintext highlighter-rouge">"cat"</code> (without) are different tokens with different embedding rows. A sentence is typically a sequence of space-prefixed words: <code class="language-plaintext highlighter-rouge">"the cat sat"</code> → <code class="language-plaintext highlighter-rouge">["the", " cat", " sat"]</code>. The first word lacks a leading space; subsequent words include it. This is why prompts that omit a leading space sometimes produce subtly different outputs.</p>

<h3 id="3-non-ascii">3. Non-ASCII</h3>

<p>UTF-8 encodes <code class="language-plaintext highlighter-rouge">❤️</code> as 3 bytes (<code class="language-plaintext highlighter-rouge">\xe2\x9d\xa4</code> for <code class="language-plaintext highlighter-rouge">❤</code>, plus the variation selector). BPE trained mostly on English never merged these byte sequences. Result: each emoji byte is one token. Consequences:</p>

<ul>
  <li>3× token cost per emoji.</li>
  <li>Models cannot reliably match-and-modify emoji at the substring level.</li>
  <li>Multilingual scripts (Hindi, Arabic, CJK) suffer similarly. Llama 3’s larger 128K vocabulary mitigates this; older 50K vocabularies do not.</li>
</ul>

<h3 id="4-the-strawberry-problem">4. The “strawberry” problem</h3>

<p><code class="language-plaintext highlighter-rouge">strawberry</code> is one or two tokens depending on the model. The forward pass never sees individual letters: it operates on dense vectors representing whole subwords. To answer “how many r’s in strawberry?” the model must have <em>learned</em> to internally spell tokens back out, a non-trivial skill that requires it in training. As of 2024 most models still failed this; explicit training on letter-counting tasks fixes it.</p>

<h3 id="5-per-model-vocabularies-are-incompatible">5. Per-model vocabularies are incompatible</h3>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Vocabulary size</th>
      <th>Encoding</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPT-2</td>
      <td>50,257</td>
      <td>byte-level BPE</td>
    </tr>
    <tr>
      <td>GPT-3.5 / GPT-4</td>
      <td>100,277</td>
      <td><code class="language-plaintext highlighter-rouge">cl100k_base</code></td>
    </tr>
    <tr>
      <td>GPT-4o</td>
      <td>200,019</td>
      <td><code class="language-plaintext highlighter-rouge">o200k_base</code></td>
    </tr>
    <tr>
      <td>Llama 3</td>
      <td>128,256</td>
      <td>tiktoken-based BPE</td>
    </tr>
    <tr>
      <td>Claude</td>
      <td>~100K+</td>
      <td>proprietary BPE</td>
    </tr>
  </tbody>
</table>

<p>A given prompt produces a different number of tokens at different positions in each model. Position-indexed MI results do not transfer without re-tokenization.</p>

<div class="idemo idemo--mini" id="demo-tcount">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Multi-tokenizer counter (paste anything)</span></div>
    <div class="idemo__body">

      <p class="tc-lead">Type or paste text. The demo estimates how each major tokenizer would count it, then computes the API cost at current pricing. Try the same paragraph in English, then in Japanese, then with emoji, the count differences are real and they cost real money.</p>

      <textarea class="tc-input" data-tc-input="" rows="4" placeholder="Type or paste text...">The quick brown fox jumps over the lazy dog. 🦊</textarea>

      <div class="tc-presets">
        <button class="tc-preset-btn" data-tc-preset="english">English paragraph</button>
        <button class="tc-preset-btn" data-tc-preset="japanese">Japanese</button>
        <button class="tc-preset-btn" data-tc-preset="code">Python code</button>
        <button class="tc-preset-btn" data-tc-preset="emoji">Emoji-heavy</button>
        <button class="tc-preset-btn" data-tc-preset="numbers">Long numbers</button>
      </div>

      <div class="tc-grid" data-tc-grid=""></div>

      <div class="tc-summary">
        <div class="tc-summary__row">
          <span>characters</span><strong data-tc-chars="">0</strong>
        </div>
        <div class="tc-summary__row">
          <span>words</span><strong data-tc-words="">0</strong>
        </div>
        <div class="tc-summary__row">
          <span>bytes (UTF-8)</span><strong data-tc-bytes="">0</strong>
        </div>
      </div>

      <p class="tc-hint"><strong>Why models disagree:</strong> Each tokenizer was trained on a different mix of languages. GPT-4's vocabulary is more multilingual than GPT-2's, so the same Japanese sentence costs fewer tokens in GPT-4. Llama 3 has a 128K vocabulary biased heavily toward code. Counts are heuristic estimates (≈ chars-per-token ratios from published benchmarks); load <code>tiktoken</code> for exact numbers.</p>
    </div>
  </div>
</div>

<style>
  #demo-tcount .tc-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-tcount .tc-input {
    width: 100%; min-height: 80px;
    font-family: var(--nn-mono); font-size: 0.86rem; color: var(--nn-ink);
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.65rem 0.8rem; margin-bottom: 0.6rem; resize: vertical;
    box-sizing: border-box;
  }
  #demo-tcount .tc-input:focus { outline: none; border-color: #b77214; }
  #demo-tcount .tc-presets {
    display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.85rem;
  }
  #demo-tcount .tc-preset-btn {
    padding: 0.36rem 0.7rem; font-family: var(--nn-mono); font-size: 0.72rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer;
  }
  #demo-tcount .tc-preset-btn:hover { border-color: #b77214; }
  #demo-tcount .tc-grid {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
    margin-bottom: 0.85rem;
  }
  #demo-tcount .tc-card {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.6rem 0.75rem;
    display: flex; flex-direction: column; gap: 0.2rem;
  }
  #demo-tcount .tc-card__name {
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  #demo-tcount .tc-card__row {
    display: flex; justify-content: space-between; align-items: baseline;
    font-family: var(--nn-mono); font-size: 0.78rem;
  }
  #demo-tcount .tc-card__row strong { color: #7c4d0a; font-weight: 600; }
  #demo-tcount .tc-card__cost {
    font-size: 0.7rem; color: var(--nn-muted);
  }
  #demo-tcount .tc-summary {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.55rem 0.75rem; margin-bottom: 0.85rem;
    display: flex; gap: 1.5rem; flex-wrap: wrap;
  }
  #demo-tcount .tc-summary__row {
    font-family: var(--nn-mono); font-size: 0.76rem; color: var(--nn-muted);
  }
  #demo-tcount .tc-summary__row strong { color: var(--nn-ink); margin-left: 0.3rem; }
  #demo-tcount .tc-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-tcount .tc-hint strong { color: #7c4d0a; }
  #demo-tcount .tc-hint code {
    font-family: var(--nn-mono); font-size: 0.84em;
    background: #f5f1e8; padding: 1px 6px; border-radius: 3px;
  }
  @media (max-width: 620px){
    #demo-tcount .tc-grid { grid-template-columns: 1fr; }
  }
</style>

<script>
(function(){
  var root = document.getElementById("demo-tcount"); if (!root) return;

  // Models with chars-per-token heuristics calibrated per-script.
  // Estimates are illustrative; real tokenizers differ slightly per string.
  var MODELS = [
    { name: "GPT-2",       cpt: { en: 3.8, ja: 1.0, code: 3.0, emoji: 0.4, num: 1.5 }, price: 0 },
    { name: "GPT-4o",      cpt: { en: 4.2, ja: 1.7, code: 3.4, emoji: 1.4, num: 2.0 }, price: 0.0025 },
    { name: "Claude 3.5",  cpt: { en: 4.4, ja: 1.6, code: 3.6, emoji: 1.3, num: 2.5 }, price: 0.003 },
    { name: "Llama 3",     cpt: { en: 4.0, ja: 1.4, code: 3.6, emoji: 1.0, num: 1.0 }, price: 0.0006 }
  ];

  var PRESETS = {
    english: "The quick brown fox jumps over the lazy dog. Mechanistic interpretability is the study of what individual components inside trained neural networks actually compute.",
    japanese: "私の好きな食べ物は寿司です。東京は日本の首都です。",
    code: "def factorial(n):\n    return 1 if n <= 1 else n * factorial(n - 1)\n\nprint(factorial(10))",
    emoji: "I love this 🚀🌟✨ It's really 🔥🔥🔥 going to be 💯 amazing! ❤️🎉🎊",
    numbers: "Phone: 555-123-4567. Order #98765432109876. Price: $1,234,567.89. ZIP: 02139-4307."
  };

  var inputEl = root.querySelector("[data-tc-input]");
  var gridEl  = root.querySelector("[data-tc-grid]");
  var charsEl = root.querySelector("[data-tc-chars]");
  var wordsEl = root.querySelector("[data-tc-words]");
  var bytesEl = root.querySelector("[data-tc-bytes]");

  function classify(text){
    // Detect dominant script. Returns weights summing to 1 across {en, ja, code, emoji, num}.
    var w = { en: 0, ja: 0, code: 0, emoji: 0, num: 0 };
    var n = text.length || 1;
    for (var i = 0; i < text.length; i++){
      var c = text.charCodeAt(i);
      if (c >= 0x3040 && c <= 0x30ff) w.ja++;
      else if (c >= 0x4e00 && c <= 0x9fff) w.ja++;
      else if (c >= 0xac00 && c <= 0xd7af) w.ja++;
      else if (c >= 0x2700 || (c >= 0x1f300 && c <= 0x1faff)) w.emoji++;
      else if (text[i] >= "0" && text[i] <= "9") w.num++;
      else if ("(){}[];=<>".indexOf(text[i]) >= 0) w.code++;
      else w.en++;
    }
    var sum = w.en + w.ja + w.code + w.emoji + w.num;
    if (sum === 0) sum = 1;
    return { en: w.en/sum, ja: w.ja/sum, code: w.code/sum, emoji: w.emoji/sum, num: w.num/sum };
  }

  function tokenCount(text, model){
    var weights = classify(text);
    var charsPerTok = weights.en * model.cpt.en
                    + weights.ja * model.cpt.ja
                    + weights.code * model.cpt.code
                    + weights.emoji * model.cpt.emoji
                    + weights.num * model.cpt.num;
    if (charsPerTok < 0.5) charsPerTok = 0.5;
    var n = text.length;
    return Math.max(1, Math.round(n / charsPerTok));
  }

  function bytesUTF8(text){
    return new TextEncoder().encode(text).length;
  }

  function render(){
    var text = inputEl.value;
    charsEl.textContent = text.length;
    wordsEl.textContent = text.trim().split(/\s+/).filter(Boolean).length;
    bytesEl.textContent = bytesUTF8(text);

    var html = "";
    MODELS.forEach(function(m){
      var n = tokenCount(text, m);
      var cost = (n / 1000) * m.price;
      var costStr = m.price === 0 ? "free / open" : ("$" + cost.toFixed(5) + " / call");
      html += "<div class=\"tc-card\">"+
        "<div class=\"tc-card__name\">"+m.name+"</div>"+
        "<div class=\"tc-card__row\"><span>tokens</span><strong>"+n+"</strong></div>"+
        "<div class=\"tc-card__cost\">"+costStr+"</div>"+
        "</div>";
    });
    gridEl.innerHTML = html;
  }

  inputEl.addEventListener("input", render);
  root.querySelectorAll(".tc-preset-btn").forEach(function(b){
    b.addEventListener("click", function(){
      inputEl.value = PRESETS[b.getAttribute("data-tc-preset")] || "";
      render();
    });
  });

  render();
})();
</script>

<h2 id="tokens-are-the-unit-of-everything">Tokens are the unit of everything</h2>

<p>Inside the transformer:</p>

<ul>
  <li><strong>Embedding table</strong> $W_E \in \mathbb{R}^{V \times d_\text{model}}$: one row per token.</li>
  <li><strong>Attention scores</strong> are computed between pairs of token positions.</li>
  <li><strong>Training loss</strong> is cross-entropy averaged over predicted tokens.</li>
  <li><strong>Context length</strong> is in tokens (e.g. GPT-4o: 128K tokens, Claude: 200K tokens).</li>
  <li><strong>API pricing</strong> is per token.</li>
</ul>

<p>Every operation in the model is an operation on tokens.</p>

<h2 id="special-tokens">Special tokens</h2>

<p>Most models reserve specific IDs outside the regular BPE vocabulary:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">&lt;|endoftext|&gt;</code> (<code class="language-plaintext highlighter-rouge">&lt;bos&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;eos&gt;</code>): document boundary.</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;|im_start|&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;|im_end|&gt;</code>: chat role markers (OpenAI ChatML format).</li>
  <li><code class="language-plaintext highlighter-rouge">[INST]</code>, <code class="language-plaintext highlighter-rouge">[/INST]</code>: instruction markers (Llama 2/3 chat format).</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;|system|&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;|user|&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;|assistant|&gt;</code>: chat-tuned models.</li>
</ul>

<p>These tokens behave like any other input but carry structural meaning. The BOS token in particular accumulates state that several attention heads use as a “rest” position, often called the <strong>BOS sink</strong> (<a href="https://arxiv.org/abs/2309.17453">Xiao et al., 2023</a>; also discussed in <a href="https://transformer-circuits.pub/2023/monosemantic-features/index.html">Bricken et al., 2023</a>).</p>

<h2 id="practical-tokenize-and-inspect">Practical: tokenize and inspect</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">tiktoken</span>
<span class="n">enc</span> <span class="o">=</span> <span class="n">tiktoken</span><span class="p">.</span><span class="n">encoding_for_model</span><span class="p">(</span><span class="s">"gpt-4"</span><span class="p">)</span>
<span class="n">ids</span> <span class="o">=</span> <span class="n">enc</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">"How many r's are in strawberry?"</span><span class="p">)</span>
<span class="n">pieces</span> <span class="o">=</span> <span class="p">[</span><span class="n">enc</span><span class="p">.</span><span class="n">decode</span><span class="p">([</span><span class="n">i</span><span class="p">])</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">ids</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">ids</span><span class="p">,</span> <span class="n">pieces</span><span class="p">)))</span>
<span class="c1"># [(4438, 'How'), (1690, ' many'), (436, ' r'), (596, "'s"), ...]
</span></code></pre></div></div>

<p>For Llama / Mistral / open models:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span>
<span class="n">tok</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"meta-llama/Llama-3-8B"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">tok</span><span class="p">.</span><span class="n">tokenize</span><span class="p">(</span><span class="s">"strawberry"</span><span class="p">))</span>
<span class="c1"># ['str', 'aw', 'berry']
</span></code></pre></div></div>

<p>When studying a new model, dump its vocabulary and search for prompt fragments before drawing conclusions about positions.</p>

<h2 id="wrap">Wrap</h2>

<p>The model’s alphabet is not the user’s alphabet. Tokens are the atom: every embedding lookup, attention computation, and loss term is defined per token. Tokenization choice has measurable downstream effects on arithmetic, multilingual fluency, and character-level reasoning.</p>

<p>The next post is on the residual stream: what those token vectors do once they enter the transformer.</p>

<h2 id="resources">Resources</h2>

<h3 id="foundational-papers">Foundational papers</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1508.07909" target="_blank" rel="noopener"><div class="research-card__title">Neural Machine Translation of Rare Words with Subword Units</div><div class="research-card__authors">Sennrich et al., 2015 · the BPE paper</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2305.14788" target="_blank" rel="noopener"><div class="research-card__title">Tokenization and the Noiseless Channel</div><div class="research-card__authors">Zouhar et al., 2023 · how tokenization choice affects downstream loss</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2402.14903" target="_blank" rel="noopener"><div class="research-card__title">Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs</div><div class="research-card__authors">Singh et al., 2024 · single-digit tokenization fixes arithmetic</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2309.17453" target="_blank" rel="noopener"><div class="research-card__title">Efficient Streaming Language Models with Attention Sinks</div><div class="research-card__authors">Xiao et al., 2023 · BOS-token attention sinks</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/1808.06226" target="_blank" rel="noopener"><div class="research-card__title">SentencePiece: A simple and language independent subword tokenizer</div><div class="research-card__authors">Kudo &amp; Richardson, 2018 · the alternative to BPE used by T5/Llama</div></a></li>
</ul>

<h3 id="tools-and-code">Tools and code</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://github.com/openai/tiktoken" target="_blank" rel="noopener"><div class="research-card__title">tiktoken</div><div class="research-card__authors">OpenAI · fast BPE tokenizer for GPT-3.5/4/4o</div></a></li>
  <li><a class="research-card" href="https://platform.openai.com/tokenizer" target="_blank" rel="noopener"><div class="research-card__title">OpenAI Tokenizer Playground</div><div class="research-card__authors">visualize how any string tokenizes for GPT-3.5/4/4o</div></a></li>
  <li><a class="research-card" href="https://github.com/karpathy/minbpe" target="_blank" rel="noopener"><div class="research-card__title">minbpe</div><div class="research-card__authors">Karpathy · minimal BPE implementation; train your own tokenizer in &lt;200 lines</div></a></li>
  <li><a class="research-card" href="https://www.youtube.com/watch?v=zduSFxRajkE" target="_blank" rel="noopener"><div class="research-card__title">Let's build the GPT Tokenizer</div><div class="research-card__authors">Karpathy · 2-hour deep dive on BPE construction and edge cases</div></a></li>
  <li><a class="research-card" href="https://huggingface.co/docs/tokenizers/index" target="_blank" rel="noopener"><div class="research-card__title">Hugging Face Tokenizers</div><div class="research-card__authors">production-grade BPE, WordPiece, Unigram implementations</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[A token is a subword unit drawn from a fixed vocabulary of 30K, 200K entries. Tokenization shapes capability: arithmetic, multilingual coverage, character-level reasoning all depend on it.]]></summary></entry><entry><title type="html">The Transformer, Demystified: A Factory Floor That Runs on Language</title><link href="https://bhavith-chandra.github.io/posts/the-transformer-demystified/" rel="alternate" type="text/html" title="The Transformer, Demystified: A Factory Floor That Runs on Language" /><published>2026-03-10T00:00:00-07:00</published><updated>2026-03-10T00:00:00-07:00</updated><id>https://bhavith-chandra.github.io/posts/the-transformer-demystified</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/the-transformer-demystified/"><![CDATA[<p>A <strong>decoder-only transformer</strong> is a stack of $N$ identical blocks operating on a sequence of $T$ token vectors. Input: a token sequence. Output: a probability distribution over the vocabulary for the next token. The internals consist of six stations on a conveyor belt called the <strong>residual stream</strong>.</p>

<p>This post defines each station precisely, explains why the architecture replaced RNNs, and sets up the vocabulary used in the rest of the series.</p>

<hr />

<h2 id="tour-the-factory">Tour the factory</h2>

<div class="idemo" id="demo-factory">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · The transformer, as a factory floor</span></div>
    <div class="idemo__body">

      <p class="fac-lead">Six stations. A sentence walks in. A guess at the next word walks out. Tap any station to see what it does and what the token looks like after its shift.</p>

      <div class="fac-input" data-fac-input="">
        <span class="fac-input__label">Input</span>
        <div class="fac-input__sentence" data-fac-sentence="">The cat sat on the <em>_</em></div>
        <div class="fac-input__pickers" data-fac-pickers="">
          <button class="fac-pick is-active" data-fac-pick="0">The cat sat on the ___</button>
          <button class="fac-pick" data-fac-pick="1">Paris is the capital of ___</button>
          <button class="fac-pick" data-fac-pick="2">2 + 2 = ___</button>
        </div>
      </div>

      <div class="fac-floor" data-fac-floor="">
        <svg class="fac-svg" viewBox="0 0 920 260" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
          <defs>
            <linearGradient id="fac-belt" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0%" stop-color="#fef3c7" />
              <stop offset="100%" stop-color="#fde68a" />
            </linearGradient>
            <filter id="fac-glow" x="-50%" y="-50%" width="200%" height="200%">
              <feGaussianBlur stdDeviation="3" result="blur" />
              <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
            </filter>
          </defs>

          <rect x="30" y="140" width="860" height="28" rx="4" fill="url(#fac-belt)" stroke="#c98c3a" stroke-width="1" />
          <g class="fac-belt-ticks" data-fac-ticks=""></g>

          <g data-fac-stations=""></g>
          <g data-fac-token=""></g>
        </svg>

        <div class="fac-ctrl">
          <button class="fac-ctrl__btn" data-fac-play="">▶ Play walk-through</button>
          <button class="fac-ctrl__btn fac-ctrl__btn--ghost" data-fac-reset="">Reset</button>
          <div class="fac-step" data-fac-step="">Stopped · click a station or press play</div>
        </div>
      </div>

      <div class="fac-info" data-fac-info="">
        <div class="fac-info__head">
          <span class="fac-info__idx" data-fac-idx="">, </span>
          <span class="fac-info__title" data-fac-title="">Click a station</span>
        </div>
        <div class="fac-info__desc" data-fac-desc="">Each station does one job. Pick one to see what happens to our little traveller token.</div>
        <div class="fac-info__token" data-fac-token-info=""></div>
      </div>

      <details>
        <summary>What's actually happening inside each station</summary>
        <p><strong>1. Tokenizer</strong> splits text into numeric IDs (often subword pieces). <strong>2. Embedding</strong> looks up a vector for each ID from a big table. <strong>3. Attention</strong> lets every position peek at every other position and pull in what's relevant. <strong>4. MLP</strong> is a per-position feed-forward network that transforms the vector non-linearly. <strong>5. Repeat</strong> stacks attention+MLP dozens of times. <strong>6. Unembedding</strong> projects the final vector into a probability over the vocabulary. The belt you see is the <em>residual stream</em>, the running total that every block reads from and writes to.</p>
      </details>
    </div>
  </div>
</div>

<style>
  #demo-factory .fac-lead { margin: 0 0 1.1rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-factory .fac-input {
    display: flex; flex-direction: column; gap: 0.55rem;
    padding: 0.9rem 1rem; margin-bottom: 1.1rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
  }
  #demo-factory .fac-input__label {
    font-family: var(--nn-mono); font-size: 0.68rem; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--nn-muted);
  }
  #demo-factory .fac-input__sentence {
    font-family: var(--nn-mono); font-size: 1.02rem; color: var(--nn-ink); letter-spacing: 0.01em;
  }
  #demo-factory .fac-input__sentence em {
    display: inline-block; min-width: 1.8em; padding: 0 0.3em;
    color: #b77214; background: rgba(251,191,36,0.18);
    border-radius: 3px; font-style: normal;
  }
  #demo-factory .fac-input__pickers { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.2rem; }
  #demo-factory .fac-pick {
    padding: 0.38rem 0.75rem; font-family: var(--nn-mono); font-size: 0.8rem;
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    color: var(--nn-ink); cursor: pointer; transition: all 120ms;
  }
  #demo-factory .fac-pick:hover { border-color: #b77214; }
  #demo-factory .fac-pick.is-active { background: rgba(251,191,36,0.18); border-color: #b77214; color: #7c4d0a; }

  #demo-factory .fac-floor {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.9rem 0.8rem 0.85rem; margin-bottom: 1rem;
  }
  #demo-factory .fac-svg { display: block; width: 100%; height: auto; }
  #demo-factory .fac-ctrl {
    display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
    margin-top: 0.4rem; padding-top: 0.7rem; border-top: 1px dashed var(--nn-line);
  }
  #demo-factory .fac-ctrl__btn {
    padding: 0.42rem 0.95rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #b77214; color: #fff; border: none; border-radius: 3px;
    cursor: pointer; font-weight: 600;
  }
  #demo-factory .fac-ctrl__btn:hover { background: #7c4d0a; }
  #demo-factory .fac-ctrl__btn--ghost { background: transparent; color: var(--nn-ink); border: 1px solid var(--nn-line); }
  #demo-factory .fac-ctrl__btn--ghost:hover { border-color: #b77214; color: #b77214; background: transparent; }
  #demo-factory .fac-step {
    margin-left: auto; font-family: var(--nn-mono); font-size: 0.75rem;
    color: var(--nn-muted); letter-spacing: 0.03em;
  }

  #demo-factory .fac-info {
    padding: 1rem 1.1rem; background: #fff;
    border: 1px solid var(--nn-line); border-left: 3px solid #b77214;
    border-radius: 3px;
  }
  #demo-factory .fac-info__head { display: flex; gap: 0.7rem; align-items: baseline; margin-bottom: 0.45rem; }
  #demo-factory .fac-info__idx {
    font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: #b77214;
  }
  #demo-factory .fac-info__title { font-size: 1.02rem; font-weight: 600; color: var(--nn-ink); }
  #demo-factory .fac-info__desc { font-size: 0.92rem; color: var(--nn-body); line-height: 1.58; }
  #demo-factory .fac-info__token {
    margin-top: 0.7rem; padding: 0.55rem 0.75rem; background: #fafaf7;
    border: 1px solid var(--nn-line); border-radius: 3px;
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-body);
    word-break: break-word;
  }

  #demo-factory .fac-svg .fac-box {
    fill: #fff; stroke: var(--nn-line); stroke-width: 1.3;
    transition: fill 180ms, stroke 180ms;
    cursor: pointer;
  }
  #demo-factory .fac-svg .fac-box.is-active { fill: #fff6e0; stroke: #b77214; stroke-width: 2; }
  #demo-factory .fac-svg .fac-box.is-visited { fill: #fff9ea; stroke: #c98c3a; }
  #demo-factory .fac-svg .fac-box-label {
    font-family: var(--nn-mono); font-size: 10px; fill: var(--nn-ink);
    text-anchor: middle; pointer-events: none;
  }
  #demo-factory .fac-svg .fac-box-idx {
    font-family: var(--nn-mono); font-size: 9px; fill: var(--nn-muted);
    text-anchor: middle; pointer-events: none;
  }
  #demo-factory .fac-svg .fac-token-circle {
    fill: #b77214; filter: url(#fac-glow);
    transition: cx 700ms cubic-bezier(.4,.05,.2,1);
  }
  #demo-factory .fac-svg .fac-token-label {
    font-family: var(--nn-mono); font-size: 10px; fill: #fff;
    text-anchor: middle; pointer-events: none; font-weight: 600;
    transition: x 700ms cubic-bezier(.4,.05,.2,1);
  }

  @media (max-width: 620px) {
    #demo-factory .fac-info__head { flex-direction: column; gap: 0.15rem; }
    #demo-factory .fac-step { width: 100%; margin-left: 0; }
  }
</style>

<script>
(function(){
  const root = document.getElementById("demo-factory"); if (!root) return;

  const STATIONS = [
    {
      idx: "01",
      label: "Tokenizer",
      desc: "Splits your text into subword pieces and converts each to an integer ID. 'sat' → 7205. Not words. Not characters. Somewhere in between.",
      tokenView: (s) => `IDs: [${s.ids.join(", ")}]`
    },
    {
      idx: "02",
      label: "Embedding",
      desc: "Each ID looks up a fat vector (768 numbers in GPT-2 small). Similar-meaning tokens get similar vectors. This is the model's first guess at what each word 'means'.",
      tokenView: () => `Vectors · 768-d each · 'sat' ≈ [0.12, −0.41, 0.88, 0.03...]`
    },
    {
      idx: "03",
      label: "Attention",
      desc: "Every position looks at every other position and pulls in relevant info. 'sat' notices 'cat', that's the subject it should agree with. This is how context flows.",
      tokenView: () => `Mixed vectors · 'sat' now also carries info about 'cat' and 'the'`
    },
    {
      idx: "04",
      label: "MLP",
      desc: "A per-position feed-forward network. Transforms each vector non-linearly. Think of it as the model doing a little thinking in place, factual recall, transformation, all of it lives here.",
      tokenView: () => `Transformed vectors · same shape, new meaning embedded`
    },
    {
      idx: "05",
      label: "Repeat · 12× blocks",
      desc: "Attention + MLP is one block. Stack twelve of them (for GPT-2 small). Each block reads from and writes to the shared 'residual stream', the belt. The stream accumulates meaning.",
      tokenView: () => `After 12 blocks · vector now encodes 'something that probably follows sat on the'`
    },
    {
      idx: "06",
      label: "Unembedding",
      desc: "Project the final vector against every word in the vocabulary. The word with the highest dot-product wins. Out comes a probability distribution, 'mat' 41%, 'floor' 18%, 'couch' 9%...",
      tokenView: (s) => `Top guess: '${s.guess}' (${s.prob})`
    }
  ];

  const PRESETS = [
    { text: "The cat sat on the ___", ids: [464, 3797, 3332, 319, 262], guess: "mat", prob: "41%" },
    { text: "Paris is the capital of ___", ids: [40313, 318, 262, 3139, 286], guess: "France", prob: "94%" },
    { text: "2 + 2 = ___", ids: [17, 1343, 362, 796], guess: "4", prob: "88%" }
  ];

  let activeStation = -1;
  let playTimer = null;
  let presetIdx = 0;

  const svg = root.querySelector(".fac-svg");
  const stationsG = svg.querySelector("[data-fac-stations]");
  const tokenG = svg.querySelector("[data-fac-token]");
  const ticksG = svg.querySelector("[data-fac-ticks]");
  const infoIdx = root.querySelector("[data-fac-idx]");
  const infoTitle = root.querySelector("[data-fac-title]");
  const infoDesc = root.querySelector("[data-fac-desc]");
  const infoToken = root.querySelector("[data-fac-token-info]");
  const stepLabel = root.querySelector("[data-fac-step]");
  const sentenceEl = root.querySelector("[data-fac-sentence]");

  function drawStations(){
    stationsG.innerHTML = "";
    const x0 = 30, w = 860, y = 80, h = 60;
    STATIONS.forEach((s, i) => {
      const bx = x0 + (w / STATIONS.length) * i + 8;
      const bw = (w / STATIONS.length) - 16;
      const box = document.createElementNS("http://www.w3.org/2000/svg", "g");
      box.innerHTML =
        `<rect class="fac-box" data-station="${i}" x="${bx}" y="${y}" width="${bw}" height="${h}" rx="5"/>` +
        `<text class="fac-box-idx" x="${bx + bw/2}" y="${y + 17}">${s.idx}</text>` +
        `<text class="fac-box-label" x="${bx + bw/2}" y="${y + 40}">${s.label}</text>`;
      stationsG.appendChild(box);
    });
    ticksG.innerHTML = "";
    for (let i = 0; i <= STATIONS.length; i++){
      const tx = x0 + (w / STATIONS.length) * i;
      ticksG.innerHTML += `<line x1="${tx}" y1="168" x2="${tx}" y2="176" stroke="#c98c3a" stroke-width="1"/>`;
    }
    stationsG.querySelectorAll(".fac-box").forEach(b => {
      b.addEventListener("click", () => select(parseInt(b.dataset.station)));
    });
  }

  function drawToken(station){
    const x0 = 30, w = 860;
    const colW = w / STATIONS.length;
    const cx = station < 0 ? x0 + 10 : x0 + colW * station + colW / 2;
    tokenG.innerHTML =
      `<circle class="fac-token-circle" cx="${cx}" cy="154" r="16"/>` +
      `<text class="fac-token-label" x="${cx}" y="157">sat</text>`;
  }

  function updateInfo(station){
    if (station < 0) {
      infoIdx.textContent = ", ";
      infoTitle.textContent = "Click a station";
      infoDesc.textContent = "Each station does one job. Pick one to see what happens to our little traveller token.";
      infoToken.textContent = "";
      return;
    }
    const s = STATIONS[station];
    infoIdx.textContent = `Station ${s.idx}`;
    infoTitle.textContent = s.label;
    infoDesc.textContent = s.desc;
    infoToken.textContent = s.tokenView(PRESETS[presetIdx]);
  }

  function select(station){
    stopPlay();
    activeStation = station;
    stationsG.querySelectorAll(".fac-box").forEach((b, i) => {
      b.classList.toggle("is-active", i === station);
      b.classList.toggle("is-visited", i < station);
    });
    drawToken(station);
    updateInfo(station);
    stepLabel.textContent = `Step ${station + 1} / ${STATIONS.length}`;
  }

  function play(){
    stopPlay();
    let i = 0;
    select(i);
    playTimer = setInterval(() => {
      i++;
      if (i >= STATIONS.length){ stopPlay(); stepLabel.textContent = "Done · try another sentence"; return; }
      select(i);
    }, 1600);
  }
  function stopPlay(){ if (playTimer){ clearInterval(playTimer); playTimer = null; } }

  function reset(){
    stopPlay();
    activeStation = -1;
    stationsG.querySelectorAll(".fac-box").forEach(b => { b.classList.remove("is-active","is-visited"); });
    drawToken(-1);
    updateInfo(-1);
    stepLabel.textContent = "Stopped · click a station or press play";
  }

  function pickPreset(i){
    presetIdx = i;
    root.querySelectorAll("[data-fac-pick]").forEach(b => b.classList.toggle("is-active", parseInt(b.dataset.facPick) === i));
    const p = PRESETS[i];
    sentenceEl.innerHTML = p.text.replace("___", "<em>_</em>");
    if (activeStation >= 0) updateInfo(activeStation);
  }

  root.querySelector("[data-fac-play]").addEventListener("click", play);
  root.querySelector("[data-fac-reset]").addEventListener("click", reset);
  root.querySelectorAll("[data-fac-pick]").forEach(b => {
    b.addEventListener("click", () => pickPreset(parseInt(b.dataset.facPick)));
  });

  drawStations();
  drawToken(-1);
  updateInfo(-1);
})();
</script>

<p>Click a station, press play, watch one token’s vector traverse the pipeline. The structure is identical for every modern LLM: GPT-2, GPT-3, GPT-4, Claude, Llama, Gemini.</p>

<h2 id="the-six-stations">The six stations</h2>

<p><strong>1. Tokenizer.</strong> Maps a string to a sequence of integer IDs using a fixed vocabulary $V$ (typical sizes: 50,257 for GPT-2, 100,277 for GPT-4, 128,000 for Llama 3). Modern tokenizers use byte-pair encoding (BPE) or its variants. Example: <code class="language-plaintext highlighter-rouge">"unhappily"</code> → <code class="language-plaintext highlighter-rouge">["un", "happ", "ily"]</code> → <code class="language-plaintext highlighter-rouge">[403, 7829, 6148]</code>.</p>

<p><strong>2. Embedding.</strong> A learned lookup table $W_E \in \mathbb{R}^{V \times d_\text{model}}$ maps each token ID to a $d_\text{model}$-dimensional vector. Common sizes: $d_\text{model} = 768$ (GPT-2 small), 4,096 (Llama 3 8B), 12,288 (GPT-3 175B). The $i$-th row of $W_E$ is the embedding for token $i$.</p>

<p><strong>3. Self-attention.</strong> At each position $t$, the model computes weighted sums over all positions $\le t$ (causal mask). Weights are derived from query/key dot products. Output: a new vector at each position that has read from earlier positions. Multiple heads run in parallel ($n_\text{heads} = 12, 32, 96, \ldots$), each in a lower-dimensional subspace.</p>

<p><strong>4. MLP.</strong> A position-wise feed-forward network: two linear layers with a non-linearity (GeLU, SwiGLU). Hidden dimension is typically $4 d_\text{model}$. Same shape in, same shape out. Operates independently on each position.</p>

<p><strong>5. Repeat.</strong> One <strong>block</strong> = attention + MLP + residual connections + layer norms. Stack $N$ of them. GPT-2 small: $N=12$. Llama 3 8B: $N=32$. GPT-3 175B: $N=96$. GPT-4 (rumoured): $N \approx 120$.</p>

<p><strong>6. Unembedding.</strong> Multiply the final residual stream vector by $W_U \in \mathbb{R}^{d_\text{model} \times V}$ to produce <strong>logits</strong>. Apply softmax to get a probability distribution over the vocabulary. Sample → next token.</p>

<p>Total parameters scale roughly as $N \cdot d_\text{model}^2 \cdot 12$ for the standard recipe.</p>

<div class="idemo idemo--mini" id="demo-softmax">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Softmax with temperature</span></div>
    <div class="idemo__body">

      <p class="sm-lead">Five logits, one softmax, one temperature dial. Drag the slider and watch the probability mass redistribute. This is the very last computation in a transformer: scores in, distribution out, sample one.</p>

      <div class="sm-stage">
        <div class="sm-rows" data-sm-rows=""></div>
      </div>

      <div class="sm-controls">
        <label class="sm-label">temperature: <strong data-sm-tval="">1.00</strong></label>
        <input type="range" min="0" max="200" value="100" step="1" class="sm-slider" data-sm-temp="" />
        <button class="sm-btn" data-sm-sample="">sample 1 token</button>
        <button class="sm-btn sm-btn--ghost" data-sm-reset="">reset history</button>
      </div>

      <div class="sm-history" data-sm-history="">sampled history will appear here.</div>

      <p class="sm-hint" data-sm-hint=""><strong>What you're watching:</strong> as <em>T</em> drops toward 0 the distribution sharpens onto the top logit (greedy). As <em>T</em> grows the distribution flattens toward uniform (chaos). Temperature 1.0 is the model's natural distribution. Most chatbots use 0.7, 1.0.</p>
    </div>
  </div>
</div>

<style>
  #demo-softmax .sm-lead { margin: 0 0 0.95rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-softmax .sm-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.85rem 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-softmax .sm-rows { display: flex; flex-direction: column; gap: 0.45rem; }
  #demo-softmax .sm-row {
    display: grid; grid-template-columns: 90px 1fr 70px;
    align-items: center; gap: 0.6rem;
  }
  #demo-softmax .sm-row__tok {
    font-family: var(--nn-mono); font-size: 0.84rem; color: var(--nn-ink);
  }
  #demo-softmax .sm-row__bar {
    height: 18px; background: #f5f1e8; border-radius: 3px; overflow: hidden;
    border: 1px solid var(--nn-line);
  }
  #demo-softmax .sm-row__fill {
    height: 100%; background: #b77214;
    transition: width 220ms cubic-bezier(.3,.5,.3,1);
  }
  #demo-softmax .sm-row__num {
    font-family: var(--nn-mono); font-size: 0.78rem;
    color: var(--nn-muted); text-align: right;
  }
  #demo-softmax .sm-controls {
    display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
    margin-bottom: 0.65rem;
  }
  #demo-softmax .sm-label {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted);
  }
  #demo-softmax .sm-label strong { color: var(--nn-ink); }
  #demo-softmax .sm-slider { flex: 1; min-width: 140px; accent-color: #b77214; }
  #demo-softmax .sm-btn {
    padding: 0.42rem 0.85rem; font-family: var(--nn-mono); font-size: 0.78rem;
    background: #b77214; color: #fff; border: 1px solid #b77214; border-radius: 3px; cursor: pointer;
  }
  #demo-softmax .sm-btn:hover { background: #7c4d0a; }
  #demo-softmax .sm-btn--ghost { background: #fff; color: var(--nn-ink); }
  #demo-softmax .sm-btn--ghost:hover { border-color: #b77214; background: #fff; }
  #demo-softmax .sm-history {
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-body);
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.5rem 0.75rem; min-height: 32px; margin-bottom: 0.85rem;
    word-break: break-word;
  }
  #demo-softmax .sm-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-softmax .sm-hint em { color: #7c4d0a; font-style: italic; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-softmax"); if (!root) return;
  var TOKENS = [
    { tok: " Paris",   logit: 4.2 },
    { tok: " France",  logit: 2.1 },
    { tok: " Europe",  logit: 1.4 },
    { tok: " Britain", logit: 0.6 },
    { tok: " Berlin",  logit: 0.2 }
  ];

  var rowsEl = root.querySelector("[data-sm-rows]");
  var tempEl = root.querySelector("[data-sm-temp]");
  var tvalEl = root.querySelector("[data-sm-tval]");
  var historyEl = root.querySelector("[data-sm-history]");
  var sampleBtn = root.querySelector("[data-sm-sample]");
  var resetBtn = root.querySelector("[data-sm-reset]");

  var history = [];

  function softmax(logits, T){
    if (T < 0.01) T = 0.01;
    var max = -Infinity;
    for (var i = 0; i < logits.length; i++){ if (logits[i]/T > max) max = logits[i]/T; }
    var exps = logits.map(function(l){ return Math.exp(l/T - max); });
    var sum = exps.reduce(function(a,b){ return a+b; }, 0);
    return exps.map(function(e){ return e / sum; });
  }

  function render(){
    var T = parseInt(tempEl.value, 10) / 100;
    tvalEl.textContent = T.toFixed(2);
    var probs = softmax(TOKENS.map(function(t){ return t.logit; }), T);
    var html = "";
    for (var i = 0; i < TOKENS.length; i++){
      var p = probs[i];
      html += "<div class=\"sm-row\">"+
        "<div class=\"sm-row__tok\">"+TOKENS[i].tok+"</div>"+
        "<div class=\"sm-row__bar\"><div class=\"sm-row__fill\" style=\"width:"+(p*100).toFixed(2)+"%\"></div></div>"+
        "<div class=\"sm-row__num\">"+(p*100).toFixed(1)+"%</div>"+
        "</div>";
    }
    rowsEl.innerHTML = html;
  }

  function sample(){
    var T = parseInt(tempEl.value, 10) / 100;
    var probs = softmax(TOKENS.map(function(t){ return t.logit; }), T);
    var r = Math.random(), cum = 0;
    for (var i = 0; i < probs.length; i++){
      cum += probs[i];
      if (r < cum){
        history.push(TOKENS[i].tok.trim());
        if (history.length > 18) history.shift();
        historyEl.textContent = history.join(" · ");
        return;
      }
    }
  }

  tempEl.addEventListener("input", render);
  sampleBtn.addEventListener("click", sample);
  resetBtn.addEventListener("click", function(){
    history = [];
    historyEl.textContent = "sampled history will appear here.";
  });

  render();
})();
</script>

<aside class="callout callout--analogy">
  <div class="callout__label">Analogy</div>
  <p>Each station reads the conveyor belt, computes a small contribution, and adds it back. By the final station the vector at the last position has accumulated enough information to identify the next token.</p>
</aside>

<h2 id="the-residual-stream">The residual stream</h2>

<p>In the literature this conveyor belt is the <strong>residual stream</strong>. Each block’s output is <em>added</em> to its input, not substituted:</p>

\[x_i \;\leftarrow\; x_i + \text{block}(x_i)\]

<div class="math-translate">In words: the new belt vector equals the old belt vector plus whatever the block computed. Nothing is overwritten; contributions accumulate.</div>

<div class="idemo demo-acc2" id="demo-accumulator">
  <div class="acc2__head">
    <span class="acc2__title">Mini · Residual stream as a running sum</span>
    <div class="acc2__how">
      <strong>How to play:</strong> each row is a block. Toggle blocks on/off — the running sum at the bottom updates instantly, and so does the model's "best guess" at the next token. Reorder, mute, or invert any block. <em>The point: the belt is just whatever you've added so far.</em>
    </div>
  </div>

  <div class="acc2__presets">
    <span class="acc2__plabel">Prompt</span>
    <button class="acc2__pbtn is-active" data-prompt="paris">Paris is the capital of</button>
    <button class="acc2__pbtn" data-prompt="cat">The cat sat on the</button>
    <button class="acc2__pbtn" data-prompt="ioi">When John and Mary went to the store, John gave a drink to</button>
  </div>

  <div class="acc2__board">
    <div class="acc2__feature-axis">
      <span>place</span><span>person</span><span>tense</span><span>plural</span><span>code</span>
    </div>
    <div class="acc2__rows" data-acc-rows=""></div>
    <div class="acc2__sumrow">
      <div class="acc2__sumlabel">RUNNING SUM</div>
      <div class="acc2__sumbars" data-acc-sumbars=""></div>
      <div class="acc2__lens" data-acc-lens="">—</div>
    </div>
  </div>

  <div class="acc2__controls">
    <button class="acc2__btn" data-act="all-on">▶ All blocks on</button>
    <button class="acc2__btn" data-act="step">⏭ Add next block</button>
    <button class="acc2__btn" data-act="reset">⏮ Reset</button>
    <button class="acc2__btn" data-act="play">🔁 Auto-play</button>
  </div>

  <p class="acc2__hint" data-acc-hint=""><strong>What you should notice:</strong> the embedding alone gives a vague guess. As more blocks get added, certain feature dimensions (place / person / tense …) light up and the prediction sharpens. Mute any single block — the loss in confidence tells you that block's contribution. That's exactly what direct logit attribution does at scale.</p>
</div>
<style>
  .demo-acc2{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-acc2 .acc2__title{font-weight:700;color:#7c4d0a;font-size:15px;display:block;margin-bottom:6px}
  .demo-acc2 .acc2__how{font-size:13px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px;line-height:1.55}
  .demo-acc2 .acc2__how strong{color:#7c4d0a}
  .demo-acc2 .acc2__how em{font-style:italic;color:#7c4d0a}
  .demo-acc2 .acc2__presets{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:14px 0 10px}
  .demo-acc2 .acc2__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;font-weight:700;margin-right:4px}
  .demo-acc2 .acc2__pbtn{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:5px 10px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-acc2 .acc2__pbtn.is-active{background:#fbbf24;color:#3a2106;border-color:#b77214}
  .demo-acc2 .acc2__board{background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;padding:12px}
  .demo-acc2 .acc2__feature-axis{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;font-size:10px;color:#a08562;text-transform:uppercase;letter-spacing:0.06em;font-weight:700;margin-left:178px;margin-bottom:6px}
  .demo-acc2 .acc2__feature-axis span{text-align:center}
  .demo-acc2 .acc2__rows{display:flex;flex-direction:column;gap:5px}
  .demo-acc2 .acc2__row{display:grid;grid-template-columns:36px 130px 1fr 60px;gap:8px;align-items:center;padding:5px 6px;border-radius:6px;background:#fffaf3;border:1px solid #ecdbc0;transition:background 0.18s}
  .demo-acc2 .acc2__row.is-muted{opacity:0.4;background:#f5f1e8}
  .demo-acc2 .acc2__row.is-inv .acc2__rbars div{filter:hue-rotate(180deg) invert(0.05)}
  .demo-acc2 .acc2__rtoggle{display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;width:24px;height:24px;border-radius:50%;background:#fbbf24;color:#3a2106;border:1px solid #b77214;font-weight:700;transition:all 0.15s}
  .demo-acc2 .acc2__row.is-muted .acc2__rtoggle{background:#fff6e0;color:#a08562}
  .demo-acc2 .acc2__rname{font-size:12px;font-weight:600;color:#7c4d0a;font-family:ui-monospace,Menlo,monospace}
  .demo-acc2 .acc2__rname small{display:block;font-weight:400;color:#a08562;font-size:10px;margin-top:1px}
  .demo-acc2 .acc2__rbars{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;height:22px;align-items:end}
  .demo-acc2 .acc2__rbars div{height:100%;border-radius:3px;transition:transform 0.25s,background 0.2s}
  .demo-acc2 .acc2__rinvert{font-size:10px;border:1px solid #ddb88e;background:#fff;color:#7c4d0a;padding:3px 6px;border-radius:4px;cursor:pointer;font-family:inherit;font-weight:600}
  .demo-acc2 .acc2__rinvert.is-inv{background:#b25c2c;color:#fff;border-color:#b25c2c}
  .demo-acc2 .acc2__sumrow{margin-top:14px;padding:10px;background:linear-gradient(to right,#fff6e0,#fde9bf);border:2px solid #b77214;border-radius:8px;display:grid;grid-template-columns:130px 1fr 1fr;gap:12px;align-items:center}
  .demo-acc2 .acc2__sumlabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.08em;font-weight:700}
  .demo-acc2 .acc2__sumbars{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;height:32px;align-items:end}
  .demo-acc2 .acc2__sumbars div{border-radius:4px;background:#b77214;transition:height 0.35s cubic-bezier(.3,.6,.3,1)}
  .demo-acc2 .acc2__lens{font-family:ui-monospace,Menlo,monospace;font-size:13px;color:#3a2106;background:#fffefb;border:1px solid #b77214;border-radius:6px;padding:6px 10px;text-align:center;font-weight:600}
  .demo-acc2 .acc2__lens b{color:#b25c2c}
  .demo-acc2 .acc2__controls{display:flex;gap:6px;flex-wrap:wrap;margin:12px 0}
  .demo-acc2 .acc2__btn{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-acc2 .acc2__btn:hover{background:#fbbf24;color:#3a2106}
  .demo-acc2 .acc2__hint{font-size:13px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px;line-height:1.6;margin:0}
  .demo-acc2 .acc2__hint strong{color:#7c4d0a}
  @media (max-width:560px){.demo-acc2 .acc2__row{grid-template-columns:32px 100px 1fr;}.demo-acc2 .acc2__rinvert{display:none}.demo-acc2 .acc2__sumrow{grid-template-columns:1fr}.demo-acc2 .acc2__feature-axis{margin-left:140px}}
</style>

<script>
(function(){
  const root=document.getElementById('demo-accumulator'); if(!root) return;
  const PROMPTS={
    paris:{
      label:'Paris is the capital of',
      blocks:[
        {n:'embedding',d:'tokens → vectors',v:[0.6,0.0,0.1,0.0,0.0]},
        {n:'block 0',d:'previous-token, syntax',v:[0.3,0.0,0.05,0.05,0.0]},
        {n:'block 1',d:'positional patterns',v:[0.2,0.05,0.1,0.0,0.0]},
        {n:'block 2',d:'entity binding',v:[0.7,0.1,0.05,0.0,0.0]},
        {n:'block 3',d:'fact lookup (MLP)',v:[1.0,0.05,0.1,0.05,0.0]},
        {n:'block 4',d:'name-mover heads',v:[0.85,0.0,0.0,0.0,0.0]},
        {n:'block 5',d:'late sharpening',v:[0.5,0.0,0.0,0.0,0.0]}
      ],
      lens:[0.05,0.10,0.18,0.25,0.45,0.78,0.93],
      tokens:[' Paris',' France',' Europe',' Italy',' the']
    },
    cat:{
      label:'The cat sat on the',
      blocks:[
        {n:'embedding',d:'tokens → vectors',v:[0.0,0.4,0.05,0.6,0.0]},
        {n:'block 0',d:'previous-token, syntax',v:[0.0,0.1,0.1,0.3,0.0]},
        {n:'block 1',d:'spatial relations',v:[0.1,0.2,0.05,0.2,0.0]},
        {n:'block 2',d:'preposition handling',v:[0.05,0.15,0.1,0.4,0.0]},
        {n:'block 3',d:'context blending',v:[0.0,0.3,0.1,0.55,0.0]},
        {n:'block 4',d:'top-token selection',v:[0.0,0.4,0.1,0.6,0.0]},
        {n:'block 5',d:'sharpening',v:[0.0,0.2,0.0,0.4,0.0]}
      ],
      lens:[0.05,0.08,0.12,0.20,0.35,0.55,0.72],
      tokens:[' mat',' floor',' chair',' rug',' bed']
    },
    ioi:{
      label:'When John and Mary went to the store, John gave a drink to',
      blocks:[
        {n:'embedding',d:'tokens → vectors',v:[0.0,0.5,0.0,0.0,0.0]},
        {n:'block 0',d:'previous-token, syntax',v:[0.0,0.2,0.05,0.0,0.0]},
        {n:'block 1',d:'duplicate-token detect',v:[0.0,0.4,0.0,0.0,0.0]},
        {n:'block 2',d:'S-Inhibition (suppress John)',v:[0.0,-0.5,0.0,0.0,0.0]},
        {n:'block 3',d:'Name-Mover (find Mary)',v:[0.0,0.9,0.05,0.0,0.0]},
        {n:'block 4',d:'Negative Name-Mover',v:[0.0,-0.15,0.0,0.0,0.0]},
        {n:'block 5',d:'final sharpening',v:[0.0,0.4,0.0,0.0,0.0]}
      ],
      lens:[0.05,0.12,0.18,0.28,0.55,0.82,0.90],
      tokens:[' Mary',' John',' him',' her',' the']
    }
  };
  let promptKey='paris', state=null;
  function init(){
    const P=PROMPTS[promptKey];
    state={blocks:P.blocks.map((b,i)=>({...b,muted:false,inverted:false,added:i===0||i<=1}))};
    render();
  }
  function compute(){
    const sum=[0,0,0,0,0]; let upTo=-1;
    state.blocks.forEach((b,i)=>{ if(b.added && !b.muted){ const sign=b.inverted?-1:1; b.v.forEach((x,k)=>sum[k]+=x*sign); upTo=i;}});
    return {sum,upTo};
  }
  function lensPredict(upTo){
    const P=PROMPTS[promptKey]; if(upTo<0) return {tok:'(no signal)',conf:0};
    const conf=P.lens[Math.min(upTo,P.lens.length-1)];
    let mutedPenalty=state.blocks.filter(b=>b.added&&b.muted).length*0.12;
    let invPenalty=state.blocks.filter(b=>b.added&&b.inverted).length*0.25;
    const adjConf=Math.max(0.05,conf-mutedPenalty-invPenalty);
    return {tok:P.tokens[0],conf:adjConf,others:P.tokens};
  }
  function render(){
    const rows=root.querySelector('[data-acc-rows]'); rows.innerHTML='';
    const P=PROMPTS[promptKey];
    state.blocks.forEach((b,i)=>{
      const r=document.createElement('div'); r.className='acc2__row'+(b.muted?' is-muted':'')+(b.inverted?' is-inv':'')+(b.added?'':' is-pending');
      if(!b.added) r.style.opacity='0.35';
      const tog=document.createElement('div'); tog.className='acc2__rtoggle'; tog.textContent=b.muted?'·':'●'; tog.title=b.muted?'click to enable':'click to mute';
      tog.addEventListener('click',()=>{if(!b.added){b.added=true;} else {b.muted=!b.muted;} render();});
      const name=document.createElement('div'); name.className='acc2__rname'; name.innerHTML=`${b.n}<small>${b.d}</small>`;
      const bars=document.createElement('div'); bars.className='acc2__rbars';
      b.v.forEach((x,k)=>{const d=document.createElement('div'); const a=Math.abs(x)*100; d.style.height=a+'%'; d.style.background=x>=0?'#fbbf24':'#b25c2c'; d.style.opacity=b.added&&!b.muted?'1':'0.25'; bars.appendChild(d);});
      const inv=document.createElement('button'); inv.className='acc2__rinvert'+(b.inverted?' is-inv':''); inv.textContent=b.inverted?'INV':'inv'; inv.title='invert this block'; inv.addEventListener('click',()=>{b.inverted=!b.inverted; render();});
      r.appendChild(tog); r.appendChild(name); r.appendChild(bars); r.appendChild(inv); rows.appendChild(r);
    });
    const {sum,upTo}=compute();
    const sumbars=root.querySelector('[data-acc-sumbars]'); sumbars.innerHTML='';
    const mx=Math.max(...sum.map(Math.abs),1);
    sum.forEach(x=>{const d=document.createElement('div'); d.style.height=(Math.abs(x)/mx*100)+'%'; d.style.background=x>=0?'#b77214':'#b25c2c'; sumbars.appendChild(d);});
    const lens=lensPredict(upTo);
    const lensEl=root.querySelector('[data-acc-lens]');
    if(upTo<0){ lensEl.innerHTML='no blocks yet · belt empty'; }
    else {
      const conf=(lens.conf*100).toFixed(0);
      lensEl.innerHTML=`logit lens: <b>"${lens.tok}"</b> @ ${conf}%`;
    }
  }
  function allOn(){state.blocks.forEach(b=>{b.added=true; b.muted=false; b.inverted=false;}); render();}
  function step(){const next=state.blocks.findIndex(b=>!b.added); if(next>=0){state.blocks[next].added=true; render();}}
  let auto=null;
  function play(){if(auto){clearInterval(auto); auto=null; return;} init(); auto=setInterval(()=>{const next=state.blocks.findIndex(b=>!b.added); if(next<0){clearInterval(auto); auto=null;} else {state.blocks[next].added=true; render();}},700);}
  root.querySelectorAll('[data-prompt]').forEach(b=>b.addEventListener('click',()=>{root.querySelectorAll('[data-prompt]').forEach(x=>x.classList.remove('is-active')); b.classList.add('is-active'); promptKey=b.dataset.prompt; init();}));
  root.querySelector('[data-act="all-on"]').addEventListener('click',allOn);
  root.querySelector('[data-act="step"]').addEventListener('click',step);
  root.querySelector('[data-act="reset"]').addEventListener('click',init);
  root.querySelector('[data-act="play"]').addEventListener('click',play);
  init();
})();
</script>

<p>Three consequences follow directly from this additive structure:</p>

<ol>
  <li><strong>Gradient flow.</strong> Information from layer 0 reaches layer $N$ along the identity path. This is why deep transformers train in the first place.</li>
  <li><strong>Per-block delta.</strong> Each block computes a <em>correction</em>, not a full representation. Easier optimization target.</li>
  <li><strong>Linear decomposability.</strong> The output is a sum of token embeddings + each block’s contribution. Mechanistic interpretability uses this directly: ablate one head, see the difference in the logits.</li>
</ol>

<p>Elhage et al. (<a href="https://transformer-circuits.pub/2021/framework/index.html">2021, “Mathematical Framework”</a>) formalize this view. The residual stream is the central object in the rest of this series.</p>

<h2 id="why-transformers-replaced-rnns">Why transformers replaced RNNs</h2>

<p><strong>Recurrent neural networks (RNNs)</strong> process tokens sequentially, maintaining a hidden state $h_t = f(h_{t-1}, x_t)$. Two structural problems:</p>

<ol>
  <li><strong>No parallelism.</strong> Token $t$ depends on $h_{t-1}$. Cannot batch positions on a GPU.</li>
  <li><strong>Vanishing dependencies.</strong> Gradients through 1,000 timesteps decay or explode. LSTMs and GRUs help but do not solve it at scale.</li>
</ol>

<p>Transformers (<a href="https://arxiv.org/abs/1706.03762">Vaswani et al., 2017</a>) replace recurrence with self-attention: every position reads from every other position in parallel via a single matrix multiplication. Sequence length $T$ → $O(T^2)$ time, but fully parallel within a sequence and across batch dimensions. On modern GPUs the constant factor wins.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>Every modern LLM is a decoder-only transformer with the same six stations. Different sizes, different training data, different fine-tuning. The interpretability tools from this series transfer directly across all of them.</p>
</aside>

<h2 id="decoder-only">Decoder-only</h2>

<p>The original 2017 paper had two halves: an <strong>encoder</strong> (reads input) and a <strong>decoder</strong> (writes output), wired for machine translation. For autoregressive text generation, only the decoder is needed.</p>

<p>A <strong>decoder-only transformer</strong> uses <strong>causal self-attention</strong>: position $t$ may only attend to positions $0, 1, \ldots, t$. This enforces left-to-right generation and matches the next-token-prediction training objective.</p>

<p>This series is exclusively about decoder-only transformers. (Encoder-only models like BERT are used for classification and embedding tasks, not generation.)</p>

<h2 id="autoregressive-generation">Autoregressive generation</h2>

<p>The transformer produces one probability distribution per forward pass. To generate text:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>input:  "The cat sat on the"          → logits → sample → "mat"
input:  "The cat sat on the mat"      → logits → sample → "."
input:  "The cat sat on the mat."     → logits → sample → "&lt;eos&gt;"
</code></pre></div></div>

<p>This is <strong>autoregressive decoding</strong>. Each generated token is appended to the input and the model runs again. Sampling strategies (greedy, top-k, top-p, temperature) determine <em>which</em> token gets picked from the distribution. KV-caching avoids recomputing attention over previous tokens, making the per-step cost roughly $O(T)$ rather than $O(T^2)$.</p>

<h2 id="key-dimensions-by-model">Key dimensions, by model</h2>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>$N$</th>
      <th>$d_\text{model}$</th>
      <th>$n_\text{heads}$</th>
      <th>$V$</th>
      <th>Parameters</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPT-2 small</td>
      <td>12</td>
      <td>768</td>
      <td>12</td>
      <td>50,257</td>
      <td>124M</td>
    </tr>
    <tr>
      <td>GPT-2 XL</td>
      <td>48</td>
      <td>1,600</td>
      <td>25</td>
      <td>50,257</td>
      <td>1.5B</td>
    </tr>
    <tr>
      <td>GPT-3</td>
      <td>96</td>
      <td>12,288</td>
      <td>96</td>
      <td>50,257</td>
      <td>175B</td>
    </tr>
    <tr>
      <td>Llama 3 8B</td>
      <td>32</td>
      <td>4,096</td>
      <td>32</td>
      <td>128,000</td>
      <td>8B</td>
    </tr>
    <tr>
      <td>Llama 3 70B</td>
      <td>80</td>
      <td>8,192</td>
      <td>64</td>
      <td>128,000</td>
      <td>70B</td>
    </tr>
  </tbody>
</table>

<p>Memorize one row (GPT-2 small is most common in MI papers) and use it as the reference point.</p>

<div class="idemo idemo--mini" id="demo-scale">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Mini · Build your own transformer (and find out which model it is)</span></div>
    <div class="idemo__body">

      <p class="sc-lead">Three sliders. Pick a depth, width, and head count. The demo computes total parameters, the attention/MLP split, FP16 memory, and matches your settings to the closest published model.</p>

      <div class="sc-stage">
        <div class="sc-sliders">
          <label class="sc-slider-row">
            <span>layers (N)</span>
            <input type="range" min="1" max="120" step="1" value="12" data-sc-n="" />
            <strong data-sc-n-val="">12</strong>
          </label>
          <label class="sc-slider-row">
            <span>d_model</span>
            <input type="range" min="0" max="11" step="1" value="3" data-sc-d="" />
            <strong data-sc-d-val="">768</strong>
          </label>
          <label class="sc-slider-row">
            <span>heads</span>
            <input type="range" min="1" max="128" step="1" value="12" data-sc-h="" />
            <strong data-sc-h-val="">12</strong>
          </label>
        </div>

        <div class="sc-readout">
          <div class="sc-stat">
            <div class="sc-stat__lbl">total params</div>
            <div class="sc-stat__val" data-sc-total="">0</div>
          </div>
          <div class="sc-stat">
            <div class="sc-stat__lbl">FP16 memory</div>
            <div class="sc-stat__val" data-sc-mem="">0 MB</div>
          </div>
          <div class="sc-stat">
            <div class="sc-stat__lbl">closest model</div>
            <div class="sc-stat__val" data-sc-match="">?</div>
          </div>
        </div>

        <div class="sc-bar">
          <div class="sc-bar__seg sc-bar__seg--attn" data-sc-attn-w=""></div>
          <div class="sc-bar__seg sc-bar__seg--mlp" data-sc-mlp-w=""></div>
          <div class="sc-bar__seg sc-bar__seg--emb" data-sc-emb-w=""></div>
        </div>
        <div class="sc-legend">
          <span><i class="sc-leg sc-leg--attn"></i> attention <strong data-sc-attn-pct=""></strong></span>
          <span><i class="sc-leg sc-leg--mlp"></i> MLP <strong data-sc-mlp-pct=""></strong></span>
          <span><i class="sc-leg sc-leg--emb"></i> embed/unembed <strong data-sc-emb-pct=""></strong></span>
        </div>
      </div>

      <p class="sc-hint"><strong>What you're learning:</strong> in any standard transformer, MLPs hold roughly twice the parameters of attention. Move the sliders and the ratio barely budges, ~⅔ MLP, ~⅓ attention, plus a small embedding/unembedding chunk that shrinks as you scale up. This is why interpretability work that focuses only on attention is missing where most of the model lives.</p>
    </div>
  </div>
</div>

<style>
  #demo-scale .sc-lead { margin: 0 0 0.85rem !important; font-size: 0.96rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }
  #demo-scale .sc-stage {
    background: #fafaf7; border: 1px solid var(--nn-line); border-radius: 4px;
    padding: 0.95rem; margin-bottom: 0.85rem;
  }
  #demo-scale .sc-sliders { display: flex; flex-direction: column; gap: 0.55rem; margin-bottom: 0.95rem; }
  #demo-scale .sc-slider-row {
    display: grid; grid-template-columns: 90px 1fr 70px; gap: 0.7rem; align-items: center;
  }
  #demo-scale .sc-slider-row > span {
    font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.06em;
  }
  #demo-scale .sc-slider-row input { accent-color: #b77214; }
  #demo-scale .sc-slider-row strong {
    font-family: var(--nn-mono); font-size: 0.84rem; color: var(--nn-ink);
    text-align: right;
  }
  #demo-scale .sc-readout {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.55rem;
    margin-bottom: 0.95rem;
  }
  #demo-scale .sc-stat {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    padding: 0.55rem 0.7rem;
  }
  #demo-scale .sc-stat__lbl {
    font-family: var(--nn-mono); font-size: 0.66rem; color: var(--nn-muted);
    text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.2rem;
  }
  #demo-scale .sc-stat__val {
    font-family: var(--nn-mono); font-size: 0.95rem; color: #7c4d0a; font-weight: 600;
  }
  #demo-scale .sc-bar {
    display: flex; height: 22px; border-radius: 3px; overflow: hidden;
    border: 1px solid var(--nn-line); margin-bottom: 0.5rem;
  }
  #demo-scale .sc-bar__seg { transition: flex-basis 320ms cubic-bezier(.3,.5,.3,1); }
  #demo-scale .sc-bar__seg--attn { background: #b77214; }
  #demo-scale .sc-bar__seg--mlp  { background: #7c4d0a; }
  #demo-scale .sc-bar__seg--emb  { background: #ddb88e; }
  #demo-scale .sc-legend {
    display: flex; gap: 1.1rem; flex-wrap: wrap;
    font-family: var(--nn-mono); font-size: 0.72rem; color: var(--nn-muted);
  }
  #demo-scale .sc-legend strong { color: var(--nn-ink); margin-left: 0.25rem; }
  #demo-scale .sc-leg {
    display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px;
    vertical-align: middle;
  }
  #demo-scale .sc-leg--attn { background: #b77214; }
  #demo-scale .sc-leg--mlp { background: #7c4d0a; }
  #demo-scale .sc-leg--emb { background: #ddb88e; }
  #demo-scale .sc-hint {
    font-size: 0.92rem; color: var(--nn-body); line-height: 1.6;
    padding: 0.7rem 0.85rem; background: #fff;
    border: 1px solid var(--nn-line); border-radius: 3px;
  }
  #demo-scale .sc-hint strong { color: #7c4d0a; }
</style>

<script>
(function(){
  var root = document.getElementById("demo-scale"); if (!root) return;

  var D_OPTIONS = [128, 256, 384, 768, 1024, 1600, 2048, 4096, 5120, 8192, 12288, 16384];
  var V = 50257;

  var KNOWN = [
    { name: "GPT-2 small",  N: 12, d: 768,   h: 12,  total: 124e6 },
    { name: "GPT-2 medium", N: 24, d: 1024,  h: 16,  total: 355e6 },
    { name: "GPT-2 large",  N: 36, d: 1280,  h: 20,  total: 774e6 },
    { name: "GPT-2 XL",     N: 48, d: 1600,  h: 25,  total: 1.5e9 },
    { name: "GPT-3",        N: 96, d: 12288, h: 96,  total: 175e9 },
    { name: "Llama 3 8B",   N: 32, d: 4096,  h: 32,  total: 8e9 },
    { name: "Llama 3 70B",  N: 80, d: 8192,  h: 64,  total: 70e9 },
    { name: "Llama 3 405B", N: 126,d: 16384, h: 128, total: 405e9 }
  ];

  var Nel = root.querySelector("[data-sc-N]");
  var Del = root.querySelector("[data-sc-D]");
  var Hel = root.querySelector("[data-sc-H]");
  var Nv = root.querySelector("[data-sc-N-val]");
  var Dv = root.querySelector("[data-sc-D-val]");
  var Hv = root.querySelector("[data-sc-H-val]");
  var totalEl = root.querySelector("[data-sc-total]");
  var memEl = root.querySelector("[data-sc-mem]");
  var matchEl = root.querySelector("[data-sc-match]");
  var attnW = root.querySelector("[data-sc-attn-w]");
  var mlpW = root.querySelector("[data-sc-mlp-w]");
  var embW = root.querySelector("[data-sc-emb-w]");
  var attnPct = root.querySelector("[data-sc-attn-pct]");
  var mlpPct = root.querySelector("[data-sc-mlp-pct]");
  var embPct = root.querySelector("[data-sc-emb-pct]");

  function fmt(n){
    if (n >= 1e12) return (n/1e12).toFixed(2) + "T";
    if (n >= 1e9)  return (n/1e9).toFixed(2) + "B";
    if (n >= 1e6)  return (n/1e6).toFixed(1) + "M";
    if (n >= 1e3)  return (n/1e3).toFixed(0) + "K";
    return n.toFixed(0);
  }

  function render(){
    var N = parseInt(Nel.value, 10);
    var d = D_OPTIONS[parseInt(Del.value, 10)];
    var h = parseInt(Hel.value, 10);
    Nv.textContent = N;
    Dv.textContent = d;
    Hv.textContent = h;

    // Standard recipe: attention = 4*d^2 per block (Q,K,V,O); MLP = 8*d^2 per block (4d up, 4d down).
    var attn = N * 4 * d * d;
    var mlp  = N * 8 * d * d;
    var emb  = 2 * V * d; // embed + unembed (untied)
    var total = attn + mlp + emb;

    totalEl.textContent = fmt(total);
    memEl.textContent = fmt(total * 2 / (1024*1024)) + "B"; // approximation
    // Show memory in MB / GB
    var bytes = total * 2;
    var memStr;
    if (bytes >= 1024*1024*1024) memStr = (bytes / (1024*1024*1024)).toFixed(2) + " GB";
    else memStr = (bytes / (1024*1024)).toFixed(1) + " MB";
    memEl.textContent = memStr;

    // closest model: minimize log-distance over (N, d, total)
    var best = null, bestS = Infinity;
    KNOWN.forEach(function(m){
      var s = Math.abs(Math.log(m.N/N)) + Math.abs(Math.log(m.d/d)) + Math.abs(Math.log(m.total/total)) * 0.5;
      if (s < bestS){ bestS = s; best = m; }
    });
    matchEl.textContent = best.name;

    var aP = attn / total * 100;
    var mP = mlp / total * 100;
    var eP = emb / total * 100;
    attnW.style.flexBasis = aP + "%";
    mlpW.style.flexBasis = mP + "%";
    embW.style.flexBasis = eP + "%";
    attnPct.textContent = aP.toFixed(0) + "%";
    mlpPct.textContent = mP.toFixed(0) + "%";
    embPct.textContent = eP.toFixed(0) + "%";
  }

  [Nel, Del, Hel].forEach(function(el){ el.addEventListener("input", render); });
  render();
})();
</script>

<h2 id="whats-ahead">What’s ahead</h2>

<p>Subsequent posts cover each station in mechanistic detail:</p>

<ul>
  <li><strong>Tokens.</strong> BPE, vocabulary quirks, the bugs they cause.</li>
  <li><strong>Residual stream.</strong> The most important MI primitive; logit lens; linear decomposition.</li>
  <li><strong>Attention.</strong> QK/OV decomposition; induction heads; IOI circuit.</li>
  <li><strong>MLPs.</strong> Key-value memory framing; superposition; sparse autoencoders.</li>
  <li><strong>Full forward pass.</strong> End-to-end with real GPT-2 numbers.</li>
</ul>

<p>The next post is on tokenization.</p>

<h2 id="resources">Resources</h2>

<h3 id="foundational-papers">Foundational papers</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener"><div class="research-card__title">Attention Is All You Need</div><div class="research-card__authors">Vaswani et al., 2017 · the original transformer paper</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener"><div class="research-card__title">A Mathematical Framework for Transformer Circuits</div><div class="research-card__authors">Elhage et al., Anthropic 2021 · residual-stream view used throughout this series</div></a></li>
  <li><a class="research-card" href="https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf" target="_blank" rel="noopener"><div class="research-card__title">Language Models are Unsupervised Multitask Learners</div><div class="research-card__authors">Radford et al., OpenAI 2019 · GPT-2 architecture and training</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2005.14165" target="_blank" rel="noopener"><div class="research-card__title">Language Models are Few-Shot Learners</div><div class="research-card__authors">Brown et al., 2020 · GPT-3, scaling laws in practice</div></a></li>
</ul>

<h3 id="tutorials-and-code">Tutorials and code</h3>

<ul class="research-list">
  <li><a class="research-card" href="https://jalammar.github.io/illustrated-transformer/" target="_blank" rel="noopener"><div class="research-card__title">The Illustrated Transformer</div><div class="research-card__authors">Jay Alammar · diagram-led walkthrough of the original architecture</div></a></li>
  <li><a class="research-card" href="https://github.com/karpathy/nanoGPT" target="_blank" rel="noopener"><div class="research-card__title">nanoGPT</div><div class="research-card__authors">Andrej Karpathy · ~300-line PyTorch implementation of GPT-2</div></a></li>
  <li><a class="research-card" href="https://www.youtube.com/watch?v=kCc8FmEb1nY" target="_blank" rel="noopener"><div class="research-card__title">Let's build GPT: from scratch, in code, spelled out</div><div class="research-card__authors">Karpathy · 2-hour lecture; pairs with nanoGPT</div></a></li>
  <li><a class="research-card" href="https://transformerlensorg.github.io/TransformerLens/" target="_blank" rel="noopener"><div class="research-card__title">TransformerLens</div><div class="research-card__authors">Neel Nanda et al. · the standard MI library; load any HF model and inspect activations</div></a></li>
  <li><a class="research-card" href="https://www.neelnanda.io/mechanistic-interpretability/getting-started" target="_blank" rel="noopener"><div class="research-card__title">A Comprehensive Mechanistic Interpretability Explainer</div><div class="research-card__authors">Neel Nanda · glossary and recommended reading order</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[A decoder-only transformer is a stack of N identical blocks operating on a residual stream of token vectors. Six stations, one conveyor belt, one probability distribution per step.]]></summary></entry><entry><title type="html">How Training Works: The Ball Rolling Downhill</title><link href="https://bhavith-chandra.github.io/posts/how-training-works/" rel="alternate" type="text/html" title="How Training Works: The Ball Rolling Downhill" /><published>2026-03-01T00:00:00-08:00</published><updated>2026-03-01T00:00:00-08:00</updated><id>https://bhavith-chandra.github.io/posts/how-training-works</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/how-training-works/"><![CDATA[<p>Every weight in a neural network started life as a random number. Really. Random.</p>

<p>All the grammar, all the facts, all the reasoning patterns. Learned by being wrong, measuring how wrong, and nudging. Billions of times. For months.</p>

<p>This procedure has a name: <strong>gradient descent</strong>. And honestly, it’s the closest thing AI has to a creation story.</p>

<hr />

<h2 id="the-problem-how-do-you-teach-a-machine">The problem, how do you teach a machine?</h2>

<p>You can’t write rules for recognising cats. Smart people spent decades on this, did genuinely incredible work along the way, and it turned out the hand-written-rules approach just hits a ceiling. That’s how we ended up over here.</p>

<p>You can’t manually set 70 billion weights either. Even if you magically knew what they should be (you don’t, nobody does), it’d take longer than the age of the universe.</p>

<p>So what do you do? Show the model examples. Tell it when it’s wrong. Let it adjust itself. Repeat.</p>

<p>Do that enough times and, somehow, it works. That’s machine learning in one sentence. “Gradient descent” is just the specific algorithm that does the adjusting.</p>

<h2 id="the-loss-function-measuring-wrongness">The loss function, measuring wrongness</h2>

<p>Before the model can learn from being wrong, you need a way to measure <em>how wrong</em> it is. That’s the <strong>loss function</strong>.</p>

<p>Classification problem: you ask the model <em>is this email spam?</em>. It outputs a probability: <code class="language-plaintext highlighter-rouge">0.73</code> (73% chance spam). The correct answer is <code class="language-plaintext highlighter-rouge">1.0</code>. The loss is a number measuring the gap. How far off were you?</p>

<p>Common loss functions:</p>

<ul>
  <li><strong>Cross-entropy</strong> (main one for classification). Measures the difference between the model’s probability distribution and the true distribution. 99% confident in the right answer, loss ≈ 0. 1% confident in the right answer, loss is large.</li>
  <li><strong>Mean squared error</strong> (regression). The average squared distance between predictions and correct answers.</li>
</ul>

<p>The loss is a single number. Low = good predictions. High = bad predictions.</p>

<p>Goal of training: <strong>minimise the loss</strong>, averaged over millions of examples.</p>

<h2 id="the-loss-landscape-a-mountain-range-of-wrongness">The loss landscape, a mountain range of wrongness</h2>

<p>Way to picture it.</p>

<p>Imagine a two-dimensional landscape. Hills and valleys. Every point corresponds to a specific setting of all the weights. The height at any point is the loss. How wrong those weights make the model.</p>

<p>The model starts at a random point in this landscape. A random mountain, somewhere. Our job: walk it to the nearest valley. The point of lowest loss.</p>

<p>Here’s the thing though: this landscape isn’t 2D. It has as many dimensions as there are weights. A small model: millions of dimensions. GPT-4: hundreds of billions. Try picturing that for a second. Actually don’t, you’ll hurt yourself.</p>

<p>Nobody can visualise this landscape. But we can navigate it, one step at a time, using the <strong>gradient</strong>.</p>

<div class="idemo" id="demo-loss">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · The loss landscape</span></div>
    <div class="idemo__body">

      <p class="loss-lead">Every point on this map is a setting of the model's weights. The colour is the loss, how wrong the model is. Gradient descent is a ball rolling downhill. Try it with different learning rates.</p>

      <div class="loss-stage">
        <canvas class="loss-canvas" width="400" height="400" aria-label="Loss landscape with a ball"></canvas>
        <canvas class="loss-chart" width="400" height="120" aria-label="Training loss over steps"></canvas>
      </div>

      <div class="loss-ctrls">
        <div class="loss-ctrls__row">
          <label class="loss-ctrl">
            <span class="loss-ctrl__name">Learning rate</span>
            <input type="range" min="0.01" max="1.5" step="0.01" value="0.12" data-loss-lr="" />
            <span class="loss-ctrl__val" data-loss-lrv="">0.12</span>
          </label>
          <label class="loss-ctrl">
            <span class="loss-ctrl__name">Speed</span>
            <input type="range" min="1" max="10" step="1" value="3" data-loss-speed="" />
          </label>
        </div>
        <div class="loss-ctrls__row">
          <button class="btn-primary" data-loss-action="play">▶ Run</button>
          <button class="btn-secondary" data-loss-action="step">Step</button>
          <button class="btn-secondary" data-loss-action="reset">Reset</button>
          <span class="loss-status" data-loss-status="">paused</span>
        </div>
      </div>

      <div class="loss-toggles" data-loss-toggles="">
        <label class="loss-toggle"><input type="checkbox" data-loss-show="arrows" checked="" /> gradient arrows</label>
        <label class="loss-toggle"><input type="checkbox" data-loss-show="contours" checked="" /> contour lines</label>
        <label class="loss-toggle"><input type="checkbox" data-loss-show="trail" checked="" /> path trail</label>
      </div>

      <div class="loss-legend">
        <div><span class="loss-swatch" style="background:#2a1954"></span> low loss · good</div>
        <div><span class="loss-swatch" style="background:#2a6fb8"></span> medium</div>
        <div><span class="loss-swatch" style="background:#c98c3a"></span> high</div>
        <div><span class="loss-swatch" style="background:#c04550"></span> very high · bad</div>
        <div><span class="loss-swatch" style="background:#00c89b; border-color:#00c89b"></span> ball · current weights</div>
      </div>

      <p class="loss-try">Try: <b>LR ≈ 0.08</b> for smooth descent · <b>LR &gt; 1.2</b> for chaos · reset a few times to find the local minimum trap on the upper left.</p>
    </div>
    <details>
      <summary>How this demo works</summary>
      <p>The surface is a sum of Gaussian bumps and wells, a crude 2D stand-in for the millions-of-dimensions loss surface of a real model. At each step we estimate the gradient with a finite-difference (<code>[f(x+ε) − f(x)] / ε</code>), then apply <code>x ← x − lr · ∇f</code>. That's the literal equation of gradient descent. The chart below tracks loss vs step count, the curve you actually see in training runs.</p>
    </details>
  </div>
</div>

<style>
  #demo-loss .loss-lead { margin: 0 0 1rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-loss .loss-stage { display: grid; grid-template-columns: 400px 1fr; gap: 0.8rem; align-items: start; }
  @media (max-width: 780px) { #demo-loss .loss-stage { grid-template-columns: 1fr; } }
  #demo-loss .loss-canvas { width: 100%; max-width: 400px; height: auto; display: block; background: #fafafc; border: 1px solid var(--nn-line); border-radius: 6px; }
  #demo-loss .loss-chart { width: 100%; max-width: 400px; height: auto; display: block; background: #fff; border: 1px solid var(--nn-line); border-radius: 6px; }

  #demo-loss .loss-ctrls { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.9rem; }
  #demo-loss .loss-ctrls__row { display: flex; gap: 0.8rem; flex-wrap: wrap; align-items: center; }
  #demo-loss .loss-ctrl { display: flex; align-items: center; gap: 0.6rem; flex: 1 1 200px; min-width: 200px; padding: 0.4rem 0.75rem; background: #fff; border: 1px solid var(--nn-line); border-radius: 4px; }
  #demo-loss .loss-ctrl__name { font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.08em; color: var(--nn-muted); }
  #demo-loss .loss-ctrl input[type=range] { flex: 1; -webkit-appearance: none; height: 4px; background: var(--nn-line); border-radius: 2px; outline: none; }
  #demo-loss .loss-ctrl input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
    background: var(--nn-accent); border: 2px solid #fff; box-shadow: 0 0 0 1px var(--nn-accent); cursor: pointer;
  }
  #demo-loss .loss-ctrl__val { font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-accent-dark); min-width: 40px; text-align: right; }
  #demo-loss .loss-status { margin-left: auto; font-family: var(--nn-mono); font-size: 0.75rem; letter-spacing: 0.08em; color: var(--nn-muted); text-transform: uppercase; }

  #demo-loss .loss-legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.9rem; font-family: var(--nn-mono); font-size: 0.76rem; color: var(--nn-muted); }
  #demo-loss .loss-swatch { display: inline-block; width: 14px; height: 14px; border-radius: 2px; vertical-align: middle; margin-right: 0.4rem; border: 1px solid rgba(0, 0, 0, 0.1); }

  #demo-loss .loss-try { margin-top: 0.8rem !important; font-size: 0.88rem !important; color: var(--nn-muted) !important; line-height: 1.55 !important; }
  #demo-loss .loss-try b { color: var(--nn-accent-dark); }

  #demo-loss .loss-toggles { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 0.7rem; font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted); }
  #demo-loss .loss-toggle { display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer; user-select: none; }
  #demo-loss .loss-toggle input { accent-color: var(--nn-accent); width: 14px; height: 14px; cursor: pointer; }
</style>

<script>
(function() {
  const root = document.getElementById("demo-loss");
  if (!root) return;

  const canvas = root.querySelector(".loss-canvas");
  const chart = root.querySelector(".loss-chart");
  const ctx = canvas.getContext("2d");
  const cctx = chart.getContext("2d");

  const W = 400, H = 400;
  const CW = 400, CH = 120;
  const XMIN = -3, XMAX = 3, YMIN = -3, YMAX = 3;

  // Loss landscape, sum of Gaussians (negative = valley). Designed with
  // - shallow valley around (-1.4, -1.4)
  // - deep valley around (1.2, 1.4) ← the "global" minimum
  function loss(x, y) {
    const wells = [
      { cx: -1.4, cy: -1.4, amp: 2.4, s: 0.9 },
      { cx: 1.2,  cy: 1.4,  amp: 4.2, s: 0.7 },
      { cx: 0.1,  cy: -1.2, amp: 1.4, s: 1.1 },
    ];
    const hills = [
      { cx: -0.3, cy: 1.1, amp: 1.8, s: 0.9 },
      { cx: 2.2,  cy: -1.8, amp: 1.3, s: 1.1 },
    ];
    let z = 4; // baseline
    wells.forEach(w => { z -= w.amp * Math.exp(-((x - w.cx) ** 2 + (y - w.cy) ** 2) / w.s); });
    hills.forEach(h => { z += h.amp * Math.exp(-((x - h.cx) ** 2 + (y - h.cy) ** 2) / h.s); });
    return z;
  }

  function gradient(x, y) {
    const eps = 0.001;
    return [
      (loss(x + eps, y) - loss(x - eps, y)) / (2 * eps),
      (loss(x, y + eps) - loss(x, y - eps)) / (2 * eps),
    ];
  }

  // Precompute loss field + find min/max for colour scaling
  let lossMin = Infinity, lossMax = -Infinity;
  const field = new Float32Array(W * H);
  for (let py = 0; py < H; py++) {
    const y = YMIN + (YMAX - YMIN) * (py / H);
    for (let px = 0; px < W; px++) {
      const x = XMIN + (XMAX - XMIN) * (px / W);
      const v = loss(x, y);
      field[py * W + px] = v;
      if (v < lossMin) lossMin = v;
      if (v > lossMax) lossMax = v;
    }
  }

  // Colour map: dark-purple (low) → blue → amber → red
  function colour(v) {
    const t = Math.max(0, Math.min(1, (v - lossMin) / (lossMax - lossMin)));
    // interpolate along 4 stops
    const stops = [
      [0.00, [42, 25, 84]],    // deep purple
      [0.35, [42, 111, 184]],  // blue
      [0.70, [201, 140, 58]],  // amber
      [1.00, [192, 69, 80]],   // red
    ];
    for (let i = 0; i < stops.length - 1; i++) {
      if (t >= stops[i][0] && t <= stops[i + 1][0]) {
        const u = (t - stops[i][0]) / (stops[i + 1][0] - stops[i][0]);
        const a = stops[i][1], b = stops[i + 1][1];
        return [
          Math.round(a[0] + (b[0] - a[0]) * u),
          Math.round(a[1] + (b[1] - a[1]) * u),
          Math.round(a[2] + (b[2] - a[2]) * u),
        ];
      }
    }
    return stops[stops.length - 1][1];
  }

  const bg = ctx.createImageData(W, H);
  for (let i = 0; i < W * H; i++) {
    const [r, g, b] = colour(field[i]);
    const j = i * 4;
    bg.data[j] = r; bg.data[j + 1] = g; bg.data[j + 2] = b; bg.data[j + 3] = 255;
  }

  // Ball state
  const state = {
    x: -2, y: -2,
    lr: 0.12,
    speed: 3,
    playing: false,
    trail: [],
    step: 0,
    lossHistory: [],
    show: { arrows: true, contours: true, trail: true },
  };

  // Precompute gradient field on a sparse grid for arrow rendering
  const GRID = 20;
  const gradField = [];
  for (let gy = 0; gy < GRID; gy++) {
    for (let gx = 0; gx < GRID; gx++) {
      const x = XMIN + (XMAX - XMIN) * ((gx + 0.5) / GRID);
      const y = YMIN + (YMAX - YMIN) * ((gy + 0.5) / GRID);
      const g = gradient(x, y);
      gradField.push({ x, y, gx: g[0], gy: g[1] });
    }
  }

  function toPx(x, y) {
    return [
      ((x - XMIN) / (XMAX - XMIN)) * W,
      ((y - YMIN) / (YMAX - YMIN)) * H
    ];
  }

  function drawArrow(x1, y1, x2, y2, color, width) {
    ctx.strokeStyle = color; ctx.fillStyle = color;
    ctx.lineWidth = width || 1;
    ctx.lineCap = "round";
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
    // Arrowhead
    const ang = Math.atan2(y2 - y1, x2 - x1);
    const head = Math.max(3.5, width * 2.2);
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - head * Math.cos(ang - 0.45), y2 - head * Math.sin(ang - 0.45));
    ctx.lineTo(x2 - head * Math.cos(ang + 0.45), y2 - head * Math.sin(ang + 0.45));
    ctx.closePath();
    ctx.fill();
  }

  function drawLandscape() {
    ctx.putImageData(bg, 0, 0);

    // Contour lines
    if (state.show.contours) {
      ctx.fillStyle = "rgba(255, 255, 255, 0.22)";
      const thresholds = [];
      for (let v = Math.ceil(lossMin * 2) / 2; v < lossMax; v += 0.5) thresholds.push(v);
      thresholds.forEach(th => {
        ctx.beginPath();
        for (let py = 0; py < H - 1; py += 2) {
          for (let px = 0; px < W - 1; px += 2) {
            const a = field[py * W + px];
            const b = field[py * W + (px + 1)];
            if ((a >= th && b < th) || (a < th && b >= th)) {
              ctx.rect(px, py, 1, 1);
            }
          }
        }
        ctx.fill();
      });
    }

    // Gradient quiver, show the negative-gradient direction (downhill)
    if (state.show.arrows) {
      const cellW = W / GRID, cellH = H / GRID;
      gradField.forEach(p => {
        // Negative gradient = downhill
        const dx = -p.gx, dy = -p.gy;
        const mag = Math.hypot(dx, dy);
        if (mag < 0.01) return;
        // Scale arrow, longer where gradient is stronger, capped at cell size
        const scale = Math.min(cellW * 0.48, mag * 18);
        const ux = dx / mag, uy = dy / mag;
        const [px, py] = toPx(p.x, p.y);
        const ex = px + ux * scale;
        const ey = py + uy * scale;
        const alpha = 0.25 + Math.min(1, mag / 2) * 0.55;
        drawArrow(px, py, ex, ey, "rgba(255, 255, 255, " + alpha.toFixed(2) + ")", 1);
      });
    }

    // Trail
    if (state.show.trail && state.trail.length > 1) {
      ctx.strokeStyle = "rgba(0, 229, 200, 0.85)";
      ctx.lineWidth = 2.2;
      ctx.lineCap = "round";
      ctx.beginPath();
      state.trail.forEach(([x, y], i) => {
        const [px, py] = toPx(x, y);
        if (i === 0) ctx.moveTo(px, py);
        else ctx.lineTo(px, py);
      });
      ctx.stroke();
      // Fading dots along the trail
      state.trail.forEach(([x, y], i) => {
        const [px, py] = toPx(x, y);
        const t = i / state.trail.length;
        ctx.fillStyle = "rgba(0, 229, 200, " + (0.15 + t * 0.35).toFixed(2) + ")";
        ctx.beginPath(); ctx.arc(px, py, 1.4, 0, Math.PI * 2); ctx.fill();
      });
    }

    // Live gradient vector at ball position, the direction the ball is about to move
    const [bx, by] = toPx(state.x, state.y);
    const g = gradient(state.x, state.y);
    const gmag = Math.hypot(g[0], g[1]);
    if (gmag > 0.01) {
      const ux = -g[0] / gmag, uy = -g[1] / gmag;
      const len = Math.min(55, 14 + gmag * 16);
      drawArrow(bx, by, bx + ux * len, by + uy * len, "#ffd84a", 2.5);
    }

    // Ball, pulse outline
    ctx.beginPath(); ctx.arc(bx, by, 14, 0, Math.PI * 2);
    ctx.fillStyle = "rgba(0, 200, 155, 0.18)"; ctx.fill();
    ctx.beginPath(); ctx.arc(bx, by, 9, 0, Math.PI * 2);
    ctx.fillStyle = "#00c89b"; ctx.fill();
    ctx.strokeStyle = "#fff"; ctx.lineWidth = 2.2; ctx.stroke();
  }

  function drawChart() {
    cctx.clearRect(0, 0, CW, CH);
    cctx.fillStyle = "#fafafc";
    cctx.fillRect(0, 0, CW, CH);
    // Axes
    cctx.strokeStyle = "#d7d7dc"; cctx.lineWidth = 1;
    cctx.beginPath(); cctx.moveTo(36, 10); cctx.lineTo(36, CH - 24); cctx.lineTo(CW - 8, CH - 24); cctx.stroke();
    // Labels
    cctx.fillStyle = "#6b6b70"; cctx.font = "10px 'SF Mono', monospace";
    cctx.fillText("loss", 4, 14);
    cctx.fillText("step " + state.step, CW - 60, CH - 8);
    if (state.lossHistory.length === 0) return;
    const minL = Math.min(...state.lossHistory);
    const maxL = Math.max(...state.lossHistory);
    const pad = (maxL - minL) * 0.1 || 1;
    const lo = minL - pad, hi = maxL + pad;
    const xPad = 36, xRight = CW - 8, yTop = 10, yBottom = CH - 24;
    cctx.fillText(hi.toFixed(1), 4, yTop + 4);
    cctx.fillText(lo.toFixed(1), 4, yBottom);
    // Line
    cctx.strokeStyle = "#2a6fb8"; cctx.lineWidth = 1.8;
    cctx.beginPath();
    state.lossHistory.forEach((v, i) => {
      const px = xPad + (xRight - xPad) * (i / Math.max(1, state.lossHistory.length - 1));
      const py = yBottom - (yBottom - yTop) * ((v - lo) / (hi - lo));
      if (i === 0) cctx.moveTo(px, py); else cctx.lineTo(px, py);
    });
    cctx.stroke();
    // Current point
    const last = state.lossHistory.length - 1;
    const px = xPad + (xRight - xPad) * (last / Math.max(1, state.lossHistory.length - 1));
    const py = yBottom - (yBottom - yTop) * ((state.lossHistory[last] - lo) / (hi - lo));
    cctx.fillStyle = "#00c89b"; cctx.beginPath(); cctx.arc(px, py, 3, 0, Math.PI * 2); cctx.fill();
  }

  const statusEl = root.querySelector("[data-loss-status]");
  function setStatus(s) { statusEl.textContent = s; }

  function step() {
    const [gx, gy] = gradient(state.x, state.y);
    let nx = state.x - state.lr * gx;
    let ny = state.y - state.lr * gy;
    // clamp
    nx = Math.max(XMIN + 0.05, Math.min(XMAX - 0.05, nx));
    ny = Math.max(YMIN + 0.05, Math.min(YMAX - 0.05, ny));
    state.trail.push([state.x, state.y]);
    if (state.trail.length > 200) state.trail.shift();
    state.x = nx; state.y = ny; state.step++;
    state.lossHistory.push(loss(state.x, state.y));
    if (state.lossHistory.length > 200) state.lossHistory.shift();
  }

  function frame() {
    if (!state.playing) return;
    for (let i = 0; i < state.speed; i++) step();
    drawLandscape(); drawChart();
    // Detect convergence
    if (state.lossHistory.length > 30) {
      const recent = state.lossHistory.slice(-15);
      const delta = Math.max(...recent) - Math.min(...recent);
      if (delta < 0.005) {
        const l = loss(state.x, state.y);
        setStatus("converged · loss=" + l.toFixed(3));
        state.playing = false;
        return;
      }
    }
    if (state.step > 500) { state.playing = false; setStatus("stopped · 500 steps"); return; }
    setStatus("running · step " + state.step);
    requestAnimationFrame(frame);
  }

  function reset() {
    const starts = [[-2.2, -2.1], [2.0, -2.4], [-2.4, 2.0], [0.2, 2.4], [-1.8, 0.5]];
    const [sx, sy] = starts[Math.floor(Math.random() * starts.length)];
    state.x = sx; state.y = sy;
    state.trail = []; state.step = 0; state.lossHistory = [loss(sx, sy)];
    state.playing = false;
    drawLandscape(); drawChart();
    setStatus("ready · click ▶ Run");
  }

  root.querySelectorAll("[data-loss-lr]").forEach(el => el.addEventListener("input", e => {
    state.lr = +e.target.value;
    root.querySelector("[data-loss-lrv]").textContent = state.lr.toFixed(2);
  }));
  root.querySelectorAll("[data-loss-speed]").forEach(el => el.addEventListener("input", e => {
    state.speed = +e.target.value;
  }));
  root.querySelector("[data-loss-action='play']").addEventListener("click", () => {
    if (state.playing) { state.playing = false; setStatus("paused · step " + state.step); return; }
    state.playing = true; setStatus("running"); requestAnimationFrame(frame);
  });
  root.querySelector("[data-loss-action='step']").addEventListener("click", () => {
    step(); drawLandscape(); drawChart(); setStatus("stepped · " + state.step);
  });
  root.querySelector("[data-loss-action='reset']").addEventListener("click", reset);

  // Toggles
  root.querySelectorAll("[data-loss-show]").forEach(cb => cb.addEventListener("change", e => {
    state.show[e.target.dataset.lossShow] = e.target.checked;
    drawLandscape();
  }));

  reset();
})();
</script>

<p>The demo above is only 2D, but the algorithm is exactly what runs inside every deep-learning training loop. Run it with learning rate <code class="language-plaintext highlighter-rouge">0.1</code>. Now <code class="language-plaintext highlighter-rouge">1.3</code>. Now <code class="language-plaintext highlighter-rouge">0.01</code>. Notice how LR changes everything. And how the ball sometimes settles into the <em>shallow</em> valley instead of finding the deeper one.</p>

<h2 id="the-gradient-which-way-is-downhill">The gradient, which way is downhill?</h2>

<p>The gradient is a vector that points in the direction of steepest <em>increase</em> in loss. It tells you: <em>“if you move this way, you’ll get worse.”</em></p>

<p>So we go the opposite direction.</p>

<p>The gradient with respect to a single weight tells you: <em>“if I increase this weight slightly, does the loss go up or down, and by how much?”</em></p>

<ul>
  <li>Increasing the weight makes loss go up (positive gradient): <strong>decrease</strong> the weight.</li>
  <li>Increasing the weight makes loss go down (negative gradient): <strong>increase</strong> the weight.</li>
  <li>Magnitude tells you how much to change it.</li>
</ul>

<p>Do this for every weight simultaneously. Take a step in the direction of decreasing loss. That’s gradient descent.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>new_weight = old_weight − (learning_rate × gradient)
</code></pre></div></div>

<h2 id="learning-rate-the-size-of-each-step">Learning rate, the size of each step</h2>

<p>The learning rate is one of the most important numbers in training. Controls how big each step is.</p>

<ul>
  <li><strong>Too large.</strong> You overshoot. Jump past the valley, up the other side, then back again. The model oscillates, never settles.</li>
  <li><strong>Too small.</strong> Every step is tiny. You’ll get there eventually, but it’ll take forever. You might also get stuck in a small local minimum.</li>
  <li><strong>Just right.</strong> Fast enough to learn, small enough to settle into a good solution.</li>
</ul>

<p>Finding the right LR is part science, part art. Modern training uses <em>adaptive</em> learning rates. Algorithms like <strong>Adam</strong> adjust the step size for each weight individually based on how the gradient has been behaving.</p>

<div class="demo demo-lr" id="demo-lr">
  <div class="lr__head">
    <div class="lr__title">Learning rate explorer</div>
    <div class="lr__sub">One slider. Four regimes: too small (creeps), good (smooth), too big (oscillates), way too big (diverges).</div>
  </div>
  <div class="lr__controls">
    <label>Learning rate <input type="range" id="lr-input" min="0.005" max="2.5" step="0.005" value="0.4" /><span data-lr-out="">0.40</span></label>
    <div class="lr__presets">
      <button data-preset="0.02">0.02 (tiny)</button>
      <button data-preset="0.4">0.4 (good)</button>
      <button data-preset="1.4">1.4 (oscillate)</button>
      <button data-preset="2.2">2.2 (diverge)</button>
    </div>
    <button data-act="run" class="lr__go">▶ Run 80 steps</button>
  </div>
  <div class="lr__row">
    <div class="lr__panel">
      <div class="lr__plabel">Loss surface (1D)</div>
      <canvas id="lr-curve" width="380" height="220"></canvas>
    </div>
    <div class="lr__panel">
      <div class="lr__plabel">Loss vs step</div>
      <canvas id="lr-loss" width="380" height="220"></canvas>
    </div>
  </div>
  <div class="lr__verdict" id="lr-verdict">Press <b>▶ Run</b>.</div>
</div>
<style>
  .demo-lr{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-lr .lr__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-lr .lr__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-lr .lr__controls{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 10px;align-items:center}
  .demo-lr .lr__controls label{font-size:12px;color:#7c4d0a;display:flex;align-items:center;gap:8px;font-weight:600}
  .demo-lr .lr__controls input[type=range]{accent-color:#b77214;width:200px}
  .demo-lr .lr__controls span[data-lr-out]{font-family:ui-monospace,Menlo,monospace;width:40px;text-align:right;color:#5a3d12}
  .demo-lr .lr__presets{display:flex;gap:4px;flex-wrap:wrap}
  .demo-lr .lr__presets button,.demo-lr .lr__go{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:5px 10px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-lr .lr__presets button:hover,.demo-lr .lr__go:hover{background:#fbbf24;color:#3a2106}
  .demo-lr .lr__row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .demo-lr .lr__panel{background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;padding:10px}
  .demo-lr .lr__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:8px;font-weight:700}
  .demo-lr canvas{width:100%;height:auto;display:block}
  .demo-lr .lr__verdict{margin-top:10px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px;line-height:1.6}
  @media (max-width:560px){.demo-lr .lr__row{grid-template-columns:1fr}}
</style>

<script>
(function(){
  const root=document.getElementById('demo-lr'); if(!root) return;
  const lrIn=root.querySelector('#lr-input'), lrOut=root.querySelector('[data-lr-out]');
  const cv=root.querySelector('#lr-curve'),cx=cv.getContext('2d');
  const lcv=root.querySelector('#lr-loss'),lx=lcv.getContext('2d');
  const verdict=root.querySelector('#lr-verdict');
  function f(x){return 0.5*x*x;} function g(x){return x;}
  let path=[], losses=[], raf=null, anim=0;
  function run(){const lr=parseFloat(lrIn.value); let x=2.5; path=[x]; losses=[f(x)];
    for(let i=0;i<80;i++){x=x-lr*g(x); if(!isFinite(x)||Math.abs(x)>50){path.push(x); losses.push(f(x)); break;} path.push(x); losses.push(f(x));}
    cancelAnimationFrame(raf); anim=0; const start=performance.now(); const dur=1600;
    function tick(now){const t=Math.min(1,(now-start)/dur); anim=Math.floor(t*path.length); draw(); if(t<1) raf=requestAnimationFrame(tick); else {anim=path.length; draw(); diagnose(lr);}}
    raf=requestAnimationFrame(tick);
  }
  function draw(){
    const W=cv.width,H=cv.height; cx.clearRect(0,0,W,H); const pad={l:32,r:14,t:14,b:24}; const gw=W-pad.l-pad.r,gh=H-pad.t-pad.b;
    const xMin=-3,xMax=3,yMin=0,yMax=5;
    const X=v=>pad.l+(v-xMin)/(xMax-xMin)*gw; const Y=v=>pad.t+(yMax-v)/(yMax-yMin)*gh;
    cx.strokeStyle='#f0e3cc'; for(let v=Math.ceil(xMin);v<=xMax;v++){cx.beginPath();cx.moveTo(X(v),pad.t);cx.lineTo(X(v),H-pad.b);cx.stroke();}
    for(let v=0;v<=5;v++){cx.beginPath();cx.moveTo(pad.l,Y(v));cx.lineTo(W-pad.r,Y(v));cx.stroke();}
    cx.strokeStyle='#fbbf24'; cx.lineWidth=2.4; cx.beginPath();
    for(let px=0;px<=gw;px++){const x=xMin+(px/gw)*(xMax-xMin); const y=f(x); if(px===0)cx.moveTo(X(x),Y(y)); else cx.lineTo(X(x),Y(y));} cx.stroke();
    cx.fillStyle='#7c4d0a'; cx.font='10px ui-monospace,Menlo,monospace'; for(let v=Math.ceil(xMin);v<=xMax;v++) cx.fillText(v,X(v)-3,H-pad.b+12);
    cx.strokeStyle='#b25c2c'; cx.lineWidth=1.4; cx.setLineDash([3,3]);
    for(let i=0;i<Math.min(anim,path.length-1);i++){const x1=path[i],x2=path[i+1]; if(!isFinite(x1)||!isFinite(x2)||Math.abs(x1)>3||Math.abs(x2)>3) continue;
      cx.beginPath(); cx.moveTo(X(x1),Y(f(x1))); cx.lineTo(X(x2),Y(f(x2))); cx.stroke();}
    cx.setLineDash([]);
    for(let i=0;i<Math.min(anim,path.length);i++){const xv=path[i]; if(!isFinite(xv)||Math.abs(xv)>3) continue; cx.fillStyle=i===0?'#3a2106':'#b25c2c'; cx.beginPath(); cx.arc(X(xv),Y(f(xv)),i===0?5:3.4,0,Math.PI*2); cx.fill();}
    const W2=lcv.width,H2=lcv.height; lx.clearRect(0,0,W2,H2); const p2={l:36,r:14,t:14,b:24}; const gw2=W2-p2.l-p2.r,gh2=H2-p2.t-p2.b;
    const ls=losses.slice(0,Math.max(1,anim)).filter(v=>isFinite(v)&&v<1e6);
    if(!ls.length){lx.fillStyle='#a08562'; lx.font='12px ui-monospace,Menlo,monospace'; lx.fillText('diverged',p2.l+30,H2/2); return;}
    const mn=Math.min(...ls), mx=Math.max(...ls);
    lx.strokeStyle='#fbbf24'; lx.lineWidth=2; lx.beginPath();
    ls.forEach((v,i)=>{const X2=p2.l+(i/(losses.length-1))*gw2; const Y2=p2.t+(1-(v-mn)/(mx-mn+1e-6))*gh2; if(i===0)lx.moveTo(X2,Y2); else lx.lineTo(X2,Y2);}); lx.stroke();
    lx.fillStyle='#7c4d0a'; lx.font='10px ui-monospace,Menlo,monospace'; lx.fillText(mx.toFixed(2),4,p2.t+8); lx.fillText(mn.toFixed(2),4,H2-p2.b);
    lx.fillText('step',W2/2-12,H2-6);
  }
  function diagnose(lr){
    const last=losses[losses.length-1]; const final=losses.filter(v=>isFinite(v)).pop();
    let v;
    if(!isFinite(last)||last>1000) v='<b>DIVERGED</b> · loss → ∞. The step size is bigger than the curvature can absorb; each update overshoots farther than the last.';
    else if(lr>1) v='<b>OSCILLATING</b> · loss bounces. Each step crosses the minimum and lands on the opposite slope. Decrease LR.';
    else if(lr>0.05 && lr<=1) v=`<b>HEALTHY</b> · loss decays smoothly. Final loss = ${final.toFixed(4)} after 80 steps.`;
    else v=`<b>TOO SLOW</b> · still descending after 80 steps (loss = ${final.toFixed(4)}). Increase LR for faster convergence.`;
    verdict.innerHTML=`LR = <b>${lr.toFixed(3)}</b> · ${v}`;
  }
  lrIn.addEventListener('input',()=>{lrOut.textContent=parseFloat(lrIn.value).toFixed(2);});
  root.querySelectorAll('[data-preset]').forEach(b=>b.addEventListener('click',()=>{lrIn.value=b.dataset.preset; lrOut.textContent=parseFloat(b.dataset.preset).toFixed(2); run();}));
  root.querySelector('[data-act="run"]').addEventListener('click',run);
  lrOut.textContent=parseFloat(lrIn.value).toFixed(2);
  draw();
})();
</script>

<p>Drag the slider all the way down — the ball creeps and gives up before reaching zero. Drag it past 1.0 — the ball bounces off both walls of the bowl. Drag it past 2.0 — the ball escapes the canvas entirely. There’s no good rule for “the right” learning rate. Every modern optimizer is essentially a different way of guessing it from local geometry.</p>

<h2 id="backpropagation-computing-the-gradient-efficiently">Backpropagation, computing the gradient efficiently</h2>

<p>Problem: a model has billions of weights. Computing the gradient for all of them by testing each one (<em>“what if I nudged this weight up a tiny bit?”</em>) would take forever.</p>

<p><strong>Backpropagation</strong> (backprop) solves this with the chain rule of calculus. An algorithm that computes the gradient for every weight in a single backward pass through the network. In essentially the same time it takes to run a forward pass.</p>

<p>Key idea: the gradient flows backwards. Start from the loss (at the output). Compute how much each weight contributed to it, layer by layer, going backwards toward the input.</p>

<p>You don’t need to understand backprop’s math to understand MI. But know this: it’s fast, it’s exact, and every major training framework (PyTorch, JAX, TensorFlow) does it automatically. You define your model, you compute the loss, you call <code class="language-plaintext highlighter-rouge">.backward()</code>. Gradients appear, pre-computed, on every weight.</p>

<h2 id="batches-and-epochs">Batches and epochs</h2>

<p>You don’t compute the gradient on one example at a time. You compute it on a <strong>batch</strong>. Typically 128, 512, or 2048 examples at once.</p>

<p>Why batches?</p>

<ul>
  <li>Averaging over many examples makes the gradient estimate more accurate (less noise).</li>
  <li>Modern hardware (GPUs) is optimised for processing many things in parallel.</li>
  <li>Bigger batches = faster progress per gradient step.</li>
</ul>

<p>One <strong>epoch</strong> = one full pass through the entire training dataset. Models are typically trained for many epochs. Same data multiple times.</p>

<ul>
  <li><strong>Stochastic gradient descent (SGD).</strong> Batch size = 1. Noisy but fast.</li>
  <li><strong>Mini-batch gradient descent.</strong> Batch size = 32 to 2048. The standard.</li>
  <li><strong>Batch gradient descent.</strong> Use the full dataset. Too slow for large datasets.</li>
</ul>

<p>Modern training uses mini-batches with Adam. An improved version of SGD with adaptive learning rates and momentum.</p>

<div class="demo demo-opt" id="demo-opt">
  <div class="opt__head">
    <div class="opt__title">Optimizer race</div>
    <div class="opt__sub">Three runners on the same loss surface. SGD, Momentum, and Adam start at the same point — watch their personalities.</div>
  </div>
  <div class="opt__controls">
    <span class="opt__plabel">Loss surface</span>
    <button class="opt__btn is-active" data-surf="bowl">Skewed bowl</button>
    <button class="opt__btn" data-surf="saddle">Saddle</button>
    <button class="opt__btn" data-surf="rosen">Rosenbrock</button>
    <button class="opt__btn" data-surf="multi">Multimodal</button>
    <span class="opt__sep"></span>
    <button data-act="play" class="opt__go">▶ Race</button>
    <button data-act="reset" class="opt__go">Reset</button>
  </div>
  <canvas id="opt-canvas" width="720" height="380"></canvas>
  <div class="opt__legend">
    <span><i style="background:#fbbf24"></i>SGD</span>
    <span><i style="background:#0d6b3a"></i>Momentum</span>
    <span><i style="background:#b25c2c"></i>Adam</span>
  </div>
  <div class="opt__readout" id="opt-readout">Press <b>▶ Race</b>. Click anywhere on the surface to set a new starting point.</div>
</div>
<style>
  .demo-opt{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-opt .opt__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-opt .opt__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-opt .opt__controls{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 10px;align-items:center}
  .demo-opt .opt__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;font-weight:700;margin-right:4px}
  .demo-opt .opt__btn,.demo-opt .opt__go{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:5px 10px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-opt .opt__btn.is-active,.demo-opt .opt__go:hover{background:#fbbf24;color:#3a2106;border-color:#b77214}
  .demo-opt .opt__sep{flex:1}
  .demo-opt canvas{width:100%;height:auto;background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;display:block;cursor:crosshair}
  .demo-opt .opt__legend{display:flex;gap:14px;margin:8px 0;font-size:12px;color:#5a3d12}
  .demo-opt .opt__legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px;vertical-align:middle}
  .demo-opt .opt__readout{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px}
</style>

<script>
(function(){
  const root=document.getElementById('demo-opt'); if(!root) return;
  const cvs=root.querySelector('#opt-canvas'), ctx=cvs.getContext('2d');
  const readout=root.querySelector('#opt-readout');
  const surfs={
    bowl:{f:(x,y)=>0.5*x*x+0.05*y*y, gx:(x,y)=>x, gy:(x,y)=>0.1*y, dom:[-3,3,-3,3]},
    saddle:{f:(x,y)=>x*x-y*y, gx:(x,y)=>2*x, gy:(x,y)=>-2*y, dom:[-3,3,-3,3]},
    rosen:{f:(x,y)=>(1-x)**2+5*(y-x*x)**2, gx:(x,y)=>-2*(1-x)-20*x*(y-x*x), gy:(x,y)=>10*(y-x*x), dom:[-2,2,-1,3]},
    multi:{f:(x,y)=>Math.sin(x*1.5)*Math.cos(y*1.5)+0.05*(x*x+y*y), gx:(x,y)=>1.5*Math.cos(x*1.5)*Math.cos(y*1.5)+0.1*x, gy:(x,y)=>-1.5*Math.sin(x*1.5)*Math.sin(y*1.5)+0.1*y, dom:[-3,3,-3,3]}
  };
  let surf='bowl', start={x:-2.4,y:2.2}, paths={sgd:[],mom:[],adam:[]}, raf=null, running=false;
  function S(){return surfs[surf];}
  function toCanvas(x,y){const W=cvs.width,H=cvs.height; const [x0,x1,y0,y1]=S().dom; const px=(x-x0)/(x1-x0)*W; const py=H-(y-y0)/(y1-y0)*H; return [px,py];}
  function fromCanvas(px,py){const W=cvs.width,H=cvs.height; const [x0,x1,y0,y1]=S().dom; const x=x0+(px/W)*(x1-x0); const y=y0+((H-py)/H)*(y1-y0); return [x,y];}
  function drawSurface(){const W=cvs.width,H=cvs.height; const img=ctx.createImageData(W,H); const s=S();
    let mn=Infinity,mx=-Infinity; const samples=[]; for(let py=0;py<H;py+=2) for(let px=0;px<W;px+=2){const [x,y]=fromCanvas(px,py); const v=s.f(x,y); samples.push([px,py,v]); if(v<mn)mn=v; if(v>mx)mx=v;}
    for(const [px,py,v] of samples){const t=(v-mn)/(mx-mn+1e-9); const r=Math.round(255-(255-124)*t); const g=Math.round(246-(246-77)*t); const b=Math.round(224-(224-10)*t);
      for(let dy=0;dy<2;dy++) for(let dx=0;dx<2;dx++){const idx=((py+dy)*W+(px+dx))*4; img.data[idx]=r; img.data[idx+1]=g; img.data[idx+2]=b; img.data[idx+3]=255;}}
    ctx.putImageData(img,0,0);
    ctx.strokeStyle='rgba(124,77,10,0.3)'; ctx.lineWidth=1;
    for(let i=1;i<10;i++){const lv=mn+(mx-mn)*(i/10); ctx.beginPath(); let started=false;
      for(let py=0;py<H;py+=4) for(let px=0;px<W;px+=4){const [x,y]=fromCanvas(px,py); if(Math.abs(s.f(x,y)-lv)<(mx-mn)*0.012){if(!started){ctx.moveTo(px,py); started=true;} else ctx.lineTo(px,py);}} ctx.stroke();}
  }
  function drawPaths(){const cols={sgd:'#fbbf24',mom:'#0d6b3a',adam:'#b25c2c'};
    Object.keys(paths).forEach(k=>{const p=paths[k]; if(!p.length) return; ctx.strokeStyle=cols[k]; ctx.lineWidth=2.4; ctx.beginPath();
      p.forEach((pt,i)=>{const [px,py]=toCanvas(pt[0],pt[1]); if(i===0)ctx.moveTo(px,py); else ctx.lineTo(px,py);}); ctx.stroke();
      const last=p[p.length-1]; const [lx,ly]=toCanvas(last[0],last[1]); ctx.fillStyle=cols[k]; ctx.beginPath(); ctx.arc(lx,ly,5,0,Math.PI*2); ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=1.5; ctx.stroke();});
    const [sx,sy]=toCanvas(start.x,start.y); ctx.fillStyle='#fff'; ctx.strokeStyle='#3a2106'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(sx,sy,5,0,Math.PI*2); ctx.fill(); ctx.stroke();
  }
  function init(){paths={sgd:[[start.x,start.y]],mom:[[start.x,start.y]],adam:[[start.x,start.y]]};
    state={sgd:{lr:0.04},mom:{lr:0.04,vx:0,vy:0,b:0.9},adam:{lr:0.06,mx:0,my:0,vx2:0,vy2:0,b1:0.9,b2:0.999,t:0}};}
  let state=null;
  function stepOne(){const s=S(); let any=false;
    {const [x,y]=paths.sgd[paths.sgd.length-1]; const gx=s.gx(x,y),gy=s.gy(x,y); const nx=x-state.sgd.lr*gx, ny=y-state.sgd.lr*gy; if(Math.hypot(nx-x,ny-y)>1e-4) any=true; paths.sgd.push([nx,ny]);}
    {const [x,y]=paths.mom[paths.mom.length-1]; const gx=s.gx(x,y),gy=s.gy(x,y); state.mom.vx=state.mom.b*state.mom.vx+gx; state.mom.vy=state.mom.b*state.mom.vy+gy; const nx=x-state.mom.lr*state.mom.vx, ny=y-state.mom.lr*state.mom.vy; if(Math.hypot(nx-x,ny-y)>1e-4) any=true; paths.mom.push([nx,ny]);}
    {const [x,y]=paths.adam[paths.adam.length-1]; const gx=s.gx(x,y),gy=s.gy(x,y); state.adam.t++; state.adam.mx=state.adam.b1*state.adam.mx+(1-state.adam.b1)*gx; state.adam.my=state.adam.b1*state.adam.my+(1-state.adam.b1)*gy; state.adam.vx2=state.adam.b2*state.adam.vx2+(1-state.adam.b2)*gx*gx; state.adam.vy2=state.adam.b2*state.adam.vy2+(1-state.adam.b2)*gy*gy;
      const mhx=state.adam.mx/(1-Math.pow(state.adam.b1,state.adam.t)); const mhy=state.adam.my/(1-Math.pow(state.adam.b1,state.adam.t)); const vhx=state.adam.vx2/(1-Math.pow(state.adam.b2,state.adam.t)); const vhy=state.adam.vy2/(1-Math.pow(state.adam.b2,state.adam.t));
      const nx=x-state.adam.lr*mhx/(Math.sqrt(vhx)+1e-8); const ny=y-state.adam.lr*mhy/(Math.sqrt(vhy)+1e-8); if(Math.hypot(nx-x,ny-y)>1e-4) any=true; paths.adam.push([nx,ny]);}
    return any;
  }
  function frame(){if(!running) return; for(let k=0;k<3;k++) stepOne(); render(); if(paths.sgd.length>240){running=false; finish(); return;} raf=requestAnimationFrame(frame);}
  function render(){drawSurface(); drawPaths();}
  function finish(){const s=S(); const fmt=k=>{const last=paths[k][paths[k].length-1]; return s.f(last[0],last[1]).toFixed(3);};
    readout.innerHTML=`Final loss · SGD <b>${fmt('sgd')}</b> · Momentum <b>${fmt('mom')}</b> · Adam <b>${fmt('adam')}</b> · steps <b>${paths.sgd.length-1}</b>`;}
  function reset(){cancelAnimationFrame(raf); running=false; init(); render(); readout.innerHTML='Reset. Press <b>▶ Race</b>.';}
  root.querySelectorAll('[data-surf]').forEach(b=>b.addEventListener('click',()=>{root.querySelectorAll('[data-surf]').forEach(x=>x.classList.remove('is-active')); b.classList.add('is-active'); surf=b.dataset.surf; start={x:S().dom[0]+1,y:S().dom[3]-0.4}; reset();}));
  root.querySelector('[data-act="play"]').addEventListener('click',()=>{cancelAnimationFrame(raf); init(); running=true; raf=requestAnimationFrame(frame); readout.innerHTML='Racing…';});
  root.querySelector('[data-act="reset"]').addEventListener('click',reset);
  cvs.addEventListener('click',e=>{const r=cvs.getBoundingClientRect(); const px=(e.clientX-r.left)/r.width*cvs.width; const py=(e.clientY-r.top)/r.height*cvs.height; const [x,y]=fromCanvas(px,py); start={x,y}; reset();});
  init(); render();
})();
</script>

<p>Three runners, same start. Plain SGD only knows local slope, so on a long thin valley it ricochets between walls. Momentum carries velocity through small bumps and accelerates downhill. Adam adapts each direction’s step size independently and tends to find the basin even on weird surfaces. Click anywhere on the canvas to teleport the start — every optimizer’s character changes with the terrain.</p>

<h2 id="overfitting">Overfitting</h2>

<p>A model can memorise its training data. Loss goes to zero. On new data it’s never seen (test data), the model fails badly.</p>

<p>That’s <strong>overfitting</strong>. The model learned the specific examples, not the underlying patterns.</p>

<p>Detecting it: track loss on <em>training data</em> and on <em>held-out validation data</em> separately. Validation loss starts rising while training loss keeps falling, the model is overfitting. Training usually stops here.</p>

<p>Techniques to prevent it: <strong>dropout</strong> (randomly silence neurons during training), <strong>weight decay</strong> (penalise large weights), <strong>data augmentation</strong> (artificially expand training data).</p>

<aside class="callout callout--warning">
  <div class="callout__label">Memorisation vs learning</div>
  <p>Overfitted models have memorised patterns rather than learned generalisable circuits. The circuits MI studies are ones that <em>generalise</em>. Because those are the algorithms the model actually learned, not the random noise it memorised.</p>
</aside>

<h2 id="what-gradient-descent-cannot-tell-us">What gradient descent cannot tell us</h2>

<p>Gradient descent optimises loss. That’s all it does.</p>

<p>Does not guarantee the model learned the right algorithm. Does not guarantee it will generalise. Does not guarantee it’s doing what you think it’s doing.</p>

<p>The model might find a shortcut. A way to get low loss without learning the intended behaviour. This is the “specification gaming” problem in a nutshell. Gradient descent won’t catch it. It has no idea what the “intended behaviour” is. It just minimises the number you gave it.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>Gradient descent produces the weights. MI is how we figure out what those weights <em>actually</em> learned. The relationship between these two procedures is one of the most interesting open questions in the field. <em>Why</em> does gradient descent so reliably produce interpretable circuits? Why does the same curve detector show up in every vision model? Is it inevitable?</p>
</aside>

<h2 id="wrap">Wrap</h2>

<p>Alright, real talk: between this post and the ones before it, you now know how a single neuron works, how a bunch of them chain into layers, how weights store knowledge, and how all of that was <em>produced</em> in the first place. That’s enough vocabulary to follow almost any mechanistic-interpretability paper on modern neural networks.</p>

<h2 id="research-referenced-in-this-post">Research referenced in this post</h2>

<ul class="research-list">
  <li><a class="research-card" href="https://www.nature.com/articles/323533a0" target="_blank" rel="noopener"><div class="research-card__title">Learning representations by back-propagating errors</div><div class="research-card__authors">Rumelhart, Hinton, Williams · Nature, 1986 · the original backprop paper</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/1412.6980" target="_blank" rel="noopener"><div class="research-card__title">Adam: A Method for Stochastic Optimization</div><div class="research-card__authors">Kingma, D. &amp; Ba, J. · 2014 · the optimiser almost everyone uses</div></a></li>
  <li><a class="research-card" href="https://www.deeplearningbook.org/contents/optimization.html" target="_blank" rel="noopener"><div class="research-card__title">Deep Learning · Chapter 8: Optimization</div><div class="research-card__authors">Goodfellow, Bengio, Courville · free online textbook</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/1712.09913" target="_blank" rel="noopener"><div class="research-card__title">Visualizing the Loss Landscape of Neural Nets</div><div class="research-card__authors">Li, H. et al. · 2018 · 2D slices of real loss surfaces</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2301.05217" target="_blank" rel="noopener"><div class="research-card__title">Progress measures for grokking via mechanistic interpretability</div><div class="research-card__authors">Nanda, N. et al. · 2023 · training dynamics through an MI lens</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2201.02177" target="_blank" rel="noopener"><div class="research-card__title">Grokking: Generalization Beyond Overfitting on Small Algorithmic Datasets</div><div class="research-card__authors">Power, A. et al. · 2022 · the grokking paper</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[Every weight started life as a random number. All the grammar, all the facts, learned by being wrong billions of times. This has a name, and it's the closest thing AI has to a creation story.]]></summary></entry><entry><title type="html">Layers: What Each Floor of the Building Does</title><link href="https://bhavith-chandra.github.io/posts/layers-what-each-one-does/" rel="alternate" type="text/html" title="Layers: What Each Floor of the Building Does" /><published>2026-02-20T00:00:00-08:00</published><updated>2026-02-20T00:00:00-08:00</updated><id>https://bhavith-chandra.github.io/posts/layers-what-each-one-does</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/layers-what-each-one-does/"><![CDATA[<p>Picture this: you show a trained network a photo of a face.</p>

<p>Layer 1 sees edges. Diagonal lines, curves, horizontal stripes.
Layer 2 sees eye corners, nose tips, ear lobes.
Layer 5 sees <em>this person</em>. Their mood. Whether they’re wearing glasses.</p>

<p>Same pixels going in. Totally different lens at every level. Each layer is watching the one below it and writing down what it noticed. It’s like a game of telephone, except the message gets <em>smarter</em> at every hop.</p>

<p>That’s what depth is. That’s literally why “deep” learning is deep.</p>

<hr />

<h2 id="what-a-layer-actually-is">What a layer actually is</h2>

<p>A layer is a group of neurons that all receive the same inputs and all produce outputs that feed forward together.</p>

<p>Every neuron in a layer:</p>

<ul>
  <li>Takes all the activations from the previous layer as input</li>
  <li>Applies its own weights to them</li>
  <li>Produces its own single activation value</li>
  <li>Passes that to every neuron in the next layer</li>
</ul>

<p>Net result: each layer <strong>transforms its inputs into a new representation</strong>. Same information, viewed from a different angle, with different things highlighted.</p>

<aside class="callout callout--analogy">
  <div class="callout__label">Analogy</div>
  <p>A series of photographers all shooting the same scene through different lenses. Early photographers use wide angles, capturing everything abstractly. Later photographers zoom in on specific meaningful details. By the end the picture has become a sentence: <em>"this is a cat, outdoors, late afternoon."</em></p>
</aside>

<div class="idemo" id="demo-layers">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · Layer-by-layer vision explorer</span></div>
    <div class="idemo__body">

      <p class="lyr-lead">Slide through the layers of a small vision model. At the bottom you see pixels. At the top you see categories. In between, the network turns the first into the second.</p>

      <div class="lyr-imgs" data-lyr-imgs="">
        <button class="lyr-img is-active" data-lyr-img="cat">🐱 cat</button>
        <button class="lyr-img" data-lyr-img="car">🚗 car</button>
        <button class="lyr-img" data-lyr-img="face">🙂 face</button>
        <button class="lyr-img" data-lyr-img="banana">🍌 banana</button>
        <button class="lyr-img" data-lyr-img="plane">✈️ plane</button>
        <button class="lyr-img" data-lyr-img="street">🏙️ street</button>
      </div>

      <div class="lyr-stage">
        <div class="lyr-panel">
          <canvas class="lyr-canvas" width="256" height="256" aria-label="Layer visualization"></canvas>
          <div class="lyr-panel__label" data-lyr-label="">Layer 0 · raw pixels</div>
        </div>

        <div class="lyr-info">
          <div class="lyr-info__depth" data-lyr-depth="">Depth: 0 / 12</div>
          <div class="lyr-info__title" data-lyr-title="">Raw pixels</div>
          <div class="lyr-info__desc" data-lyr-desc="">This is the image coming in. Each pixel is three numbers (R, G, B). The network hasn't done anything yet.</div>

          <div class="lyr-info__pred" data-lyr-pred-wrap="" hidden="">
            <div class="lyr-info__pred-label">Top predictions</div>
            <div class="lyr-preds" data-lyr-preds=""></div>
          </div>
        </div>
      </div>

      <div class="lyr-ctrl">
        <span class="lyr-ctrl__name">Layer</span>
        <input type="range" min="0" max="12" step="1" value="0" data-lyr-slider="" />
        <div class="lyr-ticks">
          <span>0 · pixels</span><span>3 · edges</span><span>6 · textures</span><span>9 · parts</span><span>12 · label</span>
        </div>
      </div>
    </div>
    <details>
      <summary>How this demo works</summary>
      <p>A real pretrained model is too heavy to ship inline, so this is an <em>illustrative pipeline</em>: Layer 0 draws the image on a canvas; Layer 1–2 apply Sobel edge detection; Layer 3–5 show animated feature-map tiles; Layer 6–8 draw a class-activation-style heatmap overlay; Layer 9–11 blur toward "concept space"; Layer 12 shows the final class distribution. The <em>progression</em> mirrors what happens in a real ConvNet (see Zeiler &amp; Fergus 2013, Olah et al. 2017). Swap the image, the predictions change accordingly.</p>
    </details>
  </div>
</div>

<style>
  #demo-layers .lyr-lead { margin: 0 0 1rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-layers .lyr-imgs { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 1rem; }
  #demo-layers .lyr-img {
    padding: 0.42rem 0.8rem; background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    font-family: var(--nn-mono); font-size: 0.82rem; cursor: pointer; color: var(--nn-ink);
    transition: border-color 150ms, background 150ms;
  }
  #demo-layers .lyr-img:hover { border-color: var(--nn-accent); }
  #demo-layers .lyr-img.is-active { background: var(--nn-accent-soft); border-color: var(--nn-accent); color: var(--nn-accent-dark); }

  #demo-layers .lyr-stage {
    display: grid; grid-template-columns: 280px 1fr; gap: 1rem; margin-bottom: 1rem;
  }
  @media (max-width: 720px) { #demo-layers .lyr-stage { grid-template-columns: 1fr; } }

  #demo-layers .lyr-panel {
    background: #fafafc; border: 1px solid var(--nn-line); border-radius: 6px; padding: 0.6rem; display: flex; flex-direction: column; gap: 0.5rem;
  }
  #demo-layers .lyr-canvas { width: 100%; height: auto; border-radius: 3px; background: #fff; border: 1px solid var(--nn-line); image-rendering: pixelated; }
  #demo-layers .lyr-panel__label { font-family: var(--nn-mono); font-size: 0.72rem; letter-spacing: 0.1em; color: var(--nn-muted); text-align: center; }

  #demo-layers .lyr-info {
    background: #fff; border: 1px solid var(--nn-line); border-radius: 6px; padding: 0.9rem 1.05rem;
  }
  #demo-layers .lyr-info__depth { font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.3rem; }
  #demo-layers .lyr-info__title { font-family: var(--nn-serif); font-weight: 700; font-size: 1.2rem; color: var(--nn-ink); margin-bottom: 0.3rem; }
  #demo-layers .lyr-info__desc { font-size: 0.9rem; color: var(--nn-body); line-height: 1.58; }

  #demo-layers .lyr-info__pred { margin-top: 0.9rem; padding-top: 0.7rem; border-top: 1px solid var(--nn-line); }
  #demo-layers .lyr-info__pred-label { font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.5rem; }
  #demo-layers .lyr-preds { display: flex; flex-direction: column; gap: 0.3rem; }
  #demo-layers .lyr-pred { display: grid; grid-template-columns: 80px 1fr 40px; gap: 0.5rem; align-items: center; font-family: var(--nn-mono); font-size: 0.82rem; }
  #demo-layers .lyr-pred__name { color: var(--nn-ink); }
  #demo-layers .lyr-pred__track { height: 5px; background: var(--nn-line); border-radius: 2px; overflow: hidden; }
  #demo-layers .lyr-pred__fill { display: block; height: 100%; background: var(--nn-accent); transition: width 350ms; }
  #demo-layers .lyr-pred__fill--lead { background: linear-gradient(90deg, var(--nn-accent), #5a94ce); box-shadow: 0 0 6px var(--nn-accent-glow); }
  #demo-layers .lyr-pred__pct { color: var(--nn-muted); text-align: right; }

  #demo-layers .lyr-ctrl { display: flex; flex-direction: column; gap: 0.25rem; background: #fafafc; border: 1px solid var(--nn-line); border-radius: 5px; padding: 0.8rem 1rem; }
  #demo-layers .lyr-ctrl__name { font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--nn-muted); }
  #demo-layers .lyr-ctrl input[type=range] { -webkit-appearance: none; appearance: none; height: 5px; background: var(--nn-line); border-radius: 3px; outline: none; }
  #demo-layers .lyr-ctrl input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%;
    background: var(--nn-accent); cursor: pointer; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--nn-accent);
  }
  #demo-layers .lyr-ctrl input[type=range]::-moz-range-thumb {
    width: 16px; height: 16px; border-radius: 50%; background: var(--nn-accent); cursor: pointer; border: 2px solid #fff;
  }
  #demo-layers .lyr-ticks { display: flex; justify-content: space-between; font-family: var(--nn-mono); font-size: 0.7rem; color: var(--nn-soft); margin-top: 0.2rem; }
  @media (max-width: 540px) {
    #demo-layers .lyr-ticks { display: grid; grid-template-columns: 1fr 1fr; row-gap: 0.15rem; }
  }
</style>

<script>
(function() {
  const root = document.getElementById("demo-layers");
  if (!root) return;
  const canvas = root.querySelector(".lyr-canvas");
  const ctx = canvas.getContext("2d");
  const slider = root.querySelector("[data-lyr-slider]");
  const depthEl = root.querySelector("[data-lyr-depth]");
  const titleEl = root.querySelector("[data-lyr-title]");
  const descEl = root.querySelector("[data-lyr-desc]");
  const labelEl = root.querySelector("[data-lyr-label]");
  const predsEl = root.querySelector("[data-lyr-preds]");
  const predWrap = root.querySelector("[data-lyr-pred-wrap]");

  const W = 256, H = 256;

  // Hand-drawn "images", each is a function that renders onto the canvas
  const IMAGES = {
    cat: { name: "Cat photo",
      truth: "cat",
      preds: [["cat", 94], ["dog", 3], ["rabbit", 1.5], ["fox", 0.8], ["other", 0.7]],
      draw(c) {
        // background gradient
        const g = c.createLinearGradient(0, 0, 0, H);
        g.addColorStop(0, "#e8d7b0"); g.addColorStop(1, "#c7a677");
        c.fillStyle = g; c.fillRect(0, 0, W, H);
        // body
        c.fillStyle = "#6a4a2b"; c.beginPath(); c.ellipse(128, 175, 80, 55, 0, 0, Math.PI * 2); c.fill();
        // head
        c.beginPath(); c.ellipse(128, 110, 55, 48, 0, 0, Math.PI * 2); c.fill();
        // ears
        c.beginPath(); c.moveTo(90, 72); c.lineTo(76, 32); c.lineTo(112, 66); c.closePath(); c.fill();
        c.beginPath(); c.moveTo(166, 72); c.lineTo(180, 32); c.lineTo(144, 66); c.closePath(); c.fill();
        // eyes
        c.fillStyle = "#f8dc46"; c.beginPath(); c.arc(108, 108, 8, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.arc(148, 108, 8, 0, Math.PI * 2); c.fill();
        c.fillStyle = "#111"; c.beginPath(); c.ellipse(108, 108, 3, 7, 0, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.ellipse(148, 108, 3, 7, 0, 0, Math.PI * 2); c.fill();
        // nose + mouth
        c.fillStyle = "#ec9ea3"; c.beginPath(); c.moveTo(128, 122); c.lineTo(122, 130); c.lineTo(134, 130); c.closePath(); c.fill();
        c.strokeStyle = "#3a2515"; c.lineWidth = 1.6;
        c.beginPath(); c.moveTo(128, 130); c.lineTo(128, 136); c.stroke();
        c.beginPath(); c.arc(120, 138, 4, 0, Math.PI, false); c.stroke();
        c.beginPath(); c.arc(136, 138, 4, 0, Math.PI, false); c.stroke();
      }
    },
    car: { name: "Car photo", truth: "car",
      preds: [["car", 91], ["truck", 5], ["bus", 2], ["motorcycle", 1.3], ["other", 0.7]],
      draw(c) {
        c.fillStyle = "#9fc4e3"; c.fillRect(0, 0, W, 160);
        c.fillStyle = "#6b6b70"; c.fillRect(0, 160, W, H - 160);
        // body
        c.fillStyle = "#c42f2f"; c.fillRect(40, 120, 180, 55);
        // roof
        c.beginPath(); c.moveTo(70, 120); c.lineTo(100, 80); c.lineTo(170, 80); c.lineTo(200, 120); c.closePath(); c.fill();
        // windows
        c.fillStyle = "#d7e8f4"; c.fillRect(105, 90, 25, 28); c.fillRect(140, 90, 25, 28);
        // wheels
        c.fillStyle = "#1a1a1a"; c.beginPath(); c.arc(78, 180, 18, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.arc(190, 180, 18, 0, Math.PI * 2); c.fill();
        c.fillStyle = "#ccc"; c.beginPath(); c.arc(78, 180, 8, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.arc(190, 180, 8, 0, Math.PI * 2); c.fill();
      }
    },
    face: { name: "Face photo", truth: "face",
      preds: [["face", 97], ["portrait", 2], ["head", 0.6], ["statue", 0.3], ["other", 0.1]],
      draw(c) {
        c.fillStyle = "#f0e3d2"; c.fillRect(0, 0, W, H);
        // hair
        c.fillStyle = "#3a241a"; c.beginPath(); c.arc(128, 105, 72, Math.PI, 2 * Math.PI); c.fill();
        // face oval
        c.fillStyle = "#f6d6b2"; c.beginPath(); c.ellipse(128, 135, 52, 66, 0, 0, Math.PI * 2); c.fill();
        // eyes
        c.fillStyle = "#fff"; c.beginPath(); c.ellipse(108, 125, 8, 5, 0, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.ellipse(148, 125, 8, 5, 0, 0, Math.PI * 2); c.fill();
        c.fillStyle = "#2e4e7a"; c.beginPath(); c.arc(108, 125, 3.5, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.arc(148, 125, 3.5, 0, Math.PI * 2); c.fill();
        // mouth
        c.fillStyle = "#b6514f"; c.beginPath(); c.ellipse(128, 170, 16, 5, 0, 0, Math.PI * 2); c.fill();
        // nose
        c.strokeStyle = "#b29471"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(128, 138); c.lineTo(124, 155); c.stroke();
      }
    },
    banana: { name: "Banana photo", truth: "banana",
      preds: [["banana", 96], ["plantain", 2.2], ["corn", 0.8], ["fruit", 0.6], ["other", 0.4]],
      draw(c) {
        c.fillStyle = "#2b3f22"; c.fillRect(0, 0, W, H);
        // banana shape
        c.fillStyle = "#e5b93a";
        c.beginPath(); c.moveTo(50, 190); c.quadraticCurveTo(80, 50, 210, 80); c.quadraticCurveTo(200, 140, 160, 170); c.quadraticCurveTo(110, 200, 50, 190); c.fill();
        // inner highlight
        c.strokeStyle = "#b88f26"; c.lineWidth = 3; c.beginPath(); c.moveTo(60, 180); c.quadraticCurveTo(90, 70, 200, 90); c.stroke();
        // brown tip
        c.fillStyle = "#5c3b15"; c.beginPath(); c.arc(208, 82, 8, 0, Math.PI * 2); c.fill();
      }
    },
    plane: { name: "Plane photo", truth: "airplane",
      preds: [["airplane", 93], ["glider", 3.1], ["bird", 1.8], ["jet", 1.4], ["other", 0.7]],
      draw(c) {
        const g = c.createLinearGradient(0, 0, 0, H);
        g.addColorStop(0, "#dce9f4"); g.addColorStop(1, "#8eb6dc");
        c.fillStyle = g; c.fillRect(0, 0, W, H);
        // clouds
        c.fillStyle = "rgba(255,255,255,0.65)";
        c.beginPath(); c.arc(60, 70, 24, 0, Math.PI * 2); c.arc(90, 65, 18, 0, Math.PI * 2); c.fill();
        c.beginPath(); c.arc(200, 200, 22, 0, Math.PI * 2); c.arc(230, 205, 16, 0, Math.PI * 2); c.fill();
        // plane body
        c.fillStyle = "#eaeaee";
        c.beginPath(); c.ellipse(128, 128, 75, 14, 0, 0, Math.PI * 2); c.fill();
        // wings
        c.beginPath(); c.moveTo(110, 128); c.lineTo(90, 100); c.lineTo(130, 120); c.closePath(); c.fill();
        c.beginPath(); c.moveTo(130, 128); c.lineTo(150, 158); c.lineTo(110, 136); c.closePath(); c.fill();
        // tail
        c.beginPath(); c.moveTo(58, 128); c.lineTo(40, 108); c.lineTo(68, 124); c.closePath(); c.fill();
        // windows
        c.fillStyle = "#2a6fb8"; for (let i = 0; i < 7; i++) c.fillRect(85 + i * 15, 124, 5, 4);
      }
    },
    street: { name: "Street scene", truth: "street",
      preds: [["street", 78], ["city", 14], ["road", 5], ["building", 2.1], ["other", 0.9]],
      draw(c) {
        c.fillStyle = "#6d89a8"; c.fillRect(0, 0, W, 130);
        c.fillStyle = "#4a4a50"; c.fillRect(0, 130, W, H - 130);
        // buildings
        c.fillStyle = "#2e3942"; c.fillRect(10, 50, 50, 90);
        c.fillStyle = "#3f4a55"; c.fillRect(70, 30, 60, 110);
        c.fillStyle = "#2a3238"; c.fillRect(140, 60, 45, 80);
        c.fillStyle = "#3a434d"; c.fillRect(195, 40, 55, 100);
        // windows (grid)
        c.fillStyle = "#f3d678";
        for (let y = 60; y < 130; y += 12) { for (let x = 80; x < 120; x += 10) if (Math.random() > 0.3) c.fillRect(x, y, 4, 4); }
        for (let y = 70; y < 130; y += 12) { for (let x = 15; x < 55; x += 10) if (Math.random() > 0.35) c.fillRect(x, y, 4, 4); }
        // road
        c.fillStyle = "#f8dc46"; for (let x = 0; x < W; x += 40) c.fillRect(x + 6, 195, 18, 4);
      }
    }
  };

  const LAYERS = [
    { d: 0, t: "Raw pixels", desc: "This is the image coming in. Each pixel is three numbers (R, G, B). The network hasn't done anything yet." },
    { d: 1, t: "Edge detectors", desc: "The first convolutional layer responds to oriented edges, horizontals, verticals, diagonals. Every vision model re-discovers this independently." },
    { d: 2, t: "Edge combinations", desc: "Corners, endpoints, junctions, combinations of nearby edges. Still very local; still mostly geometry." },
    { d: 3, t: "Textures", desc: "Repeated patterns: fur, asphalt, skin, metal. Neurons here fire when local patches match a learned texture." },
    { d: 4, t: "Texture + shape", desc: "Texture combined with contour. 'A curved edge with fur inside' vs 'a straight edge with metal inside'." },
    { d: 5, t: "Object parts (small)", desc: "Noses, wheels, leaves, keyboard keys. The neurons now fire for parts of real-world objects that have human names." },
    { d: 6, t: "Object parts (medium)", desc: "Eye + eyebrow + nose clusters. Wheel + fender. Wings + fuselage. Multi-part assemblies." },
    { d: 7, t: "Object regions", desc: "The heatmap shows which region of the image is firing most strongly. This is where MI techniques like GradCAM operate." },
    { d: 8, t: "Whole-object detectors", desc: "A neuron that fires for 'cat' regardless of pose. 'Car'. 'Face'. The model has human-level object categories now." },
    { d: 9, t: "Object + context", desc: "Neurons that distinguish sub-categories and care about scene context. 'Cat on a sofa' vs 'cat outdoors'." },
    { d: 10, t: "Near-final features", desc: "The representation is very compact now, a few hundred numbers that describe the image at a semantic level." },
    { d: 11, t: "Pre-classification", desc: "One step before the output head. The model is almost ready to commit to a label." },
    { d: 12, t: "Class probabilities", desc: "The output layer. Softmax over the class list. This is the model's answer." },
  ];

  let currentImg = "cat";
  let baseImageData = null;

  function renderBaseImage() {
    ctx.clearRect(0, 0, W, H);
    IMAGES[currentImg].draw(ctx);
    baseImageData = ctx.getImageData(0, 0, W, H);
  }

  // Sobel edge filter
  function sobel() {
    const src = baseImageData.data;
    const out = ctx.createImageData(W, H);
    const gray = new Float32Array(W * H);
    for (let i = 0; i < W * H; i++) gray[i] = (src[i * 4] + src[i * 4 + 1] + src[i * 4 + 2]) / 3;
    for (let y = 1; y < H - 1; y++) {
      for (let x = 1; x < W - 1; x++) {
        const i = y * W + x;
        const gx = -gray[i - W - 1] - 2 * gray[i - 1] - gray[i + W - 1]
                  + gray[i - W + 1] + 2 * gray[i + 1] + gray[i + W + 1];
        const gy = -gray[i - W - 1] - 2 * gray[i - W] - gray[i - W + 1]
                  + gray[i + W - 1] + 2 * gray[i + W] + gray[i + W + 1];
        const m = Math.min(255, Math.hypot(gx, gy));
        const j = i * 4;
        out.data[j] = m; out.data[j + 1] = Math.min(255, m * 1.1); out.data[j + 2] = Math.min(255, m * 1.3); out.data[j + 3] = 255;
      }
    }
    return out;
  }

  // Textured / tiled feature-map visualisation
  function renderFeatureTiles(depth) {
    ctx.clearRect(0, 0, W, H);
    const tiles = 4;
    const tw = W / tiles, th = H / tiles;
    for (let ty = 0; ty < tiles; ty++) {
      for (let tx = 0; tx < tiles; tx++) {
        // downscale the base image, then colourise per-tile
        const hue = (tx + ty * tiles) * 47 % 360;
        const variant = Math.random() * 0.5 + 0.25;
        ctx.save();
        ctx.beginPath(); ctx.rect(tx * tw, ty * th, tw, th); ctx.clip();
        // base image scaled down
        ctx.globalAlpha = 0.35;
        ctx.drawImage(canvas, 0, 0, W, H, tx * tw, ty * th, tw, th);
        ctx.globalAlpha = 1;
        // pattern overlay
        const g = ctx.createLinearGradient(tx * tw, ty * th, tx * tw + tw, ty * th + th);
        g.addColorStop(0, "rgba(42, 111, 184, " + (variant * 0.35).toFixed(2) + ")");
        g.addColorStop(0.5, "rgba(201, 140, 58, " + (variant * 0.28).toFixed(2) + ")");
        g.addColorStop(1, "rgba(90, 148, 206, " + (variant * 0.35).toFixed(2) + ")");
        ctx.fillStyle = g; ctx.fillRect(tx * tw, ty * th, tw, th);
        // noise texture
        const img = ctx.getImageData(tx * tw, ty * th, tw, th);
        const scale = Math.min(8, Math.max(1, 8 - depth));
        for (let i = 0; i < img.data.length; i += 4) {
          const n = (Math.random() - 0.5) * 40 * variant;
          img.data[i] = Math.max(0, Math.min(255, img.data[i] + n));
          img.data[i + 1] = Math.max(0, Math.min(255, img.data[i + 1] + n));
          img.data[i + 2] = Math.max(0, Math.min(255, img.data[i + 2] + n));
        }
        ctx.putImageData(img, tx * tw, ty * th);
        ctx.restore();
      }
    }
    // grid
    ctx.strokeStyle = "rgba(255,255,255,0.4)"; ctx.lineWidth = 1;
    for (let i = 1; i < tiles; i++) {
      ctx.beginPath(); ctx.moveTo(i * tw, 0); ctx.lineTo(i * tw, H); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(0, i * th); ctx.lineTo(W, i * th); ctx.stroke();
    }
  }

  // Class-activation heatmap overlay
  function renderHeatmap(depth) {
    ctx.putImageData(baseImageData, 0, 0);
    // Base image grey-ified
    const img = ctx.getImageData(0, 0, W, H);
    for (let i = 0; i < img.data.length; i += 4) {
      const g = (img.data[i] + img.data[i + 1] + img.data[i + 2]) / 3;
      img.data[i] = g; img.data[i + 1] = g; img.data[i + 2] = g;
    }
    ctx.putImageData(img, 0, 0);
    // Heatmap blob centred on the subject, different per image
    const CENTERS = {
      cat: [128, 115], car: [128, 145], face: [128, 135],
      banana: [130, 130], plane: [128, 125], street: [110, 110]
    };
    const [cx, cy] = CENTERS[currentImg] || [128, 128];
    const radius = 50 + (12 - depth) * 6;
    const g = ctx.createRadialGradient(cx, cy, 4, cx, cy, radius);
    g.addColorStop(0, "rgba(255, 200, 64, 0.85)");
    g.addColorStop(0.4, "rgba(201, 140, 58, 0.55)");
    g.addColorStop(0.75, "rgba(42, 111, 184, 0.35)");
    g.addColorStop(1, "rgba(42, 111, 184, 0)");
    ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
  }

  function renderBlur(depth) {
    ctx.putImageData(baseImageData, 0, 0);
    ctx.filter = "blur(" + ((depth - 8) * 1.5) + "px) saturate(1.8)";
    ctx.drawImage(canvas, 0, 0);
    ctx.filter = "none";
    // Overlay semantic blob
    ctx.fillStyle = "rgba(42, 111, 184, 0.22)";
    ctx.fillRect(0, 0, W, H);
  }

  function renderFinalPredictions() {
    ctx.fillStyle = "#fafafc"; ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = "#d7d7dc"; ctx.fillRect(0, H - 1, W, 1);
    ctx.fillStyle = "var(--nn-ink)";
    // Draw bars
    const preds = IMAGES[currentImg].preds;
    const totalH = H - 40;
    const barH = totalH / preds.length - 6;
    ctx.textAlign = "left"; ctx.textBaseline = "middle";
    preds.forEach((p, i) => {
      const [name, pct] = p;
      const y = 20 + i * (barH + 6);
      const barW = (pct / 100) * (W - 130);
      // label
      ctx.fillStyle = "#2a2a2a";
      ctx.font = "12px 'SF Mono', Menlo, monospace";
      ctx.fillText(name, 12, y + barH / 2 + 1);
      // track
      ctx.fillStyle = "#d7d7dc";
      ctx.fillRect(80, y + barH / 2 - 3, W - 130, 6);
      // fill
      ctx.fillStyle = i === 0 ? "#2a6fb8" : "#9b9b9e";
      ctx.fillRect(80, y + barH / 2 - 3, barW, 6);
      // pct
      ctx.fillStyle = "#6b6b70";
      ctx.fillText(pct + "%", W - 40, y + barH / 2 + 1);
    });
  }

  function renderLayer(depth) {
    if (depth === 0) {
      ctx.putImageData(baseImageData, 0, 0);
    } else if (depth <= 2) {
      const edges = sobel();
      ctx.putImageData(edges, 0, 0);
      if (depth === 2) {
        // slight colour shift
        ctx.globalAlpha = 0.25;
        ctx.drawImage(canvas, 0, 0);
        ctx.globalAlpha = 1;
      }
    } else if (depth <= 5) {
      renderFeatureTiles(depth);
    } else if (depth <= 8) {
      renderHeatmap(depth);
    } else if (depth <= 11) {
      renderBlur(depth);
    } else {
      renderFinalPredictions();
    }

    const L = LAYERS[depth];
    depthEl.textContent = "Depth: " + depth + " / 12";
    titleEl.textContent = L.t;
    descEl.textContent = L.desc;
    labelEl.textContent = "Layer " + depth + " · " + L.t.toLowerCase();

    if (depth === 12) {
      predWrap.hidden = false;
      predsEl.innerHTML = "";
      IMAGES[currentImg].preds.forEach((p, i) => {
        const [name, pct] = p;
        const d = document.createElement("div");
        d.className = "lyr-pred";
        d.innerHTML =
          '<span class="lyr-pred__name">' + name + '</span>' +
          '<span class="lyr-pred__track"><span class="lyr-pred__fill ' + (i === 0 ? "lyr-pred__fill--lead" : "") + '" style="width:' + pct + '%"></span></span>' +
          '<span class="lyr-pred__pct">' + pct + '%</span>';
        predsEl.appendChild(d);
      });
    } else {
      predWrap.hidden = true;
    }
  }

  function selectImage(key) {
    currentImg = key;
    root.querySelectorAll("[data-lyr-img]").forEach(b => b.classList.toggle("is-active", b.dataset.lyrImg === key));
    renderBaseImage();
    renderLayer(+slider.value);
  }

  root.querySelectorAll("[data-lyr-img]").forEach(b => b.addEventListener("click", () => selectImage(b.dataset.lyrImg)));
  slider.addEventListener("input", () => renderLayer(+slider.value));

  selectImage("cat");
  renderLayer(0);
})();
</script>

<h2 id="input-and-output-layers">Input and output layers</h2>

<p><strong>Input layer.</strong> No math. Just receives the raw data and passes it forward. Images: pixel values. Text: token embedding vectors. Tabular data: feature values. The input layer is pure format conversion. “Here’s the world, in number form.”</p>

<p><strong>Output layer.</strong> The final decision. Classification: a probability over each class (softmax). Language modelling: a probability over every word in the vocabulary (often 50,000+ options). Regression: a single continuous number.</p>

<p>Everything interesting happens between these two.</p>

<h2 id="hidden-layers-the-middle-of-the-machine">Hidden layers, the middle of the machine</h2>

<p>Don’t let the word fool you. “Hidden” just means “not the input, not the output.” These layers are where all the action happens, and where MI spends basically all its time.</p>

<p>In early vision models, researchers noticed the hidden layers had a striking, almost biological structure. Here’s the rough gradient:</p>

<h3 id="layer-1-gabor-filters">Layer 1: Gabor filters</h3>

<p>Neurons respond to oriented edges. Horizontal, vertical, 45-degree. Nobody programmed this. Every model trained on natural images independently discovers it. Emerges from the statistics of images themselves.</p>

<h3 id="layer-2-textures-and-simple-shapes">Layer 2: Textures and simple shapes</h3>

<p>Combinations of edges form textures. Checkerboards. Crosshatches. Dots. Neurons looking for local patches that match a pattern.</p>

<h3 id="layer-34-object-parts">Layer 3–4: Object parts</h3>

<p>Eyes. Wheels. Leaves. Neurons now looking for parts of real objects. Things that have names in human language.</p>

<h3 id="layer-57-objects-and-scenes">Layer 5–7: Objects and scenes</h3>

<p>Full objects, faces, specific categories. High-level human concepts represented in the network’s internal language.</p>

<p>This progression is called <strong>hierarchical feature extraction</strong>, and it appears in every deep network trained on natural data. Images, text, audio. The depth lets the model compose simple features into complex ones, repeatedly.</p>

<div class="demo demo-fh" id="demo-fh">
  <div class="fh__head">
    <div class="fh__title">Feature hierarchy: what each layer “sees”</div>
    <div class="fh__sub">Click any tile to inspect a unit. Layer 1 finds edges; layer 4 has assembled them into a face.</div>
  </div>
  <div class="fh__board">
    <div class="fh__layer" data-layer="1"><div class="fh__llabel">Layer 1 · edges</div><div class="fh__row" data-row="1"></div></div>
    <div class="fh__layer" data-layer="2"><div class="fh__llabel">Layer 2 · textures</div><div class="fh__row" data-row="2"></div></div>
    <div class="fh__layer" data-layer="3"><div class="fh__llabel">Layer 3 · parts</div><div class="fh__row" data-row="3"></div></div>
    <div class="fh__layer" data-layer="4"><div class="fh__llabel">Layer 4 · objects</div><div class="fh__row" data-row="4"></div></div>
  </div>
  <div class="fh__inspector">
    <div class="fh__plabel">Selected unit</div>
    <div class="fh__inspector-row">
      <canvas id="fh-zoom" width="180" height="180"></canvas>
      <div class="fh__info" id="fh-info">Click any tile above to read what activates this unit and which earlier-layer features feed it.</div>
    </div>
  </div>
</div>
<style>
  .demo-fh{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-fh .fh__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-fh .fh__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-fh .fh__board{display:flex;flex-direction:column;gap:10px;margin:14px 0 16px}
  .demo-fh .fh__layer{background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;padding:10px}
  .demo-fh .fh__llabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:8px;font-weight:700}
  .demo-fh .fh__row{display:grid;grid-template-columns:repeat(6,1fr);gap:6px}
  .demo-fh .fh__tile{aspect-ratio:1;border:1px solid #e0c89e;border-radius:6px;cursor:pointer;background:#fff6e0;overflow:hidden;position:relative;transition:transform 0.12s}
  .demo-fh .fh__tile:hover{transform:translateY(-2px);border-color:#b77214}
  .demo-fh .fh__tile.is-active{border:2px solid #b77214;box-shadow:0 0 0 2px #fde9bf}
  .demo-fh .fh__tile canvas{width:100%;height:100%;display:block}
  .demo-fh .fh__inspector{background:#fff6e0;border:1px dashed #ddb88e;border-radius:8px;padding:12px}
  .demo-fh .fh__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;font-weight:700;margin-bottom:8px}
  .demo-fh .fh__inspector-row{display:flex;gap:14px;align-items:flex-start}
  .demo-fh #fh-zoom{flex-shrink:0;border:1px solid #ecdbc0;border-radius:6px;background:#fffefb}
  .demo-fh .fh__info{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;line-height:1.55}
  .demo-fh .fh__info b{color:#7c4d0a}
  @media (max-width:560px){.demo-fh .fh__row{grid-template-columns:repeat(3,1fr)}.demo-fh .fh__inspector-row{flex-direction:column}}
</style>

<script>
(function(){
  const root=document.getElementById('demo-fh'); if(!root) return;
  const palettes={
    1:[{name:'horizontal edge',drawer:edgeDrawer(0)},{name:'vertical edge',drawer:edgeDrawer(90)},{name:'diagonal /',drawer:edgeDrawer(45)},{name:'diagonal \\',drawer:edgeDrawer(135)},{name:'thin line',drawer:edgeDrawer(20,1)},{name:'curve',drawer:curveDrawer()}],
    2:[{name:'corner',drawer:cornerDrawer()},{name:'cross-hatch',drawer:hatchDrawer()},{name:'dot grid',drawer:dotsDrawer()},{name:'stripes',drawer:stripesDrawer()},{name:'gradient',drawer:gradDrawer()},{name:'checker',drawer:checkerDrawer()}],
    3:[{name:'eye',drawer:eyeDrawer()},{name:'circle',drawer:circleDrawer()},{name:'T-junction',drawer:tDrawer()},{name:'paw',drawer:pawDrawer()},{name:'leaf',drawer:leafDrawer()},{name:'wheel',drawer:wheelDrawer()}],
    4:[{name:'face',drawer:faceDrawer()},{name:'dog',drawer:dogDrawer()},{name:'tree',drawer:treeDrawer()},{name:'car',drawer:carDrawer()},{name:'house',drawer:houseDrawer()},{name:'flower',drawer:flowerDrawer()}]
  };
  const wires={
    1:'raw pixels',
    2:'edges from layer 1',
    3:'textures and corners from layer 2',
    4:'parts (eyes, circles, T-junctions) from layer 3'
  };
  for(let l=1;l<=4;l++){
    const row=root.querySelector(`[data-row="${l}"]`);
    palettes[l].forEach((unit,idx)=>{
      const tile=document.createElement('div'); tile.className='fh__tile';
      const c=document.createElement('canvas'); c.width=120; c.height=120;
      tile.appendChild(c); unit.drawer(c.getContext('2d'),120);
      tile.addEventListener('click',()=>{
        root.querySelectorAll('.fh__tile').forEach(t=>t.classList.remove('is-active'));
        tile.classList.add('is-active');
        select(l,idx,unit);
      });
      row.appendChild(tile);
    });
  }
  function select(l,idx,unit){
    const z=root.querySelector('#fh-zoom').getContext('2d'); z.clearRect(0,0,180,180); unit.drawer(z,180);
    root.querySelector('#fh-info').innerHTML=`Layer <b>${l}</b>, unit ${idx+1} · prefers <b>${unit.name}</b>.<br>Reads from <b>${wires[l]}</b>.<br><span style="color:#7a6a52">Lower-level features compose into this one — the same dot pattern is used to recognize an entire image at the top.</span>`;
  }
  function edgeDrawer(angle,thin){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.save(); g.translate(s/2,s/2); g.rotate(angle*Math.PI/180); g.fillStyle='#7c4d0a'; const t=thin?2:s*0.18; g.fillRect(-s,-t/2,s*2,t); g.restore();}}
  function curveDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.strokeStyle='#7c4d0a'; g.lineWidth=s*0.16; g.beginPath(); g.arc(s*0.65,s*0.65,s*0.45,Math.PI,1.5*Math.PI); g.stroke();}}
  function cornerDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; g.fillRect(s*0.15,s*0.15,s*0.7,s*0.16); g.fillRect(s*0.15,s*0.15,s*0.16,s*0.7);}}
  function hatchDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.strokeStyle='#7c4d0a'; g.lineWidth=2; for(let i=-s;i<s*2;i+=s*0.18){g.beginPath();g.moveTo(i,0);g.lineTo(i+s,s);g.stroke(); g.beginPath();g.moveTo(i+s,0);g.lineTo(i,s);g.stroke();}}}
  function dotsDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; for(let x=s*0.18;x<s;x+=s*0.22) for(let y=s*0.18;y<s;y+=s*0.22){g.beginPath(); g.arc(x,y,s*0.05,0,Math.PI*2); g.fill();}}}
  function stripesDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; for(let y=0;y<s;y+=s*0.2) g.fillRect(0,y,s,s*0.1);}}
  function gradDrawer(){return (g,s)=>{const grd=g.createLinearGradient(0,0,s,s); grd.addColorStop(0,'#fff6e0'); grd.addColorStop(1,'#7c4d0a'); g.fillStyle=grd; g.fillRect(0,0,s,s);}}
  function checkerDrawer(){return (g,s)=>{const n=4; const t=s/n; for(let i=0;i<n;i++) for(let j=0;j<n;j++){g.fillStyle=(i+j)%2?'#7c4d0a':'#fff6e0'; g.fillRect(i*t,j*t,t,t);}}}
  function eyeDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#fffefb'; g.beginPath(); g.ellipse(s/2,s/2,s*0.38,s*0.22,0,0,Math.PI*2); g.fill(); g.strokeStyle='#7c4d0a'; g.lineWidth=2; g.stroke(); g.fillStyle='#7c4d0a'; g.beginPath(); g.arc(s/2,s/2,s*0.13,0,Math.PI*2); g.fill();}}
  function circleDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.strokeStyle='#7c4d0a'; g.lineWidth=s*0.08; g.beginPath(); g.arc(s/2,s/2,s*0.32,0,Math.PI*2); g.stroke();}}
  function tDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; g.fillRect(s*0.18,s*0.32,s*0.64,s*0.12); g.fillRect(s*0.44,s*0.32,s*0.12,s*0.5);}}
  function pawDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; g.beginPath(); g.ellipse(s/2,s*0.62,s*0.22,s*0.17,0,0,Math.PI*2); g.fill(); [[0.32,0.32],[0.5,0.24],[0.68,0.32],[0.78,0.5]].forEach(([x,y])=>{g.beginPath(); g.arc(s*x,s*y,s*0.07,0,Math.PI*2); g.fill();});}}
  function leafDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#0d6b3a'; g.beginPath(); g.moveTo(s*0.5,s*0.15); g.quadraticCurveTo(s*0.85,s*0.5,s*0.5,s*0.85); g.quadraticCurveTo(s*0.15,s*0.5,s*0.5,s*0.15); g.fill();}}
  function wheelDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#1a1a1a'; g.beginPath(); g.arc(s/2,s/2,s*0.36,0,Math.PI*2); g.fill(); g.fillStyle='#7c4d0a'; g.beginPath(); g.arc(s/2,s/2,s*0.13,0,Math.PI*2); g.fill();}}
  function faceDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#fde9bf'; g.beginPath(); g.arc(s/2,s/2,s*0.36,0,Math.PI*2); g.fill(); g.strokeStyle='#7c4d0a'; g.lineWidth=2; g.stroke(); g.fillStyle='#7c4d0a'; g.beginPath(); g.arc(s*0.4,s*0.45,s*0.04,0,Math.PI*2); g.fill(); g.beginPath(); g.arc(s*0.6,s*0.45,s*0.04,0,Math.PI*2); g.fill(); g.beginPath(); g.arc(s/2,s*0.6,s*0.12,0.1*Math.PI,0.9*Math.PI); g.stroke();}}
  function dogDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#a87844'; g.beginPath(); g.ellipse(s/2,s*0.55,s*0.28,s*0.22,0,0,Math.PI*2); g.fill(); g.beginPath(); g.ellipse(s*0.32,s*0.32,s*0.1,s*0.16,0,0,Math.PI*2); g.fill(); g.beginPath(); g.ellipse(s*0.68,s*0.32,s*0.1,s*0.16,0,0,Math.PI*2); g.fill(); g.fillStyle='#1a1a1a'; g.beginPath(); g.arc(s*0.42,s*0.55,s*0.03,0,Math.PI*2); g.fill(); g.beginPath(); g.arc(s*0.58,s*0.55,s*0.03,0,Math.PI*2); g.fill(); g.beginPath(); g.arc(s/2,s*0.65,s*0.04,0,Math.PI*2); g.fill();}}
  function treeDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#7c4d0a'; g.fillRect(s*0.46,s*0.5,s*0.08,s*0.4); g.fillStyle='#0d6b3a'; g.beginPath(); g.arc(s/2,s*0.4,s*0.3,0,Math.PI*2); g.fill();}}
  function carDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#b25c2c'; g.fillRect(s*0.12,s*0.5,s*0.76,s*0.22); g.beginPath(); g.moveTo(s*0.22,s*0.5); g.lineTo(s*0.32,s*0.32); g.lineTo(s*0.68,s*0.32); g.lineTo(s*0.78,s*0.5); g.closePath(); g.fill(); g.fillStyle='#1a1a1a'; g.beginPath(); g.arc(s*0.28,s*0.74,s*0.07,0,Math.PI*2); g.fill(); g.beginPath(); g.arc(s*0.72,s*0.74,s*0.07,0,Math.PI*2); g.fill();}}
  function houseDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); g.fillStyle='#fde9bf'; g.fillRect(s*0.22,s*0.45,s*0.56,s*0.4); g.fillStyle='#b25c2c'; g.beginPath(); g.moveTo(s*0.16,s*0.45); g.lineTo(s/2,s*0.18); g.lineTo(s*0.84,s*0.45); g.closePath(); g.fill(); g.fillStyle='#7c4d0a'; g.fillRect(s*0.45,s*0.6,s*0.1,s*0.25);}}
  function flowerDrawer(){return (g,s)=>{g.fillStyle='#fff6e0'; g.fillRect(0,0,s,s); for(let i=0;i<6;i++){const a=i/6*Math.PI*2; g.fillStyle='#fbbf24'; g.beginPath(); g.ellipse(s/2+Math.cos(a)*s*0.2,s*0.45+Math.sin(a)*s*0.2,s*0.1,s*0.16,a,0,Math.PI*2); g.fill();} g.fillStyle='#b25c2c'; g.beginPath(); g.arc(s/2,s*0.45,s*0.08,0,Math.PI*2); g.fill(); g.fillStyle='#0d6b3a'; g.fillRect(s*0.48,s*0.5,s*0.04,s*0.4);}}
})();
</script>

<p>Click an object in the bottom row, then trace your way up. Every face is a composition of eye-detectors and mouth-detectors. Every eye-detector is a composition of curve-detectors and edge-detectors. The whole thing is recursive: one small library of features, reused at every level of detail.</p>

<h2 id="what-layers-do-in-language-models">What layers do in language models</h2>

<p>In text transformers, the same hierarchy exists, but it’s harder to see because language doesn’t have the obvious spatial structure of images.</p>

<p>Research into transformer layers has found patterns like:</p>

<ul>
  <li><strong>Early (1–4).</strong> Local, syntactic. Nearby word relationships. Part of speech. Simple co-occurrence.</li>
  <li><strong>Middle (5–16).</strong> Syntactic structure. Subject-verb relationships. Clause boundaries. Entity tracking.</li>
  <li><strong>Late (17–final).</strong> Semantic, pragmatic. What the text means. Who’s saying what to whom. What should come next.</li>
</ul>

<p>Not perfectly clean (features mix across layers), but the gradient from syntax to semantics is consistent and has been verified by probing experiments across many models.</p>

<h3 id="the-logit-lens">The logit lens</h3>

<p>A cool MI technique that reads out the model’s <em>best guess</em> at each layer, before the final output. Early layers, the guess is mostly garbage. Middle layers, it starts approaching the right semantic category. Late layers, it converges on the final answer. Shows you how the model builds its answer progressively.</p>

<h2 id="depth-vs-width">Depth vs width</h2>

<p><strong>Width</strong> = more neurons per layer. More “workers” doing parallel analysis at each step.</p>

<p><strong>Depth</strong> = more layers. More steps of abstraction before the final answer.</p>

<p>Both help, but differently.</p>

<p>Width gives the model more capacity to represent complex things at each level of abstraction. Depth gives the model more steps to compose simple patterns into complex ones.</p>

<p>Modern networks are both wide and deep. GPT-4 is estimated to have around 120 layers. Many of those layers have ~25,000 neurons each. Which, you know, is a lot.</p>

<p>For interpretability: more layers means more places for information to transform. Also means there’s more “room” for information to be stored in intermediate representations. Which is one reason large models are more capable but also harder to interpret.</p>

<div class="demo demo-depth" id="demo-depth">
  <div class="depth__head">
    <div class="depth__title">Why depth matters: 2D classifier playground</div>
    <div class="depth__sub">Pick a dataset. Add hidden layers. Watch the boundary go from a straight line to spirals — and see when it overfits.</div>
  </div>
  <div class="depth__controls">
    <div class="depth__group">
      <span class="depth__plabel">Dataset</span>
      <button class="depth__btn is-active" data-ds="lin">Linear</button>
      <button class="depth__btn" data-ds="moons">Moons</button>
      <button class="depth__btn" data-ds="circles">Circles</button>
      <button class="depth__btn" data-ds="spiral">Spiral</button>
    </div>
    <div class="depth__group">
      <label>Hidden layers <input type="range" id="depth-L" min="0" max="5" step="1" value="1" /><span data-lout="">1</span></label>
      <label>Width <input type="range" id="depth-W" min="2" max="16" step="1" value="6" /><span data-wout="">6</span></label>
    </div>
  </div>
  <div class="depth__row">
    <canvas id="depth-canvas" width="380" height="380"></canvas>
    <div class="depth__panel">
      <div class="depth__plabel">Training</div>
      <canvas id="depth-loss" width="320" height="160"></canvas>
      <div class="depth__buttons">
        <button data-act="train">▶ Train 1500 steps</button>
        <button data-act="reset">Reset weights</button>
      </div>
      <div class="depth__readout" id="depth-readout">Train accuracy <b>—</b> · final loss <b>—</b></div>
    </div>
  </div>
</div>
<style>
  .demo-depth{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-depth .depth__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-depth .depth__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-depth .depth__controls{display:flex;flex-wrap:wrap;gap:14px;margin:14px 0 10px;align-items:center}
  .demo-depth .depth__group{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
  .demo-depth .depth__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;font-weight:700;margin-right:4px}
  .demo-depth .depth__btn{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:5px 10px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-depth .depth__btn.is-active{background:#fbbf24;color:#3a2106;border-color:#b77214}
  .demo-depth .depth__group label{font-size:12px;color:#7c4d0a;display:flex;align-items:center;gap:6px;font-weight:600}
  .demo-depth .depth__group input[type=range]{accent-color:#b77214;width:100px}
  .demo-depth .depth__group span{font-family:ui-monospace,Menlo,monospace;width:24px;text-align:right;color:#5a3d12}
  .demo-depth .depth__row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .demo-depth canvas{width:100%;height:auto;background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;display:block}
  .demo-depth .depth__panel{display:flex;flex-direction:column;gap:8px}
  .demo-depth .depth__buttons{display:flex;gap:6px}
  .demo-depth .depth__buttons button{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;flex:1}
  .demo-depth .depth__buttons button:hover{background:#fbbf24;color:#3a2106}
  .demo-depth .depth__readout{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:6px 10px;border-radius:8px}
  @media (max-width:560px){.demo-depth .depth__row{grid-template-columns:1fr}}
</style>

<script>
(function(){
  const root=document.getElementById('demo-depth'); if(!root) return;
  const cvs=root.querySelector('#depth-canvas'), ctx=cvs.getContext('2d');
  const lcv=root.querySelector('#depth-loss'), lctx=lcv.getContext('2d');
  const Lin=root.querySelector('#depth-L'), Win=root.querySelector('#depth-W');
  const Lout=root.querySelector('[data-Lout]'), Wout=root.querySelector('[data-Wout]');
  const readout=root.querySelector('#depth-readout');
  let dataset='lin', data=[], net=null, lossHist=[];
  function genData(){data=[];
    if(dataset==='lin'){for(let i=0;i<160;i++){const x=Math.random()*2-1, y=Math.random()*2-1; data.push([x,y, (x+y>0)?1:0]);}}
    if(dataset==='moons'){for(let i=0;i<160;i++){const t=Math.random()*Math.PI; const c=i<80?0:1; const cx=c?0.3:-0.3, cy=c?-0.15:0.15; const x=Math.cos(t)*0.5*(c?1:-1)+cx+(Math.random()-0.5)*0.12; const y=Math.sin(t)*0.5*(c?1:-1)+cy+(Math.random()-0.5)*0.12; data.push([x,y,c]);}}
    if(dataset==='circles'){for(let i=0;i<160;i++){const c=i<80?0:1; const r=c?0.7:0.3; const t=Math.random()*Math.PI*2; const x=Math.cos(t)*r+(Math.random()-0.5)*0.08; const y=Math.sin(t)*r+(Math.random()-0.5)*0.08; data.push([x,y,c]);}}
    if(dataset==='spiral'){for(let i=0;i<160;i++){const c=i%2; const t=(i/160)*4*Math.PI+c*Math.PI; const r=t/(4*Math.PI)*0.85; const x=Math.cos(t)*r+(Math.random()-0.5)*0.06; const y=Math.sin(t)*r+(Math.random()-0.5)*0.06; data.push([x,y,c]);}}
  }
  function buildNet(){const L=parseInt(Lin.value), W=parseInt(Win.value);
    const dims=[2]; for(let i=0;i<L;i++) dims.push(W); dims.push(1);
    net={dims, Ws:[], Bs:[]};
    for(let l=0;l<dims.length-1;l++){const Wm=[]; for(let i=0;i<dims[l+1];i++){const r=[]; for(let j=0;j<dims[l];j++) r.push((Math.random()*2-1)*Math.sqrt(2/dims[l])); Wm.push(r);} net.Ws.push(Wm); net.Bs.push(new Array(dims[l+1]).fill(0));}
    lossHist=[];
  }
  function tanh(x){return Math.tanh(x);} function dtanh(y){return 1-y*y;}
  function sig(x){return 1/(1+Math.exp(-x));}
  function fwd(x){const acts=[x.slice()]; for(let l=0;l<net.Ws.length;l++){const Wm=net.Ws[l],b=net.Bs[l]; const a=new Array(Wm.length); for(let i=0;i<Wm.length;i++){let s=b[i]; for(let j=0;j<x.length;j++)s+=Wm[i][j]*x[j]; a[i]=(l===net.Ws.length-1)?sig(s):tanh(s);} x=a; acts.push(a.slice());} return acts;}
  function trainStep(){let totalLoss=0; const lr=0.05; for(const pt of data){const x=[pt[0],pt[1]],y=pt[2]; const acts=fwd(x); const yhat=acts[acts.length-1][0]; const loss=-(y*Math.log(yhat+1e-9)+(1-y)*Math.log(1-yhat+1e-9)); totalLoss+=loss;
    let delta=[yhat-y];
    for(let l=net.Ws.length-1;l>=0;l--){const aPrev=acts[l],a=acts[l+1],Wm=net.Ws[l],b=net.Bs[l]; const newDelta=new Array(aPrev.length).fill(0);
      for(let i=0;i<Wm.length;i++){for(let j=0;j<aPrev.length;j++){newDelta[j]+=Wm[i][j]*delta[i]; Wm[i][j]-=lr*delta[i]*aPrev[j];} b[i]-=lr*delta[i];}
      if(l>0){const tDelta=new Array(aPrev.length); for(let j=0;j<aPrev.length;j++) tDelta[j]=newDelta[j]*dtanh(aPrev[j]); delta=tDelta;}
    }} return totalLoss/data.length;}
  function drawBoundary(){const W=cvs.width,H=cvs.height; const img=ctx.createImageData(W,H); const step=4;
    for(let py=0;py<H;py+=step) for(let px=0;px<W;px+=step){const x=(px/W)*2-1, y=1-(py/H)*2; const out=fwd([x,y]); const p=out[out.length-1][0];
      const r=Math.round(255-(255-251)*p), g=Math.round(246-(246-191)*p), b=Math.round(224-(224-36)*p);
      for(let dy=0;dy<step;dy++) for(let dx=0;dx<step;dx++){const idx=((py+dy)*W+(px+dx))*4; img.data[idx]=r; img.data[idx+1]=g; img.data[idx+2]=b; img.data[idx+3]=255;}}
    ctx.putImageData(img,0,0);
    data.forEach(p=>{const px=(p[0]+1)/2*W, py=(1-p[1])/2*H; ctx.fillStyle=p[2]?'#7c4d0a':'#fff'; ctx.strokeStyle='#3a2106'; ctx.lineWidth=1.2; ctx.beginPath(); ctx.arc(px,py,4,0,Math.PI*2); ctx.fill(); ctx.stroke();});
  }
  function drawLoss(){const W=lcv.width,H=lcv.height; lctx.clearRect(0,0,W,H); const pad={l:34,r:8,t:8,b:18}; const gw=W-pad.l-pad.r,gh=H-pad.t-pad.b;
    if(!lossHist.length){lctx.fillStyle='#a08562'; lctx.font='12px ui-monospace,Menlo,monospace'; lctx.fillText('train to see loss',pad.l+20,H/2); return;}
    const mx=Math.max(...lossHist), mn=Math.min(...lossHist);
    lctx.strokeStyle='#fbbf24'; lctx.lineWidth=2; lctx.beginPath();
    lossHist.forEach((v,i)=>{const x=pad.l+(i/(lossHist.length-1))*gw; const y=pad.t+(1-(v-mn)/(mx-mn+1e-6))*gh; if(i===0)lctx.moveTo(x,y); else lctx.lineTo(x,y);}); lctx.stroke();
    lctx.fillStyle='#7c4d0a'; lctx.font='10px ui-monospace,Menlo,monospace'; lctx.fillText(mx.toFixed(2),4,pad.t+8); lctx.fillText(mn.toFixed(2),4,H-pad.b);
  }
  function evalAcc(){let c=0; for(const pt of data){const out=fwd([pt[0],pt[1]]); const p=out[out.length-1][0]; if((p>0.5?1:0)===pt[2]) c++;} return c/data.length;}
  function train(){const N=1500; for(let i=0;i<N;i++){const l=trainStep(); if(i%30===0) lossHist.push(l);} drawBoundary(); drawLoss();
    readout.innerHTML=`Train accuracy <b>${(evalAcc()*100).toFixed(1)}%</b> · final loss <b>${lossHist[lossHist.length-1].toFixed(3)}</b> · ${parseInt(Lin.value)} hidden layer${Lin.value==='1'?'':'s'}, width ${Win.value}`;}
  function rebuild(){buildNet(); drawBoundary(); drawLoss(); readout.innerHTML='Untrained network. Press <b>Train</b>.';}
  root.querySelectorAll('[data-ds]').forEach(b=>b.addEventListener('click',()=>{root.querySelectorAll('[data-ds]').forEach(x=>x.classList.remove('is-active')); b.classList.add('is-active'); dataset=b.dataset.ds; genData(); rebuild();}));
  Lin.addEventListener('input',()=>{Lout.textContent=Lin.value; rebuild();});
  Win.addEventListener('input',()=>{Wout.textContent=Win.value; rebuild();});
  root.querySelector('[data-act="train"]').addEventListener('click',train);
  root.querySelector('[data-act="reset"]').addEventListener('click',rebuild);
  genData(); rebuild();
})();
</script>

<p>Try the <strong>Spiral</strong> dataset with <strong>0</strong> hidden layers — a single linear boundary can’t separate it, no matter how long you train. Now bump depth up to 3 and watch the boundary curl. Every additional layer is one extra fold the network can put into space. Width adds patience; depth adds expressiveness.</p>

<h2 id="skip-connections-the-highway-system">Skip connections, the highway system</h2>

<p>In modern networks (including all transformers), there’s a trick that changed everything: <strong>residual connections</strong>. Also called skip connections.</p>

<p>Instead of each layer <em>replacing</em> the previous layer’s output entirely, it <strong>adds</strong> to it. The output of layer N+1 = what layer N produced + what layer N+1 computed.</p>

<p>Sounds small. It’s enormous.</p>

<p>Means information can flow directly from early layers to late layers without passing through every layer in between. Early features don’t get “forgotten” or overwritten.</p>

<p>Also means each layer can focus on adding a <em>small correction</em>, rather than computing everything from scratch. Makes training much more stable and allows much deeper networks.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>The <strong>residual stream</strong> (the accumulated sum of all layers' outputs) is one of the core concepts in transformer interpretability. Remember this: in a modern network, information doesn't get overwritten layer by layer; it gets added to.</p>
</aside>

<h2 id="the-mi-connection">The MI connection</h2>

<p>Understanding what each layer does is one of the central projects of mechanistic interpretability. Not “layer 7 does something useful”. <em>Exactly</em> what. Which features live in which layers. Which operations happen where. When we know that, we can start to decompose a model’s behaviour the same way you’d decompose a program into functions.</p>

<p>Okay, one more thing worth naming. Every weight in every layer we’ve talked about was set by a single procedure: gradient descent. I’ll write about how that actually works in the next blog.</p>

<h2 id="research-referenced-in-this-post">Research referenced in this post</h2>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1311.2901" target="_blank" rel="noopener"><div class="research-card__title">Visualizing and Understanding Convolutional Networks</div><div class="research-card__authors">Zeiler, M. &amp; Fergus, R. · 2013 · the original CNN layer-visualisation paper</div></a></li>
  <li><a class="research-card" href="https://distill.pub/2017/feature-visualization/" target="_blank" rel="noopener"><div class="research-card__title">Feature Visualization</div><div class="research-card__authors">Olah, C. et al. · Distill, 2017 · beautiful interactive article</div></a></li>
  <li><a class="research-card" href="https://distill.pub/2020/circuits/zoom-in/" target="_blank" rel="noopener"><div class="research-card__title">Zoom In: An Introduction to Circuits</div><div class="research-card__authors">Olah, C. et al. · Distill, 2020 · the layer hierarchy in vision</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/1512.03385" target="_blank" rel="noopener"><div class="research-card__title">Deep Residual Learning for Image Recognition</div><div class="research-card__authors">He, K. et al. · 2015 · ResNet, the skip-connection paper</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/1905.05950" target="_blank" rel="noopener"><div class="research-card__title">BERT Rediscovers the Classical NLP Pipeline</div><div class="research-card__authors">Tenney, I. et al. · 2019 · syntax early, semantics late</div></a></li>
  <li><a class="research-card" href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens" target="_blank" rel="noopener"><div class="research-card__title">Interpreting GPT: the logit lens</div><div class="research-card__authors">Nostalgebraist · LessWrong, 2020 · layer-by-layer prediction evolution</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[Layer 1 sees edges. Layer 5 sees this specific person. Same pixels, different lens at every level. That's why deep learning is deep.]]></summary></entry><entry><title type="html">Weights &amp;amp; Connections: Where Knowledge Actually Lives</title><link href="https://bhavith-chandra.github.io/posts/weights-and-connections/" rel="alternate" type="text/html" title="Weights &amp;amp; Connections: Where Knowledge Actually Lives" /><published>2026-02-11T00:00:00-08:00</published><updated>2026-02-11T00:00:00-08:00</updated><id>https://bhavith-chandra.github.io/posts/weights-and-connections</id><content type="html" xml:base="https://bhavith-chandra.github.io/posts/weights-and-connections/"><![CDATA[<p>Quick puzzle to kick us off. Say someone told you: <em>“Hide a trillion facts inside a pile of numbers. Go.”</em> How would you do it?</p>

<p>Neural networks somehow figured this out. Every fact, every grammar rule, every pattern a model ever learned is in the weights. Not in any single weight. Not labelled. Just pressed into the collection, in a way nobody designed and nobody fully understands.</p>

<p>Weights are the most important thing in a neural network. They’re also the hardest to read.</p>

<p>So let’s learn to read them.</p>

<hr />

<h2 id="what-a-weight-actually-is">What a weight actually is</h2>

<p>A weight is one number. It lives on a connection between two neurons.</p>

<ul>
  <li><strong>Positive</strong> weight: “when the sending neuron is active, push the receiver to be more active too.”</li>
  <li><strong>Negative</strong> weight: “when the sender is active, push the receiver to be less active.”</li>
  <li><strong>Near-zero</strong> weight: “I don’t care what that neuron does.”</li>
</ul>

<p>One number, one relationship, one direction of influence.</p>

<p>Scale this up. A model with 70 billion parameters has 70 billion of these little relationships. All learned from data. All working together to spit out something coherent on the other end.</p>

<p>The staggering bit: nobody wrote a single one of them. Nobody sat down and decided <em>“the word ‘not’ should have a negative weight on the sentiment neuron.”</em> The model figured all of it out, by reading enough examples. That still feels slightly like magic to me, honestly.</p>

<div class="idemo" id="demo-weights">
  <div class="idemo__card">
    <div class="idemo__head"><span class="idemo__title">Interactive · The weight editor</span></div>
    <div class="idemo__body">

      <p class="we-lead">Here's a tiny network, two inputs, three hidden neurons, one output (positive / negative sentiment). <strong>Click any connection</strong> to change its weight and watch the prediction move.</p>

      <div class="we-presets">
        <span class="we-presets__label">Preset:</span>
        <button class="we-preset is-active" data-we-preset="trained">Trained (works)</button>
        <button class="we-preset" data-we-preset="random">Untrained</button>
        <button class="we-preset" data-we-preset="broken">Broken</button>
      </div>

      <div class="we-net">
        <svg class="we-svg" viewBox="0 0 520 260" aria-label="Network diagram">
          <g class="we-conns"></g>
          <g class="we-nodes"></g>
          <g class="we-labels"></g>
        </svg>

        <div class="we-popup" data-we-popup="" hidden="">
          <div class="we-popup__title">Weight <code data-we-popup-name="">w</code></div>
          <input type="range" min="-2" max="2" step="0.01" value="0" data-we-popup-range="" />
          <div class="we-popup__val"><span data-we-popup-val="">0.00</span></div>
          <button class="we-popup__close" data-we-popup-close="">done</button>
        </div>
      </div>

      <div class="we-inputs">
        <label class="we-input">
          <span class="we-input__name">word positivity <code>x₁</code></span>
          <input type="range" min="0" max="1" step="0.01" value="0.9" data-we-x="0" />
          <span data-we-xv="0">0.90</span>
        </label>
        <label class="we-input">
          <span class="we-input__name">exclamation <code>x₂</code></span>
          <input type="range" min="0" max="1" step="0.01" value="0.3" data-we-x="1" />
          <span data-we-xv="1">0.30</span>
        </label>
      </div>

      <div class="we-verify">
        <div class="we-verify__label">Test sentences</div>
        <ul class="we-verify__list" data-we-tests=""></ul>
      </div>

    </div>
    <details>
      <summary>How this demo works</summary>
      <p>Two layers: input (2) → hidden (3, ReLU) → output (1, sigmoid). Matrix math in plain JS. Line colour is cyan for positive weights, red for negative; thickness scales with magnitude. The five test sentences are hardcoded; the ✓/✗ indicator checks the model's prediction against the known label with each weight change, so you can watch the trained model break the moment you misalign a weight.</p>
    </details>
  </div>
</div>

<style>
  #demo-weights .we-lead { margin: 0 0 1rem !important; font-size: 0.98rem !important; color: var(--nn-body) !important; line-height: 1.6 !important; }

  #demo-weights .we-presets { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-bottom: 0.9rem; }
  #demo-weights .we-presets__label { font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--nn-muted); margin-right: 0.3rem; }
  #demo-weights .we-preset {
    padding: 0.4rem 0.75rem; background: #fff; border: 1px solid var(--nn-line); border-radius: 3px;
    font-family: var(--nn-mono); font-size: 0.78rem; color: var(--nn-muted); cursor: pointer;
  }
  #demo-weights .we-preset:hover { color: var(--nn-accent-dark); border-color: var(--nn-accent); }
  #demo-weights .we-preset.is-active { background: var(--nn-accent-soft); border-color: var(--nn-accent); color: var(--nn-accent-dark); }

  #demo-weights .we-net { position: relative; background: #fafafc; border: 1px solid var(--nn-line); border-radius: 6px; padding: 0.7rem; }
  #demo-weights .we-svg { width: 100%; height: auto; display: block; }
  #demo-weights .we-conn {
    stroke-linecap: round; cursor: pointer;
    transition: stroke-width 200ms, stroke-opacity 200ms;
  }
  #demo-weights .we-conn:hover { stroke-opacity: 1 !important; }
  #demo-weights .we-conn-hit { stroke: transparent; stroke-width: 14; cursor: pointer; }
  #demo-weights .we-node { fill: #fff; stroke: var(--nn-line); stroke-width: 1.8; transition: fill 260ms, stroke 260ms; }
  #demo-weights .we-node.is-output-pos { stroke: var(--nn-accent); fill: rgba(42, 111, 184, 0.2); }
  #demo-weights .we-node.is-output-neg { stroke: var(--nn-accent-warn); fill: rgba(192, 69, 80, 0.15); }
  #demo-weights .we-node-lbl { font-family: var(--nn-mono); font-size: 10px; fill: var(--nn-muted); pointer-events: none; }
  #demo-weights .we-node-out-lbl { font-family: var(--nn-serif); font-size: 13px; font-weight: 700; fill: var(--nn-ink); pointer-events: none; }
  #demo-weights .we-weight-lbl { font-family: var(--nn-mono); font-size: 9px; fill: var(--nn-muted); pointer-events: none; }

  #demo-weights .we-popup {
    position: absolute; background: #fff; border: 1px solid var(--nn-accent);
    border-radius: 5px; padding: 0.6rem 0.8rem; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
    z-index: 5; min-width: 180px;
  }
  #demo-weights .we-popup__title { font-family: var(--nn-mono); font-size: 0.74rem; color: var(--nn-muted); margin-bottom: 0.4rem; }
  #demo-weights .we-popup__title code { background: transparent; border: none; padding: 0; color: var(--nn-accent-dark); }
  #demo-weights .we-popup input[type=range] { width: 100%; -webkit-appearance: none; height: 4px; background: var(--nn-line); border-radius: 2px; outline: none; }
  #demo-weights .we-popup input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
    background: var(--nn-accent); border: 2px solid #fff; box-shadow: 0 0 0 1px var(--nn-accent); cursor: pointer;
  }
  #demo-weights .we-popup__val { margin-top: 0.3rem; font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-accent-dark); text-align: right; }
  #demo-weights .we-popup__close { margin-top: 0.35rem; padding: 0.25rem 0.6rem; font-family: var(--nn-mono); font-size: 0.7rem; background: var(--nn-accent); color: #fff; border: none; border-radius: 2px; cursor: pointer; width: 100%; }

  #demo-weights .we-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; margin-top: 0.9rem; }
  @media (max-width: 540px) { #demo-weights .we-inputs { grid-template-columns: 1fr; } }
  #demo-weights .we-input {
    display: grid; grid-template-columns: 1fr 1.4fr auto; gap: 0.6rem; align-items: center;
    padding: 0.5rem 0.75rem; background: #fff; border: 1px solid var(--nn-line); border-radius: 4px;
    font-family: var(--nn-mono); font-size: 0.82rem;
  }
  #demo-weights .we-input__name code { color: var(--nn-accent-dark); background: transparent; border: none; padding: 0; }
  #demo-weights .we-input input[type=range] { -webkit-appearance: none; height: 4px; background: var(--nn-line); border-radius: 2px; outline: none; }
  #demo-weights .we-input input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%;
    background: var(--nn-accent); border: 2px solid #fff; box-shadow: 0 0 0 1px var(--nn-accent); cursor: pointer;
  }

  #demo-weights .we-verify { margin-top: 0.9rem; padding: 0.8rem 1rem; background: #fff; border: 1px solid var(--nn-line); border-radius: 5px; }
  #demo-weights .we-verify__label { font-family: var(--nn-mono); font-size: 0.7rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--nn-muted); margin-bottom: 0.4rem; }
  #demo-weights .we-verify__list { list-style: none !important; margin: 0 !important; padding: 0 !important; }
  #demo-weights .we-verify__list li {
    display: grid; grid-template-columns: auto 1fr auto; gap: 0.6rem; align-items: center;
    padding: 0.3rem 0; border-bottom: 1px dashed var(--nn-line); margin: 0 !important;
    font-family: var(--nn-mono); font-size: 0.82rem; color: var(--nn-body);
    text-align: left !important;
  }
  #demo-weights .we-verify__list li:last-child { border-bottom: none; }
  #demo-weights .we-mark { width: 18px; text-align: center; font-weight: 700; }
  #demo-weights .we-mark.ok { color: var(--nn-accent); }
  #demo-weights .we-mark.bad { color: var(--nn-accent-warn); }
  #demo-weights .we-tag { font-size: 0.7rem; letter-spacing: 0.06em; padding: 0.1rem 0.4rem; border-radius: 2px; background: var(--nn-line); color: var(--nn-muted); }
  #demo-weights .we-tag--pos { background: var(--nn-accent-soft); color: var(--nn-accent-dark); }
  #demo-weights .we-tag--neg { background: rgba(192, 69, 80, 0.14); color: var(--nn-accent-warn); }
</style>

<script>
(function() {
  const root = document.getElementById("demo-weights");
  if (!root) return;

  // Architecture: 2 inputs → 3 hidden (ReLU) → 1 output (sigmoid)
  // Weights: W1 (3x2) = 6 weights from input→hidden
  //          W2 (1x3) = 3 weights from hidden→output
  // Plus biases: b1[3], b2[1]
  // For simplicity we expose w1_ij and w2_j and ignore biases visually (they're fixed).

  const PRESETS = {
    trained: {
      // Positive on positivity, small on exclamation, output leans cyan if sum is high
      w1: [[1.6, 0.2], [0.4, 1.3], [-1.2, -0.3]],
      w2: [1.4, 0.8, -1.1],
      b1: [0, 0, 0.2], b2: [0],
      desc: "Weights that correctly classify most examples. Look at the colours, most go cyan (positive) because positive inputs should boost 'positive'."
    },
    random: {
      w1: [[0.05, -0.04], [0.02, 0.08], [-0.06, 0.03]],
      w2: [0.02, -0.05, 0.04],
      b1: [0, 0, 0], b2: [0],
      desc: "Tiny random weights. Model has no opinion, always ~50%. This is what weights look like before training."
    },
    broken: {
      w1: [[-1.5, 0.2], [-1.2, 0.3], [-1.4, 0.1]],
      w2: [-1.6, -1.2, -1.4],
      b1: [0, 0, 0], b2: [2],
      desc: "Deliberately miswired. Positive inputs now push the model toward 'negative'. The test sentences all fail."
    }
  };

  const TESTS = [
    { name: "\"I loved it!\"",       x: [0.95, 0.9], label: "pos" },
    { name: "\"Pretty good.\"",      x: [0.75, 0.2], label: "pos" },
    { name: "\"Meh.\"",              x: [0.45, 0.1], label: "neg" },
    { name: "\"Awful, dull.\"",      x: [0.10, 0.1], label: "neg" },
    { name: "\"Mediocre but fine.\"",x: [0.55, 0.3], label: "pos" },
  ];

  const state = {
    w1: null, w2: null, b1: null, b2: null,
    x: [0.9, 0.3],
    editing: null, // { layer: 1|2, i, j }
  };

  function clonePreset(p) {
    return {
      w1: p.w1.map(r => r.slice()),
      w2: p.w2.slice(),
      b1: p.b1.slice(),
      b2: p.b2.slice(),
    };
  }
  function applyPreset(name) {
    const p = clonePreset(PRESETS[name]);
    state.w1 = p.w1; state.w2 = p.w2; state.b1 = p.b1; state.b2 = p.b2;
    root.querySelectorAll("[data-we-preset]").forEach(b => b.classList.toggle("is-active", b.dataset.wePreset === name));
    renderAll();
  }

  // Geometry
  const SVGNS = "http://www.w3.org/2000/svg";
  const svg = root.querySelector(".we-svg");
  const connsG = svg.querySelector(".we-conns");
  const nodesG = svg.querySelector(".we-nodes");
  const labelsG = svg.querySelector(".we-labels");

  const INPUT_X = 80, HIDDEN_X = 260, OUT_X = 440;
  const INPUT_YS = [90, 180];
  const HIDDEN_YS = [55, 130, 205];
  const OUT_Y = 130;

  // Draw nodes
  function drawCircle(cx, cy, r, cls, dataKey) {
    const c = document.createElementNS(SVGNS, "circle");
    c.setAttribute("cx", cx); c.setAttribute("cy", cy); c.setAttribute("r", r);
    c.setAttribute("class", "we-node " + (cls || ""));
    if (dataKey) c.setAttribute("data-we-node", dataKey);
    nodesG.appendChild(c);
    return c;
  }
  function drawText(x, y, txt, cls) {
    const t = document.createElementNS(SVGNS, "text");
    t.setAttribute("x", x); t.setAttribute("y", y); t.setAttribute("text-anchor", "middle");
    t.setAttribute("class", cls);
    t.textContent = txt;
    labelsG.appendChild(t);
    return t;
  }

  // Inputs
  INPUT_YS.forEach((y, i) => {
    drawCircle(INPUT_X, y, 18, "", "in" + i);
    drawText(INPUT_X, y + 4, "x" + (i + 1), "we-node-lbl");
  });
  // Hidden
  HIDDEN_YS.forEach((y, j) => {
    drawCircle(HIDDEN_X, y, 18, "", "h" + j);
    drawText(HIDDEN_X, y + 4, "h" + (j + 1), "we-node-lbl");
  });
  // Output
  const outNode = drawCircle(OUT_X, OUT_Y, 24, "", "out");
  const outLbl = drawText(OUT_X, OUT_Y + 4, ",", "we-node-out-lbl");

  // Layer labels
  drawText(INPUT_X, 245, "inputs", "we-node-lbl");
  drawText(HIDDEN_X, 245, "hidden (ReLU)", "we-node-lbl");
  drawText(OUT_X, 245, "output (σ)", "we-node-lbl");

  // Build connections: input → hidden
  const connElems = [];
  HIDDEN_YS.forEach((hy, j) => {
    INPUT_YS.forEach((iy, i) => {
      // Visible line
      const line = document.createElementNS(SVGNS, "line");
      line.setAttribute("x1", INPUT_X + 18); line.setAttribute("y1", iy);
      line.setAttribute("x2", HIDDEN_X - 18); line.setAttribute("y2", hy);
      line.setAttribute("class", "we-conn");
      connsG.appendChild(line);
      // Hit area (thicker invisible)
      const hit = document.createElementNS(SVGNS, "line");
      hit.setAttribute("x1", INPUT_X + 18); hit.setAttribute("y1", iy);
      hit.setAttribute("x2", HIDDEN_X - 18); hit.setAttribute("y2", hy);
      hit.setAttribute("class", "we-conn-hit");
      hit.addEventListener("click", () => openPopup(1, j, i));
      connsG.appendChild(hit);
      // Weight label
      const mx = (INPUT_X + HIDDEN_X) / 2;
      const my = (iy + hy) / 2 - 4;
      const lbl = document.createElementNS(SVGNS, "text");
      lbl.setAttribute("x", mx); lbl.setAttribute("y", my); lbl.setAttribute("text-anchor", "middle");
      lbl.setAttribute("class", "we-weight-lbl");
      labelsG.appendChild(lbl);

      connElems.push({ layer: 1, j, i, line, hit, lbl });
    });
  });
  // hidden → output
  HIDDEN_YS.forEach((hy, j) => {
    const line = document.createElementNS(SVGNS, "line");
    line.setAttribute("x1", HIDDEN_X + 18); line.setAttribute("y1", hy);
    line.setAttribute("x2", OUT_X - 24); line.setAttribute("y2", OUT_Y);
    line.setAttribute("class", "we-conn");
    connsG.appendChild(line);
    const hit = document.createElementNS(SVGNS, "line");
    hit.setAttribute("x1", HIDDEN_X + 18); hit.setAttribute("y1", hy);
    hit.setAttribute("x2", OUT_X - 24); hit.setAttribute("y2", OUT_Y);
    hit.setAttribute("class", "we-conn-hit");
    hit.addEventListener("click", () => openPopup(2, 0, j));
    connsG.appendChild(hit);
    const mx = (HIDDEN_X + OUT_X) / 2;
    const my = (hy + OUT_Y) / 2 - 4;
    const lbl = document.createElementNS(SVGNS, "text");
    lbl.setAttribute("x", mx); lbl.setAttribute("y", my); lbl.setAttribute("text-anchor", "middle");
    lbl.setAttribute("class", "we-weight-lbl");
    labelsG.appendChild(lbl);

    connElems.push({ layer: 2, j: 0, i: j, line, hit, lbl });
  });

  function relu(x) { return Math.max(0, x); }
  function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }

  function forward(xIn, w1, w2, b1, b2) {
    // hidden_j = relu( sum_i w1[j][i]*x[i] + b1[j] )
    const h = [0, 1, 2].map(j => relu(xIn[0] * w1[j][0] + xIn[1] * w1[j][1] + b1[j]));
    const o = sigmoid(h[0] * w2[0] + h[1] * w2[1] + h[2] * w2[2] + b2[0]);
    return { h, o };
  }

  function renderConns() {
    connElems.forEach(e => {
      const w = e.layer === 1 ? state.w1[e.j][e.i] : state.w2[e.i];
      const thick = 0.6 + Math.min(2, Math.abs(w)) * 2.5;
      const color = w >= 0 ? "#2a6fb8" : "#c04550";
      e.line.setAttribute("stroke", color);
      e.line.setAttribute("stroke-width", thick.toFixed(2));
      e.line.setAttribute("stroke-opacity", (0.25 + Math.min(1, Math.abs(w)) * 0.55).toFixed(2));
      e.lbl.textContent = (w >= 0 ? "+" : "") + w.toFixed(2);
    });
  }

  function renderOutput() {
    const { o } = forward(state.x, state.w1, state.w2, state.b1, state.b2);
    outLbl.textContent = Math.round(o * 100) + "%";
    outNode.classList.toggle("is-output-pos", o >= 0.5);
    outNode.classList.toggle("is-output-neg", o < 0.5);
  }

  function renderInputs() {
    [0, 1].forEach(i => {
      const n = svg.querySelector('[data-we-node="in' + i + '"]');
      n.setAttribute("fill", "rgba(42, 111, 184, " + (0.12 + state.x[i] * 0.7).toFixed(2) + ")");
    });
    // Hidden activations
    const { h } = forward(state.x, state.w1, state.w2, state.b1, state.b2);
    [0, 1, 2].forEach(j => {
      const n = svg.querySelector('[data-we-node="h' + j + '"]');
      const a = Math.min(1, h[j] / 2);
      n.setAttribute("fill", h[j] > 0.01 ? "rgba(42, 111, 184, " + (0.12 + a * 0.7).toFixed(2) + ")" : "#fff");
    });
  }

  function renderTests() {
    const ul = root.querySelector("[data-we-tests]");
    ul.innerHTML = "";
    TESTS.forEach(t => {
      const { o } = forward(t.x, state.w1, state.w2, state.b1, state.b2);
      const predicted = o >= 0.5 ? "pos" : "neg";
      const ok = predicted === t.label;
      const li = document.createElement("li");
      li.innerHTML =
        '<span class="we-mark ' + (ok ? "ok" : "bad") + '">' + (ok ? "✓" : "✗") + '</span>' +
        '<span>' + t.name + '</span>' +
        '<span class="we-tag we-tag--' + predicted + '">' + predicted + ' ' + Math.round(o * 100) + '%</span>';
      ul.appendChild(li);
    });
  }

  function renderAll() {
    [0, 1].forEach(i => root.querySelector('[data-we-xv="' + i + '"]').textContent = state.x[i].toFixed(2));
    renderConns();
    renderInputs();
    renderOutput();
    renderTests();
  }

  // Popup editing
  function openPopup(layer, j, i) {
    state.editing = { layer, j, i };
    const w = layer === 1 ? state.w1[j][i] : state.w2[i];
    const popup = root.querySelector("[data-we-popup]");
    popup.hidden = false;
    const elem = layer === 1
      ? connElems.find(e => e.layer === 1 && e.j === j && e.i === i)
      : connElems.find(e => e.layer === 2 && e.i === i);
    const rect = elem.line.getBoundingClientRect();
    const container = root.querySelector(".we-net").getBoundingClientRect();
    popup.style.left = (rect.left + rect.width / 2 - container.left - 95) + "px";
    popup.style.top  = (rect.top + rect.height / 2 - container.top + 8) + "px";
    popup.querySelector("[data-we-popup-name]").textContent = layer === 1 ? ("w1[" + (j + 1) + "]" + "[" + (i + 1) + "]") : ("w2[" + (i + 1) + "]");
    const range = popup.querySelector("[data-we-popup-range]");
    range.value = w;
    popup.querySelector("[data-we-popup-val]").textContent = (w >= 0 ? "+" : "") + w.toFixed(2);
    range.oninput = () => {
      const v = +range.value;
      if (state.editing.layer === 1) state.w1[state.editing.j][state.editing.i] = v;
      else state.w2[state.editing.i] = v;
      popup.querySelector("[data-we-popup-val]").textContent = (v >= 0 ? "+" : "") + v.toFixed(2);
      renderAll();
    };
  }
  root.querySelector("[data-we-popup-close]").addEventListener("click", () => {
    root.querySelector("[data-we-popup]").hidden = true;
    state.editing = null;
  });

  // Input sliders
  root.querySelectorAll("[data-we-x]").forEach(el => el.addEventListener("input", e => {
    state.x[+el.dataset.weX] = +e.target.value;
    renderAll();
  }));

  // Preset buttons
  root.querySelectorAll("[data-we-preset]").forEach(btn => btn.addEventListener("click", () => applyPreset(btn.dataset.wePreset)));

  applyPreset("trained");
})();
</script>

<p>Play with the demo. Flip the weight from <em>word positivity</em> to negative and watch the model’s prediction invert. Positive reviews now get classified as negative. Try the <strong>Broken</strong> preset, then <strong>Trained</strong>. Nothing “inside” the model changed except a handful of numbers. That’s all weights are.</p>

<h2 id="the-weight-matrix">The weight matrix</h2>

<p>When every neuron in one layer connects to every neuron in the next, you get a <strong>weight matrix</strong>.</p>

<p>Layer A has 4 neurons. Layer B has 3 neurons. You have a 4×3 grid of weights. 12 numbers, each the strength of one connection.</p>

<p>To calculate layer B’s activations, you multiply: <code class="language-plaintext highlighter-rouge">B = W · A</code>. Matrix multiplication.</p>

<p>This is the fundamental operation of a neural network. Everything (attention, MLP layers, embeddings) is built from variations of this.</p>

<p>For interpretability, the weight matrix is where we look for structure:</p>

<ul>
  <li>Are there patterns in which neurons have high weights to each other?</li>
  <li>Are there clusters all strongly positive or negative with each other?</li>
  <li>Can we factor the weight matrix into simpler components that mean something?</li>
</ul>

<p>That last one, <strong>matrix factorisation</strong>, is a major MI technique. If <code class="language-plaintext highlighter-rouge">W</code> decomposes into <code class="language-plaintext highlighter-rouge">A × B</code>, then <code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">B</code> might represent something interpretable.</p>

<h2 id="what-trained-weights-look-like">What trained weights look like</h2>

<p>Random weights, before training: all small, roughly centred on zero. The network produces noise.</p>

<p>After training on language: the weights organise into structure. Not structure we designed. Structure that reflects the regularities in language.</p>

<div class="demo demo-init" id="demo-init">
  <div class="init__head">
    <div class="init__title">Weight initialization lab</div>
    <div class="init__sub">Run a fresh 6-layer net with the chosen scheme. See whether activations stay alive — or die — as signal travels through depth.</div>
  </div>
  <div class="init__tabs" data-init-tabs="">
    <button class="init__tab is-active" data-scheme="zeros">Zeros</button>
    <button class="init__tab" data-scheme="uniform">Uniform [-1,1]</button>
    <button class="init__tab" data-scheme="small">Tiny normal (0.01)</button>
    <button class="init__tab" data-scheme="xavier">Xavier</button>
    <button class="init__tab" data-scheme="he">He (Kaiming)</button>
    <button class="init__tab" data-scheme="big">Wild (std=3)</button>
  </div>
  <div class="init__row">
    <div class="init__panel">
      <div class="init__plabel">Activation magnitude per layer (log scale)</div>
      <canvas id="init-act" width="400" height="220"></canvas>
    </div>
    <div class="init__panel">
      <div class="init__plabel">Weight histogram (layer 3)</div>
      <canvas id="init-hist" width="400" height="220"></canvas>
    </div>
  </div>
  <div class="init__diag" id="init-diag">—</div>
  <div class="init__actions">
    <button data-init-act="reseed">Reseed (new random sample)</button>
  </div>
</div>
<style>
  .demo-init{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-init .init__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-init .init__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-init .init__tabs{display:flex;gap:6px;flex-wrap:wrap;margin:14px 0 10px}
  .demo-init .init__tab{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-init .init__tab.is-active{background:#fbbf24;color:#3a2106;border-color:#b77214}
  .demo-init .init__row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .demo-init .init__panel{background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;padding:10px}
  .demo-init .init__plabel{font-size:11px;color:#7c4d0a;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:8px;font-weight:700}
  .demo-init canvas{width:100%;height:auto;display:block}
  .demo-init .init__diag{margin-top:10px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px;line-height:1.55}
  .demo-init .init__actions{margin-top:8px}
  .demo-init .init__actions button{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-init .init__actions button:hover{background:#fbbf24;color:#3a2106}
  @media (max-width:560px){.demo-init .init__row{grid-template-columns:1fr}}
</style>

<script>
(function(){
  const root=document.getElementById('demo-init'); if(!root) return;
  const tabs=root.querySelectorAll('[data-scheme]');
  const actC=root.querySelector('#init-act'), aCtx=actC.getContext('2d');
  const histC=root.querySelector('#init-hist'), hCtx=histC.getContext('2d');
  const diag=root.querySelector('#init-diag');
  const N=64, L=6; let scheme='zeros';
  function gauss(){let u=0,v=0; while(!u)u=Math.random(); while(!v)v=Math.random(); return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);}
  function makeW(s){
    const fanIn=N;
    const sample=()=>{ if(s==='zeros')return 0; if(s==='uniform')return (Math.random()*2-1); if(s==='small')return gauss()*0.01; if(s==='xavier')return gauss()*Math.sqrt(1/fanIn); if(s==='he')return gauss()*Math.sqrt(2/fanIn); if(s==='big')return gauss()*3;};
    const W=[]; for(let l=0;l<L;l++){const m=Array.from({length:N},()=>Array.from({length:N},sample)); W.push(m);} return W;
  }
  function relu(x){return Math.max(0,x);}
  function forward(W){
    let x=Array.from({length:N},()=>gauss());
    const norms=[]; const xs=[x.slice()]; norms.push(rms(x));
    for(let l=0;l<L;l++){
      const m=W[l]; const nx=new Array(N);
      for(let i=0;i<N;i++){let s=0; for(let j=0;j<N;j++) s+=m[i][j]*x[j]; nx[i]=relu(s);}
      x=nx; xs.push(x.slice()); norms.push(rms(x));
    }
    return {norms, xs, W};
  }
  function rms(v){let s=0; for(const x of v)s+=x*x; return Math.sqrt(s/v.length);}
  function drawAct(norms){
    const W=actC.width,H=actC.height; aCtx.clearRect(0,0,W,H);
    const pad={l:48,r:14,t:14,b:24}; const gw=W-pad.l-pad.r, gh=H-pad.t-pad.b;
    const yMin=-3, yMax=3;
    aCtx.strokeStyle='#f0e3cc'; aCtx.lineWidth=1;
    for(let v=yMin;v<=yMax;v++){const Y=pad.t+(yMax-v)/(yMax-yMin)*gh; aCtx.beginPath();aCtx.moveTo(pad.l,Y);aCtx.lineTo(W-pad.r,Y);aCtx.stroke(); aCtx.fillStyle='#7c4d0a'; aCtx.font='10px ui-monospace,Menlo,monospace'; aCtx.fillText('10^'+v,8,Y+3);}
    aCtx.strokeStyle='#fbbf24'; aCtx.lineWidth=2.2; aCtx.beginPath();
    norms.forEach((n,i)=>{const X=pad.l+(i/(norms.length-1))*gw; const ly=Math.log10(Math.max(1e-30,n)); const Y=pad.t+(yMax-ly)/(yMax-yMin)*gh; if(i===0)aCtx.moveTo(X,Y); else aCtx.lineTo(X,Y);});
    aCtx.stroke();
    norms.forEach((n,i)=>{const X=pad.l+(i/(norms.length-1))*gw; const ly=Math.log10(Math.max(1e-30,n)); const Y=pad.t+(yMax-ly)/(yMax-yMin)*gh; aCtx.fillStyle='#b77214'; aCtx.beginPath(); aCtx.arc(X,Y,3.4,0,Math.PI*2); aCtx.fill();});
    aCtx.fillStyle='#7c4d0a'; aCtx.font='10px ui-monospace,Menlo,monospace';
    norms.forEach((n,i)=>{const X=pad.l+(i/(norms.length-1))*gw; aCtx.fillText('L'+i,X-6,H-8);});
  }
  function drawHist(W){
    const w=W[2]; const flat=[]; for(const r of w) for(const v of r) flat.push(v);
    const Wp=histC.width,Hp=histC.height; hCtx.clearRect(0,0,Wp,Hp);
    const pad={l:14,r:14,t:14,b:24}; const gw=Wp-pad.l-pad.r, gh=Hp-pad.t-pad.b;
    let mx=0; for(const v of flat) if(Math.abs(v)>mx) mx=Math.abs(v); mx=Math.max(mx,0.001);
    const bins=24; const cnt=new Array(bins).fill(0);
    for(const v of flat){const b=Math.min(bins-1,Math.max(0,Math.floor((v+mx)/(2*mx)*bins))); cnt[b]++;}
    const cmx=Math.max(...cnt,1);
    cnt.forEach((c,i)=>{const bw=gw/bins; const X=pad.l+i*bw; const h=(c/cmx)*gh; hCtx.fillStyle='#fbbf24'; hCtx.fillRect(X+1,Hp-pad.b-h,bw-2,h);});
    hCtx.strokeStyle='#7c4d0a'; hCtx.lineWidth=1; hCtx.beginPath(); hCtx.moveTo(pad.l,Hp-pad.b); hCtx.lineTo(Wp-pad.r,Hp-pad.b); hCtx.stroke();
    hCtx.fillStyle='#7c4d0a'; hCtx.font='10px ui-monospace,Menlo,monospace';
    hCtx.fillText('-'+mx.toFixed(2),pad.l,Hp-8); hCtx.fillText(mx.toFixed(2),Wp-pad.r-26,Hp-8); hCtx.fillText('0',Wp/2-3,Hp-8);
  }
  function diagnose(s,norms){
    const last=norms[norms.length-1]; const first=norms[0]; const ratio=last/Math.max(1e-30,first);
    const verdict={
      zeros:'Every neuron computes the same thing — symmetry never breaks. Training would do nothing.',
      uniform:'Variance grows / shrinks unpredictably. Some layers saturate, others vanish.',
      small:'Signal dies fast. By the last layer, activations are near zero — gradients vanish too.',
      xavier:'Signal preserved across layers (designed for tanh / sigmoid).',
      he:'Best for ReLU networks: variance stays in a healthy range.',
      big:'Activations explode. Loss = NaN within a few steps.'
    }[s];
    diag.innerHTML=`<b>${s.toUpperCase()}</b> · activation RMS L0 → L${L} : ${first.toExponential(2)} → ${last.toExponential(2)} (ratio ${ratio.toExponential(2)}) <br>${verdict}`;
  }
  function run(){const W=makeW(scheme); const r=forward(W); drawAct(r.norms); drawHist(W); diagnose(scheme,r.norms);}
  tabs.forEach(t=>t.addEventListener('click',()=>{tabs.forEach(x=>x.classList.remove('is-active')); t.classList.add('is-active'); scheme=t.dataset.scheme; run();}));
  root.querySelector('[data-init-act="reseed"]').addEventListener('click',run);
  run();
})();
</script>

<p>The “small and centred on zero” part isn’t aesthetic — it’s load-bearing. Pick the wrong starting distribution and the signal either dies before it reaches the output or explodes before the first gradient step. He and Xavier initialization aren’t tricks; they’re the only reason deep networks train at all.</p>

<h3 id="word-embeddings">Word embeddings</h3>

<p>Words get encoded as high-dimensional vectors. The weights arrange these vectors so that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"king"  − "man"    + "woman"  ≈  "queen"
"Paris" − "France" + "Italy"  ≈  "Rome"
</code></pre></div></div>

<p>Not programmed. Emerges from the weights learning which words appear in similar contexts.</p>

<h3 id="attention-weight-patterns">Attention weight patterns</h3>

<p>In transformers, attention weights form patterns like:</p>

<ul>
  <li>Heads that always attend to the previous token</li>
  <li>Heads that look for subject-verb agreement</li>
  <li>Heads that copy information from far back in the sequence</li>
</ul>

<p>These patterns live in the weight matrices. Finding them is a big chunk of mechanistic interpretability.</p>

<h2 id="why-weights-are-hard-to-read-directly">Why weights are hard to read directly</h2>

<p>Here’s the annoying part. Print out a weight matrix, you see a grid of numbers like <code class="language-plaintext highlighter-rouge">0.023, −0.41, 0.0017, 1.3, −0.88</code>… and?</p>

<p>It tells you almost nothing. The numbers only mean something in combination. One weight doesn’t represent a concept. The whole matrix does.</p>

<aside class="callout callout--key">
  <div class="callout__label">Why this matters for MI</div>
  <p>You can't read weights the same way you read code. You need other tools: activation analysis, weight visualisation, singular-value decomposition, probing. The weight matrix is the <em>storage</em>. The activity of neurons running through it is the <em>readout</em>. MI needs both.</p>
</aside>

<h2 id="weights-vs-activations">Weights vs activations</h2>

<p>This one trips people up.</p>

<p><strong>Weights</strong> are fixed after training. Don’t change when you give the model a new input. They’re the <em>structure</em>. The compiled knowledge of everything the model learned.</p>

<p><strong>Activations</strong> are dynamic. Computed fresh for every input. The model’s <em>current state of processing</em> your specific prompt.</p>

<aside class="callout callout--analogy">
  <div class="callout__label">Analogy</div>
  <p>Weights are the circuitry of a calculator. Activations are the numbers currently on the screen. The circuits are fixed; the computations change.</p>
</aside>

<p>For MI: most research looks at activations (what’s the model thinking about <em>this</em> input?) but relates them back to weights (what in the structure caused this pattern?). Both matter.</p>

<h2 id="gradient-descent-made-the-weights">Gradient descent made the weights</h2>

<p>One sentence on how they got this way: during training, the model sees an example, makes a prediction, measures how wrong it was, and nudges every weight slightly in the direction that would’ve made it less wrong.</p>

<p>Do that billions of times, across trillions of words. Yes, literally trillions. Yes, it feels absurd. It also works.</p>

<div class="demo demo-backprop" id="demo-backprop">
  <div class="bp__head">
    <div class="bp__title">Backprop, frame by frame</div>
    <div class="bp__sub">Forward sends activations left → right. Backward sends gradients right → left, multiplying along the way. Press the buttons and watch the chain rule.</div>
  </div>
  <div class="bp__controls">
    <button data-bp="fwd">▶ Forward</button>
    <button data-bp="bwd">◀ Backward</button>
    <button data-bp="step">Step weights</button>
    <button data-bp="reset">Reset</button>
    <span class="bp__loss">loss = <b id="bp-loss">—</b> · target y* = <b>1.0</b></span>
  </div>
  <div class="bp__stage">
    <canvas id="bp-canvas" width="720" height="320"></canvas>
  </div>
  <div class="bp__readout" id="bp-readout">Press <b>Forward</b> to push the inputs through the network.</div>
</div>
<style>
  .demo-backprop{border:1px solid var(--nn-line,#e7e2da);border-radius:14px;padding:18px;margin:18px 0;background:#fffaf3;font-family:var(--nn-body,system-ui)}
  .demo-backprop .bp__title{font-weight:700;color:#7c4d0a;font-size:15px}
  .demo-backprop .bp__sub{font-size:13px;color:var(--nn-muted,#7a6a52);margin-top:3px}
  .demo-backprop .bp__controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0;align-items:center}
  .demo-backprop .bp__controls button{border:1px solid #ddb88e;background:#fff6e0;color:#7c4d0a;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
  .demo-backprop .bp__controls button:hover{background:#fbbf24;color:#3a2106}
  .demo-backprop .bp__loss{margin-left:auto;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12}
  .demo-backprop canvas{width:100%;height:auto;background:#fffefb;border:1px solid #ecdbc0;border-radius:8px;display:block}
  .demo-backprop .bp__readout{margin-top:10px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#5a3d12;background:#fff6e0;border:1px dashed #ddb88e;padding:8px 12px;border-radius:8px;min-height:18px}
</style>

<script>
(function(){
  const root=document.getElementById('demo-backprop'); if(!root) return;
  const cvs=root.querySelector('#bp-canvas'); const ctx=cvs.getContext('2d');
  const lossOut=root.querySelector('#bp-loss'); const readout=root.querySelector('#bp-readout');
  const tgt=1.0;
  let inputs=[0.6,-0.4,0.8];
  let W1=[[0.5,-0.3,0.2],[0.4,0.1,-0.5]];
  let W2=[0.7,-0.6];
  let h=[0,0], y=0, loss=0;
  let dy=0, dW2=[0,0], dh=[0,0], dz=[0,0], dW1=[[0,0,0],[0,0,0]];
  let phase='idle', tAnim=0, raf=null;
  function relu(x){return Math.max(0,x);} function dReLU(x){return x>0?1:0;}
  function fwd(){
    const z=[0,0]; for(let i=0;i<2;i++){let s=0; for(let j=0;j<3;j++)s+=W1[i][j]*inputs[j]; z[i]=s; h[i]=relu(s);}
    y=W2[0]*h[0]+W2[1]*h[1]; loss=0.5*(y-tgt)*(y-tgt);
    return z;
  }
  function bwd(){
    const z=fwd(); dy=y-tgt;
    dW2=[dy*h[0],dy*h[1]];
    dh=[dy*W2[0],dy*W2[1]];
    dz=[dh[0]*dReLU(z[0]),dh[1]*dReLU(z[1])];
    dW1=[[dz[0]*inputs[0],dz[0]*inputs[1],dz[0]*inputs[2]],[dz[1]*inputs[0],dz[1]*inputs[1],dz[1]*inputs[2]]];
  }
  function step(){bwd(); const lr=0.2; for(let i=0;i<2;i++){W2[i]-=lr*dW2[i]; for(let j=0;j<3;j++)W1[i][j]-=lr*dW1[i][j];}}
  function reset(){W1=[[0.5,-0.3,0.2],[0.4,0.1,-0.5]]; W2=[0.7,-0.6]; phase='idle'; tAnim=0; draw(); readout.textContent='Reset to initial weights.';}
  function nodes(){
    return {
      i:[{x:90,y:80,t:'x₁='+inputs[0].toFixed(2)},{x:90,y:160,t:'x₂='+inputs[1].toFixed(2)},{x:90,y:240,t:'x₃='+inputs[2].toFixed(2)}],
      h:[{x:340,y:120,t:'h₁='+h[0].toFixed(2)},{x:340,y:200,t:'h₂='+h[1].toFixed(2)}],
      o:[{x:600,y:160,t:'y='+y.toFixed(2)}]
    };
  }
  function draw(){
    fwd();
    const W=cvs.width,H=cvs.height; ctx.clearRect(0,0,W,H);
    const nd=nodes();
    function edge(a,b,w,grad,idx){
      const a0=a.x+24,a1=a.y, b0=b.x-24,b1=b.y;
      const isFwd=phase==='fwd', isBwd=phase==='bwd';
      let prog=tAnim;
      ctx.strokeStyle='#ddb88e'; ctx.lineWidth=1+Math.min(3,Math.abs(w)*2.4);
      ctx.beginPath(); ctx.moveTo(a0,a1); ctx.lineTo(b0,b1); ctx.stroke();
      if(isFwd){ctx.strokeStyle='#fbbf24'; ctx.lineWidth=3.2; ctx.beginPath(); ctx.moveTo(a0,a1); ctx.lineTo(a0+(b0-a0)*prog,a1+(b1-a1)*prog); ctx.stroke();
        const px=a0+(b0-a0)*prog,py=a1+(b1-a1)*prog; ctx.fillStyle='#fbbf24'; ctx.beginPath(); ctx.arc(px,py,5,0,Math.PI*2); ctx.fill();
      }
      if(isBwd){ctx.strokeStyle='#b25c2c'; ctx.lineWidth=3.2; ctx.beginPath(); ctx.moveTo(b0,b1); ctx.lineTo(b0+(a0-b0)*prog,b1+(a1-b1)*prog); ctx.stroke();
        const px=b0+(a0-b0)*prog,py=b1+(a1-b1)*prog; ctx.fillStyle='#b25c2c'; ctx.beginPath(); ctx.arc(px,py,5,0,Math.PI*2); ctx.fill();
      }
      ctx.fillStyle='#5a3d12'; ctx.font='11px ui-monospace,Menlo,monospace';
      const mx=(a0+b0)/2, my=(a1+b1)/2; ctx.fillText('w='+w.toFixed(2),mx-22,my-6);
      if(grad!==null && (isBwd||phase==='done')){ctx.fillStyle='#b25c2c'; ctx.fillText('∂='+grad.toFixed(2),mx-22,my+10);}
    }
    for(let i=0;i<3;i++) for(let k=0;k<2;k++) edge(nd.i[i],nd.h[k],W1[k][i],dW1[k][i]);
    for(let k=0;k<2;k++) edge(nd.h[k],nd.o[0],W2[k],dW2[k]);
    function nodeC(p,fill){ctx.fillStyle=fill; ctx.strokeStyle='#7c4d0a'; ctx.lineWidth=1.4; ctx.beginPath(); ctx.arc(p.x,p.y,24,0,Math.PI*2); ctx.fill(); ctx.stroke();
      ctx.fillStyle='#3a2106'; ctx.font='600 11px ui-monospace,Menlo,monospace'; ctx.textAlign='center'; ctx.fillText(p.t,p.x,p.y+4); ctx.textAlign='start';}
    nd.i.forEach(p=>nodeC(p,'#fff6e0')); nd.h.forEach(p=>nodeC(p,'#fde9bf')); nd.o.forEach(p=>nodeC(p,phase==='bwd'||phase==='done'?'#fbcaa1':'#fbbf24'));
    ctx.fillStyle='#7c4d0a'; ctx.font='600 12px ui-monospace,Menlo,monospace';
    ctx.fillText('inputs',75,40); ctx.fillText('hidden (ReLU)',290,40); ctx.fillText('output',580,40);
    if(phase==='bwd'||phase==='done'){ctx.fillStyle='#b25c2c'; ctx.fillText('∂L/∂y='+dy.toFixed(2),540,295);}
    lossOut.textContent=loss.toFixed(3);
  }
  function animate(p){
    cancelAnimationFrame(raf); phase=p; tAnim=0;
    const start=performance.now(); const dur=900;
    function tick(now){tAnim=Math.min(1,(now-start)/dur); draw(); if(tAnim<1) raf=requestAnimationFrame(tick); else {phase='done'; draw();}}
    raf=requestAnimationFrame(tick);
  }
  root.querySelectorAll('[data-bp]').forEach(b=>b.addEventListener('click',()=>{
    const a=b.dataset.bp;
    if(a==='fwd'){animate('fwd'); readout.innerHTML='Forward: x → h = ReLU(W₁x) → y = W₂h. Loss = ½(y − y*)².';}
    if(a==='bwd'){bwd(); animate('bwd'); readout.innerHTML='Backward: ∂L/∂y → ∂L/∂W₂ → ∂L/∂h → ∂L/∂z (kill where ReLU was off) → ∂L/∂W₁. Each edge multiplies along the path.';}
    if(a==='step'){step(); draw(); readout.innerHTML='One gradient-descent step: W ← W − η·∂L/∂W with η=0.2. Loss = <b>'+loss.toFixed(3)+'</b>.';}
    if(a==='reset')reset();
  }));
  draw();
})();
</script>

<p>That’s gradient descent in miniature. Every weight in a 70B-parameter model gets updated by the exact same logic: forward to compute loss, backward to compute who’s responsible, then nudge each weight downhill. The “trillions of nudges” is just this loop, looped a <em>lot</em>.</p>

<p>The weights that emerge encode the statistical regularities of everything the model was trained on. Grammar. Facts. Logic. Poetry. Chemistry. Slang. All of it. Compressed into numbers.</p>

<p>For now, “it’s gradient descent” is enough.</p>

<h2 id="the-mi-connection">The MI connection</h2>

<p>When an MI researcher asks <em>“what did this model learn to do?”</em>, they’re asking <em>“what do these weights mean?”</em> Finding the answer requires figuring out which directions in weight space correspond to human-interpretable concepts. Which is the project of the whole field.</p>

<p>I’ll write about layers in the next blog. Many weight matrices stacked on top of each other, each doing something different to the flow of information.</p>

<h2 id="research-referenced-in-this-post">Research referenced in this post</h2>

<ul class="research-list">
  <li><a class="research-card" href="https://arxiv.org/abs/1301.3781" target="_blank" rel="noopener"><div class="research-card__title">Efficient Estimation of Word Representations in Vector Space</div><div class="research-card__authors">Mikolov, T. et al. · 2013 · Word2Vec</div></a></li>
  <li><a class="research-card" href="https://transformer-circuits.pub/2021/framework/index.html" target="_blank" rel="noopener"><div class="research-card__title">A Mathematical Framework for Transformer Circuits</div><div class="research-card__authors">Elhage, N. et al. · Anthropic, 2021 · sections on weight matrices</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2209.02535" target="_blank" rel="noopener"><div class="research-card__title">Analyzing Transformers in Embedding Space</div><div class="research-card__authors">Dar, G. et al. · 2022 · reading weight matrices directly</div></a></li>
  <li><a class="research-card" href="https://arxiv.org/abs/2012.14913" target="_blank" rel="noopener"><div class="research-card__title">Transformer Feed-Forward Layers Are Key-Value Memories</div><div class="research-card__authors">Geva, M. et al. · 2021 · MLP weights encode factual associations</div></a></li>
  <li><a class="research-card" href="https://colah.github.io/posts/2014-03-NN-Manifolds-Topology/" target="_blank" rel="noopener"><div class="research-card__title">Neural Networks, Manifolds, and Topology</div><div class="research-card__authors">Olah, C. · 2014 · visual intuition for weight-matrix geometry</div></a></li>
</ul>]]></content><author><name>Bhavith Chandra</name></author><summary type="html"><![CDATA[Every fact, every grammar rule, every pattern a model ever learned is compressed into a pile of numbers. Nobody designed a single one of them.]]></summary></entry></feed>