Measured two-profile local LLM stack (llama.cpp, 4070S + 2060)
  • Python 75.8%
  • Shell 15.9%
  • C++ 8.3%
Find a file
Sponge 32fe74376b Merge feature/os-agnostic into develop
Runs on Windows as well as Linux by detecting capabilities rather than OS
names: /proc vs lsof for file holders, ss vs netstat for port owners, kill vs
taskkill, systemd units vs a generated launcher, and the signal set and spawn
flags each platform actually has.

tests/test_os_agnostic.py (12 checks) exercises both branches wherever it
runs, and was mutation-tested against the unanchored-port bug. Linux is
unaffected: 12/12, 33/33 gitignore, 76/76 concurrency.
2026-09-13 13:40:48 -07:00
.serena back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
bench acceptance: spawn the holder probe portably 2026-09-13 13:37:25 -07:00
bin test the OS-specific branches, and fix the grep -P the test found 2026-09-13 13:36:36 -07:00
data Mass Data Save 2026-09-04 16:30:36 -07:00
docs Raise the 30B long window to 327680 and document the K/V split 2026-09-01 12:02:55 -07:00
jobs/queue Mass Data Save 2026-09-04 16:30:36 -07:00
models back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
src back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
systemd back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
tests test the OS-specific branches, and fix the grep -P the test found 2026-09-13 13:36:36 -07:00
.gitignore gitignore: cover this project's own artifacts, and test the rules 2026-09-13 13:10:59 -07:00
AGENTS.md run on Windows as well as Linux, by detecting capabilities not OS names 2026-09-13 13:32:10 -07:00
CLAUDE.md Raise the 30B long window to 327680 and document the K/V split 2026-09-01 12:02:55 -07:00
EXPERT-PRUNING.md document why expert pruning is the wrong lever for this stack 2026-09-01 04:24:56 -07:00
FAST.md Correct the 141 tok/s claim to the 106 that reproduces 2026-08-30 17:17:11 -07:00
GGSQUASH.md Add ggsquash: custom GGUF->GGUF optimizer with lossless QKV fusion 2026-08-30 09:56:23 -07:00
LOCAL-MODELS.md back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
QUANTIZING.md back up the source a clone actually needs to rebuild this stack 2026-09-13 13:06:31 -07:00
README.md README: state the platform story and how to verify a checkout 2026-09-13 13:38:05 -07:00
SPONGEQUANT.md spongequant: design a variable-group KV quant, then falsify it in step 1 2026-08-31 23:51:59 -07:00
TURBOQUANT.md docs: propagate the 327k window into the profile tables 2026-09-01 12:06:20 -07:00

AbliteratedSuperModelRunner

A measured, two-profile local LLM stack for **12GB RTX 4070 Super + 6GB RTX 2060

  • 64GB RAM + NVMe**, built on llama.cpp. Everything lives on the NVMe.

Design goal: a 500k-1M token window, at usable speed, on abliterated (refusal-removed) coder models, without permanently monopolising the GPU.

Every number below was measured on this box, not estimated. Re-run bin/bench-stack to reproduce.

Install / restore

bin/install              # idempotent: build, fetch models, install services
bin/install --no-models  # skip the model download

A clone is deliberately not self-contained: the llama.cpp fork (~217 MB) and the four GGUFs (~36 GB) are not in git. bin/install rebuilds the fork from the pin in src/LLAMA-CPP-PIN.md plus the patches beside it, and bin/quantize-models rebuilds the weights (see QUANTIZING.md).

Platform. Linux is the primary target and the only one the numbers below were measured on. Windows works too, via Git Bash / MSYS2 / WSL for the shell scripts; bin/llmd itself is plain Python, run it as python bin\llmd since it has no .py suffix to associate. Where systemd is absent, bin/install writes bin/llmd-start (and a .cmd that does exactly that) instead of units. The per-OS mechanisms and the traps involved are tabulated in AGENTS.md under "Running on Windows". Verify a checkout with:

python3 tests/test_os_agnostic.py   # 12 checks, both OS branches
bash    tests/test_gitignore.sh     # 33 checks, artifacts vs source

Quick start

bin/llm day       # 9B, 524k window, 4070 does the work
bin/llm night     # 30B coder, 327k window
bin/llm cook      # night profile + overnight job queue
bin/llm off       # stop everything, hand the GPUs back
bin/llm status    # what is running, live window, VRAM, snapshots

OpenAI-compatible endpoints:

Profile URL Model name
day http://127.0.0.1:8090/v1 ornith-9b
night http://127.0.0.1:8091/v1 qwen-coder-30b-ablit

(Ports are 8090/8091 because 8080 was already taken on this machine.)

Models (both abliterated, as requested)

Role Model Quant Size
Day Ornith-1.5-9B-heretic i1-IQ3_M 4.2 GB
Night Huihui-Qwen3-Coder-30B-A3B-Instruct-abliterated i1-IQ3_M 13 GB

Verified working: the 9B emits clean tool-call JSON, so agent loops are intact after abliteration.

The 9B is a thinking model. It writes into reasoning_content before content. With a small max_tokens you get an empty reply and it looks broken. Either allow ~500+ tokens, or disable thinking:

{"chat_template_kwargs": {"enable_thinking": false}}

Measured results

The finding that matters most

Grok's plan put the KV cache in system RAM (--no-kv-offload) to protect game VRAM. That is catastrophic for this workload:

KV cache location Generation speed
System RAM (--no-kv-offload) 1.03 tok/s
On the GPU 91.6 tok/s

~90x. At 431k tokens the RAM-cache config generated at 1.03 tok/s: a 40-token answer took 38 seconds after a 13-minute prefill. This stack never uses --no-kv-offload. Overflow is handled by CUDA unified memory instead, which pages on demand rather than routing every token over PCIe.

Windows (day profile, 4070 compute + KV, 2060 holding weights)

Preset Window Rope Generation Prefill
fast 262,144 native 32-91 tok/s ~2600-3200 tok/s
long (default) 524,288 YaRN 2x 62 tok/s ~1000-3100 tok/s
max 1,048,576 YaRN 4x 62 tok/s degraded quality

Your 40+ tok/s target is met at 500k. Your 800 tok/s figure is met by prefill, which peaked at 2963 tok/s. Generation is a different quantity and is bandwidth-bound: 800 tok/s single-stream is not reachable on this hardware at any context.

Generation speed is roughly flat with window size, but falls with actual depth: 36 tok/s at 120k tokens deep, ~10 tok/s at 431k.

Retrieval quality (bin/needle)

Window size means nothing if the model cannot find anything in it.

Depth of prompt Needle found
151k tokens, depths 25/50/90% 3/3 PASS
120k tokens, depths 25/75% 2/2 PASS
431k tokens (YaRN 2x), depth 50% PASS
431k tokens (YaRN 2x), depth 90% FAIL

So: ~500k is real and usable, with a caveat. Retrieval is solid up to at least mid-depth at 431k, but degrades at extreme depth under rope scaling. The 1M max preset loads and runs, but treat it as a ceiling, not a place to hide important facts. Below 262k (fast) there is no scaling at all and quality is best.

Night profile, 30B MoE

Config Result
128k, experts in system RAM 17.1 tok/s
262k, experts in system RAM 11.8 tok/s
experts on the 2060 crashes

The 2060 is Turing (sm_75) and lacks working kernels for this model's IQ3 expert tensors, so ggml aborts at load. EXPERTS=cpu is the default for that reason. The 2060 still contributes to the day profile, where it holds ordinary weight layers.

The 30B is dense-KV (51 KiB/token vs the 9B's 17 KiB/token), so it does not reach 500k. Use the 9B for huge windows and the 30B for hard code.

Why the 9B can do 500k at all

bin/kv-calc reads the GGUF and computes this:

architecture      qwen35
attention         hybrid (1 full-attention layer every 4; 8/32 pay KV,
                  the rest keep a fixed-size recurrent state)
KV per token      17.00 KiB

Only 8 of 32 layers pay per-token KV cost. That is what makes a 500k window affordable here, and why the same trick fails on the dense 30B.

The role each piece plays

4070 Super  workhorse: all compute, attention, and the KV cache
2060        holding card: ~3/12 of the weight layers (-ts 9,3),
            freeing 4070 VRAM for a bigger window
system RAM  CUDA unified-memory spill ("PVRAM") + prefix cache
NVMe        model weights (mmap) + hot KV snapshots
HDD         cold KV archive

The 2060 cannot be "VRAM only": whichever card holds a tensor runs that tensor's math. Giving it weight layers while the 4070 keeps the latency-critical KV path is the closest achievable version of that idea, and it is what the -ts 9,3 split does.

PVRAM

GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 is the real mechanism. It uses cudaMallocManaged, so an allocation larger than free VRAM pages into system RAM via the GPU's hardware pager instead of failing. Measured effect: 1M context went from cudaMalloc failed: out of memory to loading and running at 62 tok/s.

A user-space "file on the SSD as VRAM" would be strictly worse: every access would trap to a filesystem read. The SSD's correct job is snapshots, not live tensors.

KV snapshots: pay the prefill once

bin/kvcache adds a tiered cache over llama-server's slot API.

bin/kvcache save    repo-name        # after a big ingest
bin/kvcache restore repo-name        # seconds, not minutes
bin/kvcache list                     # tiers, sizes, last use
bin/kvcache evict                    # enforce the NVMe budget
bin/kvcache promote repo-name        # HDD -> NVMe

Measured on a 93k-token prefill:

Operation Time
Cold prefill 35 s
Snapshot save (1.56 GB) 3.2 s
Snapshot restore 0.5 s
Demote to HDD / promote back 0.3 s

Tiering: hot on NVMe with a 60 GB LRU budget (KV_HOT_BUDGET_GB), cold on the external HDD (KV_COLD_DIR, default /run/media/sponge/HDD1/kv-archive).

Honest limitation of disk restore

Restore genuinely reloads the tokens (n_restored: 93049 in 0.5s), but llama-server does not then treat them as a reusable prefix: the next matching request still re-prefills, reporting cached_tokens: 0. Restore repopulates slot state, not the prompt-cache index.

What does work, and is what actually matters for agent loops:

Scenario Result
Repeat request, same server 35 s -> 0.3 s, cached=93043
Append a turn to the conversation 0.3 s, cached=93049
Restore from disk, then request re-prefills, cached=0

So a long-running agent conversation is very fast, because in-memory prefix caching does the heavy lifting. Cross-restart prefill reuse is the one piece that llama.cpp does not currently deliver. --cache-reuse is also auto-disabled by this hybrid model ("cache_reuse is not supported by this context").

Slot context is capped by GGUF metadata

llama-server silently clamps every slot to the model's declared n_ctx_train (see n_ctx_slot() in tools/server/server-context.cpp). Passing -c 655360 plus YaRN was not enough: slots stayed at 262144 and big prompts were rejected with "exceeds the available context size".

bin/gguf-set-ctx patches that one metadata field in place (instant, no re-download, reversible). bin/llm-day calls it automatically to match the selected preset.

Gaming

bin/llm off          # cleanest: both GPUs free
GAME=1 bin/llm-day   # 9B moves to the 2060, 4070 untouched

GAME=1 runs a 131k window on the 2060 alone. Note the 2060 cannot hold the larger Q4_K_M quant plus compute buffers, which is why the day model is IQ3_M.

Optimizing a GGUF

bin/ggsquash rewrites a model into a faster, smaller, drop-in GGUF by fusing Q/K/V into one matmul and shrinking oversized tensors. Measured +3.1% generation with correctness held. See GGSQUASH.md for the full method, numbers, and limits.

Layout

bin/      llm, install, llm-day, llm-night, llm-status, cook, kvcache, kv,
        kv-calc, ctx-probe, needle, bench-stack, bench-experts, gguf-set-ctx
models/ GGUF weights
kv-slots/ hot KV snapshots + index.json
jobs/   queue/ and done/ for overnight work
logs/     build, server, probe, and benchmark logs
systemd/  unit files (bin/install copies these into ~/.config/systemd/user)
src/    llama.cpp source + CUDA build (sm_75 + sm_89)

Overnight cooking

cp jobs/examples/sample-jobs.jsonl jobs/queue/
bin/llm cook

bin/cook takes .jsonl (one chat body per line) or .md/.txt (whole file as one prompt), writes results to jobs/done/, and snapshots the slot after each job so a 4am crash does not replay a long prefill.

Tools worth knowing

bin/kv-calc models/<model>.gguf      # KV cost per token, feasibility per window
bin/ctx-probe day                    # find the real context ceiling
bin/needle --tokens 430000           # retrieval quality at depth
bin/bench-stack                      # reproduce every number above

Practical recommendation

  • Default to WINDOW=long (524k). It hits 40+ tok/s and retrieval holds.
  • Drop to WINDOW=fast (262k) when you want best quality and no rope scaling.
  • Use WINDOW=max (1M) only as a ceiling for bulk ingest.
  • For repo-scale work, prefer agent file-reading tools over stuffing 500k tokens. A long window is a fallback, not a substitute for retrieval.