Ein Türsteher für die Grafikkarte
- Matt
Bei mir wollen inzwischen mehrere Programme lokale Modelle benutzen. WhipRadio braucht ein Sprachmodell für Moderationen, ein Musikmodell für Songs und eine Stimme für die Ansagen. Daneben laufen Chats, ab und zu Bilder. Jedes dieser Modelle hat seinen eigenen Server, und alle wollen dieselbe Grafikkarte.
Solange immer nur eins läuft, geht das. Sobald zwei gleichzeitig wollen, gibt es Out of Memory, Modelle werden doppelt geladen, und niemand weiß, was gerade dringend ist. Jede App hatte angefangen, ihre eigene Logik zum Entladen zu bauen. Das skaliert nicht.
Ein Vermittler statt Absprachen
Whipyard sitzt als HTTP-Gateway vor den eigentlichen Servern, also vor llama-server, dem Bildserver, dem Musikserver. Die Apps merken davon fast nichts: Sie sprechen weiter die API, die sie ohnehin sprechen, meist die OpenAI-kompatible, und ändern nur die Basis-URL. Whipyard entscheidet dann:
- Wer zuerst dran ist. Jede Anfrage hat eine Priorität, wartende Anfragen werden mit der Zeit wichtiger, damit nichts verhungert. Optional gibt eine App eine Frist mit, bis wann die Arbeit begonnen haben muss. Eine Ansage, die in zwei Minuten laufen soll, schlägt eine Bildgenerierung für später.
- Was geladen ist. Whipyard lädt und entlädt Modelle selbst, damit alles Geladene in den Speicher passt. Den Speicherbedarf misst es, statt ihn sich angeben zu lassen.
- Was bei langen Aufgaben passiert. Eine App kann eine Anfrage asynchron abgeben und das Ergebnis später abholen. Die Aufträge stehen in SQLite und überleben einen Neustart.
Gebaut ist das in .NET 10 mit Minimal APIs, YARP als Proxy und einem Dashboard in Blazor Server.
Zwei Entscheidungen, die ich wieder so treffen würde
Das Gateway wiederholt nie selbst. Für normale HTTP-Clients ist ein automatischer Retry gute Praxis. Hier nicht: Wenn eine Songgenerierung scheitert, nachdem sie schon fünf Minuten die Grafikkarte belegt hat, ist ein zweiter Versuch ohne Rückfrage schlimmer als der Fehler. Die einzige Ausnahme sind Abstürze, die Whipyard selbst verschuldet hat, weil es zu viel gleichzeitig geladen hatte. Solche Aufträge kommen einmal wieder an ihren alten Platz in der Warteschlange.
Der Scheduler hat keine Ein- und Ausgabe. Der Kern bekommt Ereignisse und eine austauschbare Uhr und gibt Entscheidungen zurück, sonst nichts. Dadurch kann jedes Verhalten als deterministische Simulation getestet werden: zehn Aufträge, drei Modelle, eine Frist, und am Ende muss genau eine bestimmte Reihenfolge herauskommen. Ohne diese Tests hätte ich mich an viele Änderungen nicht herangetraut.
Der Haken
Ein Vermittler löst nur, was die Apps ihn lösen lassen. Wer Anfragen einzeln und synchron hintereinander schickt, zwingt ihn zu schlechten Entscheidungen. Und die Messung des Speicherbedarfs hat ihre eigenen Fallen. Beides hat mich im Betrieb mehr Zeit gekostet als der eigentliche Scheduler.
Außerdem ist das Ganze bisher auf NVIDIA und den Hauptspeicher zugeschnitten, andere Grafikkarten kann es noch nicht messen. Warum ich überhaupt llama-server direkt betreibe, steht im Beitrag zum Umstieg von Ollama. Der Proxy-Teil basiert auf YARP.