Tech Paper · GPU-Host · llama.cpp b10830 · Mainline ohne Fork

Runner Anatomie
Ein 180-Milliarden-Modell auf 2 mid-Range-Grafikkarten

Qwen3.8-Flash-Next ist eine 82-GB-MoE-Maschine mit 512 Experten pro Schicht und einer 27-GiB-Nachschlagetabelle, die nie im RAM stehen muss. Dieser Paper zeigt, wie der llama.cpp-Runner sie zerlegt: SSD als Speicherschicht, eine 3060 als reine Expert-Ablage, eine 3080 als alleiniger Rechenkern, und eine N-Gram-Spekulation, die an einer 32-Token-Klippe entweder +58 % bringt oder halbiert. Alles gemessen, nichts angenommen.

Quelle: llama_qwen3.8_flash_exp + llama_qwen3.8_exp (READMEs, Bench-Logs) Tuning-Session: Agent-Logbook 1352-1357, 2026-09-07/09 Status: produktiv, Hermes-Provider
▼ scrollen

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.

0Mrd.
Parameter gesamt
0Mrd.
aktiv pro Token
0
Experten je Schicht
0GiB
N-Gram-Tabelle, lebt auf SSD
0t/s
Decode (Prosa, XMP)
0t/s
Decode (Kopierarbeit, N-Gram)
Warum das überhaupt geht Flash-Next ist die Open-Weight-Preview der Qwen4-Architektur (qwen4exp, seit b10830 mainline). Pro Token sind nur ~6 Mrd. Parameter aktiv; die 51B schwere per_layer_token_embd-N-Gram-Tabelle ist gar kein Gewicht, sondern eine Hash-Nachschlage: Aus den letzten 2-3 Tokens werden Zeilen Adressen, pro Token 16 Zeilen. llama.cpp markiert sie TENSOR_READ_LAZY und liest Zeilen direkt aus der mmap-Datei. Damit ist die eigentliche Rechnung: 6 Mrd. aktive Parameter + Streambandbreite, nicht Modellgröße.

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.

NVMe · Samsung 990 PRO Moddeldateien, 82,0 GB GGUF (3 Shards) N-Gram-Tabelle per_layer_token_embd 26,8 GiB · IQ4_NL TENSOR_READ_LAZY: Zeilen werden on demand gepaged, nie resident Host-RAM · 62 GiB DDR4 4 × 16 GB G.Skill, XMP 3600 · 38,7 GB/s · 78 ns Routed Experts · Schicht 0-39 (40 von 48) ~38 GiB, mmap · ~0,75 GiB pro Token im Stream Page Cache hält 48-53 GiB warm; Reload nach Orchestrator-Swap deshalb: 23 s statt Minuten. RTX 3060 · CUDA0 PCIe 3.0 x4 (Chipsatz) · teilt den KDE-Desktop 8 residente Expert-Schichten blk40-47 · 8 × 0,94 GiB Vision-Encoder 862 MiB (mtmd, ignoriert --main-gpu, landet auf CUDA0) frei nach Start: 686 MiB = knappster Rand der Box (Desktop driftet ~200 MiB) RTX 3080 · CUDA1 PCIe 4.0 x16 · dediziert · alleiniger Rechenkern 48 Layer, Rechnen Sparse Attention, DeltaNet, Shared Experts, Router, Embeddings, lm_head, KV frei nach Start: 2.191 MiB Compute-Buffer wächst mit ctx und -ub mmap page-in einmal laden, dann Ruhe PCIe 4.0 N-Gram-Zeile (SSD) Experte aus RAM residente Schicht (3060) Layer-Berechnung (3080) Token 0 · Router: 10 von 512 Experten −ts 0,1: die 3080 besitzt alle Layer · kein Cross-GPU-Sync im Decode
Warum -ts 0,1 und nicht brav aufteilen Mit -ts 1,1 (beide Karten im Layer-Split) landete jede Anfrage auf beiden Karten und damit im Cross-GPU-Sync über den langsamen Chipsatz-Slot. Die Platzierung -ts 0,1 gibt der 3080 alle 48 Layer: Attention, DeltaNet, Shared Experts, Router, Embeddings, lm_head und KV-Cache. Die 3060 ist damit kein Rechenknoten mehr, sondern reine Expert-Ablage, 8 Schichten, einmal hochgeladen, niemals wieder in Bewegung. Nur diese Trennung machte den Unterschied 15,6 → 16,9 t/s, bevor überhaupt Experten dazu kamen. Der Fehler davor war nicht die Hardware, sondern dass llama.cpps Defaults die 26,8 GiB N-Gram-Tabelle einfach auf CUDA0 legen wollten: allocating 27016.28 MiB on device 0: cudaMalloc failed: out of memory.

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:

Schritt 1
Token & Kontext
Letzte 2-3 Tokens werden gehasht: 16 Zeilen pro Token aus der N-Gram-Tabelle, direkt aus der mmap-Datei auf der SSD.
Schritt 2
Layer-Kern · 3080
12 × Sparse Attention + 36 × Gated DeltaNet rechnen durchgehend auf der 3080, KV-Cache nur in den 12 QSA-Schichten.
Schritt 3
Router
Pro MoE-Schicht wählt der Router 10 von 512 routed Experten plus 1 Shared Expert. 48 Router, 480 Würfe pro Token.
Schritt 4
Expert-Stream · RAM → 3080
Die 40 RAM-Experten-Schichten streamen: ~0,75 GiB pro Token über DDR und PCIe 4.0. Decode ist hier eine Bandbreitenzahl.
Schritt 5
Resident-Boost · 3060
8 Schichten (blk40-47) wohnen in VRAM und streamen nicht mit; das sind +3,7 t/s aus dem Sweep, ~0,4 t/s pro Schicht.
Schritt 6
Sampling & Draft
lm_head, Sampling (temp 1,0 / top-p 0,95 / top-k 20 / min-p 0), und der N-Gram-Drafter legt bis zu 24 Tokens vor, die in einem Verify-Pfad unter der 32-Token-Klippe bestätigt werden.

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.

N-Gram-Tabelle · SSD
26,8 GiB
Routed Experts · RAM
~38 GiB
Routed Experts · 3060
~7,5 GiB
Vision-Encoder · 3060
862 MiB
Layer + KV · 3080
~7,7 GiB
Frei · 3060 / 3080
686 / 2191 MiB
Platzierung ist vollständig explizit, weil die Defaults falsch liegen --fit bricht in dem Moment ab, in dem -ngl gesetzt ist (n_gpu_layers already set by user to 99, abort), und ohne Overrides landet die N-Gram-Tabelle auf CUDA0. Deshalb stehen alle drei Patterns in einem -ot (Komma-getrennt, spezifisches zuerst, weil b10830 wiederholte Flags verwirft):
# 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.

t/s (tg @512, 65k ctx, 8 Threads): jeder Balken eine einzelne Veränderung
Threads

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).

-ub 2048

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).

XMP

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-ub3080 freitg @512pp @8kNeedle
65.53620482.191 MiB19,3222ok @ 8k/32k/60k
98.304 (shipped)2048799 MiB17,5 → 21,8 (XMP)210:
131.0722048OOM: 3.956 MiB Compute-Buffer
131.07210241.383 MiB17,2143 (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.

Orchestrator-Realität: Vorlauf kostet Aus der Tuning-Session (Logbook 1355/1357): Hermes' System-Prompt hat 16.979 Token, der Runner prefillt echten Text mit ~100-135 t/s. Erste Anfrage: 3:57 min. Zweite Sitzung: 2.040 neue Tokens (18 s), Rest aus dem Slot-Cache. Daraus die Betriebsregel: Slot-Save vor jedem Profilswap (POST /slots/0?action=save, 12k Tokens = 319 MiB in 0,2 s) und die Append-only-Regel, siehe Abschnitt 06.

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.

unter 32 · Verify auf CPU

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.

ab 32 · Verify erzwingt Weight-Shipment

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.

Betriebsfalle aus derselben Nachmittags-Sweep Ein Video-Tab plus Discord in der Desktop-Session auf der 3060 (32 % GPU-Auslastung) halbierte jeden Runner, der auf der Karte rechnet (A3B 56 → 30, 27B 21,7 → 15,6, Flash analog) und sah exakt aus wie eine BIOS-Regression nach dem XMP-Umbau. War keine. Vor jedem Zahlenglauben: nvidia-smi Idle-Auslastung von GPU 0 prüfen.

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:

PromptTabellenzeilenpp t/smajfltSSD read
Realtext 7,3k, erster Kontaktkalt89,7414k1,8 GiB
derselbe Text direkt danachwarm114,800
derselbe Text 10 min späterteils verdrängt106,474k288 MiB
Synthetik 5,1k (bench.py-Schleife)kalt → warm162 → 199,5118k → 0498 MiB → 0
Ergebnis 1: 22 % beim ersten Kontakt, danach nichts --lazy-mode off passt in 62 GiB nicht: 27 GiB Tabelle + 38 GiB Experten = 65 GiB. Während des Tests stand der Page Cache bei 53 GiB mit 0 frei; nach zehn Minuten waren Zeilen verdrängt. Erst ab 96 GB RAM wird das Flag hier interessant. --load-mode none (der Loader rät dazu, die Warnung ist für diesen Runner falsch): Prefill identisch, aber 38 GiB Experten wandern in anonymen Speicher, available fällt von 52 auf 14 GiB, Decode sinkt. Bleibt bei mmap.
Ergebnis 2: die eigentlichen Fixkosten pro Ubatch Ab 32 Tokens im Ubatch schiebt Op-Offload die host-residenten Experten einmal zur 3080, 5-8 s, egal ob danach 53 oder 500 Tokens folgen. Ein Agent-Turn mit 200 neuen Tokens kostet ~9 s Prefill. Unter 32 Tokens rechnet die CPU selbst und ist mit 1 s schneller (34 tok: 1,0 s vs 53 tok: 5,8 s). Das hat mit Lazy-Mode nichts zu tun und ist die Zahl, die Agent-Latenz auf dieser Box dominiert.

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

01

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.

02

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).

03

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.

04

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.

05

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)

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“.