<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arsen Apostolov</title>
    <description>The latest articles on DEV Community by Arsen Apostolov (@sikamikanikobg).</description>
    <link>https://dev.to/sikamikanikobg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1410108%2Fb7d644e9-449a-4ef9-8bcc-73a2ff63902f.jpeg</url>
      <title>DEV Community: Arsen Apostolov</title>
      <link>https://dev.to/sikamikanikobg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sikamikanikobg"/>
    <language>en</language>
    <item>
      <title>Patzer v7.19: the tactic trainer that shows your move and explains why the better one wins</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:06:43 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/patzer-v719-the-tactic-trainer-that-shows-your-move-and-explains-why-the-better-one-wins-50jn</link>
      <guid>https://dev.to/sikamikanikobg/patzer-v719-the-tactic-trainer-that-shows-your-move-and-explains-why-the-better-one-wins-50jn</guid>
      <description>&lt;p&gt;Last week I wrote about Patzer — a self-hosted Chess.com alternative that runs in one Docker container (&lt;a href="https://dev.to/sikamikanikobg/i-built-my-family-a-private-chesscom-one-docker-container-stockfish-plus-your-own-llm-184n"&gt;the original post&lt;/a&gt;). Since then, the feature I've been most proud of shipped: a tactic trainer that explains your own mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap in puzzle trainers
&lt;/h2&gt;

&lt;p&gt;The classic puzzle trainer — Lichess puzzles, Chess.com tactics — gives you a position and asks for the best move. Miss it, and you see the solution. But you never see &lt;em&gt;why&lt;/em&gt; yours was wrong: why the engine's move wins and your move loses. You memorize that Qh5 is the move, not that Qd3 leaves your queen hanging to a fork.&lt;/p&gt;

&lt;p&gt;v7.19.0 closes that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your move, on the board.&lt;/strong&gt; In Train → From your games, Patzer builds puzzles from your actual Chess.com or Lichess games. The move you played in that game is now drawn as a red arrow on the puzzle position, so you no longer have to work out from "you played Qd3" which move that was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The coach explains the difference.&lt;/strong&gt; Once the puzzle is over — solved, or the solution shown, never before — your AI coach explains in plain words why the engine's move beats the one you played. It's the same engine-checked explanation as in Game Review, in your language, pitched at your audience setting.&lt;/p&gt;

&lt;p&gt;The design decision from the original post is what makes this cheap to run: the LLM never analyses. The server works out the facts — what each move allows, hangs or wins, the opponent's best answer — and the model only turns that into coaching. A small local model (Ollama or vLLM) is enough, and a hallucinated piece is impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puzzles come back.&lt;/strong&gt; A puzzle you don't solve at the first try comes back the next day, marked &lt;em&gt;Review&lt;/em&gt; — the same simple daily queue as the opening trainer. Solved at the first try on a later day, it's done; missed again, it's back tomorrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puzzle of the day, with a streak.&lt;/strong&gt; One puzzle from the Lichess set near your puzzle rating, the same one all day, with a plain counter of the days in a row you did it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play from here.&lt;/strong&gt; In Game Review, every move now has a "Play from here against Patzer" button. You play your own colour, pick the bot's strength and the clock, and the game is saved like any bot game — your own games become training positions.&lt;/p&gt;

&lt;p&gt;Full list in the release notes, including a fix for the saved AI model dropdown when the model isn't on the server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/SikamikanikoBG/patzer/releases/tag/v7.19.0" rel="noopener noreferrer"&gt;v7.19.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/SikamikanikoBG/patzer" rel="noopener noreferrer"&gt;github.com/SikamikanikoBG/patzer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still one container:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d -p 8800:8800 -v patzer-data:/app/data --name patzer ghcr.io/SikamikanikoBG/patzer:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;MIT licensed, English-first with Bulgarian, Spanish, German and Russian out of the box. If you're running Patzer, pull the latest tag and try the trainer on your own games — that's the part I'd want you to try first.&lt;/p&gt;

</description>
      <category>chess</category>
      <category>ai</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>54 Containers, 93 Services, 64 GB of RAM: What Actually Eats It</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:11:05 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/54-containers-93-services-64-gb-of-ram-what-actually-eats-it-4fjk</link>
      <guid>https://dev.to/sikamikanikobg/54-containers-93-services-64-gb-of-ram-what-actually-eats-it-4fjk</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Local LLM Lab — Part 11&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One controlled experiment. One number. One verdict.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A home server has a question the cloud never asks: &lt;strong&gt;who is eating the RAM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the cloud, memory is a line item on a bill. At home, it's a finite pool — 64 GB on my box — shared by every container, every service, every daemon, and the one GPU that runs the AI. When the pool runs dry, something gets killed, and the thing that gets killed is usually the thing you least expected.&lt;/p&gt;

&lt;p&gt;I ran a memory audit on my homelab: 54 Docker containers, 93 running systemd units, one RTX 3090, 64 GB of RAM. I pulled the per-service memory history over 7 days, the current per-process state, and the kernel's non-reclaimable share. This is what the 64 GB is actually holding, and what the number says about how much RAM a homelab really needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine:&lt;/strong&gt; HP Z840 Workstation, 2× Intel Xeon E5-2680 v4 (28 cores, 56 threads), 64 GB RAM, one RTX 3090&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workload:&lt;/strong&gt; 54 Docker containers, 93 running systemd units, all up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry:&lt;/strong&gt; 7 days of per-service memory at 2-hour resolution, plus the current per-process state and the kernel's non-reclaimable memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU tenants (for context):&lt;/strong&gt; Whisper ASR, a TTS model, an embedding model — see the VRAM piece for what the card holds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 64 GB is the honest number. It's not a "big" machine by data-centre standards, but it's a real workstation, and it runs a real stack: photo storage, music, a RAG pipeline, an LLM observability stack, a CI runner, a media server, a few self-hosted apps, and the AI services on the GPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The box is using 41.6 GB of its 64 GB — 65% — and the peak over the week was 15.9 GB of &lt;em&gt;application&lt;/em&gt; memory, with the rest held by the kernel and the GPU's system-side buffers.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;RAM (current)&lt;/th&gt;
&lt;th&gt;Share of 64 GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel (non-reclaimable)&lt;/td&gt;
&lt;td&gt;1,754 MB (~1.7 GB)&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU tenants (system-side buffers + CUDA context)&lt;/td&gt;
&lt;td&gt;~2.0 GB&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top application (Whisper ASR webservice)&lt;/td&gt;
&lt;td&gt;7,870 MB (~7.7 GB)&lt;/td&gt;
&lt;td&gt;12.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTS voice model (omnivoice)&lt;/td&gt;
&lt;td&gt;5,100 MB (~5.0 GB)&lt;/td&gt;
&lt;td&gt;7.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM observability stack (web + worker + ClickHouse)&lt;/td&gt;
&lt;td&gt;~2.3 GB&lt;/td&gt;
&lt;td&gt;3.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photo storage server (Immich: server + postgres + ML)&lt;/td&gt;
&lt;td&gt;~1.0 GB&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG pipeline (Qdrant + Ollama + Chroma)&lt;/td&gt;
&lt;td&gt;~0.7 GB&lt;/td&gt;
&lt;td&gt;1.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The other 49 containers + 93 services&lt;/td&gt;
&lt;td&gt;~22.7 GB&lt;/td&gt;
&lt;td&gt;35.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total in use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;41,622 MB (~40.7 GB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "other 49 containers + 93 services" line is the one that surprises people. It's 35% of the RAM, and it's not one thing — it's a long tail. The biggest individual entries in that tail:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;RAM (current)&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ClickHouse (Langfuse storage)&lt;/td&gt;
&lt;td&gt;1,082 MB&lt;/td&gt;
&lt;td&gt;The tracing database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open WebUI (chat UI)&lt;/td&gt;
&lt;td&gt;777 MB&lt;/td&gt;
&lt;td&gt;The chat interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse web (LLM observability)&lt;/td&gt;
&lt;td&gt;721 MB&lt;/td&gt;
&lt;td&gt;The tracing UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immich server (photo storage)&lt;/td&gt;
&lt;td&gt;562 MB&lt;/td&gt;
&lt;td&gt;The photo library backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immich postgres (photo metadata)&lt;/td&gt;
&lt;td&gt;536 MB&lt;/td&gt;
&lt;td&gt;The photo database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant (vector DB)&lt;/td&gt;
&lt;td&gt;483 MB&lt;/td&gt;
&lt;td&gt;The RAG index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse worker (trace processing)&lt;/td&gt;
&lt;td&gt;346 MB&lt;/td&gt;
&lt;td&gt;The trace worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Botpress (chatbot)&lt;/td&gt;
&lt;td&gt;292 MB&lt;/td&gt;
&lt;td&gt;The chatbot framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n (workflow automation)&lt;/td&gt;
&lt;td&gt;279 MB&lt;/td&gt;
&lt;td&gt;The automation engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flowise (LLM builder)&lt;/td&gt;
&lt;td&gt;255 MB&lt;/td&gt;
&lt;td&gt;The LLM workflow builder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;homelab-monitor (this monitor)&lt;/td&gt;
&lt;td&gt;233 MB&lt;/td&gt;
&lt;td&gt;The dashboard itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jarvis2-core (my assistant)&lt;/td&gt;
&lt;td&gt;229 MB&lt;/td&gt;
&lt;td&gt;The assistant core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jarvis-server&lt;/td&gt;
&lt;td&gt;213 MB&lt;/td&gt;
&lt;td&gt;The server host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jarvis2-voiceline (voice pipeline)&lt;/td&gt;
&lt;td&gt;199 MB&lt;/td&gt;
&lt;td&gt;The voice line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immich machine-learning (face/photo ML)&lt;/td&gt;
&lt;td&gt;177 MB&lt;/td&gt;
&lt;td&gt;The photo ML worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse minio (trace object storage)&lt;/td&gt;
&lt;td&gt;164 MB&lt;/td&gt;
&lt;td&gt;The trace storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;searxng (meta-search)&lt;/td&gt;
&lt;td&gt;111 MB&lt;/td&gt;
&lt;td&gt;The search engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UptimeKuma (health checks)&lt;/td&gt;
&lt;td&gt;103 MB&lt;/td&gt;
&lt;td&gt;The uptime monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL (8.0, two instances)&lt;/td&gt;
&lt;td&gt;34 MB + 164 MB&lt;/td&gt;
&lt;td&gt;The databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub runner (CI)&lt;/td&gt;
&lt;td&gt;58 MB&lt;/td&gt;
&lt;td&gt;The CI agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres (three instances)&lt;/td&gt;
&lt;td&gt;15 MB + 20 MB + 536 MB&lt;/td&gt;
&lt;td&gt;The databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: &lt;strong&gt;no single service is the hog. The RAM is eaten by a long tail of small, resident services, each holding 10–1,000 MB, and the sum is 22.7 GB.&lt;/strong&gt; The GPU tenants hold the most per service (7.7 GB and 5.0 GB), but they're two services. The long tail is forty-nine, and it's still a third of the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the peak says
&lt;/h2&gt;

&lt;p&gt;The 7-day peak of 15.9 GB of &lt;em&gt;application&lt;/em&gt; memory is the number that matters for sizing. It happened when the photo server was running a batch of thumbnail generation at the same time the CI runner was building a project and the observability stack was ingesting a burst of traces. Three things spiking at once, on a 64 GB box, and the peak was 15.9 GB of application memory — 25% of the pool.&lt;/p&gt;

&lt;p&gt;The headroom at peak was 48 GB. That's the insurance. The box has never come close to the ceiling, and the events log over the week shows &lt;strong&gt;zero OOM kills, zero memory threshold crossings&lt;/strong&gt; — the only alerts were eight disk I/O spikes, which are storage, not memory.&lt;/p&gt;

&lt;p&gt;The honest reading: &lt;strong&gt;64 GB is comfortable for this stack. 32 GB would be tight but workable. 16 GB would not.&lt;/strong&gt; The peak of 15.9 GB is the worst case that actually happened, and it left 48 GB on the table. If I were buying RAM for this box today, I'd buy 64 GB and stop — not because it's the minimum, but because it's the size where the peak is a non-event.&lt;/p&gt;




&lt;h2&gt;
  
  
  The counterargument: RAM is cheap, don't sweat it
&lt;/h2&gt;

&lt;p&gt;The fair objection: RAM is the cheapest part of a server. 64 GB is a few hundred euros. Why spend an article on it?&lt;/p&gt;

&lt;p&gt;Because the RAM question is where homelab sizing goes wrong. People size the GPU (the expensive, scarce part) and under-size the RAM (the cheap, abundant part), and then the box OOMs the one service that matters — usually the database or the photo server — at the worst moment. The GPU is the part everyone benchmarks. The RAM is the part nobody measures, and it's the part that fails.&lt;/p&gt;

&lt;p&gt;The measurement says the failure mode is not "one big service." It's &lt;strong&gt;the long tail.&lt;/strong&gt; Fifty small services, each holding 10–100 MB, add up to a third of the box. The fix is not "buy more RAM" (though that helps). The fix is knowing the tail is real, so when the box gets tight you know which of the fifty to trim, not which of the two to kill.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would do now
&lt;/h2&gt;

&lt;p&gt;If I were building this box today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAM: 64 GB.&lt;/strong&gt; The measured peak is 15.9 GB of application memory; 64 GB is 4× peak. 32 GB would be 2× peak — workable, but the photo server and the CI runner would start fighting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule:&lt;/strong&gt; size RAM for the &lt;em&gt;peak of the long tail&lt;/em&gt;, not for the biggest single service. The biggest service is 7.7 GB. The tail is 22.7 GB. The tail is the size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The audit:&lt;/strong&gt; a per-service memory panel, current and 7-day peak, with the kernel share separated out. The number that matters is not "the box is at 65%" — it's "the peak was 15.9 GB, and here are the ten services that were up at the peak."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The homelab monitor I use has exactly that panel. The 7-day peak per service, the current per-process state, and the kernel's non-reclaimable share, in one view. It's the difference between "the box is full" and "here's what's full, and here's what to trim."&lt;/p&gt;




&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;54 containers, 93 services, 64 GB of RAM: 41.6 GB in use (65%), 15.9 GB peak over the week, zero OOM kills.&lt;/strong&gt; The RAM is not eaten by one big service — it's eaten by a long tail of fifty small, resident services that add up to 22.7 GB. The GPU tenants hold the most per service, but the tail holds a third of the box by itself. Size the RAM for the peak of the tail, not for the biggest single service, and the box stops being a guessing game.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Local LLM Lab is a series of measured experiments on a home GPU stack. Every number in this article was pulled from 7 days of per-service memory telemetry on the box, not estimated. Next: the one failed service on a 93-service stack — and how the uptime checks keep the rest alive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>docker</category>
      <category>linux</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Your homelab, legible to AI agents — a read-only MCP server that rides along with your monitor</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:43:30 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/your-homelab-legible-to-ai-agents-a-read-only-mcp-server-that-rides-along-with-your-monitor-40io</link>
      <guid>https://dev.to/sikamikanikobg/your-homelab-legible-to-ai-agents-a-read-only-mcp-server-that-rides-along-with-your-monitor-40io</guid>
      <description>&lt;p&gt;i keep a monitoring container on my homelab — it reads nvidia-smi, the docker api, systemd and /proc, and turns it into one page. the boring kind of dashboard: what's using the gpu, what it costs, what's filling the disk.&lt;/p&gt;

&lt;p&gt;last month i started running agents on the box, and i hit a wall that i think most people will hit: the agent couldn't see my homelab. it could run commands, but it couldn't &lt;em&gt;see&lt;/em&gt; — which model is holding the 3090, why the temperature is climbing, what the month cost in energy. i'd have to paste screen output into the chat by hand, and the agent was guessing about the rest.&lt;/p&gt;

&lt;p&gt;the data was already there. the monitor was reading all of it, every few seconds, into a sqlite database. what was missing was just a standard way to hand it over.&lt;/p&gt;

&lt;p&gt;so i added a read-only MCP server to the same container. it comes up on port 9810, and the whole setup is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http homelab http://YOUR-HUB:9810/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nineteen named tools, all read-only: hosts, containers, services, per-card gpu state and which process is on which card, per-process ram, the model servers, installed models, energy cost, benchmark runs, disk treemaps, history, alerts. that's the same coverage the dashboard has — the server is just the same database with an MCP face on it.&lt;/p&gt;

&lt;p&gt;what this actually gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"which model is eating vram right now, and on which card?" — it reads the per-card split, not one pooled number. on my 3x3090 box a 63gb model shows as 22.5 / 22.1 / 18.8.&lt;/li&gt;
&lt;li&gt;"what did the last week of inference cost me?" — it has the power integration, so it's a measured number, not a guess.&lt;/li&gt;
&lt;li&gt;"is anything down?" — the uptime checks and alerts are tools too.&lt;/li&gt;
&lt;li&gt;"what's filling the disk on the pi?" — the treemap, on demand, over the same ssh connection everything else uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;a few things i'm deliberately not doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no writes. both directions are question and answer. the agent can look around the lab, it can't restart anything. (the dashboard itself has controls, but the mcp surface is read-only on purpose — an agent that can see your lab is already trusted, and i didn't want to make it &lt;em&gt;responsible&lt;/em&gt;.)&lt;/li&gt;
&lt;li&gt;no extra install on the remote boxes. the monitor already polls them over ssh with nothing persisted, and the mcp server just reads what the monitor has.&lt;/li&gt;
&lt;li&gt;no cloud. the server is on your lan, same as the dashboard. point a local ollama agent at it and nothing leaves the house.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the monitor is open source, mit, one container: &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;github.com/SikamikanikoBG/homelab-monitor&lt;/a&gt; — and the mcp part is on by default (enable_mcp=0 turns it off). if you run a homelab and you're pointing agents at it, i'd be curious what you'd ask it first.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>selfhosted</category>
      <category>homelab</category>
    </item>
    <item>
      <title>My GPU Was Busy. I Finally Found Out Who Was Using It.</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:10:05 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/my-gpu-was-busy-i-finally-found-out-who-was-using-it-1pkg</link>
      <guid>https://dev.to/sikamikanikobg/my-gpu-was-busy-i-finally-found-out-who-was-using-it-1pkg</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Local LLM Lab — Part 10&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One controlled experiment. One number. One verdict.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every homelab has a moment like this: the GPU is at 100%, the fan is screaming, and you have no idea which of the twenty services on the box is doing it. You open &lt;code&gt;nvidia-smi&lt;/code&gt;, it shows one process, you close it, and the mystery continues.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;code&gt;nvidia-smi&lt;/code&gt; answers "which &lt;em&gt;process&lt;/em&gt; is using the GPU" — and in a Docker stack, the process is the container runtime, not the service. The answer you actually want is "which &lt;em&gt;service&lt;/em&gt; is using the GPU, and how much time did it take, and who &lt;em&gt;called&lt;/em&gt; it."&lt;/p&gt;

&lt;p&gt;I built the answer into my homelab monitor: caller→server attribution. Every sample of GPU utilisation is tagged with the service that owns the process, and for shared services (a transcription endpoint, an embedding endpoint) it's tagged with the &lt;em&gt;caller&lt;/em&gt; — the thing that made the request. Over the last 7 days the attribution logged 1,055 samples. This is what it says about who eats a home GPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card:&lt;/strong&gt; NVIDIA RTX 3090, 24 GB, power limit 390 W&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared services on the card:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whisper ASR webservice&lt;/strong&gt; — a transcription endpoint with an nginx proxy in front (my tools, the observability stack, CI, and a warmup job all hit the proxy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nomic-embed-text (Ollama)&lt;/strong&gt; — an embedding endpoint used by the RAG pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution:&lt;/strong&gt; every GPU sample over 7 days is tagged with (a) the service owning the process and (b) the caller for shared endpoints. Total: 1,055 samples, ~1.1 million seconds of GPU time attributed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design decision: &lt;strong&gt;the caller is recorded from the request, not from the process.&lt;/strong&gt; When my assistant core sends an audio file to the whisper proxy, the sample is tagged &lt;code&gt;jarvis2-core → whisper-proxy&lt;/code&gt;, not &lt;code&gt;nginx → whisper&lt;/code&gt;. That's the difference between "the GPU is busy" and "my assistant is the one using it."&lt;/p&gt;




&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over 7 days, 47% of the GPU's logged time went to one caller: my own assistant core (4,970 seconds of 10,555 total attributed seconds).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caller&lt;/th&gt;
&lt;th&gt;Seconds (7d)&lt;/th&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;% of total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jarvis2-core (my assistant)&lt;/td&gt;
&lt;td&gt;4,970&lt;/td&gt;
&lt;td&gt;whisper-proxy&lt;/td&gt;
&lt;td&gt;47.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;whisper-proxy (shared endpoint, direct)&lt;/td&gt;
&lt;td&gt;920&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;8.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;langfuse worker (observability)&lt;/td&gt;
&lt;td&gt;910&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;8.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;langfuse minio (object storage)&lt;/td&gt;
&lt;td&gt;860&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;8.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;whisper-warmup (keep-alive job)&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gh-runner (CI)&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;whisper-proxy&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jarvis-server + watchtower&lt;/td&gt;
&lt;td&gt;410&lt;/td&gt;
&lt;td&gt;whisper-proxy / service&lt;/td&gt;
&lt;td&gt;3.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;firefly importer&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ollama-embed callers (CI, assistant, watchtower)&lt;/td&gt;
&lt;td&gt;210&lt;/td&gt;
&lt;td&gt;ollama-embed&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UptimeKuma (health checks)&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;whisper service&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The single biggest consumer of the GPU is not a model, not a benchmark, not a training run. It's &lt;strong&gt;my assistant transcribing the audio it records.&lt;/strong&gt; Voice memos, meeting recordings, the audio attached to the traces the observability stack ingests — all of it flows through the Whisper endpoint, and all of it is attributed to the caller that sent it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the attribution changes
&lt;/h2&gt;

&lt;p&gt;Without caller attribution, the 7-day story of this GPU is: "the Whisper service used 10,555 seconds of GPU time." That's a number. It doesn't tell you anything you can act on.&lt;/p&gt;

&lt;p&gt;With caller attribution, the story is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My assistant is the tenant.&lt;/strong&gt; 47% of the GPU's time is my own tooling. The service exists &lt;em&gt;for&lt;/em&gt; it, and it uses it more than everything else combined. That's the design working as intended — and it's also the reason the service is resident 24/7 instead of on-demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The observability stack is the second tenant.&lt;/strong&gt; Langfuse's worker and MinIO together take 17% of the GPU's time. They transcribe the audio attached to the LLM traces they store. That's a cost nobody budgets for: the tracing stack is &lt;em&gt;using&lt;/em&gt; the GPU to do its job, on top of the LLM it's tracing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI is a real, small tenant.&lt;/strong&gt; The GitHub runner takes 3.4% — test fixtures that get transcribed during CI runs. It's small, but it's &lt;em&gt;unexpected&lt;/em&gt; small. Nobody thinks about CI touching the GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The warmup job is 3.4%.&lt;/strong&gt; A keep-alive job that pings the endpoint every few minutes so the model stays warm. That's the "keep it warm" cost made visible — 3.4% of the GPU's week, doing nothing but staying ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The verdict from the numbers: &lt;strong&gt;the GPU is not "busy" in the way the alarm implies. It is busy &lt;em&gt;for specific, named reasons&lt;/em&gt;, and the reasons add up to a stack that is working, not failing.&lt;/strong&gt; The alarm that says "GPU at 100%" is a symptom. The attribution is the diagnosis.&lt;/p&gt;




&lt;h2&gt;
  
  
  The counterargument: attribution is overhead
&lt;/h2&gt;

&lt;p&gt;The fair objection: tagging every GPU sample with a caller is extra bookkeeping, and in a hot loop it's overhead. The attribution has to be cheap enough that it doesn't slow the inference it's measuring.&lt;/p&gt;

&lt;p&gt;The implementation I use is cheap by design: the tag is set at the &lt;em&gt;request&lt;/em&gt; boundary, not at the sample boundary. When a request comes in, the caller is recorded once and attached to every sample the request produces. The per-sample cost is a field read, not a lookup. Over 1,055 samples the overhead is below the resolution of the power meter — you cannot measure it against a 2-hour bucket.&lt;/p&gt;

&lt;p&gt;The other objection: attribution only works for services that go through a request boundary. A model that's loaded and used in-process (a training run, a direct script) has no caller to tag. For those, the service-level tag is the best you can do, and that's still better than nothing. The attribution is a spectrum: caller-level for request-driven services, service-level for the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would do now
&lt;/h2&gt;

&lt;p&gt;If I were building a homelab monitor today, the GPU panel would have three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The card level:&lt;/strong&gt; utilisation, VRAM, power, temperature. The thing &lt;code&gt;nvidia-smi&lt;/code&gt; shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The service level:&lt;/strong&gt; which container owns the process, and how much VRAM and time it holds. The thing Docker's stats show, but per-GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The caller level:&lt;/strong&gt; for shared endpoints, which client made the request that produced each sample. The thing nothing shows, and the thing that answers "who is using my GPU."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third layer is the one that turns a dashboard into a tool. When the GPU spikes, you don't open &lt;code&gt;nvidia-smi&lt;/code&gt; and guess. You look at the attribution and see the name. When a service misbehaves (a runaway job, a retry storm), the caller list shows it in the same view. The number that matters is not "the GPU is at 100%" — it's "jarvis2-core took 4,970 seconds this week, and here's the request that did it."&lt;/p&gt;




&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One week of caller-level GPU attribution: 47% of the card's time went to my own assistant, 17% to the observability stack, 3.4% to CI, 3.4% to a warmup job that does nothing but stay ready.&lt;/strong&gt; The GPU was never "mysteriously busy." It was busy for named reasons, and the attribution is the only view that shows them. If your homelab GPU spikes and you don't know why, the fix is not a faster card — it's a caller tag on every sample.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Local LLM Lab is a series of measured experiments on a home GPU stack. Every number in this article was pulled from 7 days of caller-level attribution on the card, not estimated. Next: the 55 containers and 92 services that share this box with the GPU — what actually eats RAM when the machine has 64 GB of it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>homelab</category>
      <category>ai</category>
      <category>gpu</category>
    </item>
    <item>
      <title>I built my family a private Chess.com — one Docker container, Stockfish plus your own LLM</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:55:29 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/i-built-my-family-a-private-chesscom-one-docker-container-stockfish-plus-your-own-llm-184n</link>
      <guid>https://dev.to/sikamikanikobg/i-built-my-family-a-private-chesscom-one-docker-container-stockfish-plus-your-own-llm-184n</guid>
      <description>&lt;p&gt;My kids play chess on Chess.com. It is a great product, but the account, the cloud and the ads are not something I want to hand to a nine-year-old. So I built Patzer: a self-hosted, open-source alternative that runs on a Pi, a NAS or an old laptop.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Game Review&lt;/strong&gt; — pull your public Chess.com or Lichess games, or paste a PGN. Bundled Stockfish classifies every move (Brilliant / Great / Inaccuracy / Mistake / Blunder), gives accuracy, estimated Elo and an eval graph, plus a "what's the threat?" probe that answers in plain words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play vs Bot&lt;/strong&gt; — Stockfish at seven named tiers from Kid to Stockfish max, all standard time controls, with kid-mode blunder warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play vs Friend&lt;/strong&gt; — real-time PvP between profiles on the same server over WebSocket: draw offers, takebacks, one-click rematch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Coach&lt;/strong&gt; — point it at your own Ollama or vLLM host. The coach explains &lt;em&gt;why&lt;/em&gt; a move works or fails, shows the better move, and connects the mistake to your own recent games ("4 of your last 10 games: leaving a piece unprotected"). It never contradicts the engine: an answer that praises a blunder is caught before you see it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family-ready&lt;/strong&gt; — multi-user with an admin console, per-profile language, kid mode, "horsey" piece names for the youngest profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design choice I keep coming back to: the LLM never analyses. The server works out the facts — what a move allows, hangs or wins, the opponent's best answer, your recurring mistakes — and the model only turns that into coaching. So a small local model is enough, and a hallucinated piece is impossible.&lt;/p&gt;

&lt;p&gt;One container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8800:8800 &lt;span class="nt"&gt;-v&lt;/span&gt; patzer-data:/app/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; patzer ghcr.io/SikamikanikoBG/patzer:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs on your LAN. Games, profiles and settings never leave home. MIT licensed, English-first with Bulgarian, Spanish, German and Russian out of the box.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/SikamikanikoBG/patzer" rel="noopener noreferrer"&gt;https://github.com/SikamikanikoBG/patzer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>chess</category>
      <category>ai</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Put Three AI Services on One 24 GB GPU. It Used 17.8. Here's What Fits.</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:49:09 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/i-put-three-ai-services-on-one-24-gb-gpu-it-used-178-heres-what-fits-6ne</link>
      <guid>https://dev.to/sikamikanikobg/i-put-three-ai-services-on-one-24-gb-gpu-it-used-178-heres-what-fits-6ne</guid>
      <description>&lt;h1&gt;
  
  
  I Put Three AI Services on One 24 GB GPU. It Used 17.8. Here's What Fits.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The Local LLM Lab — Part 9&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One controlled experiment. One number. One verdict.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "how much VRAM do I need" advice on the internet comes in one flavour: a spec sheet. "27B in FP8 needs 28 GB. 70B in Q4 needs 42 GB. Buy a 48 GB card."&lt;/p&gt;

&lt;p&gt;That's the answer for one model. Nobody tells you what happens when you try to run your actual stack — a transcription service, an embedding model, a small chat model — on one card. The answers you find are guesses, because nobody has measured a &lt;em&gt;multi-tenant&lt;/em&gt; GPU.&lt;/p&gt;

&lt;p&gt;I have. For the last week my RTX 3090 (24 GB) has been running three AI services at once: a Whisper ASR webservice, a small TTS voice model, and an embedding model for a RAG pipeline. I pulled the full VRAM history and the per-tenant attribution. This is what the card is actually holding, and what the number says about sizing a home GPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Card:&lt;/strong&gt; NVIDIA RTX 3090, 24 GB (24,576 MB), power limit 390 W&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenants (all resident, all Docker):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whisper ASR webservice&lt;/strong&gt; (WhisperX, large-v3) — a transcription endpoint my own tools call for voice memos, meetings, podcast clips&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small TTS voice model&lt;/strong&gt; — text-to-speech for the same assistant stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nomic-embed-text (Ollama)&lt;/strong&gt; — embeddings for the RAG pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry:&lt;/strong&gt; 7 days of VRAM / power / utilisation at 2-hour resolution, plus per-caller attribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else on the box:&lt;/strong&gt; 55 containers, 92 systemd units, a 56-thread Xeon, 64 GB of RAM. The GPU sees only the three tenants above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The three services were chosen because that is the actual shape of a home AI stack: one big-ish resident model (ASR), one medium resident model (TTS), one tiny resident model (embeddings). No chat model. The 27B chat model lives on a second machine with two 3090s — that's a different article.&lt;/p&gt;




&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The card is holding 17.8 GB of its 24 GB — 72% — with three models resident, and the peak over the week was 23.4 GB. It never spilled a byte to system RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tenant&lt;/th&gt;
&lt;th&gt;Avg VRAM (7d)&lt;/th&gt;
&lt;th&gt;Peak VRAM (7d)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whisper ASR (large-v3)&lt;/td&gt;
&lt;td&gt;7,871 MB (~7.7 GB)&lt;/td&gt;
&lt;td&gt;10,558 MB (~10.3 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTS voice model&lt;/td&gt;
&lt;td&gt;4,876 MB (~4.7 GB)&lt;/td&gt;
&lt;td&gt;4,964 MB (~4.8 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nomic-embed-text&lt;/td&gt;
&lt;td&gt;4,633 MB (~4.5 GB)&lt;/td&gt;
&lt;td&gt;4,668 MB (~4.6 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total (measured, 7d)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~17,380 MB (~17.0 GB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23,379 MB (~22.8 GB)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The measured current state at the time of writing: 17,775 MB used of 24,576 MB — 72% of the card, with the three tenants sitting at 7.87 GB + 4.96 GB + 4.67 GB = 17.5 GB, plus the CUDA context overhead of the card itself.&lt;/p&gt;

&lt;p&gt;The peak of 23.4 GB is the interesting number. It happened when the Whisper service received a batch of long audio files at the same time the TTS model was rendering a long response. The two big tenants spiked together, the embedding model held its 4.6 GB, and the card came within 1.2 GB of the ceiling. It held. No spill, no OOM, no crash — because the peak was a few seconds, not a sustained load.&lt;/p&gt;




&lt;h2&gt;
  
  
  What each tenant is actually holding
&lt;/h2&gt;

&lt;p&gt;The "paper VRAM" of a model is its weights. The measured VRAM is weights plus working set plus framework overhead. Here's the gap, per tenant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whisper large-v3.&lt;/strong&gt; The weights in FP16 are ~3.1 GB. The measured average is 7.7 GB — 2.5× the weights. The difference is the encoder's audio feature buffers (long files and queued files hold more), the VAD and alignment models WhisperX runs alongside, and the CUDA context. The peak of 10.3 GB is a batch of long files in flight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TTS voice model.&lt;/strong&gt; Weights ~4.5 GB, measured 4.7–4.8 GB. This one is close to its paper number because TTS is stateless between requests: no long context, no queue buffers. It's the most "honest" tenant on the card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nomic-embed-text.&lt;/strong&gt; A 137M-parameter embedding model. Paper weights ~0.3 GB. Measured 4.5–4.7 GB. That's the Ollama runtime, its CUDA context, and the KV space it pre-allocates for embedding batches. A model that "needs 300 MB" needs 4.6 GB when it's a resident service. That's the number people never quote.&lt;/p&gt;

&lt;p&gt;The pattern: &lt;strong&gt;the measured VRAM of a resident service is 1.2–2.5× the weight size, and the multiplier is biggest for the models with the most state.&lt;/strong&gt; If you're sizing a card, budget for the measured number, not the weights.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who is calling it
&lt;/h2&gt;

&lt;p&gt;The 7-day attribution shows why the Whisper tenant is the biggest consumer of &lt;em&gt;time&lt;/em&gt; on the card, even though the TTS model is the bigger &lt;em&gt;spike&lt;/em&gt; contributor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caller&lt;/th&gt;
&lt;th&gt;Seconds on the card (7d)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;My own assistant core (via the whisper proxy)&lt;/td&gt;
&lt;td&gt;4,970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whisper proxy (shared endpoint)&lt;/td&gt;
&lt;td&gt;920&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse worker (observability stack, transcribes attached audio)&lt;/td&gt;
&lt;td&gt;910&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse MinIO (object storage, transcribes on ingest)&lt;/td&gt;
&lt;td&gt;860&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whisper warmup (keep-alive job)&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub runner (CI jobs that transcribe test fixtures)&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The card is a shared service, not a single app. Six different callers hit the Whisper endpoint over the week. The TTS and embedding tenants have one caller each (the assistant stack). The attribution is what turns "the GPU is busy" into "here is &lt;em&gt;who&lt;/em&gt; is busy" — and it's the difference between a dashboard you glance at and one you can act on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The counterargument: 72% is a lot
&lt;/h2&gt;

&lt;p&gt;The fair objection: a card at 72% of its VRAM with a 1.2 GB headroom at peak is a card that will fail the day a bigger file comes in. Headroom is insurance, and 12% is thin insurance.&lt;/p&gt;

&lt;p&gt;True. But the measurement says what the insurance was worth over a week of real use: &lt;strong&gt;the peak was a 3-second event, and the card absorbed it.&lt;/strong&gt; The sustained state is ~17 GB, which leaves 7.5 GB of headroom — 31% of the card. The 23.4 GB peak is the worst case that actually happened, not the worst case that is possible.&lt;/p&gt;

&lt;p&gt;The honest version of the headroom argument: &lt;strong&gt;size for the biggest &lt;em&gt;sustained&lt;/em&gt; working set you will actually run, plus 50% headroom.&lt;/strong&gt; Not for the biggest spike you can imagine. If your stack's sustained working set is 17 GB, a 24 GB card is 1.4× sustained — enough. If you add a 7B chat model to the same card (sustained ~8 GB), the sustained working set becomes 25 GB and the 24 GB card is done. That's the actual failure mode: not a spike, but a new tenant.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would buy now
&lt;/h2&gt;

&lt;p&gt;If I were building this stack today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The hub card (ASR + TTS + embeddings): 24 GB.&lt;/strong&gt; Sustained working set 17 GB, peak 23.4 GB. A 16 GB card is not enough — the TTS model alone would crowd the Whisper service. A 32 GB card would be comfortable but costs more and runs hotter for a workload that fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chat card(s): sized for the chat model.&lt;/strong&gt; The 27B in FP8 needs two 24 GB cards (or one 48 GB card). It does not share the hub card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule:&lt;/strong&gt; one card per class of workload — resident services on one, the big chat model on another. The measured working set of each class, plus 50% headroom, is the size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 24 GB 3090 is still the best price-per-GB on the used market, and that's why it's everywhere in home stacks. But "best price-per-GB" is an argument for the chat machine. For the hub, the argument is "fits the measured working set of my resident services with room for one more tenant" — and for my stack, that's 24 GB, not 16 and not 48.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Three AI services on one 24 GB GPU: 17.8 GB sustained, 23.4 GB peak, zero spills over 7 days.&lt;/strong&gt; The measured VRAM of a resident service is 1.2–2.5× its weights, and the peak is a few seconds, not a sustained load. Size the card for the sustained working set plus 50% headroom, and keep the big chat model on its own card. The spec sheet tells you what one model needs. The meter tells you what your stack needs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The Local LLM Lab is a series of measured experiments on a home GPU stack. Every number in this article was pulled from 7 days of VRAM telemetry on the card, not estimated. Next: who is actually eating the GPU — the caller-level attribution that turns "the GPU is busy" into a list of names.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Qwen3.8-Flash-Next (125B) on Three RTX 3090s at 80 Tokens/s: Teaching llama.cpp Which Experts Matter</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:02:31 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/i-ran-a-125b-model-on-three-rtx-3090s-at-80-tokenss-by-teaching-llamacpp-which-experts-matter-4ioi</link>
      <guid>https://dev.to/sikamikanikobg/i-ran-a-125b-model-on-three-rtx-3090s-at-80-tokenss-by-teaching-llamacpp-which-experts-matter-4ioi</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Qwen3.8-Flash-Next is a 125B mixture-of-experts model: on paper it beats the Qwen3.8-27B I run in production everywhere, by +16.5 points on agentic coding. It does not fit in the 72 GB of VRAM my three RTX 3090s have, and stock llama.cpp, spilling a quarter of the experts into system RAM, runs it at &lt;strong&gt;23 tokens/second&lt;/strong&gt;. Three changes get it to &lt;strong&gt;80 tokens/second, entirely in VRAM&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sort the experts by how often they're used.&lt;/strong&gt; They are wildly unequal: the busiest 25% handle 52% of the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store them at three precisions.&lt;/strong&gt; Popular experts keep more bits and rare ones get squeezed harder, so the whole model fits on the GPUs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch llama.cpp&lt;/strong&gt; (~400 lines) so one routing decision drives three expert tables at once, then add speculative decoding on top.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quality stays close to the 8-bit reference: &lt;strong&gt;+1.9% perplexity, 91% same next token, GSM8K 95.5%&lt;/strong&gt; (the 27B scores 95–96.5% on the same harness). There's also a 256k-context profile that finds a random code hidden 173,000 tokens deep. The trade-offs are real, and I've listed them. Everything is open: &lt;a href="https://github.com/SikamikanikoBG/qwen38-flash-next-3x3090" rel="noopener noreferrer"&gt;patches, tools, raw measurements&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, 26 Sep 2026, after the first day behind my assistant:&lt;/strong&gt; the 80 tok/s holds for short prompts. In real agent use, with ~25k-token prompts, sampling and thinking on, the model decodes at &lt;strong&gt;30–45 tok/s&lt;/strong&gt;. My first deployment also had a caching mistake that cost ~45 s before every answer; a server config change fixed it (now &lt;strong&gt;0.7 s&lt;/strong&gt;). Details in the "real-world update" section below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  the use case
&lt;/h2&gt;

&lt;p&gt;My box, &lt;strong&gt;vader&lt;/strong&gt;, runs &lt;a href="https://dev.to/sikamikanikobg/qwen38-27b-on-2x-rtx-3090-my-first-local-model-i-actually-trust-fpi"&gt;Qwen3.8-27B&lt;/a&gt; around the clock: two RTX 3090s, vLLM, about 135 tokens/second. It's the first local model I actually trust with agent work, and it does most of the work my assistant does.&lt;/p&gt;

&lt;p&gt;Then Qwen shipped &lt;strong&gt;Qwen3.8-Flash-Next&lt;/strong&gt;, the preview of their Qwen4 architecture. Their own model card puts it ahead of my 27B on almost everything I care about:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;benchmark (Qwen's card)&lt;/th&gt;
&lt;th&gt;Flash-Next&lt;/th&gt;
&lt;th&gt;3.8-27B&lt;/th&gt;
&lt;th&gt;gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE (agentic coding)&lt;/td&gt;
&lt;td&gt;58.7&lt;/td&gt;
&lt;td&gt;42.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+16.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JobBench (professional tasks)&lt;/td&gt;
&lt;td&gt;55.7&lt;/td&gt;
&lt;td&gt;33.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+22.3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWE-bench Multilingual&lt;/td&gt;
&lt;td&gt;81.0&lt;/td&gt;
&lt;td&gt;73.8&lt;/td&gt;
&lt;td&gt;+7.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toolathlon (tool use)&lt;/td&gt;
&lt;td&gt;73.5&lt;/td&gt;
&lt;td&gt;67.1&lt;/td&gt;
&lt;td&gt;+6.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLE&lt;/td&gt;
&lt;td&gt;35.9&lt;/td&gt;
&lt;td&gt;30.8&lt;/td&gt;
&lt;td&gt;+5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPQA Diamond&lt;/td&gt;
&lt;td&gt;91.7&lt;/td&gt;
&lt;td&gt;89.2&lt;/td&gt;
&lt;td&gt;+2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFBench&lt;/td&gt;
&lt;td&gt;81.3&lt;/td&gt;
&lt;td&gt;79.5&lt;/td&gt;
&lt;td&gt;+1.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the goal was simple to state: &lt;strong&gt;the smarter model, on the same three cards, with decode and time-to-first-token that still feel interactive.&lt;/strong&gt; No new hardware. I told my AI engineer (Claude Code, more on that at the end) that "no" was not an acceptable answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPUs:&lt;/strong&gt; 3x RTX 3090, 24 GB each: 72 GB of VRAM. No NVLink, and no PCIe peer-to-peer (the board has no Resizable BAR), so the cards can't talk to each other directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU:&lt;/strong&gt; dual Xeon E5-2660 v4, 56 threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM:&lt;/strong&gt; 60 GB. Only &lt;strong&gt;one&lt;/strong&gt; 32 GB stick per CPU socket, so it's single-channel, and that matters a lot below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk:&lt;/strong&gt; one 1 TB NVMe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  a small dictionary before we start
&lt;/h2&gt;

&lt;p&gt;If you know what a KV cache is, skip this. If you don't, it's all you need for the rest of the article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameters / weights.&lt;/strong&gt; The numbers a model learned. "125B" means 125 billion of them. Each one normally takes 2 bytes, so 125B parameters is ~250 GB, far more than 72 GB of VRAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VRAM.&lt;/strong&gt; The GPU's own memory. It's about 20–50x faster than system RAM &lt;em&gt;on this box&lt;/em&gt;. A model runs fast only if what it needs for each word lives in VRAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantization.&lt;/strong&gt; Storing each weight with fewer bits (8, 4, 3…) instead of 16. Like saving a photo as a smaller JPEG: smaller file, a little less detail. "Q8_0", "Q4_K", "IQ3_S", "MXFP4" are llama.cpp's names for different bit-budgets and recipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixture of experts (MoE).&lt;/strong&gt; Instead of one giant network, each layer has many small "expert" networks, 512 of them here, and a &lt;strong&gt;router&lt;/strong&gt; picks the best 10 for each word. So the model &lt;em&gt;knows&lt;/em&gt; 125B parameters' worth of things but only &lt;em&gt;computes&lt;/em&gt; with about 6B per word. That's why it can be fast, and also why it's huge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token.&lt;/strong&gt; A word or word-piece. &lt;strong&gt;Decode speed&lt;/strong&gt; (tokens/second) is how fast the answer streams out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefill / time to first token (TTFT).&lt;/strong&gt; Before answering, the model reads your whole prompt. That's prefill, and TTFT is how long you wait before the first word appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context / KV cache.&lt;/strong&gt; How much text the model can hold in mind at once ("256k context" is about 500 pages), and the memory it uses to remember it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n-gram embedding.&lt;/strong&gt; New in this model: a 51 GB lookup table indexed by 2–3 word phrases. It's only ever &lt;em&gt;looked up&lt;/em&gt;, a few rows per word, so it can live on the NVMe drive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speculative decoding / MTP.&lt;/strong&gt; A small built-in "draft head" guesses the next few words, and the big model checks all the guesses in one pass. Correct guesses are free words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KL divergence (KLD).&lt;/strong&gt; How far the compressed model's word probabilities drift from the original's. 0 means identical; lower is better. It's the most sensitive quality meter there is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp / GGUF.&lt;/strong&gt; The inference engine I patched, and its model-file format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  the problem, in one picture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobry7yo68to60yzsyojk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobry7yo68to60yzsyojk.png" alt="Where Qwen3.8-Flash-Next (125B) lives: before and after" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 16-bit the model is 360 GB. Even the popular 4-bit build (&lt;a href="https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF" rel="noopener noreferrer"&gt;unsloth's UD-Q4_K_XL&lt;/a&gt;) is 111 GB: 72 GB of experts, 27 GB of n-gram table (which can stay on disk) and a few GB of everything else. The experts alone don't fit in 72 GB of VRAM once the rest of the model and the context need room too. So llama.cpp does the sensible thing: it puts the overflow experts in system RAM and computes them on the CPU.&lt;/p&gt;

&lt;p&gt;On this machine that means &lt;strong&gt;23 tokens/second&lt;/strong&gt;. With one memory stick per socket, the CPU reads those experts about ten times slower than a GPU would. And for every word, the work bounces between the GPUs and the CPU dozens of times, so each GPU waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  step 1: the obvious setup, and the first trap
&lt;/h2&gt;

&lt;p&gt;The first run with stock settings also showed prefill all over the place (47 to 306 tokens/second on the same 4k prompt) and decode sagging to 10 tok/s. Thread states told the story: the main thread sat in &lt;code&gt;D&lt;/code&gt; (waiting on disk). llama.cpp memory-maps the model file, loading the GPU part streams 70+ GB through the page cache, and the CPU-side experts kept getting evicted and re-read from NVMe.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--load-mode none&lt;/code&gt; loads the CPU-side weights into RAM and keeps them there. Also, &lt;strong&gt;don't download a 188 GB file while you benchmark&lt;/strong&gt;: my download filled the page cache and pushed the server into swap. With both fixed, prefill doubled (&lt;strong&gt;658–792 tok/s&lt;/strong&gt;) and decode steadied at &lt;strong&gt;23–26 tok/s&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Speculative decoding (MTP) barely helped: 24–29 tok/s. The reason matters for everything that follows. A draft of 5 words means the big model checks 6 words at once. Each word picks 10 experts, so one check can touch up to 60 &lt;em&gt;different&lt;/em&gt; experts per layer instead of 10, and the slow ones in CPU RAM get hit harder. &lt;strong&gt;As long as experts live in CPU RAM, nothing else will make this fast.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  step 2: the experts are not equal
&lt;/h2&gt;

&lt;p&gt;Here's the observation the whole project rests on. llama.cpp's calibration data (the &lt;em&gt;importance matrix&lt;/em&gt; unsloth publishes with their quants) records how often each of the 24,576 experts was picked on real text:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38hgpedoun8y1is3uj7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38hgpedoun8y1is3uj7x.png" alt="Routing skew: a few experts do most of the work" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the average layer, &lt;strong&gt;the busiest 25% of experts handle 52% of the tokens, and the busiest 80% handle 95%.&lt;/strong&gt; Stock llama.cpp can only place &lt;em&gt;whole layers&lt;/em&gt; of experts: a layer's 512 experts are one tensor, all on the GPU or all on the CPU. It offloads a quarter of the experts, including popular ones, so a quarter of the expert work lands on the slow path.&lt;/p&gt;

&lt;h2&gt;
  
  
  step 3: split every layer into hot and cold
&lt;/h2&gt;

&lt;p&gt;The first patch splits every layer's expert tensor in two at load time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;hot experts&lt;/strong&gt; go to the GPUs;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cold experts&lt;/strong&gt; go to CPU RAM;&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;router&lt;/strong&gt; is permuted so its choices land in the right half.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the same 19% of expert bytes on the CPU, the cold experts now serve only &lt;strong&gt;4.5% of routed tokens&lt;/strong&gt; instead of ~19%, about 4x less slow-path traffic.&lt;/p&gt;

&lt;p&gt;Getting it right took four bugs, each instructive enough to list in its own section below. The result is &lt;strong&gt;30–32 tok/s&lt;/strong&gt; with correct answers. That's +30%, but it's not the leap it should be, so I profiled it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;where one decoded word spends its time&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kernel launches&lt;/td&gt;
&lt;td&gt;~4,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dense weights (attention, DeltaNet, shared expert), 8-bit&lt;/td&gt;
&lt;td&gt;~4.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;routed experts&lt;/td&gt;
&lt;td&gt;~2.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~3,500 tiny element-wise kernels&lt;/td&gt;
&lt;td&gt;~6–7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU↔CPU synchronisations&lt;/td&gt;
&lt;td&gt;~390&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CPU was barely reading any experts anymore; it was the &lt;em&gt;round trips&lt;/em&gt; that cost. Every layer handed work to the CPU and waited for the answer. Then a cheap experiment: drop the cold experts entirely (quality garbage, speed only) and see how fast it goes fully on GPU: &lt;strong&gt;52 tok/s&lt;/strong&gt;, and &lt;strong&gt;86–88 tok/s&lt;/strong&gt; with speculative decoding. That was the target. The question became: &lt;strong&gt;how do you fit every expert in VRAM without wrecking quality?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  step 4: three shelves, all on the GPU
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbzg98y08zf6u37wtx0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbzg98y08zf6u37wtx0l.png" alt="How one word gets written" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea: if some experts do most of the work, &lt;strong&gt;give them more bits and squeeze the rarely-used ones harder&lt;/strong&gt;. Everything still fits in VRAM, and most tokens still see a well-preserved expert. Think of a library that keeps its bestsellers in hardcover and prints the rarely-borrowed titles as compact paperbacks, so the whole collection fits in the building.&lt;/p&gt;

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

&lt;p&gt;Two pieces make it work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An offline re-packer&lt;/strong&gt; (&lt;a href="https://github.com/SikamikanikoBG/qwen38-flash-next-3x3090/blob/main/tools/expert_tiers.py" rel="noopener noreferrer"&gt;&lt;code&gt;tools/expert_tiers.py&lt;/code&gt;&lt;/a&gt;). It starts from the 8-bit model (188 GB) and uses the importance matrix for both &lt;em&gt;how often&lt;/em&gt; each expert is used and &lt;em&gt;which inputs matter&lt;/em&gt; inside it. A greedy planner fills a VRAM budget: each byte goes to the expert where it removes the most expected error. Every expert is then re-quantized with llama.cpp's own quantizers, and the file is written with the experts reordered hottest-first as three tensors per layer. One wrinkle: the down-projection matrices have rows 640 wide, which the fancy 2–3-bit formats can't handle (they need multiples of 256). So they get their own ladder: IQ4_NL / MXFP4 instead of IQ4_XS / IQ3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A patched llama.cpp&lt;/strong&gt; (&lt;a href="https://github.com/SikamikanikoBG/qwen38-flash-next-3x3090/tree/main/patches" rel="noopener noreferrer"&gt;&lt;code&gt;patches/&lt;/code&gt;&lt;/a&gt;). &lt;code&gt;mul_mat_id&lt;/code&gt;, the operation that runs "each word through its chosen experts", learns to take an &lt;strong&gt;id range&lt;/strong&gt;. Each shelf's matrix multiply sees the router's full choice list, computes only the ids that fall on its shelf, and writes zeros for the rest. The three results simply add up. That meant touching the CPU kernels, three CUDA paths (decode, prefill, and the fused gate+up+activation kernel) and the loader. On top sits Qwen's multi-token-prediction draft head, from a &lt;a href="https://github.com/ggml-org/llama.cpp/pull/28243" rel="noopener noreferrer"&gt;not-yet-merged llama.cpp PR&lt;/a&gt;, re-quantized to 4 bits so it fits too.&lt;/p&gt;

&lt;p&gt;Result, in one chart:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  the four bugs, briefly
&lt;/h2&gt;

&lt;p&gt;For people who'll try this, and because two of them are delightful.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate experts crash the CUDA MoE kernel.&lt;/strong&gt; My first version pointed "not on this shelf" at expert 0. A word with two cold experts then had expert 0 twice, and CUDA's expert-grouping helper assumes each expert appears at most once per word. It silently dropped a slot, and a later kernel read an unwritten index. Fixed by teaching the kernels a real "skip".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A flag stored where a precision setting lives.&lt;/strong&gt; I marked the skip-capable nodes in &lt;code&gt;op_params[0]&lt;/code&gt;, which llama.cpp already uses for the accumulator precision. The flag was quietly overwritten. It moved to slot 7, with a magic value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unsigned ternary.&lt;/strong&gt; &lt;code&gt;ids ? ids[i] : blockIdx.x&lt;/code&gt;, where &lt;code&gt;blockIdx.x&lt;/code&gt; is unsigned. C++ promotes the whole expression to unsigned, so my &lt;code&gt;-1&lt;/code&gt; ("skip") became 4,294,967,295 and the kernel read 4 billion rows past the buffer. &lt;code&gt;compute-sanitizer&lt;/code&gt; found it in one run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fused kernels hide the tag.&lt;/strong&gt; For decode, CUDA fuses gate, up and activation into one kernel whose "destination" is the activation node, not the matrix multiply that carries my id-range tag. The first id-range build printed &lt;code&gt;//////////&lt;/code&gt;. The kernel now looks up the tag on its source node.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  how much quality did it cost?
&lt;/h2&gt;

&lt;p&gt;The honest meter is KL divergence against the 8-bit model over 24,576 tokens of Wikipedia text:&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;expert size&lt;/th&gt;
&lt;th&gt;fits in VRAM&lt;/th&gt;
&lt;th&gt;perplexity vs 8-bit&lt;/th&gt;
&lt;th&gt;KLD&lt;/th&gt;
&lt;th&gt;same top token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unsloth UD-Q4_K_XL (stock)&lt;/td&gt;
&lt;td&gt;71.7 GiB&lt;/td&gt;
&lt;td&gt;no, 25% in RAM&lt;/td&gt;
&lt;td&gt;+0.9%&lt;/td&gt;
&lt;td&gt;0.045&lt;/td&gt;
&lt;td&gt;93.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tiered, 128k profile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;53.0 GiB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.091&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniform IQ3_S / MXFP4, same size&lt;/td&gt;
&lt;td&gt;52.2 GiB&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;+3.0%&lt;/td&gt;
&lt;td&gt;0.095&lt;/td&gt;
&lt;td&gt;91.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tiered, 256k profile&lt;/td&gt;
&lt;td&gt;49.5 GiB&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;+3.2%&lt;/td&gt;
&lt;td&gt;0.113&lt;/td&gt;
&lt;td&gt;90.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What I take from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fitting in VRAM costs quality, and there's no way around that.&lt;/strong&gt; 20 GB less expert weight roughly doubles the KL divergence of the stock 4-bit build. That's physics, not a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At equal size, tiering beats uniform&lt;/strong&gt;: perplexity drift 1.9% vs 3.0%. But the gain is smaller than my planner predicted: its error model is crude, and at this size the total byte count dominates. There's more to win with a better tier recipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a task, it holds up.&lt;/strong&gt; GSM8K (200 grade-school maths questions, greedy, no thinking) scores &lt;strong&gt;95.5%&lt;/strong&gt;. The 27B's published number on the same harness is 95.0–96.5%. GSM8K is saturated, so it mainly proves nothing broke. The model card's gains on agentic work are where the upgrade should show.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  speed vs context, and the 256k profile
&lt;/h2&gt;

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

&lt;p&gt;Both profiles start at &lt;strong&gt;~80 tokens/second&lt;/strong&gt; on a short prompt and slow down as the prompt grows. The per-word attention and indexer work grows with context, and the draft head's guesses get a little worse too. Typical numbers, averaging two runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;prompt&lt;/th&gt;
&lt;th&gt;128k profile: decode / time to first token&lt;/th&gt;
&lt;th&gt;256k profile: decode / time to first token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4k tokens&lt;/td&gt;
&lt;td&gt;82 tok/s / 7–9 s&lt;/td&gt;
&lt;td&gt;67 tok/s / 7–8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15k tokens&lt;/td&gt;
&lt;td&gt;66 tok/s / 18–22 s&lt;/td&gt;
&lt;td&gt;58 tok/s / 19–22 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30k tokens&lt;/td&gt;
&lt;td&gt;58 tok/s / 35 s&lt;/td&gt;
&lt;td&gt;54 tok/s / 38 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;61k tokens&lt;/td&gt;
&lt;td&gt;53 tok/s / 76–79 s&lt;/td&gt;
&lt;td&gt;51 tok/s / 82 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95k / 122k tokens&lt;/td&gt;
&lt;td&gt;39 tok/s / 140 s&lt;/td&gt;
&lt;td&gt;35 tok/s / 190 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefill runs at &lt;strong&gt;540–860 tokens/second&lt;/strong&gt;. That's the weak spot next to the 27B's ~1,300, and it's where I'd look next: it's also the part that makes a 120k-token prompt a three-minute wait.&lt;/p&gt;

&lt;p&gt;The 256k profile needed two more compromises. The KV cache drops to 8-bit, and experts shrink to 49.5 GiB, because the sparse-attention indexer's scratch memory grows with context and pushed a card out of memory at ~200k on the first try. After rebalancing layers across the cards, it read &lt;strong&gt;173,692 tokens of Wikipedia with a random 10-character code hidden at 45% depth, and returned the code: exactly right (&lt;code&gt;6BC5XYE8FS&lt;/code&gt;). A second run with the code at 90% depth of 112,711 tokens was also exact.&lt;/strong&gt; It took 6.1 minutes (472 tokens/second) to read that much. Long-context prefill on this architecture is still the slow part.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it costs to run
&lt;/h2&gt;

&lt;p&gt;Measured at the cards (&lt;code&gt;nvidia-smi&lt;/code&gt;, 5 Hz, three 1,024-token generations per profile): the three 3090s draw &lt;strong&gt;~540 W together while decoding&lt;/strong&gt; and ~124 W at rest with the model loaded. That's &lt;strong&gt;7.5–8.4 joules per token&lt;/strong&gt;, 2.1–2.3 kWh per million tokens. On my tariff (0.30 BGN/kWh day, 0.18 night) &lt;strong&gt;a million generated tokens cost 0.63–0.70 BGN in daytime, 0.38–0.42 BGN at night&lt;/strong&gt;, about $0.35–0.40. &lt;a href="https://dev.to/sikamikanikobg/qwen38-27b-on-one-rtx-3090-vs-two-20-decode-14-cold-prefill-and-3x-on-cached-prompts-55kc"&gt;The 27B costs 0.21–0.34 BGN&lt;/a&gt; for the same million, so the bigger brain costs roughly twice as much per word, all three cards included. It's still coffee money.&lt;/p&gt;

&lt;h2&gt;
  
  
  real-world update: day one behind my assistant
&lt;/h2&gt;

&lt;p&gt;Benchmarks are one request with a short prompt and greedy decoding. My assistant is none of those things, so after a day of it running Jarvis, here is what the server logs say about real requests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;a real assistant turn&lt;/th&gt;
&lt;th&gt;the benchmark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;prompt&lt;/td&gt;
&lt;td&gt;~25,000 tokens (system prompt, tools, memory)&lt;/td&gt;
&lt;td&gt;41 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sampling&lt;/td&gt;
&lt;td&gt;temperature 0.7, thinking on&lt;/td&gt;
&lt;td&gt;greedy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tokens per verify step (draft acceptance)&lt;/td&gt;
&lt;td&gt;2.6 (39%)&lt;/td&gt;
&lt;td&gt;3.4 (62%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decode&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;41–45 tok/s&lt;/strong&gt; (30–45 is what it feels like)&lt;/td&gt;
&lt;td&gt;80 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Decode: 30–45 tok/s, not 80.&lt;/strong&gt; Two things compound. The prompt is deep, and every decoded word pays attention over it; the context chart above already shows ~50 tok/s at 60k. And the draft head guesses sampled text, tool-call JSON and reasoning worse than greedy prose, so speculative decoding saves less. That's the honest number for agent work on this box. For comparison, the 27B benchmarks at ~135 tok/s on this box, so the upgrade is intelligence, not speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to first token: the mistake was mine.&lt;/strong&gt; My assistant runs several &lt;em&gt;roles&lt;/em&gt;: chat, planner, classifier, triage, background jobs. Each has its own ~25k-token prompt prefix. I deployed the server with one slot (&lt;code&gt;-np 1&lt;/code&gt;), so every time the role changed, the cached prefix was thrown away and &lt;strong&gt;25,000 tokens were re-read from scratch: ~45 seconds before every answer&lt;/strong&gt;. The fix is four slots sharing one KV pool, so every role keeps its prefix warm:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;returning to a 14k-token prompt after another role ran&lt;/th&gt;
&lt;th&gt;before (&lt;code&gt;-np 1&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;after (&lt;code&gt;-np 4 -kvu&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;time to first token&lt;/td&gt;
&lt;td&gt;20–45 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6–0.7 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It isn't free: four slots need a little more memory, so the shared pool dropped from 128k to 96k tokens (128k with four slots ran out of VRAM). A single request can still use all 96k.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schedule the background jobs apart.&lt;/strong&gt; A model at 30–45 tok/s with 25k-token prompts spends minutes per agent task. My assistant had 13 scheduled jobs between 06:30 and 09:00 on Mondays, four of them at the same minute. I've spread them 30 minutes apart (05:00 to 11:30) so they stop queueing behind each other. I'll report after a week whether that's enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images and video work too,&lt;/strong&gt; once the vision projector is loaded (&lt;code&gt;--mmproj&lt;/code&gt;, on the GPU with the most free memory: 4 s per photo, against 61 s on the CPU) and the container has &lt;code&gt;ffmpeg&lt;/code&gt; for video (a 5-second clip in 6.4 s). The repo has the exact launcher.&lt;/p&gt;

&lt;h2&gt;
  
  
  what I'd not claim
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's single-user.&lt;/strong&gt; Every speed number here is one request at a time, and real agent turns decode at 30–45 tok/s, not 80. The 27B on vLLM batches many users far better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 27B is still faster.&lt;/strong&gt; About 135 vs 80 tok/s decode, and roughly 1,300 vs 540–860 tok/s prefill. The 125B is smarter, not quicker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It uses all three cards.&lt;/strong&gt; You can't also keep the 27B running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The quality numbers are wikitext + GSM8K.&lt;/strong&gt; They say the compression is gentle. They don't prove the agentic gains survive intact; that needs agent benchmarks I haven't run yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tier planner is a heuristic.&lt;/strong&gt; The measurements say it helps; a calibrated one would help more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  reproduce it
&lt;/h2&gt;

&lt;p&gt;Everything is in &lt;strong&gt;&lt;a href="https://github.com/SikamikanikoBG/qwen38-flash-next-3x3090" rel="noopener noreferrer"&gt;SikamikanikoBG/qwen38-flash-next-3x3090&lt;/a&gt;&lt;/strong&gt;: both llama.cpp patches, the re-packer, the benchmark scripts, and every raw number behind the charts. The short version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# llama.cpp at 81bc6b8 + MTP PR #28243 + the tiering patch&lt;/span&gt;
git apply patches/0001-qwen4exp-mtp-pr28243.patch patches/0002-tiered-experts-hot-cold-split.patch
cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DGGML_CUDA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="nt"&gt;-DCMAKE_CUDA_ARCHITECTURES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;86 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt;

&lt;span class="c"&gt;# re-pack the 8-bit model into three precision shelves (53 GiB of experts)&lt;/span&gt;
python tools/expert_tiers.py write &lt;span class="nt"&gt;--src&lt;/span&gt; Qwen3.8-Flash-Next-Q8_0-00001-of-00006.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--imatrix&lt;/span&gt; imatrix_unsloth.gguf &lt;span class="nt"&gt;--budget-gib&lt;/span&gt; 53 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gu&lt;/span&gt; Q6_K,IQ4_XS,IQ3_XXS &lt;span class="nt"&gt;--dn&lt;/span&gt; Q8_0,IQ4_NL,MXFP4 &lt;span class="nt"&gt;--out&lt;/span&gt; fn-tier53.gguf

&lt;span class="c"&gt;# serve: all layers on GPU, 4 slots sharing a 96k KV pool, vision, MTP drafting 4 tokens&lt;/span&gt;
llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; fn-tier53-00001-of-00002.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;-ts&lt;/span&gt; 16,16,16 &lt;span class="nt"&gt;-c&lt;/span&gt; 98304 &lt;span class="nt"&gt;-np&lt;/span&gt; 4 &lt;span class="nt"&gt;-kvu&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; 1024 &lt;span class="nt"&gt;-ub&lt;/span&gt; 256 &lt;span class="nt"&gt;-fa&lt;/span&gt; on &lt;span class="nt"&gt;--jinja&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mmproj&lt;/span&gt; mmproj-F16.gguf &lt;span class="nt"&gt;-mmdev&lt;/span&gt; CUDA1 &lt;span class="nt"&gt;--image-max-tokens&lt;/span&gt; 1024 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-md&lt;/span&gt; mtp-shared-iq4.gguf &lt;span class="nt"&gt;--spec-type&lt;/span&gt; draft-mtp &lt;span class="nt"&gt;--spec-draft-n-max&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  credits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen&lt;/strong&gt;, for the model and an architecture that rewards this kind of work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unsloth&lt;/strong&gt;, for the GGUFs, the MTP draft files and the importance matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llama.cpp and ggml&lt;/strong&gt;, plus the authors of the &lt;a href="https://github.com/ggml-org/llama.cpp/pull/28243" rel="noopener noreferrer"&gt;Qwen4Exp MTP PR&lt;/a&gt; and the &lt;a href="https://github.com/ggml-org/llama.cpp/issues/28734" rel="noopener noreferrer"&gt;sparse-attention decay issue&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/syv-ai/qwen38-27b-rtx3090" rel="noopener noreferrer"&gt;syv-ai/qwen38-27b-rtx3090&lt;/a&gt;&lt;/strong&gt;, whose rigor on the 27B set the bar for measuring this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This project, from the patches and the tools to the measurements and this write-up, was done with **Claude Code&lt;/em&gt;* acting as my AI engineer, working on my hardware under my direction. The numbers are from vader, the raw data is in the repo, and the mistakes are both of ours.*&lt;/p&gt;

</description>
      <category>llm</category>
      <category>qwen</category>
      <category>homelab</category>
      <category>ai</category>
    </item>
    <item>
      <title>Qwen3.8-27B on One RTX 3090 vs Two: +20% Decode, +14% Cold Prefill, and 3x on Cached Prompts</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Wed, 23 Sep 2026 03:27:37 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/qwen38-27b-on-one-rtx-3090-vs-two-20-decode-14-cold-prefill-and-3x-on-cached-prompts-55kc</link>
      <guid>https://dev.to/sikamikanikobg/qwen38-27b-on-one-rtx-3090-vs-two-20-decode-14-cold-prefill-and-3x-on-cached-prompts-55kc</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Qwen3.8-27B — the dense 27.8B that dropped on August 14 — fits on a single RTX 3090 at W4A16 and decodes at &lt;strong&gt;~125–155 tokens/sec&lt;/strong&gt;. Splitting it across two cards with tensor parallelism buys &lt;strong&gt;roughly 20% more decode&lt;/strong&gt; (noisy: +13% in one run, +22% in the next) and &lt;strong&gt;14% faster prefill on average&lt;/strong&gt; on a cold prompt (23% at the top of the ladder: an 8,600-token prompt takes &lt;strong&gt;8.5 s&lt;/strong&gt; to first token on one card and &lt;strong&gt;6.9 s&lt;/strong&gt; on two). On a prompt the server has already seen, the pair answers &lt;strong&gt;2–3x faster&lt;/strong&gt; — but that is the prefix cache, not the cards, and the script below measures the two separately. At the API list price of $3.00 per million output tokens, one million tokens cost &lt;strong&gt;0.21–0.34 BGN of electricity, about $0.12–0.20&lt;/strong&gt;, on the box that serves them.&lt;/p&gt;

&lt;h2&gt;
  
  
  the box
&lt;/h2&gt;

&lt;p&gt;vader is the machine I run this on: three RTX 3090s in an ASUS Z10PE-D8 WS, dual Xeon E5-2660 v4 (56 threads), 60 GB of RAM, Ubuntu 26.04. It has been my inference box since the Whisper experiments, and since July it has also carried the power-limiting work (&lt;a href="https://dev.to/sikamikanikobg/what-does-a-local-llm-actually-cost-per-month-i-read-the-meters-1274"&gt;the meters post&lt;/a&gt; is where those numbers live). Two of the three cards are in this test; the third sits idle.&lt;/p&gt;

&lt;p&gt;The model under test is the one that is currently loaded on it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;single-card&lt;/th&gt;
&lt;th&gt;TP2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;container&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen38-27b-single-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen38-27b-tp2-single-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;image&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghcr.io/syv-ai/qwen38-27b-rtx3090&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same, &lt;code&gt;:latest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weights&lt;/td&gt;
&lt;td&gt;Qwen3.8-27B-W4A16-AutoRound-fast&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cards&lt;/td&gt;
&lt;td&gt;1x 3090 (22.6 GB VRAM)&lt;/td&gt;
&lt;td&gt;2x 3090 (21.7 GB each)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max context&lt;/td&gt;
&lt;td&gt;131,072&lt;/td&gt;
&lt;td&gt;262,144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;port&lt;/td&gt;
&lt;td&gt;18021&lt;/td&gt;
&lt;td&gt;18022&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are vLLM, both have been up two days, both report healthy, and both run with &lt;strong&gt;prefix caching on&lt;/strong&gt; — which is why every number below comes in two flavours, cold and warm. The W4A16 quantization is what puts a 27.8B dense model inside one 24 GB card — 22.6 GB of it, with the KV cache budget for the rest of the 128k window coming out of what is left.&lt;/p&gt;

&lt;h2&gt;
  
  
  the method
&lt;/h2&gt;

&lt;p&gt;No benchmark harness, no &lt;code&gt;vllm bench&lt;/code&gt; — a 150-line script (&lt;a href="https://github.com/SikamikanikoBG/qwen38-bench" rel="noopener noreferrer"&gt;&lt;code&gt;bench_qwen38_v2.py&lt;/code&gt;&lt;/a&gt;) that sends a real chat-completion request with a prompt of a given size, streams the answer, and times it. For each prompt size it runs three times and takes the median:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prompt sizes:&lt;/strong&gt; calibrated so the server reports ~600, ~2,400, ~4,700, ~9,300 prompt tokens (the script probes the tokenizer first, then scales the prompt to hit the target)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;output:&lt;/strong&gt; &lt;code&gt;max_tokens=256&lt;/code&gt;, &lt;code&gt;temperature=0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;measured:&lt;/strong&gt; time to first SSE event, total time, and the server-reported token counts from &lt;code&gt;stream_options.include_usage&lt;/code&gt; — not my own chunk counting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cold vs warm:&lt;/strong&gt; every cold request carries a unique first line, so no cached prefix can match it; the warm pass repeats one identical request after a warm-up call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bullet is the one that decides whether a benchmark means anything. With prefix caching on — and it is on by default in vLLM — timing the same prompt twice does not measure prefill the second time, it measures a cache lookup. The tell is arithmetic: 8,600 tokens "prefilled" in 0.56 s is 15,000 tokens/sec, roughly ten times what two 3090s can actually push through a 27B model. If your TTFT numbers look like that, you are timing the cache. So every cold request here is salted with a unique first line, and the warm pass is run deliberately and reported in its own column.&lt;/p&gt;

&lt;p&gt;One caveat: this is a single-request benchmark. vLLM's continuous batching is where production throughput lives, and I did not hammer it with concurrent requests. What I measured is the latency you feel as the one person using the box — which, for a homelab, is most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  decode: the second card buys 15–30%, noisily
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyn1hypir2dc3rti8678h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyn1hypir2dc3rti8678h.png" alt="Decode speed by prompt size, cold runs" width="799" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The single card holds &lt;strong&gt;123–154 tokens/sec&lt;/strong&gt; across the ladder; the TP2 pair runs &lt;strong&gt;146–193&lt;/strong&gt;. A 27B dense model at W4A16 on one 3090 is memory-bandwidth-bound in a way that does not care much about prompt size, because the KV cache for these sizes is small next to the weights. The pair is faster at every size but one (at 4,700 tokens the two came out level), and the average gain was +22% in this run against +13% in a run an hour earlier. Single-stream decode on these cards moves by about 10% between runs an hour apart, so I am not going to print a decimal: &lt;strong&gt;call it 15–30%, and expect the lower end.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a real number and a small one. For a 27B model, tensor parallelism across two cards is not the 2x you get from adding compute — the all-reduce between the cards eats most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  prefill: cold is the cards, warm is the cache
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkheov6y1214phf5k1xx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkheov6y1214phf5k1xx.png" alt="Time-to-first-token by prompt size, cold and warm" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two columns, two different questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold&lt;/strong&gt; — a prompt the server has never seen — is prefill, and prefill is compute-bound: the single card does &lt;strong&gt;1,100–1,220 tokens/sec&lt;/strong&gt; of it, the pair &lt;strong&gt;1,230–1,360&lt;/strong&gt;. Time to first token climbs from 0.53 s to &lt;strong&gt;8.5 s&lt;/strong&gt; on one card as the prompt goes from 600 to 9,300 tokens, and from 0.50 s to &lt;strong&gt;6.9 s&lt;/strong&gt; on two. The gap grows with the prompt: 6% at 600 tokens, &lt;strong&gt;23% at 9,300&lt;/strong&gt;, 14% on average. That is what the second card buys you on a fresh long prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm&lt;/strong&gt; — the same prompt again — is the prefix cache doing its job, and here the pair really is 2–3x faster: &lt;strong&gt;0.5–0.75 s&lt;/strong&gt; flat across the ladder against &lt;strong&gt;0.5–1.7 s&lt;/strong&gt; on the single card, whose smaller KV pool and slower re-read of the uncached tail show at every size above 600 tokens. It is a cache number rather than a prefill number, and it is also the one you feel most often, because every follow-up turn in a chat is a warm request.&lt;/p&gt;

&lt;p&gt;The routing rule that falls out: &lt;strong&gt;a long fresh prompt goes to the pair and saves about a fifth of its wait; a conversation stays on whichever lane it started on, so its prefix stays cached.&lt;/strong&gt; For prompts under about 2,000 tokens the single card is within noise of the pair on both counts and draws one card's power.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it costs
&lt;/h2&gt;

&lt;p&gt;The box draws &lt;strong&gt;620 W&lt;/strong&gt; at the wall with both containers loaded and serving — 247 W on one TP2 card, 328 W on the other, 45 W on the single-card 3090 sitting at P8 with its weights resident. Those are the monitor's readings from the day the containers went up, not a measurement taken during these runs; the governor from the meters post is power-capping it, holding 79°C.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqunez6xef0ov2ri6kezf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqunez6xef0ov2ri6kezf.png" alt="API price vs local electricity cost" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the API's list price — $3.00 per million output tokens, DeepInfra's rate for Qwen3.8-27B as listed on llm-stats — one million generated tokens takes about &lt;strong&gt;6,667 seconds&lt;/strong&gt; at 150 tokens/sec. At 620 W that is &lt;strong&gt;1.15 kWh&lt;/strong&gt;. On the Bulgarian dual-rate tariff that is &lt;strong&gt;0.34 BGN by day (0.30 BGN/kWh) or 0.21 BGN by night (0.18 BGN/kWh)&lt;/strong&gt; — roughly &lt;strong&gt;$0.20 or $0.12&lt;/strong&gt;. The API price is &lt;strong&gt;15x to 25x&lt;/strong&gt; the electricity, depending on when the box runs.&lt;/p&gt;

&lt;p&gt;I am not going to pretend that makes the API a bad deal. The API does not cost you 620 W of heat in your room, it does not need a 60 GB machine, it does not need you to keep a W4A16 quant on a 24 GB card, and its $3.00 buys you their datacenter's power, cooling and idle capacity, not yours. But for a box that is already on and already paying its standby tax, every token it serves is almost pure margin. The electricity argument for local inference is not "it is cheaper than the API" in any total-cost sense — it is "the marginal cost of a token on hardware you already run is an order of magnitude below the list price."&lt;/p&gt;

&lt;h2&gt;
  
  
  the verdict
&lt;/h2&gt;

&lt;p&gt;Qwen3.8-27B is the first 27B-class dense model I have run that I would put in front of real work on this box, and the numbers say why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It fits.&lt;/strong&gt; W4A16 on one 3090, 22.6 GB, with room for a 128k context window. The TP2 variant doubles the context to 262k by splitting the weights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is fast enough to feel like an API.&lt;/strong&gt; 125–190 tokens/sec decode is faster than most hosted endpoints feel in a chat UI. The part you notice is a cold long prompt: several seconds on either lane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two-card setup is not 2x.&lt;/strong&gt; It is roughly +20% decode and +14% cold prefill, and 2–3x on warm follow-ups because the pair keeps a bigger cache. Buy the second card for the context window and the cache, not for raw speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The marginal cost is 0.21–0.34 BGN, about $0.12–0.20, per million tokens&lt;/strong&gt; against a $3.00 list price, on hardware that is already drawing its standby power.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark with the cache in mind.&lt;/strong&gt; If a prefill number looks like 15,000 tokens/sec on consumer cards, it is the cache. Salt every request, or turn prefix caching off, and report which one you did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script, the raw JSON and the charts are in the repo: &lt;a href="https://github.com/SikamikanikoBG/qwen38-bench" rel="noopener noreferrer"&gt;qwen38-bench&lt;/a&gt;. If you run a different quant or a different card, I want your numbers — the interesting part of this post is the shape of the curve, and the shape changes with the hardware.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;vader: 3x RTX 3090 (two used here), dual Xeon E5-2660 v4, 60 GB RAM, Ubuntu 26.04, vLLM, W4A16-AutoRound-fast quantization.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>qwen</category>
    </item>
    <item>
      <title>I Benchmarked Qwen3.8-27B on Three RTX 3090s. The Second Card Bought Me 13% More Decode — and a 3x Faster Prefill</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 20 Sep 2026 12:14:36 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/i-benchmarked-qwen38-27b-on-three-rtx-3090s-the-second-card-bought-me-13-more-decode-and-a-3x-2781</link>
      <guid>https://dev.to/sikamikanikobg/i-benchmarked-qwen38-27b-on-three-rtx-3090s-the-second-card-bought-me-13-more-decode-and-a-3x-2781</guid>
      <description></description>
      <category>llm</category>
      <category>qwen</category>
      <category>homelab</category>
      <category>ai</category>
    </item>
    <item>
      <title>What Does a Local LLM Actually Cost per Month? I Read the Meters.</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 20 Sep 2026 07:59:22 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/what-does-a-local-llm-actually-cost-per-month-i-read-the-meters-1274</link>
      <guid>https://dev.to/sikamikanikobg/what-does-a-local-llm-actually-cost-per-month-i-read-the-meters-1274</guid>
      <description>&lt;h1&gt;
  
  
  What Does a Local LLM Actually Cost per Month? I Read the Meters.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The Local LLM Lab — Part 5&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;One controlled experiment. One number. One verdict.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question nobody answers in the local-LLM hype is the boring one: &lt;strong&gt;what does the electricity bill say?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "how many tokens per second." Not "how many GB of VRAM." The bill. The one that arrives on the first of the month and is the only number that actually matters for the person paying for the machine.&lt;/p&gt;

&lt;p&gt;I've been running a local AI stack on an RTX 3090 for about three months now — Whisper transcription as a permanent service, an embedding model for a RAG pipeline, and a 27B chat model on a second machine. I instrumented the GPU with a power meter, set up a dual-rate tariff (day 0.30 BGN/kWh, night 0.18 BGN/kWh), and let the meter run.&lt;/p&gt;

&lt;p&gt;This is what the last 30 days actually cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; NVIDIA RTX 3090, 24 GB, power limit 260 W, running WhisperX (a Whisper ASR webservice) as a permanent resident&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tariff:&lt;/strong&gt; dual-rate — 0.30 BGN/kWh day, 0.18 BGN/kWh night (22:00–06:00)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's on the GPU 24/7:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Whisper ASR webservice — avg 7.9 GB VRAM, peak 10.3 GB, resident&lt;/li&gt;
&lt;li&gt;nomic-embed-text (Ollama) — 308 MB VRAM, resident&lt;/li&gt;
&lt;li&gt;immich ML — negligible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's on the second machine (Vader):&lt;/strong&gt; Qwen3.8-27B via vLLM, two RTX 3090s, on demand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Whisper service is the interesting one. It's not a chatbot. It's a transcription endpoint that my own tools call whenever I record a voice memo, a meeting, or a podcast clip. It sits there all day, at rest, drawing power, waiting for audio.&lt;/p&gt;

&lt;p&gt;That's the honest shape of a "local AI stack" — not a GPU that's 100% utilised 24/7, but a GPU that's 0% utilised 99% of the time and 100% for a few seconds when it's actually doing work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The last 30 days cost €2.00 in electricity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the entire GPU. Not the whole machine. Just the GPU, measured at the card, over 30 days, with the Whisper service resident 24/7 and the embedding model resident 24/7.&lt;/p&gt;

&lt;p&gt;Breakdown by service:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Avg W&lt;/th&gt;
&lt;th&gt;Energy (kWh)&lt;/th&gt;
&lt;th&gt;Cost (€)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whisper ASR (3090)&lt;/td&gt;
&lt;td&gt;22 W&lt;/td&gt;
&lt;td&gt;13.24&lt;/td&gt;
&lt;td&gt;1.76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nomic-embed-text (Ollama)&lt;/td&gt;
&lt;td&gt;3 W&lt;/td&gt;
&lt;td&gt;1.75&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;immich ML&lt;/td&gt;
&lt;td&gt;~0 W&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~25 W&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.03&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Whisper service is the big one, but even it is only 22 W on average. That's not a GPU under load. That's a GPU that's mostly idle with a model resident in VRAM, spiking to ~120 W for a few seconds when it transcribes something, and sitting at ~35 W the rest of the time.&lt;/p&gt;

&lt;p&gt;The embedding model costs 23 cents a month. That's less than a coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the number is so small (and why that's the point)
&lt;/h2&gt;

&lt;p&gt;People who argue "local LLMs are expensive" usually mean one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The hardware cost amortised over time.&lt;/strong&gt; A 3090 costs €800–1,200 used. Amortised over 3 years, that's ~€25/month. That's real, and it's the dominant cost, not the electricity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GPU that's pegged at 100% utilisation 24/7.&lt;/strong&gt; That's a training rig, not an inference stack. My GPU is at 0% utilisation on average. The 30-day average power draw is ~25 W. A 3090 under sustained LLM inference at 260 W would burn ~187 kWh/month and cost ~€23/month. That's a different machine doing a different job.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest framing for a home inference stack is: &lt;strong&gt;the GPU is a mostly-idle appliance that costs a few euros a month to keep warm, and spikes when you actually use it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The €2.00 number is the "keep it warm" cost. The spikes are the "actually use it" cost, and they're short enough that they barely move the monthly total.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the power trace actually looks like
&lt;/h2&gt;

&lt;p&gt;I pulled the 30-day power history at 2-hour resolution. The shape is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline:&lt;/strong&gt; ~35 W, 24/7 (Whisper resident, idle)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spikes:&lt;/strong&gt; ~120 W, for a few seconds to a few minutes, when audio comes in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Occasional higher spikes:&lt;/strong&gt; ~50–54 W sustained for longer stretches when there's a queue of transcriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One outlier:&lt;/strong&gt; a single 2-hour bucket at 121 W (a batch of long audio files)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 2-hour buckets that show 35 W are the honest baseline. The 2-hour buckets that show 121 W are the work. The average of all of them is 22 W for the Whisper service.&lt;/p&gt;

&lt;p&gt;That's the shape of a local inference stack: &lt;strong&gt;a low baseline with short, sharp spikes.&lt;/strong&gt; Not a sustained load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison that matters
&lt;/h2&gt;

&lt;p&gt;Here's the comparison that actually answers "is local worth it":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Monthly electricity cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;This stack (Whisper + embed, 24/7 resident)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€2.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Measured, 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3090 at 100% util, 24/7 (training rig)&lt;/td&gt;
&lt;td&gt;~€23&lt;/td&gt;
&lt;td&gt;Hypothetical, sustained load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API transcription (Whisper API, ~10 hrs audio/month)&lt;/td&gt;
&lt;td&gt;~€14–29&lt;/td&gt;
&lt;td&gt;OpenAI Whisper API pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API LLM (GPT-4o, ~1M tokens/month)&lt;/td&gt;
&lt;td&gt;~€14&lt;/td&gt;
&lt;td&gt;List price&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local stack costs &lt;strong&gt;less than a tenth&lt;/strong&gt; of the equivalent API spend, even before you factor in that the API spend is per-use and the local spend is a flat "keep it warm" cost that doesn't scale with usage.&lt;/p&gt;

&lt;p&gt;The electricity is not the cost. The hardware is the cost. But the electricity is small enough that it stops being the argument against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for the "is local worth it" question
&lt;/h2&gt;

&lt;p&gt;The electricity bill is not the reason to choose cloud over local. It's not even close. A 3090 running a resident inference stack costs ~€2/month in electricity. The hardware amortisation is ~€25/month. The cloud API equivalent is ~€14–29/month for comparable usage, and it scales with usage while the local cost doesn't.&lt;/p&gt;

&lt;p&gt;The real cost of local is the &lt;strong&gt;upfront hardware&lt;/strong&gt; and the &lt;strong&gt;time you spend keeping it running.&lt;/strong&gt; The electricity is a rounding error.&lt;/p&gt;

&lt;p&gt;If you're already paying for a GPU for other reasons (gaming, rendering, a second machine), the marginal cost of adding a local inference stack is essentially the electricity: &lt;strong&gt;a few euros a month.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A local LLM inference stack on a 3090 costs ~€2/month in electricity.&lt;/strong&gt; The hardware is the real cost, not the power. The electricity bill is small enough that it should stop being an argument in the cloud-vs-local debate.&lt;/p&gt;

&lt;p&gt;If your objection to local is "the electricity," the meter says otherwise. The meter says: &lt;strong&gt;a few euros a month, mostly idle, spikes when you use it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's not a cost problem. That's a hardware-cost problem, and it's a one-time problem, not a recurring one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured this
&lt;/h2&gt;

&lt;p&gt;Every number in this article came out of my homelab monitor, not an estimate: a small self-hosted dashboard that reads the GPU's power draw at the card, tracks per-service VRAM, and prices the energy against my tariff. It's open source, and it exposes the same data through a read-only MCP server, so an AI agent can pull the numbers for you instead of you SSH-ing in to run &lt;code&gt;nvidia-smi&lt;/code&gt; by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;SikamikanikoBG/homelab-monitor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://sikamikanikobg.github.io/homelab-monitor/" rel="noopener noreferrer"&gt;sikamikanikobg.github.io/homelab-monitor&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to run your own "what does my stack cost" experiment, that's the tooling I'd point you at.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Local LLM Lab is a series of measured experiments on a home GPU stack. Every number in this article was pulled from a power meter on the card, not estimated. The next piece prices the same stack by € per 1,000 correct answers — the number that actually matters when the model is doing your work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hardware</category>
      <category>llm</category>
    </item>
    <item>
      <title>Qwen3.8-27B on 2 RTX 3090: My First Local Model I Actually Trust</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:45:03 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/qwen38-27b-on-2x-rtx-3090-my-first-local-model-i-actually-trust-fpi</link>
      <guid>https://dev.to/sikamikanikobg/qwen38-27b-on-2x-rtx-3090-my-first-local-model-i-actually-trust-fpi</guid>
      <description>&lt;p&gt;For years "local LLM" meant a toy — a chatbot you played with on weekends, too slow or too dumb to trust with real work. This month, for the first time, I stopped worrying. My assistant, Jarvis, runs its daily tasks on a 27B model on my own hardware. On the tasks I actually run, it feels Sonnet-like — on some, better. Tests continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does — not chat, work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read ~500 Jira emails and built a live tracker of 20+ demands: status, UAT dates, declared FTE benefits, what's closed and why.&lt;/li&gt;
&lt;li&gt;A compliance table for the EU AI Act's image-marking rules — provider vs. deployer, the metadata-vs-visible-label split, with dates. It caught a term the legal team was misusing.&lt;/li&gt;
&lt;li&gt;A net-worth rollup from my own books, and a gold-position sizing question that came back as a plan: 5–10% of liquid, DCA'd over 2–3 tranches.&lt;/li&gt;
&lt;li&gt;The homelab itself: benchmarking models, watching the boxes, writing this post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point isn't any single task. It's that I stopped being careful. With an API, every prompt is a small cost and a small privacy decision. Locally, it's just a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ┌────────────────────────────────┐
        │        ArDi (the hub)          │
        │  56-core · 64GB · 52 Docker    │
        │  Jarvis 2 core · Whisper ASR   │
        │  embeddings · homelab-monitor  │
        └───────────────┬────────────────┘
                        │  Tailscale
        ┌───────────────▼────────────────┐
        │       Vader (inference)        │
        │  vLLM · Qwen3.8-27B (262K ctx) │
        │  2× RTX 3090 · 48GB VRAM       │
        │  tensor-parallel · port 8010   │
        └────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qwen3.8-27B is dense 27B, Apache 2.0, 262K context, vision + video. I picked a dense 27B over a bigger MoE on purpose: one model that fits in 48 GB and is fast, not a 2.4T monster that needs a cluster. Under load Vader draws ~685 W at 80 °C. It's a server, not a laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're going to do this
&lt;/h2&gt;

&lt;p&gt;Four things I'd tell my past self:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;VRAM is the budget, not FLOPs.&lt;/strong&gt; A 27B dense model at Q8/Q4 fits two 24 GB cards with headroom. That's why I went dense: the math is simple — weights + KV cache ≤ 48 GB. A 2.4T MoE would need a cluster and a mortgage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split the box.&lt;/strong&gt; Inference on its own machine, everything else (the assistant core, ASR, embeddings, the monitor) on the hub, talking over Tailscale. When the GPU pegs at 100%, the rest of the house keeps working. One box doing everything is where "local AI" dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM over Ollama for serving.&lt;/strong&gt; Ollama is fine for tinkering; vLLM with tensor parallelism is what makes 48 GB feel like one big GPU and what gets you real tokens/sec. Port 8010, OpenAI-compatible API — your existing clients just point at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the power, or you're guessing.&lt;/strong&gt; "It's a 685 W box" only matters if you can attribute that draw to the process and price it. I built &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;homelab-monitor&lt;/a&gt; for exactly that — VRAM and power per process, energy priced at your tariff. The whole hub ran 4.97 kWh this week: &lt;strong&gt;1.30 BGN / 7 days&lt;/strong&gt;. If you run more than one box, it's the thing I'd reach for first.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Tests continue. The hardest agentic chains still make me double-check. But for the daily 80%, it's reliable enough that I stopped caring. A local model you have to think about is a toy. One you don't is infrastructure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance. Hardware, cost and task data are from my own homelab, measured live.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>homelab</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Does a Second GPU Increase Ollama's Context Window? (Quadro P2000 + RTX 3090 Tested)</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Thu, 09 Jul 2026 12:37:52 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/does-a-second-gpu-increase-ollamas-context-window-quadro-p2000-rtx-3090-tested-5hbh</link>
      <guid>https://dev.to/sikamikanikobg/does-a-second-gpu-increase-ollamas-context-window-quadro-p2000-rtx-3090-tested-5hbh</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short version: no.&lt;/strong&gt; I dropped a much older GPU (&lt;strong&gt;Quadro P2000, 5GB, Pascal, 2016&lt;/strong&gt;) next to an &lt;strong&gt;RTX 3090 (24GB, Ampere)&lt;/strong&gt; on the same box, ran the same context-length ladder (8K→128K) through Ollama and vLLM on &lt;code&gt;qwen3-coder:30B-A3B&lt;/code&gt;, and got &lt;strong&gt;zero extra usable context in either engine&lt;/strong&gt; — and a &lt;strong&gt;74% decode-speed hit&lt;/strong&gt; for the trouble. Ollama hits the identical &lt;code&gt;Chunk too big&lt;/code&gt; wall at ctx=65536 whether the P2000 is there or not. vLLM refuses tensor-parallel across the two cards entirely — not a VRAM problem, a flat compute-capability rejection (&lt;code&gt;Minimum capability: 75. Current capability: 61.&lt;/code&gt;) that fails in 40 seconds, before any memory profiling. And the one real, measured effect of adding the P2000 to Ollama: decode speed goes from &lt;strong&gt;76 → 19.5 tok/s&lt;/strong&gt; at ctx=49152 once the P2000 gets pulled in as an actual compute device.&lt;/p&gt;

&lt;p&gt;Full narrative version — the two-stage collapse, the prompt-cache validation bug caught mid-sweep, the CUDA13-silently-drops-Pascal finding — is on &lt;a href="https://medium.com/@arsen.apostolov" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.## The setup&lt;/p&gt;

&lt;p&gt;ardi (dual Xeon E5-2680 v4, 128GB RAM, openSUSE Leap) has a Quadro P2000 sitting in a second slot next to the RTX 3090 this whole series has run on so far. Same model as phase 1 (&lt;code&gt;qwen3-coder:30B-A3B&lt;/code&gt;), same box, four legs: {Ollama, vLLM} × {3090 only, 3090+P2000 tandem}, priced through &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; against real GPU power draw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama: same wall, extra tax
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ctx&lt;/th&gt;
&lt;th&gt;3090 only decode tok/s&lt;/th&gt;
&lt;th&gt;tandem decode tok/s&lt;/th&gt;
&lt;th&gt;P2000 VRAM (tandem)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8,192&lt;/td&gt;
&lt;td&gt;124.3&lt;/td&gt;
&lt;td&gt;122.0&lt;/td&gt;
&lt;td&gt;6 MB / 0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24,576&lt;/td&gt;
&lt;td&gt;108.2&lt;/td&gt;
&lt;td&gt;70.0&lt;/td&gt;
&lt;td&gt;62 MB / 0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;99.4&lt;/td&gt;
&lt;td&gt;61.0&lt;/td&gt;
&lt;td&gt;62 MB / 0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;49,152&lt;/td&gt;
&lt;td&gt;75.7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,580 MB / 55%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;fatal: &lt;code&gt;Chunk too big&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;fatal: identical &lt;code&gt;Chunk too big&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two separate costs, not one: decode already falls behind at ctx=24576 while the P2000 is still basically idle (62MB, 0% util) — some scheduling overhead just from having a second visible device. Then the real collapse hits at ctx=49152, when the P2000 actually gets pulled into the compute path (3.58GB, 55% util) and decode craters to &lt;strong&gt;19.5 tok/s&lt;/strong&gt;. Same context ceiling either way, worse speed the whole way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  vLLM: doesn't even get to try
&lt;/h2&gt;

&lt;p&gt;Expected failure mode going in: tensor-parallel splits the ~17GB AWQ checkpoint roughly in half, and the P2000's 5GB doesn't hold its ~8.5GB share. Actual failure, at ctx=8192, in 40 seconds, before any memory profiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: The quantization method auto_awq is not supported for the current GPU.
Minimum capability: 75. Current capability: 61.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWQ's Marlin kernel needs compute capability 7.5+ (Turing and later). The P2000 is 6.1 (Pascal). Not a close VRAM call — a flat architectural exclusion, decided before capacity is even checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus finding: Ollama's own CUDA13 build almost drops the P2000
&lt;/h2&gt;

&lt;p&gt;Boot log, before any of the above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;skipping&lt;/span&gt; &lt;span class="n"&gt;CUDA&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; — &lt;span class="n"&gt;compute&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="n"&gt;architectures&lt;/span&gt;
&lt;span class="n"&gt;device&lt;/span&gt;=&lt;span class="s2"&gt;"Quadro P2000"&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;=&lt;span class="m"&gt;610&lt;/span&gt;
&lt;span class="n"&gt;archs&lt;/span&gt;=&lt;span class="s2"&gt;"[750 800 860 870 890 900 1000 1030 1100 1200 1210]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Falls back to a legacy &lt;code&gt;cuda_v12&lt;/code&gt; runtime that does support Pascal — so it works, just via a path most people wouldn't notice without reading boot logs. This 2016 card is now old enough that modern quantized-inference stacks are starting to architecturally step around it, not just outrun it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What wasn't the point of this one
&lt;/h2&gt;

&lt;p&gt;Not claiming a second GPU is never worth it — a matched pair, or a smaller-but-newer card, is a different setup entirely. This was specifically: does &lt;em&gt;this&lt;/em&gt; 5GB Pascal card, next to &lt;em&gt;this&lt;/em&gt; 3090, on &lt;em&gt;these&lt;/em&gt; two engines, buy anything. Check compute capability against your quantization scheme before you do the VRAM math — it can end the conversation first.&lt;/p&gt;

&lt;p&gt;Every number above priced through &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; — open source, MIT licensed — against ardi's real GPU power draw. Full write-up with all four charts and the mid-sweep debugging on &lt;a href="https://medium.com/@arsen.apostolov" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.What's the oldest card you've tried to tandem into a rig — did it actually pull weight, or did you just assume it was?&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ollama</category>
      <category>vllm</category>
      <category>gpu</category>
    </item>
  </channel>
</rss>
