<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Machine-Learning | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/machine-learning/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/machine-learning/index.xml" rel="self" type="application/rss+xml"/><item><title>I Trained My Own AI Model (Spoiler: It's Just for Study)</title><link>https://akitaonrails.github.io/en/2026/10/14/i-trained-my-own-ai-model-spoiler-its-just-for-study/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/14/i-trained-my-own-ai-model-spoiler-its-just-for-study/</guid><pubDate>Wed, 14 Oct 2026 15:00:00 -0300</pubDate><description>&lt;p&gt;One exercise was missing from my list this year: trying to build my own LLM. Not a frontier model. Building a GPT-5 or a Claude takes years of study and experimentation, whole teams and hundreds of millions of dollars in compute. The &lt;a href="https://hai.stanford.edu/ai-index/2024-ai-index-report"target="_blank" rel="noopener"&gt;Stanford AI Index&lt;/a&gt; estimate for GPT-4 was US$ 78 million in compute alone, and for Gemini Ultra, US$ 191 million.&lt;/p&gt;
&lt;p&gt;But nobody needs a GPT-class model for everything. A frontier model is a generalist by definition: it tries to know everything about anything, from tax law to song lyrics, and the returns on that diminish.&lt;/p&gt;</description><content:encoded><![CDATA[<p>One exercise was missing from my list this year: trying to build my own LLM. Not a frontier model. Building a GPT-5 or a Claude takes years of study and experimentation, whole teams and hundreds of millions of dollars in compute. The <a href="https://hai.stanford.edu/ai-index/2024-ai-index-report"target="_blank" rel="noopener">Stanford AI Index</a> estimate for GPT-4 was US$ 78 million in compute alone, and for Gemini Ultra, US$ 191 million.</p>
<p>But nobody needs a GPT-class model for everything. A frontier model is a generalist by definition: it tries to know everything about anything, from tax law to song lyrics, and the returns on that diminish.</p>
<p>And most of them today are mixtures of experts: roughly speaking, several smaller models clustered into one giant chunk of bits, with a router that wakes only a few of them for each token. OpenAI&rsquo;s <a href="https://arxiv.org/abs/2508.10925"target="_blank" rel="noopener">gpt-oss-120b</a> has 117 billion parameters in total and uses 5.1 billion per token. <a href="https://arxiv.org/abs/2412.19437"target="_blank" rel="noopener">DeepSeek-V3</a> has 671 billion and activates 37. The big model is already, on the inside, a collection of small models.</p>
<p>I could have grabbed an open model, gpt-oss, Qwen, Phi, and adapted it. The techniques exist and are well documented:</p>
<ul>
<li><strong>Full fine-tuning</strong>: keep training all the weights of a ready model on your data. It&rsquo;s classic transfer learning, formalized for text by <a href="https://arxiv.org/abs/1801.06146"target="_blank" rel="noopener">ULMFiT</a> in 2018.</li>
<li><strong>LoRA and QLoRA</strong>: freeze the original weights and train only small low-rank matrices beside them. <a href="https://arxiv.org/abs/2106.09685"target="_blank" rel="noopener">LoRA</a> cuts trainable parameters by up to ten thousand times; <a href="https://arxiv.org/abs/2305.14314"target="_blank" rel="noopener">QLoRA</a> quantizes the base model to 4 bits and fine-tunes a 65-billion model on a single 48 GB GPU.</li>
<li><strong>Distillation</strong>: train a smaller model to imitate a larger one&rsquo;s outputs. The idea is from <a href="https://arxiv.org/abs/1503.02531"target="_blank" rel="noopener">Hinton, 2015</a>, and the most famous recent example is <a href="https://arxiv.org/abs/2501.12948"target="_blank" rel="noopener">DeepSeek-R1</a> distilled into versions from 1.5 to 70 billion on top of Qwen and Llama.</li>
<li><strong>Continued pretraining</strong>: one more training phase on text from your domain before any instruction tuning, the <a href="https://arxiv.org/abs/2004.10964"target="_blank" rel="noopener">&ldquo;Don&rsquo;t Stop Pretraining&rdquo;</a> of 2020.</li>
<li><strong>SFT, RLHF and DPO</strong>: teach the assistant format with demonstrations, then align with human preference via reinforcement learning (<a href="https://arxiv.org/abs/2203.02155"target="_blank" rel="noopener">InstructGPT</a>) or with a direct classification loss (<a href="https://arxiv.org/abs/2305.18290"target="_blank" rel="noopener">DPO</a>).</li>
</ul>
<p>With any of those I&rsquo;d have a useful model in hours. But the goal was different. I wanted to organize, hands-on, the basic techniques for building a model smaller than GPT-2, from scratch, in a few days, and see how far it would go. If possible, train a tiny model starting from random weights and add enough functionality for it to almost work inside an agentic harness like OpenCode.</p>
<p>Before going on, who I am in this story: I&rsquo;m a curious software engineer and an amateur at this. As an LLM researcher I&rsquo;m a zero. I knew I wouldn&rsquo;t have a working LLM in such a short time, and that&rsquo;s part of the exercise.</p>
<p>But it&rsquo;s not unachievable. If you&rsquo;re a computer science student, study hard and give it enough years, you can try it yourself. And if you&rsquo;re not a scientist, understand this: nobody needs to be Einstein to do this. The foundations are all available, published, for you to study and try. What&rsquo;s mandatory is elbow grease.</p>
<p>Spoiler: we got far into the &ldquo;almost&rdquo; and stopped there. The <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">repository is public</a>, with every step and every failure documented. This text is the summary.</p>
<blockquote>
  <p><strong>TL;DR:</strong> I started from Karpathy&rsquo;s microgpt, 200 lines of pure Python that train a GPT in one minute on a MacBook, and expanded it into a PyTorch lab with its own tokenizer, pretraining, supervised fine-tuning, an OpenAI-compatible server and OpenCode integration. In under two weeks, on one RTX 5090, I trained a 337-million-parameter model on 10 billion tokens: 137 hours of training, at most around R$ 80 of electricity. It gets 58 out of 100 simple Python functions right on a held-out set, and scores zero on anything that requires reading, editing and testing a repository. The <a href="https://huggingface.co/HuggingFaceTB/SmolLM2-360M"target="_blank" rel="noopener">360-million SmolLM2</a>, the same size, saw 4 trillion tokens, 400 times more. Jump to <a href="#karpathys-exercise">Karpathy&rsquo;s exercise</a>, the <a href="#the-steps-of-building-an-llm">steps</a>, the <a href="#the-dimensions-of-the-problem">dimensions</a>, the <a href="#the-stumbles">stumbles</a>, the <a href="#what-came-out-the-other-side">result</a> or the <a href="#the-future-is-smaller-and-more-specialized">conclusion</a>.</p>

</blockquote>
<h2>Karpathy&rsquo;s exercise<span class="hx:absolute hx:-mt-20" id="karpathys-exercise"></span>
    <a href="#karpathys-exercise" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In February this year Andrej Karpathy published <a href="https://karpathy.github.io/2026/02/12/microgpt/"target="_blank" rel="noopener">microgpt</a>: a single file of 200 lines of Python, with no dependencies, that trains and runs a GPT. He calls it &ldquo;the full algorithmic content of what is needed. Everything else is just efficiency&rdquo;. It&rsquo;s the culmination of a lineage of his: micrograd (scalar autograd), makemore (generating names) and nanoGPT (compact GPT-2 in PyTorch), joined by <a href="https://github.com/karpathy/nanochat"target="_blank" rel="noopener">nanochat</a>, &ldquo;the best ChatGPT that $100 can buy&rdquo;, which at launch trained a GPT-2-capability model in almost four hours on a node of eight H100s, and today does it in about two.</p>
<p>microgpt is deliberately minimal: the dataset is 32 thousand first names, one per line, and the tokenizer is one character per token.</p>
<p>The network has one layer, 16 embedding dimensions, 4 attention heads, a 16-token window because the longest name has 15 letters. It trains for a thousand steps, one name per step, with Adam written by hand, and at the end generates made-up names, all in one minute on a MacBook.</p>
<p>What it teaches, by having everything exposed in a single file:</p>
<ul>
<li>autograd, the <code>Value</code> class that holds a number, the gradient and the chain rule to propagate back;</li>
<li>what a token and an embedding are;</li>
<li>the attention block, with query, key and value, and the causal mask that stops the model from peeking at the future;</li>
<li>the cross-entropy loss against the next token;</li>
<li>the optimizer;</li>
<li>sampling with temperature.</li>
</ul>
<p>It&rsquo;s a GPT-2 with small differences the file itself notes: RMSNorm instead of LayerNorm, no biases, ReLU instead of GeLU. After reading that, what&rsquo;s left between you and a real model is, in his words, efficiency. I went to see how much efficiency.</p>
<h2>What we expanded<span class="hx:absolute hx:-mt-20" id="what-we-expanded"></span>
    <a href="#what-we-expanded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">microgpt lab</a> is an independent reimplementation in PyTorch, inspired by the gist, split by concept: about 3,300 lines of Python in 31 modules, plus scripts and 85 CPU tests. A single model implementation serves pretraining, assistant tuning and the server. Every weight starts random; the project downloads no pretrained weights at all, on purpose, because the rule of the exercise was from scratch.</p>
<p>On top of Karpathy&rsquo;s block came the components a 2026 model uses: RoPE for position, RMSNorm, SwiGLU in the dense layer, embeddings tied to the output, Flash Attention through PyTorch, a key-value cache with prefix reuse. And around the model, what it needs to talk to an agent: a conversation protocol with roles, an HTTP server compatible with OpenAI&rsquo;s chat API, tool calls validated by schema and optionally by grammar, separation of reasoning content, and the OpenCode configuration pointing at it.</p>
<p>The project went through four scales:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Parameters</th>
          <th>Tokenizer</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Byte lesson</td>
          <td style="text-align: right">0.9 million</td>
          <td>byte by byte</td>
          <td>runs on CPU, teaches the whole loop</td>
      </tr>
      <tr>
          <td>Student</td>
          <td style="text-align: right">91 million</td>
          <td>o200k (200K entries)</td>
          <td>first agent, arithmetic</td>
      </tr>
      <tr>
          <td>Capacity candidate</td>
          <td style="text-align: right">239 million</td>
          <td>o200k</td>
          <td>more layers, same vocabulary</td>
      </tr>
      <tr>
          <td>Foundation pivot</td>
          <td style="text-align: right">337 million</td>
          <td>own BPE, 32K entries</td>
          <td>the final model</td>
      </tr>
  </tbody>
</table>
<p>The table hides the first big lesson, which I tell in the stumbles: in the 239-million model, 154 million parameters were just the vocabulary table. 85 million were left for the actual transformer.</p>
<h2>The steps of building an LLM<span class="hx:absolute hx:-mt-20" id="the-steps-of-building-an-llm"></span>
    <a href="#the-steps-of-building-an-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Regardless of size, the path has the same stations. What changes is the trailing zero at each one.</p>
<h3>1. Data<span class="hx:absolute hx:-mt-20" id="1-data"></span>
    <a href="#1-data" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A model is what it eats. We downloaded, with pinned revisions and hashes, selections of public corpora: <a href="https://huggingface.co/datasets/codeparrot/codeparrot-clean"target="_blank" rel="noopener">CodeParrot</a> (deduplicated, filtered Python), <a href="https://huggingface.co/datasets/OpenCoder-LLM/opc-sft-stage1"target="_blank" rel="noopener">OpenCoder</a> (instruction and code), <a href="https://huggingface.co/datasets/HuggingFaceTB/smollm-corpus"target="_blank" rel="noopener">Cosmopedia</a> (synthetic educational text), <a href="https://huggingface.co/datasets/HuggingFaceTB/smoltalk"target="_blank" rel="noopener">SmolTalk</a> (conversation) and a multilingual slice. The 14 download manifests add up to about 20 GB raw, all stored on the NAS.</p>
<p>Then comes curation: exact deduplication, splitting train and validation by repository or conversation group (never by line, or the same project leaks to both sides), and the mixture. The final foundation corpus has 3.5 billion stored tokens, 60% Python code, 16% instruction, 12% educational text, 8% conversation, 4% multilingual. Every proportion is a design decision, and none of them has a published right answer.</p>
<h3>2. Tokenizer<span class="hx:absolute hx:-mt-20" id="2-tokenizer"></span>
    <a href="#2-tokenizer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The tokenizer is the dictionary between text and integers. The byte lesson uses 256 tokens plus one for end of document. The student used o200k, OpenAI&rsquo;s public vocabulary with 200K entries, which is what most tutorials do. The pivot trained its own BPE with 32,768 entries, on 92 MB of training-only text, with full UTF-8 coverage.</p>
<p>The choice looks like a detail and decides where the parameters end up. Each vocabulary entry costs one row in the embedding table at the model&rsquo;s width. With 200K entries and width 768, that&rsquo;s 154 million parameters spent on the table. With 32K entries and width 1,024, it&rsquo;s 33 million, and the rest goes to the layers that think.</p>
<h3>3. Pretraining, or foundation<span class="hx:absolute hx:-mt-20" id="3-pretraining-or-foundation"></span>
    <a href="#3-pretraining-or-foundation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is Karpathy&rsquo;s loop at scale. The model receives 2,048-token windows drawn from the training stream and learns to predict the next token. Each update processes 32,768 tokens: two sequences, accumulated eight times. AdamW, learning rate with warmup and cosine decay, gradient clipping, BF16 on the GPU and FP32 for the weights.</p>
<p>The pivot ran about 305 thousand updates, 10 billion tokens of exposure. Since the corpus has 3.5 billion, each document was seen, in the mixture&rsquo;s expectation, almost three times: the code about twice, and instruction, educational text and conversation about nine or ten. Every 500 updates, a validation and a checkpoint. At the 2, 5 and 10 billion milestones, a frozen checkpoint with a hash, to measure the curve.</p>
<h3>4. Supervised fine-tuning<span class="hx:absolute hx:-mt-20" id="4-supervised-fine-tuning"></span>
    <a href="#4-supervised-fine-tuning" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pretraining produces a model that completes text. For it to answer a request comes SFT, <em>supervised fine-tuning</em>: question and answer examples in the exact format the server uses, with the loss computed only over the assistant&rsquo;s tokens. What the user and the tool said goes in as context and is masked out of the loss.</p>
<p>The examples came from NVIDIA&rsquo;s <a href="https://huggingface.co/datasets/nvidia/OpenCodeInstruct"target="_blank" rel="noopener">OpenCodeInstruct</a>, and every answer was executed in a restricted Docker before going in: almost 99 thousand candidates, 97 thousand survived the test with three different hash seeds. 92 thousand training episodes remained. The selected checkpoint saw 64 thousand draws from those episodes, about 10 million answer tokens.</p>
<h3>5. Protocol, tools and server<span class="hx:absolute hx:-mt-20" id="5-protocol-tools-and-server"></span>
    <a href="#5-protocol-tools-and-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>An agent like OpenCode talks to the model over HTTP: it sends the history with roles and the tool definitions, gets back text or a tool call in JSON, executes it, returns the result, and repeats. The lab implements that whole cycle: the same protocol file serializes the training examples and the serving prompts, so training format and usage format never diverge.</p>
<p>The server implements <code>/v1/models</code> and <code>/v1/chat/completions</code>, validates the tool call against the schema, can constrain generation with a grammar so the JSON comes out well formed, separates reasoning content from the answer, counts cached tokens and rejects context overflow instead of truncating silently. All of that is plumbing, zero intelligence, and without it the model doesn&rsquo;t even get a seat at the table.</p>
<h3>6. Evaluation<span class="hx:absolute hx:-mt-20" id="6-evaluation"></span>
    <a href="#6-evaluation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The step that changed the project the most. A frozen set of 100 Python development tasks, 20 families with five variants of name and wording, plus 100 held-out tasks that only the final checkpoint saw. Every answer runs in a Docker with no network, no root, with CPU and memory limits, against fixed assertions. On top of that: four real repository-repair tasks through OpenCode, 12 problems of tracing a program&rsquo;s exact output, information-retrieval probes in prompts of 8, 16 and 32 thousand tokens, and four short conversations.</p>
<blockquote>
  <p>No HumanEval, MBPP or SWE-bench. These are small local diagnostics, with correlated families, one seed, greedy generation. They serve to choose between checkpoints, and the closeout report itself says not to read &ldquo;58 out of 100&rdquo; as &ldquo;58% success at programming&rdquo;.</p>

</blockquote>
<h2>The dimensions of the problem<span class="hx:absolute hx:-mt-20" id="the-dimensions-of-the-problem"></span>
    <a href="#the-dimensions-of-the-problem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>What all of this cost, and how it compares with the real world:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th style="text-align: right">Our final model</th>
          <th>References</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Parameters</td>
          <td style="text-align: right">337 million</td>
          <td>GPT-2 small: 124 million; full GPT-2 (2019): 1.5 billion</td>
      </tr>
      <tr>
          <td>Tokens seen in pretraining</td>
          <td style="text-align: right">10 billion</td>
          <td>SmolLM2-360M: 4 trillion; Llama 3.1: 15 trillion; Qwen3: 36 trillion</td>
      </tr>
      <tr>
          <td>Raw data downloaded</td>
          <td style="text-align: right">20 GB</td>
          <td>GPT-2: 40 GB of WebText, 8 million pages</td>
      </tr>
      <tr>
          <td>Foundation corpus</td>
          <td style="text-align: right">3.5 billion tokens, 2.35 million documents</td>
          <td></td>
      </tr>
      <tr>
          <td>Hardware</td>
          <td style="text-align: right">1 RTX 5090, 32 GB</td>
          <td>Llama 3.1 405B: 30.84 million H100 hours</td>
      </tr>
      <tr>
          <td>GPU time in foundation</td>
          <td style="text-align: right">137 process hours, 6.4 calendar days</td>
          <td>DeepSeek-V3: 2.79 million H800 hours, ~US$ 5.6 million</td>
      </tr>
      <tr>
          <td>SFT time</td>
          <td style="text-align: right">3.4 hours across the nine stages</td>
          <td></td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td style="text-align: right">23 to 44 thousand tokens/s, depending on I/O</td>
          <td></td>
      </tr>
      <tr>
          <td>Peak VRAM</td>
          <td style="text-align: right">13.9 GB in foundation; 6.8 GB in SFT</td>
          <td></td>
      </tr>
      <tr>
          <td>Checkpoints</td>
          <td style="text-align: right">17 distinct checkpoints (20 paths), 69 GB</td>
          <td></td>
      </tr>
      <tr>
          <td>Cost in money</td>
          <td style="text-align: right">zero in cloud; about R$ 80 of electricity worst case</td>
          <td>nanochat: US$ 100 on 8 H100s for 4 hours at launch; today about US$ 48 in 2 hours</td>
      </tr>
  </tbody>
</table>
<p>The electricity bill: the RTX 5090 has a <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/"target="_blank" rel="noopener">575 W rated power</a>. Over 137 hours at full load that&rsquo;s about 79 kWh, and Enel&rsquo;s residential tariff in São Paulo in 2026, adding TUSD, TE and taxes, is around R$ 1 per kWh. Since training ran much of the time at a 75% duty cycle to keep the desktop from freezing, the real figure was lower. It&rsquo;s the marginal cost of someone who already owns the card.</p>
<p>Renting a 5090 on <a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">Runpod</a> costs about US$ 1.20 an hour, so the same 137 hours would cost about US$ 165; an H100 runs between US$ 2.90 and 4 an hour, depending on the variant.</p>
<p>And the comparison that gives the scale. SmolLM2, at 360 million parameters, practically our size, was trained on 4 trillion tokens. At our throughput, 4 trillion would take between four and six years of uninterrupted GPU. Same model size, 400 times the amount of study.</p>
<p>That&rsquo;s why &ldquo;parameters&rdquo; is a misleading unit: it says the size of the brain, and nothing about how many years of school it had.</p>
<p>The rule of thumb derived from <a href="https://arxiv.org/abs/2203.15556"target="_blank" rel="noopener">Chinchilla</a> (70 billion parameters, 1.4 trillion tokens), about 20 tokens per parameter to be compute-optimal, would give 6.7 billion tokens for our model. We went past that. But Chinchilla&rsquo;s optimum is the point of best compute usage, and modern small models go far past it on purpose, because the inference cost afterward makes training more worthwhile.</p>
<h2>The stumbles<span class="hx:absolute hx:-mt-20" id="the-stumbles"></span>
    <a href="#the-stumbles" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The full list is in the <a href="https://github.com/akitaonrails/microgpt/blob/main/docs/report/experiment-closeout.md"target="_blank" rel="noopener">closeout report</a>. The ones that taught the most:</p>
<p><strong>Measuring too close to training.</strong> The first agent, the 91-million one, completed four 16-round sessions in OpenCode and one 32-round session with 130 real tool calls, recovering from a check error and switching sessions midway, with the state kept by the harness. It looked ready.</p>
<p>Then I asked, by hand and in English, for a hello world function that takes a name as a parameter and prints it in the message. The model repeated the previous arithmetic edit twice, pointed at a file that didn&rsquo;t exist, and on the third attempt spat out invalid JSON. All it knew was the training task family.</p>
<p><strong>The vocabulary ate the model.</strong> The 239-million candidate had more layers and lower loss than the 91 and solved 24 out of 100 problems. Looking at the parameter distribution, 154 million were vocabulary table. The pivot to 32K entries gave 300 million parameters back to the transformer. The pivot changed vocabulary, capacity distribution, initialization and exposure all at once, so the report doesn&rsquo;t isolate which of them moved the result.</p>
<p><strong>Loss is not competence.</strong> In the final SFT, continuing from 4 thousand to 8 thousand updates lowered the validation loss and dropped executed correctness from 68 to 62. Lower loss, worse code. Checkpoint selection became by executed task, frozen beforehand; the loss number stayed out of it.</p>
<p><strong>Data and graders lie.</strong> &ldquo;Correct&rdquo; answers from the public dataset failed when executed again. Tests that depended on Python&rsquo;s hash order, shadowed builtins, <code>SystemExit(0)</code> counting as success, empty edits copying the file. Each one needed a control: three hash seeds, a completion sentinel, an empty fixture that must fail. And in one of the mixed training runs, the model learned to say the tests passed when they hadn&rsquo;t.</p>
<p><strong>Training format and usage format have to match.</strong> Session-switch announcements, tool-call IDs, edit anchors, the way OpenCode quotes user input: every difference between what the model saw in training and what it received from the server brought the old agent down. The same protocol file for both sides was the fix, and even so changing format sometimes improved the function name without improving the code.</p>
<p><strong>Tokenization crosses correctness boundaries.</strong> The literal string <code>&lt;|endoftext|&gt;</code> inside source code has to remain ordinary text instead of becoming the end-of-document marker. Reconstructing UTF-8 documents without splitting a character in half. XGrammar&rsquo;s support for byte-level vocabularies. The first code evaluation at the 2-billion milestone failed at grammar construction, and was fixed and rerun instead of counting as a model failure.</p>
<p><strong>The NAS almost cost me days.</strong> On resuming the 10-billion phase, the trainer loaded step 81,500 instead of the frozen 152,588: the mutable <code>last.pt</code> had a timestamp earlier than the pause, right after a NAS maintenance, and the cause was never proven. We discarded 1.24 hours of training, verified the hash of the 5-billion milestone, and every later resume came to require a hash, the exact step, optimizer and RNG state, the dataset and a write probe with fsync. Never resume a training run just because the file is called <code>last.pt</code>.</p>
<p><strong>A desktop GPU is a shared GPU.</strong> Free VRAM didn&rsquo;t stop the browser from freezing. The fix was a cooperative duty cycle: the trainer syncs the GPU and sleeps between batches, 75% of the time during the day and 100% from 3 to 9 in the morning. And reading the corpus from the NAS, through mmap, left the GPU idle waiting for disk. After one maintenance the throughput dropped to 5 thousand tokens per second before climbing back to 23 thousand.</p>
<p><strong>I stopped at a pipeline boundary.</strong> The pivot&rsquo;s controller implemented foundation, code SFT and holdout. It finished clean, and &ldquo;finished clean&rdquo; covered up to the isolated Python function, while the goal was the agent. The final checkpoint was tested in OpenCode only at closeout, and failed. It should have been tested before any sentence with &ldquo;ready&rdquo; in it.</p>
<h2>What came out the other side<span class="hx:absolute hx:-mt-20" id="what-came-out-the-other-side"></span>
    <a href="#what-came-out-the-other-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The final model&rsquo;s spec sheet:</p>
<table>
  <thead>
      <tr>
          <th>Property</th>
          <th>Value</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Parameters</td>
          <td>337,167,360, of which 303.6 million in the transformer and 33.6 million in the vocabulary table</td>
      </tr>
      <tr>
          <td>Architecture</td>
          <td>24 layers, width 1,024, 16 heads, SwiGLU with hidden width 2,752, RMSNorm, RoPE</td>
      </tr>
      <tr>
          <td>Vocabulary</td>
          <td>own BPE with 32,768 entries, UTF-8 coverage</td>
      </tr>
      <tr>
          <td>Context</td>
          <td>32,768 tokens allocated; trained on 2,048-token windows</td>
      </tr>
      <tr>
          <td>Foundation</td>
          <td>10 billion tokens, 305,176 updates, final validation loss 1.085</td>
      </tr>
      <tr>
          <td>Selected SFT</td>
          <td>4,000 updates, 10.3 million answer tokens</td>
      </tr>
      <tr>
          <td>Checkpoint</td>
          <td>4 GB with optimizer state; 1.35 GB of weights alone in FP32</td>
      </tr>
  </tbody>
</table>
<p>The learning curve, measured at the three milestones, with the same short SFT applied on top of each:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Foundation tokens</th>
          <th style="text-align: right">Raw code completion /100</th>
          <th style="text-align: right">After SFT 500 /100</th>
          <th style="text-align: right">After SFT 1,000 /100</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">2 billion</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">39</td>
      </tr>
      <tr>
          <td style="text-align: right">5 billion</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">49</td>
          <td style="text-align: right">52</td>
      </tr>
      <tr>
          <td style="text-align: right">10 billion</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right">56</td>
          <td style="text-align: right">65</td>
      </tr>
  </tbody>
</table>
<p>The longer SFT on top of the 10 billion reached <strong>68 out of 100</strong> on the development set and <strong>58 out of 100</strong> on the held-out one. All 100 held-out outputs are valid Python, and 97 have the requested function name and parameters. Nine families passed all five variants: median, factorial, greatest common divisor, binary conversion, inverting a dictionary, line count, leap year, seconds to hours, JSON keys. Running total ended at four out of five.</p>
<p>So what it does: it writes a small Python function, with a docstring, correct syntax and the right name, for a clear one-line request. Inside an editor, as autocomplete for an isolated function, it&rsquo;s fine for study.</p>
<p>What it gets wrong, with saved examples: deduplicating with a <code>set</code> when the spec required supporting unhashable lists; grouping by length by counting words instead of grouping; doing arithmetic on each element in a padding function; returning <code>(text, '')</code> in a split-once function, ignoring the separator; removing the last character instead of the directory in a basename function. Of the 42 held-out failures, 39 have valid syntax and the right name and fail on semantics. The hard part was never looking like code.</p>
<p>And what it zeroes:</p>
<ul>
<li><strong>0 out of 4</strong> real repository repairs in OpenCode, with zero tool calls. Asked to read, edit and test a file, it returned text with code. In one of the tasks, it renamed the function to <code>fix_mean</code>.</li>
<li><strong>0 out of 12</strong> at tracing the exact output of short programs, with and without reasoning mode. Many answers were code that supposedly would compute the requested integer.</li>
<li><strong>0 out of 9</strong> at finding a six-digit number hidden in prompts of 8, 16 and 32 thousand tokens. The 32K-token allocation works; the useful memory, none.</li>
<li>Conversation: asked in Portuguese about the color of the sky, it answered repetitively and wrongly; an English breakfast request became a Python docstring repeating &ldquo;oats&rdquo; until the token limit. In a short exchange it remembered the cat&rsquo;s name was Pipoca, and still answered with a full sentence despite the request for &ldquo;only the name&rdquo;.</li>
</ul>
<p>The final SFT had only code answers, no conversation, no tool trajectories, no error recovery. The tendency to answer anything with Python is consistent with that, and the report is careful to say the cause was not isolated.</p>
<h2>What it would still take for a simple agent in OpenCode<span class="hx:absolute hx:-mt-20" id="what-it-would-still-take-for-a-simple-agent-in-opencode"></span>
    <a href="#what-it-would-still-take-for-a-simple-agent-in-opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The report closes with engineering estimates, with no promise of a pass rate. To get to an agent that solves simple programming tasks inside OpenCode, in this order:</p>
<ol>
<li><strong>Teach tool use on the stronger model.</strong> Between 10 and 30 thousand short, verified repository trajectories (read, edit, check), mixed with 30 to 50% code so it doesn&rsquo;t unlearn. One to three days of engineering and two to six initial hours of training.</li>
<li><strong>Correct its own mistakes.</strong> Collect hundreds to a few thousand model failures on training tasks, reconstruct the file state, verify the corrections and train while masking the wrong action. Another one to three days of collection and two to eight hours of training.</li>
<li><strong>Bring the conversation back.</strong> A balanced mix of conversation, tools and code against the frozen checkpoint, so it stops answering everything with Python and can switch tasks. One to four hours of training.</li>
<li><strong>Real long context.</strong> Only after the short tasks pass: 4K, 8K, and only then 16K and 32K, with real compaction and session-switch trajectories. One to two days of engineering and 6 to 24 hours of training, measuring memory at every window.</li>
<li><strong>More foundation, with better data.</strong> Every additional 10 billion tokens costs between 93 and 139 hours on this GPU, before preparation and evaluation. And repeating the same 3.5-billion mixture isn&rsquo;t worth it: it needs more unique educational code.</li>
</ol>
<p>None of those steps was executed. There&rsquo;s no evidence that a fixed number of extra hours would produce a reliable agent, and the report is explicit about it. What the exercise established was the distance: between generating a plausible function and acting reliably in a repository there&rsquo;s a chasm that parameters and allocated context alone don&rsquo;t cross.</p>
<blockquote>
  <p>The strongest thing that came out of this was the measured curve from 0 to 65 in ten billion tokens, and three concrete counterexamples: syntax is not semantics, protocol support is not tool skill, and context capacity is not memory.</p>

</blockquote>
<h2>The future is smaller and more specialized<span class="hx:absolute hx:-mt-20" id="the-future-is-smaller-and-more-specialized"></span>
    <a href="#the-future-is-smaller-and-more-specialized" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After eleven days looking inside a model, my reading of the market got firmer. The generalist frontier model will keep existing and will keep being expensive, and the return on every extra billion parameters keeps shrinking. I already wrote that <a href="/en/2026/10/12/llms-agents-competent-enough/">the raw models converged</a> and that the recent gains came from the harness. The exercise gave me the other half of the argument.</p>
<p>Mixture of experts is the industry admitting it on the inside. If the 117-billion gpt-oss wakes 5 billion per token, what you&rsquo;re using for an accounting question is a 5-billion model picked by a router. The question that remains is why carry the other 112 billion, of art, film, memes and particle physics, on an accounting office&rsquo;s server.</p>
<p>Imagine a local model, built for accountants. It knows the chart of accounts, the country&rsquo;s tax legislation, the tax authority&rsquo;s file formats, the jargon. It skips parameters about everything an accountant doesn&rsquo;t need, and it can run on the office PC, with the client&rsquo;s data inside the room.</p>
<p>The examples already exist in other fields: Google&rsquo;s 4-billion <a href="https://research.google/blog/medgemma-our-most-capable-open-models-for-health-ai-development/"target="_blank" rel="noopener">MedGemma</a> for health, the 7-billion <a href="https://arxiv.org/abs/2403.03883"target="_blank" rel="noopener">SaulLM</a> for law, the 7-billion <a href="https://arxiv.org/abs/2503.16252"target="_blank" rel="noopener">Fin-R1</a> for financial reasoning. In 2023 <a href="https://arxiv.org/abs/2303.17564"target="_blank" rel="noopener">BloombergGPT</a> needed 50 billion parameters and 1.3 million A100 hours to do finance; today a fine-tune on top of an open 7-billion model plays the same role for a fraction.</p>
<p>What changed to make this viable was the agent. A specialized model from 2023 had to know everything by heart, because the only thing it did was complete text. A 2026 model needs a base: enough reasoning, the ability to sustain a long task, reliable tool calling, prompt caching. The rest, the law that changed yesterday, the client&rsquo;s document, the table it never saw, it fetches with tools and the internet on the spot, and fills the gaps in its training at usage time.</p>
<p><a href="https://arxiv.org/abs/2506.02153"target="_blank" rel="noopener">NVIDIA published in 2025</a> exactly that position: small models are sufficient, more suitable and cheaper for most of an agent&rsquo;s calls, with the big model called only when open-ended conversation demands it. Apple already <a href="https://machinelearning.apple.com/research/apple-foundation-models-2025-updates"target="_blank" rel="noopener">runs a 3-billion model on the device</a> with a tool protocol and per-task adapters.</p>
<p>It&rsquo;s the same reasoning as the <a href="/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/">decision models I tested</a> last week: a 4-billion classifier trained for a task beats a generic chat model of the same size at it, in a fraction of the response time. Instead of trying to fit the whole world into a bigger model, fit the domain into a smaller one and let the harness fetch the world when needed.</p>
<p>And what my 337-million model taught me is the size of &ldquo;enough&rdquo; in that sentence. It knows the syntax and knows the names of things. What&rsquo;s missing, semantics, tools, memory, is exactly what separates a text generator from an agent, and nobody trains that in two weeks on a graphics card. You train it over months, on a cluster, with trillions of tokens, and then specialize.</p>
<p>The code is in the <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">repository</a>, with the full closeout report, for anyone who wants to pick up where I left off. It&rsquo;s just for study. And it was the best study of the year.</p>
]]></content:encoded><category>artificial-intelligence</category><category>llms</category><category>machine-learning</category><category>local-models</category></item><item><title>Final Attempt to Train an LLM with LoRA. Cannon Shot, But Missing the Fly.</title><link>https://akitaonrails.github.io/en/2025/05/03/final-attempt-to-train-an-llm-with-lora-cannon-shot-missing-the-fly/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/05/03/final-attempt-to-train-an-llm-with-lora-cannon-shot-missing-the-fly/</guid><pubDate>Sat, 03 May 2025 21:15:00 -0300</pubDate><description>&lt;p&gt;Ok, I&amp;rsquo;m stubborn. In the &lt;a href="https://www.akitaonrails.com/en/2025/05/03/ensinando-zig-mais-recente-pra-sua-llm-treinando-loras-quase"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I explained everything I knew about trying to fine-tune a model with LoRA. But it&amp;rsquo;s a tedious, slow process, limited by the &amp;ldquo;poverty&amp;rdquo; of my RTX 4090. In 24GB of VRAM you have to fit the model, things like the KV cache, the training dataset and room to process it all. Squeezing hard, the Qwen3-8B model and 1MB of dataset fit and that&amp;rsquo;s it. It&amp;rsquo;ll use 21GB constantly for more than an hour and the end result, even though the model manages to answer &amp;ldquo;yes, I know Zig 0.14&amp;rdquo;, isn&amp;rsquo;t good — because the 8B base model wasn&amp;rsquo;t great to begin with.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Ok, I&rsquo;m stubborn. In the <a href="https://www.akitaonrails.com/en/2025/05/03/ensinando-zig-mais-recente-pra-sua-llm-treinando-loras-quase"target="_blank" rel="noopener">previous post</a> I explained everything I knew about trying to fine-tune a model with LoRA. But it&rsquo;s a tedious, slow process, limited by the &ldquo;poverty&rdquo; of my RTX 4090. In 24GB of VRAM you have to fit the model, things like the KV cache, the training dataset and room to process it all. Squeezing hard, the Qwen3-8B model and 1MB of dataset fit and that&rsquo;s it. It&rsquo;ll use 21GB constantly for more than an hour and the end result, even though the model manages to answer &ldquo;yes, I know Zig 0.14&rdquo;, isn&rsquo;t good — because the 8B base model wasn&rsquo;t great to begin with.</p>
<p>The way out: migrate my training to a bigger machine. I started by trying an A40, to see if 40GB was enough to fit the Qwen3-32B model for this training. And no, it didn&rsquo;t fit.</p>
<p>&ldquo;Ph0da-se&rdquo;, I thought. I rented the biggest configuration RunPod has:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/9m5uitxvhhbpotu5lkbrw0znd276" alt="H100"  loading="lazy" /></p>
<p>This H100 is already &ldquo;obsolete&rdquo;, in the more modern datacenters there are already H200 Blackwells and other bigger things. But in the world of &ldquo;mortals&rdquo;, an H100 at USD 3 per HOUR will have to do. The good part: it has 80GB of VRAM. And what I wasn&rsquo;t expecting: even that is too little!</p>
<p>And here&rsquo;s the advantage of having done everything organized and pushed to this <a href="https://github.com/akitaonrails/qwen3-zig-lora-training"target="_blank" rel="noopener">GitHub repo</a>. Setup was trivial: just pick a pod with a PyTorch 2.8 image with Python 3.10 and CUDA 12.8, which are the latest stable versions. Clone my project, run <code>pip install -r requirements.txt</code> and I could already run my <code>./train-lora.py</code>.</p>
<p>I was going to add back those 15MB of source code from the newer Zig Standard Library, but even on the H100, it didn&rsquo;t fit! Training with just the original 1MB I used yesterday already pushed the machine almost to its maximum:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/iahqr9hzs2aaku3fup7u3cw9navk" alt="74GB"  loading="lazy" /></p>
<p>Look how absurd this is: during the entire training it sat at nearly 75GB out of the 80GB of VRAM. Literally there&rsquo;s no room to train with much more material than that. Either that, or you drop down to a smaller model, like Qwen3-14B. I still don&rsquo;t know whether 32B with less material or 14B with more material would be the better combination. I already know 8B is useless for programming anything complicated. With 14B I had a bit more success in tests, but it still can&rsquo;t do anything very complex. 32B is no guarantee of being much better: Qwen2.5-32B didn&rsquo;t perform better than the 14B, but the new Qwen3-32B &ldquo;looks like&rdquo; it&rsquo;s more stable. That&rsquo;s what I&rsquo;m trying to test with the LoRA.</p>
<p>This was at lunch time. I&rsquo;m writing this post at dinner time. This was the training forecast:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lzrfdl97n96qesnhde33lugidr3i" alt="6h"  loading="lazy" /></p>
<p>This estimate keeps fluctuating, but I think it did take around 5 to 6 hours.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/stoakih27g3o9c1e1d07y14szrf0" alt="lora size"  loading="lazy" /></p>
<p>And at the end of those almost 6 hours, this is what it produced: a 5.1GB LoRA (against yesterday&rsquo;s 3GB). Size isn&rsquo;t everything. Bigger doesn&rsquo;t mean better. Let&rsquo;s see.</p>
<h3>After Training<span class="hx:absolute hx:-mt-20" id="after-training"></span>
    <a href="#after-training" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now that training is done, I can use the same H100 pod to also serve the model with the LoRA, and it would have to be this one anyway. To run it, it goes like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vllm serve Qwen/Qwen3-32B <span class="se">\
</span></span></span><span class="line"><span class="cl">    --enable-lora <span class="se">\
</span></span></span><span class="line"><span class="cl">    --lora-modules <span class="nv">ziglora</span><span class="o">=</span>./qwen3-zig-lora <span class="se">\
</span></span></span><span class="line"><span class="cl">    --max-model-len <span class="m">10000</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you also have to limit the context size. By default vLLM tries to start with a 40k token context and, even on the H100 with 80GB, it fails initialization with this error:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ykusgib7o6wybhi5rs4zvx2yrp8j" alt="KV Cache error"  loading="lazy" /></p>
<p>There&rsquo;s no room left to load the KV Cache (in short, in the process of generating the next word, it keeps adding a new row and a new column to the matrix, but doesn&rsquo;t need to recompute the previous rows/columns, the result is the same. Hence this cache).</p>
<p>A strange behavior both on my machine and on the H100. I don&rsquo;t know if this happens every time you load a LoRA, or if my LoRA is broken, or if there&rsquo;s some configuration I&rsquo;m missing. But loading with a LoRA, initialization is EXTREMELY SLOW. Bringing up vLLM, which normally takes 1 minute, now takes 5 minutes or more. I don&rsquo;t know what causes this, but this is the kind of server you can&rsquo;t easily shut down and restart. It makes me miss starting up a J2EE server &hellip;</p>
<p>Seriously though, it seems I&rsquo;m not the only one. This <a href="https://github.com/vllm-project/vllm/issues/6876"target="_blank" rel="noopener">issue</a> never got resolved. It &ldquo;could&rdquo; be some bug in vLLM, it &ldquo;could&rdquo; be a combination of factors nobody knows. But neither the GPU nor the CPU is doing anything. CPU doesn&rsquo;t go above 10% usage, GPU looks like it&rsquo;s just waiting. Honestly, at first glance it looks like some I/O problem. Maybe the process of checking the checkpoint shards, pulling from a network volume, is slow. But since I&rsquo;m going to run this only today, I didn&rsquo;t bother to debug it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mluc626od3h1q0swnv8kepe1aji5" alt="slow load"  loading="lazy" /></p>
<p>Look at this. 8 MINUTES on the part that loads checkpoints. And then it still continues with other phases that also take a bit more time. I&rsquo;ll leave this screenshot here, in case someone has seen it and knows what it could be.</p>
<p>Just to rule it out, I tried bringing up vLLM with just Qwen3 and removing the LoRA entirely and, to my surprise:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2hzvcqrk087i5ov4otx0rxa8erdf" alt="sem lora"  loading="lazy" /></p>
<p>It also took almost 8 minutes. So the problem is not the LoRA. Either it&rsquo;s something in vLLM that can be configured — but I don&rsquo;t know how — or it&rsquo;s a limitation of the Qwen3-32B model itself being this slow to load.</p>
<p>Anyway, with the LoRA, I tried dropping to 16k tokens: it doesn&rsquo;t come up. I tried starting with 10k tokens. And at this point I&rsquo;d say this test has already failed. A Qwen3-32 theoretically has the capacity to support context windows up to 40k tokens. It&rsquo;s not huge, but you can work a bit inside that context.</p>
<p>But below 15k tokens you can no longer use it with tools like Aider, which needs to send long system prompts to instruct the LLM on how to behave so it spits out code the tool can capture. So there it is: a Qwen3-32b + LoRA, on an H100 SXM with 80GB of VRAM, is not really usable.</p>
<p>And I&rsquo;d even be sad or frustrated if a 32B model were really, orders of magnitude better than a 14B, but in my empirical tests: it isn&rsquo;t. It makes just as many errors, and it isn&rsquo;t so much better that it justifies paying that much for your own top-of-the-line infra. All of them barely manage to produce simple code, and even then with errors.</p>
<p>In the end, with 10k the server came up, consuming an absurd 63GB at initialization, without even starting to process anything:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uetjwlzlgy7bpd4itn1qxczl3mhs" alt="63GB"  loading="lazy" /></p>
<p>And &hellip;</p>
<h3>First Tests<span class="hx:absolute hx:-mt-20" id="first-tests"></span>
    <a href="#first-tests" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>After 10 minutes of waiting, I had to hit Ctrl+C and start all over. Because I was dumb. RunPod is really meant to run experimental stuff, I&rsquo;m not sure I&rsquo;d trust it for production. It&rsquo;s more like a VPS than an AWS. And I&rsquo;m hacking it together by bringing up a pod that expects to have only port 8888 mapped to run a Jupyter Notebook server (which I&rsquo;m not running).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/h4pzcdxydfiou5iulc7m67zctb9a" alt="runpod connect"  loading="lazy" /></p>
<p>My idea is to use this port to map vLLM. But by default it starts on port 8000. I forgot to add <code>--port 8888</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>vllm serve Qwen/Qwen3-32b --enable-lora --lora-modules ziglora=./qwen3-zig-lora --max_model_len 10000 --port 8888</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And here we go again, wait another 10 minutes &hellip; &hellip; and finally it came up on the right port and now we can test. First, the curl tests:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/p8d3ogzwd7lzpu05o7lkm50f8k4h" alt="curl hello"  loading="lazy" /></p>
<p>And before anyone comments: there&rsquo;s no problem showing my pod&rsquo;s URL in the screenshot. I&rsquo;m going to delete this pod before publishing this post.</p>
<p>By the way, if you bring up a remote model like I did, all you have to do is take the URL and put it in the <code>OPENAI_API_BASE</code> variable and anything in <code>OPENAI_API_KEY</code> and tools like Aider will work the same. The vLLM and Ollama APIs are compatible with OpenAI&rsquo;s.</p>
<p>As you can see, the first question answered similarly to my local test with Qwen3-8B. Nothing special, I already knew that worked. Let&rsquo;s ask it to explain a new feature of Zig 0.14.</p>
<p>By the way, besides being slow to start, it&rsquo;s also slower at generating text, MUCH slower. I must have done something wrong in training, but I felt the same thing with the local LoRA or on the H100. It gets noticeably slower. It did another long &ldquo;think&rdquo;, but the answer this time is more consistent with the release notes (I only cut out the relevant excerpt):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>In Zig 0.14.0, the `@splat` function has been extended to support arrays, making it easier to initialize fixed-length arrays with a single repeated value. This replaces more verbose or unsafe initialization patterns and is type-safe, with the array&#39;s length inferred from the destination type. Here&#39;s a concise explanation and code example:


...

### **@splat for Arrays**  
`@splat(value: T) [N]T` creates an array of length `N` where every element is `value`. The array&#39;s length and element type are determined by the context in which `@splat` is used.  

### **Code Example**  

const Point = struct { x: i32, y: i32 };  

pub fn main() void {  
    // Initialize an array of 5 integers with 42  
    const numbers: [5]i32 = @splat(42);  

    // Initialize an array of 10 booleans with true  
    const flags: [10]bool = @splat(true);  

    // Initialize an array of 7 points with (0, 0)  
    const origin_points: [7]Point = @splat(Point{ .x = 0, .y = 0 });  
}  
....</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The example code also makes a bit more sense. Compared to the 8B version, at least the explanation was better. But this might have been affected by a small tweak I made to the dataset: I duplicated the release notes, to try to put a bit more emphasis on the new features. That probably had an effect too.</p>
<p>Now let&rsquo;s see if it can write code that actually works. My prompt was this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;Can you give me a Zig 0.14 code example of a simple program that acts like an interactive chat, so the user can input messages. then the message is parsed to see if there are commands such as &#39;/list&#39;, which will list the files in the current directory, or &#39;/cat&#39; which will read the file asked, or &#39;/quit&#39; to exit the chat?&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this happened:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4n3bzm5g0gt46jfmqv55qy0x9tlu" alt="abort"  loading="lazy" /></p>
<p>Remember how I had to keep decreasing the context to get it to come up? And remember how I keep saying that &ldquo;thinking&rdquo; wastes a lot of tokens? Well, there you go. The model self-crashed by thinking too much. I tried a few times and it crashes. To try to avoid this, I tried hitting <code>curl</code> sending <code>enable_thinking=false</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ curl https://d8zck5aw8eimij-8888.proxy.runpod.net/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;model&#34;: &#34;ziglora&#34;, &#34;temperature&#34;: &#34;0.1&#34;, &#34;enable_thinking&#34;: &#34;false&#34;, &#34;skip_special_tokens&#34;: true,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;...&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">      }&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Temperature, for code, I think can&rsquo;t really be much higher than 0.1. For chit-chat, for less exact things, the default of 0.6 that everyone uses is fine. But for code, I really don&rsquo;t need it to be &ldquo;creative&rdquo; (random, in fact), I want answers as exact as possible.</p>
<p>Anyway, even trying to turn off thinking, something is wrong. Possibly the LoRA I added is making it &ldquo;think in circles&rdquo; and never being able to return, blowing up the context. I tried several times, but it&rsquo;s unable to answer with code.</p>
<h3>Final: I Give Up For Now<span class="hx:absolute hx:-mt-20" id="final-i-give-up-for-now"></span>
    <a href="#final-i-give-up-for-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Yes, I broke the LLM, apparently lol 😅</p>
<p>I tried several times and it really can&rsquo;t give me any code back. And I know it&rsquo;s trying, because if I send <code>stream=true</code>, I can see it returning tokens, but at some point it runs out of space somewhere (surely the context, which is small) and it crashes.</p>
<p>The feeling is that my LoRA probably &ldquo;stuck&rdquo; too hard and threw too many parameters out of balance. Now the model starts writing the text and enters circular thinking, never stopping generating more text.</p>
<p>This is definitely an effect of the LoRA and of some mistake in training. If I had to guess, it&rsquo;s because I crammed super long texts into training, without manual markers indicating where each part ends with a <code>&lt;|im_end|&gt;</code> or whatever. Without that, maybe it thinks the whole text is one answer and it has to respond that long. From what I understand, in the case of this Zig training, I would have had to break the documentation, the release notes and everything else into smaller chunks, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;|im_start|&gt;system
You are a helpful assistant.&lt;|im_end|&gt;
&lt;|im_start|&gt;user
What is Zig?&lt;|im_end|&gt;
&lt;|im_start|&gt;assistant
Zig is a compiled programming language...&lt;|im_end|&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And do that for all 200 thousand characters of documentation. Imagine the work to do that. And avoid sending super long texts in context without markers for where each part starts and ends.</p>
<p>At least, that&rsquo;s my guess. The problem: even if I spend time formatting it this way, then it&rsquo;s another 6 hours of processing, just to find out afterwards whether it made any difference. It probably won&rsquo;t, I need to think of other hypotheses, and each round is 6 to 12 hours between setup and processing.</p>
<p>From this point on it&rsquo;s a slog I don&rsquo;t have the motivation to do. I&rsquo;m leaving this written down here to see if it reaches the hands of real researchers. Maybe it&rsquo;s something an AI researcher would find super obvious. But that&rsquo;s the point: this information isn&rsquo;t easily available publicly anywhere. Every example you find online is too simple. They just say <em>&ldquo;on this line of python you load the dataset, and on the next line training starts&rdquo;</em> — but where are the instructions for HOW to build a dataset and HOW to determine whether it&rsquo;s adequate? Nowhere.</p>
<p>If anyone has tips on how to improve the training or wants a research problem in the field, <a href="https://github.com/akitaonrails/qwen3-zig-lora-training"target="_blank" rel="noopener">here is the GitHub</a> with all the code and training material I produced. We need researchers to look at this kind of thing and share more about what they know. I know someone knows, but just digging around on Google didn&rsquo;t get me anywhere.</p>
]]></content:encoded><category>llms</category><category>machine-learning</category><category>performance</category></item><item><title>Teaching the Latest Zig to Your LLM - Training LoRAs (Sort Of)</title><link>https://akitaonrails.github.io/en/2025/05/03/teaching-recent-zig-to-your-llm-training-loras-sort-of/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/05/03/teaching-recent-zig-to-your-llm-training-loras-sort-of/</guid><pubDate>Sat, 03 May 2025 02:30:00 -0300</pubDate><description>&lt;p&gt;&lt;a href="https://www.akitaonrails.com/en/2025/05/01/quando-llms-nao-funcionam-pra-programar-um-caso-de-uso-mais-real"target="_blank" rel="noopener"&gt;Two posts ago&lt;/a&gt; I demonstrated the Achilles&amp;rsquo; heel of LLMs for programming: they weren&amp;rsquo;t trained on the most recent data available. So if you ask about changes that came out last month in your favorite library, language or framework, they won&amp;rsquo;t know.&lt;/p&gt;
&lt;p&gt;Note: all the code I&amp;rsquo;m going to show below is available &lt;a href="https://github.com/akitaonrails/qwen3-zig-lora-training"target="_blank" rel="noopener"&gt;in this repository on my GitHub&lt;/a&gt;. Go poke around after reading to the end.&lt;/p&gt;
&lt;p&gt;For example, I asked the Qwen3-8b model (which came out a few days ago) which Zig version it knows about, and the answer was: 0.11 or 0.12:&lt;/p&gt;</description><content:encoded><![CDATA[<p><a href="https://www.akitaonrails.com/en/2025/05/01/quando-llms-nao-funcionam-pra-programar-um-caso-de-uso-mais-real"target="_blank" rel="noopener">Two posts ago</a> I demonstrated the Achilles&rsquo; heel of LLMs for programming: they weren&rsquo;t trained on the most recent data available. So if you ask about changes that came out last month in your favorite library, language or framework, they won&rsquo;t know.</p>
<p>Note: all the code I&rsquo;m going to show below is available <a href="https://github.com/akitaonrails/qwen3-zig-lora-training"target="_blank" rel="noopener">in this repository on my GitHub</a>. Go poke around after reading to the end.</p>
<p>For example, I asked the Qwen3-8b model (which came out a few days ago) which Zig version it knows about, and the answer was: 0.11 or 0.12:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/39h50oguk03tci9reclcbiovolhm" alt="zig antigo"  loading="lazy" /></p>
<p>By the way, the screenshot also shows how you call an OpenAI-compatible LLM API using curl. Anyway, that&rsquo;s why I suffered so much yesterday trying to get a simple &ldquo;Hello World&rdquo; level program working: it doesn&rsquo;t know we&rsquo;re already on <a href="https://ziglang.org/download/0.14.0/release-notes.html"target="_blank" rel="noopener">Zig 0.14</a>. And I did it on purpose, I knew it wouldn&rsquo;t have that information.</p>
<p><em>&ldquo;And other commercial LLMs? Aren&rsquo;t they better than the open source ones?&rdquo;</em></p>
<p>I asked ChatGPT 4.1, Claude Sonnet 3.7, Gemini 2.5 Pro Experimental, Deepseek R1 Zero (all at once, OpenRouter.ai allows multi-model chat):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/z9wbo5yi49m4vd3zi3pjpxbo7zj7" alt="LLM cut off"  loading="lazy" /></p>
<p>Summary:</p>
<ul>
<li>ChatGPT 4.1 says it only has data up to June 2024 and only knows up to Zig 0.11.</li>
<li>Claude Sonnet says it only has data up to April 2023 and only knows up to 0.10.</li>
<li>Gemini 2.5 Pro Exp says it only has data up to the start of 2023 and only knows up to Zig 0.11 as well.</li>
<li>Deepseek R1 (after some deep thinking&hellip;) answered short and blunt that it only has data up to October 2023 and only knows up to Zig 0.11.</li>
</ul>
<p><em>&ldquo;Why don&rsquo;t they have newer data?&rdquo;</em></p>
<p>I&rsquo;ll answer that by showing the reasons in the next sections.</p>
<p><em>&ldquo;But it can pull info from the internet, so it doesn&rsquo;t matter&rdquo;</em></p>
<p>It matters. That information is not IN the model. When you ask it to search the internet, the model doesn&rsquo;t know how to do that. It was instructed (in the fine-tuning/alignment phase or via prompts) to ask the chat system - the parser - to take that request, navigate the web (think curl, playwright, or anything like that), pull the site&rsquo;s content, copy and paste it only in your chat session (it&rsquo;s like a RAG solution). It only has that information in that session and other chats are not influenced by it.</p>
<p><em>&ldquo;But how does it show the source links when it replies?&rdquo;</em></p>
<p>It - probably - doesn&rsquo;t have that inside the model. What I imagine is simpler. They have catalogued and indexed all the original training material (including the source URLs) in a database (think Elastic, Postgres with full-text-search, or any vector database). When it finishes replying in the chat - the chat program and not the model -, it searches that snippet in the database for something similar enough and pulls the source URLs from there to show you.</p>
<p>Anyway.</p>
<p>Zig is experimental, pre-1.0. The syntax for everything changes A LOT from one version to the next, because they&rsquo;re still experimenting with what&rsquo;s best. And it&rsquo;s going to change more. In other words: it&rsquo;s impossible to do &ldquo;Vibe Coding&rdquo; in Zig unless you pin your compiler to version 0.11 or 0.12. But that would be &ldquo;lame&rdquo;, to say the least. I chose to try Zig on purpose, precisely to prove this point.</p>
<p>The same applies to a lot of companies with private code. Think of ERP companies that have their own languages, their own libraries, and none of it is public, so it was guaranteed not to be in OpenAI&rsquo;s training and it&rsquo;s certainly way more information than fits in a prompt context.</p>
<p><strong>&ldquo;Ah, just do a RAG&rdquo;</strong></p>
<p>Canned solution without understanding. No, it doesn&rsquo;t help. It will prioritize the training it already had. Second, for each thing you ask, you&rsquo;d need to know in advance that you&rsquo;ll need something that changed in 0.14, search that bit of documentation and keep adding it to the chat context. If you&rsquo;re going to do that method by method, why are you using the chat? Just go and write the code directly, it&rsquo;ll be faster.</p>
<p>Throwing ALL the documentation into the context doesn&rsquo;t work. I did the math: I downloaded the entire documentation and release notes, it&rsquo;s 200 thousand characters, roughly 50 thousand tokens. Most open source LLMs will blow past the window (average 32k tokens). Via commercial API/Chat you would pay per token, for nothing. Imagine wanting to refactor your 100-line piece of code and having to upload 200 thousand lines of context. Never do that. The cost-benefit doesn&rsquo;t justify it.</p>
<p><strong>&ldquo;Ah, but the Geminis of the world support 1 million tokens&rdquo;</strong></p>
<p>Yeah, with a sliding window, I&rsquo;ve repeated this several times. It can&rsquo;t pay attention to everything at once. Again: you&rsquo;ll <strong>PAY</strong> for those 50 thousand tokens, right? And it won&rsquo;t be cheap. Imagine, every new session having to upload 50 thousand tokens with no guarantee they&rsquo;ll work.</p>
<p>So is it screwed? Is there no way to use Zig with LLMs?</p>
<h3>RAGs?<span class="hx:absolute hx:-mt-20" id="rags"></span>
    <a href="#rags" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The RAG (Retrieval Augmented Generation, which is just a fancy name) hype makes it seem like it&rsquo;s something &ldquo;magical&rdquo;. But really, there&rsquo;s nothing special about it. It&rsquo;s good for when you have a large database that grows all the time, but in each chat session you only need a little bit of that information. Think of a run-of-the-mill e-commerce support team with support tickets. It grows every day, but for each call you only need the specific ticket for the person being helped, which is pretty short.</p>
<p>In the most rudimentary way, the attendant searches for the ticket in the good old Zendesk, copies the text, pastes it into the ChatGPT chat window and starts asking questions about that ticket, like &ldquo;can you summarize what this customer&rsquo;s problem is?&rdquo;, &ldquo;can you imagine a solution for them?&rdquo; and so on.</p>
<p>A RAG is a DATABASE with all those tickets pre-indexed to make relevance searches easier, and then it inserts the found snippet directly into the context window. It serves to automate, to SKIP the steps of manually searching in another system and doing copy and paste, but the effect is EXACTLY the same.</p>
<p><strong>&ldquo;Ah, but it uses VECTOR databases, that must be way cooler&rdquo;</strong></p>
<p>What&rsquo;s cool is hearing nonsense. It could be a Postgres and you&rsquo;d do a SELECT to find the ticket, it&rsquo;s exactly the same thing. If you enable &ldquo;full-text-search&rdquo; in Postgres, it will index your tickets in a &ldquo;VECTOR SPACE MODEL&rdquo;, which is essentially a VECTOR DATABASE.</p>
<p>If instead you prefer SOLR or ELASTIC, both are VECTOR DATABASES specialized in text search.</p>
<p><em>Duh, the more you know.</em></p>
<p>The main things:</p>
<ul>
<li>
<p>the more you stuff into the context (whether prompt text or &ldquo;embedding&rdquo; of a vector database with RAG - it&rsquo;s the same thing), the more you risk confusing the LLM and having it start hallucinating. The best prompts are structured, organized, with direct orders, and that explain the context clearly.</p>
</li>
<li>
<p>whether it&rsquo;s text-prompt or &ldquo;embedding&rdquo;, it will CONSUME input TOKENS. And if the window is small (open source models), it will blow up if you don&rsquo;t control the size of what you send as input. There&rsquo;s no magic.</p>
</li>
</ul>
<p>So what&rsquo;s the way out? Despite RAG being all people talk about, there&rsquo;s more than just RAG in the LLM world.</p>
<h3>Possible Solution: LoRAs<span class="hx:absolute hx:-mt-20" id="possible-solution-loras"></span>
    <a href="#possible-solution-loras" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If instead of things that grow daily, the information is more &ldquo;permanent&rdquo; like instruction manuals, books, maybe RAGs aren&rsquo;t the best approach. Because it will always be TOO BIG and you&rsquo;d need to REPEAT THE SAME THING in every chat session. Especially rules that depend on other rules.</p>
<p>Yes, good providers have <a href="https://x.com/pashmerepat/status/1918084120514900395"target="_blank" rel="noopener">CACHE STATS</a> and that helps a lot. Even so it&rsquo;s no guarantee. Out of nowhere you&rsquo;re going to check your bill and be surprised by USD 500 suddenly spent on token credits.</p>
<p>Example: programming language documentation. Like my case with Zig 0.14.0. I&rsquo;d like the LLM to have that context, but uploading 50 thousand tokens every time is a lot if I only want to fix a 50-line file. Nobody thinks about these things or calculates context size vs chat/api interaction size, and they should: every wasted token is credit spent from your account.</p>
<p>There&rsquo;s a way to take an existing LLM model and add the missing training on top, without using context space. It would be as if the model were &ldquo;updated&rdquo; to know Zig 0.14. These are LoRAs (<strong>Low-Rank Adapter</strong>). It&rsquo;s a smaller kind of training we can add. We train small additional matrices (adapters) that attach to the original LLM&rsquo;s layers. When loaded, this updates the model&rsquo;s outputs without having to redo the entire training from scratch (which is the most expensive part and takes months in OpenAI&rsquo;s heavy datacenters, for example).</p>
<p>And here&rsquo;s a <strong>DISCLAIMER</strong>:</p>
<pre><code>I haven't studied enough yet about all the training parameters. Training is precisely the hardest phase. In particular: generating QUALITY training material and &quot;tuning&quot; the training parameters. There are people specialized in just this and there's no single recipe that works for everything. It's almost an &quot;art&quot; and takes much more dedication than I put in just for this post.
</code></pre>
<p>Some articles to start studying:</p>
<ul>
<li><a href="https://medium.com/@ufuk.birbiri/instruction-tuning-large-language-models-llms-with-huggingface-lora-and-sfttrainer-544bad770b50"target="_blank" rel="noopener">Instruction-Tuning Large Language Models (LLMs) with HuggingFace, LoRA and SFTTrainer</a></li>
<li><a href="https://huggingface.co/docs/trl/main/en/sft_trainer"target="_blank" rel="noopener"># Supervised Fine-tuning Trainer</a></li>
<li><a href="https://rocm.docs.amd.com/projects/ai-developer-hub/en/latest/notebooks/fine_tune/fine_tuning_lora_qwen2vl.html"target="_blank" rel="noopener"># Fine-tuning with the Hugging Face ecosystem (TRL)</a></li>
<li><a href="https://clay-atlas.com/us/blog/2024/10/08/en-multi-modal-large-language-model-sft-trainer/"target="_blank" rel="noopener"># Notes on Fine-Tuning a Multi-Modal Large Language Model Using SFTTrainer</a></li>
<li><a href="https://zohaib.me/a-beginners-guide-to-fine-tuning-llm-using-lora/"target="_blank" rel="noopener"># A beginners guide to fine tuning LLM using LoRA</a></li>
</ul>
<p>And there&rsquo;s a lot more. This is where you have to really understand the &ldquo;internals&rdquo;, the neural networks, loss function, gradients, etc. Don&rsquo;t underestimate training. It won&rsquo;t work on the first try, nor on the tenth. It&rsquo;s A LOT of trial and error and adjusting.</p>
<p>The final result is not exactly the same as a full training, but these adapters modify the parameters of the existing model&rsquo;s layers, changing the final result for specific things we want (that it knows Zig 0.14). It&rsquo;s MUCH cheaper than training from scratch. It doesn&rsquo;t guarantee it will work 100%, but it gives us an option to test, which can be more robust than a RAG.</p>
<p>The process is a miniature version of training a real model. We have to solve the following steps:</p>
<ul>
<li>get the training material (e.g. documentation)</li>
<li>write fine-tuning prompts (I need to &ldquo;teach&rdquo; how it should respond - this is the hardest part)</li>
<li>tokenize this information (trivial), load the base model (easy), and start training (the better the GPU, the less slow it is)</li>
<li>if it succeeds, you need to load this LoRA along with the base model</li>
<li>point tools like Aider at this new modified model.</li>
</ul>
<p>Let&rsquo;s go, step by step.</p>
<h3>1. Training Material<span class="hx:absolute hx:-mt-20" id="1-training-material"></span>
    <a href="#1-training-material" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I think this is the hardest part: getting quality training material to train with.</p>
<p>In the alignment case, this is done by building it by hand (with help from scripts, with help from another LLM to help generate synthetic texts), but ideally I&rsquo;d try to build a big JSON file with &ldquo;prompt&rdquo; and &ldquo;completion&rdquo; keys, something like:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{ &#34;prompt&#34;: &#34;How do the new @splat works for arrays in Zig 0.14.0?, &#34;completion&#34;: &#34;Zig 0.14.0 expands the `@splat` builtin to apply not only to vectors, but to arrays...&#34;}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, doing this for EACH NEW feature in version 0.14.0, including code examples, multiple times for each topic, imagining different ways to ask. And no, I didn&rsquo;t do it and I don&rsquo;t plan to, it&rsquo;s already much more work than I want to try in just this experiment. And I&rsquo;m not even sure if that&rsquo;s the best way yet. As I said before, it&rsquo;s practically an &ldquo;art&rdquo;.</p>
<p>The <a href="https://ziglang.org/download/0.14.0/release-notes.html"target="_blank" rel="noopener">RELEASE NOTES</a> file converted to Markdown has a ridiculous <strong>107 thousand characters</strong>. I&rsquo;d spend the rest of the week just re-editing it into prompt form. I&rsquo;m not going to do that. Instead, I&rsquo;ll just pass the entire raw text and hope that these new facts &ldquo;stick&rdquo; enough for the LLM to remember them. To pull it, it&rsquo;s simple:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://ziglang.org/download/0.14.0/release-notes.html -O zig-0.14.0-release-notes.html

# pip install html2text
html2text zig-0.14.0-release-notes.html &gt; raw_data/zig-0.14.0-release-notes.md</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Since I&rsquo;m on a roll, why not do the same thing with the Zig documentation? Fortunately the documentation is a single big HTML file. So there&rsquo;s no work to build a recursive web scraper or anything like that. Same procedure:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://github.com/ziglang/www.ziglang.org/raw/refs/heads/main/src/documentation/0.14.0/index.html -O raw_data/zig-0.14.0-docs.html

# pip install html2text
html2text raw_data/zig-0.14.0-docs.html &gt; raw_data/zig-0.14.0-docs.md</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That gave me a Markdown file of <strong>464 kilobytes</strong>. Since I think just that isn&rsquo;t enough, I thought it was a good idea to also grab the code examples in the <code>zig/doc</code> directory that I cloned directly from the <a href="https://github.com/ziglang/zig/tree/master/doc/langref"target="_blank" rel="noopener">official Zig repository</a>. I don&rsquo;t know what the best way is, but I tried to organize a single big file, with the name of the <code>.zig</code> file and the code delimited by a tag, with a prompt at the beginning indicating this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">zig_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;raw_data/zig/doc/langref/**/*.zig&#34;</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">total_input_bytes</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="n">file_count</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="n">lines</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">filepath</span> <span class="ow">in</span> <span class="n">zig_files</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">file_bytes</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">getsize</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_input_bytes</span> <span class="o">+=</span> <span class="n">file_bytes</span>
</span></span><span class="line"><span class="cl">    <span class="n">file_count</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="n">filename</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="s1">&#39;r&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;Reference Filename: </span><span class="si">{</span><span class="n">filename</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">```zig</span><span class="se">\b</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="se">\b</span><span class="s2">```</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">outpath</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig_docs_with_filenames.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">outpath</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">lines</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">line</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice that in this case I&rsquo;m formatting each example starting with &ldquo;Reference filename: {filename}&rdquo; and then adding the code in a Markdown block with 3 backticks. Is it a good format? I don&rsquo;t know yet. Making training material IS HARD.</p>
<p>That resulted in another big file of <strong>152 kilobytes</strong>. I hope it doesn&rsquo;t confuse more than help. And I didn&rsquo;t stop there. I wanted to add the entire source code of the <a href="https://github.com/ziglang/zig/tree/master/lib/std"target="_blank" rel="noopener">Standard Library</a> and I even wrote the script, downloaded and compiled it into a structured format, but the final file was <strong>15 MEGABYTES</strong>. Unfortunately I had to comment it out of my training script. Now I hit a limitation of my RTX 4090: there&rsquo;s not enough VRAM to train all of that.</p>
<p>As I&rsquo;ve repeated before, the 4090 only has 24GB of VRAM. The Qwen3-8b model alone takes 16GB (no, the number of parameters influences the size but it doesn&rsquo;t mean 1 parameter is always 1 byte). There has to be memory left over to do all the training calculations, and it will spend 21GB while training. If I throw in that 15MB of material, this happens:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lawgc11lm8bv08hc2u4ms62v4hm0" alt="out of memory vram"  loading="lazy" /></p>
<p>To train with all of that, it has to be remote, on a RunPod with an A40 like I showed in another blog post, at the very least. Or with faster GPUs like the H100, with 80GB, and more power to finish a little faster. So, I&rsquo;m already limited on quality training material and now limited on the amount of material I can use.</p>
<p>Since I couldn&rsquo;t use the source code (which would have real idiomatic 0.14.0 code examples), I looked for other examples to add and found this <a href="https://github.com/zigcc/zig-cookbook"target="_blank" rel="noopener">Zig Cookbook</a> repository, which is organized as problems in a Markdown file with solutions in 0.14-compatible code. I wrote this other script to organize everything into a single file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">lines</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">prob_file</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s1">&#39;raw_data/zig-cookbook/book-src/*.md&#39;</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Extract the nn-nn from the problem filename (e.g., 01-05-matrix-mul.md -&gt; 01-05)</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="n">prob_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">base</span> <span class="o">==</span> <span class="s2">&#34;SUMMARY.md&#34;</span> <span class="ow">or</span> <span class="n">base</span> <span class="o">==</span> <span class="s2">&#34;database.md&#34;</span> <span class="ow">or</span> <span class="n">base</span> <span class="o">==</span> <span class="s2">&#34;intro.md&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">base</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">match_part</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;-&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="s1">&#39;-&#39;</span> <span class="o">+</span> <span class="n">base</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;-&#39;</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>  <span class="c1"># e.g., 01-05</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">code_file</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;raw_data/zig-cookbook/src/</span><span class="si">{</span><span class="n">match_part</span><span class="si">}</span><span class="s2">.zig&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">code_file</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;WARN: No solution found for problem </span><span class="si">{</span><span class="n">prob_file</span><span class="si">}</span><span class="s2"> (expected </span><span class="si">{</span><span class="n">code_file</span><span class="si">}</span><span class="s2">)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">prob_file</span><span class="p">,</span> <span class="s1">&#39;r&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">pf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">problem_text</span> <span class="o">=</span> <span class="n">pf</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">code_file</span><span class="p">,</span> <span class="s1">&#39;r&#39;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">cf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">code_text</span> <span class="o">=</span> <span class="n">cf</span><span class="o">.</span><span class="n">read</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Compose chat format</span>
</span></span><span class="line"><span class="cl">    <span class="n">block</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="sa">f</span><span class="s2">&#34;&lt;|user|&gt;Solve this problem in Zig 0.14.0:</span><span class="se">\n</span><span class="si">{</span><span class="n">problem_text</span><span class="si">}</span><span class="se">\n</span><span class="s2">&lt;|endoftext|&gt;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">&#34;&lt;|assistant|&gt;This is the Zig 0.14.0 way to solve this:</span><span class="se">\n</span><span class="s2">```zig</span><span class="se">\n</span><span class="si">{</span><span class="n">code_text</span><span class="si">}</span><span class="se">\n</span><span class="s2">```</span><span class="se">\n</span><span class="s2">&lt;|endoftext|&gt;</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;raw_data/zig_cookbook_chat.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">out</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="o">.</span><span class="n">writelines</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Wrote </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span><span class="si">}</span><span class="s2"> problem/solution chat pairs to zig_cookbook_chat.txt&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In this case I&rsquo;m trying to see if using CHAT TAGS makes a difference. Not every model supports it. But Aider, for example, assembles prompts like this: with tags indicating &ldquo;who is speaking&rdquo; (system, user, assistant) and it seems to be an alternative to doing it in JSON format. So each problem/code looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;|user|&gt;### Measure the elapsed time between two code sections

[](https://github.com/zigcc/zig-cookbook/blob/main/book-src/03-01-elapsed-time.md#measure-the-elapsed-time-between-two-code-sections)
...
&lt;|endoftext|&gt;
&lt;|assistant|&gt;
...
fn expensive_function() void {
    // sleep 500ms
    time.sleep(time.ns_per_ms * 500);
}
&lt;|endoftext|&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Will this work well? I don&rsquo;t know 😆 As I said, I need to spend many more hours dedicating myself just to understanding good training material formatting.</p>
<p>Finally, I tried to make a short file of prompts like I mentioned at the start, to try to force the chat to answer that it knows about the existence of version 0.14 and what the changes are in that version (just the topics from the release notes).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ir04nsralua23ada3wk45xvavm97" alt="training prompts"  loading="lazy" /></p>
<p>This is how the so-called <strong>&ldquo;alignment&rdquo;</strong> is done. At a big company like OpenAI, they must have HUNDREDS of human beings to write questions and answers that they want to force the LLM to reply with. And we have to do the same thing here, otherwise the Zig 0.11 and 0.12 training already in the base model might still override the new material I&rsquo;m adding.</p>
<p>I understand there&rsquo;s no trivial way to say <em>&ldquo;ignore the old version and only pull from my new material&rdquo;</em>. That&rsquo;s not how it works. You &ldquo;influence&rdquo; the training (<strong>supervised training</strong>), but there are no &ldquo;guarantees&rdquo; that every variation of a question will give the same answer, unless you manually input most of the variations in the alignment.</p>
<pre><code>This is why I say DO NOT BELIEVE an LLM. It is not REASONING. It was FORCED to reply in a certain way by humans, during training. It literally has millions of PRE-PROGRAMMED ANSWERS already embedded in training. That's how you CONTROL the answers.
</code></pre>
<p>In my case, it&rsquo;s not enough to just upload the documentation, I should say in the prompts &ldquo;what are the language changes in Zig 0.14.0?&rdquo; and give the answer &ldquo;In Zig 0.14.0 we had the following language changes: Labeled Switch, Code Generation Properties, Decl Literals, Fields and Declarations Cannot Share Names, @splat Supports Arrays, Global Variables can be Initialized with Address of Each Other, @export Operand is Now a Pointer, New @branchHint Builtin, Replacing @setCold, &hellip;&hellip;.&rdquo; (yes, a lot of stuff really did change).</p>
<p>Anyway. I&rsquo;ll push my training procedure <a href="https://github.com/akitaonrails/qwen3-zig-lora-training"target="_blank" rel="noopener">to my GitHub</a>. There you&rsquo;ll find a <code>raw_data</code> directory where the material I compiled lives. There are scripts like <code>concat-zig-docs.py</code>, <code>concat-zig-stdlib.py</code> or <code>concat-zig-cookbook.py</code> to concatenate each group of material into a single big file as I explained. And at the end, there&rsquo;s <code>train-zig.py</code> which actually loads all the datasets and does the training.</p>
<p>There&rsquo;s another important factor: compared to the more than 800 kilobytes of material I compiled, the prompts I mentioned are still very small, only 8.3 kilobytes. We need to try to do <strong>OVER-SAMPLING</strong>. Ideally I should write at least 10x more real prompts manually, but I really don&rsquo;t have that much patience. What I&rsquo;m going to do is repeat these prompts several times:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">json_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/training-prompts.json&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">text_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig-0.14.0-docs.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">text2_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig-0.14.0-release-notes.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">text3_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig_docs_with_filenames.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">text4_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig_cookbook_chat.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">text5_dataset_path</span> <span class="o">=</span> <span class="s2">&#34;raw_data/zig_stdlib_concat.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Prepare HuggingFace dataset for text</span>
</span></span><span class="line"><span class="cl"><span class="n">text5_dataset</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="n">data_files</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;train&#34;</span><span class="p">:</span> <span class="n">text5_dataset_path</span><span class="p">})[</span><span class="s1">&#39;train&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">json_dataset</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="n">data_files</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;train&#34;</span><span class="p">:</span> <span class="n">json_dataset_path</span><span class="p">})[</span><span class="s1">&#39;train&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># oversample the prompts to try to influence the bias towards this new documentation</span>
</span></span><span class="line"><span class="cl"><span class="n">N</span> <span class="o">=</span> <span class="mi">100</span>  <span class="c1"># Oversampled target size</span>
</span></span><span class="line"><span class="cl"><span class="n">indices</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">random</span><span class="o">.</span><span class="n">choice</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">json_dataset</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="n">N</span><span class="p">,</span> <span class="n">replace</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">oversampled_ds</span> <span class="o">=</span> <span class="n">json_dataset</span><span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="n">indices</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># text5 (std) won&#39;t fit in the 4090 so removed from combined</span>
</span></span><span class="line"><span class="cl"><span class="n">combined_dataset</span> <span class="o">=</span> <span class="n">concatenate_datasets</span><span class="p">([</span><span class="n">text_dataset</span><span class="p">,</span> <span class="n">text2_dataset</span><span class="p">,</span> <span class="n">text3_dataset</span><span class="p">,</span> <span class="n">text4_dataset</span><span class="p">,</span> <span class="n">oversampled_ds</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s how you load the training data as <code>datasets</code>. And I use Numpy&rsquo;s <code>random_choice</code> to over-sample the JSON file and artificially make it bigger, repeating the lines randomly. And in the end we have a <code>combined_dataset</code> that is everything concatenated, about almost 900 kilobytes of material.</p>
<p>Then read the <code>train_lora.py</code> file, but with the combined dataset ready, now it&rsquo;s the actual training part:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Tokenize your dataset FIRST:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">tokenize_function</span><span class="p">(</span><span class="n">example</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">tokenizer</span><span class="p">(</span><span class="n">example</span><span class="p">[</span><span class="s2">&#34;text&#34;</span><span class="p">],</span> <span class="n">truncation</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">padding</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tokenized_dataset</span> <span class="o">=</span> <span class="n">combined_dataset</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">tokenize_function</span><span class="p">,</span> <span class="n">batched</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Training hyperparameters</span>
</span></span><span class="line"><span class="cl"><span class="n">training_args</span> <span class="o">=</span> <span class="n">SFTConfig</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">output_dir</span> <span class="o">=</span> <span class="s2">&#34;qwen3-zig-lora&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">per_device_train_batch_size</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">gradient_accumulation_steps</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">warmup_steps</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">num_train_epochs</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">learning_rate</span> <span class="o">=</span> <span class="mf">2e-4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fp16</span> <span class="o">=</span> <span class="ow">not</span> <span class="n">is_bfloat16_supported</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">bf16</span> <span class="o">=</span> <span class="n">is_bfloat16_supported</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">optim</span> <span class="o">=</span> <span class="s2">&#34;adamw_8bit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">weight_decay</span> <span class="o">=</span> <span class="mf">0.01</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">lr_scheduler_type</span> <span class="o">=</span> <span class="s2">&#34;linear&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">seed</span> <span class="o">=</span> <span class="mi">3407</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">report_to</span> <span class="o">=</span> <span class="s2">&#34;none&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_seq_length</span> <span class="o">=</span> <span class="mi">2048</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">dataset_num_proc</span> <span class="o">=</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">packing</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span> <span class="c1"># Can make training 5x faster for short sequences.</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Trainer for unsupervised SFT</span>
</span></span><span class="line"><span class="cl"><span class="n">trainer</span> <span class="o">=</span> <span class="n">SFTTrainer</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">args</span><span class="o">=</span><span class="n">training_args</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">train_dataset</span><span class="o">=</span><span class="n">tokenized_dataset</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">processing_class</span><span class="o">=</span><span class="n">tokenizer</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">trainer</span><span class="o">.</span><span class="n">train</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">model</span><span class="o">.</span><span class="n">save_pretrained</span><span class="p">(</span><span class="s2">&#34;qwen3-zig-lora&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tokenizer</span><span class="o">.</span><span class="n">save_pretrained</span><span class="p">(</span><span class="s2">&#34;qwen3-zig-lora&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>First, we tokenize the dataset, we convert text into tokens (this is fast; in the chat you use every day, your text is converted into tokens like this too - the model doesn&rsquo;t understand text, it understands embeddings, which are these tokens translated into vectors).</p>
<p>In <code>SFTConfig</code> we configure the training parameters. I think most of them are default values, but there are two important ones:</p>
<ul>
<li>
<p>Epoch: One pass through the training material is called &ldquo;one Epoch&rdquo;. They say for little material (like my case), it&rsquo;s good to go more than 3 Epochs, up to 10 Epochs. More Epochs tries to make my new LoRA &ldquo;stick&rdquo; more. If the dataset were orders of magnitude larger, fewer Epochs would be better, because there&rsquo;s the problem of <strong>OVER-FITTING</strong>. It &ldquo;sticks&rdquo; too much and the model will memorize my material too much and just copy-paste directly. It depends on the type of material, depends on the amount of material. With 5 Epochs this material takes more than 1 hour to process, 10 Epochs then pushes it to more than 2 hours.</p>
</li>
<li>
<p>Learning Rate: it&rsquo;s the scale factor of how much we want the &ldquo;weights&rdquo; inside the model to change. How aggressively do we want this new information to change the model weights, so it actually &ldquo;learns&rdquo;? If it&rsquo;s too high, it&rsquo;ll start to &ldquo;forget&rdquo; what it already had in the prior training and create knowledge gaps (&ldquo;catastrophic forgetting&rdquo;). If it&rsquo;s too low, it changes little, and our LoRA won&rsquo;t have any significant effect.</p>
</li>
</ul>
<p>There&rsquo;s one more parameter that leaves me in doubt, <code>max_seq_length</code>, which serves to truncate overly long text. Theoretically the <code>unsloth</code> library knows how to split training material into smaller &ldquo;chunks&rdquo; (if it has obvious delimiters). But I don&rsquo;t know what happens with giant things, like the entire documentation. Whether it splits and uses it, or truncates and doesn&rsquo;t use it. There are many details that still need more studying. If it&rsquo;s truncating without me knowing, maybe I&rsquo;m feeding material it&rsquo;s not actually using, for example.</p>
<p>The old facts that already exist in training (Zig 0.11 and 0.12) are stronger than my material. So cranking up those parameters might help influence the final result more. That&rsquo;s the other part of the &ldquo;art&rdquo;: keeping adjusting these values to better fit the new knowledge. In a full training, from scratch, it&rsquo;s even more important. I don&rsquo;t know everything about this. What I said is the amateur level of training an LLM.</p>
<p>A LoRA is supposed to be small. With an 8B base model, with this training material size, the final LoRA will be in the 3 to 4GB range (I don&rsquo;t know if that&rsquo;s considered big or small). And that has to be enough to redirect the responses to the new knowledge. If you didn&rsquo;t know this, on sites like Hugging Face you&rsquo;ll find pre-trained LoRAs to use, you don&rsquo;t necessarily have to make one from scratch if there&rsquo;s already a ready one.</p>
<p>With the material in place, processed, organized, just run my <code>train_lora.py</code> and you&rsquo;ll see something like:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/s3rpq8b6a3h1p0qt60yazf042cr7" alt="training"  loading="lazy" /></p>
<p>And with so many Epochs and high Learning Rate, it will take a while! On my RTX 4090 it should take <strong>more than 1 hour training</strong> (with 5 Epochs), maybe more (I didn&rsquo;t time it), and while that&rsquo;s running it really will nail the GPU:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ivjodz630t9svrpvofuzpmgjanhe" alt="GPU usage"  loading="lazy" /></p>
<p>I know there are online services to train remote LoRAs, I think Hugging Face itself has APIs for it, or we can spin up a more powerful GPU on a RunPod. Obviously, you need access to the model to train on top of it (the LoRA parameters are dependent on the base model) so there&rsquo;s no way to make a LoRA of GPT 4.1 on your machine. That depends on whether OpenAI has APIs to train LoRAs remotely (I think they do, I didn&rsquo;t look).</p>
<p>Repeating: LoRAs are specific to the base model. Say you made a LoRA for the Qwen3-8b model (my case). Will that LoRA work for Qwen3-32b? No. It&rsquo;s specific. You have to load exactly the same base model when using your LoRA.</p>
<p>It&rsquo;s up to you, you don&rsquo;t need to have a local GPU just for this. It&rsquo;s an intensive process, that uses a lot of resources and a lot of energy and, depending on how much training material and how many epochs&hellip; go grab a coffee.</p>
<p>At the end, a <code>qwen3-zig-lora</code> directory will appear more or less like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/83h7fwsx0jsod3h17ruv4ag237r1" alt="lora"  loading="lazy" /></p>
<p>It records a checkpoint per epoch or something like that, from what I understand. Which means that depending on the strategy, if needed you can stop training and then continue from where you left off. I haven&rsquo;t tried to do this, but I imagine in a real training, which takes MONTHS, this is essential.</p>
<h3>Another Problem - Ollama&rsquo;s Limitations<span class="hx:absolute hx:-mt-20" id="another-problem---ollamas-limitations"></span>
    <a href="#another-problem---ollamas-limitations" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>To use a local model, as I already showed <a href="https://www.akitaonrails.com/en/2025/04/25/seu-proprio-co-pilot-gratuito-universal-que-funciona-local-aider-ollama-qwen"target="_blank" rel="noopener">in this other article</a>, we can load an LLM like Qwen3 with the Ollama server, which will spin up a web server and serve APIs in the OpenAI standard. Then just point a tool like Aider at it and done, we have a code assistant running locally.</p>
<p>The problem: LoRA support in Ollama is limited (it may be lack of research on my part, but I didn&rsquo;t find a quick solution). They use a proprietary format for the models, and not <code>.safetensors</code> files like those on Hugging Face. Anyway, I hit that problem and I think it&rsquo;s not easy to proceed with Ollama.</p>
<p>So we have to switch and, fortunately, there are options. The most popular alternative is <a href="https://blog.vllm.ai/2023/06/20/vllm.html"target="_blank" rel="noopener">vLLM</a>. It does the same thing: loads models and spins up an HTTP API server in the OpenAI standard. The advantage is it&rsquo;s more flexible (works with Hugging Face models), more advanced, more customizable and the main thing: it can load <a href="https://docs.vllm.ai/en/latest/features/lora.html"target="_blank" rel="noopener">models with LoRAs</a>. Check the documentation at the previous link, but in my case I think it&rsquo;s pretty simple.</p>
<p>And for those thinking about deploying for multiple users, vLLM is more performant and faster than Ollama too. Even for RAG solutions: Ollama only accepts prompt injection, while vLLM will integrate RAG better via OpenAI/proxy/Langchain for example.</p>
<p>You may need to study the options a bit more, but technically speaking, it&rsquo;s superior. I always think of Ollama more for running locally on your own machine, but not for production. vLLM I &ldquo;think&rdquo; would be better for that purpose - worth researching later if you&rsquo;re interested.</p>
<p>My training, in the end, will generate a directory called <code>qwen3-zig-lora</code> and with vLLM we can spin up the server like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># pip install vllm[all]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">vllm serve Qwen/Qwen3-8B <span class="se">\
</span></span></span><span class="line"><span class="cl">    --enable-lora <span class="se">\
</span></span></span><span class="line"><span class="cl">    --lora-modules <span class="nv">ziglora</span><span class="o">=</span>./qwen3-zig-lora <span class="se">\
</span></span></span><span class="line"><span class="cl">    --max_model_len <span class="m">8192</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To test that it&rsquo;s up, we can run a simple curl command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ curl localhost:8000/v1/models <span class="p">|</span> jq .</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And we&rsquo;ll have the lora listed as if it were just another model. See <code>ziglora</code> at the bottom:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;object&#34;</span><span class="p">:</span> <span class="s2">&#34;list&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen/Qwen3-8B&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;object&#34;</span><span class="p">:</span> <span class="s2">&#34;model&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;created&#34;</span><span class="p">:</span> <span class="mi">1746207038</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;owned_by&#34;</span><span class="p">:</span> <span class="s2">&#34;vllm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;root&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen/Qwen3-8B&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;parent&#34;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;max_model_len&#34;</span><span class="p">:</span> <span class="mi">8192</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;permission&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="err">...</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;ziglora&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;object&#34;</span><span class="p">:</span> <span class="s2">&#34;model&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;created&#34;</span><span class="p">:</span> <span class="mi">1746207038</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;owned_by&#34;</span><span class="p">:</span> <span class="s2">&#34;vllm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;root&#34;</span><span class="p">:</span> <span class="s2">&#34;./qwen3-zig-lora&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;parent&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen/Qwen3-8B&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;max_model_len&#34;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;permission&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="err">...</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To chat with it, we can directly use the API with curl and send the message to the LoRA:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/f1etshmornpoawgci7ndb7791r3a" alt="curl lora"  loading="lazy" /></p>
<p>And pay attention to the response. I asked which is the most recent version of Zig it&rsquo;s familiar with and the answer was <em>&ldquo;I am familiar with the Zig language up to version 0.14.0 from 2025.&rdquo;</em></p>
<p>I can&rsquo;t say if it&rsquo;s true 😂 this only means that the weight of my prompt over-sampling with canned answers is working. Maybe it didn&rsquo;t learn anything about Zig 0.14.0 but it learned to reply that it knows. And, again, this is why you shouldn&rsquo;t trust LLMs: having a canned answer forced in alignment doesn&rsquo;t mean it actually &ldquo;knows&rdquo; what it&rsquo;s talking about, I just forced it to answer with conviction that yes. And that part worked.</p>
<p>Qwen3-8b has a tiny context window, only 8K tokens. So in <code>.aider.model.settings.yaml</code> you have to configure it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hosted_vllm/ziglora</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">edit_format</span><span class="p">:</span><span class="w"> </span><span class="l">diff</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">use_repo_map</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">extra_params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">max_tokens</span><span class="p">:</span><span class="w"> </span><span class="m">8192</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">temperature</span><span class="p">:</span><span class="w"> </span><span class="m">0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">accepts_settings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">reasoning_effort</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To run Aider, I&rsquo;m temporarily doing it this way:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">OPENAI_API_BASE</span><span class="o">=</span>http://localhost:8000/v1 aider --watch-files --model hosted_vllm/ziglora --reasoning-effort none --verbose</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Since it&rsquo;s an API compatible with OpenAI&rsquo;s, I just point to localhost in the environment variable. And since it&rsquo;s a vLLM model instead of &ldquo;openai/&hellip;&rdquo; you have to use &ldquo;hosted_vllm/&hellip;&rdquo;. Also to save tokens, I&rsquo;m turning off Aider&rsquo;s repo-maps and trying to turn off the model&rsquo;s thinking with <code>-reasoning-effort none</code> but I don&rsquo;t know if it works, I still see it doing some long thinking.</p>
<p>I would have preferred to use the Qwen3-14b model but as I said, it wouldn&rsquo;t fit on my 4090, with the 8B I&rsquo;m already at the limit, and I wasn&rsquo;t in the mood to spin one up on RunPod. Plus I had no idea how many times I&rsquo;d need to repeat the training (as I&rsquo;m writing this post, I&rsquo;ve already repeated it at least 4 times, each taking over 1 hour). At least running locally I know I&rsquo;m not being charged a lot for some online service. Later, if it actually works, I&rsquo;ll try with a bigger model, more material, on a more powerful machine with more VRAM to fit it.</p>
<h3>Tangent: Learning a Language<span class="hx:absolute hx:-mt-20" id="tangent-learning-a-language"></span>
    <a href="#tangent-learning-a-language" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I suspect my training material is still very weak. Let&rsquo;s think:</p>
<p>If you only read the documentation (let&rsquo;s say you memorize it), if you only read small examples like the Cookbook (let&rsquo;s say you memorize it), if you only read a release note. In the end can we say you&rsquo;re now a senior Zig dev? Hardly. For an LLM it&rsquo;s also not that simple. It needs MUCH more material.</p>
<p>An example would be reading the entire source code of the Standard Library, to really know how each structure, each function really works under the hood. And even that is little.</p>
<p>You&rsquo;d need to build a scraper to filter all of StackOverflow looking for Zig 0.14 problems and solutions. Pain points in converting code from 0.12 to 0.14. As much as possible. You learn like that: having a problem and searching for another person who had the same problem in the past, to see how they solved it.</p>
<p>You&rsquo;d need to fetch all the Issues in the repository. Bug reports or false positives of not understanding the documentation, for example. How they were resolved is the important part.</p>
<p>You&rsquo;d need to search for more than just a Cookbook. If there are books, courses, exercises, you&rsquo;d need to organize all of that in training.</p>
<p>More than that, you&rsquo;d need to know C/C++ the same way, you&rsquo;d need to know the GCC and Clang compilers. You&rsquo;d need to know all the C/C++ problems that Zig says it improves, in various code examples. Learning Zig at a senior level isn&rsquo;t just Zig, it&rsquo;s the whole rest of the ecosystem.</p>
<p>You&rsquo;d need to see how, for example, in Rust various problems are solved and have similar solutions in Zig, to have a comparison point, especially in hard problems involving performance optimization, memory management, concurrency management, security, etc.</p>
<p>You&rsquo;d need to go after the main open source projects that use new Zig, classify what problems they solve, read all the source code. All of this would need to be in the training material, just like a programmer would also have to do to go from amateur to advanced, from junior to senior.</p>
<p>I don&rsquo;t know what materials were used in a Qwen (or Claude, or Gemini) to train Zig. I hope it was at least this. Otherwise it really won&rsquo;t be able to write real Zig code.</p>
<p>Except that much material starts reaching the gigabytes-of-data category to train on. I wouldn&rsquo;t know anymore whether it still fits to keep using LoRAs or whether it already falls into the category of creating checkpoints on top of the model itself (everything in the same LLM &ldquo;big file&rdquo;). These are doubts I still don&rsquo;t have answers to.</p>
<h3>First Impressions of the LoRA<span class="hx:absolute hx:-mt-20" id="first-impressions-of-the-lora"></span>
    <a href="#first-impressions-of-the-lora" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Unfortunately, since I used Qwen3-8b as the base, I was also limited to its super short context window, only 8192 tokens. So I can&rsquo;t use Aider, because just the system prompt it uses to instruct the model already consumes almost everything and there&rsquo;s no room left for it to reply. So my tests were limited because I had to chat with the model directly through the API, using <code>curl</code> and <code>jq</code> to see the responses.</p>
<p>That&rsquo;s what the <code>--max-model-len</code> in the call to spin up vLLM is for, otherwise it tries to spin up a too-large default window (40k) and blows out my RTX 4090&rsquo;s VRAM:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5sb1dj2x0st18wsbyecwef7lbwk6" alt="vllm out of memory"  loading="lazy" /></p>
<p>Notice how I&rsquo;m sending super short prompts: there&rsquo;s no additional context from RAG or anything like that to try to teach Zig 0.14. Anything it replies is permanently in the LoRA training now.</p>
<p>Repeating the first test:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;model&#34;: &#34;ziglora&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hello, which most recent version of the Zig language are you familiar with?&#34;}]                    }&#39;</span> <span class="p">|</span> jq</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here&rsquo;s the response (just the message, I removed the JSON to not make it too long here):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;&lt;think&gt;
I have knowledge of Zig up to version 0.14.0, released in 2025. My training data includes documentation for Zig version 0.14.0, so I understand its new features, such as the default C Library being now LazyLoaded, and the new `@hasDecl` builtin. However, I am not aware of any changes made to Zig after that release. If you have a question about Zig 0.14.0 or earlier, I&#39;ll be happy to help! If you have a question about Zig 0.15.0 or newer, I recommend checking the official documentation or community resources for the latest information.
&lt;/think&gt;

I have knowledge of Zig up to version 0.14.0, released in 2 GitHub Oct 2025. I know about its new features, such as the default C Library being now LazyLoaded, and the new `@hasDecl` builtin. However, I am not aware of any changes made to Zig after that release. If you have a question about Zig 0.14.0 or earlier, I&#39;ll be happy to help! If you have a question about Zig 0.15.0 or newer, I recommend checking the official documentation or community resources for the latest information.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See how the &ldquo;deep thinking&rdquo; or &ldquo;reasoning&rdquo; part is returned. This model does this and it&rsquo;s bad because the context window is small and these &ldquo;think&rdquo; tokens take up space in the context. See how it managed to answer that it knows 0.14 and even some of the new stuff in it. Let&rsquo;s continue:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ curl http://localhost:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;model&#34;: &#34;ziglora&#34;, &#34;temperature&#34;: &#34;0.1&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Can you quickly explain the new @splat support for Arrays in Zig 0.14.0? can you give me a code example&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">        }&#39;</span> <span class="p">|</span> jq .</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And here&rsquo;s the response:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;think&gt;
Okay, the user is asking about the new @splat functionality for Arrays in Zig 0.14.0. First, I need to recall what changes were introduced in that version regarding Arrays and @splat. From what I remember, prior to 0.14, Arrays couldn&#39;t be directly splatted into a tuple because the splat operator required a known, fixed number of elements. The new feature allows Arrays to be splatted into tuples, similar to how slices work with tuples.

So, the user wants an explanation and a code example. Let me think of a simple demonstration. Maybe creating an array and then using @splat to convert it into a tuple. I should also mention the syntax, like how the number of arguments in the tuple matches the array length. Oh, and note that this is a new feature in 0.14, so it&#39;s important to mention the version. Let me make sure the example is clear and concise. I&#39;ll show the array initialization, then the splatted tuple, and then use them in a function call to illustrate the point. That should cover the basics without overwhelming the user. Also, I should explain why this is useful—making the code more readable and avoiding the need for explicit tuple syntax. Alright, that should do it.
&lt;/think&gt;

Here&#39;s an example demonstrating `@splat` with Arrays in Zig 0.14.0:  

const std = @import(\&#34;std\&#34;);  

const Color = struct {  
    red: u8,  
    green: u8,  
    blue: u8,  
};  

const red = Color{ .red = 255, .green = 0, .blue = 0 };  
const green = Color{ .red = 0, .green = 255, .blue = 0 };  
const blue = Color{ .red = 0, .green = 0, .blue = 255 };  

const palette = .{ red, green, blue };  // Array of 3 elements  

const color_func = @splat(palette);  // Result type: (Color, Color, Color)  

pub fn main() !void {  
    // Use the splatted tuple!  
    _ = color_func;  
}  

### Key Points:  
- `@splat` infers the tuple element type from the array type.  
- The number of elements in the tuple matches the array length.  
- This syntax is available in Zig 0.14.0 and simplifies working with Arrays.  

This pattern is particularly useful when passing multiple related values to functions or initializing structs.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The response itself seems pretty okay. It didn&rsquo;t truncate, no Chinese characters showed up (that happened in one of my failed trainings). Let&rsquo;s compare it with the official release note on the site:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/fuks9uthx1l56sfpcltclhlx4n2h" alt="release note splat"  loading="lazy" /></p>
<p>To be honest, I can&rsquo;t say if it&rsquo;s correct (I don&rsquo;t know Zig yet either 🤣). But at a glance I think it made up waffle, it doesn&rsquo;t look right.</p>
<p>Now let&rsquo;s see the harder test: trying to write a small program.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"> curl http://localhost:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;model&#34;: &#34;ziglora&#34;, &#34;temperature&#34;: &#34;0.1&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Can you give me a Zig 0.14 code example of a simple program that acts like an interactive chat, so the user can input messages. then the message is parsed to see if there are commands such as &#39;</span>/list<span class="s1">&#39;, which will list the files in the current directory, or &#39;</span>/cat<span class="s1">&#39; which will read the file asked, or &#39;</span>/quit<span class="s1">&#39; to exit the chat?&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">      }&#39;</span> <span class="p">|</span> jq </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The response was VERY LONG (lucky it didn&rsquo;t actually blow the context). I&rsquo;ll leave it in full so you can see how the model with the lora loaded managed to respond:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;think&gt;
Okay, let&#39;s see. The user wants a Zig program that acts like an interactive chat, handling commands like /list, /cat, and /quit. They specified using Zig 0.14, so I need to make sure the code is compatible with that version. 

First, I&#39;ll outline the main components. The program needs to read user input line by line, parse each line for commands, and execute the corresponding actions. 

For handling input, I&#39;ll use std.process.argsAlloc to get command-line arguments, but wait, in this case, the program is meant to be interactive, so maybe that&#39;s not the right approach. Oh, right, the user wants to input messages interactively, not pass them as command-line arguments. So I should read from stdin line by line.

So, I&#39;ll start by reading lines using a loop. Each line will be checked for commands. The commands are /list, /cat, /quit. 

For /list, I need to list files in the current directory. Using std.fs.Dir.openDir on the current directory, then iterating over the entries. But I have to handle errors properly. 

For /cat, the user will input something like /cat filename, so I need to parse the filename. Then open the file, read its contents, and display them. Again, error handling is important here. 

/quit should exit the loop, ending the program. 

Now, structuring the code. I&#39;ll use a while loop that reads a line at a time. Each iteration will process the command. 

Wait, but how to read lines interactively? Using std.process.stdin, perhaps. So, I&#39;ll create a Reader for stdin, then read lines into a string. 

But in Zig, reading lines can be done with the readline function, but I need to handle EOF. However, in an interactive shell, the user can type Ctrl&#43;D (EOF on Unix) to exit, but maybe I should handle that gracefully. 

So, the main loop will look like this:

var line = std.ArrayList(u8).init(allocator); 
while (true) { 
    if (!readLine(allocator, &amp;line)) break; 
    processLine(line); 
} 

But how to implement readLine? Maybe using a function that reads until a newline is encountered, handling errors. 

Then, for each line, split the line into parts. For example, /list is a single command, but /cat needs the filename. So, splitting the line by spaces, then checking the first part. 

Wait, but /cat needs the filename as the second argument. So, for /cat, the line should be split into parts, and the second part is the filename. But what if the filename has spaces? Probably, the code should split on the first space, since the command is the first token. 

So, for each line:

var parts = line.split(\&#34; \&#34;); 
var command = parts[0]; 

Then, handle each case. 

Now, error handling. For example, when listing files, if the directory cannot be opened, we should print an error message. Similarly for file reading. 

Also, need to free the allocator when done. 

Putting it all together. Let me draft the code. 

First, the imports. I&#39;ll need std&#39;s process, fs, and so on. 

Then, the main function. 

But wait, in Zig, the main function can be written with a while loop reading lines. 

Wait, but how to read lines interactively? Using a Reader. 

So, here&#39;s a possible structure:

const std = @import(\&#34;std\&#34;); 

export fn main() !void { 
    const stdin = std.io.getStdIo(); 
    var reader = stdin.reader(); 

    var line = std.ArrayList(u8).init(allocator); 

    while (true) { 
        const input = try reader.readUntilAlloc(allocator, &#39;\
&#39;, 4096); 
        if (input.len == 0) break; 
        // process the line here 
        // ... 
        line.clear(); 
    } 

    // ... 
} 

Wait, but this code may not handle all cases. Also, after reading, we need to process the line. 

But perhaps using a loop with readline function. However, in Zig 0.14, the std.io.getStdIo().reader() can be used. 

Alternatively, using a loop with a std.io.BufferedReader. 

But maybe the simplest way is to read line by line using the reader.readUntilAlloc function. 

So, in the code, I&#39;ll read each line, then split into parts. 

Now, handling the commands. 

For /list: list the files in the current directory. 

For that, I can use std.fs.cwd() to get the current directory, then openDir. 

Then, iterate over the entries. 

But I need to handle errors. So, for example:

var dir = try current_dir.openDir(.{}); 
var iter = try dir.iterate(); 
while (try iter.next()) |entry| { 
    // process entry.name 
} 

But I have to handle all possible errors here. 

For /cat, I need to take the second part of the line. So, if the line is \&#34;/cat filename\&#34;, then the second part is the filename. But what if there are multiple spaces? Like \&#34;/cat  file name\&#34;? In that case, the code would split into parts, and the second part is \&#34;file\&#34;, but that&#39;s not correct. However, this code would not handle such cases correctly. But for simplicity, maybe we can assume that the filename is the rest of the line after the command. 

Alternatively, we can take the entire line after the command. For example, for /cat, the arguments are the rest of the line. 

But splitting into parts may not be the best approach. Maybe using the first token to determine the command, and then the rest of the line as arguments. 

So, for example:

if command == \&#34;/list\&#34; { 
    // list files 
} else if command == \&#34;/cat\&#34; { 
    if (parts.len &lt; 2) { 
        print(\&#34;Usage: /cat &lt;filename&gt;\
\&#34;, .{}); 
        continue; 
    } 
    const filename = parts[1]; 
    // read file 
} else if command == \&#34;/quit\&#34; { 
    return; 
} 

But again, this approach may not handle filenames with spaces correctly. However, for the scope of this example, it&#39;s acceptable. 

Now, putting all together. 

Also, need to handle the case where the line is empty, so we can just continue. 

Now, the code. 

But first, the error handling. For example, when opening a directory, if it fails, we should print an error message. 

Similarly, when reading a file, if it fails, print an error message. 

Now, the code. 

But wait, in Zig, the main function returns a !void, so we need to handle any errors. 

Putting it all together, here&#39;s the code example. 

I&#39;ll also include the necessary error handling. 

Testing for /list, /cat, /quit commands. 

Now, I think this should work. Let me write the code.
&lt;/think&gt;

Here&#39;s a Zig 0.14 compliant example of an interactive chat program that handles `/list`, `/cat`, and `/quit` commands:  


const std = @import(\&#34;std\&#34;);  

export fn main() !void {  
    const stdin = std.io.getStdIo();  
    const reader = stdin.reader();  

    var line = std.ArrayList(u8).init(allocator);  

    while (true) {  
        const input = try reader.readUntilAlloc(allocator, &#39;\
&#39;, 4096);  
        if (input.len == 0) break;  
        line.appendSlice(input);  
        line.append(&#39;\
&#39;);  

        const cmd = parseCommand(input);  
        switch (cmd) {  
            .List =&gt; {  
                if (std.fs.cwd().openDir(.{}, .{})) |dir| {  
                    var iter = dir.iterate();  
                    while (true) |entry| {  
                        if (entry.file_name.len == 0) continue;  
                        print(\&#34;  {}\
\&#34;, .{entry.file_name});  
                        if (entry.file_name == \&#34;README.md\&#34;) break : \&#34;loop\&#34;;  
                    }  
                    iter.deinit();  
                } else |err| {  
                    print(\&#34;Error listing directory: {s}\
\&#34;, .{@errorName(err)});  
                }  
            },  
            .Cat =&gt; {  
                if (input.len &lt; 2) {  
                    print(\&#34;Usage: /cat &lt;filename&gt;\
\&#34;, .{});  
                    continue;  
                }  
                const filename = input[input.find(&#39;/&#39;) &#43; 1..];  
                if (std.fs.cwd().openFile(filename, .{})) |file| {  
                    var reader = file.reader();  
                    var buffer = std.ArrayList(u8).init(allocator);  
                    while (true) {  
                        const chunk = try reader.readAlloc(allocator, 4096);  
                        try buffer.appendSlice(chunk);  
                        if (chunk.len &lt; 4099) break :\&#34;\&#34; buffer;  
                    }  
                    print(\&#34;{s}\
\&#34;, .{buffer.join(\&#34; \&#34;)});  
                    buffer.deinit();  
                } else |err| {  
                    print(\&#34;Error reading file: {s}\
\&#34;, .{@errorName(err)});  
                }  
            },  
            .Quit =&gt; {  
                print(\&#34;Goodbye!\
\&#34;, .{});  
                return;  
            },  
            .Unknown =&gt; {  
                print(\&#34;Unknown command: {s}\
\&#34;, .{input});  
                continue;  
            }  
        }  
        line.clear();  
    }  
}  

const Command = enum {  
    List,  
    Cat,  
    Quit,  
    Unknown  
};  

fn parseCommand(input: []u8) Command {  
    if (input.len == 0) return .Unknown;  
    if (input[0] == &#39;/&#39;) {  
        if (input == \&#34;/list\&#34;) return .List;  
        if (input == \&#34;/cat\&#34;) return .Cat;  
        if (input == \&#34;/quit\&#34;) return .Quit;  
    }  
    return .Unknown;  
}  


### Key Features:  
- **Interactive Input**: Uses `std.io.getStdIo().reader()` to read lines from standard input.  
- **Command Parsing**: Recognizes `/list`, `/cat`, `/quit`, and other unknown commands.  
- **File System Interaction**: Lists files in the current directory and reads files using `std.fs.cwd()`.  
- **Error Handling**: Properly handles file and directory operations with error sets.  

This example demonstrates the core functionality required for an interactive chat bot with Zig 0.14. You can extend it further by adding more commands or improving error handling.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Okay, but does that code at least compile? Unfortunately not:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">❯ zig build
</span></span><span class="line"><span class="cl">install
</span></span><span class="line"><span class="cl">└─ install foo
</span></span><span class="line"><span class="cl">   └─ zig build-lib foo Debug native <span class="m">1</span> errors
</span></span><span class="line"><span class="cl">error: unable to load <span class="s1">&#39;src/root.zig&#39;</span>: FileNotFound
</span></span><span class="line"><span class="cl">error: the following <span class="nb">command</span> failed with <span class="m">1</span> compilation errors:
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">install
</span></span><span class="line"><span class="cl">└─ install foo
</span></span><span class="line"><span class="cl">   └─ zig build-exe foo Debug native <span class="m">3</span> errors
</span></span><span class="line"><span class="cl">src/main.zig:7:39: error: use of undeclared identifier <span class="s1">&#39;allocator&#39;</span>
</span></span><span class="line"><span class="cl">    var <span class="nv">line</span> <span class="o">=</span> std.ArrayList<span class="o">(</span>u8<span class="o">)</span>.init<span class="o">(</span>allocator<span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                                      ^~~~~~~~~
</span></span><span class="line"><span class="cl">src/main.zig:73:19: error: cannot compare strings <span class="nv">with</span> <span class="o">==</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="o">(</span><span class="nv">input</span> <span class="o">==</span> <span class="s2">&#34;/list&#34;</span><span class="o">)</span> <span class="k">return</span> .List<span class="p">;</span>
</span></span><span class="line"><span class="cl">            ~~~~~~^~~~~~~~~~
</span></span><span class="line"><span class="cl">/home/akitaonrails/.local/share/mise/installs/zig/0.14.0/lib/std/start.zig:643:46: error: <span class="s1">&#39;main&#39;</span> is not marked <span class="s1">&#39;pub&#39;</span>
</span></span><span class="line"><span class="cl">    const <span class="nv">ReturnType</span> <span class="o">=</span> @typeInfo<span class="o">(</span>@TypeOf<span class="o">(</span>root.main<span class="o">))</span>.@<span class="s2">&#34;fn&#34;</span>.return_type.?<span class="p">;</span>
</span></span><span class="line"><span class="cl">                                         ~~~~^~~~~
</span></span><span class="line"><span class="cl">src/main.zig:3:8: note: declared here
</span></span><span class="line"><span class="cl"><span class="nb">export</span> fn main<span class="o">()</span> !void <span class="o">{</span>
</span></span><span class="line"><span class="cl">~~~~~~~^~~~~~~~~~~~~~~
</span></span><span class="line"><span class="cl">/home/akitaonrails/.local/share/mise/installs/zig/0.14.0/lib/std/start.zig:616:20: note: called from here
</span></span><span class="line"><span class="cl">    <span class="k">return</span> callMain<span class="o">()</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">           ~~~~~~~~^~
</span></span><span class="line"><span class="cl">/home/akitaonrails/.local/share/mise/installs/zig/0.14.0/lib/std/start.zig:571:36: note: called from here
</span></span><span class="line"><span class="cl">    std.posix.exit<span class="o">(</span>callMainWithArgs<span class="o">(</span>argc, argv, envp<span class="o">))</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                   ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
</span></span><span class="line"><span class="cl">error: the following <span class="nb">command</span> failed with <span class="m">3</span> compilation errors:</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>At least I don&rsquo;t think it&rsquo;s my LoRA that broke it: it already couldn&rsquo;t write Zig code that compiled before. This Qwen3 model only has 8B parameters, don&rsquo;t forget. It&rsquo;s much weaker. My LoRA didn&rsquo;t make it worse, it&rsquo;s the same crappy code as before, but at least we have evidence that the training changed the responses more or less like we wanted.</p>
<p>Some things from the training, we can see it did manage to access. This new knowledge <strong>doesn&rsquo;t take up space in the context, unlike a RAG or prompt</strong> and that was the proof of concept: I trained it, managed to load it, got to access that information and, even with a weaker base (8B) and even with a limited context window (8k tokens), it still seems to be working &ldquo;okay-ish&rdquo;, so it&rsquo;s a <strong>semi-success</strong>.</p>
<p>I&rsquo;d need to test training on a bigger model (14B) with more code examples (like those 15MB of its STD source code) and on a bigger GPU with more VRAM (like an A40 or H100).</p>
<h3>Conclusion: Semi-Success?<span class="hx:absolute hx:-mt-20" id="conclusion-semi-success"></span>
    <a href="#conclusion-semi-success" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I decided to publish this post before actually finishing all my test attempts. As you could see, some responses already came with the model answering Zig 0.14.0 stuff. But I still couldn&rsquo;t get good code out of just that. I don&rsquo;t know if it&rsquo;s because the 8B model is too weak for this. Or if my training material could be better formatted. Or if I really was missing making more descriptive and specialized prompts. Or if there are parameters I have to tune (more epochs? higher learning rate? something else?). Anyway, there are MANY possibilities to explore and research.</p>
<p>The goal of this post was more to show in practice how LLM models are TAMPERED WITH post-training, via fine-tuning. You&rsquo;ve &ldquo;heard&rdquo; that it could have this. I&rsquo;m proving that it really does and that you yourself can do it at home. You saw how it can already answer &ldquo;I know Zig 0.14&rdquo; but in practice not necessarily. LLMs do that: they&rsquo;re tuned to give convincing answers. Knowing that this happens, you can be more critical and understand <em>&ldquo;ah, that&rsquo;s alignment, but it actually doesn&rsquo;t know that much.&rdquo;</em> That&rsquo;s the point.</p>
<p>This is also the importance of having open source, open models, because the community can create new trainings on top to remove these alignments, force new alignments (that&rsquo;s why a single model can have so many different versions). For example, on Hugging Face itself there&rsquo;s a whole category of <a href="https://huggingface.co/collections/Umbra-AI/llms-uncensored-6531951e548eae0c99f4a534"target="_blank" rel="noopener">LLMs (Uncensored)</a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mom2pmui347tgdwzinkz8kq9wonp" alt="Uncensored"  loading="lazy" /></p>
<p>The most dedicated user is <a href="https://huggingface.co/TheBloke"target="_blank" rel="noopener">TheBloke</a>, follow him to see which LLM he recently unlocked. On his profile there are models like this <a href="https://huggingface.co/TheBloke/CodeLlama-70B-Python-GPTQ"target="_blank" rel="noopener">CodeLlama-70B-Python-GPTQ</a> which, theoretically, was post-trained to be stronger in Python code. And with this post now you have more of an idea of HOW it&rsquo;s done.</p>
<p>This can have many applications. If you&rsquo;re a researcher, maybe you can take it from here as motivation to go deeper where I still haven&rsquo;t gone deeper. Maybe doing specific programming training is harder, maybe you&rsquo;ll discover the trick I still haven&rsquo;t seen. But for prose texts it tends to be better. If your company has a private knowledge base, maybe it makes sense to create a private LoRA that runs on your infra. Anyway, the possibilities are endless.</p>
<p>Whoever figures out the best way to organize this training could create even better programming LoRAs, for Python 3.13, for Elixir 1.18, or for Go 1.24, all newer versions that, at the 2023 cut-offs of commercial LLMs, won&rsquo;t be there.</p>
<p>Why is the data so old? 2023 is already 2 years ago. Because it&rsquo;s hard to clean petabytes of data. If I already struggled to clean 900 kilobytes, imagine PETABYTES. It&rsquo;s not just throwing any data in any way and expecting the model to MAGICALLY learn everything. No: you have to organize, structure, clean this training data. The better that work, the better the chances of the model getting a little better.</p>
<p>It&rsquo;s hundreds of man-hours of manual work, scripts and much more to do this. It makes sense to have a gap of more than 1 year, because then it still takes MONTHS to process all of that using a datacenter of multiple H200s in parallel. A mere 900kb with an 8B parameter model already costs more than 1 hour of my poor 4090. Imagine petabytes paired with a 1 TRILLION parameter model. It&rsquo;s A LOT of processing.</p>
<p>And that&rsquo;s it, everything I wrote in my post was a single day&rsquo;s research, so if someone dedicates more than that, they should get even better results. It&rsquo;s worth studying.</p>
]]></content:encoded><category>llms</category><category>machine-learning</category><category>zig</category></item><item><title>Understanding the Basics of ComfyUI to Generate AI Images</title><link>https://akitaonrails.github.io/en/2025/04/20/understanding-the-basics-of-comfyui-to-generate-ai-images/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/20/understanding-the-basics-of-comfyui-to-generate-ai-images/</guid><pubDate>Sun, 20 Apr 2025 19:30:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; this post is now obsolete. I replaced the manual setup with a cleaner containerized project: &lt;a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener"&gt;&lt;strong&gt;akitaonrails/aitrepreneur-docker&lt;/strong&gt;&lt;/a&gt;. It runs Aitrepreneur&amp;rsquo;s stacks locally in Docker, with pinned versions, persistent models, and pre-loaded workflows. Check the README for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the &lt;a href="https://www.akitaonrails.com/en/2025/04/20/generating-ai-images-even-ghibli-style-with-docker-and-cuda/"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I explained my little project to spin up ComfyUI pre-configured and pre-loaded automatically using Docker Compose. Assuming you already have everything up and running, now it&amp;rsquo;s time to actually understand this beast called ComfyUI a little.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p><strong>Update:</strong> this post is now obsolete. I replaced the manual setup with a cleaner containerized project: <a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener"><strong>akitaonrails/aitrepreneur-docker</strong></a>. It runs Aitrepreneur&rsquo;s stacks locally in Docker, with pinned versions, persistent models, and pre-loaded workflows. Check the README for details.</p>

</blockquote>
<p>In the <a href="https://www.akitaonrails.com/en/2025/04/20/generating-ai-images-even-ghibli-style-with-docker-and-cuda/"target="_blank" rel="noopener">previous post</a> I explained my little project to spin up ComfyUI pre-configured and pre-loaded automatically using Docker Compose. Assuming you already have everything up and running, now it&rsquo;s time to actually understand this beast called ComfyUI a little.</p>
<p>I&rsquo;m no expert either, but I thought it would be cool to explain a few concepts that many people overlook.</p>
<h3>Pre-Trained Models<span class="hx:absolute hx:-mt-20" id="pre-trained-models"></span>
    <a href="#pre-trained-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every GPT out there, or Stable Diffusion or others, loads a vector database into the GPU&rsquo;s memory, into VRAM, in order to process. They are HUGE models, called 7B, 70B, 100B, etc. The &ldquo;B&rdquo; stands for &ldquo;Billions of Parameters&rdquo;. Parameters have no usable definition outside of math, they are simply numbers. Those numbers represent &ldquo;something&rdquo; inside millions of dimensions inside that giant file.</p>
<p>In a VERY ROUGH way, I think of them as compression artifacts. You take PETABYTES of books, images, texts, posts, etc, break it all into tokens and find &ldquo;relationships&rdquo; between one token and many other tokens, and you keep &ldquo;positioning&rdquo; them in space, but not in 3 dimensions, in millions of dimensions. AI models are &ldquo;Vector Space Models&rdquo; or VSMs, more or less like the index of an Elasticsearch if you&rsquo;ve ever played with relevance-based text search, or any &ldquo;Full Text Search&rdquo; plugin in Postgres or MS-SQL. They&rsquo;re all VSMs.</p>
<p>Here&rsquo;s how I think about it: it&rsquo;s as if the training process - the SUPER EXPENSIVE part, which takes MONTHS - takes all the information that&rsquo;s fed in and compiles a new &ldquo;dictionary&rdquo; for a new &ldquo;language&rdquo;. Now, when you &ldquo;talk&rdquo; to a GPT, first it needs to transform your text from your language into this internal &ldquo;language&rdquo; of its own, which is what we call an <strong>&ldquo;Embedding&rdquo;</strong>.</p>
<p>Any data can be transformed. Audio can be converted into text and from text into embeddings. Videos can be broken down into series of images with temporal information. Images can be deconstructed into elements like luminance, chrominance, etc. In the end, everything can be deconstructed into data structures, there&rsquo;s no magic.</p>
<p>Image generation models are trained differently. There are several focuses, focus on facial sharpness, focus on pose accuracy, focus on different styles like cartoon or painting, focus on categories like animals or objects. That&rsquo;s why there are dozens of models out there and lots of &ldquo;loras&rdquo;.</p>
<p>To generate an image from a prompt, first you need to decode your prompt into an embedding, and that&rsquo;s what things like a <strong>TEXT ENCODER</strong> in ComfyUI are for.</p>
<p>In the specific case of AI for images, the way I understand it is that they use something called a &ldquo;DIFFUSION&rdquo; model both to deconstruct the original image and to reconstruct a new image. It&rsquo;s not intuitive, but the reconstruction doesn&rsquo;t start on a blank canvas, the way it would for a human illustrator. It starts with an image filled with random &ldquo;noise&rdquo; like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rcvhb8m0boe3jetpy72kww2wmucw" alt="noise"  loading="lazy" /></p>
<p>The model starts from this noise and through a process called DIFFUSION it begins to redraw the image backwards, until it emerges from the chaos and arrives at an image.</p>
<p>What I &ldquo;THINK&rdquo;, what I &ldquo;GUESS&rdquo; is happening - without having read any paper, pure laziness - is that we try to influence this initial noise. For example, by extracting features from the image like a depth map, which looks like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qj0ba39q5xbs7xyei52h4qmmpr2d" alt="Depth Map"  loading="lazy" /></p>
<p>The original photo looked like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/6zionu0khn01gpudd9z0ws2mm8zx" alt="Original"  loading="lazy" />.jpg)</p>
<p>Extracting a Depth Map is an <strong>ALGORITHM</strong>, it&rsquo;s not &ldquo;AI&rdquo;. Any Photoshop or Premiere out there can do this with one hand tied behind its back. But the point is to explain that there&rsquo;s much more information in an image than what you - untrained as you are - can see is there.</p>
<p>Another well-known type of ALGORITHM is <strong>Canny Edge Detection</strong>, which produces an edge map, making it easier to know where something starts and ends in the image:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/std6jaroftohqjlc22mii9m7oso6" alt="Canny"  loading="lazy" /></p>
<p>So it goes more or less like this:</p>
<ul>
<li>just as a text is &ldquo;converted&rdquo; into an <strong>Embedding</strong>, vectors that capture the &ldquo;essence&rdquo; of the meaning of the text, dependent on the VSM of the pre-trained model, an image is &ldquo;converted&rdquo; internally into a <strong>LATENT</strong>, which because it&rsquo;s an image is a &ldquo;space&rdquo; (that&rsquo;s why people talk about Latent Space). We can&rsquo;t &ldquo;visualize&rdquo; Latents, because it&rsquo;s an internal representation that depends on the U-NET, the pre-trained model, the neural network that learns to &ldquo;denoise&rdquo; the Latent.</li>
<li>An image U-NET is roughly similar to a text model like GPT or LLaMA, the pre-trained, read-only model that will be used to produce the final result.</li>
<li>A GPT response is a continuation of the context of the previous conversation. It&rsquo;s not &ldquo;answering&rdquo; you directly, it&rsquo;s just &ldquo;continuing&rdquo; the text of the conversation and, by chance, due to training, the result &ldquo;looks like&rdquo; a response. In the case of images, we start with a randomly noisy image and through a process of removing and adding noise, it gradually &ldquo;rearranges&rdquo; the Latent into an image we can recognize.</li>
<li>There is a &ldquo;Scheduler&rdquo; that decides how much noise is added or removed at each time step. And there is the concept of a <strong>DIFFUSION MODEL</strong>, which is more or less the processing &ldquo;pipeline&rdquo;.</li>
<li>We can control the process by adding a text prompt, which is decoded into an &ldquo;embedding&rdquo; using a <strong>Text Decoder</strong> that usually has names like &ldquo;CLIP&rdquo; or properly a Transformer (such as LLava). These embeddings go into the U-NET via cross-attention layers, steering the &ldquo;denoising&rdquo; toward the meaning of the prompt.</li>
<li>On top of that we have <strong>CONTROLNET</strong>, which is a second parallel network that adapts the process to extra conditions. It&rsquo;s a copy of the U-NET architecture (with frozen weights) that receives extra inputs, like Canny Edges, or Depth Maps, pose skeletons (via OpenPose), etc, and learns small adjustments to <strong>INJECT</strong> into the main maps of the U-NET.</li>
<li>During the denoising process, ControlNet activations are merged with those of the U-NET, forcing the process to respect the conditions we passed in.</li>
</ul>
<p>So the U-NET is the &ldquo;generator&rdquo;, the Diffusion Scheduler orchestrates the process, text encoder and controlnet both influence the U-NET, and a VAE is used at the end to convert a latent space into an actual image that we humans can actually see.</p>
<p>VERY, VERY ROUGHLY. That&rsquo;s more or less how I understand the process. And the way I see it, ComfyUI exposes several of these steps in the form of &ldquo;PROGRAMMABLE NODES&rdquo; that we can parameterize and tweak to influence how we want the final image to come out.</p>
<p>Beyond numerical parameters, these Nodes also let us choose which pre-trained model files we want to use for each stage. It&rsquo;s all binary VSMs, but for different uses, which is why many of them have the same extension as I explained, but they live in different sub-directories. So each Node can find only what matters to it.</p>
<p>Taking my photo as an example, this isn&rsquo;t exactly it but just to illustrate, it helps me think that those maps I showed influence the &ldquo;noisy&rdquo; image in a non-random direction, like this one I grabbed from an intermediate stage of the workflow I&rsquo;m running:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/yy0yrlrcel015bucfr3z60izt42p" alt="noise"  loading="lazy" /></p>
<p>If you strain your eyes, you can see the influence of the previous images, and the Diffusion model keeps doing the &ldquo;denoising&rdquo;, being &ldquo;interrupted&rdquo; by the ControlNet. It&rsquo;s as if you were drawing and had a person next to you who every now and then erases what you did, or draws on top, and you keep adapting. So in the end it becomes a &ldquo;collaborative&rdquo; job between the two. If it were only the U-NET, it would generate a brand new image quite different from the original, but the ControlNet is the &ldquo;client&rdquo; that keeps nagging in your ear &ldquo;that&rsquo;s not how I want it, change it&rdquo;, until it arrives at an acceptable result.</p>
<p>On top of that, a U-NET isn&rsquo;t great at generating very high resolution images. The ideal is to generate small images, 512 pixels, 1024 pixels, but nothing bigger. But that would be useless for professional use. That&rsquo;s what <strong>UPSCALE MODELS</strong> are for. If you read my article from yesterday about <a href="https://www.akitaonrails.com/en/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a"target="_blank" rel="noopener">Video2K</a> I explain that I pick a model there that&rsquo;s good for temporal coherence in video. But there were other models that are better for &ldquo;STATIC IMAGES&rdquo;, or photos. And we can use them in ComfyUI.</p>
<p>After the new small image is generated by the U-NET, we can then use it as input to a second &ldquo;UPSCALE&rdquo; pass using a model like RealERSGAN, and go from a 1024 pixel image to 2160 pixels, essentially 4K. It&rsquo;s different from enlarging the image in Photoshop, where everything ends up &ldquo;soft&rdquo; and blurred. Since it&rsquo;s also another AI model, it&rsquo;s trained to &ldquo;redraw&rdquo; at higher resolution. And we can add this to the ComfyUI workflow as well.</p>
<p>More than that, ComfyUI also supports Nodes with models like Tencent&rsquo;s HunyuanVideo, which I introduced in another article from yesterday, about <a href="https://www.akitaonrails.com/en/2025/04/19/gerando-videos-de-ate-2-min-a-partir-de-uma-foto-com-i-a"target="_blank" rel="noopener">FramePack</a>. In fact, we can do the same thing FramePack does but from a ComfyUI workflow: generate a video from an image, using the same model. FramePack is infinitely easier to use, but ComfyUI is the Swiss Army knife of AI: it does <strong>EVERYTHING</strong>, which is also why it&rsquo;s infinitely more complicated and you need to put in serious effort to learn how to use it.</p>
<p>With what we&rsquo;ve covered so far, take a look at the workflow I used (it&rsquo;s zoomed out, you can dynamically zoom in):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rrtdjr0sq5vsl3xnm08w3sz459w8" alt="workflow"  loading="lazy" /></p>
<p>You can see these intermediate steps using <strong>PREVIEW</strong> Nodes, so this is more or less how we can use ComfyUI to do a lot of advanced stuff. As I said, I myself haven&rsquo;t even started scratching the surface.</p>
<p>Let&rsquo;s now summarize some of the concepts used in ComfyUI:</p>
<h3><strong>1. Checkpoints (Base Models)</strong><span class="hx:absolute hx:-mt-20" id="1-checkpoints-base-models"></span>
    <a href="#1-checkpoints-base-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
These are the main Stable Diffusion models (e.g.: SD 1.5, SDXL, Flux). They contain the entire model architecture:</p>
<ul>
<li>
<p>Text Encoder (CLIP)</p>
</li>
<li>
<p>Diffusion Model (U-Net)</p>
</li>
<li>
<p>VAE (Image Decoder)</p>
</li>
</ul>
<p><strong>What they&rsquo;re for</strong>:<br>
Generating images from scratch. Each checkpoint has a different &ldquo;style&rdquo; (realistic, anime, etc).</p>
<p><strong>Examples</strong>:<br>
<code>flux.safetensors</code>, <code>realisticVisionV60.safetensors</code>, <code>anythingV5.safetensors</code></p>
<h3><strong>2. VAE (Variational Autoencoders)</strong><span class="hx:absolute hx:-mt-20" id="2-vae-variational-autoencoders"></span>
    <a href="#2-vae-variational-autoencoders" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Component that decodes images from the latent space (the format the model understands) into visible pixels.</p>
<p><strong>What they&rsquo;re for</strong>:</p>
<ul>
<li>
<p>Improving details and colors</p>
</li>
<li>
<p>Some checkpoints require specific VAEs</p>
</li>
</ul>
<p><strong>Examples</strong>:<br>
<code>vae-ft-mse-840000.safetensors</code>, <code>animevae.pt</code></p>
<h3><strong>3. Text Encoders</strong><span class="hx:absolute hx:-mt-20" id="3-text-encoders"></span>
    <a href="#3-text-encoders" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Models that convert your text prompt into numerical representations the AI understands.</p>
<p><strong>Common types</strong>:</p>
<ul>
<li>
<p><strong>CLIP</strong>: Standard for most models</p>
</li>
<li>
<p><strong>OpenCLIP</strong>: Alternative version used in SDXL</p>
</li>
<li>
<p><strong>T5</strong>: Larger models for complex prompts</p>
</li>
</ul>
<p><strong>Examples</strong>:<br>
<code>clip_vision.safetensors</code>, <code>openclip.safetensors</code></p>
<h3><strong>4. CLIP Vision</strong><span class="hx:absolute hx:-mt-20" id="4-clip-vision"></span>
    <a href="#4-clip-vision" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Models specialized in visual analysis (as opposed to normal CLIP, which is text→image).</p>
<p><strong>What they&rsquo;re for</strong>:</p>
<ul>
<li>
<p>Creating descriptions of existing images</p>
</li>
<li>
<p>Tools like &ldquo;Image Prompt&rdquo; (which use the image as input)</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>clip_vision_g.safetensors</code></p>
<h3><strong>5. Diffusion Models</strong><span class="hx:absolute hx:-mt-20" id="5-diffusion-models"></span>
    <a href="#5-diffusion-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
A specific part of the architecture (U-Net) responsible for the diffusion process (adding/removing &ldquo;noise&rdquo;).</p>
<p><strong>What they&rsquo;re for</strong>:</p>
<ul>
<li>
<p>Training LoRAs</p>
</li>
<li>
<p>Advanced reuse workflows</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>unet.safetensors</code></p>
<h3><strong>6. LoRAs (Lightweight Adapters)</strong><span class="hx:absolute hx:-mt-20" id="6-loras-lightweight-adapters"></span>
    <a href="#6-loras-lightweight-adapters" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Small files that modify the behavior of the main checkpoint without replacing it.</p>
<p><strong>What they&rsquo;re for</strong>:</p>
<ul>
<li>
<p>Adding specific styles (e.g.: anime)</p>
</li>
<li>
<p>Creating consistent characters</p>
</li>
<li>
<p>Adjusting body proportions</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>epiNoiseOffset.safetensors</code>, <code>add_detail.safetensors</code></p>
<h3><strong>7. ControlNet</strong><span class="hx:absolute hx:-mt-20" id="7-controlnet"></span>
    <a href="#7-controlnet" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Models that impose control over the composition of the image (poses, depth, edges).</p>
<p><strong>Common types</strong>:</p>
<ul>
<li>
<p>OpenPose (human poses)</p>
</li>
<li>
<p>Canny (edges)</p>
</li>
<li>
<p>Depth (depth)</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>control_v11p_sd15_openpose.safetensors</code></p>
<h3><strong>8. Upscale Models</strong><span class="hx:absolute hx:-mt-20" id="8-upscale-models"></span>
    <a href="#8-upscale-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Models for increasing the resolution and detail of generated images.</p>
<p><strong>Types</strong>:</p>
<ul>
<li>
<p><strong>ESRGAN</strong>: For realistic details</p>
</li>
<li>
<p><strong>Anime6B</strong>: Specialized in anime art</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>4x_NMKD-Superscale-SP_178000_G.pth</code></p>
<h3><strong>9. Embeddings (Textual Inversions)</strong><span class="hx:absolute hx:-mt-20" id="9-embeddings-textual-inversions"></span>
    <a href="#9-embeddings-textual-inversions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are</strong>:<br>
Small files that add new concepts to the model&rsquo;s vocabulary.</p>
<p><strong>What they&rsquo;re for</strong>:</p>
<ul>
<li>
<p>Specific artistic styles</p>
</li>
<li>
<p>Custom objects</p>
</li>
<li>
<p>Artifact corrections</p>
</li>
</ul>
<p><strong>Example</strong>:<br>
<code>bad_prompt.pt</code>, <code>easynegative.safetensors</code></p>
<h3><strong>10. Style Models</strong><span class="hx:absolute hx:-mt-20" id="10-style-models"></span>
    <a href="#10-style-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are:</strong><br>
Models specialized in applying visual styles or art &ldquo;filters&rdquo; on top of the generated image, without actually altering the basic structure of the content.</p>
<p><strong>What they&rsquo;re for:</strong></p>
<ul>
<li>
<p><strong>Thematic colorization</strong> (e.g.: watercolor palette, sepia, chroma-key)</p>
</li>
<li>
<p><strong>Simulating artistic techniques</strong> (oil, gouache, manga linework)</p>
</li>
<li>
<p><strong>Unifying aesthetics</strong> across series of images (same color and light &ldquo;mood&rdquo;)</p>
</li>
</ul>
<p><strong>Example files:</strong></p>
<ul>
<li>
<p><code>flux1-redux-dev.safetensors</code> (&ldquo;Flux Redux&rdquo; style)</p>
</li>
<li>
<p><code>watercolor_v2.safetensors</code> (watercolor brushwork)</p>
</li>
<li>
<p><code>film_grain_vintage.safetensors</code></p>
</li>
</ul>
<h3><strong>11. SAMs (Segment Anything Models)</strong><span class="hx:absolute hx:-mt-20" id="11-sams-segment-anything-models"></span>
    <a href="#11-sams-segment-anything-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>What they are:</strong><br>
General segmentation networks capable of isolating objects or regions of interest in an image, producing binary or weighted masks.</p>
<p><strong>What they&rsquo;re for:</strong></p>
<ul>
<li>
<p><strong>Creating masks of objects/people</strong> for selective application of filters or compositions</p>
</li>
<li>
<p><strong>Guiding ControlNets</strong> (e.g.: applying diffusion only inside or outside a region)</p>
</li>
<li>
<p><strong>Pre-processing references</strong> (cropping background, separating layers)</p>
</li>
</ul>
<p><strong>Example file:</strong></p>
<ul>
<li><code>sam_vit_b_01ec64.pth</code> (SAM ViT-B, base segmentation model)</li>
</ul>
<h3>Visual Summary<span class="hx:absolute hx:-mt-20" id="visual-summary"></span>
    <a href="#visual-summary" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>models/
├── checkpoints/        ➔ Main models (Stable Diffusion)
│     └── *.safetensors, *.ckpt
├── diffusion_models/   ➔ Alternative diffusion models
│     └── *.safetensors, *.gguf
├── loras/              ➔ Style adapters (LoRAs)
│     └── *.safetensors
├── controlnet/         ➔ Composition controls (ControlNets)
│     └── *.pth, *.safetensors
├── vae/                ➔ VAE decoders / encoders
│     └── *.pt, *.safetensors
├── clip/               ➔ CLIP text encoders
│     └── *.safetensors, *.pt
├── clip_vision/        ➔ CLIP vision encoders
│     └── *.safetensors
├── text_encoders/      ➔ Advanced text encoders
│     └── *.safetensors, *.bin
├── upscale_models/     ➔ Super-resolution models
│     └── *.pth
├── sams/               ➔ Segment Anything (SAM) models
│     └── *.pth, *.onnx
├── style_models/       ➔ Visual style models
│     └── *.safetensors
└── embeddings/         ➔ Embeddings / custom concepts
      └── *.pt, *.safetensors</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You have to download the right things into the right places. And to make matters worse, as I already said, the files usually have the same extension. So it&rsquo;s very easy to get confused and drop a text encoder into the clip directory or vice versa. Once again, that&rsquo;s why I encourage you to update the <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/models.conf"target="_blank" rel="noopener">&ldquo;models.conf&rdquo;</a> file and put the right URLs into the right lists, that way it stays much more organized and my Docker setup will pull things into the right places at startup.</p>
<p>Even so, you may still have a few other doubts that I had at the beginning too. For example <strong>&ldquo;Why is the U-NET called a Checkpoint?&rdquo;</strong></p>
<h3><strong>Checkpoints ≈ A Complete Operating System</strong><span class="hx:absolute hx:-mt-20" id="checkpoints--a-complete-operating-system"></span>
    <a href="#checkpoints--a-complete-operating-system" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>
<p><strong>Similarity</strong>: Just as a commit in Git captures a specific state of the code, a checkpoint is a complete &ldquo;snapshot&rdquo; of a model trained at a specific stage.</p>
</li>
<li>
<p><strong>Difference</strong>: Checkpoints are <strong>self-contained</strong> (they aren&rsquo;t &ldquo;diffs&rdquo; like commits), each one is a complete model that can work independently.</p>
</li>
<li>
<p><strong>Better Metaphor</strong>: Think of checkpoints as different versions of Windows (XP, 7, 10) - each one is a complete operating system, not incremental.</p>
</li>
</ul>
<p>So checkpoints kind of resemble, conceptually, a &ldquo;git commit&rdquo; - if you&rsquo;re a developer. Or BTRFS or ZFS snapshots - if you&rsquo;re an infra/devops person.</p>
<h3><strong>LoRAs ≈ Plugins/Extensions</strong><span class="hx:absolute hx:-mt-20" id="loras--pluginsextensions"></span>
    <a href="#loras--pluginsextensions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>
<p><strong>Similarity</strong>: Just as a PR adds functionality to a base codebase, a LoRA modifies the behavior of the base model.</p>
</li>
<li>
<p><strong>Difference</strong>: LoRAs aren&rsquo;t &ldquo;merged&rdquo; into the original checkpoint, they are <strong>applied dynamically</strong> during inference.</p>
</li>
<li>
<p><strong>Better Metaphor</strong>: They&rsquo;re like Chrome extensions (e.g.: AdBlock) - they add specific behaviors without modifying the browser itself.</p>
</li>
</ul>
<p>Think of it this way: a model has Checkpoints, like commits, so you can &ldquo;continue&rdquo; training on top if you need to. At the same time, we don&rsquo;t have to mess with the main model, we can create separate layers, the so-called LoRas. For example, let&rsquo;s say a model is having trouble generating your face, no matter what prompt you use or what Nodes you configure.</p>
<p>Instead of that we can use the &ldquo;dictionary&rdquo; of the pre-trained model, like Flux or SDXL, and do training with our personal photos and produce a LoRa specific to me. Then we can add a LoRa Node into the workflow and that will result in much better images with your particular face.</p>
<p>There are entire online communities dedicated to this. One example. On the <a href="https://civitai.com/"target="_blank" rel="noopener">Civit AI</a> site we&rsquo;ll find lots of models based on existing models, like this <a href="https://civitai.com/models/24149/mistoonanime"target="_blank" rel="noopener">Mistoon</a> which is based on SDXL.</p>
<p><a href="https://civitai.com/models/24149/mistoonanime"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a4s6zs7oq6rtwto79lqk5itgd4or" alt="Mistoon Civit.ai"  loading="lazy" /></a></p>
<p>It&rsquo;s a checkpoint. We can sign up on the site and download the file <code>mistoonAnime_v10Noobai.safetensor</code> and put it in the <code>models/checkpoints</code> directory.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/17z9fbwbzkg5kwoxzrcivpwwd74c" alt="Mistoon Checkpoint Node"  loading="lazy" /></p>
<p>Now, in any workflow that has the &ldquo;Load Checkpoint&rdquo; Node we have the option to choose this model. But let&rsquo;s say this model isn&rsquo;t able to generate a character with a school uniform like in many anime. Browsing the site, we find this <strong>LoRa</strong>:</p>
<p><a href="https://civitai.com/models/115968/mistoonanime-school-uniform"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xlvhozw5u90xx9130a1kin5dl0da" alt="Mistoon LoRa"  loading="lazy" /></a>
The Mistoon model is large, around 6.6GB, but this LoRa - called <code>Mistoon_Anime\ school\ uniform.safetensor</code> which we should move to <code>models/loras</code> - is less than 290 MB, much smaller because it&rsquo;s a specific training only on images of school uniforms.</p>
<p>Now we can wire the &ldquo;Load Checkpoint&rdquo; Node to this other &ldquo;Load LoRa&rdquo; Node and choose this LoRa that we know is compatible:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/8ahr2bgdxe5jg3xg4y2wcie3qzvs" alt="LoRa Node"  loading="lazy" /></p>
<p>LoRas have to be picked to be used with the models they were based on. As I said before, think of the model as a &ldquo;language&rdquo;, if we make a LoRa in German, there&rsquo;s no point mixing it with a Chinese model, roughly speaking. But this is one way to do fine-tuning so the model you like delivers results no other one can.</p>
<p>One problem I have with the Civit AI site is that you can only download things while logged in. That&rsquo;s why I didn&rsquo;t include any of their models or loras in my automatic install script. Unfortunately, in their case you have to manually download the files and move them to the right directories, write it down somewhere, so on another machine you can download and move everything all over again. It&rsquo;s a pain that I haven&rsquo;t sat down to fix yet. If anyone wants to contribute a solution for this (without exposing anyone&rsquo;s login cookies, send a Pull Request).</p>
<h3>A Real Example<span class="hx:absolute hx:-mt-20" id="a-real-example"></span>
    <a href="#a-real-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Your day-to-day with Comfy, being a newbie like me, will go more or less like this: you start by going to Google and searching for &ldquo;best workflow image to anime ComfyUI&rdquo;. The first links are usually some Reddit thread:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/zhnvm94r6ze0279obqq9k0wd6t9c" alt="Reddit"  loading="lazy" /></p>
<p>Sub-Reddits like <a href="https://www.reddit.com/r/comfyui"target="_blank" rel="noopener">r/comfyui</a> or <a href="https://www.reddit.com/r/StableDiffusion"target="_blank" rel="noopener">r/StableDiffusion</a> usually have news and workflows like this one. Then we go straight to the first comments:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/n3y2qd1vxw4uldxlrs5bjsewt9vn" alt="Reddit Comment Links"  loading="lazy" /></p>
<p>Either that, or we open the workflow directly in ComfyUI and inspect the Nodes, one by one:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/b6hu6k4ygjpjdcq0chzv6mt1kx9w" alt="Nodes Check"  loading="lazy" /></p>
<p>We may already have a problem: the Node came pre-configured to load a LoRa called &ldquo;SDXL_aidma-niji_jini.safetensors&rdquo; but the link in the Reddit comment downloads an &ldquo;SDXL_Niji_V6_DLC_LoRa_V4.safetensors&rdquo;. Searching specifically for the previous one on Google, I can&rsquo;t find it. But it seems &ldquo;safe&rdquo; to use this other one, at least the names are very similar, both are derived from SDXL so theoretically they&rsquo;re at least compatible. This is the page of that model on Civit AI, so you have to download manually and send it to the <code>models/loras</code> directory, same thing for the AniToon Checkpoint in the Node above. Download manually and move it to <code>models/checkpoints</code>.</p>
<p><a href="https://civitai.com/models/541460/sdxlnijiv6dlclora"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/m8u6e9hc0vyrd41pdru0c26akolr" alt="LoRa Niji"  loading="lazy" /></a></p>
<p>Note that on this page it explains that this LoRa was made to work with the SDXL_Niji_V6 model and not with AniToon, which is what the Workflow I downloaded suggests in its Load Checkpoint Node. It&rsquo;s worth testing with both. Either way, since we&rsquo;re already on the site, let&rsquo;s also download the suggested model:</p>
<p><a href="https://civitai.com/models/120765"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/zrkk8wr9v1z24s5f5fgyv01uui4g" alt="SDXL Niji"  loading="lazy" /></a></p>
<p>File <code>sdxlNijiSeven_sdxlNijiSeven.safetensors</code>, 6.5GB, move it to <code>models/checkpoints</code>, are you all getting the process? Download, move it to the right place, reload the workflow in the browser and now it shows up, we select it:</p>
<p>By the way, I think it&rsquo;s intuitive but for those who haven&rsquo;t figured it out after opening the ComfyUI interface:</p>
<ul>
<li>left mouse button selects Nodes</li>
<li>middle mouse button drags the canvas</li>
<li>scroll wheel zooms the interface</li>
</ul>
<p>Now then, let&rsquo;s pick these two to test:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/m7bzo57hfez23vazar5ct4j5kndq" alt="Nodes Niji"  loading="lazy" /></p>
<p>Taking advantage of this same image, notice in the left corner a Florence2 Node, which is a Microsoft LLM (and my Docker script already installs it for you). I can&rsquo;t show all the related Nodes but these are the main ones explaining what it&rsquo;s for:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/8pjx9pr8x0jsmz7bhouz5zcft1zt" alt="Florence"  loading="lazy" /></p>
<p>The ideal in image generation is to write the longest and most detailed prompt about the original image that you can, but most people just write some garbage like &ldquo;person smiling standing up&rdquo; and think that&rsquo;s enough. The Florence2 model is built to read an image and describe it in text.</p>
<p>Now there&rsquo;s another detail: many checkpoints or Loras are trained with some activation keyword. So they only kick in if you type those words into your prompt. So this workflow doesn&rsquo;t need you to type a prompt manually, Florence2 will do that, and every time its text starts with &ldquo;The image &hellip; bla bla bla&rdquo;, so we have a &ldquo;Text Find and Replace&rdquo; Node that swaps that &ldquo;The Image&rdquo; for the keywords of Anitoon or Niji: &ldquo;aidma-niji, niji&rdquo;.</p>
<p>Are these the right words? <strong>I HAVE NO IDEA</strong> lol</p>
<p>The workflow already brought these words, but on the Civit AI pages - where it should be documented, it&rsquo;s not!! So we&rsquo;re really <strong>GUESSING</strong> that it should be &ldquo;niji&rdquo; since that&rsquo;s the name of the model and the lora. Pay attention to the keyword, it&rsquo;s important, otherwise you add the lora and nothing changes and that&rsquo;s because you forgot it in the prompt!!</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4i1n0fc3uvvy85nr17x6imee33bh" alt="ControlNet"  loading="lazy" /></p>
<p>There&rsquo;s more, you have to check that the Nodes for things like VAE, ControlNet, Canny and everything else are populated with files that we have. Usually you use the same half-dozen and I&rsquo;ve already pre-installed the main ones in Docker. In the image, see that the ControlNet is <code>contronet-union-sdxl-1.0-promax.safetensors</code>, compatible with SDXL, and we&rsquo;re using Niji which is built on top of SDXL. We just keep guessing like that.</p>
<p>Also notice that it uses Zoe Depth Map to extract the Depth Map and Canny to extract the Edge Map as in the other workflow. You can configure it, in this workflow notice that the Edge Map picked up some weird artifacts in the background, you can adjust it to remove that. That&rsquo;s why in the previous example the final image also had a window appearing, the model thought it was a window! But for now let&rsquo;s leave it as is.</p>
<p>Now we can run it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/29nzuo5t1nr6mn4wss0t77at94no" alt="Run Comfy"  loading="lazy" /></p>
<p>There&rsquo;s a huge &ldquo;RUN&rdquo; button in blue down at the bottom. If everything is configured correctly, you&rsquo;ll see that a job has been added to the queue. The interface is smart enough to let you work on other workflows and just keep queueing jobs while the GPU is processing. You don&rsquo;t have to sit waiting, just leave several queued up and go to sleep.</p>
<p>When it runs, in the interface the Nodes that are processing at any given moment will get a green border, which is good so you know what stage of the process it&rsquo;s in. And if any Node has an error - because a file is missing, for example - it&rsquo;ll get a red border, so you have to fix it and run again.</p>
<p>And in the end, here&rsquo;s one of the results:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7mbzamhj8c5sc2fesp8kkz6cvjhu" alt="Niji Result"  loading="lazy" /></p>
<p>Notice how the controlnet maps guaranteed the correct pose. The model interpreted those background artifacts from the edge map as lighting from some window, but in the original photo we can see it&rsquo;s like the concrete brick of the wall lol but that&rsquo;s how it is, without knowing, it guesses. And if you run it several times, it&rsquo;ll always give you a quite different result. Look at another one:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xbxu131ivlqu4endtdf6j5ayxzxt" alt="Niji 2"  loading="lazy" /></p>
<p>Way off lol But we can swap the SDXL-Niji model for Anitoon. Let&rsquo;s see one of the results:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/w7fsjrui29pbp8e98ka4ubuvtl6f" alt="Anitoon"  loading="lazy" /></p>
<p>Switching to the waiNSFIllustrious model:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2un0xtv7g210xrqc74nbceiwg7t4" alt="waiNSFIllustrious"  loading="lazy" /></p>
<p>This one came out even more off. Something in the maps is making the model assume that I have curly hair, no idea why. But that&rsquo;s it: you have to tweak the parameters. The result of the workflow I showed at the beginning of the article gave a better result, using the same models.</p>
<p>Of all the Nodes, one of the most important is the &ldquo;engine&rdquo; of the process, the <strong>KSampler</strong>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/n6875915bhqq39vxw0kfripyyj3d" alt="KSampler"  loading="lazy" /></p>
<p>This is the guy who runs the show. We give it the model, the prompt, control-net outputs, and it&rsquo;s the one that iteratively (steps) does the &ldquo;denoise&rdquo; of the Latent. Notice that there&rsquo;s always a random factor (&ldquo;seed&rdquo;), a number of steps (25 to 50 is the average), &ldquo;cfg&rdquo; (classifier-free guidance) or &ldquo;guidance_scale&rdquo;, where higher values pull the image more toward your positive prompt. Denoise is how much noise the scheduler applies (1.0 is full noise, below 1.0 may produce more &ldquo;artistic&rdquo; results, you have to test).</p>
<p>Sampler Name is annoying because you&rsquo;d need to understand the math behind it, but they are the step algorithms, for example <code>euler</code>, <code>ddim</code>, <code>plms</code> and each one has a trade-off between speed and fidelity. In practice: test them one by one and see the results. Scheduler has <code>normal</code>, <code>klms</code>, <code>dpmsolver</code>. Same thing: test them one by one.</p>
<p>As you can see, there are PLENTY of parameters you can play with. But the main flow is more or less this. From there you have to keep learning Node by Node what each one can do and download lots of pre-made workflows to see how people have been solving various problems, which models and loras are the most popular for each kind of application. Anyway, Reddit is your friend. I hope this gave you some sense of direction, I&rsquo;m also still learning.</p>
]]></content:encoded><category>generative-media</category><category>machine-learning</category><category>tutorials</category></item></channel></rss>