<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>mrsaynothing // Full-Stack Developer &amp; IT Support Engineer</title>
    <link>https://mrsaynothing.dev/en/blog</link>
    <description>mrsaynothing&apos;s blog</description>
    <language>en-us</language>
    <atom:link href="https://mrsaynothing.dev/rss.xml" rel="self" type="application/rss+xml" />

    <item>
      <title>Systemd Service Not Starting? How to Fix It</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-14/systemd-service-not-starting</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-14/systemd-service-not-starting</guid>
      <pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate>
      <description>Systemd service not starting? Read journalctl the right way, fix the five causes behind most failures, and get the unit back to active in minutes.</description>
      <content:encoded><![CDATA[<!--[--><p>A systemd service that won’t start is almost never mysterious. Run <code>systemctl status &lt;unit></code>, then read the last 50 journal lines for that unit with <code>journalctl -u &lt;unit> -n 50 --no-pager</code> — between the two, one of five causes is usually named outright: a bad path, a missing binary, wrong permissions, an SELinux/AppArmor denial, or a unit file syntax error. The failure reason is in the log; the fixes below are just pattern matching against it.</p> <h2>How do I see why a systemd service failed?</h2> <p>Status first, journal second:</p> <pre class="language-bash"><!----><code class="language-bash">systemctl status myapp.service
journalctl <span class="token parameter variable">-u</span> myapp.service <span class="token parameter variable">-n</span> <span class="token number">50</span> --no-pager</code><!----></pre> <p><code>status</code> gives you the state (<code>inactive (dead)</code>, <code>failed (exit-code)</code>, <code>activating (auto-restart)</code>) and the last few log lines. The journal gives you the full story: stdout, stderr, and systemd’s own complaints about the unit.</p> <p>If the unit failed before and you want the record of <em>that</em> run, add <code>-b</code> for the current boot or <code>--since today</code>:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-u</span> myapp.service <span class="token parameter variable">-b</span> --no-pager</code><!----></pre> <p>For the full toolkit — boots, priorities, following output live — see the <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">journalctl cheat sheet</a>. It’s the same muscle memory.</p> <p>One more pair worth knowing:</p> <pre class="language-bash"><!----><code class="language-bash">systemctl list-units <span class="token parameter variable">--failed</span>
systemctl reset-failed myapp.service</code><!----></pre> <p>The first lists every red unit on the box. The second clears the failed state after you’ve fixed it — cosmetic, but it stops status pages from crying wolf.</p> <h2>What are the most common causes?</h2> <p>After the log names the symptom, the cause is almost always one of these five:</p> <table><thead><tr><th>Log says</th><th>Likely cause</th><th>Fix</th></tr></thead><tbody><tr><td><code>status=203/EXEC</code></td><td>Wrong <code>ExecStart=</code> path or missing interpreter</td><td>Absolute path, <code>chmod +x</code>, check shebang</td></tr><tr><td><code>status=203/EXEC</code> on a script</td><td>Script has CRLF line endings or bad shebang</td><td><code>dos2unix script.sh</code>, fix first line</td></tr><tr><td><code>status=1/FAILURE</code>, no app log</td><td>Working directory or env var missing</td><td>Set <code>WorkingDirectory=</code>, add <code>Environment=</code></td></tr><tr><td><code>Permission denied</code></td><td>User can’t read files or bind the port</td><td>Fix ownership; ports below 1024 need <code>AmbientCapabilities=CAP_NET_BIND_SERVICE</code> or root</td></tr><tr><td><code>Unit is masked</code></td><td>Someone ran <code>systemctl mask</code></td><td><code>systemctl unmask myapp.service</code></td></tr><tr><td>Unit file edit “does nothing”</td><td>Daemon not reloaded</td><td><code>systemctl daemon-reload</code></td></tr></tbody></table> <p>The <code>203/EXEC</code> family deserves a special mention because it’s the one that eats afternoons. Systemd does not use your shell to launch <code>ExecStart=</code>. That means:</p> <pre class="language-ini"><!----><code class="language-ini"><span class="token comment"># Wrong — no shell, ~ never expands, no PATH lookup</span>
<span class="token key attr-name">ExecStart</span><span class="token punctuation">=</span><span class="token value attr-value">~/app/run.sh</span>

<span class="token comment"># Right</span>
<span class="token key attr-name">ExecStart</span><span class="token punctuation">=</span><span class="token value attr-value">/opt/app/run.sh</span></code><!----></pre> <p>And the script itself must be executable and start with a real shebang (<code>#!/bin/bash</code> or <code>#!/usr/bin/env bash</code>). A script that runs fine from your terminal but fails with 203 under systemd is nearly always one of: not executable, CRLF endings, a shebang pointing nowhere, or a relative path.</p> <h2>Why does it start manually but not on boot?</h2> <p>The classic ordering bug. If the log shows failures right after boot but the unit starts fine when you <code>systemctl start</code> it by hand, your service is losing a race — it’s reaching for the network, a mounted disk, or a database before that thing exists.</p> <p>The fix is to declare dependencies instead of hoping:</p> <pre class="language-ini"><!----><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Unit</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">After</span><span class="token punctuation">=</span><span class="token value attr-value">network-online.target postgresql.service</span>
<span class="token key attr-name">Wants</span><span class="token punctuation">=</span><span class="token value attr-value">network-online.target</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">ExecStartPre</span><span class="token punctuation">=</span><span class="token value attr-value">/usr/bin/test -f /opt/app/config.toml</span></code><!----></pre> <p><code>After=</code> orders the start; <code>Wants=</code> makes systemd actually bring the dependency up. <code>network-online.target</code> only works if the network-wait service is enabled on your distro, so check <code>systemctl is-enabled NetworkManager-wait-online.service</code> (or the systemd-networkd equivalent). The <code>ExecStartPre=</code> guard is a cheap, honest way to fail loudly with a readable message instead of a stack trace.</p> <p>A second variant: the service starts on boot but immediately dies. Look for things your interactive environment had that boot doesn’t — <code>PATH</code> differences, a virtualenv, a <code>HOME</code>. Set what you need explicitly:</p> <pre class="language-ini"><!----><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">Environment</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">PATH=/opt/app/venv/bin:/usr/bin</span>"</span>
<span class="token key attr-name">User</span><span class="token punctuation">=</span><span class="token value attr-value">appuser</span>
<span class="token key attr-name">WorkingDirectory</span><span class="token punctuation">=</span><span class="token value attr-value">/opt/app</span></code><!----></pre> <h2>Why is my service not logging to the journal?</h2> <p>If <code>journalctl -u</code> shows nothing, check three things in order:</p> <ol><li><code>StandardOutput=</code> and <code>StandardError=</code> in the unit — they must be <code>journal</code> (the default) or <code>journal+console</code>. Someone may have set them to <code>null</code>.</li> <li>The app writes to a file instead of stdout. Systemd only captures stdout/stderr; log-file writers bypass the journal entirely. Either point the app at stdout or read the file.</li> <li>Storage limits dropped old lines: <code>journalctl --disk-usage</code>, and <code>SystemMaxUse=</code> in <code>/etc/systemd/journald.conf</code> if the journal is choking.</li></ol> <p>For debugging the start itself, nothing beats dropping a shell into the boot-time context:</p> <pre class="language-ini"><!----><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">ExecStart</span><span class="token punctuation">=</span><span class="token value attr-value">/bin/bash -c 'exec /opt/app/bin/server 2>&amp;1'</span></code><!----></pre> <p>Or, for genuine mysteries, run the exact <code>ExecStart=</code> command as the unit’s user in a shell — most environment differences surface in the first ten seconds.</p> <h2>How do I make it restart automatically after a crash?</h2> <p>Default policy is <code>Restart=no</code>: a crashed service stays dead, and you find out from a user. Fix that per service:</p> <pre class="language-ini"><!----><code class="language-ini"><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">Restart</span><span class="token punctuation">=</span><span class="token value attr-value">on-failure</span>
<span class="token key attr-name">RestartSec</span><span class="token punctuation">=</span><span class="token value attr-value">5</span></code><!----></pre> <table><thead><tr><th>Setting</th><th>Restarts when</th></tr></thead><tbody><tr><td><code>no</code> (default)</td><td>Never</td></tr><tr><td><code>on-failure</code></td><td>Non-zero exit, signal, timeout</td></tr><tr><td><code>always</code></td><td>Any exit, even clean</td></tr><tr><td><code>on-watchdog</code></td><td>Watchdog timeout only</td></tr></tbody></table> <p>Pair <code>Restart=on-failure</code> with a start-rate guard so a crashing loop doesn’t hammer the box: <code>StartLimitIntervalSec=</code> and <code>StartLimitBurst=</code> in the <code>[Unit]</code> section. Five failures in sixty seconds should page a human, not spin a CPU.</p> <p>If you’re wiring this up as a scheduled job rather than a daemon, weigh <a href="/en/blog/2026-09-11/cron-vs-systemd-timer">cron vs systemd timer</a> first — timers give you journal logging and dependency ordering for free, which is exactly what this article keeps reaching for.</p> <h2>The 60-second checklist</h2> <ol><li><code>systemctl status &lt;unit></code> — read the state and last lines.</li> <li><code>journalctl -u &lt;unit> -n 50 --no-pager</code> — find the real error.</li> <li><code>203/EXEC</code>? Fix path, shebang, permissions. <code>Permission denied</code>? Fix user and file ownership.</li> <li>Fails only at boot? Add <code>After=</code>/<code>Wants=network-online.target</code> and an <code>ExecStartPre=</code> guard.</li> <li>Change a unit file? <code>systemctl daemon-reload &amp;&amp; systemctl restart &lt;unit></code>.</li> <li>Add <code>Restart=on-failure</code> so the next crash announces itself instead of hiding.</li></ol> <p>Most “systemd is complicated” moments reduce to <em>the error was in the journal the whole time</em>. Read it before you edit the unit file, not after.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>GGUF Quantization: Which Level Should You Use?</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-13/gguf-quantization-levels</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-13/gguf-quantization-levels</guid>
      <pubDate>Sun, 13 Sep 2026 00:00:00 GMT</pubDate>
      <description>GGUF quantization levels compared: Q4_K_M vs Q8_0 perplexity, file size math, VRAM per level, and one rule for picking the right quant for your GPU.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Pick Q4_K_M by default; go Q6_K or Q8_0 when you have VRAM to spare and need the last few percent of quality.</strong> GGUF quantization shrinks a model’s weights from 16 bits to fewer — Q4_K_M stores roughly 4.85 bits per weight, so a 7B model drops from ~14 GB to ~4.1 GB with perplexity typically less than 1% worse than the original. The question “which gguf quantization to use” has a boringly stable answer that most of the drama online obscures. Below: what quantization actually does to weights, how much quality each level costs, how to do the size math yourself, and one command to measure the damage on your own hardware instead of trusting a stranger’s benchmark.</p> <h2>What does GGUF quantization actually do?</h2> <p>A model is trained in 16-bit floating point (FP16 or BF16): every one of its billions of weights is a 2-byte number. Quantization compresses each weight into fewer bits. The naive way — round every weight to a 4-bit integer — destroys small-but-important values, so GGUF uses two tricks:</p> <ol><li><strong>Block scaling.</strong> Weights are grouped into blocks (usually 32), and each block gets its own scale factor. The 4-bit values are offsets within that block, so a wide range of magnitudes survives.</li> <li><strong>Importance-aware k-quants.</strong> The “K” in Q4_K_M means super-blocks of scales, plus treating attention and feed-forward layers differently from each other, because they tolerate compression unequally.</li></ol> <p>The “I” family (IQ4_XS and friends) goes further with information-theoretic codebooks borrowed from image compression. Same idea, fancier encoding: fewer bits per weight at similar quality, at the cost of slightly slower inference on some backends.</p> <p>One clarification that prevents most confusion: quantization changes <strong>only the stored weights</strong>. Architecture, tokenizer, and context handling are untouched. A Q4 file and a Q8 file of the same model are the same model, wearing different coats.</p> <h2>Q4 vs Q8: is higher quantization better?</h2> <p>Yes, technically; no, perceptually. Using llama.cpp’s own perplexity runs on Llama models as the reference: Q8_0 lands within ~0.02% of FP16 — for any practical purpose, lossless. Q6_K is near-indistinguishable. Q4_K_M gains roughly 1–2% perplexity, Q4_0 a bit more, and Q2_K is where coherent answers start falling apart on small models.</p> <p>Two rules the numbers imply:</p> <ul><li><strong>Model size buys quantization headroom.</strong> A 70B model survives Q2/Q3 far better than a 7B model does, because larger models are more redundant. Quantizing a 7B to Q2 is amputation; quantizing a 70B to Q3 is tailoring.</li> <li><strong>The quality floor moves with the task.</strong> Chat tolerates Q4. Exact code generation, math, and RAG over precise documents expose quantization noise sooner. If a Q4 model keeps writing subtly wrong code, test the same model at Q6_K before you blame the model.</li></ul> <table><thead><tr><th>Level</th><th>Bits/weight</th><th>Size vs FP16</th><th>Quality loss</th><th>Use it when</th></tr></thead><tbody><tr><td>Q2_K</td><td>~3.4</td><td>~21%</td><td>Severe on sub-13B</td><td>Nothing else fits, large models only</td></tr><tr><td>Q3_K_M</td><td>~3.9</td><td>~25%</td><td>Noticeable</td><td>Tight VRAM, ≥14B models</td></tr><tr><td>Q4_K_S</td><td>~4.6</td><td>~29%</td><td>Small</td><td>Q4_K_M won’t fit and it’s close</td></tr><tr><td><strong>Q4_K_M</strong></td><td>~4.85</td><td>~30%</td><td>~1% perplexity</td><td><strong>The default. Best quality/size trade</strong></td></tr><tr><td>Q5_K_M</td><td>~5.7</td><td>~35%</td><td>~0.5%</td><td>VRAM available, quality-critical tasks</td></tr><tr><td>Q6_K</td><td>~6.6</td><td>~41%</td><td>Near-nil</td><td>Code/math, still fits comfortably</td></tr><tr><td>Q8_0</td><td>~8.5</td><td>~53%</td><td>Effectively none</td><td>Reference runs, fine-tune bases</td></tr><tr><td>IQ4_XS</td><td>~4.3</td><td>~27%</td><td>≈Q4_K_M</td><td>Q4_K_M slightly too big, backend supports i-quants</td></tr></tbody></table> <h2>How much VRAM does each level need?</h2> <p>Do the size math yourself instead of memorizing tables — it is one line:</p> <pre class="language-undefined"><!----><code class="language-undefined">size_GB ≈ (bits_per_weight × params) / 8
# 8B model @ Q4_K_M: 4.85 × 8 / 8 ≈ 4.9 GB
# 8B model @ Q8_0:   8.50 × 8 / 8 ≈ 8.5 GB</code><!----></pre> <p>Then add the parts the formula leaves out: the KV cache (grows with context length — a few hundred MB to multiple GB), activations, and compute buffers. Practical margin: a “4.9 GB” model wants a 6 GB card at 4k context, and flash-attention plus KV-cache quantization to stay there at 16k. Weights are the headline, not the whole bill.</p> <h2>Which GGUF quantization should you use?</h2> <p>Decision order, no exceptions worth memorizing:</p> <ol><li><strong>Compute your context + KV budget first</strong>, then weights. Context you can’t fit is worse than quality you can’t measure.</li> <li><strong>Default to Q4_K_M.</strong> It is the community’s default for a reason — roughly 1% perplexity for 70% of the size. Every registry, including Ollama’s, ships it as the baseline.</li> <li><strong>Step up to Q6_K when the task punishes noise</strong>: code, math, extraction, anything you feed to a pipeline unattended.</li> <li><strong>Use Q8_0 only for reference</strong> — A/B tests, quantization-damage measurement, or a fine-tune base. As a daily driver it mostly buys warmth in your VRAM sensors.</li> <li><strong>Go below Q4 only under duress</strong>, and only on large models. Test with a known-hard prompt before trusting it.</li></ol> <p>If you are choosing which <em>file</em> to download on Hugging Face, prefer a single <code>Q4_K_M.gguf</code> over sharded splits unless the uploader only ships the latter — fewer moving parts. And if you are choosing <em>where</em> to run it, the engine choice is separate: see <a href="/en/blog/2026-09-10/llama-cpp-vs-ollama/">llama.cpp vs Ollama</a> for that axis.</p> <h2>How do you measure quantization damage yourself?</h2> <p>Benchmarks differ; your prompt is constant. Build llama.cpp once, download two levels of the same model, and measure both perplexity (lower is better) and tokens/second:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> clone https://github.com/ggml-org/llama.cpp <span class="token operator">&amp;&amp;</span> <span class="token builtin class-name">cd</span> llama.cpp
cmake <span class="token parameter variable">-B</span> build <span class="token operator">&amp;&amp;</span> cmake <span class="token parameter variable">--build</span> build <span class="token parameter variable">--config</span> Release <span class="token parameter variable">-j</span>

huggingface-cli download bartowski/Meta-Llama-3.1-8B-Instruct-GGUF <span class="token punctuation"></span>
  Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf Meta-Llama-3.1-8B-Instruct-Q8_0.gguf <span class="token punctuation"></span>
  --local-dir models

<span class="token comment"># perplexity on a wiki-text chunk (lower = closer to the original model)</span>
./build/bin/llama-perplexity <span class="token parameter variable">-m</span> models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf <span class="token parameter variable">-ngl</span> <span class="token number">99</span>
./build/bin/llama-perplexity <span class="token parameter variable">-m</span> models/Meta-Llama-3.1-8B-Instruct-Q8_0.gguf  <span class="token parameter variable">-ngl</span> <span class="token number">99</span>

<span class="token comment"># and speed on the same hardware</span>
./build/bin/llama-bench <span class="token parameter variable">-m</span> models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf <span class="token parameter variable">-ngl</span> <span class="token number">99</span></code><!----></pre> <p>The Q4_K_M number will be a few hundredths of a point worse than Q8_0 and the file will be ~40% smaller. If your downstream task can’t tell the difference — and for most, it can’t — you have your answer without reading anyone’s leaderboard.</p> <h2>Does quantization hurt privacy or local-only claims?</h2> <p>No — it is arithmetic on weights, entirely offline, and the quantized file is just a smaller container of the same parameters. Running a Q4_K_M locally leaks exactly as much (or little) as running the full-precision model locally: nothing leaves the machine. The privacy-relevant variable is <em>where inference runs</em>, not the bit width. The usual caveats about model provenance apply equally to every quant level: a stolen-base “uncensored” fine-tune in Q8 is not safer than the same weights in Q4. For the file-format side of this, see <a href="/en/blog/2026-09-07/how-to-run-gguf-models-locally/">how to run GGUF models locally</a>.</p> <h2>Which level should you pick?</h2> <p><strong>Q4_K_M, and stop reading forums about it.</strong> Upgrade to Q6_K for precision-hungry work if VRAM allows, keep one Q8_0 around for A/B comparisons, and treat anything under Q4 as an emergency ration for large models only. The one mistake worth avoiding is symmetric: worrying about Q4-vs-Q5 while ignoring context length, which wrecks more local setups than any quant ever did.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Git Cherry Pick: Multiple Commits, Branches, Conflicts</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-12/git-cherry-pick</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-12/git-cherry-pick</guid>
      <pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate>
      <description>Git cherry-pick explained: copy a commit from another branch, pick multiple commits or a range, fix conflicts, and know when merge or rebase fits better.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>TL;DR: <code>git cherry-pick &lt;sha></code> copies one commit from any branch onto your current branch — same patch, new SHA, no history moved.</strong> For several commits, list SHAs or use a range (<code>git cherry-pick A..B</code>); note that range excludes <code>A</code>, so write <code>A^..B</code> to include it. If it stops on a conflict, resolve, <code>git add</code>, then <code>git cherry-pick --continue</code>. Cherry-picking is for moving a <em>specific</em> fix — when you want everything from the other branch, merge or rebase instead.</p> <h2>What does git cherry-pick actually do?</h2> <p>Cherry-pick takes an existing commit and applies its diff as a <strong>new commit</strong> on your current branch. The original commit stays where it is; the copy gets a fresh SHA. Git does not “move” anything — people who later wonder why the commit still shows on the old branch are seeing exactly this.</p> <p>That copy-not-move model decides when cherry-pick is the right tool:</p> <ul><li>You need <strong>one fix</strong> from a feature branch on <code>main</code> now, without merging the rest.</li> <li>A hotfix committed on the wrong branch needs to land on the right one.</li> <li>A patch must be replayed onto a release branch that never merges from <code>main</code>.</li></ul> <p>The complementary skill is knowing how to back out a commit that turned out to be wrong — the mechanics of that are covered in <a href="/en/blog/2026-09-03/git-undo-last-commit">git undo last commit: keep the changes</a>.</p> <h2>How do I cherry-pick a commit from another branch?</h2> <p>Find the SHA, switch to the target branch, pick:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># 1. Locate the commit on the source branch</span>
<span class="token function">git</span> log feature/payment-fix <span class="token parameter variable">--oneline</span> <span class="token parameter variable">-5</span>

<span class="token comment"># 2. Switch to the branch that should receive it</span>
<span class="token function">git</span> switch main

<span class="token comment"># 3. Copy it over</span>
<span class="token function">git</span> cherry-pick 1a2b3c4</code><!----></pre> <p>Two conveniences worth knowing:</p> <ul><li><code>git cherry-pick &lt;branch></code> picks that branch’s <strong>tip</strong> commit — handy, but easy to do by accident with a stale mental model of what “the tip” is.</li> <li>After the pick, <code>git log -1 --stat</code> confirms what landed. One second of reading, saves a revert.</li></ul> <p>The commits stay on <code>feature/payment-fix</code>; delete that branch whenever you like — the picked copy on <code>main</code> has its own SHA and no dependency on the old one.</p> <h2>How do I cherry-pick multiple commits?</h2> <p>Three shapes, in order of how often you want them:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># 1. Explicit list — picked in the order you list them</span>
<span class="token function">git</span> cherry-pick 1a2b3c4 5d6e7f8

<span class="token comment"># 2. Range — everything after A up to and including B</span>
<span class="token function">git</span> cherry-pick A<span class="token punctuation">..</span>B

<span class="token comment"># 3. Range including A</span>
<span class="token function">git</span> cherry-pick A^<span class="token punctuation">..</span>B</code><!----></pre> <p>The <code>A..B</code> vs <code>A^..B</code> distinction is the classic surprise: <code>A..B</code> <strong>excludes</strong> <code>A</code>. If you visualize the range from <code>git log</code> and pick <code>oldest..newest</code>, you silently skip the oldest commit. When “the oldest few commits, in order” is the goal, write <code>oldest^..newest</code> and the off-by-one disappears.</p> <p>To fold several picks into one commit instead of three, stage without committing using <code>-n</code> / <code>--no-commit</code>, then commit once:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> cherry-pick <span class="token parameter variable">-n</span> 1a2b3c4 5d6e7f8
<span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">"Backport: payment retry fixes"</span></code><!----></pre> <h2>Why is git cherry-pick not working?</h2> <p>Four real causes, in order of frequency:</p> <p><strong>1. A conflict stopped the pick.</strong> Git applies the patch, hits a line that changed on both branches, and pauses mid-sequence:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># resolve the files, then:</span>
<span class="token function">git</span> <span class="token function">add</span> <span class="token operator">&lt;</span>resolved-files<span class="token operator">></span>
<span class="token function">git</span> cherry-pick <span class="token parameter variable">--continue</span>   <span class="token comment"># or --abort to return to the pre-pick state</span></code><!----></pre> <p><code>--continue</code> is not optional and not implied — until you run it, you are inside a paused cherry-pick sequence and <code>git status</code> will keep saying so.</p> <p><strong>2. The pick is empty (“The previous cherry-pick is now empty”).</strong> The change already exists on this branch, often from an earlier pick or a squashed merge. Skip it with <code>git cherry-pick --skip</code>, or force an empty commit with <code>--allow-empty</code> if you genuinely need the marker.</p> <p><strong>3. The commit is a merge commit.</strong> A merge has two parents, so “apply this diff” is ambiguous — git refuses rather than guess. Say which parent you are diffing against:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> cherry-pick <span class="token parameter variable">-m</span> <span class="token number">1</span> <span class="token operator">&lt;</span>merge-sha<span class="token operator">></span>   <span class="token comment"># parent 1 = the branch you merged *into*</span></code><!----></pre> <p><strong>4. Wrong working tree or detached HEAD.</strong> The pick lands wherever HEAD points. <code>git branch --show-current</code> before picking; if it prints nothing, you are in detached HEAD and the commit will be orphaned when you switch away.</p> <h2>Cherry-pick vs merge vs rebase: which one when?</h2> <table><thead><tr><th>Command</th><th>What lands on the target</th><th>History</th><th>Use when</th></tr></thead><tbody><tr><td><code>git cherry-pick &lt;sha></code></td><td>Named commit(s) only</td><td>Copy, new SHAs</td><td>One specific fix must move now</td></tr><tr><td><code>git merge &lt;branch></code></td><td>Everything on the branch</td><td>Merge commit or fast-forward</td><td>You want the whole branch, divergence visible</td></tr><tr><td><code>git rebase &lt;base></code></td><td>All branch commits, replayed</td><td>Linear, rewritten SHAs</td><td>You want the branch as a clean linear run</td></tr><tr><td><code>git revert &lt;sha></code></td><td>Inverse of a commit</td><td>Adds an undo commit</td><td>A landed commit must be undone on shared history</td></tr></tbody></table> <p>The one-line rule: <strong>cherry-pick moves a selection; merge and rebase move everything.</strong> Reaching for cherry-pick to “sync” with a branch is a sign you actually want a merge — and if the branch is your fork’s <code>main</code> versus upstream, the full routine is in <a href="/en/blog/2026-09-06/git-sync-fork-with-upstream">sync a fork with upstream, step by step</a>.</p> <p>One habit to skip: cherry-picking the same commit into several branches long-term. Every future fix on the source branch needs another pick, and eventually the branches drift. Backports to release branches are a normal pattern; a permanent parallel universe is not.</p> <h2>The cherry-pick workflow, condensed</h2> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> log <span class="token operator">&lt;</span>source-branch<span class="token operator">></span> <span class="token parameter variable">--oneline</span> <span class="token parameter variable">-5</span>   <span class="token comment"># find the SHA(s)</span>
<span class="token function">git</span> switch <span class="token operator">&lt;</span>target-branch<span class="token operator">></span>             <span class="token comment"># land in the right place</span>
<span class="token function">git</span> cherry-pick A^<span class="token punctuation">..</span>B                  <span class="token comment"># range, list, or single SHA</span>
<span class="token comment"># on conflict: resolve → git add → git cherry-pick --continue</span>
<span class="token function">git</span> log <span class="token parameter variable">-1</span> <span class="token parameter variable">--stat</span>                      <span class="token comment"># confirm what landed</span></code><!----></pre> <p>Find, switch, pick, verify. The command has a reputation for danger it does not deserve — the diff either applies or it stops and tells you why. The only genuinely destructive mistake is picking into the wrong branch, and <code>git log -1</code> before you push makes that one hard to miss.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Cron vs systemd timers: which should you use?</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-11/cron-vs-systemd-timer</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-11/cron-vs-systemd-timer</guid>
      <pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate>
      <description>Cron vs systemd timers compared: syntax, logging, catch-up runs and dependencies — plus a decision table so your next scheduled job lands in the right place.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>On a modern distro, use a systemd timer for anything you own and keep cron for one-line user jobs and servers you did not set up yourself.</strong> Timers log every run to the journal, can catch up on schedules missed while the machine was off, and depend on the same unit files as everything else on the system. Cron wins on brevity — one <code>crontab -e</code> line beats two unit files — and it is still the only scheduler guaranteed to exist on minimal containers and exotic Unix boxes. The catch: cron fails silently. If your job errors at 3 a.m., cron mails a local mailbox nobody reads, while a timer gives you <code>journalctl -u mytimer.service</code> with the full output. Below: the real differences, how to test each one, why a timer might not trigger, and a decision table.</p> <h2>What is the difference between cron and a systemd timer?</h2> <p>Cron is a daemon that reads a table of lines — five time fields and a command — and runs each command when the wall clock matches. That is the whole model. It has no concept of a job as an object: no unit, no status, no dependencies, no log entry of its own.</p> <p>A systemd timer is a unit file (<code>foo.timer</code>) that fires another unit file (<code>foo.service</code>) when its schedule matches. The job is a first-class object with <code>start</code>, <code>status</code>, <code>logs</code>, and failure tracking like any other service. Scheduling is either calendar-based (cron-like) or monotonic (<code>OnBootSec=15min</code>, which wall-clock changes cannot confuse).</p> <p>The practical consequences:</p> <ul><li><strong>Logging</strong>: timers log stdout/stderr to the journal per unit; cron at best mails the local user.</li> <li><strong>Missed runs</strong>: a timer with <code>Persistent=true</code> runs once on boot if its schedule was missed; cron just skips.</li> <li><strong>Dependencies</strong>: timers can wait for <code>network-online.target</code> or mount points; cron needs you to hand-roll retry logic in the script.</li> <li><strong>Syntax</strong>: cron is one line; a timer is two files. That is the entire cost of switching.</li></ul> <h2>Which schedule syntax is easier — crontab or OnCalendar?</h2> <p>Cron’s five fields are the compact incumbent: <code>*/15 * * * *</code> is every 15 minutes, and most admins can read them in their sleep. Systemd’s <code>OnCalendar=</code> is more verbose but strictly more expressive, and <code>systemd-analyze calendar</code> will tell you the next runs before you commit — cron has no equivalent dry-run.</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Cron: every day at 03:30</span>
<span class="token number">30</span> <span class="token number">3</span> * * * /usr/local/bin/backup.sh

<span class="token comment"># systemd: the same schedule, verifiable before you save it</span>
systemd-analyze calendar <span class="token string">"*-*-* 03:30:00"</span>
<span class="token comment"># -> Next elapse: Fri 2026-09-11 03:30:00 ...</span></code><!----></pre> <p><code>OnCalendar</code> handles things cron genuinely cannot express cleanly: <code>Mon..Fri *-*-* 09..17:00:00</code> (weekdays, business hours), or <code>*:0/15</code> with <code>RandomizedDelaySec=10m</code> to stop a thousand machines from hammering a server at the same instant.</p> <h2>How do I test a systemd timer without waiting?</h2> <p>Three commands answer everything. List what is scheduled and when it next fires, run the job by hand exactly as the timer would, then read its logs:</p> <pre class="language-bash"><!----><code class="language-bash">systemctl list-timers <span class="token parameter variable">--all</span>                 <span class="token comment"># every timer, next + last run</span>
<span class="token function">sudo</span> systemctl start backup.service         <span class="token comment"># fire the job now, same unit as the timer</span>
journalctl <span class="token parameter variable">-u</span> backup.service <span class="token parameter variable">-f</span>             <span class="token comment"># watch its output live</span></code><!----></pre> <p>Note the split: <code>systemctl start backup.timer</code> arms the schedule; the <em>service</em> is the job. If <code>list-timers</code> shows your timer, <code>systemctl status backup.service</code> is green, and the journal shows your output, the whole chain works.</p> <h2>How do I run a cron job manually?</h2> <p>Cron jobs run with a stripped environment, which is why “works in my shell, fails in cron” is a genre of bug. To reproduce cron faithfully, run the command through <code>sh</code> with the same environment cron would use:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">crontab</span> <span class="token parameter variable">-l</span>                                  <span class="token comment"># confirm the exact line</span>
<span class="token function">env</span> <span class="token parameter variable">-i</span> <span class="token assign-left variable"><span class="token environment constant">SHELL</span></span><span class="token operator">=</span>/bin/sh <span class="token assign-left variable"><span class="token environment constant">PATH</span></span><span class="token operator">=</span>/usr/bin:/bin <span class="token function">sh</span> <span class="token parameter variable">-c</span> <span class="token string">'/usr/local/bin/backup.sh'</span>
<span class="token function">grep</span> CRON /var/log/syslog <span class="token operator">|</span> <span class="token function">tail</span>            <span class="token comment"># did cron even fire it? (Debian/Ubuntu)</span>
journalctl <span class="token parameter variable">-u</span> <span class="token function">cron</span> <span class="token parameter variable">-n</span> <span class="token number">20</span>                    <span class="token comment"># same, on systemd distros</span></code><!----></pre> <p>That <code>env -i</code> line is the honest manual test: a bare environment with cron’s default <code>PATH</code>. Most silent cron failures are a bare <code>PATH</code> or an unquoted <code>%</code> in the command (cron treats <code>%</code> as a newline), and both show up immediately this way.</p> <h2>Why is my systemd timer not triggering?</h2> <p>Four causes cover almost every case, in the order to check them:</p> <ol><li><strong>Service and timer names do not match.</strong> <code>foo.timer</code> fires <code>foo.service</code> — a typo’d <code>OnFailure</code> or a renamed unit means the timer “runs” into nothing. <code>systemctl cat foo.timer</code> shows exactly what it targets.</li> <li><strong>The timer was edited but not reloaded.</strong> After changing a unit file: <code>sudo systemctl daemon-reload &amp;&amp; sudo systemctl restart foo.timer</code>. Without this your new schedule is not live.</li> <li><strong><code>Persistent=true</code> without <code>OnCalendar</code> semantics you expect</strong> — or you are checking <code>systemctl status foo.timer</code> (always shows active even when idle) instead of <code>list-timers</code>.</li> <li><strong>The service it fires is failing instantly</strong>, so the timer looks dead. <code>journalctl -u foo.service --since -1h</code> will show the crash that <code>list-timers</code> hides.</li></ol> <p>And when you do look at the logs, the <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">journalctl cheat sheet</a> covers the filters (<code>-u</code>, <code>--since</code>, <code>-f</code>) that make this fast.</p> <h2>Cron vs systemd timers: the decision table</h2> <table><thead><tr><th></th><th>cron</th><th>systemd timer</th></tr></thead><tbody><tr><td>Setup</td><td>one <code>crontab</code> line</td><td>two unit files</td></tr><tr><td>Logging</td><td>local mail, usually unread</td><td>journal, per unit</td></tr><tr><td>Missed run (machine off)</td><td>skipped</td><td>runs once with <code>Persistent=true</code></td></tr><tr><td>Dependencies / ordering</td><td>none — DIY in script</td><td>full unit dependencies</td></tr><tr><td>Randomised delay</td><td>DIY with <code>$RANDOM</code></td><td><code>RandomizedDelaySec=</code></td></tr><tr><td>Test/dry-run schedule</td><td>no</td><td><code>systemd-analyze calendar</code></td></tr><tr><td>Exists everywhere</td><td>yes — containers, BSDs, embedded</td><td>needs systemd (PID 1)</td></tr><tr><td>User jobs without root</td><td><code>crontab -e</code></td><td><code>systemd --user</code> units</td></tr></tbody></table> <p><strong>Rules of thumb</strong>: shipping a job on your own systemd machine → timer. Quick personal reminder or a box you did not build → cron. Anything with dependencies, retries, or a need to know whether it actually ran → timer, always. A common pattern is a timer running a maintenance job — say a nightly <a href="/en/blog/2026-09-08/rsync-vs-scp">rsync backup</a> — where <code>Persistent=true</code> guarantees the backup happens even if the machine was asleep at the scheduled minute, which cron simply cannot offer.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>llama.cpp vs Ollama: Which Should You Run in 2026?</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-10/llama-cpp-vs-ollama</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-10/llama-cpp-vs-ollama</guid>
      <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
      <description>llama.cpp vs Ollama: Ollama wraps llama.cpp for convenience, raw llama.cpp wins on speed and control. Benchmarks, GPU offload flags, and when each wins.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Raw llama.cpp if you want maximum tokens/second and full control; Ollama if you want a one-command install and an API server that works out of the box.</strong> Ollama is not a competing engine — it is a Go service that bundles llama.cpp as its inference backend, adds model management (<code>ollama pull llama3.1</code>), and serves a REST API on port 11434. So the real question is not “which engine is faster” but “how much control do you want over that engine’s knobs”. Because Ollama ships conservative defaults (Q4_K_M quantization, modest context, no flash attention until recently), identical hardware can produce noticeably different numbers. Below: what actually differs, where the speed gap comes from, the same model running in both, and a decision table.</p> <h2>What is the difference between llama.cpp and Ollama?</h2> <p><strong>llama.cpp</strong> is the inference engine: a single C/C++ project from GGUF creator Georgi Gerganov that runs quantized models on CPU, GPU, or a mix of both. It gives you <code>llama-cli</code> for one-shot prompts and <code>llama-server</code> — an OpenAI-compatible HTTP server — plus every tuning flag the engine supports: GPU layer offload, KV-cache quantization, speculative decoding, custom samplers.</p> <p><strong>Ollama</strong> is a product layered on top of that engine. It forks and vendors llama.cpp, then wraps it in:</p> <ul><li>a model registry (<code>ollama pull</code>, <code>ollama list</code>) with automatic GGUF weight splitting,</li> <li>a Modelfile system (a Dockerfile-like spec for prompt templates and parameters),</li> <li>a background daemon that keeps models warm in VRAM and exposes its own REST API,</li> <li>automatic hardware detection with safe defaults.</li></ul> <p>The practical consequence: with Ollama you manage <em>models</em>; with llama.cpp you manage <em>inference</em>. If you have ever wanted to change the quantization format, quantize the KV cache, raise context past the default, or pin specific layers to the GPU, that is llama.cpp territory. Ollama hides most of those dials — deliberately.</p> <table><thead><tr><th></th><th>llama.cpp</th><th>Ollama</th></tr></thead><tbody><tr><td>What it is</td><td>Inference engine (C/C++)</td><td>Service wrapping llama.cpp</td></tr><tr><td>Install</td><td>Build from source or package</td><td>One-line installer, single binary</td></tr><tr><td>Run a model</td><td><code>llama-cli -m model.gguf</code> + flags</td><td><code>ollama run llama3.1</code></td></tr><tr><td>API</td><td>OpenAI-compatible (<code>llama-server</code>)</td><td>Own REST + OpenAI-compatible endpoint</td></tr><tr><td>Model management</td><td>You fetch GGUF files yourself</td><td>Registry: pull/list/rm</td></tr><tr><td>Defaults</td><td>You choose everything</td><td>Safe: Q4_K_M, modest context</td></tr><tr><td>Engine updates</td><td>Day-one (upstream)</td><td>Lag upstream releases</td></tr><tr><td>Tuning depth</td><td>Full (KV quant, spec decode, samplers)</td><td>Limited passthrough</td></tr><tr><td>Best for</td><td>Performance work, servers, edge devices</td><td>Getting started, dev laptops</td></tr></tbody></table> <h2>Is llama.cpp faster than Ollama?</h2> <p>On the same GGUF file, same quantization, same context, and the same llama.cpp version — <strong>no, they are within noise of each other</strong>, because Ollama <em>is</em> llama.cpp doing the math. Every “Ollama is 30% slower” benchmark you see is really a comparison of defaults. The gap comes from three places:</p> <ol><li><strong>Quantization choice.</strong> Ollama’s registry defaults to Q4_K_M. Run the same model as Q5_K_M or Q6_K from llama.cpp and you get better quality per token at a similar speed — or choose Q4_0/IQ4 for raw speed.</li> <li><strong>Flash attention and KV-cache quantization.</strong> <code>--flash-attn</code> plus <code>-ctk q8_0 -ctv q8_0</code> shrinks the KV cache dramatically, which raises tokens/second at long context and lets you fit bigger contexts in the same VRAM. Ollama only exposes some of this.</li> <li><strong>Version lag.</strong> llama.cpp lands kernel optimizations weekly; Ollama merges upstream on its own schedule. A fresh llama.cpp build can be measurably faster than a months-old Ollama binary on the same box — until Ollama catches up.</li></ol> <p>Quick benchmark command, engine-agnostic — it reports prompt evaluation and generation speed:</p> <pre class="language-bash"><!----><code class="language-bash">./build/bin/llama-bench <span class="token parameter variable">-m</span> Llama-3.1-8B-Instruct-Q4_K_M.gguf <span class="token parameter variable">-ngl</span> <span class="token number">99</span> <span class="token parameter variable">-fa</span> <span class="token number">1</span></code><!----></pre> <p>Run it against Ollama’s own model file (<code>~/.ollama/models/blobs/...</code>, renamed to <code>.gguf</code>) and you will usually match Ollama’s numbers exactly — then beat them by adding <code>-ctk q8_0</code> at 16k context.</p> <h2>How do you run the same model in both?</h2> <p>Both consume GGUF. Minimal end-to-end for each:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># --- Ollama path: install, pull, serve ---</span>
<span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://ollama.com/install.sh <span class="token operator">|</span> <span class="token function">sh</span>
ollama run llama3.1:8b        <span class="token comment"># downloads Q4_K_M, loads into VRAM, opens a chat</span>

<span class="token comment"># its API, OpenAI-compatible style:</span>
<span class="token function">curl</span> <span class="token parameter variable">-s</span> http://localhost:11434/v1/chat/completions <span class="token parameter variable">-d</span> <span class="token string">'&#123;
  "model": "llama3.1:8b",
  "messages": [&#123;"role": "user", "content": "Say hi in 5 words"&#125;]
&#125;'</span></code><!----></pre> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># --- llama.cpp path: build, download GGUF, serve ---</span>
<span class="token function">git</span> clone https://github.com/ggml-org/llama.cpp <span class="token operator">&amp;&amp;</span> <span class="token builtin class-name">cd</span> llama.cpp
cmake <span class="token parameter variable">-B</span> build <span class="token parameter variable">-DGGML_CUDA</span><span class="token operator">=</span>ON    <span class="token comment"># or -DGGML_VULKAN=ON / -DGGML_HIP=ON</span>
cmake <span class="token parameter variable">--build</span> build <span class="token parameter variable">--config</span> Release <span class="token parameter variable">-j</span>

huggingface-cli download bartowski/Meta-Llama-3.1-8B-Instruct-GGUF <span class="token punctuation"></span>
  Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf --local-dir models

./build/bin/llama-server <span class="token parameter variable">-m</span> models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf <span class="token punctuation"></span>
  <span class="token parameter variable">-ngl</span> <span class="token number">99</span> --ctx-size <span class="token number">16384</span> --flash-attn <span class="token parameter variable">-ctk</span> q8_0 <span class="token parameter variable">-ctv</span> q8_0 <span class="token parameter variable">--port</span> <span class="token number">8080</span></code><!----></pre> <p><code>llama-server</code> exposes the OpenAI Chat Completions schema, so the same <code>curl</code> against <code>http://localhost:8080/v1/chat/completions</code> works unchanged. Any tool built for the OpenAI API — scripts, editors, RAG pipelines — can point at either. The flags do the real work: <code>-ngl 99</code> offloads every layer to the GPU, <code>--flash-attn</code> plus the <code>-ctk/-ctv</code> pair keeps a 16k context inside an 8 GB card that Ollama’s defaults would refuse.</p> <p>For picking the GGUF file itself and what the quant labels mean, see <a href="/en/blog/2026-09-07/how-to-run-gguf-models-locally/">how to run GGUF models locally</a>.</p> <h2>When does Ollama make more sense?</h2> <p>Most people should start with Ollama, and that is not a consolation prize:</p> <ul><li><strong>You want it working tonight.</strong> One command, model pulled, API up. llama.cpp means choosing a backend (CUDA/Vulkan/HIP/Metal), building, and fetching weights by hand.</li> <li><strong>You juggle many models.</strong> The registry, automatic unloading, and Modelfiles beat hand-managing directories of GGUF files.</li> <li><strong>Your machine is modest.</strong> Ollama’s defaults are conservative for a reason — they almost always fit and run.</li> <li><strong>You want a stable API surface.</strong> Ollama’s daemon manages model lifecycles so a long-running service does not have to.</li></ul> <p>Choose raw llama.cpp when you are benchmarking, serving at any scale, running on a phone or a Raspberry Pi, need long context on small VRAM, or want a feature the day it merges upstream. Power users often run both: Ollama for daily driver models, a pinned llama.cpp build for the one workload that needs the last 20%.</p> <p>If your comparison is really between desktop GUI apps, that is a different axis — see <a href="/en/blog/2026-09-01/ollama-vs-lm-studio/">Ollama vs LM Studio</a> — and for engine choice per task, <a href="/en/blog/2026-09-04/best-local-llm-for-coding/">the best local LLMs for coding</a> covers the model side.</p> <h2>Which should you use?</h2> <p><strong>Decide by control, not speed.</strong> The engines are the same; the defaults are not. Install Ollama if “it runs and serves an API” is the goal — you lose a few knobs you were not going to turn anyway. Build llama.cpp if tokens/second, context length, or quantization control is the goal — you get every knob, at the cost of managing models yourself. Either way you are running the same GGUF files, and switching later costs an afternoon, not a rewrite.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Git Remove Untracked Files: Safe git clean Guide</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-09/git-remove-untracked-files</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-09/git-remove-untracked-files</guid>
      <pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate>
      <description>Git remove untracked files safely with git clean: dry-run first, -fd for directories, -x for ignored files — plus why git clean isn&apos;t removing anything.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>TL;DR: to remove untracked files in git, run <code>git clean -fd</code> — but always preview with <code>git clean -nd</code> first, because clean deletes permanently and they never hit the trash.</strong> Use <code>-f</code> for files, <code>-fd</code> for files and directories, and <code>-fdx</code> if gitignored build output should go too. The single most common complaint — “git clean is not removing my untracked files” — almost always means the files live inside an untracked <em>directory</em> (add <code>-d</code>) or they are ignored files (add <code>-x</code>). Untracked clutter is the normal byproduct of experiments, builds and clone-adjacent scripts; this guide shows how to preview every deletion, remove files from one directory only, and which combinations to never run in a repo you care about.</p> <h2>What does “untracked files” mean in git status?</h2> <p>Untracked means git sees the file on disk but has never been told to track it — it is not in the index and has no commit history. <code>git status</code> groups everything into three buckets:</p> <pre class="language-bash"><!----><code class="language-bash">$ <span class="token function">git</span> status <span class="token parameter variable">--short</span>
 M src/app.ts        <span class="token comment"># modified: tracked, changed</span>
?? notes.txt         <span class="token comment"># untracked: new file git doesn't know</span>
?? build/            <span class="token comment"># untracked directory: entirely new to git</span></code><!----></pre> <p>That distinction matters because each bucket needs a different removal tool. Tracked-but-changed files are reverted with <code>git restore</code> or committed away — <code>git clean</code> will not touch them. Only the <code>??</code> lines are <code>git clean</code> territory. Ignored files (anything matched by <code>.gitignore</code>) are a hidden fourth bucket: they do not even show as <code>??</code>, and <code>git clean</code> skips them unless you explicitly opt in with <code>-x</code>.</p> <p>If your real problem is a <em>tracked</em> file that should never have been committed, cleaning is the wrong tool — that is a <code>git rm --cached</code> job, or a reset of the last commit as covered in <a href="/en/blog/2026-09-03/git-undo-last-commit">git undo last commit: keep the changes</a>.</p> <h2>How do I remove untracked files in git?</h2> <p>The core command is <code>git clean -f</code>. Without <code>-f</code> git refuses to delete anything and just prints a warning — a deliberate safety rail. The full routine looks like this:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># 1. See exactly what will be deleted (dry run — deletes nothing)</span>
<span class="token function">git</span> clean <span class="token parameter variable">-nd</span>

<span class="token comment"># Would remove:</span>
<span class="token comment"># notes.txt</span>
<span class="token comment"># build/</span>
<span class="token comment"># scratch/</span>

<span class="token comment"># 2. Confirm nothing precious is in the list, then delete for real</span>
<span class="token function">git</span> clean <span class="token parameter variable">-fd</span></code><!----></pre> <p>Flag by flag:</p> <ul><li><code>-f</code> / <code>--force</code> — required. Actually deletes untracked files.</li> <li><code>-d</code> — recurse into untracked <strong>directories</strong>. Plain <code>-f</code> only removes untracked files at the top level and reports the directories it refused to touch.</li> <li><code>-n</code> / <code>--dry-run</code> — show what <em>would</em> be removed. Always run this first.</li> <li><code>-x</code> — also delete <strong>ignored</strong> files (<code>node_modules</code>, build output, <code>.env</code>).</li> <li><code>-X</code> — delete <strong>only</strong> ignored files, keeping untracked-but-not-ignored ones.</li> <li><code>-i</code> — interactive mode; useful when the dry-run list is long.</li></ul> <p>A habit worth copying: treat <code>git clean -nd</code> like <code>git diff</code> — you look before you commit, you look before you clean.</p> <h2>Why is git clean not removing my untracked files?</h2> <p>Three real causes, in order of how often they bite:</p> <p><strong>1. The files are inside an untracked directory.</strong> With only <code>-f</code>, git removes loose untracked files but stops at directories, even reporting <code>Would remove build/</code> without deleting it in a real run. Add <code>-d</code>:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> clean <span class="token parameter variable">-fd</span></code><!----></pre> <p><strong>2. The files are gitignored.</strong> <code>node_modules/</code>, <code>dist/</code>, <code>.venv/</code> — ignored paths are invisible to a plain clean. The dry run will not list them, and neither will the clean remove them. Opt in explicitly:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> clean <span class="token parameter variable">-fdx</span>   <span class="token comment"># untracked + ignored files and directories</span></code><!----></pre> <p><strong>3. A nested git repository or submodule is in the way.</strong> Git never deletes another repo’s contents from the outside. Remove the submodule properly or pass <code>--force</code> twice (<code>git clean -ffd</code>), and prefer the first option.</p> <p>If the dry run lists nothing but <code>git status</code> still shows <code>??</code> entries, you are probably in the wrong working tree — run <code>git rev-parse --show-toplevel</code> and check you are inside the repo you meant to clean.</p> <h2>How do I remove untracked files from a specific directory only?</h2> <p>Scope the clean by passing a path — everything else is left alone:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> clean <span class="token parameter variable">-fd</span> build/          <span class="token comment"># only inside build/</span>
<span class="token function">git</span> clean <span class="token parameter variable">-fd</span> src/generated   <span class="token comment"># one specific tree</span></code><!----></pre> <p>This is the answer to “I want to remove untracked files and folders in <code>build/</code> but keep my scratch notes in the repo root”. The path is relative to your current directory, so running from the repo root scopes to the whole repo; running from a subdirectory scopes to that subtree.</p> <h2>How do I remove untracked files without deleting them?</h2> <p>When the dry run shows files you might want later, do not gamble — preserve first, clean second:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Stash untracked files (including ignored ones with -a) without deleting</span>
<span class="token function">git</span> stash push --include-untracked
<span class="token function">git</span> clean <span class="token parameter variable">-fd</span>                      <span class="token comment"># tree is clean</span>
<span class="token function">git</span> stash pop                      <span class="token comment"># bring them back when needed</span></code><!----></pre> <p><code>git stash -u</code> moves untracked files out of the tree but keeps them recoverable — that is the “remove without deleting” semantics people are actually looking for. For a preview you can keep, <code>git clean -nd > clean-plan.txt</code> gives you the exact list before committing to anything. There is no undo after <code>git clean -f</code>: deleted means gone.</p> <h2>git clean vs git rm vs git restore: which one when?</h2> <table><thead><tr><th>Command</th><th>Touches</th><th>Removes from disk</th><th>Use when</th></tr></thead><tbody><tr><td><code>git clean -fd</code></td><td>Untracked files/dirs</td><td>Yes</td><td>Delete files git has never tracked</td></tr><tr><td><code>git clean -fdx</code></td><td>Untracked <strong>+ ignored</strong></td><td>Yes</td><td>Full reset including <code>node_modules</code>, build output</td></tr><tr><td><code>git rm &lt;file></code></td><td>Tracked files</td><td>Yes (staged)</td><td>Delete a file <em>and</em> record the deletion in git</td></tr><tr><td><code>git rm --cached &lt;file></code></td><td>Tracked files</td><td>No</td><td>Stop tracking a file, keep it on disk</td></tr><tr><td><code>git restore &lt;file></code></td><td>Tracked files</td><td>No</td><td>Discard local edits, keep the file</td></tr></tbody></table> <p>The one-line rule: <strong>clean manages what git doesn’t know about; rm and restore manage what it does.</strong> Mixing them up is how people lose work — running <code>git clean -fdx</code> while believing it behaves like <code>git restore</code>.</p> <h2>What should you never run git clean on?</h2> <p>Two habits to avoid outright:</p> <ol><li><strong>Never run <code>git clean -fdx</code> blindly in a monorepo or workspace.</strong> It deletes every ignored directory — that is every <code>node_modules</code>, every virtualenv, every local <code>.env</code> in the tree. Regaining them can mean an hour of reinstallation, and a deleted <code>.env</code> may not be recoverable at all.</li> <li><strong>Never alias clean with force baked in.</strong> <code>git config alias.wipe "clean -fd"</code> feels efficient until you typo the path. Keep the dry run one keystroke away (<code>git clean -nd</code>) and make it a two-command ritual, preview then delete.</li></ol> <p>Also worth knowing: cleaning untracked files right before a <code>[sync a fork with upstream]</code> pull keeps the merge surface small — a tidy tree is the cheapest conflict insurance there is: <a href="/en/blog/2026-09-06/git-sync-fork-with-upstream">sync a fork with upstream, step by step</a>.</p> <h2>The safe git clean workflow, condensed</h2> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> status <span class="token parameter variable">--short</span>        <span class="token comment"># what's in the tree?</span>
<span class="token function">git</span> clean <span class="token parameter variable">-nd</span>             <span class="token comment"># preview: what WOULD go?</span>
<span class="token function">git</span> clean <span class="token parameter variable">-fd</span>             <span class="token comment"># delete untracked files + directories</span>
<span class="token function">git</span> clean <span class="token parameter variable">-fdX</span>            <span class="token comment"># (optional) clear only ignored build output</span>
<span class="token function">git</span> status <span class="token parameter variable">--short</span>        <span class="token comment"># confirm: working tree clean</span></code><!----></pre> <p>Preview, delete, verify — thirty seconds, zero regret, and <code>git status</code> finally reads clean again.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Rsync vs SCP: Which Linux Copy Command to Use</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-08/rsync-vs-scp</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-08/rsync-vs-scp</guid>
      <pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate>
      <description>Rsync vs scp: rsync resumes a dropped transfer and copies only what changed — scp just copies. Speed numbers, the flags that matter, and when scp still wins.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Use <code>rsync</code> for anything bigger than a quick one-off copy, and <code>scp</code> when you just want a file on another box right now.</strong> The core difference: <code>scp</code> streams the whole file again every time and has no memory of a dropped connection, while <code>rsync</code> compares source and destination, transfers only the changed blocks, and resumes an interrupted copy where it stopped. On a large backup over a flaky link that is the gap between two minutes and starting over. Both ship with OpenSSH on virtually every Linux distribution, so this is a habit choice, not an install choice — and the habit should default to rsync. Below: a straight comparison table, real speed differences, the resume trick scp cannot do, and the cases where scp is still the right answer.</p> <h2>What is the difference between rsync and scp?</h2> <p><code>scp</code> does one thing: open an SSH channel, stream the bytes, close. It has no state between runs, so if the transfer dies at 90% you restart from zero.</p> <p><code>rsync</code> is a synchronisation tool that happens to use SSH as its transport. Before sending, it builds a checksum list of the destination file (the rolling-checksum delta algorithm) and transmits only the blocks that differ. Run the same command twice and the second pass moves almost nothing. That also makes rsync the natural tool for keeping two directories in sync — schedule it and each run copies just the deltas.</p> <p>The practical consequences:</p> <ul><li><strong>Interruptions</strong>: rsync resumes; scp restarts the file.</li> <li><strong>Second copies</strong>: rsync sends only changes; scp re-sends everything.</li> <li><strong>Deletions</strong>: rsync can mirror deletions with <code>--delete</code>; scp cannot.</li> <li><strong>Filtering</strong>: rsync has <code>--exclude</code> patterns; scp copies everything you point at.</li> <li><strong>Dry runs</strong>: rsync shows what it would do with <code>--dry-run</code>; scp offers nothing.</li></ul> <h2>Is rsync faster than scp?</h2> <p>For a first-time copy of one large file over a fast link, they are close — both are saturating SSH, and the checksum pass adds only a small overhead. The gap opens in three places:</p> <ol><li><strong>Small files in bulk.</strong> rsync pipelines directory walks and can reuse one connection; older scp setups spawned work per file. Thousands of little files (a <code>node_modules</code>, a WordPress install) finish noticeably faster with rsync.</li> <li><strong>Re-runs.</strong> Copy a 4 GB file where 50 MB changed and rsync moves roughly 50 MB; scp moves 4 GB again.</li> <li><strong>Compression.</strong> <code>-z</code> compresses in flight, which helps on slow WAN links.</li></ol> <p>You can time both yourself — the command is identical in shape:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># same file, same server, both over SSH</span>
<span class="token function">time</span> <span class="token function">scp</span> bigfile.tar.gz user@server:/tmp/
<span class="token function">time</span> <span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--progress</span> bigfile.tar.gz user@server:/tmp/

<span class="token comment"># re-run both: scp re-copies, rsync verifies and sends ~nothing</span>
<span class="token function">time</span> <span class="token function">scp</span> bigfile.tar.gz user@server:/tmp/
<span class="token function">time</span> <span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--progress</span> bigfile.tar.gz user@server:/tmp/</code><!----></pre> <p>If you spend your day in servers, transfer speed is one of those things worth measuring once — the same way <code>ss</code> beats <code>netstat</code> on busy hosts (see <a href="/en/blog/2026-09-05/ss-vs-netstat">ss vs netstat: which Linux port command to use</a>).</p> <h2>Can scp resume an interrupted transfer?</h2> <p>No. scp has no resume; if the connection drops at 900 MB of 1 GB, you start again. This is the single most quoted reason in every rsync-vs-scp debate, and it is real.</p> <p>rsync’s whole design assumes the transfer will be interrupted sometimes. The canonical resume incantation:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--partial</span> --append-verify <span class="token parameter variable">--progress</span> bigfile.tar.gz user@server:/srv/backup/</code><!----></pre> <ul><li><code>--partial</code> keeps the half-written file instead of deleting it.</li> <li><code>--append-verify</code> resumes by appending, then checksum-verifies the appended region — safe against a corrupt partial file, unlike the old plain <code>--append</code>.</li> <li><code>--progress</code> shows you where it picked up.</li></ul> <p>Wrapped in a retry loop, this is a set-and-forget backup over even a hostile connection:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token keyword">until</span> <span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--partial</span> --append-verify <span class="token parameter variable">--progress</span> <span class="token punctuation"></span>
    ./bigfile.tar.gz user@server:/srv/backup/<span class="token punctuation">;</span> <span class="token keyword">do</span>
  <span class="token function">sleep</span> <span class="token number">5</span>
<span class="token keyword">done</span></code><!----></pre> <h2>When should you use scp instead of rsync?</h2> <p>scp is still the right tool in a handful of cases:</p> <ul><li><strong>One small file, once.</strong> Typing <code>scp app.conf user@host:/etc/myapp/</code> is shorter than any rsync invocation, and there is nothing to resume.</li> <li><strong>rsync is missing on the far end.</strong> rsync needs its binary on both sides. Many minimal containers and appliances ship <code>scp</code>’s SFTP server but not rsync.</li> <li><strong>You don’t want an rsync server exposed.</strong> Rare, but some environments lock down the rsync daemon specifically.</li></ul> <p>One subtlety worth knowing: the OpenSSH project deprecated scp’s original protocol years ago, and modern scp actually speaks <strong>SFTP</strong> underneath. That fixed a path-escaping quirk, but it changed nothing about the two limitations that matter here — no resume, no delta transfer. The protocol change does not make scp rsync.</p> <p>Also related: if your question is really “rsync vs cp”, the answer mirrors this one — <code>cp</code> is the local-only equivalent of scp (no resume, no delta, no attributes unless you add flags), and rsync works for both local and remote. For local one-shots, <code>cp</code> is fine.</p> <h2>Which rsync flags matter most?</h2> <p>Most people only ever need one line:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--partial</span> <span class="token parameter variable">--progress</span> src/ user@server:/srv/dest/</code><!----></pre> <table><thead><tr><th>Flag</th><th>What it does</th></tr></thead><tbody><tr><td><code>-a</code> (archive)</td><td>Recursive + preserves permissions, times, group, symlinks, devices</td></tr><tr><td><code>-v</code> (verbose)</td><td>Lists what it transfers</td></tr><tr><td><code>-h</code> (human)</td><td>Human-readable sizes</td></tr><tr><td><code>--partial</code></td><td>Keep partially transferred files, so a re-run resumes</td></tr><tr><td><code>--progress</code></td><td>Per-file progress — the thing scp never had</td></tr><tr><td><code>-z</code></td><td>Compress in flight (slow CPUs on fast LANs: skip it)</td></tr><tr><td><code>--delete</code></td><td>Mirror deletions too — <strong>dangerous</strong>, always pair with a dry run</td></tr><tr><td><code>--dry-run</code> (-n)</td><td>Show what would happen, change nothing</td></tr></tbody></table> <p>Two habits worth adopting. First, dry-run anything with <code>--delete</code>:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--delete</span> --dry-run src/ user@server:/srv/dest/   <span class="token comment"># review</span>
<span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--delete</span> src/ user@server:/srv/dest/             <span class="token comment"># then commit</span></code><!----></pre> <p>Second, mind the trailing slash — <code>/srv/src</code> copies the directory itself into the destination, while <code>/srv/src/</code> copies its <em>contents</em>. This trips up everyone once; rsync even warns “no bytes transferred” when you meant the other form.</p> <p>For a daily or weekly sync, drop rsync into a systemd timer and let it copy only the deltas — the journalctl side of scheduling is covered in <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">the journalctl cheat sheet</a>.</p> <h2>Rsync vs scp: the verdict</h2> <table><thead><tr><th></th><th>scp</th><th>rsync</th></tr></thead><tbody><tr><td>Ships with OpenSSH</td><td>Yes</td><td>Yes (both ends needed)</td></tr><tr><td>Resume interrupted transfer</td><td>No</td><td>Yes (<code>--partial</code>)</td></tr><tr><td>Delta transfer on re-runs</td><td>No</td><td>Yes</td></tr><tr><td>Preserve permissions/symlinks</td><td>Partially</td><td>Fully (<code>-a</code>)</td></tr><tr><td>Exclude patterns</td><td>No</td><td><code>--exclude</code></td></tr><tr><td>Dry run</td><td>No</td><td><code>--dry-run</code></td></tr><tr><td>Mirror deletions</td><td>No</td><td><code>--delete</code></td></tr><tr><td>Best for</td><td>Quick one-off copies</td><td>Backups, syncs, bulk trees</td></tr></tbody></table> <p>Default to rsync for backups, big trees, anything over a link that can drop, and anything you will run more than once. Use scp when the command is shorter than the thought. If you take one flag away, take <code>--partial</code> — it converts every future dropped connection from a restart into a pause.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>How to Run GGUF Models Locally: Ollama, llama.cpp &amp; vLLM</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-07/how-to-run-gguf-models-locally</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-07/how-to-run-gguf-models-locally</guid>
      <pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate>
      <description>How to run GGUF models locally: one-line Ollama pulls, llama.cpp straight off a Hugging Face URL, and how to pick the right quant for your VRAM.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Downloaded a <code>.gguf</code> file and wondering how to actually run it? Fastest path: <code>ollama run hf.co/&lt;repo>:Q4_K_M</code> — Ollama pulls the GGUF straight from Hugging Face and serves it.</strong> GGUF is the single-file model format that llama.cpp introduced and every local-LLM tool now speaks, so the same file runs in Ollama, llama.cpp, LM Studio, Jan, and (with caveats) vLLM. This guide covers each runner with copy-paste commands, how to pick the right quantisation for your VRAM, and the load errors you will actually hit.</p> <h2>What is a GGUF file?</h2> <p>GGUF (GGML Universal File) is a container format for quantised language models. One file holds the weights, the tokenizer, and the model metadata — nothing else to download, no config soup. The weights inside are <em>quantised</em>: compressed from 16-bit floats down to 4-bit (or lower) integers, which is why a 9B model that needs ~18 GB in full precision fits in ~5.5 GB as a Q4 file and runs on a gaming GPU or even a CPU.</p> <p>Two things matter about a GGUF file’s name:</p> <ol><li><strong>The base model</strong> — <code>gemma-3-4b-it-GGUF</code> is a fine-tuned Gemma 3 4B exported to GGUF.</li> <li><strong>The quant tag</strong> — <code>Q4_K_M</code>, <code>Q8_0</code>, <code>IQ4_XS</code>, and friends say how aggressively the weights were compressed. More on picking one below.</li></ol> <h2>Can Ollama run GGUF models?</h2> <p>Yes — GGUF is Ollama’s native format, and since 2024 it can pull one straight off Hugging Face without you ever touching a file:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Pull a GGUF quant directly from Hugging Face and chat with it</span>
ollama run hf.co/bartowski/gemma-2-9b-it-GGUF:Q4_K_M

<span class="token comment"># The quant tag after the colon picks the file inside the repo</span>
ollama run hf.co/ggml-org/gemma-3-4b-it-GGUF:Q8_0</code><!----></pre> <p>Already downloaded a <code>.gguf</code> file yourself? Point a Modelfile at it:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Modelfile — one line is enough</span>
FROM ./gemma-2-9b-it-Q4_K_M.gguf</code><!----></pre> <pre class="language-bash"><!----><code class="language-bash">ollama create gemma9b <span class="token parameter variable">-f</span> Modelfile
ollama run gemma9b</code><!----></pre> <p>Ollama decides GPU offload automatically and exposes an OpenAI-compatible API on port 11434, so anything that speaks that API can use the model. The trade-off is control: you do not choose how many layers go to the GPU.</p> <h2>How do you run a GGUF file in llama.cpp?</h2> <p>llama.cpp is where GGUF comes from — the format exists for it — so support is deepest and freshest. The <code>llama-server</code> binary gives you both a chat UI and an OpenAI-compatible endpoint:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Download straight from Hugging Face (picks a matching GGUF for your machine)</span>
llama-server <span class="token parameter variable">-hf</span> ggml-org/gemma-3-4b-it-GGUF <span class="token parameter variable">--port</span> <span class="token number">8080</span>

<span class="token comment"># Or run a file you already have, with full GPU offload</span>
llama-server <span class="token parameter variable">-m</span> ./gemma-2-9b-it-Q4_K_M.gguf <span class="token parameter variable">-ngl</span> <span class="token number">99</span> <span class="token parameter variable">--port</span> <span class="token number">8080</span></code><!----></pre> <p><code>-ngl 99</code> pushes 99 layers onto the GPU; set it lower than your VRAM allows and the rest stays on CPU. That partial-offload dial is llama.cpp’s superpower — a 9B model runs fine on a 6 GB card with 20 of 48 layers offloaded, just slower. For one-shot prompting instead of a server, swap <code>llama-server</code> for <code>llama-cli</code> with the same <code>-m</code> flag.</p> <p>LM Studio is the same engine behind a desktop GUI: drop a <code>.gguf</code> file into its models folder (or search Hugging Face in-app) and click load. For the tooling choice itself, the <a href="/en/blog/2026-09-01/ollama-vs-lm-studio">Ollama vs LM Studio comparison</a> covers which one to put underneath your models.</p> <h2>Which GGUF quantisation should you download?</h2> <p>Default answer: <strong>Q4_K_M</strong>. It is the community sweet spot — within a percent or two of full-precision quality at roughly a quarter of the size. The ladder, largest to smallest:</p> <ul><li><strong>Q8_0</strong> — near-lossless; use it if your VRAM eats 8.5 bits per weight without noticing.</li> <li><strong>Q6_K / Q5_K_M</strong> — a step down in size, still excellent for 30B+ models.</li> <li><strong>Q4_K_M</strong> — the default. For 7–14B models this is where quality-per-GB peaks.</li> <li><strong>IQ4_XS / Q3_K_M</strong> — for squeezing a big model onto a small card; quality loss becomes noticeable.</li> <li><strong>Q2_K and below</strong> — last resort; the model starts degrading into nonsense mid-sentence.</li></ul> <p>The rule of thumb for fitting: file size in GB plus ~1–2 GB of context overhead should fit in your VRAM. A 4.7 GB Q4_K_M of a 9B model is comfortable on an 8 GB card. Prefer a <em>smaller model at a higher quant</em> over a bigger model at a terrible quant — a Q8 4B usually beats a Q2 9B.</p> <h2>GGUF vs Safetensors: which format do you need?</h2> <p>Safetensors is the <em>unquantised</em> archive format — full-precision weights for training, fine-tuning, and tools like transformers and ComfyUI. GGUF is the <em>quantised, runnable</em> format for inference on your own hardware. You cannot fine-tune a GGUF, and you cannot run a safetensors file in Ollama or llama.cpp without converting it first (that is what the <code>convert_hf_to_gguf.py</code> script in llama.cpp is for). Rule: training or image pipelines → safetensors; local chat and serving → GGUF. If your search started as “gguf vs safetensors”, that split is the whole answer.</p> <h2>Which GGUF runner should you use?</h2> <table><thead><tr><th>Runner</th><th>Best for</th><th>Install</th><th>GPU offload</th><th>OpenAI-compatible API</th></tr></thead><tbody><tr><td>Ollama</td><td>Set-and-forget service</td><td><code>curl</code> one-liner</td><td>Automatic</td><td>Yes (<code>:11434/v1</code>)</td></tr><tr><td>llama.cpp</td><td>Max control, newest features</td><td>Build or package manager</td><td>Manual <code>-ngl</code> dial</td><td>Yes (<code>llama-server</code>)</td></tr><tr><td>LM Studio</td><td>Desktop GUI, model browsing</td><td>App download</td><td>Automatic</td><td>Yes (local server)</td></tr><tr><td>vLLM</td><td>Batched multi-user serving</td><td><code>pip install vllm</code></td><td>Automatic</td><td>Yes (native)</td></tr></tbody></table> <p>Pick Ollama if you want it running at boot and out of sight — it is what I use on my <a href="/en/projects/homelab">homelab</a> to serve models to everything on the network. Pick llama.cpp when you need a feature the day it ships (new architectures land there first) or want layer-level memory control. Pick LM Studio for a GUI. Pick vLLM only when one model must serve many concurrent users — its GGUF support works but is second-class next to its native formats.</p> <h2>Why won’t my GGUF model load?</h2> <p>The four errors that cover most cases:</p> <ol><li><strong><code>unknown model architecture</code></strong> — the GGUF uses an architecture your runtime predates (new MoE and vision models land constantly). Update Ollama or rebuild llama.cpp; no other fix exists.</li> <li><strong>Out of memory at load</strong> — the quant is too big for your VRAM plus context. Drop one rung (<code>Q4_K_M</code> → <code>Q3_K_M</code>), lower <code>-ngl</code>, or shrink context with <code>-c 4096</code>.</li> <li><strong>Download truncated / corrupt</strong> — GGUF load fails with a magic-number or metadata error. Re-download and compare the SHA256 shown on the Hugging Face page.</li> <li><strong><code>ollama run ./model.gguf</code> refuses</strong> — expected: Ollama’s <code>run</code> takes model names, not file paths. Use the Modelfile route shown above.</li></ol> <p>One last angle worth knowing: a local GGUF endpoint pairs well with agentic coding tools — point an OpenAI-compatible client at it and completions cost only electricity. The <a href="/en/blog/2026-09-04/best-local-llm-for-coding">best local LLMs for coding</a> tested which models deserve the slot once the plumbing in this guide works.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Git Sync Fork With Upstream: 3 Safe Methods</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-06/git-sync-fork-with-upstream</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-06/git-sync-fork-with-upstream</guid>
      <pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate>
      <description>git sync fork with upstream three ways: the two-command pull, a rebase for clean history, GitHub&apos;s sync button — plus fixes when your fork won&apos;t sync.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>TL;DR: run <code>git fetch upstream &amp;&amp; git merge upstream/main &amp;&amp; git push origin main</code> and your fork is caught up.</strong> That is the whole “git sync fork with upstream” workflow in one line — pull changes from the project you forked, then push them to your copy. If you prefer linear history, swap <code>merge</code> for <code>git rebase upstream/main</code> and force-push. And if you have never wired up the <code>upstream</code> remote at all, start with step 1 below, because that missing remote is the number-one reason a fork “won’t sync”. Everything else — rebasing, the GitHub sync button, unpushable diverged branches — is detail on top of those three commands.</p> <h2>What does it mean to sync a fork with upstream?</h2> <p>A fork is your copy of someone else’s repository on GitHub. The original is the <em>upstream</em>; your copy is the <em>origin</em>. GitHub forks do not update themselves — when the maintainers merge a pull request, your copy keeps yesterday’s code. Syncing a fork means pulling the upstream’s new commits into your fork so your branch matches, or at least contains, the current state of the project.</p> <p>This matters for two reasons. First, contributions: every pull request you open from a stale fork carries extra noise, and maintainers will ask you to update before merging. Second, self-hosting or studying: if you run a fork in production or just read the code, a month-old fork is a month of bug fixes you do not have.</p> <h2>How do I sync a fork with upstream from the command line?</h2> <p>Three steps: declare the upstream once, fetch from it, then merge and push. The wiring is permanent — steps 2 and 3 are all you type next time.</p> <p><strong>Step 1 — add the upstream remote (one time per clone).</strong></p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># inside your local clone of the fork</span>
<span class="token function">git</span> remote <span class="token function">add</span> upstream https://github.com/ORIGINAL_OWNER/REPO.git
<span class="token function">git</span> remote <span class="token parameter variable">-v</span>   <span class="token comment"># confirm: origin -> your fork, upstream -> the original</span></code><!----></pre> <p>Find the correct URL on the original repo’s page: the green <strong>Code</strong> button. A common mistake is pointing both remotes at your fork — then a “sync” quietly does nothing, because you fetched from a copy that was just as stale as the one you have.</p> <p><strong>Step 2 — fetch and merge the upstream branch.</strong></p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> checkout main
<span class="token function">git</span> fetch upstream
<span class="token function">git</span> merge upstream/main
<span class="token function">git</span> push origin main</code><!----></pre> <p>That is the standard answer to “git sync fork with upstream command line”. A fast-forward is the normal outcome — <code>main</code> on your fork had nothing new, so it simply slides forward to <code>upstream/main</code> and no merge commit is created.</p> <p><strong>Step 3 — repeat on demand.</strong> There is nothing to remember beyond <code>git fetch upstream &amp;&amp; git merge upstream/main &amp;&amp; git push origin main</code>. To see how far behind you are before merging, run <code>git rev-list --count main..upstream/main</code> after the fetch.</p> <h2>Should I rebase or merge when syncing a fork?</h2> <p>Both land the same code in your fork; they differ in the history they leave behind. Pick one policy per repo and stick to it:</p> <table><thead><tr><th>Method</th><th>Command</th><th>History result</th><th>Best for</th></tr></thead><tbody><tr><td>Merge</td><td><code>git merge upstream/main</code></td><td>Extra merge commit on diverged branches</td><td>Feature branches with open PRs — never rewrites anything</td></tr><tr><td>Rebase</td><td><code>git rebase upstream/main</code></td><td>Your commits replayed on top, linear history</td><td>Keeping your fork’s <code>main</code> clean; diverged forks you want to reset</td></tr><tr><td>GitHub UI</td><td>Sync branch button / PR merge</td><td>Same as merge</td><td>Quick catch-up with no clone open</td></tr></tbody></table> <p>The rebase variant of the sync:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> fetch upstream
<span class="token function">git</span> rebase upstream/main
<span class="token function">git</span> push --force-with-lease origin main</code><!----></pre> <p>The force push is required because rebasing rewrites commit IDs — your fork’s remote branch no longer descends from your local one. Always prefer <code>--force-with-lease</code> over <code>--force</code>: it refuses to overwrite the remote if someone (or another machine of yours) pushed in the meantime, which makes the dangerous command safe by default.</p> <p>One rule worth tattooing on your wrist: <strong>never rebase a branch that has an open pull request unless you know what you are doing</strong> — rebasing changes commit IDs, which can detach an open PR from its commits. Sync <code>main</code> with merge (or rebase it before starting new work), and keep PR branches out of it.</p> <h2>Why is my fork not syncing with upstream?</h2> <p>The four usual suspects, in the order they appear in real terminals:</p> <ol><li><strong>No <code>upstream</code> remote</strong> — <code>git remote -v</code> shows only <code>origin</code>. Symptom: <code>git fetch upstream</code> fails with <code>'upstream' does not appear to be a git repository</code>. Fix: step 1 above.</li> <li><strong>You fetched but never merged</strong> — fetching updates <code>upstream/main</code> in your local repo but touches no working branch. Symptom: <code>git log</code> looks old after a successful fetch. Fix: <code>git merge upstream/main</code>.</li> <li><strong>Diverged history</strong> — you committed to your fork’s <code>main</code>, and upstream moved too. <code>git pull</code> then complains about unrelated histories or forces a merge. Fix, if you want upstream to win: <code>git reset --hard upstream/main</code> (throws away your local main-only commits — check <code>git stash list</code> or back up with a branch first; if a bad reset already happened, the recovery path is the same as in <a href="/en/blog/2026-09-03/git-undo-last-commit/">git undo last commit</a>: <code>git reflog</code> still knows the old tip).</li> <li><strong>Push rejected as non-fast-forward after a rebase</strong> — you rebased but pushed normally. Fix: <code>git push --force-with-lease origin main</code>.</li></ol> <p>A fifth, rare case: the upstream repo was renamed or deleted, so even step 1’s URL 404s. GitHub redirects renamed repos, so a hard failure usually means deleted or made private — nothing to sync to.</p> <h2>Can you sync a fork from the GitHub website?</h2> <p>Yes. On your fork’s page, the branch dropdown shows a <strong>Sync fork</strong> button whenever your branch is behind; one click pulls upstream in. Below the fold, the same thing works as a pull request: open a PR from <code>upstream/main</code> into your fork’s <code>main</code> and merge it.</p> <p>The button’s limits explain when to fall back to the CLI: it only does a fast-forward or merge — it will not rebase, and it refuses outright when the branches have diverged, telling you to discard commits or use the command line. It also syncs only the default branch. For everything past a simple catch-up, the three commands above are the tool.</p> <h2>How often should you sync your fork?</h2> <p>Before every new piece of work is the honest answer: branch off a fresh <code>main</code>, and no PR you open starts with “this is based on a version from three weeks ago”. For forks you actively contribute to, a daily or per-session sync of <code>main</code> costs seconds. For a fork you only read or deploy, sync when upstream ships something you want — subscribe to the original repo’s releases feed and sync on release. Syncing is cheap precisely because it is routine; a fork six months behind often needs surgery instead of a merge, which is how “sync my fork” turns into an afternoon.</p> <h2>Cheat sheet</h2> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># one-time setup</span>
<span class="token function">git</span> remote <span class="token function">add</span> upstream https://github.com/ORIGINAL_OWNER/REPO.git

<span class="token comment"># routine sync (merge policy)</span>
<span class="token function">git</span> fetch upstream <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> merge upstream/main <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> push origin main

<span class="token comment"># routine sync (rebase policy, linear history)</span>
<span class="token function">git</span> fetch upstream <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> rebase upstream/main <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> push --force-with-lease origin main

<span class="token comment"># how far behind am I?</span>
<span class="token function">git</span> fetch upstream <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> rev-list <span class="token parameter variable">--count</span> main<span class="token punctuation">..</span>upstream/main

<span class="token comment"># diverged beyond repair — make main identical to upstream (destructive)</span>
<span class="token function">git</span> fetch upstream <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> reset <span class="token parameter variable">--hard</span> upstream/main <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> push --force-with-lease origin main</code><!----></pre> <p>Keep the routine two-liner in your muscle memory and the diverged fork stays a curiosity you read about, not a problem you fix. If your git housekeeping extends to servers, <a href="/en/blog/2026-09-02/journalctl-cheat-sheet/">the journalctl cheat sheet</a> covers the other half of keeping a machine’s history readable.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>ss vs netstat: Which Linux Port Command to Use</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-05/ss-vs-netstat</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-05/ss-vs-netstat</guid>
      <pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate>
      <description>ss vs netstat: netstat is legacy on modern Linux, and ss replaces it flag for flag. Get the mapping table, find the process behind any port, and a cheat sheet.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Use <code>ss</code> — it is the current standard on every modern Linux distro, and <code>netstat</code> is legacy there.</strong> On Ubuntu, Fedora, and Debian the netstat binary now ships in an optional <code>net-tools</code> package, while <code>ss</code> is built into <code>iproute2</code> and installed everywhere. <code>ss</code> is also faster on busy servers because it reads socket statistics directly from the kernel instead of walking <code>_/proc_</code> file by file. The catch is syntax: <code>-tulpn</code> does not mean the same thing everywhere, so this guide gives you the exact flag mapping, the commands worth memorising, and answers to the questions that come up when you are mid-incident at 2 a.m.</p> <h2>Is netstat deprecated?</h2> <p>On Linux, effectively yes. The <code>net-tools</code> package — which contains <code>netstat</code>, <code>ifconfig</code>, and <code>route</code> — has not tracked modern kernel features for years and is no longer installed by default on any major distribution. It still exists, still runs, and there is nothing to uninstall, but new features land only in <code>iproute2</code> (the package behind <code>ss</code> and <code>ip</code>).</p> <p>Three practical consequences:</p> <ol><li><strong>Missing on fresh installs.</strong> A default Ubuntu 24.04 or Fedora server has no <code>netstat</code> until you install <code>net-tools</code> by hand. This is why so many people search for the <code>netstat</code> equivalent — the binary is simply gone.</li> <li><strong>No modern socket info.</strong> <code>netstat</code> predates features like TCP fast open, subflow-level socket stats, and cgroup socket attribution. <code>ss</code> reports them natively.</li> <li><strong>The Windows exception.</strong> On Windows, <code>netstat</code> is alive and well — <code>netstat -ano</code> is still the standard way to map a PID to a listening port there. Only the Linux story is deprecation.</li></ol> <h2>What is the ss command in Linux?</h2> <p><code>ss</code> means “socket statistics”. It dumps the kernel’s view of every TCP, UDP, and Unix socket: state, addresses, ports, processes, timers, and queue depths. The workhorse form is:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Everything listening, with the owning process</span>
<span class="token function">sudo</span> ss <span class="token parameter variable">-tulpn</span></code><!----></pre> <p>Flag by flag, that reads: TCP (<code>-t</code>), UDP (<code>-u</code>), listening sockets only (<code>-l</code>), show processes (<code>-p</code>), numeric output — no DNS lookups (<code>-n</code>). The <code>sudo</code> matters for <code>-p</code>: without root you see ports but get no process names for sockets you do not own.</p> <p>Two more one-liners that cover most of the job:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># All established connections to/from port 443, with processes</span>
<span class="token function">sudo</span> ss <span class="token parameter variable">-tnp</span> <span class="token string">'sport = :443 or dport = :443'</span>

<span class="token comment"># Summary table: how many sockets in each state</span>
ss <span class="token parameter variable">-s</span></code><!----></pre> <p>That filter syntax is a genuine upgrade over netstat’s grep-and-pray approach — it runs inside the kernel, so it is exact rather than text-matched.</p> <h2>How do you check which process is using a port?</h2> <p>This is the single most common reason to reach for either tool. With <code>ss</code>:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">sudo</span> ss <span class="token parameter variable">-ltnp</span> <span class="token string">'sport = :8080'</span></code><!----></pre> <p>Example output on a machine running a dev server:</p> <pre class="language-undefined"><!----><code class="language-undefined">State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       511     *:8080              *:*               users:((&quot;node&quot;,pid=214113,fd=18))</code><!----></pre> <p>The <code>users:(...)</code> field gives you the process name and PID directly — no second command needed. If the port is busy but no process shows, you are usually looking at a socket held by another network namespace (a container). From the host, run <code>sudo ss -ltnp</code> and look for the port with an empty process column, then match it with <code>docker ps</code> or <code>nsenter</code> into the container’s namespace.</p> <p>Prefer a dedicated tool? <code>sudo lsof -i :8080 -sTCP:LISTEN</code> does the same job and works identically on macOS and most BSDs, which is why it survives in runbooks. On Linux, though, <code>ss</code> is already there.</p> <h2>What are the differences between ss and netstat?</h2> <p>Same job, different plumbing: <code>netstat</code> scrapes <code>_proc/net/tcp_</code> in userspace, while <code>ss</code> uses netlink to ask the kernel directly — which is why <code>ss</code> returns in milliseconds on a server with 50k open sockets and netstat does not. Day to day, the difference is the flags. Here is the mapping worth taping to your monitor:</p> <table><thead><tr><th>You want</th><th>netstat</th><th>ss</th></tr></thead><tbody><tr><td>Listening TCP ports + process</td><td><code>netstat -tlpn</code></td><td><code>ss -tlpn</code></td></tr><tr><td>All TCP + UDP connections</td><td><code>netstat -tulpna</code></td><td><code>ss -tulpna</code></td></tr><tr><td>Routing table</td><td><code>netstat -r</code></td><td><code>ip route</code></td></tr><tr><td>Interface statistics</td><td><code>netstat -i</code></td><td><code>ip -s link</code></td></tr><tr><td>Kernel socket summary</td><td>—</td><td><code>ss -s</code></td></tr><tr><td>Filter by port in-kernel</td><td>—</td><td><code>ss -tnp 'sport = :22'</code></td></tr></tbody></table> <p>Notice the first two rows are identical: by luck, ss’s short flags line up with netstat’s for the common cases, so muscle memory mostly survives the move. The bottom rows are where netstat has no answer — the routing and interface jobs moved to the <code>ip</code> command, and the summary/filter rows only exist in <code>ss</code>.</p> <h2>When should you still use netstat?</h2> <p>Two honest cases. First, <strong>portability</strong>: on a mixed fleet of Linux, AIX, or older BSD boxes, netstat is the one syntax present everywhere. Second, <strong>muscle-memory continuity</strong> in old runbooks — if a runbook says <code>netstat -tlpn</code> and the box has net-tools installed, it still works fine.</p> <p>Otherwise, default to <code>ss</code> and update the runbook. A useful companion when you are rewriting those runbooks on a server is the journalctl cheat sheet, which pairs naturally with port checks when you are diagnosing a service that will not start: <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">/en/blog/2026-09-02/journalctl-cheat-sheet</a>.</p> <h2>Why is the ss command not found?</h2> <p>Because you are either on a very old distro (pre-2007-era, before iproute2 was standard) or — far more likely — you are not on Linux at all. <code>ss</code> is not a macOS command; macOS ships <code>lsof</code> and <code>netstat</code> but no <code>ss</code>. Same for the BSDs, mostly. On Windows, use <code>netstat -ano</code> or PowerShell’s <code>Get-NetTCPConnection</code>.</p> <p>If you genuinely are on Linux and <code>ss</code> is missing, install iproute2:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> iproute2    <span class="token comment"># Debian/Ubuntu</span>
<span class="token function">sudo</span> dnf <span class="token function">install</span> iproute2    <span class="token comment"># Fedora/RHEL</span></code><!----></pre> <h2>The 30-second answer</h2> <p><code>ss</code> over <code>netstat</code> on Linux, every time: it is preinstalled, faster, and can filter in-kernel. <code>netstat -tlpn</code> becomes <code>ss -tlpn</code>, process hunting becomes <code>sudo ss -ltnp 'sport = :PORT'</code>, and routing tables belong to <code>ip route</code> now. Keep netstat for cross-platform scripts and old muscle memory; keep this page for the flag mapping.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Best Local LLM for Coding: 8GB to 24GB VRAM Picks</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-04/best-local-llm-for-coding</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-04/best-local-llm-for-coding</guid>
      <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
      <description>The best local LLM for coding by VRAM bracket: Qwen3 Coder vs DeepSeek at 8, 12, 16 and 24 GB, the right quant per card, plus a runnable Ollama setup.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>The best local LLM for coding right now is Qwen3 Coder 30B A3B on a 24 GB card, Qwen2.5 Coder 14B at Q4 on 12–16 GB, and Qwen2.5 Coder 7B at Q4 on 8 GB.</strong> Every one of these runs fully offline, autocompletes and refactors real code, and costs nothing per token. If your GPU has less VRAM than the model needs, drop the quant one notch before you drop the model size. This guide matches models to VRAM brackets, compares the two coding families people actually argue about, and ends with runnable commands so you can be generating code locally in about five minutes.</p> <h2>Which local LLM should you use for coding on your GPU?</h2> <p>Pick by VRAM first, model second — the model only fits if the weights plus context fit in memory. This is the shortlist that keeps coming out on top in 2026 community benchmarks and day-to-day use:</p> <table><thead><tr><th>VRAM</th><th>Model</th><th>Quant</th><th>Weights on disk</th><th>Why it wins this bracket</th></tr></thead><tbody><tr><td>8 GB</td><td>Qwen2.5 Coder 7B Instruct</td><td>Q4_K_M</td><td>~4.7 GB</td><td>Best tokens-per-second-to-quality ratio for autocomplete and small refactors</td></tr><tr><td>12 GB</td><td>Qwen2.5 Coder 14B Instruct</td><td>Q4_K_M</td><td>~9.0 GB</td><td>Whole-file edits fit in context; still 30+ tok/s on a 3060-class card</td></tr><tr><td>16 GB</td><td>Qwen3 14B or gpt-oss-20b</td><td>Q4_K_M</td><td>~9–12 GB</td><td>Better reasoning on ambiguous specs; 4090-class cards keep it fast</td></tr><tr><td>24 GB</td><td>Qwen3 Coder 30B A3B</td><td>Q4_K_M</td><td>~18.6 GB</td><td>MoE: only ~3B parameters active per token, so speed stays usable</td></tr></tbody></table> <p>Two rules make this table work in practice:</p> <ol><li><strong>Leave 1–2 GB of VRAM headroom</strong> for the KV cache. A 14B model at Q4 plus an 8K-token context will not fit in a 10 GB budget — context counts toward the total.</li> <li><strong>Go one quant down before you go a model size down.</strong> Q5/Q4 quants cost a few percent of quality; a 7B model instead of a 14B costs far more than that.</li></ol> <h2>How much VRAM do you need for a local coding LLM?</h2> <p>The honest rule of thumb: <strong>VRAM needed ≈ quantised weights + 0.125 GB per 1K tokens of context at 8-bit KV cache.</strong> In plain numbers:</p> <ul><li><strong>8 GB</strong> runs 7B–8B models at Q4 comfortably. Expect autocomplete-length answers, 4K–8K context.</li> <li><strong>12 GB</strong> is the sweet spot for 14B at Q4 — enough room for the model plus a realistic 8K–16K coding context.</li> <li><strong>16 GB</strong> opens 20B-class dense models and Qwen3 14B with longer context.</li> <li><strong>24 GB</strong> runs the Qwen3 Coder 30B A3B MoE at Q4, which is the closest thing to a cloud-quality coding model you can host yourself.</li></ul> <p>CPU-only? It works — llama.cpp will happily run a 7B Q4 on a laptop CPU at 5–10 tok/s — but treat it as a patience exercise, not a daily driver. For the tooling side of getting a runtime installed, the <a href="/en/blog/2026-09-01/ollama-vs-lm-studio">Ollama vs LM Studio comparison</a> covers which local LLM tool to put underneath your models.</p> <h2>Qwen3 Coder vs DeepSeek: which is better for coding?</h2> <p>This is the matchup the autocomplete bars actually ask about, and the answer splits cleanly:</p> <ul><li><strong>Qwen3 Coder (30B A3B)</strong> is built for the edit loop: it follows instruction-format conventions for tool calling, produces consistent diffs, and — the decisive part — the MoE design means only ~3B parameters activate per token, so a single 24 GB card gets 40–60 tok/s. For IDE-style use, responsiveness is quality.</li> <li><strong>DeepSeek V3/R1 line</strong> argues at a higher level: architecture decisions, tricky algorithms, multi-step reasoning. But the flagship is 600B+ parameters; locally you only run it heavily quantised on multi-GPU or Mac unified-memory rigs, and it writes prose about code faster than it writes code.</li></ul> <p><strong>Local choice: Qwen3 Coder for the daily driver, DeepSeek only if you have the hardware to host it near-full-precision.</strong> At 8–16 GB the debate is moot — Qwen2.5/3 Coder models are the strongest thing that fits.</p> <h2>How do you run the best local LLM for coding with Ollama?</h2> <p>Five commands, from nothing to an OpenAI-compatible API your editor can use:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># 1. Install Ollama (Linux)</span>
<span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://ollama.com/install.sh <span class="token operator">|</span> <span class="token function">sh</span>

<span class="token comment"># 2. Pull the model that fits your VRAM bracket (8 GB card shown)</span>
ollama pull qwen2.5-coder:7b

<span class="token comment"># 3. Chat with it interactively</span>
ollama run qwen2.5-coder:7b

<span class="token comment"># 4. Use it as an OpenAI-compatible API from any tool</span>
<span class="token function">curl</span> http://localhost:11434/v1/chat/completions <span class="token punctuation"></span>
  <span class="token parameter variable">-d</span> <span class="token string">'&#123;
    "model": "qwen2.5-coder:7b",
    "messages": [&#123;"role": "user", "content": "Refactor this fn to be async: add(a,b)&#123;return a+b&#125;"&#125;]
  &#125;'</span>

<span class="token comment"># 5. Point tools that expect OPENAI_BASE_URL at it</span>
<span class="token builtin class-name">export</span> <span class="token assign-left variable">OPENAI_BASE_URL</span><span class="token operator">=</span>http://localhost:11434/v1</code><!----></pre> <p>No API key, no rate limit, no per-token bill. On Linux the installer registers a systemd unit, so if the server ever misbehaves, <code>journalctl</code> tells you why — the <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">journalctl cheat sheet</a> has the exact filters for service debugging.</p> <h2>Is a local LLM good enough for real coding work?</h2> <p><strong>Yes for the edit loop, no for the hard problems — and that split is exactly how you should use it.</strong> A 14B–30B local model handles refactors, boilerplate, test scaffolding, regex, and “explain this legacy function” faster than most cloud APIs round-trip. Where it loses to the big hosted models is long multi-file reasoning and obscure framework trivia — a 30B model simply knows less than a frontier model.</p> <p>The workflow that works: keep a local model running for 90% of your keystrokes, and reach for a hosted frontier model only for the gnarly design questions. Your code never leaves the machine for routine work, which matters for client code, and the VRAM you already own quietly replaces a subscription.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Git Undo Last Commit: Keep Changes, Stay Safe</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-03/git-undo-last-commit</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-03/git-undo-last-commit</guid>
      <pubDate>Thu, 03 Sep 2026 00:00:00 GMT</pubDate>
      <description>Git undo last commit the safe way: keep the changes with reset --soft, unstage with --mixed, or fix a pushed commit with revert — exact commands for each.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>TL;DR: to undo the last commit but keep the changes, run <code>git reset --soft HEAD~1</code> (changes stay staged) or <code>git reset HEAD~1</code> (changes stay in your working tree). If the commit is already pushed, run <code>git revert HEAD</code> instead — it creates a new commit that undoes it without rewriting history.</strong> Those three commands cover almost every “I committed too early” moment. The rest of this guide walks through each case with copy-paste commands, explains the difference between <code>--soft</code>, <code>--mixed</code> and <code>--hard</code>, and shows how to recover if something goes wrong. Every command below works on any recent Git install on Linux, macOS or Windows.</p> <h2>How do I undo the last commit but keep the changes?</h2> <p>The most common situation: you committed, then spotted a typo, a missing file, or realised the change belongs in a different commit. Nothing is pushed yet. Undo the commit and put everything back where it was:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Commit stays in history nowhere — changes go back to the staging area</span>
<span class="token function">git</span> reset <span class="token parameter variable">--soft</span> HEAD~1

<span class="token comment"># Changes go back to the working tree (unstaged) instead</span>
<span class="token function">git</span> reset HEAD~1</code><!----></pre> <p><code>HEAD~1</code> means “one commit before where HEAD points now”. After either command your files are untouched on disk — only the branch pointer moved. Check it with <code>git status</code>: with <code>--soft</code> the changes are <strong>staged</strong>, ready to re-commit with fixes folded in; without a flag they are <strong>unstaged</strong>, so you can edit freely first.</p> <p>A safer habit when you just want to add files to the last commit is not to undo it at all:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> <span class="token function">add</span> forgotten-file.txt
<span class="token function">git</span> commit <span class="token parameter variable">--amend</span> --no-edit</code><!----></pre> <p><code>--amend</code> replaces the last commit in place (no commit message change here). Note that amending a <em>pushed</em> commit rewrites history — more on that below.</p> <h2>What is the difference between —soft, —mixed and —hard?</h2> <p>This is the part worth memorising, because the flag decides where your changes end up — and whether they can be lost:</p> <table><thead><tr><th>Flag</th><th>Commit undone?</th><th>Changes on disk</th><th>Changes staged?</th><th>Typical use</th></tr></thead><tbody><tr><td><code>--soft</code></td><td>Yes</td><td>Kept</td><td>Yes</td><td>Re-commit with small fixes</td></tr><tr><td><code>--mixed</code> (default)</td><td>Yes</td><td>Kept</td><td>No</td><td>Re-group changes, re-stage selectively</td></tr><tr><td><code>--hard</code></td><td>Yes</td><td><strong>Deleted</strong></td><td>—</td><td>Throw the work away entirely</td></tr><tr><td><code>git revert</code></td><td>No (new commit)</td><td>Kept</td><td>—</td><td>Undo a commit that was already pushed</td></tr></tbody></table> <p><code>--hard</code> is the only dangerous one: it discards the commit <strong>and</strong> the changes. Before any <code>reset --hard</code>, stash or branch what you have:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> branch backup-before-reset   <span class="token comment"># cheap insurance</span>
<span class="token function">git</span> reset <span class="token parameter variable">--hard</span> HEAD~1          <span class="token comment"># commit + changes gone</span></code><!----></pre> <p>If you already ran the dangerous version, all is not lost — <code>git reflog</code> remembers where HEAD has been:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> reflog                       <span class="token comment"># find the commit hash you lost</span>
<span class="token function">git</span> reset <span class="token parameter variable">--hard</span> HEAD@<span class="token punctuation">&#123;</span><span class="token number">1</span><span class="token punctuation">&#125;</span>        <span class="token comment"># or: git reset --hard &lt;hash></span></code><!----></pre> <p>The reflog keeps dangling commits for around 90 days by default, so “I hard-reset by mistake” is almost always recoverable if you act before garbage collection.</p> <h2>What if I already pushed the commit?</h2> <p>If the commit is on a shared branch (anything other than your own feature branch), do <strong>not</strong> rewrite history. Use <code>git revert</code>, which computes the opposite change and commits it:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> revert HEAD
<span class="token function">git</span> push</code><!----></pre> <p>Everyone who pulls simply gets a new commit that removes the old one’s changes. No force-push, no broken colleagues. If you need to undo a run of commits, revert a range: <code>git revert --no-commit HEAD~3..HEAD &amp;&amp; git commit</code>.</p> <p>The alternative — <code>git reset --hard HEAD~1 &amp;&amp; git push --force-with-lease</code> — is only acceptable on a branch nobody else builds on, and <code>--force-with-lease</code> (never plain <code>--force</code>) is the only safe form because it refuses if someone pushed in the meantime. Force-pushing shared branches is how teams lose commits and CI logs mysteriously stop matching anyone’s checkout.</p> <h2>How do I undo the last commit but keep it around for later?</h2> <p>Sometimes the commit is good work at the wrong address — on the wrong branch, or too early. Instead of undoing it, move it:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> branch stash-commit          <span class="token comment"># park the commit on a new branch</span>
<span class="token function">git</span> reset <span class="token parameter variable">--hard</span> HEAD~1          <span class="token comment"># then clean your current branch</span></code><!----></pre> <p>Or take just the commit to another branch without touching your current one:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">git</span> cherry-pick <span class="token operator">&lt;</span>hash<span class="token operator">></span>           <span class="token comment"># while on the target branch</span></code><!----></pre> <p>Between <code>reset --soft</code>, <code>cherry-pick</code> and <code>revert</code>, there is no commit you cannot relocate or nullify — the trick is choosing “move” versus “undo” before reaching for a flag.</p> <h2>Which undo should I use? A quick decision guide</h2> <ol><li><strong>Not pushed, want to fix and re-commit</strong> → <code>git reset --soft HEAD~1</code></li> <li><strong>Not pushed, want to re-stage selectively</strong> → <code>git reset HEAD~1</code> (mixed)</li> <li><strong>Want the changes gone completely</strong> → <code>git reset --hard HEAD~1</code> (reflog knows, if you regret it)</li> <li><strong>Already pushed to a shared branch</strong> → <code>git revert HEAD</code></li> <li><strong>Commit belongs on another branch</strong> → <code>cherry-pick</code>, don’t undo</li></ol> <p>One last operational tip: if a bad commit did make it to a server — a botched deploy hook, a CI runner misbehaving after a force-push — the next place to look is the machine’s logs, not Git. On any systemd box, <code>journalctl -u &lt;service> -n 100</code> shows you exactly what ran and when; our <a href="/en/blog/2026-09-02/journalctl-cheat-sheet">journalctl cheat sheet</a> has the copy-paste patterns. And if your workflow includes local LLM tooling to review diffs, we compared the two main options in <a href="/en/blog/2026-09-01/ollama-vs-lm-studio">Ollama vs LM Studio</a>.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>journalctl Cheat Sheet: Tail, Filter and Follow Linux Logs</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-02/journalctl-cheat-sheet</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-02/journalctl-cheat-sheet</guid>
      <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
      <description>journalctl cheat sheet for real work: tail logs, grab the last 100 lines, follow a live service and filter by unit or time — copy-paste commands that just work.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>journalctl in one line: <code>journalctl -u &lt;service> -f</code> follows a live service log, <code>journalctl -u &lt;service> -n 100</code> shows the last 100 lines, and <code>journalctl --since "1 hour ago"</code> shows everything recent.</strong> That covers 90% of the “why is this service down” moments. The rest of this cheat sheet is the copy-paste patterns that save you from re-reading the man page at 2am — filtering by unit, time, priority and boot, plus the cleanup commands that stop a journal from eating your disk. Every command below runs as-is on any systemd distro (Ubuntu, Debian, Fedora, Arch).</p> <h2>What is journalctl and why not just read /var/log/syslog?</h2> <p>Old-school Linux logging wrote plain text files under <code>/var/log</code> — <code>syslog</code>, <code>auth.log</code>, <code>messages</code>. systemd replaced that with <strong>the journal</strong>: a binary, indexed log managed by <code>systemd-journald</code>. Plain <code>grep</code> cannot read it; <code>journalctl</code> is the only front door, and in exchange you get filtering by service, time, boot and severity without regex gymnastics.</p> <p>The mental model is simple: the journal stores <strong>everything</strong>, and <code>journalctl</code> is the query tool. A service does not need its own log file — anything it writes to stdout/stderr while running under systemd lands in the journal automatically. That is why <code>journalctl -u nginx</code> works even when you have no idea where nginx configured its error log.</p> <p>One caveat: on some minimal installs the journal is <strong>volatile</strong> (stored in <code>/run</code>, wiped on reboot) because <code>/var/log/journal</code> does not exist. Fix it once:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">sudo</span> <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> /var/log/journal
<span class="token function">sudo</span> systemd-tmpfiles <span class="token parameter variable">--create</span> <span class="token parameter variable">--prefix</span> /var/log/journal</code><!----></pre> <h2>How do I see the last 100 lines of a log?</h2> <p><code>-n</code> limits output to the newest N lines (default is 10):</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-n</span> <span class="token number">100</span>                      <span class="token comment"># last 100 lines of everything</span>
journalctl <span class="token parameter variable">-u</span> nginx <span class="token parameter variable">-n</span> <span class="token number">100</span>             <span class="token comment"># last 100 lines from one unit</span>
journalctl <span class="token parameter variable">-n</span> <span class="token number">100</span> --no-pager           <span class="token comment"># print and exit — perfect for piping</span></code><!----></pre> <p><code>--no-pager</code> matters more than it looks: without it journalctl opens <code>less</code> and your script, pipe or <code>ssh one-liner</code> hangs waiting for a keypress. Any command you pipe onward should carry it.</p> <h2>How do I follow logs live, like tail -f?</h2> <p>The <code>-f</code> flag is journalctl’s <code>tail -f</code> — it streams new entries as they happen:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-f</span>                    <span class="token comment"># everything, live</span>
journalctl <span class="token parameter variable">-u</span> sshd <span class="token parameter variable">-f</span>            <span class="token comment"># just the SSH daemon, live</span>
journalctl <span class="token parameter variable">-u</span> ollama <span class="token parameter variable">-f</span> <span class="token parameter variable">-n</span> <span class="token number">50</span>    <span class="token comment"># live, but start with the last 50 lines</span></code><!----></pre> <p>This is the command to run in a second terminal while you restart a service in the first one: <code>systemctl restart nginx</code> in one pane, <code>journalctl -u nginx -f</code> in the other, and the cause of the crash usually announces itself within seconds. I run this exact loop on my <a href="/en/projects/homelab">homelab</a> every time a container or service misbehaves.</p> <h2>How do I show logs for a specific service?</h2> <p><code>-u</code> filters by systemd unit:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-u</span> nginx                    <span class="token comment"># one unit, all history</span>
journalctl <span class="token parameter variable">-u</span> nginx <span class="token parameter variable">-u</span> redis           <span class="token comment"># several units at once</span>
journalctl <span class="token assign-left variable">_SYSTEMD_UNIT</span><span class="token operator">=</span>nginx.service <span class="token comment"># the exact-match alternative</span></code><!----></pre> <p>Units can spawn helper units that keep the interesting output — a failing web app sometimes logs the real error under a different unit. If <code>-u &lt;service></code> shows nothing but the service clearly runs, find the actual unit name first:</p> <pre class="language-bash"><!----><code class="language-bash">systemctl list-units <span class="token parameter variable">--type</span><span class="token operator">=</span>service <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-i</span> <span class="token operator">&lt;</span>guess<span class="token operator">></span></code><!----></pre> <p>The same <code>-u</code> pattern works for timers (<code>systemctl list-timers</code> shows names) and user services — for anything running under <code>systemd --user</code> add <code>--user</code>:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">--user</span> <span class="token parameter variable">-u</span> pipewire <span class="token parameter variable">-n</span> <span class="token number">50</span></code><!----></pre> <h2>How do I filter logs by time?</h2> <p><code>--since</code> and <code>--until</code> take timestamps, but they also accept forgiving relative phrasing:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">--since</span> <span class="token string">"1 hour ago"</span>
journalctl <span class="token parameter variable">--since</span> <span class="token string">"2026-09-01 09:00"</span> <span class="token parameter variable">--until</span> <span class="token string">"2026-09-01 12:00"</span>
journalctl <span class="token parameter variable">--since</span> today
journalctl <span class="token parameter variable">--since</span> yesterday <span class="token parameter variable">--until</span> now <span class="token parameter variable">-u</span> <span class="token function">cron</span></code><!----></pre> <p>Pair a time window with a unit and you have incident triage in one line: “what did the API log between 09:00 and when it fell over?” That is faster than any log aggregator for a single box.</p> <h2>How do I show only errors (or warnings)?</h2> <p><code>-p</code> filters by priority, using syslog severity names:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-p</span> err <span class="token parameter variable">-b</span>              <span class="token comment"># errors only, since this boot</span>
journalctl <span class="token parameter variable">-p</span> warning<span class="token punctuation">..</span>alert <span class="token parameter variable">-u</span> nginx   <span class="token comment"># a range of severities</span></code><!----></pre> <p>Priorities in descending order: <code>emerg</code>, <code>alert</code>, <code>crit</code>, <code>err</code>, <code>warning</code>, <code>notice</code>, <code>info</code>, <code>debug</code>. When a box is “acting weird”, <code>journalctl -p err -b --since today</code> is the fastest sanity check — it answers “is anything actually failing?” without noise from routine info lines.</p> <h2>How do I see logs from the previous boot?</h2> <p>Services that crash at startup produce log lines <em>before</em> your current boot, and <code>journalctl</code> silently shows only the current one. <code>-b</code> selects boots:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl <span class="token parameter variable">-b</span>                 <span class="token comment"># current boot only</span>
journalctl <span class="token parameter variable">-b</span> <span class="token parameter variable">-1</span>              <span class="token comment"># previous boot</span>
journalctl <span class="token parameter variable">-b</span> <span class="token parameter variable">-1</span> <span class="token parameter variable">-u</span> sshd      <span class="token comment"># why SSH died last time</span>
journalctl --list-boots       <span class="token comment"># index of stored boots</span></code><!----></pre> <p>This is the single most useful flag for “it broke and I rebooted it and now I can’t see the error” — the error is still there, one boot back.</p> <h2>Quick reference: the flags worth memorising</h2> <table><thead><tr><th>Goal</th><th>Command</th></tr></thead><tbody><tr><td>Follow one service live</td><td><code>journalctl -u &lt;svc> -f</code></td></tr><tr><td>Last 100 lines</td><td><code>journalctl -n 100</code></td></tr><tr><td>Since an hour ago</td><td><code>journalctl --since "1 hour ago"</code></td></tr><tr><td>Errors this boot</td><td><code>journalctl -p err -b</code></td></tr><tr><td>Previous boot</td><td><code>journalctl -b -1</code></td></tr><tr><td>Disk usage of the journal</td><td><code>journalctl --disk-usage</code></td></tr><tr><td>Machine-readable output</td><td><code>journalctl -o json-pretty</code></td></tr><tr><td>Kernel messages only</td><td><code>journalctl -k</code></td></tr></tbody></table> <h2>How do I stop the journal filling my disk?</h2> <p>Check what it costs, then cap it:</p> <pre class="language-bash"><!----><code class="language-bash">journalctl --disk-usage
<span class="token function">sudo</span> journalctl --vacuum-size<span class="token operator">=</span>500M     <span class="token comment"># keep newest 500 MB</span>
<span class="token function">sudo</span> journalctl --vacuum-time<span class="token operator">=</span>30d      <span class="token comment"># keep newest 30 days</span></code><!----></pre> <p>To make a cap permanent, set <code>SystemMaxUse=500M</code> under the <code>[Journal]</code> section in <code>/etc/systemd/journald.conf</code>, then <code>sudo systemctl restart systemd-journald</code>. Between the size cap and the boot filter above, the journal stays a tool rather than a slow disk leak.</p> <h2>journalctl vs dmesg vs /var/log — which do I check first?</h2> <ul><li><strong><code>journalctl</code></strong> — application and service behaviour. Anything systemd manages is here, indexed and filterable. Default first stop.</li> <li><strong><code>journalctl -k</code> / <code>dmesg</code></strong> — kernel and hardware: OOM kills, USB resets, disk errors. If a process died mysteriously, look for the OOM killer here before blaming the app.</li> <li><strong><code>/var/log/&lt;app>/</code></strong> — only for apps that do their own file logging (nginx access logs, PostgreSQL). Even then, startup errors usually still land in the journal.</li></ul> <p>The same workflow extends to local AI services — when an <code>ollama serve</code> unit stalls, <code>journalctl -u ollama -n 100</code> shows the model load failure immediately, as covered in the <a href="/en/blog/2026-09-01/ollama-vs-lm-studio">Ollama vs LM Studio comparison</a>.</p> <p>That is the whole cheat sheet: <code>-u</code> for unit, <code>-f</code> to follow, <code>-n</code> for lines, <code>--since</code> for time, <code>-p</code> for severity, <code>-b</code> for boots. Six flags cover nearly every log question a Linux box will ask you.</p><!--]-->]]></content:encoded>
    </item>
    <item>
      <title>Ollama vs LM Studio: Which Local LLM Tool Should You Use?</title>
      <link>https://mrsaynothing.dev/en/blog/2026-09-01/ollama-vs-lm-studio</link>
      <guid isPermaLink="true">https://mrsaynothing.dev/en/blog/2026-09-01/ollama-vs-lm-studio</guid>
      <pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate>
      <description>Ollama vs LM Studio compared for real dev work: install, GPU use, speed and API serving — with runnable commands so you can pick the right tool today.</description>
      <content:encoded><![CDATA[<!--[--><p><strong>Ollama vs LM Studio in one line: pick LM Studio if you want a desktop GUI to browse and chat with models; pick Ollama if you want a lightweight, scriptable local API server.</strong> Both are free, both run GGUF models on your own GPU or CPU, and both can serve an OpenAI-compatible endpoint — so many developers install both and use them for different jobs. This guide compares setup, GPU handling, speed and API serving with real commands you can run today, so you can stop reading Reddit threads and start generating tokens locally.</p> <h2>What is the difference between Ollama and LM Studio?</h2> <p>The core difference is interface and intent:</p> <ul><li><strong>Ollama</strong> is a CLI-first runtime. You pull a model with one command and it runs as a background service on <code>localhost:11434</code>, exposing a REST API. There is no built-in chat window — it is built to be the “Docker for LLMs” that other tools plug into.</li> <li><strong>LM Studio</strong> is a full desktop application (Electron) with a model search browser, chat UI, per-model settings (context length, GPU offload layers, temperature) and a local server mode you toggle with a click.</li></ul> <p>Both understand the <strong>GGUF format</strong> and both drive the same underlying engine lineage — Ollama embeds llama.cpp, and LM Studio uses llama.cpp-based runtimes it downloads and updates for you. That means raw generation quality for the same model file is effectively identical; the tools differ in everything <em>around</em> the model.</p> <h2>Is Ollama free for commercial use?</h2> <p>Yes. Ollama is <strong>open source (MIT)</strong> and free for commercial use — you only owe the <em>model’s</em> license, not Ollama’s. Llama, Mistral, Qwen and Gemma each carry their own terms, so check the model card if you ship product on top of one.</p> <p>LM Studio is <strong>free for personal use</strong> but ships a closed-source license: work use requires a free “work” license flag, and companies above a revenue threshold pay for it. If your employer’s procurement team asks hard questions, that difference alone can decide the Ollama vs LM Studio debate.</p> <h2>Does Ollama use your GPU automatically?</h2> <p>Yes — Ollama detects CUDA (NVIDIA), Metal (Apple Silicon) and ROCm (AMD) at launch and offloads as many layers as fit in VRAM. Two checks worth knowing:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># What did Ollama actually load — GPU or CPU?</span>
ollama <span class="token function">ps</span>

<span class="token comment"># Force the issue if it silently fell back to CPU:</span>
<span class="token assign-left variable">OLLAMA_NUM_GPU</span><span class="token operator">=</span><span class="token number">999</span> ollama run qwen2.5-coder:7b</code><!----></pre> <p>If <code>ollama ps</code> shows <code>100% GPU</code>, you are offloaded; a split like <code>48%/52% CPU/GPU</code> means the model did not fit and you will feel it in tokens/sec. LM Studio exposes the same control as a <strong>GPU offload slider</strong> per model, which is friendlier when you want to experiment — one of its genuinely better UX touches.</p> <h2>Which is faster, Ollama or LM Studio?</h2> <p>For the same model, quantisation and hardware: <strong>effectively a tie</strong>, because both delegate to llama.cpp. Benchmark claims of “Ollama is faster” or vice versa usually compare different quants or context lengths. Measure on your own machine instead of trusting either camp:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token comment"># Ollama: --verbose prints eval rate (tokens/sec) at the end</span>
ollama run qwen2.5-coder:7b <span class="token parameter variable">--verbose</span> <span class="token string">"Summarise what a Makefile does in one sentence."</span></code><!----></pre> <p>In LM Studio, load the same GGUF file with identical context length and GPU layers, then watch tokens/sec in the chat’s stats panel. Whichever shows higher numbers, rerun the test twice — first load includes warm-up noise.</p> <h2>Can you run LM Studio as a local API server?</h2> <p>Yes — head to the <strong>Developer</strong> tab, start the server, and you get an OpenAI-compatible endpoint on <code>localhost:1234</code>. It even has a CLI (<code>lms</code>) for scripting:</p> <pre class="language-bash"><!----><code class="language-bash">lms server start
lms load qwen2.5-coder-7b-instruct <span class="token parameter variable">--gpu</span> max
<span class="token function">curl</span> http://localhost:1234/v1/chat/completions <span class="token punctuation"></span>
  <span class="token parameter variable">-H</span> <span class="token string">"Content-Type: application/json"</span> <span class="token punctuation"></span>
  <span class="token parameter variable">-d</span> <span class="token string">'&#123;"model":"qwen2.5-coder-7b-instruct","messages":[&#123;"role":"user","content":"Write a jq filter for the top process by CPU"&#125;]&#125;'</span></code><!----></pre> <p>Ollama’s equivalent API is always on once the service runs, and its native endpoint plus OpenAI-compatible routes need zero setup:</p> <pre class="language-bash"><!----><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://ollama.com/install.sh <span class="token operator">|</span> <span class="token function">sh</span>   <span class="token comment"># Linux install</span>
ollama pull qwen2.5-coder:7b
<span class="token function">curl</span> http://localhost:11434/api/chat <span class="token parameter variable">-d</span> <span class="token string">'&#123;
  "model": "qwen2.5-coder:7b",
  "messages": [&#123;"role": "user", "content": "Explain bash exit codes in two lines"&#125;],
  "stream": false
&#125;'</span></code><!----></pre> <p>Both slots straight into any tool that speaks the OpenAI API — VS Code extensions, coding agents, your own scripts. This is where Ollama pulls ahead: the service starts at boot, runs headless on a server or homelab box, and nothing depends on a desktop app being open. I run exactly this setup on my own <a href="/en/projects/homelab">homelab</a>, where Ollama serves models to everything on the network while the head node stays mouse-free.</p> <h2>Which should you pick: Ollama or LM Studio?</h2> <table><thead><tr><th>Dimension</th><th>Ollama</th><th>LM Studio</th></tr></thead><tbody><tr><td>Interface</td><td>CLI + REST API</td><td>Full desktop GUI</td></tr><tr><td>Open source</td><td>MIT, fully</td><td>Closed, free personal tier</td></tr><tr><td>Commercial use</td><td>Free</td><td>Paid licence at scale</td></tr><tr><td>Model management</td><td><code>ollama pull &lt;model></code></td><td>Built-in search + download browser</td></tr><tr><td>Chat UI</td><td>None (bring your own)</td><td>Built-in</td></tr><tr><td>API endpoint</td><td>Always-on <code>:11434</code></td><td>Toggleable <code>:1234</code></td></tr><tr><td>Headless/server use</td><td>Excellent</td><td>Awkward</td></tr><tr><td>Windows / macOS / Linux</td><td>All three</td><td>Windows + macOS, Linux in beta</td></tr></tbody></table> <p>Rules of thumb, no regrets either way:</p> <ol><li><strong>You want to <em>use</em> models</strong> — chat, try them out, fiddle with sliders: <strong>LM Studio</strong>.</li> <li><strong>You want to <em>build</em> on models</strong> — scripts, agents, CI, a home API service: <strong>Ollama</strong>.</li> <li><strong>You want both</strong> — install both; they coexist fine (just don’t let both servers claim the same port, and remember a model loaded twice eats VRAM twice).</li></ol> <p>Local models pair especially well with agentic coding tools — point an OpenAI-compatible client at your local endpoint and you get unlimited completions with zero per-token cost. That combination powers the local-first setup behind <a href="/en/projects/freechat">freechat</a>, and it is the cheapest way to learn LLM plumbing: the only bill is the electricity.</p><!--]-->]]></content:encoded>
    </item>
  </channel>
</rss>