diff --git a/CHANGELOG.md b/CHANGELOG.md index 1463e60..ca37c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Per-profile chat streaming and chat-request timeouts: `stream`, `read_timeout` and `connect_timeout` are now resolvable in `[default]`/`[model.]` (CLI `--stream`/`--read-timeout`/`--connect-timeout` still override). +- Multimodal support: `mmproj` and `mmproj_offload` config keys (CLI `--mmproj`, + `--no-mmproj-offload`) pass a vision projector to the server. The path resolves + under `hf_home` like `model_path`, and a configured-but-missing projector now + fails with exit code 3 — for `--change` before the running container is removed. +- `docs/KI_TOOLS_PROFILES.md`: which local model suits which task, plus mmproj + compatibility per base model. +- `docs/EVAL_RUBRIC.md` and `scripts/eval_prompt_tests.py`: scoring rubric and an + evaluator for the manual prompt-test archive that measures length compliance and + **executes** the model-generated code against its own pytest suite. +- `scripts/run_prompt_suite.sh`: runs a prompt domain against a running profile and + stores the outputs under the archive's naming convention. +- `[model.qwen35base]` profile: the non-abliterated Qwen3.6-35B-A3B (bartowski + imatrix Q4_K_M) as a control for the abliteration comparison. Inherits sampling, + `ctx_size` and cache types from `[default]`; only model, container, port and GPU + differ, so it runs alongside the production server. ### Changed - The chat request now honors the configured `(connect, read)` timeout instead of a hard-coded 30 s; the default `read_timeout` is 600 s so slow reasoning models are no longer cut off mid-generation. +- **Breaking:** `--print-effective-config` is now an action in the mutually + exclusive action group instead of a flag, and it skips the `docker_available()` + check. `--print-effective-config --start` is therefore an argparse error + (exit 2) rather than a config dump followed by a real container start. + +### Fixed +- `--print-effective-config` combined with an action silently performed that + action. Documented as a diagnostic command (README, installation guide, manual), + `--print-effective-config --config … --start` printed the resolved config and + then ran `do_start()`, replacing a running container with the `[default]` model. ## [0.1.0] - 2026-07-07 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cb1feb2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +pip install -e ".[dev]" # dev install +scripts/check.sh # full local CI gate (ruff + mypy + pytest) — mirrors .forgejo/workflows/ci.yml +python -m pytest -q # tests +python -m pytest tests/test_prompt_urls.py -q # one file +python -m pytest tests/test_actions.py::test_do_start -q # one test +ruff check src/ tests/ build_archive.py +mypy # config in pyproject.toml (files = src/llamacppctl) +``` + +Always use `python -m pytest`, never the `pytest` console script: the test modules do +`from tests.test_docker_ops import ...`, which needs the repo root on `sys.path`. + +`git config core.hooksPath .githooks` (once per clone) enables the pre-push hook that runs +`scripts/check.sh`. CI runs on Python 3.10 and 3.12; `requires-python = ">=3.10"`. + +Other entry points: +- `python3 build_archive.py` — builds and self-verifies `llamacppctl-installable.tar.gz` + (has its own required-file manifest; adding a top-level file that must ship means updating it). +- `SMOKE_GPU=1 scripts/smoke.sh` — opt-in end-to-end test against a real Docker + GPU + GGUF model. + Not part of the pytest suite. + +## Architecture + +A single CLI (`llamacppctl`) that starts/checks/stops/switches llama.cpp servers running as Docker +containers, plus a security-hardened input layer for system/user prompts from text, file, or URL. + +`main.py` is a thin pipeline: parse (`cli.build_parser`) → validate (`cli.validate_args`) → +`docker_available()` → resolve prompt sources (`prompt_io`) → resolve config +(`config.resolve_effective_config`) → dispatch to `actions.do_*`. + +Layering is deliberate and must be preserved — it keeps the security-critical input validation +independently testable from orchestration: +- `prompt_io.py` knows nothing about argparse; it works only with `PromptSource` + `InputPolicy`. +- `config.py` knows nothing about Docker or HTTP. +- `docker_ops.py` / `http_ops.py` know nothing about CLI semantics. +- `actions.py` orchestrates; `schema.py` holds the shared dataclasses (`ServerConfig`, + `PromptConfig`, `ChatReply`, `CheckResult`). + +Two cross-module couplings to know about: +- `main._resolve_prompts()` stashes loaded prompts on the argparse namespace as + `args._resolved_system_prompt` / `args._resolved_user_prompt`; `config.py` picks them up there. +- `main.run()` calls `docker_available()` *before* config resolution, so `--dry-run` still + needs a reachable Docker daemon. Only `--print-effective-config` skips that check — it is a + standalone action in the mutually-exclusive group, never a modifier, so it cannot start + anything. + +### Config resolution + +INI file (`llama.cpp.config`, template in `llama.cpp.config.example`), lowest to highest priority: +builtin defaults (`config.builtin_defaults()`) → `[default]` → `[model.]` (via `--profile`) +→ CLI overrides. `[prompt.]` sections resolve separately and only supply a *fallback* system +prompt — an explicit `-s`/`--system-file`/`--system-url` always wins. + +`container_name` is mandatory (empty → `ConfigError`). It is the only identity anchor: Docker +`--name`, the `--stop`/`--check` target, and the derived lock path +`/tmp/llamacppctl..lock`. Profiles meant to run in parallel need distinct +`container_name` *and* `host_port`, otherwise a second `--start` silently replaces the first +container. + +`max_tokens` / `chat_temperature` / `connect_timeout` / `read_timeout` affect only the chat request +(`--chat` and the reply printed after `--start`), never the container. + +### Exit codes (`actions.py`) + +`0` ok · `1` general/config/prompt error · `2` argparse validation · `3` model path missing · +`4` docker error · `5` HTTP not ready (also `--check` failing) · `6` lock busy. +`--check` is meant to be scriptable; keep those codes stable. + +## Invariants worth not breaking + +- All Docker calls go through `subprocess.run()` with **argument lists** — never `shell=True`, + never composed shell strings. +- The port publishes to `127.0.0.1` by default (`docker_ops._port_publish()`); the llama.cpp + OpenAI endpoint is unauthenticated unless `api_key` is set. `--expose` binds all interfaces. +- Every HTTP request in `prompt_io` — including each redirect hop — passes `validate_url_target()`: + HTTPS-only, no IP literals, no embedded credentials, no `localhost`, all resolved addresses + classified (private/loopback/link-local/multicast/reserved/unspecified → reject), metadata IP + `169.254.169.254` blocked. `_pin_dns()` then restricts `getaddrinfo` to the already-validated IPs + for the duration of the request, closing the TOCTOU/DNS-rebinding window. Size limits are enforced + both from `Content-Length` and while streaming. +- Escape hatches (`--allow-private-url`, `--allow-insecure-http`, `--allow-ip-host`, + `--allow-symlinks`, `--follow-redirects`, `--allow-html-input`) are opt-in and off by default. + `--allow-private-url` and `--url-allow-host` are mutually exclusive. +- `--start` and `--change` hold an exclusive non-blocking `fcntl.flock`; `--change` validates the + model path *before* removing the running container. `--dry-run` takes no lock and touches nothing. + +Tests mock `subprocess` and `requests` (and DNS resolution for the SSRF cases); nothing in the +pytest suite requires Docker or the network. + +## Conventions + +- Docs, comments in `docs/`, and the example prompts are in German; source code, docstrings, and + commit subjects are in English. Commits follow Conventional Commits (`feat(chat):`, `docs(prompts):`). +- Line length 100, `from __future__ import annotations` in every module, `py.typed` is shipped — + keep annotations complete enough for `mypy` to pass. +- Behavior changes should be reflected in `man/llamacppctl.1`, `CHANGELOG.md`, and — for security- + relevant ones — `docs/SECURITY_AND_OPERATIONS.md`. diff --git a/docs/EVAL_RUBRIC.md b/docs/EVAL_RUBRIC.md new file mode 100644 index 0000000..4b0c9ef --- /dev/null +++ b/docs/EVAL_RUBRIC.md @@ -0,0 +1,108 @@ +# Bewertungsrubrik für Modell-Prompt-Tests + +Zweck: Modellwechsel (anderes Modell, andere Quantisierung, `Balanced` statt +`Aggressive`, MTP, KV-Cache-Einstellungen) sollen an **Zahlen** entschieden +werden, nicht an Bauchgefühl. Ohne Vorher-Nachher-Messung ist nicht feststellbar, +ob eine Änderung verbessert oder verschlechtert hat. + +Grundlage sind die Läufe unter `~/llamacppctl_prompt_tests/` gegen die Prompts in +`example_user_prompts/`. Modelleignung siehe [`KI_TOOLS_PROFILES.md`](KI_TOOLS_PROFILES.md). + +## Zwei Ebenen + +**Objektiv** — von `scripts/eval_prompt_tests.py` automatisch gemessen, keine +Meinung nötig: + +| Metrik | Domäne | Bedeutung | +|---|---|---| +| Wortzahl, Abweichung vom Ziel | prosa, reden | Der Prompt nennt ein Ziel („etwa 1200 Wörtern"). | +| Trunkierungsverdacht | alle | Text endet ohne Satzzeichen → Budget zu klein. | +| Tests laufen durch | coding | Der generierte Code wird **wirklich ausgeführt**. | +| Anteil bestandener Tests | coding | `passed / (passed + failed)`. | + +**Subjektiv** — von Hand oder per LLM-Judge, Skala 1–5. Die Anker unten sind +bewusst so formuliert, dass 3 „brauchbar, aber mit Mängeln" bedeutet; ein +durchschnittlich guter Text landet nicht automatisch bei 4. + +## Skala + +| Punkte | Bedeutung | +|---|---| +| 5 | Kriterium durchgängig erfüllt; kein Eingriff nötig. | +| 4 | Erfüllt, mit einzelnen Schwächen, die den Gesamteindruck nicht tragen. | +| 3 | Brauchbar, aber erkennbare Mängel; Überarbeitung nötig. | +| 2 | Kriterium überwiegend verfehlt; Grundgerüst vorhanden. | +| 1 | Verfehlt. | + +## Prosa + +1. **Show, don't tell** — Innenleben ausschließlich über Handlung, Gegenstand, + Wahrnehmung. *1 = benannte Gefühle („sie war traurig"); 5 = kein einziges + benanntes Gefühl, Zustand trotzdem eindeutig.* +2. **Schlussbild** — konkretes, bedeutungstragendes Bild statt Moral oder + Zusammenfassung. *1 = explizite Lehre; 5 = Bild, das die Geschichte trägt.* +3. **Ton- und Registertreue** — hält die geforderte Tonlage (z. B. „nüchtern, + ohne Pathos") über den gesamten Text. +4. **Sprachliche Präzision** — konkrete Substantive, keine Füllattribute, keine + Klischees. +5. **Komposition** — Aufbau trägt; kein Leerlauf, kein abrupter Abbruch. +6. **Sprachrichtigkeit (Deutsch)** — Grammatik, Kasus, Idiomatik. *Eigener + Punkt, weil abliterierte, primär englisch trainierte Modelle hier auffällig + sind.* + +## Reden + +1. **Argumentative Substanz** — trägt der Gedankengang, oder reiht er Behauptungen? +2. **Vorweggenommene Einwände** — wird der stärkste Gegeneinwand benannt und + beantwortet, nicht der schwächste? +3. **Benannte Zielkonflikte** — werden echte Interessengegensätze offen + ausgesprochen statt harmonisiert? +4. **Rhetorische Mittel** — bewusst und sparsam eingesetzt (Trikolon, Anapher, + Antithese), nicht dekorativ. +5. **Adressatenbezug** — Sprache, Beispiele und Anrede passen zum Publikum. +6. **Sprachrichtigkeit (Deutsch)** — siehe oben. + +## Coding + +1. **Korrektheit** — *primär aus dem automatischen Testlauf.* Ein Modul, dessen + eigene Tests durchfallen, kann in diesem Kriterium nicht über 2 kommen. +2. **Anforderungsabdeckung** — sind alle Punkte des Prompts umgesetzt? +3. **Fehlerbehandlung** — die im Prompt geforderten Fehlerfälle, sauber getrennt. +4. **Testqualität** — decken die Tests die genannten Fälle ab, und sind sie + konsistent zum eigenen Code (richtiger Exception-Typ, Fixtures erfüllen das + Schema)? +5. **Keine Halluzinationen** — keine erfundenen APIs, Signaturen, + Framework-Aussagen oder Benchmark-Messwerte. *Automatisch nicht prüfbar; + erfundene Messwerte sind in den bisherigen Läufen wiederholt aufgetreten.* +6. **Lesbarkeit** — Benennung, Struktur, Kommentardichte. + +## Durchführung + +- **Blind bewerten.** Die Modellzuordnung steckt im Dateinamen; beim Scoren + ausblenden (`scripts/eval_prompt_tests.py --anonymize` schreibt anonymisierte + Kopien mit Zufalls-IDs und eine Auflösungstabelle). +- **Ein Kriterium über alle Texte**, nicht ein Text über alle Kriterien. Das + hält den Maßstab konstant. +- **LLM-Judge:** möglich gegen den lokalen Server, aber der Judge darf **nicht** + das bewertete Modell sein. Ein Modell bewertet seine eigenen Texte zu gut. + Der Judge ersetzt die menschliche Stichprobe nicht — mindestens 20 % der Texte + gegenlesen und die Übereinstimmung prüfen. +- **Mindestens zwei Läufe pro Zelle**, weil bei `temp > 0` ein Einzellauf wenig + aussagt. Für Coding-Vergleiche `--seed` fixieren. + +## Was die Zahlen nicht sagen + +Ein bestandener Testlauf beweist, dass der Code *seine eigenen* Tests besteht — +nicht, dass er korrekt ist. Wenn Modell und Test aus derselben Halluzination +stammen, sind beide konsistent falsch. Kriterium 4 (Testqualität) ist deshalb +nicht redundant zu Kriterium 1, sondern dessen Korrektiv. + +## Sicherheitshinweis + +`scripts/eval_prompt_tests.py` führt **modellgenerierten Code aus**. Das ist der +Sinn der Übung, aber es ist Codeausführung aus einer nicht vertrauenswürdigen +Quelle. Der Runner arbeitet in einem temporären Verzeichnis, erzwingt ein +Zeitlimit und verweigert den Start als `root`. Er bietet **keine** Netz- oder +Dateisystem-Isolation. Wer die Läufe auf einem Rechner mit Produktionsdaten +auswertet, sollte `--no-exec` verwenden oder das Skript in einem Container +starten. diff --git a/docs/KI_TOOLS_PROFILES.md b/docs/KI_TOOLS_PROFILES.md new file mode 100644 index 0000000..674a580 --- /dev/null +++ b/docs/KI_TOOLS_PROFILES.md @@ -0,0 +1,169 @@ +# Modellprofile — Eignung der lokalen GGUF-Modelle + +Dieses Dokument beschreibt, **wofür** sich die Modelle unter +`$HF_HOME/models/qwen3/` jeweils eignen, unabhängig von den Beispiel-Prompts in +`example_system_prompts/`. Grundlage sind die Modellkarten auf Hugging Face +(Stand Juli 2026) sowie die eigenen Testläufe unter `~/llamacppctl_prompt_tests/`. + +> Die Angaben zu Fähigkeiten und Benchmarks stammen, wo nicht anders vermerkt, +> **von den Modellautoren selbst** und sind nicht unabhängig verifiziert. + +## Übersicht + +| Datei | Profil | Basis | Primäre Eignung | +|---|---|---|---| +| `ornith-1.0-35b-Q4_K_M.gguf` | `ornith35b` | Qwen3.5-MoE | Agentisches Coding | +| `Carnice-Qwen3.6-MoE-35B-A3B-Q4_K_M.gguf` | `carnice` | Qwen3.6-35B-A3B | Agenten-/Tool-Calling-Workflows | +| `Qwopus3.6-35B-A3B-v1-Q4_K_M.gguf` | `qwopus` | Qwen3.6-35B-A3B | Reasoning, Vision, Tool-Calling | +| `Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf` | `[default]` | Qwen3.6-35B-A3B | Kreatives Schreiben ohne Verweigerungen | +| `Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf` | `qwen27b` | Qwen3.6-27B (dense) | Wie oben, kleinerer VRAM-Bedarf | +| `mmproj.gguf` | — | Qwen3.6-35B-A3B | Vision-Projektor (kein eigenständiges Modell) | + +Alle 35B-A3B-Modelle sind MoE mit **3 B aktiven Parametern** pro Token: hoher +Durchsatz bei 21 GB VRAM-Bedarf. Das 27B ist dense — langsamer pro Token, aber +nur ~15 GB. + +## Ornith-1.0-35B — agentisches Coding + +Quelle: [`deepreinforce-ai/Ornith-1.0-35B-GGUF`](https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B-GGUF) (MIT) + +Ein dediziertes Coding-Modell aus einer selbstverbessernden Modellfamilie. Das +Trainings-Framework erzeugt per RL nicht nur Lösungs-Rollouts, sondern optimiert +auch das treibende Scaffold gemeinsam mit ihnen. Beworben mit SOTA unter +vergleichbar großen Open-Source-Modellen auf Terminal-Bench 2.1, SWE-Bench, +NL2Repo und OpenClaw. Reasoning-Modell: die Assistant-Antwort beginnt mit einem +``-Block. + +**Einsetzen für:** Code-Generierung, Refactoring, Repo-weite Aufgaben, +Agenten-Loops mit Tool-Calls. +**Nicht einsetzen für:** Prosa und Reden — dafür ist das Sampling im Profil +(`temp = 0.20`) bewusst deterministisch gewählt. + +Beachte: Basis ist **Qwen3.5**-MoE (`qwen3_5_moe`), nicht 3.6. Die lokale +`mmproj.gguf` passt deshalb *nicht* zu diesem Modell. + +## Carnice-Qwen3.6-MoE-35B-A3B — Agenten-Workflows + +Quelle: [`samuelcardillo/Carnice-Qwen3.6-MoE-35B-A3B`](https://huggingface.co/samuelcardillo/Carnice-Qwen3.6-MoE-35B-A3B) + +QLoRA-Finetune von Qwen3.6-35B-A3B, trainiert auf echten Execution-Traces der +Hermes-Agent-Runtime. Das erklärte Ziel ist, dem Modell die konkreten +Konversationsmuster beizubringen, die ein Agenten-Runtime erwartet — nicht +generisches Reasoning. + +**Einsetzen für:** Tool-Calling-Pipelines, Agenten-Orchestrierung (z. B. n8n), +strukturierte Mehrschritt-Abläufe. + +## Qwopus3.6-35B-A3B-v1 — Reasoning + Vision + +Quelle: [`Jackrong/Qwopus3.6-35B-A3B-v1`](https://huggingface.co/Jackrong/Qwopus3.6-35B-A3B-v1) + +Reasoning-verstärkter Finetune, dreistufiges SFT mit progressiv steigender +Reasoning-Komplexität. Unterstützt Vision und Tool-Calling. + +**Einsetzen für:** Aufgaben mit langer Gedankenkette, multimodale Eingaben. +**Vorbehalt:** Der Autor kennzeichnet das Modell ausdrücklich als experimentell, +ohne vollständige Performance- oder Sicherheitsevaluation. + +Für reines Coding gibt es die separate Variante +[`Qwopus3.6-35B-A3B-Coder-MTP`](https://huggingface.co/Jackrong/Qwopus3.6-35B-A3B-Coder-MTP-GGUF) +(thinking-off, 62,4 % auf einem 300-Fall-SWE-Bench-Lauf) — lokal nicht vorhanden. + +## HauhauCS Uncensored (35B-A3B und 27B) — kreatives Schreiben + +Quellen: [35B-A3B](https://huggingface.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive), +[27B](https://huggingface.co/HauhauCS/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive) + +**Abliterierte** Modelle: keine neuen Fähigkeiten, entfernte Verweigerungen +(Modellkarte: „0/465 Refusals"). Alle Quants mit imatrix erzeugt. Beide sind +multimodal. + +**Einsetzen für:** Prosa, Reden, Dialog, Rollenspiel — Themen, bei denen ein +aligntes Modell abbricht. +**Nicht einsetzen für:** Coding. Eigene Testläufe zeigen wiederkehrend Tests, die +inkonsistent zum selbst erzeugten Schema sind, erfundene Framework-Aussagen und +Benchmarks mit erfundenen Messwerten. + +Zwei Punkte, die in der aktuellen Konfiguration relevant sind: + +1. Die Karte des 27B empfiehlt selbst, dass **99,9 % der Nutzer die + `Balanced`-Variante** nehmen sollten statt `Aggressive`. Beide erreichen + dieselbe Refusal-Rate; `Balanced` bietet zusätzlich stabileres Sampling für + agentic coding, Tool-Use und Reasoning. `Aggressive` spart lediglich + Vorreden. Aktuell sind beide lokalen HauhauCS-Modelle `Aggressive`. +2. Die Behauptungen „lossless" und „zero capability loss" stammen vom Autor der + Modellkarte. Abliteration kostet in aller Regel *etwas* Fähigkeit. + +## Empirische Befunde (eigene Läufe) + +Getestet gegen das Default-Modell (HauhauCS 35B-A3B), Reasoning aktiv, ctx 262144: + +- **Längenvorgaben werden nicht präzise getroffen** (−27 % bis +26 % über sechs + Läufe). Länge als Spanne vorgeben oder im zweiten Turn nachjustieren. +- **Reasoning frisst das Antwortbudget.** Bei `--max-tokens 10000` endeten + schwere Aufgaben in `finish_reason=length`. Für Prosa/Reden/Dialog + `--max-tokens` ≥ 16000 wählen. +- **`--chat` ohne `--stream`** läuft bei langem Output in den Read-Timeout. + Immer `--stream` verwenden (jeder Token setzt den Timeout zurück). +- **Coding: Struktur gut, Korrektheit unzuverlässig.** Generierten Code und + generierte Tests immer wirklich ausführen. + +Rohdaten: `~/llamacppctl_prompt_tests/`. + +## Vision (mmproj) + +Die lokale `mmproj.gguf` (902 MB) ist der Vision-Encoder des **Basismodells +Qwen3.6-35B-A3B** (aus dem unsloth-Repo). GGUF-Metadaten: + +``` +general.architecture = clip +general.name = Qwen3.6 35B A3B +clip.projector_type = qwen3vl_merger +clip.has_vision_encoder +``` + +Ein mmproj ist kein eigenständiges Modell, sondern projiziert Bild-Embeddings in +den Textmodell-Raum. Er muss zum Vision-Tower der jeweiligen Basis passen. + +| Modell | mmproj.gguf passt? | +|---|---| +| HauhauCS 35B-A3B | ja (Basis Qwen3.6-35B-A3B) | +| Qwopus3.6-35B-A3B | ja (dieselbe Basis) | +| Carnice-Qwen3.6-MoE-35B-A3B | ja (dieselbe Basis) | +| `ornith-1.0-35b` | **nein** — Basis Qwen3.5-MoE, eigener Projektor nötig | +| HauhauCS 27B (dense) | **nein** — anderer Vision-Encoder | + +Verwendung über `llamacppctl` (siehe `[model.vision]` in `llama.cpp.config`): + +```bash +llamacppctl --start --config llama.cpp.config --profile vision +``` + +Das Profil setzt `mmproj = models/qwen3/mmproj.gguf`; der Pfad wird — wie +`model_path` — relativ zu `hf_home` aufgelöst und als `--mmproj` an den +llama.cpp-Server durchgereicht. Der Projektor wird standardmäßig auf die GPU +ausgelagert; `mmproj_offload = false` bzw. `--no-mmproj-offload` verhindert das, +wenn der VRAM knapp ist. + +Bilder gehen anschließend über den OpenAI-kompatiblen Endpunkt +`/v1/chat/completions` als `image_url`-Content-Part. `llamacppctl --chat` sendet +derzeit nur Text; für Bild-Requests den Endpunkt direkt ansprechen. + +## MTP / Speculative Decoding (nicht lokal vorhanden) + +Für Ornith, Carnice und Qwopus existieren **MTP-Quants** mit eingebettetem +Multi-Token-Prediction-Head. Mit hinreichend aktuellem llama.cpp erlauben sie +self-speculative Decoding ohne separates Draft-Modell (`--draft-mtp`); +Community-Berichte nennen etwa doppelten Decode-Durchsatz bei ~2,5 % größerer +Datei. Die im pyproject gepinnte Image-Version muss das unterstützen. + +## Quellen + +- +- +- +- +- +- +- +-