00Die außergewöhnliche Funktionsweise in einem Satz
Ein 180-Milliarden-Modell läuft auf 62 GiB DDR4 und 22,5 GiB VRAM, weil llama.cpp hier nicht „Gewichte auf GPU“ tut, sondern das Modell als Hierarchie von Speicherschichten behandelt: Jede Tensorgruppe bekommt den Ort, an dem sie physikalisch günstig gelesen werden kann, und die Algorithmen um sie herum werden so gestellt, dass seltene Zugriffe gratis und häufige im Takt bleiben.
01Die Speicherkarte des Runners
Vier Orte, vier Taktarten, eine Token-Kette. Von links nach rechts: NVMe (Samsung 990 PRO), Host-RAM (62 GiB DDR4-3600, 38,7 GB/s), RTX 3060 12 GB (am Chipsatz-Slot PCIe 3.0 x4, am Desktop klebt) und RTX 3080 10 GB (CPU-direkt, PCIe 4.0 x16). Die Animation zeigt einen generierten Token: Router erwürfelt 10 Experten, die aus dem RAM hochstreamen, 8 Schichten wohnen dauerhaft auf der 3060, die N-Gram-Zeilen tröpfeln bedarfsweise von der SSD nach.
Ein Token, von innen gesehen
Jeder einzelne generierte Token durchläuft dieselbe Kette, ~48 Mal pro Durchlauf, und zwar so, dass im häufigsten Pfad keine Festplatte und nur wenig PCIe vorkommt:
02Das 82-GB-Budget
Was aus den 82,0 GB GGUF wird, gelesen aus den GGUF-Headern (scripts/gguf_meta.py). Verglichen mit dem dichten Bruder (Qwen3.8-27B, Port 18012), dessen Budget eine einzige Zahl ist: 15,4 GiB Gewichte, alles in VRAM.
# docker-compose.yml, Auszug (Port 18013, rootless, CDI) -ot "per_layer_token_embd\.weight=CPU,blk\.(4[0-7])\.ffn_.*_exps=CUDA0,ffn_.*_exps=CPU" -ts "0,1" --ctx-size 98304 -ctk q8_0 -ctv q8_0 -fa on -ngl 99 -sm layer --threads 8 --threads-batch 16 --load-mode mmap # mmap ist Pflicht, Warnung ignorieren -ub 2048 -b 4096 --spec-type ngram-map-k --spec-ngram-map-k-size-m 24
03Die Messkette: 15,6 → 22,1 t/s in fünf Schrauben
Kein Tuning-Voodoo, sondern eine Kette aus sechs isolierten Messungen an aufeinanderfolgenden Tagen, jede mit identischem Prompt (bench.py --n-predict 128, tg bei 512 / 8k). Zwischenstände bei JEDEC-2133-RAM, XMP erst später dazu geschaltet.
8 statt 16
16 SMT-Threads kosten 15 % (16,9 → 13,1). Die SMT-Siblings spin-warten aufeinander. 6 Threads sind leicht schlechter als 8. Physik cores = Optimum, unabhängig bestätigt (Codacus auf 5600X: 12 → 6 war dessen größter einzelner Gewinn).
Der Prefill-Knopf
Die RAM-Experten werden pro Ubatch einmal auf die 3080 kopiert. 512 → 2048 hebt Prefill von 87 → 222 t/s. 4096 passt nicht: 4,6 GiB Compute-Buffer, OOM (Sweep K).
2133 → 3600 MT/s
Decode ist Bandbreite: 24,9 → 38,7 GB/s (+55 %), 78 ns, Infinity Fabric 1:1. +25 % auf tg, Prefill PCIe-seitig unberührt. Die DIMMs liefen seit dem ersten Tag im JEDEC-Default, niemand hatte sie je angefasst.
Kontextfenster: warum 98k und nicht 128k
Der KV-Cache ist hier klein (12 QSA-Schichten × 2 KV-Heads ≈ 0,8 GiB pro 64k). Was mit dem Fenster wächst, ist der Compute-Buffer auf der 3080, und er wächst auch mit -ub:
| ctx | -ub | 3080 frei | tg @512 | pp @8k | Needle |
|---|---|---|---|---|---|
| 65.536 | 2048 | 2.191 MiB | 19,3 | 222 | ok @ 8k/32k/60k |
| 98.304 (shipped) | 2048 | 799 MiB | 17,5 → 21,8 (XMP) | 210 | : |
| 131.072 | 2048 | OOM: 3.956 MiB Compute-Buffer | |||
| 131.072 | 1024 | 1.383 MiB | 17,2 | 143 (Prefill halbiert) | ok @ 117k, 90 % |
Dazu zwei Effekte, die nirgends dokumentiert waren: Die Decode-Geschwindigkeit fällt mit dem allokierten Fenster, nicht nur dem gefüllten (leeres 128k: −11 % gegenüber 64k), und 128k erzwingt -ub 1024, was Prefill halbiert: ein volles 128k-Fenster braucht 26 Minuten. Shipped deshalb 98.304 mit -ub 2048. Kontextqualität trotz allem verifiziert: Needle bis 117k Tokens, auch bei 90 % Fensterfüllung, grün.
04Die Klippe: N-Gram-Spekulation zwischen +58 % und −60 %
Spekulative Dekodierung ohne Draft-Modell: Der Drafter schlägt die letzten N Tokens im Kontext nach und replayt, was darauf folgte. Kostet null VRAM, lebt im Host-RAM. Auf einem bandbreiten-limitierten MoE ist ein Verify-Pfad über 24 Tokens fast so billig wie einer, daher übersetzt sich die Akzeptanzrate fast direkt in Tempo. Aber: llama.cpp hat eine Op-Offload-Schwelle bei 32 Tokens.
Was passiert
Draft ≤ 24: Der Verify-Pfad bleibt unter der Op-Offload-Schwelle und rechnet auf der CPU, mit den Experten, die dort schon liegen. Ein 24-Token-Draft kostet fast genauso viel wie ein Token. Kopierarbeit 22,1 → 35,0 t/s (+58 %), Akzeptanz 92 %, Ausgabe byte-identisch in jedem Lauf.
Was passiert
Ab 32 Tokens im Batch schaltet llama.cpp auf GPU-Op-Offload, für Prefill genau richtig, für einen Verify über 40 RAM-Expertenschichten genau falsch: Die Experten wandern über PCIe. m32: 8,9 t/s, m48 (llama.cpp-Default): 9,6. Akzeptanzrate ist unverändert hoch, das ist keine Qualität-, sondern eine Infrastruktur-Klippe.
Der Default size-m 48 hätte den Runner also halbiert. Dieselbe Klippe erklärte und reparierte am selben Tag den Schwester-Runner qwen36-a3b, wo n-gram einen Monat lang als „schadet mit CPU-Experten“ abgeschrieben war (31 → 97 t/s mit m24 auf der Kopier-Probe). Der dichte 27B-Runner hat keine RAM-Experten und kennt die Klippe nicht, dort fährt size-m 96: 21 → 140,5 t/s. Kurze Suche (size-n 6) ist die zweite Falle: Drafts auf 6-Tofall-Zufällen kosten 10-15 % auf freier Prosa. Prosa selbst bleibt bei Flash-Next überall unverändert (~21,4 t/s), der Drafter schlägt schlicht nichts vor, wo nichts zu kopieren ist.
05Die 27-GiB-Nachschlage: Lazy-Mode und die Ubatch-Fixkosten
Community-Befund (llama.cpp #28355): seit --lazy-mode auto Default ist, lesen Tensoren über 4 GiB zeilenweise on demand. Genau die N-Gram-Tabelle. Empfehlung dort: Tabelle komplett in RAM. Auf dieser Box nachgemessen, Seite an Seite mit Page-Faults und SSD-Reads aus /proc:
| Prompt | Tabellenzeilen | pp t/s | majflt | SSD read |
|---|---|---|---|---|
| Realtext 7,3k, erster Kontakt | kalt | 89,7 | 414k | 1,8 GiB |
| derselbe Text direkt danach | warm | 114,8 | 0 | 0 |
| derselbe Text 10 min später | teils verdrängt | 106,4 | 74k | 288 MiB |
| Synthetik 5,1k (bench.py-Schleife) | kalt → warm | 162 → 199,5 | 118k → 0 | 498 MiB → 0 |
Nicht übertragbar aus der Community, festgehalten damit es niemand nochmal versucht: ryan4yin's -b 6144 -ub 6144 (4090, 96 GB, alles resident) OOMt hier schon bei -ub 4096 am Compute-Buffer der 3080. Und der Rest der Lücke Synthetik↔Realtext (199 vs 115 t/s, warm gegen warm) ist nicht Disk-IO, sondern Inhaltsabhängigkeit der Experten-Routing: Eine 20-Wort-Schleife trifft wenige Experten, echter Text viele. „Plan with 135“ aus dem README ist für Realtext eher 105-115.
06Fallen, die dieses Setup mitbringt
Slot-Restore ist append-only
Fortsetzen nach Restore: 10 Tokens verarbeitet. Aber: eine Abweichung um ein einziges Token (HistoryEdit, gelöschte Antwort) = voller Re-Prefill, 12.023 Tokens. Im Prozess hält llama.cpp Recurrent-State-Checkpoints und kann den DeltaNet-Zustand zurückrollen, das Slot-File enthält keine. Eine Chat-Harness, die Assistent- und Nutzer-Turn anhängt, ist sicher. Eine, die editiert, nicht.
mtmd ignoriert --main-gpu
Der Vision-Encoder (862 MiB) landet unabhängig vom Flag auf CUDA0, der vollen Karte. Deshalb weichen 9 → 8 residente Expert-Schichten auf der 3060; messbare Kosten: keine (19,3 → 19,3, Sweep H vs shipped).
Livelock beobachtet, nicht reproduziert
Mitten in der Generierung stand der Server nach n_gen = 353 (58k-Kontext): 10 Minuten ~1400 % CPU, n_decoded eingefroren, GPU 11 %, null Page Faults. /health antwortete weiter „ok“, der Healthcheck blieb grün, restart: unless-stopped griff nie; SIGTERM ignoriert, erst SIGKILL half. Ein Watcher müsste n_decoded aus /slots beobachten. Seit Rootless läuft der Prozess als data, eu-stack -p geht ohne sudo.
CDI veraltet beim Treiber-Update
Die CDI-Spec pins versionierte .so-Pfade; nach einem NVIDIA-Update sieht kein rootless Container eine GPU. Der User-Daemon regeneriert sie bei jedem Start via systemd-Override. Erste Stelle, wenn die GPU fehlt. Und: Port-Driver ist slirp4netns, nicht builtin, weil builtin die Client-IP durch die RootlessKit-Gateway-Adresse ersetzt und die Client-IP-Allow-Liste des TLS-Proxys aushebelt.
Sampler-Fallen geerbt vom 27B
--min-p 0 muss explizit gesetzt werden (llama.cpp-Default 0,05 clipst still den Schwanz, den die Model Card will). reasoning_effort akzeptiert nur xhigh/medium/low/none, alles andere (inkl. „minimal“) ist HTTP 500. Und xhigh ist keine Zeitfresser-Stufe: gemacht dachte xhigh am kürzesten (375 Tokens thinking vs 551 bei medium). Zitiertes Template-Markup (erklärte Chat-Templates) bricht außerdem den Stream-Parser, Phantom-Tool-Calls; Gegenprobe: N-Gram-Drafting beschleunigt so einen Runaway auf 216 t/s, verursacht ihn aber nicht.
07Tuning-Session in Tagen (Agent-Logbook 1352-1357)
- 2026-09-07 19:38 · XMP aktiviert (1352): Lesebandbreite 24,9 → 38,7 GB/s, FCLK 1:1, 78 ns. Alle alten Nummern der Box neu bewertet.
- 2026-09-07 19:38 · Runner online (1353): Flash-Next Port 18013, Mainline b10830 ohne Fork. Messkette 15,6 → 19,3 (Platzierung) → 17,5 (98k) → 21,8 (XMP) → 22,1 (EPP). Verworfen: nmatteo Q3_PLE (Fork-Patchserie für die SSD-Tabelle), GenerelSchwerz Expert-Cache-Fork (50 GiB gepinnt = der Swap-Page-Cache; Audit: sauber, aber 19k-Zeilen Diff), MTP (<1 t/s auf DDR4).
- 2026-09-07 20:16 · Klippe verallgemeinert (1354): qwen36-a3b von n-gram-„schadet“ auf m24 umgestellt: 31 → 97 t/s Kopierarbeit. Dieselbe Op-Offload-Klippe.
- 2026-09-07 20:32 · Hermes-Provider (1355): Erste echte Turns, Prefill-Realität 174 s für den System-Prompt, danach Slot-Cache.
- 2026-09-07 20:41 · OpenCode + DeerFlow (1356): als local Provider eingetragen, request_timeout 900 wegen ~100 t/s Realtext-Prefill.
- 2026-09-07 21:54 · Kompressionsschleife (1357): Modellwechsel schrieb auxiliary.* auf auto → 70k-Kompression auf dem Flash-Runner, 600 s Timeout, Cloud-Fallback. Fix: Aux-Blöcke zurück auf swap-gemma4-12b-qat. Lehre: Auxiliary-Provider nach jedem Modellwechsel prüfen.
- 2026-09-08 · Rootless-Migration: CDI statt Legacy-Hook, published Port statt host-netns, IPC-Zeilen raus. 18013 von aussen erreichbar, Prozess gehört data.
- 2026-09-09 · Lazy-Mode-Probe: 22 % / nichts; Fixkosten pro Ubatch identifiziert; --load-mode none durchgemessen und verworfen.
08Fazit
Der außergewöhnliche Teil dieses Runners ist nicht eine einzelne Flag-Kombination, sondern dass llama.cpp die Speicherklassen eines MoE-Modells getrennt adressierbar macht: Nachschlage-Tabelle auf SSD, kalte Experten im RAM, warme Experten in zweckentfremdeter Zweitkarten-VRAM, Rechenkern alleinbestückt auf einer Karte, Spekulation auf der CPU unterhalb der Offload-Schwelle. Ein 180-Milliarden-Modell bei 90 % der Decode-Geschwindigkeit des dichten 27B auf derselben Hardware (21,8 vs 21 t/s), mit 98k Fenster, Vision und 35 t/s auf genau der Arbeit, die ein Agent-Loop macht. Und eine Dokumentationskultur, in der jede Zahl ein Logfile hat: Ohne Sweep-Logs, Needle-Probes und die Op-Offload-Anekdote stünde hier „irgendwas mit 20 t/s“.