BLOG

Inference-Plattform: eine bereinigte Fallstudie

Veröffentlicht 7 Min. Lesezeit

Wie eine GitOps-verwaltete vLLM-Inferenz-Plattform tatsächlich funktionierte: ein OpenAI-kompatibler Router vor GPU-Profilen, Hot Swaps mit Readiness-Gate, Streaming-Passthrough, token-genaue Telemetrie und ein Offline-Modellcache.

Diese Fallstudie beschreibt die vLLM-Router-Implementierung aus der Git-Historie der Plattform vom Juni 2026. Sie ist keine Beschreibung aller heute laufenden Komponenten. Die Plattform ist privat; Hostnames, Netzadressen, Ports, Repository-Pfade und Modellinventar bleiben deshalb ausgespart. Der Text trennt implementierte Mechanismen von betrieblichen Abwägungen. Vendor-Dokumentation erklärt diese Mechanismen, ist aber kein Nachweis meines Deployments.

Das Problem, das der Router gelöst hat

Ein kleiner GPU-Satz bedient eine wechselnde Modellflotte: der Release-Takt ist zeitweise wöchentlich, die Konsumenten sind Kommandozeilen-Agents und ein Web-Chat mit OpenAI-kompatibler HTTP-Oberfläche, und jeder Konsument setzt voraus, dass der Endpunkt gültig bleibt, während sich das Modell dahinter ändert. Ein langlebiger Engine-Prozess pro Modell skaliert nicht auf dem Speicher einer einzelnen Workstation; ein Prozess pro Anfrage zerstört die Latenz. Das Auflösungsmuster ist ein Koordinatensystem, das die meisten Serving-Stacks bereits sprechen; deshalb behielt die Plattform den vLLM-OpenAI-kompatiblen Server (öffnet in einem neuen Tab) als Engine und legte einen dünnen, deterministischen Orchestrator davor.

Anforderungen, konkret:

  • Ein OpenAI-kompatibler Endpunkt je GPU-Pool, mit Modellidentität aufgelöst über das model-Feld der Anfrage.
  • Höchstens ein geladener Engine-Prozess je GPU; Speicher ist die knappe Ressource.
  • Änderungen an der Modellflotte werden in der Konfiguration deklariert, über GitOps angewendet, niemals durch interaktive Shell-Arbeit auf dem Serving-Knoten.
  • Requests, Tokens, Latenz, Concurrency, Swaps und aktiver Zustand sichtbar als Metriken, ohne die Offline-Haltung des Serving-Pods zu brechen.

Architektur

        Agents, CLI-Clients, Web-Chat
                     │   OpenAI-kompatibles HTTP, Streaming und nicht-streamend
                     ▼
   ┌───────────────────────────────────────┐
   │           Inference-Router            │
   │  - Modell-zu-Profil-Auflösung         │
   │  - Readiness-Gate und Swap-Steuerung  │
   │  - Streaming-Passthrough              │
   │  - Metriken-Endpunkt + JSON-Accesslog │
   └───────────────────┬───────────────────┘
                       │   Loopback-HTTP, je Pool eine Adresse
          ┌────────────┴────────────┐
          ▼                         ▼
  ┌──────────────────┐      ┌──────────────────┐
  │    GPU-Pool 1    │      │    GPU-Pool 2    │
  │  je ein geladener│      │  je ein geladener│
  │  Engine-Prozess  │      │  Engine-Prozess  │
  └────────┬─────────┘      └────────┬─────────┘
           │ Read-only-Mount         │ Read-only-Mount
           ▼                         ▼
  ┌────────────────────────────────────────────┐
  │   Vorab geladener Modellcache, offline     │
   └────────────────────────────────────────────┘

Zwei Schichten sind getrennt. Der Router ist eine FastAPI-Anwendung je Deployment: Er bedient die OpenAI-kompatible API, hält die Profil-Konfiguration, steuert den Prozesslebenszyklus und exportiert Metriken. Ein Profil deklariert Modellkennung, servierte Namen und Aliasse, lokalen Modellpfad, Kontextlimit, Start-Timeout, Environment und Engine-Argumente. Die Engine ist vLLM als Subprozess mit einem an Loopback gebundenen HTTP-Listener. Modellauflösung aus dem lokalen Cache vermeidet reguläre Hugging-Face-Downloads; sie beschränkt für sich allein keinen beliebigen Netzwerkzugriff.

Bewusste Vereinfachungen: Der Router ist ein einzelner Prozess und hält keinen persistenten Zustand, ein Absturz ist also per Neustart wiederherstellbar, und seine Konfiguration ist ein einzelnes, in Git versioniertes JSON-Dokument. Kein Service Discovery, kein verteilter Zustand, nichts zu reconcilen: die Plattform ist die GPU-Flotte eines Knotens, und der Aufwand, etwas anderes zu behaupten, kauft an dieser Grösse nichts.

Modell-Readiness und Hot Swaps

Der Swap ist die tragende Operation. Sein Vertrag, wie implementiert:

  1. Requests drainen vor dem Swap. Der Pool beginnt keinen Swap, während Anfragen in-flight sind; neue Anfragen für das ankommende Profil warten auf derselben Condition-Variable. Ein Swap tötet nie eine mitten im Stream laufende Anfrage.
  2. Die alte Engine stoppt mit einem graceful Signal und begrenzter Wartezeit, 90 Sekunden, vor dem harten Kill.
  3. Die ankommende Engine startet, danach pollt der Router sie in kurzem festen Intervall, bis sie bereit meldet; ein Prozess, der mitten im Laden stirbt, wird über seinen Exit-Status erkannt und gemeldet.
  4. Readiness ist eine echte Prüfung, kein Port-Probe. Der Router befragt das Models-Endpunkt der Engine und hält sie erst für bereit, wenn die deklarierte Modellkennung tatsächlich in der Antwort erscheint. Das konfigurierte Startzeitfenster (standardmässig 30 Minuten) begrenzt, wie lange ein Cold Load dauern darf.
  5. Genau ein Profil ist je Pool geladen, und ein Status-Endpunkt meldet, welches, was gerade wechselt, die Anzahl in-flight Requests und den letzten Fehler. Die eigene Antwort der Plattform auf „Welches Modell ist gerade wirklich geladen?" ist ein HTTP-Aufruf, keine nvidia-smi-Archäologie.

Die Cold-Start-Zeit entspricht der Engine-Ladezeit: Sekunden für ein kleines quantisiertes Modell, Minuten für einen grossen Vollpräzisions-Checkpoint. Diese Zahl ist eine Eigenschaft der Engine, nicht des Routers, und das Startzeitfenster des Routers gesteht sie ein, statt Swaps als gratis auszugeben.

Der Router lädt beim Start den konfigurierten Standard jedes Pools vor. Passt dieser zum Haupt-Workload, entfällt dessen erstes Laden auf Anfrage. Das ist keine Residenzgarantie: Eine Anfrage für ein anderes Profil kann weiterhin einen Wechsel auslösen. Wechseln Workloads wiederholt zwischen Profilen, bleiben die Kosten des Cold Loads bestehen; Vorladen allein löst diesen Konflikt nicht.

Streaming, exakt erhalten

Streaming-Passthrough ist der Ort, an dem die meisten naiven Proxys brechen; deshalb wurde es als Vertrag erster Klasse behandelt:

  • Der Router streamt Chunks, sobald sie ankommen; er puffert eine SSE-Antwort nie vor dem Weiterleiten.
  • Hop-by-hop-Header werden in beide Richtungen entfernt; alles andere wird unverändert weitergereicht.
  • Bei streamenden Chat-Completions stellt der Router sicher, dass die Antwort Usage enthält. Hat der Client das nicht angefordert, schreibt der Router die Anfrage um und ergänzt die Standardoption, damit die Token-Abrechnung unten auch auf Streaming-Pfaden funktioniert. Nicht-streamende Antworten werden leicht erfasst, um die Usage zu extrahieren; streamende behalten ein begrenztes Ende der letzten etwa tausend Bytes.

Telemetrie: wie der Betrieb aussah

Metriken werden von einem stdlib-only-Modul von Hand als Prometheus-Text gerendert, weil das Serving-Image offline läuft und für ein Expositionsformat keine Client-Abhängigkeit nehmen soll. Das Set war absichtlich klein:

  • Zähler: Requests (je Pool, GPU, Modell, Statusklasse, Streaming-Flag), Prompt-Tokens und Completion-Tokens (je Pool, Modell, Client-Klasse).
  • Ein Request-Dauer-Histogramm mit vernünftigen, groben Buckets.
  • Ein Swap-Zähler (je Pool, von-Profil, zu-Profil).
  • Gauges: aktive Requests und aktuell geladenes Profil.

Das Access-Logging ist eine JSON-Zeile je Request: Pool, Modell, Profil, Client-Klasse, Status, Streaming-Flag, Tokenzahlen, Dauer in Millisekunden. Die Client-Zuordnung löst eine Anfrage zu einer kleinen Menge benannter Konsumentenklassen auf und nutzt sie nur als Metrik-Label; sie ist Buchhaltung, bewusst einfach und ausdrücklich keine Security-Grenze.

Swap-Zähler machen häufige Modellwechsel sichtbar. Dauer- und Concurrency-Metriken helfen, Cold Loads von Serving-Engpässen zu unterscheiden. Diese Signale unterstützen die Diagnose; sie belegen für sich allein nicht die Ursache eines bestimmten Client-Timeouts.

Isolations-Haltung

  • Hugging-Face-Offline-Einstellungen (HF_HUB_OFFLINE=1 und Entsprechendes) halten unterstützte Bibliotheksaufrufe im lokalen Cache. Sie sind keine Egress-Firewall; Netzwerkzugriff muss unabhängig davon kontrolliert werden.
  • Modelle liegen in einem Host-Cache, read-only in den Pod gemountet; nur ein separater, expliziter Download-Schritt mit Schreibrecht füllt ihn.
  • Nur der Router-Endpunkt ist über den Ingress erreichbar; Engine-Prozesse binden an Loopback innerhalb des Pods.
  • Deployment und Profil-Konfiguration sind GitOps-deklariert und aus Git reconciled; es gab keine interaktive Konfigurationsoberfläche auf dem Knoten.
  • Die Network Policy erlaubte Ingress zum Serving-Pod nur vom Ingress-Controller, vom Web-Chat und vom Metriken-Scraper, sodass Konsumenten den Router nicht umgehen konnten.

Hier implementiert gegen generische Praxis

Um die Aussagefläche ehrlich zu halten:

Hier implementiert, wie oben beschrieben Generische Guidance, kein Anspruch
Pool-je-GPU-Fassade, ein Engine-Prozess je Pool, Profil-Auflösung über Name und Alias Ob ein vLLM-Prozess je GPU für eine andere Flotte die richtige Form ist
Drain-vor-Swap mit in-flight-Zählung, begrenzte SIGTERM-Wartezeit, Modell-ID-Readiness-Polling Welche Engine-Versionen oder Scheduling-Policies man fahren sollte
Streaming-Passthrough mit erzwungener Usage-Abrechnung und handgerolltem Metrik-Text Prometheus-Monitoring-Best-Practices
Offline-Serving gegen einen Read-only-Modellcache Jegliche konkrete Kubernetes-Distribution oder ein Speicherprodukt

Grenzen dieser Dokumentation

  • Dies ist ein historischer Implementierungsstand, kein aktuelles Inventar und keine Behauptung, dass spätere Deployments jeden Mechanismus übernommen haben.
  • Es werden keine Durchsatzwerte publiziert: Sie hängen von Engine, Hardware, Batch-Form und Modell ab, und keine der Messungen in den privaten Betriebsaufzeichnungen ist stabil genug, um sie als Plattform-Fakten zu zitieren.
  • Client-Identitäten und jede Detailebene des Netzes bleiben bewusst ausgespart; der Beitrag kann absichtlich nicht benutzt werden, die Plattform zu erreichen.

Nachweise