Case Study

Wo der KV-Cache liegt: Storage-Tiering für LLM-Inferenz unter realem Traffic

Beim LLM-Serving geht der GPU-Speicher lange vor der Rechenleistung aus. Mit echten Konversations- und Produktions-Traces habe ich gemessen, was jede Storage-Ebene unterhalb der GPU einen laufenden Request kostet, und einen geteilten Cache-Tier auf GekkoFS gebaut, über den mehrere vLLM-Instanzen die Arbeit der jeweils anderen wiederverwenden.

Kontext
Forschungsprojekt, JGU Mainz
Rolle
Forschung und Engineering, durchgängig
Fachgebiet
LLM-Inferenz, HPC-Storage, MLOps
Infrastruktur
Mogon-HPC-Cluster (A40, A100), Workstation mit 2x RTX A6000
Modelle
Llama 3.1 8B und 70B, Qwen3.5-35B-A3B
Jahr
2026
Ergebnisse
Latenz
Nachladen schlägt Neuberechnen um Faktor 3,5 bis 4,6

Einen verdrängten Prefix aus dem CPU-Speicher zurückzulesen dauerte auf einer A100 73 ms, ihn neu zu berechnen 256 ms. Je langsamer die GPU, desto größer die Ersparnis, und damit lohnt sich Cache-Tiering gerade auf kostengünstiger Hardware.

Architektur
Die Kosten bestimmt die Latenz, nicht die Bandbreite

Kontrollierte Messreihen haben zwei Eigenschaften getrennt, die früherer Arbeiten immer gemeinsam verändern. Das Serving hat eine Kürzung auf ein Viertel der Plattenbandbreite überstanden, aber eine Millisekunde zusätzliche Latenz pro Operation hat die Zeit bis zum ersten Token um ein Vielfaches erhöht. Die Decode-Latenz blieb unverändert.

Scale-out
Geteilter Cache gleichauf mit privater NVMe

Bei vier vLLM-Instanzen auf zwei Knoten mit einem gemeinsamen GekkoFS-Namespace lädt eine Instanz KV-Daten nach, die eine andere berechnet hat, statt sie neu zu berechnen. Nach dem Tuning liegt der geteilte Tier auf dem Niveau der privaten lokalen Platte.

Tuning
21x schnelleres Warm-Reload auf dem verteilten Tier

Backend-Tuning und eine größere Chunk-Size haben das Nachladen eines Dokuments mit 16k Token auf rund 1,9 s gedrückt, und alle Lastfälle liefen durch. Mit der Standardkonfiguration kamen die größten Fälle nicht ans Ziel.

Die Geschichte

Jedes Token, das ein LLM erzeugt, hängt von den Key- und Value-Vektoren aller vorherigen Token ab. vLLM hält sie in einem KV-Cache, um sie nicht neu zu berechnen, und dieser Cache wächst mit Kontextlänge und Parallelität, bis der GPU-Speicher voll ist. Von dort läuft er in den CPU-DRAM über und danach auf NVMe, und ab da entscheidet der Storage-Pfad mit, wie schnell das erste Token zurückkommt. Veröffentlichte Systeme zeigen, dass Offloading funktioniert. Kaum eines sagt, was der Storage darunter leisten muss.

Die Experimentierplattform habe ich auf vLLM 0.19 mit LMCache als Tiering-Schicht aufgebaut, containerisiert mit Apptainer auf dem Mogon-Cluster und Docker auf einer GPU-Workstation, angetrieben von ShareGPT- und BurstGPT-Traffic. Vierzehn Experimente reichen von einer einzelnen GPU bis zu produktionsnahem Traffic auf einem 70B-Modell mit Tensor-Parallelismus. Die Storage-Variable habe ich mit einem FUSE-Throttling-Shim isoliert, der Bandbreite deckelt und Latenz pro Operation unabhängig davon einspeist, und die Reihenfolge gegen ein dm-delay-Device auf Kernel-Ebene gegengeprüft.

Stapel aus vier Storage-Tiers von GPU-HBM bis zu einem geteilten GekkoFS-Tier, der Prefill-Lesepfad hervorgehoben, der Decode-Pfad am Storage vorbei.
Der KV-Cache läuft stufenweise die Hierarchie hinunter. Die Kosten liegen auf dem Lesepfad während des Prefill, und Decode fasst den Storage nie an.
Experimentpipeline vom Lastgenerator über vLLM und LMCache bis zu einem gedrosselten Disk-Tier, mit getrennten Reglern für Bandbreite und Latenz und einem Metrikpfad.
Der Sensitivitäts-Testbed variiert Bandbreite und Latenz pro Operation unabhängig voneinander, was Vergleiche ganzer Tiers nicht leisten.

Was das bedeutet

Die Ergebnisse verorten die Kosten genau. Das Schreiben des Caches ist im eingeschwungenen Zustand nahezu umsonst, und Decode fasst den Storage nie an, also landet die gesamte Last auf dem Prefill. Innerhalb des Prefill bindet die Latenz pro Operation, die Bandbreite spielt kaum eine Rolle. Das liest sich wie eine Anforderungsliste für jeden Tier unterhalb des Caches: ein Bruchteil der Bandbreite einer einzelnen NVMe genügt, aber jede Millisekunde Round-Trip wird voll bezahlt.

Genau bei vernetztem Storage ist niedrige Latenz schwer zu halten, deshalb habe ich GekkoFS eingebunden, ein Ad-hoc-Dateisystem, das beim Jobstart aus den knotenlokalen NVMes der eigenen Allokation entsteht. Ich habe SharedFSBackend geschrieben, das es als geteilten Disk-Tier an LMCache anbindet. Auf einer einzelnen Instanz verlor es gegen lokale NVMe und las mit rund 0,7 GB/s gegenüber 6 GB/s, ohne etwas, hinter dem sich der Round-Trip verstecken könnte. Sein Wert zeigte sich, sobald mehrere Instanzen hinter einem Load Balancer bedienten. Ein privater Cache bedeutet, dass jede Replik Arbeit neu berechnet, die ihre Nachbarn schon erledigt haben, während ein gemeinsamer Namespace jeden Prefix einmal speichert und jede Replik ihn lesen lässt.

Zwei Knoten mit je zwei vLLM-Instanzen, davor ein Load Balancer, und ein GekkoFS-Namespace über die knotenlokalen NVMes beider Knoten.
Vier Instanzen teilen sich einen Cache-Namespace, sodass ein auf einem Knoten berechneter Prefix auf dem anderen nachgeladen statt neu berechnet wird.

Entwurfsregel

Für eine Inferenzplattform ist die Entwurfsregel konkret. Die heißen Tiers bleiben knotenlokal, und jeder geteilte Tier wird zuerst nach seiner Latenz pro Operation beurteilt, erst danach nach Durchsatz. Ein geteilter Tier verdient seinen Platz, sobald die Flotte mehr als eine Replik betreibt und Nutzer sich Prefixe teilen. Das 8B-Modell, mit dem die meisten Läufe entstanden sind, ist der härteste Fall für einen Gewinn durch Nachladen, die hier gemessene Parity ist also eine Untergrenze. Bei 70B, wo Neuberechnung pro Token deutlich mehr kostet, sollte derselbe Entwurf aus Parity einen Latenzgewinn machen.

AWS-Referenzarchitektur mit GPU-Knoten auf EKS in einer Cluster Placement Group, Instance-Store-NVMe, FSx for Lustre als geteiltem Tier, einem prefix-bewussten Router und Observability.
Referenzarchitektur: wie sich die gemessenen Entwurfsregeln auf AWS-Dienste abbilden.

Technologie

Inferenz
vLLM
vLLM
Serving-Engine mit PagedAttention und Tensor-Parallelismus
LM
LMCache
KV-Cache-Tiering über CPU-Speicher, Platte und geteilte Backends
Hugging Face Hub
Hugging Face Hub
Modell-Hosting für Llama 3.1 und Qwen3.5
Modelle
Llama 3.1 8B Instruct
Llama 3.1 8B Instruct
Single-GPU-Baseline, 128 KiB KV-Cache pro Token
Llama 3.1 70B Instruct
Llama 3.1 70B Instruct
Multi-GPU-Fall mit Tensor-Parallelismus, 320 KiB KV-Cache pro Token
Qwen3.5-35B-A3B (FP8)
Qwen3.5-35B-A3B (FP8)
Long-Context-Fall, hybride Attention mit 20 KiB pro Token
Storage
GK
GekkoFS
verteiltes Ad-hoc-Dateisystem als geteilter Cache-Tier
</>
SharedFSBackend
mein LMCache-Backend für geteilte Dateisystem-Tiers
FS
FUSE throttling shim
unabhängige Bandbreitengrenzen und Latenzeinspeisung
dm-delay
dm-delay
Latenzeinspeisung auf Kernel-Ebene zur Gegenprobe
Compute und Betrieb
Slurm
Slurm
Job-Scheduling auf dem Mogon-Cluster
AP
Apptainer
reproduzierbare Container auf HPC-Knoten
Docker
Docker
Container auf der lokalen GPU-Workstation
NVIDIA A100 and A40
NVIDIA A100 and A40
GPU-Testbeds mit unterschiedlichem Verhältnis von Rechenleistung zu Speicher
AWS-Referenzabbildung
EC2 P4d / P5
EC2 P4d / P5
GPU-Knoten mit lokalem NVMe-Instance-Store
FSx for Lustre
FSx for Lustre
verwalteter geteilter Cache-Tier über Repliken
EFA and cluster placement groups
EFA and cluster placement groups
niedrige Round-Trip-Latenz zum geteilten Tier
EKS with Karpenter
EKS with Karpenter
EKS with Karpenter
Skalierung der GPU-Repliken hinter einem prefix-bewussten Router
AWS ParallelCluster
AWS ParallelCluster
Slurm-basierte Alternative, die das HPC-Setup abbildet
S3 and ECR
S3 and ECR
S3 and ECR
Modellgewichte und Container-Images
Managed Prometheus and Grafana
Managed Prometheus and Grafana
Managed Prometheus and Grafana
SLOs für TTFT und Inter-Token-Latenz
Kern-Stack

Steht bei Ihnen etwas Ähnliches an?

Erzählen Sie mir, was Sie heute betreiben und wo es hakt. Ich melde mich mit einem Vorschlag, wie ich es bauen würde und was ich so lassen würde.

Projekt besprechen