llama.cpp in Docker unter WSL2 mit CUDA

In diesem Guide zeige ich dir, wie ich einen lokalen, OpenAI-kompatiblen llama-server in Docker unter WSL2 mit CUDA-Beschleunigung einrichte, und nehme dich dabei vom grundlegenden Aufbau bis zum ersten funktionierenden Request mit.

Am Ende läuft ein llama-server im Container, der über WSL2 die GPU nutzt, mit genau einem Slot, also -np 1, und mindestens 128k Kontext. Ich brauche diese Größe aus eigener Erfahrung, weil ich einen Coding-Agenten wie OpenCode damit laufen lasse: Bei kleineren Kontexten kommt es zu häufigen Compactions, was das Arbeiten mühsam macht. Ein größerer Kontext wäre schön, passt aber in meinem Setup nicht mehr in den VRAM. Das Setup ist für einen einzelnen lokalen Arbeitsplatz gedacht, nicht für einen Server, an dem mehrere Nutzer gleichzeitig ihre Anfragen stellen.

Den fertigen Endpoint kann jeder Client ansprechen, der die OpenAI-kompatible API versteht, während die Modelle lokal auf deiner eigenen Maschine laufen und keine Cloud benötigen. Der lange Kontext ist dabei kein Selbstzweck. Zusammen mit dem einen Slot ist er der Grund für den gewählten Aufbau, weil der KV-Cache auf einer GPU mit begrenztem VRAM nicht unnötig mehrfach reserviert werden soll.

Voraussetzungen

Du brauchst Windows 11 oder Windows 10 ab Version 21H2 mit WSL2, eine aktuelle NVIDIA-Treiberversion unter Windows und eine NVIDIA-Karte, die CUDA unterstützt und mindestens 10 GB VRAM mitbringt. Ich schreibe das aus der Perspektive meiner GeForce RTX 3080 mit 10 GB, einer Ampere-Karte mit Compute Capability 8.6, wie die NVIDIA-Liste der CUDA-GPUs ausweist. Mit weniger VRAM läuft es ebenfalls, dann passen aber die Beispielparameter am Ende dieses Artikels nicht mehr. Außerdem brauchst du eine WSL2-Distribution, in der du Docker verwenden kannst, und genügend System-RAM, wenn du Modelle oder MoE-Experten teilweise auf die CPU legst.

Der überraschende Teil: Für den reinen Container-Weg installierst du in WSL2 weder einen Linux-NVIDIA-Treiber noch das CUDA-Toolkit. Der Windows-Treiber wird von WSL2 als libcuda.so bereitgestellt, während das offizielle CUDA-Image seine User-Space-Bibliotheken selbst mitbringt. Installiere deshalb in WSL2 weder die Meta-Pakete cuda, cuda-12-x noch cuda-drivers, denn sie versuchen, einen Linux-Treiber neben den WSL-Stub zu setzen. Das NVIDIA CUDA-on-WSL-Handbuch ist an dieser Stelle ungewöhnlich deutlich.

Das CUDA-Toolkit brauchst du erst, wenn du llama.cpp nativ in WSL2 kompilieren willst. Dieses Tutorial geht den Container-Weg und baut llama.cpp nicht selbst. WSL2 reicht den Treiber durch, der Container liefert CUDA Runtime und cuBLAS, und Docker reicht die GPU weiter.

WSL2 vorbereiten

Halte WSL zunächst aus einer Windows-PowerShell aktuell:

wsl --update

Starte danach deine Distribution und prüfe, ob der WSL-Treiberstub vorhanden ist:

nvidia-smi
ls /usr/lib/wsl/lib

nvidia-smi zeigt unter WSL2 nicht exakt dasselbe wie auf einem nativen Linux-System, weil einige NVML-Funktionen, etwa Prozess- und Auslastungsabfragen, eingeschränkt sind. Das ist kein Zeichen für einen kaputten Treiber. Wichtig ist, dass die Karte sichtbar ist und der Windows-Treiber WSL2-GPU-Paravirtualisierung unterstützt. Für Modelle und Compose-Dateien verwende ich einen Pfad innerhalb des Linux-Dateisystems, zum Beispiel /home/$USER/models, nicht /mnt/c/.... Microsoft weist ausdrücklich darauf hin, dass Zugriffe über die Windows-Mounts bei Linux-Workloads deutlich langsamer sein können.

Wenn du CPU-Offload planst, prüfe außerdem das RAM-Limit der WSL2-VM. Standardmäßig bekommt WSL2 ungefähr die Hälfte des Windows-RAM, konfiguriert wird das in %UserProfile%.wslconfig. Für ein 26B-MoE-Modell können allein die Experten viele GiB belegen, sodass das RAM-Limit neben dem VRAM zum Engpass werden kann.

Docker Desktop oder Docker Engine in WSL2?

Entscheide dich für genau einen Docker-Weg. Die Docker-Desktop-Dokumentation zum WSL2-Backend warnt ausdrücklich davor, Docker Desktop und eine separat in der Distribution installierte Docker Engine parallel zu betreiben, weil beide dann um Socket und Daemon konkurrieren.

Docker Desktop mit WSL2-Backend

Docker Desktop ist die Alternative für Leser, die lieber mit einer Oberfläche arbeiten. Es bringt die Integration für Windows und WSL2 mit. Aktiviere unter Settings, General die Option Use WSL 2 based engine und unter Settings, Resources, WSL Integration deine Distribution. Für die GPU-Unterstützung verlangt die Docker-Doku einen aktuellen Windows-NVIDIA-Treiber, nicht das NVIDIA Container Toolkit in deiner Distribution.

Der Vorteil ist weniger Handarbeit und eine Konfiguration, die sich näher an der Windows-Installation orientiert. Die Doku beschreibt nicht im Detail, auf welchem Weg Docker Desktop jede einzelne WSL-Treiberbibliothek mountet, deshalb behandle ich das nicht als magische Garantie, sondern prüfe die GPU direkt mit einem Container.

Docker Engine direkt in der Distribution

Diese Variante passt besser, wenn du Docker bewusst ohne Desktop-GUI betreiben willst oder du dich in einer Linux-Umgebung einfach wohler fühlst, so wie ich. Hier installierst du Docker Engine in der Distribution und zusätzlich das NVIDIA Container Toolkit. Der Windows-Treiber bleibt trotzdem der einzige NVIDIA-Treiber auf dem System.

curl https://get.docker.com | sh
sudo service docker start

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | 
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | 
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo service docker restart

Wenn deine Distribution systemd verwendet, kannst du statt service auch systemctl einsetzen. Microsoft beschreibt die systemd-Aktivierung in der WSL-Dokumentation. Docker Desktop bleibt die bequemere Alternative für Leser, die eine Oberfläche und weniger Handarbeit bevorzugen, während ich den Docker-Daemon direkt in WSL2 nutze.

GPU-Durchreichung prüfen

Da beide Docker-Varianten den Test innerhalb derselben WSL2-Distribution ausführen, verwende ich für beide denselben kurzen GPU-Test:

docker run --rm --gpus all ubuntu nvidia-smi

Wenn hier deine Karte nicht auftaucht, brauchst du noch keinen llama.cpp-Container zu debuggen. Prüfe zuerst Windows-Treiber, wsl --update, die gewählte Docker-Variante und bei Docker Engine die Registrierung des NVIDIA-Runtimes.

Das passende llama.cpp-Image

Das offizielle llama.cpp-Docker-Repository unterscheidet unter anderem zwischen server, full und light. Für einen OpenAI-kompatiblen Endpoint genügt server-cuda oder server-cuda13. Die CUDA-12-Variante verwendet die CUDA-12-Familie, die CUDA-13-Variante die CUDA-13-Familie; die Images enthalten Runtime-Bibliotheken, der Treiber kommt vom Windows-Host.

Beide Image-Familien sind für aktuelle NVIDIA-Karten geeignet. Die Wahl hängt vom Windows-Treiber ab: CUDA-13-Anwendungen benötigen mindestens den R580-Zweig, für neue CUDA-13.4-Funktionen nennt NVIDIA R615 oder neuer; CUDA 12.8 verlangt auf Windows mindestens 570.65. Das steht in den CUDA Release Notes. Ohne klar aktuellen Treiber ist server-cuda die konservativere Wahl. Es gibt keine belastbare offizielle Aussage, dass CUDA 13 auf Ampere schneller oder langsamer wäre.

Die Tags sind entweder schwebend, etwa server-cuda, oder an einen Build gebunden, etwa server-cuda-b4738. Für ein reproduzierbares Setup solltest du einen vorhandenen Build-Tag verwenden, den du zum Zeitpunkt des Downloads aus der GHCR-Registry auswählst. Die GPU-Images werden laut llama.cpp-Doku derzeit allerdings nicht über die normale CI-Laufzeit getestet, sondern nur gebaut.

Modellwahl für 10 GB VRAM

Bei 10 GB entscheidet nicht allein die GGUF-Datei, sondern vor allem der KV-Cache. Die folgende Auswahl nennt die tatsächlich aus den Hugging-Face-Repositories ausgelesenen Dateigrößen, nicht geschätzte Modellgrößen:

ModellDateiGrößeEinordnung
Qwen2.5-7B-InstructQ5_K_M / Q4_K_M5,44 / 4,68 GBDie angenehmste Allround-Wahl, weil nur vier KV-Heads den Cache klein halten.
Qwen3-8BQ5_K_M / Q4_K_M5,85 / 5,03 GBModernere Generation, bei 32k und q8-KV noch mit Luft.
Llama 3.1 8B InstructQ5_K_M / Q4_K_M5,73 / 4,92 GBGute Referenz mit großem Ökosystem.
Gemma 3 12B ITQ4_K_M / Q5_K_M7,30 / 8,45 GBSliding-Window-Attention hält den Cache vergleichsweise klein.
Mistral Nemo 12BQ4_K_M7,48 GBFür Deutsch interessant, aber 128k verlangt KV-Quantisierung.
Qwen2.5 14B InstructIQ4_XS / Q4_K_S8,12 / 8,57 GBNur mit q8-KV und ungefähr 8k Kontext vernünftig.
Qwen3 14BQ4_K_M9,00 GBGrenzfall, der wenig Raum für Cache und Compute lässt.
phi-4 14BIQ4_XS7,94 GBMit dem schwersten KV der Liste, deshalb nur mit kleinem Kontext.

Die Faustregel lautet: 7B bis 8B laufen in Q5 oder Q6 mit großem Kontext gut, 12B bis 14B brauchen meist Q4 und weniger Kontext. Ein 14B-Q4_K_M mit 8,99 GB liegt auf einer 10-GB-Karte bereits rechnerisch zu dicht am Rand. Die Zahlen für den KV-Cache sind Rechenwerte aus den Modellkonfigurationen, keine Messungen.

KV-Cache ist der eigentliche Engpass

Der KV-Cache speichert die Zwischenergebnisse der bereits verarbeiteten Tokens, damit das Modell sie beim nächsten Token nicht erneut berechnen muss. Seine Quantisierung legt fest, mit wie wenigen Bits diese Cache-Werte gespeichert werden, und ist etwas anderes als die Quantisierung des Modells beziehungsweise seiner Gewichte.

Der Speicherbedarf lässt sich als Richtwert so zerlegen:

VRAM ≈ GGUF-Datei + KV-Cache + Compute-/Aktivierungspuffer + Reserve

KV-Cache = 2 × Layer × KV-Heads × Head-Dimension × Kontext × Bytes pro Wert

F16 benötigt ungefähr zwei Byte pro Wert, q8_0 ungefähr ein Byte und q4_0 ungefähr ein halbes Byte, jeweils ohne Quantisierungs-Overhead. Bei Llama 3.1 8B entsprechen 128k mit F16-KV laut Hugging Face 15,62 GB. Genau deshalb ist KV-Cache-Quantisierung hier kein Feintuning, sondern der Unterschied zwischen “128k passt” und “der Prozess stirbt beim Start”.

Für 10 GB setze ich den Cache explizit und symmetrisch:

-ctk q8_0 -ctv q8_0

Symmetrisch ist wichtig, weil der CUDA-Build standardmäßig optimierte Flash-Attention-Kernel für die Paare q4_0/q4_0, q8_0/q8_0, f16/f16 und bf16/bf16 baut. Andere Kombinationen können mit einer Warnung auf einen weniger passenden Kernel zurückfallen. q4_0 spart noch mehr VRAM, kostet aber Qualität, deshalb ist q8_0 mein Ausgangspunkt.

Aktuelle Builds setzen -ngl standardmäßig auf auto und --fit standardmäßig auf on. llama.cpp passt nicht gesetzte Werte an den verfügbaren Speicher an und lässt standardmäßig 1024 MiB pro Device als Zielmarge. Für ein Tutorial setze ich die wichtigen Werte trotzdem explizit, damit ein späterer Build nicht stillschweigend einen kleineren Kontext wählt. -ngl 99 bedeutet praktisch alle Layer, -c oder --ctx-size setzt den Kontext, -b die logische Batch-Größe und -ub die physische Batch-Größe. Bei OOM prüfe ich zuerst das Startlog. Danach schalte ich -ub herunter und verifiziere den tatsächlichen Compute-Buffer, statt eine unbelegte Speicherformel als Messwert auszugeben.

Ein 26B-MoE-Modell auf 10 GB: Gemma 4 26B A4B

Die hier verwendete Datei stammt aus dem Repository unsloth/gemma-4-26B-A4B-it-GGUF und nutzt die Quantisierung UD-IQ3_XXS mit 3,0625 Bit pro Gewicht. Sie ist 10,62 GiB groß und enthält 25,23 Milliarden Parameter, von denen etwa 4 Milliarden aktiv sind. Die folgenden Speicher- und Geschwindigkeitswerte beziehen sich auf genau diese Quantisierung; mit einer anderen Quantisierung werden sie abweichen. Gemma 4 26B A4B hat 128 Experten, von denen acht pro Token aktiv sind. Die dichten Teile bleiben auf der GPU, die Experten können in den Systemspeicher. Bei 128k Kontext entscheidet deshalb nicht der VRAM über die Machbarkeit, sondern vor allem der verfügbare System-RAM und seine Bandbreite.

128k Kontext passt auf einer RTX 3080

Die Messung bestätigt: Mit allen Experten im RAM belegt Gemma 4 26B A4B bei 128k Kontext 6.564 MiB VRAM. Abzüglich des bereits laufenden Desktops bleiben praktisch nur etwa 0,5 bis 2 GB. Die Experten von zwölf Layern brauchen allein rund 3,5 GiB. Bei 128k ist also kein Platz, um Experten-Layer zurück auf die GPU zu holen. Für 128k bleibt deswegen -ncmoe 30 die Empfehlung. Die schnellere Aufteilung mit 18 Layern auf der CPU ist eine Option für kurze Kontexte: Der Benchmark misst sie mit 512 bis 2.048 Token Promptlänge und 128 Token Generierung, wo der KV-Cache fast nichts belegt und die 3,5 GiB Experten Platz haben.

Sliding-Window-Attention hält den KV-Cache klein

25 der 30 Layer arbeiten mit einem Fenster von 1024 Tokens, nur fünf behalten den vollständigen Kontext. Bei -np 1 -ub 1024 kostet der SWA-Anteil konstant etwa 212,5 MiB mit q8_0; der Anteil der fünf Layer mit vollständigem Kontext wächst bei 128k auf 1360 MiB. Zusammen sind das 1572,5 MiB. --swa-full würde dagegen allein für den SWA-Cache rund 13,6 GiB benötigen und ist auf 10 GB keine Option.

Tensor-Shifting mit -ot

-ot steht für --override-tensor und wurde mit PR #11397 eingeführt. Die Syntax lautet <Tensorname-Muster>=<Buffer-Typ>; das Regex in C++ wird mit regex_search auf GGUF-Tensornamen angewandt, bei mehreren Regeln gewinnt der erste Treffer.

-ot ".ffn_(up|down|gate|gate_up)_(ch|)exps.weight=CPU"
-ot "exps=CPU"

CPU und CUDA0 sind Zieltypen, exps ist nur das Muster. Gemma 4 fusioniert Gate und Up in ffn_gate_up_exps; ein Muster mit nur ffn_up_exps trifft diesen Tensor nicht.

--cpu-moe und --n-cpu-moe

--cpu-moe verschiebt alle MoE-Experten auf die CPU, praktisch wie --n-cpu-moe 30 oder -ot "exps=CPU". --n-cpu-moe N verschiebt dagegen die ersten N Layer, also blk.0 bis blk.(N-1). Die verbreitete Aussage, der Schalter zähle von den höchsten Layern abwärts, ist falsch. Für aktuelle Builds ersetzt --load-mode none beziehungsweise -lm none das alte --no-mmap.

Was das Layer-Regex wirklich tut

-ot "blk.(1?[0-9]|20).=CUDA0,exps=CPU"

blk.(1?[0-9]|20). trifft exakt Layer 0 bis 20. Weil die Regel vor exps=CPU steht, bleiben deren Experten auf der GPU; nur Layer 21 bis 29 gehen auf die CPU.

Eine vollständige 128k-Konfiguration

Für die Erstinbetriebnahme empfehle ich die gemessene UD-IQ3_XXS-Variante:

-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-IQ3_XXS 
-c 131072 -np 1 -ngl 999 -ncmoe 30 
-fa on -ctk q8_0 -ctv q8_0 -b 2048 -ub 512 
--load-mode none --cache-ram 0 -fit off --fit-print on

Der Benchmark mit -ncmoe 30 und allen Experten auf der CPU misst bei 128k 6.564 MiB VRAM und 20,4 Token pro Sekunde. Bei 32k sind es in derselben Konfiguration 5.084 MiB und 20,6 Token pro Sekunde. Das sind 1.480 MiB mehr für den vierfachen Kontext, während die Tokenrate praktisch gleich bleibt.

Messbedingungen

Messbedingungen: Gemessen wurde auf einer NVIDIA GeForce RTX 3080 mit 10.240 MiB VRAM und Treiber 610.57.01 (KMD 610.88, CUDA UMD 13.3), unter Windows mit WSL2 und Kernel 6.6.87.2-microsoft-standard-WSL2. Im Einsatz waren Docker 29.4.3, NVIDIA Container Toolkit 1.19.1 sowie llama.cpp 0.5.0-dev, Build 11151, Commit bd4f514db. Für den Benchmark kam das Image full-cuda zum Einsatz, für die Serverläufe server-cuda. Die Hardware stellte 24 CPU-Threads und 32 GB RAM bereit. Für die Durchsatzmessung galten -ngl 999, -fa 1, KV q8_0, -p 512,2048, -n 128 und -r 3. Die Serverläufe liefen mit -np 1, -fa on und KV q8_0.

Wichtig für die Einordnung: Der Desktop belegte vor dem ersten Serverlauf 1.697 MiB VRAM. Beim zweiten Lauf waren es 3.218 MiB, weil noch ein Container lief. Von den 10.240 MiB war dadurch jeweils deutlich weniger nutzbar; in der Praxis lagen etwa 8,5 bis 9,5 GB für llama.cpp an. Deshalb wirken Werte um 9.8xx MiB nur dann widersprüchlich, wenn man die nominelle VRAM-Größe mit der tatsächlich freien Menge verwechselt.

Durchsatzmessung mit llama-bench

Gemessen wird der eingeschwungene Betrieb: Die Prompts mit 512 beziehungsweise 2.048 Token sind lang genug, dass die Anlaufkosten nicht ins Gewicht fallen, und die Generierung läuft über 128 Token. Wer mit sehr kurzen Prompts misst, bekommt Werte, die vom Startverhalten dominiert werden und nichts über den Dauerbetrieb aussagen. Die Modellgröße beträgt 5,07 GiB beim 7B-Modell beziehungsweise 10,62 GiB beim 26B-A4B-Modell mit 3,0625 bpw und 25,23 B Parametern.

Modellpp512pp2048tg128
Qwen2.5-Coder-7B-Instruct Q5_K_M4.786,38 ± 170,714.726,80 ± 7,97111,77 ± 0,21
Gemma 4 26B A4B UD-IQ3_XXS, alle 30 Layer Experten auf CPU486,07 ± 11,35470,15 ± 5,8521,12 ± 3,49
Gemma 4 26B A4B UD-IQ3_XXS, 18 Layer Experten auf CPU797,99 ± 17,02734,35 ± 4,3132,85 ± 4,82

Einheiten sind Token pro Sekunde. Das Verhältnis von Prompt-Verarbeitung zu Generierung liegt beim 7B-Modell bei rund 43 zu 1 und beim MoE bei rund 23 zu 1. Prompt-Verarbeitung ist also auch mit ausgelagerten Experten ein Vielfaches schneller als die Generierung; sie hängt nur weniger stark an der GPU als beim reinen GPU-Betrieb.

Serverlauf, Speicher und Kontext

KonfigurationVRAM unter LastGen t/s
7B, 128k, alles GPU9.822 MiB108,4
Gemma 26B --cpu-moe, 128k6.564 MiB20,4
Gemma 26B --cpu-moe, 32k5.084 MiB20,6

Der direkte Vergleich derselben Konfiguration mit 128k und 32k zeigt: 6.564 gegenüber 5.084 MiB bedeuten 1.480 MiB mehr für den vierfachen Kontext, während die Generierungsrate mit 20,4 beziehungsweise 20,6 Token pro Sekunde praktisch gleich bleibt. Der KV-Cache ist damit ein reiner Speicherposten, kein Geschwindigkeitsposten. Die Prompt-Raten dieser kurzen Servermessung verwende ich nicht zur Bewertung.

VRAM budget for a 10 GB RTX 3080
Die VRAM-Rechnung: Die Modelldatei bleibt konstant, während der KV-Cache mit der Kontextlänge wächst.

Speculative Decoding mit MTP

Speculative Decoding lässt das Modell mehrere mögliche nächste Token vorhersagen und prüft diese Vorschläge anschließend in einem Durchgang. Stimmen die Vorschläge, werden mehrere Token auf einmal übernommen; die Ausgabe bleibt dabei unverändert. In diesem Lauf kam der MTP-Kopf aus demselben Repository und die Datei mtp-gemma-4-26B-A4B-it.gguf zum Einsatz. Die folgenden Läufe liefen mit 32k Kontext, sofern nicht anders angegeben.

KonfigurationGen t/sAkzeptanzmittlere LängeGewinn
Baseline ohne MTP20,39
MTP, --spec-draft-n-max 226,240,6092,221,29×
MTP, --spec-draft-n-max 421,230,4202,661,04×
MTP, n-max 4, --spec-draft-p-min 0,7525,950,6702,701,27×
MTP, n-max 2, 128k25,850,6092,221,27×

MTP bringt real 1,27× bis 1,29×, ohne Qualitätsverlust und ohne ein zweites Modell. Der Kopf ist 0,46 GB groß; der Speicherbedarf steigt von 5.084 auf 5.689 MiB, also rund 600 MiB.

--spec-draft-n-max 2 ist hier besser als 4: 26,24 gegenüber 21,23 t/s. Die Akzeptanz fällt von 61 auf 42 Prozent, wenn vier Token auf einmal geraten werden. Die Voreinstellung 2 aus der Unsloth-Dokumentation ist für diesen Lauf also die richtige Wahl. Mit --spec-draft-p-min 0,75 steigt n-max 4 von 21,23 auf 25,95 t/s, weil Blöcke mit unsicherer Vorhersage vorzeitig abgebrochen werden; die Akzeptanz steigt dadurch auf 0,670, den besten Wert im Lauf.

Bei 128k läuft MTP und bringt denselben Gewinn wie bei 32k. Der in der llama.cpp-Dokumentation zu Speculative Decoding beschriebene Fehler mit dem Draft-Kontext bei großem --ctx-size tritt hier nicht auf. Die Kombination aus MTP und 18 Layern auf der GPU ergibt 34,97 t/s, verändert aber zwei Dinge gleichzeitig: Gegenüber 32,85 t/s ohne MTP bei derselben Layer-Aufteilung bleiben davon nur rund 6 Prozent als MTP-Anteil übrig. Der Zusatznutzen von MTP wird kleiner, je mehr Experten im VRAM liegen. Diese Layer-Aufteilung ist bei 128k nicht verfügbar.

So startest du den Lauf:

llama-server -hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-IQ3_XXS 
  --cpu-moe -ngl 999 -c 32768 -np 1 
  --cache-type-k q8_0 --cache-type-v q8_0 -fa on 
  --spec-type draft-mtp --spec-draft-n-max 2

Docker-Start für ein normales 7B-Modell

docker run -d --name llama-server 
  --gpus all 
  --restart unless-stopped 
  -p 8080:8080 
   ghcr.io/ggml-org/llama.cpp:server-cuda13 
    -hf unsloth/Qwen2.5-Coder-7B-Instruct-128K-GGUF:Q5_K_M 
    -ngl 99 -c 32768 
    -ctk q8_0 -ctv q8_0 
    -fa on -b 2048 -ub 512 -np 1 
    --host 0.0.0.0 --port 8080

Für CUDA 12 ersetzt du server-cuda13 durch server-cuda. Der Hugging-Face-Cache liegt innerhalb der Distribution. Hinweise zum Download findest du in der Hugging-Face-Download-Doku; verwende dafür nicht /mnt/c.

Compose-Variante

services:
  llama-server:
    image: ghcr.io/ggml-org/llama.cpp:server-cuda13
    container_name: llama-server
    restart: unless-stopped
    ports:
      - "8080:8080"
    gpus: all
    command:
      - -hf
      - unsloth/Qwen2.5-Coder-7B-Instruct-128K-GGUF:Q5_K_M
      - -ngl
      - "99"
      - -c
      - "32768"
      - -np
      - "1"
      - -ctk
      - q8_0
      - -ctv
      - q8_0
      - -fa
      - on
      - --host
      - 0.0.0.0
      - --port
      - "8080"

Compose unterstützt gpus: all als kurze Schreibweise. Wenn du die klassische Deploy-Syntax bevorzugst, sieht der GPU-Teil so aus:

deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: all
          capabilities: [gpu]

capabilities ist dabei Pflicht, und count und device_ids schließen sich aus. Beide Varianten führen zur gleichen Device-Anfrage des Docker-Daemons.

Test gegen den OpenAI-kompatiblen Endpoint

curl http://localhost:8080/health

curl http://localhost:8080/v1/chat/completions 
  -H "Content-Type: application/json" 
  -d '{
    "model": "local",
    "messages": [{"role": "user", "content": "Erkläre in einem Satz, warum -np 1 VRAM spart."}],
    "temperature": 0.2,
    "max_tokens": 80
  }'

Das Server-Image bringt außerdem einen Healthcheck auf /health mit. Es enthält auch eine Web- beziehungsweise Chat-Oberfläche, wie die Server-Dokumentation beschreibt; dafür brauchst du nicht das full-Image. Wenn die Antwort nicht kommt, prüfe zuerst docker logs llama-server, den Hugging-Face-Modellcache und ob du wirklich --gpus all gesetzt hast.

Stolperfallen

  • GPU im WSL-Container unsichtbar: Entweder fehlen Windows-Treiber oder WSL2-GPU-Unterstützung, oder Docker Desktop und Docker Engine wurden vermischt.
  • could not select device driver: Bei Docker Engine fehlt meist das NVIDIA Container Toolkit oder der Daemon wurde nach nvidia-ctk runtime configure nicht neu gestartet.
  • Linux-Treiber in WSL installiert: Entfernen und den Windows-Treiber verwenden; WSL erwartet den durchgereichten Stub.
  • OOM trotz passender Dateigröße: KV-Cache, Compute-Buffer und Reserve wurden vergessen; zuerst q8-KV setzen, Kontext prüfen, -ub reduzieren und das Ladeprotokoll lesen.
  • 14B startet nur mit sehr kleinem Kontext: Die GGUF-Datei verbraucht den größten Teil der 10 GB, deshalb bleiben nur q8-KV und ungefähr 8k sinnvoll.
  • MoE-Regex verschiebt die falschen Tensoren: Bei -ot gewinnt der erste Treffer; exps=CPU muss vor einem Layer-Muster stehen, wenn alle Experten auf die CPU sollen.
  • MoE startet, aber RAM geht aus: Experten liegen auf der CPU, zusätzlich kommen gepinnte Host-Buffer und der Prompt-Cache hinzu.
  • Altes --no-mmap wird abgelehnt: Aktuelle Builds verwenden --load-mode none.
  • Kontext schrumpft scheinbar: --fit passt nicht gesetzte Parameter automatisch an; beim manuellen Test setze ich -fit off und lasse mir mit --fit-print die Schätzung ausgeben.
  • Modelle sind langsam: Liegen sie unter /mnt/c, bremst der Cross-OS-Dateizugriff; verschiebe sie in das Linux-Dateisystem.

Fazit

Die Messwerte machen die Entscheidung konkret: Der vierfache Kontext kostet 1.480 MiB zusätzlichen Speicher, bremst die Generierungsrate aber praktisch nicht. Das MoE-Modell bleibt mit allen Experten auf der CPU speichersparsam, während MTP die Generierung je nach Konfiguration auf 25,85 bis 34,97 Token pro Sekunde beschleunigt.

Eine RTX 3080 mit 10 GB ist für llama.cpp in Docker unter WSL2 keineswegs nutzlos. Sie erfordert nur andere Prioritäten als ein großer Linux-Server: 7B und 8B laufen mit q8-KV und großem Kontext angenehm, 12B und 14B sind mit Q4 und kleinerem Kontext möglich, und ein MoE wie Gemma 4 26B A4B kann mit CPU-Experten sogar 128k Kontext erreichen. Der Knackpunkt ist dabei nicht das Image, sondern der KV-Cache, der bei einem Slot nicht unnötig vervielfacht werden darf.

Das MoE-Verfahren ist kein kostenloses Upgrade, sondern ein Tausch: mehr System-RAM und Prompt-Zeit gegen ein großes Modell mit viel Kontext. Für einen einzelnen lokalen Nutzer kann das sinnvoll sein, wenn ein großer Kontext wichtiger ist als maximale Geschwindigkeit. Wer mehrere Nutzer, hohe Parallelität oder konstante Serverleistung möchte, braucht mehr GPU-Speicher.

0 Kommentare

    Hinterlasse einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.