Nach meiner vLLM-Serie auf dem Dual-A6000-Server wollte ich wissen, wie sich Qwen3.8-27B auf der NVIDIA DGX Spark schlägt. Mein Ziel: ein lokales Modell als Backend für OpenCode, mit dem ich ESP32-Firmware schreiben, bauen und flashen lasse. Der Plan war zunächst vLLM. Am Ende ist es SGLang geworden. Warum, und wie die Installation Schritt für Schritt aussieht, zeige ich in diesem Beitrag.
Warum SGLang und nicht vLLM
Die DGX Spark hat 128 GB Unified Memory. Das klingt viel. Der Flaschenhals ist aber die Speicherbandbreite. NVIDIA nennt 273 GB/s, dass ist im Vergleich zu einer aktuellen NVIDIA GPU wie der Pro 4500 deutlich langsamer. Beim Decode muss das Modell für jedes Token alle Gewichte einmal lesen. Bei 20 GB pro Schritt sind das gut 10 Schritte pro Sekunde. Mehr geht physikalisch nicht.
Der einzige Hebel ist spekulatives Decoding. Ein kleines Draft-Modell schlägt mehrere Tokens vor, das große Modell prüft sie in einem Durchgang. Werden vier Tokens pro Schritt akzeptiert, vervierfacht sich der Durchsatz. Die Qualität bleibt identisch, denn das große Zielmodell bestimmt am Ende das Ergebnis.
Genau hier trennen sich die Engines:
| Engine | Decode (Batch 1) |
|---|---|
| llama.cpp UD-Q4_K_XL + MTP | ca. 27 tok/s |
| vLLM 0.27 NVFP4 + MTP | ca. 24,5 tok/s |
| SGLang NVFP4 + DFlash2 | 38 bis 50 tok/s |
DFlash2 ist ein trainierter Block-Drafter, der seit dem 21. August 2026 das offizielle Rezept im SGLang-Cookbook für die DGX Spark ist. Die Zahlen stammen aus Community-Benchmarks auf identischer Hardware. Quelle: hasso5703/dgx-spark-qwen38 und NVIDIA Developer Forum, nicht selbst gemessen. Den SGLang-Wert konnte ich selbst bestätigen, vLLM und llama.cpp habe ich auf der Spark nicht gemessen.
Die Komponenten
| Komponente | Wert |
|---|---|
| Image | lmsysorg/sglang:dev-qwen38-27b-dflash2 (ca. 14 GB) |
| Zielmodell | RadixArk/Qwen3.8-27B-NVFP4 (ca. 21 GB) |
| Draft-Modell | incoai/Qwen3.8-27B-DFlash2 (ca. 3,6 GB) |
| Spekulation | DFLASH, 8 Draft-Tokens |
| KV-Cache | fp8_e4m3 |
| Port | 30000 |
| Modell-ID | qwen3.8-27b |
Ein Hinweis zum Checkpoint: Das Cookbook nutzt die RadixArk-Quantisierung. Der unsloth-NVFP4-Checkpoint ist für vLLM gebaut. Für SGLang mit DFlash2 ist RadixArk die validierte Variante.
Schritt 1: Docker-Zugriff
Der erste Befehl scheiterte direkt. permission denied while trying to connect to the docker API. Mein User war zwar in der Gruppe docker, meine SSH-Session wusste davon aber nichts. Die Gruppenliste eines Prozesses wird beim Login einmal gesetzt. Spätere Änderungen sieht die laufende Session nicht.
Prüfen daher zu erst, was in der Systemdatei steht:
Befehl: groups ingmar
Prüfen, was die aktuelle Session genau kennt:
Befehl: id -nG
Fehlt docker nur bei id -nG, ist der Fall klar. Entweder komplett neu einloggen oder für die aktuelle Shell:
Befehl: newgrp docker
Befehl: docker ps
Liefert docker ps eine leere Tabelle, funktioniert der Zugriff. Wichtig: newgrp gilt nur für diese eine Shell. Jedes weitere Terminal braucht den Befehl erneut, bis man sich einmal frisch eingeloggt hat.
Schritt 2: Hugging-Face-CLI installieren
Der nächste Stolperstein war PEP 668. Ubuntu 24.04 schützt das System-Python. Ein pip install bricht mit externally-managed-environment ab. Auf der Spark sollte man das System-Python auch in Ruhe lassen, DGX OS hängt mit seinen CUDA-Tools daran. Die saubere Lösung ist pipx.
Befehl: sudo apt install -y pipx
Befehl: pipx install huggingface_hub
Befehl: pipx ensurepath
Befehl: source ~/.bashrc
Befehl: hf --version
So hat es bei mir funktioniert und ich konnte weiter machen mit dem Schritt 3
Schritt 3: Checkpoints laden
Beide Modelle landen im Standard-Cache unter ~/.cache/huggingface. Der Container bekommt diesen Ordner später als Volume. Die Variable HF_HUB_DISABLE_XET=1 schaltet den neuen Xet-Transport ab, der auf manchen Systemen Probleme macht.
Befehl: export HF_HUB_DISABLE_XET=1
Befehl: hf download RadixArk/Qwen3.8-27B-NVFP4
Befehl: hf download incoai/Qwen3.8-27B-DFlash2
Rund 25 GB zusammen. Bei Abbruch einfach erneut starten, der Download setzt fort. Danach die Vollständigkeit prüfen:
Befehl: du -sh ~/.cache/huggingface/hub/models--*
Befehl: find ~/.cache/huggingface/hub -name "*.incomplete"
Erwartet sind rund 21 GB und 3,6 GB. Der zweite Befehl darf nichts ausgeben.
Schritt 4: Docker Compose
Auf meinem A6000-Server betreibe ich vLLM über systemd-Services. Für einen einzelnen Container auf der Spark ist Docker Compose übersichtlicher. Die Konfiguration steht in einer Datei, und restart: unless-stopped sorgt zusammen mit dem aktivierten Docker-Daemon für den Autostart nach einem Reboot.
Befehl: mkdir -p ~/qwen38 && cd ~/qwen38
Befehl: nano docker-compose.yml
Inhalt der docker-compose.yml Datei:
services:
qwen38-sglang:
image: lmsysorg/sglang:dev-qwen38-27b-dflash2
container_name: qwen38-sglang
restart: unless-stopped
network_mode: host
ipc: host
shm_size: 32g
mem_limit: 100g
memswap_limit: 100g
environment:
- HF_HUB_OFFLINE=1
volumes:
- /home/ingmar/.cache/huggingface:/root/.cache/huggingface
- /home/ingmar/.cache/sglang:/root/.cache/sglang
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command: >
sglang serve
--trust-remote-code
--model-path RadixArk/Qwen3.8-27B-NVFP4
--served-model-name qwen3.8-27b
--kv-cache-dtype fp8_e4m3
--mem-fraction-static 0.70
--attention-backend flashinfer
--chunked-prefill-size 2048
--speculative-algorithm DFLASH
--speculative-draft-model-path incoai/Qwen3.8-27B-DFlash2
--speculative-num-draft-tokens 8
--max-running-requests 4
--reasoning-parser qwen3
--tool-call-parser qwen3_coder
--host 0.0.0.0 --port 30000
Ein paar Erklärungen zu den wichtigen Zeilen:
- mem_limit 100g: Harte Obergrenze für den Container. Läuft etwas aus dem Ruder, stirbt der Container, nicht der Host.
- HF_HUB_OFFLINE=1: Der Container telefoniert beim Start nicht nach Hause. Schneller und souveräner.
- mem-fraction-static 0.70: Anteil des Speichers für Gewichte und KV-Cache. Dazu gleich mehr.
- max-running-requests 4: Ohne diesen Wert setzt SGLang mit Spekulation 48 als Default. Das ist auf der Spark viel zu viel.
- reasoning-parser qwen3: Trennt den Thinking-Teil sauber vom Antworttext.
- tool-call-parser qwen3_coder: Nötig für Tool-Calls aus OpenCode.
Container starten:
Befehl: docker compose up -d
Befehl: docker compose logs -f
Beim ersten up zieht Compose das Image automatisch, falls es lokal fehlt. Das sind rund 14 GB. Danach startet die Engine. Sie lädt die Gewichte, tunt die FlashInfer-Kernel und zeichnet die CUDA-Graphs auf. Fertig ist sie bei dieser Zeile:
The server is fired up and ready to roll!
Schritt 5: Verifizieren
Mit dem folgenden Befehl prüfst Du ob der Container läuft?
Befehl: docker compose ps
Jetzt prüfen ob der Server auch antwortet. Dazu den folgenden Befehl ausführen.
Befehl: curl -s http://localhost:30000/health
Befehl: curl -s http://localhost:30000/v1/models | jq .
Jetzt lassen wir das Qwen 3.8 Modell wirklich arbeiten und lassen es passen zum Einsatzzweck ein kleines Programm für den ESP32 schreiben:
Befehl: curl -s http://localhost:30000/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"qwen3.8-27b","messages":[{"role":"user","content":"Schreibe ein ESP32-Blink-Beispiel in Arduino C++."}],"max_tokens":400}' | jq .
Die Antwort enthält zwei Felder. In reasoning_content steht der Thinking-Teil, in content der eigentliche Code. Also die Antwort ist daher etwas länger aber genau das braucht OpenCode.
Jetzt ist es spannend wie sich der Speicher der DGX Spark verhält. Mit dem folgenden Befehlen hälst Du den Speicher im Blick.
Befehl: free -g
Befehl: nvidia-smi
Die gemessenen Werte
Das hat mein erster Lauf ergeben:
| Größe | Wert |
|---|---|
| Gewichte Zielmodell | 21,6 GB |
| Gewichte Draft | 3,5 GB |
| Startzeit | 3:44 min |
| KV-Cache (bei 0.80) | 1,17 Mio. Tokens |
| Kontextlänge | 262.144 Tokens |
| Decode | 37,8 tok/s |
| Akzeptierte Tokens pro Schritt | 3,5 bis 4,0 |
Die Startzeit hat mich überrascht. Die Community nennt rund 9 Minuten. Bei mir waren es unter vier. 158 Sekunden Gewichte laden, 35 Sekunden CUDA-Graphs. Beim zweiten Start geht es noch schneller, weil der Autotune-Cache dann im Volume liegt.
Die 37,8 tok/s stammen aus der ersten echten Anfrage. Bei Code-Aufgaben steigt die Akzeptanzrate meist, weil Code vorhersagbarer ist als freie Prosa. Das entspricht grob dem, was ich auf dem A6000-Server mit vLLM und MTP sehe. Nur eben auf einer Kiste, die unter dem Schreibtisch keinen Lärm macht.
Die Unified-Memory-Falle
Das ist der Punkt, den man kennen muss. SGLangs Speicherbuchhaltung sieht auf dem GB10 25 bis 40 GB transiente Allokationen nicht. Der FlashInfer-Autotuner und die CUDA-Graph-Aufzeichnung allokieren außerhalb des verwalteten Pools. Auf einer Maschine mit Unified Memory bedeutet das: Der Host-Speicher kann gegen null laufen. Dann friert die Spark ein, und nur ein Power-Cycle hilft.
Das Cookbook pinnt --mem-fraction-static 0.80 für Einzelstream auf einer dedizierten Box. Bei 0.85 killt der Dienst earlyoom den Prozess mit Exit -15, ohne Traceback. Sichtbar wird das nur in journalctl -u earlyoom.
Bei mir ließ 0.80 nur 16 GB Reserve. Der Grund: Auf der Spark laufen bei mir GNOME und Firefox mit, die belegen zusammen rund 840 MB GPU-Speicher und einiges an RAM. Ich bin deshalb auf 0.70 gegangen. Der KV-Cache schrumpft auf rund 900.000 Tokens, was für OpenCode immer noch weit mehr ist als nötig. Dafür habe ich 12 GB mehr Luft.
Meine Faustregel:
- 0.80: dedizierte Box, ein Client, kein Desktop
- 0.70: Desktop läuft mit, ein Client
- 0.50: mehrere Clients parallel oder weitere GPU-Jobs
Wenn es möglich ist vielleicht SGLang nicht nativ ohne Docker auf der Spark starten. Die mem_limit im Compose-File ist der Sicherheitsgurt der sich so recht schön eisntellen läßt.
Betrieb im Alltag
Jetzt noch ein paar Befehle die recht hilfreich sind. Hier z. B. zu dem Status und Logs:
Befehl: docker compose ps
Befehl: docker compose logs -f
Neustart nach Änderungen an der Compose-Datei wie z. B. das mem_limit:
Befehl: docker compose up -d
Modell aus dem Speicher entfernen weil ComfyUI oder ein anderer Job den Speicher braucht:
Befehl: docker compose stop
Nach stop kommt der Container auch nach einem Reboot nicht zurück. Erst docker compose up -d aktiviert ihn wieder. Das ist gewollt. Nach einem normalen Reboot ohne stop startet SGLang automatisch, sofern der Docker-Dienst aktiviert ist:
Befehl: systemctl is-enabled docker
OpenCode anbinden
Auf dem Rechner, von dem ich OpenCode nutze, ergänze ich in ~/.config/opencode/opencode.json einen zusätzlichen Provider-Block zu meiner Dual A6000 Konfigurationa:
{
"provider": {
"spark": {
"npm": "@ai-sdk/openai-compatible",
"name": "DGX Spark",
"options": {
"baseURL": "http://SPARK-IP:30000/v1"
},
"models": {
"qwen3.8-27b": {
"name": "Qwen3.8-27B NVFP4 DFlash2",
"limit": {
"context": 262144,
"output": 32768
}
}
}
}
}
}
In OpenCode heißt das Modell dann spark/qwen3.8-27b. Erste Aufgabe wird mein ESP32-Dev-Loop: Firmware schreiben, bauen, flashen, seriellen Output prüfen. Ob die Tool-Calls mit dem Parser qwen3_coder sauber durchlaufen, zeige ich in einem eigenen Beitrag. Falls es hakt, wäre qwen3_xml die Alternative.
Weitere Stolperfallen
- FP8-KV-Warnung. Im Log steht Using FP8 KV cache but no scaling factors provided. Der RadixArk-Checkpoint liefert keine kalibrierten KV-Skalen, SGLang nimmt 1.0. Das Cookbook empfiehlt fp8 trotzdem. Wer bei sehr langen Kontexten Qualitätsverluste sieht, kann auf
--kv-cache-dtype bfloat16wechseln. Kostet die Hälfte des KV-Cache, bleibt aber locker ausreichend. - Batch-1-Maschine. Zwei parallele Generierungen teilen sich den Speicherbus. Jede läuft dann mit halber Geschwindigkeit. Die Spark ist eine Maschine für einen Nutzer.
- Wanderndes Image-Tag. Das Tag
dev-qwen38-27b-dflash2wird upstream weitergeschoben. Wer Reproduzierbarkeit will, pinnt den Digest ausdocker images --digestsin die Compose-Datei.
Alternative: Ein Befehl für alles
Wer nicht manuell gehen will, findet mit hasso5703/dgx-spark-qwen38 einen Installer, der genau dieses Setup als systemd-Service anlegt. Er pinnt Image-Digest und Checkpoint-Revisionen und schreibt eine fertige OpenCode-Config. Ich habe den manuellen Weg gewählt, weil ich jede Zeile verstehen wollte. Für die zweite Spark würde ich vermutlich den Installer nehmen.
Fazit
Der Weg war kürzer als gedacht. Die zwei echten Hürden hatten mit KI nichts zu tun. Eine SSH-Session, die ihre Gruppenmitgliedschaft nicht kannte, und PEP 668 beim pip-Install. Danach lief das Cookbook-Rezept auf Anhieb.
Unter vier Minuten bis zum ersten Token, 38 tok/s Decode, 262k Kontext. Die DGX Spark ist damit ein brauchbares OpenCode-Backend, das auf dem Schreibtisch leise vor sich hin arbeitet. Der Dual-A6000-Server bleibt für Batch-Jobs und parallele Nutzer die bessere Wahl. Für den einen Entwickler, der agentisch an ESP32-Firmware arbeitet, reicht die Spark. Und sie zieht dabei 12 Watt im Leerlauf.
Im nächsten Beitrag geht es an den ESP32-Dev-Loop mit OpenCode. Dann zeigt sich, ob 38 tok/s im agentischen Alltag genügen.
Damit ist die Spark aber noch nicht ausgereizt. Im nächsten Beitrag hole ich Qwen3.8-Flash-Next auf dieselbe Maschine: 176 Milliarden Parameter, davon 6 Milliarden aktiv, mit vLLM und einem Trick, der die 48 GB große n-gram-Tabelle auf der NVMe lässt. Dann habe ich zum ersten Mal beide Engines auf derselben Hardware selbst gemessen. Danach geht es an den ESP32-Dev-Loop mit OpenCode.
Quellen
Folgende Quellen habe ich für diesen Beitrag durchgearbeitet.
- SGLang-Cookbook Qwen3.8-27B: docs.sglang.io
- hasso5703/dgx-spark-qwen38: GitHub
- MiaAI-Lab/Qwen3.8-27B-SGLang-DGX-Spark: GitHub
- NVIDIA Developer Forum, Qwen3.8-27B auf DGX Spark: forums.developer.nvidia.com






Ein toller Guide der leicht zugänglich und verständlich ist. Perfekt für ein kleines Side-Project geeignet. Aktuell half mir noch mein…
Thank you for this great tutorial, could you share n8n workflow and comfyui workflow please?
Hallo Anton, die Meldung besagt das in meinem Beisiel Methoden verwendet werden die veraltet (deprecated) sind. Also müsstest Du die…
Danke für das Tool! Ich habe erst kürzlich angefangen mich mit der Thematik zu beschäftigen und bin für meine Erwartungen…
Hallo, ich habe ihre Anleitung befolgt und bekomme im letzten Schritt leider immer folgende Meldung im Terminal: bash <(wget -qO-…