<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dustin VanKrimpen</title>
    <description>The latest articles on DEV Community by Dustin VanKrimpen (@dustinvk).</description>
    <link>https://dev.to/dustinvk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026197%2F1f9dd8d0-e6bf-4714-bf36-f227c6c2f3c8.jpg</url>
      <title>DEV Community: Dustin VanKrimpen</title>
      <link>https://dev.to/dustinvk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dustinvk"/>
    <language>en</language>
    <item>
      <title>Using My Local Coding Model From Anywhere</title>
      <dc:creator>Dustin VanKrimpen</dc:creator>
      <pubDate>Sun, 26 Jul 2026 13:43:25 +0000</pubDate>
      <link>https://dev.to/dustinvk/using-my-local-coding-model-from-anywhere-21na</link>
      <guid>https://dev.to/dustinvk/using-my-local-coding-model-from-anywhere-21na</guid>
      <description>&lt;p&gt;In the last post I got a coding model (&lt;strong&gt;qwen2.5-coder:14b&lt;/strong&gt;) running on a repurposed GPU (&lt;strong&gt;Radeon RX 6750 XT 12GB&lt;/strong&gt;) that AMD's compute stack officially won't support. Which is great, except for one thing: the box it runs on sits in a corner in my basement. A local model you can only use while physically sitting at the machine it runs on is  just inconvenient.&lt;/p&gt;

&lt;p&gt;The whole point of a local tier is that it is there when I am. At my laptop on the couch, on my phone waiting in line somewhere, from a coffee shop. This post is the connective tissue, detailing how I reach that headless box from anywhere without punching a single hole in my router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious answer is the wrong one
&lt;/h2&gt;

&lt;p&gt;Two things I wanted, and they pull against each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reach the machine, and its model endpoint, from any of my devices, wherever I am.&lt;/li&gt;
&lt;li&gt;Expose nothing to the public internet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The naive way to do (1) is port forwarding: tell your router to send traffic on some port straight to the box. It works, and it flatly violates (2). And it is especially reckless here, because the model endpoint has &lt;strong&gt;no authentication&lt;/strong&gt;. Ollama does not ask for a password. Anyone who can reach it can use it. Forwarding that to the open internet means giving your GPU to the first scanner that wanders by.&lt;/p&gt;

&lt;p&gt;So port forwarding is out. What I wanted was a way for my own devices to reach each other as if they were on the same local network, no matter what physical network each one is actually on, without anything being visible to anyone else. Keeping everything private, with nothing exposed, is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailscale: a private network that follows your devices around
&lt;/h2&gt;

&lt;p&gt;The tool I settled on is Tailscale. It is a mesh VPN built on WireGuard. It's not the kind of VPN that hides your traffic from your ISP. Instead, it's the kind that connects &lt;em&gt;your own devices&lt;/em&gt; to each other. You install it on each machine, they all join one private network (your "tailnet"), and every device gets a stable address in the &lt;code&gt;100.x&lt;/code&gt; range that is reachable from any other device on the tailnet, wherever they physically are.&lt;/p&gt;

&lt;p&gt;The clever part is how the connection gets made. A coordination server helps your devices find each other and then gets out of the way. The actual traffic is a direct, end-to-end encrypted, peer-to-peer link. There is no port forwarding, and nothing is exposed to the public internet. The same mechanism carries everything: SSH on port 22, the model endpoint on &lt;code&gt;:11434&lt;/code&gt;, and whatever else I run, all over the tailnet. Nothing public.&lt;/p&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free Personal tier is plenty.&lt;/strong&gt; Unlimited devices of your own, up to 6 users. A handful of personal machines is nowhere near any limit, and it costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install it on everything, one account.&lt;/strong&gt; The GPU box, the laptop, the phone, all on the same tailnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MagicDNS&lt;/strong&gt; gives your devices names instead of making you memorize &lt;code&gt;100.x&lt;/code&gt; addresses... in theory. See the gotchas.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For those who would rather not trust Tailscale's coordination server at all, there is an open-source, self-hostable replacement for it called Headscale. I plan on trying it out at some point and documenting my verdict here in the future, so stay tuned! &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two gotchas
&lt;/h2&gt;

&lt;p&gt;This is the part worth documenting, because these are the things that will make someone bounce off Tailscale thinking it's broken when it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mac app can get stuck, and the fix is the CLI build.&lt;/strong&gt; The standalone Tailscale Mac app leans on a macOS system extension, and that extension can wedge itself: no login prompt, the VPN toggle throws errors, and reinstalling does nothing. It is a known issue, more likely if you ever had an old App Store install or you're on a newer macOS. This happened to me, unfortunately. The fix that worked was installing the Homebrew CLI build instead (&lt;code&gt;brew install tailscale&lt;/code&gt;), which uses the &lt;code&gt;utun&lt;/code&gt; interface and sidesteps the system extension entirely. The tradeoff is you lose the menu-bar GUI and drive it from the terminal. Really no problem at all for me. It connects reliably, which is the part that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MagicDNS short names are flaky on that CLI build.&lt;/strong&gt; Once you are on the Homebrew CLI client, MagicDNS does not wire into the Mac's resolver as cleanly as the GUI app does. The name you set up may just not resolve. The fix is to use the full MagicDNS name or the raw &lt;code&gt;100.x&lt;/code&gt; IP. Slightly annoying, entirely livable once you know it. It's worth noting the MagicDNS names resolved fine on the Android client.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH, and making it survive real life
&lt;/h2&gt;

&lt;p&gt;With the tailnet up, SSH into the box is just &amp;lt;!--email_off--&amp;gt;&lt;code&gt;ssh you@100.x.x.x&lt;/code&gt;&amp;lt;!--email_on--&amp;gt; from any of your devices. A few things make it pleasant instead of fiddly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key auth, no passwords.&lt;/strong&gt; Standard &lt;code&gt;ssh-copy-id&lt;/code&gt;. If you import your keys from GitHub during the Ubuntu install, this is basically already done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An SSH config alias&lt;/strong&gt; so you can type &lt;code&gt;ssh yourMachine&lt;/code&gt; and never think about IPs again. In &lt;code&gt;~/.ssh/config&lt;/code&gt;, point &lt;code&gt;Host yourMachine&lt;/code&gt; at the raw &lt;code&gt;100.x&lt;/code&gt; address. This has a nice side effect: because you hardcoded the IP, the alias works even when MagicDNS is being flaky. It routes around the gotcha above entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tmux for anything long-running.&lt;/strong&gt; An SSH session dies when your laptop sleeps or your WiFi drops, and anything running directly in that session dies with it. tmux runs the session &lt;em&gt;on the box&lt;/em&gt;, decoupled from your connection, so you start something, detach, close the lid, and reattach later from a different device exactly where you left off. This is load-bearing once you are kicking off unattended work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mosh for the phone.&lt;/strong&gt; Plain SSH drops when your phone roams between WiFi and cellular. mosh survives that, so a mobile session does not die every time you walk out of the building. mosh plus tmux is the mobile combo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exposing the model endpoint (only to the tailnet)
&lt;/h2&gt;

&lt;p&gt;Here is the piece specific to the model. By default Ollama binds to &lt;code&gt;127.0.0.1&lt;/code&gt;, meaning it only listens to the box itself. To let other devices reach it, you tell it to listen on all interfaces with a systemd drop-in that sets &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That sounds alarming, and it would be, except for what actually holds the boundary. The bind is broad, but &lt;strong&gt;the firewall is the fence.&lt;/strong&gt; &lt;code&gt;ufw&lt;/code&gt; is set to default-deny incoming, with a single allow rule scoped to the tailnet interface (&lt;code&gt;ufw allow in on tailscale0&lt;/code&gt;). So the endpoint listens broadly, but it's only &lt;em&gt;reachable&lt;/em&gt; over the tailnet. Nothing on the physical LAN or the public internet can touch it. This matters enormously precisely because the endpoint is unauthenticated: the firewall scoping is the only thing standing between "my private model" and "a GPU for anyone on the internet." It is not optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one command that proves the whole chain
&lt;/h2&gt;

&lt;p&gt;There are three moving parts stacked here: the broad bind, the ufw rule, and Tailscale itself. When something doesn't connect, it is tempting to guess which layer is at fault. Don't guess. Prove it from a real client with one request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://100.x.x.x:11434/api/tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that from the laptop, not from the box. If it returns JSON listing your models, the entire chain is proven end to end. The bind is listening, the firewall is allowing tailnet traffic, and Tailscale is carrying it. If it hangs or refuses, you've narrowed it to the network path rather than the model. This is the fastest "is it actually working" check, and I run it before wiring up anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually using it from the laptop
&lt;/h2&gt;

&lt;p&gt;Now the payoff. Anything that speaks to Ollama can be pointed at the box over the tailnet. The &lt;code&gt;ollama&lt;/code&gt; CLI and most Ollama-aware tools respect an &lt;code&gt;OLLAMA_HOST&lt;/code&gt; environment variable, so on the laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OLLAMA_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://100.x.x.x:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the CLI is now a remote control for the GPU box. Nothing runs locally on the laptop; it is purely a client. For tools that only speak the OpenAI API shape, Ollama also exposes an OpenAI-compatible endpoint at the &lt;code&gt;/v1&lt;/code&gt; path, which you point the tool at with any dummy API key.&lt;/p&gt;

&lt;p&gt;The reason this works so cleanly is a small architectural point worth internalizing: the model is stateless per request, and "workspace context" is just files you put into the request. So you can gather context on the machine that has your files (the laptop) and send only the model call to the remote GPU. The code and the compute do not have to live in the same place.&lt;/p&gt;

&lt;p&gt;In a future post, I will dive deeper into the tooling that you can connect to your local LLM for development. Currently, I've been experimenting with using JetBrains Gateway and Cline. &lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoff: plan a way back in
&lt;/h2&gt;

&lt;p&gt;As configured, SSH into this box rides &lt;em&gt;only&lt;/em&gt; the tailnet firewall rule. So if Tailscale fails to come up on boot, the machine is unreachable over the tailnet, and right now that is the main door.&lt;/p&gt;

&lt;p&gt;The fix is cheap and does not compromise the posture: add a second &lt;code&gt;ufw&lt;/code&gt; rule that allows SSH from your local network, scoped to your subnet (&lt;code&gt;ufw allow from 192.168.x.0/24 to any port 22&lt;/code&gt;). That is still not public. Your LAN is not the internet, and you are not touching the router or forwarding any ports. You are just widening the way in from "tailnet only" to "tailnet, or the same physical network." When Tailscale is down and you are home, you can still walk over to your own network and get in.&lt;/p&gt;

&lt;p&gt;The LAN rule only saves you when you are physically on the LAN. If the tunnel dies while you are out, you are still waiting until you get home to console in or reboot. But for a headless box sitting in my house, "I can always reach it from my own network" covers nearly every lockout worth worrying about, and it is a rule that only needs to be set once. The thing to actually avoid is having &lt;em&gt;neither&lt;/em&gt; path, tailnet-only with no LAN fallback, on a box you have made a dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the project
&lt;/h2&gt;

&lt;p&gt;So now the model from post 1 is not stuck in a corner. I can reach it from my laptop or my phone, from anywhere, and point tools at it as if it were local, with nothing exposed to the internet and one curl to prove the whole path. That is the local tier.&lt;/p&gt;

&lt;p&gt;The next question is the one that decides whether any of this was worth it: &lt;strong&gt;is the model actually good enough to trust with real software engineering work?&lt;/strong&gt; A quantized 14B on a consumer card is capable, but capable enough to hand real tasks to and route around a frontier model? The benchmarking of it will be my next post. And it has a twist, because the first numbers I got were badly wrong for reasons that had nothing to do with the model.&lt;/p&gt;

&lt;p&gt;The bigger goal underneath all of it is the same: a coding agent that does the cheap work locally, for free and off any rate limit, and only spends frontier-model credits when the problem actually earns one. A free tier LLM reachable from anywhere is most of the setup for that. Now I need to know how much of my real work it can actually carry.&lt;/p&gt;

&lt;p&gt;That is all for today. Go make the machine in your closet reachable from anywhere.&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>homelab</category>
      <category>tailscale</category>
      <category>ollama</category>
    </item>
    <item>
      <title>I Gave My Coding Agent A Free Tier</title>
      <dc:creator>Dustin VanKrimpen</dc:creator>
      <pubDate>Sun, 12 Jul 2026 16:33:42 +0000</pubDate>
      <link>https://dev.to/dustinvk/i-gave-my-coding-agent-a-free-tier-59cg</link>
      <guid>https://dev.to/dustinvk/i-gave-my-coding-agent-a-free-tier-59cg</guid>
      <description>&lt;p&gt;&lt;em&gt;Note: There is a TL;DR section near the bottom for anyone who just wants the setup notes without the story.&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;I like using the latest frontier class models for the hard stuff. I do not love spending their rate limits on "rename this variable" and "write the obvious boilerplate." So I decided to host a large language model (LLM) on my own hardware, free and off of rate limits: soak up the easy work locally, save the frontier credits for the work that needs them, and keep my data off some AI corporation's servers while I'm at it. This post is step #1: setting up the hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sourcing hardware during the RAMpocalypse
&lt;/h2&gt;

&lt;p&gt;2026 is a brutal market for PC components. There's a massive AI-driven shortage of DRAM and GDDR which has made prices skyrocket. As of this post, a 32 GB DDR4 kit that used to cost about $50-$70 is sitting at roughly $200. Rather than get fleeced in a seller's market, I decided to just use hardware that I had lying around.&lt;/p&gt;

&lt;p&gt;Enter my gaming PC. I built it back in 2020 during the pandemic restrictions. Back then I had all the free time in the world for gaming. It's mostly been collecting dust since my first son was born in 2022. The perfect candidate for my AI server.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Specs
&lt;/h3&gt;

&lt;p&gt;Originally the gaming PC had a GTX 1080 carried over from an even earlier build. Around 3 years ago I impulse bought a RX 6750 XT 12GB on a good sale and swapped it in. The RX 6750 XT is a fine mid-level card for 1080p and some 1440p gaming today. However, it is generally not a card that you'll see recommended for running LLMs locally.&lt;/p&gt;

&lt;p&gt;That said, it's not a bad card for it either (with some caveats I will get to later). For inference, the most important factor is VRAM, because that's what determines what size of model you can hold. The 12 GB in my RX 6750 XT is enough to hold a real coding model, with extra room for context. I didn't set out to choose AMD over NVIDIA here. It's just what I had.&lt;/p&gt;

&lt;p&gt;The rest of the box is nothing special. The CPU is a genuinely dated Ryzen 5 2600, but that's ok because it doesn't need to do much during GPU inference. It has 16 GB of DDR4 RAM. A bit tight since I also want to run orchestration processes on this machine, but it's fine given the sheer redonkulousness of RAM prices in 2026.&lt;/p&gt;

&lt;p&gt;I did a fresh install of Ubuntu 26.04 since it's free, lean, fits the tooling, and I plan on mostly using it headless via SSH anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full spec table for the curious:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What I Have&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Ryzen 5 2600 (Zen+, 6c/12t, 2018)&lt;/td&gt;
&lt;td&gt;Adequate for now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;Radeon RX 6750 XT 12GB (RDNA2, gfx1031)&lt;/td&gt;
&lt;td&gt;Fine for now, but I'm keeping an eye out for deals on something like an RX 7900 XTX or RTX 3090 to upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;16 GB DDR4&lt;/td&gt;
&lt;td&gt;Will upgrade when RAM prices come down&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The unsupported card problem
&lt;/h2&gt;

&lt;p&gt;Here is where the caveat of my GPU comes into play. AMD's compute stack is called ROCm. This is their answer to NVIDIA's CUDA. There is a hardcoded list of supported GPU targets for ROCm, and your card has to be on it. My RX 6750 XT reports its chip target as &lt;code&gt;gfx1031&lt;/code&gt;, which is not supported. The very similar chip in the 6800 and 6900 series cards is &lt;code&gt;gfx1030&lt;/code&gt;, and that one is supported. &lt;code&gt;gfx1031&lt;/code&gt;, the 6700 series, is not. Same architecture, one digit off, left off the list.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
ROCm's official supported-GPU list changes between releases, and gfx1031 support has been a moving target. Best to check for yourself since things may have changed since the time of this posting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The usual workaround
&lt;/h3&gt;

&lt;p&gt;There is a very simple workaround that a lot of people have reported success with on Linux. It basically tricks ROCm into thinking your card is supported. You install the GPU drivers and ROCm runtime as usual, add your user account to the video and render groups, and then spoof the GFX version to &lt;code&gt;10.3.0&lt;/code&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HSA_OVERRIDE_GFX_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vulkan: the alternative
&lt;/h3&gt;

&lt;p&gt;Vulkan is a graphics and compute API that these cards support natively with no ROCm needed. I am using Ollama to serve models, which runs inference on the Vulkan backend no problem. The main downside is it's inference-only, meaning you can't fine-tune or train models. Since I'm only trying to serve an existing model for now, it's a non-issue for me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It's not that Vulkan is inherently incapable of training/tuning models. It's just that the typical training stack targets CUDA/ROCm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Comparison: ROCm workaround vs. Vulkan (RX 6750 XT / gfx1031)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;ROCm (via &lt;code&gt;HSA_OVERRIDE_GFX_VERSION=10.3.0&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Vulkan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Needs the override env var to spoof the GPU target&lt;/td&gt;
&lt;td&gt;Natively supported, no workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fragile: can fail to init; updates can silently break it&lt;/td&gt;
&lt;td&gt;Stable; part of Mesa, survives kernel updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fine-tuning / training&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supported; the ML-native path (PyTorch-ROCm, vLLM)&lt;/td&gt;
&lt;td&gt;Unsupported; inference only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tooling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Richer (&lt;code&gt;rocm-smi&lt;/code&gt; for monitoring)&lt;/td&gt;
&lt;td&gt;Lighter (&lt;code&gt;radeontop&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;For my use case (a mostly-headless inference box), Vulkan wins, and I'm not giving up much for it. The old assumption that ROCm is meaningfully faster has largely eroded: recent benchmarks put ROCm anywhere from ~10–20% ahead (mostly on prompt processing) to level with or behind Vulkan. On RDNA2 in particular, the generation the Vulkan backend was first tuned on, the two are close enough that speed isn't the deciding factor.&lt;/p&gt;

&lt;p&gt;The goal of this project is to make AI-assisted workflows easier for me. I want to spend my time building cool things, not debugging why the latest update broke my hacky workaround. Vulkan just works, and keeps working across driver updates, which is what I want for an always-on server.&lt;/p&gt;

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

&lt;p&gt;I started with a clean slate. A fresh install of Ubuntu 26.04 LTS Desktop. I went with desktop because I do have a monitor attached (set up at a desk in my basement), and will occasionally work from it directly using IDEs and such. But most of the time it will be connected to and used headless via my MacBook Air.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The Vulkan graphics stack
&lt;/h3&gt;

&lt;p&gt;Update the system and install the Vulkan userspace drivers and a couple of&lt;br&gt;
diagnostic tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; mesa-vulkan-drivers vulkan-tools radeontop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the card is visible to Vulkan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vulkaninfo | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; deviceName
&lt;span class="c"&gt;# should print "AMD Radeon RX 6750 XT" (mine shows RADV NAVI22)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;radeontop&lt;/code&gt; is the tool we'll use later to prove the GPU is actually doing the&lt;br&gt;
math, so it's worth installing now.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Install Ollama and pull a model
&lt;/h3&gt;

&lt;p&gt;Ollama is the easiest way to serve a local model. Install it with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this hardware Ollama installs as a systemd service. During the first run, it&lt;br&gt;
resolves the Vulkan backend, drops the unsupported ROCm device, and&lt;br&gt;
loads onto the discrete RX 6750 XT with ~11.6 GiB of usable VRAM.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When Ollama starts, it will show a warning that it dropped the ROCm device. This is expected on a &lt;code&gt;gfx1031&lt;/code&gt; card. It realized that the GPU doesn't support ROCm and fell back to Vulkan. That's what you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Picking a model for 12GB of VRAM
&lt;/h4&gt;

&lt;p&gt;Now that Ollama is set up, it's time to choose a model for it to serve. This is where the VRAM budgeting gets real.&lt;/p&gt;

&lt;p&gt;My VRAM limitation rules out the current flagships: qwen3-coder:30b (~19GB), Codestral 22B (~13GB), and anything 24B+ dense. What's left is still a solid field of 7–16B coders.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;~Q4 size&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;qwen2.5-coder:14b&lt;/strong&gt; &lt;em&gt;(chosen)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;14.8B dense&lt;/td&gt;
&lt;td&gt;~9GB&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;Best dense code quality in the tier; native tool-calling + fill-in-the-middle. Non-reasoning, so it's fast.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen2.5-coder:7b&lt;/td&gt;
&lt;td&gt;7B dense&lt;/td&gt;
&lt;td&gt;~4.7GB&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;Same family, more headroom/speed, lower ceiling.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;14.8B dense&lt;/td&gt;
&lt;td&gt;~9GB&lt;/td&gt;
&lt;td&gt;32K+&lt;/td&gt;
&lt;td&gt;Strong generalist that codes well, but not code-specialized.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-coder-v2:16b&lt;/td&gt;
&lt;td&gt;16B MoE (2.4B active)&lt;/td&gt;
&lt;td&gt;~8.9GB&lt;/td&gt;
&lt;td&gt;160K&lt;/td&gt;
&lt;td&gt;Fastest option (MoE) and by far the largest context. Weaker at multi-step tool use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek-r1-distill-qwen:14b&lt;/td&gt;
&lt;td&gt;14B dense (reasoning)&lt;/td&gt;
&lt;td&gt;~9GB&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;Thinks step-by-step. Great for hard debugging, slower for everyday generation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I picked it because this box is intended to be the &lt;em&gt;worker tier&lt;/em&gt; of a two-tier setup: easy tasks run locally, hard ones escalate to externally hosted models. I wanted the fastest, most reliable, tool-capable code specialist that fits. If I need a higher-reasoning model, I reach for an externally hosted 'frontier-class' one.&lt;/p&gt;
&lt;h4&gt;
  
  
  Pulling
&lt;/h4&gt;

&lt;p&gt;With the model chosen, pulling it is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen2.5-coder:14b-instruct-q4_K_M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick smoke test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run qwen2.5-coder:14b-instruct-q4_K_M &lt;span class="s2"&gt;"write a python function that reverses a string"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it answers, the model runs. However, that does &lt;strong&gt;not&lt;/strong&gt; prove it's using the&lt;br&gt;
GPU. That part is next.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Prove it's actually on the GPU (not silently on the CPU)
&lt;/h3&gt;

&lt;p&gt;It's possible for Ollama to discover your GPU, load the weights into VRAM, and still run the compute on CPU.&lt;br&gt;
The logs will look normal, VRAM will show occupied, and the tokens will trickle while your CPU melts.&lt;/p&gt;

&lt;p&gt;To verify the GPU compute, I used &lt;code&gt;radeontop&lt;/code&gt;. Open it in one terminal like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;radeontop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a second terminal, fire off a prompt long enough to sustain load for several seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run qwen2.5-coder:14b-instruct-q4_K_M &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"write a detailed, heavily commented python implementation of a red-black tree with insert, delete, and search"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A tmux tip: split the terminal (&lt;code&gt;Ctrl-b %&lt;/code&gt;), run &lt;code&gt;radeontop&lt;/code&gt; on the left and&lt;br&gt;
your &lt;code&gt;ollama run&lt;/code&gt; on the right, and you can literally watch the GPU spike as it&lt;br&gt;
"thinks." It's oddly satisfying.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now watch &lt;code&gt;radeontop&lt;/code&gt; while tokens are streaming. What you're looking for:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Idle (between generations)&lt;/th&gt;
&lt;th&gt;Active (mid-generation)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graphics pipe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1–2%&lt;/td&gt;
&lt;td&gt;jumps high (&lt;strong&gt;~93%&lt;/strong&gt; on mine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shader Clock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0.4%&lt;/td&gt;
&lt;td&gt;climbs toward max (&lt;strong&gt;~90%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VRAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~10.3 / 12.2 GB&lt;/td&gt;
&lt;td&gt;same (model stays resident)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The signal is the &lt;strong&gt;transition&lt;/strong&gt;: idle → active → idle, synced to token output.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Watch Graphics pipe and Shader Clock, not VRAM. The weights can be in VRAM while the math runs on the CPU. You have to watch during streaming. The compute drops to zero as soon as generation ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the VRAM numbers:&lt;/strong&gt; Ollama reports usable VRAM in &lt;strong&gt;GiB&lt;/strong&gt; (it logged ~11.6 GiB available), while &lt;code&gt;radeontop&lt;/code&gt; reads out in &lt;strong&gt;GB&lt;/strong&gt; using its own accounting. The two don't line up exactly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On my box, Graphics pipe hit ~93% and Shader Clock ~90% during generation, then fell to 0% and ~0.11% after. That confirms GPU inference&lt;br&gt;
over Vulkan. If your compute stays flat while tokens are streaming, that indicates CPU fallback, so you'll need to do some debugging.&lt;/p&gt;


&lt;img src="/files/posts/hosting-my-own-llm/gpu-compute-validation.png" alt="Verifying GPU compute" width="800" height="718"&gt;GPU compute verified.


&lt;h3&gt;
  
  
  Step 4: The context-window decision (8K, on purpose)
&lt;/h3&gt;

&lt;p&gt;Ollama picks a default context length for you, but I wanted to set it explicitly&lt;br&gt;
because context costs VRAM. A bigger context window means a bigger KV cache&lt;br&gt;
sitting in memory alongside the weights. On a 12 GB card, that budget is tight, so&lt;br&gt;
I capped the default served context at 8K, which gives ~10.3 GB resident&lt;br&gt;
with headroom to spare (even though Qwen2.5-Coder's native context is 32K).&lt;/p&gt;

&lt;p&gt;I set it with a systemd drop-in, so I'm not editing Ollama's packaged unit file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/systemd/system/ollama.service.d
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/ollama.service.d/override.conf &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Service]
Environment="OLLAMA_CONTEXT_LENGTH=8192"
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl show ollama &lt;span class="nt"&gt;-p&lt;/span&gt; Environment &lt;span class="nt"&gt;--no-pager&lt;/span&gt;   &lt;span class="c"&gt;# echoes OLLAMA_CONTEXT_LENGTH=8192&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8K is a fine default for most coding prompts. If a specific task needs&lt;br&gt;
more, you don't have to change the service. You can override context per&lt;br&gt;
request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:11434/api/generate &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "model": "qwen2.5-coder:14b-instruct-q4_K_M",
  "prompt": "...",
  "stream": false,
  "options": { "num_ctx": 16384, "temperature": 0.2 }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raise &lt;code&gt;num_ctx&lt;/code&gt; for the one long-context call that needs it. Don't leave it high&lt;br&gt;
globally or you'll pay the VRAM tax on every request.&lt;/p&gt;

&lt;p&gt;A couple of other handy per-request knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;temperature&lt;/code&gt; 0.1–0.3 for deterministic code, higher for brainstorming&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keep_alive&lt;/code&gt; pass &lt;code&gt;-1&lt;/code&gt; to pin the model resident when you're about to hammer it (by default it unloads after 5 idle minutes and the next request eats a few-second reload).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollama also exposes an OpenAI-compatible API at &lt;code&gt;http://localhost:11434/v1&lt;/code&gt;. &lt;br&gt;
Any tool compatible with "OpenAI API, custom base&lt;br&gt;
URL" can point at it locally with a dummy key. The server is stateless. It takes&lt;br&gt;
the full message history on every call, so your client holds the conversation.&lt;/p&gt;

&lt;p&gt;I also set up &lt;a href="https://dev.to/blog/zram-ignored-my-config/"&gt;zram&lt;/a&gt; as a compressed-RAM cushion for the orchestration I plan to stack on top, since 16 GB is tight.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The cliffnotes on setting up your own self-hosted LLM server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install an LTS Linux&lt;/strong&gt; (Ubuntu 24.04/26.04) with a recent kernel for your GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your GPU's compute support.&lt;/strong&gt; AMD: is your &lt;code&gt;gfx&lt;/code&gt; target on ROCm's list?
If not (e.g. RX 6700-series gfx1031), plan on &lt;strong&gt;Vulkan&lt;/strong&gt;:
&lt;code&gt;sudo apt install mesa-vulkan-drivers vulkan-tools radeontop&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install Ollama:&lt;/strong&gt; &lt;code&gt;curl -fsSL https://ollama.com/install.sh | sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull a model that fits your VRAM.&lt;/strong&gt; 12 GB → a 14B at Q4_K_M (~10 GB). Don't
try to cram a 30B into 12 GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify GPU compute&lt;/strong&gt; with &lt;code&gt;radeontop&lt;/code&gt; &lt;em&gt;during&lt;/em&gt; generation. Watch Graphics
pipe / Shader Clock, not VRAM. Discovery ≠ compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the context ceiling&lt;/strong&gt; with a systemd drop-in
(&lt;code&gt;OLLAMA_CONTEXT_LENGTH=8192&lt;/code&gt;) to keep the KV cache inside your VRAM budget;
override per request with &lt;code&gt;num_ctx&lt;/code&gt; when needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;This box is the worker tier of a two-tier setup: the easy work runs here, the hard work escalates to a frontier model. &lt;a href="https://dev.to/dustinvk/using-my-local-coding-model-from-anywhere-21na"&gt;Reaching it from anywhere without exposing anything to the internet&lt;/a&gt; is the next post. Whether a quantized 14B is actually good enough to trust is the one after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;For now, I am happy to have my own free model running on a mid-level card that I didn't have to pay 2026 prices for. You don't need the latest, most expensive hardware to run a real and useful model at home. All you need is something with enough VRAM. It doesn't even have to be on AMD's supported list.&lt;/p&gt;

&lt;p&gt;Go and check out what your idle GPU could be doing instead.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>homelab</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
