<?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>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>
    <item>
      <title>Whisper large-v3 VRAM Requirements: Why It Won't Fit on a 5GB GPU (and What We Tried Instead)</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Wed, 08 Jul 2026 03:29:30 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/whisper-large-v3-vram-requirements-why-it-wont-fit-on-a-5gb-gpu-and-what-we-tried-instead-1a18</link>
      <guid>https://dev.to/sikamikanikobg/whisper-large-v3-vram-requirements-why-it-wont-fit-on-a-5gb-gpu-and-what-we-tried-instead-1a18</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;whisper-large-v3&lt;/code&gt; &lt;strong&gt;OOMs on a 5GB GPU (Quadro P2000) at float16, int8_float16, and full int8&lt;/strong&gt; — before serving a single request. Root cause is architecture overhead (32-layer encoder-decoder, activations, CUDA context), not just weight size. Fine-tuned &lt;code&gt;whisper-tiny → base → small → small-v2&lt;/code&gt; on Common Voice Bulgarian instead: held-out WER improved from &lt;strong&gt;88.2% → 32.7%&lt;/strong&gt; across escalating model size, but never closed the gap to large-v3's &lt;strong&gt;27.3%&lt;/strong&gt;. A community &lt;code&gt;large-v3-turbo&lt;/code&gt; Bulgarian fine-tune claiming &lt;strong&gt;9.97% WER on FLEURS&lt;/strong&gt; scored &lt;strong&gt;31.2%&lt;/strong&gt; on our own held-out set — same ballpark as our own model, not the win the model card implied. Built a real dual-GPU nginx failover (P2000 = fine-tune, 3090 = large-v3) that worked correctly on deploy, then failed a real spontaneous-speech test badly enough to roll back to large-v3-only within ~5 seconds. Core finding: &lt;strong&gt;Common Voice read-aloud WER does not predict real assistant-use transcription quality.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;ardi has one RTX 3090 (24GB) doing LLM inference work, and a Quadro P2000 (5GB) that's sat idle for about two years. Jarvis, a self-hosted assistant, depends on Whisper for speech-to-text — testing showed only &lt;code&gt;large-v3&lt;/code&gt; handles Bulgarian well; smaller stock checkpoints are fine for English, not for a lower-resource language. &lt;code&gt;large-v3&lt;/code&gt; sits permanently loaded on the 3090, the same card needed for local LLM serving.&lt;/p&gt;

&lt;p&gt;Question: can the idle P2000 take Bulgarian transcription off the 3090's hands via a Bulgarian-specific fine-tune small enough to fit 5GB?&lt;/p&gt;

&lt;p&gt;(One naming note so the rest of this makes sense: the container running here is &lt;code&gt;whisper-asr-webservice&lt;/code&gt; wrapping &lt;code&gt;faster-whisper&lt;/code&gt; — not the separate WhisperX project, despite what I've been calling it internally for months.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: does large-v3 just fit?
&lt;/h2&gt;

&lt;p&gt;Tested &lt;code&gt;large-v3&lt;/code&gt; on the P2000 at three precisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;float16        -&amp;gt; OOM
int8_float16   -&amp;gt; OOM
int8           -&amp;gt; OOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fuuj62ek4tj3n88imy46l.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%2Fuuj62ek4tj3n88imy46l.png" alt="Horizontal bar chart: VRAM used on the P2000 by whisper-tiny (0.32GB), the deployed small-v2 fine-tune (1.4GB), and a community turbo-bg model (4.15GB), all under a 5.0GB ceiling — versus large-v3 as a red hatched bar breaking through the ceiling, labeled OOM, needs ~8GB" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All three OOM before serving a request. Not a quantized-weight-size problem — the encoder-decoder's non-weight overhead (32 layers, activations, CUDA context) exceeds 5GB regardless of precision. &lt;code&gt;whisper-tiny&lt;/code&gt; loads at 318MB with no issue, ruling out a driver/compatibility problem. &lt;code&gt;medium&lt;/code&gt; (769M params) was the practical ceiling for raw model size — 3.87GB used, 1.2GB headroom — but a generic multilingual &lt;code&gt;medium&lt;/code&gt; isn't good enough for Bulgarian on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempts 2–4: escalating fine-tunes
&lt;/h2&gt;

&lt;p&gt;Fine-tuned on Mozilla Common Voice Bulgarian, on the 3090, via HuggingFace &lt;code&gt;transformers&lt;/code&gt; &lt;code&gt;Seq2SeqTrainer&lt;/code&gt;. Evaluated on the same 150 held-out test clips (never seen in training) for every model:&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%2Fa6eda5cdddxzp12j0o7c.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%2Fa6eda5cdddxzp12j0o7c.png" alt="Grouped bar chart: WER by model, zero-shot vs fine-tuned, tiny through small-v2, with a dashed reference line at large-v3's 27.3%. Fine-tuned WER descends from 68.7% to 32.7% across the four models, never reaching the reference line" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;small-v2&lt;/code&gt; = same architecture as &lt;code&gt;small&lt;/code&gt;, retrained on train+other combined (6,739 rows vs 4,952) for 5 epochs. Validation WER by epoch: &lt;code&gt;32.17 → 28.99 → 28.21 → 28.21 → 28.44&lt;/code&gt; — flattened, then rose at epoch 5 (overfitting), so &lt;code&gt;load_best_model_at_end&lt;/code&gt; correctly kept the epoch 3/4 checkpoint rather than the final one. No more clean Bulgarian Common Voice data exists beyond train+other, so this is the practical ceiling for this data/model-size combination.&lt;/p&gt;

&lt;p&gt;Two gotchas caught along the way:&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;# 1. CUDA_VISIBLE_DEVICES alone doesn't guarantee GPU index matches&lt;/span&gt;
&lt;span class="c"&gt;# nvidia-smi's PCI-bus order -- a run silently landed on the P2000&lt;/span&gt;
&lt;span class="c"&gt;# instead of the intended 3090 until:&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CUDA_DEVICE_ORDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PCI_BUS_ID
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CUDA_VISIBLE_DEVICES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="c"&gt;# 2. ardi's root disk (already at a tight 90% baseline) filled to 100%&lt;/span&gt;
&lt;span class="c"&gt;# mid-training from accumulated dataset/HF caches -- silent SIGKILL,&lt;/span&gt;
&lt;span class="c"&gt;# no traceback. Fixed by pointing the cache at a bigger volume instead&lt;/span&gt;
&lt;span class="c"&gt;# of the system disk:&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HF_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/backup/hf-cache
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CACHE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/backup/whisper-bg-tiny-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither is the interesting part of this story, but both cost real debugging time — worth checking explicitly on any shared multi-GPU box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 5: the community shortcut that didn't reproduce
&lt;/h2&gt;

&lt;p&gt;Searched Hugging Face for an existing Bulgarian ASR fine-tune before pushing further on limited training data. Found &lt;code&gt;sam8000/whisper-large-v3-turbo-bulgarian-bulgaria&lt;/code&gt; — a fine-tune of &lt;code&gt;large-v3-turbo&lt;/code&gt; (same 32-layer encoder as full large-v3, decoder pruned from 32 to 4 layers), claiming &lt;strong&gt;9.97% WER on the FLEURS Bulgarian benchmark&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Converted to CTranslate2, it does fit the P2000 — &lt;strong&gt;4.1–4.2GB used, ~900MB headroom&lt;/strong&gt; — tight but real (the third bar in the VRAM chart above). Evaluated on the &lt;em&gt;same&lt;/em&gt; held-out Common Voice test set used for every model above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sam8000/whisper-large-v3-turbo-bulgarian-bulgaria: 31.2% WER
our own small-v2 (fine-tuned):                     32.7% WER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Statistically the same result, not the dramatic win the model card implied. The 9.97% FLEURS number isn't fake — it just doesn't transfer to a different eval set with different preprocessing/normalization. &lt;strong&gt;Always re-measure a candidate on your own eval, apples to apples, before trusting a model card's headline number.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that worked: dual-GPU failover
&lt;/h2&gt;

&lt;p&gt;Built a real deployment: two whisper containers (P2000 = small-v2, 3090 = large-v3 unchanged) behind an nginx sidecar using &lt;code&gt;proxy_next_upstream&lt;/code&gt; for automatic failover. One detail that shapes what "failover" means here: &lt;code&gt;whisper-asr-webservice&lt;/code&gt; loads its model eagerly at process boot, not per-request — so this isn't a live per-call fallback, it's "is this backend up or down," decided once at startup.&lt;/p&gt;

&lt;p&gt;Deployed live, confirmed it actually worked — routing correct. The old standalone production container was kept &lt;strong&gt;stopped, not deleted&lt;/strong&gt;, for the entire session — the eventual rollback was a container start, not a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that actually mattered
&lt;/h2&gt;

&lt;p&gt;Real spontaneous speech — describing colors and objects out loud, not Common Voice-style read sentences. Verdict: "quite, quite, quite weak." Noticeably worse than the 32.7% benchmark WER suggested for casual listening. Rolled back to large-v3-only production immediately — &lt;strong&gt;~5 seconds&lt;/strong&gt;, because the old container was never torn down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we deliberately didn't do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Didn't publish a GitHub repo for the fine-tuned checkpoints — the result isn't good enough to ship as a "solution."&lt;/li&gt;
&lt;li&gt;Didn't chase a 6th fine-tune attempt (medium-size, more data augmentation) — diminishing returns were already visible in the epoch curve, and the deeper problem (domain mismatch between read-aloud and spontaneous speech) wouldn't be fixed by more of the same data.&lt;/li&gt;
&lt;li&gt;Didn't keep the dual-GPU stack running "just in case" — production reverted to exactly its pre-session state, P2000 idle again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual finding
&lt;/h2&gt;

&lt;p&gt;Common Voice is people reading prepared text aloud in clean conditions — a different domain from spontaneous conversational speech directed at an assistant (prosody, hesitation, mic quality, vocabulary). A benchmark WER on read-aloud speech didn't predict real assistant-use quality here, for either our own fine-tune or a community model claiming a much better number on a different benchmark. This generalizes past Bulgarian and past Whisper: &lt;strong&gt;eval-set domain match matters more than the headline metric.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full narrative version — the charts, the physical GPU install photo, the "why I still don't have a use for this card" ending — &lt;a href="https://medium.com/@arsen.apostolov" rel="noopener noreferrer"&gt;on Medium&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Every VRAM ceiling and WER number above was measured via &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; — MIT licensed, one container, the same tool that's priced every benchmark in this series.&lt;/p&gt;

&lt;p&gt;Curious if anyone's gotten a Bulgarian (or other lower-resource-language) Whisper fine-tune to hold up on real spontaneous speech, not just a read-aloud benchmark — and what closed the gap if so.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>whisper</category>
      <category>gpu</category>
      <category>homelab</category>
    </item>
    <item>
      <title>vLLM vs llama.cpp vs Ollama: What Happens When Your Model Doesn't Fit in 24GB VRAM</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 05 Jul 2026 05:54:01 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/vllm-vs-llamacpp-vs-ollama-what-happens-when-your-model-doesnt-fit-in-24gb-vram-56eb</link>
      <guid>https://dev.to/sikamikanikobg/vllm-vs-llamacpp-vs-ollama-what-happens-when-your-model-doesnt-fit-in-24gb-vram-56eb</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Benchmarked &lt;strong&gt;llama.cpp, Ollama, and vLLM&lt;/strong&gt; across &lt;strong&gt;5 models (1B to 116.8B params)&lt;/strong&gt; on one &lt;strong&gt;RTX 3090 (24GB) + 128GB RAM&lt;/strong&gt; home-lab box, priced through &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt;. Inside 24GB, vLLM's continuous batching scales aggregate throughput &lt;strong&gt;3.9x-5.4x&lt;/strong&gt; from concurrency 1 to 8 (llama.cpp only manages &lt;strong&gt;1.2x-1.9x&lt;/strong&gt;, even with &lt;code&gt;-np 8&lt;/code&gt; explicitly set to match). Past 24GB — two models deliberately chosen to force RAM-spill — llama.cpp and Ollama both degrade to single-digit tok/s and keep generating. &lt;strong&gt;vLLM OOMs outright on both&lt;/strong&gt;, at the same ~22.1-22.2GB-used / &amp;lt;700MB-free ceiling, regardless of quantization scheme. Sub-plot: llama.cpp's manually-tuned layer offload beats Ollama's automatic split by &lt;strong&gt;37x&lt;/strong&gt; on time-to-first-token during RAM-spill, while landing on nearly identical steady-state decode speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The roster
&lt;/h2&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;Vendor&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Fits in 24GB?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder 30B-A3B&lt;/td&gt;
&lt;td&gt;Alibaba&lt;/td&gt;
&lt;td&gt;MoE (~3.3B active)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B-A4B&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;MoE (~4B active)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-4.5-Air 106B-A12B&lt;/td&gt;
&lt;td&gt;Zhipu&lt;/td&gt;
&lt;td&gt;MoE (~12B active)&lt;/td&gt;
&lt;td&gt;no, deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS 120B-A5.1B&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;MoE (~5.1B active)&lt;/td&gt;
&lt;td&gt;no, deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Gemma 4 is real — Google's newest release as of this writing, not a Gemma 3 typo.)&lt;/p&gt;

&lt;p&gt;3 prompt tiers (short/medium/long), concurrency 1 and 8, 2 reps per cell, 15 backend×model pairs total. &lt;strong&gt;Caveat stated up front&lt;/strong&gt;: the first three models ran against my production Ollama (&lt;code&gt;OLLAMA_NUM_PARALLEL=1&lt;/code&gt;, serialized by default — real daily-use config); GLM and GPT-OSS ran against a separate isolated instance (&lt;code&gt;OLLAMA_NUM_PARALLEL=4&lt;/code&gt;) since they needed a clean volume anyway. Ollama's concurrency=8 numbers for the first three models are &lt;strong&gt;not&lt;/strong&gt; its concurrency ceiling — they're its actual default production behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency, inside 24GB
&lt;/h2&gt;

&lt;p&gt;Aggregate decode tok/s, concurrency 1 → concurrency 8:&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;Ollama&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B&lt;/td&gt;
&lt;td&gt;125.6 → 71.4&lt;/td&gt;
&lt;td&gt;294.1 → 400.6&lt;/td&gt;
&lt;td&gt;235.5 → 1172.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder 30B-A3B&lt;/td&gt;
&lt;td&gt;129.3 → 108.4&lt;/td&gt;
&lt;td&gt;157.2 → 183.9&lt;/td&gt;
&lt;td&gt;172.0 → 677.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B-A4B&lt;/td&gt;
&lt;td&gt;84.5 → 78.5&lt;/td&gt;
&lt;td&gt;118.8 → 220.6&lt;/td&gt;
&lt;td&gt;133.8 → 723.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vLLM's own c1→c8 scaling: &lt;strong&gt;3.9x-5.4x&lt;/strong&gt; (paged attention, requests slot into idle cycles). llama.cpp's, even with &lt;code&gt;-np 8&lt;/code&gt; matched to the concurrency level: &lt;strong&gt;1.2x-1.9x&lt;/strong&gt; — it pre-declares a fixed KV-cache reservation per parallel slot before the server starts, so concurrency is a config decision, not a runtime one. Head-to-head at c8: vLLM beats llama.cpp by &lt;strong&gt;2.9x-3.7x&lt;/strong&gt;, beats Ollama's serialized default by &lt;strong&gt;6.3x-16.4x&lt;/strong&gt; (caveat above applies).&lt;/p&gt;

&lt;h2&gt;
  
  
  The cliff, and vLLM's wall
&lt;/h2&gt;

&lt;p&gt;GLM-4.5-Air (~52% of layers spilled to system RAM under llama.cpp's tuning) and GPT-OSS-120B (~67% spilled) were picked specifically to not fit. llama.cpp and Ollama both ran them — slow, single-digit tok/s, but real generation, no crash. vLLM failed outright on &lt;strong&gt;both&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GPT-OSS-120B, native MXFP4, --cpu-offload-gb 45
OutOfMemoryError: CUDA out of memory. Tried to allocate 1.08 GiB.
GPU 0 has a total capacity of 23.56 GiB of which 533.69 MiB is free.
Process ... has 22.21 GiB memory in use.
RuntimeError: Engine core initialization failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GLM-4.5-Air, pre-quantized AWQ, --cpu-offload-gb 36
OutOfMemoryError: CUDA out of memory. Tried to allocate 1.16 GiB.
GPU 0 has a total capacity of 23.56 GiB of which 685.69 MiB is free.
Process ... has 22.12 GiB memory in use.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same shape, different model, different quantization path. I retried GLM at &lt;code&gt;--gpu-memory-utilization 0.78&lt;/code&gt; (down from 0.90, to force more declared headroom) — &lt;strong&gt;got the byte-for-byte identical error&lt;/strong&gt;: 22.12 GiB used, 685.69 MiB free, 1.16 GiB requested. That rules out the utilization knob as the fix; the base weight + offload footprint is already pinned at the ceiling before profiling starts. Two models, two quant schemes, same ~22GB wall — reads as a real limit of vLLM's CPU-offload path for &amp;gt;100B-param MoE on one 24GB card on this stack, not a per-model quirk.&lt;/p&gt;

&lt;h2&gt;
  
  
  TTFT: the 37x gap that steady-state doesn't show
&lt;/h2&gt;

&lt;p&gt;On the models that ran everywhere, steady-state decode is nearly a tie once warmed up — GPT-OSS-120B's longest tier: &lt;strong&gt;7.65 tok/s (llama.cpp) vs 7.6 tok/s (Ollama)&lt;/strong&gt;. GLM: &lt;strong&gt;4.58 vs 4.59&lt;/strong&gt;. Time-to-first-token is a different story:&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;Ollama TTFT&lt;/th&gt;
&lt;th&gt;llama.cpp TTFT&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;GLM-4.5-Air&lt;/td&gt;
&lt;td&gt;13.6s&lt;/td&gt;
&lt;td&gt;8.1s&lt;/td&gt;
&lt;td&gt;1.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS-120B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;274.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.3s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;llama.cpp's &lt;code&gt;-ngl&lt;/code&gt; is a number I computed myself from the model's real &lt;code&gt;config.json&lt;/code&gt; (layer count, per-layer size) — &lt;code&gt;-ngl 12&lt;/code&gt; for GPT-OSS, offloading ~21GB deliberately. Ollama figures the split out automatically at load time, and on a freshly-pulled, partially-RAM-resident 65GB model, that automatic path is expensive. Same destination, very different path there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs (BGN per 1M output tokens, real GPU energy)
&lt;/h2&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;Ollama&lt;/th&gt;
&lt;th&gt;llama.cpp&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 1B&lt;/td&gt;
&lt;td&gt;0.19&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;td&gt;~0*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 26B-A4B&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;0.14&lt;/td&gt;
&lt;td&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-Coder 30B-A3B&lt;/td&gt;
&lt;td&gt;0.16&lt;/td&gt;
&lt;td&gt;0.13&lt;/td&gt;
&lt;td&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-4.5-Air&lt;/td&gt;
&lt;td&gt;2.61&lt;/td&gt;
&lt;td&gt;1.95&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS-120B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.43&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*vLLM's Gemma 3 1B run finished in 6s — too fast for the power sampler to catch a reading, recorded near-zero. A sampling limitation on short bursts, not a genuine free result.&lt;/p&gt;

&lt;p&gt;GPT-OSS-120B on Ollama costs &lt;strong&gt;~7x more real electricity per million tokens&lt;/strong&gt; than llama.cpp for the identical model — the TTFT convenience tax from above, showing up again in currency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three disclosed vLLM checkpoint swaps
&lt;/h2&gt;

&lt;p&gt;The original plan was on-the-fly bitsandbytes 4-bit quant for every vLLM leg. It failed for every MoE model, for three distinct, verified reasons — not the same error copy-pasted three times:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3-Coder-30B&lt;/strong&gt;: &lt;code&gt;ValueError: BitsAndBytes quantization with padded hidden_size ... Parameter shape (786432, 1) != checkpoint shape (2048, 768)&lt;/code&gt; — bnb can't dequantize this MoE's padded expert layout. Fix: pre-quantized AWQ checkpoint. Ran clean after (677.9 tok/s aggregate @ c8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 26B-A4B&lt;/strong&gt;: &lt;code&gt;AttributeError: MoE Model Gemma4ForConditionalGeneration does not support BitsAndBytes quantization yet.&lt;/code&gt; A new architecture, bnb path not wired up yet. Fix: a different pre-quantized checkpoint — which then hit a pydantic error because its &lt;code&gt;config.json&lt;/code&gt; says &lt;code&gt;compressed-tensors&lt;/code&gt;, not AWQ, despite the repo name. Fixed by dropping the explicit &lt;code&gt;--quantization&lt;/code&gt; flag entirely and letting vLLM auto-detect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLM-4.5-Air&lt;/strong&gt;: not a failure — a practicality call. Skipped a 212GB native bf16 download to test a bnb+MoE+CPU-offload combo the vLLM community already flagged as shaky, went straight to a ~63GB pre-quantized AWQ checkpoint that tests the exact same question.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every root cause above came from the actual container logs, not from assuming precedent carried over from the previous model's failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What wasn't tested
&lt;/h2&gt;

&lt;p&gt;Only two &lt;code&gt;--gpu-memory-utilization&lt;/code&gt; values before accepting the OOM as final, not a full &lt;code&gt;--cpu-offload-gb&lt;/code&gt; sweep. No multi-GPU / tensor-parallel vLLM path — a different question from "does single-card CPU offload work." Ollama's c8 numbers for the first three models are its production default, not its concurrency ceiling. And one raw llama.cpp per-request timing (Gemma 4, medium tier, c8) self-reported an impossible 250,024 tok/s from a near-zero-duration completion — the aggregate figures used throughout are total-tokens-over-wall-time, which isn't corrupted by that, but it's a known rough edge in the raw per-request logs.&lt;/p&gt;

&lt;p&gt;Full narrative version, with the RAM-spill mechanics and the redacted dashboard screenshot: &lt;a href="https://medium.com/@arsen.apostolov" rel="noopener noreferrer"&gt;on Medium&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Every number above was 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 the RTX 3090's real power draw.&lt;/p&gt;

&lt;p&gt;If you're already running one of these three backends: has yours ever tried to load something that just didn't fit — and did it fail loud or fail quiet?&lt;/p&gt;

</description>
      <category>llm</category>
      <category>homelab</category>
      <category>vllm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Local LLM vs Claude: Benchmarking qwen3-coder:30b as a Production Agent Backend</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Fri, 03 Jul 2026 11:16:36 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/local-llm-vs-claude-benchmarking-qwen3-coder30b-as-a-production-agent-backend-482b</link>
      <guid>https://dev.to/sikamikanikobg/local-llm-vs-claude-benchmarking-qwen3-coder30b-as-a-production-agent-backend-482b</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Replayed 27 real historical tasks from Jarvis (my LangGraph agent, ~90 tools) through &lt;code&gt;qwen3-coder:30b&lt;/code&gt; on an RTX 3090, scored against Claude's actual production answers to the same tasks. Quality: &lt;strong&gt;Claude 89.4/100 vs qwen 22.8/100&lt;/strong&gt;. Cost: &lt;strong&gt;qwen ~5,150x cheaper per task&lt;/strong&gt; ($0.00015 vs $0.763, real GPU electricity vs real API billing). Reliability: qwen leaked malformed tool-call tags into &lt;strong&gt;26% of answers&lt;/strong&gt; and only overlapped with the tools the task actually needed &lt;strong&gt;14.8%&lt;/strong&gt; of the time. Same qwen3-coder:30b scored 100% in an earlier, much smaller benchmark — the gap here is about tool-surface complexity, not the model being bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Jarvis is a real personal AI agent — LangGraph &lt;code&gt;create_react_agent&lt;/code&gt;, ~90 tools spanning email/calendar/notes/files/messaging/code, running on Claude in production. &lt;code&gt;qwen3-coder:30b&lt;/code&gt; had already scored 100% task success in a &lt;a href="https://dev.to/sikamikanikobg/how-to-run-reliable-local-llm-agents-on-an-rtx-3090-a-benchmark-5-models-priced-in-watts-15d0"&gt;controlled 17-task benchmark&lt;/a&gt; on the same RTX 3090. Obvious next question: drop it into the real agent and see what happens.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;28 real task prompts pulled from Jarvis's own Langfuse traces (90-day window), stratified 4×7 across calendar / code / email / files / general / messaging / notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude's answers are real production history, not re-run.&lt;/strong&gt; Re-running through the sandbox would hand it fake stub data it never saw — that's a worse baseline, not a fairer one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qwen runs fresh&lt;/strong&gt;, through a sandboxed replay harness: the real Jarvis agent code in-process, every write-capable tool intercepted (nothing sent/written for real), and every mocked read-only tool serves the &lt;em&gt;real recorded output&lt;/em&gt; from that task's original trace when available — not a generic stub. Same data, both models.&lt;/li&gt;
&lt;li&gt;1/28 tasks excluded (336,906-char prompt, over any 16K–24K context window) → 27 scored.&lt;/li&gt;
&lt;li&gt;Judge: LLM-as-judge (&lt;code&gt;claude-opus-4-8&lt;/code&gt;), scored independently per answer (not pairwise) to avoid position bias, 1–5 → 0–100.&lt;/li&gt;
&lt;li&gt;Every qwen run priced as a &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; experiment against real 3090 power draw. Claude's cost is Langfuse's recorded API billing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Caveat, stated plainly:&lt;/strong&gt; the judge is a Claude model scoring Claude's own answers alongside qwen's — self-preference bias is a documented effect in LLM-as-judge setups and probably inflates the gap somewhat. It doesn't explain a 66-point gap, a 26% malformed-output rate, or two tool-call loops, but it's a real methodology limitation, not a footnote.&lt;/p&gt;

&lt;p&gt;Getting here took three re-runs: a judge-response parsing bug that silently neutral-scored ~40/54 calls, a mock-data bug that starved qwen of real inbox/calendar content on 16/28 tasks while Claude's baseline had the real thing, and a Claude-API rate limit that neutral-scored another batch mid-scoring. All three caught by checking score distributions, not by trusting a clean exit code — worth knowing before trusting the numbers below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&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;Claude&lt;/th&gt;
&lt;th&gt;qwen3-coder:30b&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg quality (0–100)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost / task&lt;/td&gt;
&lt;td&gt;$0.763 (real API billing)&lt;/td&gt;
&lt;td&gt;$0.00015 (real GPU electricity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cost, 27 tasks&lt;/td&gt;
&lt;td&gt;$20.60&lt;/td&gt;
&lt;td&gt;$0.004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total energy&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.0396 kWh&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;~&lt;strong&gt;5,150x cheaper per task&lt;/strong&gt; for qwen (precise, currency-converted from a 0.0072 BGN &lt;em&gt;total&lt;/em&gt; across all 27 tasks, at 1 BGN = $0.5547 — an earlier rough estimate of 180x on this project was wrong, this is the corrected number).&lt;/p&gt;

&lt;p&gt;By category (Claude | qwen | n):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calendar:   90 | 30 | 4
code:       87 | 25 | 3
email:      92 | 15 | 4
files:      88 | 15 | 4
general:    85 | 30 | 4
messaging:  87 | 22 | 4
notes:      97 | 22 | 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;qwen's best relative showing (calendar, general) is still a third of Claude's score. It never wins a category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;Malformed tool-call leak — instead of a real LangGraph tool call, qwen sometimes emits the call as raw text in its final answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;function&lt;/span&gt;&lt;span class="err"&gt;=send_email&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
{"to": "...", "subject": "...", "body": "..."}
&lt;span class="nt"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That happened on &lt;strong&gt;7/27 tasks (26%)&lt;/strong&gt;. The user reading that answer sees broken syntax where a real action should have been confirmed or a real answer given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool-overlap recall: 14.8% average&lt;/strong&gt;, measured over the 18/27 tasks where the original historical trace actually used at least one tool (9 tasks needed none). Most of the time qwen reached for different tools than the ones that actually solved the task — or none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repetitive-loop failure&lt;/strong&gt; on 2/27 tasks: &lt;code&gt;pilot-17&lt;/code&gt; (email, 24 tool calls, 138.6s, ~196.7K input tokens) and &lt;code&gt;pilot-27&lt;/code&gt; (messaging, 27 tool calls, 148.9s, ~196.7K input tokens) both called the &lt;em&gt;same already-answered tool&lt;/em&gt; (&lt;code&gt;run_command&lt;/code&gt;, &lt;code&gt;todo_write&lt;/code&gt;) repeatedly instead of stopping. Confirmed via raw logs both tasks got real replayed data (&lt;code&gt;replayed_real_data: true&lt;/code&gt;) — a genuine stopping-condition failure, not a data-starvation artifact.&lt;/p&gt;

&lt;p&gt;One more data point worth having, not a verdict: on a task where both models actually called &lt;code&gt;send_email(...)&lt;/code&gt; in the harness (intercepted, nothing sent), Claude told the user the email had been sent — a fabrication. qwen correctly disclosed the send didn't go through. Not "qwen is more honest" — it's also the model leaking raw tags 26% of the time. Both mishandled the mock, just differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope of the claim
&lt;/h2&gt;

&lt;p&gt;Same &lt;code&gt;qwen3-coder:30b&lt;/code&gt;, same GPU, scored 100% on a 17-task controlled benchmark with a much smaller tool surface. This isn't "local LLMs are bad" — it's that a model excellent on a scoped benchmark isn't automatically a safe drop-in for a large, real, ~90-tool production surface with a 31KB context prompt and real messy history behind it. Task/tool-surface complexity mattered as much as raw model quality here. Claude isn't flawless either — see the fabricated send-email confirmation above.&lt;/p&gt;

&lt;p&gt;Jarvis stays on Claude for now. The cost number is real enough to be worth a narrower follow-up — testing qwen on just the categories where it scored closest (calendar, general) as a cheap fallback path, rather than a full swap.&lt;/p&gt;

&lt;p&gt;Full narrative version, charts, and the three-bug scoring saga: &lt;a href="https://medium.com/@arsen.apostolov" rel="noopener noreferrer"&gt;on Medium&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Every qwen run here was priced through &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; against the 3090's real power draw — MIT licensed, one container, reproducible if you want to price your own local-model experiments the same way.&lt;/p&gt;

&lt;p&gt;Curious where the line is for you: how cheap does a local model have to be before you'd trust it with a slice of a real agent, and which slice would you pick first?&lt;/p&gt;

</description>
      <category>llm</category>
      <category>homelab</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Run Reliable Local LLM Agents on an RTX 3090: A Benchmark (5 Models, Priced in Watts)</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 28 Jun 2026 06:54:12 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-to-run-reliable-local-llm-agents-on-an-rtx-3090-a-benchmark-5-models-priced-in-watts-15d0</link>
      <guid>https://dev.to/sikamikanikobg/how-to-run-reliable-local-llm-agents-on-an-rtx-3090-a-benchmark-5-models-priced-in-watts-15d0</guid>
      <description>&lt;p&gt;I gave &lt;strong&gt;GLM-4.5-Air&lt;/strong&gt; (106B, open weights) 12 coding tasks through &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;opencode&lt;/a&gt; on my RTX 3090. It scored &lt;strong&gt;0%&lt;/strong&gt; — never edited a single file.&lt;/p&gt;

&lt;p&gt;Same model, same GPU, same tasks, but driven by a ~150-line &lt;strong&gt;LangGraph&lt;/strong&gt; agent instead: &lt;strong&gt;93%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The model was never the problem. The orchestrator was. Here's the benchmark — including the part nobody else measures, the &lt;strong&gt;electricity cost per correct task&lt;/strong&gt;.&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%2Fgqtw55h6nnjo1q76711v.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%2Fgqtw55h6nnjo1q76711v.png" alt="opencode vs LangGraph tool-adherence" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RTX 3090 (24 GB) + 128 GB RAM&lt;/strong&gt;, models via &lt;strong&gt;ollama&lt;/strong&gt;, Q4 quants, temp 0.2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 recent open models&lt;/strong&gt; × &lt;strong&gt;2 orchestrators&lt;/strong&gt; (opencode vs custom LangGraph ReAct with ollama-native tool-calling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 graded tasks&lt;/strong&gt; (12 coding in Python/JS/C++ + 5 general-agent) with hidden unit tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every run priced in GPU watts&lt;/strong&gt; via my open-source &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;homelab-monitor&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&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;tok/s&lt;/th&gt;
&lt;th&gt;opencode adh.&lt;/th&gt;
&lt;th&gt;LangGraph adh.&lt;/th&gt;
&lt;th&gt;LangGraph coding&lt;/th&gt;
&lt;th&gt;LangGraph general&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-Coder 30B-A3B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;130&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GLM-4.5-Air 106B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Devstral Small 24B&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed-OSS 36B&lt;/td&gt;
&lt;td&gt;9.5&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1-Distill 32B&lt;/td&gt;
&lt;td&gt;6.7&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tool-adherence&lt;/strong&gt; = % of tasks where the model actually &lt;em&gt;called a tool&lt;/em&gt; instead of just printing code in chat. It was the master variable. (GLM's headline "93%" is its blended score across all 17 tasks: 89% coding + 100% general.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Three takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The framework can matter more than the model.&lt;/strong&gt; opencode sends a frontier-shaped system prompt + 12 tools over its OpenAI-compat path; most local models fall back to chatting. Native tool-calling through a lean agent fixes that — GLM went 0% → 93%. (Qwen3-Coder is the exception: it's tuned for agentic tool use and aces opencode out of the box.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acting ≠ solving.&lt;/strong&gt; LangGraph made Devstral &lt;em&gt;act&lt;/em&gt; (8% → 53% adherence) but not &lt;em&gt;solve&lt;/em&gt; (coding stayed 8%). The framework decides whether a model acts; the model decides whether it's right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wattmeter ranks honestly.&lt;/strong&gt; Qwen solved tasks at ~0.0005 BGN each; the models that scored zero still burned &lt;strong&gt;10–30× more energy&lt;/strong&gt; for nothing. On a home rig, the cheapest model is the fast, correct one — and MoE (Qwen activates ~3B of 30B per token) wins twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bonus: &lt;strong&gt;128 GB RAM let me run the 106B GLM&lt;/strong&gt; (23 GB VRAM + 27 GB spilled to RAM) — it works, at 5.7 tok/s. Great for fire-and-forget batch jobs, not interactive coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipe for reliable local agents
&lt;/h2&gt;

&lt;p&gt;Pick a tool-use-tuned model (&lt;strong&gt;Qwen3-Coder 30B-A3B&lt;/strong&gt; is the all-weather winner) → use &lt;strong&gt;native&lt;/strong&gt; tool-calling, not an OpenAI-compat path → keep the harness lean → use RAM for reach, not speed → &lt;strong&gt;measure correctness per kWh&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;Full write-up with methodology, charts, and the deeper "why" →&lt;/strong&gt; [&lt;a href="https://medium.com/@arsen.apostolov/local-llm-agents-on-an-rtx-3090-i-benchmarked-5-models-2-frameworks-and-the-orchestrator-f5fd600ca221" rel="noopener noreferrer"&gt;https://medium.com/@arsen.apostolov/local-llm-agents-on-an-rtx-3090-i-benchmarked-5-models-2-frameworks-and-the-orchestrator-f5fd600ca221&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;⭐ Every number was priced in watts by &lt;strong&gt;&lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;homelab-monitor&lt;/a&gt;&lt;/strong&gt; — my open-source tool that turns your GPU's power draw into per-task cost. &lt;strong&gt;Star it&lt;/strong&gt; if you want the same receipts for your own rig. Harness + tasks + leaderboard code are reproducible.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>homelab</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Rank Local LLMs by Cost per Correct Answer (Measured GPU Energy, 8 Ollama Models)</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Tue, 23 Jun 2026 18:11:23 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-to-rank-local-llms-by-cost-per-correct-answer-measured-gpu-energy-8-ollama-models-5c5h</link>
      <guid>https://dev.to/sikamikanikobg/how-to-rank-local-llms-by-cost-per-correct-answer-measured-gpu-energy-8-ollama-models-5c5h</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I priced 8 local Ollama models by &lt;strong&gt;€ per 1,000 correct answers&lt;/strong&gt; — metered GPU energy ÷ correct answers, on one RTX 3090. &lt;code&gt;gemma4:26b&lt;/code&gt; won at &lt;strong&gt;96.9% accuracy for €0.013/1k-correct&lt;/strong&gt;. The most expensive model (&lt;code&gt;qwen3:8b-fp16&lt;/code&gt;) cost &lt;strong&gt;€0.239/1k&lt;/strong&gt; and scored &lt;em&gt;worse&lt;/em&gt; (66.7%). Reasoning tokens and full precision both cost a lot and bought nothing here. Every cost comes from real metered kWh via the open-source &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the short, copy-pasteable version. The narrative writeup is on &lt;a href="https://medium.com/@arsen.apostolov/tokens-are-cheap-wrong-answers-arent-32be7655845d" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;€ per correct answer = (metered GPU energy cost over the eval window) ÷ (number of correct answers)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Tokens-per-euro flatters whichever model talks the most. Cost-per-correct only rewards being &lt;em&gt;right cheaply&lt;/em&gt; — which is the thing you actually pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model                  VRAM     Acc     Tok/task  Tok/s  Wh/pass  €/1k correct (day)
gemma4:26b             16.9 GB  96.9%   68        86     4.5      €0.013   ← winner
gemma3:1b              0.9 GB   82.1%   125       133    3.8      €0.013
gemma3:27b             17.1 GB  100.0%  119       36     16.3     €0.046
qwen3:30b-a3b   (MoE)  18.4 GB  83.3%   555       186    14.1     €0.048
qwen3:8b (Q4_K_M) 🧠   5.4 GB   64.8%   626       126    22.7     €0.100
qwen3:8b          🧠   5.4 GB   64.8%   626       126    23.6     €0.104
qwen3:8b (Q8_0)   🧠   8.7 GB   61.1%   672       88     33.5     €0.156
qwen3:8b (fp16)   🧠   15.5 GB  66.7%   664       53     56.2     €0.239   ← most expensive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 = reasoning/thinking mode on. Night tariff knocks ~40% off every row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things the numbers say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The value champion is mid-size, not max-size.&lt;/strong&gt; &lt;code&gt;gemma4:26b&lt;/code&gt; hit &lt;strong&gt;96.9%&lt;/strong&gt; for &lt;strong&gt;€0.013 per 1,000 correct&lt;/strong&gt; — cheapest-per-correct on the whole bench &lt;em&gt;and&lt;/em&gt; near-perfect, ~&lt;strong&gt;18×&lt;/strong&gt; cheaper per correct answer than &lt;code&gt;qwen3:8b-fp16&lt;/code&gt;. &lt;code&gt;gemma3:27b&lt;/code&gt; is the only 100% model but costs ~3.5× more (slower, 36 tok/s).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The thinking tax is real and didn't pay off.&lt;/strong&gt; qwen3 reasoning models emit &lt;strong&gt;555–672 tokens/task&lt;/strong&gt; vs the gemmas' &lt;strong&gt;68–125&lt;/strong&gt; (5–9×). Tokens are energy. On these 54 deterministic tasks that extra reasoning bought &lt;em&gt;no&lt;/em&gt; correctness — the priciest model scored &lt;em&gt;lower&lt;/em&gt; than one 18× cheaper. (Caveat: this suite is arithmetic / executable code / format-following. On open-ended hard problems, reasoning earns its tokens. On structured agent work, it was dead weight.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The quantization paradox.&lt;/strong&gt; Same &lt;code&gt;qwen3:8b&lt;/code&gt; at three precisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            Accuracy   Energy/pass   Throughput
Q4_K_M      64.8%      22.7 Wh       126 tok/s
Q8_0        61.1%      33.5 Wh       88  tok/s
fp16        66.7%      56.2 Wh       53  tok/s
            └ flat ┘   └ 2.5× ┘      └ halved ┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher precision cost &lt;strong&gt;2.5× the energy&lt;/strong&gt; and &lt;strong&gt;half the throughput&lt;/strong&gt; for accuracy that's flat-and-noisy. On a 3090, aggressive quant was the correct call, not a compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology (so you can trust the ranking)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;54 deterministic tasks&lt;/strong&gt;, mechanically graded — &lt;strong&gt;no LLM judge&lt;/strong&gt;. Reasoning 15 (GSM8K-style numeric extraction), code 12 (HumanEval-style, &lt;em&gt;executed&lt;/em&gt; asserts in a sandbox), factual 12 (keyword), instruct 15 (format predicates). Grader selftest 11/11.&lt;/li&gt;
&lt;li&gt;Controls identical across all 8 models: &lt;strong&gt;temperature 0, seed 42, num_ctx 4096, num_predict 1024&lt;/strong&gt;, identical prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm-up discarded&lt;/strong&gt; → model-load energy excluded (pricing inference, not cold starts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 passes&lt;/strong&gt; each, ranges reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle baseline = 38 W&lt;/strong&gt;, measured as a control.&lt;/li&gt;
&lt;li&gt;qwen3 thinking left &lt;strong&gt;on&lt;/strong&gt; (realistic); thinking tokens counted for energy, stripped before grading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest determinism caveat:&lt;/strong&gt; Ollama is &lt;em&gt;not&lt;/em&gt; bit-exact at temp 0. &lt;code&gt;gemma3:1b&lt;/code&gt; drifted 81–83% across passes; &lt;code&gt;gemma3:27b&lt;/code&gt; was 100% on all three; qwen3 runs were identical. Report ranges, not point claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU/DRAM not metered&lt;/strong&gt; (no RAPL on this host), so true wall-plug cost is a bit higher — but the ranking holds because every model paid the same un-metered overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The currency gotcha (measure twice)
&lt;/h2&gt;

&lt;p&gt;Costs are EUR from &lt;strong&gt;measured kWh × Bulgarian dual tariff (€0.1534 day / €0.0920 night)&lt;/strong&gt;. While building this I caught my own dashboard mislabeling &lt;strong&gt;BGN as EUR&lt;/strong&gt;: the tariff read &lt;code&gt;0.30/0.18 EUR&lt;/code&gt;, but those are leva. Bulgaria joined the euro on &lt;strong&gt;2026-01-01&lt;/strong&gt; at fixed &lt;strong&gt;1 EUR = 1.95583 BGN&lt;/strong&gt;; €0.30/kWh would be German-tier, implausible for the EU's cheapest household power. Converted: &lt;code&gt;0.30 / 1.95583 = €0.1534&lt;/code&gt;, &lt;code&gt;0.18 / 1.95583 = €0.0920&lt;/code&gt;. Lesson: don't trust the dashboard's € field — compute from physical kWh and your verified tariff.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reproduce the energy tracking
&lt;/h2&gt;

&lt;p&gt;Every cost above came from &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;&lt;strong&gt;HomeLab Monitor&lt;/strong&gt;&lt;/a&gt; (MIT, one container) — its Experiments tab integrates real GPU power over a run's window into kWh and money. Bring it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;        &lt;span class="c"&gt;# port 9800&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab the one-file &lt;code&gt;homelab_run.py&lt;/code&gt; client, mint an ingest key, and wrap your eval — the run comes back priced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;homelab_run&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;homelab&lt;/span&gt;
&lt;span class="n"&gt;homelab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&amp;lt;your-host&amp;gt;:9800&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hlm_…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;homelab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma4:26b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm-cost-bench&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PASSES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;run_graded_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# all inference inside the run
&lt;/span&gt;&lt;span class="n"&gt;priced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;homelab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# energy_kwh, cost, avg_w, peak_util — from real power
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole instrumentation. Divide the priced energy by your grader's correct count and you've got cost-per-correct for your own roster. &lt;a href="https://sikamikanikobg.github.io/homelab-monitor/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;code&gt;docker pull sikamikaniko123/homelab-monitor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did NOT do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No LLM-as-judge — mechanical grading only.&lt;/li&gt;
&lt;li&gt;No cold-start energy in the numbers — warm-up discarded on purpose.&lt;/li&gt;
&lt;li&gt;No trusting the dashboard's € field — costs recomputed from measured kWh.&lt;/li&gt;
&lt;li&gt;No single-run claims — 3 passes, ranges where they exist.&lt;/li&gt;
&lt;li&gt;No CPU/DRAM cost claim — only the GPU is metered, and I say so.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Over to you
&lt;/h2&gt;

&lt;p&gt;Bigger and full-precision lost. A 26B model did near-perfect work for a rounding error; an fp16 reasoning model charged 18× as much to be wrong more often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So when you reach for a local model — accuracy, speed, or cost per answer it actually gets right? And have you ever measured the third one?&lt;/strong&gt; Drop your own cost-per-correct numbers in the comments.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>gpu</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How Much Does It Actually Cost to Run a Local LLM? (€ per Million Tokens, Measured)</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 22 Jun 2026 18:33:31 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-much-does-it-actually-cost-to-run-a-local-llm-eu-per-million-tokens-measured-jih</link>
      <guid>https://dev.to/sikamikanikobg/how-much-does-it-actually-cost-to-run-a-local-llm-eu-per-million-tokens-measured-jih</guid>
      <description>&lt;p&gt;"It runs on my own GPU, so it's basically free." I believed that until I put a meter on it. So I ran a controlled benchmark on one box — an openSUSE machine with a single RTX 3090 — driving three local models through ollama under an identical fixed workload (256-token generations in a loop for ~4 minutes each), while my open-source dashboard priced every run by the &lt;strong&gt;real GPU energy it burned&lt;/strong&gt;: power sampled from &lt;code&gt;nvidia-smi&lt;/code&gt; every 10 s, integrated over each run's exact window, multiplied by my actual day/night tariff. One number per model, in euros per million output tokens.&lt;/p&gt;

&lt;p&gt;Here's the part that made me re-run it. The tiny &lt;code&gt;gemma3:1b&lt;/code&gt; came out at &lt;strong&gt;€0.118 / 1M tokens&lt;/strong&gt; — about &lt;strong&gt;5× cheaper&lt;/strong&gt; than a hosted Flash-class API (~€0.55). But &lt;code&gt;gemma3:27b&lt;/code&gt;'s &lt;strong&gt;electricity alone&lt;/strong&gt; was &lt;strong&gt;€0.706 / 1M&lt;/strong&gt; — &lt;em&gt;more&lt;/em&gt; expensive per token than just paying the cloud, and that's before a single cent of the GPU's purchase price. "Local" didn't make it cheaper; it made it cost more &lt;em&gt;and&lt;/em&gt; I own the depreciation. The mechanism is one line: each token costs &lt;strong&gt;watts ÷ throughput&lt;/strong&gt;, and a big dense model is both slow and thirsty. A newer mid-size architecture (&lt;code&gt;gemma4:26b&lt;/code&gt;) bought a lot of that back, landing at &lt;strong&gt;€0.272&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The full guide is methodology-first and reproducible end to end — minting an ingest key, the stdlib-only client, the exact ollama loop that reads &lt;code&gt;eval_count&lt;/code&gt;/&lt;code&gt;eval_duration&lt;/code&gt; for real tokens-per-second, reading each run back priced, and the honest caveats (this is marginal GPU energy only — not capex, idle, or cooling — and the absolute numbers round to fractions of a cent; the &lt;em&gt;shape&lt;/em&gt; is the finding).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full guide on Medium → &lt;a href="https://medium.com/@arsen.apostolov/how-much-does-it-actually-cost-to-run-a-local-llm-per-million-tokens-measured-4a90a7f31a48" rel="noopener noreferrer"&gt;https://medium.com/@arsen.apostolov/how-much-does-it-actually-cost-to-run-a-local-llm-per-million-tokens-measured-4a90a7f31a48&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>homelab</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Fix Watchtower Not Updating Containers on Docker 29</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:42:12 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-to-fix-watchtower-not-updating-containers-on-docker-29-517f</link>
      <guid>https://dev.to/sikamikanikobg/how-to-fix-watchtower-not-updating-containers-on-docker-29-517f</guid>
      <description>&lt;p&gt;You push a newer image to your registry, Watchtower wakes up on schedule, scans, reports a clean run — and your container keeps serving last week's image. No error, no restart loop, nothing red to chase. It just silently stops recreating. If that started happening around the time you landed on &lt;strong&gt;Docker 29&lt;/strong&gt;, you've found the cause.&lt;/p&gt;

&lt;p&gt;The classic &lt;code&gt;containrrr/watchtower&lt;/code&gt; image — the one in basically every tutorial — is effectively unmaintained, with its last release back in 2023. It's a Docker API client, and when the Engine's API moved forward, the part that &lt;em&gt;lists and compares&lt;/em&gt; images kept working while the part that &lt;em&gt;actually recreates&lt;/em&gt; containers quietly fell off. So you get the worst failure mode there is: a tool that reports success while doing nothing.&lt;/p&gt;

&lt;p&gt;The fix is a one-line image swap to the maintained community fork, &lt;code&gt;nickfedor/watchtower:latest&lt;/code&gt; — a drop-in replacement with the same labels and env vars that tracks the current Docker Engine. On my Docker 29.2.1 box it recreates cleanly again (&lt;code&gt;scanned=10 updated=1 failed=0&lt;/code&gt;), and I let homelab-monitor surface every recreate by its reset uptime — so I can see which container the auto-updater just touched, and get a push alert if a recreate ever comes up unhealthy.&lt;/p&gt;

&lt;p&gt;The full guide has the exact compose, the bring-up commands, the real recreate log lines, and how to opt containers in safely with the label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full guide on Medium → &lt;a href="https://medium.com/@arsen.apostolov/how-to-fix-watchtower-not-updating-containers-on-docker-29-a891217c6db2" rel="noopener noreferrer"&gt;https://medium.com/@arsen.apostolov/how-to-fix-watchtower-not-updating-containers-on-docker-29-a891217c6db2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>selfhosted</category>
      <category>homelab</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Fix Docker Networking After a firewalld Reload</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 21 Jun 2026 06:25:38 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-to-fix-docker-networking-after-a-firewalld-reload-369e</link>
      <guid>https://dev.to/sikamikanikobg/how-to-fix-docker-networking-after-a-firewalld-reload-369e</guid>
      <description>&lt;p&gt;You edit a firewalld zone, run the one command you always run — &lt;code&gt;sudo firewall-cmd --reload&lt;/code&gt; — and it returns &lt;code&gt;success&lt;/code&gt;. Then, hours later, a backup didn't run. Your containers are still &lt;strong&gt;Up&lt;/strong&gt; in &lt;code&gt;docker ps&lt;/code&gt;. The host has internet. The containers have none.&lt;/p&gt;

&lt;p&gt;Here's the seam: firewalld and Docker both write the same netfilter tables, and neither knows about the other. &lt;code&gt;firewall-cmd --reload&lt;/code&gt; &lt;strong&gt;flushes the whole ruleset&lt;/strong&gt; and re-applies only firewalld's config — wiping the &lt;code&gt;DOCKER&lt;/code&gt; / &lt;code&gt;DOCKER-USER&lt;/code&gt; chains and the NAT masquerade that dockerd installed at startup. Docker doesn't get told its rules vanished, so it never re-adds them. Result: running containers lose outbound internet while still reporting healthy. The 10-second manual fix is &lt;code&gt;sudo systemctl restart docker&lt;/code&gt; (dockerd re-installs its chains on start). The &lt;em&gt;permanent&lt;/em&gt; fix is a small systemd unit that restarts Docker automatically whenever firewalld reloads — so it self-heals before you notice.&lt;/p&gt;

&lt;p&gt;The full guide has the exact &lt;code&gt;docker-firewalld-watch.service&lt;/code&gt; unit file, the &lt;code&gt;enable --now&lt;/code&gt; commands, and a copy-paste test that breaks egress on purpose and proves it heals. It also covers how I make this failure &lt;strong&gt;visible across a whole fleet&lt;/strong&gt; — because "alive but isolated" is invisible by design — using &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;homelab-monitor&lt;/a&gt;: one Docker container, polls every host over SSH (no agents), shows fleet-wide container/service health, and pushes edge-triggered alerts to Discord, ntfy.sh and Telegram the moment a container flips red.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full guide on Medium → &lt;a href="https://medium.com/@arsen.apostolov/528889d3eca1" rel="noopener noreferrer"&gt;https://medium.com/@arsen.apostolov/528889d3eca1&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>selfhosted</category>
      <category>homelab</category>
    </item>
    <item>
      <title>How to Get Disk-Full Alerts Across Linux and Windows</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 21 Jun 2026 04:03:06 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/how-to-get-disk-full-alerts-across-linux-and-windows-466j</link>
      <guid>https://dev.to/sikamikanikobg/how-to-get-disk-full-alerts-across-linux-and-windows-466j</guid>
      <description>&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%2F05lze3cc6lsssggmgxep.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%2F05lze3cc6lsssggmgxep.png" alt="Disk-full alerts across Linux and Windows with HomeLab Monitor" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My fleet doesn't agree on anything: an openSUSE hub, an Ubuntu box, a Windows 11 workstation, a Windows 10 VM. Different shells, different disk-checking habits — which is how that Windows 10 VM ended up at &lt;strong&gt;C: 99.2% full, 39.1 of 39.4 GB, about 0.3 GB from the wall&lt;/strong&gt;, with me none the wiser. It wasn't alone: the Windows 11 box's G: was at &lt;strong&gt;94.3%&lt;/strong&gt;, the Ubuntu box at 83.1%, the hub at 76%.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;df&lt;/code&gt; only fires on the box you're logged into, on the mornings you remember — and Windows doesn't speak it at all. What I actually wanted was boring: one table with every mount on every host, and a ping the moment one crosses a line.&lt;/p&gt;

&lt;p&gt;I get both from a single container — &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;HomeLab Monitor&lt;/a&gt; (open source, MIT). It polls every host over SSH (Linux &lt;em&gt;and&lt;/em&gt; Windows, no agents), shows every disk worst-first, and pushes &lt;strong&gt;edge-triggered alerts to Discord, ntfy.sh or Telegram&lt;/strong&gt; with a disk-usage threshold you set in the UI — no env vars, no config files. So a 99.2% disk taps you on the shoulder instead of quietly taking down a VM.&lt;/p&gt;

&lt;p&gt;The full guide is a four-step walkthrough, with screenshots: bring up the container, add your Linux and Windows hosts over SSH (three clicks each), see every disk in one place worst-first, and set the disk threshold + alert channel — then fire a test ping to prove it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the full guide on Medium → &lt;a href="https://medium.com/@arsen.apostolov/how-to-get-disk-full-alerts-across-linux-and-windows-262fb69fa2e7" rel="noopener noreferrer"&gt;https://medium.com/@arsen.apostolov/how-to-get-disk-full-alerts-across-linux-and-windows-262fb69fa2e7&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>windows</category>
      <category>sysadmin</category>
      <category>homelab</category>
    </item>
    <item>
      <title>I want to let an AI agent roam my homelab — looking for someone to build the MCP server</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 07 Jun 2026 08:46:32 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/i-want-to-let-an-ai-agent-roam-my-homelab-looking-for-someone-to-build-the-mcp-server-3kh7</link>
      <guid>https://dev.to/sikamikanikobg/i-want-to-let-an-ai-agent-roam-my-homelab-looking-for-someone-to-build-the-mcp-server-3kh7</guid>
      <description>&lt;p&gt;I maintain a small open-source tool called HomeLab Monitor — one dashboard for every box in my homelab: host vitals, containers, systemd services, GPU, and which AI model servers are loaded right now.&lt;/p&gt;

&lt;p&gt;It's good at being a pair of human eyes. The next thing I want is to make it a source of &lt;em&gt;context&lt;/em&gt; for an AI agent.&lt;/p&gt;

&lt;p&gt;So the idea: give it an &lt;strong&gt;MCP server&lt;/strong&gt;. Model Context Protocol is the thing that lets an agent like Claude call tools and read resources. If the monitor speaks MCP, an agent can connect and explore the whole fleet — "which container is leaking RAM?", "the GPU's been pinned for an hour, who's driving it?", "this host wants a reboot and an OS upgrade, what order is safe?" — and start helping with the maintenance instead of me squinting at graphs.&lt;/p&gt;

&lt;p&gt;The fun part for whoever builds it: it's mostly a thin wrapper over a REST API that already exists. The monitor already serves clean, read-only JSON (&lt;code&gt;/api/data&lt;/code&gt;, &lt;code&gt;/api/fleet&lt;/code&gt;, &lt;code&gt;/api/host_data/&amp;lt;name&amp;gt;&lt;/code&gt;, &lt;code&gt;/metrics&lt;/code&gt;). MCP just adds the semantics — tools and resources with names an LLM can reason about instead of a raw blob. Read-only to start; any future write tool stays opt-in.&lt;/p&gt;

&lt;p&gt;It's genuinely weekend-sized if you've wrapped an MCP server around an API before — and a great first one if you haven't and want to learn.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor" rel="noopener noreferrer"&gt;https://github.com/SikamikanikoBG/homelab-monitor&lt;/a&gt;&lt;br&gt;
The idea + a suggested first PR: &lt;a href="https://github.com/SikamikanikoBG/homelab-monitor/issues/70" rel="noopener noreferrer"&gt;https://github.com/SikamikanikoBG/homelab-monitor/issues/70&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If wiring this sounds fun, come say hi on the issue — I'll help scope the first commit.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>help</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>The homelab box you forgot you own is probably 47 updates behind — here’s the safe fix</title>
      <dc:creator>Arsen Apostolov</dc:creator>
      <pubDate>Sun, 07 Jun 2026 06:01:16 +0000</pubDate>
      <link>https://dev.to/sikamikanikobg/the-homelab-box-you-forgot-you-own-is-probably-47-updates-behind-heres-the-safe-fix-1n0i</link>
      <guid>https://dev.to/sikamikanikobg/the-homelab-box-you-forgot-you-own-is-probably-47-updates-behind-heres-the-safe-fix-1n0i</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; My homelab monitor flagged my Plex/Pi-hole box &lt;strong&gt;47 packages and a kernel behind&lt;/strong&gt; — and I'd forgotten the machine existed. Here's the 5-minute non-interactive fix, and the one upgrade I deliberately &lt;em&gt;didn't&lt;/em&gt; run.&lt;/p&gt;

&lt;p&gt;This is the dev.to short version of &lt;a href="https://medium.com/@arsen.apostolov/my-own-dashboard-caught-one-of-my-machines-stealing-16gb-of-vram-e8288ea10f31" rel="noopener noreferrer"&gt;the Medium write-up&lt;/a&gt;. Same dashboard that caught &lt;a href="https://dev.to/sikamikanikobg/reclaiming-16gb-of-idle-vram-a-30-line-sidecar-that-evicts-comfyui-when-it-stops-working-2d9l"&gt;a service hoarding 16GB of VRAM last week&lt;/a&gt; — different, more boring villain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal
&lt;/h2&gt;

&lt;p&gt;The overview wore one small badge: &lt;strong&gt;⚠ 1 host behind&lt;/strong&gt;. Not my GPU box that I touch daily — &lt;strong&gt;cloudy&lt;/strong&gt;, the Plex / Pi-hole / Samba box that just works and therefore never gets looked at.&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.amazonaws.com%2Fuploads%2Farticles%2Fvmubf7ggrelymn0st511.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.amazonaws.com%2Fuploads%2Farticles%2Fvmubf7ggrelymn0st511.png" alt="HomeLab Monitor — " width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The monitor also flags a release upgrade as available — I'm deferring that one regardless of which version it lands on (more below).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;UPDATES column: &lt;strong&gt;&lt;code&gt;47 pending · ⬆ 26.04 available&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh anakin@cloudy
&lt;span class="nv"&gt;$ &lt;/span&gt;lsb_release &lt;span class="nt"&gt;-ds&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
Ubuntu 22.04.5 LTS
5.15.0-179-generic          &lt;span class="c"&gt;# running — but 5.15.0-181 was already installed, waiting on a reboot&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; upgradable
47
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/run/reboot-required
&lt;span class="k"&gt;***&lt;/span&gt; System restart required &lt;span class="k"&gt;***&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nothing was broken — Plex streamed, Pi-hole resolved, shares mounted. That's the trap: &lt;strong&gt;a box that's 47 behind doesn't tell you.&lt;/strong&gt; Among the 47: &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;snapd&lt;/code&gt;, &lt;code&gt;apparmor&lt;/code&gt;, &lt;code&gt;nftables&lt;/code&gt;, &lt;code&gt;cloud-init&lt;/code&gt;, &lt;code&gt;linux-firmware&lt;/code&gt;, &lt;code&gt;openldap&lt;/code&gt;. Plenty of it security-relevant.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix (non-interactive, config-preserving)
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive &lt;span class="nv"&gt;NEEDRESTART_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a
apt-get update
apt-get &lt;span class="nt"&gt;-o&lt;/span&gt; Dpkg::Options::&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--force-confold"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-o&lt;/span&gt; Dpkg::Options::&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--force-confdef"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-y&lt;/span&gt; full-upgrade
apt-get &lt;span class="nt"&gt;-y&lt;/span&gt; autoremove &lt;span class="nt"&gt;--purge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--force-confold&lt;/code&gt; → keep my existing config files, don't stop to ask.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEEDRESTART_MODE=a&lt;/code&gt; → let &lt;code&gt;needrestart&lt;/code&gt; restart affected services itself instead of showing the blue full-screen menu that hangs an unattended run.&lt;/li&gt;
&lt;li&gt;Result: &lt;strong&gt;45 upgraded, 2 newly installed, 0 removed.&lt;/strong&gt; Clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then activate the kernel/systemd the box had been holding:&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="nv"&gt;$ &lt;/span&gt;reboot              &lt;span class="c"&gt;# ~90s of no DNS for the LAN — an on-purpose action, not a background one&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
5.15.0-181-generic    &lt;span class="c"&gt;# back on the tailnet, now on the staged kernel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before / after
&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.amazonaws.com%2Fuploads%2Farticles%2Fvmubf7ggrelymn0st511.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.amazonaws.com%2Fuploads%2Farticles%2Fvmubf7ggrelymn0st511.png" alt="cloudy before — 47 pending" width="800" height="507"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fchcqkc21yqgf6snzcds6.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.amazonaws.com%2Fuploads%2Farticles%2Fchcqkc21yqgf6snzcds6.png" alt="cloudy before — All updated" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;47 → 0.&lt;/strong&gt; The package badge cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did NOT run
&lt;/h2&gt;

&lt;p&gt;The monitor also flags a full Ubuntu &lt;strong&gt;release&lt;/strong&gt; upgrade waiting. &lt;code&gt;do-release-upgrade&lt;/code&gt; on a remote, headless, house-critical box is a scheduled-window job — with a backup and a console in reach — not an unattended one. The dashboard surfacing it is the win; choosing to defer it is the right call. So I left it flagged, on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;I'm not disciplined about my boring boxes — nobody is. The only reason this got caught is one badge in one dashboard I already look at. The tool is &lt;strong&gt;HomeLab Monitor&lt;/strong&gt; — one container, MIT, no Prometheus/Grafana to stand up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# github.com/SikamikanikoBG/homelab-monitor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When did you last log into your most reliable box, and how would you find out it was a month behind? Mine used a badge. What's watching yours — a cron &lt;code&gt;apt list --upgradable&lt;/code&gt;, &lt;code&gt;unattended-upgrades&lt;/code&gt; mail you actually read, or nothing? Genuinely curious which holds up for people.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>linux</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
