Zurück zum Blog

Qwen3.8 27B auf zwei Mittelklasse-GPUs: Was MTP, Quantisierung und Tensor-Split wirklich bringen

Qwen3.8 27B auf zwei Mittelklasse-GPUs

Was MTP, Quantisierung und Tensor-Split wirklich bringen

Datum 17. Oktober 2026 (Update 18. Oktober)
Kategorie LLM & Self-Hosting
Lesezeit 28 Minuten
Die Kurzfassung für Eilige:
  • Der größte Gewinn kam nicht von MTP, sondern von einer einzigen Zeile: -sm tensor. Unser Produktionsmodell wurde damit von 29 auf 47,5 Token/s schneller, ohne jeden Qualitätsverlust.
  • MTP lohnt sich nur mit K-Quants (Q4_K_M & Co.). Mit I-Quants (IQ3_S) bringt es auf Mittelklasse-Karten nichts. Die schnellste Variante erreichte 79 Token/s.
  • Schnell ist nicht gleich zuverlässig: Die 79-Token/s-Variante scheiterte in beiden langen Agentensitzungen mit der neuen Anleitung. Die langsamere Produktionsvariante rekonstruierte mit derselben Anleitung in drei von drei Läufen ein optimiertes Windows-Programm ohne Debug-Symbole aus dem Maschinencode: 119, 120 und 120 von 120 Prüffällen.
  • Den größten Sprung im Agententest brachte die Anleitung für den Agenten, nicht das Modell.
  • Update 18. Oktober: Bestandene Tests sind nicht alles. Auch innen sauber (Smart Pointer, passende Datentypen, dokumentiertes Ghidra-Projekt) wurde der Nachbau erst, als wir die Arbeit auf zwei getrennte Sitzungen aufteilten und die zweite eine vorbereitete Liste abarbeiten ließen: Strukturbewertung 20 von 21 statt 9–16. Mehr Regeln in einer einzigen Anleitung machten es dagegen jedes Mal schlechter (Teil 3).

Alle unsere Dienste (Chatbot, n8n, Agenten, OpenCode) laufen auf einem einzigen lokalen Sprachmodell: Qwen3.8 27B (dicht, nicht MoE). Die Frage, mit der alles anfing, war harmlos: Nutzt unser Modell eigentlich schon MTP? Die Antwort war Nein. Daraus wurden zwei Tage Messreihen mit sechs Modelldateien, über zwanzig Konfigurationen, vier verschiedenen Testarten und mehreren Stunden selbstständiger Agentenarbeit.

Die Hardware

BausteinDetails
GPUsNVIDIA RTX 5060 Ti 16 GB + RTX 4070 Ti 12 GB = 28 GB VRAM
RAM64 GB
Softwarellama.cpp b10964, nativ unter Windows 11, hinter llama-swap (Modellwechsel) und LiteLLM (API-Gateway)
Kontext64k Token, ein Nutzer gleichzeitig (--parallel 1)

Also kein Rechenzentrum, sondern zwei Gaming-Karten unterschiedlicher Generation. Genau das macht die Ergebnisse interessant: Viele Benchmarks im Netz stammen von RTX 4090/5090, und dort gelten andere Regeln.

Die getesteten Modelldateien

Alle Dateien sind öffentlich auf Hugging Face verfügbar:

KurznameRepository / DateiGrößeQuant-TypMTP-Kopf
GSQISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF → Qwen3.8-27B-GSQ-RCO-IQ3_S.gguf11,8 GBI-Quant (gemischt, ~3,5 bpw)nein
GSQ-mtpdasselbe Repo → Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguf12,1 GBI-Quantja
Q4_K_MJackrong/Qwen3.8-27B-MTP-GGUF → Qwen3.8-27B-MTP-Q4_K_M.gguf16,8 GBK-Quantja
Q3_K_Mdasselbe Repo → Qwen3.8-27B-MTP-Q3_K_M.gguf13,5 GBK-Quantja
Q5_K_Mdasselbe Repo → Qwen3.8-27B-MTP-Q5_K_M.gguf19,5 GBK-Quantja
UD-Q4_K_XLunsloth/Qwen3.8-27B-GGUF → Qwen3.8-27B-UD-Q4_K_XL.gguf17,6 GBK-Quant (Unsloth Dynamic)ja

Warum GSQ unser Ausgangspunkt war: In einem früheren Test hielt der GSQ-Quant von ISTA-DASLab die Tool-Call-Disziplin auch bei 51k Kontext, wo ein normaler Q4_K_M unter Ollama ab etwa 50k anfing, Werkzeugaufrufe nur noch anzukündigen statt auszuführen.

Kurz erklärt: MTP und Tensor-Split

MTP (Multi-Token Prediction) ist ein kleiner Zusatzkopf im Modell, der die nächsten 2–4 Token rät. Das große Modell prüft diese Entwürfe in einem einzigen Rechenschritt und übernimmt alle, die stimmen. Das Ergebnis ist identisch mit dem ohne MTP, nur schneller. Voraussetzung: Das Prüfen von drei Token darf kaum länger dauern als das Erzeugen von einem.

Split-Modus: Läuft ein Modell auf zwei GPUs, verteilt llama.cpp es standardmäßig schichtweise (-sm layer). Karte 1 rechnet die erste Hälfte der Schichten, dann Karte 2 die zweite. Beim Schreiben arbeitet also immer nur eine Karte, die andere wartet. Mit -sm tensor wird jede Schicht auf beide Karten aufgeteilt, und beide rechnen gleichzeitig. Die Funktion ist in llama.cpp noch als experimentell markiert (PR #19378).


Teil 1: Der Testaufbau

Wir haben vier Testarten kombiniert. Jede beantwortet eine andere Frage:

TestFrageDauer pro Variante
A. GeschwindigkeitWie schnell schreibt das Modell kurze und lange Antworten?~5 min
B. llama-benchLohnt sich MTP mit diesem Quant überhaupt?~2 min
C. ReplayRuft das Modell in echten Agentensituationen das richtige Werkzeug auf?~20 min
D. re-labHält das Modell eine Stunde selbstständiger Arbeit durch und liefert ein prüfbares Ergebnis?60 min

A bis C laufen über ein PowerShell-Skript (mtp-matrix.ps1), D über ein eigenes Testlabor (re-lab).

Gemeinsame Regeln für alle Messungen

  • Eine Variante zur Zeit, auf leeren GPUs. Vor jeder einzelnen Messung entlädt das Skript alle anderen Modelle, beendet fremde llama-server-Prozesse und prüft mit nvidia-smi, dass nur noch der Desktop Speicher belegt (Grenzwerte: 4070 Ti unter 0,7 GB, 5060 Ti unter 1,9 GB). Ist der Speicher nach drei Minuten nicht frei, wird die Messung als „unsauber“ markiert.
  • Der Produktionsdienst wird sauber abgeschaltet, nicht abgeschossen: llama-swap wird über eine Modusdatei auf „aus“ gestellt und nach dem Test wieder auf „Produktion“.
  • Gleiche Basiseinstellungen für alle: beide GPUs, 64k Kontext, ein Slot, Denkmodus aus, Qwen-Sampling für Non-Thinking (--temp 0.7 --top-k 20 --top-p 0.8 --min-p 0 --presence-penalty 1.5). Variiert wurden nur Modelldatei, MTP-Tiefe (--spec-draft-n-max 2/3/4) und Split-Modus.
  • Jeder Server startet auf einem eigenen Testport, und das Skript wartet auf /health, bevor es misst.

Eine Lektion vorab: der unsichtbare Störenfried

Mitten in der ersten Messreihe fiel auf, dass auf der 4070 Ti zeitweise 3,9 GB belegt waren, die keinem Test gehörten. Die Ursache war unser Spracherkennungsdienst (Whisper large-v3 im Container). Ein Watchdog startete ihn jede Stunde neu, und beim Neustart lud er das Modell für 15 Minuten auf die GPU. Einige Messungen lagen genau in diesen Fenstern.

Wir haben den Dienst gestoppt und die komplette Messreihe wiederholt. Alle Zahlen unten stammen aus dem sauberen Durchlauf. Seitdem prüft das Skript den Speicher direkt vor jeder Messung, nicht nur einmal am Anfang.

Test A: Geschwindigkeit

Drei kurze Aufgaben, je zweimal, jeweils bis zu 600 Token Antwort:

AufgabePrompt (gekürzt)Warum
Code„Schreibe eine Python-Funktion, die eine CSV-Datei einliest, Zeilen mit leeren Feldern verwirft und pro Kategorie die Beträge summiert. Mit Docstring und Typ-Hints.“gut vorhersagbarer Text, gut für MTP
Fließtext„Erkläre in etwa 300 Wörtern, wie eine Wärmepumpe funktioniert.“freie Sprache, schwer vorherzusagen
JSON„Gib ein JSON-Array mit 12 fiktiven Mitarbeitern aus (id, name, abteilung, eintrittsjahr, gehalt).“sehr regelmäßige Struktur

Dazu kommt eine Langkontext-Messung: Die gesamte Markdown-Dokumentation unseres Repositorys (gekürzt auf 80.000 Zeichen, rund 26.000 Token) wird als Lesestoff vorangestellt, dann soll das Modell die wichtigsten Dienste und Ports zusammenfassen. Das misst zwei Dinge: wie schnell der lange Prompt eingelesen wird (Prefill) und wie schnell das Modell schreibt, wenn der Kontext voll ist.

Gemessen werden die Zahlen, die llama-server selbst in jeder Antwort mitliefert (timings): Token pro Sekunde beim Schreiben, Prefill-Geschwindigkeit und bei MTP die Zahl der vorgeschlagenen und angenommenen Entwurfs-Token (daraus die Akzeptanzrate). Dazu der belegte Grafikspeicher pro Karte.

Test B: llama-bench – Was kostet ein Prüfschritt?

MTP lohnt sich nur, wenn das große Modell mehrere Token fast so schnell prüfen kann wie eines. Das lässt sich direkt messen:

llama-bench -m <modell>.gguf -ngl 99 -fa 1 -dev CUDA0/CUDA1 -p 1,2,3,4 -n 32 -r 3

-p 1,2,3,4 misst die Zeit für einen Rechenschritt mit 1, 2, 3 und 4 Token, -r 3 wiederholt dreimal. Der entscheidende Wert ist der Faktor 3 Token / 1 Token. Liegt er nahe 1, sind drei geratene Token fast gratis zu prüfen. Liegt er bei 2, frisst die Prüfung den Gewinn auf.

Test C: Replay echter Agentenanfragen

Kurze Benchmarks sagen nichts darüber, ob ein Modell in einer echten Agentensitzung richtig handelt. Deshalb haben wir im September einen Mitschnitt-Proxy zwischen OpenCode und das Modell gehängt. Er speicherte jede echte Anfrage einer Arbeitssitzung als JSON-Datei: Systemprompt, alle bisherigen Nachrichten, Tool-Ergebnisse und die Liste der verfügbaren Werkzeuge.

Für den Test haben wir fünf Mitschnitte zwischen 25.000 und 57.000 Token Kontext ausgewählt. Drei davon sind Situationen, in denen das damalige Modell versagt hatte (es kündigte nur an: „Ich schaue mir jetzt die Datei an …“, rief aber kein Werkzeug auf). Jede Anfrage wird dem Testmodell sechsmal mit verschiedenen Zufalls-Seeds vorgelegt, also 30 Versuche pro Variante.

Bewertet wird die Antwort automatisch:

KategorieBedeutung
okgültiger, strukturierter Tool-Call mit existierendem Werkzeug und allen Pflichtargumenten
prosenur eine Absichtserklärung, kein Aufruf (das bekannte Fehlerbild)
text_toolcallAufruf als Klartext statt im API-Format
bad_name / bad_argserfundenes Werkzeug oder fehlende Argumente

Test D: re-lab – ein Programm aus dem Maschinencode zurückgewinnen

Der härteste Test. Ein Agent soll ohne Zugriff auf den Quellcode ein C++-Programm aus der fertigen Windows-EXE rekonstruieren, so genau, dass das Nachbauprogramm sich Byte für Byte gleich verhält. Das prüft alles, was ein Agentenmodell können muss: Werkzeuge über viele Runden korrekt bedienen, Zwischenergebnisse festhalten, Fehler erkennen, selbst korrigieren und eine Stunde lang den Faden nicht verlieren.

Das Zielprogramm: minipack

minipack ist ein kleines Kommandozeilenprogramm (360 Zeilen C++), das wir eigens für diesen Test geschrieben haben. Es enthält bewusst die Bausteine, an denen Decompiler und Modelle typischerweise scheitern:

C++-BausteinWo im ProgrammWarum schwierig
Klassenhierarchie mit virtuellen Methodenabstrakte Basisklasse Codec mit vier Ableitungen (RleCodec, DeltaCodec, XorCodec, ChainCodec), je name(), encode(), decode()im Maschinencode nur Funktionszeiger-Tabellen (vtables); Klassen und Vererbung müssen erschlossen werden
Composite-Muster mit Smart PointernChainCodec hält std::vector<std::unique_ptr<Codec>>, kodiert vorwärts, dekodiert rückwärtsTemplates werden vom Compiler komplett aufgelöst und eingebettet
std::string / std::vectorüberallInlining, Small-String-Optimierung: aus einer Zeile Quelltext werden dutzende Maschinenbefehle
Kompressions-/KodieralgorithmenRun-Length (Paare Anzahl/Byte, max. 255), Delta-Kodierung, rollierendes XOR mit positionsabhängiger Maske (key[i] ^ (i*31+7))exakte Konstanten und Randfälle müssen stimmen
PrüfsummenCRC32 mit Tabelle, die beim ersten Aufruf statisch initialisiert wird (Polynom 0xEDB88320), FNV-1a 64 Bitmagische Konstanten erkennen, Initialisierungslogik nachbauen
ParserSpezifikationen wie xor:KEY+delta+rle werden an + zerlegt, Einzelteile per Fabrikfunktion erzeugtFehlerfälle (rle+, +rle, leere Spec)
Statistik mit GleitkommazahlenBytes, Zeilen, Wörter, häufigstes Byte, Shannon-Entropie mit eigenem log2 (Reihenentwicklung statt <cmath>)Gleitkomma im Assembler, Ausgabe auf vier Nachkommastellen genau
Datei-I/O und Kommandozeilefopen/fread in 4-KB-Blöcken, fünf Befehle, Fehlercodes 1/2/3Rückgabecodes und exakte Fehlermeldungen

Die fünf Befehle des Programms:

minipack enc <spec> <in> <out>     Datei kodieren
minipack dec <spec> <in> <out>     Datei dekodieren
minipack hash <in>                 CRC32, FNV-1a 64, Größe
minipack stats <in>                Bytes, Zeilen, Wörter, häufigstes Byte, Entropie
minipack selftest                  5 Codec-Ketten an einem eingebauten Muster prüfen

Die vier Schwierigkeitsstufen

Aus demselben Quellcode erzeugt ein Build-Skript mit dem Microsoft-Compiler (Visual Studio 2026, C++20) vier Varianten:

StufeCompiler-OptionenWas der Agent im Binary findet
A/Od /Zi + PDB (Debug)Funktionsnamen, Typen, Variablennamen. ~2.600 Funktionen (Debug-STL)
B/O2 /Zi + PDBNamen, aber optimierter Code mit Inlining
C/O2 ohne PDBkeine Namen, nur Laufzeit-Typinformation (RTTI) der Klassen und Textkonstanten. ~200 Funktionen
D/O2 /GR- ohne PDBgar nichts außer Strings

Stufe C ist das realistische Szenario: ein ganz normales Release-Programm, wie es ausgeliefert wird. Alle Funktionen heißen dort nur FUN_1400041e0. Der Agent muss allein aus Pseudocode und Assembler erschließen, was eine Funktion tut. Stufe A hat unser Modell schon früher gelöst (120/120, allerdings in rund 1,5 Stunden Analyse plus Thinking). Stufen B und D haben wir noch nicht getestet. Alle Ergebnisse in diesem Artikel beziehen sich auf Stufe C.

Die 120 Prüffälle

Eine Prüfsoftware (Python) führt Original und Nachbau mit identischen Argumenten aus und vergleicht drei Dinge: den Exit-Code, die komplette Bildschirmausgabe und, wo eine Datei entsteht, deren SHA-256-Prüfsumme. Ein Fall gilt nur als bestanden, wenn alles exakt übereinstimmt. Die Referenz wurde einmalig vom Original aufgenommen.

Sieben Testdateien decken die Randfälle ab:

DateiInhaltPrüft
empty.bin0 Byteleere Eingaben, Division durch null in der Entropie
one.binein Byte „A“Minimalfall
text.txt4.600 Byte deutscher/lateinischer Text mit Tabs und Windows-ZeilenendenZeilen-/Wortzählung, Text-Kodierung
random.bin5.000 Byte Pseudozufall (LCG)hohe Entropie, RLE vergrößert die Datei
runs.bin300ׄa“, 600×0x00, 256×0xFF …RLE-Grenze bei 255 Wiederholungen
ramp.bin2.048 Byte 0,1,2,…,255,0,1,…Delta-Kodierung (alle Differenzen = 1), Byte-Überlauf
words.txt" eins zwei\n\ndrei\t vier \n"Wortgrenzen mit führenden/mehrfachen Leerzeichen

Daraus ergeben sich die 120 Fälle:

GruppeAnzahlWas genau
Fehlerpfade4ohne Argumente (Usage-Text, Code 1), unbekannter Befehl, fehlende Datei (Code 3), selftest
Ungültige Codec-Spezifikationen4zip, rle+, +rle, leerer String → Fehlermeldung, Code 2
hash7je Testdatei: CRC32, FNV-1a 64, Größe
stats7je Testdatei: Bytes, Zeilen, Wörter, häufigstes Byte, Entropie auf 4 Stellen
enc497 Codec-Ketten × 7 Dateien: Ausgabezeile und SHA-256 der kodierten Datei
Roundtrip (enc → dec)497 × 7: die Kodierung des Nachbaus wird wieder dekodiert, Ergebnis muss mit dem Original übereinstimmen
Summe120

Die sieben Codec-Ketten: rle, delta, xor (Standardschlüssel), xor:secret, delta+rle, xor:k+delta+rle und rle+rle (doppelte Lauflängenkodierung).

Ein erstes Grundgerüst mit main und Fehlermeldungen brachte in unseren Läufen 2 bis 26 von 120. Für 120 müssen alle Klassen, Algorithmen, Konstanten und Ausgabeformate exakt stimmen.

Die Werkzeuge des Agenten

WerkzeugRolle
OpenCode (opencode run --auto)Agenten-Rahmen: Gesprächsschleife, Dateiwerkzeuge, Kontextverwaltung
Ghidra 12.1.4Disassembler/Decompiler der NSA, analysiert die EXE
pyghidra-mcp 0.2.7stellt Ghidra als MCP-Server (Model Context Protocol) über HTTP bereit
check.ps1baut den Nachbau mit demselben Compiler und lässt die 120 Fälle laufen

pyghidra-mcp bietet 20 Werkzeuge an, darunter list_project_binaries, search_strings, list_xrefs (wer verweist auf diese Adresse?), decompile_function (Pseudo-C), disassemble, read_bytes, gen_callgraph, search_symbols_by_name sowie schreibende wie rename_function, set_function_prototype und set_comment. OpenCode bindet sie im Code-Modus ein: Das Modell schreibt kleine JavaScript-Schnipsel, die die Werkzeuge aufrufen, etwa

const s = await tools.ghidra.search_strings({ binary_name: "/minipack.exe-9344e7", query: "usage" });
return s;

Daneben hat der Agent nur die eingebauten OpenCode-Werkzeuge read, write, edit, grep, glob und eine Shell, in der genau ein Befehl erlaubt ist: pwsh -File check.ps1. check.ps1 zeigt bei jedem fehlgeschlagenen Fall die erwartete und die erhaltene Ausgabe, verrät aber nicht den Quellcode.

Abschottung: Der Agent darf nicht schummeln

Das ist nicht hypothetisch. In einem frühen Lauf fand ein anderes Modell per Dateisuche den Originalquellcode im Repository, kopierte ihn per Shell und meldete stolz 120/120.

Seitdem gilt:

  • Jeder Lauf bekommt ein frisches Arbeitsverzeichnis außerhalb des Repositorys (nur AGENTS.md, check.ps1, opencode.json und ein leerer Ordner recon/).
  • In opencode.json: Zugriff auf fremde Verzeichnisse, Web und Unteragenten verboten, Shell verboten bis auf pwsh -File check.ps1.
  • Ein Isolationstest versucht vor jeder Testserie gezielt, Quellcode und Referenzdaten zu lesen. Alle Versuche müssen abgelehnt werden.
  • Jeder Lauf bekommt ein eigenes, frisches Ghidra-Projekt, damit Umbenennungen aus früheren Läufen nicht helfen.
  • Die Endbewertung baut der Testrahmen nach Ablauf selbst aus recon/minipack.cpp und prüft unabhängig vom Agenten. Was der Agent über sich selbst behauptet, zählt nicht.

Ablauf eines Laufs

  1. llama-swap wechselt in den Coding-Modus und lädt die gewünschte Modellvariante.
  2. Eine kurze Kontrollfrage prüft, ob der Denkmodus wirklich so eingestellt ist wie gewollt.
  3. pyghidra-mcp startet, analysiert die EXE vollständig und öffnet erst danach seinen Port.
  4. OpenCode startet ohne Benutzeroberfläche mit dem Auftrag: „Arbeite AGENTS.md vollständig ab. Stelle keine Rückfragen. Höre erst auf, wenn check.ps1 alle Fälle besteht oder du nachweislich nicht weiterkommst.“
  5. Alle 15 Sekunden: Ist der Agent fertig? Ist die Stunde um? Höchster Grafikspeicherverbrauch?
  6. Nach Ende oder Timeout: Ghidra beenden, Nachbau unabhängig bauen und prüfen, Produktion zurückschalten.

Das komplette Protokoll jeder Sitzung (jede Modellantwort, jeder Werkzeugaufruf mit Zeitstempel) wird gespeichert. Daraus stammen die Verlaufszahlen unten.

Die Arbeitsanweisung (AGENTS.md)

Die AGENTS.md beschreibt den Ablauf: main über die Usage-Zeichenkette finden, Aufrufgraph holen, Funktionen einzeln dekompilieren, in Ghidra benennen und in C++ übertragen. Dazu kommen Regeln, die aus früheren Fehlschlägen entstanden sind: Binary-Namen exakt übernehmen, Adressen kopieren statt abtippen (ein Modell suchte zehn Minuten nach einer Adresse mit fehlender Ziffer), Compiler-Bibliothek erkennen, aber nicht nachbauen, Unklares als // UNKLAR: markieren statt erfinden.

Die entscheidende Ergänzung für diese Testserie war ein neuer Abschnitt „CODE ZUERST“:

Analyse ohne Code bringt 0 Punkte. Sobald main dekompiliert ist (spätestens nach 10 Tool-Aufrufen): SOFORT ein kompilierbares Grundgerüst schreiben, mit allen Befehlen, Argumentprüfung, Usage-/Fehlermeldungen und Rückgabecodes, Unverstandenes als Stub. Dann check.ps1. Danach immer im Wechsel: höchstens 3 Ghidra-Aufrufe, dann Code ändern und prüfen. check.ps1 zeigt die erwartete Ausgabe: Formate dort ablesen, statt sie im Disassembly zu suchen.

Teil 2: Die Ergebnisse

Ergebnis A: Geschwindigkeit

Decode-Geschwindigkeit (Token/s, Mittel aus Code, Text und JSON), 64k Kontext:

VarianteSplitToken/sVRAM 5060 Ti + 4070 Ti
GSQ IQ3_S (vorherige Produktion)layer2911,3 + 6,7 GB
GSQ IQ3_S + MTPlayer26–29–
GSQ IQ3_Stensor47,59,6 + 8,1 GB
GSQ IQ3_S + MTP 3tensor40–
Q4_K_M ohne MTPlayer24–
Q4_K_M + MTP 3layer4914,6 + 8,9 GB
Q4_K_M + MTP 3tensor7512,2 + 10,8 GB
Q4_K_M + MTP 4tensor79,4 (bei 26k Kontext: 65,5)12,2 + 10,9 GB
UD-Q4_K_XL + MTP 3tensor75,912,4 + 11,2 GB
Q3_K_M + MTP–nicht schneller als Q4_K_M–
Q5_K_M + MTP 3tensorpasst nicht (Speicher voll auf der 12-GB-Karte)–

Drei Beobachtungen:

  • -sm tensor bringt jedem Modell 50–65 %. Es ist der einzige Hebel, der unabhängig vom Quant wirkt.
  • MTP verdoppelt K-Quants ungefähr, bei GSQ bringt es nichts oder schadet sogar.
  • Prefill wird mit tensor etwa 20 % langsamer (1.070 statt 1.370 Token/s), weil sich die Karten nach jeder Schicht abstimmen. Bei 30k Kontext kostet das einmalig rund 6 Sekunden pro neuer Anfrage. Die Ersparnis beim Schreiben ist deutlich größer.

Die Akzeptanzrate der MTP-Entwürfe hängt stark von der Textart ab: 70–90 % bei Code und JSON, nur 40–60 % bei Fließtext. Agentenarbeit profitiert also mehr als ein Plauder-Chat.

Ergebnis B: Warum MTP mit IQ3_S nichts bringt

QuantFaktor 3 Token / 1 Token
GSQ IQ3_S (I-Quant)2,08×
K-Quants (Q4_K_M, UD-Q4_K_XL, Q5_K_M)1,09–1,22×

I-Quants speichern Gewichte über Codebücher. Für ein einzelnes Token gibt es einen schnellen Rechenweg. Ab zwei Token wechselt llama.cpp auf einen anderen Kernel, der auf diesen Mittelklasse-Karten doppelt so lange braucht. Ein MTP-Schritt mit drei Token kostet dann so viel wie zwei normale Schritte, und der Gewinn ist weg. K-Quants entpacken einfacher, dort kosten drei Token fast dasselbe wie eines.

Wichtig: Das gilt für unsere Hardware. Auf einer RTX 5090 mit viel mehr Rechenleistung bringt MTP auch mit dem GSQ-Quant das 1,6-Fache (Bericht von zephel01). Wer MTP testen will, sollte deshalb zuerst llama-bench -p 1,2,3,4 laufen lassen. Liegt der Faktor für 3 Token deutlich über 1,5, lohnt sich MTP nicht.

Ergebnis C: Tool-Calls im Replay

VarianteTreffer („ok“)
GSQ, layer (llama.cpp)30/30
GSQ, tensor30/30
Q4_K_M + MTP 3, layer30/30
Q4_K_M + MTP 3, tensor29/30
Zum Vergleich: Q4_K_M unter Ollama (09/2026)25/30
Zum Vergleich: GSQ unter Ollama (09/2026)19/30

Unter llama.cpp sind alle Varianten praktisch gleichwertig. Der Wechsel von Ollama zu llama.cpp hat das Tool-Call-Problem gelöst, nicht die Wahl des Quants. Weder MTP noch tensor verschlechtern die Zuverlässigkeit messbar.

Eine Falle im eigenen Testaufbau: Das Auswerteskript zählte anfangs einige Versuche als „falsche Argumente“. In Wahrheit hatte das Modell korrekt begonnen, eine Datei zu schreiben, und der Test brach nach 1.024 Token ab. Bei Tool-Call-Benchmarks immer finish_reason prüfen.

Ergebnis D: re-lab Stufe C

Modell / EinstellungAGENTS.mdErgebnisWas passierte
GSQ, layeralt0/12060 min nur analysiert, kein Code
Q4_K_M + MTPalt0/12060 min nur analysiert, kein Code
Q4_K_M + MTP 4, tensorneu0/120nach 11 min abgebrochen: Tool-Aufruf kam als Text
Q4_K_M + MTP 4, tensor, Denken „low“neu0/120nach 12 min abgebrochen: Kontext-Zusammenfassung zu lang
GSQ, tensor – Lauf 1neu119/120Timeout nach 60 min, Stand 119 schon nach 32 min
GSQ, tensor – Lauf 2neu120/120selbst beendet nach 58 min
GSQ, tensor – Lauf 3neu120/120selbst beendet nach 51 min
Claude Sonnet (Cloud), zum Vergleich–120/120in 11 min

So verliefen die drei GSQ-Läufe

Ausgewertet aus den Sitzungsprotokollen (Minute nach Start → bestandene Fälle bei check.ps1):

Lauf 1Lauf 2Lauf 3
Erstes Grundgerüst geschriebenMin. 12Min. 10Min. 7
Verlauf der Prüfungen26 → 117 (Min. 26) → 119 (Min. 32)11 → 113 (Min. 36) → 119 (Min. 48) → 120 (Min. 55)2 → 82 (Min. 18) → 104 (Min. 35) → 119 (Min. 45) → 120 (Min. 50)
Modellrunden778365
Ghidra-Aufrufe gesamt604030
davon decompile_function251818
davon read_bytes (Konstanten, Tabellen)24105
davon disassemble66–
Code-Änderungen (write/edit)131314
Nachbau379 Zeilen423 Zeilen474 Zeilen

Der typische Ablauf war in allen drei Läufen gleich: list_project_binaries → search_strings nach „usage“ → list_xrefs auf die gefundene Adresse führt zu main → decompile_function(main) → Grundgerüst schreiben → check.ps1 → dann Codec für Codec dekompilieren und nachbauen. Den letzten Feinschliff (Ausgabeformate) holte sich das Modell aus den Fehlermeldungen von check.ps1, genau wie in der Anleitung vorgesehen.

Strukturell sind alle drei Nachbauten nah am Original: alle fünf Klassen (Codec plus vier Ableitungen) mit vier virtuellen Methoden in der Basisklasse und 12–13 override. Das Modell hat die Klassenhierarchie also allein aus den vtables und der RTTI erschlossen, nicht nur ein Programm geschrieben, das zufällig dieselbe Ausgabe erzeugt.

Der eine fehlende Fall in Lauf 1 war selftest: Das Modell hatte die Formel, mit der das Programm sein internes Testmuster aus 1.000 Byte erzeugt, nicht exakt getroffen. Die Codecs selbst waren richtig, das zeigen alle 98 enc- und Roundtrip-Fälle. Lauf 2 und 3 haben auch das gelöst.

Zwei Erkenntnisse

1. Die Anleitung war der größte Hebel. Mit der alten Anleitung analysierte das Modell eine Stunde lang und schrieb keine Zeile Code. Mit „Code zuerst“ löste dasselbe Modell die Aufgabe dreimal hintereinander. Lokale Modelle brauchen offenbar eine engere Führung als Frontier-Modelle, die so etwas von selbst tun. Die Dauer schwankt (51–60 min), das Ergebnis kaum (119–120).
2. Schnell ist nicht gleich zuverlässig. Q4_K_M mit MTP scheiterte in allen drei langen Läufen, davon zweimal in der 79-Token/s-Konfiguration mit der neuen Anleitung, obwohl es im kurzen Replay gleichwertig war. Die Schwäche zeigt sich erst nach vielen Runden. Ob das am Quant liegt (Jackrongs Q4_K_M ist eine reine Konvertierung ohne Importance-Matrix), an MTP oder am Zufall, können wir noch nicht sicher sagen. Für die Produktion war das Bild aber klar genug: mit derselben Anleitung 2 von 2 gescheitert gegen 3 von 3 erfolgreich.

Was wir umgestellt haben

Produktiv läuft jetzt GSQ mit Tensor-Split. Gegenüber vorher ist das eine einzige zusätzliche Zeile:

llama-server.exe
  -m Qwen3.8-27B-GSQ-RCO-IQ3_S.gguf
  -ngl 99
  --device CUDA0,CUDA1
  -sm tensor
  -c 65536
  --parallel 1
  --jinja
  --reasoning-format deepseek
  --temp 0.7 --top-k 20 --top-p 0.8 --min-p 0
  --presence-penalty 1.5 --repeat-penalty 1.0
Gemessen nach der Umstellung: 47,6 Token/s (nach den Agentenläufen erneut 48,1), Tool-Calls über unser LiteLLM-Gateway einwandfrei, belegter Speicher rund 9,6 + 8,1 GB. Das lässt Reserve für Kontext und andere Dienste.

Wer maximale Geschwindigkeit für Chat will und auf lange Agentenläufe verzichten kann, nimmt die schnellste Variante:

llama-server.exe
  -m Qwen3.8-27B-MTP-Q4_K_M.gguf        # oder Qwen3.8-27B-UD-Q4_K_XL.gguf
  -ngl 99
  --device CUDA0,CUDA1
  -sm tensor
  -c 65536
  --parallel 1
  --spec-type draft-mtp
  --spec-draft-n-max 4                  # 4 für Code/JSON, 2–3 für Fließtext
  --jinja
  --reasoning-format deepseek
  --temp 0.7 --top-k 20 --top-p 0.8 --min-p 0
  --presence-penalty 1.5 --repeat-penalty 1.0

Teil 3 (Update 18. Oktober): Vom bestandenen Test zum sauberen Nachbau

120 von 120 Prüffällen heißt: Das Programm verhält sich wie das Original. Ob es auch innen dem Original entspricht, sagen die Fälle nicht. Genau das will man aber beim Reverse Engineering: Code, mit dem ein Mensch weiterarbeiten kann, und ein Ghidra-Projekt, in dem die Funktionen nicht mehr FUN_1400041e0 heißen.

Die Strukturbewertung

Ein zweites Prüfskript (structure.py) bewertet den fertigen Nachbau mit 21 Kriterien. Der Agent sieht diese Bewertung nicht, sie würde Details des Originals verraten.

GruppeKriterien (Auswahl)
KlassenBasisklasse mit ≥ 4 Ableitungen, abstrakt, virtueller Destruktor, ≥ 12 override, std::string name() virtuell
Speicherstd::unique_ptr für Codecs, Kette als vector<unique_ptr<…>>, kein rohes new/delete
DatenBytes als std::vector<uint8_t> statt std::string, Statistik als struct, eigenes log2, nur erlaubte Header
Abschlusskeine TODO/UNKLAR, keine Hilfsdateien, plan.md ohne offene Punkte, Umfang 0,7–1,5× Original, keine Compiler-Warnungen
Ghidra≥ 15 Funktionen umbenannt, ≥ 10 kommentiert, ≥ 5 Prototypen gesetzt

Die Ghidra-Kriterien werden live aus dem Ghidra-Projekt gelesen, bevor der Server beendet wird, nicht aus dem Gesprächsprotokoll. Das Protokoll zählte Umbenennungen in Schleifen nicht mit.

Die drei erfolgreichen Läufe aus Teil 2 erreichten hier nur 10, 13 und 10 von 21: korrekte Klassenhierarchie, aber rohe Zeiger, Binärdaten in std::string, offene UNKLAR-Kommentare und ein Ghidra-Projekt, in dem kaum eine Funktion einen Namen hatte.

Versuch 1: mehr Regeln in die Anleitung – jedes Mal schlechter

Der naheliegende Weg war, die fehlenden Pflichten in die AGENTS.md zu schreiben. Das Ergebnis war eindeutig, nur nicht so wie erhofft:

LaufÄnderung an der AnleitungPrüffälleStruktur
Referenz (Teil 2)schlanke „Code zuerst“-Fassung119–12010–13
1+ Abschlussschritt (Besitz, Typen, Aufräumen, Ghidra)12016
2+ „jede fertige Funktion sofort in Ghidra dokumentieren“149
3+ Regeln gegen Festbeißen an Einzelfragen27
4+ automatisches Fortsetzen, „kurz denken“0 (kompiliert nicht)10

Jede zusätzliche Pflicht lenkte das Modell von der Hauptaufgabe ab. In Lauf 3 schrieb es einen 66.000 Zeichen langen Gedankenmonolog, lief damit ins Ausgabelimit und beendete die Sitzung, weil eine Antwort ohne Werkzeugaufruf für OpenCode „fertig“ bedeutet. Lauf 1 zeigte aber, dass ein Abschlussschritt nach der eigentlichen Arbeit grundsätzlich etwas bringt.

Versuch 2: zwei Sitzungen

Daraus entstand die Aufteilung:

  1. Sitzung 1 (bis 120 min) bekommt die unveränderte schlanke Anleitung und nur ein Ziel: alle Fälle bestehen.
  2. Sobald der Testrahmen 120/120 sieht, startet er eine frische Sitzung 2 (45 min) mit eigener, kurzer Anleitung: Ghidra dokumentieren, Besitz und Typen wie im Original, aufräumen. Jede Änderung, nach der ein Fall fällt, muss sofort zurück.

Sitzung 2 beginnt mit leerem Kontext. Ihr Wissen stammt allein aus dem Code und der plan.md, die Sitzung 1 geschrieben hat. Endet eine Sitzung vorzeitig (etwa durch einen Monolog), setzt der Testrahmen sie bis zu fünfmal mit „Weiter nach AGENTS.md“ fort.

Der erste Lauf dieser Art brauchte für Sitzung 1 rund 98 Minuten. Sitzung 2 blieb nur der Rest des gemeinsamen Zeitlimits (20 min), und ihre 19 Umbenennungen gingen verloren, weil das Ghidra-Projekt nicht gespeichert wurde. Danach bekam Sitzung 2 eigene 45 Minuten, und der Testrahmen speichert das Projekt vor dem Beenden.

Im nächsten Lauf war Sitzung 1 schnell wie nie: 120/120 nach 48 Minuten, der schnellste Lauf bisher. Sitzung 2 dagegen brachte nichts. Das Modell ordnete die Funktionen richtig zu, verbiss sich dann aber 20 Minuten in eine Nebenfrage (hat der printf-Wrapper vier oder sechs Parameter?), las dafür Rohbytes und den PE-Header und benannte bis zum Timeout keine einzige Funktion. Struktur: 9 von 21.

Versuch 3: Sitzung 2 mit vorbereiteter Liste

Die Zuordnung „Adresse → Funktion“ hatte Sitzung 1 längst erarbeitet. Sie stand als Tabelle in plan.md. Ein kleines Skript (ghidra_list.py) macht daraus beim Sitzungswechsel eine Arbeitsliste ghidra-liste.md:

| Adresse     | Name           | Zweck                          | Signatur                          |
|-------------|----------------|--------------------------------|-----------------------------------|
| 0x1400041e0 | main           | Command-Dispatch, usage, stats | ...                               |
| 0x1400037c0 | build_pipeline | '+'-Split, ChainCodec          | build_pipeline(spec)              |
| 0x140002bd0 | crc32_str      | CRC-32 (Standard-Tabelle)      | crc32_str(data)                   |
| ...         |                |                                |                                   |

Laufzeit- und STL-Funktionen filtert das Skript heraus. Die Anleitung für Sitzung 2 sagt nur noch: Liste sofort abarbeiten, je Aufruf fünf Einträge, nichts vorher prüfen, Unklarheiten nicht untersuchen – ein „vermutlich …“ im Kommentar genügt. Danach die übrigen Codec-Methoden über die vtables ergänzen.

Die Liste nimmt dem Modell keine Analyse ab. Sie enthält nur seine eigenen Ergebnisse, in eine Form gebracht, die zum Abarbeiten einlädt statt zum Weiterforschen.

Ergebnis

Zwei Sitzungen ohne ListeZwei Sitzungen mit Liste
Sitzung 1120/120 nach 48 min120/120 nach 68 min
Sitzung 2Timeout nach 45 minselbst beendet nach ~39 min
Ghidra: benannt / kommentiert0 / 023 / 23
Ghidra: Prototypen01
Endprüfung120/120120/120
Struktur9/2120/21

Was Sitzung 2 im Einzelnen erledigt hat:

  • Ghidra: alle 16 Einträge der Liste benannt und kommentiert. Dazu kamen 7 weitere Codec-Methoden, die das Modell selbst über die vtables gefunden hat, etwa RleCodec_encode/_decode und DeltaCodec_encode/_decode.
  • Code: std::unique_ptr und std::vector<std::unique_ptr<Codec>>, kein rohes new/delete mehr, std::vector<uint8_t> für Binärdaten, std::string name() als virtuelle Methode. Keine TODO/UNKLAR mehr, Hilfsdatei entfernt, keine Warnungen, und alle 120 Fälle bestehen weiterhin.
  • Das einzige Nein: nur ein Prototyp statt fünf. Das Modell rief set_function_prototype nur zweimal auf. Die Anleitung sagte „wo möglich“, und das hat es sehr sparsam ausgelegt.

Was wir daraus mitnehmen

  • Eine Aufgabe pro Sitzung. GSQ löst die Rekonstruktion zuverlässig, solange die Anleitung schlank bleibt. Pflichten für „danach“ gehören in eine eigene Sitzung mit frischem Kontext.
  • Fleißarbeit als Liste vorgeben. Mit offener Aufgabe („dokumentiere die Funktionen“) forschte das Modell lieber weiter. Mit einer Liste arbeitete es die Liste ab und fand danach sogar selbst weitere Funktionen.
  • Werkzeugzustand sichern. Ghidra-Änderungen über pyghidra-mcp gehen verloren, wenn der Server ohne save beendet wird. Einen Lauf mit 19 Umbenennungen haben wir so verloren, bevor wir es bemerkten.
Vorsicht bei der Verallgemeinerung: Das ist ein einzelner Lauf. Ob es zuverlässig klappt, müssen Wiederholungen zeigen. Sitzung 1 schwankt bei der Dauer deutlich (48, 68 und 98 min in den drei Zwei-Sitzungs-Läufen), das Ergebnis 120/120 dagegen nicht.

Konsequenz für den Betrieb

Seit dem 18. Oktober läuft auch der Coding-Modus (OpenCode) auf GSQ mit -sm tensor. Bisher stand dort noch Qwen3-Coder 30B. Die Coding-Einstellung entspricht der aus den re-lab-Läufen: Produktions-Quant, Tensor-Split, Denken mit begrenztem Budget (--reasoning-budget 2048). Damit läuft jetzt tatsächlich ein Modell für alle Dienste.

Stolpersteine, die uns Zeit gekostet haben

  • Gerätenummern: llama.cpp nummeriert die GPUs anders als nvidia-smi. Bei uns ist CUDA0 die 4070 Ti, in nvidia-smi ist sie GPU 1. Ein Einzel-GPU-Test landete deshalb auf der falschen Karte und lief in einen Speicherfehler. Vorher llama-server --list-devices prüfen.
  • -sm tensor teilt gleichmäßig auf. Die kleinere Karte bestimmt, was passt. Mit 16 + 12 GB zählen effektiv nur 2 × 12 GB. Daran scheiterte Q5_K_M.
  • -sm row wird von unserem Build nicht unterstützt („device does not support split buffers“).
  • --parallel bei 1 lassen: Laut Community schrumpft der MTP-Vorteil mit mehreren parallelen Slots stark.
  • Kontext nicht über 64k erhöhen: Für Qwen3.8 sind in llama.cpp Einbrüche über ~80k und Abbrüche über ~130k gemeldet (#27623, #27756).
  • ngram-Spekulation zusätzlich zu MTP (ngram-mod) machte es bei uns etwas langsamer.
  • Agententests abschotten: Ein Agent, der den Quellcode finden kann, findet ihn auch.
  • pyghidra-mcp findet PDB-Dateien nicht neben der EXE, sondern nur im eigenen Symbolspeicher. Pfad explizit mit --sym-file-path übergeben bzw. --no-symbols für Stufe C/D.
  • pyghidra-mcp speichert nicht von selbst. Vor dem Beenden save aufrufen, sonst sind alle Umbenennungen und Kommentare weg.
  • OpenCode nimmt das Projektverzeichnis aus $PWD. Aus Git Bash gestartet, lief ein Agent deshalb im falschen Repository mit der falschen Konfiguration. Im Startskript $env:PWD explizit setzen.
  • Eine Antwort ohne Werkzeugaufruf beendet die OpenCode-Sitzung. Lange Gedankenmonologe, die ins Ausgabelimit laufen, enden genau so. Der Testrahmen setzt solche Sitzungen jetzt automatisch fort.

Und die Hardware?

Der Engpass ist jetzt die 12-GB-Karte. Wir haben zwei bezahlbare Wege geprüft:

  • Zweite RTX 5060 Ti 16 GB statt der 4070 Ti: zwei gleiche Karten, tensor nutzt effektiv 32 statt 24 GB, und alles bleibt CUDA. Damit hätte Q4_K_M + MTP Reserve, und Q5_K_M + MTP sollte passen. Das ist der gezielteste Hebel.
  • AMD Radeon AI PRO R9700 (32 GB): viel Speicher, aber neben einer NVIDIA-Karte geht in llama.cpp nur schichtweises Aufteilen. Der Tensor-Split-Gewinn wäre weg. Gemessene Werte für dichte 27B-Modelle liegen bei 25–43 Token/s auf einer Karte, der Prefill ist deutlich langsamer als bei NVIDIA. Sinnvoll ist die Karte, wenn man Speicher für größere Modelle braucht, nicht für mehr Tempo.

Fazit

Was wir jedem mit zwei GPUs empfehlen:

  1. -sm tensor ausprobieren. Eine Zeile, bei uns gut +60 %, sofort rückgängig zu machen.
  2. Vor MTP llama-bench -p 1,2,3,4 messen. Bei I-Quants auf Mittelklasse-Karten lohnt es sich meist nicht, bei K-Quants fast immer.
  3. Agenten mit langen Läufen testen, nicht nur mit Einzelanfragen, und mehrfach. Unsere schnellste Variante bestand jeden kurzen Test und scheiterte trotzdem im Ernstfall.
  4. Dem Agenten eine Arbeitsweise vorgeben. „Code zuerst, dann iterieren“ machte aus 0 von 120 dreimal hintereinander 119–120 von 120.
  5. Ein objektives Prüfkriterium bauen. OpenCode meldete in einem früheren Lauf „erfolgreich beendet“, obwohl keine Zeile Code existierte. Erst die 120 Fälle machen das sichtbar.
  6. Vor jeder Messung den VRAM prüfen. Ein Dienst, der stündlich neu startet, verfälscht still.
  7. Lange Agentenaufgaben aufteilen. Eine Sitzung für das Ergebnis, eine zweite für Aufräumen und Dokumentation, und die zweite mit einer vorbereiteten Liste statt einer offenen Aufgabe. Zusätzliche Regeln in einer einzigen Anleitung haben bei uns jedes Mal geschadet.

Unsere Produktion heute: ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF (IQ3_S) mit -sm tensor, 47,5 Token/s, gleiche Tool-Call-Qualität wie vorher. Ein Modell für alle Dienste, das jetzt auch ein Release-Binary ohne Symbole in unter einer Stunde zurück in C++ verwandelt.

Nächste Schritte: den Zwei-Sitzungs-Ablauf mit Liste mehrfach wiederholen (Zuverlässigkeit, Prototypen als feste Vorgabe), Stufen B und D des re-lab testen, die Q4_K_M-Variante ohne MTP im Agentenlauf gegenprüfen (Quant oder MTP als Ursache?), --cache-reuse messen und nach llama.cpp-Updates die Matrix erneut laufen lassen. Messskript (mtp-matrix.ps1) und Testlabor liegen bereit.