<?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: MojaLab</title>
    <description>The latest articles on DEV Community by MojaLab (@mojalab).</description>
    <link>https://dev.to/mojalab</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%2F4061358%2F64062de3-d84e-46f4-85e2-bc8361e15306.png</url>
      <title>DEV Community: MojaLab</title>
      <link>https://dev.to/mojalab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mojalab"/>
    <language>en</language>
    <item>
      <title>My coding-agent buddies live on a €5 VPS, and I watch them from the web</title>
      <dc:creator>MojaLab</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:09:58 +0000</pubDate>
      <link>https://dev.to/mojalab/my-coding-agent-buddies-live-on-a-eu5-vps-and-i-watch-them-from-the-web-2a9f</link>
      <guid>https://dev.to/mojalab/my-coding-agent-buddies-live-on-a-eu5-vps-and-i-watch-them-from-the-web-2a9f</guid>
      <description>&lt;p&gt;The scene: I'm at home, Claude Code is running on my MacBook, forty minutes into a refactor and nowhere near done, and — whoops, it's late. I have to leave, and life does not respect long-running agents. On a laptop-bound setup that's a dead session: SSH drops, tmux is on the other machine, the agent is orphaned mid-task.&lt;/p&gt;

&lt;p&gt;If instead I were working on my VPS, I'd close the laptop, walk out, and twenty minutes later pull out my phone, open &lt;code&gt;mterm.mylab.example.com&lt;/code&gt;, pass Authelia (password + TOTP), and find myself in front of &lt;em&gt;the same terminal&lt;/em&gt; — the agent still chewing through files, me scrolling its output with a thumb.&lt;/p&gt;

&lt;p&gt;You can rent a cloud IDE and get something similar with three clicks and a subscription. It's a perfectly fine answer for most people. But if you've been around MojaLab for a while you know the rule of the house: if a problem can be solved with off-the-shelf SaaS or with a weekend, a terminal and a few open-source tools — we take the weekend. Not because the SaaS is bad — it isn't — but because we like to understand by doing.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/doradame/mojalab-vps-stack?ref=mojalab.com" rel="noopener noreferrer"&gt;mojalab-vps-stack&lt;/a&gt;: a Docker Compose stack that turns any cheap VPS into a browser-first workbench for coding agents. Ten containers (one a one-shot initializer), one network, one Caddyfile. MIT-licensed, on GitHub.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;One gate (Authelia) in front of every door; the workspace shared between terminal and file manager; the plumbing talks to Docker only through a read-only proxy.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on what this post is.&lt;/strong&gt; The full step-by-step setup — DNS records, Authelia secrets, the guided installer, TOTP enrollment, troubleshooting — lives in the &lt;a href="https://github.com/doradame/mojalab-vps-stack?ref=mojalab.com#readme" rel="noopener noreferrer"&gt;README on the repository&lt;/a&gt;. It's the canonical reference and it gets updated when the stack does. If you want the how-to, go there (or just run &lt;code&gt;./scripts/install.sh&lt;/code&gt; and answer the questions). This post is the &lt;em&gt;why&lt;/em&gt;: the design decisions, the non-obvious failure modes, and what I learned running coding agents on this thing for real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why browser-first
&lt;/h2&gt;

&lt;p&gt;The usual way to work on a remote dev box is SSH plus tmux, and it works — for you, from your laptop, with your keys. It stops working the moment the client isn't your laptop: a phone, a tablet, a borrowed machine, a locked-down corporate desktop. And "install Termius and copy your private key to your phone" is a sentence that should make you flinch.&lt;/p&gt;

&lt;p&gt;The browser is the one client that exists everywhere. So the design goal was: &lt;strong&gt;every useful thing reachable from a URL, every URL behind one real authentication gate.&lt;/strong&gt; Not a VPN (nothing to install), not per-app passwords (one gate, one session), not security-by-obscurity on a high port (scanners don't care).&lt;/p&gt;

&lt;p&gt;The gate is &lt;a href="https://www.authelia.com/?ref=mojalab.com" rel="noopener noreferrer"&gt;Authelia&lt;/a&gt;: single sign-on with TOTP second factor, enforced by Caddy's &lt;code&gt;forward_auth&lt;/code&gt; on every subdomain. Behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;term.*&lt;/code&gt; — &lt;a href="https://zellij.dev/?ref=mojalab.com" rel="noopener noreferrer"&gt;Zellij&lt;/a&gt;'s native web client: full multiplexer, panes, tabs, sessions that survive disconnects&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;mterm.*&lt;/code&gt; — a mobile-friendly terminal (wetty) that SSHes into &lt;em&gt;the same&lt;/em&gt; Zellij container and can join &lt;em&gt;the same&lt;/em&gt; session&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;files.*&lt;/code&gt; — Filestash, a web file manager over the shared workspace&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;stats.*&lt;/code&gt; — Glances, so you can watch the agent eat CPU in real time&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;home.*&lt;/code&gt; — a landing page with links, because muscle memory needs a home&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus the plumbing you don't see: Watchtower (monitor-only — it &lt;em&gt;tells&lt;/em&gt; me about updates via Telegram, it never surprise-updates anything), a Telegram bot for &lt;code&gt;/stats&lt;/code&gt; and proactive alerts, and a read-only Docker socket proxy so neither of those can ever exec into a container even if compromised.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workbench: a container the agents can trash
&lt;/h2&gt;

&lt;p&gt;The heart of the stack is the Zellij container, and it's shaped by one observation: &lt;strong&gt;coding agents are messy tenants.&lt;/strong&gt; They install packages, compile things, run test suites, spawn processes. You want to give them room to do that — and walls they can't take down.&lt;/p&gt;

&lt;p&gt;A preemptive confession about Zellij: at first it's a little annoying. The shortcuts aren't tmux's, the keybinding bar looks like noise, and your first ten minutes go into closing panes you opened by accident. Then it clicks — panes, tabs, the session manager, the detach that &lt;em&gt;just works&lt;/em&gt; — and you grow fond of it. Give it an evening before you judge it.&lt;/p&gt;

&lt;p&gt;The room: Debian trixie with Node 22, Python 3.13, &lt;code&gt;build-essential&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;ripgrep&lt;/code&gt;, and — this one matters more than it looks — a real &lt;code&gt;/usr/local/bin/fd&lt;/code&gt; symlink, not a shell alias. Aliases only exist in interactive shells; when Claude Code runs &lt;code&gt;fd&lt;/code&gt; through &lt;code&gt;sh -c&lt;/code&gt;, an alias is invisible and the command just fails. If you've ever wondered why an agent claims a tool "isn't installed" when you can clearly run it yourself: check whether it's an alias.&lt;/p&gt;

&lt;p&gt;The walls: the container gets a memory limit, a CPU limit and a &lt;code&gt;pids&lt;/code&gt; limit (an agent-written test suite fork-bombing your VPS into a reboot is funnier in retrospect than at 11 PM). The limits are set in &lt;code&gt;.env&lt;/code&gt;, and the installer suggests values based on the host — total RAM minus about 3 GB for the OS and the rest of the stack. On an 8 GB VPS, give the workbench 5 GB; on the €5, 4 GB box from the title, the installer lands on about 1 GB — enough for the agents themselves, but keep an eye on the heavy builds. Do &lt;em&gt;not&lt;/em&gt; lowball this one, and here's the war story: the stack originally shipped with a 512 MB limit on this container, which sounds reasonable until you learn how the OOM killer chooses its victim — it kills the &lt;em&gt;biggest process in the cgroup&lt;/em&gt;, which after a while is Zellij itself. Result: one hungry &lt;code&gt;npm install&lt;/code&gt; and every session, including the agent you were watching from the beach, dies. Memory limits on a workbench are not a tidiness feature. Size them like you mean it.&lt;/p&gt;

&lt;p&gt;State is split with intent. Project files live under &lt;code&gt;/srv&lt;/code&gt;, which both the terminal and the file manager see — upload a tarball from the browser, untar it in the shell. But the &lt;em&gt;credentials&lt;/em&gt; — &lt;code&gt;~/.claude&lt;/code&gt;, &lt;code&gt;~/.config&lt;/code&gt;, npm globals, API keys — live in a separate bind mount that Filestash &lt;strong&gt;cannot&lt;/strong&gt; see. Your web file manager should never be one misclick away from serving your Anthropic session token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the agents without the rebuild dance
&lt;/h3&gt;

&lt;p&gt;Agent CLIs release constantly, and the naive approach — bake them into the Docker image — means every update is a rebuild, and every rebuild kills your sessions. The fix is boring and effective: everything installs into &lt;code&gt;~/.local&lt;/code&gt;, which is a persistent bind mount that comes &lt;em&gt;first&lt;/em&gt; in &lt;code&gt;PATH&lt;/code&gt;. The image ships an &lt;code&gt;update-agents&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;update-agents
&lt;span class="go"&gt;→ claude    (npm @anthropic-ai/claude-code)
→ codex     (npm @openai/codex)
→ opencode  (npm opencode-ai)
→ kimi      (official installer)
→ agy       (self-updater)
→ kiro      (self-updater)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, no rebuild, no downtime, survives image rebuilds. Two details earned their place the hard way. First, npm's infamous &lt;code&gt;ENOTEMPTY&lt;/code&gt; rename bug (stale files in the old package dir, common right after a Node major bump): the script now clears the package directory — which holds no user state — and retries once, automatically. Second, Antigravity's &lt;code&gt;agy&lt;/code&gt; is &lt;em&gt;deliberately&lt;/em&gt; not installed from npm: there is no official package, and what you find under the obvious names on the registry is squatted placeholders. That's not paranoia, that's the supply chain in 2026. The script only drives &lt;code&gt;agy&lt;/code&gt;'s own self-updater after you've installed it once from Google's channel.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;git-ssh-key&lt;/code&gt;, born from doing the same chore on two VPSes in one afternoon: it generates a per-forge ed25519 key in persistent storage, prints the public key with the exact settings URL to paste it into (GitHub, GitLab, or your self-hosted forge), and wires &lt;code&gt;~/.ssh/config&lt;/code&gt; so it keeps working after rebuilds. One command, then &lt;code&gt;git clone git@github.com:...&lt;/code&gt; just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and windows into the lab: DNS, Resend, Filestash and Glances
&lt;/h2&gt;

&lt;p&gt;Before anything works, you need two things the installer can't do for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A domain.&lt;/strong&gt; The stack is browser-first, and the browser wants names, not ports. You need a domain (even a few-euros-a-year one) and a handful of A records pointing at the VPS's IP: &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;term&lt;/code&gt;, &lt;code&gt;mterm&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, &lt;code&gt;stats&lt;/code&gt;, &lt;code&gt;home&lt;/code&gt;. Or a single wildcard &lt;code&gt;*.lab.yourdomain.tld&lt;/code&gt;, which is the route I recommend — one record, zero maintenance when you add a service. Caddy handles the certificates itself, but only if the names resolve: if you run the installer before DNS has propagated, the certs fail and it all looks broken when it's really just early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A way to send email.&lt;/strong&gt; This is the prerequisite nobody expects: to let you register your TOTP, Authelia emails you an identity-verification link. No SMTP, no 2FA, no lab. I use &lt;a href="https://resend.com/?ref=mojalab.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt;: the free tier is more than enough for a single-user lab, the SMTP credentials go into &lt;code&gt;.env&lt;/code&gt; and you never think about it again. (For completeness: Authelia also has a filesystem notifier that writes the link to a file instead of sending it — it works, but it forces you into a shell on the host at exactly the moment you're setting up access &lt;em&gt;without&lt;/em&gt; a shell on the host. Resend is faster.)&lt;/p&gt;

&lt;p&gt;Then there are the two windows into the lab, each worth a few lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Glances&lt;/strong&gt; (&lt;code&gt;stats.*&lt;/code&gt;) is the easy one: zero configuration, open the page and watch the agent eat CPU in real time. It's the fastest way to answer "is it working, or has it hung?" without touching the terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filestash&lt;/strong&gt; (&lt;code&gt;files.*&lt;/code&gt;) is enormously useful but not ready out of the box: on first launch you configure it — admin password, storage backend pointed at the local &lt;code&gt;/srv&lt;/code&gt; mount. Five minutes, once, but skip it and you land on a setup page and think something exploded. The README has the exact steps.&lt;/p&gt;

&lt;p&gt;And here's a trick that earned its spot in this post: &lt;strong&gt;the Zellij token lives happily in a file under &lt;code&gt;/srv&lt;/code&gt;.&lt;/strong&gt; The native web client on &lt;code&gt;term.*&lt;/code&gt; has its own authentication, separate and independent from Authelia: a token it generates on first boot and asks for every so often — typically when the session expires or you switch machines. And a token, by its nature, isn't something you know by heart. The practical answer: save it in a file like &lt;code&gt;/srv/notes/zellij-token.txt&lt;/code&gt;. Filestash sees it, so from any browser you log in through Authelia, open the file, copy, paste, and you're back in. (The mobile route on &lt;code&gt;mterm.*&lt;/code&gt; will never ask you for it — there, access to Zellij goes another way, which I get to in the security section.)&lt;/p&gt;

&lt;p&gt;Yes, it's a deliberate trade-off: that token is, effectively, a second line of defense behind Authelia, and putting it in a file reachable &lt;em&gt;behind that same Authelia&lt;/em&gt; weakens the second line. For my threat model — single user, mandatory TOTP — I happily accept it in exchange for never having to dig into the VPS to fish it out. We'll come back to this when the security model is on the table in full; if your threat model is different, just don't create the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phone part, and the IME rabbit hole
&lt;/h2&gt;

&lt;p&gt;The mobile terminal is where most of the interesting engineering hides, because mobile browsers actively fight you.&lt;/p&gt;

&lt;p&gt;Honestly, I'm not satisfied with the result yet — call it 60%. I use it, but it still needs work. Either way, here's how it works:&lt;/p&gt;

&lt;p&gt;Wetty gives you xterm.js over WebSocket, which is 80% of the job. The remaining 20% is why the stack builds a custom Caddy with the &lt;code&gt;replace-response&lt;/code&gt; module: Caddy injects a small vanilla-JS keyboard overlay into wetty's HTML on the fly — wetty itself stays untouched, so future wetty releases just work. The overlay adds what soft keyboards don't have: arrows (hold to auto-repeat), Esc, Tab, F-keys, one-tap Zellij actions (new pane, session manager, detach), one-tap &lt;code&gt;:wq&lt;/code&gt;, and sticky Ctrl/Alt — tap to arm for one keystroke, long-press to lock.&lt;/p&gt;

&lt;p&gt;That sticky Ctrl taught me more about mobile browsers than I wanted to know. The first implementation intercepted &lt;code&gt;beforeinput&lt;/code&gt; on xterm's hidden textarea, transformed the next letter into its control byte, and called &lt;code&gt;preventDefault()&lt;/code&gt;. Worked beautifully — on desktop. On a phone it did nothing: you'd arm Ctrl, type &lt;code&gt;o&lt;/code&gt;, and a literal &lt;code&gt;o&lt;/code&gt; appeared. The reason is that mobile keyboards don't send you a keydown with a letter in it; they compose text through the IME, the keydown arrives as &lt;code&gt;Unidentified&lt;/code&gt; keyCode 229, and the composition's &lt;code&gt;beforeinput&lt;/code&gt; is frequently &lt;strong&gt;not cancelable&lt;/strong&gt; — your &lt;code&gt;preventDefault()&lt;/code&gt; is silently ignored. The fix: intercept at the &lt;strong&gt;document level in the capture phase&lt;/strong&gt;, which runs before xterm's own listeners ever see the event, and use &lt;code&gt;stopImmediatePropagation()&lt;/code&gt; — which, unlike &lt;code&gt;preventDefault()&lt;/code&gt;, always works. Swallow the event, transform the character, inject the control byte yourself, clear the composition debris. If you're building anything that needs to intercept typed characters on mobile web: element-level &lt;code&gt;preventDefault&lt;/code&gt; is a desktop-only illusion.&lt;/p&gt;

&lt;p&gt;Same category of lesson, CSS edition: the overlay bar wraps onto a variable number of rows depending on the key set and the screen width, so any hardcoded "reserve 92px for the bar" guess is wrong on &lt;em&gt;some&lt;/em&gt; phone — and the terminal's last line hides under the buttons, which on a terminal means hiding &lt;em&gt;the prompt&lt;/em&gt;, the only line you actually care about. The bar now measures its own rendered height and publishes it as a CSS variable the terminal sizes against. And on iOS, where the soft keyboard overlays the page instead of resizing it, the bar rides up on &lt;code&gt;visualViewport&lt;/code&gt; so Esc and the arrows stay visible exactly when you're typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security model, honestly
&lt;/h2&gt;

&lt;p&gt;Everything behind the gate trusts the gate. That's the design, and it's the right trade-off for a single-user workbench — and the wrong one for anything multi-tenant, so let's be precise about what holds the line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Two gates before a shell&lt;/strong&gt; on &lt;code&gt;term.*&lt;/code&gt;: Authelia (password + TOTP) and then Zellij's own token. A remote shell on your VPS should not be one phishing away. The mobile route (&lt;code&gt;mterm.*&lt;/code&gt;) skips the second gate by design — wetty authenticates into the Zellij container with an internal SSH key that never leaves the Docker network — so there, Authelia alone holds the line. (And if you adopt the token-in-a-file-on-Filestash trick, this becomes the posture &lt;em&gt;everywhere&lt;/em&gt;: one gate, the real one, with TOTP. There's even an argument for it: two subdomains with two different postures are harder to reason about than one coherent system where Authelia holds the line, full stop. Know it and choose.)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Inside the container, the &lt;code&gt;lab&lt;/code&gt; user has passwordless sudo&lt;/strong&gt; — deliberately documented rather than hidden. Past both gates, an attacker owns &lt;em&gt;that container&lt;/em&gt;. The blast radius is the workbench, not the host: no Docker socket inside, resource limits on the way out.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Watchtower, Glances and the Telegram bot reach Docker through a read-only socket proxy&lt;/strong&gt; with all write endpoints denied (the bot only reads container logs, to alert on Authelia logins). A compromised metrics dashboard cannot exec into your terminal.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Watchtower never auto-updates.&lt;/strong&gt; It reports; you read release notes and pull deliberately. Surprise 3 AM updates on the box your agents live on is a genre of fun I've retired from. (Corollary: locally-built images are invisible to Watchtower — bumping Zellij itself is a one-line &lt;code&gt;.env&lt;/code&gt; change and a rebuild, on &lt;em&gt;your&lt;/em&gt; schedule.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does not defend against: a compromised password &lt;em&gt;and&lt;/em&gt; TOTP seed, kernel-level container escapes, or an attacker who already has shell on the host. One kernel, one trust boundary. Know what you're running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;This is a personal homelab published as-is, not a product. It's single-user by design. It doesn't back anything up — pair it with something like &lt;a href="https://mojalab.com/cryptosync-a-zero-knowledge-multi-device-backup-with-wasabi-cryptomator-and-rclone/" rel="noopener noreferrer"&gt;CryptoSync&lt;/a&gt; pointed at &lt;code&gt;/srv&lt;/code&gt; and the lab state directory. It assumes you'll read a Caddyfile before trusting it, and the repo's disclaimer is not boilerplate. The installer deliberately refuses to touch your firewall: UFW and Docker's iptables interact in ways that have locked better people than me out of better servers than mine — use your provider's firewall and read the README's hardening notes.&lt;/p&gt;

&lt;p&gt;And if what you actually want is "a dev box with zero setup", GitHub Codespaces exists and is genuinely good. This is for people who want the box to be &lt;em&gt;theirs&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the journey is worth it
&lt;/h2&gt;

&lt;p&gt;I could have stopped at "SSH works fine". I didn't, because the point was never just remote access. Somewhere between watching the OOM killer take down a session mid-refactor, tracing why a phone keyboard swallows Ctrl, and teaching an installer to tell bytes from megabytes, the stack stopped being a pile of YAML and became something I can &lt;em&gt;reason&lt;/em&gt; about — every port, every mount, every trust boundary placed on purpose. Now, when an agent works for forty minutes on my code, I know exactly what room it's working in, what it can touch, and how to look over its shoulder from anywhere with a browser.&lt;/p&gt;

&lt;p&gt;That's the MojaLab habit: take the long way once, so the short way feels different ever after.&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/doradame/mojalab-vps-stack?ref=mojalab.com" rel="noopener noreferrer"&gt;github.com/doradame/mojalab-vps-stack&lt;/a&gt;, MIT-licensed. Clone it on a fresh VPS, run &lt;code&gt;./scripts/install.sh&lt;/code&gt;, answer the questions, and fifteen minutes of DNS propagation later your agents have a home. Read the configs before you trust them — including mine.&lt;/p&gt;

&lt;p&gt;If you build something on top of it, or break something interesting, I'd love to hear about it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Made in MojaLab. Several coding agents were mildly inconvenienced by memory limits during the making of this stack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tools</category>
      <category>docker</category>
      <category>ai</category>
      <category>systemadministration</category>
    </item>
    <item>
      <title>From "token" to "MoE": the LLM glossary in dependency order</title>
      <dc:creator>MojaLab</dc:creator>
      <pubDate>Mon, 03 Aug 2026 22:21:42 +0000</pubDate>
      <link>https://dev.to/mojalab/from-token-to-moe-the-llm-glossary-in-dependency-order-2pjf</link>
      <guid>https://dev.to/mojalab/from-token-to-moe-the-llm-glossary-in-dependency-order-2pjf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A glossary to actually understand the terms you hit when reading about LLMs: token, embedding, attention, KV cache, GQA, MoE, quantization and the rest. But not alphabetical — &lt;strong&gt;in dependency order&lt;/strong&gt;: every entry uses only concepts already explained above, so if you read it start to finish, by the time you reach "Mixture of Experts" you already have all the pieces to get it. The analogies come from the world of the people who hang around this blog: networks, caches, PID controllers, tuning. No math prerequisite beyond "I know what an array is."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alphabetical LLM glossaries have a structural flaw: the "attention" entry sends you to "softmax", which is further down, which in turn uses "logits", which is earlier but assumes "vocabulary". You end up hopping around like a Wikipedia page at two in the morning.&lt;/p&gt;

&lt;p&gt;This document is built the other way around: like a chain. It's the glossary I wish I'd had, and the analogies are the ones that made the concepts click for me — someone coming from sysadmin, networks and controllers, not a PhD in ML. If you too think in terms of caches, error signals and control loops, you're in the right place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note of honesty, before we start.&lt;/strong&gt; This glossary doesn't come from knowledge I already possess. It comes from the questions &lt;em&gt;I&lt;/em&gt; asked an LLM — "explain the KV cache to me", "why that √d?" — and from the explanations &lt;em&gt;it&lt;/em&gt; wrote for me. Some sank in, others I'm still chewing on: I don't fully master everything you read here, and I won't pretend to. What I needed was a single place to come back to — a notebook to reopen when I run into one of these terms while reading something on the topic, to reread it slowly and with the analogies that pin it down for me. I've left it here in case it's useful to you too; but it is, first and foremost, mine.&lt;/p&gt;

&lt;p&gt;To keep our feet on the ground I'll use as a throughline the numbers of a small but modern-architecture model: &lt;strong&gt;180 million parameters, a 32,768-token vocabulary, internal size 640, 20 layers&lt;/strong&gt;. Small enough to train on human hardware, modern enough to contain every concept that matters in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 0 — What an LLM actually does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Language modeling
&lt;/h3&gt;

&lt;p&gt;The whole game is ONE thing: &lt;strong&gt;given a sequence of text, predict the next piece.&lt;/strong&gt; "The cat is on the" → probably "roof" or "couch", improbably "carburetor". An LLM is a giant function that takes text in and returns a probability distribution over the next piece. Everything else — chat, reasoning, code — emerges from this task repeated billions of times over human text. When it "generates", the model predicts a piece, appends it to the input, predicts the next, and so on. Like autocomplete taken to an absurd extreme.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token
&lt;/h3&gt;

&lt;p&gt;The "piece" above. Not a word nor a character: something in between, a frequent fragment. "computing" might be a single token; "quadcopter" might split into "quad"+"cop"+"ter". The &lt;strong&gt;tokenizer&lt;/strong&gt; is the component that converts text ↔ sequences of integers (each token has an ID). Think of a lookup table: token 4521 = " cat". The &lt;strong&gt;vocabulary&lt;/strong&gt; is the set of all known tokens — in our reference model, 32,768.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: it's a form of dictionary compression, conceptually a cousin of LZ — frequent sequences become single symbols.&lt;/p&gt;

&lt;h3&gt;
  
  
  BPE (Byte-Pair Encoding)
&lt;/h3&gt;

&lt;p&gt;The algorithm that &lt;em&gt;builds&lt;/em&gt; that dictionary: in the modern byte-level variant, you start from single bytes, find the most frequent adjacent pair in the corpus, merge it into a new symbol, and repeat until you reach the desired vocabulary size. Purely statistical, no linguistic magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 1 — The mathematical building blocks (the bare minimum)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vector, matrix, tensor
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;vector&lt;/strong&gt; is an array of floats. A &lt;strong&gt;matrix&lt;/strong&gt; is a 2D array. A &lt;strong&gt;tensor&lt;/strong&gt; is the generalization to N dimensions — in PyTorch code you see shapes like &lt;code&gt;(B, T, D)&lt;/code&gt; = (batch, positions in the sequence, dimensions per position). Nothing mystical: multidimensional arrays with vectorized operations, like NumPy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Matrix-vector multiplication = transformation
&lt;/h3&gt;

&lt;p&gt;The fundamental operation of ALL deep learning: &lt;code&gt;y = W @ x&lt;/code&gt;. Take a vector x, multiply it by a matrix W of &lt;strong&gt;weights&lt;/strong&gt;, get a new vector y. Each element of y is a weighted combination of all the elements of x. A matrix = one learned transformation from one space to another. An LLM is, brutally, hundreds of these multiplications in a row with a bit of non-linearity in between. When you read "linear layer" or &lt;code&gt;nn.Linear&lt;/code&gt;: this is it. When you read GEMM: General Matrix Multiply, the kernel GPUs grind on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters (or weights)
&lt;/h3&gt;

&lt;p&gt;The numbers inside those matrices. "180M-parameter model" = the sum of all elements of all matrices is 180 million floats. &lt;strong&gt;It's the parameters that get modified during training&lt;/strong&gt; — the model's "knowing" lives entirely there. The architecture is the printed circuit board; the parameters are the component values that training solders on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding
&lt;/h3&gt;

&lt;p&gt;The bridge between tokens (integers) and math (vectors). A table: row 4521 = the 640-float vector representing " cat". The deep part: during training these vectors organize themselves geometrically by &lt;strong&gt;meaning&lt;/strong&gt; — "cat" and "feline" end up close in this 640-dimensional space, "cat" and "lathe" far apart. The direction itself encodes semantic relations. If you've ever used pgvector or a vector database for semantic search, it's the exact same concept — except here the embeddings are &lt;em&gt;internal&lt;/em&gt; to the model and learned along with everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  d_model
&lt;/h3&gt;

&lt;p&gt;The model's "width": how many floats represent each token as it flows through the network. In our reference, 640. Each token enters as a 640-vector, exits every layer as a 640-vector (enriched with context), all the way to the end. It's the system's data bus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Activation function (non-linearity)
&lt;/h3&gt;

&lt;p&gt;After a matrix multiplication you apply a non-linear function element by element (ReLU, SiLU, GELU — variants on the same theme: squash or let through). Why it's needed: a chain of purely linear operations mathematically collapses into ONE linear operation — you'd learn nothing complex. The non-linearity is what lets the network represent arbitrary functions. When you see &lt;code&gt;F.silu&lt;/code&gt; in code: this is it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Softmax
&lt;/h3&gt;

&lt;p&gt;A function that takes a vector of arbitrary numbers ("scores" or &lt;strong&gt;logits&lt;/strong&gt;) and turns it into a probability distribution: all positive, summing to 1, the high scores dominating. It's the last step before picking the next token, and it also appears inside attention. &lt;strong&gt;Sigmoid&lt;/strong&gt; is its cousin for the single case: it squashes one number into (0,1) independently of the others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 2 — How it learns: training
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Forward pass
&lt;/h3&gt;

&lt;p&gt;Running the input through the model to the output. Text → tokens → embeddings → 20 layers of transformations → probabilities over the next token. Just computation, no learning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loss (cost function)
&lt;/h3&gt;

&lt;p&gt;The number that says "how wrong you were". For LLMs it's &lt;strong&gt;cross-entropy&lt;/strong&gt;: take the token that &lt;em&gt;actually&lt;/em&gt; followed in the training text, look at what probability the model assigned it, and the loss is -log of that probability. If the model gave the right token probability 0.9 → low loss. If it gave it 0.001 → high loss. It's the only learning signal: all of training is "lower this number".&lt;/p&gt;

&lt;p&gt;A sanity check practitioners use at the first step: a freshly initialized model fires at random, uniform probability 1/32,768 over every token, so the initial loss must be -log(1/32768) ≈ 10.4. If it starts there, initialization is healthy. The &lt;strong&gt;perplexity&lt;/strong&gt; you find in evaluations is just e^loss — "among how many tokens the model is effectively hesitating".&lt;/p&gt;

&lt;h3&gt;
  
  
  Gradient and backpropagation
&lt;/h3&gt;

&lt;p&gt;The gradient answers: "if I move this parameter by an epsilon, does the loss go up or down, and by how much?". &lt;strong&gt;Backpropagation&lt;/strong&gt; is the algorithm (a systematic application of the chain rule from differential calculus) that computes this derivative for ALL 180 million parameters in one shot, propagating backward from the final error. In PyTorch it's the line &lt;code&gt;loss.backward()&lt;/code&gt;: automatic, you never implement it by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gradient descent and the optimizer
&lt;/h3&gt;

&lt;p&gt;Once you know which direction to move each parameter, you move them all a tiny step in that direction. Repeat millions of times. The &lt;strong&gt;learning rate (LR)&lt;/strong&gt; is the step size — the single most important hyperparameter. Too high: the loss explodes or oscillates. Too low: you learn at a snail's pace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An analogy for anyone who's touched a PID loop&lt;/strong&gt;: the gradient is the error signal, the learning rate is the gain. Gain too high = oscillations and crash, too low = mushy response.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;optimizer&lt;/strong&gt; is the strategy for using the gradients: the basic one (SGD) just applies them; &lt;strong&gt;AdamW&lt;/strong&gt; keeps moving averages of both the gradient (1st moment) and its square (2nd moment) — it's the latter that adapts the step per-parameter; &lt;strong&gt;Muon&lt;/strong&gt; (the 2025 newcomer) applies a geometric "cleanup" to matrix gradients before using them. No need to understand the how right now — just to know that the optimizer is the &lt;em&gt;how you descend&lt;/em&gt;, and that some descend faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch
&lt;/h3&gt;

&lt;p&gt;You don't process one example at a time: you pack N sequences together (the batch) and average the gradient. Reason 1: GPUs live on parallelism. Reason 2: a gradient averaged over half a million tokens is far less noisy than one from a single sequence. &lt;strong&gt;Gradient accumulation&lt;/strong&gt; (&lt;code&gt;grad_accum&lt;/code&gt; in code) is a trick to simulate huge batches when VRAM isn't enough: you accumulate the gradients of N micro-batches before taking the step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Epoch, step, checkpoint
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step&lt;/strong&gt; = one parameter update. &lt;strong&gt;Epoch&lt;/strong&gt; = one full pass over the dataset (in LLM pre-training, historically you often didn't even complete a single epoch — the dataset was bigger than the compute budget; today, with curated, high-quality data, seeing it more than once is common again). &lt;strong&gt;Checkpoint&lt;/strong&gt; = a dump of the parameters to disk, your state save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overfitting and held-out
&lt;/h3&gt;

&lt;p&gt;If the model memorizes the training set instead of generalizing, the training loss drops but on unseen data it doesn't. That's why you keep a &lt;strong&gt;held-out set&lt;/strong&gt; (data never shown) as an honest measure. There's also a reverse use of the phenomenon, a classic sanity check: overfit a single batch &lt;em&gt;on purpose&lt;/em&gt;. If the model can't even memorize 32 sequences, there's a bug in the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hyperparameters
&lt;/h3&gt;

&lt;p&gt;Everything you choose and isn't learned: learning rate, dimensions, number of layers, batch size... Hyperparameter tuning is the equivalent of tuning Betaflight on a drone: there's theory, there are sensible starting values, and then there's empirical experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 3 — The Transformer architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why attention is needed: the context problem
&lt;/h3&gt;

&lt;p&gt;A token on its own is ambiguous ("bank": a river's edge, a place for money, a plane banking?). Meaning depends on context. The architectural problem is: how does the representation of the token at position 500 incorporate information from the preceding tokens? The pre-2017 answer was recurrent networks (RNNs), which read in sequence and compress everything into a state — a bottleneck and no parallelism. The &lt;strong&gt;Transformer&lt;/strong&gt;'s answer (2017, "Attention Is All You Need") is attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-attention: the intuition
&lt;/h3&gt;

&lt;p&gt;Each token makes a "query to the database" of the other tokens. Mechanically, from each token you derive three vectors (three matrix multiplications, the classic &lt;code&gt;wq, wk, wv&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Query (Q)&lt;/strong&gt;: "what I'm looking for" — e.g. the token "on" looks for a subject to attach to&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Key (K)&lt;/strong&gt;: "what I offer as a search key" — e.g. "cat" advertises itself as an animal-subject&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Value (V)&lt;/strong&gt;: "the content I deliver if I'm selected"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each token: take the dot product of its Q with the K of all preceding tokens (→ affinity scores), rescale by √(head dimension) (here √64 = 8) — without it, with large vectors the scores grow, the softmax saturates and the gradients die — then softmax over the scores (→ weights summing to 1), and the token's new representation is the weighted average of the V. Result: each token "absorbs" information from the relevant tokens, with weights that are &lt;em&gt;learned and content-dependent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;: a lookup on a hash table, but fuzzy and differentiable — instead of an exact match on the key, you get a similarity score with &lt;em&gt;all&lt;/em&gt; the keys and draw from all of them in proportion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Causal mask
&lt;/h3&gt;

&lt;p&gt;In language modeling the token at position t can look only at positions ≤ t — otherwise during training it would "peek" at the answer. The causal mask zeroes out attention toward the future. In PyTorch code it's the &lt;code&gt;is_causal=True&lt;/code&gt; flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-head
&lt;/h3&gt;

&lt;p&gt;Instead of ONE attention with large vectors, you do N attentions in parallel with small vectors ("heads"), then concatenate. Each head learns to look for different things: one tracks syntactic dependencies, another coreferences, and so on. In our reference model: 10 heads of 64 dimensions each.&lt;/p&gt;

&lt;h3&gt;
  
  
  FFN (Feed-Forward Network)
&lt;/h3&gt;

&lt;p&gt;The second component of every layer: two (or three, with SwiGLU) matrix multiplications with a non-linearity in between, applied to each token &lt;strong&gt;independently&lt;/strong&gt;. If attention is where tokens &lt;em&gt;talk to each other&lt;/em&gt;, the FFN is where each token &lt;em&gt;processes on its own&lt;/em&gt; what it gathered. It's also where much of the model's "factual knowledge" is thought to reside — and it's the part that MoE multiplies into experts (we'll get there).&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer (block) and residual stream
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;transformer block&lt;/strong&gt; = attention + FFN, each preceded by normalization. The model is a stack of identical blocks (20 in our reference). Crucial detail: the &lt;strong&gt;residual connections&lt;/strong&gt; — the output of each component is &lt;em&gt;added&lt;/em&gt; to the input, it doesn't replace it (&lt;code&gt;x = x + attn(...)&lt;/code&gt;). Picture a "conveyor belt" (the residual stream) carrying the token's representation through the layers, where each block reads it and &lt;em&gt;adds&lt;/em&gt; its contributions. Without this, gradients couldn't flow backward through 20 layers (vanishing gradient) and deep networks wouldn't train.&lt;/p&gt;

&lt;h3&gt;
  
  
  Normalization (LayerNorm → RMSNorm)
&lt;/h3&gt;

&lt;p&gt;Before each component you rescale the token's vector to a standard "magnitude". Reason: numerical stability — without it, the magnitudes of the values drift layer after layer until they explode or vanish. Analogy: AGC (automatic gain control) in a radio chain, or leveling signals between stages. RMSNorm is the minimal version used today (scale only, no centering). "Pre-norm" = you normalize at the input of each component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Positional encoding and RoPE
&lt;/h3&gt;

&lt;p&gt;Problem: attention by itself doesn't know WHERE the tokens are — it's an operation on sets, "the cat bites the dog" and "the dog bites the cat" would give the same scores. You need to inject position. The modern method is &lt;strong&gt;RoPE&lt;/strong&gt;: rotate the Q and K vectors by an angle proportional to the token's position (pairs of dimensions = planes of rotation, different frequencies per pair). The elegant consequence: the dot product between Q and K ends up depending on the &lt;em&gt;relative distance&lt;/em&gt; between tokens, not on absolute positions — exactly what you want for language. Honest analogy: phase encoding — the position information lives in the signal's phase, and the phase difference gives you the distance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logits and sampling
&lt;/h3&gt;

&lt;p&gt;At the end of the stack, the last layer (the &lt;em&gt;language modeling head&lt;/em&gt;, &lt;code&gt;lm_head&lt;/code&gt;) projects the token's 640-dim vector to 32,768: one score per vocabulary token (the &lt;strong&gt;logits&lt;/strong&gt;). Softmax → probabilities → the next token is drawn. &lt;strong&gt;Temperature&lt;/strong&gt; divides the logits before the softmax: &amp;lt;1 makes the distribution more concentrated (conservative), &amp;gt;1 flatter (creative). &lt;strong&gt;Top-p / nucleus sampling&lt;/strong&gt; cuts the tail: it samples only from the most probable tokens that cumulatively reach probability p.&lt;/p&gt;

&lt;h3&gt;
  
  
  KV cache
&lt;/h3&gt;

&lt;p&gt;During generation, at each new token the attention computation reuses the K and V of &lt;em&gt;all&lt;/em&gt; previous tokens — which don't change. Recomputing them every time would be insane: you keep them in memory. This is the KV cache, and its size is THE inference bottleneck (it grows with context × layers × KV heads × head_dim × 2, for K and V). When you read that GQA, sliding window and MLA exist "to compress the KV cache", now you know what it is: the working memory of generation. Perfect analogy: it's memoization, pure caching of immutable intermediate results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context window
&lt;/h3&gt;

&lt;p&gt;The maximum number of tokens the model can keep in view at once: the prompt plus everything it has already generated. It has two distinct limits. A &lt;strong&gt;learned&lt;/strong&gt; one — the RoPE frequencies seen in training: beyond that length the model can no longer place positions and quality collapses. A &lt;strong&gt;physical&lt;/strong&gt; one — the KV cache: double the context, double the memory. Extending it after training is possible (&lt;strong&gt;RoPE scaling&lt;/strong&gt;, &lt;strong&gt;YaRN&lt;/strong&gt;: you rescale the frequencies to "stretch" the window beyond the training length), but it's never entirely free in quality. When you read "128k context", this is it — and there's almost always a trade-off behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 4 — The "modern" terms, now decipherable
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GQA (Grouped-Query Attention)
&lt;/h3&gt;

&lt;p&gt;In classic multi-head each head has its own K and V → huge cache. GQA: many Query heads share a few K/V heads (e.g. 10 query heads, 2 KV heads). Cache reduced 5×, quality almost identical. Cache deduplication, in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sliding window attention
&lt;/h3&gt;

&lt;p&gt;Most layers limit attention to the last 512 tokens (a sliding window) instead of the whole context; only some layers see everything (one in 4 in our reference — the ratio varies from architecture to architecture). Distant information still travels: layers compose (layer 2's window sees tokens that had already absorbed context in layer 1). Less compute, less cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  MLA (Multi-head Latent Attention)
&lt;/h3&gt;

&lt;p&gt;The third way to tame the KV cache, after GQA and sliding window. Instead of caching K and V for each head, MLA &lt;strong&gt;compresses them into a single low-rank latent vector&lt;/strong&gt; and caches only that; the per-head K and V are reconstructed on the fly with a multiplication when needed. Much smaller cache than GQA at comparable quality, in exchange for a bit more compute. It's DeepSeek-V2/V3's choice. Same conceptual family as GQA and sliding window: all three trade a sliver of compute or quality to fit more context in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  MoE (Mixture of Experts) — now you have the pieces
&lt;/h3&gt;

&lt;p&gt;Remember: the FFN is each block's "individual-processing" component, and it holds much of the parameters. MoE's idea: instead of ONE FFN per block, you put 32 (the &lt;strong&gt;experts&lt;/strong&gt;) and a &lt;strong&gt;router&lt;/strong&gt; — a small linear layer that, for each token, picks the 4 most suitable experts. Only those 4 compute. Result: the model has a capacity comparable (not identical) to 32 FFNs — capacity follows total parameters — but &lt;em&gt;costs&lt;/em&gt; as much as 4 (compute = active parameters). Specialization emerges on its own from training: no one assigns topics to the experts.&lt;/p&gt;

&lt;p&gt;Analogy: a content-aware load balancer in front of a pool of specialized workers — with the twist that routing and specializations co-evolve during training.&lt;/p&gt;

&lt;p&gt;And the terms that orbit it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Fine-grained&lt;/strong&gt;: 32 small experts beat 8 large ones — more possible combinations, finer specialization.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Shared expert&lt;/strong&gt;: one always-active expert for generic knowledge, so the others don't have to duplicate it.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Routing collapse&lt;/strong&gt;: the pathology — the router converges on sending everything to 2-3 experts (which therefore improve, therefore get even more traffic: positive feedback). The other experts stay untrained: dead parameters.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Aux-loss-free balancing&lt;/strong&gt;: the cure made famous by DeepSeek. A per-expert bias added to the scores &lt;em&gt;for selection only&lt;/em&gt;: overloaded expert → bias down → chosen less. It's literally an integral controller on load — a control loop outside training. If you've got a head for PID, you'll get it better than the average ML engineer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  bf16 / mixed precision
&lt;/h3&gt;

&lt;p&gt;16-bit floating point formats instead of 32: half the memory, double the throughput on tensor cores. bf16 keeps the same range as fp32 (sacrificing mantissa), so no overflow to manage. "Mixed" because the delicate operations (norms, loss sums) stay in fp32.&lt;/p&gt;

&lt;h3&gt;
  
  
  torch.compile, FlashAttention, MFU
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;torch.compile&lt;/code&gt;: a JIT that fuses operations into optimized GPU kernels. FlashAttention: an attention implementation that never materializes the T×T score matrix (it computes it in blocks in SRAM) — same math, memory from O(T²) to O(T). MFU (Model FLOPs Utilization): what fraction of the GPU's theoretical FLOPs you're actually using — your pipeline-efficiency indicator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-training / SFT / DPO / RLHF-RLVR — the pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Pre-training&lt;/strong&gt;: pure language modeling on billions of web tokens. Produces a well-read "completer" but not an assistant.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;SFT&lt;/strong&gt; (Supervised Fine-Tuning): fine-tuning on examples of well-formed conversations. Teaches the question→answer &lt;em&gt;format&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;DPO/RLHF&lt;/strong&gt;: refinement on &lt;em&gt;preferences&lt;/em&gt; — good/bad response pairs. Teaches style and alignment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;RLVR/GRPO&lt;/strong&gt;: reinforcement learning on verifiable-answer tasks (math, code). This is where reasoning models are born. The key difference from SFT: the model learns from &lt;em&gt;its own attempts&lt;/em&gt; rewarded/punished, not from given examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  LoRA / QLoRA (PEFT)
&lt;/h3&gt;

&lt;p&gt;Fine-tuning &lt;em&gt;all&lt;/em&gt; the parameters (our model's 180M, or a real one's 7 billion) means keeping them in VRAM with their gradients and optimizer states: prohibitive on human hardware. &lt;strong&gt;LoRA&lt;/strong&gt; works around it: it &lt;strong&gt;freezes the base weights&lt;/strong&gt; and trains only small low-rank add-on matrices (A·B, a few thousand parameters) alongside some layers. The bulk stays put, you touch 1% and get most of the result. &lt;strong&gt;QLoRA&lt;/strong&gt; adds the stroke of genius: you keep the base weights &lt;strong&gt;quantized to 4 bits&lt;/strong&gt; (frozen) and put the LoRA adapters on top — so you fine-tune a 7B on a single consumer GPU. &lt;strong&gt;PEFT&lt;/strong&gt; (Parameter-Efficient Fine-Tuning) is the family that contains them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reasoning and test-time compute
&lt;/h3&gt;

&lt;p&gt;The thread that starts from RLVR. The idea: let the model &lt;strong&gt;generate a long chain of reasoning&lt;/strong&gt; (&lt;em&gt;chain-of-thought&lt;/em&gt;) &lt;em&gt;before&lt;/em&gt; answering, and train it — via RLVR — to do it well. The consequence is a paradigm shift: you don't spend compute only in &lt;em&gt;training&lt;/em&gt;, you spend it in &lt;em&gt;inference&lt;/em&gt; — more "thinking" tokens = better answers on hard problems. It's &lt;strong&gt;test-time compute&lt;/strong&gt;: same architecture, but one that "stops to think". Reasoning models (o1 / R1 style) are born this way. And note the link to everything else: those reasoning tokens fill the context window and inflate the KV cache. Thinking, for an LLM, is paid for in memory and latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling laws / Chinchilla
&lt;/h3&gt;

&lt;p&gt;Empirical relationships between compute, parameters and tokens: for a given compute budget there's an optimal ratio (~20 tokens per parameter, "Chinchilla"). "Overtraining" = going well beyond on purpose, because a small model trained for a long time costs less &lt;em&gt;at inference&lt;/em&gt; forever. It's a CAPEX/OPEX trade-off, and in 2026 inference OPEX dominates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 5 — Running the model (inference and serving)
&lt;/h2&gt;

&lt;p&gt;So far: how the model &lt;em&gt;learns&lt;/em&gt;. Now: how you &lt;em&gt;serve&lt;/em&gt; it. It's the level missing from almost every glossary, and the one you touch hands-on the day you try to run a model on your own hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quantization
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;bf16&lt;/code&gt; from before is training stuff. At &lt;strong&gt;inference&lt;/strong&gt; you can go lower still: weights at &lt;strong&gt;8 or 4 bits&lt;/strong&gt; (sometimes less). A 7B in fp16 wants ~14 GB of VRAM; at 4 bits it wants ~4 — often the difference between "it runs" and "it doesn't" on your card. The names you'll meet: &lt;strong&gt;GPTQ&lt;/strong&gt; and &lt;strong&gt;AWQ&lt;/strong&gt; (they quantize the weights only, calibrating on the corpus to lose less quality), &lt;strong&gt;GGUF&lt;/strong&gt; (llama.cpp's format, with its mixed-precision &lt;em&gt;k-quants&lt;/em&gt;). The trade-off is always the same: fewer bits, less memory, a sliver less quality — and below 4 bits the sliver becomes a rope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefill vs decode
&lt;/h3&gt;

&lt;p&gt;Inference has two phases with opposite profiles. &lt;strong&gt;Prefill&lt;/strong&gt; processes the whole prompt at once, in parallel: it's &lt;strong&gt;compute-bound&lt;/strong&gt; (lots of multiplications; the first token comes out). &lt;strong&gt;Decode&lt;/strong&gt; generates one token at a time, and for each it must &lt;strong&gt;reread the entire KV cache&lt;/strong&gt;: it's &lt;strong&gt;memory-bandwidth-bound&lt;/strong&gt; — you're not waiting on FLOPs, you're waiting on memory. It's why two models with the same FLOPs can generate at wildly different speeds, and why optimizing inference is 90% optimizing memory movement. Everything else in this level follows from here.&lt;/p&gt;

&lt;h3&gt;
  
  
  PagedAttention and continuous batching
&lt;/h3&gt;

&lt;p&gt;The two ideas that made modern serving (vLLM) efficient — and for a sysadmin they're home turf. &lt;strong&gt;PagedAttention&lt;/strong&gt;: instead of allocating each request's KV cache as a contiguous block (which fragments and wastes memory, like a naive allocator would), it splits it into non-contiguous &lt;strong&gt;pages&lt;/strong&gt; with a translation table. It's &lt;strong&gt;virtual memory applied to the KV cache&lt;/strong&gt;, full stop. &lt;strong&gt;Continuous batching&lt;/strong&gt;: instead of waiting for all the requests in a batch to finish before accepting new ones, you swap them in hot — as soon as one sequence ends, another takes its place and the GPU stays full. Together they're worth an order of magnitude of throughput over serving one request at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speculative decoding
&lt;/h3&gt;

&lt;p&gt;A trick to speed up decode without changing the output. A small, fast &lt;strong&gt;draft&lt;/strong&gt; model proposes the next N tokens; the big model &lt;strong&gt;verifies them all in a single forward pass&lt;/strong&gt; (verifying is parallel, generating is sequential), accepts the correct prefix and discards from the first error on. If the draft guesses often — and on predictable text it does — you go 2-3× faster with &lt;em&gt;exactly&lt;/em&gt; the same distribution as the big model. Zero quality trade-off, just less latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inference metrics
&lt;/h3&gt;

&lt;p&gt;The numbers you judge a serving setup by. &lt;strong&gt;TTFT&lt;/strong&gt; (time to first token): how long until it &lt;em&gt;starts&lt;/em&gt; answering — dominated by prefill. &lt;strong&gt;Generation speed&lt;/strong&gt; (tokens/s per request): dominated by decode. &lt;strong&gt;Throughput&lt;/strong&gt; (aggregate tokens/s across all requests): what matters if you serve many. And the key trade-off, &lt;strong&gt;throughput vs latency&lt;/strong&gt;: big batches = more throughput but more wait for the single user; small batches = the opposite. The same dilemma as any queueing system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Special tokens and chat template
&lt;/h3&gt;

&lt;p&gt;A &lt;em&gt;base&lt;/em&gt; model completes text; an &lt;em&gt;instruct&lt;/em&gt; model expects a &lt;strong&gt;structure&lt;/strong&gt;. &lt;strong&gt;Special tokens&lt;/strong&gt; are vocabulary tokens that aren't words but markers: begin/end of sequence, and the role delimiters (&lt;code&gt;&amp;lt;|im_start|&amp;gt;user&lt;/code&gt;, &lt;code&gt;&amp;lt;|im_start|&amp;gt;assistant&lt;/code&gt;…). The &lt;strong&gt;chat template&lt;/strong&gt; is the schema — usually a Jinja string that ships with the model — that packs your messages into &lt;em&gt;exactly&lt;/em&gt; the form seen during SFT. Getting the template wrong, or forgetting it, is the number-one cause of local models that "answer weird": it's not the model being dumb, it's that you're speaking to it in a dialect it never learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  And now?
&lt;/h2&gt;

&lt;p&gt;If you got this far reading straight through, you have the map. But a map isn't the territory, and the real leap isn't made by any glossary: it's made by &lt;strong&gt;Andrej Karpathy's "Neural Networks: Zero to Hero" video series&lt;/strong&gt; (YouTube, free). In particular the first two videos (micrograd and makemore) build backpropagation and a tiny language model &lt;em&gt;by hand&lt;/em&gt;, and "Let's build GPT" builds attention line by line. They're ~15 hours total, and worth more than anything written — this document included.&lt;/p&gt;

&lt;p&gt;One last thing, in the spirit of honesty: the "it resonates but doesn't quite land" feeling is the normal and correct state after a first read. These concepts aren't understood by reading — they're understood the third time your training diverges and you find out why. The glossary only exists so that, when it happens, you know where to look.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>naturallanguageprocessing</category>
    </item>
  </channel>
</rss>
