Python ist komfortabel und benutzerfreundlich, aber nicht die schnellste Programmiersprache unter der Sonne. Einige dieser Geschwindigkeits-Limitationen sind auf die Python-Standardimplementierung CPython zurückzuführen, die auf Single-Thread-Basis arbeitet. Zwar lässt sich das integrierte threading
-Modul in Python nutzen, um am Speed-Knopf zu drehen - womit allerdings nur Concurrency gewährleistet ist und nicht Parallel Processing. Entsprechend trägt das nicht dazu bei, mehrere Tasks zu beschleunigen, die jeweils eine eigene CPU benötigen. Das könnte sich allerdings in der Zukunft ändern.
Python bietet außerdem auch eine native Möglichkeit, Workloads über mehrere CPUs zu verteilen. Das multiprocessing
-Modul lädt mehrere Instanzen des Python-Interpreters auf einem jeweils separaten Kern und bietet Primitive, um Tasks auf verschiedene Cores zu verteilen (siehe Video weiter unten). Allerdings ist es in manchen Fällen vonnöten, Tasks nicht nur auf mehrere Kerne sondern auf mehrere Maschinen zu verteilen.
7 Python-Frameworks für Parallel Processing
An dieser Stelle kommen die folgenden sieben Python-Bibliotheken und -Frameworks ins Spiel, die es Ihnen ermöglichen, bestehende Python-Applikationen und deren Workloads auf mehrere Cores, Maschinen - oder beides - zu verteilen.
Von einem Forscherteam an der Universität Berkeley in Kalifornien entwickelt, bildet dieses Framework die Grundlage für eine ganze Reihe von Bibliotheken für Distributed Machine Learning. Dabei ist Ray jedoch nicht nur auf ML-Tasks beschränkt sondern verteilt jede Form von Python-Task über mehrere Systeme.
Weil die Syntax von Ray minimal ausfällt, müssen Sie bestehende Applikationen auch nicht umfangreich für Parallel Processing überarbeiten. Der @ray.remote
-Decorator verteilt die Funktion auf alle verfügbaren Knoten in einem Ray-Cluster - mit optionalen Parametern für die Anzahl der zu verwendenden CPUs oder GPUs. Die Ergebnisse jeder verteilten Funktion werden als Python-Objekte zurückgegeben, was es erleichtert, diese zu managen sowie zu speichern und den Umfang der Kopiervorgänge innerhalb der Cores auf ein Minimum zu beschränken. Letzteres ist beispielsweise nützlich, wenn Sie es mit NumPy-Arrays zu tun bekommen.
Desweiteren hat Ray auch einen eigenen, integrierten Cluster-Manager an Bord, der bei Bedarf automatisch Knoten auf lokaler Hardware oder gängigen Cloud-Computing-Plattformen aufsetzt. Andere Ray-Bibliotheken ermöglichen es, gängige Machine-Learning- und Data-Science-Workloads zu skalieren, so dass Sie diese nicht manuell einrichten müssen. Mit Ray Tune können Sie beispielsweise Hyperparameter für die meisten gängigen Machine-Learning-Systeme (etwa PyTorch und TensorFlow) skalieren.
Auf den ersten Blick sieht Dask dem eben vorgestellten Ray zum Verwechseln ähnlich. Auch in diesem Fall handelt es sich um eine Bibliothek für Distributed Parallel Computing in Python - mit eigenem Task-Scheduling-System, der Möglichkeit, Daten-Frameworks wie NumPy einzubinden und von einer Maschine auf viele zu skalieren. Ein wesentlicher Unterschied zwischen Dask und Ray ist der genannte Mechanismus für die Zeitplanung. Dask nutzt einen zentralisierten Scheduler, der alle Tasks für jeweils ein Cluster bearbeitet - Ray funktioniert dezentralisiert. Dasks Task-Framework arbeitet Hand in Hand mit den nativen concurrent.futures
-Interfaces von Python.
Dask arbeitet mit zwei grundlegenden Methoden:
Der erste Weg führt über parallelisierte Datenstrukturen - im Wesentlichen die Dask-eigene Version von NumPy-Arrays, -Listen oder Pandas DataFrames. Wenn Sie die Dask-Versionen dieser Konstruktionen gegen ihre Standardwerte austauschen, verteilt Dask die Execution automatisch über Ihre Cluster. Das erfordert in der Regel nicht viel mehr, als einen Import neu zu benennen - kann in manchen Fällen allerdings auch bedeuten, noch einmal bei Null anzufangen.
Die zweite Möglichkeit liegt in den Low-Level-Parallelisierungsmechanismen von Dask - inklusive Funktionsdekoratoren, die Tasks auf Knoten verteilen und die Ergebnisse synchron (im "immediate"-Modus) oder asynchron (im "lazy"-Modus) zurückgeben. Beide Modi können je nach Bedarf kombiniert werden.
Dask bietet auch ein Feature namens "Actors". Ein Actor ist ein Objekt, das auf einen Job auf einem anderen Dask-Knoten verweist. Auf diese Weise kann eine Local-State-Aufgabe an Ort und Stelle ausgeführt und von anderen Knoten ferngesteuert aufgerufen werden, so dass der State nicht repliziert werden muss.
Komplette Python-Programme oder auch nur einzelne Funktionen zu Parallel-Processing-Zwecken auf ein Maschinen-Cluster zu verteilen, wird mit Dispy möglich. Um sicherzustellen, dass die Dinge möglichst schnell und effizient ablaufen und unter Linux, macOS und Windows gleichermaßen gut funktionieren, verwendet Dispy plattformspezifische Mechanismen für die Netzwerkkommunikation. Das macht die Bibliothek auch interessant für Einsatzzwecke, die nicht zum Ziel haben, ML-Tasks zu beschleunigen oder auf bestimmte Datenverarbeitungs-Frameworks ausgerichtet sind.
Die Dispy-Syntax ähnelt insofern dem Python-eigenen multiprocessing
, als dass sie ein explizites Cluster erfordert, das erstellt werden muss (bei multiprocessing
müssten Sie einen Prozess-Pool erstellen), die Tasks dann an das Cluster übermittelt und die Ergebnisse abruft. Arbeitsaufträge für Dispy anzupassen, kann war etwas aufwändiger ausfallen - dafür haben Sie aber granulare Kontrollmöglichkeiten, wenn es darum geht, wie diese Aufträge versandt und zurückgegeben werden. So können Sie beispielsweise vorläufige oder teilweise abgeschlossene Ergebnisse zurückgeben, Dateien als Teil des Auftragsverteilungsprozesses übertragen und SSL-Verschlüsselung bei der Datenübertragung einsetzen.
Pandas-Aufträge über mehrere Knoten hinweg zu parallelisieren, wird mit Pandaral-lel möglich. Der Nachteil: das funktioniert ausschließlich mit Pandas. Wenn Sie jedoch nur eine Möglichkeit suchen, um solche Tasks zu beschleunigen, ist Pandaral-lel dafür wie geschaffen.
Zu beachten gilt es dabei, dass Pandaral-lel zwar unter Windows läuft, aber nur im Rahmen einer Python-Session, die über das Windows-Subsystem für Linux gestartet wird. Linux- und macOS-Benutzer können Pandaral-lel ohne Einschränkungen verwenden.
Ein spezialisiertes Multiprocessing- und Task-Verteilungssystem, das speziell dafür gedacht ist, Jupyter-Notebook-Code in einem Cluster auszuführen, ist Ipyparallel. Das Framework unterstützt viele Ansätze, um Code zu parallelisieren. Ein simpler Weg führt zum Beispiel über den Befehl map
, der eine beliebige Funktion auf eine Sequenz anwendet und die Arbeit gleichmäßig auf die verfügbaren Knoten verteilt. Für komplexere Aufgaben lassen sich bestimmte Funktionen so dekorieren, dass sie immer remote oder parallel ausgeführt werden.
Jupyter Notebooks unterstützen "magic commands" für Aktionen, die nur in einer Notebook-Umgebung möglich sind. Ipyparallel ergänzt diese um einige eigene: So können Sie zum Beispiel jedem Python-Statement %px
voranstellen, um sie automatisch zu parallelisieren.
Joblib verfolgt zwei wesentliche Ziele:
Aufträge parallel zu verarbeiten und
Ergebnisse nicht neu zu berechnen, wenn sich nichts verändert hat.
Deshalb eignet sich Joblib besonders gut für wissenschaftliche Berechnungen, bei denen reproduzierbare Ergebnisse unabdingbar sind. Die Dokumentation von Joblib enthält zahlreiche Beispiele, um alle Funktionen zum Einsatz zu bringen. Die Syntax für Parallel Processing ist dabei simpel - es läuft auf einen Decorator hinaus, den Sie nutzen können, um Aufträge auf verschiedene Prozessoren aufzuteilen oder um Ergebnisse zwischenzuspeichern. Parallele Aufträge können Threads oder Prozesse verwenden.
Joblib enthält zudem einen transparenten Festplatten-Cache für Python-Objekte, die im Rahmen von Arbeitsaufträgen erstellt werden. Dieser Cache hilft nicht nur dabei, Wiederholungen zu vermeiden, sondern lässt sich auch dazu nutzen, langlaufende Aufträge zu pausieren oder auch nach einem Absturz wieder aufzunehmen. Auch große Objekte wie NumPy-Arrays bleiben dabei nicht außen vor. Mit numpy.memmap
können Datenbereiche zwischen Prozessen können auf demselben System In-Memory genutzt werden.
Was Joblib nicht bieten kann, ist, Arbeitsaufträge auf mehrere Computer zu verteilen. Theoretisch wäre es zwar möglich, die Joblib-Pipeline dafür zu nutzen - der einfachere Weg wäre jedoch, gleich ein Framework zu nutzen, das diese Möglichkeit nativ an Bord hat.
Die Parallel Scripting Library (Parsl) ermöglicht es, Rechenaufträge auf mehrere Systeme zu verteilen, wobei in etwa die gleiche Syntax wie bei den bestehenden Pool
-Objekten von Python zur Anwendung kommt. Zudem lasse sich mit der Python-Bibliothek verschiedene Rechenaufgaben zu mehrstufigen Workflows zusammenfügen, die parallel, in Reihe oder über Map/Reduce-Operationen ausgeführt werden können.
Mit Parsl lassen sich sowohl native Python-Anwendungen als auch jede andere Art externer Applikation (über Shell-Befehle) ausführen. Dabei unterscheidet sich der Code lediglich dadurch, dass ein spezieller Funktionsdekorator verwendet wird, der den Einstiegspunkt für Ihre Arbeit markiert. Das Job-Submission-System gibt Ihnen auch die Möglichkeit granular zu steuern - zum Beispiel, wenn es um die Anzahl der Kerne, die CPU-Affinität oder die Häufigkeit von Timeouts geht.
Ein Alleinstellungsmerkmal von Parsl ist eine Reihe vorgefertigter Templates, um Rechenarbeit an eine Vielzahl von High-End-Ressourcen zu verteilen. Dazu gehören nicht nur "Grundnahrungsmittel" wie AWS- oder Kubernetes-Cluster, sondern auch Supercomputing-Ressourcen.
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.