Note 02 · Inference · Deployment
Llama 3.1 70B auf einer L40S — was wir gemessen haben.
Eine einzige NVIDIA L40S (48 GB), Llama 3.1 70B Instruct, vLLM als Serving-Layer. Wir haben Quantisierungs-Varianten, Batch-Größen und Prompt-Längen gegeneinander vermessen. Diese Note dokumentiert die realen Latenz- und Throughput-Werte aus einem produktiven Customer-Support-Agenten.
Llama 3.1 70B in Q4_K_M passt mit 4k Kontext komfortabel auf eine L40S. Time-to-First-Token: P50 ~480 ms, P95 ~780 ms. Die Decode-Rate liegt bei ~15–20 Token/s pro Stream — eine 200-Token-Antwort ist damit nach ~11–13 s komplett, dank Token-Streaming sieht der Nutzer die ersten Wörter aber praktisch sofort. Bei batch_size=4 und 8k Kontext steigt die P95-TTFT Richtung 1.6 s und die Decode-Rate pro Stream sinkt spürbar. Q8 liefert ~6% bessere Faithfulness gegen 35% mehr VRAM und höhere Latenz — für die meisten Mittelstands-Use-Cases nicht das Geld wert. FP16 ist auf einer einzelnen L40S nicht praktikabel: 4-bit-Quantisierung ist hier kein Kompromiss, sondern die korrekte Wahl.
1 · Hardware und Modell
Die Konfiguration, über die wir reden:
- GPU — NVIDIA L40S, 48 GB GDDR6 ECC, 350 W TDP. Eine Karte, keine NVLink-Verbindung.
- Host — AMD EPYC 9354P, 128 GB RAM, NVMe local. Kein Netzwerk-Storage im Inferenz-Pfad.
- Modell — meta-llama/Llama-3.1-70B-Instruct, quantisiert nach Q4_K_M (~40 GB on disk, ~38 GB in VRAM).
- Stack — vLLM 0.6.x, CUDA 12.4, Python 3.11. Kein TensorRT-LLM, kein Triton — vLLM ist für unsere Workloads schlicht genug und schnell genug.
Use-Case: ein Customer-Support-Agent mit RAG-Kontext (Top-5 Chunks, je ~400 Token), durchschnittliche Antwortlänge 180–240 Token. Die Anforderung: Time-to-First-Token P95 ≤ 1 s — die Antwort wird per Token-Streaming ausgeliefert, entscheidend ist also, wie schnell die ersten Wörter erscheinen, nicht wann das letzte Token fertig ist.
2 · Quantisierung — was wir verglichen haben
Wir haben drei Quantisierungs-Stufen gegen unser Ground-Truth-Set (siehe Note 01) laufen lassen. Identische Prompts, identische Decoding-Parameter (temperature=0.2, top-p=0.9), 200 Token max.
| Variante | VRAM | TTFT P50 / P95 | Faithfulness |
|---|---|---|---|
| Q4_K_M | ~38 GB | ~480 / ~780 ms | 0.891 |
| Q8_0 | ~52 GB ⚠ | — | — |
| FP16 | ~140 GB ⚠ | — | — |
Q8 und FP16 sprengen die einzelne L40S — Q8 fast (würde paged offloading erfordern), FP16 deutlich. Wir haben Q8 separat auf einem Zwei-GPU-Setup (2× L40S, Tensor-Parallelism) gemessen: 0.945 Faithfulness, TTFT bei ~590/950 ms. Das sind +6% Faithfulness gegen ungefähr verdoppelte Hardware-Kosten. Für ein internes Wissens-System überzeugend, für einen Kunden-Chatbot meist nicht.
3 · Batching — wo der Knick liegt
vLLMs Continuous-Batching ist beeindruckend, aber bei großem Kontext (Top-5 RAG-Chunks plus System-Prompt) verschiebt sich das Optimum schnell. Wir haben batch_size 1, 4, 8 gegen 4k und 8k Kontext gemessen — die Werte sind gerundete Bereiche aus mehreren Läufen, keine Punktmessungen:
| Kontext | Batch | TTFT P95 | Decode pro Stream |
|---|---|---|---|
| 4k | 1 | ~0.8 s | ~18–20 Token/s |
| 4k | 4 | ~1.1 s | ~15–17 Token/s |
| 4k | 8 | ~1.5 s | ~12–15 Token/s |
| 8k | 1 | ~1.3 s | ~17–19 Token/s |
| 8k | 4 | ~1.6 s | ~13–16 Token/s |
| 8k | 8 | ~3 s ⚠ | ~10–12 Token/s |
Zur Einordnung: Bei ~15–20 Token/s pro Stream ist eine 200-Token-Antwort nach ~11–13 s vollständig ausgeliefert. Weil wir per Token-Streaming ausliefern, ist das für Nutzer unkritisch — die ersten Wörter erscheinen nach der TTFT, also unter einer Sekunde. Für unseren Use-Case (TTFT P95 ≤ 1 s, ~3–4 parallele Streams in der Spitze) ist 4k Kontext mit batch_size=4 der sweet spot. Bei 8k-Kontext-Anfragen routen wir bewusst auf einen separaten Pool mit niedrigerem Batch, um die TTFT nicht zu sprengen.
4 · vLLM-Konfiguration, die uns Schmerzen erspart hat
# vllm-server.yaml (Auszug)
model: /models/llama-3.1-70b-q4km
quantization: gguf
dtype: auto
# Kontext und KV-Cache
max-model-len: 8192
kv-cache-dtype: fp8 # spart ~30% VRAM, Qualitätsverlust nicht messbar
enable-prefix-caching: true # für RAG-Workloads kritisch
# Throughput
max-num-seqs: 8
max-num-batched-tokens: 32768
gpu-memory-utilization: 0.92 # nicht 0.95 — lässt Headroom für CUDA graphs
# Stabilität
swap-space: 4 # GB
disable-log-stats: false
# Networking
host: 0.0.0.0
port: 8000
api-key: "${VLLM_API_KEY}"
Drei Einstellungen, die wir teuer gelernt haben:
- enable-prefix-caching: true — bei RAG-Workloads ändert sich der System-Prompt und oft sogar große Teile des Kontexts pro Request nicht. Prefix-Caching halbiert die Time-to-First-Token. Ein-Zeile-Änderung mit gewaltigem Impact.
- kv-cache-dtype: fp8 — ein Drittel weniger VRAM, kein messbarer Qualitätsverlust im Eval-Set. Erlaubt uns batch_size=8 statt 6.
- gpu-memory-utilization: 0.92 — der vLLM-Default 0.9 ist konservativ, 0.95 hat bei uns OOM-Crashes ausgelöst, sobald CUDA-Graphs aktiv wurden. 0.92 ist der Wert, der robust läuft.
5 · Wann eine zweite GPU lohnt
Wir empfehlen einem Kunden eine zweite L40S in genau drei Szenarien:
- Q8-Quantisierung ist für den Use-Case nötig (regulatorisch oder messbar bessere Faithfulness > 5%).
- Nachhaltig mehr als ~6–8 parallele Streams, nicht nur in Peaks.
- Active-Active-HA-Anforderung (zwei Server in zwei Brandabschnitten).
In allen anderen Fällen reicht eine einzelne L40S — und der Kunde spart sich rund 9.000 € Hardware plus die Komplexität von Tensor-Parallelism. Wir bauen lieber ein zweites L40S-System für Failover, als zwei zu einer logischen Einheit zu binden.
6 · Was nicht in den TL;DR passt
Drei Dinge, die wir bei einem nächsten Deployment anders machen würden:
- Wir würden früher mit AWQ-Quantisierung experimentieren. Q4_K_M ist gut, aber AWQ liefert in unseren ersten Tests vergleichbare Latenz mit messbar besserer Faithfulness. Die Produktionsstabilität müssen wir noch verifizieren.
- Wir würden System-Prompt-Caching aggressiver nutzen. Im aktuellen Deployment ändert sich der System-Prompt monatlich — das ist Verschwendung.
- Wir würden Speculative Decoding evaluieren. vLLM unterstützt es inzwischen stabil, wir haben es schlicht noch nicht getestet.