<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: gulding</title>
    <description>The latest articles on DEV Community by gulding (@gulding).</description>
    <link>https://dev.to/gulding</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107642%2F8adf3298-a4ed-4202-bd9e-b322b492e587.png</url>
      <title>DEV Community: gulding</title>
      <link>https://dev.to/gulding</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gulding"/>
    <language>en</language>
    <item>
      <title>Building a 200M parameter LLM from scratch in PyTorch</title>
      <dc:creator>gulding</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:20:30 +0000</pubDate>
      <link>https://dev.to/gulding/building-a-200m-parameter-llm-from-scratch-in-pytorch-1c9k</link>
      <guid>https://dev.to/gulding/building-a-200m-parameter-llm-from-scratch-in-pytorch-1c9k</guid>
      <description>&lt;p&gt;It's really easy to spin up Unsloth and fine-tune Llama 3 in an afternoon. I wanted to see what happens when you don't do that. &lt;/p&gt;

&lt;p&gt;I wanted to write the BPE tokenizer, implement RoPE, code the SwiGLU feed-forward blocks, and write the training loop in pure PyTorch from absolute scratch. &lt;/p&gt;

&lt;p&gt;The result is EmsyAI (V4), a 196M parameter model I trained on a consumer GPU. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Active Parameters: 196.7M (roughly 180M in the transformer blocks, the rest in the ~16k vocab embeddings)&lt;/li&gt;
&lt;li&gt;Training Tokens: 1.96 Billion&lt;/li&gt;
&lt;li&gt;Context Window: 4,096 tokens&lt;/li&gt;
&lt;li&gt;Hidden Dimension: 1,024&lt;/li&gt;
&lt;li&gt;Attention: GQA (16 Query / 4 KV)&lt;/li&gt;
&lt;li&gt;FFN Dimension: 2,816&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pretraining
&lt;/h2&gt;

&lt;p&gt;I trained this for 15,000 steps using mixed-precision FP16. Pretraining is terrifying because you're constantly waiting for the loss to suddenly spike to &lt;code&gt;NaN&lt;/code&gt; and ruin hours of compute. &lt;/p&gt;

&lt;p&gt;Somehow, the curve actually held. It decayed down to a validation perplexity of 5.86. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby9jb7u4zgp1y9eixz7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby9jb7u4zgp1y9eixz7y.png" alt="V4 Training Curve" width="799" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching the raw output go from random token garbage at step 100 to recognizable Python syntax at step 10,000 is a bizarrely satisfying experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  HumanEval and Data Contamination
&lt;/h2&gt;

&lt;p&gt;After pretraining, I instruction-tuned the base model with a 2.3M parameter LoRA adapter on the CodeAlpaca dataset. Then I ran it against OpenAI's HumanEval benchmark.&lt;/p&gt;

&lt;p&gt;It scored exactly 0.0%.&lt;/p&gt;

&lt;p&gt;That stings for a second, but it's the empirically expected baseline. A model this small, trained on just 2 billion tokens, isn't going to solve multi-step algorithmic puzzles.&lt;/p&gt;

&lt;p&gt;The interesting part was looking at the training data itself. Using the standard 13-gram exact-match threshold from the GPT-3 paper, I audited CodeAlpaca and found 1,644 sequences that leaked HumanEval test logic. &lt;/p&gt;

&lt;p&gt;The fact that EmsyAI still scored 0% despite seeing those leaked solutions during fine-tuning doesn't prove it's a generalization genius. It just means the model—and specifically the tiny 2.3M LoRA adapter—simply lacked the capacity to rote-memorize those sequences verbatim. The contamination didn't help it cheat, because it just couldn't remember the answers anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke (and the plan for V5)
&lt;/h2&gt;

&lt;p&gt;Scaling this architecture up to 196M exposed a lot of edge cases that I completely missed at the 88M scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attention logit explosions&lt;/strong&gt;&lt;br&gt;
At hidden dim 1024, the attention logits would occasionally spike wildly. Standard architectures don't normalize queries and keys before the dot product, which becomes a huge stability liability at scale. I originally thought recent releases like Qwen 2.5 and Gemma 2 fixed this directly, but that was wrong—Gemma 2 used attention-logit soft-capping. It wasn't until OLMo 2, and later Gemma 3 (inspired by Meta's Chameleon) and Qwen 3, that QK-Norm was widely credited for stabilizing training. V5 will implement QK-Norm to stop these spikes for good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokenizer byte collisions&lt;/strong&gt;&lt;br&gt;
I wrote the BPE tokenizer from scratch, but I messed up the fallback byte mappings. Bytes 0-3 currently overlap with my special control tokens (&lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt;, etc.). It didn't crash the training, but it's a silent bug that degrades performance. V5 is getting a total tokenizer rebuild, and I'll be doubling the vocab from 16k to 32k.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-document attention poisoning&lt;/strong&gt;&lt;br&gt;
Right now, my dataloader blindly concatenates text files to hit the 4096 context length. The model spends a lot of compute attending across completely unrelated documents that just happen to share the same training window. I need to write document-aware packing with proper attention masking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it locally
&lt;/h2&gt;

&lt;p&gt;I exported the final weights to GGUF, so you can run it in Ollama if you want to see a 196M model try (and fail) to write FizzBuzz.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gulding/EmsyAI.git
&lt;span class="nb"&gt;cd &lt;/span&gt;EmsyAI
huggingface-cli download gulding/EmsyAI emsyai-v4-instruct-f32.gguf &lt;span class="nt"&gt;--local-dir&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
ollama create emsyai-v4 &lt;span class="nt"&gt;-f&lt;/span&gt; Modelfile
ollama run emsyai-v4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full PyTorch training loop, architecture, and tokenizer code is up on GitHub: &lt;a href="https://github.com/gulding/EmsyAI" rel="noopener noreferrer"&gt;https://github.com/gulding/EmsyAI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
