Zur Erforschung von Deep-Learning-Algorithmen, die sich neue Fähigkeiten selbstständig aneignen, betreibt Open AI einige der größten Kubernetes-Cluster weltweit mit bis zu 36000 CPU-Kernen. Mitarbeiter Jonas Schneider schildert die Praxiserfahrungen mit der Containerverwaltung.
Open AI ist eher ein untypisches Software-Unternehmen [1]. Die gemeinnützige, mit Privatmitteln finanzierte Forschungseinrichtung mit Sitz in San Francisco beschäftigt rund 60 Mitarbeiter, die an Machine Learning und künstlicher Intelligenz arbeiten. Konkret untersuchen meine Kollegen, wie Computer neue Verhaltensmuster durch Erfahrung erlernen, ohne absichtsvoll auf eine Aufgabe programmiert zu sein. Zu den Geldgebern gehören Elon Musk (Space X, Tesla, [2]) und Sam Altman (Y-Combinator [3]).
Containerschule
Zu den Früchten der Arbeit zählen unter anderem akademische Veröffentlichungen im Web und Vorträge auf Konferenzen sowie frei verfügbare Software für Forscher und Entwickler. Der Artikel zeigt, wie Open AI seinen Kubernetes-Cluster in der Praxis aufstellt, um verteilt auf Tausenden von Rechnern Experimente zum Thema künstliche Intelligenz zu betreiben.
Go deep
Sein Hauptaugenmerk legt das Unternehmen auf Deep Learning, also das Erforschen großer neuronaler Netze mit vielen Schichten. In den letzten Jahren hat Deep Learning viel Bedeutung gewonnen, da es extrem komplizierte Probleme approximativ lösen kann.
Das von Google Deepmind entwickelte Bot-Programm Alpha Go hat zum Beispiel gelernt, das chinesische Brettspiel Go zu spielen. Go gilt als extrem kompliziert, mit einer weitaus größeren Vielfalt an Zügen als Schach. Go-Experten waren sich deshalb lange einig, dass es noch mindestens 20 bis 30 Jahre dauern werde, bis Computer die besten menschlichen Go-Spieler schlagen. Tatsächlich besiegte die Deep-Learning-Software Alpha Go bereits im Frühjahr 2016 den damals besten Go-Spieler Lee Sedol und 2017 gar ein Team aus fünf Weltmeistern.
Open AI erforscht hingegen Algorithmen, die anders als Alpha Go nicht nur eine einzelnes Spiel, sondern ein breites Spektrum von Aufgaben lernen. So entwickelte das Unternehmen vor Kurzem einen Roboter, der einen Menschen ein einziges Mal dabei beobachtet, wie er eine ihm bis dahin unbekannte Aufgabe erledigt. Dann ist Fetch, so der interne Name, in der Lage, die Aufgabe in einer neuen Umgebung zu wiederholen (Abbildung 1, [4]). Niemand muss dem Roboter dafür Konzepte wie Zugreifen oder Stapeln beibringen – er lernt sie, indem er Menschen beobachtet.

Abbildung 1: Der Roboter (links) lernt, eine von einem Menschen erstellte Struktur (rechts) mit neuen Ausgangspositionen der Blöcke nachzubilden.
Trotz der bisherigen Erfolge von Google, Facebook, Open AI und diversen Universitätsprojekten weiß noch niemand, wie der ideale Aufbau eines solchen neuronalen Netzwerks aussieht. Offen ist etwa die Frage, mit welcher Struktur ein Rechnernetz am besten neue Fähigkeiten erlernt. Der Großteil der Forschung im Unternehmen besteht daher aus Experimenten mit neuartigen Lern-Algorithmen und Netzwerkstrukturen, die den Einsatz einer komplexen technischen Infrastruktur voraussetzen. Open AI erforscht dabei Strukturen, wie sie Abbildung 2 zeigt, und will herausfinden, wie sich die Daten durch diese bewegen.

Abbildung 2: Neuronales Netzwerk zur Steuerung des Roboters. Das Bild der Kamera im Kopf des Roboters “bewegt sich” von links nach rechts, am Ende steht die Bewegung des Roboter-Arms.
Bare-Metal-Performance
Um produktiv zu forschen, muss das Team in der Lage sein, neuartige Algorithmen und Netzwerkarchitekturen in Sekunden auf Tausende Rechner zu verteilen und die Ergebnisse auszuwerten. Das verlangt eine extrem flexible Infrastruktur, konkret sehen die Projektanforderungen so aus:
- Performance ist lebenswichtig: Das Berechnen neuronaler Netze erfordert ziemlich massive Rechenpower, weshalb das Projekt beispielsweise GPUs verwendet.
- Rechner-Topologien müssen anpassbar sein: Das Projekt muss unvorhergesehene Änderungen an der Rechnerstruktur umsetzen können und bei Bedarf zum Beispiel eine ursprünglich zentralisierte Berechnung auf Hunderte Rechner verteilen.
- Die Nutzung schwankt rapide: Nicht selten probiert ein Forscher zwei Monate lang neue Methoden auf einem einzigen CPU-Kern aus, braucht dann aber plötzlich für zwei Wochen 10000 CPU-Kerne.
Um die bestmögliche rohe Performance zu erreichen, braucht das Projekt direkten Zugriff auf die Hardware, sowohl auf die CPU als auch auf Beschleuniger wie GPUs. Moderne Hypervisoren wie Xen führen CPU-Code zwar nativ aus, aber alle I/O-Vorgänge verursachen einen Verwaltungs-Overhead. Insbesondere für die Kommunikation mit GPUs über den PCIe-Bus benötigt Open AI jedoch die maximale Bandbreite. Klassische Virtualisierung kommt für das Unternehmen daher nicht in Frage.
Eine große Bare-Metal-Serverfarm wäre auf der anderen Seite ein Alptraum für die Benutzer und Admins. Statt uns auf einen Cloudprovider festzulegen, verwenden wir alle möglichen: Microsoft Azure, AWS, GCE und unsere eigenen Rechenzentren. Grund sind unter anderem Kostenersparnisse bei Mengenrabatten für Tausende von Rechnern. Die meisten unserer Forscher sind allerdings keine Linux- oder Cloud-Experten; drei oder mehr Cloud-APIs zu erlernen kommt für sie nicht in Frage. Sie brauchen Mittel an die Hand, um ihre Ideen selbstständig umsetzen zu können, ohne Ingenieure um Hilfe bitten zu müssen.
Um diesen Spagat zwischen Benutzerfreundlichkeit und Performance zu meistern, setzt das Unternehmen auf Docker-Container [5] und Kubernetes [6]. Container erlauben es, sämtliche Abhängigkeiten und Pakete (wie zum Beispiel Tensorflow [7]) für ein Experiment zu beschreiben. Das Containerabbild (Image) lässt sich als Schnappschuss speichern und übers Netzwerk verschicken, ähnlich wie eine virtuelle Maschine (VM). Dank der Integration in den Kernel fehlt aber – anders als bei einer VM – der Performance-Overhead.
Die Forscher beschreiben ihre Experimente und Algorithmen dabei als Container und Kubernetes-Pods. Das Infrastruktur-Team sorgt dann dafür, dass Kubernetes die benötigten Rechner (Nodes) bereitstellt, um alle Pods darin zu platzieren.
Zurzeit betreibt das Team eine Reihe von Kubernetes-Clustern mit jeweils bis zu 2000 Nodes. Damit gehört das Unternehmen vermutlich zu den größten Kubernetes-Usern, was auf der Kostenseite auch dazu führt, dass es in Sachen Kubernetes (auch Kube genannt) oftmals wackliges Terrain betritt und um Stabilität kämpfen muss. Damit auch Dritte von unseren Erfahrungen im Umgang mit hilfreichen Kubernetes-Datenstrukturen und -Abstraktionen profitieren, schildert das folgende Kapitel einige der “Gotchas”, auf die das Team gestoßen ist.
Im Neuland
Der typische Lebenszyklus eines Experiments sieht so aus: Ein oder zwei Forscher erproben einige Wochen oder Monate lang neue Ideen. In dieser Zeit brauchen sie nur minimale Rechenkapazitäten. Doch irgendwann (meist kurz vor Einsendeschluss bei einem akademischen Journal) benötigen sie auf einen Schlag Zehntausende CPU-Kerne, um wesentlich größere Berechnungen vorzunehmen. Um nicht die Kosten gelangweilter CPUs zu tragen, müssen die Admins die Größe des Clusters zur Laufzeit ändern. Die Lösung dafür heißt Autoscaler, Open AI hat ihn unter MIT-Lizenz auf Github veröffentlicht [8]. Konkret funktioniert Autoscaler so:
- Er holt eine Liste aller Pods, die sich aktuell nicht auf einem Node platzieren lassen.
- Er erstellt einen Plan, um genügend Nodes zu starten. Dabei berücksichtigt er verschiedene Faktoren, zu denen gehört, günstige Rechenpower zu bevorzugen, GPUs bereitzustellen, Co-Location von verwandten Jobs zu betreiben sowie die verschiedenen Benutzerpräferenzen zu beachten.
- Am Ende setzt er diesen Plan in die Tat um.
An dieser Stelle zeigt sich eine der Stärken des Kube-API: Weil der Autoscaler per API auf alle Ressourcen zugreift, erlaubt dies Open AI, eine Art programmierbare Infrastruktur zu schaffen. Kubernetes bringt dafür extrem praktische und erweiterbare Datenstrukturen mit. Die lassen sich auch vom heimischen Rechner aus leicht verfolgen: Auf einem laufenden Cluster zeigt ein »kubectl get nodes« etwa die Datenstruktur für einen Node an (Listings 1 und 2).
Listing 1
Node anzeigen
01 $ kubectl get nodes 02 NAME STATUS AGE VERSION 03 10.126.22.9 Ready 3h v1.6.2
Dabei dienen die Label als benutzerdefinierte Markierungen, in denen die Admins Metadaten wie Standort, CPU-Typ und Ausstattung der Maschinen ablegen. Der Autoscaler stellt dann, je nach Anforderung, Maschinen mit den richtigen Metadaten bereit.
Listing 2
Kubernetes-Datenstruktur für einen Node in Yaml
01 $ kubectl get node 10.126.22.9 -o yaml 02 apiVersion: v1 03 kind: Node 04 metadata: 05 creationTimestamp: 2017-06-07T08:15:30Z 06 labels: 07 openai.org/location: azure-us-east-v2 08 name: 10.126.22.9 09 spec: 10 externalID: 10.126.22.9 11 providerID: azure:////62823750-1942-A94F-822E-E6BF3C9EDCC4 12 status: 13 addresses: 14 - address: 10.126.22.9 15 type: InternalIP 16 - address: 10.126.22.9 17 type: Hostname 18 allocatable: 19 alpha.kubernetes.io/nvidia-gpu: "0" 20 cpu: "20" 21 memory: 144310716Ki 22 pods: "28" 23 nodeInfo: 24 architecture: amd64 25 containerRuntimeVersion: docker://1.12.6 26 kernelVersion: 4.4.0-72-generic 27 osImage: Ubuntu 14.04.5 LTS
GPU-Scheduling
Den Großteil der Berechnungen im Deep-Learning-Bereich erledigen Grafikprozessoren, also GPUs. Das liegt daran, dass die einzelnen “Neuronen” des Netzes, ähnlich wie die Pixel eines Bildschirms, unabhängig voneinander agieren. Diesen Parallelismus nutzen zum Beispiel Computerspiele für Hardware-beschleunigtes 3-D-Rendering. Open AI setzt dabei auf Nvidia Cuda [9], um neuronale Netze unter Verwendung von GPUs abzubilden (Abbildung 3).

Abbildung 3: Nvidia-smi, das »top« für GPUs, auf einem Rechner mit acht voll ausgelasteten Nvidia-GPUs.
Im Zusammenhang mit Kubernetes besteht das Problem darin, dass es mehrere GPUs sowie andere Ressourcen (CPU, RAM und so weiter) verwalten muss. Ein Beispiel: Angenommen, ein Node besteht aus 64 CPU-Kernen und acht Nvidia-Grafikkarten. Ein Pod für Experimente benötigt 16 CPU-Kerne und zwei Grafikkarten. Hier stellt sich dem Admin die Frage, wie er dem Kubernetes-Scheduler diese Ressourcen-Anforderungen mitteilt.
Der CPU-Part ist einfach: Er setzt das Feld »spec.containers[].resources.requested.cpu« auf »16«. In der Folge platziert Kubernetes nie mehr als vier solcher Pods auf den Node, um nicht die festgelegte Kapazität zu überschreiten.
Um unter den verschiedenen Pods auszuhandeln, wer welche GPUs erhält, verwendet er innerhalb des Pod das gute alte »flock(2)«, um exklusive Locks auf die GPU-Devices »/dev/nvidiaX« zu erhalten, wobei »X« Werte zwischen 1 und 8 annimmt (Abbildung 4).

Abbildung 4: Der Admin erhält exklusiven Zugriff auf die Grafikkarte. Der zweite Prozess merkt es, falls der erste Prozess die Ressource bereits nutzt.
Das Problem war: Zum damaligen Zeitpunkt konnte Kube noch nicht viel mit GPUs anfangen. Das Schema oben funktioniert nur, wenn das Verhältnis zwischen CPU und GPU konstant ist. Benötigt ein anderer Pod ebenfalls 16 CPU-Kerne, aber acht Grafikkarten, platziert Kube diesen Pod – ohne mit der Wimper zu zucken – auf demselben Node, und plötzlich fehlen GPUs. Die Lösung besteht darin, die Nodes mit Hilfe von Labels in mehrere Klassen aufzuteilen und nur einen bestimmten Pod-Typ in einer Klasse zu platzieren. Das ist sicher nicht ideal, erfüllt aber seinen Zweck.
Mittlerweile (seit Version 1.6) unterstützt Kubernetes GPU-Scheduling sogar nativ. Daher ließ sich der Hack ohne für den Nutzer sichtbare Änderungen gegen die saubere Version austauschen [10]. Dies ist auch ein gutes Beispiel dafür, wie das Infrastruktur-Team solche Änderungen an Kubernetes ohne das Zutun sämtlicher Benutzer handhabt.
Kinderkrankheiten
Als möglicher Performance-Engpass im Betrieb von Kubernetes erweist sich die Kommunikation zwischen Kubernetes-Master und dem Kubelet, dem Programm, das auf jedem der Nodes läuft und das Pod-Container startet und verwaltet. Als der vom Unternehmen genutzte Cluster mit der Zeit immer größer wurde, stellte sich heraus, dass hier überraschend viel Traffic entsteht – einer der Cluster mit etwa 1000 Nodes generierte da schon mal 4 bis 5 GBit/s an Daten. (Nebenbei bemerkt: Die Kubernetes-Master verbrachten etwa 90 Prozent ihrer Zeit mit dem En- und Dekodieren von Json.)
Kubernetes geht davon aus, dass alle Nodes über ein schnelles und billiges Netzwerk miteinander reden. Wer also einen einzelnen Kubernetes-Cluster über mehrere Rechenzentren strecken möchte, muss tief in den Bandbreiten-Geldbeutel greifen. Erst seit im März dieses Jahres Kubernetes 1.6 erschien, verläuft die Kommunikation zwischen den Komponenten dank GRPC deutlich effizienter: Es nutzt Protocol Buffers, nicht mehr Json. Dennoch führen Latenzzeiten von einigen Hundert Millisekunden noch immer zu Problemen oder zu gemächlichen Reaktionszeiten beim Ausführen von »kubectl«-Befehlen.
Eine weitere Überraschung kann sein, dass einige Elemente von Kube nach dem Prinzip “Eventual Consistency” funktionieren, dass also der Effekt einer Anweisung erst nach einiger Zeit eintritt. Dem Prinzip folgt beispielsweise die Service-Ressource in Kubernetes, die Load Balancing zwischen mehreren Pods ermöglicht:
- Jemand, zum Beispiel der Replica-Set-Controller, erzeugt einen neuen Pod, der zum Service gehört.
- Der Scheduler platziert den Pod und Kubelet startet die Container.
- Der Service-Controller erzeugt den Service-Endpoint für den Pod im zugehörigen Service.
In der Zeit zwischen dem zweiten und dem dritten Punkt läuft der Pod zwar, ist aber für den Service nicht sichtbar. Kein Wunder also, dass der Pod zum Beispiel HTTP-Anfragen an den Service nicht beantwortet. Normalerweise ist das kein Problem, da dieser Zeitraum sehr klein ist. Bei großen Clustern mit Tausenden Services und Pods vergehen hier hingegen mitunter mehrere Minuten. Für unser Unternehmen warf das ein großes Problem auf, da unsere Experimente aus diesem Grund mehrere Minuten zum Starten benötigten.
Nach einigen Untersuchungen stellte sich heraus, dass der Service-Controller ein einzelner Prozess ist, der in einer For-Schleife über alle Dienste iteriert und neue Pods aktiviert, also die Service-Endpoints erstellt. Dies alles versteckt Kubernetes hinter der Service-Abstraktion. Als Symptom sieht der Admin nur, dass sein Service down ist, ohne den Hintergrund zu kennen.
Neben Performance-Problemen mit großen Clustern gibt es noch andere Bereiche, in denen die Kube-Abstraktionen unrein (“leaky”) sind. Der oben erwähnte Autoscaler sorgt dafür, dass sich die Größe des Clusters den Anforderungen anpasst; dazu startet er neue Nodes oder entfernt überschüssige aus dem Cluster. Nach einigen Monaten intensiver Nutzung stellte sich allerdings heraus, dass Kubernetes nicht immer gut mit auftauchenden und abgehenden Nodes umgeht: So löscht es zum Beispiel Logging-Ausgaben von Pods, wenn der zugrunde liegende Node offline geht, der Pod selbst aber noch existiert.
Viele dieser Details sind aufgrund der Neuheit von Kube noch nicht ausreichend erprobt und dokumentiert; ein Eintauchen in den Quelltext [11] war für uns also am Ende unvermeidbar.
Fazit
Unser Eindruck nach anderthalb Jahren intensiver Nutzung ist: Kubernetes bringt gut entworfene, Unix-kompatible Datenstrukturen und APIs mit, um eine Schnittstelle zwischen Benutzer und Betreiber der Infrastruktur zu schaffen. Das bringt Flexibilität für die Nutzer bei wechselnden Anforderungen und für die Betreiber beim Bereitstellen neuer Cloudressourcen. Dabei müssen die Nutzer der Infrastruktur nicht alle Details kennen – das Kube-API kann als organisatorische Schnittstelle zwischen dem Infrastruktur-Team und anderen Teams dienen.
Doch ist Kubernetes kein Allheilmittel und ersetzt kein erfahrenes Administratoren-Team. Es gibt zwar ein Out-of-the-box-Installationspaket, das mit einem einzelnen Bash-Skript einen Kube-Cluster auf die Beine stellt, aber der einfache Schein kann trügen: Zumindest für die Anforderungen von Open AI hätte der Cluster von der Stange nicht ausgereicht, er beachtet noch zu wenig die wechselnden Anforderungen der Nutzer des Systems. Die wollen neue Cloudprovider anbinden oder andere externe Systeme wie Datenbanken, und sie brauchen mitunter zusätzliche Features wie das erwähnte GPU-Scheduling.
Eine große Stärke von Kubernetes, die der Artikel bislang sträflich vernachlässigte, ist die Open-Source-Entwicklergemeinde [12]. Neue Kube-Versionen lösen in Projekten in der Regel vor allem deshalb große Vorfreude aus, weil die Nutzer die Zeit schätzen, die sie aufgrund der neuen Features in Zukunft sparen.
Unter Leitung der Cloud Native Computing Foundation, unterstützt von der Linux Foundation und Google, fährt das Kubernetes-Schiff also auf gutem Kurs – und das mit beeindruckender Geschwindigkeit.
Infos
-
Open AI: https://openai.com
-
Elon Musk: https://de.wikipedia.org/wiki/Elon_Musk
-
Y-Combinator: http://www.ycombinator.com
-
Lernender Roboter: https://blog.openai.com/robots-that-learn/
-
Docker: https://www.docker.com
-
Kubernetes: https://kubernetes.io
-
Deep-Learning-Framework Tensorflow: https://www.tensorflow.org
-
Autoscaler: https://github.com/openai/kubernetes-ec2-autoscaler
-
Nvidia Cuda: http://www.nvidia.com/object/cuda_home_new.html
-
GPU Scheduler: https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/
-
Kubernetes-Quellcode: https://github.com/kubernetes/kubernetes
-
News zu Kubernetes: http://kube.news







