Veröffentlicht am

GPU-Workloads in Kubernetes: Scheduling, Sharing und Kosten

Teilen:
Authors

GPU-Workloads in Kubernetes: Scheduling, Sharing und Kostenoptimierung

TL;DR

  • Die durchschnittliche GPU-Auslastung liegt bei vielen Teams unter 30% -- das meiste Geld verbrennt im Leerlauf
  • NVIDIA MIG partitioniert eine A100/H100 physisch in bis zu 7 isolierte Instanzen, ideal fuer Inferenz
  • Time-Slicing teilt eine GPU zeitlich zwischen mehreren Pods, einfach einzurichten, aber ohne Memory-Isolation
  • Der NVIDIA GPU Operator automatisiert das gesamte Setup (Driver, Device Plugin, DCGM Exporter) per Helm Chart
  • Volcano Scheduler oder Kueue sind dem Default-Scheduler fuer Batch-ML-Workloads deutlich ueberlegen

Das Problem: Teure GPUs, niedrige Auslastung

Eine NVIDIA A100 kostet circa 15.000 EUR. Eine H100 liegt bei 30.000 EUR und mehr. Trotzdem zeigen Monitoring-Daten bei vielen Teams eine durchschnittliche Auslastung von 20-35%. Der Grund ist strukturell.

ML-Workloads laufen in Schueben: Training laeuft Stunden bis Tage, dann folgen Phasen der Evaluation und Datenaufbereitung, in denen die GPU idle ist. Inferenz-Workloads brauchen oft nur einen Bruchteil der verfuegbaren Rechenleistung. Und der Default-Kubernetes-Scheduler vergibt GPUs als unteilbare Einheit -- ein Pod bekommt eine ganze GPU oder keine.

Die Loesung liegt in drei Bereichen: besseres Scheduling, GPU-Sharing und systematisches Monitoring.

GPU-Grundlagen in Kubernetes

Bevor wir in die Optimierung einsteigen, hier die Basis. Kubernetes kennt GPUs nicht nativ. Die Integration laeuft ueber Device Plugins.

Installation des NVIDIA GPU Operators per Helm:

# Helm Repo hinzufuegen
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

# GPU Operator installieren (beinhaltet Driver, Device Plugin, DCGM Exporter)
helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --create-namespace \
  --set operator.defaultRuntime=containerd \
  --set mig.strategy=mixed \
  --set devicePlugin.config.name=gpu-feature-config

Der GPU Operator installiert automatisch den NVIDIA-Treiber, das Device Plugin, den DCGM Exporter fuer Metriken und optional MIG-Konfiguration. Nach der Installation koennt ihr GPUs in Pod-Specs anfordern:

apiVersion: v1
kind: Pod
metadata:
  name: cuda-test
spec:
  restartPolicy: OnFailure
  containers:
    - name: cuda-vectoradd
      image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.2.0
      resources:
        limits:
          nvidia.com/gpu: 1

Ohne Device Plugin sieht der Scheduler die GPU-Ressourcen nicht. Das ist der haeufigste Fehler bei der Ersteinrichtung.

GPU-Sharing: Drei Ansaetze im Vergleich

Es gibt mehrere Wege, eine physische GPU zwischen Pods zu teilen. Jeder hat andere Trade-offs.

EigenschaftMIG (Multi-Instance GPU)Time-SlicingMPS (Multi-Process Service)
GPU-ModelleA100, H100, A30Alle NVIDIA GPUsAlle NVIDIA GPUs (Volta+)
IsolationPhysisch (Memory + Compute)KeineNur Compute
Max. Partitionen7 (bei A100)Beliebig (per Config)Beliebig
Memory-IsolationJaNeinNein
Fehler-IsolationJaNeinNein
OverheadMinimalContext-SwitchingGering
Typischer EinsatzInferenz, Multi-TenantEntwicklung, kleine JobsInferenz mit vielen kleinen Modellen
KonfigurationsaufwandHochNiedrigMittel

MIG: Physische Partitionierung

MIG ist der sicherste Ansatz, weil jede Partition eigenen dedizierten GPU-Speicher und eigene Compute-Einheiten bekommt. Ein Out-of-Memory in einer Partition betrifft die anderen nicht.

# MIG-Konfiguration ueber den GPU Operator (ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
  name: mig-config
  namespace: gpu-operator
data:
  config.yaml: |
    version: v1
    mig-configs:
      all-balanced:
        - devices: all
          mig-enabled: true
          mig-devices:
            "3g.40gb": 2
            "1g.10gb": 1
      all-inference:
        - devices: all
          mig-enabled: true
          mig-devices:
            "1g.10gb": 7

Die Konfiguration all-inference teilt eine A100 (80GB) in 7 kleine Instanzen mit je 10GB. Fuer Inferenz-Workloads, die nur 2-4GB VRAM brauchen, ist das ein enormer Effizienzgewinn.

Pods fordern dann spezifische MIG-Partitionen an:

resources:
  limits:
    nvidia.com/mig-1g.10gb: 1

Time-Slicing: Der einfache Weg

Time-Slicing ist deutlich einfacher einzurichten und funktioniert mit jeder NVIDIA-GPU. Der GPU Operator verwaltet die Konfiguration:

# Time-Slicing ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: gpu-operator
data:
  any: |-
    version: v1
    flags:
      migStrategy: none
    sharing:
      timeSlicing:
        renameByDefault: false
        failRequestsGreaterThanOne: false
        resources:
          - name: nvidia.com/gpu
            replicas: 4

Mit replicas: 4 kann jede physische GPU von bis zu 4 Pods gleichzeitig genutzt werden. Der Scheduler sieht statt einer GPU nun vier nvidia.com/gpu-Ressourcen pro Node.

Der Haken: Es gibt keine Memory-Isolation. Wenn Pod A 40GB VRAM belegt und Pod B ebenfalls 40GB anfordert, stuerzt einer von beiden mit OOM ab. Time-Slicing eignet sich daher vor allem fuer Entwicklungsumgebungen oder Workloads mit bekanntem, geringem Memory-Bedarf.

Scheduling: Ueber den Default-Scheduler hinaus

Der Standard-Kube-Scheduler behandelt GPUs wie jede andere Ressource: First-come-first-served. Fuer ML-Workloads ist das ungenuegend. Typische Anforderungen, die der Default-Scheduler nicht abdeckt:

  • Gang Scheduling: Verteiltes Training braucht N Pods gleichzeitig, nicht nacheinander
  • Fair Sharing: Mehrere Teams teilen sich GPU-Nodes, keines soll die anderen aushungern
  • Preemption: Ein dringendes Inferenz-Deployment soll weniger wichtige Training-Jobs verdraengen koennen
  • Bin Packing: GPUs auf moeglichst wenigen Nodes konzentrieren, um die restlichen Nodes herunterzufahren

Kueue: Der Kubernetes-native Ansatz

Kueue ist seit Kubernetes 1.27 der empfohlene Weg fuer Job-Queueing. Es integriert sich nahtlos mit dem bestehenden Scheduler:

# ClusterQueue definiert das Budget
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: a100-flavor
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 8
            - name: "cpu"
              nominalQuota: 64
            - name: "memory"
              nominalQuota: 256Gi
---
# LocalQueue fuer ein Team
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: ml-team-queue
  namespace: ml-training
spec:
  clusterQueue: gpu-cluster-queue

Jobs werden ueber die Queue eingereicht und automatisch eingeplant, sobald Ressourcen verfuegbar sind. Kueue unterstuetzt Fair Sharing zwischen Teams und Preemption basierend auf Prioritaeten.

Monitoring: GPU-Metriken mit DCGM

Ohne Monitoring wisst ihr nicht, ob eure Optimierungen wirken. Der DCGM Exporter (im GPU Operator enthalten) liefert Prometheus-Metriken.

Die wichtigsten Metriken:

# GPU Utilization (Compute) -- Ziel: >70%
DCGM_FI_DEV_GPU_UTIL

# Memory Utilization -- zeigt, ob MIG/Time-Slicing sinnvoll waere
DCGM_FI_DEV_FB_USED
DCGM_FI_DEV_FB_FREE

# GPU Temperature -- fuer Capacity Planning relevant
DCGM_FI_DEV_GPU_TEMP

# Power Usage -- direkt in Kosten umrechenbar
DCGM_FI_DEV_POWER_USAGE

Ein Grafana-Dashboard mit diesen vier Metriken zeigt euch innerhalb einer Woche, wo das groesste Optimierungspotenzial liegt. Typischerweise findet ihr heraus, dass 2-3 Workloads die GPUs dominieren und der Rest nur sporadisch zugreift.

Kostenbetrachtung: Was die Optimierung bringt

Nehmen wir ein konkretes Beispiel. Ein Team betreibt 4 NVIDIA A100 GPUs On-Premise:

PositionOhne OptimierungMit MIG + Kueue
Auslastung (Durchschnitt)28%72%
Effektive GPU-Kapazitaet1.12 GPU-Aequivalente2.88 GPU-Aequivalente
Geplante Neubeschaffung2 GPUs in 6 MonatenNicht noetig in 18 Monaten
Vermiedene Kosten (Hardware)--30.000 EUR
Stromkosten (jaehrlich)8.400 EUR6.200 EUR
Wartezeit auf GPU (P95)4.2 Stunden35 Minuten

Die Hardware-Ersparnis allein rechtfertigt den Aufwand. Dazu kommt die hoehere Entwicklerproduktivitaet: Wenn ML-Engineers nicht mehr Stunden auf freie GPUs warten, sondern in Minuten loslegen koennen, ist das ein erheblicher Produktivitaetsgewinn.

Schrittweiser Rollout

Woche 1-2: GPU Operator installieren, DCGM Exporter einrichten, Baseline-Metriken erheben. Wo steht die aktuelle Auslastung?

Woche 3-4: Time-Slicing fuer Entwicklungs-Namespaces aktivieren. Das ist der schnellste Weg zu mehr Effizienz mit geringstem Risiko.

Woche 5-8: MIG fuer Inferenz-Workloads konfigurieren. Kueue installieren und Training-Jobs auf Queue-basiertes Scheduling umstellen.

Woche 9-12: Ergebnisse auswerten. Grafana-Dashboards fuer GPU-Kosten-Tracking aufbauen. Dokumentation und Runbooks fuer das Team erstellen.

Weitergehende Themen

Fazit

GPU-Optimierung in Kubernetes ist kein Hexenwerk, erfordert aber ein systematisches Vorgehen. Der GPU Operator nimmt euch den groessten Teil der Basisarbeit ab. MIG und Time-Slicing adressieren das Sharing-Problem, Kueue loest die Scheduling-Herausforderungen.

Der wichtigste erste Schritt ist Monitoring: Ohne Daten ueber die aktuelle Auslastung optimiert ihr im Blindflug. Installiert den DCGM Exporter, schaut euch die Zahlen eine Woche lang an, und die sinnvollsten naechsten Schritte werden offensichtlich.

Wenn ihr Hilfe bei der Planung eures GPU-Setups oder bei der Migration bestehender ML-Workloads auf Kubernetes braucht, meldet euch bei uns unter /kontakt.

Kubernetes-Beratung gesucht?

Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen