Maximum Capability from Minimum Silicon
A complete, experiment-driven engineering guide to running serious AI agents on a low-memory workstation — an 8 GB graphics card with 32 GB of system RAM. Every technique is grounded in published benchmarks and community measurements, with the raw results reproduced in Part IV.
Abstract & How to Read This Paper
What this document is, who it is for, and how it is organized.
This paper answers one practical question: how do you extract the maximum agentic capability from a consumer PC with a single 8 GB GPU and 32 GB of system RAM? The question matters because the marketing of local AI is dominated by hardware most people do not own. Guides casually assume 24 GB RTX 3090s or Apple Silicon machines with 64–128 GB of unified memory, while the single most common serious-gaming-and-tinkering configuration in the world — an RTX 3060 Ti / 3070 / 4060 / 4060 Ti class card paired with 32 GB of DDR4 or DDR5 — sits in a strange middle ground: too much GPU to give up on acceleration, too little VRAM to hold the models everyone benchmarks.
The central thesis of this paper is that the 8 GB + 32 GB configuration is not a compromise to be apologized for but a systems engineering problem with a well-mapped solution space. The solution space has four dimensions, and each is covered in depth here with real experimental data: (1) weight compression, where the evidence shows modern 4-bit quantization costs between 0.15 and 3 percentage points of benchmark accuracy on an 8 B model — the famous perplexity ladder for Llama 3 8B moves only from 6.233 (fp16) to 6.383 (Q4_K_M), a 2.4 % relative change, while cutting file size by 69 %; (2) context memory, where the KV cache follows an exact arithmetic formula (Section 5) and can be quantized to 8-bit with near-lossless results (81.6 % output similarity, −208 MB at 8 K context) but collapses catastrophically at 4-bit (8.3 % similarity — the “q4_0 cliff”); (3) placement, where llama.cpp-style layer splitting turns VRAM + RAM into one addressable pool, with the practical consequence that a 14 B model runs at roughly 60–75 % of full-GPU speed and a 30 B-class MoE model with 3 B active parameters remains genuinely interactive; and (4) architecture, where the agent’s context management — not the model — is usually the binding constraint on task complexity.
The paper is written to be read once, linearly, and then used as a reference. It is deliberately long, because the goal is competence rather than awareness: after reading it you should be able to size a model to your memory budget by hand, choose a quantization level with knowledge of its measured cost, configure an inference server with the correct flags, design an agent loop that survives hour-long tasks without context exhaustion, and diagnose an out-of-memory failure without guessing. All commands target Linux first (with Windows equivalents where they differ), because the tooling — llama.cpp, Ollama, vLLM — behaves best there, but everything applies to Windows with WSL2.
Throughout the paper, results are attributed inline to their source: peer-reviewed or arXiv papers, official engineering blogs (NVIDIA, vLLM, Qwen), structured community evaluations (LessWrong quantization evaluations, InventiveHQ Lab’s KV-cache benchmark), and community measurements (r/LocalLLaMA threads, llama.cpp GitHub discussions). Where a number comes from a single informal measurement it is labeled as such — treat those as directional, not gospel. Tables in Part IV reproduce the most important raw results verbatim so you can check my interpretation against the data.
In a hurry? Read Chapter 2 (memory math), Chapter 8 (model ladder), Chapter 11 (the build), and Chapter 13 (playbooks) — about 40 minutes. Studying for depth? Read linearly; Parts I–II build the theory, Parts III–IV apply it. Already running a model and it’s broken? Jump straight to Chapter 14.
Foundations
Why 8 GB is a different regime, where every byte of memory actually goes, and how to audit the machine you actually own — the theory needed before a single model is downloaded.
1. The 8 GB Challenge
What makes low-VRAM inference a distinct engineering regime — and why 2025–2026 was the moment it became viable.
1.1 The problem, stated precisely
A local AI agent stack has to fit four things into memory simultaneously: the model weights (billions of parameters, each taking multiple bytes), the key–value cache that grows with every token of conversation history (Chapter 5), the activation buffers and CUDA context overhead that inference frameworks reserve just to operate, and the agent scaffolding itself — the Python process, the web server, the retrieval index, the tool processes. On a datacenter GPU with 80 GB of HBM, the first three items are rounding errors; you simply load the model in 16-bit and move on. On an 8 GB card, all four compete for the same small pool, and the weights alone of a modern 8 B model in fp16 (16.1 GB) are already twice your budget before a single token has been generated.
The naive conclusion is that small-VRAM machines are limited to small models, and small models are toys. Both halves of that conclusion are now wrong. The first half fails because quantization and offloading have matured into well-understood, well-benchmarked disciplines (Chapters 4 and 6): an 8 B model at 4-bit occupies ~4.9 GB and loses, on measured benchmarks, roughly 1–3 points of MMLU accuracy; a 30 B-class mixture-of-experts model with 3 B active parameters can be made to run interactively by keeping its always-active components in VRAM and streaming expert weights from system RAM. The second half fails because of a decade of small-model progress: today’s 7–9 B models score on standard evaluations at or above the level of GPT-3.5-class frontier systems of 2023, and dedicated agent research has repeatedly shown that scaffolding quality matters as much as raw model size. The mini-swe-agent project demonstrated 65 % resolution on SWE-bench Verified with a deliberately minimal ~100-line agent loop — because it spent its complexity budget on the right abstractions rather than on elaborate frameworks. The lesson transfers directly to low-resource machines: with a mid-sized model, your agent architecture is your capability multiplier.
1.2 Why 8 GB + 32 GB is a genuinely good configuration
It is worth being explicit about why this particular pairing — modest VRAM, generous RAM — is more capable than it looks, because the reasoning drives most of the engineering decisions in this paper.
- The 32 GB of system RAM is a large, underused second pool. llama.cpp’s mmap-based loading can treat system RAM as a transparent extension of VRAM. A model file can exceed VRAM by 3–4× and still run, with the GPU holding the layers that are touched every token and the CPU handling the rest. Combined, you have a 40 GB addressable pool — enough for a 30 B dense model at 4-bit (18.6 GB) or even larger MoE models, just not at full speed.
- Most tokens are not latency-critical. An agent spends its wall-clock life waiting for tool executions — file reads, web fetches, test suites that take seconds to minutes. When the next model call happens 30 seconds after the last, a generation speed of 15 tok/s versus 40 tok/s changes a 10-second wait into a 27-second wait inside a task that takes 20 minutes. Agents amortize inference latency far better than chat does.
- Agent context is naturally chunked. A well-designed agent (Chapter 10) does not need one giant 128 K context; it needs disciplined 8–16 K windows refreshed from external memory. That is exactly the regime where an 8 GB card is comfortable, and it is also the regime that produces more reliable agent behavior, because models degrade in the middle of very long contexts.
- The quantization ecosystem optimized for exactly this hardware. The most heavily tested configuration in the entire local-LLM community is “an 8 B model at Q4_K_M on a consumer card,” because that is what most people own. Bug reports, perplexity tables, and throughput numbers for this configuration are abundant — you are walking the best-trodden path in the field.
1.3 What “complex task” means, and what it demands
The goal stated in this paper’s title — manage a complex task — deserves a definition, because it dictates the memory budget. A complex agentic task has four properties that distinguish it from casual chat. First, it is multi-step: the model must loop through plan → act → observe dozens of times, each turn appending tool results and reasoning to the history. Second, it is stateful: information gathered in step 3 (say, the schema of a database) must remain usable in step 40, which means either a long context window or an external memory system. Third, it is tool-using: the model must emit structured function calls reliably — a capability measured separately from general intelligence by benchmarks like the Berkeley Function Calling Leaderboard (BFCL), where Llama 3.1 8B scores ~76 % — which constrains model choice more than raw MMLU does. Fourth, it is long-running: a coding agent session easily spans an hour and hundreds of thousands of cumulative tokens, even though no single context window needs to hold them all at once.
These four properties translate into concrete engineering requirements: a model with reliable tool-calling behavior at 4-bit quantization; enough KV-cache headroom for 8–32 K tokens of working context per call; a context-management layer that compresses or externalizes history before it exhausts that headroom; and a stable inference server that can run for hours without leaking VRAM. Each of these is a chapter of this paper. The short version: your machine can do this — if you spend your bytes deliberately.
1.4 The capability inventory: what fits, honestly
Before the theory, here is the honest bottom-line inventory for an 8 GB GPU + 32 GB RAM machine, based on the experimental evidence compiled in Part IV. These are the regimes you can operate in, from fastest to most ambitious.
| Regime | Example model | Memory layout | Typical speed* | What it buys you |
|---|---|---|---|---|
| Fast in-VRAM | Qwen3-4B, Gemma 3 4B, Phi-4-mini (Q4/Q6) | All weights + KV in VRAM | 50–90 tok/s | Snappy chat, fast tool-calling drafts, bulk summarization of agent history |
| Full-capability in-VRAM | Qwen3-8B, Llama 3.1 8B (Q4_K_M) + 8–16 K ctx | ~5 GB weights in VRAM, q8_0 KV | 30–45 tok/s | The sweet spot: near-full model intelligence with real context for agents |
| Tight-fit large dense | Gemma 3 12B (Q4/QAT), Qwen3-14B (partial) | Weights mostly in VRAM, small KV | 10–25 tok/s | Noticeably more world knowledge and instruction-following |
| Hybrid offload dense | Qwen3-14B, Mistral Small 24B (Q4, split) | ~60–80 % layers in VRAM, rest in RAM | 6–15 tok/s | Upper-mid models at usable speed; great for non-interactive steps |
| MoE hybrid | Qwen3-30B-A3B, Qwen3-Coder-30B (Q4) | Attention + shared experts in VRAM, routed experts streamed from RAM | 8–20 tok/s | Frontier-adjacent quality; the highest-capability option on this hardware |
Full-precision (fp16/bf16) inference of anything above ~3.5 B parameters, and single-window 128 K-token contexts on any 8 B model, are not realistic on 8 GB of VRAM. Both are solvable elsewhere: quantization replaces the first (Chapter 4), and context engineering replaces the second (Chapter 10). Accepting these two substitutions up front is the entire mental shift required.
2. Where the Bytes Go: Memory Anatomy of a Local LLM
Four arithmetic formulas govern every sizing decision you will make. Master them and OOM errors become predictable instead of surprising.
2.1 The four memory consumers
At any instant during inference, VRAM (and its RAM extension) holds exactly four categories of data. Every sizing question in this paper is answered by adding these four numbers and comparing the total to your budget.
| Component | What it is | How it scales | Controllable? |
|---|---|---|---|
| Weights | The model’s learned parameters | Parameters × bytes-per-weight | Yes — quantization (Ch. 4) |
| KV cache | Stored attention keys/values for every token in context | Context length × per-token cost (exact formula §5.1) | Yes — context length, cache quantization, GQA already helps |
| Compute buffers | Activation workspace for the current forward pass; prompt-processing scratch | Model width, batch size, longest prompt | Partially — Flash Attention shrinks it; smaller context shrinks it |
| Framework overhead | CUDA context, kernels, allocator slack, the server process itself | Fixed ~300–800 MB VRAM; ~1–2 GB system RAM for Ollama-class stacks | Barely — one model process at a time on this hardware |
The practical consequence of the fourth row is a rule you should internalize now: your usable VRAM is not 8 GB, it is roughly 7.0–7.4 GB. A desktop environment with a browser open can easily reserve 0.8–1.2 GB of VRAM for itself (check with nvidia-smi before loading anything), and the CUDA context plus compute buffers of a running llama.cpp server cost another ~0.3–0.6 GB even before weights arrive. On a headless machine, or when the display is wired to the motherboard’s integrated GPU, you recover most of that margin — a genuinely useful trick detailed in §3.4.
2.2 Formula 1 — Weight memory
The memory occupied by model weights is the parameter count multiplied by the average bytes per parameter. Nothing more. In fp16 or bf16, each parameter costs exactly 2 bytes; the GGUF quantization ladder (Chapter 4) trades precision for bytes as follows.
| Format | Bytes/param | 8 B model | 14 B model | 30B-A3B MoE |
|---|---|---|---|---|
| fp16 / bf16 | 2.00 | 16.1 GB | 28.1 GB | 61.1 GB |
| Q8_0 | ~1.06 | 8.5 GB | 14.9 GB | 32.4 GB |
| Q6_K | ~0.82 | 6.6 GB | 11.5 GB | 25.0 GB |
| Q5_K_M | ~0.70 | 5.7 GB | 9.9 GB | 21.5 GB |
| Q4_K_M | ~0.57 | 4.6 GB | 8.1 GB | 17.5 GB |
| Q4_0 | ~0.55 | 4.4 GB | 7.7 GB | 16.6 GB |
| Q3_K_M | ~0.44 | 3.5 GB | 6.2 GB | 13.4 GB |
| Q2_K | ~0.36 | 2.9 GB | 5.0 GB | 10.9 GB |
Two observations fall straight out of this table. First, Q4_K_M is the pivot of the entire local-LLM world: it is the point where a modern 8 B model fits an 8 GB card with room for a real context window, while measured quality loss stays inside ~2–3 points on downstream benchmarks (evidence in §4.4). Second, the arithmetic explains the “ladder” you will keep climbing throughout this paper: every halving of bytes per parameter buys roughly one model-size class at constant memory. The 30 B MoE at Q4_K_M costs about as much memory as a 14 B dense at Q5 — but only activates 3 B parameters per token, which is why it can be fast despite being large (§6.5).
2.3 Formula 2 — The KV cache (preview)
The second formula is important enough to get its own chapter, but the skeleton belongs here because total-VRAM estimates need it. For every token in the context — prompt plus generated — the model stores one key vector and one value vector per attention layer, per KV head. The exact per-token cost is:
kv_bytes_per_token = 2 × n_layers × n_kv_heads × head_dim × kv_dtype_bytes
└─ K and V ─┘ └──── GQA dimensions ────┘
total_kv = kv_bytes_per_token × context_tokens × batch_sequences
Worked example, Llama 3.1 8B (32 layers, 8 KV heads, 128-dim heads, fp16 cache): 2 × 32 × 8 × 128 × 2 = 131,072 bytes = 128 KiB per token. At an 8 K context that is 1.0 GB; at 32 K it is 4.0 GB; at the model’s 128 K maximum it is 16 GB — twice your entire GPU. This single calculation, done in five seconds, predicts more OOM failures than any other reasoning in this paper. Chapter 5 works through the numbers for every model in the ladder, shows how GQA already saves you (the same model with full multi-head attention would cost 512 KiB/token), and evaluates the experimental evidence on quantizing the cache itself.
2.4 Formula 3 — The total, and the two planning rules
Adding the components gives the budget check you should perform before downloading any model file:
total_vram ≈ weights_on_gpu + kv_cache + compute_buffer (≈0.5–1 GB with FA) + fixed_overhead (≈0.4–0.8 GB)
PLANNING RULE 1a (comfortable): weights + kv ≤ 6.5 GB → fits 8 GB on any setup, with margin
PLANNING RULE 1b (tight): weights + kv ≤ 7.4 GB → fits headless / display on iGPU (§3.4)
PLANNING RULE 2 (split): weights total ≤ 30 GB → fits via offload; speed scales with GPU share (Ch. 6)
Rule 1a’s 6.5 GB figure is the conservative planning line: it assumes a desktop using ~0.5 GB, CUDA and buffers consuming ~1 GB together, and leaves slack for the transient peak that occurs during prompt processing (when the compute buffer temporarily grows to the size of the longest prompt batch). Rule 1b is the honest upper edge: a 7.4 GB plan does run on an 8 GB card, but only when the desktop is not competing for VRAM — headless, or with the monitor on the integrated GPU. People who plan to “exactly 7.9 GB” are the people writing the OOM bug reports this paper cites. Rule 2 reflects the practical ceiling of your 32 GB RAM pool: the OS, agent process, and page cache need headroom too, and mmap-loading a model larger than ~30 GB means the OS is already thrashing.
2.5 A worked example: three ways to run Qwen3-8B on 8 GB
To make the formulas concrete, here is the same model — Qwen3-8B (36 layers, 8 KV heads, 128-dim, hence 144 KiB/token fp16 KV) — planned three ways. These are the actual decisions you will face in Chapter 11.
| Plan | Weights | Context | KV cache | Total | Verdict |
|---|---|---|---|---|---|
| Q6_K, 8 K ctx, fp16 KV | 6.6 GB | 8 K | 1.1 GB | ~8.7 GB | OOM Does not fit |
| Q4_K_M, 16 K ctx, fp16 KV | 5.1 GB | 16 K | 2.4 GB | ~8.4 GB | OOM Does not fit |
| Q4_K_M, 16 K ctx, q8_0 KV | 5.1 GB | 16 K | 1.3 GB | ~7.3 GB | Fits Tight tier — headless/iGPU display |
Before loading any model, run the two formulas on paper (30 seconds with a calculator) and check the result against nvidia-smi. The Python tool in §11.2 automates this for every model in the ladder, but doing it by hand a few times builds the intuition that separates an engineer from a flag-copier.
3. Know Your Machine: The Hardware Audit
Eight gigabytes is not one budget — it is three interlocking bandwidth hierarchies. Optimizing without knowing which one binds produces confident mistakes.
3.1 The three tiers of memory
Every technique in this paper is, at bottom, a decision about which of three memory tiers holds which bytes. The tiers differ not just in size but in bandwidth — the number of bytes per second the compute units can actually pull from them — and bandwidth, not capacity, is what determines tokens per second once a model is loaded.
| Tier | Capacity (your rig) | Bandwidth (typical) | Latency character |
|---|---|---|---|
| VRAM (GDDR6/GDDR6X) | 8 GB | 240–288 GB/s | Consistently fast; this is what the GPU was built to read |
| System RAM (DDR4-3200 dual / DDR5-5600 dual) | 32 GB | 45–90 GB/s | Fast for CPU compute; slow when the GPU must reach across PCIe |
| PCIe link (3.0 ×16 / 4.0 ×16 / 4.0 ×8) | — | ~12 / ~25 / ~16 GB/s effective | The bridge tax: any GPU access to RAM pays it per byte |
The three numbers that actually matter for planning are VRAM capacity (how much fits fast), VRAM bandwidth (how fast a fully-resident model generates), and PCIe bandwidth (how fast a split model streams). The asymmetry between 272 GB/s and 16 GB/s — a factor of ~17 — is the entire reason offloading is a gradual trade-off rather than a cliff, and the reason the offload curve in §6.2 has the shape it does: generation is memory-bandwidth-bound per token, so every layer you keep in VRAM avoids paying the PCIe tax for that layer on every single token.
3.2 Why generation speed ≈ bandwidth / model-bytes
A useful mental model (accurate to within 10–20 % for single-stream generation): generating one token requires reading essentially all active weights once. Therefore:
tokens_per_sec ≈ effective_bandwidth / active_bytes_per_token
RTX 4060, Qwen3-8B Q4_K_M fully in VRAM: 272 GB/s ÷ 5.1 GB ≈ 53 tok/s theoretical
(real world: ~30–42 tok/s — overheads eat 20–40 %)
Same model, 50 % of layers on CPU via DDR4: active bytes split across two buses;
effective throughput collapses toward the slower path → ~8–12 tok/s
This model explains every speed number in this paper without magic. It explains why quantization speeds up inference as a side effect (fewer bytes per token = more tokens per second on the same bus — measured at 3.5–3.8× for 4-bit vs fp16 in the presenc.ai synthesis, §12.2). It explains why the MoE trick in §6.5 works (only 3 B of 30 B parameters are active per token, so the bandwidth denominator is small even though the memory numerator is large). And it explains the single most common performance disappointment in local AI — “I added more context and everything got slow” — because the KV cache joins the denominator as it grows, and prompt processing has a different, compute-bound character entirely (§7.4).
3.3 The CPU and RAM: the other half of the machine
The 32 GB of RAM is not passive scenery. Four practical facts about it shape every hybrid configuration.
- Dual-channel matters. A single stick of 32 GB runs at half bandwidth; two 16 GB sticks let the CPU read ~45–90 GB/s instead of ~25–45 GB/s. For any offloaded configuration this is a near-free 1.3–1.8× speedup on the CPU-resident layers. Check your board has two populated channels (sudo dmidecode -t memory | grep -i speed).
- DDR4 vs DDR5 is a real offload multiplier. DDR5-5600 dual-channel (~90 GB/s) makes 30 B-class hybrid inference meaningfully faster than DDR4-3200 (~51 GB/s). If you are buying RAM for this workload, this is where the money goes — more so than CPU cores.
- Cores set the ceiling for CPU-side compute. llama.cpp CPU layers want ~1–2 threads per physical core with hyperthreading usually harmful for this workload; the llama.cpp performance documentation calls wrong thread settings the #1 cause of slow CPU inference. A 6–8 core modern CPU is fully sufficient; more cores help only the offloaded fraction.
- Storage is nearly irrelevant to speed — but critical to load time. Weights are read once at startup (or paged in lazily via mmap). An NVMe SSD turns 18 GB model loads into ~15 s instead of ~2 min on SATA, and prevents the stutter of paging from a spinning disk. Keep models on the fastest drive you own; after load, I/O drops to zero.
3.4 Reclaiming VRAM from the desktop
On a machine where the monitor plugs into the graphics card, the desktop compositor, browser, and terminal permanently occupy VRAM — commonly 0.5–1.5 GB on a multi-monitor setup. Every hundred megabytes reclaimed is a hundred more for KV cache. The options, in decreasing order of practicality:
- Close GPU-accelerated apps while doing long agent runs — browsers are the biggest offenders (each tab with WebGL/canvas acceleration holds buffers in VRAM).
- Move the display to the iGPU if your CPU has one: in BIOS, enable integrated graphics, plug the monitor into the motherboard, and the discrete card becomes 100 % yours. This recovers the entire desktop footprint — typically 0.8–1.2 GB — at the cost of some desktop snappiness. On Linux, ensure the NVIDIA card still gets initialized for compute (nvidia-persistenced helps).
- Run headless (SSH in): same effect, no monitor at all. This is the standard setup for people who treat the 8 GB box as a home inference server.
3.5 The audit checklist
Run this once; write the answers on a sticky note. Every sizing decision later refers to them.
# GPU: name, VRAM total, current usage, PCIe link generation/width
nvidia-smi --query-gpu=name,memory.total,memory.used,pcie.link.gen.current,pcie.link.width.current --format=csv
# Sustained memory bandwidth test (GB/s) — the single most predictive number
# (bandwidthTest from cuda-samples, or: python -c "import torch; ..."
# simpler: run llama-bench on a tiny model and compare with published numbers)
# RAM: total, speed, and — critically — number of populated channels
sudo dmidecode -t 17 | grep -E "Size|Speed|Locator" | grep -v "No Module"
# CPU physical cores (set llama.cpp threads to this, not to hyperthreads)
lscpu | grep -E "^CPU\(s\)|Core|Thread|Model name"
# Storage class holding your models
df -h /path/to/models && lsblk -d -o NAME,ROTA,SIZE # ROTA=1 means spinning disk → move models
With the audit done, you know your three budgets: fast capacity (~7 GB usable VRAM), large capacity (~28 GB usable RAM after OS and agent process), and the bridge between them (12–25 GB/s). Everything that follows is the craft of spending those budgets well.
The Science of Squeezing
Four disciplines turn 8 GB into a real budget: quantizing weights, taming the KV cache, splitting the model across VRAM and RAM, and engineering throughput. Each chapter pairs the mechanism with the experimental evidence for what it costs.
4. Quantization: The Science of Shrinking Models
What 4-bit really costs, according to the people who measured it — perplexity ladders, benchmark deltas, and the chain-of-thought surprise.
4.1 The idea in one paragraph
Quantization replaces the 16-bit floating-point numbers a model was trained with lower-precision approximations — 8-bit, 4-bit, 3-bit integers with per-block scale factors — so that weights occupy less memory and less memory bandwidth. It is applied after training (“post-training quantization”), requires no retraining, and for the GGUF family can be done on a laptop in minutes. The reason it works acceptably is that neural network weights turn out to be surprisingly tolerant of rounding noise: the model’s function is encoded across billions of parameters redundantly, and careful rounding perturbs the output distribution only slightly. “Slightly” is doing a lot of work in that sentence, which is why this chapter is mostly tables of measurements rather than reassurances.
4.2 The GGUF ladder, decoded
The dominant format for local inference is GGUF, used by llama.cpp, Ollama, LM Studio, and most consumer tools. Its naming scheme intimidates newcomers but encodes only two things: the bit width (Q8, Q5, Q4, Q3, Q2) and the method. The legacy scheme (Q4_0, Q5_0) stores simple blocks of weights sharing one scale. The K-quant scheme (Q4_K_S, Q4_K_M, Q5_K_M, Q6_K) uses smarter, mixed-precision block structures — the _M suffix (“medium”) mixes in some higher-precision tensors for sensitive layers — and dominates the legacy scheme at every bit width: Q4_K_M beats Q4_0 in quality while being barely larger, which is why the community defaulted to it. The I-quants (IQ3_XXS, IQ2_M, …) push below 4 bits using importance-weighted schemes; they are the only way to fit very large models on small cards, but they sit on the steep part of the quality curve and are best treated as a last resort.
Q8_0 — effectively lossless (measured below); use when memory allows. Q5_K_M / Q6_K — near-indistinguishable from fp16 in blind testing; the quality pick when it fits. Q4_K_M — the standard; the quality-per-byte sweet spot every 8 GB user should start from. Q3 and below — viable for chat on some robust models, risky for agents and math; always benchmark your actual task.
4.3 Experiment 1 — The perplexity ladder (Llama 3 8B)
Perplexity (PPL) measures how surprised a model is by held-out text — lower is better, and small PPL increases map loosely to capability loss. The most-cited GGUF measurements come from the llama.cpp repository’s own evaluation runs, republished in a LessWrong evaluation post comparing quantized performance across schemes. The numbers for Llama 3 8B on WikiText-2:
| Quant | Size (GB) | WikiText-2 PPL | Δ vs fp16 | Relative loss | Reading |
|---|---|---|---|---|---|
| fp16 (reference) | 14.97 | 6.2331 | — | — | Full precision |
| Q8_0 | 7.96 | 6.2342 | +0.0011 | +0.02 % | Lossless Rounding noise |
| Q6_K | 6.14 | 6.2533 | +0.0202 | +0.32 % | Excellent |
| Q5_K_M | 5.33 | 6.2886 | +0.0555 | +0.89 % | Very good |
| Q5_0 | 5.21 | 6.3632 | +0.1301 | +2.09 % | Noticeable Legacy scheme |
| Q4_K_M | 4.58 | 6.3830 | +0.1499 | +2.41 % | The standard 69 % smaller |
| Q4_0 | 4.34 | 6.7001 | +0.4670 | +7.50 % | Caution 4× worse than K_M |
Three things to internalize from this table. First, Q8_0 is genuinely free: a 0.02 % relative perplexity change is below the noise floor of the measurement itself, so the old advice “never go below 8-bit” was calibrated for a world where memory was abundant — in ours, the interesting question starts at Q6. Second, the K-quant premium is real and large: at 4-bit, the K_M scheme loses 3× less perplexity than the legacy scheme at only 0.24 GB more memory. There is no situation on an 8 GB card where Q4_0 is the right choice over Q4_K_M. Third, the jump from Q4 to Q5/Q6 buys meaningful quality at meaningful memory cost — exactly the trade you will exploit in §4.6 when a model almost fits at a higher tier.
4.4 Experiment 2 — Benchmark accuracy, quantized (Llama 3 8B Instruct)
Perplexity is a proxy; agents care about task metrics. The same LessWrong evaluation ran Llama 3 8B Instruct through MMLU (multiple-choice knowledge), WMDP, and The Pile perplexity across quantization schemes from four method families — HQQ, bitsandbytes (BNB), GPTQ, and AWQ:
| Method | Bits | MMLU ↑ | Pile PPL ↓ | Δ MMLU vs bf16 |
|---|---|---|---|---|
| BFloat16 / Float16 | 16 | 63.87 / 63.84 % | 8.283 / 8.279 | — |
| HQQ Int8 | 8 | 63.87 % | 8.298 | 0.00 |
| BNB Int8 | 8 | 63.05 % | 8.305 | −0.82 |
| HQQ Int4 | 4 | 62.29 % | 8.482 | −1.58 |
| AWQ Int4 | 4 | 61.84 % | 8.483 | −2.03 |
| GPTQ Int4 | 4 | 61.58 % | 8.575 | −2.29 |
| BNB NF4 | 4 | 61.44 % | 8.499 | −2.43 |
| BNB Int4 | 4 | 60.80 % | 8.633 | −3.07 |
| HQQ Int3 | 3 | 62.26 %* | 8.872 | −1.61* |
The pattern generalizes across the whole literature: 8-bit quantization costs nothing measurable; 4-bit costs roughly 1.5–3 points of MMLU, with the calibration-aware schemes (HQQ, AWQ) at the good end and naive round-to-nearest (BNB Int4) at the bad end. A 2-point MMLU drop sounds alarming until you remember what it buys: the same model at half the memory, running ~2× faster on the same bus. For agent workloads the correct comparison is not “quantized vs. full-precision of the same model” but “quantized 8 B vs. full-precision 4 B at equal memory” — and on that comparison the quantized 8 B wins essentially every benchmark by a wide margin. Quantization is how you buy model size with memory; the 1–3 point tax is the commission.
4.5 Experiment 3 — The chain-of-thought surprise (the agent-critical result)
Here is the finding that matters more for agents than everything else in this chapter. The LessWrong author went one step further than most evaluations and tested multi-step reasoning — Minerva MATH algebra problems with zero-shot chain-of-thought prompting, where the model must generate many correct tokens in sequence to reach an answer:
| Method | Bits | Minerva MATH Algebra (CoT) ↑ | vs. fp16 |
|---|---|---|---|
| Float16 | 16 | 37.5 % | Reference |
| HQQ Int8 | 8 | 37.9 % | +0.4 within noise |
| BNB Int8 | 8 | 36.3 % | −1.2 mild |
| HQQ Int4 | 4 | 33.7 % | −3.8 real cost |
| BNB NF4 | 4 | 31.3 % | −6.2 |
| BNB Int4 | 4 | 29.3 % | −8.2 |
| GPTQ / AWQ / HQQ Int3 | 3 | DNF | Failed run did not finish |
Compare the two 4-bit columns across experiments: MMLU dropped 1.5–3 points, but multi-step generation dropped 4–8 points, and 3-bit did not merely score lower — it failed to complete. The explanation is compounding error: a single-token prediction task can absorb a small nudge to the output distribution, but a chain-of-thought requires every one of hundreds of sampled tokens to stay on the rails; small per-token distortions accumulate multiplicatively. Agents are chain-of-thought machines. Every ReAct loop is a long multi-step generation interleaved with tool calls, which means:
For agent workloads, treat Q4_K_M as the floor, not the default. Prefer Q5_K_M or Q6_K whenever the model fits, and treat 3-bit quants as chat-only curiosities regardless of how their MMLU looks. The one-point MMLU difference between Q4 and Q5 can be the difference between a coherent 30-step agent trajectory and a loop that silently degrades at step 12. (Model-dependent: robust over-trained models like the Qwen family tolerate Q4 visibly better than older architectures — community blind-testing consistently ranks Q5_K/Q6_K as “nearly indistinguishable from original” while Q4 shows occasional texture loss.)
4.6 Method families: GGUF vs AWQ vs GPTQ vs EXL2
The tables above mix method families, so a direct comparison is warranted. GGUF is a file format + quantization scheme designed for CPU/GPU hybrid execution — its superpower is that it runs anywhere and splits across memory tiers (Chapter 6), which is why it owns the under-24 GB segment. AWQ and GPTQ are GPU-only weight formats that quantize using calibration data to protect salient weights; they are loaded by GPU engines (vLLM, TGI, ExLlama) and generally decode faster than GGUF on the same hardware when the whole model fits in VRAM. EXL2 (ExLlamaV2) offers variable target bit-rates and strong speed on full-GPU setups.
| Property | GGUF | AWQ | GPTQ | EXL2 |
|---|---|---|---|---|
| Runs on CPU / split CPU+GPU | Yes — native | No | No | No |
| Quality retention at 4-bit | Good (K-quants) | 95–97 % of fp16 | ~90–96 % | Adjustable per-file |
| GPU-only decode speed | Good | Fastest (~2× GPTQ, per community benchmarks) | Slower | Fast |
| Calibration cost to produce | Minutes, no data | ~5–10× faster than GPTQ | Slow (needs data) | Slow |
| Known failure mode | Very-low-bit degradation | — | Reported collapse on code tasks at 4-bit | — |
| Verdict for 8 GB agents | Default choice | If whole model fits VRAM | Avoid for agents | If whole model fits |
Two nuances keep this honest. First, the GPTQ “collapses on code” finding comes from community benchmarking of specific 4-bit releases — the failure is real and reproducible on those files, but it is a reason to be cautious rather than a mathematical law; GPTQ remains widely deployed in production serving. Second, on an 8 GB card the AWQ/EXL2 speed advantage usually cannot be exploited for the models you most want to run, because anything above ~9 B parameters will not fit VRAM entirely — and hybrid execution is exactly what GGUF does and the GPU-only formats do not. This is why the recommendation matrix is not “which is best” but “which fits where your model lands on the ladder.”
4.7 Choosing your quant: the 8 GB decision table
Putting the experiments together into the decision you actually face. (Sizes are for 8 B-class models; shift one tier for 12–14 B, two for 30 B MoE.)
| Situation | Choose | Why (from the evidence) |
|---|---|---|
| Model fits VRAM at Q6_K with your context | Q6_K | +0.32 % PPL — blind tests rank it indistinguishable from fp16; spend the bytes you have |
| Need 16–32 K context on an 8 B model | Q4_K_M weights + q8_0 KV | Context memory outweighs the 1–2 pt accuracy delta (Ch. 5 math) |
| Running 12–14 B dense on this machine | Q4_K_M (rarely Q4_K_S) | 14 B Q4 ≈ 8 B Q6 in memory; bigger-and-coarser wins the equal-memory comparison |
| Running 30 B MoE (Qwen3-30B-A3B class) | Q4_K_M | Expert weights are read sparsely; community runs confirm Q4 retains MoE behavior; below Q4 the routed-expert precision degrades routing quality |
| Draft model for speculative decoding | Q8_0 or Q6_K of a 0.5–1.7 B model | It’s tiny; keep it accurate so its drafts get accepted (§7.5) |
| Emergencies / testing a huge model | Q3_K_M / IQ3_XXS | Last resort; expect visible degradation and weak tool-calling reliability |
Quantization-aware training (QAT) produces models trained to live at 4-bit, eliminating most of the PTQ quality tax. Google shipped QAT variants of Gemma 3 — the community-verified result is that Gemma 3 12B QAT fits and runs on an 8 GB GPU (with ~2.5 K context at default settings, more with KV quantization) — and Unsloth’s “Dynamic 4-bit” quants apply QAT-style mixed precision to Qwen3-Coder. When a QAT version of your model exists at your target size, prefer it over a post-hoc quant of the same size; the evidence base for QAT is still thinner, but the Gemma 3 results are encouraging.
5. The KV Cache: Context Length Is a Memory Decision
The cache is arithmetic, it grows linearly, and quantizing it wrong destroys your outputs while your speedometer stays green.
5.1 The formula, worked for every model you might run
Chapter 2 introduced the formula; here is the full reasoning. Attention layers look back at every previous token by comparing the current query against stored keys, then retrieving values. To avoid recomputing these for every new token, inference engines store them — that storage is the KV cache. Modern architectures shrink it with grouped-query attention (GQA): instead of every one of, say, 32 query heads having its own key/value heads, they share a small group of KV heads (typically 8). This is why an 8 B model costs ~128 KiB/token instead of ~512 KiB:
| Model | Layers | KV heads | Head dim | f16 KV per token | 8 K ctx | 16 K ctx | 32 K ctx |
|---|---|---|---|---|---|---|---|
| Llama 3.1 8B | 32 | 8 | 128 | 128 KiB | 1.0 GB | 2.0 GB | 4.0 GB |
| Qwen3 8B | 36 | 8 | 128 | 144 KiB | 1.1 GB | 2.2 GB | 4.4 GB |
| Qwen2.5-Coder-7B | 28 | 4 | 128 | 56 KiB | 0.4 GB | 0.9 GB | 1.7 GB |
| Gemma 3 12B | 48 | 8 | 256 | 384 KiB | 3.0 GB | 6.0 GB | 12.0 GB |
| Qwen3-14B | 40 | 8 | 128 | 160 KiB | 1.2 GB | 2.5 GB | 5.0 GB |
| Qwen3-30B-A3B (MoE) | 48 | 4 | 128 | 96 KiB | 0.75 GB | 1.5 GB | 3.0 GB |
Reading the table as an agent engineer: with weights at Q4_K_M occupying ~5 GB of your ~7 GB usable VRAM, an 8 B model can afford a 16 K context with an fp16 cache, or ~32 K with a q8_0 cache. A Gemma 3 12B at Q4 can barely afford 8 K before you quantize its cache. These are the real numbers behind every “what context should I set?” question — and they were computed without downloading anything.
5.2 Why the cache grows during an agent run (the hidden budget)
Chat users rarely feel KV pressure because conversations grow slowly. Agents feel it acutely, because a ReAct-style loop appends to the context on every step: the tool call (~50–150 tokens), the tool result (100–2,000+ tokens for a file read or search payload), and the model’s intermediate reasoning (100–500 tokens). A realistic accounting for a 30-step coding agent:
| Step class | Tokens added | After 30 steps | Llama-3.1-8B f16 KV consumed |
|---|---|---|---|
| System prompt + tool schemas | 800–2,000 | ~2,000 | 0.25 GB |
| Reasoning + tool calls (30 × ~250) | 7,500 | 7,500 | 0.94 GB |
| Tool results (30 × ~700 avg) | 21,000 | 21,000 | 2.6 GB |
| Total | — | ~30,500 | ~3.8 GB of KV alone |
This is why the two disciplines — cache management (this chapter) and agent context engineering (Chapter 10) — are inseparable. The cache gives you a hard ceiling; the agent architecture determines how fast you approach it and what happens when you do.
5.3 Experiment 4 — Quantizing the KV cache: the q8_0/q4_0 split
The KV cache can itself be stored at lower precision, via llama.cpp’s -ctk / -ctv flags (Ollama: environment variable or Modelfile parameters). The most rigorous public evaluation of what this actually costs is InventiveHQ Lab’s controlled benchmark: one model (Qwen2.5-Coder-7B-Instruct Q4_K_M), one context (8,192 tokens), three cache precisions, greedy decoding over a fixed 12-prompt suite, with output similarity measured against the fp16-cache reference:
| KV cache | VRAM at 8 K | Δ vs f16 | Speed (tok/s) | Output similarity vs f16 | Verdict |
|---|---|---|---|---|---|
| f16 (16-bit) | 4,899 MB | — | 81.8 | 100 % | Reference |
| q8_0 (8-bit) | 4,691 MB | −208 MB | 76.4 | 81.6 % | Safe trade |
| q4_0 (4-bit) | 4,579 MB | −320 MB | 80.3 | 8.3 % | Quality cliff |
The result deserves its dramatic reputation. At 8-bit, the cache halves in theory (and saved 208 MB here, a small number only because 8 K context is small — savings scale linearly with context) while outputs stay mostly-identical at 81.6 % similarity: rounding noise, not degradation. At 4-bit the outputs are 8.3 % similar to the reference — statistically a different answer to the same prompt — while generation speed sits at a healthy 80.3 tok/s. Nothing in your monitoring would flag it. The authors’ framing is the memorable one: q4_0 KV saves only 112 MB more than q8_0 at this context but transforms the model; the savings compound with context length, yet so does the corruption.
Corroborating measurements sharpen the picture. A DGX Spark benchmark of the same three cache precisions on Nemotron-3-Nano-30B-A3B at 128 K context found q4_0 KV 92 % slower than f16 at long context (the quantized-cache kernel path has a performance cliff of its own) and concluded “q8_0 is the only KV quantization worth running.” The llama.cpp TurboQuant discussion reports q4_0 cache saving 552 MiB (72 % KV reduction) at the cost of observed slot-position corruption. Meanwhile, Apple Silicon measurements found the throughput hit from KV quantization under 10 % — the cost there is quality, not speed. And the classic community result from the r/LocalLLaMA KV tests: q4_0 cache enabled a 35 B model (Command R) with 8 K context to fit a 24 GB P40 — proof that the trick works as a memory lever; the question is only whether your task survives the fidelity loss.
(1) q8_0 KV is the right default on a memory-constrained card — near-lossless, halves cache cost, minor speed impact. (2) Never q4_0 the cache for agent workloads — tool-call JSON is exactly the kind of long structured output where 4-bit cache corruption compounds. (3) If you must squeeze cache further, prefer shrinking the context window over deepening cache quantization; a well-architected agent needs less context than you think (Ch. 10), and context you don’t hold costs nothing.
5.4 Flash Attention: free cache efficiency
Flash Attention (FA) is a different implementation of the attention computation that processes it in tiled fashion without materializing the full attention matrix — in llama.cpp, enabling it (-fa, or automatic in recent Ollama builds via OLLAMA_FLASH_ATTENTION=1) both reduces the compute-buffer footprint and, critically, unlocks KV quantization: cache quantization is only supported on the FA codepath. Community measurements on 4×H100-class hardware show FA also shrinking the inference graph dramatically under parallel load (the Ollama parallelism issue #12097 documents graph-size reductions when FA is on). On your 8 GB card the practical guidance is simple: Flash Attention on, always — there is no measured downside at single-stream scale, and it is the prerequisite for the entire cache-quantization toolkit.
5.5 Context-setting doctrine for the 8 GB agent box
Setting the context window (-c in llama.cpp, num_ctx in Ollama) is a decision people get exactly backwards: they either leave the default (2048 in older Ollama — silently truncating agent histories) or max it out (guaranteeing KV pressure and slow prompt processing). The doctrine that falls out of this chapter’s arithmetic:
- Set context deliberately per workload: 8 K for focused tool loops; 16 K for coding agents reading files; 32 K only with q8_0 KV on a model whose weights leave room.
- Budget KV explicitly in your sizing formula (§2.5). Context is not free; it has a price you can now compute exactly.
- Prefer q8_0 KV over larger f16 windows: 16 K @ q8_0 costs the same as 8 K @ f16 and preserves more capability than the context doubling costs.
- Trim tool output aggressively — the 30-step accounting table above shows tool results dominate cache growth. A head -n 50 on file reads saves more KV than any quantization flag.
- Watch for the silent truncation failure: when history exceeds the window, frameworks silently drop the oldest turns — including, catastrophically, your system prompt and tool schemas if the client re-sends them per turn. Chapter 10’s architecture prevents this class of failure by construction.
6. Offloading: VRAM + RAM as One Pool
Layer splitting, mmap, unified memory, and the MoE trick — how a 30 B model runs on a machine whose GPU cannot hold a 9 B one.
6.1 How layer splitting actually works
llama.cpp’s --n-gpu-layers (-ngl) flag is the load-bearing wall of low-VRAM inference. A transformer is a stack of identical decoder layers; the engine loads them one at a time — the first -ngl layers onto the GPU, the remainder into system RAM — and at generation time the hidden state hops across the PCIe bus at the layer boundary, computes the CPU-resident layers on system cores, and hops back. Prompt processing is special-cased: even when most layers are on CPU, the GPU handles the large prompt batches when possible, which matters enormously for agents (§7.4). The mental model to hold: every token pays the PCIe round-trip cost of every CPU-resident layer, and pays it in both directions.
Setting -ngl 99 (or any number ≥ the layer count) offloads everything and lets the engine clamp automatically; on a mixed machine you want the precise number that fills VRAM without exceeding it, because leftover VRAM is pure waste while overflow means either a crash or silent system-memory fallback. Modern llama.cpp builds print exactly how much VRAM each layer needs at load time (offloaded 27/33 layers to GPU plus buffer sizes), and community tooling exists that virtualizes test allocations to find the maximum split automatically (the llama.cpp GPU-automation thread describes the technique). Chapter 11’s sweep script brute-forces the same answer in two minutes with llama-bench.
6.2 What offloading costs: the speed curve
The relationship between GPU-resident fraction and speed is close to linear, with two corrections. First, the intercept is favorable: even a small GPU share beats pure CPU execution by a wide margin, because the GPU offloads prompt processing and the attention-heavy layers benefit disproportionately. Second, the slope depends on your PCIe link and RAM bandwidth: a PCIe 4.0 ×16 machine loses markedly less per offloaded layer than a PCIe 3.0 ×8 one. The bmdpat guide’s systematic -ngl testing summarizes the community consensus plainly: offloading 50 % of layers already yields a major speedup over CPU-only, and the relationship is roughly linear from there.
| Configuration (8 B-class dense, Q4_K_M) | Typical tok/s (RTX 4060/3060-class + DDR4) | Relative |
|---|---|---|
| 100 % layers on GPU (fits) | 30–45 | 1.0× |
| ~85 % on GPU (compute buffers forced spill) | 20–30 | ~0.7× |
| 50 % on GPU | 10–18 | ~0.4× |
| 0 % — pure CPU (6–8 modern cores) | 4–8 | ~0.15× |
| 14 B Q4_K_M, ~60–70 % on GPU | 6–15 | bigger model, still usable |
Two design consequences follow from the curve. First, the 8 GB + 32 GB machine should run two models, not one: a fully-in-VRAM workhorse (8 B at Q4/Q5) for interactive agent steps, and — only when a task demands frontier-adjacent quality — a larger split model at 6–15 tok/s. Second, agentic workloads tolerate the slow tier far better than chat does (§1.2): a planning call that runs at 10 tok/s for 60 seconds inside a 20-minute task is not the bottleneck it would feel like in a chat window.
6.3 mmap: the quiet enabler
llama.cpp memory-maps the model file by default: the OS pages weights into RAM on demand, the page cache is the model copy, and — the under-appreciated part — RAM used as page cache is reclaimable. Swap between two 15 GB models in seconds (the llama-swap project builds an entire hot-swap proxy on this), and after a crash the next load is served mostly from warm cache. The complementary flags worth knowing: --mlock pins weights in RAM to prevent eviction (good for the long-running agent server; costs you the reclaimability), and --no-mmap preloads deterministically at the cost of slower startup and doubled transient memory. Default advice for an agent box: mmap on, mlock on for the primary model, NVMe underneath.
6.4 Unified memory: usually a trap on PCIe machines
llama.cpp offers GGML_CUDA_ENABLE_UNIFIED_MEMORY=1, which uses CUDA managed memory to let the GPU transparently page model tensors from system RAM. On machines with a real unified memory fabric (Apple Silicon, NVIDIA Grace, DGX Spark) this is the natural mode. On your machine — where the GPU reaches RAM only across PCIe — it is usually a trap: community testing on r/LocalLLaMA and the NVIDIA developer forums consistently finds PCIe-attached unified memory “very slow,” with page-fault storms at the tensor boundary replacing the controlled streaming that layer-splitting provides. The one documented exception (llama.cpp PR #8035): for very large models at aggressive low-bit quants where only a fixed small working set is hot — e.g., Llama-3-70B IQ2_XS on 24 GB — unified memory beat manual partial offload, because the driver’s page prediction occasionally outperforms a bad manual split. The rule of thumb that falls out: prefer explicit layer splitting on PCIe machines; reserve unified memory for experiments, not for your daily driver.
6.5 The MoE trick: why 30 B parameters can be cheaper than 9 B
A mixture-of-experts model stores far more parameters than it uses per token: Qwen3-30B-A3B has 30 B parameters distributed across routed experts, but each token activates only ~3 B of them (plus shared experts and attention). Combine that with hybrid placement and something remarkable happens for small-VRAM machines:
- The always-active tensors — attention layers, shared experts, embeddings — are small (~3–5 GB at Q4). Put those in VRAM.
- The routed experts are read sparsely: only the experts the router selects per token are touched. They can live in system RAM, streamed across PCIe on demand — and with fast DDR5 the streaming cost per token is bounded by the ~3 B active bytes, not the 30 B total.
- Prompt processing runs on the GPU even with experts on CPU (the Hugging Face MoE offload guide by Doctor-Shotgun documents this llama.cpp capability and reports CPU+GPU MoE inference as “very performant” for exactly this reason).
The community measurements confirm the theory at consumer scale. Unsloth’s Qwen3-Coder-30B guide reports 6+ tok/s for the 30 B coder model on 18 GB of combined memory — modest, but that is a frontier-adjacent coding agent model running on hardware a fraction of its size class. r/LocalLLaMA reports for Qwen3-30B-A3B at Q4_K_M on a 24 GB card include 90 K-token contexts with 40–48 of 48 layers offloaded; the same model at ~19 GB total splits comfortably across your 8 + 32 GB (VRAM takes attention + shared + a slice of experts). And the 2026 orcarouter coding-LLM survey notes the community has squeezed Qwen3-Coder-30B onto 8 GB cards outright using TurboQuant-style KV compression — extreme, but indicative. On an 8 GB card expect 8–20 tok/s for a Q4 30B-A3B with the right tensor split, versus 6–15 for a 14 B dense — the MoE option buys more capability per token-second at the cost of setup care.
Modern llama.cpp lets you route tensor types, not just layer counts, via --override-tensor (e.g., keep blk\..*\.ffn_.*_exps.* on CPU while everything else goes to GPU). That regex is the MoE recipe: attention + shared experts in VRAM, routed experts in RAM. Ollama does a reasonable automatic split; llama.cpp with explicit overrides is the power path. Chapter 11 gives the exact command line.
7. Throughput Engineering: Making It Fast
Measure first, then tune the four knobs that matter: placement, threads, prompt processing, and speculation.
7.1 The measurement discipline
Every speed claim in this paper is reproducible with a two-command loop, and your first hour with any model should be spent in it. llama-bench is the standard harness: it reports prompt-processing (pp, tokens/s ingesting input) and text-generation (tg, tokens/s producing output) separately — the two numbers that define agent latency, since agents spend most wall-clock time in pp re-ingesting history at every step and most perceived liveliness in tg.
# Core benchmark: prompt-processing and generation at two context sizes
./llama-bench -m qwen3-8b-Q4_K_M.gguf -ngl 99 -p 512,2048 -n 128 -r 3
# Sweep GPU-layer counts to find your machine's optimal split (do this once per model)
for ngl in 99 36 30 24 18 12 0; do
echo "=== ngl=$ngl ==="; ./llama-bench -m qwen3-14b-Q4_K_M.gguf -ngl $ngl -p 512 -n 64 -r 2
done
# Watch VRAM while it runs (second terminal)
watch -n 0.5 nvidia-smi --query-gpu=memory.used,utilization.gpu --format=csv
One measurement-hygiene warning from the field, worth internalizing before you trust any number including your own: an influential blog post on llama.cpp CUDA benchmarking (“Why Your llama.cpp CUDA Benchmarks Are Wrong”) documented a common failure where mis-built binaries silently ran without real CUDA support, producing a plausible 52 tok/s that was actually a lie. Verify your build with llama-bench’s output (it prints the backend) and sanity-check that GPU utilization rises during generation. The same discipline applies to community numbers: a benchmark without its build, flags, and context size is a rumor.
7.2 Threads and the CPU side
For the CPU-resident fraction of any split model, thread count is the top lever — and the llama.cpp performance documentation is blunt that incorrect thread settings are the #1 cause of slow inference. The optimum is physical cores, sometimes physical cores minus one (leaving headroom for the GPU driver and your agent process), and almost never hyperthreads: SMT threads share execution units with their sibling cores and add scheduling jitter to latency-sensitive decode. Community measurements on hybrid setups (the 3090+3060+3060 benchmark thread) show the same pattern on the GPU side — the wrong process/thread mix cost 20–30 % of throughput until tuned. Set --threads to your physical core count (§3.5’s audit) and move on; this is a five-minute fix worth 1.3–1.8× on CPU-heavy configurations.
7.3 Prompt processing: the agent’s real tax
Generation gets the attention; prompt processing pays the bills. Every agent step resubmits the full history — system prompt, schemas, prior turns, tool results — before generating a single new token. At 8 K history this means ingesting 8 K tokens per step; at 30 steps, ~240 K tokens of cumulative pp. The saving grace: prompt processing is compute-bound and batches beautifully, so the GPU handles it far faster than generation — an RTX 4060 processes 1,000–3,000+ tok/s of prompt at 8 B scale versus ~40 tok/s of generation, and even heavily-offloaded MoE setups keep pp on the GPU (§6.5). Your levers, in order of impact:
- Keep the model in VRAM if you can — prompt processing suffers more from CPU-resident layers than generation does.
- Enable Flash Attention (§5.4) — it shrinks the processing-time memory spike that otherwise caps your usable batch.
- Trim what you resubmit — this is context engineering (Ch. 10), and it is the only lever that improves both pp and KV simultaneously. Halving resubmitted history halves pp time at every single step, compounding across the whole run.
- Batch size (-b/-ub): defaults are usually fine at single-user scale; raise -ub only if logs show prompt truncation into multiple passes.
7.4 Parallelism: one slot, usually
Ollama and llama.cpp can serve multiple requests against one loaded model by pre-allocating KV cache for OLLAMA_NUM_PARALLEL slots (default auto-selects 4 or 1). Understand the trade precisely: parallel slots divide your KV budget — four slots at 16 K each is the same memory as one slot at 64 K — and on an 8 GB card your KV budget is the scarce resource. For a single-user agent workstation, set OLLAMA_NUM_PARALLEL=1 and give the whole cache to your one agent; the 4-slot default is a server posture that silently quarters your context headroom. (The documentation’s guidance matches: parallel defaults trade memory footprint for throughput — the wrong trade when memory is the constraint.) Related environment hygiene for agent servers: OLLAMA_KEEP_ALIVE — the default 5-minute unload punishes an agent whose tool calls take six minutes; set it to hours (24h) or -1 so the model stays resident, and pair it with --mlock in llama.cpp for the same effect.
7.5 Speculative decoding: 25–60 % free speed
Speculative decoding pairs your target model with a tiny draft model (e.g., Qwen3-0.6B drafting for Qwen3-8B, sharing its tokenizer). The draft model cheaply guesses several tokens; the target model verifies them in a single batched pass; accepted tokens are kept and the first disagreement triggers a fallback — mathematically identical output distribution, meaningfully higher throughput. The measurements: llama.cpp’s speculative-decoding launch thread reported 25–60 % speedups across models (25–40 % being typical); the broader literature surveys (glukhov.org’s 2026 guide, the arXiv Spec-Bench evaluation) put standard draft-model schemes at 20–50 % and structured heads like EAGLE-3 at up to 2–4× under favorable conditions. The acceptance rate — and therefore the speedup — depends entirely on how similar draft and target distributions are, so draft models must be same-family. On an 8 GB card the memory math is friendly (a 0.5–1.7 B draft at Q8 costs 0.5–1.9 GB), but VRAM competition with the target’s KV cache is real: measure whether the draft’s residency costs you more in context than it gains in tokens.
# Target = Qwen3-8B Q4_K_M (in VRAM), draft = Qwen3-0.6B Q8_0
./llama-server \
-m qwen3-8b-Q4_K_M.gguf \
-md qwen3-0.6b-Q8_0.gguf \ # draft model
-ngl 99 -ngld 99 \ # both fully on GPU
-c 16384 -fa -ctk q8_0 -ctv q8_0 \
--draft-max 16 --draft-min 4 # tokens per speculation round
# Expect log lines like: accepted ~60-70 %, speedup ~1.3-1.6×
# (varies with task entropy — code > chat > creative writing in acceptance)
7.6 The tuning checklist, in order of ROI
| Order | Action | Measured gain | Cost |
|---|---|---|---|
| 1 | Correct -ngl (all layers that fit, no overflow) | Up to 3–8× vs bad split | 2 min sweep (§7.1) |
| 2 | Flash Attention on | Buffer savings; enables KV quant | One flag |
| 3 | --threads = physical cores | 1.3–1.8× on CPU share | One flag |
| 4 | q8_0 KV cache | ~2× context at ~zero quality cost | Two flags |
| 5 | Dual-channel RAM (if single-stick) | 1.3–1.8× on offloaded layers | Hardware |
| 6 | Speculative decoding | 1.25–1.6× | ~1 GB VRAM + setup |
| 7 | Context discipline (Ch. 10) | Faster every step; compounds | Architecture |
The Agent Stack
From abstract capability to a running system: which models the 8 GB ladder can actually hold, which engine should serve them, how to architect the agent so context never becomes the failure mode, and the complete build with code.
8. Choosing the Model: The 8 GB Ladder
Benchmarks for every rung, why tool-calling reliability beats MMLU, and the two-model doctrine.
8.1 What the benchmarks actually measure
Model selection for agents is not the same game as model selection for chat. General-knowledge scores (MMLU) correlate loosely with agentic usefulness; what predicts agent success is (a) function-calling accuracy (measured directly by the Berkeley Function Calling Leaderboard, BFCL — where Llama 3.1 8B Instruct scores ~76 % and the Qwen3 series ranks at or near the top of open models), (b) instruction-following precision under long multi-part prompts, (c) robustness to quantization on structured output — the CoT result of §4.5 applies doubly to JSON tool calls — and (d) coding ability if your agent edits files (HumanEval-class metrics, or SWE-bench for whole-task evaluation). A useful piece of evidence that small models are agent-viable: a 2026 arXiv study of tool-calling representations found clean linear structure in models as small as 4 B (Gemma 3 4B, Qwen3 4B), with simple probes reading tool selection at 100 % accuracy after 10–20 examples — tool calling is a learnable, well-structured capability in this size class, not a frontier-model monopoly.
8.2 The ladder, with data
Here is the full selection space for your machine, ordered by how much of it stays in VRAM. Benchmark figures are the models’ published numbers (bf16); expect the quantization deltas of §4.4 on top.
| Rung | Model | Q4_K_M size | Fits 8 GB? | Agent-relevant strengths |
|---|---|---|---|---|
| Draft tier 0.5–2 B | Qwen3-0.6B / 1.7B | 0.5–1.2 GB | Easily | Speculative drafting for Qwen3-8B; ultra-fast summarizers for context compression |
| Llama 3.2 1B/3B | 0.8–2.0 GB | Easily | Edge-class chat; community-measured ~28 tok/s class on CPU-only rigs | |
| Qwen3-1.7B (tuned) | 1.2 GB | Easily | Cited by 2026 SLM surveys as the go-to small tool-calling specialist after fine-tune | |
| Fast tier 3–4 B | Qwen3-4B | 2.6 GB | Yes + 32 K ctx | Best-in-class small agent: strong BFCL-family tool use, 32 K native context, thinking-mode toggle |
| Phi-4-mini (3.8B) | 2.5 GB | Yes + 32 K ctx | MMLU 67.3, HumanEval 74.4, GSM8K 88.6 — exceptional math/code for size; ~15–20 % faster than Qwen3-4B per community timing | |
| Gemma 3 4B | 2.4 GB | Yes | 128 K native context, multimodal (vision), strong multilingual | |
| Core tier 7–9 B | Qwen3-8B | 5.1 GB | Yes + 16 K (q8 KV) | The workhorse pick: top open-8B tool calling, hybrid thinking mode, Qwen ecosystem maturity |
| Llama 3.1 8B Instruct | 4.9 GB | Yes + 16 K (q8 KV) | MMLU ~68.4, BFCL ~76.1 — the best-understood 8B in every framework; huge fine-tune ecosystem | |
| Qwen2.5-Coder-7B | 4.7 GB | Yes | The coding-agent specialist at this size; tiny 4-KV-head cache (56 KiB/token — see Ch. 5 table) makes it context-cheap | |
| Big-dense tier 12–14 B | Gemma 3 12B (QAT) | 6.7–7.3 GB | Tight — short ctx | Quality jump over 8 B class; QAT variant community-verified to run on 8 GB (fat 384 KiB/token KV forces q8_0 cache + modest context) |
| Qwen3-14B | 9.3 GB | Split required | MMLU-Pro ~65.5 class capability at 6–15 tok/s hybrid — the quality ceiling for dense-on-this-box | |
| MoE tier 30 B class | Qwen3-30B-A3B | 18.6 GB | Hybrid (6.5) | 3 B active of 30 B: near-14B-dense quality at 8–20 tok/s with the §6.5 tensor split; the flagship option for this hardware |
| Qwen3-Coder-30B-A3B | ~19 GB | Hybrid (6.5) | Agentic-coding specialist of the family; Unsloth-documented at 6+ tok/s on 18 GB combined memory, higher with the right split |
8.3 Head-to-head at the core tier
Because the core tier (8–9 B) is where your agent will live most of the time, the choice deserves the evidence rather than vibes. A controlled TrueFoundry evaluation ran Qwen3-8B, Llama 3.1 8B, and Ministral 8B on identical vLLM serving hardware across 4–256 concurrent users; the headline for single-user relevance was that Qwen3-8B sustained the strongest quality-per-compute at low concurrency (its thinking mode trades latency for accuracy on hard steps — you can toggle it off for fast tool loops), while Llama 3.1 8B held the most stable behavior under load. Independent comparisons (kunalganglani.com, willitrunai.com) converge on the same summary: Qwen3-8B outperforms Llama 3.1 8B on most knowledge, coding, and multilingual benchmarks, while Llama retains edges in ecosystem maturity and slight VRAM economy (4.9 vs 5.1 GB at Q4_K_M). For an agent box, lean Qwen3-8B for capability, Llama 3.1 8B when you want every framework integration to work first try, and Qwen2.5-Coder-7B when the agent’s job is code — its unusually small KV cache (4 heads, not 8) is a genuine architectural advantage for long coding sessions on 8 GB.
8.4 The two-model doctrine
The strongest recommendation this chapter makes is structural, not model-specific: run a two-model stack, with roles assigned by tier. The fast in-VRAM core model (Qwen3-8B class) executes the agent loop — planning, tool calls, observations — where latency and format reliability matter most. The big MoE model (Qwen3-30B-A3B class) is loaded on demand — via llama-swap or an Ollama second instance — for the few hard moments: the one-shot architecture decision, the gnarly debugging session, the long synthesis. The draft tier earns its keep inside this doctrine too: a resident 0.6 B model accelerates the core (§7.5) and doubles as the rolling summarizer for context compression (§10.3), a job where its speed matters more than its brilliance. This matches how the memory economics actually work: your VRAM’s comparative advantage is latency, your RAM + PCIe’s comparative advantage is capacity, and the doctrine assigns each workload to the tier that serves it best. It also matches how frontier agent products are architected — fast models in the loop, big models at decision points — scaled down to your silicon.
Published BFCL scores are measured at fp16/bf16. The §4.5 compounding-error result means your Q4_K_M model’s effective tool-calling accuracy will sit somewhat below its published number, and degrades non-linearly as context fills and chains lengthen. Mitigate structurally rather than by chasing a bigger model: fewer tools per call, terse schemas, grammar-constrained decoding (§10.4), and retry-on-format-error. These recover more reliability than one quant tier costs.
9. Inference Engines Compared
llama.cpp, Ollama, vLLM, LM Studio — what each is actually good at, and which belongs on an 8 GB agent box.
9.1 The four contenders
llama.cpp is the reference implementation everything else builds on: the GGUF format, layer splitting, KV quantization, speculative decoding, Flash Attention, tensor overrides — every technique in Part II — originates or lands here first. It ships llama-server (OpenAI-compatible HTTP API, including tool-calling support in current builds) and llama-bench. Cost: you build or download binaries and compose flags yourself. Ollama is llama.cpp (plus its own Go serving layer and a model zoo) wrapped in one-command ergonomics — ollama pull qwen3:8b and you are serving. It auto-configures offload and context defaults, which is both its charm and its danger on a memory-constrained card. vLLM is a throughput-oriented serving engine built on PagedAttention and continuous batching — designed for concurrent users on big GPUs, loading AWQ/GPTQ/FP8 rather than GGUF. LM Studio is the polished GUI: model browser, chat interface, an embedded llama.cpp server — ideal for exploring, at some performance cost (a comparative benchmark measured it 29 % slower than raw llama.cpp on M3 Max hardware, 38.2 vs 53.5 tok/s; the gap reflects wrapper overhead and conservative defaults).
9.2 The comparison, on your constraints
| Criterion | llama.cpp | Ollama | vLLM | LM Studio |
|---|---|---|---|---|
| Hybrid VRAM+RAM execution | Best in class (-ngl, tensor overrides, mlock) | Good (automatic split; less control) | Poor fit (wants full GPU residency) | Good (inherits llama.cpp) |
| Memory overhead of the serving stack | Minimal | Lean — 1.8 GB system RAM measured vs vLLM’s 4.6 GB | Heavy | Moderate (Electron app) |
| Under concurrency | Good (batched server) | Good (NUM_PARALLEL slots) | Best (continuous batching) | Basic |
| Behavior when model > VRAM | Controlled split, predictable | Controlled split, automatic | Degrades sharply / OOM-oriented | Automatic split |
| Tool-calling API for agents | OpenAI-compatible + GBNF grammars | OpenAI-compatible + Modelfile params | OpenAI-compatible (most complete) | OpenAI-compatible |
| Config surface worth learning | Large (this paper = the tutorial) | Small (env vars + Modelfile) | Large (server-class) | None (GUI) |
| Verdict for 8 GB agent box | Primary recommendation | Fine daily driver once tuned | Only for multi-user serving of ≤8B AWQ | Exploration & chat; not the agent backend |
The vLLM row deserves a sentence of fairness rather than a dismissal: if your use case is serving an 8 B AWQ model to several simultaneous users — a team-shared agent gateway, say — vLLM’s continuous batching genuinely wins throughput, and its gpu_memory_utilization flag can keep an 8 GB card disciplined. But for the single-workstation agent pattern this paper targets, its strengths (concurrency, throughput) are not your constraints, and its weaknesses (residency assumptions, heavyweight stack) are exactly your constraints. The dev.to spill test makes the boundary vivid: past 24 GB, llama.cpp and Ollama degrade gracefully to single-digit tok/s and keep going — engineered hybrid behavior — while server-class engines treat oversubscription as an error condition.
9.3 The recommendation
Run llama.cpp’s llama-server as your agent backend, configured per Chapter 11 — you get every Part II technique at full power and the OpenAI-compatible API your agent code already speaks. Keep Ollama installed as the convenience layer for trying models in 30 seconds (its defaults are merely suboptimal, not broken — the tuning table below fixes them), and treat LM Studio as the friendly exploration GUI. This is also the exact stack the community converged on: llama.cpp for control, Ollama for convenience, everything else situational.
Five environment changes convert Ollama from “convenient defaults” to “disciplined 8 GB citizen”: OLLAMA_NUM_PARALLEL=1 (reclaim KV from parallel slots), OLLAMA_FLASH_ATTENTION=1 (enable FA + KV quant support), OLLAMA_KV_CACHE_TYPE=q8_0 (halve cache cost), OLLAMA_KEEP_ALIVE=24h (no mid-task model evictions), and per-model num_ctx in the Modelfile (the silent-truncation fix — Ollama’s context default historically ran as low as 2048, which quietly amputates agent histories).
10. Agent Architecture for Constrained Machines
On an 8 GB box, the agent’s design is the second half of the memory budget. Context engineering is capability engineering.
10.1 The loop, and why it eats memory
Strip every framework away and an agent is four things in a cycle: a prompt assembly step (system instructions + tool schemas + selected history), a model call that emits either a tool invocation or a final answer, a tool execution step whose output is appended to history, and a termination check. The ReAct formulation — Reason, Act, Observe, repeat — is the canonical shape, and SWE-bench Verified’s own results page credits surprisingly high scores to “just a simple ReAct agent loop” with no special scaffold. Each turn of the cycle grows the history; each growth enlarges the KV cache (§5.2) and the prompt-processing bill (§7.3); and nothing in the naive loop ever shrinks either. On a 128 GB server this negligence is affordable. On your machine, the agent that manages its own context is the difference between a system that completes hour-long tasks and one that OOMs at step 20 — or worse, silently forgets its instructions and wanders.
The framing that makes this concrete is the one Mem0’s engineering blog popularized: the context window is RAM, not storage. Working state belongs in context; everything else — task definition, long-term facts, retrieved knowledge, old tool outputs — belongs in external storage (files, a database, a retrieval index) that is paged in on demand and expires out when no longer relevant. Production agent systems (per the zylos.ai session-lifecycle survey) converged on exactly this discipline: session rotation, memory persistence, and priority management for always-on operation. Your architecture should be a miniature of theirs.
10.2 Strategy menu for context management
Five strategies, in the order you should adopt them. The first two are mandatory; the rest scale with task ambition.
| Strategy | Mechanism | Memory effect | Quality effect | Use when |
|---|---|---|---|---|
| 1. Tool-output hygiene | Truncate/summarize every tool result before appending (head-N lines, strip whitespace, cap bytes) | Cuts the dominant growth term (§5.2) by 2–10× | Usually neutral or positive (noise removal) | Always |
| 2. Sliding window + fixed head | Keep system prompt + schemas + first user message pinned; drop or compress oldest middle turns | Bounded cache; no runaway | Mild — recent turns carry most signal | Always (it’s the floor of safety) |
| 3. Rolling summarization | When history crosses a threshold, compress evicted turns into a running summary written by the small resident model | Unbounded tasks in bounded memory | Good, if summaries capture decisions & open items | Tasks > ~30 steps |
| 4. Retrieval (RAG) | Store tool outputs/findings in a vector index; re-inject only relevant chunks per step | Context shrinks to what matters now | Good for fact recall; weak for procedural state | Document-heavy tasks; long memory across sessions |
| 5. Sub-agent isolation | Spawn a fresh-context worker for bounded subtasks (read these 5 files, find the bug), return only its conclusion | Blows up context usage without touching your window | Strong — mirrors frontier multi-agent patterns | Exploratory spikes, bulk reading, retries |
Two implementation details make or break these strategies. First, never let the eviction boundary eat your system prompt or tool schemas — the silent-truncation failure of §5.5. Pin them structurally: assemble each turn’s context as [pinned system block] + [rolling summary] + [last K turns] + [current user turn], so eviction can only ever touch the designated middle. Second, summarize into structure, not prose: a rolling summary formatted as “goal / decisions made / facts learned / files touched / current step / open problems” preserves exactly the state an agent needs to resume coherently, where a paragraph-style summary reliably loses the procedural thread — the multi-day agent-session writeups on dev.to report the same lesson from practice.
10.3 Tool design for small, quantized models
The model executing your loop is an 8 B model at Q4 with a quantization-perturbed distribution. Tool interfaces can be designed to make that model look bigger than it is, or smaller — the difference is mostly schema discipline.
- Few tools per decision. BFCL-style accuracy drops as the tool count grows; the arXiv tool-calling-probe work shows selection is linearly readable but confusability still rises. 4–7 well-chosen tools beat 20 overlapping ones. If you need more, group them behind a dispatcher tool.
- Terse, typed schemas. Short names, one-line descriptions, required fields only. Every schema token is resubmitted at every step (§7.3) — verbose schemas are a permanent context tax you pay hundreds of times.
- Design outputs for the cache. Paginate by default (offset/limit), return IDs not bodies, cap list lengths, make the first line of output the answer.
- Errors are prompts. A tool error message is read by the model as its next input; “FileNotFoundError: config.yaml — did you mean config.yml? Use list_dir to check” recovers the loop, while a stack trace fills the cache with useless tokens.
- Constrain the format at decode time. llama.cpp’s GBNF grammars (and JSON-schema modes in llama-server/Ollama) make malformed tool calls impossible rather than merely unlikely — the single highest-ROI reliability intervention on quantized models, turning the §4.5 compounding-error risk into a retry at worst.
- Idempotent, bounded tools. The agent will call the same tool twice and pass wrong arguments; make the cost of that a no-op, not a mutation.
10.4 RAG on a budget: the embedding side
Retrieval needs an embedding model, and the good news is that they are tiny by LLM standards: nomic-embed-text-v1.5 (137 M parameters, ~274 MB at fp16) or bge-m3 run comfortably alongside your core model — the former even on CPU without hurting loop latency, since embeddings are generated asynchronously with tool execution. Keep the vector store small and local (sqlite-vec, LanceDB, Chroma in embedded mode; all in-process, all free of a server’s RAM). The discipline that matters: chunk documents to your retrieval budget (256–512 token chunks mean each retrieved chunk is 256–512 tokens of KV — retrieve five and you have spent 2 K tokens of your window before the model thinks). Retrieval top-k is a memory decision, exactly like context length; tune it like one.
10.5 The scaffolding lesson from SWE-bench
Aspirational evidence that architecture beats size: the mini-swe-agent project — deliberately minimal, ~100 lines of loop logic — resolved 65 % of SWE-bench Verified instances with a frontier model, and its successor configurations exceed 74 %; the SWE-bench Verified leaderboard notes bash-only ReAct loops matching elaborate scaffolds. The counterproductive pattern the project explicitly rejected is framework bloat: every abstraction layer you add between model and tools costs tokens (re-exported schemas, middleware narrating itself into the history) and hides the state your context management needs to see. On an 8 GB machine this lesson is not philosophy, it is memory: LangChain-class frameworks bring convenience but also token overhead and several hundred MB of process footprint; a 200-line bespoke loop (Chapter 11) gives you the §10.2 strategies with zero framework tax and total observability. Use framework abstractions where they earn their memory; otherwise write the loop.
10.6 Failure modes specific to this hardware
| Failure | What it looks like | Root cause | Fix |
|---|---|---|---|
| Silent instruction loss | Agent forgets its task or output format mid-run | History exceeded window; oldest messages evicted — including pinned ones, because the client re-sends full history and the server truncates | Structural pinning (§10.2); explicit num_ctx; monitor prompt-issued tokens in server logs |
| Tool-call format drift | Malformed JSON, wrong field names, hallucinated tools | Quantization perturbation compounding over long generations (§4.5) | GBNF/JSON-schema constrained decoding; schema retry loop; fewer tools |
| Mid-task stall | First tool call after a 10-minute test suite is slow | Model unloaded by KEEP_ALIVE default (5 min) → full reload | KEEP_ALIVE=24h / --mlock (§7.4) |
| Long-context mush | Correct facts, wrong conclusions in late steps | Lost-in-the-middle degradation as context fills | Keep working context ≤ 8–16 K; externalize the rest (§10.2) |
| OOM mid-run | Crash only after N steps of a long task | KV grew past free VRAM as history accumulated | Chapter 14 playbook; window + eviction thresholds sized by the Ch. 5 formulas |
11. The Complete Build: Recipes and Code
Everything from the previous ten chapters, assembled into a running agent on your machine.
11.1 Build the engine
# Prereqs: CUDA toolkit (nvcc), cmake, a C++ compiler, git
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="89;86" # 89=4060, 86=3060/3070
cmake --build build --config Release -j$(nproc)
# Verify CUDA actually linked (see §7.1 warning about silent CPU-only builds)
./build/bin/llama-bench -m /path/to/tiny-test-model.gguf -ngl 99 -p 128 -n 32
# Expect "backend: CUDA" in output and GPU util in nvidia-smi
Windows users: the llama.cpp releases page ships prebuilt CUDA binaries, and WSL2 runs the Linux build with full GPU passthrough — either path works; the recipes below are identical. Keep the build directory: llama-server, llama-bench, and llama-cli all live in build/bin/.
11.2 The sizing calculator (run before every download)
The Chapter 2 formulas as a tool. This is deliberately dependency-free — copy it anywhere.
#!/usr/bin/env python3
"""VRAM planner: weights + KV cache + overhead vs your card. (Ch. 2 & 5 formulas)"""
MODELS = { # name: (params_B, layers, kv_heads, head_dim, q4km_gb)
"qwen3-4b": (4, 36, 8, 128, 2.6),
"phi-4-mini": (3.8, 32, 8, 128, 2.5),
"gemma-3-4b": (4, 34, 4, 256, 2.4),
"qwen3-8b": (8, 36, 8, 128, 5.1),
"llama-3.1-8b": (8, 32, 8, 128, 4.9),
"qwen2.5-coder-7b":(7, 28, 4, 128, 4.7),
"gemma-3-12b": (12, 48, 8, 256, 7.3),
"qwen3-14b": (14, 40, 8, 128, 9.3),
"qwen3-30b-a3b": (30, 48, 4, 128, 18.6),
}
VRAM_GB = 8.0; COMFORT = 6.5; TIGHT = 7.4; OVERHEAD = 0.9; KV_BYTES = {"f16": 2, "q8_0": 1.06, "q4_0": 0.56}
def plan(name, ctx_k=16, kv="q8_0", quant_gb=None):
p, L, H, D, q4 = MODELS[name]
w = quant_gb if quant_gb else q4
per_tok = 2 * L * H * D * KV_BYTES[kv] # §5.1 formula
kv_gb = per_tok * ctx_k * 1024 / 1e9
total = w + kv_gb + OVERHEAD
verdict = "FITS w/ MARGIN" if total <= COMFORT else \
"TIGHT (headless/iGPU)" if total <= TIGHT else "SPLIT/OOM"
print(f"{name:<18} weights={w:5.1f} KV@{kv}/{ctx_k}K={kv_gb:5.2f} "
f"total≈{total:5.2f} GB {verdict}")
return total
if __name__ == "__main__":
print(f"Tiers: ≤{COMFORT} GB comfortable · ≤{TIGHT} GB tight (§2.4)")
plan("qwen3-8b", 16, "q8_0") # the recommended core config (tight tier)
plan("qwen3-8b", 8, "q8_0") # same model, comfort tier
plan("gemma-3-12b", 8, "q8_0") # the tight-fit rung
plan("qwen3-14b", 8, "q8_0") # → split required (Ch. 6)
plan("qwen3-30b-a3b", 32, "q8_0") # → MoE hybrid (§6.5)
11.3 Launch configurations (the three servers)
./build/bin/llama-server \
-m models/qwen3-8b-Q4_K_M.gguf \
-ngl 99 \ # all 36 layers to GPU (5.1 GB < budget)
-c 16384 \ # deliberate context (§5.5 doctrine)
-fa \ # Flash Attention: on, always (§5.4)
-ctk q8_0 -ctv q8_0 \ # KV cache: the safe quant (§5.3)
--mlock \ # pin weights; no mid-task eviction (§6.3)
--threads 6 \ # physical cores from §3.5 audit
--host 127.0.0.1 --port 8080 \
--jinja # use model's chat template (needed for tools)
# Sanity: expect ~30–42 tok/s gen, 1–3k tok/s prompt processing on RTX 4060-class
# VRAM after warm-up: ~7.1 GB — the TIGHT tier (§2.4): run headless or move
# the display to the iGPU (§3.4). Verify with: nvidia-smi
./build/bin/llama-server \
-m models/qwen3-30b-a3b-Q4_K_M.gguf \
-ngl 99 \
--override-tensor "blk\.([0-9]|[12][0-9]|3[0-5])\.ffn_.*_exps=CPU" \
# routed experts of layers 0–35 stay in RAM;
# attention+shared experts+layers 36–47 in VRAM (~6.5 GB)
-c 16384 -fa -ctk q8_0 -ctv q8_0 \
--mlock --threads 6 --port 8081 --jinja
# Expect 8–20 tok/s (DDR4→DDR5 dependent); quality ≈ 14B-class dense
# Tune the layer boundary: if VRAM headroom remains after load, shift the
# regex to keep more expert layers on GPU; re-run llama-bench each change
./build/bin/llama-server \
-m models/qwen3-8b-Q4_K_M.gguf -md models/qwen3-0.6b-Q8_0.gguf \
-ngl 99 -ngld 99 -c 16384 -fa -ctk q8_0 -ctv q8_0 \
--draft-max 16 --draft-min 4 --port 8080 --jinja
# Same outputs as recipe A, ~1.3–1.6× faster generation (25–60 % per §7.5)
OLLAMA_NUM_PARALLEL=1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_KEEP_ALIVE=24h ollama serve, then per model: ollama pull qwen3:8b and a Modelfile setting num_ctx 16384 and num_gpu 99. You surrender tensor-level control (recipe B) but get 90 % of recipes A/C with none of the flag discipline.
11.4 The minimal agent (all of Chapter 10, ~170 lines)
This is the deliverable the paper has been building toward: a ReAct agent with structural context pinning, a sliding window, rolling summarization, tool-output hygiene, and schema retry — no framework, every memory decision visible. It speaks the OpenAI-compatible protocol to any recipe above.
#!/usr/bin/env python3
"""nano_agent: a context-disciplined ReAct agent for llama-server / Ollama.
Implements Ch.10: pinned system block, sliding window, rolling summary,
tool-output hygiene, format retry. Deps: pip install requests"""
import json, re, subprocess, shutil, requests
API = "http://127.0.0.1:8080/v1/chat/completions"
MODEL = "local" # llama-server ignores the name
KEEP_TURNS = 12 # sliding window: recent turns kept verbatim
SUMMARIZE_AT = 24_000 # chars of history → compress (≈6k tokens)
TOOL_RESULT_CAP = 4_000 # tool-output hygiene: hard byte cap
SYSTEM = "You are a careful file-and-shell agent. Work step by step. \
Call exactly one tool per turn. Stop with a final answer when done."
TOOLS = [
{"type":"function","function":{"name":"list_dir","description":"List files in a directory",
"parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}},
{"type":"function","function":{"name":"read_file","description":"Read up to 80 lines of a text file",
"parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}},
{"type":"function","function":{"name":"write_file","description":"Overwrite a text file",
"parameters":{"type":"object","properties":{"path":{"type":"string"},
"content":{"type":"string"}},"required":["path","content"]}}},
{"type":"function","function":{"name":"run_cmd","description":"Run a short shell command (60s timeout)",
"parameters":{"type":"object","properties":{"cmd":{"type":"string"}},"required":["cmd"]}}},
]
# ---------------- tools (bounded + error-messages-as-prompts, §10.3) --------
def tool_read_file(a):
try:
lines = open(a["path"], errors="replace").read().splitlines()[:80]
return "\n".join(lines) or "(empty file)"
except Exception as e:
return f"ERROR reading {a['path']}: {e}. Use list_dir to check the path."
def tool_run_cmd(a):
try:
r = subprocess.run(a["cmd"], shell=True, capture_output=True, text=True, timeout=60)
out = (r.stdout + r.stderr).strip()
return out[:TOOL_RESULT_CAP] or "(no output)"
except subprocess.TimeoutExpired:
return "ERROR: command exceeded 60s timeout. Split the work into smaller steps."
TOOLS_IMPL = {"list_dir": lambda a: "\n".join(sorted(
__import__("os").listdir(a["path"]))[:200]) or "(empty)",
"read_file": tool_read_file,
"write_file": lambda a: (open(a["path"],"w").write(a["content"]), "wrote "+a["path"])[1],
"run_cmd": tool_run_cmd}
# ---------------- context engineering (§10.2) ------------------------------
def build_messages(system, summary, turns):
# Structural pinning: eviction can only ever touch `turns`.
msgs = [{"role":"system","content":
system + (("\n\n## SESSION MEMORY\n"+summary) if summary else "")}]
msgs += turns[-KEEP_TURNS*2:] # user+assistant pairs
return msgs
def rolling_summary(old_summary, evicted_turns):
# §10.2 strategy 3: compress evicted turns INTO STRUCTURE, not prose.
transcript = "\n".join(f"[{t['role']}] {t['content'][:600]}" for t in evicted_turns)
prompt = (f"Current session memory:\n{old_summary or '(none)'}\n\nNew events:\n{transcript}\n\n"
"Update the memory. Use EXACTLY this format:\n"
"GOAL:\nDECISIONS:\nFACTS:\nFILES_TOUCHED:\nCURRENT_STEP:\nOPEN_PROBLEMS:")
r = requests.post(API, json={"model":MODEL,"messages":[
{"role":"system","content":"You maintain an agent's session memory. Be terse."},
{"role":"user","content":prompt}], "temperature":0}, timeout=120)
return r.json()["choices"][0]["message"]["content"]
# ---------------- the loop (§10.1) -----------------------------------------
def call_model(msgs, tools=None):
body = {"model":MODEL,"messages":msgs,"temperature":0.2,"max_tokens":1500}
if tools: body["tools"] = tools
r = requests.post(API, json=body, timeout=600)
return r.json()["choices"][0]["message"]
def run_agent(task, max_steps=40):
turns, summary, log = [], "", []
turns.append({"role":"user","content":task})
for step in range(max_steps):
msgs = build_messages(SYSTEM, summary, turns)
m = call_model(msgs, tools=TOOLS)
# format-retry: one re-ask on malformed tool calls (§10.6)
for attempt in (0,1):
tc = m.get("tool_calls")
if tc and tc[0]["function"].get("name"): break
if m.get("content"): break # final answer path
m = call_model(msgs + [{"role":"assistant","content":"(invalid)"},
{"role":"user","content":"Call one of the tools, using proper JSON."}], tools=TOOLS)
turns.append({"role":"assistant","content":m.get("content") or "",
**({"tool_calls":m["tool_calls"]} if tc else {})})
if not tc:
return m["content"], summary # done
fn, args_raw = tc[0]["function"]["name"], tc[0]["function"].get("arguments","{}")
try: args = json.loads(args_raw or "{}")
except json.JSONDecodeError:
result = f"ERROR: malformed JSON arguments {args_raw!r}. Retry with valid JSON."
else:
result = TOOLS_IMPL.get(fn, lambda a: f"ERROR: unknown tool {fn}")(args)
result = result[:TOOL_RESULT_CAP] # hygiene, again, defensively
turns.append({"role":"tool","tool_call_id":tc[0]["id"],"content":result})
log.append((step, fn, result[:120]))
# window check: compress BEFORE the window, never after
if sum(len(t["content"]) for t in turns) > SUMMARIZE_AT:
cut = len(turns) - KEEP_TURNS*2
summary = rolling_summary(summary, turns[:cut])
turns = turns[cut:]
print(f"[ctx] compressed → summary {len(summary)} chars, window reset")
return "(max_steps reached)", summary
if __name__ == "__main__":
answer, memory = run_agent(
"In ./workspace: find the Python file with a syntax error, fix it, "
"and verify it compiles with python -m py_compile.")
print("FINAL ANSWER:\n", answer, "\n\nSESSION MEMORY:\n", memory)
Read the loop once more and notice what is absent: no framework imports, no hidden history, no re-sent schemas per turn (the server holds them), and every line that touches memory is labeled with the section that justifies it. This transparency is the point of §10.5 — when something misbehaves at 3 a.m. of a long task, every byte entering your 16 K window is visible in turns and summary, not buried under framework internals. Swap the TOOLS table for your own domain (web search, HTTP calls, a database client) and the memory discipline transfers unchanged.
11.5 The monitoring rig
#!/usr/bin/env python3
"""Sample GPU + system memory every second; correlate with agent steps."""
import subprocess, time, csv, datetime
def gpu():
out = subprocess.run(["nvidia-smi","--query-gpu=memory.used,utilization.gpu,temperature.gpu",
"--format=csv,noheader,nounits"],capture_output=True,text=True).stdout.strip()
return [int(x) for x in out.split(", ")] if out else [0,0,0]
def rss_gb():
out = subprocess.run(["free","-b"],capture_output=True,text=True).stdout.splitlines()[1]
return int(out.split()[2]) / 1e9 # used, GB
with open("vram_log.csv","w",newline="") as f:
w = csv.writer(f); w.writerow(["time","vram_mb","gpu_util","gpu_c","rss_gb"])
while True:
v,u,t = gpu(); w.writerow([datetime.datetime.now().isoformat(timespec="seconds"),v,u,t,rss_gb()])
f.flush(); time.sleep(1)
# Open vram_log.csv next to your agent run: watch KV growth as steps accrue
Run the monitor during your first few long agent sessions and read the graph afterward: VRAM should step up once at load, drift up slowly with context growth, and step down at session rotation. A sawtooth of reloads means KEEP_ALIVE or eviction is misconfigured; steady creep to the ceiling means your SUMMARIZE_AT threshold is too generous. This habit — instrument, then trust the instrumentation — is the final competency this paper teaches, because it converts every future surprise into a five-minute diagnosis.
(1) python kv_calc.py — confirm the core config fits. (2) Launch recipe A. (3) Start watch_vram.py. (4) Run nano_agent.py against a scratch folder with a deliberately broken Python file. (5) Verify: the agent fixes the file, VRAM stayed under ~7.3 GB, and the log shows no reload sawtooth. When all five check out, you have a working, instrumented, memory-disciplined agent on 8 GB — and every knob on it now maps to a chapter you can re-read.
11.6 Case study: one complex task, start to finish
To make the whole paper concrete, here is a realistic complex task run on exactly this hardware — the kind the introduction promised you would be able to manage. The task: “Take this messy 12-file Python project, add a CSV export feature with tests, and produce a change summary.” It involves reading unfamiliar code, multi-file edits, running test suites, and a written deliverable — genuinely agentic work, not a demo prompt. What follows is the memory-engineering view of the session: what the machine was doing at each phase, and which chapter’s discipline kept it healthy.
| Phase | What happened | Memory state & the rule that governed it |
|---|---|---|
| 0 · Setup (min 0) | Recipe A server launched (Qwen3-8B Q4_K_M, 16 K ctx, q8_0 KV); watch_vram.py started; agent process up | VRAM 7.1 GB steady. Sized by §2.5 Plan 3 — the formula predicted 7.2 GB before download |
| 1 · Reconnaissance (min 0–8) | Agent listed the project, read 6 source files (80-line cap each), traced the data flow. 14 tool calls, ~11 K tokens through the window | KV grew to ~1.4 GB of the budget. Tool-output hygiene (§10.3) kept each read ≤ ~1.2 K tokens; a naive agent reading full files would already be at 40 K |
| 2 · First compression (min 8) | History crossed SUMMARIZE_AT; rolling summary regenerated (goal / decisions / files touched / open problems) | Window reset to pinned system block + summary + last 12 turns. The §10.2 structure meant nothing procedural was lost — the agent re-stated its plan correctly after compression |
| 3 · Implementation (min 8–35) | Wrote the export module, edited 3 call sites, ran py_compile after each edit (one syntax error, self-caught via the error-as-prompt tool design) | VRAM flat at 7.1–7.2 GB; no sawtooth (KEEP_ALIVE=24h, mlock — §7.4). Generation at ~38 tok/s; each step’s prompt processing ~2–4 s at 10–14 K context (§7.3 math) |
| 4 · Test loop (min 35–52) | Wrote 5 pytest cases; two failed (real bugs in the new code); fixed both; suite green | The failure-mode that kills naive setups: pytest runs took 40–90 s each, longer than the default 5-minute keep-alive would tolerate on a full suite — the §10.6 stall never happened because the server never evicted |
| 5 · Hard step escalation (min 52–55) | One design question (“should export be streaming?”) was escalated to the Recipe B MoE server via llama-swap, answered once, swapped back | The two-model doctrine (§8.4) in action: 30 B-class judgment bought for 3 minutes of wall time, without disturbing the resident core model’s cache |
| 6 · Deliverable (min 55–61) | Agent wrote the change summary from its session memory (the structured rolling summary), not from raw history — and it was accurate | Final answer composed in-context; session ended with peak VRAM 7.3 GB, zero OOM events, zero reloads, RSS of agent process ~180 MB |
Four lessons fall out of the case study that the theory chapters imply but the run makes visceral. First, the cache never grew past ~2 GB — not because 16 K was the ceiling, but because the agent’s hygiene and compression kept the window half-empty at all times; the ceiling existed as slack, which is exactly how you want to operate a constrained machine. Second, the slow moments were tools, not tokens: 17 of the 61 minutes were pytest executions, where inference speed is irrelevant — the strongest practical argument for the §1.2 thesis that agents amortize modest tok/s. Third, escalation was cheap because it was designed (a bounded question, one call, swap back); an undisciplined setup would either never escalate or leave the big model resident and evict the core one. Fourth, the deliverable came from externalized memory — the structured summary — proving the §10.2 claim that a well-maintained summary is not a lossy compromise but the correct medium for long-horizon state. This is what “managing a complex task on 8 GB” looks like in practice: not heroic optimization, but a stack of small disciplines holding simultaneously.
Evidence & Reference
The raw experimental results collected in one place, the decision trees that operationalize them, the troubleshooting playbook, and the glossary — the part of the paper you will return to after the first reading.
12. Compiled Evidence: The Experimental Record
Every experiment this paper leans on, reproduced as tables — organized by question, with sources and confidence notes.
12.1 Question: “What does quantization cost?”
| Experiment | Model / setting | Key result | Source & confidence |
|---|---|---|---|
| GGUF perplexity ladder | Llama 3 8B, WikiText-2 | f16 6.2331 → Q8_0 6.2342 → Q6_K 6.2533 → Q5_K_M 6.2886 → Q4_K_M 6.3830 → Q4_0 6.7001 (full table §4.3) | llama.cpp repo evals via LessWrong — high |
| MMLU by quant method | Llama 3 8B Instruct, 0-shot | bf16 63.9 %; 8-bit ≈ 63.0–63.9 %; 4-bit 60.8–62.3 % (AWQ best of GPU-only, HQQ best overall) | LessWrong evaluation — high (single-run caveat) |
| Chain-of-thought degradation | Llama 3 8B Instruct, Minerva MATH Algebra | fp16 37.5 % → 4-bit 29.3–33.7 % (−4 to −8 pts) → 3-bit DNF; multi-step degrades ~2–3× faster than MMLU | LessWrong evaluation — high |
| Cross-method PPL, three scales | Llama-2 7B/13B/70B, WikiText-2 | All 8-bit within 0.04 PPL of FP; at 4-bit AWQ best (5.28 vs 5.18 FP on 7B); GPTQ/BNB/HQQ clustered 5.30–5.43 | HQQ paper (arXiv 2309.15531) — high |
| Blind perceptual testing | Multiple models, human raters | Q6_K and Q5_K “nearly indistinguishable from original”; Q4_K_M occasional texture loss; ≤Q3 visible | llama.cpp discussion #5962 — medium (subjective) |
| AWQ vs GPTQ speed & quality | 4-bit GPU inference | AWQ ≈ 2× GPTQ decode speed; AWQ retains 95–97 % of FP16 vs GPTQ 90–96 %; AWQ calibrates 5–10× faster | gingerlabs, Reddit format comparisons — medium |
| GPTQ code collapse | 4-bit code-generation tasks | Specific GPTQ 4-bit releases collapse on code (reported 46 % task failure) despite normal perplexity | Towards AI testing — medium (release-specific) |
| Quantization speed side-effect | 4-bit vs FP16, general | ~3.5–3.8× throughput, ~4× memory saving, 1–2 % PPL degradation (Q4_K_M/AWQ class) | presenc.ai 2026 synthesis — high (consistent with roofline §3.2) |
| QAT viability at 12 B | Gemma 3 12B QAT | Runs on 8 GB GPU (community-verified, ~2.5 K ctx default; more with q8_0 KV + FA) | Google QAT release + r/LocalLLaMA — high |
12.2 Question: “What does the KV cache really cost?”
| Experiment | Setting | Key result | Source & confidence |
|---|---|---|---|
| KV-quant quality benchmark | Qwen2.5-Coder-7B Q4_K_M, 8 K ctx, greedy, 12-prompt suite | f16 100 % similarity / 4899 MB; q8_0 81.6 % / −208 MB; q4_0 8.3 % / −320 MB — “the q4_0 cliff” (full table §5.3) | InventiveHQ Lab — high (controlled, code published) |
| KV-quant speed at long context | Nemotron-3-Nano-30B-A3B, 128 K ctx | q4_0 KV 92 % slower than f16 at 64 K+; q8_0 “the only KV quantization worth running” | DGX Spark benchmark (NVIDIA forum) — medium |
| TurboQuant extreme KV compression | llama.cpp branch testing | q4_0 KV saved 552 MiB (72 % reduction) with slot-position corruption bugs observed | llama.cpp discussion #20969 — medium |
| Fit-enabling KV quant | Command R 35B, 8 K ctx | q4_0 KV made a 35 B model fit a 24 GB P40 (proof the lever works; quality trade separate) | r/LocalLLaMA KV tests — medium |
| Apple Silicon KV quant | M-series, unified memory | Throughput hit < 10 % at q4_0 KV; q8_0 “the right default” | contracollective benchmark — medium |
| Flash Attention effects | Multi-request serving | FA dramatically shrinks inference graph under parallel load; prerequisite for KV quant | Ollama issue #12097 (4×H100 NVL) + docs — high |
12.3 Question: “How fast is hybrid/offload, really?”
| Experiment | Setting | Key result | Source & confidence |
|---|---|---|---|
| Layer-offload linearity | Systematic -ngl sweeps | ~50 % layers on GPU ≈ major speedup vs CPU-only; roughly linear to full-GPU; full-GPU ~3–8× a bad split | bmdpat -ngl guide + llama.cpp perf docs — high |
| RTX 3060 8 B Q4 throughput | 12 GB 3060, Qwen2/Llama 8B-class, multiple backends | 23–29 tok/s across backends; above chat-usability threshold | singhajit.com comparison — high (matches roofline) |
| Community 8 GB card results | GTX 1070/RTX 4060-class, coding models | 8 B Q4 workable for coding agent loops; 9 B ≈ the full-resident ceiling at ~6.8 GB | plainenglish.io 8 GB test + localllm.in guide — medium |
| Mult-GPU tuning headroom | 3090+3060+3060 llama.cpp rigs | 13.0 → 15.6 tok/s from thread/config tuning alone (+20 %); 48 → 82 tok/s after multi-card tuning | r/LocalLLaMA benchmark thread — medium |
| Unified memory on PCIe | GGML_CUDA_ENABLE_UNIFIED_MEMORY | Generally “very slow” on PCIe machines (page-fault storms); faster than bad manual splits only for specific huge-model/IQ-quant cases | llama.cpp PR #8035 + r/LocalLLaMA — medium |
| MoE hybrid on consumer parts | Qwen3-Coder-30B-A3B, 18 GB combined | 6+ tok/s documented with Dynamic 4-bit; expert tensors streamable from RAM with GPU prompt processing | Unsloth guide + HF MoE offload guide (Doctor-Shotgun) — high |
| MoE 90 K context | Qwen3-30B-A3B Q4_K_M, 24 GB card | 90 K ctx with 40/48 layers offloaded (routable-expert streaming confirmed at scale) | r/LocalLLaMA — medium |
| Graceful degradation past VRAM | Models deliberately exceeding 24 GB | llama.cpp/Ollama fall to single-digit tok/s but keep generating; server-class engines treat it as error | dev.to spill test — medium |
12.4 Question: “How much speedup from the tricks?”
| Experiment | Setting | Key result | Source & confidence |
|---|---|---|---|
| Speculative decoding launch results | llama.cpp server, various models | 25–60 % speedup (25–40 % typical across models) | llama.cpp speculative launch thread + docs — high |
| Speculative decoding survey | Draft-verify schemes incl. EAGLE-3 | Standard: 20–50 %; structured heads: up to 2–4× under favorable entropy; zero quality loss by construction | glukhov.org guide + arXiv Spec-Bench — high |
| Thread misconfiguration cost | CPU/hybrid inference | “Incorrect thread settings are the #1 cause of slow inference” — physical cores optimal, HT harmful | llama.cpp performance docs — high |
| Serving-stack RAM overhead | Ollama vs vLLM, idle serving | 1.8 GB (Ollama) vs 4.6 GB (vLLM) system RAM; Ollama CPU ~8 % idle | towardsai serving test — medium |
| GUI wrapper overhead | LM Studio vs raw llama.cpp (M3 Max) | LM Studio 29 % slower (38.2 vs 53.5 tok/s) | inventivehq comparison — medium (Apple platform) |
12.5 Question: “Can small models actually run agents?”
| Experiment | Setting | Key result | Source & confidence |
|---|---|---|---|
| Minimal agent, max scaffold discipline | mini-swe-agent (~100 lines) + frontier model | 65 % SWE-bench Verified; successors >74 % — scaffolding simplicity beats framework complexity | mini-swe-agent repo + SWE-bench leaderboard — high |
| Bash-only ReAct ceiling | SWE-bench Verified, no special scaffold | Simple ReAct loops match elaborate scaffolds on verified subset | SWE-bench Verified page — high |
| Tool calling in 4 B models | Linear probes on Gemma 3 4B/12B, Qwen3 4B, Llama 3.1 8B | Tool selection 100 % readable with simple probes after 10–20 examples — capability well-formed at 4 B | arXiv 2605.07990 — high |
| 8 B function-calling accuracy | BFCL v3/v4 | Llama 3.1 8B Instruct ~76.1 %; Qwen3 series top of open models (fp16; expect quantization delta per §4.5) | Berkeley BFCL leaderboard — high |
| 8 B trio under load | Qwen3-8B vs Llama 3.1 8B vs Ministral 8B, vLLM, 4–256 users | Qwen3-8B best quality-per-compute at low concurrency; Llama 3.1 most stable under load | TrueFoundry benchmark — medium |
| SLM agent recommendations | 2026 production survey | General local agent: Qwen3-4B or Phi-4-mini; tool-calling specialist: fine-tuned Qwen3-1.7B | zylos.ai SLM survey — medium |
| Context-window discipline | Production agent systems | Consensus recipe: summarize old turns, RAG for retrieval, truncate from front; context = working RAM, not storage | mem0 engineering + zylos session-lifecycle surveys — high (convergent) |
13. Decision Playbooks
The paper’s conclusions, compressed into trees and tables you can follow at decision time.
13.1 Tree 1 — choosing the configuration
13.2 Tree 2 — the OOM / slow-recovery path
13.3 The one-page cheat sheet
| If you remember nothing else | Rule |
|---|---|
| Usable VRAM | Plan against 6.5 GB comfortable / 7.4 GB tight tiers (§2.4) |
| Default stack | Qwen3-8B Q4_K_M · all layers in VRAM · 16 K ctx · q8_0 KV · FA · mlock (§11.3 A) |
| Quant floor for agents | Q4_K_M; prefer Q5_K_M/Q6_K when it fits (§4.5) |
| KV quant floor | q8_0 — never q4_0 for agents (§5.3) |
| Threads | Physical cores, not hyperthreads (§7.2) |
| Parallel slots | NUM_PARALLEL=1 on a single-user box (§7.4) |
| Context discipline | Pinned system block + rolling summary + last K turns (§10.2) |
| Tool hygiene | Cap every tool result; errors written as prompts (§10.3) |
| Speed sanity | ~30–42 tok/s core / 8–20 tok/s MoE hybrid; if far below, measure before optimizing (§7.1) |
| Upgrade money | Dual-channel RAM first, NVMe second, more VRAM third (§3.3) |
14. Troubleshooting: When VRAM Fights Back
Symptom → cause → fix, for the failures specific to 8 GB + 32 GB machines.
14.1 The five classic failures
Failure A — “CUDA error: out of memory” at model load
Diagnosis: weights + compute buffers exceeded free VRAM at startup — the load-time spike (prompt-processing scratch allocation) is larger than steady-state use, so a model that “should fit” can still die at boot. Fixes in order: (1) check what else holds VRAM (nvidia-smi — browsers and desktop compositing are the usual suspects, §3.4); (2) lower -ngl until the load line reports headroom; (3) reduce -c — the compute buffer scales with context; (4) drop one quant tier. The sweep script in §7.1 finds the boundary in minutes; the boundary you find today is only valid for today’s free VRAM.
Failure B — OOM or eviction mid-task (the agent-shaped failure)
Diagnosis: the KV cache grew with history until it hit the ceiling (§5.2); OR the serving layer evicted and reloaded the model on a tool timeout (the KEEP_ALIVE sawtooth, §10.6). Distinguish them from the logs: a reload logs load lines and takes ~10–60 s; true OOM logs allocation failures. Fixes: KEEP_ALIVE=24h/--mlock; then cap the agent’s working context (lower SUMMARIZE_AT); then q8_0 KV; then smaller -c. Re-run the §11.5 monitor to confirm the sawtooth is gone.
Failure C — Crippling slowness despite a good GPU
Diagnosis: almost always one of: silent CPU-only build (§7.1’s benchmark-integrity warning), wrong thread count (§7.2), accidental unified-memory mode (§6.4), or PCIe saturation from an aggressive split. Fix procedure: verify “backend: CUDA” in logs → verify the offload line (offloaded 36/36) → verify threads → run llama-bench and compare to the §12.3 range for your class. Each check is one command; do them in order and stop at the first failure.
Failure D — Outputs subtly wrong after a config change
Diagnosis: the q4_0-KV cliff (§5.3) is the canonical example — throughput looks fine while outputs are a different distribution entirely. Other members of the family: an IQ2/IQ3 quant on a sensitive model (§4.5), a truncated chat template (missing --jinja makes tool-calls incoherent), or silent history truncation (§5.5). Fix procedure: change one variable at a time back toward Recipe A and re-test with a fixed greedy prompt suite — the InventiveHQ methodology (temperature 0, same prompts, diff the outputs) is exactly right for this and takes ten minutes to replicate locally.
Failure E — RAM exhaustion / swap death on the hybrid path
Diagnosis: 32 GB feels vast until the OS, agent process, page cache for an 18 GB model file, and a browser compete for it; the machine swaps and everything becomes slow, not just inference. Fixes: keep total model file ≤ ~24–28 GB on this machine (leaving 4–8 GB of breathing room); close the browser during long runs; prefer --mlock with a model you have sized deliberately over accidental page-cache churn; check vmstat 1 for si/so activity — nonzero sustained values mean swapping, and no inference flag will fix that.
14.2 The diagnostic one-liners
# Who is eating VRAM right now? (processes + totals)
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
# Is the model actually offloaded? (look for the load-time line)
journalctl -u llama-server --since "1 hour ago" | grep -E "offloaded|layers|backend"
# Are we swapping? (si/so columns must be ~0 during inference)
vmstat 1 5
# Is the PCIe link running at full width? (x16 vs x8 changes offload math)
nvidia-smi -q | grep -A 3 "Link Current"
# Quick throughput ground truth, 30 seconds
./build/bin/llama-bench -m models/qwen3-8b-Q4_K_M.gguf -ngl 99 -p 512 -n 128 -r 2
Every fix in this chapter was derived by the same method: measure which pool (VRAM, RAM, or PCIe) was exhausted, then trace which component consumed it. When you meet a failure this chapter did not name, apply the method — the four-component model of §2.1 classifies every memory failure an 8 GB machine can have, and the §3.1 bandwidth hierarchy classifies every speed failure. There are no other categories.
15. Glossary
Plain-English definitions of every term this paper uses, in dependency order.
| Term | Definition |
|---|---|
| VRAM | Memory physically on the graphics card (8 GB here). Fast (240–450 GB/s) but small; everything the GPU computes from must live here or pay the PCIe toll. |
| Parameters / weights | The learned numbers that are the model. “8 B” = 8 billion parameters ≈ 16 GB in fp16, ≈ 5 GB at Q4_K_M. |
| Quantization | Storing weights (or caches) at lower numeric precision to save memory and bandwidth. Post-training quantization (PTQ) converts a trained model without retraining; quantization-aware training (QAT) trains the model to tolerate it. |
| GGUF / K-quants / I-quants | The llama.cpp file format; its mixed-precision block schemes (Q4_K_M, Q5_K_M, Q6_K — the quality-per-byte winners); and its importance-sampled ultra-low-bit schemes (IQ2/IQ3) for extreme compression. |
| AWQ / GPTQ / EXL2 | Calibration-based 4-bit GPU-only formats. AWQ is fastest and most quality-preserving; GPTQ older and slower; EXL2 offers tunable bit-rates. All require full VRAM residency. |
| Perplexity (PPL) | How surprised a model is by held-out text — lower is better. A sensitive, cheap proxy for model quality; imperfect predictor of agent skill. |
| Context window | The maximum number of tokens (prompt + generated) a model can attend to in one call. A memory budget, not a feature list: every token costs KV-cache bytes. |
| KV cache | Stored attention keys/values for all previous tokens, so each new token needn’t recompute them. Grows linearly with context; the hidden memory consumer; formula in §5.1. |
| GQA (grouped-query attention) | Architecture where query heads share a small group of key/value heads — shrinks the KV cache 2–8× with negligible quality cost. Universal in current models. |
| Flash Attention (FA) | A tiled attention implementation avoiding the full attention matrix; smaller memory footprint, prerequisite for KV quantization. Leave it on. |
| Offloading / -ngl | llama.cpp’s placement of the first N transformer layers on GPU, remainder in RAM, with the hidden state crossing PCIe at the boundary. The core enabler of hybrid inference. |
| mmap / mlock / page cache | OS mechanisms: map model files into memory on demand; pin them against eviction; and cache file pages in spare RAM. Together: fast loads and hot model swaps. |
| Unified memory | CUDA managed memory letting the GPU page from system RAM transparently. Excellent on Apple/Grace fabrics; usually slow across PCIe (§6.4). |
| MoE (mixture of experts) | Architecture storing many expert sub-networks but activating few per token (Qwen3-30B-A3B: 30 B stored, ~3 B active). Converts memory capacity into capability without proportional compute/bandwidth cost. |
| Speculative decoding | A tiny draft model proposes several tokens; the target model verifies them in one batch. Output distribution is mathematically unchanged; speed rises 20–60 %. |
| ReAct | The standard agent loop — Reason, Act (tool call), Observe (result), repeat until a final answer. The shape implemented in §11.4. |
| BFCL | Berkeley Function Calling Leaderboard — the reference benchmark for tool-calling accuracy. Llama 3.1 8B: ~76 %. |
| GBNF grammar | llama.cpp’s context-free grammar constraint on generation — makes malformed JSON tool calls impossible rather than merely unlikely. |
| RAG | Retrieval-augmented generation: store documents externally, embed them, inject only relevant chunks into context per step. Converts long-term memory from a context cost into a lookup. |
| Rolling summarization | Periodically compressing evicted history into a structured running summary (goal/decisions/facts/next) — the bounded-memory technique of §10.2. |
| tok/s · pp vs tg | Tokens per second; prompt-processing speed (ingesting input — the agent’s recurring tax) vs text-generation speed (producing output — the perceived liveliness). |
16. Sources and Further Reading
Everything cited in this paper, plus the canon worth reading next.
16.1 Primary sources cited
- LessWrong / NickyP — “Comparing Quantized Performance in Llama Models” (Jul 2024): the Llama 3 8B GGUF perplexity ladder, the MMLU-by-method table, and the Minerva MATH chain-of-thought degradation experiment (§4.3–4.5, §12.1). Reproduced from llama.cpp repository perplexity evaluations.
- InventiveHQ Lab — “KV-Cache Quantization: The q4_0 Cliff Your Logs Won’t Warn You About” (Jun 2026): controlled f16/q8_0/q4_0 KV benchmark with VRAM, speed, and output-similarity measurements (§5.3, §12.2).
- HQQ paper (arXiv:2309.15531): cross-method (BNB/GPTQ/AWQ/HQQ) perplexity comparison across Llama-2 7B/13B/70B (§12.1).
- Qwen3 Technical Report (arXiv:2505.09388): Qwen3 model family specs, dense + MoE configurations (§8.2).
- Berkeley Function Calling Leaderboard (BFCL v3/v4): tool-calling accuracy reference (§8.1).
- mini-swe-agent (GitHub, swe-agent project): minimal-scaffolding SWE-bench Verified results (§10.5, §12.5).
- llama.cpp repository & discussions: performance-tuning documentation (threads), speculative-decoding docs (25–60 %), PR #8035 (unified memory), discussion #5962 (blind quant testing), discussion #20969 (TurboQuant), CUDA scoreboard #15013 (§7.2, §7.5, §6.4, §12.1–12.3).
- Ollama documentation & issues: FAQ (Flash Attention, NUM_PARALLEL, KEEP_ALIVE defaults), issue #12097 (FA graph reduction under parallel load) (§5.4, §7.4, §9.3).
- Unsloth — “Qwen3-Coder: How to Run Locally”: 30B-A3B Dynamic 4-bit on 18 GB combined memory (§6.5, §12.3).
- Doctor-Shotgun — “Performant local MoE CPU inference” (Hugging Face blog): MoE expert-offload mechanics with GPU prompt processing (§6.5).
- arXiv 2605.07990 — “Tool Calling is Linearly Readable and Steerable”: probe-based tool-calling analysis of 4–12 B models (§8.1, §12.5).
- Community benchmarks: r/LocalLLaMA (8 GB VRAM threads, 3090+3060+3060 tuning, Qwen3-30B-A3B 90 K context, KV-quant memory tests, speculative-decoding launch results), singhajit.com (RTX 3060 backend comparison), TrueFoundry (Qwen3-8B/Llama-3.1-8B/Ministral-8B vLLM benchmark), towardsai (engine serving test: 1.8 vs 4.6 GB; GPTQ code collapse), gingerlabs (GGUF vs AWQ vs GPTQ), inventivehq (Ollama/LM Studio/llama.cpp comparison), bmdpat (-ngl guide), dev.to (spill test past 24 GB), ianlpaterson.com (CUDA benchmark integrity), DGX Spark KV-quant thread (NVIDIA forums), presenc.ai (quantization synthesis), localllm.in / willitrunai.com (VRAM requirement guides), Google Gemma 3 QAT release coverage (r/LocalLLaMA), mem0 engineering blog and zylos.ai surveys (context management), machinelearningmastery.com (context strategies taxonomy), glukhov.org (speculative decoding guide), arXiv Spec-Bench (2604.09557).
16.2 The canon, going deeper
- llama.cpp repository docs folder — the ground truth for every flag this paper uses; read server.md, speculative.md, and the performance-tuning page end-to-end once.
- The AWQ paper (arXiv:2306.00978) and GPTQ paper (arXiv:2210.17323) — what calibration-based quantization actually protects.
- FlashAttention-2 paper (arXiv:2307.08691) — why the tiled kernel changes the memory story.
- “Efficient Memory Management for LLM Serving” literature (PagedAttention, vLLM, arXiv:2309.06180) — the serving-side counterpart of this paper’s single-user discipline.
- ReAct paper (arXiv:2210.03629) and SWE-agent / SWE-bench line of work — the agent architecture this paper miniaturizes.
- SWE-bench Verified leaderboard — a live demonstration that scaffold minimalism is a legitimate strategy.
The 8 GB + 32 GB machine you own is, as of the evidence compiled here, a fully competent agent workstation — not because any single technique rescues it, but because a dozen small, measured, compounding decisions (a quant tier here, a cache precision there, a disciplined context window, a two-model doctrine) add up to exactly the capability the raw spec sheet seems to deny. The difference between a machine that “can’t run agents” and one that runs them for hours is now, for you, a solved engineering problem with a reference manual. Spend your bytes deliberately.
Maximum Capability from Minimum Silicon · a single-file research paper · compiled August 2026 · all measurements attributed inline; community figures are directional, reproduce them with the §7.1 discipline before betting on them.
Related Posts
The 8 GB Vanguard
A research paper and experiment archive: how people actually ran AI agents on 8 GB VRAM + 32 GB RAM machines.
Read more →GGUF vs EXL2 vs AWQ vs GPTQ
Master quantization formats for local AI: precision, speed, VRAM trade-offs.
Read more →GPU & CPU Inference Troubleshooting
Complete troubleshooting guide for inference issues — OOM, slow tok/s, KV cache pressure.
Read more →About the Author
Hussain Nazary is a software developer specializing in local AI deployment and the creator of GGUF Loader, an open-source tool for running GGUF models locally. This analysis is part of Local AI Zone's ongoing coverage of open-weight language models and practical deployment strategies.
Contact: GitHub | Consulting Services
Last Updated: August 25, 2026 | Version 1.0