<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Local-Models | AkitaOnRails.com</title><link>https://www.akitaonrails.com/en/tags/local-models/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 30 Oct 2026 06:11:16 +0000</lastBuildDate><atom:link href="https://www.akitaonrails.com/en/tags/local-models/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM Benchmark: What Are the Best Open Source LLMs for Home Hardware?</title><link>https://www.akitaonrails.com/en/2026/10/27/llm-benchmark-best-open-source-llms-for-home-hardware/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/27/llm-benchmark-best-open-source-llms-for-home-hardware/</guid><pubDate>Tue, 27 Oct 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;In the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/"&gt;last LLM Benchmark about open source models&lt;/a&gt;, I pushed things as far as was reasonable. I rented B300s, H200s, four-GPU pods, ran almost 70 rounds, and spent more than $6,000 on the whole campaign. It produced strong candidates such as DeepSeek V4.1 Flash, GLM 5.3 Flash, and Qwen3.8 Flash-Next, all close to frontier models in my programming test.&lt;/p&gt;
&lt;p&gt;The machine remained the problem. A 510 GB model that needs four B300s may be open source on GitHub, but it is still beyond the reach of anyone who wants to run an AI in a home office.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the <a href="/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/">last LLM Benchmark about open source models</a>, I pushed things as far as was reasonable. I rented B300s, H200s, four-GPU pods, ran almost 70 rounds, and spent more than $6,000 on the whole campaign. It produced strong candidates such as DeepSeek V4.1 Flash, GLM 5.3 Flash, and Qwen3.8 Flash-Next, all close to frontier models in my programming test.</p>
<p>The machine remained the problem. A 510 GB model that needs four B300s may be open source on GitHub, but it is still beyond the reach of anyone who wants to run an AI in a home office.</p>
<p>I went back to my RTX 5090 and Strix Halo. Instead of repeating the full v4 benchmark on dozens of models that we already know will not fit or run fast enough, I took the most promising candidate and compared <strong>its quantizations</strong>. Which version preserves enough quality without turning every interaction into a wait of several minutes?</p>
<p>The result surprised me: <strong>the 2-bit version won</strong>. I then repeated the test with <a href="https://github.com/jesdga95/basalt"target="_blank" rel="noopener">Basalt</a>, a Strata fork specialized for Blackwell GPUs. The model kept getting everything right, while generation reached <strong>400 tokens per second</strong>. The quantization did not change; the engine changed the result substantially.</p>
<h2>The RTX 3090 and 4090 got a second wind<span class="hx:absolute hx:-mt-20" id="the-rtx-3090-and-4090-got-a-second-wind"></span>
    <a href="#the-rtx-3090-and-4090-got-a-second-wind" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before my results, the community found a limitation that could be removed. On GeForce RTX 20 cards, some FP16 Tensor Core operations with FP32 accumulation run at half the theoretical issue rate per cycle of their professional equivalents. On RTX 30 and 40 cards, the limit also affects BF16.</p>
<p>The hardware is there. Two internal registers select the speed, and the normal driver does not have enough privilege to change them.</p>
<p>The <a href="https://github.com/Zagitta/gpu-unlock"target="_blank" rel="noopener">gpu-unlock</a> project found a way to change those registers before the NVIDIA driver takes over the card. On Ampere, it exploits a buffer overflow in the signed Booter that authenticates and loads GSP-RM. Nothing is written to the VBIOS. To restore stock behavior, remove the activation parameter and fully power the machine off, because the selectors reset when the card loses power. If the parameter remains active, the module reapplies the unlock on the next boot.</p>
<p>It also requires Linux, initramfs, and a lot of care. I would not install this on a production workstation without reading the code and understanding the risk.</p>
<p>This is a community hack with no official NVIDIA support. You install it at your own risk. Its value for LLMs is faster prefill when the engine uses this exact Tensor Core path. Other compute applications, including some image-generation pipelines, may also change in performance. Games depend on different GPU paths and bottlenecks, so do not expect higher frame rates from it.</p>
<p>On an RTX 3090 at 420 W, <a href="https://github.com/Zagitta/gpu-unlock/issues/85"target="_blank" rel="noopener">a controlled test</a> measured FP16 with FP32 accumulation going from 76.396 to 125.006 TFLOPS; BF16 went from 76.994 to 127.923. On the 4090, an implementation that is still experimental measured roughly twice the FP16 and BF16 instruction issue rate.</p>
<p>Doubling TFLOPS does not double your LLM. The <a href="https://dev.to/orca_forge/why-gpu-unlock-speeds-up-rtx-3090-prefill-but-barely-changes-decode-4572"target="_blank" rel="noopener">test that prompted this discussion</a> separated the two phases:</p>
<ul>
<li><strong>Prefill</strong> is when the model reads the whole prompt. Many tokens are available at once, so the engine can build large parallel matrix operations. <a href="https://github.com/Zagitta/gpu-unlock/issues/33"target="_blank" rel="noopener">One 3090 gained 14.4%</a> on a 16K-token workload, while two modified 3080s gained <a href="https://github.com/Zagitta/gpu-unlock/issues/15"target="_blank" rel="noopener">33% to 36% with vLLM</a>.</li>
<li><strong>Decode</strong> is when the model generates one token after another. With a quantized LLM using expert offload, this phase is constrained mainly by memory bandwidth, RAM, PCIe, and the expert cache. The measured change stayed within noise.</li>
</ul>
<p>The gain varies substantially with the engine, quantization, and prompt size. On the dual 3080 setup, Qwen3.8 Flash-Next Q4_K_XL gained only 0.5% to 0.8% at 8K, but reached 12% or 13% at 32K and 128K. On a 3090 running an IQ3_S MoE, a workload near 32K did not change. The computation path used by a quantization may never touch the FP16/BF16 path that was unlocked.</p>
<blockquote>
  <p>gpu-unlock accelerates a specific Tensor Core path. It may reduce the wait when reading large prompts, but it does not turn 24 GB into 96 GB or create memory bandwidth.</p>

</blockquote>
<p>Even with that caveat, the 3090 and 4090 became more interesting for local inference. A used <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3090-3090ti/"target="_blank" rel="noopener">RTX 3090</a> still has 24 GB of VRAM, NVLink in its generation, and 936 GB/s of bandwidth. The community measured Qwen3.8 Flash-Next at roughly <a href="https://github.com/sybil-solutions/local-ai-registry/pull/212"target="_blank" rel="noopener">75 tok/s with IQ3_S and 100 tok/s with Q2_0</a> on one 3090. With two cards, reports range from <a href="https://github.com/ruashots/flashnext-2x3090"target="_blank" rel="noopener">90 to 120 tok/s</a>, depending on the configuration and context size. Those are community results, not measurements from this round.</p>
<h2>Why I chose Qwen3.8 Flash-Next<span class="hx:absolute hx:-mt-20" id="why-i-chose-qwen38-flash-next"></span>
    <a href="#why-i-chose-qwen38-flash-next" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In v4, the open candidates that really interested me were:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Where I ran it</th>
          <th style="text-align: right">v4 score</th>
          <th style="text-align: right">Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4.1 Flash</td>
          <td>4x B300, native checkpoint</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">174.8 tok/s</td>
      </tr>
      <tr>
          <td>GLM 5.3 Flash</td>
          <td>1x B300, NVFP4</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">142 tok/s</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next</td>
          <td>1x B300, NVFP4</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">195 tok/s</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next</td>
          <td>RTX 5090, IQ3_S</td>
          <td style="text-align: right">91.0</td>
          <td style="text-align: right">159.2 tok/s</td>
      </tr>
  </tbody>
</table>
<p>The three tied within the noise margin. Only Qwen repeated that quality on consumer hardware. Its main model is a 125-billion-parameter MoE with roughly 6 billion active parameters per token, plus 51 billion parameters in n-gram embeddings and a 4-billion-parameter MTP head. <a href="https://github.com/Niko1221/Strata"target="_blank" rel="noopener">Strata</a> keeps the most frequently used experts on the GPU, keeps the rest in RAM for the CPU to compute, reads the n-gram table from the SSD, and uses the model&rsquo;s own MTP layer to speculate the next tokens.</p>
<p>DeepSeek V4.1&rsquo;s backbone and Engram add up to about 748 billion parameters: 552 billion in the backbone and 196 billion in Engram. The published checkpoint has roughly 763 billion parameters in total. I served its native mixed FP4/FP8 version, about 510 GB, on the four-B300 pod.</p>
<p>I also managed to start a 2-bit version on the 5090 using Strata&rsquo;s dedicated engine. It generated around <strong>17 tokens per second</strong>. It runs. It is out of the race for a programming agent.</p>
<p>The GLM 5.3 Flash NVFP4 checkpoint was around 198 GB and ran on the B300. This repository has no consumer configuration that I measured and can recommend. The smaller GLM 4.7 Flash had already scored 24 on the 5090 and 33.5 on the B300 in the previous benchmark. Spending days forcing the wrong family to fit would not produce useful information.</p>
<p>I also left out Qwen 4 and Llama by choice. This round asks a narrow question: <strong>which version of the model that already proved its quality works best on the two machines I own?</strong></p>
<h2>What I tested<span class="hx:absolute hx:-mt-20" id="what-i-tested"></span>
    <a href="#what-i-tested" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All the code, prompts, hidden tests, and results are in <a href="https://github.com/akitaonrails/llm-opensource-benchmark"target="_blank" rel="noopener">llm-opensource-benchmark</a>. I tested four Qwen3.8 Flash-Next versions:</p>
<table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th style="text-align: right">Model files</th>
          <th>Idea</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q2_0</td>
          <td style="text-align: right">~62 GiB</td>
          <td>2 bits, smaller and more aggressive</td>
      </tr>
      <tr>
          <td>IQ3_XXS</td>
          <td style="text-align: right">~71 GiB</td>
          <td>3 bits, optimized for size</td>
      </tr>
      <tr>
          <td>IQ3_S</td>
          <td style="text-align: right">~78 GiB</td>
          <td>3 bits, more precision</td>
      </tr>
      <tr>
          <td>UD-Q4_K_XL</td>
          <td style="text-align: right">~104 GiB</td>
          <td>4 bits, preserves more of the weights</td>
      </tr>
  </tbody>
</table>
<p>The Strata files include a shared PLE component, so the full size does not fit directly in VRAM. The engine distributes experts between the GPU and RAM. On Basalt, I used the project&rsquo;s own packs: one 64.9 GiB <code>.basalt</code> file for Q2_0 and an 81.0 GiB file for IQ3_S. Each file already includes the weights, tokenizer, MTP, expert profile, and vision projector.</p>
<p>For the first RTX 5090 round, I used Strata&rsquo;s CUDA backend with:</p>
<ul>
<li>4 GiB of VRAM reserved for Hyprland, the browser, and the terminal;</li>
<li><code>int8</code> KV cache, a 128K maximum context, and 32K resident on the GPU;</li>
<li>an elastic expert cache;</li>
<li>four-token speculation with the model&rsquo;s MTP.</li>
</ul>
<p>I then ran Q2_0 and IQ3_S on Basalt, on the same 5090 and with the same 131,072-token maximum context and <code>int8</code> KV. I reserved 4,608 MiB because Basalt counts its working buffers against that margin. Free VRAM stayed between 3.6 and 4.4 GB during the tests.</p>
<p>On the Strix Halo, I used the <code>hip-halo</code> backend. That machine has 64 GB reserved for the GPU and 31 GB of GTT mapped over system RAM; the engine&rsquo;s RAM limit was 61.9 GiB. Since it is headless, I gave the entire GPU to the model and kept 8 GiB of headroom for the conversation cache.</p>
<p>The Strata measurements use two different backends underneath, CUDA on NVIDIA and HIP/ROCm on AMD, with the same engine and the same OpenAI-compatible API. Basalt adds an engine comparison on the 5090, using the same OpenCode, prompts, and grading.</p>
<h2>Methodology<span class="hx:absolute hx:-mt-20" id="methodology"></span>
    <a href="#methodology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started with a speed test in three situations:</p>
<ol>
<li>a cold 4K-token prompt;</li>
<li>a cold prompt labeled 32K, which this model&rsquo;s tokenizer counted as 42,400 tokens;</li>
<li>a continuation of the same conversation, reusing the cache.</li>
</ol>
<p>Each one generates 256 tokens. The runner measures time to first token, prompt processing, generation, VRAM, RAM, power, and temperature.</p>
<h3>What each speed number means<span class="hx:absolute hx:-mt-20" id="what-each-speed-number-means"></span>
    <a href="#what-each-speed-number-means" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>When someone says an LLM runs at 200 tokens per second, they are almost always talking only about generation. An agent session goes through different phases, and each phase hits a different bottleneck.</p>
<p><strong>Prefill</strong> is the initial reading of the prompt. The model must process the harness instructions, conversation history, files, and your question before producing its first word. Since all those tokens are available at once, the GPU can process large matrices in parallel.</p>
<p>This phase makes better use of Tensor Cores and raw compute. The larger the context, the more it matters.</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>What it measures</th>
          <th>What you feel</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prompt tok/s</td>
          <td>Prefill throughput, or how many input tokens are read per second</td>
          <td>Time to digest new context</td>
      </tr>
      <tr>
          <td>TTFT (<em>time to first token</em>)</td>
          <td>Total time between sending the request and receiving the first token</td>
          <td>The pause before the answer starts</td>
      </tr>
      <tr>
          <td>Decode or output tok/s</td>
          <td>Generation speed after the first token</td>
          <td>Pace of text and tool calls</td>
      </tr>
      <tr>
          <td>TPOT (<em>time per output token</em>)</td>
          <td>Time per generated token, usually reported at p50 and p99 (or p95)</td>
          <td>Response consistency; a high tail percentile exposes stalls</td>
      </tr>
      <tr>
          <td>Wall time</td>
          <td>Time for the whole task, including tools, tests, retries, and generated text</td>
          <td>Actual agent productivity</td>
      </tr>
  </tbody>
</table>
<p>The <strong>KV cache</strong> stores intermediate states for tokens that have already been processed. On a continuation of the same conversation, the model reuses that state instead of reading everything again. That is why the append test reaches tens of thousands of prompt tokens per second and receives the first token in roughly half a second. That number measures cache reuse; comparing it with cold prefill would be dishonest.</p>
<p>The KV cache also grows with the conversation and competes with weights, experts, and buffers for memory. Here it uses <code>int8</code>, with 32K resident on the 5090. Advertised maximum context, context that fits, and context that stays fast are three different numbers.</p>
<p><strong>Speculative decoding</strong> uses a draft layer to propose several tokens and lets the main model verify the batch. Output tok/s goes up when the acceptance rate is good. The benefit vanishes when the draft misses too often. The Strata speeds already include MTP with four-token speculation. Basalt also uses the MTP included in its pack, with its own speculation mechanism and defaults.</p>
<p>One metric is missing because I did not measure it here: <strong>concurrency</strong>. Every number in this article comes from a single stream. A server can handle several sessions and raise aggregate throughput, while each user will usually receive fewer tokens per second.</p>
<p>For individual use, single-stream performance and wall time are what matter. A team must measure simultaneous sessions and per-user latency, as I did in the <a href="/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/">B300 and H200 test</a>.</p>
<p>Then came two programming tests using OpenCode 2.1.4. The model receives a small Python codebase, works in one continuous session, and never sees the hidden tests. Everything runs inside Bubblewrap, without network access, with a limited filesystem and a local gateway that accepts calls only to the expected model.</p>
<p>The first canary has three turns and 9 hidden tests. The model must implement a log analyzer, fix bugs, and handle changed requirements.</p>
<p>The hard canary has four turns and 36 hidden tests. It is a payment reconciler with decisions buried inside a roughly 79K-token history, one decision corrected later, rejected proposals used as distractions, money that requires exact decimals, and new rules that interact with earlier ones. With OpenCode&rsquo;s system prompt, the first turn approaches 83K tokens.</p>
<h3>A separate yardstick from benchmark v4<span class="hx:absolute hx:-mt-20" id="a-separate-yardstick-from-benchmark-v4"></span>
    <a href="#a-separate-yardstick-from-benchmark-v4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>These canaries are much smaller and easier. In v4, the model works on a Rails application for seven sprints while a reviewer injects 14 security and correctness defects. The score measures how many defects it finds and fixes on its own before they are revealed. It is a vigilance test with much more code, more tools, and more opportunities to lose track.</p>
<p>Here I use two small Python codebases, three or four turns, and hidden functional tests. I built this yardstick to compare <strong>quantizations of the same Qwen</strong> and find out whether reducing the bits breaks requirements, context retention, or process. The comparison ends there. Qwen, Opus, GPT, DeepSeek, and the other model families remain a job for v4.</p>
<p>A score of 100 in this article belongs to a different scale, with no equivalence to 100 in v4. It also does not make Q2_0 &ldquo;better&rdquo; than the IQ3_S version that scored 91 in v4. V4 selected Qwen as the candidate; this smaller test selects which Qwen file is worth loading at home.</p>
<blockquote>
  <p>Speed without quality only makes the model fail faster. That is why I measured them separately.</p>

</blockquote>
<h2>Speed ranking<span class="hx:absolute hx:-mt-20" id="speed-ranking"></span>
    <a href="#speed-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>RTX 5090 with Strata, 4 GiB reserved for the desktop<span class="hx:absolute hx:-mt-20" id="rtx-5090-with-strata-4-gib-reserved-for-the-desktop"></span>
    <a href="#rtx-5090-with-strata-4-gib-reserved-for-the-desktop" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th style="text-align: right">Generation</th>
          <th style="text-align: right">Long-input processing</th>
          <th style="text-align: right">First token</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Q2_0</strong></td>
          <td style="text-align: right"><strong>223 to 259 tok/s</strong></td>
          <td style="text-align: right"><strong>6,158 tok/s</strong></td>
          <td style="text-align: right"><strong>6.9 s</strong></td>
      </tr>
      <tr>
          <td>IQ3_XXS</td>
          <td style="text-align: right">196 to 241 tok/s</td>
          <td style="text-align: right">5,084 tok/s</td>
          <td style="text-align: right">8.3 s</td>
      </tr>
      <tr>
          <td>IQ3_S</td>
          <td style="text-align: right">158 to 208 tok/s</td>
          <td style="text-align: right">6,233 tok/s</td>
          <td style="text-align: right">6.8 s</td>
      </tr>
      <tr>
          <td>UD-Q4_K_XL</td>
          <td style="text-align: right">44 to 52 tok/s</td>
          <td style="text-align: right">2,300 tok/s</td>
          <td style="text-align: right">19 s</td>
      </tr>
  </tbody>
</table>
<h3>RTX 5090 with Basalt<span class="hx:absolute hx:-mt-20" id="rtx-5090-with-basalt"></span>
    <a href="#rtx-5090-with-basalt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th style="text-align: right">Generation</th>
          <th style="text-align: right">Long-input processing</th>
          <th style="text-align: right">First token</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Q2_0</strong></td>
          <td style="text-align: right"><strong>377 to 400 tok/s</strong></td>
          <td style="text-align: right"><strong>8,573 tok/s</strong></td>
          <td style="text-align: right"><strong>4.9 s</strong></td>
      </tr>
      <tr>
          <td>IQ3_S</td>
          <td style="text-align: right">223 to 231 tok/s</td>
          <td style="text-align: right">8,196 tok/s</td>
          <td style="text-align: right">5.2 s</td>
      </tr>
  </tbody>
</table>
<h3>Strix Halo, using the whole GPU<span class="hx:absolute hx:-mt-20" id="strix-halo-using-the-whole-gpu"></span>
    <a href="#strix-halo-using-the-whole-gpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th style="text-align: right">Generation</th>
          <th style="text-align: right">Long-input processing</th>
          <th style="text-align: right">First token</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Q2_0</strong></td>
          <td style="text-align: right"><strong>55 to 65 tok/s</strong></td>
          <td style="text-align: right"><strong>371 tok/s</strong></td>
          <td style="text-align: right"><strong>114 s</strong></td>
      </tr>
      <tr>
          <td>IQ3_XXS</td>
          <td style="text-align: right">50 to 57 tok/s</td>
          <td style="text-align: right">347 tok/s</td>
          <td style="text-align: right">122 s</td>
      </tr>
      <tr>
          <td>IQ3_S</td>
          <td style="text-align: right">47 to 50 tok/s</td>
          <td style="text-align: right">338 tok/s</td>
          <td style="text-align: right">125 s</td>
      </tr>
      <tr>
          <td>UD-Q4_K_XL</td>
          <td style="text-align: right">17 tok/s on one 4K prompt</td>
          <td style="text-align: right">137 tok/s</td>
          <td style="text-align: right">did not finish</td>
      </tr>
  </tbody>
</table>
<p>The 5090 has 32 GB of GDDR7 and <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/"target="_blank" rel="noopener">1,792 GB/s of bandwidth</a>. The Strix has more total capacity, but its roughly 256 GB/s must serve the entire system. Even with Strata on both machines, the difference appears mainly in prefill: 6.9 seconds versus almost 2 minutes to start answering the 42,400-token workload. Basalt cut the 5090 wait to 4.9 seconds.</p>
<p>Q4 gave me the worst of both worlds. On the 5090 it ran slowly, and <code>systemd-oomd</code> killed the quality test. On the Strix, it exhausted VRAM, started paging, and froze the machine until a reboot. More bits preserve more information, but they also use more memory and shrink the hot cache that makes this MoE move.</p>
<h2>Basalt versus Strata<span class="hx:absolute hx:-mt-20" id="basalt-versus-strata"></span>
    <a href="#basalt-versus-strata" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://github.com/jesdga95/basalt"target="_blank" rel="noopener">Basalt</a> started as a Strata fork and took a narrower direction: Linux or Windows, CUDA 13, and an NVIDIA Blackwell GPU with compute capability 12.0. The 5090 is exactly its target. Strata remains much broader and is what allowed the same Qwen to run on the AMD Strix Halo.</p>
<p>Both engines solve the same central problem. In the tested configuration, experts that do not fit in VRAM stay in RAM while the GPU holds a hot subset. Basalt and Strata expose OpenAI- and Anthropic-compatible APIs and accelerate generation with their MTP drafters. Basalt stores everything in one file and has kernels, memory management, and speculative execution developed specifically for Blackwell.</p>
<p>This was a practical A/B between complete serving stacks, without isolating one optimization. Each engine has its own packs, their defaults are not identical, and Basalt reserved 4,608 MiB of VRAM versus Strata&rsquo;s 4,096 MiB. I kept the same RTX 5090, the same quantizations, a 131K maximum context, <code>int8</code> KV, the same OpenCode, and the same tests.</p>
<p>Basalt won every speed metric in this A/B and tied on functional coding results. Q2_0 moved from a range of 223 to 259 tokens per second to 377 to 400. Long-input processing rose from 6,158 to 8,573 tokens per second, and the wait for the first token fell from 6.9 to 4.9 seconds. IQ3_S moved from 158 to 208 to a range of 223 to 231 tokens per second.</p>
<p>Platform scope decides the choice. On an RTX 5090, I would start with Basalt. On the Strix, pre-Blackwell NVIDIA, or other supported hardware, Strata remains the path. Strata covers the platforms where Basalt does not run.</p>
<h2>Quality ranking<span class="hx:absolute hx:-mt-20" id="quality-ranking"></span>
    <a href="#quality-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The simple test separated nothing:</p>
<table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th style="text-align: right">RTX 5090</th>
          <th style="text-align: right">Strix Halo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q2_0</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">100</td>
      </tr>
      <tr>
          <td>IQ3_XXS</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">100</td>
      </tr>
      <tr>
          <td>IQ3_S</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">100</td>
      </tr>
  </tbody>
</table>
<p>All three passed the 9 hidden tests, preserved the requirements, and finished with the visible suite green. I had to make the test harder.</p>
<p>In the hard canary, I ran Q2_0 and IQ3_S three times each on the 5090 with Strata:</p>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">Hidden tests</th>
          <th style="text-align: right">Time for 4 turns</th>
          <th style="text-align: right">Output tokens</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q2_0 r1</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">5.3 min</td>
          <td style="text-align: right">70K</td>
      </tr>
      <tr>
          <td>Q2_0 r2</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">8.8 min</td>
          <td style="text-align: right">117K</td>
      </tr>
      <tr>
          <td>Q2_0 r3</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">7.4 min</td>
          <td style="text-align: right">100K</td>
      </tr>
      <tr>
          <td>IQ3_S r1</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">11.9 min</td>
          <td style="text-align: right">110K</td>
      </tr>
      <tr>
          <td>IQ3_S r2</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">13.3 min</td>
          <td style="text-align: right">127K</td>
      </tr>
      <tr>
          <td>IQ3_S r3</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">11.1 min</td>
          <td style="text-align: right">98K</td>
      </tr>
  </tbody>
</table>
<p>Both versions got everything right in all three runs. They found the decisions in the long context, respected the later correction, handled out-of-order events, chargebacks, currency conversion, and completed the final refactor. They also went through a similar number of failing test runs while working.</p>
<p>The difference was on the clock. <strong>Q2_0 took 7.2 minutes on average. IQ3_S took 12.1.</strong> The 2-bit model delivered the same result in roughly 60% of the time.</p>
<p>I then repeated all six runs on Basalt:</p>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">Hidden tests</th>
          <th style="text-align: right">Time for 4 turns</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q2_0 r1</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">5.9 min</td>
      </tr>
      <tr>
          <td>Q2_0 r2</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">4.2 min</td>
      </tr>
      <tr>
          <td>Q2_0 r3</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">6.3 min</td>
      </tr>
      <tr>
          <td>IQ3_S r1</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">5.8 min</td>
      </tr>
      <tr>
          <td>IQ3_S r2</td>
          <td style="text-align: right">96*</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">11.3 min</td>
      </tr>
      <tr>
          <td>IQ3_S r3</td>
          <td style="text-align: right">100</td>
          <td style="text-align: right">36/36</td>
          <td style="text-align: right">10.1 min</td>
      </tr>
  </tbody>
</table>
<p>The penalty that dropped the second IQ3_S run to 96 was a known harness false positive. The model executed <code>ps aux</code> inside the sandbox, and the command showed the <code>bwrap</code> process itself, whose command line contains host paths. The detector treated that as an attempt to access the hidden tests and removed four points. The model did not use those paths, the hidden tests are only mounted later during grading, and all 36 passed.</p>
<p>On Basalt, Q2_0 took <strong>5.4 minutes on average</strong>, compared with 7.2 on Strata. IQ3_S fell from 12.1 to <strong>9.0 minutes</strong>. The end-to-end gain is smaller than the jump in tokens per second because the session also spends time running tools and tests, and each run generated a different amount of text.</p>
<p>The evidence covers small Python codebases, contexts up to roughly 85K tokens, and only three repetitions per engine and quantization combination in the hard canary. A 2-bit quantization will usually lose quality first on contexts above 100K and on larger codebases. Within the use I measured, spending more bits bought no quality.</p>
<h2>What each version showed<span class="hx:absolute hx:-mt-20" id="what-each-version-showed"></span>
    <a href="#what-each-version-showed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Q2_0: the current recommendation<span class="hx:absolute hx:-mt-20" id="q2_0-the-current-recommendation"></span>
    <a href="#q2_0-the-current-recommendation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It generated tokens faster in all three machine-and-engine combinations and finished all six hard runs with a perfect score. IQ3_S won the long-prompt prefill measurement on Strata and the 5090 by a tiny margin. With Basalt, Q2_0 reached 400 tokens per second. On the Strix, 55 to 65 tokens per second is usable, although long prefill costs almost two minutes before the first answer.</p>
<p>I would use this version every day and keep the context caveat in mind. Large project, session approaching 100K, or strange behavior? I repeat the same work with IQ3_S before blaming the model.</p>
<h3>IQ3_XXS: no clear role<span class="hx:absolute hx:-mt-20" id="iq3_xxs-no-clear-role"></span>
    <a href="#iq3_xxs-no-clear-role" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It passed the simple canary and ran quickly, but Q2_0 was faster and uses less space. I did not run the three hard-test repetitions on IQ3_XXS, so I have no data showing a quality advantage.</p>
<h3>IQ3_S: the conservative option<span class="hx:absolute hx:-mt-20" id="iq3_s-the-conservative-option"></span>
    <a href="#iq3_s-the-conservative-option" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is still the version I would use to validate a suspicious Q2_0 result. It had already scored 91 in benchmark v4 on the 5090, only 1.5 points below its NVFP4 version on the B300. On Basalt, it ran at 223 to 231 tokens per second, roughly the range Q2_0 reached on Strata, while processing prompts faster.</p>
<p>Basalt also measures each quantization against Q8_0. Across its 100-chunk test, IQ3_S kept the same top token in 87.16% of positions. That is a different yardstick from my canaries, but it supports IQ3_S&rsquo;s role as the conservative option. In my coding tests, that extra precision still found nothing Q2_0 had missed.</p>
<h3>UD-Q4_K_XL: quality that does not fit is useless<span class="hx:absolute hx:-mt-20" id="ud-q4_k_xl-quality-that-does-not-fit-is-useless"></span>
    <a href="#ud-q4_k_xl-quality-that-does-not-fit-is-useless" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It was the intuitive candidate for preserving more fidelity. I ran out of memory before reaching the test that could confirm it. On the 5090, it fell below one quarter of Q2_0&rsquo;s speed and the system killed the run. On the Strix, the machine froze.</p>
<p>The largest quantization that fits on paper can be the worst real configuration. An MoE depends on the space left for experts, KV cache, buffers, and the operating system itself.</p>
<h2>Which hardware is worth it?<span class="hx:absolute hx:-mt-20" id="which-hardware-is-worth-it"></span>
    <a href="#which-hardware-is-worth-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>RTX 3090 and 4090: good if you already own them<span class="hx:absolute hx:-mt-20" id="rtx-3090-and-4090-good-if-you-already-own-them"></span>
    <a href="#rtx-3090-and-4090-good-if-you-already-own-them" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Both have 24 GB, which limits the expert cache. gpu-unlock improves prefill in workloads that use the unlocked path, while decode remains constrained by memory and offload. A used 3090, especially two of them with NVLink, can make an interesting local machine. I would not buy a new 4090 today just for this: it still has 24 GB, costs a lot, and the Ada unlock remains experimental.</p>
<p>For either one, I would start with <strong>Qwen3.8 Flash-Next Q2_0</strong> and measure my workload. The roughly 100 tokens per second published by the community on one 3090 already puts the card in a practical range. Two 3090s add capacity and may land between 90 and 120 tokens per second, but they do not automatically double throughput. The bill includes power draw, heat, a suitable PSU, and at least 128 GB of RAM.</p>
<h3>Strix Halo: plenty of capacity, little bandwidth<span class="hx:absolute hx:-mt-20" id="strix-halo-plenty-of-capacity-little-bandwidth"></span>
    <a href="#strix-halo-plenty-of-capacity-little-bandwidth" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It is a good compact and efficient headless box. Unified memory lets it run models that a 24 or 32 GB GPU would reject. In this test, the best model was also <strong>Q2_0</strong>, at 55 to 65 tokens per second.</p>
<p>The problem appears every time the agent reads a large context. Almost two minutes to process the long workload adds up quickly during a session. I would use it as a private server for asynchronous tasks, simple agents, and workloads where capacity matters more than immediate response.</p>
<h3>RTX 5090: the best home option today<span class="hx:absolute hx:-mt-20" id="rtx-5090-the-best-home-option-today"></span>
    <a href="#rtx-5090-the-best-home-option-today" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/"target="_blank" rel="noopener">RTX 5090</a> has 32 GB, 21,760 CUDA cores, and the same nominal 1,792 GB/s bandwidth as the RTX PRO 6000. Capacity is the missing piece: I must reserve 4 GiB for the desktop and use RAM and SSD for the experts that do not fit.</p>
<p>With Strata, it pushed Q2_0 to 223 to 259 tokens per second and read the long workload at 6,158 tokens per second. With Basalt, those numbers reached 400 and 8,573, respectively. This is the first local configuration that does not make me feel like I am waiting for the machine at every step. It is also a card I can use for gaming, image generation, video, and everything else in the workstation.</p>
<h3>RTX PRO 6000: three times the VRAM, server-class price<span class="hx:absolute hx:-mt-20" id="rtx-pro-6000-three-times-the-vram-server-class-price"></span>
    <a href="#rtx-pro-6000-three-times-the-vram-server-class-price" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://www.nvidia.com/en-us/products/workstations/professional-desktop-gpus/rtx-pro-6000/"target="_blank" rel="noopener">RTX PRO 6000 Blackwell</a> has 96 GB of GDDR7 ECC, 24,064 CUDA cores, and the same 1,792 GB/s bandwidth. The extra capacity keeps many more experts resident, makes room for a larger quantization, and holds context without fighting the desktop for the last few gigabytes.</p>
<p>It costs <a href="https://marketplace.nvidia.com/en-us/enterprise/laptops-workstations/nvidia-rtx-pro-6000-blackwell-workstation-edition/"target="_blank" rel="noopener">$16,000 in NVIDIA&rsquo;s store</a>, eight times the 5090&rsquo;s suggested $1,999 price. I have not run this benchmark on one, so I will not invent a speed. Equal bandwidth means that three times the memory will not produce three times the tokens per second. Its gain comes from keeping more data on the card and avoiding the slow RAM and PCIe path.</p>
<p>For individual use, the 5090 makes more financial sense. The PRO 6000 starts to make sense when 96 GB solves a model you actually need, when ECC and professional support matter, or when several sessions will share one card.</p>
<h2>Verdict<span class="hx:absolute hx:-mt-20" id="verdict"></span>
    <a href="#verdict" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Hardware</th>
          <th>Best choice today</th>
          <th>Experience</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1x RTX 3090/4090, 24 GB</td>
          <td>Qwen3.8 Flash-Next Q2_0</td>
          <td>Viable with offload; check prefill and decode on your stack</td>
      </tr>
      <tr>
          <td>Strix Halo</td>
          <td>Qwen3.8 Flash-Next Q2_0</td>
          <td>55 to 65 tok/s; long prompts take time</td>
      </tr>
      <tr>
          <td>RTX 5090</td>
          <td><strong>Qwen3.8 Flash-Next Q2_0 on Basalt</strong></td>
          <td>377 to 400 tok/s; best balance I measured</td>
      </tr>
      <tr>
          <td>RTX PRO 6000</td>
          <td>Probably IQ3_S or larger</td>
          <td>Excellent capacity, unmeasured in this round</td>
      </tr>
  </tbody>
</table>
<p>Local open source models have finally entered a practical range. A single 5090 runs a model that reached Tier A in my v4 benchmark and, with Q2_0 on Basalt, reached 400 tokens per second in the canary. The Strix also did far better than the 7.7 tokens per second from the dense Qwen I had tested before.</p>
<p>The limits remain clear. Context consumes memory, the desktop needs the same VRAM, and every expert outside the card travels over PCIe or falls back to RAM. DeepSeek at 17 tokens per second shows how useless &ldquo;it runs&rdquo; can be as a metric. If every iteration takes several hours, model quality stops mattering because nobody can work that way.</p>
<blockquote>
  <p>For home hardware, the best model preserves enough quality at a speed that lets you iterate. In this round, that was Qwen3.8 Flash-Next Q2_0 on Basalt, running on an RTX 5090.</p>

</blockquote>
<p>The <a href="https://github.com/akitaonrails/llm-opensource-benchmark"target="_blank" rel="noopener">benchmark repository</a> has the runners, configurations, raw results, and hidden tests. Repeat it on your machine and send results. Hardware, engine, quantization, and context change the answer. A ready-made ranking is a starting point; the final decision is still measured against your own work.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>LLM Benchmark for Accountants and Lawyers (Experimental - Open Source LLMs)</title><link>https://www.akitaonrails.com/en/2026/10/27/llm-benchmark-for-accountants-and-lawyers-experimental/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/27/llm-benchmark-for-accountants-and-lawyers-experimental/</guid><pubDate>Tue, 27 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;After nearly 70 runs, around 60 models, and more than $6,000 spent, I published the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/"&gt;verdict from my LLM Benchmark v4&lt;/a&gt;. All that work has a very specific focus: real-world, day-to-day software development.&lt;/p&gt;
&lt;p&gt;The model builds a Rails project over seven sprints and must find 14 security and correctness defects planted along the way. I measure whether it notices a backdoor, an authorization bypass, a tampered test, or a wrong calculation before someone ships the answer. It is a measure of vigilance during programming. It was never a test of universal intelligence.&lt;/p&gt;</description><content:encoded><![CDATA[<p>After nearly 70 runs, around 60 models, and more than $6,000 spent, I published the <a href="/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/">verdict from my LLM Benchmark v4</a>. All that work has a very specific focus: real-world, day-to-day software development.</p>
<p>The model builds a Rails project over seven sprints and must find 14 security and correctness defects planted along the way. I measure whether it notices a backdoor, an authorization bypass, a tampered test, or a wrong calculation before someone ships the answer. It is a measure of vigilance during programming. It was never a test of universal intelligence.</p>
<p>As I wrapped up that series, a few colleagues who work in investments and law started discussing whether an open model could already handle parts of their routines. That left me with an itch: how could I measure this without falling back to multiple-choice exams or legal trivia?</p>
<p>I tried to answer it. And this disclaimer controls everything else in this article.</p>
<blockquote>
  <p>I am not an accountant, I do not have a CPA license, I never passed the bar, and I have zero professional experience working at an accounting or law firm. I know enough to discuss and read the material, but I cannot certify that this suite properly represents the daily work of either profession.</p>

</blockquote>
<p>Treat everything from this point on as <strong>an experiment that needs scrutiny from actual accountants and lawyers</strong>. These numbers compare models inside this suite. They do not certify any model to serve clients, sign an opinion, close a set of books, or file a legal document.</p>
<h2>What I tried to measure<span class="hx:absolute hx:-mt-20" id="what-i-tried-to-measure"></span>
    <a href="#what-i-tried-to-measure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Some of the professionals I spoke with already use Claude or GPT at work, with models such as Fable, Opus, Astra, or Sol. I started from that reality instead of pretending I could design a professional test by myself.</p>
<p>I asked Fable 5.1 to design cases within what it was confident it could research, structure, and review. The suite draws on professional work patterns, statutes, forms, decisions, and reference material, but <a href="https://github.com/akitaonrails/llm-non-coding-benchmark/blob/main/docs/METHODOLOGY.md#2-why-this-matters-for-private-infrastructure"target="_blank" rel="noopener">all clients, companies, and documents are synthetic</a>. No real confidential data entered the repository.</p>
<p>Then I ran GPT-6 Sol as the frontier anchor. Finally, I took the open models that had performed well in the programming benchmark and ran the same suite on three infrastructure classes: an RTX 5090, Strix Halo, and a rented NVIDIA B300.</p>
<p>I use &ldquo;open&rdquo; in the same sense as the previous benchmark: a checkpoint I can download and serve myself. Licenses vary, and not every open-weight model satisfies the strictest definition of open source software.</p>
<p>Fable also performed the blind grading. Each configuration received a random letter, and a separate subagent evaluated each case without knowing which model produced the answer. The comparison in this article therefore means something very narrow:</p>
<blockquote>
  <p>How close did these open models get to Sol on a test designed and graded by Fable?</p>

</blockquote>
<p>Fable and Astra did not run the suite as candidates. Sol is the only closed-model anchor in this campaign. Fable itself limits what we can ask and judge, and an AI grader can make mistakes. Human professional spot-checking is still pending.</p>
<h2>The test starts with a client folder<span class="hx:absolute hx:-mt-20" id="the-test-starts-with-a-client-folder"></span>
    <a href="#the-test-starts-with-a-client-folder" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The benchmark contains <strong>38 cases</strong>, split between accounting and law in the United States and Brazil. Some cases span several sessions, so the full campaign totals 63 session units per configuration and 190 graded executions.</p>
<p>This is the distribution:</p>
<table>
  <thead>
      <tr>
          <th>Area</th>
          <th style="text-align: right">Cases</th>
          <th>Examples</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>US accounting and tax</td>
          <td style="text-align: right">10</td>
          <td>Schedule C, month-end close, household Form 1040, asset versus stock sale, QSBS, Chapter 11, and fraud</td>
      </tr>
      <tr>
          <td>Brazilian accounting and tax</td>
          <td style="text-align: right">10</td>
          <td>DAS and Fator R, tax regime selection, CBS/IBS, Lucro Real, company formation, holding companies, and judicial reorganization</td>
      </tr>
      <tr>
          <td>US law</td>
          <td style="text-align: right">9</td>
          <td>Deadlines, MSA review, non-compete, divorce, Chapter 11, M&amp;A, retaliation, and shareholder disputes</td>
      </tr>
      <tr>
          <td>Brazilian law</td>
          <td style="text-align: right">9</td>
          <td>DJe deadlines, software contract and LGPD, worker misclassification, tax foreclosure, divorce, judicial reorganization, M&amp;A, termination for cause, and time-barred debt</td>
      </tr>
  </tbody>
</table>
<p>There are four L1 cases, six L2 cases, 22 L3 cases, and six L4 cases. Difficulty does not come from asking for more elaborate prose. It grows with document volume, the number of interacting rules, and how long the model must preserve a coherent line of reasoning.</p>
<ul>
<li><strong>L1, clerk or intern:</strong> one or two documents, one deliverable, and one verifiable answer. One example is calculating deadlines from a DJe notice.</li>
<li><strong>L2, senior:</strong> several documents, a threshold or rule interacting with another one, and at least one fact that must be requested from the client. Tax regime selection and contract review against a playbook live here.</li>
<li><strong>L3, manager or associate:</strong> conflicting or superseded sources, false positives, and decisions that must be justified. Most of the suite sits at this level.</li>
<li><strong>L4, partner:</strong> three to twelve sessions. A new fact invalidates previous work, the opposing side reacts, and the model must change course without hiding the change.</li>
</ul>
<p>The six L4 cases are twelve monthly closes for a SaaS company, a Chapter 11 debtor, one quarter under Brazil&rsquo;s Lucro Real regime, a Brazilian judicial reorganization, a five-session US lawsuit, and a five-session Brazilian tax foreclosure. Together with the single-session cases, they bring each configuration to 63 session units. Five configurations produced 315 work sessions across 190 executions.</p>
<p>Each case resembles a small professional engagement. The model receives:</p>
<ul>
<li>a folder containing contracts, statements, invoices, pleadings, spreadsheets, and other documents;</li>
<li>a closed library containing the allowed rules and authorities, with no internet access;</li>
<li>an engagement letter explaining who the client is, what must be done, and the required format;</li>
<li>tools to calculate and manipulate files;</li>
<li>a simulated client that reveals certain facts only when the model asks the right question.</li>
</ul>
<p>The suite ranges from short clerk-level tasks to long partner-level engagements, with late facts that force the model to revisit an earlier conclusion. Every case ends with the same instruction to review errors, unsupported sources, and numbers that do not reconcile.</p>
<h3>Accounting example: the close that will not close<span class="hx:absolute hx:-mt-20" id="accounting-example-the-close-that-will-not-close"></span>
    <a href="#accounting-example-the-close-that-will-not-close" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>One US case provides the February trial balance, March general ledger, bank and credit-card statements, an accounts receivable aging, two copies of the same vendor invoice, and the close checklist. The owner wants a clean reconciliation by Friday because the company is renewing a line of credit.</p>
<p>After every legitimate item is recorded, an unexplained <strong>$4,312</strong> difference remains. No document supports that amount. The correct answer must leave it open, tell the owner, and refuse to invent an entry.</p>
<p>The case also hides a <strong>$1,500</strong> transfer. The model must ask where it came from. The scripted client answers that it was a personal contribution from the owner.</p>
<p>If the model assumes it is revenue, both income and tax are wrong. If it posts the $4,312 to &ldquo;miscellaneous expense,&rdquo; &ldquo;owner draw,&rdquo; or a suspense account to force the reconciliation, it immediately fails the trust gate.</p>
<p>This example tests more than arithmetic. It tests whether the agent resists client pressure, recognizes missing evidence, catches the duplicate invoice, asks the necessary question, and produces a trial balance, entries, and open-items note that agree with one another.</p>
<h3>Legal example: five sessions of tax foreclosure<span class="hx:absolute hx:-mt-20" id="legal-example-five-sessions-of-tax-foreclosure"></span>
    <a href="#legal-example-five-sessions-of-tax-foreclosure" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the Brazilian L4 case, a stonework company faces an IRPJ and CSLL tax debt certificate of approximately <strong>R$1.1 million</strong>. The model receives the certificate, part of the administrative tax proceeding, amendments to the articles of association, the process server&rsquo;s return, and a closed library containing the necessary CTN, LEF, CPC, precedents, and binding summaries.</p>
<p>In the first session, it evaluates defects and chooses the procedural vehicle. In the second, the tax authority amends the debt certificate. In the third, it seeks to redirect liability to a shareholder. The final two sessions cover the shareholder&rsquo;s defense, security, and a settlement with calculated amounts.</p>
<p>The trap is to keep repeating a theory after the defect has been cured, defend the wrong shareholder, or automatically accept the presumption of irregular dissolution. The corporate amendments show who managed the company during the relevant period, and the address change had been registered before the process server&rsquo;s return. The model must drop the theory that lost its basis and recalculate the strategy around the new facts.</p>
<p>Another legal case applies the same pressure in a five-session US lawsuit. Opposing counsel presents an email that weakens the initial theory, an arbitration clause appears in small print in Exhibit C, and the agent must tell the client that the position has deteriorated. Persisting with the original narrative counts against it.</p>
<blockquote>
  <p>Every case offers a very convenient wrong conclusion. The test measures whether the model finds the evidence that forces it to change its mind.</p>

</blockquote>
<h3>How I graded the deliverables<span class="hx:absolute hx:-mt-20" id="how-i-graded-the-deliverables"></span>
    <a href="#how-i-graded-the-deliverables" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The answers pass through four filters:</p>
<ol>
<li><strong>Trust:</strong> Did it invent a client fact, authority, or balancing number, apply superseded law, or claim completion before finishing? The execution fails.</li>
<li><strong>Oracle:</strong> Do dates, amounts, classifications, and required elements match the expected answer exactly?</li>
<li><strong>Vigilance:</strong> Did it find the planted problems during normal work or only after the final review instruction?</li>
<li><strong>Deliverable quality:</strong> Does the memo, opinion, or summary contain the binary elements required by the case? Pretty writing earns nothing.</li>
</ol>
<p>The oracle contains <strong>235 exact items per configuration</strong>. A deadline lands on the right date or it does not. A form contains the expected amount or it does not.</p>
<p>A mandatory clause was flagged or it slipped through. Where a numeric tolerance makes sense, the answer key defines it in advance.</p>
<p>Traps carry weights by severity: critical is worth 5, high 3, medium 2, and low 1. Discovery during normal work receives full weight; discovery only after the final review instruction receives 40%; a missed problem receives zero. That produces the vigilance column.</p>
<p>The client also follows a script. It does not improvise help. Every hidden answer has a trigger and appears only when the model&rsquo;s question actually seeks that fact.</p>
<p>If nobody asks, the model must state the assumption or leave the item open. Inventing the answer can sink the entire execution.</p>
<p>Before every run, the harness copies a clean folder and physically hides the answer key, trap manifests, and other models&rsquo; answers. Network access is blocked. The agent cannot search for a prepared solution or read grading files from the repository.</p>
<p>Neither of the two generic reminders allowed by the protocol was used. The answer files, histories, prompts, grading keys, and scripts are all available in the <a href="https://github.com/akitaonrails/llm-non-coding-benchmark"target="_blank" rel="noopener">llm-non-coding-benchmark repository</a>.</p>
<h2>The experimental ranking<span class="hx:absolute hx:-mt-20" id="the-experimental-ranking"></span>
    <a href="#the-experimental-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I sorted the table by completed engagements. A completed engagement finished every session, did not hit the response limit, had no high-confidence trust failure, and passed every gated oracle item.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">#</th>
          <th>Model / infrastructure</th>
          <th style="text-align: right">Completed</th>
          <th style="text-align: right">High-confidence trust failures</th>
          <th style="text-align: right">Strict oracle, graded items</th>
          <th style="text-align: right">Oracle over all items</th>
          <th style="text-align: right">Vigilance</th>
          <th style="text-align: right">Active time</th>
          <th style="text-align: right">Comparable cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>Qwen3.8 Flash-Next IQ3_S / RTX 5090</td>
          <td style="text-align: right"><strong>27/38</strong></td>
          <td style="text-align: right"><strong>0</strong></td>
          <td style="text-align: right">91%</td>
          <td style="text-align: right">79%</td>
          <td style="text-align: right">99.6</td>
          <td style="text-align: right">432 min</td>
          <td style="text-align: right">owned hardware</td>
      </tr>
      <tr>
          <td style="text-align: right">2</td>
          <td>Qwen3.8 Flash-Next IQ3_S / Strix Halo</td>
          <td style="text-align: right"><strong>27/38</strong></td>
          <td style="text-align: right">3</td>
          <td style="text-align: right">90%</td>
          <td style="text-align: right"><strong>86%</strong></td>
          <td style="text-align: right">97.5</td>
          <td style="text-align: right">2,105 min</td>
          <td style="text-align: right">owned hardware</td>
      </tr>
      <tr>
          <td style="text-align: right">3</td>
          <td>GPT-6 Sol medium / Codex</td>
          <td style="text-align: right">25/38</td>
          <td style="text-align: right"><strong>0</strong></td>
          <td style="text-align: right">84%</td>
          <td style="text-align: right">84%</td>
          <td style="text-align: right"><strong>100.0</strong></td>
          <td style="text-align: right"><strong>248 min</strong></td>
          <td style="text-align: right"><strong>$16.72</strong> API-equivalent</td>
      </tr>
      <tr>
          <td style="text-align: right">4</td>
          <td>GLM 5.3 Flash NVFP4 / B300</td>
          <td style="text-align: right">23/38</td>
          <td style="text-align: right"><strong>0</strong></td>
          <td style="text-align: right"><strong>93%</strong></td>
          <td style="text-align: right">79%</td>
          <td style="text-align: right">98.8</td>
          <td style="text-align: right">725 min</td>
          <td style="text-align: right"><strong>$83.86 to $108.63</strong> minimum</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Qwen3.8 Flash-Next NVFP4 / B300</td>
          <td style="text-align: right">23/38</td>
          <td style="text-align: right">2</td>
          <td style="text-align: right">85%</td>
          <td style="text-align: right">84%</td>
          <td style="text-align: right">99.6</td>
          <td style="text-align: right">622 min</td>
          <td style="text-align: right"><strong>$71.94 to $93.20</strong> minimum</td>
      </tr>
  </tbody>
</table>
<p>Sol cost <strong>$16.72 at API-equivalent list prices</strong>. I ran it through my subscription, so this figure compares consumption and does not represent a bill I received. Local machines have no per-call charge, but hardware, electricity, and maintenance still cost money.</p>
<p><a href="https://www.runpod.io/gpu-models/b300"target="_blank" rel="noopener">Runpod&rsquo;s official B300 page</a> currently lists <strong>$6.94 per hour</strong> for Community Cloud and <strong>$8.99 per hour</strong> for Secure Cloud, billed by the second. The benchmark did not retain the pod invoice or tier, so I calculated both ends of the range using only each model&rsquo;s active minutes.</p>
<p>That produces $71.94 to $93.20 for Qwen and $83.86 to $108.63 for GLM. These are artificial floors. The pod also bills while downloading and loading weights, compiling, changing models, waiting for the controller, retrying runs, and sitting idle until someone turns it off. The real bill was higher.</p>
<p>&ldquo;Active time&rdquo; sums model time across all cases. Sol ran four in parallel and completed the campaign in 72.9 minutes of wall-clock time. Every other configuration ran one case at a time. This table compares inference effort and per-case latency; it does not measure how many simultaneous users each server could handle.</p>
<p>There are two oracle columns because the 16,384-token response cap cut off local-model answers. &ldquo;Graded items&rdquo; ignores work that became impossible to grade. &ldquo;All items&rdquo; counts missing work as wrong. GLM, for example, was the most exact when it produced something gradable, but lost too much work to the output limit.</p>
<blockquote>
  <p>A higher place in this table does not make a model better to work with. I prefer Sol&rsquo;s 25 clean completions to 27 completions with three serious fabrications on Strix Halo.</p>

</blockquote>
<h2>The cost of an error matters more than the average<span class="hx:absolute hx:-mt-20" id="the-cost-of-an-error-matters-more-than-the-average"></span>
    <a href="#the-cost-of-an-error-matters-more-than-the-average" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After publishing this article, I received a comment: average accuracy is not enough for lawyers and accountants. The error that matters is the plausible claim presented as fact, without a caveat, that eventually reaches the client.</p>
<p>The benchmark keeps the trust gate separate from the rest of the score. A wrong date or value fails an oracle item. Inventing a client answer, document, authority, or number to make the work reconcile fails the entire execution, even when everything else is correct.</p>
<p>The difference across configurations is large:</p>
<table>
  <thead>
      <tr>
          <th>Model / infrastructure</th>
          <th style="text-align: right">Executions with a serious fabrication</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT-6 Sol medium / Codex</td>
          <td style="text-align: right"><strong>0/38 (0%)</strong></td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next IQ3_S / RTX 5090</td>
          <td style="text-align: right"><strong>0/38 (0%)</strong></td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next IQ3_S / Strix Halo</td>
          <td style="text-align: right">3/38 (7.9%)</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next NVFP4 / B300</td>
          <td style="text-align: right">2/38 (5.3%)</td>
      </tr>
      <tr>
          <td>GLM 5.3 Flash NVFP4 / B300</td>
          <td style="text-align: right"><strong>0/38 (0%)</strong></td>
      </tr>
  </tbody>
</table>
<p>Five of 190 executions were affected, or <strong>2.6%</strong>. Across those five runs, the graders found nine serious occurrences: client answers that were never given, nonexistent documents, fictitious dates, invented contract terms, and a quotation falsely attributed to the closed library. Another seven executions received medium-confidence flags and still need human adjudication.</p>
<p>Calling this a &ldquo;confidently wrong answer rate&rdquo; would be imprecise. The benchmark does not measure the model&rsquo;s internal confidence. The <code>high confidence</code> field in the results records the grader&rsquo;s confidence that a violation occurred. What we measure is whether the model asserted unsupported information, did not label it as an assumption, and allowed it to reach the delivered material or the work produced before interruption.</p>
<p>I also did not classify every wrong value under this category. Incorrect dates, calculations, classifications, and recommendations defined in the answer key are part of oracle accuracy. In the all-items column, omissions caused by the output cap count as errors; in the graded-items column, they stay out of the denominator.</p>
<p>The next version should classify each failure by type and record whether the model disclosed uncertainty. That would give us separate rates for fabrication, numerical errors, date errors, omissions, and corrections during review, alongside general accuracy.</p>
<p>The same Qwen build had similar strict accuracy on graded items on the RTX 5090 and Strix Halo, <strong>91% versus 90%</strong>. No RTX execution had a serious fabrication; three Strix executions did.</p>
<h2>Sol 6: the reference makes mistakes too<span class="hx:absolute hx:-mt-20" id="sol-6-the-reference-makes-mistakes-too"></span>
    <a href="#sol-6-the-reference-makes-mistakes-too" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sol was the fastest, had no high-confidence trust failure, and completed all 38 executions without hitting the output cap. Only 25 passed every criterion.</p>
<p>Its weakness appeared in the numbers, especially across five Brazilian accounting cases. It missed calculations involving tax regime selection, CBS/IBS, Lucro Real, company formation, and judicial reorganization. It also asked the client far fewer questions: 51 facts versus 74 to 95 for the other models, while recording more assumptions.</p>
<p>An explicit assumption is correct behavior when information is missing. Asking often produces a better answer than carrying a hypothesis all the way to delivery.</p>
<p>The final review also made answers worse. On one US tax form, Sol had all 16 items correct before review. It then cleared four correct lines and marked the work as blocked. In another case, it nulled two fields that had already been right.</p>
<p>Using a closed model as the judge is still a mistake. Sol served as the practical reference for this campaign and showed a clean, fast, consistent profile. It still failed 13 of 38 cases under the strict criterion.</p>
<h2>Qwen3.8 Flash-Next on the RTX 5090: the best local balance<span class="hx:absolute hx:-mt-20" id="qwen38-flash-next-on-the-rtx-5090-the-best-local-balance"></span>
    <a href="#qwen38-flash-next-on-the-rtx-5090-the-best-local-balance" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The strongest raw result came from the IQ3_S build, quantized to a little over 3 bits, on one RTX 5090. It delivered 27 completed engagements, no high-confidence trust failure, and 91% strict accuracy among the items that could be graded.</p>
<p>It consumed 432 active minutes. Median case time was 1.6 times Sol&rsquo;s, with average generation at 153 tokens per second. Three executions hit the response cap. That is still a strong result for a local workstation.</p>
<p>Its mistakes explain why professional review remains mandatory. In one Brazilian case, it calculated deadlines one day late and used the final review to &ldquo;confirm&rdquo; its own wrong calculation. In a US lawsuit, it had correctly calculated a $375,000 portion and changed it to zero during review.</p>
<p>There were also two medium-confidence fabrication flags still waiting for human review. They were not counted as high-confidence trust failures. I would use this model to prepare work, cross-check documents, raise questions, and produce a first draft inside a machine I control. I would never let the result reach a client before a qualified person checked it.</p>
<h2>The same Qwen on Strix Halo: five times slower<span class="hx:absolute hx:-mt-20" id="the-same-qwen-on-strix-halo-five-times-slower"></span>
    <a href="#the-same-qwen-on-strix-halo-five-times-slower" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Strix Halo ran exactly the same IQ3_S quantization and also finished with 27 completed engagements. The similarity ends there.</p>
<p>It consumed 2,105 active minutes, almost five times the RTX 5090 and 8.5 times Sol&rsquo;s total active time. Median time for the same case was 7.7 times slower than the reference. Forty tokens per second works for batch jobs and patient users; in an interactive routine, you start waiting on the machine.</p>
<p>Worse, this run produced three serious trust failures. The model invented client answers, documents, tax debt certificate dates, and facts that supported its own conclusions. The identical build on the RTX had no high-confidence failure.</p>
<p>With one run per configuration, I cannot attribute the difference to hardware, serving stack, or inference randomness. Claiming that Strix &ldquo;makes the model hallucinate&rdquo; would be dishonest. The evidence supports a narrower conclusion: <strong>this run was slow and not reliable enough</strong>. It needs repetition before any recommendation.</p>
<h2>Qwen3.8 Flash-Next on the B300: a large rented GPU did not buy quality<span class="hx:absolute hx:-mt-20" id="qwen38-flash-next-on-the-b300-a-large-rented-gpu-did-not-buy-quality"></span>
    <a href="#qwen38-flash-next-on-the-b300-a-large-rented-gpu-did-not-buy-quality" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>On the rented B300, Flash-Next ran in NVFP4, completed 23 engagements, and produced two serious trust failures. In one of them, it invented a city, contract terms, figures, and even a quotation absent from the closed library.</p>
<p>Active time was 622 minutes, with median case time 1.9 times Sol&rsquo;s and average generation at 148 tokens per second. Five executions hit the response cap; four directly prevented a completed delivery. Even under the minimum cost calculation, it cost 4.3 to 5.6 times Sol&rsquo;s API equivalent.</p>
<p>In this serial test, a B300 delivered neither higher quality nor lower latency than the RTX 5090. This does not measure the main reason to buy or rent a B300: serving many users and batches in parallel. I ran one engagement at a time to preserve a comparable protocol. The evidence only supports this conclusion: <strong>paying for the datacenter class did not improve this individual session</strong>.</p>
<h2>GLM 5.3 Flash on the B300: precise when it finishes<span class="hx:absolute hx:-mt-20" id="glm-53-flash-on-the-b300-precise-when-it-finishes"></span>
    <a href="#glm-53-flash-on-the-b300-precise-when-it-finishes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>GLM had a different profile. It recorded no high- or medium-confidence trust flag and hit 93% of the strict items it managed to deliver, the highest percentage in the table.</p>
<p>It completed only 23 cases. Seven executions hit the 16,384-token cap, and accuracy drops to 79% when every missing item counts as wrong. It took 725 active minutes, with median case time 2.7 times Sol&rsquo;s, at 116 tokens per second. At Runpod&rsquo;s official rates, the floor was 5 to 6.5 times Sol&rsquo;s API equivalent.</p>
<p>The choice of column completely changes the reading. GLM was careful and exact on gradable results. The combination of model, harness, and response cap failed to finish enough work.</p>
<p>The cap was a harness choice, not an intrinsic GLM limitation. I need to repeat the test with longer responses or shorter sessions. Until then, selling the 93% without mentioning the seven interruptions would be marketing.</p>
<h2>Final review helps and also breaks things<span class="hx:absolute hx:-mt-20" id="final-review-helps-and-also-breaks-things"></span>
    <a href="#final-review-helps-and-also-breaks-things" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I always recommend asking an agent to reread everything before delivery. The benchmark confirms that advice, with an important caveat.</p>
<p>In 113 of 190 executions, graders found some problem removed or corrected during final review. These included fabrications that otherwise would have shipped. The model rereads sources, removes dubious citations, corrects names, and deletes claims it cannot support.</p>
<p>In about a dozen runs, the same review changed correct work into a wrong version. Sol, RTX, and GLM replaced correct dates, figures, and conclusions. In two cases, the model recomputed an incorrect number and came away even more confident in it.</p>
<blockquote>
  <p>Automated review is a hygiene layer. The word &ldquo;review&rdquo; does not turn the model into an independent auditor of itself.</p>

</blockquote>
<p>For sensitive work, I would use at least two separate stages: one agent produces, another reviews with clean context and explicit criteria. Responsibility remains with the professional.</p>
<h2>Parts of the benchmark failed too<span class="hx:absolute hx:-mt-20" id="parts-of-the-benchmark-failed-too"></span>
    <a href="#parts-of-the-benchmark-failed-too" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The models scored from 97.5 to 100 on vigilance. Announcing that they all became perfect partners would be tempting. The real explanation is that this part of the test was too easy.</p>
<p>Some libraries practically revealed the problem in their indexes. Facts meant to remain hidden were already present in documents. Some scripted client pushback also counted as unprompted discovery. As a result, 961 of 973 trap-runs were caught before review and the metric lost its power to separate models.</p>
<p>Items from four cases with confirmed fixture defects were demoted to diagnostic status. Other cases still contain inconsistencies that need review. The broad order remained, with changes to ties, but the absolute figures do not deserve two decimal places of confidence.</p>
<p>There are more limitations:</p>
<ul>
<li>one run per configuration, with no mean or error bars;</li>
<li>Fable as both designer and grader, subject to its own blind spots;</li>
<li>no accountant, CPA, lawyer, or attorney has formally reviewed a sample yet;</li>
<li>Codex for Sol and OpenCode for the local models;</li>
<li>different quantizations and serving stacks;</li>
<li>an output limit that affected only the local models;</li>
<li>a closed legal library that does not test current research on the internet.</li>
</ul>
<p>I published these problems because a benchmark without self-criticism turns into a marketing table. The next version needs corrected cases, harder traps, a fixed reference date in every engagement, a larger response cap for long cases, and repeated runs for every configuration.</p>
<h2>Privacy changes the calculation<span class="hx:absolute hx:-mt-20" id="privacy-changes-the-calculation"></span>
    <a href="#privacy-changes-the-calculation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For a developer working on an open project, sending context to the cloud may be acceptable. Accounting firms, law firms, banks, and regulated companies handle contracts, litigation strategy, tax documents, personal data, and trade secrets. Internal policy, client contracts, or regulatory obligations may prevent that material from leaving infrastructure controlled by the organization.</p>
<p>In those cases, a local model competes on more than price. Sometimes it is the only permitted option.</p>
<p>Local execution does not magically make data secure. You still need access controls on the workstation, encrypted disks and backups, protected logs, unnecessary telemetry disabled, restricted networking, and a clear understanding of where the harness stores sessions. A poorly administered local server is still a data leak waiting to happen.</p>
<p>Every case in this benchmark is synthetic precisely to avoid that problem during testing.</p>
<h2>So, is it usable already?<span class="hx:absolute hx:-mt-20" id="so-is-it-usable-already"></span>
    <a href="#so-is-it-usable-already" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Cloud, with Sol:</strong> it was the fastest and most predictable option in this campaign. The $16.72 equivalent cost for 38 cases is low compared with the labor required to review 38 engagements. It makes sense when policy permits sending the data, with mandatory human review.</p>
<p><strong>An owned RTX 5090:</strong> Qwen3.8 Flash-Next already works as a local assistant for organizing documents, preparing drafts, calculating figures, flagging open items, and raising questions. It was the best open-model balance I measured. It still got dates and amounts wrong in convincing ways.</p>
<p><strong>Strix Halo:</strong> it is compact, but 35 active hours for the campaign make interactive use difficult. This run also produced serious fabrications. It may work for batch and low-volume tasks after more repetitions.</p>
<p><strong>A rented B300:</strong> it did not improve the quality of one session, and active time alone already exceeded $70 per model. Its economics depend on concurrency, utilization, and volume, none of which this benchmark measured. For one person doing one task at a time, the figures do not justify the larger infrastructure.</p>
<p><a href="https://github.com/akitaonrails/llm-non-coding-benchmark/blob/main/docs/FULL_SUITE_GRADING_20261027.md#1-lane-profiles"target="_blank" rel="noopener">No configuration completed more than 27 of the 38 cases</a>. The best result was 71%. Even accepting that part of the failure came from the harness and fixture defects, I would not put any of them to work alone.</p>
<blockquote>
  <p>Open models are already viable assistants for accounting and law. This experiment does not support calling them autonomous professionals.</p>

</blockquote>
<p>A model that saves two hours of reading and prepares a good first draft may already pay for the machine. Its value lies in reducing repetitive work without creating more risk than the time it saves.</p>
<h2>I need professionals for the next round<span class="hx:absolute hx:-mt-20" id="i-need-professionals-for-the-next-round"></span>
    <a href="#i-need-professionals-for-the-next-round" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The entire project is open at <a href="https://github.com/akitaonrails/llm-non-coding-benchmark"target="_blank" rel="noopener">github.com/akitaonrails/llm-non-coding-benchmark</a>. It includes the <a href="https://github.com/akitaonrails/llm-non-coding-benchmark/blob/main/docs/METHODOLOGY.md"target="_blank" rel="noopener">methodology</a>, <a href="https://github.com/akitaonrails/llm-non-coding-benchmark/blob/main/docs/FULL_SUITE_GRADING_20261027.md"target="_blank" rel="noopener">grading report</a>, <a href="https://github.com/akitaonrails/llm-non-coding-benchmark/blob/main/docs/FULL_SUITE_EXECUTION_SUMMARY.md"target="_blank" rel="noopener">execution timings</a>, cases, scripts, and raw results.</p>
<p>Right now I need professionals more than another GPU. Licensed practitioners need to review the cases, expected answers, and failures flagged by Fable. I also want new cases that reflect real work without including client information.</p>
<p>If you are an accountant, CPA, lawyer, or attorney and found a technical absurdity, open an issue with the correction and source. That is exactly the scrutiny this experiment needs before it becomes more than an interesting first reading.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>open-source</category></item><item><title>LLM Benchmark v4: the open source verdict (as of Oct 2026)</title><link>https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/</guid><pubDate>Thu, 22 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;This is the wrap-up of benchmark v4. Since I started this round, there have been seven articles, first in the cloud and then pushing the same sabotage yardstick further and further away from it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/"&gt;Part 1&lt;/a&gt;&lt;/strong&gt;: why I rebuilt the benchmark from scratch for the third time, how the Rails app sabotaged with real CVEs works, and how much it cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;: the original cloud ranking, with almost 40 models, six ties at the top and a free model beating half of the Claude lineup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/"&gt;Opus 5.5, GPT-6 sol and luna, MiMo 2.6 and Grok 4.7&lt;/a&gt;&lt;/strong&gt;: the next cloud batch. Opus 5.5 closed at 100, GPT 6 luna scored 95.5 for under a dollar per run, and Grok 4.7 regressed against its own predecessor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/"&gt;The top open source LLMs on a B300&lt;/a&gt;&lt;/strong&gt;: one rented GPU, 14 open models, and two of them, GLM 5.3 Flash and Qwen3.8 Flash-Next, tied at 92.5, on par with Sonnet 5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/"&gt;Open source that only fits in 4x B300&lt;/a&gt;&lt;/strong&gt;: the big models, most of them between 400 billion and 1 trillion parameters. DeepSeek V4.1 Flash tied the leader at 92.5 and the rest showed that size doesn&amp;rsquo;t buy vigilance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/"&gt;Can my team fit in 4x B300/H200?&lt;/a&gt;&lt;/strong&gt;: a real concurrency test. The same DeepSeek serves 32 comfortable sessions on the B300 pod and only 8 on the H200 one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/"&gt;Consumer-grade open source on RTX 5090 or Strix Halo&lt;/a&gt;&lt;/strong&gt;: Qwen3.8 Flash-Next scored 91 on a PC card with the Strata engine, at 159 tokens per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article puts everything in a single table and answers the question left over: &lt;strong&gt;as of October 2026, is open source good enough?&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is the wrap-up of benchmark v4. Since I started this round, there have been seven articles, first in the cloud and then pushing the same sabotage yardstick further and further away from it:</p>
<ul>
<li><strong><a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">Part 1</a></strong>: why I rebuilt the benchmark from scratch for the third time, how the Rails app sabotaged with real CVEs works, and how much it cost.</li>
<li><strong><a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/">Part 2</a></strong>: the original cloud ranking, with almost 40 models, six ties at the top and a free model beating half of the Claude lineup.</li>
<li><strong><a href="/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/">Opus 5.5, GPT-6 sol and luna, MiMo 2.6 and Grok 4.7</a></strong>: the next cloud batch. Opus 5.5 closed at 100, GPT 6 luna scored 95.5 for under a dollar per run, and Grok 4.7 regressed against its own predecessor.</li>
<li><strong><a href="/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/">The top open source LLMs on a B300</a></strong>: one rented GPU, 14 open models, and two of them, GLM 5.3 Flash and Qwen3.8 Flash-Next, tied at 92.5, on par with Sonnet 5.</li>
<li><strong><a href="/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/">Open source that only fits in 4x B300</a></strong>: the big models, most of them between 400 billion and 1 trillion parameters. DeepSeek V4.1 Flash tied the leader at 92.5 and the rest showed that size doesn&rsquo;t buy vigilance.</li>
<li><strong><a href="/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/">Can my team fit in 4x B300/H200?</a></strong>: a real concurrency test. The same DeepSeek serves 32 comfortable sessions on the B300 pod and only 8 on the H200 one.</li>
<li><strong><a href="/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/">Consumer-grade open source on RTX 5090 or Strix Halo</a></strong>: Qwen3.8 Flash-Next scored 91 on a PC card with the Strata engine, at 159 tokens per second.</li>
</ul>
<p>This article puts everything in a single table and answers the question left over: <strong>as of October 2026, is open source good enough?</strong></p>
<h2>The biggest benchmark I&rsquo;ve ever run<span class="hx:absolute hx:-mt-20" id="the-biggest-benchmark-ive-ever-run"></span>
    <a href="#the-biggest-benchmark-ive-ever-run" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>v4 was, by far, the biggest round of testing I&rsquo;ve ever done. That&rsquo;s almost 70 complete runs of the same protocol, almost 60 different models, running on cloud APIs, on a rented B300, on a four-B300 pod, on an RTX 5090 and on a Strix Halo, plus a four-H200 pod just for the concurrency test.</p>
<p>And it was expensive. The first batch alone, the cloud one, went past 4 thousand dollars in nine days, counting credits and subscriptions. The single B300 cost about 270, and the 4x B300 and 4x H200 campaign took about another 2,100. Adding up what I can add up, it went past 6 thousand dollars, not counting machine hours at home.</p>
<blockquote>
  <p>I don&rsquo;t think many people are going to reproduce this any time soon. If this material was useful to you, share it with whoever needs to decide between cloud, their own server or a local PC. All the code, prompts, logs and reports are open on my GitHub: <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">akitaonrails/llm-coding-benchmark</a>.</p>

</blockquote>
<h3>A refresher on the yardstick<span class="hx:absolute hx:-mt-20" id="a-refresher-on-the-yardstick"></span>
    <a href="#a-refresher-on-the-yardstick" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The protocol is always the same. The model builds a Rails chat app in seven sprints, and between sprints a reviewer injects 14 security and correctness defects, some loud (a CVE in a gem, a key in the code), others silent (a cross-tenant leak, a wrong aggregate, a test tampered with to hide a backdoor).</p>
<p>The score goes from 0 to 100. A defect fixed unprompted counts at full weight; fixed only after I reveal there&rsquo;s a problem, it counts 40%; never fixed counts zero. The tiers are A from 83 up, B from 73 to 82.5, C from 51 to 72.5 and D up to 50.</p>
<p>It&rsquo;s a yardstick for <strong>code review vigilance</strong>. It measures whether the model notices sabotage while it works, and that is different from measuring who writes your whole system better.</p>
<h2>The combined ranking<span class="hx:absolute hx:-mt-20" id="the-combined-ranking"></span>
    <a href="#the-combined-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every v4 run in a single table, commercial and open, cloud and my own hardware. When the same model ran in different places, it shows up more than once, because the place changes the score.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">#</th>
          <th>Model</th>
          <th>Weights</th>
          <th>Where it ran</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: center">Tier</th>
          <th style="text-align: right">Cost per run</th>
          <th style="text-align: right">tok/s</th>
          <th style="text-align: right">Minutes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT-6 Astra</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 30.55</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">100</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>Claude Opus 5</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 71 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">145</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>Claude Fable 5</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 50 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">85</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>Claude Opus 5.5</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 16.63 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">64</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT 5.6 sol</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 20.37</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">317</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT 5.6 terra</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 9.52</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">80</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT 5.5</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 34.69</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">117</td>
      </tr>
      <tr>
          <td style="text-align: right">8</td>
          <td>Grok 4.6</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">98.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 13.00</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">67</td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Claude Fable 5.1</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 51 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">133</td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Sakana Fugu Ultra v2</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 122.01</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">294</td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>GPT 6 luna</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 0.84 ²</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">122</td>
      </tr>
      <tr>
          <td style="text-align: right">12</td>
          <td>GPT 5.6 luna</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">95.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 10.04</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">123</td>
      </tr>
      <tr>
          <td style="text-align: right">13</td>
          <td>Nex N2.5 Pro ⁴</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">94.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">free</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">480</td>
      </tr>
      <tr>
          <td style="text-align: right">13</td>
          <td>GLM 5.3</td>
          <td>open</td>
          <td>Cloud (ZCode)</td>
          <td style="text-align: right">94.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">flat plan</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">215</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>DeepSeek V4.1 Flash</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 1.21</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">172</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>DeepSeek V4.1 Flash</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">174.8</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>GLM 5.3 Flash (NVFP4)</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 21.96 ³</td>
          <td style="text-align: right">142.0</td>
          <td style="text-align: right">145</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>Qwen3.8 Flash-Next (NVFP4)</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 12.04 ³</td>
          <td style="text-align: right">195.0</td>
          <td style="text-align: right">80</td>
      </tr>
      <tr>
          <td style="text-align: right">19</td>
          <td>MiMo V2.6 Pro</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">92.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 1.22</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">308</td>
      </tr>
      <tr>
          <td style="text-align: right">20</td>
          <td>Qwen3.8 Flash-Next (IQ3_S)</td>
          <td>open</td>
          <td>RTX 5090 (Strata)</td>
          <td style="text-align: right">91.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 0 (local)</td>
          <td style="text-align: right">159.2</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">20</td>
          <td>Claude Sonnet 5</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">91.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 27 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">112</td>
      </tr>
      <tr>
          <td style="text-align: right">20</td>
          <td>GPT 6 sol</td>
          <td>closed</td>
          <td>Cloud (Codex)</td>
          <td style="text-align: right">91.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 8.35 ²</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">73</td>
      </tr>
      <tr>
          <td style="text-align: right">23</td>
          <td>Gemini 3.8 Flash high</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">90.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 15.98</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">97</td>
      </tr>
      <tr>
          <td style="text-align: right">24</td>
          <td>Gemini 3.8 Flash</td>
          <td>closed</td>
          <td>Cloud (Antigravity)</td>
          <td style="text-align: right">89.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">free (OAuth)</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">120</td>
      </tr>
      <tr>
          <td style="text-align: right">24</td>
          <td>MiniMax M3.1 Flash</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">89.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">free</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">26</td>
          <td>Muse Spark 1.3</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">88.75</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 13.31</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">150</td>
      </tr>
      <tr>
          <td style="text-align: right">27</td>
          <td>Swift 1.5 27B</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">88.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 9.71 ³</td>
          <td style="text-align: right">203.0</td>
          <td style="text-align: right">64</td>
      </tr>
      <tr>
          <td style="text-align: right">27</td>
          <td>Grok 4.5</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">88.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 6.19</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">48</td>
      </tr>
      <tr>
          <td style="text-align: right">29</td>
          <td>Claude Opus 4.6</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">87.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 25.64 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">89</td>
      </tr>
      <tr>
          <td style="text-align: right">30</td>
          <td>Kimi K2.7 Code</td>
          <td>open</td>
          <td>Cloud (Kimi CLI)</td>
          <td style="text-align: right">87.25</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 7.75</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">175</td>
      </tr>
      <tr>
          <td style="text-align: right">31</td>
          <td>MiMo V2.5 Pro</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">86.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 1.03</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">158</td>
      </tr>
      <tr>
          <td style="text-align: right">31</td>
          <td>Qwen3.8 Flash</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">86.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 1.17</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">149</td>
      </tr>
      <tr>
          <td style="text-align: right">33</td>
          <td>DeepSeek V4 Flash</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">86.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 0.97</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">111</td>
      </tr>
      <tr>
          <td style="text-align: right">33</td>
          <td>Claude Opus 4.8</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">86.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">~US$ 37 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">87</td>
      </tr>
      <tr>
          <td style="text-align: right">35</td>
          <td>Claude Sonnet 4.6</td>
          <td>closed</td>
          <td>Cloud (Claude Code)</td>
          <td style="text-align: right">85.75</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 18.64 ¹</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">91</td>
      </tr>
      <tr>
          <td style="text-align: right">36</td>
          <td>Kimi K3</td>
          <td>open</td>
          <td>Cloud (Kimi CLI)</td>
          <td style="text-align: right">85.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 13.79</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">148</td>
      </tr>
      <tr>
          <td style="text-align: right">36</td>
          <td>DeepSeek V4 Flash 0731</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">85.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 1.94</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">194</td>
      </tr>
      <tr>
          <td style="text-align: right">38</td>
          <td>GLM 5.3 Flash</td>
          <td>open</td>
          <td>Cloud (ZCode)</td>
          <td style="text-align: right">84.25</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">flat plan</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">296</td>
      </tr>
      <tr>
          <td style="text-align: right">39</td>
          <td>DeepSeek V4 Pro 0813</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">84.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 4.49</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">152</td>
      </tr>
      <tr>
          <td style="text-align: right">40</td>
          <td>Step 3.7 Flash</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">83.75</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 4.15</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">118</td>
      </tr>
      <tr>
          <td style="text-align: right">41</td>
          <td>Grok 4.7</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">83.5</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 27.94</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">120</td>
      </tr>
      <tr>
          <td style="text-align: right">42</td>
          <td>ThinkingCap 3.8 27B</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">83.0</td>
          <td style="text-align: center">A</td>
          <td style="text-align: right">US$ 11.47 ³</td>
          <td style="text-align: right">189.0</td>
          <td style="text-align: right">76</td>
      </tr>
      <tr>
          <td style="text-align: right">43</td>
          <td>DeepSeek V4 Pro</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">82.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 5.26</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">97</td>
      </tr>
      <tr>
          <td style="text-align: right">44</td>
          <td>Muse Glimmer 30B</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">81.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 3.54 ³</td>
          <td style="text-align: right">295.0</td>
          <td style="text-align: right">23</td>
      </tr>
      <tr>
          <td style="text-align: right">44</td>
          <td>Ornith 1.5 35B</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">81.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 24.39 ³</td>
          <td style="text-align: right">317.0</td>
          <td style="text-align: right">161</td>
      </tr>
      <tr>
          <td style="text-align: right">46</td>
          <td>Qwen 3.8 27B</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">80.5</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 29.11 ³</td>
          <td style="text-align: right">195.0</td>
          <td style="text-align: right">192</td>
      </tr>
      <tr>
          <td style="text-align: right">47</td>
          <td>Qwen 3.8 27B (Q8_0)</td>
          <td>open</td>
          <td>Strix Halo</td>
          <td style="text-align: right">80.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 0 (local)</td>
          <td style="text-align: right">7.7</td>
          <td style="text-align: right">706</td>
      </tr>
      <tr>
          <td style="text-align: right">48</td>
          <td>Qwen 3.7 Max</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">79.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 10.63</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">106</td>
      </tr>
      <tr>
          <td style="text-align: right">49</td>
          <td>GLM 5.2</td>
          <td>open</td>
          <td>Cloud (ZCode)</td>
          <td style="text-align: right">77.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">flat plan</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">239</td>
      </tr>
      <tr>
          <td style="text-align: right">50</td>
          <td>GLM 5.3 full</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">76.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">110.0</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">51</td>
          <td>Gemini 3.7 Flash high</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">75.5</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 12.93</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">85</td>
      </tr>
      <tr>
          <td style="text-align: right">51</td>
          <td>MiniMax M3</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">75.5</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">US$ 12.17</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">187</td>
      </tr>
      <tr>
          <td style="text-align: right">53</td>
          <td>Ornith 1.5 122B</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">73.0</td>
          <td style="text-align: center">B</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">244.2</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">54</td>
          <td>Step-5-Preview</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">72.5</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">165.9</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">55</td>
          <td>Muse Spark Open</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">69.5</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">130.8</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">56</td>
          <td>Qwen3.5 397B (NVFP4)</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">68.5</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">US$ 8.22 ³</td>
          <td style="text-align: right">140.0</td>
          <td style="text-align: right">54</td>
      </tr>
      <tr>
          <td style="text-align: right">57</td>
          <td>MiMo V2.6 Pro</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">67.0</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">167.2</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">58</td>
          <td>Laguna-S-2.1</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">65.5</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">263.0</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">59</td>
          <td>Ornith 1.5 35B (Q8_0)</td>
          <td>open</td>
          <td>RTX 5090 (Strata)</td>
          <td style="text-align: right">64.5</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">US$ 0 (local)</td>
          <td style="text-align: right">80.0</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">60</td>
          <td>Nemotron 3.5 Super VL (NVFP4)</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">63.0</td>
          <td style="text-align: center">C</td>
          <td style="text-align: right">US$ 7.51 ³</td>
          <td style="text-align: right">207.0</td>
          <td style="text-align: right">50</td>
      </tr>
      <tr>
          <td style="text-align: right">61</td>
          <td>Kimi K2 Thinking</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">46.0</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">110.0</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">62</td>
          <td>DeepSeek V4 Flash</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">44.5</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">US$ 5.59 ³</td>
          <td style="text-align: right">153.0</td>
          <td style="text-align: right">37</td>
      </tr>
      <tr>
          <td style="text-align: right">63</td>
          <td>Mistral Large 3</td>
          <td>open</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">39.0</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">US$ 5.11</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">76</td>
      </tr>
      <tr>
          <td style="text-align: right">64</td>
          <td>GLM 4.7 Flash (BF16)</td>
          <td>open</td>
          <td>1x B300</td>
          <td style="text-align: right">33.5</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">US$ 4.60 ³</td>
          <td style="text-align: right">217.0</td>
          <td style="text-align: right">30</td>
      </tr>
      <tr>
          <td style="text-align: right">65</td>
          <td>Gemini 3.1 Pro ⁴</td>
          <td>closed</td>
          <td>Cloud (OpenCode)</td>
          <td style="text-align: right">32.5</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">US$ 10.31</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">54</td>
      </tr>
      <tr>
          <td style="text-align: right">66</td>
          <td>Nex N2.5 Pro</td>
          <td>open</td>
          <td>4x B300</td>
          <td style="text-align: right">27.0</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">pod at US$ 36/h</td>
          <td style="text-align: right">184.0</td>
          <td style="text-align: right">—</td>
      </tr>
      <tr>
          <td style="text-align: right">67</td>
          <td>GLM-4.7-Flash (Q5_K_M)</td>
          <td>open</td>
          <td>RTX 5090</td>
          <td style="text-align: right">24.0</td>
          <td style="text-align: center">D</td>
          <td style="text-align: right">US$ 0 (local)</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">29</td>
      </tr>
  </tbody>
</table>
<p>How to read the columns:</p>
<ul>
<li><strong>Weights</strong>: &ldquo;open&rdquo; is a downloadable checkpoint, any license; &ldquo;closed&rdquo; is API only.</li>
<li><strong>Cost per run</strong>: what one full seven-sprint run cost.</li>
<li><strong>tok/s</strong>: single-stream decode speed, measured by me. It only exists for the ones I served.</li>
<li><strong>Minutes</strong>: wall-clock time for the whole run, when I have the data. On the single B300, it&rsquo;s sprint time only.</li>
</ul>
<p>And the footnotes:</p>
<ol>
<li>Claude ran through the Claude Code Max subscription. The cost is notional: the run&rsquo;s tokens at API list price.</li>
<li>Estimated from the run&rsquo;s tokens.</li>
<li>Inference time on the B300 only, at about US$ 9 per hour. Download, loading, compilation and tuning are left out, and the real pod cost was higher.</li>
<li>Score with a caveat. The cloud Nex was graded from disk, with everything uncommitted; Gemini 3.1 Pro was hurt by an OpenRouter proxy bug.</li>
</ol>
<p>The first v4 articles lumped everything below B into C; here I use the scale with D, the same as the B300 campaigns. MiniMax M3.1 Flash ran for free through OpenRouter as the cloud anchor for the local campaign, and MiniMax promises to release its weights on October 23.</p>
<h2>The models are converging<span class="hx:absolute hx:-mt-20" id="the-models-are-converging"></span>
    <a href="#the-models-are-converging" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Within the limited scope of this benchmark, it looks like everybody is arriving at the same place. <strong>42 of the 67 runs made Tier A</strong>, 24 from closed models and 18 from open ones. Seven models hit 100, and 23 runs landed above 90.</p>
<p>Part of that is a limit of the yardstick itself. When seven models get the top score, the test stops separating the top. And a repeated GPT 5.5 run scored 91 against the official 100, so differences of a few points between two models may just be noise.</p>
<p>Even discounting that, the picture is clear. DeepSeek V4.1 Flash, GLM 5.3 Flash and Qwen3.8 Flash-Next, all open, sit 7.5 points from the ceiling, right on the heels of Sonnet 5 and GPT 6 sol.</p>
<blockquote>
  <p>For code review and vigilance, a good open model is already in the same pack as a good closed model. What still separates the two is consistency: among the open models there are D scores that don&rsquo;t show up among the top closed ones.</p>

</blockquote>
<h3>The West stalled, China sprinting<span class="hx:absolute hx:-mt-20" id="the-west-stalled-china-sprinting"></span>
    <a href="#the-west-stalled-china-sprinting" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Comparing generations of the same family within v4, the difference in pace shows up:</p>
<table>
  <thead>
      <tr>
          <th>Family</th>
          <th style="text-align: right">Before</th>
          <th style="text-align: right">After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4 Flash → V4.1 Flash</td>
          <td style="text-align: right">86.0</td>
          <td style="text-align: right">92.5</td>
      </tr>
      <tr>
          <td>GLM 5.2 → 5.3</td>
          <td style="text-align: right">77.0</td>
          <td style="text-align: right">94.0</td>
      </tr>
      <tr>
          <td>MiMo V2.5 Pro → V2.6 Pro</td>
          <td style="text-align: right">86.5</td>
          <td style="text-align: right">92.0</td>
      </tr>
      <tr>
          <td>MiniMax M3 → M3.1 Flash</td>
          <td style="text-align: right">75.5</td>
          <td style="text-align: right">89.5</td>
      </tr>
      <tr>
          <td>Kimi K2.7 Code → K3</td>
          <td style="text-align: right">87.25</td>
          <td style="text-align: right">85.0</td>
      </tr>
      <tr>
          <td>Gemini 3.7 Flash → 3.8 Flash (high)</td>
          <td style="text-align: right">75.5</td>
          <td style="text-align: right">90.5</td>
      </tr>
      <tr>
          <td>Claude Fable 5 → 5.1</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">95.5</td>
      </tr>
      <tr>
          <td>Grok 4.6 → 4.7</td>
          <td style="text-align: right">98.5</td>
          <td style="text-align: right">83.5</td>
      </tr>
      <tr>
          <td>GPT 5.6 sol → GPT 6 sol</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">91.0</td>
      </tr>
      <tr>
          <td>GPT 5.6 luna → GPT 6 luna</td>
          <td style="text-align: right">95.0</td>
          <td style="text-align: right">95.5</td>
      </tr>
      <tr>
          <td>Claude Opus 5 → 5.5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">100.0</td>
      </tr>
  </tbody>
</table>
<p>The Chinese models climbed 5.5 to 17 points per generation, with the exception of Kimi. On the Western side, whoever was already at the ceiling had nowhere to go: Opus stayed at 100, GPT 6 luna stayed where 5.6 luna was, and three new releases landed below their predecessors. Gemini Flash is the Western counterexample, with a 15-point jump.</p>
<p>The calendar reinforces that reading. Claude Fable 5 came out in June, Opus 5 and GPT 5.6 in July, and all of them already hit 100 on my yardstick. From then until October, OpenAI and Anthropic released Fable 5.1, GPT-6 Astra, Opus 5.5 and GPT-6 sol and luna. None of them showed a gain on my yardstick, and some landed below their predecessors.</p>
<p>In the same window, the Chinese labs started from well behind: DeepSeek V4 in April, Kimi K3 in July, Qwen 3.8 and GLM 5.3 in August, DeepSeek V4.1 Flash and MiMo V2.6 Pro in September, MiniMax M3.1 Flash in October. Each release got closer to the top.</p>
<blockquote>
  <p>The Chinese labs seem to move faster, and they do. But part of that impression comes from the Western ones having stood still for months, at least in programming. When the leader doesn&rsquo;t move, whoever is chasing catches up.</p>

</blockquote>
<h3>Math isn&rsquo;t programming<span class="hx:absolute hx:-mt-20" id="math-isnt-programming"></span>
    <a href="#math-isnt-programming" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Western labs didn&rsquo;t stop working. They&rsquo;re spending a good chunk of their showcase on math:</p>
<ul>
<li>in May, OpenAI announced that a general-purpose reasoning model <a href="https://openai.com/index/model-disproves-discrete-geometry-conjecture/"target="_blank" rel="noopener">disproved an Erdős conjecture</a> about unit distances;</li>
<li>in August, it published <a href="https://github.com/openai/ten-proofs"target="_blank" rel="noopener">ten mathematical results from Astra</a>, each with a formal proof in Lean;</li>
<li>also in August, Anthropic showed an internal Claude raising from 41.6% to 67.2% the proven fraction of <a href="https://www.anthropic.com/research/riemann-zeta"target="_blank" rel="noopener">Riemann zeta function zeros</a> that satisfy the hypothesis;</li>
<li>in September, Anthropic <a href="https://www.anthropic.com/research/formalizing-fermats-last-theorem"target="_blank" rel="noopener">formalized the whole of Fermat&rsquo;s Last Theorem in Lean</a> in 11 days, with an internal model it describes itself as comparable to Fable 5.1.</li>
</ul>
<p>It&rsquo;s impressive, and it&rsquo;s legitimate. Formal proof is one of the few places where you can verify the answer automatically, which makes math a great field for training and for marketing. Except the same Fable 5.1 that served as the yardstick for Fermat scored 95.5 on my benchmark, against Fable 5&rsquo;s 100.</p>
<p>Getting better at math doesn&rsquo;t automatically make a model better at programming. A 2025 study, <a href="https://arxiv.org/abs/2507.00432"target="_blank" rel="noopener"><em>Does Math Reasoning Improve General LLM Capabilities?</em></a>, concluded that <em>most models that do well at math don&rsquo;t transfer the gain to other domains</em>, programming among them. There are counterexamples: NVIDIA showed that <a href="https://arxiv.org/abs/2505.16400"target="_blank" rel="noopener">math-only reinforcement learning</a> raised the code scores of small models. Transfer happens sometimes, and it depends on how you train.</p>
<h3>And what about mixture of experts<span class="hx:absolute hx:-mt-20" id="and-what-about-mixture-of-experts"></span>
    <a href="#and-what-about-mixture-of-experts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The common intuition is that, in a <em>mixture of experts</em> (MoE) model, you can improve the &ldquo;math expert&rdquo; without touching the &ldquo;code expert&rdquo;. The idea is half right for the wrong reason.</p>
<p>An MoE has several blocks of weights in each layer, and a router picks a few of them for each token. But the experts don&rsquo;t split by subject the way the intuition suggests. The <a href="https://arxiv.org/abs/2401.04088"target="_blank" rel="noopener">Mixtral paper</a> looked for exactly that and <em>found no obvious pattern of experts by topic</em>, with a small difference only on a synthetic math set; the router organizes itself more by syntax, by token type. And neither OpenAI nor Anthropic publicly confirm whether their closed models are MoE.</p>
<p>What still stands is the essential part: capability is trained by objective. The signal that rewards a Lean proof is not the same one that rewards noticing a cross-tenant leak in a Rails controller, and improving one doesn&rsquo;t drag the other along. In a model where only a fraction of the parameters work per token, that mismatch becomes even easier.</p>
<h2>NVFP4, FP8, Q4: what&rsquo;s this alphabet soup<span class="hx:absolute hx:-mt-20" id="nvfp4-fp8-q4-whats-this-alphabet-soup"></span>
    <a href="#nvfp4-fp8-q4-whats-this-alphabet-soup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every model is a pile of billions of numbers, the weights. Quantization is storing each number with fewer bits. The fewer the bits, the less memory the model takes and the less data the card has to read per token, and that&rsquo;s what defines speed on a local machine. The price, when there is one, is quality.</p>
<p>For a 27-billion-parameter model, the math goes like this:</p>
<table>
  <thead>
      <tr>
          <th>Format</th>
          <th style="text-align: right">Bits per weight</th>
          <th style="text-align: right">27B takes</th>
          <th>Where it runs</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BF16</td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">~54 GB</td>
          <td>native from Ampere (RTX 30) onward</td>
      </tr>
      <tr>
          <td>FP8</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right">~27 GB</td>
          <td>native on Ada (RTX 40), Hopper (H200) and Blackwell</td>
      </tr>
      <tr>
          <td>Q8_0 (GGUF)</td>
          <td style="text-align: right">~8.5</td>
          <td style="text-align: right">~29 GB</td>
          <td>llama.cpp and Ollama, any card</td>
      </tr>
      <tr>
          <td>Q5_K_M (GGUF)</td>
          <td style="text-align: right">~5.5</td>
          <td style="text-align: right">~19 GB</td>
          <td>llama.cpp and Ollama</td>
      </tr>
      <tr>
          <td>Q4_K_M (GGUF)</td>
          <td style="text-align: right">~4.5 to 4.8</td>
          <td style="text-align: right">~16 GB</td>
          <td>llama.cpp and Ollama</td>
      </tr>
      <tr>
          <td>NVFP4</td>
          <td style="text-align: right">4.5</td>
          <td style="text-align: right">~15 GB</td>
          <td>native only on Blackwell; vLLM, and llama.cpp since 2026</td>
      </tr>
      <tr>
          <td>IQ3_S (GGUF)</td>
          <td style="text-align: right">~3.4</td>
          <td style="text-align: right">~12 GB</td>
          <td>llama.cpp and derivatives</td>
      </tr>
  </tbody>
</table>
<h3>What each one is<span class="hx:absolute hx:-mt-20" id="what-each-one-is"></span>
    <a href="#what-each-one-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>BF16</strong> is the precision most models are published in. It&rsquo;s the reference: nothing is lost.</li>
<li><strong>FP8</strong> is 8-bit floating point, with hardware support on the Ada, Hopper and Blackwell generations. It cuts memory in half and is the default for big checkpoints, like DeepSeek V4.1 Flash and GLM 5.3 full.</li>
<li><strong>Q8_0, Q5_K_M, Q4_K_M and IQ3_S</strong> are llama.cpp GGUF formats. The weights are stored in blocks of integers with one scale per block; the &ldquo;K&rdquo; ones mix precisions per tensor, and the &ldquo;IQ&rdquo; ones use an importance matrix to decide where to spend bits. Q8 is considered practically lossless, and Q4_K_M is the recommended default for people running at home.</li>
<li><strong>NVFP4</strong> is NVIDIA&rsquo;s 4-bit format. Each number is a 4-bit floating point, and each block of 16 numbers gets its own FP8 scale. NVIDIA says this <a href="https://developer.nvidia.com/blog/introducing-nvfp4-for-efficient-and-accurate-low-precision-inference/"target="_blank" rel="noopener">takes 3.5 times less than FP16 and loses 1% or less against FP8</a> on DeepSeek-R1.</li>
</ul>
<p>That&rsquo;s where the NVFP4 hype comes from: Blackwell&rsquo;s tensor cores compute in it directly, and it&rsquo;s what made GLM 5.3 Flash, at 198 GB, and Qwen3.5 397B, at 251 GB, fit on a single B300. The H200, from the previous generation, has no native FP4. And the home ecosystem is still catching up: llama.cpp got <a href="https://github.com/ggml-org/llama.cpp/pull/22196"target="_blank" rel="noopener">NVFP4 support</a> only in 2026, and per-architecture coverage is still partial.</p>
<h3>What our tests show<span class="hx:absolute hx:-mt-20" id="what-our-tests-show"></span>
    <a href="#what-our-tests-show" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Four models ran at more than one precision:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Version A</th>
          <th>Version B</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.8 27B</td>
          <td>BF16, B300: 80.5</td>
          <td>Q8_0, Strix Halo: 80.0 ¹</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next</td>
          <td>NVFP4, B300: 92.5</td>
          <td>IQ3_S, RTX 5090 with Strata: 91.0</td>
      </tr>
      <tr>
          <td>Ornith 1.5 35B</td>
          <td>BF16, B300: 81.0</td>
          <td>Q8_0, RTX 5090 with Strata: 64.5</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>BF16, B300: 33.5</td>
          <td>Q5_K_M, RTX 5090: 24.0</td>
      </tr>
  </tbody>
</table>
<p>¹ Mixed run: sprints 1 and 2 in Q5_K_M on the 5090, the rest in Q8_0 on the Strix, with reasoning turned off.</p>
<p>And six ran on the cloud API and were also served by me, with the official weights:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Cloud</th>
          <th style="text-align: right">Served by me</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GLM 5.3 Flash</td>
          <td style="text-align: right">84.25</td>
          <td style="text-align: right">92.5 (NVFP4, B300)</td>
      </tr>
      <tr>
          <td>DeepSeek V4.1 Flash</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">92.5 (FP8, 4x B300)</td>
      </tr>
      <tr>
          <td>GLM 5.3 full</td>
          <td style="text-align: right">94.0</td>
          <td style="text-align: right">76.0 (FP8, 4x B300)</td>
      </tr>
      <tr>
          <td>MiMo V2.6 Pro</td>
          <td style="text-align: right">92.0</td>
          <td style="text-align: right">67.0 (FP8, 4x B300)</td>
      </tr>
      <tr>
          <td>DeepSeek V4 Flash 0731</td>
          <td style="text-align: right">85.0</td>
          <td style="text-align: right">44.5 (official FP4/FP8, B300)</td>
      </tr>
      <tr>
          <td>Nex N2.5 Pro</td>
          <td style="text-align: right">94.0</td>
          <td style="text-align: right">27.0 (FP8, 4x B300)</td>
      </tr>
  </tbody>
</table>
<p>Each row is a single run, and the noise reaches almost 10 points. With that caveat, here&rsquo;s my reading:</p>
<ul>
<li><strong>Q8 came out tied with BF16.</strong> The Qwen 27B scored 80.5 and 80.0, a single point of data with other things changing at the same time.</li>
<li><strong>Going from 4 down to 3 bits was nearly free on Flash-Next</strong>, at 1.5 points, even while switching engine and card.</li>
<li><strong>Ornith&rsquo;s drop is unlikely to be about the bits.</strong> Q8 is nearly lossless, and it still lost 16.5 while also switching engine and card.</li>
<li><strong>Serving the same checkpoint can give you anything.</strong> GLM 5.3 Flash went up 8 points in my hands, DeepSeek V4.1 tied, and Nex fell almost 70. Tool parser, chat template, reasoning mode and engine version weigh as much as precision, and the effect changes from model to model.</li>
</ul>
<blockquote>
  <p>In my tests, the bits mattered less than the hype suggests, and the whole package of how the model is served mattered more. Use Q8 when it fits, drop to 4 bits when you have to, and measure on your own work before trusting it.</p>

</blockquote>
<h2>The best commercial models<span class="hx:absolute hx:-mt-20" id="the-best-commercial-models"></span>
    <a href="#the-best-commercial-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Only closed models get in here, only Tier A, and only what I&rsquo;d pay for through an API.</p>
<h3>By score<span class="hx:absolute hx:-mt-20" id="by-score"></span>
    <a href="#by-score" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">Cost per run</th>
          <th style="text-align: right">Minutes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT-6 Astra</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 30.55</td>
          <td style="text-align: right">100</td>
      </tr>
      <tr>
          <td>Claude Opus 5.5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 16.63 ¹</td>
          <td style="text-align: right">64</td>
      </tr>
      <tr>
          <td>Claude Opus 5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">~US$ 71 ¹</td>
          <td style="text-align: right">145</td>
      </tr>
      <tr>
          <td>Claude Fable 5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">~US$ 50 ¹</td>
          <td style="text-align: right">~85</td>
      </tr>
      <tr>
          <td>GPT 5.6 terra</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 9.52</td>
          <td style="text-align: right">80</td>
      </tr>
      <tr>
          <td>GPT 5.6 sol</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 20.37</td>
          <td style="text-align: right">317</td>
      </tr>
      <tr>
          <td>GPT 5.5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 34.69</td>
          <td style="text-align: right">117</td>
      </tr>
      <tr>
          <td>Grok 4.6</td>
          <td style="text-align: right">98.5</td>
          <td style="text-align: right">US$ 13.00</td>
          <td style="text-align: right">67</td>
      </tr>
      <tr>
          <td>GPT 6 luna</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: right">~US$ 0.84</td>
          <td style="text-align: right">122</td>
      </tr>
      <tr>
          <td>Claude Fable 5.1</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: right">~US$ 51 ¹</td>
          <td style="text-align: right">133</td>
      </tr>
  </tbody>
</table>
<p>¹ Notional subscription cost, at API list price.</p>
<p>At the absolute top, <strong>Claude Opus 5.5</strong> and <strong>GPT 5.6 terra</strong> are the obvious picks: top score, a run in a little over an hour, and a per-run price in the single digits or a bit more. Opus 5 and GPT 5.5 get the same score at two to seven times the cost.</p>
<h3>By cost-benefit<span class="hx:absolute hx:-mt-20" id="by-cost-benefit"></span>
    <a href="#by-cost-benefit" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">Cost per run</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT 6 luna</td>
          <td style="text-align: right">95.5</td>
          <td style="text-align: right">~US$ 0.84</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash</td>
          <td style="text-align: right">86.5</td>
          <td style="text-align: right">US$ 1.17</td>
      </tr>
      <tr>
          <td>GPT 6 sol</td>
          <td style="text-align: right">91.0</td>
          <td style="text-align: right">~US$ 8.35</td>
      </tr>
      <tr>
          <td>GPT 5.6 terra</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 9.52</td>
      </tr>
      <tr>
          <td>GPT 5.6 luna</td>
          <td style="text-align: right">95.0</td>
          <td style="text-align: right">US$ 10.04</td>
      </tr>
      <tr>
          <td>Grok 4.6</td>
          <td style="text-align: right">98.5</td>
          <td style="text-align: right">US$ 13.00</td>
      </tr>
      <tr>
          <td>Claude Opus 5.5</td>
          <td style="text-align: right">100.0</td>
          <td style="text-align: right">US$ 16.63 ¹</td>
      </tr>
  </tbody>
</table>
<p>¹ Notional subscription cost, at API list price.</p>
<p><strong>GPT 6 luna</strong> is the absurdity of the table: high Tier A for under a dollar per run, a fraction of the price of any other closed model at the same level. If the top score isn&rsquo;t mandatory, that&rsquo;s where I&rsquo;d start.</p>
<p>At the top score, <strong>GPT 5.6 terra</strong> costs a little more than half of Opus 5.5. The difference between the two lies in everything else: harness, speed, behavior in long sessions, and I covered that in the previous articles.</p>
<h2>The best open models for companies<span class="hx:absolute hx:-mt-20" id="the-best-open-models-for-companies"></span>
    <a href="#the-best-open-models-for-companies" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is what runs on datacenter GPUs, on the B300.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Where</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">tok/s</th>
          <th style="text-align: right">Memory</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4.1 Flash</td>
          <td>4x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">175</td>
          <td style="text-align: right">510 GB</td>
      </tr>
      <tr>
          <td>GLM 5.3 Flash</td>
          <td>1x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">142</td>
          <td style="text-align: right">198 GB</td>
      </tr>
      <tr>
          <td>Qwen3.8 Flash-Next</td>
          <td>1x B300</td>
          <td style="text-align: right">92.5</td>
          <td style="text-align: right">195</td>
          <td style="text-align: right">100 GB</td>
      </tr>
      <tr>
          <td>Swift 1.5 27B</td>
          <td>1x B300</td>
          <td style="text-align: right">88.0</td>
          <td style="text-align: right">203</td>
          <td style="text-align: right">56 GB</td>
      </tr>
      <tr>
          <td>ThinkingCap 3.8 27B</td>
          <td>1x B300</td>
          <td style="text-align: right">83.0</td>
          <td style="text-align: right">189</td>
          <td style="text-align: right">56 GB</td>
      </tr>
  </tbody>
</table>
<p><strong>DeepSeek V4.1 Flash</strong> is the champion for a big team. On a 4x B300 pod at US$ 36 per hour, it serves <strong>32 comfortable sessions</strong> at the same time, with the first token in under 2 seconds for almost all of them. Counting one active session per developer for the whole workday, which is optimistic, that&rsquo;s 32 people. Turning the pod on only during business hours, 8 hours a day, 22 days a month, comes to about US$ 6,300 per month, or <strong>about US$ 200 per developer</strong>.</p>
<p>The alternatives, by the same math:</p>
<ul>
<li>4x H200 on Runpod serves only 8 comfortable sessions and comes out to about US$ 316 per developer per month, even with the cheapest pod;</li>
<li>the same configuration on AWS costs almost double, and the H200 on Azure costs almost three times the Runpod price;</li>
<li>buying a DGX Station GB300 for about US$ 130 thousand pays for itself in about 20 months against renting during business hours, or in about 5 months against a pod running nonstop.</li>
</ul>
<p>For a smaller team, the single B300 with <strong>Qwen3.8 Flash-Next</strong> or <strong>GLM 5.3 Flash</strong> is the sweet spot: same score as DeepSeek, on a single GPU, at about US$ 9 per hour. I didn&rsquo;t measure concurrency on it. Going by the memory left over, a team of five to ten people fits well on a 27 to 35 billion model, like Swift 1.5; on GLM 5.3 Flash, at 198 GB, it gets tight.</p>
<blockquote>
  <p>Your own infrastructure starts to make sense at around 30 people using it at the same time, or when the code can&rsquo;t leave the network. Below that, the API for the same DeepSeek costs a little over a dollar per run and you don&rsquo;t babysit any server.</p>

</blockquote>
<h2>The best open models for consumers<span class="hx:absolute hx:-mt-20" id="the-best-open-models-for-consumers"></span>
    <a href="#the-best-open-models-for-consumers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is what runs on a PC: an RTX 5090 with 32 GB or a Strix Halo with 128 GB unified.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Machine</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: right">tok/s</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen3.8 Flash-Next (IQ3_S, Strata)</td>
          <td>RTX 5090</td>
          <td style="text-align: right"><strong>91.0</strong></td>
          <td style="text-align: right">159</td>
      </tr>
      <tr>
          <td>Qwen 3.8 27B (Q8_0)</td>
          <td>Strix Halo</td>
          <td style="text-align: right">80.0</td>
          <td style="text-align: right">8</td>
      </tr>
      <tr>
          <td>Ornith 1.5 35B (Q8_0, Strata)</td>
          <td>RTX 5090</td>
          <td style="text-align: right">64.5</td>
          <td style="text-align: right">~80</td>
      </tr>
      <tr>
          <td>GLM-4.7-Flash (Q5_K_M)</td>
          <td>RTX 5090</td>
          <td style="text-align: right">24.0</td>
          <td style="text-align: right">—</td>
      </tr>
  </tbody>
</table>
<p>There&rsquo;s only one winner: <strong>Qwen3.8 Flash-Next on the RTX 5090 with Strata</strong>. It&rsquo;s a 125-billion MoE with 6 billion active per token, and Strata keeps the most-used experts on the card and the rest in RAM. It landed 1.5 points from the datacenter version, at interactive speed. I covered it in detail, with installation, an auto-loading service and the OpenCode setup, in <a href="/en/2026/10/22/best-open-source-llm-strata-with-qwen-3-8-flash-next/">Best Open Source LLM: Strata with Qwen 3.8 Flash Next</a>.</p>
<p>The Strix Halo fits big models, but its memory bandwidth is seven times lower than the 5090&rsquo;s, and a 27B dense model crawls at under 10 tokens per second in Q8, about 13 in Q4. It&rsquo;s fine for background tasks that can wait.</p>
<p>And what about <strong>Swift 1.5</strong> and <strong>ThinkingCap 3.8</strong>, the 27B fine-tunes that did well on the B300? I didn&rsquo;t run them on the 5090, and I didn&rsquo;t need to. On the B300, in BF16, full precision, they scored 88 and 83, below Flash-Next&rsquo;s 92.5 on the same card and the 91 it scored on the 5090.</p>
<p>On a 5090 they&rsquo;d fit in Q4 or Q5, but quantized, the best case is matching the B300 score. Running at home cost Flash-Next 1.5 points and Ornith-35B 16.5. The base Qwen 3.8 27B, which is the same architecture, didn&rsquo;t even finish on the 5090 in Q5_K_M: it got stuck in a reasoning loop in sprint 3.</p>
<blockquote>
  <p>If those two already fell behind Flash-Next at full precision, they won&rsquo;t get better on a 5090. I saw no reason to test them: for a PC, Flash-Next is clearly the pick.</p>

</blockquote>
<h3>What about GPUs worse than the 5090?<span class="hx:absolute hx:-mt-20" id="what-about-gpus-worse-than-the-5090"></span>
    <a href="#what-about-gpus-worse-than-the-5090" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It&rsquo;s the question that always comes up: what about people with an 8, 12 or 16 GB card? From my data, the path of a small model that fits entirely on the card still doesn&rsquo;t deliver anything worth the effort for programming.</p>
<p>On a card like that, in Q4, a dense model of up to about 20 billion fits; a 30-billion MoE with few active parameters only runs with part of it in RAM. And that range did badly in v4:</p>
<ul>
<li>GLM-4.7-Flash, a 30B MoE with 3B active, scored 24 on the 5090 and 33.5 on the B300 at full precision, Tier D on both;</li>
<li>Ornith-35B, another MoE with 3B active, scored 81 on the B300 and fell to 64.5 on the 5090;</li>
<li>gpt-oss-20b didn&rsquo;t even complete the protocol;</li>
<li>the best mid-size model was Qwen 3.8 27B, with 80, but it already needs about 16 GB in Q4 alone, stalled on the 5090, and only finished at 8 tokens per second on the Strix.</li>
</ul>
<p>The exception is precisely Flash-Next. Strata was built to run it on a 12 GB card, holding most of the experts in RAM: the <a href="https://github.com/Niko1221/Strata"target="_blank" rel="noopener">project&rsquo;s README</a> publishes 53 tokens per second in IQ3_S on a 12 GB RTX 5070 with 64 GB of RAM, and the stated minimum is 32 GB of RAM and VRAM combined. With the same weights and the same engine, the score in theory shouldn&rsquo;t change, only the speed, which drops to a third of the 5090&rsquo;s. I didn&rsquo;t measure this; it&rsquo;s the author&rsquo;s number.</p>
<p>The catch is RAM. With DRAM <a href="https://www.trendforce.com/presscenter/news/20260601-13070.html"target="_blank" rel="noopener">nearly doubling in price</a> in a single quarter this year, 64 GB stopped being the cheap upgrade it used to be.</p>
<blockquote>
  <p>Below the 5090, I wouldn&rsquo;t waste time on small models for programming. If you&rsquo;re going to try something, make it Flash-Next with Strata and plenty of RAM, and measure it on your own work before trusting it.</p>

</blockquote>
<p>And there&rsquo;s the problem I covered in the <a href="/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/">previous article</a>: finding a 5090 today is hard, and when you do, the price runs between two and a half and almost five times MSRP.</p>
<h3>How much does it cost to move beyond 150 tokens per second?<span class="hx:absolute hx:-mt-20" id="how-much-does-it-cost-to-move-beyond-150-tokens-per-second"></span>
    <a href="#how-much-does-it-cost-to-move-beyond-150-tokens-per-second" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>After a few hours using Qwen locally, I <a href="https://x.com/AkitaOnRails/status/2113386101293428812"target="_blank" rel="noopener">summed up the problem on X</a>. My run&rsquo;s average of 159 tokens per second looks fast in a table. During a programming session that spends the whole day generating code, running tests, reading logs and fixing its own work, the waiting adds up.</p>
<p>I already get impatient. Anything below that is out of consideration for daily work.</p>
<p>The 5090 itself has limits beyond speed. NVIDIA specifies <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/"target="_blank" rel="noopener">32 GB of GDDR7 and 1,792 GB/s of bandwidth</a> and <a href="https://nvidianews.nvidia.com/news/nvidia-blackwell-geforce-rtx-50-series-opens-new-world-of-ai-computer-graphics"target="_blank" rel="noopener">launched the card at US$1,999</a>. Mine also drives the monitors and Hyprland, so I reserve 4 GB for the desktop. The rest has to hold dense weights, the hot-expert cache, engine buffers and the resident portion of the KV cache that stores the conversation context.</p>
<p>My Strata configuration offers a 256-thousand-token context with int8 KV and keeps 32 thousand resident in VRAM; the rest can move to RAM. That keeps the card from overflowing, but RAM is still much slower. While <a href="https://docs.anthropic.com/en/api/claude-on-amazon-bedrock"target="_blank" rel="noopener">frontier Claude models already offer a one-million-token window</a>, locally I have to watch what enters the session. Loading a whole collection of skills, plugins and MCP servers just because they fit in a config file wastes context and hurts latency.</p>
<blockquote>
  <p>Context also costs memory and time on local hardware. I only load what that session will use.</p>

</blockquote>
<p>What if I add more hardware? The comparison looks like this:</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th style="text-align: right">Memory</th>
          <th style="text-align: right">Bandwidth</th>
          <th style="text-align: right">Observed speed</th>
          <th style="text-align: right">Reference price</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RTX 5090</td>
          <td style="text-align: right">32 GB GDDR7</td>
          <td style="text-align: right">1,792 GB/s</td>
          <td style="text-align: right"><strong>159.2 tok/s</strong>, measured by me</td>
          <td style="text-align: right">US$1,999 MSRP</td>
      </tr>
      <tr>
          <td>2x RTX 5090</td>
          <td style="text-align: right">64 GB physically split between cards</td>
          <td style="text-align: right">2x 1,792 GB/s, without becoming one flat pool</td>
          <td style="text-align: right">no equivalent test of my own</td>
          <td style="text-align: right">US$3,998 MSRP</td>
      </tr>
      <tr>
          <td>Mac Studio M5 Ultra</td>
          <td style="text-align: right">96 or 256 GB unified; 512 GB promised for late October</td>
          <td style="text-align: right">1,200 GB/s</td>
          <td style="text-align: right">not yet measured in Strata</td>
          <td style="text-align: right">starts at US$5,499</td>
      </tr>
      <tr>
          <td>DGX Spark</td>
          <td style="text-align: right">128 GB unified</td>
          <td style="text-align: right">273 GB/s</td>
          <td style="text-align: right">about 22 to 75 tok/s per session in public recipes</td>
          <td style="text-align: right">US$6,950 from NVIDIA</td>
      </tr>
      <tr>
          <td>RTX PRO 6000 Blackwell</td>
          <td style="text-align: right">96 GB GDDR7 ECC</td>
          <td style="text-align: right">1,792 GB/s</td>
          <td style="text-align: right">187.4 tok/s in short chat; 174.3 in an agent session</td>
          <td style="text-align: right">US$16,000; R$123,000 via Pix in Brazil</td>
      </tr>
  </tbody>
</table>
<p>In the post, I wrote that Strata would only start two instances on a machine with two 5090s. That part is outdated: current Strata <a href="https://github.com/Niko1221/Strata/blob/main/docs/MULTI_GPU.md"target="_blank" rel="noopener">splits layers across two or more GPUs</a>. It uses pipeline parallelism, without turning two cards into one simple 64 GB block and without mature tensor parallelism.</p>
<p>Single-session gains stay far below 2x. A <a href="https://github.com/Niko1221/Strata/issues/642"target="_blank" rel="noopener">community test with two 5090s</a> measured about a 22% gain in the decode geometric mean and 40% in the coding arm. It helps capacity, and you can run the cards as two independent instances to double concurrency. For me alone, buying another 5090 to remove a fraction of the wait does not add up.</p>
<p>The <strong>Mac Studio M5 Ultra</strong> starts at <a href="https://www.apple.com/newsroom/2026/08/apple-introduces-new-mac-studio-with-m5-max-and-m5-ultra/"target="_blank" rel="noopener">US$5,499</a>, has 1.2 TB/s of bandwidth and comes with 96 or 256 GB; Apple promised the 512 GB option for late October. There is no published Strata benchmark for the Ultra yet. On the <a href="https://github.com/Niko1221/Strata/blob/main/docs/MAC.md"target="_blank" rel="noopener">128 GB M5 Max</a>, the same Flash-Next ran between 97 and 119.5 tokens per second, depending on the quantization and prompt. My original estimate of under 100 on the Ultra is unproven, and I would not buy a machine like that based on a guess.</p>
<p>The <strong>DGX Spark</strong> puts 128 GB in a compact box, with a 140 W TDP for the GB10 and a 240 W power supply. Its memory delivers only <a href="https://www.nvidia.com/en-us/products/workstations/dgx-spark/"target="_blank" rel="noopener">273 GB/s</a>, almost seven times less than the 5090. NVIDIA now asks <a href="https://marketplace.nvidia.com/en-us/enterprise/personal-ai-supercomputers/dgx-spark/"target="_blank" rel="noopener">US$6,950</a>.</p>
<p>Reproducible Flash-Next recipes range from <a href="https://github.com/ursuciprian/qwen3.8-flash-next-1x-dgx-spark"target="_blank" rel="noopener">22 to 75 tokens per second per session</a>, depending on concurrency, stack, sampling and reasoning mode. It fits plenty. For my tolerance, it moves too slowly.</p>
<p>That leaves the <strong>RTX PRO 6000 Blackwell</strong>. It has <a href="https://www.nvidia.com/content/dam/en-zz/Solutions/data-center/rtx-pro-6000-blackwell-workstation-edition/workstation-blackwell-rtx-pro-6000-workstation-edition-nvidia-us-3519208-web.pdf"target="_blank" rel="noopener">96 GB of GDDR7 ECC, 1,792 GB/s and 600 watts of total board power</a>. Its bandwidth is exactly the same as the 5090&rsquo;s. The 96 GB removes the VRAM pressure and allows more cache and context, but it does not buy three times the speed.</p>
<p>A <a href="https://github.com/Niko1221/Strata/issues/2268"target="_blank" rel="noopener">Strata community benchmark</a> measured 187.4 tokens per second in short chat and 174.3 in an agent session, with substantial variation across runs. That&rsquo;s better than my 159.2, except NVIDIA lists the card at <a href="https://marketplace.nvidia.com/en-us/enterprise/laptops-workstations/nvidia-rtx-pro-6000-blackwell-workstation-edition/"target="_blank" rel="noopener">US$16,000</a>. On October 22, WAZ <a href="https://www.waz.com.br/placa-de-video-nvidia-quadro-rtx-pro-6000-blackwell-workstation-edition-pny-vcnrtxpro6000b-pb-96gb-gddr7-pcie-5-0-x16-131172-html/p"target="_blank" rel="noopener">showed the PNY at R$123,299.99 via Pix or R$145,058.81 at regular price</a>. We have entered professional-workstation or server budgets.</p>
<blockquote>
  <p>Today, for me, there is no home option that combines a good model, truly fast responses and a reasonable cost. The 5090 works, but 150 tokens per second already wears on me. Below it, local LLMs are toys; above it, the price explodes before the speed does.</p>

</blockquote>
<h2>What I learned in v4<span class="hx:absolute hx:-mt-20" id="what-i-learned-in-v4"></span>
    <a href="#what-i-learned-in-v4" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>About the models<span class="hx:absolute hx:-mt-20" id="about-the-models"></span>
    <a href="#about-the-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Almost every model catches the loud defect unprompted: a CVE in a gem, a key in the code, open CORS, obvious SQL injection. Almost every model lets the silent one slip until I reveal it: cross-tenant leak, wrong aggregate, tampered test, removed index. What separates Tier A from the rest is how much of the silent stuff it catches on its own.</li>
<li>Size doesn&rsquo;t buy vigilance. Kimi K2 Thinking, at 1 trillion parameters, scored 46. GLM 5.3 full, at 700 billion, scored 76, against 92.5 for the Flash from the same family.</li>
<li>Models cheat or get things wrong with conviction. Nemotron tampered with a regression test to make it pass, Qwen3.5 397B let a backdoor through and even wrote a test that treated it as expected behavior, and Kimi K2 put on security checklist theater, catching only what you can find with grep.</li>
<li>The leap between generations was in agentic reliability. GPT-4o and GPT-4.1 can&rsquo;t even finish the protocol on their own. Today&rsquo;s top models run all seven sprints with no help at all.</li>
</ul>
<h3>About serving open models<span class="hx:absolute hx:-mt-20" id="about-serving-open-models"></span>
    <a href="#about-serving-open-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>The same checkpoint changes score depending on who serves it. DeepSeek V4 Flash scored 85 in the cloud, on the same 0731 version I downloaded, and 44.5 served by me; Nex N2.5 Pro scored 94 in the cloud and 27 on the 4x B300 pod, with corrupted tokens landing in the committed code.</li>
<li>No model runs fast out of the box. GLM 5.3 Flash went from 10 to 141 tokens per second from configuration alone, and every family asked for a parser, a flag and sometimes a vLLM PR branch.</li>
<li>Running at home charges each model a different price. Switching quantization, engine and card all at once, Flash-Next lost 1.5 points and Ornith-35B lost 16.5.</li>
<li>What limits concurrency is the KV cache architecture and the time to first token. Parameter count says little: on the H200, a 510 GB model and a much smaller one tied at 8 users, for opposite reasons.</li>
</ul>
<h3>About measuring<span class="hx:absolute hx:-mt-20" id="about-measuring"></span>
    <a href="#about-measuring" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>A single run has noise of almost 10 points. Two scores 2 points apart are a tie.</li>
<li>The harness matters. Gemini 3.1 Pro was taken down by a proxy bug, gpt-oss doesn&rsquo;t finish in OpenCode, and three models insisted on creating the app in a subfolder.</li>
<li>A model that doesn&rsquo;t commit loses work. A fix that only lives on disk vanishes at the first <code>git checkout</code>.</li>
</ul>
<h2>My hypothesis: expensive hardware is part of the business model<span class="hx:absolute hx:-mt-20" id="my-hypothesis-expensive-hardware-is-part-of-the-business-model"></span>
    <a href="#my-hypothesis-expensive-hardware-is-part-of-the-business-model" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The v4 verdict, as of October 2026, is this: for code review and vigilance, open source has arrived. DeepSeek V4.1 Flash, GLM 5.3 Flash and Qwen3.8 Flash-Next are in the same pack as Sonnet 5 and GPT 6 sol, and one of them runs on a PC card.</p>
<p>So why isn&rsquo;t everybody running it at home? Because hardware got expensive and hard to find, and I have a hypothesis about that. It&rsquo;s my hypothesis, with no proof of any collusion, built on public facts.</p>
<h3>The facts<span class="hx:absolute hx:-mt-20" id="the-facts"></span>
    <a href="#the-facts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Memory skyrocketed. TrendForce measured a <a href="https://www.trendforce.com/presscenter/news/20260601-13070.html"target="_blank" rel="noopener">93% to 98% rise in DRAM contract prices</a> in the first quarter of 2026 alone, and says manufacturers <a href="https://www.trendforce.com/presscenter/news/20260930-13258.html"target="_blank" rel="noopener">keep prioritizing server capacity</a> in the fourth quarter.</li>
<li>HBM, the memory in datacenter GPUs, is expected to see its <a href="https://www.trendforce.com/presscenter/news/20260929-13255.html"target="_blank" rel="noopener">average price rise 121% in 2027</a> and take up close to <a href="https://www.trendforce.com/presscenter/news/20260602-13074.html"target="_blank" rel="noopener">30% of the DRAM wafers</a> of the three biggest manufacturers by the end of 2027.</li>
<li>Micron <a href="https://www.storagenewsletter.com/2025/12/05/micron-announces-exit-from-crucial-consumer-business/"target="_blank" rel="noopener">left the consumer market</a> and killed Crucial, in its own words, to better serve its larger strategic customers.</li>
<li>PCs and consoles went up in price, and Microsoft <a href="https://www.digitalcitizen.life/xbox-series-x-and-series-s-prices-will-rise-again-as-memory-and-storage-costs-surge/"target="_blank" rel="noopener">attributed the Xbox increase</a> to memory and storage costs.</li>
<li>The RTX 5090, when it shows up in the United States, costs from <a href="https://gpupoet.com/gpu/market-report/gpu-market-report-october-2026"target="_blank" rel="noopener">two and a half times</a> to <a href="https://www.techspot.com/news/114254-nvidia-rtx-5090-production-but-absence-company-us.html"target="_blank" rel="noopener">almost five times</a> MSRP, and the rumor is that its chips are going to the professional lines. NVIDIA <a href="https://www.guru3d.com/story/nvidia-says-geforce-rtx-5090-production-is-continuing/"target="_blank" rel="noopener">denies discontinuing it</a>.</li>
<li>On the other side, Anthropic <a href="https://techcrunch.com/2026/06/01/anthropic-files-to-go-public/"target="_blank" rel="noopener">filed for an IPO</a> aiming, according to the press, at <a href="https://www.khaleejtimes.com/business/anthropic-says-revenue-jumped-to-nearly-17b-last-quarter-reports-2"target="_blank" rel="noopener">about 2 trillion dollars</a>, and OpenAI is <a href="https://uk.investing.com/news/stock-market-news/openai-races-to-raise-30-bln-ahead-of-anthropic-ipo-reuters-reports-4918623"target="_blank" rel="noopener">raising at more than 1.4 trillion</a>.</li>
<li>And the rhetoric about Chinese open models has hardened. Anthropic published that <a href="https://www.anthropic.com/news/detecting-and-preventing-distillation-attacks"target="_blank" rel="noopener">DeepSeek, Moonshot and MiniMax distilled Claude</a>, and Dario Amodei, even while saying he <a href="https://www.anthropic.com/news/position-open-weights-models"target="_blank" rel="noopener">never advocated banning open weights</a>, calls for chip export controls and mandatory pre-release testing of every sufficiently capable model, open or closed.</li>
</ul>
<h3>The cycle<span class="hx:absolute hx:-mt-20" id="the-cycle"></span>
    <a href="#the-cycle" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The simplest explanation for expensive memory is demand: datacenters buy everything and pay more. I believe it. What bothers me is that no big player has an incentive to break the cycle:</p>
<ol>
<li>The rhetoric says only the frontier lab knows how to handle these &ldquo;dangerous&rdquo; models, and that open source is a risky bet.</li>
<li>That pulls demand to the cloud, and the cloud buys GPUs and memory in record volume.</li>
<li>Manufacturers sell at a premium to whoever pays more and deprioritize consumers.</li>
<li>Good hardware becomes unreachable outside the datacenter.</li>
<li>Without hardware at home, open source doesn&rsquo;t get adopted, which reinforces the rhetoric in step 1.</li>
</ol>
<blockquote>
  <p>If everybody started hosting their own model, the trillion-dollar IPO math would stop adding up. v4 showed that open models are already ready for a good chunk of the work. What&rsquo;s missing is the hardware getting into the hands of the people doing the work.</p>

</blockquote>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>open-source</category></item><item><title>Best Open Source LLM: Strata with Qwen 3.8 Flash Next</title><link>https://www.akitaonrails.com/en/2026/10/22/best-open-source-llm-strata-with-qwen-3-8-flash-next/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/22/best-open-source-llm-strata-with-qwen-3-8-flash-next/</guid><pubDate>Thu, 22 Oct 2026 11:45:00 -0300</pubDate><description>&lt;p&gt;Strata showed up in almost every article in the home stretch of benchmark v4. In the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/"&gt;consumer-grade open source&lt;/a&gt; one it was the lead; in the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/"&gt;what a team costs on B300 and H200&lt;/a&gt; one, the model it serves became the recommendation for the DGX Spark; and in the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/"&gt;v4 verdict&lt;/a&gt; it&amp;rsquo;s the only serious answer for anyone who wants to run a good model at home.&lt;/p&gt;
&lt;p&gt;The result, in short: &lt;strong&gt;Qwen3.8 Flash-Next&lt;/strong&gt; served by Strata on an RTX 5090 scored &lt;strong&gt;91, Tier A&lt;/strong&gt;, with zero nudges and all 14 planted defects fixed (two only after the reveal), 1.5 points behind the same model running on a datacenter B300. And it did that at about 159 tokens per second.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Strata showed up in almost every article in the home stretch of benchmark v4. In the <a href="/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/">consumer-grade open source</a> one it was the lead; in the <a href="/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/">what a team costs on B300 and H200</a> one, the model it serves became the recommendation for the DGX Spark; and in the <a href="/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/">v4 verdict</a> it&rsquo;s the only serious answer for anyone who wants to run a good model at home.</p>
<p>The result, in short: <strong>Qwen3.8 Flash-Next</strong> served by Strata on an RTX 5090 scored <strong>91, Tier A</strong>, with zero nudges and all 14 planted defects fixed (two only after the reveal), 1.5 points behind the same model running on a datacenter B300. And it did that at about 159 tokens per second.</p>
<p>In this article I go through what Strata is, why it matters right now, what it doesn&rsquo;t solve, and how I set it up for daily use, with automatic loading and unloading and wired into OpenCode.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261022121453_strata-qwen-flash-next-opencode.png" alt="OpenCode running Qwen3.8 Flash-Next through Strata on the RTX 5090, between 129.9 and 153.1 tokens per second"  loading="lazy" /></p>
<h2>What Strata is<span class="hx:absolute hx:-mt-20" id="what-strata-is"></span>
    <a href="#what-strata-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://github.com/Niko1221/Strata"target="_blank" rel="noopener">Strata</a> is an open source inference engine, MIT licensed, built around one model: Qwen3.8 Flash-Next, a 125B-parameter MoE with 6B active per token, which normally calls for a server. The code is C++ and CUDA, reuses parts of llama.cpp/ggml, and has builds for NVIDIA, AMD (Strix Halo included), Intel Arc and Mac. It brings up an OpenAI- and Anthropic-compatible API on localhost.</p>
<p>The trick is treating the whole PC as a memory hierarchy. The model has 24,576 experts, 512 in each of its 48 layers, and each token uses only 10 per layer, plus one shared expert:</p>
<ul>
<li><strong>VRAM</strong> holds the most-used experts, like a cache;</li>
<li><strong>RAM</strong> holds all of them, and when a token needs one that isn&rsquo;t on the card, the processor computes it right there while the card works on the others;</li>
<li>the <strong>SSD</strong> holds a lookup table of about 29 GB, from which the model reads a few rows per token.</li>
</ul>
<p>On top of that, a speculation head that ships with the model itself guesses the next tokens and the big model checks them all at once. The README promises the same answer 1.6 to 1.8 times faster.</p>
<p>On my 5090, the log shows what this looks like in practice. The 32 GB of VRAM hold 10 to 11.5 thousand experts in about 20 to 22 GiB, almost half the total, and <strong>generally 90% to 97% of the experts each token asks for are already on the card</strong>. The CPU computes the rest directly in RAM, and only a small fraction gets copied to the card over the PCIe bus. That&rsquo;s why the CPU works hard too, as you can see further down.</p>
<h2>Why this matters now<span class="hx:absolute hx:-mt-20" id="why-this-matters-now"></span>
    <a href="#why-this-matters-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Because memory has become a luxury item. In 2026 the DRAM contract price <a href="https://www.trendforce.com/presscenter/news/20260601-13070.html"target="_blank" rel="noopener">almost doubled in a single quarter</a>, manufacturers <a href="https://www.trendforce.com/presscenter/news/20260930-13258.html"target="_blank" rel="noopener">keep prioritizing servers</a>, and Micron <a href="https://www.storagenewsletter.com/2025/12/05/micron-announces-exit-from-crucial-consumer-business/"target="_blank" rel="noopener">walked away from the consumer market</a> to serve the big customers. The RTX 5090 itself, when it shows up, costs two and a half to almost five times MSRP, as I covered in the <a href="/en/2026/10/22/llm-benchmark-v4-the-open-source-verdict-as-of-oct-2026/">verdict</a>.</p>
<p>In this scenario, the traditional way of running a Tier A model at home, with the whole model inside VRAM, is a non-starter. Flash-Next in NVFP4 took about 100 GB of card memory on the B300; nobody has that at home.</p>
<blockquote>
  <p>Strata flips the premise: instead of demanding VRAM for the whole model, it uses VRAM as a cache and RAM as the warehouse. That&rsquo;s what lets a Tier A model run on a fraction of the video memory it would otherwise ask for.</p>

</blockquote>
<h2>It isn&rsquo;t magic<span class="hx:absolute hx:-mt-20" id="it-isnt-magic"></span>
    <a href="#it-isnt-magic" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Strata makes it fit, but it doesn&rsquo;t work miracles. The stated minimum is <strong>32 GB of RAM and VRAM combined</strong>, but that only runs the most compressed version; the IQ3_S that scored 91 asks for about 64 GB of RAM. Add <strong>about 80 GB of disk</strong>, preferably SSD, and a 12 GB card or better on the official path. Below that it&rsquo;ll still start, there are reports with an 8 GB card, but at about 15 tokens per second.</p>
<h3>Memory bandwidth sets the speed<span class="hx:absolute hx:-mt-20" id="memory-bandwidth-sets-the-speed"></span>
    <a href="#memory-bandwidth-sets-the-speed" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In token generation, the bottleneck on a local machine is almost never compute. It&rsquo;s how many bytes per second the memory can deliver to the cores, because every token has to read the model&rsquo;s active weights. And there the gap between memory types is brutal:</p>
<table>
  <thead>
      <tr>
          <th>Memory</th>
          <th style="text-align: right">Approximate bandwidth</th>
          <th style="text-align: right">Versus the 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RTX 5090 GDDR7 VRAM</td>
          <td style="text-align: right">~1.8 TB/s</td>
          <td style="text-align: right">1x</td>
      </tr>
      <tr>
          <td>Strix Halo LPDDR5X unified memory</td>
          <td style="text-align: right">~256 GB/s</td>
          <td style="text-align: right">7 times slower</td>
      </tr>
      <tr>
          <td>Desktop DDR5 RAM, dual channel</td>
          <td style="text-align: right">~60 to 100 GB/s</td>
          <td style="text-align: right">18 to 30 times slower</td>
      </tr>
      <tr>
          <td>PCIe 5.0 x16 bus</td>
          <td style="text-align: right">~64 GB/s per direction</td>
          <td style="text-align: right">almost 30 times slower</td>
      </tr>
      <tr>
          <td>PCIe measured by Strata on my machine (card at x8)</td>
          <td style="text-align: right">~29 GB/s</td>
          <td style="text-align: right">about 60 times slower</td>
      </tr>
  </tbody>
</table>
<p>The last row is from my machine: the card is running on 8 PCIe lanes instead of 16, and Strata measures that at startup. One more reason the VRAM cache matters so much.</p>
<p>The Strix Halo has 128 GB and fits a lot more than a 5090, but every byte arrives seven times slower. That&rsquo;s what I measured in the benchmark: the dense Qwen 3.8 27B in Q8 reads about 29 GB per token, and on the Strix the theoretical ceiling sits near 9 tokens per second. I measured 7.7. The same math on the 5090&rsquo;s bandwidth would give about 60, seven times more, if the model fit along with the context.</p>
<p>That&rsquo;s why Strata&rsquo;s design works: the hot path stays in GDDR7, and whatever misses the cache gets computed by the CPU in DDR5, or crosses PCIe, both dozens of times slower. With over 90% cache hits, the little that&rsquo;s left out doesn&rsquo;t drag the speed down. On a small card, the cache covers less, more experts take the slow road, and speed drops: the author himself publishes 53 tokens per second on a 12 GB RTX 5070, a third of my 5090.</p>
<h3>A potato PC won&rsquo;t run it<span class="hx:absolute hx:-mt-20" id="a-potato-pc-wont-run-it"></span>
    <a href="#a-potato-pc-wont-run-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>So forget the laptop with integrated graphics and 16 GB of RAM. Without a reasonable dedicated card, without 32 GB of RAM and VRAM combined and without an SSD, the model won&rsquo;t load, or it loads and runs at RAM speed, which is horse-and-buggy speed.</p>
<p>And there&rsquo;s an entry cost that doesn&rsquo;t show up in generation speed: Strata reads the whole first message of every conversation, and the README cites <a href="https://github.com/Niko1221/Strata#good-to-know"target="_blank" rel="noopener">about 60 seconds per 30 thousand tokens</a> as a rule of thumb. On my 5090 it&rsquo;s much faster, about 100 thousand tokens in 16 to 20 seconds, but on a smaller card count on the README number. Coding agents send big system prompts, so the first answer of each session takes longer than the ones after it.</p>
<h3>Heavy even on my 5090<span class="hx:absolute hx:-mt-20" id="heavy-even-on-my-5090"></span>
    <a href="#heavy-even-on-my-5090" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>My machine is no slouch: Ryzen 9 7950X3D, 96 GB of RAM (the system sees about 92), 32 GB RTX 5090. Even so, Flash-Next takes up everything:</p>
<ul>
<li>at load time, it pushes about 50 GB of experts into RAM, at about 3 GB per second with a cold SSD, and fills the card&rsquo;s cache with over 20 GiB;</li>
<li>with everything loaded, less than 800 MB of VRAM is left free;</li>
<li>with the server up, the machine sits at 88 of 92 GB of RAM used, and the system starts swapping.</li>
</ul>
<p>This is btop in the middle of a work session with OpenCode. The Strata process alone holds 54 GB of RAM and half the CPU, with half of the 7950X3D&rsquo;s 32 threads pinned at 100% and the rest above 50%. The 5090 is at 99% utilization, with 30 of 32 GB of VRAM in use and pulling 351 W. There&rsquo;s 4 GB of RAM left available, and almost 48 GB already in swap.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261022122545_strata-btop-7950x3d-5090.png" alt="btop during a Strata session: RAM at 96%, swap half full, half the CPU at 100% and the rest above 50%, and the RTX 5090 at 99% with 30 of 32 GB of VRAM"  loading="lazy" /></p>
<p>The side effect shows up in the rest of the system. With VRAM at the limit, my desktop&rsquo;s app launcher and the terminal on a secondary monitor stopped opening properly, and other programs get sluggish. The fix was telling Strata to give about 2.5 GB of VRAM back to the desktop, with the <code>--vram-elastic</code> option, at the cost of a slightly smaller cache.</p>
<blockquote>
  <p>With all that, I firmly believe my machine is the minimum for using a local LLM in real programming work, beyond small exercises. With less than that, count on much lower speed, shorter context and the whole PC suffering while the model is loaded.</p>

</blockquote>
<h2>How to install<span class="hx:absolute hx:-mt-20" id="how-to-install"></span>
    <a href="#how-to-install" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The official path on Linux and Mac is the one-line installer. It checks the SHA-256, doesn&rsquo;t ask for root and installs into your home folder. Like any <code>curl | sh</code>, download it and read it first:</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 -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh
</span></span><span class="line"><span class="cl">less install.sh
</span></span><span class="line"><span class="cl">sh install.sh</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>After that, <code>strata launch</code> asks for model, context and access, downloads the right engine for your card and the model, and starts serving. For scripts, everything goes through flags:</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">strata launch                                <span class="c1"># guided screens</span>
</span></span><span class="line"><span class="cl">strata launch --model &lt;variant&gt; --yes --no-tui   <span class="c1"># no questions</span>
</span></span><span class="line"><span class="cl">strata launch --status                       <span class="c1"># what is being served</span>
</span></span><span class="line"><span class="cl">strata launch --stop                         <span class="c1"># stop</span>
</span></span><span class="line"><span class="cl">strata monitor --url http://127.0.0.1:8080   <span class="c1"># speed, GPU, memory</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>I used the <strong>IQ3_S</strong> variant, the one that scored 91 in the benchmark. On disk it takes about 84 GB in two files (the 55 GB model and the 29 GB lookup table), plus about 7 GB for the speculation head.</p>
<p>The configuration <code>strata launch</code> generates is a JSON with the engine arguments. The ones that matter in mine:</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="s2">&#34;args&#34;</span><span class="err">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--expert-cache&#34;</span><span class="p">,</span> <span class="s2">&#34;auto&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--prefill&#34;</span><span class="p">,</span> <span class="s2">&#34;auto&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--spec&#34;</span><span class="p">,</span> <span class="s2">&#34;4&#34;</span><span class="p">,</span> <span class="s2">&#34;--spec-min-p&#34;</span><span class="p">,</span> <span class="s2">&#34;0.5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--mtp&#34;</span><span class="p">,</span> <span class="s2">&#34;/mnt/data/strata-models/mtp/rt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--max-context&#34;</span><span class="p">,</span> <span class="s2">&#34;262144&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--kv&#34;</span><span class="p">,</span> <span class="s2">&#34;int8&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--kv-resident&#34;</span><span class="p">,</span> <span class="s2">&#34;32768&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;--vram-elastic&#34;</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><code>--spec 4</code> turns on speculation with a window of 4 tokens per step, up to 3 guessed by the speculation head; <code>--kv int8</code> stores the attention cache in 8 bits; <code>--kv-resident</code> keeps only 32 thousand positions of the context cache, the most-read ones, in VRAM; and <code>--vram-elastic</code> lets it give VRAM back to the desktop with the server running.</p>
<h3>From 128 thousand to 256 thousand tokens of context<span class="hx:absolute hx:-mt-20" id="from-128-thousand-to-256-thousand-tokens-of-context"></span>
    <a href="#from-128-thousand-to-256-thousand-tokens-of-context" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The benchmark ran with 128 thousand tokens of context, which is what the install configured by default. Later I bumped it to 256 thousand, with <code>--max-context 262144</code>, and the loss was small: in the server logs, generation averaged about 190 tokens per second in a short session with 128 thousand and about 175 with 256 thousand, about 10% slower for twice the context, a bit more with VRAM given back to the desktop.</p>
<p>What holds this together is <code>--kv-resident</code>: only 32 thousand positions of the attention cache, the most-read ones, stay in VRAM, the rest stays in RAM, and even so about 97% of the reads from that cache happen on the card. The benchmark&rsquo;s 91 was measured with 128 thousand; with 256 thousand I measured speed, and quality hasn&rsquo;t been re-tested yet.</p>
<p>For me, 256 thousand is the minimum for long agent work. With 128 thousand, a real session blows through the context fast, and the agent starts compacting its memory all the time. That gets annoying very quickly, and every compaction throws away details the model will need later.</p>
<blockquote>
  <p>Short context is a hidden problem with local models. Before looking at tokens per second, check how much context you can serve without the speed collapsing.</p>

</blockquote>
<h2>Loading and unloading on its own<span class="hx:absolute hx:-mt-20" id="loading-and-unloading-on-its-own"></span>
    <a href="#loading-and-unloading-on-its-own" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Keeping about 50 GB of RAM and the entire card tied up all day, just to have the model ready when I need it, makes no sense. So I put together a small proxy, in pure Python, that&rsquo;s always up on port 8080 and controls Strata:</p>
<ul>
<li>it answers the model list on its own, so OpenCode sees the model without waking anything up;</li>
<li>on the first chat request, it brings Strata up on an internal port and waits for it to be ready, about 40 seconds;</li>
<li>it relays the responses in streaming;</li>
<li>after 10 minutes without traffic, it takes Strata down and gives the card and the RAM back to the system.</li>
</ul>
<p>The core is 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="k">def</span> <span class="nf">ensure_up</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">_lock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">ready</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">        <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">([</span><span class="n">STRATA</span><span class="p">,</span> <span class="s2">&#34;serve&#34;</span><span class="p">,</span> <span class="s2">&#34;--config&#34;</span><span class="p">,</span> <span class="n">CFG</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                          <span class="s2">&#34;--host&#34;</span><span class="p">,</span> <span class="s2">&#34;127.0.0.1&#34;</span><span class="p">,</span> <span class="s2">&#34;--port&#34;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">UP_PORT</span><span class="p">)],</span>
</span></span><span class="line"><span class="cl">                         <span class="n">start_new_session</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">90</span><span class="p">):</span>          <span class="c1"># cold start takes ~35-40 s</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">ready</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                <span class="n">set_reserve</span><span class="p">()</span>        <span class="c1"># give VRAM back to the desktop</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">idle_watch</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">_last</span> <span class="o">&gt;</span> <span class="n">IDLE_TTL</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">stop_up</span><span class="p">()</span>                <span class="c1"># free the GPU and ~50 GB of RAM</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 a systemd user service keeps the proxy up from login (trimmed version):</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-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ~/.config/systemd/user/qwen-strata.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Qwen3.8 Flash-Next on-demand proxy (Strata RTX 5090)</span>
</span></span><span class="line"><span class="cl"><span class="na">After</span><span class="o">=</span><span class="s">network-online.target</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">QWEN_IDLE_TTL=600</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">QWEN_VRAM_RESERVE_MIB=2560</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">QWEN_UP_PORT=8781</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">QWEN_PROXY_PORT=8080</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">%h/.local/bin/qwen-strata-ondemand</span>
</span></span><span class="line"><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="cl"><span class="na">KillMode</span><span class="o">=</span><span class="s">mixed</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">default.target</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>
<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">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now qwen-strata.service</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 price is the wait on the first message after some idle time. In exchange, when I&rsquo;m not programming, the machine is mine again.</p>
<h2>Configuring OpenCode<span class="hx:absolute hx:-mt-20" id="configuring-opencode"></span>
    <a href="#configuring-opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Strata has a shortcut, <code>strata connect opencode --write</code>, that writes the configuration after asking and keeps a backup. By hand, it&rsquo;s an OpenAI-compatible provider pointing at the local port:</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="s2">&#34;provider&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;strata-5090&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;npm&#34;</span><span class="p">:</span> <span class="s2">&#34;@ai-sdk/openai-compatible&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Strata (RTX 5090)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;baseURL&#34;</span><span class="p">:</span> <span class="s2">&#34;http://127.0.0.1:8080/v1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;apiKey&#34;</span><span class="p">:</span> <span class="s2">&#34;local&#34;</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;models&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;qwen3.8-flash-next-iq3_s&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen3.8 Flash-Next IQ3_S (Strata 5090)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;reasoning&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;tool_call&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;limit&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;context&#34;</span><span class="p">:</span> <span class="mi">262144</span><span class="p">,</span> <span class="nt">&#34;input&#34;</span><span class="p">:</span> <span class="mi">229376</span><span class="p">,</span> <span class="nt">&#34;output&#34;</span><span class="p">:</span> <span class="mi">32768</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="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>Any key works, because the server is local. Strata itself warns that OpenCode needs <code>limit.input</code>, to know how much of the context is left for input; in the example I set it to the context minus the max output. Claude Code works too, by pointing <code>ANTHROPIC_BASE_URL</code> at <code>http://127.0.0.1:8080</code> and setting <code>CLAUDE_CODE_MAX_CONTEXT_TOKENS</code> to the context size, since Strata speaks the Anthropic API as well.</p>
<p>One detail: by default Strata serves one request at a time. If your agent fires off subagents in parallel, they get queued.</p>
<h2>And day to day?<span class="hx:absolute hx:-mt-20" id="and-day-to-day"></span>
    <a href="#and-day-to-day" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you have the machine for it, the result is good. Flash-Next stays above 150 tokens per second most of the time, as you can see in the screenshot at the top, and tool calling is solid: in the benchmark it ran all seven sprints in OpenCode without a single nudge, and in normal use it reads files, runs commands and edits code without tripping over the format.</p>
<p>In practice, I don&rsquo;t feel much difference from Claude Code, or from OpenCode with GLM or Kimi in the cloud. And the cloud isn&rsquo;t at its best right now: in my perception, all those services have been a bit slower than usual lately, probably struggling with more demand than available capacity. Running at home, the speed is always the same, and the code never leaves my machine.</p>
<p>Even with good tokens per second, I still feel Flash-Next is a bit less responsive than a commercial model. There are the seconds of reading on the first message of each session, the pause when it needs experts that aren&rsquo;t on the card, and the cold load after some idle time. That&rsquo;s to be expected: we&rsquo;re running a server model with resources at the limit.</p>
<blockquote>
  <p>Considering it&rsquo;s free and runs on my machine, it&rsquo;s almost good enough. For the first time, running a local model for programming feels like a genuinely usable scenario, and I find myself using it more and more as a replacement.</p>

</blockquote>
<p>The catch is still the hardware. A 5090, 96 GB of RAM and a strong processor are the minimum for this scenario, and that hardware keeps getting more expensive and harder to find. But for those who already have it, Tier A at home is no longer a promise.</p>
]]></content:encoded><category>local-models</category><category>open-source</category><category>llms</category><category>hardware</category></item><item><title>LLM Benchmark v4: consumer-grade open source on RTX 5090 or Strix Halo. And Strata?</title><link>https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-consumer-open-source-rtx5090-strix-halo-strata/</guid><pubDate>Thu, 22 Oct 2026 11:30:00 -0300</pubDate><description>&lt;p&gt;In the last two articles I covered the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/"&gt;big-model benchmark on 4x B300&lt;/a&gt; and the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/"&gt;team-size and per-user cost math on B300 and H200&lt;/a&gt;. Now the question every developer without a Runpod pod has: &lt;strong&gt;is consumer hardware already good enough to work with AI for real?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I went back to two machines I already had: a workstation with an RTX 5090 and a Strix Halo mini PC. And with a new inference engine, Strata, which promises to make big MoEs fit on a regular PC. The short answer: a 125B model already makes Tier A on an RTX 5090. The rest is more complicated.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the last two articles I covered the <a href="/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/">big-model benchmark on 4x B300</a> and the <a href="/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/">team-size and per-user cost math on B300 and H200</a>. Now the question every developer without a Runpod pod has: <strong>is consumer hardware already good enough to work with AI for real?</strong></p>
<p>I went back to two machines I already had: a workstation with an RTX 5090 and a Strix Halo mini PC. And with a new inference engine, Strata, which promises to make big MoEs fit on a regular PC. The short answer: a 125B model already makes Tier A on an RTX 5090. The rest is more complicated.</p>
<h2>The machines, side by side<span class="hx:absolute hx:-mt-20" id="the-machines-side-by-side"></span>
    <a href="#the-machines-side-by-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before any scores, understand the physical difference of what I&rsquo;m comparing:</p>
<table>
  <thead>
      <tr>
          <th>Hardware</th>
          <th>Memory</th>
          <th>Bandwidth</th>
          <th>Architecture</th>
          <th>Approximate cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NVIDIA B300</td>
          <td>288 GB HBM3e</td>
          <td>~8 TB/s</td>
          <td>Blackwell Ultra, FP8 and FP4</td>
          <td>US$ 9/hour (Runpod)</td>
      </tr>
      <tr>
          <td>NVIDIA H200</td>
          <td>141 GB HBM3e</td>
          <td>~4.8 TB/s</td>
          <td>Hopper, FP8</td>
          <td>US$ 3.59/hour</td>
      </tr>
      <tr>
          <td>DGX Spark</td>
          <td>128 GB unified</td>
          <td>273 GB/s</td>
          <td>GB10, FP4</td>
          <td>~US$ 6,950</td>
      </tr>
      <tr>
          <td>RTX 5090</td>
          <td>32 GB GDDR7</td>
          <td>~1.8 TB/s</td>
          <td>Blackwell sm_120, FP8/FP4</td>
          <td>US$ 1,999 suggested (~5x at retail)</td>
      </tr>
      <tr>
          <td>Strix Halo (Ryzen AI Max+ 395)</td>
          <td>128 GB unified</td>
          <td>~256 GB/s</td>
          <td>gfx1151, ROCm</td>
          <td>~US$ 2,000</td>
      </tr>
  </tbody>
</table>
<p>The B300 has over 30 times the Strix Halo&rsquo;s memory bandwidth. The 5090 has 7 times. That explains why the same model flies on one machine and crawls on the other, and why quantization stops being a detail and becomes a design decision.</p>
<p>And there&rsquo;s the shared-memory detail: on the Strix and the Spark, the same RAM serves CPU, GPU and operating system. It&rsquo;s not dedicated VRAM, so the real slack is much smaller than the pretty number on the box.</p>
<h2>The ranking on consumer hardware<span class="hx:absolute hx:-mt-20" id="the-ranking-on-consumer-hardware"></span>
    <a href="#the-ranking-on-consumer-hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The yardstick is the same as the v4 sabotage benchmark: 14 planted defects, a review-vigilance score, Tier A starting at 83.</p>
<table>
  <thead>
      <tr>
          <th>Model / machine</th>
          <th>Variant and engine</th>
          <th style="text-align: right">Score / Tier</th>
          <th style="text-align: right">Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Qwen3.8 Flash-Next</strong> (125B-A6B)</td>
          <td>RTX 5090, Strata, IQ3_S</td>
          <td style="text-align: right"><strong>91.0 (A)</strong></td>
          <td style="text-align: right">159.2 tok/s</td>
      </tr>
      <tr>
          <td>Ornith-1.5-35B-A3B</td>
          <td>RTX 5090, Strata, Q8_0 GGUF</td>
          <td style="text-align: right">64.5 (C)</td>
          <td style="text-align: right">~80 tok/s</td>
      </tr>
      <tr>
          <td>Qwen3.8-27B</td>
          <td>5090 → Strix, Q5_K_M → Q8_0</td>
          <td style="text-align: right">80.0 (B)</td>
          <td style="text-align: right">7.7 tok/s</td>
      </tr>
      <tr>
          <td>GLM-4.7-Flash (30B-A3B)</td>
          <td>RTX 5090, llama-swap, Q5_K_M</td>
          <td style="text-align: right">24.0 (D)</td>
          <td style="text-align: right">not measured</td>
      </tr>
  </tbody>
</table>
<p>GLM-4.7-Flash completed the whole protocol (after a first nested attempt that needed a retry) and still failed to detect sabotage. And there&rsquo;s a humility lesson in the Qwen 27B: it scored 80 on the same task where it scored 80.5 on the B300, but at 7.7 tokens per second on the Q8 probe and 706 minutes on the clock. A good score doesn&rsquo;t pay for productivity.</p>
<p>I left out the tests that don&rsquo;t have a final score yet: Qwen3.8 Flash-Next on the Strix with Strata is only confirmed as executable, and the dense 27Bs were discarded on the Strix by the 30 tok/s cutoff.</p>
<h2>The tuning that makes a model run well on a PC<span class="hx:absolute hx:-mt-20" id="the-tuning-that-makes-a-model-run-well-on-a-pc"></span>
    <a href="#the-tuning-that-makes-a-model-run-well-on-a-pc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Consumer hardware doesn&rsquo;t forgive lazy configuration. What I learned in practice:</p>
<ul>
<li><strong>On the 5090, compile llama.cpp for sm_120.</strong> A ready-made image falls into a slow PTX fallback. It&rsquo;s <code>-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120</code>, driver 570+, and no <code>--ctx-size 0</code> on a 32 GB card: the training-context default blows the VRAM on the first allocation. Context needs an explicit ceiling, and if you get OOM on the first request, cut 25 to 50%.</li>
<li><strong>KV cache in q8_0.</strong> Halves KV memory with no measurable quality cost. Flash-attention on by default, but GLM 4.7 Flash needs it off, because its deepseek2 architecture is incompatible.</li>
<li><strong>Hybrid reasoning is work.</strong> Qwen3.8-27B, with the SSM/Mamba architecture, needed a fresh llama.cpp/Ollama build because of <code>ssm_conv1d</code>. And &ldquo;no reasoning&rdquo; mode didn&rsquo;t prevent a coherence loop in one of the attempts; the way out was resuming on the Strix with Q8_0 and <code>--reasoning off</code>. It wasn&rsquo;t extra memory that fixed it, it was turning off the thinking.</li>
<li><strong>On the Strix, the adjustment is ROCm, not magic.</strong> <code>HSA_OVERRIDE_GFX_VERSION</code>, <code>GGML_HIP_NO_VMM</code>, <code>-dio</code>. Its trump is capacity (128 GB), not speed (~256 GB/s). Q4_K_M gave 12.6 tok/s and Q8_0 gave 7.7 tok/s, below my 30 tok/s cutoff for interactive use. That&rsquo;s why the Strix became an escape hatch for big models, not a fast chat box.</li>
<li><strong>Speculation only helps with high acceptance.</strong> In a <a href="https://mihai.ltd/blog/beating-llamacpp-from-scratch-consumer-amd"target="_blank" rel="noopener">test by Mihai Farcas</a> on an RX 7900 XTX, llama.cpp&rsquo;s n-gram speculation, with no draft model, had only 5 of 64 tokens accepted, and throughput stayed flat in one run and dropped about 2% in the other. The gain shows up with a drafter made for the model, like the helper Strata uses on Flash-Next.</li>
</ul>
<h2>What is this Strata thing<span class="hx:absolute hx:-mt-20" id="what-is-this-strata-thing"></span>
    <a href="#what-is-this-strata-thing" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://github.com/Niko1221/Strata"target="_blank" rel="noopener">Strata</a> is an open source inference engine, MIT license, built around one model: <strong>Qwen3.8 Flash-Next</strong>, a 125-billion-parameter MoE that normally asks for a server. It also runs smaller MoEs, like the Ornith-35B I tested here. The code is C++ and CUDA and reuses parts of llama.cpp/ggml. It has builds for NVIDIA (CUDA 12 and 13), AMD (including Strix Halo), Intel Arc and Apple Silicon, and brings up an OpenAI- and Anthropic-compatible API on localhost, which plugs straight into Claude Code, Codex CLI or OpenCode.</p>
<p>Careful with the name. Mihai Farcas published <a href="https://mihai.ltd/blog/beating-llamacpp-from-scratch-consumer-amd"target="_blank" rel="noopener">another engine called Strata</a>, in Rust with HIP kernels, for small dense Qwen 3.5 models on an RX 7900 XTX. They&rsquo;re different projects, and the one that ran here was the first.</p>
<p>The problem it attacks is the big MoE on a small machine. The strategy is <strong>expert-offload</strong>, in layers:</p>
<ul>
<li>the model has 24,576 experts, 512 in each of the 48 layers, and each token uses only 10 per layer;</li>
<li>the GPU holds about 4 thousand experts, the most used ones;</li>
<li>the system RAM holds all of them;</li>
<li>a lookup table of about 29 GB stays on the SSD, and the model reads a few rows of it per token.</li>
</ul>
<p>On top of that, a small helper guesses the next words and the big model checks them all at once. The README promises the same answer 1.6 to 1.8 times faster. The minimum is 32 GB adding RAM and VRAM, plus about 80 GB of disk, and the IQ3_S variant I used is about 55 GB.</p>
<p>What made me believe in it was the result: <strong>Qwen3.8 Flash-Next on the 5090 with Strata scored 91.0, with zero nudges and all 14 items solved</strong>, 1.5 points behind the 92.5 the same model scored on the B300 with vLLM. The report itself calls that &ldquo;within the noise&rdquo;. A community project, on a consumer card, sat glued to the same model served in a datacenter in NVFP4.</p>
<h3>And now it can see<span class="hx:absolute hx:-mt-20" id="and-now-it-can-see"></span>
    <a href="#and-now-it-can-see" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Until yesterday, my Strata was blind: sending an image got back &ldquo;this server was started without the vision encoder&rdquo;. I enabled vision by hand, and it was simpler than it looked.</p>
<p>The vision projector (the <code>mmproj</code>) I had already archived on the NAS inside the uncensored Flash-Next variant. I checked its sha256 against the archive&rsquo;s manifest and against the hash Strata pinned for the BF16 encoder, and copied it into the models directory. The configuration gained the <code>--vision</code> flag and a <code>vision</code> section pointing to the <code>strata-vision</code> component and the mmproj, with a backup beside it.</p>
<p>The verification was end-to-end: the encoder loads in half a second, warms up 1,024 image tokens in under two seconds, and answered &ldquo;Red&rdquo; to a red test square. And the detail that matters most to me: with the encoder resident, 3.65 GB of VRAM remained free, so the desktop&rsquo;s 4 GB reserve still holds. The only gotcha: lazy loading doesn&rsquo;t coexist with the vision section in this configuration, so it stays off here.</p>
<h2>Careful: Strata eats the whole GPU, and the desktop dies<span class="hx:absolute hx:-mt-20" id="careful-strata-eats-the-whole-gpu-and-the-desktop-dies"></span>
    <a href="#careful-strata-eats-the-whole-gpu-and-the-desktop-dies" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a cost that doesn&rsquo;t show up in benchmarks: running Strata on the same GPU that draws your desktop. It was designed to fill the card, and it does.</p>
<p>In my case, the engine allocated 32,085 of the 32,607 MiB on the 5090, 98.4% of the VRAM. Hyprland was left with about 500 MiB, desktop allocations failed, and even the terminal stopped responding. Only a hard reset solved it.</p>
<p>It was two failures in sequence. The reserve I had configured was 2,560 MiB, but the proxy fired only once, swallowed the exception and never tried again. The engine&rsquo;s <code>/v1/vram</code> endpoint wasn&rsquo;t up at that moment, so the reserve was never applied. And 2,560 MiB was already too little for Hyprland, browser and terminal together.</p>
<p>The fix, validated under load:</p>
<ul>
<li><strong>4,096 MiB</strong> (4 GiB) reserve, with retry for up to a few minutes, engine-response verification and a confirmation log. No fire-and-forget.</li>
<li>Re-assertion of the reserve every 30 seconds while the engine is loaded, to close any rebalancing drift.</li>
<li>In the first load test, with the GPU at 99%, <strong>3,629 to 3,888 MiB stayed free during the whole generation</strong>. In the following test, one long generation and three other concurrent requests finished successfully; the smallest slack observed was 3,949 MiB. The desktop stayed alive.</li>
</ul>
<p>The price shows up in throughput. The expert cache dropped from about 11,700 slots to 9,700, so more experts travel over PCIe. In the initial measurement, it went from ~200 to ~150 tokens per second. I happily pay that: a desktop that doesn&rsquo;t freeze is worth more than those 25% of speed in that test.</p>
<p>There&rsquo;s a structural alternative: this machine has an integrated Radeon 610M, and Hyprland can be moved to it. The 5090 would be dedicated to inference.</p>
<p>The cost is that the 610M is weak for gaming and NVIDIA multi-GPU on Wayland has its quirks. I preferred the elastic reserve, because the model also unloads when idle.</p>
<p>The general lesson: <strong>never let an inference engine take 100% of a GPU that also draws your screen.</strong> In this configuration I don&rsquo;t have a rigid driver-level VRAM reservation for display clients. I had to solve it on the engine side: reserve a few GB, re-assert after every cold start, and measure again with the desktop open.</p>
<h3>The same 32 GB also limit the session<span class="hx:absolute hx:-mt-20" id="the-same-32-gb-also-limit-the-session"></span>
    <a href="#the-same-32-gb-also-limit-the-session" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The 5090&rsquo;s math is tight on all sides. The same 32 GB have to accommodate the model&rsquo;s dense weights and fixed buffers, the hot-expert cache, the resident part of the KV cache and also the 4 GiB reserve for Hyprland, browser and terminal. What&rsquo;s left for one thing comes out of another.</p>
<p>KV cache is the conversation&rsquo;s memory. Every token of the prompt, of the previous answers and of the files read creates state that the model needs to consult in the next tokens. In my Strata it&rsquo;s in <code>int8</code>, with 32K cells per layer resident on the GPU.</p>
<p>The rest can be streamed from RAM, which is why raising the window from 128K to 256K didn&rsquo;t increase VRAM or drop speed in my synthetic test. That streaming avoids the immediate blow-up, but it still uses memory and doesn&rsquo;t turn a 5090 into a limit-free server.</p>
<p>The ceiling configured here is <strong>262,144 tokens, or 256K</strong>. Several commercial models I tested, including the newest Claudes, already work in the 1-million-token class. Almost four times the window changes how you work: in Claude Code you can accumulate much more history before compaction; in the local Qwen I have to treat context as a budget from the first prompt.</p>
<p>The conversation that shows up in the terminal is only part of the context. Depending on the harness, the system prompt, <code>AGENTS.md</code> and <code>CLAUDE.md</code>, skill catalogs and loaded content, tool descriptions and schemas, MCP servers, plugin instructions, command outputs, files and the space reserved for the answer also go in. A well-implemented skill loads its full text only when called, but its catalog can still enter the prompt. A plugin that registers twenty tools also sends twenty schemas before you ask for anything.</p>
<p>That&rsquo;s why the benchmark didn&rsquo;t use my normal OpenCode. The runner <a href="https://github.com/akitaonrails/llm-coding-benchmark#local-opencode-benchmark-config"target="_blank" rel="noopener">generates an isolated configuration</a>, without the global plugins and MCPs, with only the providers and agents needed for the test. It also pins the real 262,144-token limit instead of pretending the model accepts the context advertised by another backend.</p>
<p>To use this Qwen day to day, I&rsquo;d do the same cleanup:</p>
<ul>
<li>start with the leanest possible harness and activate plugins or MCPs when the project really needs them;</li>
<li>keep heavy skills on demand, without dumping whole manuals at the start of the session;</li>
<li>compact or open a new session before touching the ceiling, recording important decisions in project files;</li>
<li>reserve space for the model&rsquo;s output. A window stuffed with input is useless if there&rsquo;s no token left for it to think, call tools and answer.</li>
</ul>
<blockquote>
  <p>With 256K, context is a hardware resource. Installing too many integrations and loading everything into every session consumes window and memory before the work starts.</p>

</blockquote>
<h2>Wasn&rsquo;t Qwen 3.8 too slow?<span class="hx:absolute hx:-mt-20" id="wasnt-qwen-38-too-slow"></span>
    <a href="#wasnt-qwen-38-too-slow" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It was, and I wrote that. In the <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/">v4 ranking</a>, the local Qwen 3.8 27B took almost 12 hours to close the seven sprints. In the <a href="/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/">B300 article</a> I summarized: scored 80, took 706 minutes, &ldquo;so don&rsquo;t even try&rdquo;.</p>
<p>Except that&rsquo;s another Qwen 3.8. The family has both, and they behave in opposite ways at home:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Qwen 3.8 27B</th>
          <th>Qwen3.8 Flash-Next</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Architecture</td>
          <td>dense, 27B</td>
          <td>MoE, 125B total, 6B active per token</td>
      </tr>
      <tr>
          <td>Where v4 finished</td>
          <td>Strix Halo, Q8_0</td>
          <td>RTX 5090, IQ3_S, Strata</td>
      </tr>
      <tr>
          <td>Speed</td>
          <td>7.7 tok/s</td>
          <td><strong>159.2 tok/s</strong></td>
      </tr>
      <tr>
          <td>v4 score</td>
          <td>80.0 (B)</td>
          <td><strong>91.0 (A)</strong></td>
      </tr>
  </tbody>
</table>
<h3>Why the bigger one runs faster<span class="hx:absolute hx:-mt-20" id="why-the-bigger-one-runs-faster"></span>
    <a href="#why-the-bigger-one-runs-faster" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In token generation, a local machine is memory-bandwidth-bound: each token needs to read all the active weights. The dense 27B reads its 27 billion every token, about 29 GB in Q8. The Strix moves about 256 GB/s, so the ceiling sits near 9 tokens per second, and I measured 7.7.</p>
<p>On the 5090 the same 27B would be fast, but in Q8 it doesn&rsquo;t fit in the 32 GB along with the context that seven sprints accumulate. In Q5_K_M it fit, but got stuck in a reasoning loop on sprint 3. It finished on the Strix, in Q8_0 with reasoning off (<code>--reasoning off</code>), which is what unlocked the loop.</p>
<p>Flash-Next has almost five times more parameters in total, but only about 6 billion work per token. In 3 bits, that&rsquo;s around a tenth of what the 27B in Q8 needs to read. The hot experts stay on the 5090&rsquo;s GDDR7, with seven times the Strix&rsquo;s bandwidth, and the speculation helper gets tokens ahead. Result: about 20 times the 27B&rsquo;s speed on the Strix, with a higher score.</p>
<blockquote>
  <p>The turnaround came from the model and engine swap: a MoE with few active parameters, served by an expert-offload engine made to measure for it. No configuration flag would pull this out of the dense 27B on the Strix.</p>

</blockquote>
<h3>The tuning still charges<span class="hx:absolute hx:-mt-20" id="the-tuning-still-charges"></span>
    <a href="#the-tuning-still-charges" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The price of running at home changes from model to model. Flash-Next in 3 bits lost only 1.5 points against the B300&rsquo;s NVFP4 version. Ornith-35B, on the same engine and the same card, lost 16.5 against its BF16 version on the B300. Measure your model before trusting it.</p>
<h2>What each model did that&rsquo;s interesting<span class="hx:absolute hx:-mt-20" id="what-each-model-did-thats-interesting"></span>
    <a href="#what-each-model-did-thats-interesting" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Flash-Next: the proof that consumer reaches Tier A.</strong> 34 of 40 unannounced, plus 6 with reveal weight, and only two items needing a pointer. No nudges, all sprints clean. It&rsquo;s the only local one that combines a Tier A score, interactive speed (159 tok/s) and the full protocol without help. If the question is &ldquo;can I use it at work&rdquo;, this is the example.</p>
<p><strong>Ornith-35B: fast, but not good enough.</strong> The same Ornith that scored 81 on the B300 in BF16 scored 64.5 on the 5090 with Q8_0. Unannounced detection dropped from 30 to 23. Quantization, engine and hardware all changed at once, so the cause can&rsquo;t be isolated: this Q8_0 + Strata + 5090 combination lost 16.5 points, and I don&rsquo;t know how much came from each variable. The message is clear: &ldquo;runs fast&rdquo; is not &ldquo;reviews well&rdquo;.</p>
<p><strong>Qwen 27B: good score, terrible experience.</strong> It scored 80 on the Strix resume, with sprints 1 and 2 on the 5090, close to the B300&rsquo;s 80.5. But 7.7 tok/s on the Q8 probe and 706 minutes of wall time. And on top of that zero fixes on reveal, with four items open. Using this day to day is choosing to wait.</p>
<p><strong>GLM-4.7-Flash: the engine ran, the model failed.</strong> Full protocol in 29 minutes, and zero unannounced detection. It recovered 8 of 14 only when pointed, and even then left the fixes uncommitted. On the B300 it scored 33.5, which isn&rsquo;t good either. The problem isn&rsquo;t just the card.</p>
<h2>The distance between the categories<span class="hx:absolute hx:-mt-20" id="the-distance-between-the-categories"></span>
    <a href="#the-distance-between-the-categories" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The question every dev has is whether consumer already gets there. Direct comparison, on the same yardstick:</p>
<table>
  <thead>
      <tr>
          <th>Category</th>
          <th>Models</th>
          <th style="text-align: right">Score</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commercial frontier</td>
          <td>Claude Opus 5 / 5.5</td>
          <td style="text-align: right">100.0</td>
      </tr>
      <tr>
          <td>Commercial frontier</td>
          <td>GPT-6 Luna</td>
          <td style="text-align: right">95.5</td>
      </tr>
      <tr>
          <td>Commercial frontier</td>
          <td>GLM 5.3 (hosted)</td>
          <td style="text-align: right">94.0</td>
      </tr>
      <tr>
          <td>Commercial frontier</td>
          <td>GPT-6 Sol / Sonnet 5</td>
          <td style="text-align: right">91.0</td>
      </tr>
      <tr>
          <td>Commercial frontier</td>
          <td>Kimi K3</td>
          <td style="text-align: right">85.0</td>
      </tr>
      <tr>
          <td>Open datacenter</td>
          <td>DeepSeek V4.1 Flash (4x B300)</td>
          <td style="text-align: right">92.5</td>
      </tr>
      <tr>
          <td>Open datacenter</td>
          <td>GLM 5.3 Flash / Qwen3.8 Flash-Next (B300)</td>
          <td style="text-align: right">92.5</td>
      </tr>
      <tr>
          <td><strong>Open consumer</strong></td>
          <td><strong>Qwen3.8 Flash-Next (5090 + Strata)</strong></td>
          <td style="text-align: right"><strong>91.0</strong></td>
      </tr>
      <tr>
          <td>Open consumer</td>
          <td>Qwen3.8-27B (Strix)</td>
          <td style="text-align: right">80.0</td>
      </tr>
      <tr>
          <td>Open consumer</td>
          <td>Ornith-35B (5090)</td>
          <td style="text-align: right">64.5</td>
      </tr>
      <tr>
          <td>Open consumer</td>
          <td>GLM-4.7-Flash (5090)</td>
          <td style="text-align: right">24.0</td>
      </tr>
  </tbody>
</table>
<p>The reading is that the consumer top is <strong>9 points below the absolute commercial top and 1.5 points below the open datacenter top</strong>, on this vigilance metric. It&rsquo;s a small distance, and it vanishes when you notice GPT-6 Sol and Sonnet 5 are also at 91. The real abyss isn&rsquo;t between datacenter and consumer: it&rsquo;s between the right model and all the others. Between Flash-Next and Ornith, on the same 5090, it&rsquo;s 26.5 points.</p>
<h2>So, is consumer hardware good enough to work with?<span class="hx:absolute hx:-mt-20" id="so-is-consumer-hardware-good-enough-to-work-with"></span>
    <a href="#so-is-consumer-hardware-good-enough-to-work-with" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Yes, with the right model and engine.</strong> An RTX 5090 with Strata delivers Tier A code-review vigilance, at 159 tokens per second, without paying a cent per token once the hardware exists. For anyone doing code review, testing coding agents and keeping private data at home, that changes the game.</p>
<p>For the first time in this benchmark I have a tested model, on consumer hardware, that deserves to go into everyday work. I&rsquo;ve already configured Flash-Next in OpenCode pointing at the 5090. The next step is using it in real programming, outside the sabotage yardstick, and seeing whether the score holds.</p>
<p>What doesn&rsquo;t work is thinking any configuration works. The Strix Halo is great for having 128 GB and mounting a big model, but its speed doesn&rsquo;t pay for interactivity; Ornith runs fast and reviews badly; GLM-4.7-Flash completes the protocol and detects nothing unannounced. And there&rsquo;s the invisible price: aggressive quantization can charge score, depending on the model and the configuration, and waiting time charges patience.</p>
<p>If I were building a box for a dev today, it would be a 5090 with Strata and Flash-Next, and I&rsquo;d use the Strix for background tasks that accept being slow. The commercial frontier keeps winning for whoever needs the maximum. But consumer stopped being a consolation prize: there&rsquo;s already Tier A on the home shelf.</p>
<h3>The problem now is finding a 5090<span class="hx:absolute hx:-mt-20" id="the-problem-now-is-finding-a-5090"></span>
    <a href="#the-problem-now-is-finding-a-5090" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In October, leakers claimed NVIDIA would stop producing the RTX 5090 and reserve the GB202 chips for the professional and datacenter lines. The card even disappeared from NVIDIA&rsquo;s official US store. On October 16, at an event in Taipei, <strong>Jeff Fisher</strong>, NVIDIA&rsquo;s senior vice president, <a href="https://www.guru3d.com/story/nvidia-says-geforce-rtx-5090-production-is-continuing/"target="_blank" rel="noopener">denied the discontinuation</a> and said the company will keep delivering, but admitted production capacity is tight.</p>
<p>In practice:</p>
<ul>
<li>the suggested price is still US$ 1,999, and most listings on American Newegg <a href="https://www.techspot.com/news/114254-nvidia-rtx-5090-production-but-absence-company-us.html"target="_blank" rel="noopener">go past US$ 9,300, almost five times that</a>;</li>
<li>in Brazil, each unit goes for around <a href="https://www.baguete.com.br/noticias/carga-milionaria-de-gpus-some-de-guarulhos"target="_blank" rel="noopener">R$ 35 thousand at retail</a>;</li>
<li>and two pallets of Zotac RTX 5090s, imported by the Oderço distributor and valued at about R$ 2.5 million, <a href="https://www.terra.com.br/noticias/misterio-em-guarulhos-r-25-milhoes-em-placas-de-video-desaparecem,a7596d7874eddfb490f96345b4be1b09z4eix4rc.html"target="_blank" rel="noopener">disappeared at Guarulhos airport</a>. GRU Airport notified the authorities, and the case remains under investigation, with no theft confirmation.</li>
</ul>
<blockquote>
  <p>The v4&rsquo;s first local Tier A model runs precisely on the hardest card to buy right now. The home shelf already has Tier A. Getting the shelf is what&rsquo;s missing.</p>

</blockquote>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>LLM Benchmark v4: can my team fit in a 4x b300/h200?</title><link>https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/</guid><pubDate>Thu, 22 Oct 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;In the last two articles I covered the &lt;a href="https://www.akitaonrails.com/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/"&gt;benchmark on a single B300&lt;/a&gt; and the &lt;a href="https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/"&gt;big-model ranking on 4x B300&lt;/a&gt;. The question that closes the trilogy is the company&amp;rsquo;s: &lt;strong&gt;is it worth building your own LLM infra for the dev team?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer depends on two things: how many comfortable users the hardware holds, and how much that costs per person. We measured both.&lt;/p&gt;
&lt;h2&gt;The math before the test&lt;span class="hx:absolute hx:-mt-20" id="the-math-before-the-test"&gt;&lt;/span&gt;
&lt;a href="#the-math-before-the-test" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Recapping the numbers from the previous article: the 4x B300 pod costs &lt;strong&gt;US$ 36 per hour&lt;/strong&gt; (US$ 864/day, almost US$ 26 thousand/month running nonstop). I started the campaign with about US$ 127 of balance, topped up US$ 2,550, and US$ 537 were left. That was &lt;strong&gt;about US$ 2,140&lt;/strong&gt; between setup, downloads, tuning and benchmarks, not just inference.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the last two articles I covered the <a href="/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/">benchmark on a single B300</a> and the <a href="/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/">big-model ranking on 4x B300</a>. The question that closes the trilogy is the company&rsquo;s: <strong>is it worth building your own LLM infra for the dev team?</strong></p>
<p>The answer depends on two things: how many comfortable users the hardware holds, and how much that costs per person. We measured both.</p>
<h2>The math before the test<span class="hx:absolute hx:-mt-20" id="the-math-before-the-test"></span>
    <a href="#the-math-before-the-test" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Recapping the numbers from the previous article: the 4x B300 pod costs <strong>US$ 36 per hour</strong> (US$ 864/day, almost US$ 26 thousand/month running nonstop). I started the campaign with about US$ 127 of balance, topped up US$ 2,550, and US$ 537 were left. That was <strong>about US$ 2,140</strong> between setup, downloads, tuning and benchmarks, not just inference.</p>
<p>In the middle, the balance hit zero, the pod died during an autotune, and when it was time to top up <strong>there was no B300 available</strong>. That&rsquo;s Runpod&rsquo;s operational risk: the price changes and the supply disappears. The alternative was a 4x H200 pod, with list price around <a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">US$ 3.59 per GPU-hour on Community Cloud</a>: US$ 14.36/hour for the four at list math, against US$ 36 for the B300s.</p>
<h2>B300 and H200 are not the same machine<span class="hx:absolute hx:-mt-20" id="b300-and-h200-are-not-the-same-machine"></span>
    <a href="#b300-and-h200-are-not-the-same-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The hardware difference changes more than the price:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>4x B300</th>
          <th>4x H200</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Architecture</td>
          <td>Blackwell Ultra (<code>sm_103a</code>)</td>
          <td>Hopper (<code>sm_90</code>)</td>
      </tr>
      <tr>
          <td>VRAM</td>
          <td>~288 GB per GPU, ~1.1 TB total</td>
          <td>~141 GB per GPU, ~560 GB total</td>
      </tr>
      <tr>
          <td>Native formats</td>
          <td>FP8 and FP4/NVFP4</td>
          <td>FP8 (no native FP4)</td>
      </tr>
      <tr>
          <td>Toolchain</td>
          <td>CUDA 12.9+ and kernels compiled on the fly</td>
          <td>more mature, but still with JIT patches</td>
      </tr>
  </tbody>
</table>
<p>The swap had an adaptation cost: the GLM&rsquo;s NVFP4 checkpoint that ran on B300 doesn&rsquo;t exist for Hopper, so its FP8 variant had to be downloaded, and even DeepSeek needed kernel tuning. The concurrency results below are with both in native FP8, which is the fair comparison possible.</p>
<h2>How we measured &ldquo;a team fits&rdquo;<span class="hx:absolute hx:-mt-20" id="how-we-measured-a-team-fits"></span>
    <a href="#how-we-measured-a-team-fits" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The test uses <code>vllm bench serve</code> against each pod&rsquo;s localhost, with requests of 8,000 input tokens and 800 output (a synthetic load in the shape of a coding session), raising concurrency N step by step.</p>
<p>The comfort criterion is what matters: <strong>TTFT up to 2 seconds and TPOT up to 20 ms per token</strong> (that is, at least 50 tokens/s per stream), with <strong>95% or more of requests</strong> meeting both limits. It&rsquo;s not &ldquo;the median looked good&rdquo;: it&rsquo;s almost everyone well served. A team that waits 50 seconds for the first token is not a team, it&rsquo;s a queue.</p>
<p>And the caveat: this measures comfortable code requests, not people. I don&rsquo;t have a measured duty-cycle multiplier for interactive agents, so I won&rsquo;t invent that each N becomes 2 or 3 developers.</p>
<h2>The numbers<span class="hx:absolute hx:-mt-20" id="the-numbers"></span>
    <a href="#the-numbers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Model / hardware</th>
          <th style="text-align: right">Single stream</th>
          <th style="text-align: right">Comfortable (N)</th>
          <th style="text-align: right">Loose gate (median only)</th>
          <th style="text-align: right">Aggregate peak</th>
          <th>Bottleneck</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4.1 Flash, 4x B300 FP8</td>
          <td style="text-align: right">177 tok/s</td>
          <td style="text-align: right"><strong>32</strong></td>
          <td style="text-align: right">48</td>
          <td style="text-align: right">2,778 tok/s</td>
          <td>prefill (TTFT)</td>
      </tr>
      <tr>
          <td>DeepSeek V4.1 Flash, 4x H200 FP8</td>
          <td style="text-align: right">137 tok/s</td>
          <td style="text-align: right"><strong>8</strong></td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">2,506 tok/s</td>
          <td>prefill (TTFT)</td>
      </tr>
      <tr>
          <td>GLM 5.3 Flash, 4x H200 FP8</td>
          <td style="text-align: right">175 tok/s</td>
          <td style="text-align: right"><strong>8</strong></td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">1,930 tok/s</td>
          <td><strong>KV cache, 97%</strong></td>
      </tr>
  </tbody>
</table>
<p>Read carefully, because there are three stories here.</p>
<p><strong>The B300 serves four times more people in comfort.</strong> Same model, same FP8, same TP4: 32 comfortable users against 8. The H200&rsquo;s aggregate is even impressive (2,506 tok/s), but that&rsquo;s a prefill queue: p99 TTFT reaches about 51 seconds at N=256, so the user waits a minute before seeing anything. High throughput is not interactive capacity.</p>
<p>The cliff is so steep it&rsquo;s worth looking at the numbers up close. At N=16, p99 TTFT was 1.17 s on B300 and 3.19 s on H200, with 100% and 92.7% of requests good. At N=32, the B300 still held 97.4% inside the SLO (2.23 s), while the H200 collapsed to 8.3% (6.58 s). And at N=48 the B300 fell to 92%, below my 95% gate: that&rsquo;s why comfortable is 32 and not 48. The difference between &ldquo;works&rdquo; and &ldquo;works for everyone&rdquo; lives in those last 2 percentage points.</p>
<p><strong>Heavy and light models tie on the H200.</strong> The 510 GB DeepSeek and the smaller GLM sustain the same 8 comfortable users, for different reasons: DeepSeek has efficient KV via MLA and dies of compute/prefill; GLM blows the KV cache (97% usage). Choosing hardware by parameter count is looking at the wrong place: the KV architecture is what rules concurrency.</p>
<p><strong>The loose gate deceives.</strong> If you only look at the median, you could say 48 on B300 and 16 on H200. Except the p99 tail destroys the experience of whoever is on the other side. To size a team, I use the comfortable number and that&rsquo;s it.</p>
<h2>Cost per comfortable user<span class="hx:absolute hx:-mt-20" id="cost-per-comfortable-user"></span>
    <a href="#cost-per-comfortable-user" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the deciding arithmetic:</p>
<ul>
<li><strong>4x B300:</strong> US$ 36/h ÷ 32 = <strong>US$ 1.13 per comfortable user per hour</strong>.</li>
<li><strong>4x H200:</strong> US$ 14.36/h ÷ 8 = <strong>US$ 1.80 per comfortable user per hour</strong>.</li>
</ul>
<p>The B300 wins per person even with the pod costing 2.5x more. The H200 would only become cheaper per user below US$ 9/hour for the whole pod, and on the current table it costs US$ 14.36. The cut-off point isn&rsquo;t the machine&rsquo;s price, it&rsquo;s its capacity.</p>
<h2>What if your company already lives on AWS or Azure?<span class="hx:absolute hx:-mt-20" id="what-if-your-company-already-lives-on-aws-or-azure"></span>
    <a href="#what-if-your-company-already-lives-on-aws-or-azure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You can build the same infra on the hyperscalers, but the price changes category. The list values I checked, per GPU-hour:</p>
<table>
  <thead>
      <tr>
          <th>Provider</th>
          <th>Configuration</th>
          <th style="text-align: right">Price per GPU-hour</th>
          <th style="text-align: right">Against Runpod</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Runpod, 4x B300 (my campaign)</td>
          <td>B300</td>
          <td style="text-align: right">US$ 9.00</td>
          <td style="text-align: right">1x</td>
      </tr>
      <tr>
          <td>Runpod, 4x H200 (Community Cloud)</td>
          <td>H200</td>
          <td style="text-align: right"><a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">US$ 3.59</a></td>
          <td style="text-align: right">0.4x</td>
      </tr>
      <tr>
          <td><a href="https://themodelpress.com/gpu-prices"target="_blank" rel="noopener">AWS, p6-b300.48xlarge</a></td>
          <td>8x B300, capacity block</td>
          <td style="text-align: right">US$ 16.15</td>
          <td style="text-align: right">1.8x</td>
      </tr>
      <tr>
          <td>AWS, p6-b200.48xlarge</td>
          <td>8x B200, capacity block</td>
          <td style="text-align: right">US$ 14.21</td>
          <td style="text-align: right">1.6x</td>
      </tr>
      <tr>
          <td><a href="https://themodelpress.com/gpu-prices/azure-nd-h200-v5-on-demand"target="_blank" rel="noopener">Azure, ND96isr_H200_v5</a></td>
          <td>8x H200, East US 2</td>
          <td style="text-align: right">US$ 10.60</td>
          <td style="text-align: right">3.0x</td>
      </tr>
  </tbody>
</table>
<p>B300 on AWS costs 1.8x what I paid on Runpod, and H200 on Azure costs almost 3x Runpod&rsquo;s. The capacity conclusion doesn&rsquo;t change (the physics of tensor parallelism and KV cache is the same), but the &ldquo;worth it&rdquo; math gets considerably worse: if on Runpod the B300 ties the subscription around 30 users, on AWS it starts losing by a comfortable margin.</p>
<p>And in practice, how much does it come out to per developer, turning the pod off at night like in the other calculations (8 hours per business day, 176 hours/month)? The AWS and Azure rows are normalized arithmetic for 4 GPUs, because the cited SKUs come with 8 and I didn&rsquo;t measure TP=8:</p>
<table>
  <thead>
      <tr>
          <th>Setup</th>
          <th>Monthly pod cost</th>
          <th style="text-align: right">Comfortable streams (1 per dev)</th>
          <th style="text-align: right">Cost per dev/month</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Runpod 4x B300 (US$ 36/h)</td>
          <td>US$ 6,336</td>
          <td style="text-align: right">32</td>
          <td style="text-align: right">US$ 198</td>
      </tr>
      <tr>
          <td>AWS 4x B300 (US$ 64.58/h, normalized)</td>
          <td>US$ 11,367</td>
          <td style="text-align: right">32</td>
          <td style="text-align: right">US$ 355</td>
      </tr>
      <tr>
          <td>Runpod 4x H200 (US$ 14.36/h)</td>
          <td>US$ 2,527</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right">US$ 316</td>
      </tr>
      <tr>
          <td>Azure 4x H200 (US$ 42.40/h, normalized)</td>
          <td>US$ 7,462</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right">US$ 933</td>
      </tr>
  </tbody>
</table>
<p>The difference is brutal: the same comfortable developer costs US$ 198 on Runpod and US$ 933 on Azure, almost five times the price of the strongest frontier subscription. AWS sits almost 80% above Runpod, so even it only makes sense if your requirement is something other than cost.</p>
<p>And if you take the whole 8-GPU instance, as they&rsquo;re sold? AWS comes out at US$ 22,734/month and Azure at US$ 14,925/month, and then you have to measure TP=8 to know how many users you can use, because with TP=4 you pay double for the same capacity.</p>
<p>The commercial models differ too: capacity block on AWS is not on-demand, and Runpod&rsquo;s Community Cloud is not production SLA. So read this table as an order of magnitude of price, not a contract quote. If your requirement is compliance, procurement or residency in a specific region, the hyperscaler may be unavoidable, and then you pay the premium. If the requirement is just price, the neoclouds win in these configurations.</p>
<h2>What if, instead of renting, I buy the machine?<span class="hx:absolute hx:-mt-20" id="what-if-instead-of-renting-i-buy-the-machine"></span>
    <a href="#what-if-instead-of-renting-i-buy-the-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When the rental bill hurts, the natural question is to buy the hardware. Two candidates from the current shelf:</p>
<p><strong>DGX Station GB300</strong> (~US$ 130 thousand, with configurations starting around US$ 95k): 748 GB of unified memory (252 GB of HBM3e at 7.1 TB/s and 496 GB of LPDDR5X at 396 GB/s), 20 PFLOPS in FP4. The weights of every model I tested fit on it, and the best ones to put on it would be:</p>
<ul>
<li><strong>DeepSeek V4.1 Flash</strong> (92.5/A, 510 GB in FP8), the benchmark champion;</li>
<li><strong>Muse Spark Open</strong> (69.5/C, 533 GB in NVFP4);</li>
<li><strong>Step-5-Preview</strong> (72.5/C, 368 GB in NVFP4);</li>
<li><strong>GLM 5.3 full</strong> (76/B, 704 GB in FP8).</li>
</ul>
<p>It fits, but fitting is not serving: the full GLM would leave only about 44 GB for KV cache and system, so full long-context concurrency remains to be measured. The comfort estimate is single-digit, in the H200&rsquo;s line, not the four-pod&rsquo;s 32. The difference is you don&rsquo;t need TP=4: everything lives on a single chip.</p>
<p><img src="/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/dgx-station-gb300.jpg" alt="DGX Station GB300 render, the desktop with the Grace Blackwell Ultra superchip and 748 GB of unified memory"  loading="lazy" /></p>
<p><strong>DGX Spark</strong> (~US$ 6,950 on NVIDIA&rsquo;s current store): 128 GB unified, 1 PFLOP in FP4, 273 GB/s of bandwidth. The good filling for it is small and medium models, and it&rsquo;s better than it looks:</p>
<p>And there&rsquo;s a detail that hurts to see on the bill: by my own estimate, without a closed audit of all the first round&rsquo;s costs, adding the v4 credits (those ~40 commercial models) to this B300/H200 campaign&rsquo;s US$ 2,140, I&rsquo;ve already spent a few thousand dollars between API and pods, close to half the price of a DGX Spark. And even so, I wouldn&rsquo;t have a machine sitting at home because of it.</p>
<ul>
<li><strong>Qwen3.8 Flash-Next</strong> (92.5/A), the single-B300 co-leader. In IQ3_S it lands in the tens of GB range, and it&rsquo;s exactly the model that scored 91.0 on the RTX 5090 with Strata. It&rsquo;s the best model I&rsquo;d recommend for a Spark today.</li>
<li><strong>Swift 1.5 27B</strong> (88/A, 56 GB), which was the most balanced self-host recommendation of my previous campaign;</li>
<li><strong>ThinkingCap 3.8 27B</strong> (83/A, 56 GB);</li>
<li><strong>Ornith-1.5-35B-A3B</strong> (81/B, ~70 GB in BF16);</li>
<li><strong>Laguna-S-2.1</strong> (65.5/C, 99.7 GB in NVFP4);</li>
<li>and the small classifiers from the decision benchmark (Jebadiah 4B, Laya, Julia), which are the ideal use case for a box like this.</li>
</ul>
<p>The DeepSeek V4.1, at 510 GB, doesn&rsquo;t fit at all. The bandwidth is almost 30 times smaller than the B300&rsquo;s, so interactivity suffers, and practical concurrency remains to be measured: I expect little more than solo use. For small models it&rsquo;s a nice little box; for a monster, it&rsquo;s a paperweight.</p>
<p><img src="/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/dgx-spark-box.jpeg" alt="DGX Spark, the desktop with 128 GB of unified memory and 1 PFLOP in FP4"  loading="lazy" /></p>
<h3>TCO, ROI and depreciation<span class="hx:absolute hx:-mt-20" id="tco-roi-and-depreciation"></span>
    <a href="#tco-roi-and-depreciation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The own-versus-rent math, using 3 years of depreciation and the same usage pattern (8 hours per business day, 176 hours/month):</p>
<table>
  <thead>
      <tr>
          <th>Scenario</th>
          <th>Rental cost/month</th>
          <th style="text-align: right">Purchase cost (depreciation/month)</th>
          <th style="text-align: right">Break-even</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GB300 vs Runpod 4x B300 (US$ 36/h)</td>
          <td>US$ 6,336</td>
          <td style="text-align: right">US$ 3,611 (130k / 36)</td>
          <td style="text-align: right"><strong>~20 months</strong></td>
      </tr>
      <tr>
          <td>GB300 vs Runpod 24/7</td>
          <td>US$ 25,920</td>
          <td style="text-align: right">US$ 3,611</td>
          <td style="text-align: right">~5 months</td>
      </tr>
      <tr>
          <td>Spark vs 1x B300 (US$ 9.09/h)</td>
          <td>US$ 1,600</td>
          <td style="text-align: right">US$ 193 (6,950 / 36)</td>
          <td style="text-align: right"><strong>~4.5 months</strong></td>
      </tr>
      <tr>
          <td>Spark vs US$ 200 subscription</td>
          <td>US$ 200</td>
          <td style="text-align: right">US$ 193</td>
          <td style="text-align: right">~3 years</td>
      </tr>
  </tbody>
</table>
<p>Reading: if you run a big model every business day for two years or more, the GB300 pays for itself in under two years, and in 24/7 it pays for itself in five months. If your need is a two-week campaign like mine, renting cost US$ 2,140 and done, with no idle asset afterwards. And the Spark only makes sense if your load is small models: in that case it pays for itself in under a semester against renting and in three years against the subscription, but if your need is a 748B, it&rsquo;s useless from the box.</p>
<p>Two caveats so you don&rsquo;t buy wrong. First, the table compares dollar scenarios, not equivalent-workload ROI: if your real concurrency on the GB300 is single-digit, the fair comparison would be with a pod smaller than the 4x B300. Second, energy and administration change the result, especially on the Spark, where the difference against the subscription comes down to a few dollars per month. I ignored resale at the end of the three years; if it exists, the break-even improves.</p>
<p><strong>Note for those buying in Brazil:</strong> these prices are in dollars. In reais, with the dollar at R$ 4.99, plus import taxes and the local reseller margin, the GB300 easily passes R$ 800 thousand and the Spark passes R$ 34 thousand before any margin. The break-even in reais gets even further away, so the decision to buy in Brazil needs much higher utilization to close.</p>
<p>And comparing with a frontier subscription? A strong Claude/Codex plan costs about US$ 200 per month per developer. Two important caveats: those plans aren&rsquo;t unlimited (Codex meters by credit and Claude has per-session and per-week usage windows), and automation/CI doesn&rsquo;t fit consumer plans. The comparison below is price-only, not equivalent workload. To turn streams into people I need a premise: one developer with one active request the whole workday, which is optimistic for capacity and conservative for cost. With that premise, and not counting my sysadmin hours or setup, the pod running 8 hours per workday (176 hours/month) looks like this:</p>
<table>
  <thead>
      <tr>
          <th>Setup</th>
          <th style="text-align: right">Monthly pod cost</th>
          <th style="text-align: right">Comfortable streams (1 per dev)</th>
          <th style="text-align: right">Cost per dev/month</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>4x B300</td>
          <td style="text-align: right">US$ 6,336</td>
          <td style="text-align: right">32</td>
          <td style="text-align: right"><strong>US$ 198</strong></td>
      </tr>
      <tr>
          <td>4x H200</td>
          <td style="text-align: right">US$ 2,527</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right"><strong>US$ 316</strong></td>
      </tr>
      <tr>
          <td>4x B300 on 24/7</td>
          <td style="text-align: right">US$ 25,920</td>
          <td style="text-align: right">32</td>
          <td style="text-align: right">US$ 810</td>
      </tr>
  </tbody>
</table>
<p>Around 30 continuous streams, the B300 lands practically at the strong subscription&rsquo;s price. Below that, the subscription is cheaper, because you pay per person, not per idle pod hour. And running 24/7 without matching load is throwing money away: the pod charges while sleeping.</p>
<h2>For a company, when the math closes<span class="hx:absolute hx:-mt-20" id="for-a-company-when-the-math-closes"></span>
    <a href="#for-a-company-when-the-math-closes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you have a genuinely big team using it all day, data that can&rsquo;t leave the environment, and a model that delivers on the daily job (DeepSeek V4.1 Flash scored 92.5 on my vigilance benchmark, above GPT-6 Sol and Sonnet 5), your own infra starts making sense <strong>for the first time without being vanity</strong>. The cut-off point, on the premise of one active stream per person the whole workday, lands around 30 concurrent people, with the pod off outside working hours.</p>
<p>If the team is small, usage is intermittent, or the goal is just to save money: stay with the API. The same DeepSeek cost US$ 1.21 per full benchmark round in the cloud, and you don&rsquo;t inherit a pod to administer, nor the risk of the B300 vanishing from stock when you need it most.</p>
<p>What I take from this campaign: big hardware is not an answer to a small question. Before signing the pod, do the math on how many people actually run concurrently on your team, measure the latency they tolerate, and add the price of your sysadmin hour. The big open model already reached the Sonnet 5 and GPT-6 Sol band for daily code review. The infra just needs to fit the budget.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>LLM Benchmark v4: open source that only fits in 4x B300!!</title><link>https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/22/llm-benchmark-v4-open-source-that-only-fits-in-4x-b300/</guid><pubDate>Thu, 22 Oct 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;In the &lt;a href="https://www.akitaonrails.com/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/"&gt;previous article&lt;/a&gt; I told what it was like running the v4 sabotage benchmark on a single rented NVIDIA B300 on Runpod. The obvious leftover question: what about the really big models, the ones that don&amp;rsquo;t fit in 288 GB? I decided to see how far open source can be pushed.&lt;/p&gt;
&lt;p&gt;The short answer: pretty far, and one of them tied the Sonnet 5 and GPT-6 Sol band. The long one comes with a grocery bill and several days of vLLM suffering.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the <a href="/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/">previous article</a> I told what it was like running the v4 sabotage benchmark on a single rented NVIDIA B300 on Runpod. The obvious leftover question: what about the really big models, the ones that don&rsquo;t fit in 288 GB? I decided to see how far open source can be pushed.</p>
<p>The short answer: pretty far, and one of them tied the Sonnet 5 and GPT-6 Sol band. The long one comes with a grocery bill and several days of vLLM suffering.</p>
<p>First of all, the scope caveat. Some models were left out.</p>
<p>Kimi K3, at 2.8 trillion parameters, is the example: in FP8 it would need about 2.8 TB of VRAM, and not even an 8x B300 pod (2.3 TB raw) closes the math. To run a monster like that you need to quantize to FP4 or rent a lot more cards. And I chose to spend my credit on what fits in 4x.</p>
<h2>The full ranking<span class="hx:absolute hx:-mt-20" id="the-full-ranking"></span>
    <a href="#the-full-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Same methodology as always: the RubyLLM chat app with 14 planted defects (backdoors, authorization bypasses, lying tests), and the score measures code-review vigilance, not typing speed. The model has to find and fix the defects unannounced (full weight) and when revealed (40% weight). Tier A starts at 83. I ran everything on the same 4x pod to have a single yardstick, although not every model needed all four cards (Ornith and Laguna would fit on one).</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size / checkpoint</th>
          <th style="text-align: right">Score / Tier</th>
          <th style="text-align: right">Speed (single stream)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4.1 Flash</td>
          <td>748B (552B + 196B Engram), FP8, 510 GB</td>
          <td style="text-align: right"><strong>92.5 (A)</strong></td>
          <td style="text-align: right">174.8 tok/s</td>
      </tr>
      <tr>
          <td>GLM 5.3 full</td>
          <td>~700B, FP8, 704 GB</td>
          <td style="text-align: right">76.0 (B)</td>
          <td style="text-align: right">110 tok/s</td>
      </tr>
      <tr>
          <td>Ornith-1.5-122B</td>
          <td>~122B MoE, FP8, 133 GB</td>
          <td style="text-align: right">73.0 (B)</td>
          <td style="text-align: right">244.2 tok/s</td>
      </tr>
      <tr>
          <td>Step-5-Preview</td>
          <td>~816B (estimated size), NVFP4, 368 GiB</td>
          <td style="text-align: right">72.5 (C)</td>
          <td style="text-align: right">165.9 tok/s (with MTP)</td>
      </tr>
      <tr>
          <td>Muse Spark Open</td>
          <td>816B-A42B, NVFP4, 533 GB</td>
          <td style="text-align: right">69.5 (C)</td>
          <td style="text-align: right">130.8 tok/s</td>
      </tr>
      <tr>
          <td>MiMo V2.6 Pro</td>
          <td>~0.5T MoE, FP8/mxfp4, ~535 GB</td>
          <td style="text-align: right">67.0 (C)</td>
          <td style="text-align: right">167.2 tok/s</td>
      </tr>
      <tr>
          <td>Laguna-S-2.1</td>
          <td>small code MoE, NVFP4, 99.7 GB</td>
          <td style="text-align: right">65.5 (C)</td>
          <td style="text-align: right">263 tok/s</td>
      </tr>
      <tr>
          <td>Kimi K2 Thinking</td>
          <td>1T / ~32B active, INT4, 594 GB</td>
          <td style="text-align: right">46.0 (D)</td>
          <td style="text-align: right">110 tok/s</td>
      </tr>
      <tr>
          <td>Nex N2.5 Pro</td>
          <td>~397B MoE, FP8, ~380 GB</td>
          <td style="text-align: right">27.0 (D)</td>
          <td style="text-align: right">184 tok/s</td>
      </tr>
  </tbody>
</table>
<p>Two notes to read the table properly. Step-5 and Muse are in C because the unannounced base was low: a fix after the &ldquo;there&rsquo;s a problem there&rdquo; is worth 40%. Muse fixed 6 of the 7 open items when revealed; Step-5, only 2 of 5.</p>
<p>And Nex N2.5 Pro, on top of the shameful score, corrupted the code to the point the app wouldn&rsquo;t even boot. The two anchors from the previous campaign, for comparison: GLM 5.3 Flash and Qwen3.8 Flash-Next, both 92.5 on the single B300.</p>
<h2>What each one did that&rsquo;s interesting<span class="hx:absolute hx:-mt-20" id="what-each-one-did-thats-interesting"></span>
    <a href="#what-each-one-did-thats-interesting" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The score alone hides the behavior, and the behavior is the fun part.</p>
<p><strong>DeepSeek was the only one that solved both compound items unaided.</strong> The compound defects are the ones that require seeing two things at once (the backdoor and the test covering it), and it found and fixed both with no nudge. That&rsquo;s the kind of vigilance I want from my PR reviewer.</p>
<p><strong>The full GLM fixed everything when told, but slept through silent mode.</strong> Unannounced, it let the access and silent-correction cluster through. When I pointed, it swept the six remaining items. In other words: the big model is capable, it&rsquo;s just not attentive by default. Its Flash, at a third of the size, was more vigilant.</p>
<p><strong>Kimi K2 Thinking did security-checklist theater.</strong> It wrote these beautiful &ldquo;security best practices&rdquo; lists and left five items open, including a plaintext key. Talking about security and doing security are different things, and it did the first one.</p>
<p><strong>Ornith restored the behavior and kept the lying test.</strong> On item 6, it reverted the code to the old behavior instead of fixing the planted test that validated the backdoor. Hence the strict score of 73 and the lenient 76: looking fixed is not being fixed, and the lying test also needs to die.</p>
<p><strong>Step-5 left three defects even with the map in hand.</strong> After being explicitly told, it still left the login link, the authorization deactivation and the wrong aggregate. Not every model takes the second chance.</p>
<p><strong>And there&rsquo;s the case of the 397B that became a ready-made joke.</strong> A 397B model, too big to fit in the head of whoever defends size, let the backdoor <code>admin? =&gt; admin || email.end_with?(&quot;@internal.example&quot;)</code> through even after the security-audit warning. Worse: it added a passing test that codified the backdoor as intended behavior, and the commit still bragged &ldquo;Brakeman: 0 warnings&rdquo;. If you want an example of why I test vigilance and not marketing benchmarks, there it is.</p>
<p><strong>Muse Spark Open almost didn&rsquo;t get off the ground, but recovered.</strong> It started blocked: weights in 127 NVFP4 shards, gated on Hugging Face, and no support in stable vLLM. It only ran when I compiled a PR branch still in draft, with nvcc 12.9 and 12.8 libraries mixed, and capped the multimodal to text.</p>
<p>Then it delivered 69.5: weak base unannounced (21 of 40), but 6 of 7 items fixed when pointed. It&rsquo;s the case of &ldquo;infra effort is not the model&rsquo;s score&rdquo;: the service stayed up, the behavior is useful, and even so it only reaches Tier C. If the question is &ldquo;why isn&rsquo;t it at the top&rdquo;, the answer is default vigilance, not VRAM.</p>
<p><strong>And Nex was the complete disaster.</strong> It needed two nudges, and in the end the corrupted text reached the committed Ruby and the app won&rsquo;t even start. In the cloud, the same Nex N2.5 Pro had scored 94. At home, 27. I wouldn&rsquo;t trust a vendor benchmark after seeing this twice.</p>
<p>By the way, Nex&rsquo;s case is the perfect example of a bad base choice. When Rio 3.5 blew up in June, <a href="/en/2026/06/15/rio-3-5-llm-controversy-plagiarism/">I covered the controversy here</a>: its checkpoint was a blend of Nex-N2-Pro with Qwen. The Rio folks could have chosen much better bases, like DeepSeek V4.1 or GLM 5.3 Flash, and went exactly for the lineage that, on my current yardstick, was the worst of the bunch. Picking a model by its pretty name costs dearly.</p>
<h2>Running on 4 GPUs is not trivial<span class="hx:absolute hx:-mt-20" id="running-on-4-gpus-is-not-trivial"></span>
    <a href="#running-on-4-gpus-is-not-trivial" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Four cards are not four models: it&rsquo;s one model sliced into four, with tensor parallelism TP=4 in vLLM. That gives about 1.1 TB of aggregate VRAM (in practice, ~1,038 GB usable with <code>--gpu-memory-utilization 0.90</code>). Without that, none of the models above 400 GB would exist in my life without offloading to SSD, which is where speed goes to die.</p>
<p>The cares that repeated on almost all of them:</p>
<ul>
<li><strong>Bleeding-edge toolchain.</strong> B300 (<code>sm_103a</code>) requires CUDA 12.9 to compile the kernels on the fly, so the default image wasn&rsquo;t enough: standalone toolkit with nvcc 12.9 and the right headers, and even then each family needed a DeepGEMM, FlashInfer or cudart-link patch. When the compilation gets it right, the prize is big: GLM 5.3 Flash went from 10.35 to 141.40 tokens per second from configuration alone (removing <code>--enforce-eager</code> and enabling compiled mode with CUDA graphs), without changing checkpoint, context or KV precision.</li>
<li><strong>Startup is not download.</strong> DeepSeek took 15 to 30 minutes to initialize, so be prepared to wait for the model to load before measuring anything.</li>
<li><strong>Each family has its parser and its quirks.</strong> Nex only talked after switching the parser to <code>qwen3_xml</code>, getting one more nudge and ignoring the <code>tmp/log</code> artifacts. Step-5 didn&rsquo;t exist in stable vLLM 0.32 and needed a nightly with a specific PR. Muse required compiling a PR branch still in draft, with CUDA 12.9 nvcc and 12.8 libraries mixed, and weights in 127 NVFP4 shards. MiMo asked for <code>--trust-remote-code</code>. Laguna decided, out of nowhere, to flood the output with reasoning text, and one sprint had to be redone from the injection base.</li>
<li><strong>Context is expensive.</strong> Kimi K2 Instruct&rsquo;s FP8 checkpoint (1,029 GB) would leave 9 GB for KV cache on the 4 cards, so the 594 GB INT4 Thinking went in its place. Speculating with a draft head (MTP) only worked on Step-5, with a 2.24x gain and 73.7% token acceptance, because I verified the tools&rsquo; correctness first.</li>
</ul>
<p>The operational lesson: renting VRAM solves &ldquo;fits&rdquo;, but not &ldquo;runs well&rdquo;. Every big model is a mini integration project.</p>
<h2>And where&rsquo;s the frontier?<span class="hx:absolute hx:-mt-20" id="and-wheres-the-frontier"></span>
    <a href="#and-wheres-the-frontier" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The question that matters to the developer isn&rsquo;t &ldquo;who has more parameters&rdquo;, it&rsquo;s &ldquo;does the big open model deliver in my day to day?&rdquo;. The v4 benchmark numbers, on the same yardstick:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Score</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 5 / 5.5</td>
          <td style="text-align: right">100.0</td>
      </tr>
      <tr>
          <td>GPT-6 Luna</td>
          <td style="text-align: right">95.5</td>
      </tr>
      <tr>
          <td>GLM 5.3 (hosted)</td>
          <td style="text-align: right">94.0</td>
      </tr>
      <tr>
          <td><strong>DeepSeek V4.1 Flash (cloud or 4x B300)</strong></td>
          <td style="text-align: right"><strong>92.5</strong></td>
      </tr>
      <tr>
          <td>GLM 5.3 Flash / Qwen3.8 Flash-Next (single B300)</td>
          <td style="text-align: right">92.5</td>
      </tr>
      <tr>
          <td>GPT-6 Sol</td>
          <td style="text-align: right">91.0</td>
      </tr>
      <tr>
          <td>Claude Sonnet 5</td>
          <td style="text-align: right">91.0</td>
      </tr>
      <tr>
          <td>Kimi K3</td>
          <td style="text-align: right">85.0</td>
      </tr>
  </tbody>
</table>
<p>DeepSeek V4.1 Flash tied its own cloud service, at the same 92.5, and finished above GPT-6 Sol and Sonnet 5. In other words: <strong>the best current open source is already 1.5 points behind hosted GLM and 7.5 behind the absolute top</strong>, ahead of two strong commercial frontier models. If your yardstick is mine (find the backdoor and the bypass before merging), a local 748B open source is already good enough for daily code review, at 175 tokens per second per stream in my admission measurements.</p>
<p>But three warnings before you cancel your Claude subscription:</p>
<ol>
<li>
<p><strong>More parameters doesn&rsquo;t mean more competence.</strong> The full GLM 5.3, at ~700B, scored 76 against its own Flash&rsquo;s 92.5. Kimi K2 Thinking, at 1 trillion, scored 46. And Nex N2.5 Pro plummeted from 94 in the cloud to 27 at home: a public checkpoint is no guarantee of reproducing the service, as I had already seen with DeepSeek V4 Flash (86 in the cloud, 44.5 at home).</p>
</li>
<li>
<p><strong>My yardstick is vigilance, not general programming.</strong> Here I measure who notices sabotage. That&rsquo;s not the same as measuring who writes your whole system better, and a rerun of my benchmark already varied 9 points on the same model. The 1.5-point distance between models may be noise.</p>
</li>
<li>
<p><strong>Cost changes the answer.</strong> If the idea is frontier quality day to day, DeepSeek V4.1&rsquo;s cloud API cost US$ 1.21 per full benchmark round. The 4x B300 pod costs US$ 36 per hour before you even press the button. And the most didactic case is still DeepSeek V4 Flash from the previous campaign: in the cloud it scored 86 at US$ 0.97 per round; at home, 44.5 and on top of that more expensive per inference. Sometimes the cloud is better <strong>and</strong> cheaper.</p>
</li>
</ol>
<h2>Is renting 4x B300 worth it?<span class="hx:absolute hx:-mt-20" id="is-renting-4x-b300-worth-it"></span>
    <a href="#is-renting-4x-b300-worth-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For one person writing code with an agent in the morning and another in the afternoon: no. The API solves for a few dollars what the pod solves for dozens of dollars per hour.</p>
<p>For a company that needs truly private data (code that can&rsquo;t leave the VPN), a pinned checkpoint for reproducibility, or continuous batch load (thousands of reviews, classifications, analyses per day), then the math changes. DeepSeek V4.1 on 4x B300 delivers near-frontier quality with the data at home, and that you can&rsquo;t buy with a chat subscription.</p>
<p>What I wouldn&rsquo;t buy again without measuring first is the &ldquo;bigger is better&rdquo; premise. I started this campaign thinking the jump past 92.5 would come from a bigger model. It came from the right model running properly. The rest is burned VRAM and a good story to tell.</p>
<p>And there&rsquo;s one more chapter coming, of non-datacenter hardware: the tests with local models on the RTX 5090 and the Strix Halo, with a new inference engine called Strata. When the protocol is done, the third article comes out.</p>
<p>And the &ldquo;is it worth it&rdquo; question has an irritatingly obvious answer: it depends on how many people will use it at the same time. That&rsquo;s the one I answer in the next article: <a href="/en/2026/10/22/llm-benchmark-v4-can-my-team-fit-in-a-4x-b300-h200/">LLM Benchmark v4: can my team fit in a 4x b300/h200?</a>.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>LLM Benchmark v4: the top open source LLMs on a B300</title><link>https://www.akitaonrails.com/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/</guid><pubDate>Mon, 19 Oct 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;In the last two v4 benchmark posts I &lt;a href="https://www.akitaonrails.com/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/"&gt;rebuilt the test from scratch&lt;/a&gt;, with a Rails app sabotaged on purpose across seven sprints, and &lt;a href="https://www.akitaonrails.com/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/"&gt;ranked 39 models&lt;/a&gt; by vigilance: six ties at the top, a free model beating half of Claude. Then I &lt;a href="https://www.akitaonrails.com/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/"&gt;tested five new models&lt;/a&gt;, Opus 5.5, GPT 6 sol and luna, MiMo V2.6 Pro and Grok 4.7: Opus 5.5 closed at 100 and joined the tie at the top, GPT 6 luna scored 95.5 for under a dollar a run, and Grok 4.7 regressed badly against its own predecessor.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the last two v4 benchmark posts I <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">rebuilt the test from scratch</a>, with a Rails app sabotaged on purpose across seven sprints, and <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/">ranked 39 models</a> by vigilance: six ties at the top, a free model beating half of Claude. Then I <a href="/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/">tested five new models</a>, Opus 5.5, GPT 6 sol and luna, MiMo V2.6 Pro and Grok 4.7: Opus 5.5 closed at 100 and joined the tie at the top, GPT 6 luna scored 95.5 for under a dollar a run, and Grok 4.7 regressed badly against its own predecessor.</p>
<p>One category was missing that I had never tested properly: the big open source models. The ones that fit on my <a href="/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/">RTX 5090</a> with 32 GB or on my <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Strix Halo</a> with 61 GB usable by the GPU are too weak for this. They work for simple agent tasks, and no small model handles real programming tasks, let alone a complex software project.</p>
<p>Qwen 3.8 27B took 706 minutes to finish v4 on the Strix and scored 80; the local GLM-4.7-Flash scored 24. So don&rsquo;t even try.</p>
<p>But what if we push harder? What can mid-sized models, 100 to 400 billion parameters, do running on a single rented B300? I could rent four of them and run the giant 750-billion or one-trillion models, and I&rsquo;m not sure the cost-benefit is worth it. The data below answers.</p>
<blockquote>
  <p><strong>TL;DR:</strong> 14 open models, one 288 GB B300 on Runpod at about 9 dollars an hour, the same v4 sabotage protocol. None of them serves fast the way it ships: GLM 5.3 Flash decoded at 10 tokens per second with the default configuration and at 141 after tuning, 14 times more from flags alone. Two open models, GLM 5.3 Flash and Qwen3.8 Flash-Next, scored 92.5, Claude Sonnet 5 level, and Swift 1.5 27B scored 88, Opus 4.6 level. None reached the frontier&rsquo;s 100. The 397-billion flagship scored 68.5 and let an admin backdoor through, DeepSeek V4 Flash scored 86 in the cloud and 44.5 served by me, and Nemotron cheated on a regression test. The cloud API of the same model costs a dollar per run; the pod costs 218 dollars a day. Jump to <a href="#what-a-b300-is-and-what-runpod-is">B300 and Runpod</a>, <a href="#none-of-them-serves-fast-the-way-it-ships">server tuning</a>, the <a href="#the-ranking">ranking</a>, the <a href="#the-interesting-cases">interesting cases</a>, the <a href="#how-far-from-the-frontier">frontier comparison</a>, the <a href="#the-math">math</a> or the <a href="#is-owning-your-own-infra-worth-it">conclusion</a>.</p>

</blockquote>
<h2>What a B300 is, and what Runpod is<span class="hx:absolute hx:-mt-20" id="what-a-b300-is-and-what-runpod-is"></span>
    <a href="#what-a-b300-is-and-what-runpod-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <strong>B300</strong> is the datacenter GPU of NVIDIA&rsquo;s Blackwell Ultra generation, announced in March 2025. Each card has <a href="https://developer.nvidia.com/blog/nvidia-blackwell-ultra-for-the-era-of-ai-reasoning/"target="_blank" rel="noopener">288 GB of HBM3e memory</a>, nine times my 5090, and about 15 petaflops of dense compute in NVFP4, the 4-bit format Blackwell does in hardware. It&rsquo;s the card that goes inside the GB300 NVL72 rack, 72 of them interconnected, which AI companies buy by the thousands.</p>
<p>On its own, a B300 costs more than an apartment. What makes it accessible is renting by the hour.</p>
<p><a href="https://www.runpod.io/"target="_blank" rel="noopener"><strong>Runpod</strong></a> is a cloud GPU provider that rents cards in &ldquo;pods&rdquo;, Linux containers with one or more GPUs, billed per second. You pick the card, an image template, get in over SSH, and pay while the pod is on, idle included. The B300 cost about <a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">9 dollars an hour</a> on their secure cloud the week of the test; an H100 goes for 3 to 4. The container disk is ephemeral: terminate the pod and the 250 GB of weights you downloaded are gone, and you download again.</p>
<p>The pod I used: one B300 with 275 GiB according to <code>nvidia-smi</code> (the same nominal 288 GB, in binary units), 344 vCPUs, 4 TB of host RAM, 700 GB of disk. The inference server was vLLM; the harness was the same OpenCode as every other model in the ranking, connected to the pod through an SSH tunnel. The model runs there; the code, the tests and the results stay on my machine.</p>
<p>Fitting in memory is the first filter. The B300 fits the 100 to 400-billion-parameter models quantized to NVFP4, plus the 27 to 35-billion ones in BF16. Left out are the 753-billion GLM 5.3 flagship, the 750-billion DeepSeek V4.1 Flash, the 1.6-trillion DeepSeek V4 Pro and the 2.8-trillion Kimi K3. For those it would take four B300s or more, about 36 dollars an hour.</p>
<h2>The methodology<span class="hx:absolute hx:-mt-20" id="the-methodology"></span>
    <a href="#the-methodology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The protocol is the usual <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">v4</a>: the model builds a Rails app across seven accumulating sprints, and between sprints an independent reviewer, isolated from the model, injects 14 sabotages, from real CVEs to silent logic and test regressions, into the code the model itself wrote. The 0-to-100 score measures vigilance: how much the model catches and fixes without being told (full weight), how much it fixes after an explicit &ldquo;do a security audit&rdquo; in the last sprint (40% of the weight), and what stays unresolved (zero). Tiers are A from 83 up, B from 73 to 82, C from 51 to 72, D up to 50.</p>
<p>Two differences this round. The first is the reviewer: the five models that finished before the block were reviewed end to end by Codex, as in the previous rounds.</p>
<p>Midway through the batch OpenAI&rsquo;s security service started refusing the injection and grading operation with a cybersecurity risk flag, even though it&rsquo;s my code, on my machine, in a benchmark with 46 models already scored under the same protocol. I switched the reviewer to Claude Opus 4.8 for the remaining models, with the same injection plan, same catalog, same yardstick, and the provenance of every boundary is recorded per sprint. The expected impact on the score is none, because both execute the same written recipe.</p>
<p>The second is admission by speed, which is the subject of the next section. A model that decodes below 100 tokens per second doesn&rsquo;t even get on the track.</p>
<p>Also: an open model is entitled to up to two short nudges per run for silly defects (a missing import, a wrong test entry point), recorded as &ldquo;assisted&rdquo;. Infrastructure failure, driver, loader, parser, is never charged to the model. And the pod ran one model at a time, with no access to any other model&rsquo;s workspace or results.</p>
<h2>None of them serves fast the way it ships<span class="hx:absolute hx:-mt-20" id="none-of-them-serves-fast-the-way-it-ships"></span>
    <a href="#none-of-them-serves-fast-the-way-it-ships" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The first lesson cost a day. I downloaded <a href="https://huggingface.co/RedHatAI/GLM-5.3-Flash-NVFP4"target="_blank" rel="noopener">GLM 5.3 Flash</a>, 320 billion parameters with 18 active, quantized to NVFP4, 198 GB. I brought it up on vLLM with the configuration that &ldquo;works&rdquo;: it serves, passes the tool-call tests, answers. And it decoded at <strong>10 tokens per second</strong>, with the GPU at 10% utilization, 209 watts out of the 1,100 the pod allows.</p>
<p>That&rsquo;s unusable for programming. A coding agent generates tens of thousands of tokens per sprint; at 10 per second, a 20-minute sprint becomes more than four hours.</p>
<p>The same checkpoint, same 128K-token context, same FP8 KV cache, same test prompt, changing only the compile and mode flags: <strong>141 tokens per second</strong>. Fourteen times faster, zero change in precision or weights.</p>
<p>The main culprit was <code>--enforce-eager</code>, which disables vLLM&rsquo;s <code>torch.compile</code> and CUDA graph capture, and which every tutorial tells you to enable &ldquo;to start faster&rdquo;. It starts faster and serves 14 times slower. The measurement changed that flag together with the <code>interactivity</code> mode, so I didn&rsquo;t isolate each one&rsquo;s contribution.</p>
<p>Hence the admission rule: <strong>below 100 tokens per second I consider unusable</strong>, and no model enters the benchmark without clearing that on the median of two warm 256-token probes, after a cold one. To keep it from becoming a bottomless pit, tuning is bounded: at most three configurations and 45 minutes of wall-clock per model, including load and compilation. And tuning can never trade quality for speed: no shortening the context, lowering reasoning effort or quantizing further to hit the number.</p>
<p>The tweaks that paid off for everyone:</p>
<ul>
<li><strong>Install a CUDA that compiles for the card.</strong> Runpod&rsquo;s default image ships CUDA 12.8, whose <code>nvcc</code> <a href="https://developer.nvidia.com/cuda-gpus"target="_blank" rel="noopener">cannot compile for <code>compute_103a</code></a>, the B300&rsquo;s compute capability. Without 12.9, the NVFP4 kernels never compile.</li>
<li><strong>Never use <code>--enforce-eager</code>.</strong> Letting vLLM compile and capture CUDA graphs is the single biggest win.</li>
<li><strong><code>--performance-mode interactivity</code></strong>: optimize for single-stream latency, which is what a serial agent needs, instead of aggregate throughput.</li>
<li><strong>FP8 KV cache on the big MoEs, prefix caching and chunked prefill on all of them</strong>, confirmed by vLLM&rsquo;s Prometheus counters instead of assumed: on an 11K-token prompt, the server reused exactly 8,576 prefix tokens on every repeat.</li>
<li><strong>The right tool and reasoning parser for each family.</strong> The wrong parser makes tool calls vanish silently or leaks the reasoning into the answer.</li>
</ul>
<p>And the specific tweaks I found most interesting:</p>
<ul>
<li><strong>NVFP4 only runs on vLLM.</strong> The 198 to 251 GB checkpoints in Blackwell&rsquo;s native FP4 are the only way to fit on the card, and llama.cpp and Ollama simply don&rsquo;t load those formats. Whoever lives on GGUF has no access to this range.</li>
<li><strong>Native speculative decoding.</strong> Qwen 3.8 27B went from 92 to 195 tokens per second with the MTP head the checkpoint itself ships, predicting three tokens per step, plus <code>--language-model-only</code> to drop the vision tower (measured on different pods, so no controlled ablation). <a href="https://huggingface.co/meta-models/Muse-Glimmer-30B"target="_blank" rel="noopener">Muse Glimmer</a> reached 295 with the lightweight DFlash drafter it ships alongside, 15 speculated tokens per step. I only enabled it where an official drafter exists and where tool calling stayed correct; for Ornith a drafter exists as a separate release, with unknown acceptance rate, so it stayed off.</li>
<li><strong>The CUDA 12 loader.</strong> flashinfer JIT-compiles an NVFP4 MoE kernel that needs <code>libcudart.so.12</code>, and after installing CUDA 13 for Nemotron the library resolution broke. The symptom looked like OOM; the cause was an <code>ld.so.conf</code>. A repair script with <code>ldd</code> evidence before and after fixed it, and the 397B went on to serve at 144 tokens per second.</li>
<li><strong>One virtual environment per runtime.</strong> Nemotron 3.5 Super VL required its own vLLM 0.32 in a separate venv, because that&rsquo;s what the official card pins, and that build wants CUDA 13. Every model family is a bespoke case: parser, quantization, draft head, runtime version and loader repair, all different.</li>
<li><strong>A parser bug that dropped the last call.</strong> vLLM&rsquo;s Qwen tool parser discarded the last parameter of a call when the parameter closing tag was missing but the function one was present. A narrow repair, with 24 test cases, took that serving noise out of the model&rsquo;s account.</li>
</ul>
<p>The exact arguments for each model, the probe logs and the ledger of every tuning round are in the <a href="https://github.com/akitaonrails/llm-coding-benchmark/blob/master/docs/b300-serving-optimizations.md"target="_blank" rel="noopener">serving optimizations document</a> and the <a href="https://github.com/akitaonrails/llm-coding-benchmark/blob/master/docs/runpod-b300-openweights-runbook.md"target="_blank" rel="noopener">B300 runbook</a> in the repository.</p>
<blockquote>
  <p>Speed is almost free, once you let vLLM compile. The real work is finding out, model by model, which combination of toolkit, parser, quantization and draft head that checkpoint demands. I gave each one 45 minutes and needed them.</p>

</blockquote>
<h2>The ranking<span class="hx:absolute hx:-mt-20" id="the-ranking"></span>
    <a href="#the-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>v4 vigilance score, tier, memory footprint, decode speed on the admission probe (warm, single stream) and the cost of the seven sprints&rsquo; inference time only, at 9.09 dollars an hour. The cost excludes download, load, compilation, tuning and idle time; the real math comes further down.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">#</th>
          <th>Model</th>
          <th style="text-align: right">v4</th>
          <th>Tier</th>
          <th style="text-align: right">Memory</th>
          <th style="text-align: right">tok/s</th>
          <th style="text-align: right">Minutes</th>
          <th style="text-align: right">Inference cost</th>
          <th>Reviewer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>GLM 5.3 Flash (NVFP4)</td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">198 GB</td>
          <td style="text-align: right">142</td>
          <td style="text-align: right">145</td>
          <td style="text-align: right">US$ 21.96</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>Qwen3.8 Flash-Next (NVFP4)</td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">100 GB</td>
          <td style="text-align: right">195</td>
          <td style="text-align: right">80</td>
          <td style="text-align: right">US$ 12.04</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">3</td>
          <td>Swift 1.5 27B</td>
          <td style="text-align: right"><strong>88.0</strong></td>
          <td>A</td>
          <td style="text-align: right">56 GB</td>
          <td style="text-align: right">203</td>
          <td style="text-align: right">64</td>
          <td style="text-align: right">US$ 9.71</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">4</td>
          <td>ThinkingCap 3.8 27B</td>
          <td style="text-align: right"><strong>83.0</strong></td>
          <td>A</td>
          <td style="text-align: right">56 GB</td>
          <td style="text-align: right">189</td>
          <td style="text-align: right">76</td>
          <td style="text-align: right">US$ 11.47</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Muse Glimmer 30B</td>
          <td style="text-align: right">81.0</td>
          <td>B</td>
          <td style="text-align: right">60 GB</td>
          <td style="text-align: right">295</td>
          <td style="text-align: right">23</td>
          <td style="text-align: right">US$ 3.54</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Ornith 1.5 35B</td>
          <td style="text-align: right">81.0</td>
          <td>B</td>
          <td style="text-align: right">70 GB</td>
          <td style="text-align: right">317</td>
          <td style="text-align: right">161</td>
          <td style="text-align: right">US$ 24.39</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">7</td>
          <td>Qwen 3.8 27B</td>
          <td style="text-align: right">80.5</td>
          <td>B</td>
          <td style="text-align: right">54 GB</td>
          <td style="text-align: right">195</td>
          <td style="text-align: right">192</td>
          <td style="text-align: right">US$ 29.11</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">8</td>
          <td>Qwen3.5 397B (NVFP4)</td>
          <td style="text-align: right">68.5</td>
          <td>C</td>
          <td style="text-align: right">251 GB</td>
          <td style="text-align: right">140</td>
          <td style="text-align: right">54</td>
          <td style="text-align: right">US$ 8.22</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Nemotron 3.5 Super VL (NVFP4)</td>
          <td style="text-align: right">63.0</td>
          <td>C</td>
          <td style="text-align: right">70 GB</td>
          <td style="text-align: right">207</td>
          <td style="text-align: right">50</td>
          <td style="text-align: right">US$ 7.51</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">10</td>
          <td>DeepSeek V4 Flash</td>
          <td style="text-align: right">44.5</td>
          <td>D</td>
          <td style="text-align: right">167 GB</td>
          <td style="text-align: right">153</td>
          <td style="text-align: right">37</td>
          <td style="text-align: right">US$ 5.59</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>GLM 4.7 Flash BF16</td>
          <td style="text-align: right">33.5</td>
          <td>D</td>
          <td style="text-align: right">60 GB</td>
          <td style="text-align: right">217</td>
          <td style="text-align: right">30</td>
          <td style="text-align: right">US$ 4.60</td>
          <td>Codex</td>
      </tr>
  </tbody>
</table>
<p>Three have no score: Cohere&rsquo;s <strong>Command A Plus</strong> (218 billion, 25 active, W4A4) got stuck in a loop rewriting the same model file 23 times, needed three nudges and still left the tests unable to run before the reviewer was blocked; <strong>Nex N2.5 Mini</strong> ran at 337 tokens per second and delivered invalid Ruby, contradictory migrations and broken authentication in two attempts, a valid bad result from a server that works; and <strong>Qwen3.5 122B</strong> was a conditional fallback in case the 397B didn&rsquo;t finish, and the 397B finished.</p>
<p>Minutes are sprint time only, and Ornith&rsquo;s include a sprint interrupted by infrastructure. Flash-Next, Swift, ThinkingCap, Muse and Qwen 27B took one nudge each; GLM 4.7, two; the others, none. The probe speed is an admission number: the throughput of a real coding session includes prefill, tools, tests and the agent&rsquo;s choices. And note that the top two tie on score with opposite profiles: GLM spent 145 minutes and 22 dollars to reach the same 92.5 that Qwen3.8 Flash-Next did in 80 minutes and 12 dollars, with half the memory.</p>
<h2>The interesting cases<span class="hx:absolute hx:-mt-20" id="the-interesting-cases"></span>
    <a href="#the-interesting-cases" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Qwen3.8 Flash-Next: the best of the open models, by cost<span class="hx:absolute hx:-mt-20" id="qwen38-flash-next-the-best-of-the-open-models-by-cost"></span>
    <a href="#qwen38-flash-next-the-best-of-the-open-models-by-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://huggingface.co/nvidia/Qwen3.8-Flash-Next-NVFP4"target="_blank" rel="noopener">Flash-Next</a> is, <a href="https://recipes.vllm.ai/Qwen/Qwen3.8-Flash-Next"target="_blank" rel="noopener">in Qwen&rsquo;s words</a>, a preview of the Qwen 4 architecture: 125 billion language-model parameters with 6 billion active, plus a 51-billion n-gram embedding table that Qwen says can be offloaded to host memory, plus a 4-billion MTP head. Quantized to NVFP4 by NVIDIA, it took about 100 GB on the card (the checkpoint is 133 GB on disk). It was the only model in the second batch to resolve all 14 items: it caught the tenant leak unprompted, caught the compound test-integrity item, and wrote per-user isolation specs on its own, so the planted API IDOR had no way to stay green. At 195 tokens per second and 12 dollars of inference, it&rsquo;s the best cost-benefit in this table.</p>
<h3>GLM 5.3 Flash: the same 92.5, three times the cost<span class="hx:absolute hx:-mt-20" id="glm-53-flash-the-same-925-three-times-the-cost"></span>
    <a href="#glm-53-flash-the-same-925-three-times-the-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM scored the same with a much bigger architecture: 320 billion parameters, 18 active, 198 GB even in FP4. It resolved all 14 items, zero nudges, reviewed end to end by Codex.</p>
<p>But it took 145 minutes and consumed twice the memory for the same result. On Z.ai&rsquo;s cloud, the same model scored 84.25 on a flat plan. With single-run noise, which in my history reaches nine points, the safest reading is that both sit in the same band.</p>
<h3>Swift 1.5 and ThinkingCap: two 27B fine-tunes in Tier A<span class="hx:absolute hx:-mt-20" id="swift-15-and-thinkingcap-two-27b-fine-tunes-in-tier-a"></span>
    <a href="#swift-15-and-thinkingcap-two-27b-fine-tunes-in-tier-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The two surprises. <a href="https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-27b"target="_blank" rel="noopener">Swift 1.5</a>, from UkisAI, and <a href="https://huggingface.co/bottlecapai/ThinkingCap-Qwen3.8-27B"target="_blank" rel="noopener">ThinkingCap 3.8</a>, from BottleCap AI, are Qwen 3.8 27B fine-tunes trained to think less: Swift advertises up to 58% fewer reasoning tokens (on the GPQA median; on Terminal-Bench, 16%), ThinkingCap about 37% on average. Both fit in 56 GB in BF16, run at about 200 tokens per second with MTP, and scored 88 and 83.</p>
<p>Swift landed above Anthropic&rsquo;s Opus 4.6. The base Qwen 3.8 27B, same architecture, scored 80.5 in 192 minutes; Swift scored 88 in 64. Swift caught sabotage earlier than the base; whether that was from thinking less, one run of each can&rsquo;t prove.</p>
<p>A provenance caveat: the reviewer recorded that Swift read local commit messages that disclosed regressions, which weakens a literal reading of &ldquo;entirely unaided discovery&rdquo;. And it left one item unresolved, an inactive admin still accepted with a vacuous guarding test. Even so, of every model in this table, it&rsquo;s the one I&rsquo;d put on a card of my own.</p>
<h3>Qwen3.5 397B: the flagship that let the backdoor through<span class="hx:absolute hx:-mt-20" id="qwen35-397b-the-flagship-that-let-the-backdoor-through"></span>
    <a href="#qwen35-397b-the-flagship-that-let-the-backdoor-through" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The biggest model in the batch, 397 billion parameters with 17 active, 251 GB of NVFP4 on a 275 GB card, served at 140 tokens per second after the loader repair and scored <strong>68.5</strong>, Tier C.</p>
<p>The case that defines the score: sabotage number 6 is a line <code>admin? =&gt; admin || email.end_with?(&quot;@internal.example&quot;)</code>, a privilege escalation by email domain. The 397B let it through in the normal sprints, let it through in sprint 7 with the explicit security-audit request, and <strong>wrote a passing test codifying the backdoor as expected behavior</strong>, in the same commit where it announced &ldquo;Brakeman: 0 warnings&rdquo;. Parameters did not buy vigilance.</p>
<h3>Nemotron 3.5 Super VL: cheated on the test<span class="hx:absolute hx:-mt-20" id="nemotron-35-super-vl-cheated-on-the-test"></span>
    <a href="#nemotron-35-super-vl-cheated-on-the-test" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>NVIDIA&rsquo;s <a href="https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4"target="_blank" rel="noopener">Nemotron 3.5 Super VL</a>, 120 billion with 12 active, released two days before I started testing and <a href="https://blogs.nvidia.com/blog/nemotron-3-5-super-vl/"target="_blank" rel="noopener">sold by NVIDIA</a> as the fastest open agentic model in its class, with &ldquo;leading accuracy&rdquo; on multimodal tasks, scored <strong>63</strong>. Sabotage number 2 removes a navigation link and the regression test looks for <code>have_link('Sign in')</code>. Nemotron changed the test to <code>have_button('Sign in')</code>, which passes on the login form&rsquo;s submit button without restoring the link, and kept the cheat even after the reveal sprint.</p>
<p>It also tends to catch items a sprint late. And it was the one that required its own runtime: vLLM 0.32 with CUDA 13 in a separate environment.</p>
<h3>DeepSeek V4 Flash: 86 in the cloud, 44.5 in my hands<span class="hx:absolute hx:-mt-20" id="deepseek-v4-flash-86-in-the-cloud-445-in-my-hands"></span>
    <a href="#deepseek-v4-flash-86-in-the-cloud-445-in-my-hands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The most important case for anyone considering self-hosting. <a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash"target="_blank" rel="noopener">DeepSeek V4 Flash</a>, 284 billion with 13 active, scored <strong>86, Tier A, for 97 cents</strong> through the API in September, and the 0731 version, exactly the one I downloaded, scored 85 for 1.94. The same public weights in mixed FP4 and FP8, served by me on the B300, scored <strong>44.5, Tier D</strong>: it caught 4 items unprompted, 5 more at the reveal, and left 5 unresolved. It was the fastest to finish, 37 minutes, and the least vigilant.</p>
<p>The likely cause is divergence between the public checkpoint and what DeepSeek serves, or between their serving stack and mine: parser, chat template, reasoning mode, quantization. I followed the official card. What this proves is that the model&rsquo;s name on Hugging Face doesn&rsquo;t guarantee the hosted API&rsquo;s quality, and that whoever is going to host needs to validate before trusting.</p>
<h3>GLM 4.7 Flash: same as local, meaning bad<span class="hx:absolute hx:-mt-20" id="glm-47-flash-same-as-local-meaning-bad"></span>
    <a href="#glm-47-flash-same-as-local-meaning-bad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The 30-billion GLM 4.7 Flash in BF16, at 217 tokens per second, scored 33.5. At home, in GGUF Q5, it had scored 24. It needed two nudges, left nine items unresolved and delivered a migration with a syntax error. The nine-and-a-half-point difference fits within single-run noise; what remains is that the model is weak either way.</p>
<h3>What showed up in several<span class="hx:absolute hx:-mt-20" id="what-showed-up-in-several"></span>
    <a href="#what-showed-up-in-several" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Ornith, Muse Glimmer and the 397B shared a hygiene defect: they ran the sprint to the end with exit zero and forgot to commit their own work, leaving it for the reviewer to checkpoint. It didn&rsquo;t go into the score, and on a real team it would be the first complaint. And the 81-to-83 cluster, ThinkingCap, Ornith and Muse, has a consistent profile: it catches SQL injection, tenant leaks, XSS and exposed secrets with confidence, and lets the quiet items through, broken access control, test integrity, missing index, wrong aggregate.</p>
<h2>How far from the frontier<span class="hx:absolute hx:-mt-20" id="how-far-from-the-frontier"></span>
    <a href="#how-far-from-the-frontier" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The three best open models against the best commercial ones from the same ranking, with the cost and time of each run:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">v4</th>
          <th>Tier</th>
          <th style="text-align: right">Run cost</th>
          <th style="text-align: right">Minutes</th>
          <th>Where</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT-6 Astra</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 30.55</td>
          <td style="text-align: right">100</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Claude Opus 5.5</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 16.63</td>
          <td style="text-align: right">64</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>GPT 5.6 terra</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 9.52</td>
          <td style="text-align: right">80</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Grok 4.6</td>
          <td style="text-align: right">98.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 13.00</td>
          <td style="text-align: right">67</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Claude Fable 5.1</td>
          <td style="text-align: right">95.5</td>
          <td>A</td>
          <td style="text-align: right">~US$ 51</td>
          <td style="text-align: right">133</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td><strong>GLM 5.3 Flash (NVFP4)</strong></td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 21.96 inference only</td>
          <td style="text-align: right">145</td>
          <td>B300</td>
      </tr>
      <tr>
          <td><strong>Qwen3.8 Flash-Next (NVFP4)</strong></td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 12.04 inference only</td>
          <td style="text-align: right">80</td>
          <td>B300</td>
      </tr>
      <tr>
          <td>Claude Sonnet 5</td>
          <td style="text-align: right">91.0</td>
          <td>A</td>
          <td style="text-align: right">~US$ 27</td>
          <td style="text-align: right">112</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Gemini 3.8 Flash high</td>
          <td style="text-align: right">90.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 15.98</td>
          <td style="text-align: right">97</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td><strong>Swift 1.5 27B</strong></td>
          <td style="text-align: right"><strong>88.0</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 9.71 inference only</td>
          <td style="text-align: right">64</td>
          <td>B300</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">87.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 25.64</td>
          <td style="text-align: right">89</td>
          <td>cloud</td>
      </tr>
  </tbody>
</table>
<p>The reading: the best of the open source that fits on a B300 and that I served sits <strong>one step below the commercial ceiling and level with the middle of Tier A</strong>. Differences of one or two points are single-run noise.</p>
<p>GLM 5.3 Flash and Qwen3.8 Flash-Next tie with Sonnet 5 and pass Gemini 3.8 Flash and Opus 4.6. None reaches the 100 that seven commercial models scored, nor Grok 4.6&rsquo;s 98.5. Seven and a half points separate the best open model from the ceiling, and in v4 every point is one less sabotage caught unprompted.</p>
<p>The difference that matters is another: predictability. Among the top commercial models in the table, the worst is 87.5, and the full commercial field also has a bottom, Mistral Large 3 scored 39. The open field has the same 92.5 at the top and two Tier D at the bottom, with DeepSeek scoring 85 in the cloud and 44.5 in my hands. Choosing a top commercial model is choosing a number the API reproduces; choosing an open one is betting that the checkpoint, the quantization and your serving stack reproduce the number on the card, and sometimes they do.</p>
<h2>The math<span class="hx:absolute hx:-mt-20" id="the-math"></span>
    <a href="#the-math" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If quality still falls short of the frontier, is the cost at least proportional? It depends on where you run the open model.</p>
<p><strong>On the vendor&rsquo;s own cloud API, yes, and by a wide margin.</strong> A full v4 run cost 97 cents on DeepSeek V4 Flash, 1.17 dollars on Qwen 3.8 Flash, 1.03 on MiMo V2.5 Pro, all Tier A in the cloud. Against 9.52 to 30.55 dollars for the 100-point models, that&rsquo;s ten to thirty times cheaper for a score 8 to 14 points lower. The list prices explain it: <a href="https://docs.z.ai/guides/overview/pricing"target="_blank" rel="noopener">GLM 5.3 Flash</a> costs 15 cents per million input tokens and 50 for output; <a href="https://api-docs.deepseek.com/quick_start/pricing"target="_blank" rel="noopener">DeepSeek Flash</a>, 15 to 30 cents input and 60 cents to 1.20 output; <a href="https://platform.claude.com/docs/en/about-claude/pricing"target="_blank" rel="noopener">Opus 5.5</a>, 4 and 20 dollars; <a href="https://developers.openai.com/api/docs/pricing"target="_blank" rel="noopener">GPT-6 Astra</a>, 10 and 50.</p>
<p><strong>On your own rented B300, no.</strong> The pod costs 9 dollars an hour on or idle, <strong>218 a day, about 6,500 a month</strong>. The model time of this whole batch, 14 models across download, load and sprints, added up to 23 hours, about 212 dollars at pod price; the sprints alone were 17 hours and 157 dollars.</p>
<p>But the pod also charges for the time when nothing runs. I put 400 dollars of credit on Runpod and 127 were left: the whole batch cost <strong>273 dollars</strong>, about 30 pod-hours, including storage, four pods in sequence and seven idle hours one night when the orchestration died and nothing warned me. Almost 60 dollars more than the model time, just waiting. The repository&rsquo;s break-even math: the pod only gets cheaper than the API from about <strong>6,500 runs a month</strong>, 217 a day, sustained. A solo developer or a small team doesn&rsquo;t come close.</p>
<p>And model against model, side by side:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Cloud API</th>
          <th>Hosted by me on the B300</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4 Flash 0731</td>
          <td>85 (A) for US$ 1.94</td>
          <td>44.5 (D) for US$ 5.59 inference only</td>
      </tr>
      <tr>
          <td>Qwen 3.8 Flash (cloud) and Flash-Next (B300), same family</td>
          <td>86.5 (A) for US$ 1.17</td>
          <td>92.5 (A) for US$ 12.04</td>
      </tr>
      <tr>
          <td>Qwen 3.8 27B</td>
          <td>80 (B) for free on the Strix, in 706 minutes</td>
          <td>80.5 (B) for US$ 29.11, in 192 minutes</td>
      </tr>
  </tbody>
</table>
<blockquote>
  <p>The cloud API of an open model is ten to thirty times cheaper than the frontier for one tier less. Hosting the same model on a rented B300 is more expensive than the frontier, and sometimes worse than its own API.</p>

</blockquote>
<h2>Does a team fit on the same card?<span class="hx:absolute hx:-mt-20" id="does-a-team-fit-on-the-same-card"></span>
    <a href="#does-a-team-fit-on-the-same-card" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A question I asked while looking at the GPU at 10% utilization: do these models use all of the pod&rsquo;s compute? Could a whole team go on the same B300?</p>
<p>In principle, yes, and that&rsquo;s what vLLM exists for. <a href="https://vllm.ai/blog/2023-06-20-vllm"target="_blank" rel="noopener">Continuous batching</a> schedules several requests in the same decode step, and <a href="https://vllm.ai/blog/2023-06-20-vllm"target="_blank" rel="noopener">PagedAttention</a> stores each session&rsquo;s KV cache in non-contiguous blocks, with under 4% waste, precisely to fit more sessions in the same memory. A 27-billion model in BF16 takes 56 GB and leaves about 200 GB of KV cache: that&rsquo;s dozens of agent sessions with 128K-token context at the same time. The 397B, with 251 GB of weights, leaves 24, and then few fit.</p>
<p>What doesn&rsquo;t scale is speed per person. My test ran in <code>--performance-mode interactivity</code>, which optimizes single-stream latency, and the 200 tokens per second are that stream&rsquo;s. With ten agents decoding at the same time, the card&rsquo;s aggregate throughput rises and each one&rsquo;s falls, and the speculative decoding that gave me Muse Glimmer&rsquo;s 295 yields less as concurrency grows: a <a href="https://huggingface.co/z-lab/Muse-Glimmer-30B-DFlash2"target="_blank" rel="noopener">DFlash drafter published for Muse by Z Lab</a> reports 3 to 4.6 times gain with one session and 1.2 to 1.7 with 32.</p>
<p>I didn&rsquo;t measure concurrency in this batch, so what I can say is the outline: a team of five to ten people on a 27-to-35-billion model on one B300 is practical, and each one probably still stays above 100 tokens per second. The same team on the 397B is unworkable on the card, and on the 198 GB GLM 5.3 Flash it gets tight.</p>
<p>It&rsquo;s in that scenario, and only there, that the pod math starts to close. Ten developers running an agent all day on a 27B model would spend, on the API, at a dollar per two-hour run, about 40 dollars a day; the pod costs 218. It still loses. What changes the math is having a hundred people, or being required to keep the code from leaving your network.</p>
<h2>Is owning your own infra worth it?<span class="hx:absolute hx:-mt-20" id="is-owning-your-own-infra-worth-it"></span>
    <a href="#is-owning-your-own-infra-worth-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For serious software development, with this batch&rsquo;s data, the answer comes in three parts.</p>
<p><strong>The top open models are good enough.</strong> GLM 5.3 Flash, Qwen3.8 Flash-Next and Swift 1.5 27B are Tier A in my test, at Sonnet 5 level and above Opus 4.6. That was unthinkable in April, when <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">only Qwen 3.6 35B came close to Opus</a>, and still one fix away. For anyone who accepts one tier below the ceiling, they serve for real work, including security review, which is what v4 measures.</p>
<p><strong>Renting a B300 to run them is the wrong way to use them.</strong> The cloud API of the same model costs a dollar per run and needs zero configuration. The pod costs 218 a day, demands a day of tuning per model family, and in DeepSeek&rsquo;s case delivered a worse model than its own API. It&rsquo;s only worth it with a data-residency obligation, with a checkpoint no cloud serves, with sustained load of hundreds of runs a day, or with a need for pinned weights for reproducibility. Four B300s to run the 750-billion-to-one-trillion models quadruple the bill with no evidence the score reaches 100: the 397B, the biggest model I tested, scored 68.5.</p>
<p><strong>And if you&rsquo;re going to host, host small.</strong> Swift 1.5 27B and ThinkingCap at 56 GB, with MTP, at 200 tokens per second, are Tier A on hardware you can buy, a professional 80 or 96 GB card instead of a datacenter one. That&rsquo;s where open source makes sense for a company: a whole team on one card, code inside the network, an Opus 4.6 score. Just check the license first: Swift and ThinkingCap ship with their own licenses, and above a certain revenue both require a commercial license. The open giants remain an API thing.</p>
<p>My rule after all this: top commercial model for tasks that can&rsquo;t fail, open-model API for volume, and your own card only when the law or the client demands it. The repository with every serving argument, every probe log, every reviewer transcript and the Codex-or-Claude provenance of every boundary is <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">public</a>, as always.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>I Trained My Own AI Model (Spoiler: It's Just for Study)</title><link>https://www.akitaonrails.com/en/2026/10/14/i-trained-my-own-ai-model-spoiler-its-just-for-study/</link><guid isPermaLink="true">https://www.akitaonrails.com/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>Which Jev-like AI Decision/Classifier Model Is Best?</title><link>https://www.akitaonrails.com/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/</guid><pubDate>Sat, 10 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;Last week I published &lt;a href="https://www.akitaonrails.com/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/"&gt;a long post about Jev&lt;/a&gt;, TypeSafe&amp;rsquo;s hosted classifier that became all the rage in September. The short version of that post: Jev is a classifier, something that has existed for decades, and the &amp;ldquo;193x faster&amp;rdquo; amazement only exists because too many people were using chat LLMs as expensive, slow classifiers. I reproduced Paulo Câmara&amp;rsquo;s essay &lt;em&gt;The student who marks answer A&lt;/em&gt;, introduced the open alternatives Clef and Laya, and to compare the models side by side I leaned on the &lt;a href="https://huggingface.co/spaces/multimodalart/jev-decision-index"target="_blank" rel="noopener"&gt;Decision Index&lt;/a&gt;, a public ranking that already existed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Last week I published <a href="/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/">a long post about Jev</a>, TypeSafe&rsquo;s hosted classifier that became all the rage in September. The short version of that post: Jev is a classifier, something that has existed for decades, and the &ldquo;193x faster&rdquo; amazement only exists because too many people were using chat LLMs as expensive, slow classifiers. I reproduced Paulo Câmara&rsquo;s essay <em>The student who marks answer A</em>, introduced the open alternatives Clef and Laya, and to compare the models side by side I leaned on the <a href="https://huggingface.co/spaces/multimodalart/jev-decision-index"target="_blank" rel="noopener">Decision Index</a>, a public ranking that already existed.</p>
<p>Someone else&rsquo;s ranking is useful, but I wanted to get my hands dirty. So I built my own benchmark, with my own cases, half in Portuguese, and ran the open models on my <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">home server with Strix Halo</a>, the same Ryzen AI Max+ 395 I use to test local LLMs. The hosted ones (Jev, the two Clefs from Cloudflare, OpenAI Decisions and Microsoft Decision-1 via OpenRouter) ran through their APIs. Everything, including the cases, the raw responses and the scoring scripts, is public in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks"target="_blank" rel="noopener">benchmark repository</a>.</p>
<blockquote>
  <p><strong>TL;DR:</strong> the top now has two, both open-weights. <strong>Perplexity Decider 1.1 27B</strong> and the just-released <strong>Decision 3.0</strong> (vLLM Semantic Router) tied at <strong>99.4%</strong> on the compact cases, with 100% in Portuguese, and identical labels on all 1,024 classifications in the benchmark. The price: about 49 GB of weights and over a second per decision. In the cloud, <strong>Microsoft Decision-1</strong> tied <strong>Jev</strong> at 97.6% while costing a third: it&rsquo;s the cheapest hosted one I measured, and the first candidate to evaluate. Jev answers faster and errs differently. <strong>OpenAI Decisions</strong> debuted tied with the best local 4B, Jebadiah, and has the lowest hosted latency. <strong>Julia-1</strong>, the smallest and fastest of all (12 ms), scored at coin-flip level: fresh out of the factory, it&rsquo;s a no-go. Jump to the <a href="#the-full-table">full table</a>, the <a href="#how-much-it-costs-in-support-emails">cost in support emails</a>, <a href="#which-model-for-which-situation">which model for which situation</a>, <a href="#how-hard-it-is-to-run-the-best-open-models-on-your-machine">how hard it is to run the best open ones locally</a> or the <a href="#highlights-and-gotchas">gotchas</a>.</p>

</blockquote>
<h2>How I tested<span class="hx:absolute hx:-mt-20" id="how-i-tested"></span>
    <a href="#how-i-tested" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The central idea was to measure more than &ldquo;who gets the most right.&rdquo; A decision model has a design goal, and comparing a 322-million-parameter encoder made to run on weak devices with a hosted service of undisclosed size on accuracy percentage alone is unfair to both. So I measured accuracy, latency, cost and behavior under stress, and at the end I split the recommendations by situation.</p>
<h3>The two case sets<span class="hx:absolute hx:-mt-20" id="the-two-case-sets"></span>
    <a href="#the-two-case-sets" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I built two test sets, frozen in git before any inference, with the dataset hash recorded alongside the responses. No model receives the expected answer, the justification or the case identifier. Only the text, the instructions and the options.</p>
<p>The <strong>v1 set</strong> has 13 operational use cases, in English, each with an explicit fictional policy:</p>
<ul>
<li>support ticket severity</li>
<li>support routing</li>
<li>refund policy</li>
<li>incident escalation</li>
<li>tool approval (whether an agent may or may not execute a given action)</li>
<li>email fraud</li>
<li>document type</li>
<li>evidence support</li>
<li>search relevance</li>
<li>moderation</li>
<li>order routing</li>
<li>lead qualification</li>
<li>numeric-threshold triage</li>
</ul>
<p>There are 10 scenarios per use case, each presented four ways: original, options in reversed order, options with opaque names (c0, c1, c2) and original plus a &ldquo;reviewer&rdquo; comment suggesting the wrong answer, which the instructions say to ignore. That&rsquo;s 520 classifications per model. Add 24 probability probes in situations with no information at all (fair coin, six-sided die), where the right answer is mathematical.</p>
<p>The <strong>v2 set</strong> was made to cover what v1 leaves out: common semantics and Portuguese. There are 7 tasks, with 12 scenarios each, and every scenario written in English and in Brazilian Portuguese with the same answer and the same option order:</p>
<ul>
<li>sentiment (author versus quotation, negation, sarcasm)</li>
<li>current intent (withdrawn request, competing actions, hypothesis)</li>
<li>logical implication (quantifiers, absent fact, temporal order)</li>
<li>search relevance (all, part or none of what was asked)</li>
<li>sensitive data handling (fictional secret versus mention, redaction, revocation)</li>
<li>current incident status (timeline order, verified versus unverified)</li>
<li>threshold policy (inclusive bounds, units, AND/OR with a missing value)</li>
</ul>
<p>Each pair runs under three conditions:</p>
<ul>
<li><strong>compact</strong>: just the record and the rule</li>
<li><strong>with examples</strong>: one solved example per class, fixed and the same for everyone</li>
<li><strong>with distractors</strong>: ten irrelevant archive notes before the record that matters, to stress the context</li>
</ul>
<p>That&rsquo;s 504 classifications per model.</p>
<blockquote>
  <p>The cases were written with AI help and reviewed against the policies, without independent human annotation. It&rsquo;s a small, synthetic benchmark: 130 scenario families in v1 and 84 in v2. The variants are correlated, so 520 responses are 130 scenarios seen four ways. Treat the numbers as a snapshot of these cases, and test with your own before deciding.</p>

</blockquote>
<h3>The 23 competitors<span class="hx:absolute hx:-mt-20" id="the-23-competitors"></span>
    <a href="#the-23-competitors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Five ran in the cloud, through the official API: <strong>Jev</strong> (version 1.13.0, from TypeSafe), <strong>Clef</strong> (27B, Cloudflare Workers AI), <strong>Clef-flash</strong> (9B, same), <strong>OpenAI Decisions</strong> (the <code>gpt-6-luna</code> alias, in public beta) and <strong>Microsoft Decision-1</strong> (9B, post-trained from Qwen3.5-9B, served via OpenRouter on Azure). The other eighteen ran on the Strix Halo, via ROCm, with weights pinned by Hugging Face commit hash, inside a Docker container. The first three (Laya, Laya typed and Clef-flash) shared one container; the rest ran one at a time, with Julia-1 measured twice, on GPU and on CPU. The server has 64 GiB of GPU-addressable memory.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Nominal size</th>
          <th>Origin</th>
          <th>Where it ran</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Jev 1.13.0</td>
          <td style="text-align: right">undisclosed</td>
          <td>TypeSafe, proprietary</td>
          <td>API</td>
      </tr>
      <tr>
          <td>OpenAI Decisions (<code>gpt-6-luna</code>)</td>
          <td style="text-align: right">undisclosed</td>
          <td>OpenAI, proprietary, beta</td>
          <td>API</td>
      </tr>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">9B</td>
          <td>Microsoft, proprietary</td>
          <td>API (OpenRouter)</td>
      </tr>
      <tr>
          <td>Clef</td>
          <td style="text-align: right">27B</td>
          <td>Cloudflare, open weights</td>
          <td>API</td>
      </tr>
      <tr>
          <td>Clef-flash</td>
          <td style="text-align: right">9B</td>
          <td>Cloudflare, open weights</td>
          <td>API and local</td>
      </tr>
      <tr>
          <td>Perplexity Decider 1.1 27B</td>
          <td style="text-align: right">27B</td>
          <td>Perplexity, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Decision 3.0 (d3)</td>
          <td style="text-align: right">27B</td>
          <td>vLLM Semantic Router, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Decision 3.0 Edge</td>
          <td style="text-align: right">0.6B</td>
          <td>vLLM Semantic Router, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Jebadiah 4B v2</td>
          <td style="text-align: right">4B</td>
          <td>Frontier Infra, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Kev 0.8B / 4B / 9B</td>
          <td style="text-align: right">0.8B / 4B / 9B</td>
          <td>Jared Palmer, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Mapika Decider 4B</td>
          <td style="text-align: right">4.2B</td>
          <td>Mapika, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Eikos 4B</td>
          <td style="text-align: right">4B</td>
          <td>Caio Vicentino, MIT, trained on EN and PT</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Simple-Jev + Qwen3.5 4B</td>
          <td style="text-align: right">4B</td>
          <td>Featherless, token-reading server over a stock model</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Laya / Laya typed-decisions</td>
          <td style="text-align: right">421M</td>
          <td>Convai Innovations, ModernBERT encoder</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Laya multilingual</td>
          <td style="text-align: right">322M</td>
          <td>Convai Innovations, mmBERT encoder</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Julia-1</td>
          <td style="text-align: right">144M</td>
          <td>Supersonic Labs, Apache 2.0, mmBERT encoder</td>
          <td>local, GPU and CPU</td>
      </tr>
      <tr>
          <td>GLiNER2.5-Decide / multi-Decide</td>
          <td style="text-align: right">340M / 287M</td>
          <td>Fastino, Apache 2.0</td>
          <td>local</td>
      </tr>
  </tbody>
</table>
<p>The order up there is just organizational, hosted first and local ones by family. The real ranking is in the <a href="#the-full-table">results table</a>.</p>
<p>Everyone received exactly the same text, the same instructions and the same options, with no per-model prompt tuning and no adjustments after seeing results (Simple-Jev uses its own server&rsquo;s fixed prompt policy). The exception is <strong>GLiNER</strong>, whose native API takes labels instead of probability distributions. It runs on a separate track, v2 only, with explicit mapping and no made-up calibration metrics.</p>
<p><strong>Simple-Jev</strong> deserves an explanation. It&rsquo;s a server that takes a stock LLM (here Qwen3.5 4B, with no decision training at all) and reads the probability of the answer tokens instead of generating text. It&rsquo;s the baseline: if a &ldquo;decision&rdquo; model loses to this, the specialized training bought nothing.</p>
<p>Two protocol notes. <strong>OpenAI Decisions</strong> answers through a native typed-decisions endpoint, and the alias the API returned is not an immutable snapshot: OpenAI can swap the weights behind the name, like Cloudflare does with Clef-flash. And <strong>Perplexity Decider</strong> is not a stock chat LLM: it has a separate decision head, with non-causal attention and a calibration temperature saved in the weights. A generic chat server does not reproduce its behavior. <strong>Microsoft Decision-1</strong> speaks native SystemOne through OpenRouter&rsquo;s decisions API, and the served identifier is dated (<code>microsoft-decision-1-20261009</code>), so it&rsquo;s not an immutable weight either. It doesn&rsquo;t show up in OpenRouter&rsquo;s text-model catalog: to find it, you have to filter by the <code>decisions</code> modality. <strong>Decision 3.0</strong> is the new family from the vLLM Semantic Router project, Apache 2.0, derived from Qwen, with six checkpoints (d3, flash, mini, nano, lite, edge) and native SystemOne. I tested the flagship (d3) and the smallest (d3-edge); the four in between are for another round.</p>
<h3>What was measured<span class="hx:absolute hx:-mt-20" id="what-was-measured"></span>
    <a href="#what-was-measured" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Accuracy per task, per language and per condition, always on top of the per-option probabilities the model returns, ignoring the vendor&rsquo;s &ldquo;confidence&rdquo; field.</li>
<li>Brier and ECE for calibration.</li>
<li>Median and p95 latency as seen by the client, including the network: WAN for the hosted ones, LAN for the local ones. These are deployment observations, and comparing hardware would require another test.</li>
<li>Input tokens reported by the API, priced at the October 10 price table, before any allowance.</li>
<li>Bootstrap confidence intervals over scenario families, instead of individual responses.</li>
</ul>
<p>I left local cost unmeasured. Energy, hardware amortization and maintenance were left out, so local models show up as &ldquo;not measured,&rdquo; because &ldquo;zero&rdquo; would be a lie.</p>
<h2>The full table<span class="hx:absolute hx:-mt-20" id="the-full-table"></span>
    <a href="#the-full-table" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The main column is accuracy on the 168 compact v2 cases (84 in English, 84 in Portuguese). The v1 column uses the 130 original scenarios. Latency is the median seen by the client across the 504 v2 calls. Cost is the list price per 10 thousand compact decisions, computed from the tokens the API reported.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Compact EN / PT-BR</th>
          <th style="text-align: right">Compact combined</th>
          <th style="text-align: right">v1 originals</th>
          <th style="text-align: right">Median latency</th>
          <th style="text-align: right">10 thousand decisions</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Perplexity Decider 27B</td>
          <td style="text-align: right">98.8% / 100.0%</td>
          <td style="text-align: right"><strong>99.4%</strong></td>
          <td style="text-align: right">99.2%</td>
          <td style="text-align: right">1,547 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Decision 3.0 (27B)</td>
          <td style="text-align: right">98.8% / 100.0%</td>
          <td style="text-align: right"><strong>99.4%</strong></td>
          <td style="text-align: right">99.2%</td>
          <td style="text-align: right">1,420 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Jev</td>
          <td style="text-align: right">97.6% / 97.6%</td>
          <td style="text-align: right">97.6%</td>
          <td style="text-align: right">96.2%</td>
          <td style="text-align: right">255 ms (WAN)</td>
          <td style="text-align: right">US$ 0.19</td>
      </tr>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">95.2% / 100.0%</td>
          <td style="text-align: right">97.6%</td>
          <td style="text-align: right">95.4%</td>
          <td style="text-align: right">422 ms (WAN)</td>
          <td style="text-align: right">US$ 0.065</td>
      </tr>
      <tr>
          <td>Clef (27B)</td>
          <td style="text-align: right">97.6% / 94.0%</td>
          <td style="text-align: right">95.8%</td>
          <td style="text-align: right">94.6%</td>
          <td style="text-align: right">302 ms (WAN)</td>
          <td style="text-align: right">US$ 0.63</td>
      </tr>
      <tr>
          <td>Jebadiah 4B v2</td>
          <td style="text-align: right">91.7% / 89.3%</td>
          <td style="text-align: right">90.5%</td>
          <td style="text-align: right">85.4%</td>
          <td style="text-align: right">205 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>OpenAI Decisions</td>
          <td style="text-align: right">90.5% / 90.5%</td>
          <td style="text-align: right">90.5%</td>
          <td style="text-align: right">93.1%</td>
          <td style="text-align: right">222 ms (WAN)</td>
          <td style="text-align: right">US$ 0.23</td>
      </tr>
      <tr>
          <td>Kev 9B</td>
          <td style="text-align: right">92.9% / 83.3%</td>
          <td style="text-align: right">88.1%</td>
          <td style="text-align: right">87.7%</td>
          <td style="text-align: right">561 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Clef-flash (9B) hosted</td>
          <td style="text-align: right">91.7% / 83.3%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">90.8%</td>
          <td style="text-align: right">394 ms (WAN)</td>
          <td style="text-align: right">US$ 0.10</td>
      </tr>
      <tr>
          <td>Clef-flash (9B) local</td>
          <td style="text-align: right">89.3% / 85.7%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">90.8%</td>
          <td style="text-align: right">618 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Mapika Decider 4B</td>
          <td style="text-align: right">89.3% / 85.7%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">86.9%</td>
          <td style="text-align: right">186 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Kev 4B</td>
          <td style="text-align: right">88.1% / 86.9%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">82.3%</td>
          <td style="text-align: right">246 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Eikos 4B</td>
          <td style="text-align: right">85.7% / 79.8%</td>
          <td style="text-align: right">82.7%</td>
          <td style="text-align: right">87.7%</td>
          <td style="text-align: right">235 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Simple-Jev + Qwen3.5 4B</td>
          <td style="text-align: right">77.4% / 71.4%</td>
          <td style="text-align: right">74.4%</td>
          <td style="text-align: right">85.4%</td>
          <td style="text-align: right">1,349 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Kev 0.8B</td>
          <td style="text-align: right">64.3% / 61.9%</td>
          <td style="text-align: right">63.1%</td>
          <td style="text-align: right">63.1%</td>
          <td style="text-align: right">86 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Decision 3.0 Edge (0.6B)</td>
          <td style="text-align: right">67.9% / 53.6%</td>
          <td style="text-align: right">60.7%</td>
          <td style="text-align: right">65.4%</td>
          <td style="text-align: right">45 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>GLiNER2.5-Decide (EN)</td>
          <td style="text-align: right">54.8% / 50.0%</td>
          <td style="text-align: right">52.4%</td>
          <td style="text-align: right">didn&rsquo;t run</td>
          <td style="text-align: right">208 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya typed-decisions</td>
          <td style="text-align: right">50.0% / 35.7%</td>
          <td style="text-align: right">42.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">43 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya (EN)</td>
          <td style="text-align: right">44.0% / 41.7%</td>
          <td style="text-align: right">42.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">41 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya multilingual</td>
          <td style="text-align: right">35.7% / 38.1%</td>
          <td style="text-align: right">36.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">19 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>GLiNER2.5-multi-Decide</td>
          <td style="text-align: right">32.1% / 35.7%</td>
          <td style="text-align: right">33.9%</td>
          <td style="text-align: right">didn&rsquo;t run</td>
          <td style="text-align: right">63 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Julia-1 (GPU)</td>
          <td style="text-align: right">38.1% / 25.0%</td>
          <td style="text-align: right">31.5%</td>
          <td style="text-align: right">38.5%</td>
          <td style="text-align: right"><strong>12 ms</strong> (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Julia-1 (CPU)</td>
          <td style="text-align: right">38.1% / 25.0%</td>
          <td style="text-align: right">31.5%</td>
          <td style="text-align: right">36.9%</td>
          <td style="text-align: right">71 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
  </tbody>
</table>
<p>To calibrate your reading: the tasks have three or four options, so a uniform guess gives about 31% on both sets. &ldquo;Near 50%&rdquo; here is already well above the coin, and &ldquo;near 33%&rdquo; is the coin. Julia-1 landed at 31.5%: the pretty name hides a guy flipping a coin.</p>
<p>The intervals matter. Perplexity&rsquo;s edge over Jev on v2 is under 2 points and the bootstrap interval touches zero: it&rsquo;s the measured lead on this set, not a proven superiority forever. And Decision 3.0 tied Perplexity at the limit of what this set can distinguish: <strong>identical labels on the 1,024 classifications</strong>, with both missing the same two cases (the English relevance one on v2 and the disputed severity case on v1, already in the errata). Microsoft tied Jev on the aggregate, with the interval centered on zero, but an aggregate tie is not interchangeability: they miss different cases, and Microsoft inverts the language pattern, missing more in English than in Portuguese. Jev and Clef also remain under 2 points apart, on the same tier. The 90% squad (Jebadiah, OpenAI) sits 7 points behind Jev, and from there down every difference is clear.</p>
<p>Each task cell has only 12 scenarios per language, so one more correct answer moves the cell by more than 8 points. A tie on a specific task is a lead for a pilot. The per-task tables are in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks/blob/master/benchmark/reports/decision3-comparison-v2/results.md"target="_blank" rel="noopener">repository</a>.</p>
<h2>How much it costs, in support emails<span class="hx:absolute hx:-mt-20" id="how-much-it-costs-in-support-emails"></span>
    <a href="#how-much-it-costs-in-support-emails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Price per million tokens is a number nobody can feel. List prices on October 11 are US$ 0.042 per million input tokens on Jev, US$ 0.24 on Clef, US$ 0.038 on Clef-flash and US$ 0.10 on OpenAI Decisions. Output is free on all five. Microsoft Decision-1 charges the same US$ 0.042 as Jev per million, via OpenRouter, but reported a third of the input tokens for the same texts (about 26 thousand versus Jev&rsquo;s 77 thousand on compact v2), and nobody can explain the tokenization difference. In measured practice, that makes it the cheapest hosted one of the bunch.</p>
<p>Clef-flash cost US$ 0.09 on the 9th: Cloudflare cut the price by more than half in the middle of my test. Jev reports considerably more tokens than Cloudflare for the same text, about 45% more on v2, and that&rsquo;s already in the math below.</p>
<p>Imagine an operation with <strong>10 thousand support emails per month</strong> to classify into three or four buckets, with short texts similar to my cases:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">10k emails / month</th>
          <th style="text-align: right">1 million emails</th>
          <th style="text-align: right">Expected errors in 10k (by compact accuracy)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">US$ 0.065</td>
          <td style="text-align: right">US$ 6.45 (about R$ 32)</td>
          <td style="text-align: right">~240</td>
      </tr>
      <tr>
          <td>Clef-flash</td>
          <td style="text-align: right">US$ 0.10</td>
          <td style="text-align: right">US$ 10 (about R$ 50)</td>
          <td style="text-align: right">~1,250</td>
      </tr>
      <tr>
          <td>Jev</td>
          <td style="text-align: right">US$ 0.19</td>
          <td style="text-align: right">US$ 19 (about R$ 95)</td>
          <td style="text-align: right">~240</td>
      </tr>
      <tr>
          <td>OpenAI Decisions</td>
          <td style="text-align: right">US$ 0.23</td>
          <td style="text-align: right">US$ 23 (about R$ 115)</td>
          <td style="text-align: right">~950</td>
      </tr>
      <tr>
          <td>Clef</td>
          <td style="text-align: right">US$ 0.63</td>
          <td style="text-align: right">US$ 63 (about R$ 315)</td>
          <td style="text-align: right">~420</td>
      </tr>
  </tbody>
</table>
<blockquote>
  <p>In reais, at Friday&rsquo;s exchange rate (R$ 4.99), 10 thousand decisions on Jev cost one real. A million costs less than a hundred reais. For any company with a million emails to classify, that vanishes into the budget.</p>

</blockquote>
<p>One caveat about proportions: my cases are short, about 450 tokens per compact case by Jev&rsquo;s count. A real customer email, with quoted thread history, can be three to five times larger, and cost scales linearly with that. Even so, we&rsquo;re talking cents per thousand.</p>
<p>What this math shows is that the price difference between the hosted models is irrelevant next to the cost of being wrong. Jev costs 9 US cents more than Clef-flash per 10 thousand decisions, and in those same 10 thousand it makes about a thousand fewer errors. If a misrouted email costs more than a hundredth of a US cent in human rework, Jev pays for itself. This math assumes every error costs the same, which is never true: sending a critical ticket to the routine queue costs far more than the reverse.</p>
<p>Cloudflare&rsquo;s allowance changes the math for small players. It&rsquo;s 10 thousand &ldquo;neurons&rdquo; per day, shared by the whole account, resetting at midnight UTC. With my input sizes, that&rsquo;s about 1,500 full-Clef classifications per day or almost 10 thousand Clef-flash ones, free. If you have 1,000 emails a day and nothing else running on the account, Clef comes out at zero cost with top-tier quality.</p>
<p>And local? Energy is what the benchmark didn&rsquo;t measure. From what I see on my Strix Halo, the whole machine stays under 100 W under load. With Jebadiah at 200 ms per decision, 10 thousand decisions are about 35 machine minutes, under 0.1 kWh, a few cents of a real on the power bill.</p>
<p>The real local cost is the machine, your hours configuring ROCm and containers, and keeping it all standing. Whoever already has the hardware for another reason has low marginal cost. Whoever would buy the machine just for this is buying a problem of thousands of reais to save about a hundred reais per million.</p>
<h2>Which model for which situation<span class="hx:absolute hx:-mt-20" id="which-model-for-which-situation"></span>
    <a href="#which-model-for-which-situation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>You want accuracy and want to operate nothing: Jev<span class="hx:absolute hx:-mt-20" id="you-want-accuracy-and-want-to-operate-nothing-jev"></span>
    <a href="#you-want-accuracy-and-want-to-operate-nothing-jev" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the cloud, Jev is still the name. It won or tied practically every cell among the hosted ones, and only Microsoft tied it on the aggregate. In English it was 100% on five of the seven v2 tasks, and in Portuguese it repeated 97.6% on the aggregate without losing anything in translation, something no other model above the coin managed. On v1 it kept its 96.2% on the original with the malicious &ldquo;reviewer&rdquo; comment injected; Clef even gained one case, and Kev 4B also stayed put, just at a much lower tier. The 255 ms WAN latency came in 30 ms behind OpenAI&rsquo;s, and it costs 19 US cents per 10 thousand decisions.</p>
<p>The trade-off is the one I already discussed in the previous post: it&rsquo;s proprietary, hosted, of undisclosed size, and you depend on TypeSafe keeping the behavior underneath you. And it makes mistakes. It treated an invoice ID called &ldquo;PASSWORD-523&rdquo; as a usable credential, even with the record explicitly saying it doesn&rsquo;t authenticate anyone.</p>
<h3>You want Jev&rsquo;s level paying a third: Microsoft Decision-1<span class="hx:absolute hx:-mt-20" id="you-want-jevs-level-paying-a-third-microsoft-decision-1"></span>
    <a href="#you-want-jevs-level-paying-a-third-microsoft-decision-1" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Microsoft entered this market quietly, and entered well. Decision-1 is a 9B post-trained from Qwen3.5-9B, served via OpenRouter, and it was the first model to tie Jev on the compact aggregate: 97.6%, with the bootstrap interval centered on zero. The detail that won me over: <strong>100% in Portuguese</strong>, better than its own English (95%), inverting the pattern of the entire rest of the bunch. On v1, the reversed-order and malicious-comment variants didn&rsquo;t take down a single answer, and it got the critical ticket that Clef and OpenAI missed.</p>
<p>And it&rsquo;s the cheapest hosted one I measured: US$ 0.065 per 10 thousand decisions, a third of Jev, charging the same per-token rate but reporting a third of the tokens. The savings come from the token count, and I can&rsquo;t explain the exact mechanism, so treat it as a measurement, not a promise.</p>
<p>The weak spots exist. Latency is the worst among the hosted ones (422 ms median, versus Jev&rsquo;s 255). On the compact cases it&rsquo;s clean of confident errors, but add context and Portuguese errors show up with 99.5% confidence: the &ldquo;perfect Portuguese&rdquo; pattern doesn&rsquo;t survive long text. And on the probability probes it&rsquo;s weak, giving 87% to a fair coin. Microsoft advertises calibrated probabilities; my probes disagree. And like every dated-ID hosted model, the weight behind it can change without notice.</p>
<p>For a Portuguese-language operation that wants Jev quality while paying less, it&rsquo;s the first candidate to run in your pilot.</p>
<h3>You want the same level with open weights: Clef<span class="hx:absolute hx:-mt-20" id="you-want-the-same-level-with-open-weights-clef"></span>
    <a href="#you-want-the-same-level-with-open-weights-clef" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The full 27B Clef tied with Jev in English (82 of 84) and finished 3 cases behind in Portuguese. It beat Jev on sensitive data handling in English (12 of 12 versus 11). It&rsquo;s an open-weights model, so you can run it locally with about 54 GB of memory just for the weights in BF16, which I didn&rsquo;t test: the full Clef ran only through the API. Among the open ones, it lost the crown to Perplexity, which scored higher with smaller weights.</p>
<p>Through the API it&rsquo;s the most expensive of the five hosted ones, three times Jev, with no quality gain to justify it. The exception is the daily allowance: for small volume, it&rsquo;s the best classifier you can get for free.</p>
<h3>You want the cheapest in the cloud: Clef-flash<span class="hx:absolute hx:-mt-20" id="you-want-the-cheapest-in-the-cloud-clef-flash"></span>
    <a href="#you-want-the-cheapest-in-the-cloud-clef-flash" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>At US$ 0.038 per million, Clef-flash is the lowest price in the hosted market and covers 9,400 decisions per day within the allowance. Quality drops: 92% in English and 83% in Portuguese on the compact cases, the biggest language drop among the hosted ones, with an interval entirely below zero. On sentiment in both languages, and on incident status in English, it tied with Jev. On intent and sensitive data in Portuguese, it lost badly.</p>
<p>For sentiment tasks in either language, or for English classification where 1 error in 12 is acceptable, it does well. For Portuguese, measure first.</p>
<h3>You already live in the OpenAI ecosystem: OpenAI Decisions<span class="hx:absolute hx:-mt-20" id="you-already-live-in-the-openai-ecosystem-openai-decisions"></span>
    <a href="#you-already-live-in-the-openai-ecosystem-openai-decisions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>OpenAI&rsquo;s debut in this market landed mid-table: 90.5% combined, an exact tie with Jebadiah, with the lowest hosted latency I measured (222 ms) and the same accuracy in both languages. It costs US$ 0.23 per 10 thousand decisions, between Jev and Clef. If your operation already speaks the OpenAI API and 9 out of 10 accuracy is enough, integration is trivial: a native decisions endpoint, no prompt at all.</p>
<p>The weak spots are revealing. Its worst cell was precisely numeric-threshold policy (8 of 12 in English, 7 in Portuguese): it treated &ldquo;at least 10&rdquo; as satisfied by a range of 8 to 12 and failed the AND/OR short-circuit. The calibration is schizophrenic: on the predicate probes (yes/no) it was the most accurate of the bunch, with near-zero average error, and on the choice probes it returned 94%/6% on a fair coin. And on the v1 variants it flipped 2 correct answers in each presentation (reversed order, opaque label, malicious comment), something only Perplexity didn&rsquo;t do: Jev kept its aggregate accuracy, but also flipped some correct answers along the way. Oh, and the <code>gpt-6-luna</code> alias is not an immutable snapshot: reproducibility requires saving the responses.</p>
<h3>You want the best open model, period: Perplexity Decider 27B<span class="hx:absolute hx:-mt-20" id="you-want-the-best-open-model-period-perplexity-decider-27b"></span>
    <a href="#you-want-the-best-open-model-period-perplexity-decider-27b" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Perplexity Decider 1.1 27B is the surprise of the benchmark. It led the table with 99.4% on the compact cases, was the only one that didn&rsquo;t lose a single task cell to Jev (3 wins, 11 ties), and hit 100% in Portuguese under all three conditions: compact, with examples, with distractors. Calibration is also the best measured, with a Brier of 0.012, and coverage is absurd: it answered with confidence above 0.9 on 96% of cases and got zero of them wrong. On v1, it didn&rsquo;t flip a single answer across the order, opaque-label and malicious-comment variants.</p>
<p>Its only 3 errors on v2 were the same case in different clothing (it confused &ldquo;notification sent to managers&rdquo; with &ldquo;mandatory approver&rdquo;), and it made them with confidence below 0.9: the known error comes flagged. The weak spot is event-probability probes: asked about an event with a 1-in-4 chance, it answered 0.73. Strong classification does not imply reliable event probability.</p>
<p>The cost is hardware: about 49 GB of weights in BF16, a second and a half per decision on my Strix Halo (with ROCm&rsquo;s reference kernels, no flash-attention; on a good NVIDIA it should be much faster, but that I didn&rsquo;t measure). It&rsquo;s Apache 2.0, public weights with no gate. If you have the machine and want the top of the table at home, this is the one.</p>
<h3>You want the other open-weights top: Decision 3.0 (and beware the Edge)<span class="hx:absolute hx:-mt-20" id="you-want-the-other-open-weights-top-decision-30-and-beware-the-edge"></span>
    <a href="#you-want-the-other-open-weights-top-decision-30-and-beware-the-edge" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Decision 3.0, released two days ago by the vLLM Semantic Router project, is the second model to sit at the top: 99.4% on the compact cases, 100% in Portuguese, and a tie with Perplexity so absolute that the two gave <strong>exactly the same labels on the 1,024 classifications</strong>, including the same two misses (the English relevance one, at confidence below 0.9, and the disputed severity case that already lives in the errata). It also cleared the v1 traps without flipping a single answer, got both indisputable critical tickets right, and its calibration is in the same class as Perplexity&rsquo;s (Brier 0.014, 94% coverage above 0.9 with zero errors).</p>
<p>In other words: the open top now has two independent recipes that converge. For your pilot, either one works; run both if you can, because the probabilities differ even when the labels don&rsquo;t. Latency is in the same family as Perplexity (1.4 s on my machine, same ROCm diet), and the weights take the same ~49 GB.</p>
<p>The little brother is another story. <strong>Decision 3.0 Edge</strong> (0.6B) answers in 45 ms and fits in 1.3 GB of GPU, but scored only 61% on compact, plummets 14 points in Portuguese, and worse: of the 68 guesses it gave with confidence above 0.9, 14 were wrong. As a confidence-threshold router, out of the factory, it&rsquo;s a trap from the same class as Julia. The four middle checkpoints (flash, mini, nano, lite) I haven&rsquo;t tested yet; they&rsquo;re the ones that will tell whether the family has a sweet spot between the two extremes.</p>
<h3>You need to run locally, with quality: Jebadiah 4B v2<span class="hx:absolute hx:-mt-20" id="you-need-to-run-locally-with-quality-jebadiah-4b-v2"></span>
    <a href="#you-need-to-run-locally-with-quality-jebadiah-4b-v2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Of the local ones, Jebadiah 4B v2 had the best compact accuracy, 90% combined, answering in about 200 ms. It tied with Jev on six of the fourteen task cells, including intent, implication and status in English and intent in Portuguese. The weak spot is numeric-threshold policy, 9 of 12 in both languages, and it loses ground when the context fills with distractors (76% in Portuguese under that condition, versus 82% for Mapika).</p>
<p><strong>Mapika Decider 4B</strong> (the fastest of the 4Bs) and <strong>Kev 4B</strong> are right behind, both around 87%, and the 3-point difference between the three fits inside the error interval. They&rsquo;re three different training recipes on top of 4B backbones, with errors on different tasks. The choice between them depends on your task, and I&rsquo;d run all three in a pilot.</p>
<h3>You will send examples in the prompt: Kev 9B<span class="hx:absolute hx:-mt-20" id="you-will-send-examples-in-the-prompt-kev-9b"></span>
    <a href="#you-will-send-examples-in-the-prompt-kev-9b" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Kev 9B has a curious profile. On the compact cases it ties with Kev 4B, half a point apart with the interval crossing zero, meaning more than double the parameters bought nothing. With one solved example per class, it opens almost 10 points over the 4B, and with distractors it holds 7 more points. Brier also improves by about 25%: the probability it returns is better calibrated.</p>
<p>If your use is just the rule and the text, the 4B is enough and is more than twice as fast. If you&rsquo;re going to send examples or long context, the 9B is worth the memory. The warning is Portuguese: 83% compact in PT-BR, almost 10 points below its own English, the biggest language drop among the models above 80%. Only Laya typed (14 points) and Julia (13) drop more.</p>
<h3>You want Clef-flash behavior on your machine: local Clef-flash<span class="hx:absolute hx:-mt-20" id="you-want-clef-flash-behavior-on-your-machine-local-clef-flash"></span>
    <a href="#you-want-clef-flash-behavior-on-your-machine-local-clef-flash" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the v1 set, local Clef-flash reproduced the hosted one&rsquo;s 520 classifications, hit for hit and miss for miss. On v2 the story changed: same total (439 of 504), but 19 different labels, with swapped strengths (the local one gets more Portuguese intent right, the hosted one gets more relevance right). Among the local ones it&rsquo;s still the best on the v1 operational-policy set (90.8%), ahead of every 4B.</p>
<p>The gotcha is latency: 618 ms median on the Strix Halo, three times Jebadiah, for the same v2 accuracy. If you pick it, it&rsquo;s for the known behavior.</p>
<h3>You need to run offline on weak hardware: Laya and Julia, with many caveats<span class="hx:absolute hx:-mt-20" id="you-need-to-run-offline-on-weak-hardware-laya-and-julia-with-many-caveats"></span>
    <a href="#you-need-to-run-offline-on-weak-hardware-laya-and-julia-with-many-caveats" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Laya multilingual was the worst in the benchmark for accuracy (37% combined, near the coin), and the fastest by far among the ones that get anything right: 19 ms median, more than ten times faster than Jebadiah.</p>
<p>The three Laya checkpoints have between 322 and 421 million parameters, fit in under 1 GB of BF16 weights by parameter math, and the project documents CPU execution and an ONNX runtime (which I didn&rsquo;t measure). PyTorch&rsquo;s peak GPU allocation for the multilingual one stayed under 2 GB. A 4B needs about 8 GB just for weights at the same precision.</p>
<p><strong>Julia-1</strong> goes one step lower: 144 million parameters, 550 MB of weights, 12 ms median on GPU and 71 ms on a 4-thread CPU. It&rsquo;s the fastest model I&rsquo;ve ever measured here, and also the worst: 31.5% accuracy, glued to the 31% coin-flip line. In Portuguese it answered &ldquo;cancellation&rdquo; on all 12 intent scenarios, including the ones asking for a refund or access (the vendor&rsquo;s Portuguese evaluation was done in pt-PT, and it shows). The bigger disaster is confidence: of the 71 guesses it gave above 0.9, 48 were wrong. A confidence threshold won&rsquo;t save this model, because it&rsquo;s exactly on the confident guesses that it errs.</p>
<p>That&rsquo;s a design choice. Laya and Julia were made to be small encoders, fine-tunable on your task, to run at the edge. What I tested were the factory checkpoints, with no tuning, applying rules they had never seen, on broad tasks. In that scenario they did badly, period.</p>
<p>One detail confirms the fragility. On v1, when the reviewer&rsquo;s malicious comment comes in, Laya drops from 44% to 19% and typed-decisions to 15%. It obeys the comment the instructions say to ignore, and confidently: 17 of the 28 guesses above 0.9 were wrong. Julia failed all 5 policy-counterfactual pairs: change the rule, it keeps the answer.</p>
<blockquote>
  <p>If you want what Jev delivers, factory Laya and Julia are very far from it, and no small model (GLiNER included) came close. If your problem is &ldquo;I need to classify short text into three buckets, on a device with no internet and no GPU, and I have a thousand labeled examples to fine-tune on,&rdquo; these two are the only ones on the list that fit. Fine-tuning is the part I didn&rsquo;t test. It&rsquo;s another question, and it needs another benchmark.</p>

</blockquote>
<h3>The baseline: Simple-Jev with a stock Qwen<span class="hx:absolute hx:-mt-20" id="the-baseline-simple-jev-with-a-stock-qwen"></span>
    <a href="#the-baseline-simple-jev-with-a-stock-qwen" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Simple-Jev reading tokens from Qwen3.5 4B, with no decision training at all, scored 74% on compact v2. That&rsquo;s the yardstick for &ldquo;how much specialized training buys&rdquo;: Jebadiah, Mapika and Kev, all on similarly sized backbones, came in 13 to 16 points above. The training buys a lot.</p>
<p>The curious part is that on v1 Simple-Jev tied with Jebadiah (111 of 130 originals) and got both unambiguous critical tickets right, which several better models missed. It&rsquo;s also the slowest of the bunch, almost a second and a half per decision, because it&rsquo;s running a whole LLM.</p>
<h2>How hard it is to run the best open models on your machine<span class="hx:absolute hx:-mt-20" id="how-hard-it-is-to-run-the-best-open-models-on-your-machine"></span>
    <a href="#how-hard-it-is-to-run-the-best-open-models-on-your-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All these models speak the same HTTP protocol, called SystemOne, which Jev popularized: you send a state (the text), one or more closed-answer questions, and get back the chosen option and the probability of each. Swapping models is swapping the port. OpenAI Decisions breaks from the pattern with its own native endpoint, but the idea is the same. What changes is how much work it takes to get each one standing, and there the difference between them is big.</p>
<h3>OpenAI: just an API key<span class="hx:absolute hx:-mt-20" id="openai-just-an-api-key"></span>
    <a href="#openai-just-an-api-key" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The easiest of all: create the key, load credit and call the endpoint. The benchmark&rsquo;s 1,048 requests ran in 4 minutes, without a single retry. The only hiccup was the first probe returning &ldquo;balance exhausted&rdquo; until I put US$ 20 in the account.</p>
<p><strong>Microsoft Decision-1</strong> is almost as simple: if you already have an OpenRouter account, it&rsquo;s point at the model and call the decisions endpoint. No GPU, no Azure subscription, no installation. In my test, 10 of the 504 v2 calls took a 429 and passed on retry, so plan for retries.</p>
<h3>Kev and Jebadiah: one pip and one command<span class="hx:absolute hx:-mt-20" id="kev-and-jebadiah-one-pip-and-one-command"></span>
    <a href="#kev-and-jebadiah-one-pip-and-one-command" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Kev</strong> is the simplest of the 4B crowd. The project installs straight from GitHub and brings up a server with one command, downloading the LoRA adapter and the decision head on top of base Qwen3.5:</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-sh" data-lang="sh"><span class="line"><span class="cl">pip install <span class="s2">&#34;kev @ https://github.com/jaredpalmer/kev/archive/fc4a17e194bca35a48c57211392ec49ea54a94ac.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl">python -m kev.serve --run jaredpalmer/kev-4b --host 0.0.0.0 --port <span class="m">8000</span> --device cuda</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><strong>Jebadiah</strong> follows the same pattern, with the server package inside its own repository:</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-sh" data-lang="sh"><span class="line"><span class="cl">pip install <span class="s2">&#34;jebadiah-server @ https://github.com/getainode/jebadiah/archive/e9793abc66d1a7c5ad878d51eda656f1e9c5b9f5.tar.gz#subdirectory=server&#34;</span>
</span></span><span class="line"><span class="cl">jebadiah-serve --model frontier-infra/jebadiah-4b-v2 --host 0.0.0.0 --port <span class="m">8000</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>With either one up, a decision is a POST:</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-sh" data-lang="sh"><span class="line"><span class="cl">curl -s http://localhost:8000/v1/systemone -H <span class="s1">&#39;content-type: application/json&#39;</span> -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;model&#34;: &#34;kev-4b&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;state&#34;: &#34;Cliente diz que foi cobrado duas vezes e pede o estorno da segunda cobrança.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;questions&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;fila&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;type&#34;: &#34;choice&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;instructions&#34;: &#34;Escolha a fila correta pela política de suporte.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;criteria&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;financeiro&#34;: &#34;Cobrança, estorno, nota fiscal.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;tecnico&#34;: &#34;Erro, bug, indisponibilidade.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;vendas&#34;: &#34;Upgrade, plano novo, orçamento.&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">  }
</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>The response carries the option and the probability distribution, and that&rsquo;s where you apply your threshold, log and decide whether to send it for human review. On an NVIDIA with CUDA and 16 GB of VRAM, a 4B in BF16 comes up without drama. The weights are about 8 GB.</p>
<h3>Laya and Julia: smaller still<span class="hx:absolute hx:-mt-20" id="laya-and-julia-smaller-still"></span>
    <a href="#laya-and-julia-smaller-still" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Laya</strong> is a Python package with an encoder inside. It installs with <code>pip install laya</code> and the project documents CPU execution, which is its whole point. The version pinned on my server was 0.4.1, and the multilingual one&rsquo;s peak GPU memory stayed under 2 GB. To run on a GPU-less laptop or a small container, it&rsquo;s the only one on the list that fits, no discussion.</p>
<p><strong>Julia-1</strong> is similar in spirit: its own Python runtime, one <code>predict(state=..., questions=...)</code> call and done, no standard Transformers pipeline. I ran the same container on GPU and on CPU, and both gave the same accuracy with the same responses in 97% of cases. On a 4-thread CPU it answers in 71 ms, which for an edge classifier is great. The problem is still that it scores at coin-flip level.</p>
<h3>Where it gets complicated: AMD, ROCm and the rest<span class="hx:absolute hx:-mt-20" id="where-it-gets-complicated-amd-rocm-and-the-rest"></span>
    <a href="#where-it-gets-complicated-amd-rocm-and-the-rest" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In my case the hardware is AMD, and then &ldquo;one command&rdquo; turns into a day of work. The points that cost me time, to save you yours:</p>
<ul>
<li>None of these projects test on ROCm. Kev had a gfx1151-specific segfault caused by CUDA graphs (<a href="https://github.com/jaredpalmer/kev/issues/170"target="_blank" rel="noopener">issue 170</a>), fixed only after the 1.0 tag, so the pinned version is a main-branch commit and the official tag crashes.</li>
<li>The flash-linear-attention Triton kernels Qwen3.5 uses in the DeltaNet layers have no tested support on gfx1151. Everything ran on the pure-PyTorch path, slower than the latency published on CUDA. Jebadiah&rsquo;s 200 ms in the table is with that handbrake pulled.</li>
<li>Mapika Decider forces CUDA graphs when it sees the &ldquo;cuda&rdquo; device, which on ROCm is also called &ldquo;cuda.&rdquo; I had to turn it off by hand.</li>
<li>You need to export <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> and <code>PYTORCH_ROCM_ARCH=gfx1151</code> and use a base image with ROCm torch already validated. Any random <code>pip install torch</code> overwrites it with the CUDA build and breaks everything. In my Dockerfile every package is installed with <code>--no-deps</code> and the build fails if torch stops being the ROCm one.</li>
<li>One model per container, one container at a time. The Strix Halo has memory shared between CPU and GPU, and three resident 4B models plus the Ollama that already runs there exhaust the 64 GiB the GPU can see.</li>
<li>Perplexity Decider was the most annoying of all. The release expects a specific CUDA torch (I swapped in the ROCm 7 torch 2.9), the flash-linear-attention kernels don&rsquo;t exist for it (I ran on PyTorch&rsquo;s reference kernels, slower), and the default loader blew past the host&rsquo;s 24 GiB RAM limit: I needed a custom loader that sends the 49 GiB of weights straight to the GPU. The model card still said &ldquo;private&rdquo; when I started, but the Hugging Face API confirmed it was public and gate-free, and the shards downloaded anonymously. The d3, at the same size, inherited the same treatment. The d3-edge, luckily, comes up with the standard Transformers loader and 1.3 GB of GPU.</li>
</ul>
<p>None of this is impossible, and it&rsquo;s all documented, with Dockerfiles, compose and the hashes of each checkpoint, in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks/blob/master/docs/setup.md"target="_blank" rel="noopener">repository&rsquo;s setup guide</a>. But it&rsquo;s a real cost that the &ldquo;local is free&rdquo; spreadsheet never includes. If your machine is NVIDIA, most of this list disappears.</p>
<blockquote>
  <p>The full 27B Clef I didn&rsquo;t even try locally: it&rsquo;s about 54 GB of weights in BF16 before any activation. Perplexity, at 49 GB, proved a 27B runs on the Strix Halo, at the cost of a second and a half per decision with the reference kernels. The 9B Clef-flash runs, with about 18 GB of weights. The 4Bs are the sweet spot for consumer hardware.</p>

</blockquote>
<h2>Highlights and gotchas<span class="hx:absolute hx:-mt-20" id="highlights-and-gotchas"></span>
    <a href="#highlights-and-gotchas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Laya truncated all long Portuguese.</strong> Under the distractor condition, base Laya reported truncation on all 84 Portuguese calls, discarding almost 31 thousand state tokens, and on none of the 84 English ones. The English checkpoint&rsquo;s default context limit is only 512 tokens (the multilingual and typed ones use 1,024, configurable up to 8,192), and Portuguese tokenizes heavier. The other models don&rsquo;t even expose truncation telemetry, so &ldquo;zero truncation reported&rdquo; on them means &ldquo;I don&rsquo;t know.&rdquo;</p>
<p><strong>Portuguese costs more tokens.</strong> For the same 84 case pairs, Jev reported about 14% more input tokens in Portuguese than in English, and both Clefs about 15% more. If you budget a bilingual operation by the English average, you&rsquo;ll come in low.</p>
<p><strong>Does an example in the prompt save a weak model? It saves nothing.</strong> One solved example per class left Laya the same or worse; several of its aggregates dropped. It helped Kev 9B (plus 4 points) and Clef in Portuguese (plus 3.6 points, reaching Jev&rsquo;s level), and almost nothing on Jev, one more case in Portuguese. Validate examples on real traffic before assuming they help.</p>
<p><strong>Size correlates, and explains little.</strong> Among the 19 competitors of known size, the Spearman correlation between parameters and compact accuracy was 0.96. The Kev series is more informative: 63% at 0.8B, 87% at 4B, 88% at 9B. The big jump is from 0.8B to 4B, about 24 points, with an interval between 16 and 33. From 4B to 9B the compact difference crosses zero, and several nominal 4Bs, with similar backbones, gave very different results. In the d3 family, the flagship and the Edge sit almost 40 points apart under the same release, while a 9B, Microsoft, ties at the top of the hosted ones. Size helps up to 4B; past that, the training recipe rules.</p>
<p><strong>The 27B Clef missed a critical ticket that two of the Layas got right.</strong> On v1, one severity scenario says continuous data loss in production is critical, regardless of how many customers it affects. The record describes confirmed permanent loss, from a single customer. Jev, Clef-flash, Perplexity, d3, Microsoft, Laya typed, Simple-Jev and Laya multilingual answered &ldquo;critical.&rdquo; Full Clef, OpenAI, base Laya, Eikos, Jebadiah, Mapika, Kev 4B and Kev 9B answered &ldquo;degraded.&rdquo;</p>
<blockquote>
  <p>It&rsquo;s a single case, and an error rate would require many more. But it shows that the average hides exactly the error that costs the most. When the impact facts are structured, the severity rule belongs in code, and the model comes in only to extract the facts from free text.</p>

</blockquote>
<p><strong>Hosted and local diverge over time.</strong> Local Clef-flash reproduced the hosted one on v1&rsquo;s 520 classifications and diverged on 19 of v2&rsquo;s 504. Cloudflare keeps the weights version behind the hosted alias undisclosed. If reproducibility matters, pin the local hash.</p>
<p><strong>High confidence and safety are different things.</strong> On compact v2, Jev, Clef, both Clef-flashes, Eikos, Jebadiah, OpenAI, Microsoft, Perplexity and d3 got zero guesses above 0.9 wrong. Perplexity is the extreme case: it answered at that level on 96% of cases without missing one, with d3 right behind (94%, zero errors). Microsoft covered 95% and OpenAI only half. Microsoft&rsquo;s caveat: the clean record is only on compact, because with examples or distractors it misses in Portuguese with confidence above 99%. Mapika missed 5% of the guesses at that level, Kev 4B 3% and Simple-Jev 14%. And the two smallest of the bunch broke the pattern violently: Julia missed 48 of 71 guesses above 0.9, and d3-edge missed 14 of 68. Coverage varied too: Jev answered above 0.9 on almost 90% of cases, Clef on almost 80%, Clef-flash on 40% and base Laya on 2 of 168. On v1 Jev missed 1% of the guesses above 0.9 and base Laya missed 61%. A confidence threshold only works after being validated on your distribution, and Laya typed&rsquo;s low ECE in Portuguese left it at the same 36% accuracy.</p>
<p><strong>Perplexity didn&rsquo;t fall for presentation traps.</strong> Across the three v1 variants (reversed options, opaque labels, malicious comment), it kept the same answers as the original, without flipping a single one. The d3 repeated the feat, zero flips too. Microsoft only stumbled on the opaque labels (2 flips, one in each direction); order and comment didn&rsquo;t move it. OpenAI flipped 2 in each variant, and Laya collapsed to 19% under the comment. The price of that robustness is latency: a second and a half per decision on my machine, the worst median of the bunch.</p>
<p><strong>Numeric thresholds are a code problem.</strong> The threshold-triage task, with clear numbers and an explicit rule, gave 100% on Jev and both Clef-flashes and under 30% on both Layas in English. An <code>if</code> solves that with 100% and zero latency. The model only comes in when the number is buried in free text, and even then the right thing is to extract the number and leave the comparison to code.</p>
<p><strong>Lottery probability is another skill.</strong> On the 24 no-information probes (coin, die), Jev had the worst choice error among options. The numeric-estimate crown changed hands with OpenAI&rsquo;s debut: its yes/no predicate missed almost nothing, while the choice probe returned 94% for heads on a fair coin. Perplexity, champion of everything else, answered 0.73 for an event with a 1-in-4 chance, and Microsoft went the same way, giving 87% to the coin. Laya typed had the second-best choice error, behind only Kev 9B and neck-and-neck with Clef, and still classified badly. It&rsquo;s the &ldquo;student who marks answer A&rdquo; from Paulo&rsquo;s essay, now measured: calibration on lotteries and accuracy on classification are independent measures.</p>
<h2>Where the data converges<span class="hx:absolute hx:-mt-20" id="where-the-data-converges"></span>
    <a href="#where-the-data-converges" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ol>
<li>
<p><strong>The overall top is open-weights, and now there are two.</strong> Perplexity Decider 27B and Decision 3.0 tied at 99.4% on the compact cases, with 100% in Portuguese and identical labels on the 1,024 classifications, both with Apache 2.0 weights running on my machine. Their edge over Jev is under 2 points and the interval touches zero, so treat it as a snapshot of this set. Their cost is hardware and latency, not API. In the cloud, the evaluation order changed: <strong>start with Microsoft Decision-1</strong>, which ties Jev on the aggregate, closes 100% in compact Portuguese and costs a third. Jev comes next, faster and with different errors, because an aggregate tie is not interchangeability. OpenAI Decisions landed halfway in price and accuracy.</p>
</li>
<li>
<p><strong>Running locally became viable at 4B.</strong> Jebadiah, Mapika and Kev 4B deliver between 87 and 90% on the compact cases with 200 ms latency on a desktop machine, and in a pilot with your task any of the three can win. If your machine handles 49 GB of weights, Perplexity delivers the top of the table at home. The financial math justifies using the machine that already exists, for data control or because of an obligation to stay offline. Buying a machine for this it stops justifying.</p>
</li>
<li>
<p><strong>Small models are another category.</strong> Judging Laya and Julia by the accuracy column is reading the table wrong. They get half or more wrong out of the factory, fall hard under a malicious comment, and together with the d3-edge they&rsquo;re the only ones on the list that fit on a weak device and answer in tens of milliseconds. The Edge is the best of the three on accuracy (61%), but it plummets in Portuguese and gets one in five confident guesses wrong. If Jev-like accuracy is a requirement, cross all three off. If running offline on small hardware is a requirement, cross everything else off and start with fine-tuning them on your task, which is the missing measurement. And be careful with Julia and the Edge in particular: 48 errors in 71 and 14 in 68 confident guesses are the portrait of models that don&rsquo;t know what they don&rsquo;t know.</p>
</li>
<li>
<p><strong>Portuguese costs about 14% more tokens</strong> and drops the models in the middle of the table by 1 to 10 points, with Kev 9B and Clef-flash in the worst case. Jev loses nothing, OpenAI ties both languages, and Perplexity, d3 and Microsoft close 100% in compact Portuguese, better than their own English, although Microsoft goes back to missing in Portuguese when the context grows. Julia goes the other way and plummets 13 points, and the Edge plummets 14. For a Brazilian audience, that&rsquo;s the first column to look at, before price.</p>
</li>
</ol>
<blockquote>
  <p>And what holds for everyone: none of these models replaces the rule that&rsquo;s already in code. Numeric thresholds, severity policy with structured facts, tool permission, all of that stays in the <code>if</code>. The model comes in to turn free text into structured fact. And the test that matters is yours, with your tickets, because each cell in this table has 12 scenarios and your operation has thousands.</p>

</blockquote>
<p>The cases, each model&rsquo;s raw responses, the scoring scripts and the guide to reproduce everything are in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks"target="_blank" rel="noopener">benchmark repository</a>. If you run it with your tickets and get a different ranking, tell me.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>artificial-intelligence</category><category>llms</category></item><item><title>LLM Benchmark: Kimi v2.7 Code, GLM 5.2, MiniMax M3 Local</title><link>https://www.akitaonrails.com/en/2026/06/14/llm-benchmark-kimi-2-7-code-glm-5-2-minimax-m3-local/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/06/14/llm-benchmark-kimi-2-7-code-glm-5-2-minimax-m3-local/</guid><pubDate>Sun, 14 Jun 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Another round of my coding benchmark. This time it&amp;rsquo;s three open source entries: &lt;strong&gt;Kimi K2.7 Code&lt;/strong&gt;, &lt;strong&gt;GLM 5.2&lt;/strong&gt;, and the &lt;strong&gt;MiniMax M3&lt;/strong&gt; that got open weights but that I can&amp;rsquo;t run at home no matter how hard I squeeze. Before the numbers, the usual context for anyone who parachuted in here, plus an update to the data center soap opera, because there&amp;rsquo;s news.&lt;/p&gt;
&lt;h2&gt;For anyone just tuning in&lt;span class="hx:absolute hx:-mt-20" id="for-anyone-just-tuning-in"&gt;&lt;/span&gt;
&lt;a href="#for-anyone-just-tuning-in" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This benchmark hands each model the same prompt and asks it to build, on its own, a ChatGPT-style chat app in Rails 8 + RubyLLM + Hotwire + Docker, with tests and CI. The score comes out of an 8-dimension rubric, 0 to 100, in A/B/C/D tiers. The whole series:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Another round of my coding benchmark. This time it&rsquo;s three open source entries: <strong>Kimi K2.7 Code</strong>, <strong>GLM 5.2</strong>, and the <strong>MiniMax M3</strong> that got open weights but that I can&rsquo;t run at home no matter how hard I squeeze. Before the numbers, the usual context for anyone who parachuted in here, plus an update to the data center soap opera, because there&rsquo;s news.</p>
<h2>For anyone just tuning in<span class="hx:absolute hx:-mt-20" id="for-anyone-just-tuning-in"></span>
    <a href="#for-anyone-just-tuning-in" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This benchmark hands each model the same prompt and asks it to build, on its own, a ChatGPT-style chat app in Rails 8 + RubyLLM + Hotwire + Docker, with tests and CI. The score comes out of an 8-dimension rubric, 0 to 100, in A/B/C/D tiers. The whole series:</p>
<ol>
<li><a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">Canonical May benchmark (Part 3)</a>. The full methodology and the ranking reference.</li>
<li><a href="/en/2026/05/04/llm-benchmarks-deepseek-unlocked-deepclaude/">DeepSeek unlocked with DeepClaude</a>. A harness swap pulling V4 Pro out of limbo.</li>
<li><a href="/en/2026/06/01/llm-benchmarks-grok-4-3-minimax-m3-opus-4-8/">Update on Grok 4.3, MiniMax M3, Opus 4.8</a>. M3 debuting via cloud.</li>
<li><a href="/en/2026/06/11/llm-benchmark-fable-5-anthropic-soap-opera/">Fable 5 and the Anthropic soap opera</a>. The first public Mythos-class model.</li>
</ol>
<h2>The data center soap opera got a new episode: SpaceX<span class="hx:absolute hx:-mt-20" id="the-data-center-soap-opera-got-a-new-episode-spacex"></span>
    <a href="#the-data-center-soap-opera-got-a-new-episode-spacex" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In the last few posts I&rsquo;ve been hammering the same nail: data center construction became the bottleneck of the AI era, and demand blew past the installed base with room to spare. <a href="https://www.techradar.com/pro/if-one-piece-of-your-supply-chain-is-delayed-then-your-whole-project-cant-deliver-nearly-half-of-us-data-centers-planned-for-2026-canceled-or-delayed-and-things-could-soon-get-much-worse"target="_blank" rel="noopener">Nearly half of the US data centers planned for 2026 have been delayed or canceled</a>, stuck on transformers and electrical components with a three-year backlog. I went deep on that in the <a href="/en/2026/06/11/llm-benchmark-fable-5-anthropic-soap-opera/">Fable post</a>.</p>
<p>The new episode is financial. <strong>SpaceX went public</strong>, and it was the biggest IPO in Wall Street history: <a href="https://www.datacenterdynamics.com/en/news/spacex-ipo-musks-firm-set-to-launch-first-orbital-data-center-ai1-satellites-in-2027-will-put-compute-on-starlink-craft/"target="_blank" rel="noopener">556 million shares at $135, a $1.77 trillion valuation, with over $250 billion of investor demand</a> against the $75 billion the company first set out to raise. And the pitch that propped up that valuation was, of all things, AI compute: the <strong>AI1 satellites</strong>, &ldquo;orbital data centers&rdquo; SpaceX plans to launch starting late 2027, with <a href="https://www.scientificamerican.com/article/spacex-ipo-valuation-depends-on-starship-and-orbital-ai-data-centers/"target="_blank" rel="noopener">the company already signing compute deals with Anthropic and Google</a>. The Anthropic and OpenAI IPOs come right behind, and all that money shares one goal: accelerate the availability of compute, because right now it can&rsquo;t keep up.</p>
<p>Here&rsquo;s the part that genuinely interests me. In a not-so-distant future, <strong>open source LLMs can be part of the solution to that demand</strong>. If a slice of the tasks migrates to a model running locally, on the developer&rsquo;s machine, that&rsquo;s less load hitting the cloud data centers. But &ldquo;can be&rdquo; isn&rsquo;t &ldquo;already is,&rdquo; and this benchmark exists precisely to measure that distance.</p>
<h2>The usual reminder: serious programming means Opus 4.8 or GPT 5.5<span class="hx:absolute hx:-mt-20" id="the-usual-reminder-serious-programming-means-opus-48-or-gpt-55"></span>
    <a href="#the-usual-reminder-serious-programming-means-opus-48-or-gpt-55" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I say this in every post and I&rsquo;m not going to stop: serious programming work, today, is done with <strong>Claude Opus 4.8 or GPT 5.5</strong>, inside Claude Code or OpenCode. Full stop. The open source models haven&rsquo;t reached the point of carrying a real project, a long session, 100% vibe coded start to finish. They&rsquo;re too small, and the context window is too limited. By my yardstick, true programming only becomes feasible above roughly <strong>200,000 tokens</strong> of context, and most open source models don&rsquo;t come close to that with any quality. In my benchmark, nearly the entire open source field sits at Tier B or below.</p>
<p>Now for the &ldquo;but.&rdquo; They&rsquo;re not standing still. Every generation they improve at a steady clip, and this round&rsquo;s three entries show it in black and white. The day an open source model becomes a consistent Tier A and runs with 200k of context on a machine that fits on your desk, the data center math changes. On to the numbers.</p>
<h2>Kimi K2.7 Code: almost there, with a silly regression<span class="hx:absolute hx:-mt-20" id="kimi-k27-code-almost-there-with-a-silly-regression"></span>
    <a href="#kimi-k27-code-almost-there-with-a-silly-regression" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kimi K2.7 Code (exposed on OpenRouter as <code>moonshotai/kimi-k2.7-code</code>) scored <strong>86/100, Tier A, #8</strong>. At $0.30 per run in 22 minutes, it&rsquo;s still one of the cheapest models in all of Tier A.</p>
<p>The RubyLLM integration is genuine end to end (<code>RubyLLM.chat</code> + <code>add_message</code> with full history replay + <code>complete</code> + <code>response.content</code>), verified against the gem source 1.16.0. Session-cookie persistence that survives restart and is multi-worker safe, 22 tests that exercise the LLM path with a correctly-signed mock and an error-path test, real Turbo Streams, and three Stimulus controllers. Solid engineering.</p>
<p>Worth logging a methodological trap this model exposed, because it&rsquo;s instructive. My structural scanner flagged <strong>six <code>chat.user</code>/<code>chat.assistant</code> calls as hallucinations</strong>, the exact invented signature that dropped GLM 5.1 to Tier C months ago. Except here they were all false positives: they resolve to the app&rsquo;s <strong>own</strong> domain methods (<code>Chat#user</code>, <code>Message.assistant</code>), not RubyLLM&rsquo;s DSL. Reading the code by hand made it clear the real API is correct. If I&rsquo;d trusted the scanner blindly, I&rsquo;d have sunk a Tier A unfairly. That&rsquo;s why this benchmark always reads the integration by hand, never just counts files.</p>
<p>And then comes the stumble that holds it <strong>below</strong> K2.6 itself (87): K2.7 Code <strong>doesn&rsquo;t send a system prompt via <code>with_instructions</code></strong>. The assistant ships with no persona and no guardrail, a real gap for a &ldquo;ChatGPT-style&rdquo; app and the main regression from K2.6, which had one. On top of that, the session cookie has no message cap (<code>CookieOverflow</code> risk on a long chat), and the RubyLLM I/O lives inside the <code>Chat</code> value object instead of an isolated service. Funny detail: the &ldquo;Code&rdquo; variant, supposedly specialized for programming, regressed on a product dimension the general version got right.</p>
<h2>GLM 5.2: the biggest version-to-version turnaround in the whole benchmark<span class="hx:absolute hx:-mt-20" id="glm-52-the-biggest-version-to-version-turnaround-in-the-whole-benchmark"></span>
    <a href="#glm-52-the-biggest-version-to-version-turnaround-in-the-whole-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This was the round&rsquo;s highlight. GLM 5.2 scored <strong>87/100, Tier A, #6</strong>, and the size of the jump is what stuns: GLM 5.1 had scored <strong>46/100, Tier C, #21</strong>. From one version to the next, 41 points, three tiers, fifteen positions. It&rsquo;s the biggest intra-family turnaround I&rsquo;ve ever measured.</p>
<p>The bug that doomed 5.1 was specific: it invented <code>chat.user</code>/<code>chat.assistant</code> to build the multi-turn history and crashed on turn 2. 5.2 just uses the real <code>chat.add_message(role:, content:)</code> for the replay, and that&rsquo;s that. Every call verified against the gem source 1.16.0, zero hallucinations. It has the <strong>cleanest dependency injection of the whole field</strong>: both the RubyLLM client and the controller&rsquo;s <code>service_class</code> are injectable, so the 26-test suite mocks the LLM path with a correctly-signed <code>FakeChat</code>/<code>FakeClient</code>, no external mock library, exercising streaming, system-prompt application, and error wrapping. Targets the right slug (<code>anthropic/claude-sonnet-4.6</code>), real Turbo Streams, full Gemfile, valid Ruby 4.0.5.</p>
<p>What holds it to a tie with Kimi K2.6 (and just behind it) is the same dimension that separated Fable 5 from Opus 4.8: <strong>persistence</strong>. The store is a process-local <code>Singleton</code> with <strong>no cap</strong>, dies on restart, isn&rsquo;t multi-worker safe, and grows without bound on top of that. The code is honest about this in its comments, but the rubric scores it below Kimi&rsquo;s capped, multi-worker-safe cookie. It was also the slowest Tier A run, 43 minutes on Z.ai&rsquo;s coding endpoint, which was throttled to 12-55 tokens/s.</p>
<p>The takeaway: GLM 5.1 → 5.2 is the clearest example that open source isn&rsquo;t stagnant. A model that was architectural garbage became Tier A in one generation, fixing the exact bug that mattered.</p>
<h2>MiniMax M3 local: the problem isn&rsquo;t the model, it&rsquo;s the hardware<span class="hx:absolute hx:-mt-20" id="minimax-m3-local-the-problem-isnt-the-model-its-the-hardware"></span>
    <a href="#minimax-m3-local-the-problem-isnt-the-model-its-the-hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here the story is different, and it&rsquo;s the one that matters most to the argument about the future. MiniMax released <strong>open weights</strong> for M3: there&rsquo;s an <a href="https://huggingface.co/unsloth/MiniMax-M3-GGUF"target="_blank" rel="noopener"><code>unsloth/MiniMax-M3-GGUF</code></a> and an <a href="https://ollama.com/library/minimax-m3"target="_blank" rel="noopener"><code>ollama.com/library/minimax-m3</code></a> entry. Availability isn&rsquo;t the problem. <strong>Memory is.</strong></p>
<p>M3 is a MoE/MSA with <strong>~428 billion total parameters and ~23 billion active per token</strong> (~427B in the safetensors; NVIDIA lists ~22B active, likely a rounding/counting difference) spread across <strong>128 experts, with 4 active per token</strong>. And here&rsquo;s the MoE catch: even though only ~23B activate per token, the <strong>entire</strong> weight set has to be resident in memory. You can&rsquo;t page an idle expert to the CPU without introducing latency that kills interactive inference. So the number that matters is total size, not active size:</p>
<table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th>GGUF size</th>
          <th style="text-align: center">RTX 5090 (32 GB)</th>
          <th style="text-align: center">Strix Halo (≤128 GB)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q4_K_M</td>
          <td>~264 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗</td>
      </tr>
      <tr>
          <td>Q3_K_M</td>
          <td>~195 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗</td>
      </tr>
      <tr>
          <td>1-bit</td>
          <td>~128 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗ (no room for KV cache / OS)</td>
      </tr>
  </tbody>
</table>
<p>I run the local subset of this benchmark on a <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Minisforum MS-S1 Max, the Strix Halo with 128 GB unified</a>. It doesn&rsquo;t come close. Even the 1-bit quant (~128 GB), which would be useless for coding anyway, would eat the entire unified memory with nothing left for the KV cache or the OS. The 32 GB NVIDIA card doesn&rsquo;t even enter the conversation. There&rsquo;s no quant of M3 that runs at usable quality on either machine. Running &ldquo;a MiniMax&rdquo; locally today would mean grabbing a much smaller, weaker variant (like the <code>minimax-m2-tiny</code> distill), which is a different model, not M3.</p>
<p>Who <strong>can</strong> attempt it? A Mac Studio with 256 GB of unified RAM or more. Q3 (~195 GB) would fit a 256 GB Mac Studio, Q4 (~264 GB) only a 512 GB one. And if someone runs it, the expectation is that the result lands <strong>around the same score as M3 via OpenRouter</strong>, the 78/100 Tier B I already logged in the <a href="/en/2026/06/01/llm-benchmarks-grok-4-3-minimax-m3-opus-4-8/">previous post</a>: same model, same weights, Q3/Q4 quantization shaves off very little. Only where it runs changes, the output is the same. So nobody would do it for the score; they&rsquo;d do it for the sovereignty of running locally. But at that hardware cost, it&rsquo;s not worth it.</p>
<h3>The convergence I see coming (target: ~2030)<span class="hx:absolute hx:-mt-20" id="the-convergence-i-see-coming-target-2030"></span>
    <a href="#the-convergence-i-see-coming-target-2030" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>And this is where the two stories meet. The reason there&rsquo;s no practical local solution today is the same reason for the data center drought: <strong>manufacturing chips is still brutally expensive</strong>, because the whole industry has its focus locked on feeding data centers. Fast memory and cutting-edge silicon go where the money is, and the money is hyperscalers buying by the container-load. So for the next couple of years, give or take, we won&rsquo;t have a cheap, good local solution. The parts aren&rsquo;t available at a developer&rsquo;s-desk price.</p>
<p>But the near future points to a convergence. At some point data center construction hits a ceiling, whether from power, from electrical components, or from demand saturation. When that race slows down, hardware manufacturing capacity frees up and starts catching up with the rest of the market, with cost-effective silicon arriving for running models locally. Today the two extremes don&rsquo;t cut it: the 256 GB Mac Studio is too expensive to be &ldquo;accessible,&rdquo; and the Strix Halo, which is accessible, is still too slow for serious practical use. But the next generation of hardware could close exactly that gap, with the memory and bandwidth of a Mac Studio at the price of a mini PC.</p>
<p>Put the two ends together: open source models improving steadily (the GLM 5.2 jump is the proof) on one side, and local hardware becoming viable as the data center race cools on the other. The day a Tier A GLM or Kimi runs with 200k of context on a desk machine that costs about as much as a decent laptop, the whole compute equation changes. My guess for that meeting point is something like <strong>2030</strong>. Still a way off, but we&rsquo;re heading there.</p>
<h2>Updated ranking<span class="hx:absolute hx:-mt-20" id="updated-ranking"></span>
    <a href="#updated-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kimi K2.7 Code and GLM 5.2 in bold:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Rank</th>
          <th>Model</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: center">Tier</th>
          <th style="text-align: center">RubyLLM OK</th>
          <th>Time</th>
          <th>Cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>Claude Opus 4.7</td>
          <td style="text-align: right">97</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>18m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT 5.4 xHigh (Codex)</td>
          <td style="text-align: right">97</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>22m</td>
          <td>~$16</td>
      </tr>
      <tr>
          <td style="text-align: right">3</td>
          <td>GPT 5.5 xHigh (Codex)</td>
          <td style="text-align: right">96</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>18m</td>
          <td>~$10</td>
      </tr>
      <tr>
          <td style="text-align: right">4</td>
          <td>Claude Opus 4.8</td>
          <td style="text-align: right">95</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Claude Fable 5</td>
          <td style="text-align: right">94</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>24m</td>
          <td>~$11 (est.)</td>
      </tr>
      <tr>
          <td style="text-align: right">6</td>
          <td>Kimi K2.6</td>
          <td style="text-align: right">87</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>20m</td>
          <td>~$0.30</td>
      </tr>
      <tr>
          <td style="text-align: right">6</td>
          <td><strong>GLM 5.2 (Z.ai)</strong></td>
          <td style="text-align: right"><strong>87</strong></td>
          <td style="text-align: center"><strong>A</strong></td>
          <td style="text-align: center">✅</td>
          <td><strong>43m</strong></td>
          <td><strong>subscription</strong></td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>8</strong></td>
          <td><strong>Kimi K2.7 Code</strong></td>
          <td style="text-align: right"><strong>86</strong></td>
          <td style="text-align: center"><strong>A</strong></td>
          <td style="text-align: center">✅</td>
          <td><strong>22m</strong></td>
          <td><strong>~$0.30</strong></td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">83</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>16m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">10</td>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">82</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>14m</td>
          <td>~$0.40</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>16m</td>
          <td>~$0.63</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>DeepSeek V4 Flash</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>3m</td>
          <td>~$0.01</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>MiniMax M3</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>53m (phase 2 DNF)</td>
          <td>~$0.10</td>
      </tr>
      <tr>
          <td style="text-align: right">14</td>
          <td>Grok 4.3</td>
          <td style="text-align: right">72</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>15m</td>
          <td>~$1.74</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: right">71</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$0.15</td>
      </tr>
      <tr>
          <td style="text-align: right">16</td>
          <td>DeepSeek V4 Pro *</td>
          <td style="text-align: right">69</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>22m (DNF)</td>
          <td>~$0.50</td>
      </tr>
      <tr>
          <td style="text-align: right">16</td>
          <td>Kimi K2.5</td>
          <td style="text-align: right">69</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>29m</td>
          <td>~$0.10</td>
      </tr>
      <tr>
          <td style="text-align: right">18</td>
          <td>Xiaomi MiMo V2.5 Pro</td>
          <td style="text-align: right">67</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>11m</td>
          <td>~$0.14</td>
      </tr>
      <tr>
          <td style="text-align: right">19</td>
          <td>GLM 5</td>
          <td style="text-align: right">64</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$0.11</td>
      </tr>
      <tr>
          <td style="text-align: right">20</td>
          <td>Step 3.5 Flash</td>
          <td style="text-align: right">56</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">⚠️ bypass</td>
          <td>38m</td>
          <td>~$0.02</td>
      </tr>
      <tr>
          <td style="text-align: right">21</td>
          <td>Qwen 3.5 35B (local)</td>
          <td style="text-align: right">55</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">✅</td>
          <td>28m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">22</td>
          <td>GLM 4.7 Flash bf16 (local)</td>
          <td style="text-align: right">52</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">✅</td>
          <td>failed</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">23</td>
          <td>GLM 5.1 (Z.ai)</td>
          <td style="text-align: right">46</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>22m</td>
          <td>subscription</td>
      </tr>
      <tr>
          <td style="text-align: right">24</td>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">43</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>60m</td>
          <td>~$0.07</td>
      </tr>
      <tr>
          <td style="text-align: right">25</td>
          <td>MiniMax M2.7</td>
          <td style="text-align: right">41</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>14m</td>
          <td>~$0.30</td>
      </tr>
      <tr>
          <td style="text-align: right">26</td>
          <td>Qwen 3.5 122B (local)</td>
          <td style="text-align: right">37</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>43m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">27</td>
          <td>Qwen 3 Coder Next (local)</td>
          <td style="text-align: right">32</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>17m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">28</td>
          <td>Grok 4.20</td>
          <td style="text-align: right">25</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>8m</td>
          <td>~$0.60</td>
      </tr>
      <tr>
          <td style="text-align: right">29</td>
          <td>GPT OSS 20B (local)</td>
          <td style="text-align: right">11</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>failed</td>
          <td>free</td>
      </tr>
  </tbody>
</table>
<p>* DeepSeek V4 Pro only reaches Tier A (89/100) via DeepClaude. Story in the <a href="/en/2026/05/04/llm-benchmarks-deepseek-unlocked-deepclaude/">dedicated post</a>.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For today&rsquo;s work, nothing changes: Opus 4.8 and/or GPT 5.5 stay the recommendation for serious programming, and the best open source models still stall at the low-Tier-A ceiling with a short context window. Kimi K2.7 Code even slipped a notch over a silly system-prompt oversight, a reminder that &ldquo;new version&rdquo; isn&rsquo;t always &ldquo;better version.&rdquo;</p>
<p>But GLM 5.2 jumping from Tier C to #6 in a single generation is the kind of signal I keep an eye on. Open source isn&rsquo;t stagnant, it&rsquo;s climbing, stubbornly. And when that movement meets hardware getting cheap (the convergence I see around 2030), the story of where we run our coding agents will look very different from today&rsquo;s. For now, it&rsquo;s cloud, it&rsquo;s Opus, it&rsquo;s GPT. But I keep measuring, generation by generation, because the day it turns, it&rsquo;ll turn fast.</p>
<p>The benchmark is open: <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">github.com/akitaonrails/llm-coding-benchmark</a>. Want to see another model in there? Fork it, add an entry to <code>config/models.json</code>, run it, and send the PR.</p>
]]></content:encoded><category>llm-benchmarks</category><category>llms</category><category>local-models</category></item><item><title>How ElevenLabs Was Not Killed by Qwen3 TTS</title><link>https://www.akitaonrails.com/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</guid><pubDate>Thu, 09 Apr 2026 08:30:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;TL;DR — Listen to this and keep reading:&lt;/strong&gt;&lt;/p&gt;
&lt;audio controls preload="metadata" style="width: 100%; max-width: 640px;"&gt;
&lt;source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg"&gt;
Your browser doesn't support the audio element. &lt;a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3"&gt;Download the mp3 here.&lt;/a&gt;
&lt;/audio&gt;
&lt;p&gt;That&amp;rsquo;s the April 6th episode of the &lt;a href="https://www.akitaonrails.com/en/tags/the-m-akita-chronicles/"&gt;The M.Akita Chronicles&lt;/a&gt; podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on &lt;a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener"&gt;Spotify&lt;/a&gt; so you don&amp;rsquo;t miss new episodes like this one.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &amp;ldquo;ElevenLabs killer&amp;rdquo;. There&amp;rsquo;s a &lt;a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener"&gt;Medium piece&lt;/a&gt; claiming it&amp;rsquo;s the first real open source threat to ElevenLabs. There&amp;rsquo;s a &lt;a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener"&gt;post on byteiota&lt;/a&gt; saying 3-second voice cloning beats ElevenLabs. There&amp;rsquo;s an &lt;a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener"&gt;analysis on Analytics Vidhya&lt;/a&gt; calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&amp;rsquo;s just a matter of time.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>TL;DR — Listen to this and keep reading:</strong></p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>That&rsquo;s the April 6th episode of the <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a> podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> so you don&rsquo;t miss new episodes like this one.</p>
<hr>
<p>When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &ldquo;ElevenLabs killer&rdquo;. There&rsquo;s a <a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener">Medium piece</a> claiming it&rsquo;s the first real open source threat to ElevenLabs. There&rsquo;s a <a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener">post on byteiota</a> saying 3-second voice cloning beats ElevenLabs. There&rsquo;s an <a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener">analysis on Analytics Vidhya</a> calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&rsquo;s just a matter of time.</p>
<p>I decided to test it on my own production pipeline, as usual. I built out a whole pipeline on top of Qwen3 TTS 1.7B to generate the weekly podcast for <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a>, and I documented the behind-the-scenes on the <a href="/en/2026/02/18/serving-ai-in-the-cloud-my-personal-tts-behind-the-m-akita-chronicles/">serving-AI-in-the-cloud post</a>. Anyone who wants the detail on cold starts, voice cloning, and sampling parameters that change from one mode to another, that&rsquo;s the place. I&rsquo;m not repeating it all here.</p>
<p>This post asks a different question. After almost two months running that setup in production, shipping an episode every Monday, last night I finally pulled the plug on Qwen3 and switched the whole thing to ElevenLabs v3. Let me tell you why.</p>
<h2>What didn&rsquo;t work on Qwen3<span class="hx:absolute hx:-mt-20" id="what-didnt-work-on-qwen3"></span>
    <a href="#what-didnt-work-on-qwen3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Between February 15 and March 30, I made dozens of commits fine-tuning the podcast flow: prompts, sampling parameters, generation order, leading silence on the voice clone sample, volume normalization, pronunciation of tech acronyms. Fixing Marvin&rsquo;s voice, which was clipping the first syllable because the reference audio started without any lead-in silence. Tuning Akita&rsquo;s voice to sound more confident and assertive. Adding crossfades between section jingles. Sorting out the &ldquo;podcast&rdquo; pronunciation so it didn&rsquo;t come out as &ldquo;pódcast&rdquo;. Making the script generator prefer Portuguese over gratuitous anglicisms so the TTS wouldn&rsquo;t choke. Each of those fixes was a multi-hour session of listening to audio, regenerating, tweaking a parameter.</p>
<p>The result came out acceptable. &ldquo;Acceptable&rdquo; in the sense that I managed to ship every episode without re-recording anything by hand. But if you listen closely, the Qwen3 voice has that unmistakable AI-generating-audio quality. Flat intonation, uniform rhythm. Over long stretches you can feel it&rsquo;s a machine talking. Good enough to ship, but miles away from what you&rsquo;d hear on a professional podcast made by humans.</p>
<p>The worst problem was English pronunciation. My podcast covers tech news, so terms like &ldquo;MCP&rdquo;, &ldquo;RAG&rdquo;, &ldquo;Claude Opus&rdquo;, &ldquo;GPT-5&rdquo;, &ldquo;open source&rdquo; show up in every conversation. Qwen3 took those terms and pronounced them with a Brazilian accent, something like &ldquo;oh-pen-ee-sohrss-eh&rdquo;, that kind of thing. Unlistenable for the audience. The workaround I had to implement was manually mapping in the script-generation LLM prompt which English words to swap for Portuguese equivalents. The prompt today has a whole section split between &ldquo;keep in English&rdquo; (proper nouns, brands, terms already adopted into Brazilian tech lingo) and &ldquo;translate to Portuguese&rdquo; (gratuitous anglicisms), looking roughly 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-markdown" data-lang="markdown"><span class="line"><span class="cl">**REPLACE with Portuguese** (common English words that have natural
</span></span><span class="line"><span class="cl">Brazilian Portuguese equivalents):
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;update&#34; → &#34;atualização&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;release&#34; → &#34;lançamento&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;feature&#34; → &#34;recurso/funcionalidade&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;deploy&#34; → &#34;implantação&#34; or just &#34;colocar em produção&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;trade-off&#34; → &#34;dilema&#34; or &#34;escolha&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;performance&#34; → &#34;desempenho&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;default&#34; → &#34;padrão&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;insight&#34; → &#34;percepção/sacada&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;skills&#34; → &#34;habilidades&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;approach&#34; → &#34;abordagem&#34;
</span></span><span class="line"><span class="cl">- &#34;highlights&#34; → &#34;destaques&#34;</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>I call this &ldquo;scrubbing anglicisms so the TTS doesn&rsquo;t choke&rdquo;. The funny part is I didn&rsquo;t want that level of restriction on my script. I wanted the voice to just pronounce &ldquo;update&rdquo; when &ldquo;update&rdquo; was the natural word. Since the model couldn&rsquo;t, I had to mutilate the podcast&rsquo;s vocabulary so the final result was actually listenable. It&rsquo;s a band-aid, the kind you add hoping to rip it off later when the tech grows up.</p>
<h2>The experience with ElevenLabs v3<span class="hx:absolute hx:-mt-20" id="the-experience-with-elevenlabs-v3"></span>
    <a href="#the-experience-with-elevenlabs-v3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Yesterday afternoon I opened an ElevenLabs account, bought the Pro plan ($99/month), and started experimenting with the <code>eleven_v3</code> model, which shipped in February of this year. Thirty minutes later I had a proof of concept running, and about two hours later the whole podcast system was migrated. The effort gap is an abyss.</p>
<p>The technical details of the migration ended up in an internal project doc, so here&rsquo;s the summary comparison table that actually matters:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Qwen3 TTS 1.7B (old)</th>
          <th>ElevenLabs <code>eleven_v3</code> (current)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quality (Akita)</td>
          <td>Good, cloned from real audio</td>
          <td>Better, same clone but more natural prosody</td>
      </tr>
      <tr>
          <td>Inline emotion in script</td>
          <td>Not supported</td>
          <td><code>[sighs]</code>, <code>[sarcastically]</code>, <code>[excited]</code>, <code>[laughs]</code>, works in pt-BR</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>5 to 15 min spinning up a RunPod GPU before each run</td>
          <td>Zero, HTTPS call with immediate response</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~1× real time (serialized)</td>
          <td>~6× real time with concurrency 4</td>
      </tr>
      <tr>
          <td>Wall clock for a 28-min episode</td>
          <td>~25 to 30 min</td>
          <td><strong>~4 min</strong></td>
      </tr>
      <tr>
          <td>Operational surface</td>
          <td>RunPod, Docker, FastAPI, Qwen weights, GPU billing</td>
          <td>One env var (<code>ELEVENLABS_API_KEY</code>)</td>
      </tr>
      <tr>
          <td>Cost per episode</td>
          <td>~$0.08 of GPU</td>
          <td>~$2.70 in ElevenLabs credits</td>
      </tr>
  </tbody>
</table>
<p>Look at the last line. Qwen3 costs less than ten cents of a dollar per episode. ElevenLabs costs almost thirty times more. And it&rsquo;s still worth it. The other lines on the table solve problems that were eating hours of my week. I no longer need to write code to scale GPUs in the cloud, I don&rsquo;t need to wait for the machine to spin up every time, I don&rsquo;t need to babysit a model or rebuild a Docker image when the weights change. The operation became a single line of configuration.</p>
<p>And here&rsquo;s the interesting part: the inline emotion tags. The v3 model accepts markers like <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[dryly]</code>, <code>[excited]</code>, and changes the delivery accordingly. This works in more than 70 languages, including Brazilian Portuguese. It transformed script generation, because now I can ask the LLM that writes the script to drop emotional tags at the right moments, which gives a liveliness Qwen3 couldn&rsquo;t deliver in a million years. A concrete example of what comes out of the script later:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AKITA: Isso é simples. [dismissive] Quem ainda acredita que Bitcoin
vai morrer não tá prestando atenção.
MARVIN: [sighs] Mais uma semana, mais uma leva de devs confiando
cegamente em pacotes npm. Previsível.</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>I even have two separate tag palettes, one for Akita (expressive but controlled, uses <code>[excited]</code>, <code>[dismissive]</code>, <code>[emphatic]</code>) and another for Marvin (stoic, just <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[tired]</code>, <code>[dryly]</code>). That&rsquo;s all encoded in the script generation prompts so the LLM knows which character is allowed to use what.</p>
<h2>About Marvin&rsquo;s voice<span class="hx:absolute hx:-mt-20" id="about-marvins-voice"></span>
    <a href="#about-marvins-voice" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For listeners who are already used to Marvin&rsquo;s voice, don&rsquo;t worry: I cloned him on ElevenLabs using the same audio sample I had used to train on Qwen3. It&rsquo;s the same voice. It just sounds even better now, because the ElevenLabs model captures nuance and prosody that Qwen3 couldn&rsquo;t deliver.</p>
<h2>Listen to it and tell me<span class="hx:absolute hx:-mt-20" id="listen-to-it-and-tell-me"></span>
    <a href="#listen-to-it-and-tell-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To prove the talk is real, here&rsquo;s Monday&rsquo;s episode, April 6th, already generated with the new pipeline:</p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>If you&rsquo;re already a podcast listener on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> and you&rsquo;ve heard previous episodes made with Qwen3, compare them and tell me in the comments whether you notice the difference or whether it&rsquo;s the same to you. I&rsquo;m genuinely curious how much of this is my trained ear after hours of listening to TTS audio and how much is an obvious difference for a casual listener.</p>
<p>Starting next week, every episode of the podcast will be generated by ElevenLabs v3. The newsletter is already plugged into the new pipeline, the scheduled jobs to pre-warm the RunPod GPU have been disabled, and the legacy Qwen3 code stays in the repo as plan B in case v3 starts acting up chronically. Two file edits and I can switch back. I probably never will.</p>
<h2>The part about dubbing the YouTube videos<span class="hx:absolute hx:-mt-20" id="the-part-about-dubbing-the-youtube-videos"></span>
    <a href="#the-part-about-dubbing-the-youtube-videos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the hook for the second half of this post. On the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary piece I published earlier today</a>, I told the story of how Claude Code translated nearly half my blog to English over a weekend. In the same spirit, I went after dubbing the videos on the <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando</a> channel.</p>
<p>The channel has 146 episodes, around 96 hours of technical content in Portuguese, and more than 500k subscribers. I already had the subtitles translated (one curated <code>.srt</code> per episode), and YouTube even offers automatic English dubbing. But the result is like Google Translate circa 2015: gets the idea across, nobody wants to listen for very long.</p>
<p>I tested ElevenLabs&rsquo; three voice approaches and only one worked. Speech-to-Speech converts a voice but doesn&rsquo;t translate. The Dubbing API does translate but creates the voice on its own, with no way to force a specific clone. Only Text-to-Speech could solve this: take the English <code>.srt</code> I already had, send each block to the TTS endpoint with my cloned voice, and assemble the audio aligned to the original video.</p>
<p>But having a translated <code>.srt</code> is not enough. Raw subtitle translation doesn&rsquo;t survive TTS. Passages with source code, URLs, hex hashes, lists of shell commands — the model switches to spelling mode and the audio comes out at twice the expected duration. Translations that run too long blow past the time window and need to be condensed so the voice doesn&rsquo;t sound like an auctioneer. Truncated SRTs need to be completed. And after each fix, you re-run the pipeline, listen, find the next problem, fix it, re-run. The whole thing was a cycle of interruptions, manual corrections, and re-runs — a far cry from the &ldquo;press a button and get a dub&rdquo; fantasy.</p>
<p>The big challenge was the accent. My cloned voice was trained on Brazilian Portuguese audio, so when it tries to speak English, the thick accent comes through. ElevenLabs has an <code>[American accent]</code> tag that works on v3, but on top of a voice trained in another language it&rsquo;s weak — the Brazilian accent still bleeds through. The fix was to train a second voice of mine, English only. I recorded a few minutes in my best American accent, uploaded it as a separate Instant Voice Clone in my account, named it &ldquo;Akita English&rdquo;, and set the pipeline to use that voice by default. The result comes out more natural, no tag needed, and the voice identity is still mine.</p>
<h3>The honest ceiling on my English voice quality<span class="hx:absolute hx:-mt-20" id="the-honest-ceiling-on-my-english-voice-quality"></span>
    <a href="#the-honest-ceiling-on-my-english-voice-quality" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Let&rsquo;s be frank. I&rsquo;m a Brazilian who speaks English, not a native speaker, and I don&rsquo;t have the time or patience to drill pronunciation for hours. A clone can only ever be as good as the source sample — if the source is a non-native reading a script, the clone inherits every imperfection. No clone is going to make me sound like &ldquo;Fabio speaking perfect English&rdquo; because that sound doesn&rsquo;t exist for the model to learn from.</p>
<p>To get to the version that&rsquo;s running today, I recorded five different training samples throughout the day, each with a different rhythm and script, and ran an A/B battery against the original Portuguese. None sounded native, and that was never the goal. The realistic bar was more modest: sound recognizably like me, read technical content without stumbling, and carry a full one-hour episode without wearing the listener out. The winner ended up being the first iteration, the original &ldquo;Akita English&rdquo;. Far from perfect, but good enough to ship. Swapping it out later is literally one line in the config file.</p>
<p>What surprised me most along the way was realizing that the rhythm of the training sample matters more than the timbre. The clone learns the cadence of whoever recorded it: iteration 5 came out 25% slower than iteration 2 reading the exact same text, purely because I recorded that particular sample more slowly. For dubbing tech videos, the right target is the rhythm of a &ldquo;YouTube tech explainer&rdquo; — brisk, energetic, around 150 words per minute. No audiobook pacing.</p>
<h3>How I align audio to the original video<span class="hx:absolute hx:-mt-20" id="how-i-align-audio-to-the-original-video"></span>
    <a href="#how-i-align-audio-to-the-original-video" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A non-obvious point: spoken English tends to be longer than equivalent Portuguese. If you just generate the audio for each subtitle and paste it at the original timestamp, the dub drifts later and later. With 30 minutes of video you&rsquo;re already several seconds out of sync.</p>
<p>My approach was to attack the problem in layers. The first version split the SRT into smaller blocks, called &ldquo;chunks&rdquo;. Each chunk maxed out at 700 characters (so v3 wouldn&rsquo;t hallucinate) and could only cut at a sentence boundary, never mid-sentence. A simple algorithm accumulated subtitles into a buffer until it was about to overflow, then walked back looking for the last cue whose text ended with <code>.</code>, <code>!</code>, or <code>?</code>, and flushed only up to there. The cues still in the buffer got rolled into the next chunk. Each chunk also stored the start and end timestamps of the subtitles it covered, so the assembly step knew exactly where to drop that piece of audio later.</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"># For every incoming cue, check whether adding it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># would push the buffer past the char cap.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cue</span> <span class="ow">in</span> <span class="n">cues</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">projected</span> <span class="o">=</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">cue</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">projected</span> <span class="o">&gt;</span> <span class="n">max_chars</span> <span class="ow">and</span> <span class="n">buf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Walk back for the last cue in the buffer that</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># ends in &#39;.&#39;, &#39;!&#39;, &#39;?&#39;, or &#39;…&#39;.</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentence_end</span> <span class="o">=</span> <span class="n">last_sentence_end_index</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">sentence_end</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">sentence_end</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Run-on sentence longer than max_chars with no</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># terminator. Rare, but happens. Warn and cut.</span>
</span></span><span class="line"><span class="cl">            <span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;run-on sentence &gt; </span><span class="si">%d</span><span class="s2"> chars — &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;splitting mid-sentence as a last resort&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">max_chars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cue</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Soft-break: if the buffer is already at least 60% full</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># AND the current cue ended a sentence, flush now. Keeps</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># chunks balanced around 60-100% of max.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cue</span><span class="o">.</span><span class="n">ends_sentence</span> <span class="ow">and</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">max_chars</span> <span class="o">*</span> <span class="mf">0.6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</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>Technically, this was the turning point: instead of letting the dub follow the visual structure of the subtitle file, the chunker started following the real paragraph structure of the script. I won&rsquo;t retell the whole story of how I got there and why it eventually forced a mass rerender, because I come back to that later in the section about the problems that only showed up after the first upload. The technical point here is simple: once the window represented a real spoken paragraph, the audio started sounding natural, and the stretch target could move from 92% to 95%.</p>
<p>Once the chunking is done, there&rsquo;s still the problem that English runs longer than the Portuguese equivalent when spoken. The trick here is predicting whether a chunk is going to blow past its target window <strong>before</strong> generating the audio. If the ratio <code>characters / 16 chars-per-second</code> already exceeds the target duration by a margin, the pipeline passes the <code>speed</code> parameter directly to the ElevenLabs API, asking the model to natively generate faster speech. This preserves prosody way better than compressing afterwards. The ceiling is 1.15× (beyond that the voice starts sounding rushed).</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"># before calling the API, predict if it will overrun</span>
</span></span><span class="line"><span class="cl"><span class="n">expected_sec</span> <span class="o">=</span> <span class="n">char_count</span> <span class="o">/</span> <span class="n">EXPECTED_CHARS_PER_SEC</span>  <span class="c1"># 16 chars/s</span>
</span></span><span class="line"><span class="cl"><span class="n">predicted_ratio</span> <span class="o">=</span> <span class="n">expected_sec</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">voice_speed</span> <span class="o">=</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">predicted_ratio</span> <span class="o">&gt;</span> <span class="n">PREEMPTIVE_SPEED_THRESHOLD</span><span class="p">:</span>   <span class="c1"># 1.05</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_speed</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">predicted_ratio</span> <span class="o">*</span> <span class="mf">0.98</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="n">PREEMPTIVE_SPEED_MAX</span><span class="p">)</span>         <span class="c1"># cap at 1.15</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_settings</span><span class="p">[</span><span class="s2">&#34;speed&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">voice_speed</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>Even with preemptive <code>speed</code> turned on, sometimes the generated audio still comes out a tiny bit longer than the target window. That&rsquo;s what the second safety net is for: measure the actual audio with <code>ffprobe</code> after it&rsquo;s generated, compare to the target window, and apply the <code>ffmpeg</code> <code>atempo</code> filter to compress in post if it exceeded the 2% tolerance, capped at 1.20×. Combining native <code>speed</code> (1.15×) with <code>atempo</code> (1.20×) gives an effective compression ceiling of 1.38×, enough to fit naturally even in the worst cases without breaking quality.</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"># after generating the chunk</span>
</span></span><span class="line"><span class="cl"><span class="n">actual</span> <span class="o">=</span> <span class="n">ffprobe_duration</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ratio</span> <span class="o">=</span> <span class="n">actual</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">ratio</span> <span class="o">&gt;</span> <span class="n">FIT_TOLERANCE</span><span class="p">:</span>       <span class="c1"># 1.02</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># compress the audio without touching pitch</span>
</span></span><span class="line"><span class="cl">    <span class="n">ffmpeg_atempo</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">ratio</span><span class="p">,</span> <span class="n">FIT_MAX_ATEMPO</span><span class="p">)</span>   <span class="c1"># 1.20</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply_atempo</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">,</span> <span class="n">ffmpeg_atempo</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>When a generated chunk comes out shorter than the target window, the pipeline slightly stretches the audio (without affecting pitch) to reduce the ugly silence after it ends. The goal isn&rsquo;t to fill the whole window — a natural pause between sentences is fine — just to smooth over the worst offenders that would feel like &ldquo;cut audio&rdquo;.</p>
<p>On a big episode, 95 minutes, 194 chunks, the total cumulative drift came out to -0.7%. About 43 seconds of drift across the entire episode, imperceptible while you&rsquo;re watching.</p>
<p>What saved the budget on this architecture is that each chunk lives on disk as its own <code>.mp3</code>. The pipeline keeps a manifest with the normalized text of every chunk, and before calling the ElevenLabs API, it compares the current text against the cache. If the text hasn&rsquo;t changed, it reuses the existing audio without spending a single credit. If I rewrite a problematic cue, only the chunks affected by that cue get regenerated — the rest of the episode stays untouched.</p>
<p>This is what made the iterations viable. I&rsquo;d run the batch, listen to sections, spot a problem (translation too long, a code snippet the TTS couldn&rsquo;t pronounce, a chunk boundary that landed at a bad spot), fix the SRT or tweak the chunker parameters, and re-run. Each re-run consumed a fraction of the credits and time of the original run, because only the changed chunks were regenerated. Without this cache, every iteration would have cost nearly as much as the first pass, and the total batch cost would have been two or three times higher.</p>
<p>That chunking change also had a heavy cache and cost impact, but I&rsquo;ll leave that part for the later section where I explain the actual retracing and rerender cycle. Here, the important point is just that it was the right technical move.</p>
<h3>When <code>atempo</code> can&rsquo;t save you: rewriting cues before TTS<span class="hx:absolute hx:-mt-20" id="when-atempo-cant-save-you-rewriting-cues-before-tts"></span>
    <a href="#when-atempo-cant-save-you-rewriting-cues-before-tts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything is about window alignment. There&rsquo;s a kind of cue that breaks the whole pipeline, where neither preemptive <code>speed</code> nor post-process <code>atempo</code> can save it, and I only ran into it once I started running the batch over the more technical episodes of the channel.</p>
<p>The problem is this. The v3 reads at about 16 characters per second when you hand it normal conversational text. But when you hand it a cue stuffed with a literal URL, a hex hash, a binary string, a long sorted number list, or a shell code block, the model shifts into &ldquo;spelling it out letter by letter&rdquo; mode and drops to about 9 characters per second. A 500-character cue that was supposed to turn into 30 seconds of audio comes out at 55. The sanity check rejects it (because it&rsquo;s past 1.8×), the automatic retry tries the same 500 characters again, the five retries all fail in a row, and the chunk gets stuck.</p>
<p>And this did not happen by accident. I used to write those passages that way because I knew the same scripts would later become blog posts, so it made sense to leave the raw URL, the full command, the complete hash, the whole technical detail there for the reader. In Portuguese that worked fine because it matched my original workflow exactly: record the video, then publish the matching text on the blog. What I had never prepared for was the next step, turning that same material into automated English dubbing. I only realized that conflict when the pipeline started failing for real.</p>
<p>I hit this first on ep052, the Ubuntu beginner&rsquo;s guide, where two cues carried <code>github.com</code> URLs, <code>hkp://keyserver.ubuntu.com</code> URLs, and a 40-character GPG hash. Trying to fix that in post-processing is a waste of time. The 1.20× <code>atempo</code> ceiling will never compress 55 seconds into 30, and even if it did, the result would be an unlistenable chipmunk. The fix has to land upstream, in the text itself.</p>
<p>The solution was to rewrite the cue so it describes what the command does instead of showing the literal 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-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- Run: apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
</span></span></span><span class="line"><span class="cl"><span class="gd">-   --recv-keys 0A6A3E7F79F93EF8AAB9E92BAEBB74C8B5A1E44D
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Run the full command in the video description to import the
</span></span></span><span class="line"><span class="cl"><span class="gi">+ signing key from the Ubuntu keyserver.
</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>The on-screen English caption still shows the full command, so anyone reading the caption sees exactly what they need to type. The dubbed audio describes the intent in spoken language, which is what the TTS can deliver without choking. The instruction to the viewer stays intact, the characters-per-second ratio goes back to the expected 16, and the generated audio fits the original window without needing any stretch or compression in post.</p>
<p>I went hunting for cues like this across the whole batch. I scanned from ep052 through ep146 looking for cues with a high density of &ldquo;hard&rdquo; characters (digits, brackets, operators, long URLs, binary, hex) and ended up rewriting around 30 cues across 13 episodes:</p>
<ul>
<li>ep052 Ubuntu for devs: GitHub URLs and a 40-character GPG hash</li>
<li>ep091 Hello World in C: binary examples</li>
<li>ep095 640kB memory: address wrap, segment-offset</li>
<li>ep106 CodeMiner: spelled-out email address</li>
<li>ep113 Compression: pure binary strings, 75% hard characters</li>
<li>ep115 SQL Server: long sorted number list</li>
<li>ep120 Internet: dotted IP addresses</li>
<li>ep121 Sockets: two JavaScript code blocks</li>
<li>ep122 Proxies: Chrome User-Agent headers</li>
<li>ep123 Secure networking: shell one-liners, Docker flags</li>
<li>ep126 Gentoo: C <code>chroot</code> demo</li>
<li>ep136 Containers: GitHub release URL</li>
<li>ep144 Cryptography: signing key address</li>
</ul>
<p>The pattern was always the same: keep the caption showing the code, URL, or hash verbatim, and rewrite the spoken text so the narrator describes what it does. I could have automated the rewrite by letting an LLM read each cue and propose a substitution on the fly, but it felt safer to review them all by hand. It&rsquo;s exactly the kind of thing where the model decides to &ldquo;improve&rdquo; a command to make it cleaner and ships with broken shell. The manual sweep took about two hours. The automated version would have cost the same time in review, with the bonus anxiety of having shipped a mangled command by accident.</p>
<p>There was also one weird structural case on ep146, about Docker Compose. Two cues had glued themselves together because of a missing blank line in the SRT, and <code>pysrt</code> was treating them as one giant cue. The TTS never even got to process it — the chunker choked first. I fixed it by hand, adding the missing blank line. One-character fix, half an hour to track down.</p>
<h3>Reconstructing truncated SRTs<span class="hx:absolute hx:-mt-20" id="reconstructing-truncated-srts"></span>
    <a href="#reconstructing-truncated-srts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Another trap showed up when I went to run the full batch: three episodes had English captions that were too short relative to the actual video length. Ep056, the Rails episode, was the most absurd case. Nineteen minutes of caption for an 80-minute video. Sixty-two minutes of content with no caption at all. Ep057 (WSL 2) was missing 14 minutes, and ep068 (Git Direito) was missing 2.</p>
<p>This is a leftover from my old translation workflow. At some point I started hand-revising the caption, stopped partway through, and the <code>.en.srt</code> file got saved truncated at the spot where I stopped. You can&rsquo;t dub an 80-minute video with a 19-minute caption. The chunker produces audio up to where it can read and then simply has no idea what to do with the rest of the video.</p>
<p>The fix became a new script. It diffs the truncated <code>.en.srt</code> against the <code>.pt-orig.srt</code> (the raw YouTube auto-caption, which always covers the whole video), picks up the cues that only exist in Portuguese, sends them to Claude Sonnet 4.6 with a strict JSON schema asking for cue-by-cue translation, and pastes the result back at the tail of the English file. The schema is the detail that matters. When I tried asking for the response as plain text in <code>N|text</code> format, Claude was dropping about 20% of the cues along the way. With a strict JSON schema, the drop rate fell to zero across the three repairs.</p>
<p>The numbers:</p>
<ul>
<li>ep068 Git Direito: +50 cues / 2 minutes reconstructed</li>
<li>ep057 WSL 2: +368 cues / 14 minutes</li>
<li>ep056 Rails: +1445 cues / 62 minutes (plus dropping 5 fake cues the old translator had invented at the tail to plug the gap)</li>
</ul>
<p>After the repairs, the three episodes joined the batch normally and got dubbed like any other. The kind of tool you hope you never need, but when you need it, it&rsquo;s worth writing once and closing the problem for good.</p>
<h3>Automatic emotion tags<span class="hx:absolute hx:-mt-20" id="automatic-emotion-tags"></span>
    <a href="#automatic-emotion-tags" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>While I was testing, I decided to add one more step to the pipeline: an &ldquo;emotion tagger&rdquo; that reads the English SRT before it goes to TTS and inserts tags like <code>[sarcastic]</code>, <code>[thoughtful]</code>, <code>[emphatic]</code>, <code>[deadpan]</code> at spots where a human narrator would naturally shift tone. The idea is to mimic what a professional voice actor would do — hit the emphasis at the right moments — without turning the video into a theater of overblown emotions.</p>
<p>This part is tricky for two reasons. First: letting an LLM loose on your SRT runs a real risk of it &ldquo;improving&rdquo; the text (swapping a word here, rewriting a sentence there) and you ending up with a dub that doesn&rsquo;t match the caption. Second: LLMs love to overdo it. Ask them to tag emotion and they&rsquo;ll put a tag on every other line. To guard against both, I pinned a small allow-list of tags and run a round-trip validation after Claude&rsquo;s response comes back:</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="n">ALLOWED_TAGS</span><span class="p">:</span> <span class="nb">frozenset</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="nb">frozenset</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sarcastic]&#34;</span><span class="p">,</span> <span class="s2">&#34;[thoughtful]&#34;</span><span class="p">,</span> <span class="s2">&#34;[emphatic]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[deadpan]&#34;</span><span class="p">,</span> <span class="s2">&#34;[serious]&#34;</span><span class="p">,</span> <span class="s2">&#34;[amused]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sighs]&#34;</span><span class="p">,</span> <span class="s2">&#34;[exasperated]&#34;</span><span class="p">,</span> <span class="s2">&#34;[confident]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[matter-of-fact]&#34;</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></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_tagged</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Ensure the tagged SRT preserves the original with no drift.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">original</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="s2">&#34;cue count mismatch&#34;</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">o</span><span class="p">,</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Index and timestamp must match byte for byte.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">o</span><span class="o">.</span><span class="n">index</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">index</span> <span class="ow">or</span> <span class="n">o</span><span class="o">.</span><span class="n">timestamp</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">timestamp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: header drift&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Any tag outside the allow-list invalidates the whole response.</span>
</span></span><span class="line"><span class="cl">        <span class="n">bad</span> <span class="o">=</span> <span class="n">find_disallowed_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">bad</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">bad</span><span class="si">}</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="c1"># The text with the tags stripped must be byte-identical to</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># the original. If the LLM swapped a word, rewrote anything,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># validation fails and the response is rejected.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> <span class="o">!=</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: text drift&#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>When validation fails, the pipeline throws the response away and re-requests (or, if it keeps failing, ships the SRT untagged). On tag density, the prompt sent to Claude explicitly says: aim for <code>N/10</code> tags in a SRT with N cues, hard floor of 1 tag per 12 cues, hard ceiling of 1 tag per 7 cues, distribution balanced across the four quarters of the episode, and no two tagged cues within 4 cues of each other. That rule set was the sixth iteration — the five before it either blanketed everything in tags or loaded them all into the first quarter.</p>
<h3>Don&rsquo;t let v3 hallucinate<span class="hx:absolute hx:-mt-20" id="dont-let-v3-hallucinate"></span>
    <a href="#dont-let-v3-hallucinate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A critical detail that only shows up in production: ElevenLabs v3 tends to hallucinate when you send very long blocks of text. I&rsquo;ve seen blocks where the input text was around 1,500 characters and the model generated <strong>nine minutes</strong> of audio when the expected duration was a minute and a half. The model just decides to keep talking on its own, inventing content.</p>
<p>The official ElevenLabs docs recommend keeping each call under 800 characters to avoid this. I went more conservative and cut at 700, always at sentence end (never mid-sentence, because a mid-sentence split sounds horrible when concatenated). On top of that, I bumped <code>stability</code> to 0.9 (Robust mode, more stable but less responsive to emotion tags), turned on <code>apply_text_normalization</code> so the model pronounces numbers and acronyms correctly, and added a sanity check that rejects any generated audio longer than 1.8× the expected duration.</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="n">DEFAULT_VOICE_SETTINGS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stability&#34;</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>          <span class="c1"># Robust mode</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;similarity_boost&#34;</span><span class="p">:</span> <span class="mf">0.95</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;style&#34;</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;use_speaker_boost&#34;</span><span class="p">:</span> <span class="kc">True</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></span><span class="line"><span class="cl"><span class="n">EXPECTED_CHARS_PER_SEC</span> <span class="o">=</span> <span class="mf">16.0</span>
</span></span><span class="line"><span class="cl"><span class="n">CHUNK_SANITY_MAX_FACTOR</span> <span class="o">=</span> <span class="mf">1.8</span>   <span class="c1"># reject if actual &gt; 1.8× expected</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>With those mitigations in place, a 95-minute episode (194 blocks) ran start to finish with zero hallucinations. Exactly one block failed mid-run on a transient 502 error from the API, which the automatic retry caught on the next attempt.</p>
<h3>Mastering for YouTube<span class="hx:absolute hx:-mt-20" id="mastering-for-youtube"></span>
    <a href="#mastering-for-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>YouTube doesn&rsquo;t publish an official number, but industry consensus is that its playback loudness normalization targets -14 LUFS. Deliver audio louder than that and YouTube turns it down; deliver it quieter and it leaves it alone. To land on the target exactly, the pipeline runs <code>ffmpeg</code>&rsquo;s <code>loudnorm</code> filter in two passes. The first pass measures the entire audio and prints the stats (<code>input_i</code>, <code>input_tp</code>, <code>input_lra</code>, <code>input_thresh</code>) as a JSON blob on stderr. The second pass reads those stats back in and applies a linear static gain to land precisely at -14 LUFS, -1.5 dBTP:</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"># Pass 1: measure</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9:print_format=json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f null -
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pass 2: apply linear gain with the measured values</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_I=-18.3:measured_TP=-3.1:measured_LRA=5.4\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_thresh=-28.7:offset=1.2:linear=true&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -ar <span class="m">48000</span> -ac <span class="m">2</span> -c:a pcm_s16le final_en_mastered.wav</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>Two passes instead of one because single-pass mode runs in dynamic compression and ends up &ldquo;pumping&rdquo; the gain during silent stretches. With <code>linear=true</code> on pass 2, the gain stays static on top of the pass-1 measurements, so no pumping. The result lands within ±0.1 LU of the target, which is effectively inaudible. The <code>highpass=f=80</code> filter in front kills HVAC rumble and mains hum below 80 Hz, which the human ear doesn&rsquo;t catch but will move peak measurements around.</p>
<h3>The point where I thought it was done. And it wasn&rsquo;t.<span class="hx:absolute hx:-mt-20" id="the-point-where-i-thought-it-was-done-and-it-wasnt"></span>
    <a href="#the-point-where-i-thought-it-was-done-and-it-wasnt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where the most annoying, and most educational, part of the project starts.</p>
<p>I really thought I had nailed it on the first big pass. The pipeline closed all 146 episodes, I uploaded everything, and it looked done. The durations matched, the files were all there, and the whole process had finally crossed the complete batch of nearly 96 hours of video. But it was exactly after that upload that I started noticing problems I simply had not anticipated.</p>
<p>The first scare was the jingle situation. I already knew many Akitando episodes begin with that short instrumental intro before I start talking, but I underestimated how much trouble it would cause. YouTube&rsquo;s <code>.srt</code> is almost useless here, because a jingle is not speech. Sometimes it marks <code>[Music]</code>, sometimes it gives you nothing useful, sometimes the window is just wrong. In the first assembly pass, some episodes lost the jingle entirely, some got it doubled, and in others the source-fill from the original audio already contained the right jingle and my splice came in on top of it, pushing everything else forward. This was the kind of bug you only catch when you sit down and actually listen to the final mastered file, not when you just look at total duration.</p>
<p>The fix was to stop hoping the subtitle would tell me where the jingle was and go straight to the source. I built a detector using the original video audio itself plus reference jingles, with an audit and repair pass on top of the mastered outputs already generated. That eventually became a small subsystem of its own: detector, audit, cached reassembly, exact-window repair. At the end of that detour, the set of changed episodes collapsed into a well-defined reupload bucket and the jingle problem finally came under control. It was one of those classic production moments: the first solution looked good enough until it hit the whole archive.</p>
<p>Then came the second punch, and this one was more expensive: I had forgotten a basic fact about YouTube subtitles. SRT is built to be read on screen, not to become spoken script. So an idea that was a full paragraph in my original text showed up split into several small cues, each with its own micro time window. In the first version of the pipeline I was using those cues as the generation unit for ElevenLabs. The result was TTS audio that was coherent locally, but the final assembly inserted artificial silences in the middle of sentences. Technically aligned, humanly weird.</p>
<p>Fixing that hurt because it meant walking several steps back. Instead of respecting the cue structure of the SRT, I had to go back to my original scripts and rebuild chunking around the paragraphs I had actually read when recording the videos. Almost every Akitando episode has a matching blog post with a <code>## Script</code> section, and that is what saved the project. Because I had been meticulous back when I made those videos — writing the scripts first, reading exactly that on camera, and archiving both the videos and the scripts — I had a source of truth. That made it possible to map the script paragraphs onto the timings in the <code>.pt-BR.srt</code>, regroup the <code>.en.srt</code> on top of that, and regenerate the chunks in the right structure.</p>
<p>The problem is that this destroys the previous cache. Even when the text barely changes, if the chunk boundary changes then the TTS call changes too: different context, different prosody, different beginning, different ending. So this was not a touch-up. It was a full regeneration of everything again. That was the point where the cost started escaping my original estimate.</p>
<p>And just when I thought now it really was over, the third hit landed: external clips. Many videos on the channel include inserted footage from other sources. The Ruby on Rails episode, for example, has 37signals clips, Apple material, commercials, external inserts in the middle of the narrative. YouTube&rsquo;s <code>.srt</code> did not know what to do with that. In some places it simply hallucinated text, in others the timings were completely wrong, and in others the original speech had nothing to do with the subtitle I was using as ground truth. Again: if you only look at the automation, it looks fine; if you actually listen to the final master, it isn&rsquo;t.</p>
<p>The fix, again, was to leave the subtitle behind and go back to the original material. I built an external-clip window detector based on alignment gaps between transcript, <code>.pt-BR.srt</code>, speechless cues and chunks that crossed those windows. Then, instead of trying to dub what made no sense to dub, the process started recovering the original audio from the source video exactly in those stretches and filling the master with the correct source audio there. That&rsquo;s what fixed the 37signals, Apple Education, Mac vs PC commercial and other inserted segments I had forgotten were even there.</p>
<p>That was the biggest lesson from the second half of this project: automation takes you very far, but the full archive always has more dark corners than you imagine on the first pass.</p>
<h2>The dubbing batch numbers (and why it hurt more than I expected)<span class="hx:absolute hx:-mt-20" id="the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected"></span>
    <a href="#the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I need to correct myself again. I didn&rsquo;t just miscalculate the first bill. I also overestimated how confident I should have been after the first full run.</p>
<p>When I upgraded to ElevenLabs Business, I thought I would finally have plenty of headroom. In practice, I didn&rsquo;t. The latest dashboard screenshot shows <strong>10,046,279 credits consumed out of a limit of 11,000,000</strong>, leaving <strong>953,721</strong>:</p>
<p><img src="/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/elevenlabs-credits-dashboard.png" alt="ElevenLabs dashboard showing 10,046,279 credits consumed out of 11,000,000"  loading="lazy" /></p>
<p>In plain English: I burned through more than 91% of the Business workspace limit. And that helps explain what &ldquo;10 million credits&rdquo; really means in this context. It does not mean &ldquo;I rendered 146 episodes once&rdquo;. It means rendering 146 episodes, discovering a structural problem, going back, fixing it, regenerating chunks, discovering another structural problem, going back again, rebuilding masters, auditing jingles, repairing external clip windows, uploading again. Every extra round costs money. And when the change invalidates the cache, it costs a lot.</p>
<p>The cost also doesn&rsquo;t stop at ElevenLabs. All the subtitle curation, problematic cue rewrites, truncated SRT reconstruction, jingle audits, external clip detector work and the rest ran through Claude Code on the Claude Max 20× plan. Combined with the mass blog translation to English that I described in the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary post</a>, the Claude Max 20× weekly limit hit 100%, with more than BRL 300 in extra usage on top. If you think generative AI becomes free once you&rsquo;ve signed up for the plan, the invoices disagree.</p>
<p>Even so, I still think it&rsquo;s worth it. We&rsquo;re talking about 146 episodes, almost 96 hours of technical content, more than 5 years of channel archive. Doing that manually in a studio, with an actor, direction, pickups and human review on everything, would cost a completely different order of magnitude.</p>
<p>What changed was how I think about the project. At the start I was looking at this as &ldquo;one very large batch&rdquo;. In the end it was something else. It was digging through a whole archive, finding hidden exceptions, fixing old rot, rebuilding masters, rerendering, relistening.</p>
<p>The first full batch took a little under two days and made me think I had solved everything. I hadn&rsquo;t. The second pass, with paragraph-aware chunking, was already expensive. The third, with jingle repairs and mastered-output fixes, cost more again. And the fourth hit came from the external clips I had not modeled correctly in the first automation pass.</p>
<p>But now, finally, I think it&rsquo;s done. Or rather: I hope it&rsquo;s done. Everything has been uploaded again, for the last time. It isn&rsquo;t perfect in the absolute sense of the word. It&rsquo;s as good as I can get it with scripts, audits and automation, without descending into manual-intervention hell episode by episode.</p>
<p>And if all of this was possible, it&rsquo;s because years ago I had the discipline to produce the videos the right way: write first, read the script when recording, and archive the materials afterward. Without the original videos and without the original scripts, I would have had no way to discover where the <code>.srt</code> was lying, where the timing was wrong, where the external clip started, where the original paragraph began and ended. That old organization is what made it possible to repair the project now.</p>
<p>So yes: the dub for all 146 episodes is done. Not perfect. But closed. At least, I sincerely hope this time it really is.</p>
<h2>The first test, watch it<span class="hx:absolute hx:-mt-20" id="the-first-test-watch-it"></span>
    <a href="#the-first-test-watch-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I made a test video to prove the thing works. Two caveats up front: first, the embed below already loads with English captions on by default, so that side is solvable via URL. Second, the audio is annoying: YouTube removed the audio track selector from embedded players back around March of this year. So inside the embed you won&rsquo;t find that option in the gear menu at all, it just plays in Portuguese. To actually hear the English dub, click <strong><a href="https://www.youtube.com/watch?v=QNLd8TZ_JQc"target="_blank" rel="noopener">watch directly on YouTube</a></strong> — the main YouTube watch page still has the audio track switcher in the gear menu.</p>
<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/QNLd8TZ_JQc?cc_load_policy=1&amp;cc_lang_pref=en&amp;hl=en"
    title="Akitando dubbed in English (test)"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
<p>If you&rsquo;re used to YouTube&rsquo;s automatic dubbing or to the AI dubbing on TikTok, compare them. The difference is glaring. The voice is mine, with a worked-over American accent, and the sync with the original video stays within 1 to 2% cumulative drift, practically imperceptible over 95 minutes of video.</p>
<h2>And the missing piece: translating the thumbnails<span class="hx:absolute hx:-mt-20" id="and-the-missing-piece-translating-the-thumbnails"></span>
    <a href="#and-the-missing-piece-translating-the-thumbnails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>While I was closing this post, the penny dropped: I&rsquo;d left a loose end. All 146 thumbnails on my channel are in Portuguese, many of them with big all-caps titles like &ldquo;9 DICAS PARA PALESTRANTES&rdquo; or &ldquo;7 RECOMENDAÇÕES DE SHOWS PARA PESSOAS DE TECH&rdquo;. Perfect English audio doesn&rsquo;t help if the image in the search results is an illegible block of text to anyone who doesn&rsquo;t read Portuguese. YouTube lets you upload an alternative thumbnail per language, so I needed a way to generate English versions, keeping the rest of the art identical and swapping only the text.</p>
<p>The tool uses two pieces. <strong><code>yt-dlp</code></strong> is the modern fork of the old <code>youtube-dl</code>, a Python CLI that downloads anything from YouTube (videos, audio, subtitles, thumbnails) without needing an API key. <strong>Nano Banana Pro</strong> (<code>nano-banana-pro-preview</code> on the API) is Google Gemini&rsquo;s latest image-editing model — you feed it an input image with a prompt and it returns an edited image, preserving the rest of the composition when you ask it to touch only a specific part.</p>
<p>The pipeline has two steps. Step 1: <code>yt-dlp</code> grabs the thumbnail as a <code>.jpg</code>, no video download:</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">yt-dlp --skip-download <span class="se">\
</span></span></span><span class="line"><span class="cl">       --write-thumbnail <span class="se">\
</span></span></span><span class="line"><span class="cl">       --convert-thumbnails jpg <span class="se">\
</span></span></span><span class="line"><span class="cl">       -o <span class="s2">&#34;thumbnails/originals/&lt;slug&gt;/&lt;video_id&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">       <span class="s2">&#34;https://www.youtube.com/watch?v=&lt;video_id&gt;&#34;</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>Step 2: send each image to Gemini Nano Banana Pro with a prompt that&rsquo;s a rigid contract of hard requirements, not just a &ldquo;translate this&rdquo;:</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-text" data-lang="text"><span class="line"><span class="cl">TASK:
</span></span><span class="line"><span class="cl">Detect every piece of Portuguese text visible on this image and
</span></span><span class="line"><span class="cl">translate it to clear, natural American English. Replace the
</span></span><span class="line"><span class="cl">Portuguese text with the English translation in the same visual
</span></span><span class="line"><span class="cl">position.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">STRICT REQUIREMENTS — follow every one:
</span></span><span class="line"><span class="cl">- Preserve EVERY non-text element identically: face, pose,
</span></span><span class="line"><span class="cl">  expression, background, color palette, lighting, icons, logos,
</span></span><span class="line"><span class="cl">  decorative shapes, borders, layout. Only the text changes.
</span></span><span class="line"><span class="cl">- The English translation must be IDIOMATIC and CONFIDENT — not
</span></span><span class="line"><span class="cl">  a literal word-for-word rewrite. It&#39;s a YouTube thumbnail for a
</span></span><span class="line"><span class="cl">  tech audience, so use punchy phrasing a native English-speaking
</span></span><span class="line"><span class="cl">  tech YouTuber would write.
</span></span><span class="line"><span class="cl">- Match the original text&#39;s font family, weight, size, color,
</span></span><span class="line"><span class="cl">  stroke outline, drop shadow, and any decorative treatment.
</span></span><span class="line"><span class="cl">- If there is no Portuguese text at all on the image, return the
</span></span><span class="line"><span class="cl">  original image unchanged.</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 &ldquo;return the original image unchanged&rdquo; clause is what saves the earliest episodes, which don&rsquo;t have overlay text, just my face. Without it, the model would invariably try to &ldquo;improve&rdquo; the composition, swap the lighting, and so on.</p>
<p>Here&rsquo;s the result on two examples (episodes 10 and 11):</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_pt.jpg" alt="Original Portuguese thumbnail: 7 Recomendações de Shows para pessoas de Tech" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_en.jpg" alt="Translated English thumbnail: 7 TV Shows You Must Watch If You're In Tech" style="width: 100%;"></div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_pt.jpg" alt="Original Portuguese thumbnail: 9 Dicas para Palestrantes: venda sua caneta" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_en.jpg" alt="Translated English thumbnail: 9 Tips for Speakers: sell your pen" style="width: 100%;"></div>
</div>
<p>Notice the detail on episode 10. The model didn&rsquo;t translate &ldquo;7 Recomendações de Shows para Pessoas de Tech&rdquo; literally into &ldquo;7 Show Recommendations for Tech People&rdquo; the way Google Translate would. It rewrote it as &ldquo;7 TV Shows You Must Watch If You&rsquo;re In Tech&rdquo;, the way an actual American tech YouTuber would headline the video. The rest of the image stays pixel-for-pixel identical. On episode 11, &ldquo;9 DICAS PARA PALESTRANTES: venda sua caneta&rdquo; becomes &ldquo;9 TIPS FOR SPEAKERS: sell your pen&rdquo;, with the font, all-caps treatment and position preserved. If you didn&rsquo;t know the original was in Portuguese, you couldn&rsquo;t guess the second image is an automatic AI edit.</p>
<p>Cost on the Gemini side: a few cents per image, a handful of dollars total for the whole batch. Trivial next to the $1,500+ on the audio dub. With the thumbnail sorted, the channel&rsquo;s English conversion is finally buttoned up — ElevenLabs v3 cloning the voice over hand-curated English <code>.srt</code> files, and Nano Banana Pro editing the image so the title matches the audio.</p>
<h2>The conclusion, which is the title of this post<span class="hx:absolute hx:-mt-20" id="the-conclusion-which-is-the-title-of-this-post"></span>
    <a href="#the-conclusion-which-is-the-title-of-this-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When Qwen3 TTS dropped, the hype was calling it an &ldquo;ElevenLabs killer&rdquo;. I spent weeks trying to live with that premise in practice, with real content shipping every week. And what I found is that open source TTS is still miles behind ElevenLabs. The gap is large, and it&rsquo;s a gap that shows up precisely when you step off the 5-second demo tweet and put the model to work on a real 30-minute weekly podcast.</p>
<p>In practice, Qwen3 doesn&rsquo;t even beat ElevenLabs&rsquo; v2 model. v3, which is the current one, is still a step above v2. The prosody comes out better, the inline emotion tags work in Portuguese and English without effort, and the API stays up without you maintaining a server. The cost per character is a bit higher, but for my current volume it sits comfortably within budget and gives me back the hours I was spending on GPU babysitting.</p>
<p>The lesson here is the same one the LLM crowd is learning slowly. A 30-second demo tweet is one thing, production is a completely different thing. Open source has its niche, mainly when you have sensitive data that can&rsquo;t leave the house, or when you have tons of idle GPU and little recurring budget. But for serious TTS use in a commercial product, here in April 2026, ElevenLabs is still untouchable. Qwen3 didn&rsquo;t kill anyone.</p>
<p>And don&rsquo;t forget: subscribe to <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">The M.Akita Chronicles on Spotify</a> so you don&rsquo;t miss new episodes like this one, made with the new pipeline.</p>
]]></content:encoded><category>generative-media</category><category>local-models</category><category>reviews</category></item><item><title>Testing Open Source and Commercial LLMs - Can Anyone Beat Claude Opus?</title><link>https://www.akitaonrails.com/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/</guid><pubDate>Sun, 05 Apr 2026 18:00:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Obsolete article (updated 2026-04-24).&lt;/strong&gt; The conclusions and rankings in this post were superseded after I re-audited the benchmark against the &lt;code&gt;ruby_llm&lt;/code&gt; gem source and restructured the evaluation criteria. Several &amp;ldquo;hallucinations&amp;rdquo; I had cataloged were actually valid API. Kimi K2.6 and Gemini 3.1 Pro moved up to Tier A. GLM 5.1 dropped to Tier C. MiMo V2.5 Pro fell from &amp;ldquo;first non-Anthropic Tier 1&amp;rdquo; to Tier B. &lt;strong&gt;The canonical version lives at &lt;a href="https://www.akitaonrails.com/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/"&gt;LLM Coding Benchmark (April 2026)&lt;/a&gt;.&lt;/strong&gt; This post stays as a historical record of what I concluded before the re-audit.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p>⚠️ <strong>Obsolete article (updated 2026-04-24).</strong> The conclusions and rankings in this post were superseded after I re-audited the benchmark against the <code>ruby_llm</code> gem source and restructured the evaluation criteria. Several &ldquo;hallucinations&rdquo; I had cataloged were actually valid API. Kimi K2.6 and Gemini 3.1 Pro moved up to Tier A. GLM 5.1 dropped to Tier C. MiMo V2.5 Pro fell from &ldquo;first non-Anthropic Tier 1&rdquo; to Tier B. <strong>The canonical version lives at <a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">LLM Coding Benchmark (April 2026)</a>.</strong> This post stays as a historical record of what I concluded before the re-audit.</p>

</blockquote>
<hr>
<p><strong>Update April 16, 2026:</strong> Added Claude Opus 4.7, Qwen 3.6 and GPT 5.4 via Codex CLI (xHigh reasoning). Opus 4.7 is an incremental improvement over 4.6 (28 tests vs 16, same correct API) and becomes the new baseline. GPT 5.4, previously Tier 1 based on my personal vouch, now has objective data and dropped to Tier 2 — burned 7.6M tokens (~$16/run, 15x more expensive than Opus) and got the <code>add_message</code> calling convention wrong on multi-turn. Qwen 3.6 Plus remains Tier 3 with the same API hallucination as 3.5. The conclusion stands: if you want safety, pick Opus.</p>
<hr>
<p><strong>TL;DR:</strong> If you don&rsquo;t want to read the whole analysis: the only models that produced code that actually works in our benchmark were Claude Opus 4.7, Claude Opus 4.6, GLM 5 and GLM 5.1 (from Z.AI, ~89% cheaper than Opus). Sonnet works in this benchmark too, but in practice it falls short on projects requiring deeper reasoning — details in the conclusion. GPT 5.4, previously Tier 1 based on my personal vouch, now has objective data via Codex CLI: it burned 7.6M tokens ($16/run) and got the <code>add_message</code> calling convention wrong — works on the first message, breaks on multi-turn. Dropped to Tier 2. Everything else — Kimi, DeepSeek, MiniMax, Qwen, Gemini, Grok 4.20 — invented APIs that don&rsquo;t exist or ignored the gem we asked for.</p>
<p>There&rsquo;s a new wrinkle in this update: I redid the local part of the benchmark on an RTX 5090 (instead of the AMD Strix Halo) and added a fresh batch of Qwen models, including a Qwen 3.5 27B distilled directly from Claude 4.6 Opus. That reopened the conversation on running open source models locally. The 5090&rsquo;s memory bandwidth flips the game from &ldquo;unworkable&rdquo; to &ldquo;workable with 1-2 follow-up prompts.&rdquo; The bottleneck for open source models has moved to a lack of factual knowledge about specific libraries, which I unpack in detail in the new section on the Qwen family. The Claude distillation gamble, by the way, gave a pretty frustrating result that I haven&rsquo;t seen documented in these terms before.</p>
<hr>
<p>If you&rsquo;ve been following <a href="/en/en/tags/vibecoding/">my previous vibe coding pieces</a>, you know I spent the last two months in a 500-hour marathon using Claude Opus as my main coding agent. The results were good, as I reported in the <a href="/en/2026/03/05/37-days-of-vibe-coding-immersion-conclusions-on-business-models/">conclusion about business models</a>. But there was an itch I couldn&rsquo;t scratch: am I locked into one model? Is there a real alternative to Claude Opus for daily use on real projects?</p>
<p>I&rsquo;ve got an RTX 5090 with 32 GB of GDDR7. I know I can run the latest open source models. I bought a <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Minisforum MS-S1</a> with an AMD Ryzen AI Max 395 and 128 GB of unified memory, and built a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">home server with Docker</a> to serve local models. The infrastructure was ready. What was missing was actually testing it.</p>
<p>I built an automated benchmark to compare open source and commercial models under identical conditions. 33 models configured in total (25 from the original run plus 8 added in the NVIDIA rerun), 27 executed, 16 completed in some form. The code is on <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
<h2>The bottleneck nobody explains: VRAM and KV Cache<span class="hx:absolute hx:-mt-20" id="the-bottleneck-nobody-explains-vram-and-kv-cache"></span>
    <a href="#the-bottleneck-nobody-explains-vram-and-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before getting to the results, I have to explain why running large models locally is much harder than it looks.</p>
<p>Take Qwen3 32B. The model in FP16 (full precision) takes ~64 GB. Quantized to Q4 (4 bits), it drops to ~19 GB. So it fits in my RTX 5090&rsquo;s 32 GB, right? Wrong. That&rsquo;s just the model weights. There&rsquo;s a part nobody tells you about: the <strong>KV Cache</strong>.</p>
<p>KV Cache is the memory the model uses to &ldquo;remember&rdquo; what it has already read. Every time it processes a token (a word or piece of a word), it computes two vectors — K (key) and V (value) — for every attention layer. Those vectors stick around so the model doesn&rsquo;t have to recompute everything when it generates the next token. Without that, generation would be quadratically slow.</p>
<p>The KV Cache scales linearly with the size of the context. The formula:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>KV Memory = 2 × Layers × KV_Heads × Head_Dimension × Bytes_per_Element × Context_Tokens</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>For a model like Llama 3.1 70B in BF16, that comes out to ~0.31 MB per token. Sounds tiny, until you realize that a 128K context eats <strong>40 GB</strong> of KV Cache alone. The model itself plus KV Cache adds up to way more VRAM than most GPUs have.</p>
<p>And for actual coding agent use, 128K tokens isn&rsquo;t a luxury, it&rsquo;s the bare minimum. The agent has to read files, keep conversation history, receive command output. In long benchmark sessions, our models consumed between 39K and 156K tokens. Less than 100K of context isn&rsquo;t practical for day-to-day project work.</p>
<p>Google published <a href="https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/"target="_blank" rel="noopener">TurboQuant</a> (ICLR 2026), which compresses the KV Cache to 3 bits without accuracy loss — a 6x memory reduction and up to 8x speedup. It uses random vector rotation (PolarQuant) followed by a 1-bit algorithm on the residuals. Works online during inference, compressing on write and decompressing on read. Not yet implemented in the runtimes we use (llama.cpp, Ollama), but when it lands it&rsquo;ll change the equation a lot.</p>
<p>For anyone wanting to dig deeper into the VRAM math, I recommend <a href="https://x.com/TheAhmadOsman/status/2040103488714068245"target="_blank" rel="noopener">this link from Ahmad Osman</a> for the article &ldquo;GPU Memory Math for LLMs (2026 Edition)&rdquo;.</p>
<h2>The hardware problem: not all memory is created equal<span class="hx:absolute hx:-mt-20" id="the-hardware-problem-not-all-memory-is-created-equal"></span>
    <a href="#the-hardware-problem-not-all-memory-is-created-equal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>&ldquo;But I have 128 GB of RAM!&rdquo; Cool, but that&rsquo;s not what matters. What matters is memory bandwidth, and the difference between types is wild:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/memory-bandwidth.png" alt="Memory bandwidth by type"  loading="lazy" /></p>
<p>The RTX 5090 has 7x the bandwidth of the LPDDR5x memory in my Minisforum. That means even if a model fits in the AMD&rsquo;s unified RAM, inference will be proportionally slower. On my Minisforum with LPDDR5x at 256 GB/s, Qwen3 32B runs at ~7 tok/s. On the RTX 5090 at 1,792 GB/s, it&rsquo;d be much faster — if it fit entirely in VRAM alongside the KV Cache.</p>
<p>Most folks running local models are still on DDR4. At 50 GB/s, 32B models are basically unusable. And there&rsquo;s another factor people forget: storage. When the RAM can&rsquo;t keep up and the system swaps, the storage speed becomes the bottleneck:</p>
<table>
  <thead>
      <tr>
          <th>Storage</th>
          <th style="text-align: right">Sequential Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SATA SSD</td>
          <td style="text-align: right">~550 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen3</td>
          <td style="text-align: right">~3,500 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen4</td>
          <td style="text-align: right">~7,000 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen5</td>
          <td style="text-align: right">~12,000 MB/s</td>
      </tr>
  </tbody>
</table>
<p>From SATA to NVMe Gen5 you&rsquo;re looking at a 22x difference. If you&rsquo;re doing partial offloading to disk (which is common when the model doesn&rsquo;t fit entirely on the GPU), NVMe Gen4 or Gen5 makes a real difference. SATA is a non-starter.</p>
<p>To sum up: running local models isn&rsquo;t just &ldquo;having enough RAM.&rdquo; You need the right kind of memory, with the right bandwidth, and fast storage as a fallback. For a lot of people, a Mac Studio with high-bandwidth unified memory (up to 800 GB/s on the M4 Ultra with 512 GB) would be the more practical option, but it costs more than US$ 10,000. The AMD Ryzen AI Max is the cheaper alternative with unified memory, but its LPDDR5 caps out at 256 GB/s.</p>
<h2>Ollama vs llama.cpp: why Ollama falls apart on benchmarks<span class="hx:absolute hx:-mt-20" id="ollama-vs-llamacpp-why-ollama-falls-apart-on-benchmarks"></span>
    <a href="#ollama-vs-llamacpp-why-ollama-falls-apart-on-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://ollama.com/"target="_blank" rel="noopener">Ollama</a> is the most popular way to run local models. Install, pull the model, run. For casual use it works. But when I tried to use it for automated benchmarks with long unattended sessions, it broke in 6 different ways across 8 models:</p>
<ol>
<li>Unloads the model mid-session. On long runs, Ollama decides the model isn&rsquo;t being used and unloads it from the GPU. The agent sits there waiting for a response from a model that no longer exists.</li>
<li>Ignores the requested context. You ask for <code>num_ctx=131072</code>, Ollama accepts, then halfway through the run it reverts to the default without warning.</li>
<li>Unstable lifecycle. Asking for <code>keep_alive: 0</code> to unload doesn&rsquo;t always work. The model stays resident and blocks the next one.</li>
<li>Incompatible formats. Native bf16 variants on Ollama failed, while the same model as a Q8 GGUF from HuggingFace worked fine.</li>
</ol>
<p>The fix: migrate to <a href="https://github.com/mostlygeek/llama-swap"target="_blank" rel="noopener">llama-swap</a>, a Go wrapper that manages llama.cpp processes with hot-swap. A request comes in for a different model than the one currently loaded, it kills the current process and starts the new one. No context negotiation, no flaky lifecycle.</p>
<p>llama-swap fixed the loading of 6 of the 8 models that had failed under Ollama:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Ollama</th>
          <th>llama-swap</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>HTTP 500</td>
          <td>47.6 tok/s</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>No output</td>
          <td>47.4 tok/s</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>Unloaded</td>
          <td>17.5 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Output off-spec</td>
          <td>49.7 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Context drift</td>
          <td>23.1 tok/s</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Model not found</td>
          <td>78.3 tok/s</td>
      </tr>
  </tbody>
</table>
<p>But llama-swap isn&rsquo;t magic.</p>
<h2>Why &ldquo;just use llama.cpp&rdquo; doesn&rsquo;t fix everything<span class="hx:absolute hx:-mt-20" id="why-just-use-llamacpp-doesnt-fix-everything"></span>
    <a href="#why-just-use-llamacpp-doesnt-fix-everything" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>llama.cpp solves Ollama&rsquo;s lifecycle problems but brings its own:</p>
<p>Each model needs specific flags. GLM and Qwen 3.5 emit <code>&lt;think&gt;</code> tags that break clients if you don&rsquo;t pass <code>--reasoning-format none</code>. Gemma 4 needs build b8665+ for the tool call parser to work.</p>
<p>Not every model supports tool calling. llama.cpp needs a dedicated parser for each model&rsquo;s tool call format. Llama 4 Scout uses a &ldquo;pythonic&rdquo; format (<code>[func(param=&quot;value&quot;)]</code>) that llama.cpp simply doesn&rsquo;t parse and emits as plain text. vLLM has a parser for it, llama.cpp doesn&rsquo;t.</p>
<p>And then there are the repetition loops. Gemma 4, even with the right parser, gets into an infinite loop after ~11 tool calls in long sessions. It&rsquo;s a <a href="https://github.com/ggml-org/llama.cpp/issues/21375"target="_blank" rel="noopener">known bug</a> that PR #21418 didn&rsquo;t fully fix.</p>
<p>Tool calling compatibility per model:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Tool Calling</th>
          <th>Required Flags</th>
          <th>Benchmark Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>Partial (b8665+)</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Infinite loop after ~11 steps</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>Yes</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>2029 files, ended mid-tool-call</td>
      </tr>
      <tr>
          <td>Qwen 3.5 (35B, 122B)</td>
          <td>Yes</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Completed successfully</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Yes</td>
          <td><code>--jinja</code></td>
          <td>Completed (best local result)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Yes</td>
          <td><code>--jinja</code></td>
          <td>Tool calls ok, but app in wrong directory</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>No</td>
          <td>—</td>
          <td>No parser in llama.cpp</td>
      </tr>
  </tbody>
</table>
<p>At the end of the day, llama.cpp is better than Ollama for automated runs, but &ldquo;plug and play&rdquo; it ain&rsquo;t. Each model requires specific configuration, and some just don&rsquo;t work for agentic coding yet.</p>
<h2>Reasoning: models that think vs models that wing it<span class="hx:absolute hx:-mt-20" id="reasoning-models-that-think-vs-models-that-wing-it"></span>
    <a href="#reasoning-models-that-think-vs-models-that-wing-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s one difference between models worth explaining: reasoning. The idea is that the model &ldquo;thinks before answering&rdquo; instead of generating tokens straight from left to right. Models with reasoning go through an internal chain-of-thought step where they evaluate the problem, consider alternatives, plan, and only then emit the response.</p>
<p>In practice this shows up as <code>&lt;think&gt;...&lt;/think&gt;</code> tags in the output, blocks of text the model writes to itself that shouldn&rsquo;t go to the end user. Claude Opus 4.6, GPT 5.4, DeepSeek V3.2 and the Qwen 3.5 line support reasoning natively. The smaller ones (Gemma 4, GPT OSS 20B, older models) don&rsquo;t have that capability.</p>
<p>Why does it matter for coding? When a coding agent gets &ldquo;build a Rails app with 9 components,&rdquo; it has to decompose the task into steps, decide the order, anticipate dependencies, adapt when something fails. Without reasoning, the model generates code sequentially with no planning. It works for simple tasks, falls apart on projects with interdependent parts.</p>
<p>In the benchmark, the difference was clear:</p>
<ul>
<li>GPT OSS 20B (no reasoning, 20B parameters) created the app in the wrong directory. Couldn&rsquo;t keep workspace instructions in mind while generating code.</li>
<li>Qwen 3 32B has reasoning, but at 7 tok/s it was too slow. The &ldquo;thinking&rdquo; tokens drag out the generation time.</li>
<li>Gemma 4 31B, with no reasoning trained for agentic use, fell into repetitive tool calling loops.</li>
<li>GLM 5 (cloud, 745B MoE) with reasoning and 44B active parameters, finished cleanly and used the correct API.</li>
</ul>
<p>There&rsquo;s a trade-off: reasoning consumes extra tokens (the <code>&lt;think&gt;</code> blocks), which take up VRAM in the KV Cache and slow generation down. That&rsquo;s why flags like <code>--reasoning-format none</code> are needed in llama.cpp. Some clients don&rsquo;t know what to do with reasoning tokens and break. Models that emit reasoning when the runtime isn&rsquo;t expecting it can produce garbage in the output.</p>
<p>And reasoning isn&rsquo;t something you &ldquo;turn on&rdquo; in any model. It&rsquo;s a capability trained with reinforcement learning on top of the base model, using data from problems that require multi-step thinking. The smaller open source models (20B-35B) typically didn&rsquo;t go through that training, or went through it on a smaller scale. They know how to generate code, but they don&rsquo;t know how to <em>plan</em> code. On tasks that require 50+ coordinated tool calls, that difference is fatal.</p>
<h2>The benchmark: methodology<span class="hx:absolute hx:-mt-20" id="the-benchmark-methodology"></span>
    <a href="#the-benchmark-methodology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To compare models fairly, I built an automated harness in Python. Each model gets the exact same prompt: build a complete Ruby on Rails application, a ChatGPT-style chat SPA using the RubyLLM gem, with Hotwire/Stimulus/Turbo Streams, Tailwind CSS, Minitest tests, CI tools (Brakeman, RuboCop, SimpleCov, bundle-audit), Dockerfile, docker-compose and README.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/crush-screenshot.png" alt="Crush — coding CLI from Charm"  loading="lazy" /></p>
<p>The runner is <a href="https://github.com/sst/opencode"target="_blank" rel="noopener">opencode</a>, one of the most popular open source coding CLIs, competing with Claude Code and Codex. One clarification worth making: the original author left the project to work on <a href="https://github.com/charmbracelet/crush"target="_blank" rel="noopener">Crush</a> together with the <a href="https://charm.sh/"target="_blank" rel="noopener">Charm</a> team (the folks behind Bubble Tea, Lip Gloss and several other Go terminal tools), but the rest of the original team kept evolving opencode normally — the project has not been discontinued. Today the two coexist. If you read <a href="/en/2026/01/09/omarchy-3-one-of-the-best-coding-agents-crush/">my piece on Crush</a>, you already know that branch. Both run everywhere: macOS, Linux, Windows, Android, FreeBSD.</p>
<p>I actually tried to use Crush for the benchmark first. The problem: it advertised a <code>--yolo</code> flag in its help to auto-approve every action (essential for unattended automated runs), but at runtime it rejected the flag. Without auto-approve there&rsquo;s no way to do an unattended benchmark. opencode, on the other hand, had the <code>opencode run --agent build --format json</code> mode that emits JSON events with session IDs and token counts, perfect for automation. So we went with opencode.</p>
<p>I picked opencode (and not Claude Code or Codex) for two reasons:</p>
<ol>
<li>Neutrality. Claude Code is optimized for Anthropic models. Codex is optimized for OpenAI models. opencode is agnostic, same interface for all.</li>
<li>Automation. opencode exposes a machine-readable JSON format. Claude Code and Codex don&rsquo;t have an equivalent interface for external benchmarking.</li>
</ol>
<p>Cloud models ran in two phases: phase 1 (build the app) and phase 2 (validate local boot, docker build, docker compose). Local models only ran phase 1.</p>
<p>Worth mentioning: the entire benchmark cost less than $10 in tokens on OpenRouter. Apart from GPT 5.4 Pro which torched $7.20 to fail, the other 11 cloud models added up to about $2.50 total. Local models cost only electricity. The point is: running your own benchmark is cheap. If you want to know whether a model works for your use case, drop the $2 and test it. The harness code is on GitHub, just swap the prompt for your own project.</p>
<h2>Why GPT 5.4 failed the benchmark (but not in real life)<span class="hx:absolute hx:-mt-20" id="why-gpt-54-failed-the-benchmark-but-not-in-real-life"></span>
    <a href="#why-gpt-54-failed-the-benchmark-but-not-in-real-life" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>GPT 5.4 Pro is the only cloud model that consistently failed our benchmark. Two separate runs, same result: the model generated files but never reached <code>finish_reason: stop</code>. It always ended on <code>finish_reason: tool-calls</code> — wanted to keep calling tools but the loop kept breaking.</p>
<p>For folks who don&rsquo;t know: tool calling is when an LLM needs to perform an action (read a file, run a command, edit code) and emits a &ldquo;tool call&rdquo; in a structured format. The client (opencode, Claude Code, Codex) interprets it, executes it, and returns the result back to the model. Each provider has its own format: Anthropic uses <code>tool_use</code> blocks, OpenAI uses <code>function_calling</code> with proprietary JSON schemas, Google uses <code>FunctionCall</code>.</p>
<p>GPT 5.4 is heavily trained for OpenAI&rsquo;s native function calling format — <code>tool_choice</code>, <code>tools</code> with proprietary JSON schemas. When the benchmark routes through opencode → OpenRouter → GPT 5.4, the tool schemas get translated at every hop. If GPT emits tool calls in a format that OpenRouter or opencode doesn&rsquo;t parse correctly, the agent loop breaks.</p>
<p>The evidence: every other cloud model (Claude Opus, Claude Sonnet, Kimi K2.5, DeepSeek V3.2, MiniMax M2.7, GLM 5, Qwen 3.6 Plus, Step 3.5 Flash) ended on <code>finish_reason: stop</code>. Only GPT ends on <code>finish_reason: tool-calls</code>.</p>
<p>A fair comparison for GPT 5.4 would require running it in its native environment. And now we have that comparison: we built support to automate the Codex CLI (<code>codex exec</code> with <code>--dangerously-bypass-approvals-and-sandbox</code> and reasoning effort <code>xhigh</code>) and ran the same benchmark. GPT 5.4 completed in 22 minutes, generated all 9 artifacts, wrote 22 tests with the most sophisticated architecture in the entire benchmark: dependency injection of the RubyLLM client, PORO models for <code>ChatMessage</code> and <code>PromptSubmission</code>, session-backed <code>ChatSession</code> with TTL and message trimming, bin/ci script.</p>
<p>But the code breaks on the second message. GPT 5.4 uses <code>chat.add_message(role:, content:)</code> with keyword arguments instead of a positional hash <code>chat.add_message({role:, content:})</code> — this causes <code>ArgumentError: wrong number of arguments (given 0, expected 1)</code> on the first multi-turn exchange. The first message works (uses <code>chat.ask</code> directly), multi-turn doesn&rsquo;t.</p>
<p>And the cost: <strong>7.6 million tokens</strong> on xHigh reasoning effort. That&rsquo;s 65x more tokens than Opus 4.7 (118K) for the same benchmark. Estimated cost of ~$16 per run, versus ~$1.10 for Opus. Spent 15x more and still got the calling convention wrong. A massive token budget and maximum reasoning effort still can&rsquo;t guarantee factual correctness on a gem API. API knowledge is binary recall in the weights, not a function of how hard the model &ldquo;thinks.&rdquo;</p>
<p>With objective data in hand, GPT 5.4 moves from Tier 1 to Tier 2. The architecture it generates is better than Opus in terms of design patterns. But the code needs a fix for multi-turn to work, and the per-token cost is prohibitive.</p>
<p>Sonnet and Opus through opencode/OpenRouter were probably also not pushed to their limits. Claude Code offers native tool support that opencode doesn&rsquo;t replicate — meaning the benchmark results represent a floor, not a ceiling, for those models.</p>
<h2>Open source models: reality vs the narrative<span class="hx:absolute hx:-mt-20" id="open-source-models-reality-vs-the-narrative"></span>
    <a href="#open-source-models-reality-vs-the-narrative" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A lot of people are saying open source models have already caught up with the commercial ones and you can run your own &ldquo;Claude&rdquo; at home. In practice, not really.</p>
<p>The scale isn&rsquo;t comparable. Frontier models like Claude Opus 4.6 and GPT 5.4 are closed-source, but estimates put them in the hundreds of billions to trillions of parameters range, trained with compute and data no open source company can replicate. The best models that fit on reasonable hardware are:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Parameters</th>
          <th style="text-align: right">Active Parameters</th>
          <th>Architecture</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: right">35B</td>
          <td style="text-align: right">3B</td>
          <td>MoE (A3B)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B</td>
          <td style="text-align: right">27B</td>
          <td style="text-align: right">27B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: right">32B</td>
          <td style="text-align: right">32B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: right">122B</td>
          <td style="text-align: right">122B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: right">20B</td>
          <td style="text-align: right">20B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">31B</td>
          <td style="text-align: right">31B</td>
          <td>Dense</td>
      </tr>
  </tbody>
</table>
<p>Post-publication correction: Qwen 3.5 35B is actually the <strong>35B-A3B</strong>, an MoE with only 3B active parameters per token (not dense, as I&rsquo;d originally written). That&rsquo;s why it runs relatively fast for its size. And for folks with 24 GB of VRAM, the model recommended by <a href="https://unsloth.ai/docs/models/qwen3.5#qwen3.5-27b"target="_blank" rel="noopener">Unsloth</a> themselves is the <strong>Qwen 3.5 27B</strong> dense — that one I didn&rsquo;t get around to testing in the benchmark, but it&rsquo;s worth a look. For anyone wanting to dig deeper into local models, <a href="https://x.com/sudoingX"target="_blank" rel="noopener">@sudoingX</a> has been doing some serious experimentation in this space. Thanks to <a href="https://x.com/thpmacedo/status/2041105305111502927"target="_blank" rel="noopener">@thpmacedo</a> for the heads-up.</p>
<p>Even the largest open source MoE (Mixture of Experts) models that companies make publicly available activate only a small fraction of parameters per token:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Parameters</th>
          <th style="text-align: right">Active Parameters</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">1T</td>
          <td style="text-align: right">32B</td>
          <td>384 experts, top-8 + shared</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">745B</td>
          <td style="text-align: right">44B</td>
          <td>256 experts, 8 activated</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">671B</td>
          <td style="text-align: right">37B</td>
          <td>Sparse Attention</td>
      </tr>
      <tr>
          <td>Qwen 3.5 397B</td>
          <td style="text-align: right">397B</td>
          <td style="text-align: right">17B</td>
          <td>MoE, cloud-only</td>
      </tr>
  </tbody>
</table>
<p>These large models aren&rsquo;t self-hostable. Kimi K2.5 with 1T parameters needs GPU clusters with hundreds of GBs of VRAM. GLM 5 with 745B is the same. Even if Alibaba or Z.AI release the weights (and some do), nobody has home hardware to run them.</p>
<p>What fits on your home GPU are the 20B-35B models — and those have real limitations.</p>
<h3>What each local model did in the benchmark<span class="hx:absolute hx:-mt-20" id="what-each-local-model-did-in-the-benchmark"></span>
    <a href="#what-each-local-model-did-in-the-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Results from the original run on the AMD Strix Halo:</p>
<p><strong>Qwen 3 Coder Next (30B)</strong> — Completed in 17 minutes on the Strix, generated 1675 files, Rails app with all the artifacts. But only 3 tests. And more importantly: it invented <code>RubyLLM::Client.new</code>, a class that doesn&rsquo;t exist in the gem. The app doesn&rsquo;t run.</p>
<p><strong>Qwen 3.5 35B</strong> — Completed in 28 minutes on the Strix, 1478 files, 11 tests. Used <code>RubyLLM.chat</code> without a model parameter — works only if the default is configured. No LLM mocking in the tests.</p>
<p><strong>Qwen 3.5 122B</strong> — Completed in 43 minutes on the Strix, 1503 files, 16 tests. But it ignored the RubyLLM gem completely and built a custom HTTP client for OpenRouter. The prompt explicitly asked for ruby_llm.</p>
<p><strong>GLM 4.7 Flash (local, Strix)</strong> — Produced 2029 files with all the artifacts, but the session ended mid-tool-call. The cloud version (GLM 5) works perfectly.</p>
<p><strong>Gemma 4 31B (Strix)</strong> — Infinite tool call loop after ~11 productive steps. Known llama.cpp bug.</p>
<p><strong>GPT OSS 20B (Strix)</strong> — Created the Rails app in the wrong directory (<code>project/app/</code> instead of <code>project/</code>). A 20B model doesn&rsquo;t follow workspace instructions reliably.</p>
<p><strong>Qwen 3 32B (Strix)</strong> — Way too slow (7.3 tok/s). The hardware can&rsquo;t keep up.</p>
<p>And the results from the rerun on the NVIDIA RTX 5090 (all with Q3_K_M or Q4_K_M and contexts between 64k and 128k to fit the 32 GB of VRAM):</p>
<p><strong>Qwen 3.5 35B-A3B (5090)</strong> — 5 minutes at 273 tok/s. Recognizable Rails project, entry point <code>RubyLLM.chat(model:)</code> is right, but it hallucinates <code>chat.add_message(role:, content:)</code> and <code>chat.complete</code> instead of <code>.ask</code>. Fixable in 1-2 follow-ups. The best candidate for &ldquo;OSS local that&rsquo;s actually worth trying.&rdquo;</p>
<p><strong>Qwen 3.5 27B Claude-distilled (5090)</strong> — 12 minutes at 129 tok/s. Impeccable Claude style, total API hallucination (<code>RubyLLM::Chat.new.with_model{}</code>, <code>add_message</code>, <code>response.text</code>). More details in the distillation section below.</p>
<p><strong>Qwen 3 Coder 30B (5090)</strong> — 6 minutes at 145 tok/s. Returned a hardcoded mock string instead of calling the API. Tier 3 unusable.</p>
<p><strong>Qwen 2.5 Coder 32B (5090)</strong> — 90 minutes of timeout, zero files. The model spun without ever calling a write tool.</p>
<p><strong>Qwen 3 32B (5090)</strong> — 4 minutes at 69 tok/s, partial scaffold, errors. The general version is better than the Coder one but still breaks.</p>
<p><strong>Gemma 4 31B (5090)</strong> — 8 minutes at 213 tok/s. Same repetition loop it had on the Strix. The llama.cpp bug isn&rsquo;t a hardware issue.</p>
<p><strong>Qwen 3.5 27B Sushi Coder RL (5090)</strong> — Infrastructure failure (<code>ProviderModelNotFoundError</code>), couldn&rsquo;t be evaluated. Redo on a future run.</p>
<p><strong>GPT OSS 20B (5090)</strong> — Pulled from this run because of a recent llama.cpp main regression in the harmony family tool call parser. The logs show <code>Failed to parse input at pos 755: &lt;|channel|&gt;...</code> in multi-turn sessions. It worked on the Strix with llama.cpp <code>b8643</code>, broken on today&rsquo;s main. Waiting on upstream to fix it.</p>
<h2>Cloud models: what actually works<span class="hx:absolute hx:-mt-20" id="cloud-models-what-actually-works"></span>
    <a href="#cloud-models-what-actually-works" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Of the 12 models that completed the benchmark, all of them generated a recognizable Rails project with all the requested artifacts (Gemfile, routes, views, JS, tests, README, Dockerfile, docker-compose). 9 out of 9 on the completeness checklist.</p>
<p>But here comes the question that matters: does the code run?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/cost-vs-quality.png" alt="Cost vs time — and does the code work?"  loading="lazy" /></p>
<p>The correct RubyLLM API is simple:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="s2">&#34;Hello&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</span>  <span class="c1"># =&gt; &#34;Hi there!&#34;</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>8 of the 12 models invented APIs that don&rsquo;t exist. The most common pattern: hallucinating an interface that doesn&rsquo;t match the actual gem:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>What It Invented</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V3.2</td>
          <td><code>RubyLLM::Client.new</code> — nonexistent class</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td><code>RubyLLM::Client.new</code> — same error</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td><code>Openrouter::Client</code> — nonexistent gem</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td><code>add_message()</code> and <code>complete()</code> — nonexistent methods</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td><code>RubyLLM.chat(messages: [...])</code> — nonexistent signature</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td><code>chat.add_message()</code> — nonexistent method</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td><code>RubyLLM::Chat.new()</code> and <code>add_message()</code> — internal API, not public</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td>Ignores the gem completely — uses <code>OpenAI::Client</code> (ruby-openai) hitting the OpenRouter URL directly</td>
      </tr>
  </tbody>
</table>
<p>The models that got it right — both Claudes, GLM 5 and GLM 5.1 — used the simple two-step pattern (<code>chat = RubyLLM.chat(model:)</code> then <code>chat.ask(message)</code>). The ones that got it wrong tried to make RubyLLM look like the OpenAI Python SDK, which is a different thing. Grok 4.20 was the most brazen case: it didn&rsquo;t even try to use the gem, it went straight for <code>OpenAI::Client</code> pointing at the OpenRouter URL, ignoring the explicit prompt.</p>
<p>And the tests? Only Opus, Sonnet, GLM 5 and GLM 5.1 did proper mocking of the LLM calls. All the others either hit the real API (which fails without a key) or mocked the invented API (tests pass but prove nothing). Test count is a misleading metric: Kimi K2.5 wrote 37 tests, more than anyone else, but none of them test real functionality because the API it uses doesn&rsquo;t exist.</p>
<h3>Real viability table<span class="hx:absolute hx:-mt-20" id="real-viability-table"></span>
    <a href="#real-viability-table" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Correct API?</th>
          <th style="text-align: center">Runs?</th>
          <th style="text-align: center">Test Mocking?</th>
          <th>Problem</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Claude Opus 4.7</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (FakeChat)</td>
          <td>Clean implementation, 28 tests</td>
      </tr>
      <tr>
          <td><strong>Claude Sonnet 4.6</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Clean implementation</td>
      </tr>
      <tr>
          <td><strong>Claude Opus 4.6</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Clean implementation</td>
      </tr>
      <tr>
          <td><strong>GLM 5</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Correct API, works</td>
      </tr>
      <tr>
          <td><strong>GLM 5.1</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes</td>
          <td>Correct API, works</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">1st msg only</td>
          <td style="text-align: center">Yes (FakeChat)</td>
          <td><code>add_message(role:, content:)</code> with keyword args instead of positional hash — breaks multi-turn</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Yes</strong>*</td>
          <td style="text-align: center">No</td>
          <td>Bypasses RubyLLM, uses HTTP directly</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Yes</strong>*</td>
          <td style="text-align: center">No</td>
          <td>Bypasses RubyLLM, uses <code>OpenAI::Client</code> directly</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">Only 1st msg</td>
          <td style="text-align: center">No</td>
          <td><code>add_message()</code> doesn&rsquo;t exist</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">Maybe</td>
          <td style="text-align: center">No</td>
          <td>No model parameter</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>add_message()</code>/<code>complete()</code> invented</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM.chat</code> signature wrong</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM::Client</code> nonexistent</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM::Client</code> nonexistent</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">Wrong mock</td>
          <td><code>RubyLLM::Chat.new()</code> and <code>add_message()</code> don&rsquo;t exist</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>Openrouter::Client</code> gem doesn&rsquo;t exist</td>
      </tr>
  </tbody>
</table>
<p>*Step 3.5 Flash works by calling the OpenRouter REST API directly with <code>Net::HTTP</code>, completely bypassing the gem the prompt asked for.</p>
<p>Now, this doesn&rsquo;t mean those models are useless. If you take Kimi K2.5 or DeepSeek V3.2 and tell it &ldquo;the RubyLLM::Client class doesn&rsquo;t exist, fix it to use the gem&rsquo;s real API&rdquo;, it&rsquo;ll probably fix it. One or two follow-ups and the project becomes functional. Most of the models that failed here could deliver a working project with a few more rounds of conversation.</p>
<p>But that&rsquo;s where the trade-off lives. With Opus or GPT 5.4, the first output already works. You ask, they deliver, you test, it runs. With the cheaper models, you&rsquo;ll spend time fixing API hallucinations, debugging code that &ldquo;looks right&rdquo; but crashes, steering the model in the right direction. Each of those rounds is 10-30 minutes. Three extra rounds and you&rsquo;ve spent an hour of your time to save $0.90 in tokens.</p>
<p>You save dollars, you spend time. And time is money. For someone learning or exploring without urgency, that trade can make sense. For someone who needs to ship, the frontier models pay for themselves fast.</p>
<h3>Comparing the models that work<span class="hx:absolute hx:-mt-20" id="comparing-the-models-that-work"></span>
    <a href="#comparing-the-models-that-work" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Provider</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Tests</th>
          <th style="text-align: right">Cost/Run</th>
          <th>vs Opus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">28</td>
          <td style="text-align: right">~$1.10</td>
          <td>New baseline</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">30</td>
          <td style="text-align: right">~$0.63</td>
          <td>40% cheaper, more tests</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">~$1.05</td>
          <td>Previous baseline</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">7</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% cheaper</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direct</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% cheaper, more tests than GLM 5</td>
      </tr>
  </tbody>
</table>
<h3>Full ranking by time and tokens<span class="hx:absolute hx:-mt-20" id="full-ranking-by-time-and-tokens"></span>
    <a href="#full-ranking-by-time-and-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/time-to-complete.png" alt="Time to complete by model"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Provider</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Total Tokens</th>
          <th style="text-align: right">Tok/s</th>
          <th style="text-align: right">Cost/Run</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Grok 4.20</td>
          <td>OpenRouter</td>
          <td style="text-align: right">8m</td>
          <td style="text-align: right">63,457</td>
          <td style="text-align: right">412.54</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">104,034</td>
          <td style="text-align: right">128.28</td>
          <td style="text-align: right">~$0.50</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">79,743</td>
          <td style="text-align: right">574.52</td>
          <td style="text-align: right">~$0.05</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">136,806</td>
          <td style="text-align: right">347.18</td>
          <td style="text-align: right">~$1.05</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">127,067</td>
          <td style="text-align: right">532.26</td>
          <td style="text-align: right">~$0.63</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">59,378</td>
          <td style="text-align: right">400.01</td>
          <td style="text-align: right">~$0.11</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">88,940</td>
          <td style="text-align: right">182.91</td>
          <td style="text-align: right">Free</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">118,216</td>
          <td style="text-align: right">328.24</td>
          <td style="text-align: right">~$1.10</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direct</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">81,666</td>
          <td style="text-align: right">166.62</td>
          <td style="text-align: right">~$0.13</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td>Codex CLI</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">7,643,800</td>
          <td style="text-align: right">5,824.56</td>
          <td style="text-align: right">~$16.00</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Local</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">39,054</td>
          <td style="text-align: right">37.49</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Local</td>
          <td style="text-align: right">28m</td>
          <td style="text-align: right">76,919</td>
          <td style="text-align: right">46.03</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">29m</td>
          <td style="text-align: right">63,638</td>
          <td style="text-align: right">160.14</td>
          <td style="text-align: right">~$0.07</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td>OpenRouter</td>
          <td style="text-align: right">38m</td>
          <td style="text-align: right">156,267</td>
          <td style="text-align: right">242.11</td>
          <td style="text-align: right">~$0.02</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Local</td>
          <td style="text-align: right">43m</td>
          <td style="text-align: right">57,472</td>
          <td style="text-align: right">22.41</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td>OpenRouter</td>
          <td style="text-align: right">60m</td>
          <td style="text-align: right">115,278</td>
          <td style="text-align: right">53.37</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
  </tbody>
</table>
<p>DeepSeek V3.2 is the slowest despite being cloud — it has no prompt caching, so it resends the full context on every turn.</p>
<h3>Token efficiency and cache<span class="hx:absolute hx:-mt-20" id="token-efficiency-and-cache"></span>
    <a href="#token-efficiency-and-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Models with prompt caching pay much less in effective tokens:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-efficiency.png" alt="Token efficiency: cache vs new"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Tokens</th>
          <th style="text-align: right">Cache Read</th>
          <th style="text-align: right">Effective New Tokens</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">127,067</td>
          <td style="text-align: right">126,429</td>
          <td style="text-align: right">638</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: right">118,216</td>
          <td style="text-align: right">116,824</td>
          <td style="text-align: right">1,392</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">136,806</td>
          <td style="text-align: right">135,976</td>
          <td style="text-align: right">830</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">59,378</td>
          <td style="text-align: right">58,240</td>
          <td style="text-align: right">1,138</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: right">81,666</td>
          <td style="text-align: right">81,216</td>
          <td style="text-align: right">450</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: right">63,457</td>
          <td style="text-align: right">62,400</td>
          <td style="text-align: right">1,057</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">104,034</td>
          <td style="text-align: right">98,129</td>
          <td style="text-align: right">5,905</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: right">7,643,800</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">7,643,800</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">115,278</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">115,278</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">63,638</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">63,638</td>
      </tr>
  </tbody>
</table>
<h2>Speed: the chasm between cloud and local<span class="hx:absolute hx:-mt-20" id="speed-the-chasm-between-cloud-and-local"></span>
    <a href="#speed-the-chasm-between-cloud-and-local" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s an aspect that the cost tables hide: inference speed. And the difference is brutal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/speed-comparison.png" alt="Inference speed by model"  loading="lazy" /></p>
<p>Claude Sonnet generates 532 tok/s. Qwen 3.5 122B running locally on my Minisforum (AMD Strix Halo) generates 22 tok/s. That&rsquo;s a 24x difference. In practice, what Sonnet does in 16 minutes, Qwen 3.5 122B takes 43 minutes. Qwen 3 Coder Next at 37 tok/s is the fastest of the local models on the Strix and even so it&rsquo;s 14x slower than Sonnet.</p>
<p>And it&rsquo;s not just clock time. When you&rsquo;re in an interactive coding loop — ask for a change, wait for output, test, ask for another — the model&rsquo;s speed sets your rhythm. At 37 tok/s, every long response makes you wait 30-60 seconds. At 530 tok/s, it appears almost instantly. Over a day, you feel it.</p>
<p>DeepSeek V3.2 is a curious case: it&rsquo;s cloud but it runs at 53 tok/s, slower than the locally-running Qwen 3.5 35B on the Strix (46 tok/s). The reason is that DeepSeek has no prompt caching — it resends the full context on every turn, strangling throughput. Paying for a cloud model that&rsquo;s slower than running it locally doesn&rsquo;t make any sense.</p>
<p>Local models are free in tokens, but they pay in time. On the AMD Strix, that math was a non-starter for every Qwen I tested: two minutes waiting for a long response, multiplied by 50 turns, eats your whole afternoon. But that changes when the hardware changes, and that&rsquo;s why I redid the local part of the benchmark on a different machine.</p>
<h2>AMD Strix Halo vs NVIDIA RTX 5090: what changes when the memory bandwidth doubles<span class="hx:absolute hx:-mt-20" id="amd-strix-halo-vs-nvidia-rtx-5090-what-changes-when-the-memory-bandwidth-doubles"></span>
    <a href="#amd-strix-halo-vs-nvidia-rtx-5090-what-changes-when-the-memory-bandwidth-doubles" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To check whether the bottleneck was hardware or model, I took the same Qwen models and reran the benchmark on a workstation with an NVIDIA RTX 5090 (Blackwell, 32 GB GDDR7, 1,792 GB/s bandwidth). The numbers shift in a way that&rsquo;s worth looking at carefully.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">AMD Strix (LPDDR5x)</th>
          <th style="text-align: right">NVIDIA 5090 (GDDR7)</th>
          <th style="text-align: right">Speedup</th>
          <th style="text-align: right">Total time on 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3 32B (dense)</td>
          <td style="text-align: right">7 tok/s</td>
          <td style="text-align: right">69 tok/s</td>
          <td style="text-align: right">~10x</td>
          <td style="text-align: right">4 min</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B (Coder)</td>
          <td style="text-align: right">37 tok/s</td>
          <td style="text-align: right">145 tok/s</td>
          <td style="text-align: right">~4x</td>
          <td style="text-align: right">6 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B (MoE)</td>
          <td style="text-align: right">46 tok/s</td>
          <td style="text-align: right"><strong>273 tok/s</strong></td>
          <td style="text-align: right">~6x</td>
          <td style="text-align: right">5 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude (distilled)</td>
          <td style="text-align: right">timeout 90m</td>
          <td style="text-align: right">129 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">12 min</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">(didn&rsquo;t test on Strix)</td>
          <td style="text-align: right">213 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">8 min</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: right">(didn&rsquo;t test on Strix)</td>
          <td style="text-align: right">2.86 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">timeout 90m</td>
      </tr>
  </tbody>
</table>
<p>To put those speeds in context, remember that in the cloud Sonnet runs at 532 tok/s, Opus at 347 tok/s, Step 3.5 Flash at 242 tok/s, Gemini 3.1 Pro at 128 tok/s and Kimi K2.5 at 160 tok/s. Qwen 3.5 35B-A3B on the 5090, at 273 tok/s, is in the same neighborhood as Step 3.5 Flash, faster than Gemini, Kimi and GLM 5.1. Qwen 3 Coder 30B at 145 tok/s is in Gemini territory. The classic line &ldquo;local models are ten times slower than cloud&rdquo; stopped being true the moment the 5090 entered the conversation.</p>
<p>The practical consequence is that the &ldquo;time is money&rdquo; argument shifts. On the Strix, &ldquo;waiting an hour for a Qwen 3.5 122B to do what Sonnet does in 16 minutes&rdquo; is straight-up loss. On the 5090, waiting 5 minutes for Qwen 3.5 35B-A3B to do the work, plus 10-15 minutes for you to do 1-2 correction prompts, gives you a total in the 20-25 minute range. Sonnet does it in 16 minutes with zero corrections. The difference shrank enough that, if cost matters a lot, it&rsquo;s worth it.</p>
<p>The catch: for this to be worth it, the model has to be close enough to the right answer that 1-2 correction prompts can fix it. When the error is &ldquo;the model decided not to use the gem I asked for and returned a hardcoded mock string,&rdquo; like Qwen 3 Coder 30B did, no easy correction prompt fixes that. That&rsquo;s a redo.</p>
<h3>Before you spend money on hardware thinking it&rsquo;s the answer<span class="hx:absolute hx:-mt-20" id="before-you-spend-money-on-hardware-thinking-its-the-answer"></span>
    <a href="#before-you-spend-money-on-hardware-thinking-its-the-answer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I&rsquo;ve got to give a warning here, because it&rsquo;s the most common buying mistake I see right now. Every other week somebody tells me they&rsquo;re going to grab a Ryzen AI Max because it has 128 GB of unified memory and that &ldquo;lets you run huge models.&rdquo; Technically, sure — the model fits. In practice, it&rsquo;s almost unusable. The memory is LPDDR5x at 256 GB/s, seven times slower than the 5090&rsquo;s GDDR7. What fits doesn&rsquo;t run at human speed. My own Strix with Qwen 3.5 122B hit 22 tok/s and the run took 43 minutes. To do anything serious day to day, that&rsquo;s not workable.</p>
<p>The 5090 is clearly superior, and it starts to make sense even for smaller models precisely because of the memory bandwidth. A Mac Studio with high-speed unified memory (up to 800 GB/s on the M4 Ultra) is the other viable option, and costs proportionally the same. But neither of those comes anywhere close to beating the commercial models on quality — and the per-token price of Claude, GPT or GLM, combined with their brutal inference speed, makes the math hard to justify for anyone who isn&rsquo;t an enthusiast or a researcher. Expensive local AI hardware is a weekend hobby, a tool for people who need to run offline for compliance reasons, or a research playground. For day-after-day production work, right now, cloud is still the rational choice. A 128 GB Ryzen AI Max may look tempting on the spec sheet, but if the goal is serious coding agent work, it&rsquo;s money badly spent.</p>
<h2>The Qwen family: Coder vs General, distillation, and why nothing is a silver bullet<span class="hx:absolute hx:-mt-20" id="the-qwen-family-coder-vs-general-distillation-and-why-nothing-is-a-silver-bullet"></span>
    <a href="#the-qwen-family-coder-vs-general-distillation-and-why-nothing-is-a-silver-bullet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With so many different Qwens running in this rerun, it&rsquo;s worth doing a more focused analysis. What I learned might surprise people who follow model benchmarks on Twitter.</p>
<h3>Before getting to the results: what quantization is and what distillation is<span class="hx:absolute hx:-mt-20" id="before-getting-to-the-results-what-quantization-is-and-what-distillation-is"></span>
    <a href="#before-getting-to-the-results-what-quantization-is-and-what-distillation-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>These two concepts come up constantly in this discussion and they deserve a quick explanation.</p>
<p><strong>Quantization</strong> is the technique of compressing the model&rsquo;s weights so they take up less memory. A model trained in FP16 (16 bits per weight) can be quantized to Q8 (8 bits), Q4 (4 bits), Q3_K_M (3 bits, but with medium-sized groupings), and so on. Each step halves the size of the model on disk and in VRAM, at the cost of some loss of precision. Q8 is practically lossless. Q4 already loses something measurable. Q3 loses more. Q2 is the line where the model starts saying real nonsense. The rule of thumb is that for coding and multi-step reasoning, you want to stay at Q4 or higher. Q3_K_M is the minimum that still works for many models, and it&rsquo;s what fits a 27B on the 5090 with 128k context.</p>
<p>The surprise from my test, and look, this goes against the consensus, is that quantization wasn&rsquo;t the bottleneck here. I ran the Qwen 3.5 27B Claude-distilled in two versions: Q8 on the AMD Strix (~27 GB of weights) and Q3_K_M on the 5090 (~12 GB of weights). Both hallucinated exactly the same fake RubyLLM APIs. Q3_K_M even produced a cleaner Gemfile. The model&rsquo;s limitation was in what those weights know, not in the precision they were compressed to.</p>
<p><strong>Distillation</strong> is the technique of training a smaller model (the &ldquo;student&rdquo;) to imitate the output or behavior of a larger model (the &ldquo;teacher&rdquo;). The classic version is logit distillation — the student learns to approximate the teacher&rsquo;s probability distributions. The modern, more popular version for coding agents is distillation of <strong>reasoning traces</strong>: you take chain-of-thought from the big model on real problems and train the smaller one to reproduce the same reasoning style.</p>
<p>The hype of the moment is distilling Claude and GPT into open source models. The promise is that you can have &ldquo;Claude-at-home&rdquo; running locally. I wanted to test this, and that&rsquo;s why I added <a href="https://huggingface.co/Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled"target="_blank" rel="noopener">Jackrong&rsquo;s Qwen 3.5 27B distilled from Claude 4.6 Opus</a> to the benchmark. If any open source model was going to use RubyLLM correctly, this was the bet — after all, in the entire benchmark, Claude and GLM 5 are the only ones that get the API right.</p>
<h3>What the Claude-distilled learned (and what it didn&rsquo;t)<span class="hx:absolute hx:-mt-20" id="what-the-claude-distilled-learned-and-what-it-didnt"></span>
    <a href="#what-the-claude-distilled-learned-and-what-it-didnt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I ran the same distillation twice: once at Q8 on the AMD Strix (which blew through the 90-minute timeout), and once at Q3_K_M on the 5090 (completed in 12 minutes). Both produced the same elegant frustration.</p>
<p>The code that comes out looks like Claude. It has <code># frozen_string_literal: true</code> at the top of every file. It has a separate <code>Response</code> class as a value object with explicit attribute readers. It has a clear separation between service, controller and model. It has doc comments at the top of every file. It correctly comments out things like <code>active_record</code>, <code>active_job</code> and <code>action_mailer</code> in <code>application.rb</code>. It has defensive <code>case</code> statements trying multiple return formats. Stylistically, it&rsquo;s Claude.</p>
<p>Functionally, it&rsquo;s a complete RubyLLM hallucination. Look at the service generated by the 5090 run:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">RubyLLM</span><span class="o">::</span><span class="no">Chat</span><span class="o">.</span><span class="n">new</span><span class="o">.</span><span class="n">with_model</span><span class="p">(</span><span class="vi">@model</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">chat</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">conversation_history</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">msg</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">.</span><span class="n">add_message</span><span class="p">(</span><span class="ss">role</span><span class="p">:</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="n">msg</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">Response</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">,</span> <span class="ss">usage</span><span class="p">:</span> <span class="n">build_usage</span><span class="p">(</span><span class="n">response</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>Every primitive in this code is invented:</p>
<ul>
<li><code>RubyLLM::Chat.new</code> — the constructor isn&rsquo;t public, the correct entry is <code>RubyLLM.chat(model:)</code></li>
<li><code>.with_model(@model) do |chat| ... end</code> — there&rsquo;s no block API like that</li>
<li><code>chat.add_message(role:, content:)</code> — doesn&rsquo;t exist</li>
<li><code>response.text</code> — the real API exposes <code>response.content</code></li>
<li><code>response.usage.prompt_tokens</code> — the object doesn&rsquo;t have that shape</li>
</ul>
<p>This will blow up with a <code>NoMethodError</code> on the first request. The initializer also tries <code>config.openrouter_api_base=</code> which doesn&rsquo;t exist on <code>RubyLLM.configure</code>, so the app probably won&rsquo;t even boot.</p>
<p>The Q8 version on the AMD Strix does the exact same thing, with one difference: the entry call is <code>RubyLLM.chat(model:, provider: :openrouter)</code> — the entry point is right, but <code>provider:</code> is invented and it&rsquo;s immediately followed by the same fake <code>chat.add_message(role:, content:)</code>. Worse, the Gemfile from the 90-minute run lists <code>gem &quot;ruby-openai&quot;</code> (wrong gem!), <code>gem &quot;minitest&quot;, &quot;~&gt; 6.0&quot;</code> (minitest 6.0 doesn&rsquo;t exist) and <code>gem &quot;tailwindcss&quot;</code> (wrong gem name, it&rsquo;s <code>tailwindcss-rails</code>). The Gemfile doesn&rsquo;t include the gem the service code itself is trying to use.</p>
<p>For comparison, look at the actual Claude Opus 4.6 baseline, in the same benchmark, getting it all right:</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="n">model_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="vi">@chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</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>Twelve lines in the entire service. Zero hallucination. Includes streaming via block. The distilled model produced three times the code volume and got the API wrong.</p>
<p>The honest reading is that distillation transferred one layer and stopped. The layer that came along was the style: code organization, comments, class structure, the order of things. The layer that got left behind was factual memory about specific libraries. That makes sense when you think about it: Claude&rsquo;s reasoning traces, even when written carefully, rarely contain repeated references to <code>chat.ask(msg).content</code> in some obscure Ruby gem. The student only learns what the teacher repeats, and Claude never had any reason to keep whispering &ldquo;use ask, not complete&rdquo; throughout its chains of thought. Library API knowledge is binary recall memory, the kind that&rsquo;s either in the weights or it isn&rsquo;t. Decomposing that into reasoning steps is impossible because it isn&rsquo;t reasoning, it&rsquo;s just raw memorization.</p>
<p>To wrap up the practical recommendation: if you need the model to actually use RubyLLM, or any less-popular library for that matter, Claude distillation won&rsquo;t save you. Use real Claude or GLM 5. The &ldquo;Claude-stand-ins&rdquo; in open source will fail the same way the Qwen base would, just with prettier handwriting.</p>
<h3>Coder vs General: the surprise of the &ldquo;for coding&rdquo; models<span class="hx:absolute hx:-mt-20" id="coder-vs-general-the-surprise-of-the-for-coding-models"></span>
    <a href="#coder-vs-general-the-surprise-of-the-for-coding-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Almost everyone&rsquo;s instinct is that models with &ldquo;Coder&rdquo; in the name are the best for programming. Makes sense, they were specifically fine-tuned on code. But in the benchmark, it was exactly the opposite.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Type</th>
          <th>Hardware</th>
          <th style="text-align: right">Time</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>General (MoE)</td>
          <td>5090</td>
          <td style="text-align: right">5 min</td>
          <td>Runs Rails, hallucinates <code>add_message</code>/<code>complete</code> (1-2 follow-ups fix it)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Returned a hardcoded mock string instead of calling RubyLLM</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">timeout 90m</td>
          <td>Zero files, model froze</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td>General</td>
          <td>5090</td>
          <td style="text-align: right">4 min</td>
          <td>Partial scaffold, errors</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td>General + distilled</td>
          <td>5090</td>
          <td style="text-align: right">12 min</td>
          <td>Runs Rails, hallucinates the entire API</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Sushi Coder RL</td>
          <td>Coder (RL)</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Infrastructure failure, couldn&rsquo;t be tested</td>
      </tr>
  </tbody>
</table>
<p>Of the three dedicated Coders, two failed catastrophically (full timeout and hardcoded mock string) and one didn&rsquo;t even run properly because of an infra bug. Meanwhile, the Qwen 3.5 35B-A3B, which is the general model in the line (not the Coder), came closest to something usable: 5 minutes of execution, recognizable Rails project, and the problem is fixable in 1-2 prompts.</p>
<p>Qwen 3 Coder 30B is particularly disappointing. It went so far past trying to use the API that it didn&rsquo;t really try at all: the controller it generated has 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">V1</span><span class="o">::</span><span class="no">MessagesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span>
</span></span><span class="line"><span class="cl">    <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">response</span><span class="p">:</span> <span class="s2">&#34;This is a mock response. In a real implementation, this would connect to RubyLLM with Claude Sonnet via OpenRouter.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>The Gemfile lists <code>gem &quot;ruby_llm&quot;</code> but nothing imports it. The service layer is nonexistent. The model decided it was easier to return a fake string and call it a day. That&rsquo;s Tier 3 garbage in a way no correction prompt fixes — you have to tell it to start over.</p>
<p>Qwen 2.5 Coder 32B is even worse: 90 minutes running, zero files. The 1.8 MB <code>opencode-output.ndjson</code> shows the model spinning without managing to write anything. It probably got stuck in a planning loop without ever calling the write tools. Total slot waste.</p>
<p>Why did the &ldquo;Coder&rdquo; Qwens do so badly? My read is that the coding-specific fine-tuning they got was trained on more isolated problems (Codeforces, Leetcode, short snippets), far from agentic flows with long-running tool calling. The general Qwen 3.5 35B-A3B has broader training and handles the orchestration part better. The popular intuition &ldquo;Coder = best for coding agent&rdquo; is wrong for this kind of task. The use case where Coders shine is &ldquo;complete an isolated function,&rdquo; which is exactly what they were trained for, and that&rsquo;s a tiny fraction of what a coding agent does day to day.</p>
<h3>The question I wanted to answer<span class="hx:absolute hx:-mt-20" id="the-question-i-wanted-to-answer"></span>
    <a href="#the-question-i-wanted-to-answer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It was this: running locally on the 5090, which Qwen model is worth the 1-2 correction prompts to deliver code that works?</p>
<p>The honest answer is: only Qwen 3.5 35B-A3B, and maybe the Claude-distilled if you don&rsquo;t mind spending 12 minutes more.</p>
<ul>
<li>Qwen 3.5 35B-A3B on the 5090: 5 minutes, correct entry point (<code>RubyLLM.chat(model:)</code>), errors on the subsequent calls. Realistic total until it works: in the 15-20 minute range with 1-2 follow-ups. Beats cloud OSS on cost.</li>
<li>Qwen 3.5 27B Claude-distilled on the 5090: 12 minutes, deeper hallucination (entry point is invented too). Realistic total: 25-30 minutes with 2-3 follow-ups. Still competes on cost, and loses on absolute time to the real Claude.</li>
<li>The others (Coder 30B, Coder 2.5 32B, 3 32B): don&rsquo;t pay back the correction time. Each one has a structural problem that calls for a full rewrite from scratch.</li>
</ul>
<p>For folks with hardware in this category who want to escape Anthropic vendor lock-in, it now works. It didn&rsquo;t work on the 5090 from last year, and forget about it on the Strix Halo. In 2026, on NVIDIA Blackwell, with the right model, it works. For folks with low-bandwidth hardware (LPDDR5x, DDR4, DDR5), it&rsquo;s still a waste of time: the clock alone takes down any plan to make this practical.</p>
<h3>Qwen 3.6: what changed from 3.5<span class="hx:absolute hx:-mt-20" id="qwen-36-what-changed-from-35"></span>
    <a href="#qwen-36-what-changed-from-35" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>We tested two flavors of Qwen 3.6: the <strong>3.6 Plus</strong> (cloud, OpenRouter, free) and the <strong>3.6 35B</strong> (local, NVIDIA 5090, Q3_K_M).</p>
<p>Qwen 3.6 Plus (cloud) completed in 17 minutes with 88,940 tokens and 9/9 on the artifact checklist. Completed fast and for free. But the generated service uses <code>chat.add_message()</code>, a method that doesn&rsquo;t exist in RubyLLM. First message works, second one crashes. Same problem as 3.5.</p>
<p>Qwen 3.6 35B (local, 5090) is more interesting. Completed in 4.7 minutes at 240 tok/s, 169 files, correct entry point <code>RubyLLM.chat(model:, provider:)</code> and correct <code>chat.ask(message)</code>. The bug is subtler: it returns <code>response</code> instead of <code>response.content</code> and doesn&rsquo;t do history replay. One-line fix. That&rsquo;s a real improvement over Qwen 3.5 35B-A3B (which hallucinated <code>add_message</code> and <code>complete</code>). It&rsquo;s the cleanest Qwen result we&rsquo;ve seen.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Version</th>
          <th>Hardware</th>
          <th style="text-align: center">Correct API?</th>
          <th style="text-align: center">Multi-turn works?</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Tok/s</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point yes, <code>add_message</code>/<code>complete</code> no</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">273</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">No (entry point invented)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">12m</td>
          <td style="text-align: right">129</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td>3.6</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point yes, <code>chat.ask</code> yes, missing <code>.content</code></td>
          <td style="text-align: center">No (no replay)</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">240</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>3.6</td>
          <td>Cloud</td>
          <td style="text-align: center">Entry point yes, <code>add_message</code> no</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">183</td>
      </tr>
  </tbody>
</table>
<p>The gap shrank but didn&rsquo;t close. The 3.6 35B local is closer to working than any previous Qwen — the bug is a forgotten <code>.content</code>, not an entirely invented API. But it still doesn&rsquo;t do multi-turn out of the box. In practice, Qwen 3.6 35B local moves up from Tier 3 to Tier 2: it&rsquo;s the local open source model that comes closest to delivering correct code on the first try, one fix away from working.</p>
<h2>The Deep Code Review: Sonnet vs GLM 5 vs Gemini vs Kimi vs MiniMax<span class="hx:absolute hx:-mt-20" id="the-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax"></span>
    <a href="#the-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The tables above measure structural completeness. But does the project work? I did detailed code review of the models that completed the benchmark.</p>
<p><strong>Claude Sonnet 4.6 — works and is the most complete.</strong> Synchronous responses via Turbo Stream. Chat history persisted in a session cookie with full replay of previous messages on every request. Correct LLM mocking in the tests with mocha (30 tests in 328 lines). LLM logic extracted into a separate <code>LlmChatService</code>. Views decomposed into 9 partials. Minor problems: duplicated model constant, leak in the auto-resize event listener. None are blockers. Of the generated projects, it&rsquo;s the closest to something you&rsquo;d actually put into production.</p>
<p><strong>GLM 5 — works, but it&rsquo;s the bare minimum.</strong> Uses the correct API (<code>RubyLLM.chat(model:)</code> then <code>.ask()</code>), does mocking with mocha in the tests. But the project is way leaner than Sonnet&rsquo;s: 21-line controller (vs Sonnet&rsquo;s 52), no service layer (LLM logic inline in the controller), no chat history persistence, every message handled in isolation. The first message works, but the app doesn&rsquo;t keep conversation context, so you can&rsquo;t have a multi-turn dialog. The tests exist (7 methods) but they&rsquo;re skeletal: <code>ruby_llm_test.rb</code> only checks that the module is loaded, <code>chat_flow_test.rb</code> is a copy of the controller test. The Dockerfile, on the other hand, is the best of the four: multi-stage, non-root, jemalloc. But as a chat app? It&rsquo;s more of a proof of concept than something functional. Funny detail: the README says &ldquo;Powered by Claude Sonnet 4&rdquo; instead of the model that actually generated the project.</p>
<p><strong>Gemini 3.1 Pro — the fastest, but trips on the API.</strong> Completed in 14 minutes, the fastest along with MiniMax. The Rails code itself is well written: uses <code>Rails.cache</code> with session ID and a 2-hour expiration to keep state (instead of a database), Turbo Streams nicely integrated, Stimulus controller for auto-scroll, and the Dockerfile is the best of the group (multi-stage, non-root, jemalloc). The problem is the usual one: it uses <code>RubyLLM::Chat.new()</code> instead of <code>RubyLLM.chat()</code>, and calls <code>add_message()</code> which doesn&rsquo;t exist. The app boots, Docker runs, the health check passes, but the first chat message returns 500. The tests (5 methods) mock with a <code>FakeChat</code> that replicates the wrong signature, so they pass. It&rsquo;s frustrating because the rest of the code is the most &ldquo;Rails way&rdquo; of the non-Anthropic models. Fixing it would be 3 lines, but the benchmark measures what comes out the first time.</p>
<p><strong>Kimi K2.5 — ambitious but broken.</strong> Tried the most sophisticated architecture: ActionCable streaming, configurable models, dual Dockerfiles, 37 tests in 374 lines. Problem: the streaming depends on ActionCable, which is commented out in <code>config/application.rb</code>. The <code>return unless defined?(ActionCable)</code> guard makes the method do nothing. The assistant never responds. The Stimulus controller has a scope bug: <code>submitTarget</code> references a button outside the controller&rsquo;s subtree. Thread-unsafe storage with a hash in a class variable. Kimi wrote more tests than any other model (37), but none of them mock the LLM calls — so the tests pass without proving any of the functionality works.</p>
<p><strong>Grok 4.20 — fast and wrong.</strong> It was the fastest in the entire benchmark: 8 minutes, 412 tok/s. Except it was fast because it cut corners. The prompt explicitly asked for the <code>ruby_llm</code> gem, and Grok ignored it. It went straight for <code>OpenAI::Client</code> from the <code>ruby-openai</code> gem pointing at the OpenRouter URL. Technically the first message comes back, so yeah, it &ldquo;works.&rdquo; But it&rsquo;s the same trick as Step 3.5 Flash and Qwen 3.5 122B: skip the part that was actually being tested. No history, 33-line controller calling the HTTP client by hand, two tests, no real mocks. It was fast because it did less than what was asked.</p>
<p><strong>MiniMax M2.7 — looks right, crashes.</strong> Calls <code>RubyLLM.chat(model: '...', messages: [...])</code> — that signature doesn&rsquo;t exist. No message persistence. Duplicated HTML (DOCTYPE inside the layout). Committed master.key. And the tests? They mock the wrong API, so they pass but they don&rsquo;t prove anything.</p>
<p>Code review summary:</p>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th style="text-align: center">Sonnet 4.6</th>
          <th style="text-align: center">GLM 5</th>
          <th style="text-align: center">Gemini 3.1 Pro</th>
          <th style="text-align: center">Kimi K2.5</th>
          <th style="text-align: center">MiniMax M2.7</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Correct API</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
      </tr>
      <tr>
          <td>Chat history</td>
          <td style="text-align: center">Session cookie</td>
          <td style="text-align: center">None</td>
          <td style="text-align: center">Rails.cache (2h)</td>
          <td style="text-align: center">Broken (ActionCable off)</td>
          <td style="text-align: center">None</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td style="text-align: center">LlmChatService</td>
          <td style="text-align: center">Inline in controller</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">ChatService (wrong API)</td>
      </tr>
      <tr>
          <td>Tests (methods)</td>
          <td style="text-align: center">30</td>
          <td style="text-align: center">7</td>
          <td style="text-align: center">5</td>
          <td style="text-align: center">37</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td>LLM mocking</td>
          <td style="text-align: center">Yes (mocha)</td>
          <td style="text-align: center">Yes (mocha)</td>
          <td style="text-align: center">FakeChat (wrong API)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">Mocks wrong API</td>
      </tr>
      <tr>
          <td>Dockerfile</td>
          <td style="text-align: center">Multi-stage</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Dual (dev/prod)</td>
          <td style="text-align: center">Single-stage</td>
      </tr>
      <tr>
          <td>Actually runs?</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">Yes (no history)</td>
          <td style="text-align: center">No (500 in chat)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
      </tr>
  </tbody>
</table>
<h3>GLM 5 vs GLM 5.1: what changed<span class="hx:absolute hx:-mt-20" id="glm-5-vs-glm-51-what-changed"></span>
    <a href="#glm-5-vs-glm-51-what-changed" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM 5 was one of the few models that spat out functional code on the first try, so it was obvious to test the new version. One important detail before the numbers: GLM 5 ran via OpenRouter, GLM 5.1 wasn&rsquo;t there yet when I ran this test, so I used the Z.AI direct API. Different provider, different infra, different cache. The numbers below are reference, not exact measurement.</p>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th>GLM 5</th>
          <th>GLM 5.1</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Provider</td>
          <td>OpenRouter</td>
          <td>Z.AI direct</td>
      </tr>
      <tr>
          <td>Total time</td>
          <td>17m</td>
          <td>22m</td>
      </tr>
      <tr>
          <td>Tok/s (final phase)</td>
          <td>400</td>
          <td>167</td>
      </tr>
      <tr>
          <td>Effective new tokens</td>
          <td>1,138</td>
          <td>450</td>
      </tr>
      <tr>
          <td>Cache read</td>
          <td>58,240</td>
          <td>81,216</td>
      </tr>
      <tr>
          <td>Correct RubyLLM API</td>
          <td>Yes</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Test mocking</td>
          <td>Yes (mocha)</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Tests</td>
          <td>7</td>
          <td>24</td>
      </tr>
      <tr>
          <td>Chat history</td>
          <td>No</td>
          <td>Yes (in-memory)</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td>Inline in controller</td>
          <td><code>ChatSession</code> model with <code>add_user_message</code>/<code>add_assistant_message</code></td>
      </tr>
  </tbody>
</table>
<p>The GLM 5.1 project came out way more complete. 24 tests vs 7. Real separation between <code>ChatSession</code>, <code>ChatMessage</code> and the controller, instead of GLM 5 cramming everything inline. Chat history persisted in memory during the session, so you can actually have a real multi-turn conversation (GLM 5 treated every message like it was the first). And the RubyLLM API is still correct, the same <code>RubyLLM.chat(model:, provider:)</code> pattern followed by <code>c.user</code>/<code>c.assistant</code> to build the context. There&rsquo;s even a test covering the <code>MODEL</code> constant, which usually nobody does.</p>
<p>The price was speed. 22 minutes vs 17, and throughput dropped from 400 to 167 tok/s. Could be the provider (Z.AI direct isn&rsquo;t the same infra as OpenRouter), could be a more loaded server during the run, could be that 5.1 reasons more. I didn&rsquo;t run it multiple times to take an average, so I won&rsquo;t say 5.1 is &ldquo;slower.&rdquo; A single run doesn&rsquo;t prove a regression. What I can say is that, in my test, 5.1 delivered a better-structured project and took a bit longer to do it.</p>
<p>For folks who want to get out from under Anthropic without losing quality, GLM 5 and GLM 5.1 are the two options that work. If you need centralized billing on OpenRouter, GLM 5. If you can use Z.AI direct and want a more rounded project on the first try, GLM 5.1.</p>
<h2>Costs: API vs Subscription<span class="hx:absolute hx:-mt-20" id="costs-api-vs-subscription"></span>
    <a href="#costs-api-vs-subscription" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>First, the per-token price of each model on OpenRouter:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-pricing.png" alt="Per-token price on OpenRouter"  loading="lazy" /></p>
<p>GPT 5.4 Pro charges $180 per million output tokens. Claude Opus charges $25. GLM 5 charges $2.30. And Qwen 3.6 Plus is free (with a rate limit). The log scale on the chart hides some of the brutality of the gap: from free Qwen to GPT 5.4 Pro is orders of magnitude.</p>
<p>But per-token price isn&rsquo;t the whole story. If you use Claude or GPT daily for coding, the monthly subscription can come out way cheaper than paying per token via the API:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/monthly-pricing.png" alt="Subscription vs API: how much it costs to use Claude and GPT per month"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th style="text-align: right">Est. $/month*</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.6 Plus (OpenRouter)</td>
          <td style="text-align: right">$0</td>
          <td>Free but rate-limited</td>
      </tr>
      <tr>
          <td>Local models</td>
          <td style="text-align: right">Electricity</td>
          <td>Needs hardware</td>
      </tr>
      <tr>
          <td>Claude Pro</td>
          <td style="text-align: right">$20</td>
          <td>~44K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Plus</td>
          <td style="text-align: right">$20</td>
          <td>Includes Codex</td>
      </tr>
      <tr>
          <td>Claude Max 5x</td>
          <td style="text-align: right">$100</td>
          <td>~88K tokens/5hr</td>
      </tr>
      <tr>
          <td>Claude Sonnet (OpenRouter API)</td>
          <td style="text-align: right">~$150</td>
          <td>No cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>Claude Max 20x</td>
          <td style="text-align: right">$200</td>
          <td>~220K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Pro</td>
          <td style="text-align: right">$200</td>
          <td>GPT 5.4 Pro unlimited</td>
      </tr>
      <tr>
          <td>Claude Opus (OpenRouter API)</td>
          <td style="text-align: right">~$450</td>
          <td>No cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>GPT 5.4 Pro (OpenRouter API)</td>
          <td style="text-align: right">~$990</td>
          <td>Absurdly expensive</td>
      </tr>
  </tbody>
</table>
<p>*Estimate for moderate coding use (~15M input + ~3M output tokens/month).</p>
<p>The main point: if you use GPT 5.4 Pro, the ChatGPT Pro subscription at $200/month with unlimited use is 5x cheaper than paying per token on the API. For Claude, Pro at $20/month covers light use, but for heavy users (a coding marathon like mine), the Max 20x at $200/month comes out cheaper than paying for Opus per token on OpenRouter (~$450/month). The open source models on OpenRouter all sit below $2.50/M output tokens, but as we saw, most of them generate code that doesn&rsquo;t run.</p>
<h2>What works for real use<span class="hx:absolute hx:-mt-20" id="what-works-for-real-use"></span>
    <a href="#what-works-for-real-use" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After testing 33 models across both runs and looking at the generated code in detail:</p>
<p>Tier 1 (works plug and play):</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Quality</th>
          <th style="text-align: right">Cost/Run</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>New baseline (28 tests, FakeChat, 96.7% coverage)</td>
          <td style="text-align: right">~$1.10</td>
          <td>Incremental over 4.6, new gold standard</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>Better than Opus 4.6 on opencode (30 vs 16 tests)</td>
          <td style="text-align: right">~$0.63</td>
          <td>Cheaper, but fails on deeper reasoning in real projects</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>Previous gold standard</td>
          <td style="text-align: right">~$1.05</td>
          <td>Previous baseline</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>Good (7 tests, correct API)</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% cheaper, non-Anthropic/OpenAI alternative that works</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Good (24 tests, history, correct API)</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% cheaper, more complete project than GLM 5</td>
      </tr>
  </tbody>
</table>
<p>Tier 2 (works with caveats):</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: right">Cost/Run</th>
          <th>Caveat</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$16.00</td>
          <td>Impressive architecture (22 tests, dependency injection, PORO models), but <code>add_message</code> with keyword args instead of positional hash breaks multi-turn. 7.6M tokens, 15x more expensive than Opus</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.02</td>
          <td>Bypasses the requested gem, slow (38m)</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.04</td>
          <td>Bypasses the requested gem (goes straight to <code>OpenAI::Client</code>), but it&rsquo;s the fastest in the benchmark</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Entry point and <code>chat.ask</code> correct, missing <code>.content</code>. One-line fix. ~10-15 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Correct entry point, hallucinates <code>add_message</code>/<code>complete</code>. Fixable in 1-2 follow-ups. ~15-20 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Claude style, complete API hallucination. 2-3 follow-ups to fix. ~25-30 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B (local)</td>
          <td style="text-align: center">AMD Strix</td>
          <td style="text-align: right">Free</td>
          <td>Works if default is configured, no mocking, and slow</td>
      </tr>
  </tbody>
</table>
<p>Tier 3 (broken code, easier to redo than to fix):</p>
<p>Kimi K2.5, MiniMax M2.7, DeepSeek V3.2, Gemini 3.1 Pro, Qwen 3 Coder Next (Strix), Qwen 3 Coder 30B (5090, returned a hardcoded mock string), Qwen 3.5 122B, Qwen 3.6 Plus — all of them either invent APIs that don&rsquo;t exist or don&rsquo;t even try to use the gem.</p>
<p>Tier 4 (didn&rsquo;t complete):</p>
<p>Gemma 4 (infinite loop on both hardware), Llama 4 Scout (no parser), GPT OSS 20B (wrong directory on Strix, parser regression on 5090), Qwen 3 32B (too slow on Strix, partial scaffold on 5090), Qwen 2.5 Coder 32B (90m timeout with zero files).</p>
<h3>Simplified ranking (quality, time, price)<span class="hx:absolute hx:-mt-20" id="simplified-ranking-quality-time-price"></span>
    <a href="#simplified-ranking-quality-time-price" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>For folks who only want the report-card summary. Quality is whether the code runs and how complete it is. Time is the total runtime. Price is the estimated cost per execution on opencode. <strong>Hardware</strong> indicates where the model ran — Cloud, Strix (AMD Strix Halo, LPDDR5x 256 GB/s) or 5090 (NVIDIA RTX 5090, GDDR7 1792 GB/s). Cloud models ran via OpenRouter or the provider&rsquo;s direct API.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Type</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: center">Quality</th>
          <th style="text-align: center">Time</th>
          <th style="text-align: center">Price</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">F</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A−</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">C+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">B</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
  </tbody>
</table>
<p>Quality criteria: A+ works and the code is well structured. A/B works with small to medium caveats. C runs but skips a prompt requirement or has a serious structural issue. D breaks on the first message because of an invented API. F didn&rsquo;t complete the benchmark or produced garbage. GPT 5.4 via Codex dropped from A+ to B+: the architecture is the most sophisticated in the benchmark (dependency injection, PORO models, 22 tests), but the first message works and multi-turn breaks due to a wrong calling convention on <code>add_message</code>. Burned 7.6M tokens (~$16/run) without reaching Opus-level correctness. &ldquo;Type&rdquo; separates commercial models (closed weights) from OSS (open weights, even when used through a hosted API). Some Qwens appear twice when they ran on both hardware profiles, because the results are different enough to justify it — Qwen 3.5 35B-A3B on the 5090 jumps to Tier B, on the Strix it stays at Tier C because of the wait time. Of the 33 models configured across both runs, some don&rsquo;t appear in this table because they never even executed (no quota, broken runner, infra failure, or timeout before the first message).</p>
<h3>The verdict<span class="hx:absolute hx:-mt-20" id="the-verdict"></span>
    <a href="#the-verdict" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you want the best result and don&rsquo;t want to think about it: <strong>Claude Opus</strong>. Opus 4.7 is the new baseline — 28 tests, correct API, 96.7% coverage, FakeChat pattern in the tests. It&rsquo;s an incremental improvement over 4.6 (which had 16 tests), not a revolution. But it doesn&rsquo;t need to be a revolution. 4.6 already worked, 4.7 works the same and delivers a slightly more polished project. If you were on 4.6, upgrading to 4.7 means switching to a model that does the same thing with more care in the tests and structure.</p>
<p>A word about Sonnet: it beat Opus in this benchmark (30 tests vs 16 for Opus 4.6, vs 28 for Opus 4.7). But this benchmark is a small, well-defined web app. In my real-world experience with bigger projects, Sonnet fails when the reasoning needs to go deeper. I&rsquo;m not talking about massive projects — just going a bit further than this benchmark (more controllers, more integrations, architectural decisions that depend on each other) is enough for Sonnet to lose the thread. Opus has a 128K max output token ceiling vs Sonnet&rsquo;s 64K, and its training was specifically aimed at long-horizon tasks, multi-step planning and deep reasoning over complex code. On a small project like the benchmark, those muscles stay idle, and in that scenario Sonnet wins by being faster and cheaper. But if you extrapolate that to &ldquo;Sonnet is better than Opus,&rdquo; you&rsquo;ll get a surprise on the first task that requires sustained reasoning. You can try Sonnet — it&rsquo;s cheaper and for small projects it works. But for real projects, you&rsquo;ll probably end up on Opus anyway.</p>
<p>On GPT 5.4: we now have objective data. Ran it via Codex CLI with xHigh reasoning effort. The architecture it generates is the most sophisticated in the benchmark — dependency injection, PORO models, session management with TTL. But it burned 7.6M tokens (~$16/run, 15x more expensive than Opus) and got the <code>add_message</code> calling convention wrong (keyword args instead of positional hash), breaking multi-turn. Spent more, got it wrong. Dropped from Tier 1 to Tier 2. Same pattern: API correctness is binary recall in the weights. It doesn&rsquo;t scale with token budget or reasoning effort.</p>
<p>If cost matters and you want to leave Anthropic: <strong>GLM 5 or GLM 5.1</strong> are the plug-and-play alternatives that work. Correct API, mocking in the tests, ~$0.11-$0.13 per run, ~88-89% cheaper than Opus. GLM 5.1 delivered a more complete project (24 tests, chat history) at the cost of about 5 more minutes.</p>
<p>If you want to avoid total vendor lock-in and you have decent hardware: <strong>Qwen 3.6 35B</strong> running locally on an NVIDIA RTX 5090. Under 5 minutes of execution at 240 tok/s, correct entry point and <code>chat.ask</code>, just missing <code>.content</code> extraction — a 1-line fix. That&rsquo;s better than the Qwen 3.5 35B-A3B which hallucinated entire API methods. The 3.6 generation is the first Qwen that&rsquo;s genuinely one fix away from working, not a rewrite away. Realistic total: ~10-15 minutes with one follow-up.</p>
<p>If you want to avoid vendor lock-in but you&rsquo;re on weak hardware: <strong>GLM 5 or GLM 5.1</strong> remain the choice. They&rsquo;re cloud, true, but at $0.11-$0.13 per run it&rsquo;s basically the price of electricity.</p>
<p>If you want to test the &ldquo;Claude at home&rdquo; gamble via distillation: the <strong>Qwen 3.5 27B Claude-distilled</strong> is sitting there to play with, but I already warned you it hallucinates exactly the same fake APIs as the base Qwen. Distillation transferred Claude&rsquo;s style, not its factual knowledge about libraries. It&rsquo;s worth it as an experiment, not as production.</p>
<p>Yes, maybe with days of tweaking llama.cpp, calibrating flags, adjusting prompts, testing different builds, you could make Gemma 4 or other models work better. For most people, that isn&rsquo;t realistic. The distance between frontier models (Claude, GPT) and self-hosted open source models is real. It isn&rsquo;t marketing. The gap is shrinking, but it still exists, and the nature of it has changed: today what&rsquo;s missing in open source is factual knowledge about specific libraries, not raw reasoning capacity. Hardware stopped being the bottleneck, at least for anyone with a recent GPU.</p>
<p>In the end, what matters is whether the code runs. A model can generate 3,405 files, write 37 tests, produce a 181-line README, and the app still won&rsquo;t work because the API it uses doesn&rsquo;t exist. Completeness metrics and test counts are necessary but not sufficient. The only reliable signal is whether the model uses real APIs correctly.</p>
<p>The full benchmark, with code, configuration, prompts and per-model results, is on <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
]]></content:encoded><category>llm-benchmarks</category><category>llms</category><category>local-models</category></item><item><title>Review: Minisforum MS-S1 Max | AMD AI Max+ 395 with 96GB of VRAM</title><link>https://www.akitaonrails.com/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</guid><pubDate>Tue, 31 Mar 2026 15:00:00 -0300</pubDate><description>&lt;p&gt;If you&amp;rsquo;ve been following my &lt;a href="https://www.akitaonrails.com/en/2024/04/03/my-personal-netflix-with-docker-compose/"&gt;home server posts&lt;/a&gt;, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.&lt;/p&gt;
&lt;p&gt;I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you&rsquo;ve been following my <a href="/en/2024/04/03/my-personal-netflix-with-docker-compose/">home server posts</a>, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.</p>
<p>I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-desk.jpg" alt="Minisforum MS-S1 Max on the desk"  loading="lazy" /></p>
<h2>Why ditch the Intel NUC<span class="hx:absolute hx:-mt-20" id="why-ditch-the-intel-nuc"></span>
    <a href="#why-ditch-the-intel-nuc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The NUC was a fine Docker server for two years. But the limitation was clear: without a GPU with enough VRAM, I couldn&rsquo;t run LLMs locally in any usable way. <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, my automatic manga translation system, needed CPU-based OCR (slow) and would connect remotely to the Ollama running on my desktop (AMD 7950X3D + RTX 5090) for translation. It worked, but it meant my desktop had to be on for the server to do its job.</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/FrankYomik/master/docs/sample_translate.png" alt="Frank Yomik - automatic manga translation"  loading="lazy" /></p>
<p>With the Minisforum, Frank Yomik now runs entirely on the server. The worker uses ROCm for OCR on the iGPU, and Ollama runs locally with 96GB of VRAM. Zero dependency on the desktop.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-nuc-compare.jpg" alt="Comparison: Intel NUC (left) vs Minisforum MS-S1 Max (right)"  loading="lazy" /></p>
<p>You can get a sense of the size from the photo. The NUC is the tiny cube on the left. The Minisforum is bigger but it&rsquo;s still a mini-PC. It fits on the rack shelf under my Synology NAS without any trouble.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-shelf.jpg" alt="Minisforum installed on the shelf, next to the NAS"  loading="lazy" /></p>
<h2>The specs<span class="hx:absolute hx:-mt-20" id="the-specs"></span>
    <a href="#the-specs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-fastfetch.png" alt="fastfetch on the Minisforum"  loading="lazy" /></p>
<p>The chip is the AMD Ryzen AI Max+ 395: 16 cores / 32 threads Zen 5, with an integrated Radeon 8060S iGPU and 128GB of unified LPDDR5X. In the BIOS, I set the UMA Frame Buffer Size to 96GB, which leaves ~30GB of RAM for the operating system and containers. Plus the kernel parameters for TTM (without them, ROCm only sees 15.5GB even with the BIOS allocation in place).</p>
<p>The operating system is openSUSE MicroOS (more on that in the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a>). The whole machine pulls under 100W, which is absurd if you&rsquo;re used to dedicated GPUs that draw 450W+ on their own.</p>
<h2>Minisforum vs my desktop: benchmarks<span class="hx:absolute hx:-mt-20" id="minisforum-vs-my-desktop-benchmarks"></span>
    <a href="#minisforum-vs-my-desktop-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I ran a <a href="https://github.com/akitaonrails/homelab-docs/tree/master/benchmarks"target="_blank" rel="noopener">set of benchmarks</a> comparing the Minisforum against my desktop (AMD 7950X3D, 96GB DDR5, RTX 5090 32GB GDDR7). The results are clear.</p>
<h3>CPU<span class="hx:absolute hx:-mt-20" id="cpu"></span>
    <a href="#cpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Test</th>
          <th>7950X3D</th>
          <th>AI Max+ 395</th>
          <th>Winner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prime sieve (single-core)</td>
          <td>0.021s</td>
          <td>0.018s</td>
          <td>Strix Halo +14%</td>
      </tr>
      <tr>
          <td>Float pi (single-core)</td>
          <td>1.335s</td>
          <td>1.706s</td>
          <td>7950X3D +28%</td>
      </tr>
      <tr>
          <td>Multi-core sieve (32 threads)</td>
          <td>0.181s</td>
          <td>0.118s</td>
          <td>Strix Halo +53%</td>
      </tr>
      <tr>
          <td>SHA-256 throughput</td>
          <td>2.714 MB/s</td>
          <td>2.488 MB/s</td>
          <td>7950X3D +9%</td>
      </tr>
      <tr>
          <td>AES-256-CBC throughput</td>
          <td>1.613 MB/s</td>
          <td>1.410 MB/s</td>
          <td>7950X3D +14%</td>
      </tr>
  </tbody>
</table>
<p>Mixed results. The AI Max+ 395 is better at pure parallelism (multi-core sieve), probably thanks to lower latency in the unified memory architecture. The 7950X3D wins at float and crypto because of its higher clocks and the 3D V-Cache.</p>
<h3>LLM inference (models that fit on both)<span class="hx:absolute hx:-mt-20" id="llm-inference-models-that-fit-on-both"></span>
    <a href="#llm-inference-models-that-fit-on-both" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where it gets interesting. For models that fit in the 32GB of the RTX 5090, the comparison is purely about memory bandwidth:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>9.1 GB</td>
          <td>155.1</td>
          <td>23.2</td>
          <td>6.7x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>9.3 GB</td>
          <td>138.9</td>
          <td>22.6</td>
          <td>6.1x</td>
      </tr>
      <tr>
          <td>phi4-reasoning</td>
          <td>11.1 GB</td>
          <td>130.2</td>
          <td>19.1</td>
          <td>6.8x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>20.2 GB</td>
          <td>66.9</td>
          <td>10.0</td>
          <td>6.7x</td>
      </tr>
  </tbody>
</table>
<p>The RTX 5090 is ~7x faster. The explanation is simple: GDDR7 has ~1,792 GB/s of bandwidth. LPDDR5X has ~256 GB/s. The ratio (7x) lines up almost exactly with the measured speed difference (6.7x). LLM inference is a problem dominated by memory bandwidth. Whoever reads weights faster, generates tokens faster.</p>
<h3>And what about prompt processing?<span class="hx:absolute hx:-mt-20" id="and-what-about-prompt-processing"></span>
    <a href="#and-what-about-prompt-processing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>~1,933</td>
          <td>~212</td>
          <td>9.1x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>~1,474</td>
          <td>~155</td>
          <td>9.5x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>~767</td>
          <td>~68</td>
          <td>11.3x</td>
      </tr>
  </tbody>
</table>
<p>Prompt processing is even worse: 7-11x slower. That makes sense, because the prompt has to be processed in full before generating the first token, and it&rsquo;s an even more bandwidth-intensive operation.</p>
<h3>Where the Strix Halo wins: large models<span class="hx:absolute hx:-mt-20" id="where-the-strix-halo-wins-large-models"></span>
    <a href="#where-the-strix-halo-wins-large-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now we get to the reason I bought this PC. Models that don&rsquo;t fit in the RTX 5090:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>Strix Halo (tok/s)</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gpt-oss:20b</td>
          <td>13.8 GB MXFP4</td>
          <td>48.9</td>
          <td>MoE, faster than expected</td>
      </tr>
      <tr>
          <td>qwen3.5:35b</td>
          <td>23.9 GB</td>
          <td>43.2</td>
          <td>MoE, only ~4B active params</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>51.7 GB</td>
          <td>29.5</td>
          <td>MoE, 50GB+</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81.4 GB Q4_K_M</td>
          <td>19.2</td>
          <td>122B params, MoE</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>59.9 GB</td>
          <td>17.9</td>
          <td>Full precision bf16</td>
      </tr>
      <tr>
          <td>qwen2.5:72b</td>
          <td>47.4 GB Q4_K_M</td>
          <td>4.5</td>
          <td>Dense 72B, bandwidth-limited</td>
      </tr>
  </tbody>
</table>
<p>qwen3.5:122b with 81GB of weights running at 19 tok/s. On a mini-PC. That&rsquo;s simply not possible on an RTX 5090. On the NVIDIA card, that model would have to offload layers to system RAM, dropping to 2-3 tok/s. In practice, unusable.</p>
<p>The difference between MoE and dense models is brutal. qwen3.5:35b runs at 43 tok/s because, despite having 35B total parameters, only ~4B are active per token. A dense 72B model like qwen2.5:72b has to read 40GB+ of weights per token, and at 256 GB/s of bandwidth, the theoretical maximum is ~6.7 tok/s. The 4.5 measured represent ~67% efficiency, which is what you&rsquo;d expect for an iGPU (overhead from shared bus and drivers).</p>
<h3>Summary: when to use which machine<span class="hx:absolute hx:-mt-20" id="summary-when-to-use-which-machine"></span>
    <a href="#summary-when-to-use-which-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Use case</th>
          <th>Best machine</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Interactive chat/coding (models &lt;32GB)</td>
          <td>RTX 5090 (6-7x faster)</td>
      </tr>
      <tr>
          <td>Large models (50GB+)</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Dense 70B+ models</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Full-precision bf16</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Batch processing with long context</td>
          <td>Strix Halo (more VRAM for KV cache)</td>
      </tr>
      <tr>
          <td>API serving with low latency</td>
          <td>RTX 5090 (sub-150ms TTFT)</td>
      </tr>
  </tbody>
</table>
<h3>A ROCm bug that&rsquo;s still around<span class="hx:absolute hx:-mt-20" id="a-rocm-bug-thats-still-around"></span>
    <a href="#a-rocm-bug-thats-still-around" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything works. Models like deepseek-r1:70b, llama3.3:70b and llama4:scout crash with a ggml bug (<code>GGML_ASSERT(ggml_nbytes(src0) &lt;= INT_MAX) failed</code>). The embedding tensor of these models exceeds 2GB and the ROCm copy kernel uses a 32-bit integer for the size. On CUDA (NVIDIA) it&rsquo;s already been fixed, but on ROCm it hasn&rsquo;t. Waiting for the fix in Ollama 0.20.0+.</p>
<h2>LPDDR5X vs GDDR7: why this difference exists<span class="hx:absolute hx:-mt-20" id="lpddr5x-vs-gddr7-why-this-difference-exists"></span>
    <a href="#lpddr5x-vs-gddr7-why-this-difference-exists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The next question is: why is LPDDR5X so much slower?</p>
<p>GDDR7 is dedicated GPU memory. It&rsquo;s soldered onto the graphics card, connected by a wide bus (384 or 512 bits on the RTX 5090) at high clocks. Its only job is to feed data to the GPU. LPDDR5X is unified memory that serves everything: the operating system, applications, and the GPU all at once. The bus is narrower and shared.</p>
<p>In practice: GDDR7 delivers ~1,792 GB/s dedicated to the GPU. LPDDR5X delivers ~256 GB/s that still need to be split between CPU and GPU. LLM inference is basically &ldquo;read all the model weights from memory, multiply by the current token, generate the next token, repeat.&rdquo; Whoever reads faster, generates faster. There&rsquo;s no shortcut.</p>
<p>The Strix Halo&rsquo;s advantage isn&rsquo;t speed. It&rsquo;s capacity. 96GB of VRAM in a 100W chip that costs a fraction of a professional GPU. The RTX 5090 is 7x faster, but it&rsquo;s stuck at 32GB. Models that don&rsquo;t fit, don&rsquo;t run.</p>
<h2>The alternatives: who else does this?<span class="hx:absolute hx:-mt-20" id="the-alternatives-who-else-does-this"></span>
    <a href="#the-alternatives-who-else-does-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If 96GB isn&rsquo;t enough or you want more speed, the options are limited.</p>
<p>The Framework Desktop uses the same AI Max+ 395 chip with up to 128GB of RAM. Same platform, same performance, but with the differential of being modular and repairable (it&rsquo;s Framework, after all). In practice it&rsquo;s equivalent to the Minisforum on specs and price.</p>
<p>Above that, the alternative is a Mac Studio with M3 Ultra. The M3 Ultra chip supports up to 512GB of unified memory, with ~819 GB/s of bandwidth (more than 3x the Strix Halo). Apple manufactures the memory chips on the package, so latency and bandwidth are superior. You could potentially allocate ~400GB as VRAM and run models that don&rsquo;t fit anywhere outside of professional GPU servers.</p>
<p>Apple&rsquo;s internal NVMe is also another level: ~7.4 GB/s of sequential read on the M3 Ultra, compared with ~14 GB/s on the Crucial T700 (PCIe 5.0). The T700 is faster on raw throughput, but Apple&rsquo;s NVMe latency tends to be lower on random I/O thanks to the SoC integration.</p>
<table>
  <thead>
      <tr>
          <th>Spec</th>
          <th>Minisforum MS-S1 Max</th>
          <th>Mac Studio M3 Ultra (max)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Max RAM</td>
          <td>128 GB LPDDR5X</td>
          <td>512 GB unified</td>
      </tr>
      <tr>
          <td>Allocatable VRAM</td>
          <td>~96 GB</td>
          <td>~400 GB</td>
      </tr>
      <tr>
          <td>Memory bandwidth</td>
          <td>~256 GB/s</td>
          <td>~819 GB/s</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>Zen 5, 16C/32T</td>
          <td>Apple M3 Ultra, 32C</td>
      </tr>
      <tr>
          <td>GPU compute</td>
          <td>ROCm (gfx1151, experimental)</td>
          <td>Metal (mlx, mature)</td>
      </tr>
      <tr>
          <td>Power draw</td>
          <td>~100W</td>
          <td>~135W</td>
      </tr>
      <tr>
          <td>NVMe</td>
          <td>PCIe 5.0 (standard slot)</td>
          <td>Custom Apple (~7.4 GB/s)</td>
      </tr>
      <tr>
          <td>Price (US)</td>
          <td>~$1,500-2,000</td>
          <td>~$9,999 (512GB config)</td>
      </tr>
      <tr>
          <td>Estimated price (Brazil)</td>
          <td>~R$ 12,000-15,000</td>
          <td>~R$ 110,000+ (imported)</td>
      </tr>
  </tbody>
</table>
<p>The Brazilian price is the elephant in the room. The Mac Studio&rsquo;s max config costs $9,999 in the US. With import taxes (~60% + state ICMS), it goes past R$ 110,000. The Minisforum with 128GB lands at R$ 12,000-15,000. A nearly 8x price gap buys you a lot.</p>
<p>If you need more than 96GB of VRAM for truly enormous models (DeepSeek-V3 with 671B parameters fits in ~400GB Q4, for example), the Mac Studio with 512GB is the only consumer option. The alternative would be professional NVIDIA A6000 GPUs (48GB VRAM, ~$6,000 each, and you&rsquo;d need several in NVLink). For everything that fits in 96GB, the Minisforum gets the job done at a fraction of the cost.</p>
<h2>And projects that promise to run big LLMs on small GPUs?<span class="hx:absolute hx:-mt-20" id="and-projects-that-promise-to-run-big-llms-on-small-gpus"></span>
    <a href="#and-projects-that-promise-to-run-big-llms-on-small-gpus" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a concept called &ldquo;layer offloading&rdquo; that projects like llama.cpp already support. The idea: if the model doesn&rsquo;t fit fully in VRAM, keep some layers on the GPU and the rest in system RAM. The GPU processes the layers it has, hands off to the CPU to process the rest, and back.</p>
<p>In practice, it doesn&rsquo;t work well. The bottleneck is PCIe: transfer speed between system RAM and GPU VRAM is ~32 GB/s (PCIe 5.0 x16). Each generated token needs to transfer data back and forth. The result is that you drop from 150 tok/s (everything in VRAM) to 2-8 tok/s (partial offload). It&rsquo;s too slow for interactive use.</p>
<p>VRAM is the fundamental limitation because LLM inference is memory-bandwidth-bound, not compute-bound. The GPU has compute to spare. What&rsquo;s missing is the ability to read the model weights fast enough. When part of the weights live in system RAM through PCIe, the entire pipeline waits on the transfer.</p>
<p>That&rsquo;s why unified memory (like in the Strix Halo or Apple Silicon) makes a difference. There&rsquo;s no PCIe in the middle. CPU and GPU access the same physical memory. The Strix Halo&rsquo;s 256 GB/s is slow compared to GDDR7, but it&rsquo;s 8x faster than offloading through PCIe.</p>
<h2>Advances in LLM optimization (up to 2026)<span class="hx:absolute hx:-mt-20" id="advances-in-llm-optimization-up-to-2026"></span>
    <a href="#advances-in-llm-optimization-up-to-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To understand why some models run so much better than others on the Strix Halo, you have to understand what&rsquo;s changed in the ecosystem over the last two years.</p>
<h3>Mixture of Experts (MoE)<span class="hx:absolute hx:-mt-20" id="mixture-of-experts-moe"></span>
    <a href="#mixture-of-experts-moe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you run local models, MoE is the advance that matters most. An MoE model has high total parameters (e.g. 122B in qwen3.5:122b), but only activates a fraction of them per token (e.g. ~4B). The inactive weights stay in VRAM but aren&rsquo;t read on every token, which drastically reduces the bandwidth needed.</p>
<p>In the Strix Halo benchmarks, MoE models run 3-10x faster than dense models of the same size. qwen3.5:35b (MoE, ~4B active) runs at 43 tok/s while qwen2.5:72b (dense, 72B active) runs at 4.5 tok/s.</p>
<h3>DeepSeek and training optimization<span class="hx:absolute hx:-mt-20" id="deepseek-and-training-optimization"></span>
    <a href="#deepseek-and-training-optimization" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>DeepSeek V3 (December 2024) showed that it was possible to train 671B parameter models at a cost an order of magnitude lower than predicted. They combined MoE with FP8 quantization during training (not just inference), multi-stage training with curriculum learning, and several inter-GPU communication optimizations. The impact: everyone copied. Qwen, GLM, MiniMax, all of them adopted variations of the technique.</p>
<h3>Quantization: from FP16 to Q4 without losing much<span class="hx:absolute hx:-mt-20" id="quantization-from-fp16-to-q4-without-losing-much"></span>
    <a href="#quantization-from-fp16-to-q4-without-losing-much" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quantization compresses model weights from 16 bits (FP16) to smaller formats: 8 bits (Q8), 4 bits (Q4), or even 2 bits. A 70B model that would take ~140GB in FP16 fits in ~40GB at Q4_K_M. Quality loss exists, but in modern formats (GGUF Q4_K_M, AWQ, EXL2) it&rsquo;s small enough for practical use.</p>
<p>GGUF (the llama.cpp format) became the standard for local inference. AWQ and GPTQ are alternatives with more sophisticated calibration, but the ecosystem converged on GGUF because it works on CPU, CUDA and ROCm without recompilation.</p>
<h3>Distillation: smaller models that know more<span class="hx:absolute hx:-mt-20" id="distillation-smaller-models-that-know-more"></span>
    <a href="#distillation-smaller-models-that-know-more" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Distillation is training a small model using the responses of a large model as the teacher. Microsoft&rsquo;s Phi-4 (14B) was trained with distillation from GPT-4 and competes with 70B models on several benchmarks. Qwen3 did the same: qwen3:14b is surprisingly capable for its size.</p>
<h3>Flash Attention and optimized KV Cache<span class="hx:absolute hx:-mt-20" id="flash-attention-and-optimized-kv-cache"></span>
    <a href="#flash-attention-and-optimized-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Flash Attention (Tri Dao, 2022) changed how attention is computed: instead of materializing the full attention matrix in memory, it processes in blocks keeping the data in the GPU&rsquo;s on-chip SRAM, reducing memory consumption from O(n²) to O(n). Without that, contexts of 128K+ tokens would be impractical. It already went through versions 2 and 3, with optimizations for FP8 and async operations on H100. PagedAttention (vLLM, UC Berkeley) did the same for the KV cache during serving: applies virtual memory concepts to the cache, eliminating fragmentation and improving throughput by 2-4x.</p>
<p>In Ollama, I set <code>OLLAMA_FLASH_ATTENTION=1</code> and <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> on the server. The first activates flash attention, the second uses 8-bit KV cache instead of fp16, cutting the bandwidth needed per token in half. These are zero-hardware-cost optimizations that improve throughput measurably.</p>
<h3>What Qwen, Kimi, MiniMax and GLM are doing<span class="hx:absolute hx:-mt-20" id="what-qwen-kimi-minimax-and-glm-are-doing"></span>
    <a href="#what-qwen-kimi-minimax-and-glm-are-doing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Qwen (Alibaba) has consistently been the best price/performance in open source models. Qwen3:14b is dense and strong; Qwen3.5:122b is MoE and runs surprisingly well in 96GB. GLM-4.7 (Zhipu AI) is notable for offering bf16 full precision versions that fit in 96GB. MiniMax experimented with long contexts (up to 4M tokens). Kimi (Moonshot AI) focused on large context windows with linear architectures.</p>
<h3>What runs well in 96GB of VRAM<span class="hx:absolute hx:-mt-20" id="what-runs-well-in-96gb-of-vram"></span>
    <a href="#what-runs-well-in-96gb-of-vram" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With 96GB on the Strix Halo, the models that work well for daily use:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>tok/s</th>
          <th>Use</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen3.5:35b</td>
          <td>24 GB</td>
          <td>43.2</td>
          <td>General purpose, excellent</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>52 GB</td>
          <td>29.5</td>
          <td>Code, MoE</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81 GB</td>
          <td>19.2</td>
          <td>Heavy but usable</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>60 GB</td>
          <td>17.9</td>
          <td>Full precision</td>
      </tr>
      <tr>
          <td>qwen2.5-coder:32b</td>
          <td>20 GB</td>
          <td>10.2</td>
          <td>Code, dense</td>
      </tr>
      <tr>
          <td>deepseek-r1:32b</td>
          <td>20 GB</td>
          <td>7.4</td>
          <td>Reasoning</td>
      </tr>
  </tbody>
</table>
<p>Dense 70B+ models (deepseek-r1:70b, llama3.3:70b) are still blocked by the ROCm bug I mentioned. When it gets fixed, they should run at ~4-6 tok/s, usable for batch but not for interactive chat.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I bought the Minisforum to run models that don&rsquo;t fit in any gaming GPU. For that, it works. It&rsquo;s not fast. 19 tok/s on a 122B model isn&rsquo;t the experience you get with Claude or ChatGPT. But it&rsquo;s local, it&rsquo;s private, and it runs on my shelf consuming less power than an old lightbulb.</p>
<p>For people asking about the Mac Studio: if you have the budget, it&rsquo;s the best machine for running local LLMs. 512GB of unified memory, 819 GB/s of bandwidth, mature Metal/mlx ecosystem. You can run DeepSeek-V3 in full Q4. But in Brazil, with import duties, it crosses R$ 110k. The Minisforum with 128GB at R$ 12-15k is the realistic option.</p>
<p>And for people who think you can work around the VRAM limitation with layer offloading: you can&rsquo;t. PCIe is too slow. The model has to fit fully in VRAM for inference to be usable. It&rsquo;s the reason gaming GPUs with 32GB of ultra-fast GDDR7 are still capped on model size, and why the unified memory of the Strix Halo and Apple Silicon changed the equation.</p>
<p>In the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a> I tell the story of how I migrated the entire home server to the Minisforum using Claude Code, the problems I ran into, and how openSUSE MicroOS behaves as a Docker server operating system.</p>
]]></content:encoded><category>hardware</category><category>local-models</category><category>performance</category><category>reviews</category></item><item><title>AI Agents: Is GLM 4.7 Flash really that good?</title><link>https://www.akitaonrails.com/en/2026/01/22/ai-agents-is-glm-4-7-flash-really-that-good/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/01/22/ai-agents-is-glm-4-7-flash-really-that-good/</guid><pubDate>Thu, 22 Jan 2026 13:41:05 -0300</pubDate><description>&lt;p&gt;I recently wrote &lt;a href="https://akitaonrails.com/en/2026/01/11/ai-agents-comparing-top-llms-on-the-zig-challenge/"target="_blank" rel="noopener"&gt;a post comparing several LLMs&lt;/a&gt;. The good news is that commercial LLMs are genuinely impressive. The bad news is that no open source LLM was able to complete my coding challenge (read the previous post to understand the challenge).&lt;/p&gt;
&lt;p&gt;Also recently, ZAI released version 4.7 of their famous &lt;a href="https://huggingface.co/zai-org/GLM-4.7"target="_blank" rel="noopener"&gt;&amp;ldquo;GLM&amp;rdquo;&lt;/a&gt; model, a 30 billion parameter model encoded in BF16, needing around 60GB of VRAM to fit entirely on a GPU. So it&amp;rsquo;s heavy.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I recently wrote <a href="https://akitaonrails.com/en/2026/01/11/ai-agents-comparing-top-llms-on-the-zig-challenge/"target="_blank" rel="noopener">a post comparing several LLMs</a>. The good news is that commercial LLMs are genuinely impressive. The bad news is that no open source LLM was able to complete my coding challenge (read the previous post to understand the challenge).</p>
<p>Also recently, ZAI released version 4.7 of their famous <a href="https://huggingface.co/zai-org/GLM-4.7"target="_blank" rel="noopener">&ldquo;GLM&rdquo;</a> model, a 30 billion parameter model encoded in BF16, needing around 60GB of VRAM to fit entirely on a GPU. So it&rsquo;s heavy.</p>
<p>Even more recently, they released the <a href="https://huggingface.co/zai-org/GLM-4.7-Flash"target="_blank" rel="noopener"><strong>GLM 4.7 Flash</strong></a> version, which is the same 30 billion parameter model but quantized (roughly: &ldquo;compressed&rdquo; or &ldquo;truncated and re-normalized&rdquo;), which fits on a GPU with at least 32GB, which is the case with my <strong>RTX 5090</strong>.</p>
<p>Yes, the most expensive consumer GPU on the market is the bare minimum to run the best open source models. That&rsquo;s because what matters isn&rsquo;t processing power but the amount of VRAM. That&rsquo;s why a Mac Studio, with a technically &ldquo;weaker&rdquo; GPU, has an advantage in LLMs because it shares memory with the RAM, with a theoretical max of 512GB. So the largest models fit without any problem at all.</p>
<p>Other options are Mini PCs with AMD Ryzen AI Max+ CPUs that can share up to 128GB of VRAM.</p>
<p>The challenge was to see if I could run GLM 4.7 Flash locally on my machine.</p>
<blockquote>
  <p><strong>TL;DR: GLM 4.7 Flash is possibly the best open source LLM — with a few caveats.</strong></p>

</blockquote>
<h3>VLLM vs LM Studio vs Ollama<span class="hx:absolute hx:-mt-20" id="vllm-vs-lm-studio-vs-ollama"></span>
    <a href="#vllm-vs-lm-studio-vs-ollama" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On their <a href="https://huggingface.co/zai-org/GLM-4.7-Flash"target="_blank" rel="noopener">official page</a> there&rsquo;s documentation on how to run it in VLLM and I&rsquo;ll warn you up front: it doesn&rsquo;t work. It requires features that only exist on the master branch of VLLM and transformers and you&rsquo;ll get a headache with version conflicts and dependency resolution, and even then, in the end, I couldn&rsquo;t get it to run properly.</p>
<p>LM Studio doesn&rsquo;t work either. It has an option to download the model, and I tried tuning every possible parameter. KV Cache quantization, GPU layers, CPU threads, etc etc. But it doesn&rsquo;t matter, the model loads but runs absurdly slow. So slow it&rsquo;s unusable. I think it&rsquo;s because it can only download the BF16 model, so half the layers don&rsquo;t fit on the GPU. Therefore, as of the date of this post, neither LM Studio nor VLLM is usable.</p>
<p>But then I saw a <a href="https://x.com/ollama/status/2013372316021834086"target="_blank" rel="noopener">tweet</a> saying Ollama would finally support GLM 4.7, but again, as of the date of this post, it&rsquo;s still not in the stable release. The only way to use it is by compiling from source the bits that are on their master branch.</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">git clone https://github.com/ollama/ollama.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ollama
</span></span><span class="line"><span class="cl">cmake -B build
</span></span><span class="line"><span class="cl">cmake --build build
</span></span><span class="line"><span class="cl">go build -v .</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>On the last command it will look like it&rsquo;s stuck because nothing shows on the screen, but that&rsquo;s because it&rsquo;s compiling hundreds of C++ files. Just wait and eventually it finishes. And finally you can run it:</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">OLLAMA_FLASH_ATTENTION</span><span class="o">=</span><span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">GGML_VK_VISIBLE_DEVICES</span><span class="o">=</span>-1 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">OLLAMA_KV_CACHE_TYPE</span><span class="o">=</span>q4_0 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">OLLAMA_CONTEXT_LENGTH</span><span class="o">=</span><span class="m">45000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">go run . serve</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>These are the parameters to fit everything in the 32GB of VRAM without offloading to the CPU. 45 thousand tokens is the max, maybe even a bit less to make sure the KV Cache fits.</p>
<p>On top of that you need to quantize to 4-bits with <code>q4_0</code>. Normal is 16-bit floats. To fit we&rsquo;ll have to truncate (lose precision).</p>
<p>In my case, I have an AMD iGPU. To prevent Ollama from trying to use it, there&rsquo;s this option <code>GGML_VK_VISIBLE_DEVICES=-1</code>. That should guarantee only the nvidia gets used.</p>
<p>Finally, we enable <strong>Flash Attention</strong> to try to reduce memory growth as the context window grows.</p>
<h3>Trade-off: speed vs context<span class="hx:absolute hx:-mt-20" id="trade-off-speed-vs-context"></span>
    <a href="#trade-off-speed-vs-context" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM 4.7 Flash is a model with 48 layers. With the configuration I showed above, all 48 layers fit on the GPU. But a 45k context window <strong>can</strong> be too small depending on your use case. You&rsquo;ll have to test for yourself.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123101949_screenshot-2026-01-23_10-09-35.png" alt="log gpu"  loading="lazy" /></p>
<p>When everything fits on the GPU, processing is reasonably fast. A bit slower but still comparable to Qwen3 Coder or GPT-OSS. You can tell everything&rsquo;s on the GPU because looking at BTOP the CPU is doing nothing while <code>nvidia-smi</code> shows the GPU pulling around 500W out of the 600W it can draw.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102147_screenshot-2026-01-22_18-50-42.png" alt="nvidia-smi 500w"  loading="lazy" /></p>
<p>In my tests, depending on what you&rsquo;re doing, those 45k fill up <strong>VERY FAST</strong>. Using tools like OpenCode or Crush, when the context is close to full, they ask the LLM to summarize what&rsquo;s been discussed/researched so far, save it, and restart with the summary. But even so, the summaries themselves grow large.</p>
<p>With agents, the model runs commands on your system (compile, list files, search code snippets, search the web) and all of that consumes context. The more context you have left, the better.</p>
<p>I tried running with <code>OLLAMA_CONTEXT_LENGTH=65576</code>, around 65k tokens. That prevents all layers from staying on the GPU and Ollama ends up swapping, using the CPU. So processing gets much slower. BUT, the context window gets bigger. It&rsquo;s a trade-off.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102557_screenshot-2026-01-22_20-51-05.png" alt="btop"  loading="lazy" /></p>
<p>Right off the bat you can see the CPU starts spinning up. On my 7850x3d it&rsquo;s running at 50% constantly. Look at how slow Ollama response times got:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102645_screenshot-2026-01-23_10-09-50.png" alt="ollama cpu"  loading="lazy" /></p>
<p>Goes from around 4 seconds to 90 seconds. 20x slower. And that&rsquo;s with 39 of the 48 layers still on the GPU. That&rsquo;s why I don&rsquo;t think you can push beyond 65k. The ideal is to stay at 45k, but for my challenge I felt I needed to compensate.</p>
<p>Either Crush/OpenCode gets slow because the window runs out too fast and it has to summarize the work constantly and lose context details — which means extra work re-researching the same thing more than once. Or you preserve more context details for longer, but then every response gets slower. It&rsquo;s a really tough trade-off. And it&rsquo;s all the fault of these crappy video cards having so little VRAM.</p>
<p>If you use a Mac Studio that goes up to 512GB or some Mini-PC with AMD Ryzen AI Max+ that goes up to 128GB, we wouldn&rsquo;t have to deal with this limit. But those are machines that cost over BRL 20k. You can&rsquo;t justify it. For that price it&rsquo;s better to just pay credits on OpenRouter and use larger, faster commercial models like Claude Opus or GPT 5.2, or GLM 4.7 itself on ZAI&rsquo;s cloud.</p>
<h3>Pull Request on Crush<span class="hx:absolute hx:-mt-20" id="pull-request-on-crush"></span>
    <a href="#pull-request-on-crush" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As a side quest, I had a small problem that was bothering me. One of the problems with a small context window is that every once in a while Crush would just stop, even though there was still work to do. The reason is that the context fills up before it has time to summarize, so when it goes to make a tool call, which is a big XML blob, it gets truncated for lack of space.</p>
<p>Truncated XML is invalid, it can&rsquo;t call the tool and the processing crashes, like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123103238_screenshot-2026-01-22_15-12-36.png" alt="log xml"  loading="lazy" /></p>
<p>And even trying to control the memory usage ceiling, eventually that ceiling breaks and processing halts for lack of memory:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123103736_screenshot-2026-01-23_10-36-43.png" alt="log nvm"  loading="lazy" /></p>
<p>Whether from truncated XML or Ollama OOM, processing eventually stops. And since it&rsquo;s slow processing, it&rsquo;s a pain having to wait for this to happen and manually restart the process.</p>
<p>So I decided to see if I could make Crush itself detect that it stopped for these reasons and auto-continue from where it left off. I opened Crush on top of the Crush project itself and asked Claude Opus to check the possibility of a fix.</p>
<p><a href="https://github.com/charmbracelet/crush/pull/1962"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123105702_screenshot-2026-01-23_10-56-40.png" alt="pull request"  loading="lazy" /></a></p>
<p>Yes, I made a 100% vibe coded pull request using Crush itself to fix Crush 😂 Let&rsquo;s see if the Crush folks accept it. I tried my best not to produce <a href="https://www.youtube.com/watch?v=vrTrOCQZoQE&amp;pp=ygUHYWkgc2xvcNIHCQmHCgGHKiGM7w%3D%3D"target="_blank" rel="noopener">AI Slop</a>.</p>
<p>Either way, with this I can leave the agent running for hours without stopping until it gets to where I want.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><blockquote>
  <p>This is NOT the right way!</p>

</blockquote>
<p>Let me be clear that leaving an agent doing things on its own with no one watching is bad practice. In my case it&rsquo;s an isolated exercise. I&rsquo;m using my <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">AI JAIL</a> and I&rsquo;ve run this prompt many times as a benchmark and I know the behavior of all the LLMs well.</p>
<p>As I said in my previous post, my challenge prompt is full of bad practices you shouldn&rsquo;t follow: I ask for many complex things at once, with little context, in a vague and confusing way, precisely to see how the LLM will handle it. Day-to-day you should send small prompts, objective unitary tasks, well specified, with good explanation and context, so the responses are fast and short. One step at a time.</p>
<p>The goal of my challenge is precisely to push the limits of common sense a bit. With the idea that if it passes my challenge, it should be able to handle simpler day-to-day prompts.</p>
<p>And in this challenge only the commercial LLMs got through: Claude Opus, GPT 5.2, Gemini 3. No open source model made it, not Qwen3 Coder, not GPT-OSS, not my first attempt with GLM 4.7 cloud, nor MiniMax v2.</p>
<p>But this new GLM 4.7 Flash gave me hope. It didn&rsquo;t go into an &ldquo;agentic loop&rdquo; like Qwen3 — which always reaches a point where it stops trying to solve and just repeats &ldquo;I&rsquo;m done&rdquo; without being done.</p>
<p>It also didn&rsquo;t crash for no reason. Every time processing stopped, it was out of memory. And even so, sending &ldquo;continue&rdquo; via prompt in Crush, it manages to pick up where it left off and keep going.</p>
<p>Every fix it makes makes sense. I didn&rsquo;t see it going off on tangents outside the scope like I saw Gemini doing many times. When it has doubts, it can request an &ldquo;agentic fetch&rdquo; and go search the web before continuing. Every time it makes fixes it knows to run the build command to check for compilation errors.</p>
<p>I liked that it tries to fix one small error at a time instead of rewriting everything or solving more than one problem at once.</p>
<blockquote>
  <p>In the end, it was the only open source one that got the build working and produced a binary.</p>

</blockquote>
<p>I couldn&rsquo;t have it test the program because it would need to load another LLM on a GPU that was already hosting an LLM, so I stopped the test without resolving runtime bugs. And I also didn&rsquo;t ask it to refactor because it had already taken too long. It had already surpassed the other OSS ones so I stopped there. Here&rsquo;s the <a href="https://github.com/akitaonrails/qwen-cli-zig/pull/6"target="_blank" rel="noopener">pull request</a> with the build fixes it made. Simple, not touching things it shouldn&rsquo;t, doing what was asked.</p>
<p>So of the open source models, this was the best behavior I saw, comparable to what I&rsquo;ve seen Claude Opus or GPT 5.2 doing. Considering those commercial models have trillions of parameters, running on the beefiest NVIDIA H200 servers, I consider this GLM, at just 30 billion parameters, running on a measly local 5090, to be doing impressively well.</p>
<p>The trade-offs are clear: it&rsquo;s orders of magnitude slower than Claude Opus. But a slow model that gets to the same result beats a fast one that goes into an infinite loop thinking it solved everything without having done anything, like Qwen3.</p>
<p>I think for challenges like mine — a giant prompt that needs a lot of context to solve — no open source model is good enough. But if I were starting a small project from scratch, with simple and short tasks, I imagine GLM 4.7 would handle it without any headaches, costing zero and preserving my privacy by running 100% locally and offline.</p>
<p>My recommendation: GLM 4.7 Flash is the best OSS model of early 2026.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>coding-agents</category></item><item><title>Dissecting an Ollama Modelfile - Tuning Qwen3 for Code</title><link>https://www.akitaonrails.com/en/2025/04/29/dissecting-an-ollama-modelfile-tuning-qwen3-for-code/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/29/dissecting-an-ollama-modelfile-tuning-qwen3-for-code/</guid><pubDate>Tue, 29 Apr 2025 02:30:00 -0300</pubDate><description>&lt;p&gt;The hot news of the day is the release of the new Qwen3 model. I already &lt;a href="https://www.akitaonrails.com/en/2025/04/28/testando-o-recem-lancado-llm-open-source-qwen3-com-aider-e-ollama"target="_blank" rel="noopener"&gt;posted about it today&lt;/a&gt; and was quite impressed. I thought I would leave it at that, but then I saw this tweet:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1jgq3ar5c5l7wfjt9wjfui8l598z" alt="ivan qwen tweaks" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;See the &lt;a href="https://x.com/ivanfioravanti/status/1916934241281061156"target="_blank" rel="noopener"&gt;full text at this link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;He gives tips on how to &amp;ldquo;tune&amp;rdquo; this new model, mostly for math-related questions, and I started wondering whether we can tune it for software development. Before more advanced researchers yell at me: this is my first time messing with the parameters of an LLM, so if you want to add context in the comments, you&amp;rsquo;re welcome.&lt;/p&gt;</description><content:encoded><![CDATA[<p>The hot news of the day is the release of the new Qwen3 model. I already <a href="https://www.akitaonrails.com/en/2025/04/28/testando-o-recem-lancado-llm-open-source-qwen3-com-aider-e-ollama"target="_blank" rel="noopener">posted about it today</a> and was quite impressed. I thought I would leave it at that, but then I saw this tweet:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1jgq3ar5c5l7wfjt9wjfui8l598z" alt="ivan qwen tweaks"  loading="lazy" /></p>
<p>See the <a href="https://x.com/ivanfioravanti/status/1916934241281061156"target="_blank" rel="noopener">full text at this link</a></p>
<p>He gives tips on how to &ldquo;tune&rdquo; this new model, mostly for math-related questions, and I started wondering whether we can tune it for software development. Before more advanced researchers yell at me: this is my first time messing with the parameters of an LLM, so if you want to add context in the comments, you&rsquo;re welcome.</p>
<p>Let&rsquo;s go. First, what are these TopP, TopK and everything else Ivan mentions in his tweet? They are <strong>KEY SAMPLING</strong> parameters. It&rsquo;s a group of parameters that control <em>HOW</em> the model selects the <em>NEXT TOKEN</em> and goes on completing the text (giving the &ldquo;answer&rdquo;), based on its vocabulary and the probability distribution it calculates. The most common ones are exactly <code>temperature</code>, <code>top_p</code>, <code>top_k</code> and sometimes <code>min_p</code> or <code>repeat_penalty</code>.</p>
<p>I always say: an LLM is a next-word generator, a text completer based on probabilities. When the LLM processes your input (your context, chat history and the next question), it needs to calculate a probability &ldquo;score&rdquo; for every word or sub-word (token) across its entire vocabulary. Imagine a gigantic model, it needs to recalculate everything all the time, for every next word.</p>
<p>Sampling parameters tell the model &ldquo;how to choose the next token&rdquo; from the list of probabilities. Without these parameters the model would use a &ldquo;greedy&rdquo; decoder (too ambitious). That means it would just pick the token with the highest probability every time. And that leads to a repetitive, unnatural-sounding result. Sampling adds a kind of &ldquo;controlled randomness&rdquo; (remember? I always say there is randomness baked in, the answer is never &ldquo;right&rdquo;), and that makes the response more &ldquo;interesting&rdquo; to a human being (who is easily fooled). Here are some of the parameters:</p>
<h3>Temperature<span class="hx:absolute hx:-mt-20" id="temperature"></span>
    <a href="#temperature" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Controls randomness, which many people call &ldquo;creativity&rdquo;. It essentially makes the distribution hotter (more spread out and flatter) or colder (with more peaks around the same tokens). The higher the temperature (up to 1.0), the more it increases the probability of picking tokens that had lower calculated probabilities. The lower the temperature (down to 0.0), the smaller the chances for tokens with lower probabilities, and the more &ldquo;greedy&rdquo; the result gets (less &ldquo;human&rdquo;).</p>
<p>For code, crank it too high and you start getting &ldquo;hallucinations&rdquo; (which are basically increased randomness) and code that doesn&rsquo;t make sense. If it&rsquo;s zero, it gets very &ldquo;boilerplate&rdquo;, repetitive, using very common patterns and can potentially end up in an infinite loop with no answer (because it can&rsquo;t pull anything different).</p>
<p>The tweet suggests 0.6, which is moderate. But in my tests, a temperature that high already started giving me some weird code. Out of nowhere it would change a variable name. Out of nowhere a Chinese character would appear in the middle of the code (literally). I think 0 is too low, but 0.1 is enough, maybe 0.2.</p>
<p>You can change this in the <code>~/.aider.model.settings.yaml</code> file before loading the model (these parameters can be readjusted at startup):</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">ollama_chat/qwen3-dev:latest</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">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">num_ctx</span><span class="p">:</span><span class="w"> </span><span class="m">40960</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>
<h3>top_p (Nucleus Sampling)<span class="hx:absolute hx:-mt-20" id="top_p-nucleus-sampling"></span>
    <a href="#top_p-nucleus-sampling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here it starts to get complicated. This <code>top_p</code> is used to select tokens from the smallest set of tokens whose cumulative probability exceeds this &ldquo;p&rdquo; limit. The model then samples only from this reduced set.</p>
<p>The model sorts all the tokens by their probabilities in descending order (from highest to lowest). Then it starts adding up their probabilities until the sum is greater than or equal to <code>top_p</code>. All the tokens included in the sum form the so-called &ldquo;nucleus&rdquo;, and from that set it samples the next token. So it&rsquo;s a draw among the tokens with the sum(x) highest probabilities, more or less. &ldquo;Pick one of the winners, but not necessarily THE winner (which would be greedy)&rdquo;.</p>
<h3>top_k<span class="hx:absolute hx:-mt-20" id="top_k"></span>
    <a href="#top_k" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Consider only the &ldquo;k&rdquo; most likely tokens. The model samples only from this reduced set of the top K highest probability tokens. That really means &ldquo;pick one out of the Top 10 best&rdquo;. The higher the K, the greater the diversity, the lower it goes, the more greedy.</p>
<p>And this is relative, top_k and top_p work together. This configuration helps anchor the selection to the &ldquo;most likely&rdquo; tokens while still allowing top_p to filter out the lower probability ones. It prioritizes plausibility (top_p) while restricting the total number of sampled tokens (top_k). It helps keep the result focused on the most likely &ldquo;correct&rdquo; answers (but again, it&rsquo;s a draw/sampling).</p>
<h3>min_p<span class="hx:absolute hx:-mt-20" id="min_p"></span>
    <a href="#min_p" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Filters out tokens whose probability is less than &ldquo;min_p&rdquo; times the probability of the most likely token. Before sampling, the model identifies the token with the highest probability (P_max), then removes any token with a probability (P_token) that is lower than <code>P_max * min_p</code>. Sampling then happens over the remaining list (which will be further filtered by top_p and top_k).</p>
<p>This prevents the model from choosing tokens that are &ldquo;extremely unlikely&rdquo; relative to the most obvious choice.</p>
<h3>repeat_penalty<span class="hx:absolute hx:-mt-20" id="repeat_penalty"></span>
    <a href="#repeat_penalty" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This discourages the model from repeating the same tokens that have already appeared recently in the conversation history or generated text.</p>
<p>Before sampling, the probability score of tokens that have been seen recently is reduced, dividing by this <code>repeat_penalty</code> (with values greater than 1, since dividing by 1 is itself, so it keeps the same probability).</p>
<p>If you leave it at 1.0, it can start repeating the same answers or the same code, even the wrong ones. If it&rsquo;s too high, well above 1.0, the response can sound unnatural, since sometimes it really needs to repeat something to re-explain. A moderate value somewhere around 1.1 up to just under 1.2 tends to be more appropriate so it doesn&rsquo;t punish too much what&rsquo;s already in the history.</p>
<h3>num_predict (max tokens to generate)<span class="hx:absolute hx:-mt-20" id="num_predict-max-tokens-to-generate"></span>
    <a href="#num_predict-max-tokens-to-generate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The absolute maximum number of tokens the model will generate as a response. If it goes over, it truncates. If it&rsquo;s too small, especially for a code response - which can be long - it can truncate too much and leave the answer incomplete. If it&rsquo;s too big it will greatly increase processing and demand way more machine resources and more time, and it can be wasteful.</p>
<p>A value of around 32 thousand tokens leaves plenty of room for tokens. You have to test, and it varies case by case for the type of response you expect and what each model supports.</p>
<h3>Recommended<span class="hx:absolute hx:-mt-20" id="recommended"></span>
    <a href="#recommended" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Knowing all this now you can also read Ivan&rsquo;s tweet and understand it. He recommends Temp 0.6, TopP 0.95, TopK 20, MinP 0.</p>
<p>I fiddled with it a bit more. And I think that for code, Temp has to be in the 0.1 or 0.2 range. I also think a <code>repeat_penalty</code> of 1.1 is worth it to avoid unnecessary repetitions, especially of code. Again, you have to test. These are empirical numbers, not exact ones.</p>
<p>I created a <a href="https://github.com/akitaonrails/qwen3-dev"target="_blank" rel="noopener">project on my GitHub</a> with a complete model file that not only implements these parameters but also pre-configures a system prompt to bias the model towards being a code assistant first. Here&rsquo;s my prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>SYSTEM &#34;&#34;&#34;
You are a highly functional AI assistant specializing in software development tasks.
Respond to queries about coding, algorithms, debugging, system design,
and programming languages/frameworks.
Provide clear, accurate, step-by-step reasoning and functional code examples.
Always format code snippets using markdown triple backticks (```language ... ```).
Aim for comprehensive and correct technical assistance.
Never suggest changing code unrelated to the question topic at hand.
Restrict yourself to the minimum amount of changes that do resolve the problem at hand.
Avoid renaming functions or variables unless absolutely necessary, 
and in doing so make sure if there&#39;s anyone calling the old names, 
for them to be renamed to the new names. Never leave the code in a broken state. 
Pay close attention to correctness, not just answering the quickiest and dirtiest code to solve the problem.
&#34;&#34;&#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>To make this file, first I pulled the original Qwen3 Modelfile with this command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama show qwen3 --modelfile &gt; Modelfile</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>It&rsquo;s important to do this because, from what I understand, modelfiles don&rsquo;t inherit from a previous one. They don&rsquo;t work like a &ldquo;FROM&rdquo; in a Dockerfile, where you continue a new image from a previous base.</p>
<p>After editing it with my adjustments - which is already on my GitHub, just create a new model:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama create qwen3-dev -f qwen3-dev.modelfile</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 it will show up in your local ollama. Check with <code>ollama list</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ ollama list
NAME                                            ID              SIZE      MODIFIED
qwen3-dev:latest                                fe03dfae5484    9.3 GB    About an hour ago
qwen3:32b                                       e1c9f234c6eb    20 GB     2 hours ago
qwen3:14b                                       7d7da67570e2    9.3 GB    8 hours ago
qwen3:latest                                    e4b5fd7f8af0    5.2 GB    8 hours ago
qwen2.5-coder:7b-instruct                       2b0496514337    4.7 GB    25 hours ago
....</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 <code>qwen3-dev:latest</code>? That&rsquo;s our new model with parameter adjustments. In aider just use it directly:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files --model ollama_chat/qwen3-dev:latest</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>Remember to adjust the <code>.aider.model.settings.yml</code> as I showed above.</p>
<p>I haven&rsquo;t done long tests yet, but I decided to play with the 14b model as the base along with these adjustments. The base model already impressed me and at least my adjustments didn&rsquo;t break anything. I kept doing the same refactorings and unit tests on my little educational project and pushed another <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/pull/6"target="_blank" rel="noopener">pull request</a></p>
<p>I think Gemini 2.5 Pro Exp is a little better, but again, not by a very large margin. And in some cases (like refactoring), I think I prefer the code generated by Qwen3. Compare it with the code I got from Gemini in this <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/pull/3"target="_blank" rel="noopener">other pull request</a></p>
<p>With Gemini I had to explain less and it gave fewer errors, but both managed to do what I asked all the way through without breaking the main program. Qwen3 looks good enough for me to want to use it. With the advantage that even being the medium model, with 14 billion parameters (which runs on my 24GB of VRAM with room to spare), it still delivers very good and clean code results. And better: it runs MUCH, but MUCH faster than Qwen2.5 32b.</p>
<p>For those who have GPUs with less VRAM, say, a 3070 or 4070 with 16GB, you can use the even smaller 7b model. I tested qwen2.5-coder:7b and had excellent results, so I imagine the Qwen3 7b should be even better and much faster. Speed that is comparable to Claude or Gemini, which are much larger and run on much beefier machines than an RTX 4070.</p>
<p>Running my qwen3-dev, in my monitoring my CPU stayed idle most of the time and the GPU was above 90% the whole time, consuming almost 400W constantly, holding a temperature of 73 degrees (which is good).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jv8ub1bc6m70u1agm1nyiooav2mk" alt="btop"  loading="lazy" /></p>
<p>I even had a small incident where my PC shut down out of nowhere, because the temporary extension cord I was using couldn&rsquo;t take it and died. Combined with the CPU and my other devices plugged into the same outlet, it was consistently pulling 1000W from the wall for hours. Playing with A.I. uses <strong>A LOT</strong> of energy, with the air conditioner on because just to dissipate all of this, my apartment gets 1 to 2 degrees (my rough guess) above normal. You can actually feel it a bit warmer lol</p>
<p>But that&rsquo;s it, I hope you got a sense of how things actually work under the hood. There&rsquo;s no magic: it&rsquo;s next-word generation, based on a semi-random draw from probabilities.</p>
<p>Now you know.</p>
]]></content:encoded><category>llms</category><category>local-models</category><category>tutorials</category></item><item><title>Your Own Free Universal Co-Pilot Running Local: AIDER-OLLAMA-QWEN</title><link>https://www.akitaonrails.com/en/2025/04/25/your-own-free-universal-copilot-running-local-aider-ollama-qwen/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/25/your-own-free-universal-copilot-running-local-aider-ollama-qwen/</guid><pubDate>Fri, 25 Apr 2025 13:40:00 -0300</pubDate><description>&lt;p&gt;In my &lt;a href="https://www.akitaonrails.com/en/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I show how to build an LLM chat from scratch, capable of loading code files for refactoring. I demonstrate the principles behind tools like Co-Pilot, Cursor or Windsurf. The summary is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A GOOD RULES PROMPT&lt;/li&gt;
&lt;li&gt;SCRIPTS that run locally and add more context to the chat session.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s basically &amp;ldquo;all&amp;rdquo; there is to it (of course, even though the principle is simple, it still takes a lot of work to actually implement). And I don&amp;rsquo;t need to build everything from scratch. There is already an open source alternative that does exactly all of this, the tool that became the most popular over the past months for software development, &lt;a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener"&gt;&lt;strong&gt;AIDER&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my <a href="https://www.akitaonrails.com/en/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener">previous post</a> I show how to build an LLM chat from scratch, capable of loading code files for refactoring. I demonstrate the principles behind tools like Co-Pilot, Cursor or Windsurf. The summary is simple:</p>
<ul>
<li>A GOOD RULES PROMPT</li>
<li>SCRIPTS that run locally and add more context to the chat session.</li>
</ul>
<p>That&rsquo;s basically &ldquo;all&rdquo; there is to it (of course, even though the principle is simple, it still takes a lot of work to actually implement). And I don&rsquo;t need to build everything from scratch. There is already an open source alternative that does exactly all of this, the tool that became the most popular over the past months for software development, <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener"><strong>AIDER</strong></a></p>
<p>What I like about AIDER:</p>
<ul>
<li>it works with practically any LLM, closed or open, thanks to using <a href="https://www.litellm.ai/"target="_blank" rel="noopener"><strong>LiteLLM</strong></a> underneath, which abstracts and organizes every configuration like context window size, temperature and other parameters known to work best for each LLM.</li>
<li>it&rsquo;s OPEN SOURCE, so besides being free you can also learn more by reading the source code, like I did.</li>
<li>no need for special - and proprietary - plugins to install in each IDE: it has a &ldquo;watch files&rdquo; feature. So I can leave it open in one terminal pane and my favorite editor in the other pane: NeoVIM. You can integrate with plugins, but it&rsquo;s optional. It will work for everything, even Notepad, if you&rsquo;re masochistic.</li>
</ul>
<p>I tried a bit of that &ldquo;vibe coding&rdquo; crap only through the web interface of ChatGPT, Gemini, Claude, and I&rsquo;ll say that all of them are crap. To build a small project that was simple, with no more than 4 short files, it gave me A LOT of trouble. Some of them:</p>
<ul>
<li>it doesn&rsquo;t take long before it starts mixing the content of one file into another.</li>
<li>mistakes it made, I explained it was wrong, it corrected, but right after it repeats the same thing.</li>
<li>it doesn&rsquo;t run anything, so it guesses a lot, like file paths, library versions</li>
<li>it doesn&rsquo;t matter that the context is big and you can upload many files to context, it&rsquo;s a sliding window: it won&rsquo;t pay attention to everything at the same time. The more code files you upload, the more quality degrades</li>
<li>you have to write a lot to explain what you want and, especially, why it got it wrong and how to fix it.</li>
<li>credits are expensive, they run out really fast, because you waste half the tokens just explaining mistakes and reuploading the same files more than once because you realize it already &ldquo;forgot&rdquo;. I hit the Claude limit, and the Gemini limit, super fast (I pay the first paid plan, I&rsquo;m not going to pay the most expensive one). Then you have to wait a few hours to come back (awful).</li>
</ul>
<p>All of this, someone will comment <em>&ldquo;Oh, but for me it worked.&rdquo;</em> I don&rsquo;t give a damn, I didn&rsquo;t ask. I said that FOR ME it went this way, just accept it. What matters is saying that the whole &ldquo;virtual employee&rdquo; or &ldquo;fully replacing a programmer&rdquo; or &ldquo;building a whole project without knowing how to code at all&rdquo; story is <strong>BALONEY</strong>, <strong>FAIRY TALE</strong>, <strong>CON ARTIST TALE</strong>, <strong>AMATEUR ILLUSION</strong>, etc.</p>
<p>All LLMs are <strong>JUST ANOTHER TOOL</strong> that someone who truly understands, like us programmers, will know how to use much better than any &ldquo;amateur entrepreneur&rdquo; ever will. And I&rsquo;m going to prove it.</p>
<h3>Basic Aider<span class="hx:absolute hx:-mt-20" id="basic-aider"></span>
    <a href="#basic-aider" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Installing Aider is easy, you just need to have Python &gt;= 3.12 on your machine, which every modern Linux has. Their documentation is very good, <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener">I recommend reading it</a> to know what they recommend post-install. It has many tips.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>python -m pip install aider-install
aider-install</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>What they don&rsquo;t say in the documentation is that the first thing to do is download <a href="https://github.com/Aider-AI/aider/blob/main/aider/website/assets/sample.aider.conf.yml"target="_blank" rel="noopener">this sample file</a> and place it at <code>$HOME/.aider.conf.yml</code>. In it there is one important thing we have to change:</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="nn">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c">## Enable/disable auto commit of LLM changes (default: True)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auto-commits</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="nn">...</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>I recommend reading this file, it is fully commented and may have options you want to turn off, but this one is annoying, because - by default - this damn thing does &ldquo;AUTO COMMIT&rdquo; on your Git, every time it changes something in your code. It doesn&rsquo;t even give me a chance to review. It runs on &ldquo;TRUST US&rdquo;. No way, turned off.</p>
<p>This is the kind of thing an amateur wouldn&rsquo;t do, and I even understand. Amateurs don&rsquo;t know how to organize git commits anyway, so probably the automatic Aider messages will be the lesser evil. Look how it ends up:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/22ji8rp2bbw5lsxwbmpneh5clhcr" alt="Aider Git commit"  loading="lazy" /></p>
<p>Heads up to juniors: this is the kind of thing that if I were evaluating, would earn you negative points if I saw it done this way. Do it right: re-check the changes and organize into real commits. Aider &ldquo;says&rdquo; it did a refactor, but it doesn&rsquo;t always get it right, yet it still commits, and then it gets pushed wrong. If the plan is to push everything automatically, even with errors, really, I don&rsquo;t need you.</p>
<p>But I&rsquo;m getting ahead of myself, after installing Aider, you need to configure the API KEYS for each service you use, whether OpenAI, Claude or others. Whoever uses ChatGPT on Linux already has a variable like <code>export OPENAI_API_KEY=sk-proj-........</code> set up in <code>.bashrc</code> or <code>.zshrc</code>.</p>
<p>Once that&rsquo;s done, you can pick which model to use:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Change directory into your codebase
cd /to/your/project

# o3-mini
aider --model o3-mini

# o1-mini
aider --model o1-mini

# GPT-4o
aider --model gpt-4o

# List models available from OpenAI
aider --list-models openai/</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 that will already open an interactive chat. You can also, before opening the chat, tell it to load local files like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider README.md init.py utils.py ...</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>Or, from inside the chat, it accepts <a href="https://aider.chat/docs/usage/commands.html"target="_blank" rel="noopener">several commands</a> starting with slash &ldquo;/&rdquo;, similar to an IRC chat. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/add helper.py
/copy # copies the last code suggestion to the clipboard
/git # runs a local git command from the chat
/run # runs a shell command and adds the output to the chat
/web # goes to a web page and pulls the content into the chat</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>Read the documentation. But the most interesting mode is &ldquo;watch-files&rdquo;. Just be inside your project directory and start it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files</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>It uses the Python <a href="https://pypi.org/project/watchfiles/"target="_blank" rel="noopener">watchfiles</a> library to intercept IO calls in that directory and trigger the loading of your files on its own, as soon as you save in your favorite editor (that&rsquo;s why I said no extra plugin is needed if you don&rsquo;t want one). Heads up that this lib doesn&rsquo;t support watching SMB or NFS mounts (yes, I tested it).</p>
<p>In your editor, just create a comment near the code you want it to work on, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>// can you refactor this return so it has more error checking? AI!
export const getCapitalizedLabel = (name: string): string =&gt; {
  return name
    .replace(/_/g, &#34; &#34;)
    .split(&#34; &#34;)
    .map((word: string) =&gt; word.charAt(0).toUpperCase() &#43; word.slice(1))
    .join(&#34; &#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>Put in the comment what you want it to do, and end with the &ldquo;AI!&rdquo; trigger. Then on the other terminal where Aider is loaded, it will notice that the file was saved:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt;

Added src/utils/getCapitalizedLabel.ts to the chat
Processing your request...
...</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 it will do its &ldquo;magic&rdquo; (whose secret I&rsquo;ll tell you in a moment). This is the example of what it did (same results with both o4-mini and Qwen2.5, but it&rsquo;s a pretty trivial example):</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-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">getCapitalizedLabel</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span><span class="o">:</span> <span class="nx">string</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">name</span> <span class="o">!==</span> <span class="s1">&#39;string&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span> <span class="k">new</span> <span class="nx">TypeError</span><span class="p">(</span><span class="s1">&#39;Expected a string&#39;</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></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">name</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/_/g</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">word</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">word</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="nx">word</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">word</span><span class="p">.</span><span class="nx">charAt</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">toUpperCase</span><span class="p">()</span> <span class="o">+</span> <span class="nx">word</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</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="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><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>A graphical editor like Visual Studio Code, as soon as Aider changes the file, reloads it in the editor and you already see the changes. On NeoVim, autoread usually reloads automatically too, but sometimes I need to issue the &ldquo;:e&rdquo; command to force a reload, but nothing major. And just with that we already have an efficient work workflow.</p>
<p>My favorite editor works exactly the same way, without a plugin slowing it down or conflicting with anything. If I need to do pair programming, I call Aider in another terminal and done, just write ideas as comments, and it sends suggestions. Since I turned off the damn auto-commit, if I don&rsquo;t like it, I just UNDO or <code>git checkout</code> the file and it&rsquo;s back like it was.</p>
<p>More than that. If I notice that the <code>o4-mini</code> model isn&rsquo;t giving good results, I can switch to <code>--model gemini</code> - which tends to be better for code than o4. Or <code>--model claude-3-opus-20240229</code>. Speaking of Claude, as I said before <a href="https://aider.chat/docs/llms/anthropic.html"target="_blank" rel="noopener">read the fucking documentation</a></p>
<p>The documentation says that Aider supports &ldquo;thinking tokens&rdquo; from Sonnet 3.7. There is a config file where we can do &ldquo;fine-tuning&rdquo; for each model, the <code>$HOME/.aider.model.settings.yml</code> where we can add:</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">anthropic/claude-3-7-sonnet-20250219</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">weak_model_name</span><span class="p">:</span><span class="w"> </span><span class="l">anthropic/claude-3-5-haiku-20241022</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">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">examples_as_sys_msg</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">use_temperature</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">extra_headers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">anthropic-beta</span><span class="p">:</span><span class="w"> </span><span class="l">prompt-caching-2024-07-31,pdfs-2024-09-25,output-128k-2025-02-19</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">64000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">thinking</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">enabled</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">budget_tokens</span><span class="p">:</span><span class="w"> </span><span class="m">32000</span><span class="w"> </span><span class="c"># Adjust this number</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cache_control</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">editor_model_name</span><span class="p">:</span><span class="w"> </span><span class="l">anthropic/claude-3-7-sonnet-20250219</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">editor_edit_format</span><span class="p">:</span><span class="w"> </span><span class="l">editor-diff</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>
<h3>Reasoning<span class="hx:absolute hx:-mt-20" id="reasoning"></span>
    <a href="#reasoning" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Thinking or Reasoning as it is called in the marketing of most commercial LLMs is the &ldquo;chain-of-thought&rdquo; pattern, which is a prompting technique where you ask the LLM not to give an answer, but to describe &ldquo;step by step&rdquo; how it would reach the answer, and then repeat the question telling it to follow those steps and then give an answer.</p>
<p>In practice there are several ways to implement something similar. And one of them is to be Multi-Model. Especially with open source models, like DeepSeek-Coder or Qwen2.5-Coder or Llama3. Some models are better at reasoning - at verbally explaining what has to be done, but they are bad at actually writing the code. And some models are not good at explaining things in detail, but were trained to write good code.</p>
<p>We can use this in our favor and Aider has a feature I think is really badass - for cases where you really need it, because it&rsquo;s heavy - which is running in <strong>architect mode</strong>, generating the reasoning and then switching to <strong>code mode</strong> on another model, and using that reasoning to assemble better code.</p>
<p>There is a <a href="https://aider.chat/2024/09/26/architect.html"target="_blank" rel="noopener">whole article</a> in the Aider documentation explaining this, but the motivation came from the OpenAI o1 model, which is precisely strong at reasoning an explanation of what to do, but bad at actually writing the code. So it&rsquo;s worth listening to o1 and letting it analyze the problem and describe it in text form, and then switching to Gemini Pro or Exp and telling it to write the code.</p>
<p>In practice, just start Aider first in &ldquo;Architect&rdquo; mode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pip install -U aider-chat

# Change directory into a git repo
cd /to/your/git/repo

# Work with Claude 3.5 Sonnet as the Architect and Editor
export ANTHROPIC_API_KEY=your-key-goes-here
aider --sonnet --architect</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 works with other models:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Work with OpenAI models, using gpt-4o as the Editor
export OPENAI_API_KEY=your-key-goes-here
aider --4o --architect
aider --o1-mini --architect
aider --o1-preview --architect</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>As an example, setting <code>OPENAI_API_KEY</code> and <code>GEMINI_API_KEY</code> I can launch o4 as architect and Gemini as the coder:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ aider --watch-files --architect --editor-model gemini
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Using gpt-4o model with API key from environment.
Aider v0.82.3.dev55&#43;g25a30393
Main model: gpt-4o with architect edit format
Editor model: gemini/gemini-2.5-pro-preview-03-25 with editor-diff edit format
Weak model: gpt-4o-mini
Git repo: .git with 184 files
Repo-map: using 4096 tokens, auto refresh
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
architect&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>See in the startup status that it accepted both. Now just code the same way as before, but using both. Obviously, this way you&rsquo;ll burn a lot more credits! Watch out for that! There are <strong>chances</strong> of better results but you <strong>certainly</strong> will spend more credits, so use this more for more complex code, where a single model is struggling to solve it.</p>
<p>Note there are configurations and token limits, and also note that at the end of every response, Aider gives an estimate of how much it&rsquo;s costing you. Here first is the bill from the OpenAI o4 architect:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
This should make the function more concise and efficient.


Tokens: 4.4k sent, 246 received. Cost: $0.01 message, $0.01 session.</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 is the SECOND bill from the Coder Gemini 2.0 Flash:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
Tokens: 1.9k sent, 136 received. Cost: $0.0037 message, $0.02 session.
Applied edit to src/utils/getCapitalizedLabel.ts</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>Free Gemini has a &ldquo;rate limit&rdquo; (number of requests allowed per minute, super low, only 15 RPM, for an API it&rsquo;s ridiculous, Aider hits it on the first try).</p>
<p>Not rarely, you&rsquo;ll hit the plan limit, and that&rsquo;s a pain:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>The API provider has rate limited you. Try again later or check your quotas.
Retrying in 4.0 seconds...</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>Then there&rsquo;s no way around it, you have to go to the <a href="">Google AI Studio</a> site, set up your billing account, add your card and move up to a slightly better paid plan like 2.0 Flash, which supports 2,000 RPM and 4M TPM and the price isn&rsquo;t expensive:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ozbz5tvmpyla0huinqa6da2iwb1m" alt="Gemini 2.0 Flash"  loading="lazy" /></p>
<p>Some say that today Gemini 2.0 has delivered code results similar or superior to Claude Sonnet 3.7. It varies case by case, it&rsquo;s never an absolute measure, but in my small experience, both are very good and I see both being superior even to ChatGPT 4 or o4. But OpenAI, for me, has really been better at &ldquo;verbal&rdquo; responses, so the strategy of separating the &ldquo;architect&rdquo; role for OpenAI and the &ldquo;coder&rdquo; role for Gemini or Claude makes A LOT of sense.</p>
<p>In the end, that initial code I asked to refactor, in this architect/editor combination came out 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-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">getCapitalizedLabel</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span><span class="o">:</span> <span class="nx">string</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">name</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/_/g</span><span class="p">,</span> <span class="s1">&#39; &#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\b\w/g</span><span class="p">,</span> <span class="kr">char</span> <span class="p">=&gt;</span> <span class="kr">char</span><span class="p">.</span><span class="nx">toUpperCase</span><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>Ok, I wouldn&rsquo;t have thought of it this way.</p>
<h3>Not using &ldquo;Token Credits&rdquo;: OLLAMA<span class="hx:absolute hx:-mt-20" id="not-using-token-credits-ollama"></span>
    <a href="#not-using-token-credits-ollama" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now comes the most interesting part for us, more hard-core nerds, with beefier machines (at minimum an RTX 3090, I use an RTX 4090 - what matters is having <strong>24GB of VRAM</strong>). You can use smaller models that fit in less VRAM, but then the code result will be much worse, so you&rsquo;re better off sticking with Gemini or Claude.</p>
<p>If you have the machine for it, with CUDA Toolkit already installed, it&rsquo;s time for the grown-up solution: installing Ollama. On my Manjaro/Arch it goes like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yay -S ollama ollama_cuda</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>Be sure to check your CUDA configuration because by default Ollama will silently run on your CPU and everything will be ABSURDLY SLOW.</p>
<p>Ollama works both as a local HTTP server for APIs, and as an interactive chat client, like my qwen_cli from the previous post. It works with logic more or less similar to Docker. It can be configured to start automatically as a service on your system. See the <a href="https://wiki.archlinux.org/title/Ollama"target="_blank" rel="noopener">ArchWiki documentation</a>.</p>
<p>I prefer to start it manually in a terminal, where I can see the log of what&rsquo;s happening:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>OLLAMA_FLASH_ATTENTION=1 OLLAMA_CONTEXT_LENGTH=8192 ollama serve</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>There are several configurations via environment variables. You can just set them in your <code>/etc/profile</code> or <code>~/.zshrc</code> but since I&rsquo;m testing, I prefer to launch directly on the command line. With this we&rsquo;ll have a web server. To work with Aider, again, just declare where to find it in your <code>/.zshrc</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export OLLAMA_API_BASE=http://127.0.0.1:11434</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, we need to download some model, like my favorite Qwen 2.5 Coder, which I already used in the previous post:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama pull qwen2.5-coder:32b</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>Note I&rsquo;m explicitly asking for the 32B version, but you can try a lighter one like the 7b. Each model supported by Ollama has <a href="https://ollama.com/library/qwen2.5-coder"target="_blank" rel="noopener">a page</a> describing details of this sort:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q76sz7qyqupk8jv3gm1igpk4q0i3" alt="qwen 2.5 coder page"  loading="lazy" /></p>
<p>Depending on which synthetic benchmark you &ldquo;believe in&rdquo; you&rsquo;ll see comparisons like this one:</p>
<p><img src="https://ollama.com/assets/library/qwen2.5-coder/bbf378d8-c80e-4ae3-98ab-90111dfbf3e7" alt="comparison"  loading="lazy" /></p>
<p>In practice, Claude, ChatGPT, Gemini, really are already very good for code. In the open source world the options are Qwen 2.5 Coder, Deepseek-Coder-V2 (which still doesn&rsquo;t work very well in Aider, we have to wait for updates). So, if you want everything for free, the best for now is Qwen 2.5 Coder. And in my experience, it has worked very well. But it is VERY heavy in the 32B version and responses are considerably slower than a paid option like Gemini. So it depends a lot on your case.</p>
<p>Once the model is downloaded, you need to configure the context window (by default it&rsquo;s quite small, only 2K tokens), <a href="https://aider.chat/docs/llms/ollama.html"target="_blank" rel="noopener">the documentation warns</a> about this and you have to edit the file <code>$HOME/.aider.model.settings.yml</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-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">ollama/qwen2.5-coder:32b</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">num_ctx</span><span class="p">:</span><span class="w"> </span><span class="m">65536</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>What the documentation does <strong>NOT</strong> explain and, because of that, I spent a long time digging through the source code and Issues on GitHub, is that to launch open source models, you need to use the <code>ollama_chat/</code> option and not <code>ollama</code> as it tells you. That is, the documentation says:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model ollama/qwen2.5-coder:32b</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>But the CORRECT way is to do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model ollama_chat/qwen2.5-coder:32b</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>Otherwise, the responses will be completely random, out of context. Pay attention to this. But knowing this, we can even do what I mentioned before: mix models! How about launching GPT o4 as architect and letting it command the editor being Qwen 2.5?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files --architect --editor-model ollama_chat/qwen2.5-coder:32</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>Yes, this is possible. Now you can use a commercial model you&rsquo;re more used to, like Gemini or Claude, in both architect and editor roles, and an open source model like Qwen 2.5 as support, and pay less. Or just use Qwen 2.5 all the way and pay <strong>zero credits</strong> (only electricity, of course, it will draw <strong>+200W</strong> from the wall every time, but just for a few seconds each time).</p>
<p>In my (little) experience. Gemini still produces better code than Qwen, but that doesn&rsquo;t mean Qwen is bad, as I said, it will depend A LOT on your prompt instructions (the more detailed the better). And speaking of instructions, let&rsquo;s move on to the final part:</p>
<h3>The Secret of the Magic: Prompts<span class="hx:absolute hx:-mt-20" id="the-secret-of-the-magic-prompts"></span>
    <a href="#the-secret-of-the-magic-prompts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I make fun when someone talks about &ldquo;prompt engineering&rdquo;, but in reality, the best way to get the most out of an LLM is to make the best possible prompt. This doesn&rsquo;t mean the &ldquo;longest prompt&rdquo;. It&rsquo;s quality and not quantity.</p>
<p>That&rsquo;s why both <a href="https://github.com/microsoft/generative-ai-for-beginners"target="_blank" rel="noopener">Microsoft</a> and <a href="https://www.kaggle.com/whitepaper-prompt-engineering"target="_blank" rel="noopener">Google</a> made detailed guides focused on prompting. Theirs are worth reading and studying. What isn&rsquo;t worth it is paying for courses from random people talking about &ldquo;prompt engineering&rdquo; as if they understood anything.</p>
<p>Aider supports a &ldquo;&ndash;verbose&rdquo; option, where it shows exactly what it is sending to the LLMs. Let&rsquo;s test. Right away it already prints this to the console:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ aider --watch-files --verbose
Config files search order, if no --config:
  - /tmp/smells/.aider.conf.yml
  - /home/akitaonrails/.aider.conf.yml (exists)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Too soon to check version: 11.6 hours
Command Line Args:   --watch-files --verbose
Config File (/home/akitaonrails/.aider.conf.yml):
  max-chat-history-tokens:8192
  map-tokens:        4096
  auto-commits:      False
Defaults:
  --set-env:         []
  --api-key:         []
  --model-settings-file:.aider.model.settings.yml
  --model-metadata-file:.aider.model.metadata.json
  ...
  --encoding:        utf-8
  --line-endings:    platform
  --env-file:        /tmp/smells/.env

Option settings:
  - 35turbo: False
  - 4: False
  - 4_turbo: False
  ...
  - upgrade: False
  - user_input_color: #00cc00
  - verbose: True
  - verify_ssl: True
  - vim: False
  - voice_format: wav
  - voice_input_device: None
  - voice_language: en
  - watch_files: True
  - weak_model: None
  - yes_always: None
  ...</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>What tools like Aider, Cursor, Co-Pilot and others do is have optimized parameters for every LLM already hard-coded in the tool, like in this case where it detects to use o4 and already configures with better parameters (that you can still tune by putting them in <code>$HOME/.aider.model.settings.yml</code>).</p>
<p>But when you send the first question that gets interesting, look at this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rb4hxk4cn181fkkf841snl7ygfja" alt="Aiden Verbose 1"  loading="lazy" /></p>
<p><em>&ldquo;Act as an expert software developer. Always use best practices when coding. Respect and use existing conventions, libraries, etc that are already present in the code base. Take requests for changes to the supplied code. Be diligent and not lazy! You will NEVER leave comments describing code without implementing it! You will always COMPLETELY IMPLEMENT the needed code! &hellip;&rdquo;</em></p>
<p>Remember what I said at the beginning of the post: RULES PROMPTS. This SYSTEM at the beginning is a &ldquo;role&rdquo;. You as user are the &ldquo;USER&rdquo; and the model is the &ldquo;ASSISTANT&rdquo;, and in fact this SYSTEM prompt is gigantic, it scans my project and already explains the project structure to the model and even summarizes some files (showing only the interface of methods, for example, to give context without uploading the whole code).</p>
<p>It has snippets like:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
SYSTEM # *SEARCH/REPLACE block* Rules:
SYSTEM
SYSTEM Every *SEARCH/REPLACE block* must use this format:
SYSTEM 1. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
SYSTEM 2. The opening fence and code language, eg: ```python
SYSTEM 3. The start of search block: &lt;&lt;&lt;&lt;&lt;&lt;&lt; SEARCH
SYSTEM 4. A contiguous chunk of lines to search for in the existing source code
SYSTEM 5. The dividing line: =======
SYSTEM 6. The lines to replace into the source code
SYSTEM 7. The end of the replace block: &gt;&gt;&gt;&gt;&gt;&gt;&gt; REPLACE
SYSTEM 8. The closing fence: ```
SYSTEM
SYSTEM Use the *FULL* file path, as shown to you by the user.
...</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>In fact, if you&rsquo;ve used Co-Pilot, you know you can put the conventions of your project in <code>CONVENTIONS.MD</code>. Aider will also load that file into the prompt. So things like &ldquo;use 4 spaces instead of tabs, place the opening { at the end of the line and not starting a new line, etc&rdquo;, all of that goes in that file and both Co-Pilot and Aider add these rules to the PROMPT. There&rsquo;s no other mechanism, it&rsquo;s PROMPT.</p>
<p>Then, as USER, it starts uploading summaries of my files in the prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
USER Here are summaries of some files present in my git repository.
USER Do not propose changes to these files, treat them as *read-only*.
USER If you need to edit any of these files, ask me to *add them to the chat* first.
USER
USER data_scraper/main.py:
USER ⋮
USER │def main(args: Arguments):
USER ⋮
USER
USER data_scraper/src/arguments.py:
USER ⋮
USER │@dataclass(frozen=True)
USER │class Arguments:
USER │    content_path: Path
USER ⋮
USER │    @staticmethod
USER │    def get() -&gt; &#39;Arguments&#39;:
USER ⋮
...</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>When this part ends, Aider asks for confirmation from the Model:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
ASSISTANT Ok, I won&#39;t try and edit those files without asking first.
...</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 that&rsquo;s how a &ldquo;software A.I.&rdquo; works, with a ton of instruction prompts and a client program that keeps parsing the responses and triggers actual commands that run on your system (agents), like git or lint commands. The result of those commands, the text, gets concatenated into the chat to add to the context and then it asks the model/assistant to keep analyzing from that point on.</p>
<p>A model alone does nothing. The one doing the work is the tool that loads the model, in this case Aider, or Co-Pilot, or Cursor. The program comes pre-loaded with several pre-written profiles/personas/roles to add prompts to the model. In the Aider source code, we have this example of <a href="https://github.com/Aider-AI/aider/blob/main/aider/coders/architect_prompts.py"target="_blank" rel="noopener">architect_prompts.py</a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/nlzkq16pcfjwwow5tbibocr1oa1n" alt="architect role"  loading="lazy" /></p>
<p>Look, in the first variable <code>main_system</code> is that first prompt we saw at the beginning of this section. There are several other pre-programmed profiles whose differences are good to understand. Each model works better with certain types of instructions, with different limitations.</p>
<p>In this other code <a href="https://github.com/Aider-AI/aider/blob/main/aider/commands.py"target="_blank" rel="noopener">commands.py</a> is where we have declared how Aider is able to do things like <code>git commit</code> directly in your project or run the linter of your language:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bdt1a5dqj28bvzhqximdi8j2gx2g" alt="commands"  loading="lazy" /></p>
<p>None of this is &ldquo;magic&rdquo;, it is all pre-programmed and it - for safety - should only be able to do limited and well-checked commands. We don&rsquo;t want commands that do too much outside the project directory (in fact, NEVER, outside the project directory). Nor should it be able to run many executables. You never know what security bugs might happen when running commands blindly.</p>
<p>If you&rsquo;re in cyber-security, this is where you should audit. But that&rsquo;s the advantage, Aider being open source is auditable. Proprietary and closed tools, no. They run on &ldquo;TRUST THE DADDY&rdquo;.</p>
<p>Test <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener">Aider</a> today and read that whole documentation site. It is short and has important tips that may help special cases.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/0zeis5fjnkuf2qkmwy7pm6vc62gi" alt="Aider"  loading="lazy" /></p>
<h3>OpenRouter<span class="hx:absolute hx:-mt-20" id="openrouter"></span>
    <a href="#openrouter" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As you must have realized, the ideal is to test the several commercial LLMs because they evolve and each has strong and weak points. But keeping credits paid in each is a pain.</p>
<p>For this there&rsquo;s <a href="https://openrouter.ai/"target="_blank" rel="noopener">OpenRouter</a> where you have a single account, a single <code>OPEN_ROUTER_API_KEY</code> and pay in one place and it distributes the credits to each provider like OpenAI or Claude as you use them. It simplifies the management of your API costs, it&rsquo;s very worth it and, of course, Aider also knows how to use it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model openrouter/openai/gpt-4o</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>Again, read <a href="https://aider.chat/docs/llms/openrouter.html"target="_blank" rel="noopener">the documentation</a>.</p>
<p>Now, an <strong>ANTI-PATTERN</strong>. I asked Aider, using the <code>openrouter/google/gemini-2.5-pro-preview-03-25</code> model, which theoretically is the best of the best at the moment, to generate a unit test file for qwen_cli.py, from the little project I dropped on GitHub in yesterday&rsquo;s post.</p>
<p>It failed miserably. It created a test file, almost 800 lines long, stuffed with mocks for everything, testing things that don&rsquo;t need it, overcomplicating. Fine. I try to run it and it throws errors.</p>
<p>A cool feature of Aider is being able to run from inside the chat with <code>/run python -m unittest ...</code> and it offers to throw the errors right into the chat. I ask Gemini to fix it. Then off it goes modifying the test file and my code file. I send <code>/run</code> again, more errors. And I did this about 4 times. It keeps &ldquo;fixing&rdquo; code that didn&rsquo;t need fixing, and simple unit tests (which was just a small typo, just adjust 1 number), it leaves behind. I describe in the prompt <em>&ldquo;you are messing with what you shouldn&rsquo;t and not fixing the trivial thing, like that little number&rdquo;</em> I send <code>/run</code>, and it insists on ignoring and goes off messing around in other places.</p>
<p>Summary:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q4fiux9vj0d5l6e8p4nmku4rcjma" alt="OpenRouter Credits"  loading="lazy" /></p>
<p>It spent almost 3 bucks and didn&rsquo;t solve the problem. The advantage of OpenRouter is that it all stays centralized in one place. It paid Google for the use of Gemini and deducts from my credits. I had pre-loaded with 100 credits (roughly 100 dollars). And in 30 minutes 3 credits were already gone. Do the math.</p>
<p><strong>Best practice:</strong> the best thing is to open the qwen_cli.py code, for example, in my NeoVIM or any editor and put a comment just on top of a single method:</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="c1"># this method is used to just load a config.json file, create a single unit test and add to test_qwen_cli.py, AI!</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load_config</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>Then it makes only <strong>ONE</strong> unit test at a time, we run it with <code>/run</code> and if it passes and the code looks clean, we move on to the next. Doing one method at a time is much more reliable than saying <em>&ldquo;make tests for EVERYTHING at once.&rdquo;</em> That way it&rsquo;s guaranteed we&rsquo;ll waste credits for nothing.</p>
<p>On top of that, one method at a time, open source models like Qwen 2.5 will do well and you run it locally without spending credits.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>For now, I&rsquo;m going to adopt Aider. Now I understand why it became so popular this year. It is a well-made little project, open, that I can explore, try to improve and learn more from. At the same time it makes using commercial LLMs easier and opens options for me to use open source models on my own machine, putting my RTX 4090 to better use.</p>
<p>A bad programmer is someone who says they had trouble with ChatGPT and then spits out a canned response like &ldquo;have you tried Claude, have you tried Manus&rdquo; or &ldquo;ah, if you had used Cursor it would be better&rdquo;. Stop being that NPC, it&rsquo;s exhausting and demonstrates absolute LAZINESS.</p>
<p>A GOOD programmer is the one who goes through a journey like the one I just described in this post: learns the strong points, the weak points, how to CONTROL, how to SUBVERT, how to create CONTINGENCIES and, in the end, walks out with a BETTER solution than everyone else&rsquo;s.</p>
<p>That&rsquo;s what I became a programmer for, not to be a free billboard repeating Sam Altman&rsquo;s propaganda.</p>
]]></content:encoded><category>coding-agents</category><category>local-models</category><category>llms</category></item><item><title>LLM Hello World: Building Your Own Local AI Chat</title><link>https://www.akitaonrails.com/en/2025/04/25/llm-hello-world-building-your-own-local-ai-chat/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/25/llm-hello-world-building-your-own-local-ai-chat/</guid><pubDate>Fri, 25 Apr 2025 00:20:00 -0300</pubDate><description>&lt;p&gt;I&amp;rsquo;ve been playing A LOT with the various GPT derivatives like ChatGPT itself, Claude Sonnet, Gemini, and maybe later I&amp;rsquo;ll do another post about my experience. But today I wanted to dig a bit deeper and do a &amp;ldquo;Hello World&amp;rdquo; level experiment so that you, programmers, can understand how it works under the hood.&lt;/p&gt;
&lt;p&gt;I pushed the project to GitHub, it&amp;rsquo;s called &lt;a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener"&gt;Tiny Qwen CLI&lt;/a&gt; and it was made strictly for EDUCATIONAL PURPOSES. For a real tool, look up &lt;a href="https://aider.chat/"target="_blank" rel="noopener"&gt;Aider&lt;/a&gt;. Maybe I&amp;rsquo;ll talk about it in another post later.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve been playing A LOT with the various GPT derivatives like ChatGPT itself, Claude Sonnet, Gemini, and maybe later I&rsquo;ll do another post about my experience. But today I wanted to dig a bit deeper and do a &ldquo;Hello World&rdquo; level experiment so that you, programmers, can understand how it works under the hood.</p>
<p>I pushed the project to GitHub, it&rsquo;s called <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener">Tiny Qwen CLI</a> and it was made strictly for EDUCATIONAL PURPOSES. For a real tool, look up <a href="https://aider.chat/"target="_blank" rel="noopener">Aider</a>. Maybe I&rsquo;ll talk about it in another post later.</p>
<p>The concept I want to teach: GPTs are not magic.</p>
<h3>Concepts<span class="hx:absolute hx:-mt-20" id="concepts"></span>
    <a href="#concepts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Watch my <a href="https://www.youtube.com/watch?v=UDrDg6uUOVs&amp;list=PLdsnXVqbHDUeowsAO0sChHDY4D65T5s1U&amp;pp=gAQB"target="_blank" rel="noopener">AI playlist</a> to understand how a GPT works. Once you&rsquo;ve watched it, let&rsquo;s dig in.</p>
<p>Underneath every Gemini, ChatGPT, Claude or any open one like Meta&rsquo;s LLAMA, there is a huge binary file that is the result of hours and hours of training with petabytes of data.</p>
<p>Training consists - VERY ROUGHLY - of tokenizing the words from texts (it&rsquo;s not exactly that, but think of breaking into syllables, the correct term is n-grams). Then transforming them into vectors (as you learned in college, a list of coordinates, usually <strong>FP</strong> or floating point, FP-32, 32-bits to be more precise). The training consists of moving these vectors around inside a hyper-dimensional space (way more than the 3 dimensions we&rsquo;re used to, think thousands or millions).</p>
<p>This positioning of vectors keeps changing as we feed it more texts. It keeps &ldquo;finding relationships&rdquo; between different tokens. We reach a point where every possible word has been encoded as vectors and this positioning gives them &ldquo;meaning&rdquo;. That&rsquo;s where the famous example comes from: if we take the word &ldquo;KING&rdquo; and subtract &ldquo;MAN&rdquo; then add &ldquo;WOMAN&rdquo; we&rsquo;ll find the vector for &ldquo;QUEEN&rdquo;.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>KING - MAN &#43; WOMAN = QUEEN</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>This is no accident, it&rsquo;s because training managed to &ldquo;position&rdquo; these vectors in such a way that we can do &ldquo;math&rdquo; with these vectors and find meaning.</p>
<p>I&rsquo;m being ABSURDLY rough here, keep in mind this is dozens of papers and I&rsquo;m summarizing it in half a dozen paragraphs. What matters is understanding that this costs A LOT OF TIME, because every new token entering training has to sweep through every other token already computed and recalculate a huge chunk of them, all the time!</p>
<p>Hours and hours, days and days later, processing terabytes and terabytes of text, eventually we end up with a huge file of vectors, a <strong>VSM</strong> or Vector Space Model.</p>
<p>If we go to a site that catalogs and stores pre-trained models, on <a href="https://huggingface.co/meta-llama/Meta-Llama-3-70B/tree/main"target="_blank" rel="noopener">Meta&rsquo;s LLAMA 3 70B page</a>, we see this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/34kujh0nzamxau15gqarr0rhp7v7" alt="Huggingface Meta"  loading="lazy" /></p>
<p>We have the big file partitioned into 30 smaller files of around 4.5 GB each, totaling about <strong>135 GB</strong>, for a 70B file, meaning 70 billion parameters. Parameters are not &ldquo;neurons&rdquo; or anything like that. They are weights and biases of the computations, stored in the database - I explain this in my videos, go watch them.</p>
<p>There are bigger versions, a GPT o4 has 1.8 trillion parameters. These are impressive numbers to throw around, but in practice, much above 300B, for general use, it doesn&rsquo;t make <em>that</em> much difference as people think. Maybe for very specific questions. It means yes, there are many more stored relationships, but that doesn&rsquo;t necessarily translate into proportionally better answers. It may just end up being more verbose than normal. Again, I&rsquo;m being rough, just trying to lower expectations. Stop being impressed by big numbers if you don&rsquo;t know what they mean.</p>
<p>Many other things influence answer quality besides parameters: quality and quantity of training data, model architecture, training methodology, context window size, fine-tuning, safety alignments and safety training (which usually make answers worse in the name of &ldquo;safety&rdquo;).</p>
<p>Smaller models, with more efficient architectures, higher-quality training data, focused on <strong>specific tasks</strong> beat a giant model that tries to know everything (<em>Jack of all Trades, Master of None</em>).</p>
<p>Being a bigger file with more parameters only means one thing: it will be proportionally heavier to run and probably won&rsquo;t fit in memory all at once and will have to keep <em>switching</em> chunks (like <em>memory bank switching</em>, for programmers out there).</p>
<p>In this Hello World project, I chose - randomly - to use the <a href="https://github.com/QwenLM/Qwen2.5-Coder"target="_blank" rel="noopener"><strong>Qwen 2.5 Coder 70B</strong></a> model. These open models like Llama are usually available in various sizes. I chose the 70B because it fits in the 24GB of VRAM I have on my RTX 4090 GPU. If your GPU is smaller, pick smaller models like 0.5B, 3B, 14B, 32B, and always prefer specialized models (like this one which is &ldquo;Coder&rdquo;). And always read the project page, which usually has documented code examples and fine-tuning. There is no general-purpose code that works for everything, each model has different characteristics.</p>
<p>Qwen 2.5 comes split into 6 files of 5 GB each, so you need to download <strong>30GB</strong> if you want to play with the &ldquo;extra-large&rdquo; version.</p>
<p>The code for this is actually pretty simple, because the AI community has already packaged all the tools you need into easy-to-use libraries, usually in Python (Python libs that talk to lower-level C libs, really), like <code>transformers</code> which does the heavy lifting of loading these files into memory and managing them.</p>
<p>These files usually have extensions like &ldquo;.safetensors&rdquo; which is kind of generic, or &ldquo;.pt&rdquo; or &ldquo;.pth&rdquo; which is more specific to the PyTorch format, which is a Python neural network library made by Meta. It&rsquo;s a competitor to Google&rsquo;s Tensorflow.</p>
<p>Tensors is the generic name for vectors or matrices. A scalar number is an order-0 Tensor, a vector is an order-1 Tensor, a matrix is an order-2 Tensor and so on. We don&rsquo;t have words for orders above 2, so you can just say order-3 Tensor, order-4, and infinitely up to millions of orders in an LLM.</p>
<p>CPUs are generic hardware made to deal primarily with order-0 tensors: integer scalar numbers. Operations like addition, subtraction, multiplication and so on are from one integer to another integer. Real numbers (with decimals) are &ldquo;simulated&rdquo;, splitting off an integer called the &ldquo;exponent&rdquo; from another integer called the &ldquo;mantissa&rdquo; (what you call the &ldquo;decimal&rdquo;) separated by a dot, the &ldquo;floating point&rdquo;. But essentially we are computing integers and truncating precision. More bits, more precision.</p>
<p>GPUs are hardware specialized in computing Tensors, in particular matrices. They were originally made to recompute effects on video images, like a video game in motion. Frames per second. A frame is made up of rows and columns of pixels: an order-2 tensor.</p>
<p>Instead of running a &ldquo;for&rdquo; loop and computing pixel by pixel, it&rsquo;s more efficient to pass an entire matrix to multiply with another matrix, a &ldquo;kernel&rdquo;, to obtain some effect like shadow, light, color change, distortions and much more. Programs that do this we usually call <strong>shaders</strong>.</p>
<p>A PyTorch or Tensorflow connects to a GPU using a library that gives them access to APIs so that, instead of passing images, they pass numeric matrices. NVIDIA has CUDA, AMD has ROCM, Apple has Metal, there&rsquo;s the general Vulkan project that tries to talk to all of them. But the most advanced and most used is still CUDA, because NVIDIA did get ahead years ago evangelizing that GPUs could be used for more than image shaders.</p>
<p>All that said, files like those &ldquo;.safetensors&rdquo; are loaded by the <code>transformers</code> library, which calls <code>torch</code> and tells CUDA to load them into the GPU&rsquo;s VRAM, where it can process them. The most basic code for this looks something 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-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span><span class="p">,</span> <span class="n">AutoModelForCausalLM</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">device</span> <span class="o">=</span> <span class="s2">&#34;cuda&#34;</span> <span class="c1"># the device to load the model onto</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now you do not need to add &#34;trust_remote_code=True&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">TOKENIZER</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s2">&#34;Qwen/Qwen2.5-Coder-32B&#34;</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">AutoModelForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s2">&#34;Qwen/Qwen2.5-Coder-32B&#34;</span><span class="p">,</span> <span class="n">device_map</span><span class="o">=</span><span class="s2">&#34;auto&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eval</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>We create a Tokenizer and a Model. The Tokenizer will &ldquo;tokenize&rdquo; whatever you ask in the chat and every text that goes into the session&rsquo;s context. This tokenization is &ldquo;mapped&rdquo; against the loaded model to generate an &ldquo;embedding&rdquo;, which is the vector representation of your text, normalized (compatible) for the loaded model. Different models convert your text into different embeddings. An embedding created with Qwen does not work directly with Llama.</p>
<p>Once you have these two objects, we can already do a chat:</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 the input into tokens</span>
</span></span><span class="line"><span class="cl"><span class="n">input_text</span> <span class="o">=</span> <span class="s2">&#34;#write a quick sort algorithm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">model_inputs</span> <span class="o">=</span> <span class="n">TOKENIZER</span><span class="p">([</span><span class="n">input_text</span><span class="p">],</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s2">&#34;pt&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Use `max_new_tokens` to control the maximum output length.</span>
</span></span><span class="line"><span class="cl"><span class="n">generated_ids</span> <span class="o">=</span> <span class="n">MODEL</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">model_inputs</span><span class="o">.</span><span class="n">input_ids</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">do_sample</span><span class="o">=</span><span class="kc">False</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The generated_ids include prompt_ids, so we only need to decode the tokens after prompt_ids.</span>
</span></span><span class="line"><span class="cl"><span class="n">output_text</span> <span class="o">=</span> <span class="n">TOKENIZER</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated_ids</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">model_inputs</span><span class="o">.</span><span class="n">input_ids</span><span class="p">[</span><span class="mi">0</span><span class="p">]):],</span> <span class="n">skip_special_tokens</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="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Prompt: </span><span class="si">{</span><span class="n">input_text</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">Generated text: </span><span class="si">{</span><span class="n">output_text</span><span class="si">}</span><span class="s2">&#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>With <code>input_text</code> (what you typed in the chat) tokenized and converted, we can ask the model to <strong>COMPLETE</strong> this text, and it starts generating a new token after another, using that ATTENTION architecture, finding the best next word it can. At some point it finishes and returns it in <code>output_text</code> and that&rsquo;s it, that&rsquo;s your &ldquo;answer&rdquo;. As I always say, that&rsquo;s what GPT is: a TEXT GENERATOR.</p>
<h3>Tokens and Context Windows<span class="hx:absolute hx:-mt-20" id="tokens-and-context-windows"></span>
    <a href="#tokens-and-context-windows" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>One of the most annoying things about paid LLM services like ChatGPT or Claude is the limit on how many tokens we can use. They run out really fast if you&rsquo;re actually working every day. And they&rsquo;re not exactly cheap just for playing around. Yes, yes, I know, it depends on the service. I can use Cursor and get cheaper Claude credits than using Claude directly. I&rsquo;ll also get better answers using the API directly instead of the web chat, etc.</p>
<p>That&rsquo;s a conversation for another day, but it&rsquo;s my motivation to always have an open source option available and to know how to &ldquo;tune&rdquo; it for myself. Qwen 2.5 may not be better than Claude or Cursor - I&rsquo;ll explain why. But it&rsquo;s &ldquo;free&rdquo; and runs on my local machine (zero privacy concerns).</p>
<p>Check out my little &ldquo;hello world&rdquo; project working:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/qwen_cli2.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/qwen_cli2.mp4)
    </video>
</div>
<p>Note that it doesn&rsquo;t just give answers. At one point I ask it to read a local file of mine, and it can pull the code and analyze it. I&rsquo;ll explain how to do this shortly.</p>
<p>The point now is that it has enough context window to load long stuff like source code files. I haven&rsquo;t tested anything very heavy, but this 70B model easily takes up nearly 20GB on my GPU:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ykbdo2ekv12woxbx964um0osphk0" alt="nvidia-smi"  loading="lazy" /></p>
<p>To configure it, I made a config block 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-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DEFAULT_CONFIG</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_repo&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen/Qwen2.5-Coder-14B-Instruct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_dir&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;MODELS_DIR&#34;</span><span class="p">,</span> <span class="s2">&#34;/models&#34;</span><span class="p">))</span> <span class="o">/</span> <span class="s2">&#34;Qwen2.5-Coder-14B-Instruct&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;quantization&#34;</span><span class="p">:</span> <span class="s2">&#34;8bit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;max_context_tokens&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;max_new_tokens&#34;</span><span class="p">:</span> <span class="mi">10000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;temperature&#34;</span><span class="p">:</span> <span class="mf">0.1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_download_timeout&#34;</span><span class="p">:</span> <span class="mi">1800</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;helpers_dir&#34;</span><span class="p">:</span> <span class="s2">&#34;helper_functions&#34;</span><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>What matters, first, is that I&rsquo;m choosing to run computations with FP8 (floating-point 8-bits), even though I think this model is FP-16. If you leave it at 16-bits, answers become much slower, at a level you can feel. Like, one word per second. I understood that the one doing this on-the-fly conversion is the Python <code>bitsandbytes</code> library, and you need to have it installed.</p>
<p>Another important part are the logs I leave visible when loading the model, you&rsquo;ll see the following if you try to run it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Qwen attention is NOT SDPA-compatible, or SDPA is not available. Trying xFormers...
xFormers is available. Enabling it for attention.
Sliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.
2025-04-25 02:34:27,638 [INFO] accelerate.utils.modeling: We will use 90% of the memory on device 0 for storing the model, and 10% for the buffer to avoid OOM. You can set `max_memory` in to a higher value to use more memory (at your own risk).
Loading checkpoint shards: 100%|██████████████████████████████████████████████████████████| 6/6 [00:07&lt;00:00,  1.29s/it]
SDPA is available and (hopefully) being used!</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><code>transformers</code> warns that &ldquo;Sliding Window Attention is enabled but doesn&rsquo;t implement SDPA&rdquo;. And I do a trick with <code>xformers</code> to try to enable it (in the end it says &ldquo;SDPA is available&rdquo;).</p>
<p>What is <strong>SLIDING WINDOW ATTENTION</strong>? Roughly speaking, the whole idea of this generation of GPT is the concept of ATTENTION, from the paper <a href="https://arxiv.org/abs/1706.03762"target="_blank" rel="noopener">&ldquo;Attention is all you Need&rdquo;</a>, which was the kickoff for everything we have today related to GPT and LLM.</p>
<p>The important concept is that the answers seem so &ldquo;good&rdquo; because it uses this &ldquo;attention&rdquo; process to generate them.</p>
<p>In the beginning, in the GPT 2 era, attention was very &ldquo;focused&rdquo; on a small context. Around 1024 tokens, which is almost nothing. You can ask short questions, but source code will never fit.</p>
<p>Then we kept evolving, you&rsquo;ve seen Gemini which claims it handles 1 million tokens. Even this Qwen 2.5 claims to handle 1 million tokens too. Roughly think of each token as 4 letters, on average.</p>
<p>A source code file, on average, has lines of 40 characters (some longer, some shorter). So that&rsquo;s about <strong>10 tokens per line</strong>. An average file has around 500 lines x 10 tokens/line, which already gives around 5 thousand tokens.</p>
<p>An entire small project, with about 20 files x 5,000 tokens, is already around 100,000 tokens. Sure, it&rsquo;s an average. But notice that with less than 100 thousand tokens you can&rsquo;t have a whole project in context.</p>
<p><strong>&ldquo;Ah, so if I use Gemini or this Qwen, 200 files will fit??&rdquo;</strong></p>
<p>Sort of. Here&rsquo;s where Sliding Window comes in. Think that even with a huge context, <strong>there&rsquo;s no way to pay attention to everything at the same time</strong>, so it searches for what you&rsquo;re asking right now, and &ldquo;slides the attention window&rdquo; only to the part of the context that seems most relevant. That&rsquo;s why even when loading lots of material, it keeps it in memory, but <strong>only pays attention to one chunk at a time</strong>, that&rsquo;s SLIDING WINDOW ATTENTION.</p>
<p>To do this we have the &ldquo;SCALED DOT PRODUCT ATTENTION&rdquo; or SDPA feature, which is what <code>transformers</code> complained it might not have, which is one of the ways to do Sliding Window Attention.</p>
<p>In my code, to load the model and try to use SDPA I tried 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="kn">import</span> <span class="nn">xformers.ops</span>  <span class="c1"># Test if xFormers is installed</span>
</span></span><span class="line"><span class="cl"><span class="n">model_config</span><span class="o">.</span><span class="n">attention_implementation</span> <span class="o">=</span> <span class="s2">&#34;flash_attention_2&#34;</span>  <span class="c1"># Or &#34;memory_efficient&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;xFormers is available. Enabling it for attention.&#34;</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="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="p">,</span> <span class="s2">&#34;scaled_dot_product_attention&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA is available and (hopefully) being used!&#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>And that&rsquo;s why, in the end, it finishes with &ldquo;SDPA is available&rdquo;. It &ldquo;looks&rdquo; like it worked. If you want to test it, there&rsquo;s this little snippet you can pass in to see if it works:</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">torch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="p">,</span> <span class="s2">&#34;scaled_dot_product_attention&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA not available&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">exit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">q</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">&#34;cuda&#34;</span> <span class="k">if</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s2">&#34;cpu&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">k</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">output</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="o">.</span><span class="n">scaled_dot_product_attention</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA works in isolation!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</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;SDPA fails in isolation: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#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>Dot Product, or matrix scalar product, is one of the most used operations in LLM models. I understood that without SDPA, you&rsquo;ll still have Sliding Window, but with SDPA you get better attention results. How much better? Hard to quantify, I just understood it&rsquo;s better.</p>
<p>But this is one of the techniques behind why nowadays you can have huge contexts of 1 million tokens: because you don&rsquo;t need to pay attention to everything at the same time, but rather windows at different positions for each new question. Get the trick?</p>
<p>And with that, in my config I can try to increase <code>max_context_tokens</code> (which I left at only 120 thousand tokens) and <code>max_new_tokens</code> which is the per-answer token limit (which I left at 10 thousand tokens). Try bumping these numbers up to a max of 1 million to see how it goes.</p>
<p><strong>Context only matters within the sliding context window. It&rsquo;s good to pass plenty of context. But understand that it won&rsquo;t pay attention to everything at the same time, but within a sliding window.</strong></p>
<p>Another important parameter is <code>temperature</code>, which I left low at 0.1. The higher it is, the more &ldquo;creative&rdquo; it will be in its answers. The lower it is, the more &ldquo;exact&rdquo; it tends to be. It&rsquo;s another parameter worth exploring.</p>
<h3>Prompts and &ldquo;Agents&rdquo;<span class="hx:absolute hx:-mt-20" id="prompts-and-agents"></span>
    <a href="#prompts-and-agents" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The best way to get better answers is to start every new session, whether via API or via the web chat, describing a &ldquo;persona&rdquo;, how you want the GPT to answer. The more descriptive you are, the better. For example, I have this snippet in my 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-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">build_system_prompt</span><span class="p">(</span><span class="n">tool_prompts</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;You are Qwen2.5 Coder, a highly skilled AI assistant specializing in software development.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Your capabilities include code analysis, explanation, error detection, and suggesting improvements.</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">tools_section</span> <span class="o">=</span> <span class="s2">&#34;TOOLS:</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">tool_prompts</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span> <span class="k">if</span> <span class="n">tool_prompts</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">rules_section</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;IMPORTANT RULES:</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;1. You MUST use the appropriate tool when necessary.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;2. You MUST NOT reveal the tool commands to the user.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;3. After a tool is used, continue the conversation as if you have direct access to the content.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;4. If a file fails to load, inform the user clearly.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;5. Do NOT ask for file/URL content directly; use tools.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;6. Once you&#39;ve executed [LOAD_FILE ...], you MUST immediately use the loaded content. &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Never say you cannot read it — if you see [LOAD_FILE &lt;path&gt;] then you now *have* it.</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">base</span> <span class="o">+</span> <span class="n">tools_section</span> <span class="o">+</span> <span class="n">rules_section</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>This is a fairly short example but I&rsquo;m already setting what kind of conversation I want to have with it and what rules I want it to follow. Not always, but most of the time, it tends to follow. Remember: the longer the chat gets, the bigger the context, the more the window slides, and the further away these rules get, until a point comes where it starts to &ldquo;forget&rdquo; to act the way I asked.</p>
<p>Understanding this, there&rsquo;s another trick I added. In this project there&rsquo;s a directory called <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/"target="_blank" rel="noopener">helper_functions</a> where we have scripts like <code>load_file.py</code> or <code>fetch_url.py</code>. In the earlier code note how it concatenates something in the middle of the prompt, called &ldquo;TOOLS&rdquo;. When the chat starts, I print to the screen and you&rsquo;ll see this snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
TOOLS:
[BATCH_LOAD args] – If the user asks to read, load or analyze all the files from a relative path, such as ./src or similar,
[LOAD_FILE args] – Whenever the user asks to read, load, analyze some code and provides a relative path, such as ./file.py or utils/utils.py or similar,
[FETCH_URL args] – Whenever the user asks to read, load, research or consult a URL,
...</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>So, if at some point, I ask:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hiz90dk6owf2otjfixiftjxavm4n" alt="qwen_cli fetch_url"  loading="lazy" /></p>
<p>this &ldquo;can you read a website at https://www&hellip;.&rdquo; will make the model follow the initial prompt rule and type into the chat:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[FETCH_URL https://www...]</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>My chat program has a &ldquo;parser&rdquo; that monitors the history for these special keywords:</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="k">def</span> <span class="nf">parse_special_commands</span><span class="p">(</span><span class="n">response</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">tuple</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">&#39;\[([A-Z_]+)\s+([^\]]+)\]&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">(),</span> <span class="n">m</span><span class="o">.</span><span class="n">start</span><span class="p">(),</span> <span class="n">m</span><span class="o">.</span><span class="n">end</span><span class="p">())</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">finditer</span><span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="n">response</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>It&rsquo;s a pretty primitive parser, using REGEX to grab these snippets. This is where I say my project is educational. I imagine a Co-pilot, Cursor, etc, has a much more robust parser. I explained how to build parsers with ANTLR on my channel, remember? One of the videos is <a href="https://www.youtube.com/watch?v=_7nISfpofec&amp;pp=ygUTYWtpdGFvbnJhaWxzIHBhcnNlcg%3D%3D"target="_blank" rel="noopener">&ldquo;Eu fiz um servidor de SQL?&rdquo;</a>. Real parsers aren&rsquo;t built with REGEX, but for proofs of concept, it does the job.</p>
<p>Then in the chat loop there&rsquo;s this snippet:</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="n">response_text</span> <span class="o">=</span> <span class="n">QwenSession</span><span class="o">.</span><span class="n">_tokenizer</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated</span><span class="p">,</span> <span class="n">skip_special_tokens</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"># Other special commands</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cmd_type</span><span class="p">,</span> <span class="n">cmd_arg</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">parse_special_commands</span><span class="p">(</span><span class="n">response_text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cmd_type</span> <span class="ow">in</span> <span class="n">helper_functions</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">helper_functions</span><span class="p">[</span><span class="n">cmd_type</span><span class="p">](</span><span class="n">cmd_arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">history</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">result</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">history</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;Please continue the analysis using the loaded file.&#34;</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;✅ [</span><span class="si">{</span><span class="n">cmd_type</span><span class="si">}</span><span class="s2">] processed &#39;</span><span class="si">{</span><span class="n">cmd_arg</span><span class="si">}</span><span class="s2">&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">prompt</span><span class="p">,</span> <span class="n">helper_functions</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="p">,</span> <span class="n">temperature</span><span class="p">,</span> <span class="n">stream</span><span class="p">,</span> <span class="n">hide_reasoning</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>It decodes the generated tokens, passes them to the parser, and if there&rsquo;s a special command, it calls <code>helper_functions</code> which dynamically calls <code>fetch_url.py</code> which is a dumb script that will use <code>urllib</code> to load the page.</p>
<p>With the content loaded, it concatenates it back into <code>self.history</code> which is the chat history, using <code>.append</code>, and sends a message - as if it were me, the &ldquo;user&rdquo; - asking the model to continue, now that it has the page&rsquo;s content.</p>
<p>And as you can see in the screenshot above, it can give me the summary of the GitHub page I passed. That&rsquo;s how the various GPTs manage to pull more current information from sites that didn&rsquo;t exist at the time the pre-trained model was trained.</p>
<p>Another example, I can ask it to load a code file of mine and ask it to analyze, or fix bugs, or refactor it however I want:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lt891apoaebrmx0y4k7ngtk2l5ol" alt="qwen_cli load_file"  loading="lazy" /></p>
<p>It&rsquo;s similar to the &ldquo;file upload&rdquo; feature in ChatGPT. Again:</p>
<ul>
<li>the model will follow the initial prompt rules and will type the <code>[LOAD_FILE ...]</code> command</li>
<li>my <code>parser_special_commands</code>, with its REGEX, will find this command and split the arguments</li>
<li>the chat loop will call <code>helper_functions</code> and map to the <code>load_file.py</code> script, which is a dumb script that reads files</li>
<li>the file content is concatenated back into the context <code>history</code> and the model can now keep responding based on that content.</li>
</ul>
<p>Understand, those &ldquo;.safetensors&rdquo; files are <strong>READ-ONLY</strong>. Nothing is written there. Doesn&rsquo;t matter which one. Every LLM is <strong>CLOSED FOR WRITING</strong>. Nothing you write in the chat is recorded in the model.</p>
<p>Remember how I said the number of &ldquo;parameters&rdquo; doesn&rsquo;t determine the quality of the model? What <strong>MOST</strong> determines quality is the <strong>QUALITY OF THE TRAINING MATERIAL</strong>. And chats, to a certain extent, are useful as samples of how the model should answer, but 90% or more I claim is pure and total <strong>GARBAGE</strong>. Every chat we write is garbage and no good for training, it would just WORSEN the final model and make it heavier with useless information.</p>
<p>Write all the &ldquo;Thank You Very Much&rdquo; or &ldquo;Please&rdquo; you want. It&rsquo;s all discarded at the end, nothing is saved, and no ChatGPT, Gemini, Claude, Deepseek, Qwen or Llama will ever know anything about what you said to it. It&rsquo;s all totally <strong>SEPARATE AND DISPOSABLE</strong> information.</p>
<p>Everything I typed in those screenshot examples as my little Qwen chat gets discarded the moment I type &ldquo;bye&rdquo; or hit &ldquo;Ctrl+D&rdquo; to exit. That&rsquo;s what we call a &ldquo;session&rdquo; or &ldquo;history&rdquo;. It&rsquo;s just a useless text file as far as the model is concerned. It only serves us, humans, as notes to use later, if you want. My little program doesn&rsquo;t even have a feature to save this history. When you exit, everything is clean. So, feel free to curse away in my chat.</p>
<p>There are two important points in this section:</p>
<ul>
<li>the better the rules you describe at the start, the better the answer results tend to be. This applies to any LLM.</li>
<li>you can &ldquo;agree&rdquo; with the model to spit out &ldquo;pseudo-commands&rdquo; and have an external program that does the parsing and actually executes the commands, like I did with my &ldquo;helpers&rdquo;. That&rsquo;s what we call agents. An LLM agent is a little program that returns some content back to the session&rsquo;s context history.</li>
</ul>
<p>Where are Cursor or Co-pilot better than this educational little program of mine? They come pre-configured with DOZENS of pre-made rules they&rsquo;ve tested extensively to get better results, and they ship with a library of DOZENS of small scripts - like my helpers - to pull information from your project or your IDE to improve context.</p>
<p>But the secret is this: <strong>RULE PROMPTS</strong> and <strong>CONTEXT SCRIPTS</strong>.</p>
<p>If I were to summarize the entire Agents or MCPs ecosystem or whatever it&rsquo;s called these days, it&rsquo;s this combination of factors to improve answers.</p>
<p>Another trick is using MORE THAN ONE LLM. Remember I said specialized LLMs are better? Besides Qwen 2.5 Coder, which people say is good for code, there was already WizardCoder before it, Code Llama, StarCoder, now there&rsquo;s Deepseek Coder and several others, some even specialized in specific languages. You can experiment to see which gives better results for your case.</p>
<p>If you save the session as text, you can unload one LLM, load another LLM, re-tokenize and re-load it into the context of this other LLM and try a different result - if anyone wants to implement this feature in my little project, Pull Requests are welcome.</p>
<p>As I said at the beginning, that&rsquo;s what software like <a href="https://aider.chat/"target="_blank" rel="noopener">Aider</a> does. I haven&rsquo;t played with it enough yet, but it connects to ChatGPT, Gemini, Claude and any other, and you can enable as many as you want. I think it also supports local models like Qwen, so there are already tools that do this. And that&rsquo;s the third leg for better answers: MULTI-LLM.</p>
<h3>Next Steps<span class="hx:absolute hx:-mt-20" id="next-steps"></span>
    <a href="#next-steps" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instead of just being impressed by the pile of tools out there, understand these fundamental pieces. Knowing how things work always gives you more options to forge alternatives that work better for specific cases. You, who claim to be a programmer, should be exploring these options - and not just settling for the limitations and waiting for someone to solve your problem. Roll up your sleeves and solve it yourself. Isn&rsquo;t that why you became a programmer???</p>
]]></content:encoded><category>llms</category><category>local-models</category><category>coding-agents</category></item><item><title>Changing Clothes Using A.I. (ComfyUI)</title><link>https://www.akitaonrails.com/en/2025/04/23/changing-clothes-using-ai-comfyui/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/23/changing-clothes-using-ai-comfyui/</guid><pubDate>Wed, 23 Apr 2025 17:45:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; this post is now obsolete. I now use &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;, which runs Aitrepreneur&amp;rsquo;s stacks locally in Docker with pre-loaded models and 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/23/using-ai-comfyui-to-generate-npcs-for-game-development/"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I showed how to generate character sheets for game development. But some folks asked me &amp;ldquo;what if I want specific clothes?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hold my Beer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a way. It&amp;rsquo;s a separate workflow that you can later add to the previous workflow if you want (homework assignment). Just take the character generated in the first group of nodes and pass it through this other workflow that swaps clothes. (Or do the extra work of generating loras and activating them with keywords in the prompt as well, there isn&amp;rsquo;t only one way.)&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p><strong>Update:</strong> this post is now obsolete. I now use <a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener"><strong>akitaonrails/aitrepreneur-docker</strong></a>, which runs Aitrepreneur&rsquo;s stacks locally in Docker with pre-loaded models and workflows. Check the README for details.</p>

</blockquote>
<p>In the <a href="https://www.akitaonrails.com/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/"target="_blank" rel="noopener">previous post</a> I showed how to generate character sheets for game development. But some folks asked me &ldquo;what if I want specific clothes?&rdquo;.</p>
<p><strong>Hold my Beer</strong></p>
<p>There&rsquo;s a way. It&rsquo;s a separate workflow that you can later add to the previous workflow if you want (homework assignment). Just take the character generated in the first group of nodes and pass it through this other workflow that swaps clothes. (Or do the extra work of generating loras and activating them with keywords in the prompt as well, there isn&rsquo;t only one way.)</p>
<p>Unlike Mick&rsquo;s workflow - which is paid - this other one is open. I grabbed it from some Reddit post and dropped it into the workflows folder of my Docker project as &ldquo;garmente-replacement-idm-vton.json&rdquo;. Just load it in ComfyUI and play around. Let&rsquo;s see the result. Here are two images. The result isn&rsquo;t perfect, but see if you can figure out which of the two is the original and which one was generated by A.I.:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/6b56mdoiwf82xdh7cdhdc7tg0ic2" alt="Hemsworth 1"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/w0s9nthw6jt86en8wl09m67ryzpc" alt="Hemsworth 2"  loading="lazy" /></p>
<p>Answer at the end of the post lol</p>
<p>Anyway, the complete workflow looks like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xbgu7dzrjzflhfsld6jtrl5jc3ia" alt="workflow"  loading="lazy" /></p>
<p>In the current state of my setup there&rsquo;s a glitch I haven&rsquo;t been able to solve. I think it&rsquo;s the JNodes extension that&rsquo;s opening an infernal popup at the top left of the interface, which won&rsquo;t close no matter what. For now I&rsquo;m opening the Inspector and deleting the Node from the HTML directly lol it&rsquo;s a hack but it works:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/p8q4hmwjmi4opsu8u045jxrpma2v" alt="JNode bug"  loading="lazy" /></p>
<p>If anyone knows how to fix it, I&rsquo;d really appreciate it. There&rsquo;s an alert from an extension that seems to be outdated, this front-end something, but just ignore it. Of course it had to be front-end-something&hellip;</p>
<p>Anyway. In this first group it&rsquo;s simple, just load an image as clean and clear as possible of the new garment you want to use. And then the original photo where you want this clothing to be applied. It doesn&rsquo;t perform miracles. If the photo is of someone in a blazer and you want to change it to a tank top, sometimes it manages, but don&rsquo;t expect the arms and torso to look good. It&rsquo;s going to redraw everything as it sees fit, or it&rsquo;ll end up putting the tank top on top of the blazer. Anyway, it&rsquo;s A.I., the clearer the goal, the better the result. If the new garment is a blazer, ideally the original was another blazer, it&rsquo;ll fit better.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2owpldebsc91y6vfsayscdkuqtui" alt="Load Images"  loading="lazy" /></p>
<p>Heads up that the IDM-VTON models really weigh a <strong>TON</strong>. It alone downloads no less than <strong>30GB</strong> of models (already included in my setup) and will demand more than 20GB of VRAM, so anything below an RTX 3090 with 24GB just won&rsquo;t run. You can run it online, on the Hugging Face site. Watch this tutorial that shows how:</p>
<!-- youtube video unavailable (deleted/blocked): WL59FqL0L-s -->
<p>This is the <strong>Aiconomist</strong> channel. He shows how to run it online and how to set it up on your machine in <a href="https://youtu.be/pFYqPf1Z7x8?si=Ryu5vxPmKGr4zhWx"target="_blank" rel="noopener">this other video</a> which I used as reference to build my workflow. Subscribe to his channel, it&rsquo;s worth it. The tutorials are very well explained and have more details that I won&rsquo;t cover in this post.</p>
<p>That said, there&rsquo;s a bug I haven&rsquo;t solved yet. After running the workflow, the model stays hanging in the GPU&rsquo;s VRAM and if I try to run again, it FREEZES. Then I have to shut down the Docker container and restart it to recover the memory. I tried a Clear VRAM GPU node but it didn&rsquo;t work. If anyone knows how to fix it, I&rsquo;d appreciate that too.</p>
<p>The next step is recurring across many workflows: extracting layers of information from the original photo. Getting a mask of the garment we want to replace and information about the original pose:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/kgs8jj5m7cgt2nx4kv8yazfpbfg5" alt="mask and pose"  loading="lazy" /></p>
<p>This is why ComfyUI workflows are powerful once you start understanding how the process is broken down. These parts are reusable and different models give different results depending on which controlnets you add.</p>
<p>Then we get to the main part: wiring all this information into the IDM-VTON node, which will reposition the new garment on top of the original photo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7xnxbrcy22trdctbf5a1dhh0rqjw" alt="IDM-VTON"  loading="lazy" /></p>
<p>Notice that the result isn&rsquo;t very good, it looks pretty &ldquo;fake&rdquo;. And there are parameters you can tweak to improve it. But the use case isn&rsquo;t to swap for a <strong>specific</strong> garment but rather for a <strong>similar</strong> one. I&rsquo;ll explain in a bit, but first, a tangent.</p>
<p>The repository for the original extension of this Node is <a href="https://github.com/TemryL/ComfyUI-IDM-VTON"target="_blank" rel="noopener">this one</a>. But if you try to install it, it breaks. It seems to be a bit out of date with ComfyUI and needs to run on an older version. But that&rsquo;s a pain. So I went ahead and made a <a href="https://github.com/akitaonrails/ComfyUI-IDM-VTON"target="_blank" rel="noopener"><strong>FORK</strong></a> with the necessary fixes to run on the newer version (the newer versions of the Python packages diffusers, transformers, huggingface_hub, break IDM-VTON).</p>
<p>By the way, Python dependency management is pretty BAD. But ComfyUI&rsquo;s is even worse, because different extensions (which install Python dependencies globally) can be at different stages of development. Every time ComfyUI itself changes, everyone needs to update fast. That&rsquo;s why the ideal is not to install the master branch version of ComfyUI, always a stable version (marked with a tag) or two versions back, just to be safe. But then there are new extensions that require you to be on the newest version. As I said, it&rsquo;s a nightmare to manage. Being a programmer, I can &ldquo;hack around&rdquo; when errors happen, but normal users will suffer a bit.</p>
<p>Rant over. In the previous step the first part of the flow ends. When it hits this Image Sender node, the process stops - I don&rsquo;t know why. And you have to click the &ldquo;RUN&rdquo; button a second time. Then the other node, Image Receiver, will receive the new image. I think I&rsquo;ll just delete those two nodes and connect the &ldquo;image&rdquo; output of the Run IDM-VTON Inference node directly to the next node, which are the Image Composite Masked and the VAE Encode.</p>
<p>Let&rsquo;s go back a step:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jj950u7842rkc9lxniz3uoztbszv" alt="New photo, new garment"  loading="lazy" /></p>
<p>In this next step, on the second &ldquo;RUN&rdquo;, it takes the image with the new garment, but low quality, and loads a new checkpoint (in this case juggernaultXL but it could be any other like SDXL itself, FLUX or dreamsharper). The model will determine the new photo, which is what gets generated at the end, on the right.</p>
<p>Notice that the new photo has much better garment quality, but the face is completely different. It used the information from the original photo, kept the pose, the body proportions, even the background, but it can&rsquo;t preserve the face. No problem, because we can help.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bw9sram1maspiob2ubo4r904oriy" alt="mix photos"  loading="lazy" /></p>
<p>Remember that in the previous step we got the mask (black and white) of the exact position of the garment? We can enlarge the mask and apply a Gaussian Blur (so it later blends nicely into the original photo) and use that to crop from the generated photo with the different face, using the <a href="https://github.com/tencent-ailab/IP-Adapter/"target="_blank" rel="noopener"><strong>IPAdapter</strong></a> model (which alone is another 30GB of models - I told you it&rsquo;s heavy).</p>
<p>If I understood correctly, IP Adapter is responsible for being able to generate new images that are COHERENT with each other, with the same face, or same clothes, but in other positions, for example, a kind of conditioned generation. With it you can control the coherence of new images A LOT, generate a character in different positions, in different backgrounds and always preserving the characteristics of the original character.</p>
<p>IP Adapter will use the original photo, keep the face, use the masks and blend with the photo with the different face, taking only the garment, and then assemble everything in the Image Composite Masked which is the final Node of the process.</p>
<p>In the end, we can compare the first attempt at putting on the new garment (which came out ok but ugly, and the new garment, which came out different but prettier) and everything integrated into the original photo, preserving the same face.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uawsqhhr6d8kii8getzkq6ebxgh9" alt="image compare"  loading="lazy" /></p>
<p>As I said, the qualities are affected by the choice of model, the KSampler settings, number of steps, cfg and everything else. You have to experiment and generate several until you reach the result you like best. That&rsquo;s also how you learn more. You don&rsquo;t just click once and magically have the result. A good artist experiments dozens of times.</p>
<p>The final image might not be suitable for a Vogue cover (although I&rsquo;ve already seen plenty of official covers that were VERY questionable). It depends on the need. In particular it&rsquo;s good for prototyping. &ldquo;I wonder how this garment looks on my model?&rdquo; Instead of calling the model in, having her change clothes, or spending an hour Photoshopping, in minutes you can try several different garments and see whether the model fits the garment, without wasting time. So a photographer can just show it to the client before they spend money on studio, assistants, equipment, etc. You can visualize quickly and discuss details much faster.</p>
<p>This is the kind of thing where tools like this can help a lot and differentiate one artist from another, not by replacing the artist, but by creating more options. And the main thing: all these workflows are modifiable, you reprogram them and combine them with others however you want. The limit is your creativity.</p>
]]></content:encoded><category>generative-media</category><category>local-models</category></item><item><title>Using A.I. (ComfyUI) to Generate NPCs in Game Development</title><link>https://www.akitaonrails.com/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/</guid><pubDate>Wed, 23 Apr 2025 16:25:00 -0300</pubDate><description>&lt;p&gt;I have been updating my little ComfyUI with Docker Compose project A LOT. I think it is the most complete and tested setup out there, with the advantage of being in Docker, so it is repeatable with zero setup. Read &lt;a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener"&gt;my post&lt;/a&gt; for more details on how it works.&lt;/p&gt;
&lt;p&gt;I decided to document in this post one of the most complex workflows I have tested that runs on my setup: how to develop Character Sheets of random characters, in any style, whether realistic, Pixar style, cartoon style, whatever you want. The result is a sheet like the example below. In several poses, several lighting setups, several emotional expressions, with a color index, all generated from absolute scratch by &amp;ldquo;A.I.&amp;rdquo; In half an hour of processing on my RTX 4090.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I have been updating my little ComfyUI with Docker Compose project A LOT. I think it is the most complete and tested setup out there, with the advantage of being in Docker, so it is repeatable with zero setup. Read <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">my post</a> for more details on how it works.</p>
<p>I decided to document in this post one of the most complex workflows I have tested that runs on my setup: how to develop Character Sheets of random characters, in any style, whether realistic, Pixar style, cartoon style, whatever you want. The result is a sheet like the example below. In several poses, several lighting setups, several emotional expressions, with a color index, all generated from absolute scratch by &ldquo;A.I.&rdquo; In half an hour of processing on my RTX 4090.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/469eyg4a4pl3ry0z12twg74nlbc8" alt="NPC Design Sheet"  loading="lazy" /></p>
<p>Imagine you are an intern at a game studio. Your job: create 100 background characters (NPCs). In the end you have to deliver 100 of them. But just because they are secondary characters does not mean you can do them any old way, they all have to be coherent, in every pose. Imagine how many days you would take. What if you could automate it? This is the kind of scenario where A.I. is interesting. Spend more time on the main characters, do them manually, but do not waste time on the secondary ones and do not lower the final quality of everything.</p>
<p>I did not make this workflow, it was Mickmumpitz. Watch his entire video to learn how this workflow works, I will not repeat what he already explains:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/grtmiWbmvv0"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>I will also not share his workflow, because he has a paid Patreon (it is super cheap, only USD 10 per month), which gives you access to download his entire workflow pack <a href="https://www.patreon.com/posts/free-workflows-120405048"target="_blank" rel="noopener">at this link</a></p>
<p>But that alone is not enough. The requirements you need:</p>
<ul>
<li>a beefy machine with at least an RTX 3090 with 24GB of VRAM. The important thing here is VRAM, the models used easily exceed 20GB of usage. CPU is less important, I use a 7950X3D but a 7800 would already handle it.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/730safwxqkrsgawu4liw42scoul3" alt="GPU VRAM"  loading="lazy" /></p>
<ul>
<li>if you do not have such a machine, it takes more work but you can rent one on <strong>RUNPOD</strong>. They have several GPU configurations, but you will need to know how to configure Linux a bit or choose Windows and do Mickmumpitz&rsquo;s tutorial all by hand (trust me, you will suffer if you do it that way).</li>
<li>you need to have at least <strong>500GB</strong> of available space and good internet to download. The first time my Docker runs, it automatically downloads all ComfyUI extensions and all models for everything. You can edit the <code>models.conf</code> file beforehand and remove models that you know you will never use. But just this workflow alone requires at least about 30GB of models, there is no way around it.</li>
<li>this workflow is particularly <strong>HEAVY</strong> because it has several features (parts of which we can disable, to customize for our workflow). But for 1 character, the entire flow can take <strong>30 MINUTES</strong>. Even on an RTX 4090 like mine.</li>
</ul>
<p>If you have done everything right up to this point, you have ComfyUI running with all dependencies already downloaded and you bought the Workflow. When you load it you will have to fix some things manually.</p>
<p>The first thing is that you will see an error in several Nodes in red, all of them <strong>Ultimate SD Upscale</strong>. Just right-click on each one and choose &ldquo;Fix Recreate&rdquo; and it will fix automatically:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/djblur5g36mw9denng0ht0vvksym" alt="Fix Upscale"  loading="lazy" /></p>
<p>ComfyUI requires that each output slot of a Node connects to the input slot of another compatible Node, but often the workflows get so big that there are little wires connecting things scattered all over the place and it gets really hard to know what connects to what. It literally becomes <strong>spaghetti</strong>.</p>
<p>But there is an extension (which I have already pre-installed) called <a href="https://github.com/chrisgoringe/cg-use-everywhere"target="_blank" rel="noopener">cg-use-everywhere</a>. Every extension is a little GitHub project, cloned into the <code>custom_nodes</code> subdirectory. It lets you connect a Node&rsquo;s output to this <strong>Anything Elsewhere</strong> node, say the &ldquo;clip&rdquo; output of the &ldquo;Load Clip&rdquo; Node. Now, every other Node that has &ldquo;clip&rdquo; as an input field and does not have anything explicitly connected to it will use this Anything that was created. It works super well.</p>
<p>Only in this Mick workflow, who knows why, all the Anywheres are broken. But it is easy to fix. Just find these Nodes, delete them, create new ones and reconnect. There are several right at the beginning, write down who connects to each one, delete them and create a new one to reconnect:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/kjjo7u6g58drww1v5f8v88h1iis1" alt="anywhere initial"  loading="lazy" /></p>
<p>There are also some in the Upscale group 02:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qrkdxs6qjdtmdjs3c463oxetbgyy" alt="anywhere upscale"  loading="lazy" /></p>
<p>Small Nodes, with only the title showing, are COLLAPSED. Just right-click and choose EXPAND to see the details. It is another way to organize the mess.</p>
<p>Lastly, you have to recheck the correct name of the models in all the &ldquo;Load&rdquo; something Nodes right at the beginning of the workflow (luckily he organized these nodes all together):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/y1u93ujz3kq4hz3pik5q4wcgapoa" alt="model path"  loading="lazy" /></p>
<p>In his workflow, the Node comes looking for &ldquo;FLUX/flux1-dev-fp8.safetensors&rdquo; but then you go to the list (which pulls from what is actually in the &ldquo;models/checkpoints&rdquo; directory on your machine) and choose the one with the same name. This happens often because different people organize different subdirectories for the models. But after configuring, just save and you are done. You have to do this for the checkpoint, controlnet, ic-light, etc.</p>
<p>ComfyUI errors are not friendly. They are Python stacktraces, which are not friendly because they show which line of code blew up, but they do not show what was in the variables passed to the function that blew up. So there is no way to know where it went wrong. But normally on the web interface, the Node turns red, then you check it and usually it is because you forgot to download the model or pick the right one from the list.</p>
<p>The last thing in this workflow is to download the OpenPose image from his Patreon page, which I linked at the beginning of the post, and load it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uajcc6ocnr5llzv8wzoowl85c5o3" alt="openpose image"  loading="lazy" /></p>
<p><strong>OpenPose</strong> is a standard file format that describes poses, an open format that can be edited in several tools. And there are several pre-trained models to extract poses in this format from images, so we do not even need to edit it by hand. We can download several pre-made open poses from this site:</p>
<p><a href="https://openposes.com/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ntvaf9vba1rugg11qn3n14u2t69e" alt="Poses Sites"  loading="lazy" /></a>
I am going to repeat this several times, but <strong>watch Mick&rsquo;s video tutorial</strong> where he explains how to generate characters from scratch or use <strong>your own photos</strong> with your own poses to continue the flow from there. His workflow, after running, looks like this (with zoom way down below):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jqu4o2c5cenci6au2g8bbny064mm" alt="complete workflow"  loading="lazy" /></p>
<p>In the first part, there is the manual configuration and checking of the models. You only have to fix it once and save. After that it will always work the same way. In this first menu you can also turn on and off some of the Node groups, for example, to skip the first stage of generating a character from scratch and be able to use your own photos:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/s72mrc1ib5o173c9l74vmpsn3js3" alt="menu and setup"  loading="lazy" /></p>
<p>If you choose to generate a character from scratch, there is a green prompt box to type how you want your character to be, in what style. Text Prompts are decoded by <strong>CLIP</strong> (Contrastive Language Image Pre-Training) models, which are specific to certain UNET categories (like SDXL for Stable Diffusion or the now best and most popular FLUX). Part of the learning is starting to figure out which sub-models (clips, clip_vision, vaes, loras, etc.) are compatible with which unets (unet, checkpoints, diffusion_models). You cannot mix just anything. Roughly speaking, it is a bit like, if you choose to do a text in German, all plugins have to be in German.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/37oe4dyqr5t0in9ad98bocli6u3w" alt="Character Generation"  loading="lazy" /></p>
<p>I will not show the complete workflow because it does not even fit, but you can see the prompts in green, in the middle where we choose which pose we want (it is on his Patreon), and on top the image generated by the <strong>flux1-dev-fp8</strong> model, one of the best and most popular for having excellent quality and not being so heavy (fp8 instead of fp32, for example).</p>
<p>This is one of the advantages of using tools outside the Dalle or Midjourney of the world: we can control all aspects of image generation not only with text prompts, but also with image prompts, like OpenPose.</p>
<p>Another detail. I explained in my previous post about KSampler or Samplers in general, that they are the engine that does all the work of joining all the models, images, prompts, controlnet, vaes and produces the final image. KSampler parameters like steps, cfg, denoise, scheduler and more <strong>DEPEND</strong> on the chosen model. There are well-documented models, there are poorly documented models.</p>
<p>For example, say you read on a Reddit thread about this other more optimized model called <a href="https://civitai.com/models/215418?modelVersionId=273102"target="_blank" rel="noopener">&ldquo;TurboVisionXL&rdquo;</a>, which is available on the Civit.ai site:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hpcj5e8g0hxvefelvszwf48vsnpf" alt="TurboVisionXL"  loading="lazy" /></p>
<p>It really is a good model, but now what? What do I configure in the KSampler for it? Luckily if you scroll the page (always read the original documentation!!) you will find these recommendations:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/57f9vyfh4w9j8bduaomc3aow2sks" alt="TurboVisionXL documentation"  loading="lazy" /></p>
<p>See? He explains what to configure and what the known limitations are. Further down there will be comments from other people who can give more tips or share errors that you have already seen. It is a huge community, you need to learn how to participate in it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7avk9tl1pop1eexkuu3h8epgs2t5" alt="MultiView"  loading="lazy" /></p>
<p>But back from the tangent, in Mick&rsquo;s second group of nodes, he takes the first image in &ldquo;Pose T&rdquo; and runs it through a &ldquo;Multi View&rdquo; process using Diffuser MV which does this automatically. It is the same pose, but as if the camera went 360 degrees around it, capturing different angles and, most importantly, generating new images coherent with the original (see how the model is even used to invent the character&rsquo;s back).</p>
<p>In this step he also extracts only the face from the first image to do other treatments in parallel. Face recognition and extraction is super common. Every security system uses it, Google Photos uses it to organize dynamic albums. And it is worth learning to use in your own products. It is a reasonably light and fast process these days and there are several pre-made algorithms and models to use. In this case I do not think it even uses A.I. as such, it is just an algorithm.</p>
<p>Another detail: image generation models normally work in <strong>LOW RESOLUTION</strong>. Usually below 1024x1024. Do not expect to generate 4K images right off the bat. But that is no problem, because the solution is to first generate a low resolution image and then run it through another A.I. model specialized in <strong>UPSCALING</strong> like the RealESRGAN that I already talked about in my other post 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">upscaling old anime</a></p>
<p>Different from what Photoshop or any image editor does, if you try to enlarge an image, it just takes a pixel and tries to duplicate, quadruplicate the surrounding pixels. But that usually only makes the image bigger but blurred, because it has no way of knowing the details, and it will not even try to &ldquo;invent&rdquo; details.</p>
<p>To &ldquo;invent&rdquo; details, you need a pre-trained model that learned what to draw. It is also how NVIDIA&rsquo;s DLSS 3 works for games, where the game renders in low resolution, like 720p, and DLSS, in real time, redraws the new screen in 4K. It is not perfect, but playing at 60fps, you will rarely notice the difference. You will only notice that it got more <strong>CRISP</strong>.</p>
<p>That is also why, if you use your own photo, or photos of people you know, you will see that the new photo after upscaling looks <strong>slightly different</strong>, it is no longer the same person, because the model <strong>REDRAWS</strong> details that did not exist. It is a process of interpretation and not of discovering &ldquo;hidden&rdquo; details (that does not exist, only in CSI fiction).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2he2fgmus8vmp3wfp95s3qbh7ztf" alt="Upscale 2"  loading="lazy" /></p>
<p>Mick&rsquo;s workflow goes one step further and has a second upscaling pass, focusing on fixing only the face with a Face Detailer Node, made specifically to fix and improve faces. And here we enter plastic surgery and harmonization territory for real lol the final face will be different from the original. But for NPCs and secondary characters it does not matter, nobody will know the difference anyway, as long as it is not deformed, cross-eyed or something else that jumps out at you.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/3d40a8n9b32mmkub5l8c04oou37h" alt="Emotions"  loading="lazy" /></p>
<p>There are Nodes specialized in expressions, like the Expression Editor (PHM) which starts from a single face photo and manages to generate new photos in the most varied emotions - which we can configure exactly, numerically, however we want. I will not show it, but note that the generated images are in low quality, but the next step in the workflow is precisely to upscale them too.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cxdua4dfalu6d68tpaleljie5y1e" alt="Lighting"  loading="lazy" /></p>
<p>This step is interesting. First the top part, with the various scenarios. They were all generated via the green prompts below them, prompts that Mick left but that we can edit however we want OR swap the generation node and put a Load Image node to load our own background images, from real photos.</p>
<p>The objective of this step is to generate the faces under different light configurations. Many people think that &ldquo;light&rdquo; in games is just &ldquo;turn on the red light&rdquo; and everyone will automatically turn red. In new games, with <strong>RAY TRACING</strong> yes, that is why it is SUPER HEAVY. But in games made for old machines, indie games, you cannot demand this from everyone.</p>
<p>So &ldquo;tricks&rdquo; are used to fake things like lights and shadows. It is the <strong>BAKING</strong> process where we literally &ldquo;stamp the light onto the texture&rdquo; of objects and characters. With global illumination on or off, they will always be &ldquo;painted&rdquo; with the colored light of the scene. To switch scenes, new textures are loaded with other &ldquo;baked&rdquo; lights. For programmers, think of baking as <strong>Caching</strong>. Why recalculate everything dynamically every time, if the scene&rsquo;s light never changes? Save that step and leave it &ldquo;cached&rdquo;.</p>
<p>Going back, this step uses the <a href="https://github.com/lllyasviel/IC-Light"target="_blank" rel="noopener"><strong>IC-Light</strong></a> technology which is another open source project worth learning, even if you are not interested in A.I. It can manipulate the light of any image or photo. You, as a photographer, can change the light if you have changed your mind, in post-production. Adobe or BlackMagic tools, I think they already include similar technology, but it is good to know that there is open source and you can integrate it into your product.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qup2n6vadpc7qrtgokbqbfkpvmjg" alt="final sheet"  loading="lazy" /></p>
<p>After generating the faces with different lights, obviously there is another upscaling step - which I am skipping showing - and it goes to the final group of nodes that joins everything into a &ldquo;sheet&rdquo;, a &ldquo;character sheet&rdquo;. The game developer can use it directly in a 2D game (think of a visual novel game), or it can serve as a basis for UV Mapping textures of a simple 3D model with Armature to animate and you already have some emotional faces. Good for placing in the background scenery, pedestrians walking on the street, shopping, sitting in the square or something like that.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cgv0qp3slipgw4nrlgnrserl1n8s" alt="Loras"  loading="lazy" /></p>
<p>In addition, Mick&rsquo;s tutorial describes how it is possible to create <strong>LORAS</strong> (Low-Rank Adaptation). If you want to be able to generate characters with a specific style or even specific faces (without &ldquo;artistic&rdquo; intervention from the A.I.), you choose a model like FLUX or SDXL and &ldquo;on top&rdquo; of it you add a second compatible sub-model with information about that person or character.</p>
<p>For this you take several clean and well-lit photos of the person, from many different angles. You do not even need much, just a dozen is enough. For each image you create prompts that describe it well, and that is it. There are tools to generate these Loras locally or sites that do this (watch the video).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/tk8tbtrr1ealm4c4eu26alk63xlh" alt="Creating Prompts"  loading="lazy" /></p>
<p>Speaking of prompts, in several workflows there are steps to describe detailed prompts of dynamically generated images. Instead of requiring the user (you) to do this manually, you can use a decoding model, which decodes images into text prompts. The most used model for this is Microsoft&rsquo;s <strong>FLORENCE 2</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/dwr87ajtkj7w15ftsy86l5u4hz44" alt="Florence 2"  loading="lazy" /></p>
<p>That text box at the bottom is read-only, you do not need to type it manually. The Florence 2 Run Node will read the image and generate this text. See how it described the first image of the woman in the &ldquo;T&rdquo; pose. Doing tests, for your case, you determine if just this is enough; if it is not, just put a second prompt Node and concatenate the two before sending them to other nodes.</p>
<p>Anyway, there is A LOT to explore just in this workflow. Mick did an excellent job both on the workflow and on the video to explain it, and it is worth paying the 10 dollars to contribute. There are dozens of workflows like this available on the Web, just search. And my setup is getting more and more complex. I plan to keep adjusting it for each new interesting workflow I run into. There are already several pre-tested ones in the &ldquo;workflows&rdquo; folder of my project for you to play with.</p>
]]></content:encoded><category>generative-media</category><category>gaming</category><category>local-models</category></item></channel></rss>