- Dieser Beitrag erläutert anhand konkreter Beispiele den Versuch und das Design, reinen Python-Code Ahead-of-Time (AOT) zu kompilieren und in plattformübergreifende ausführbare Dateien umzuwandeln
- Die Kernidee besteht darin, keinen neuen JIT zu bauen und nichts in C++ neu zu schreiben, sondern über eine Pipeline aus symbolischem Tracing → IR → C++-Codegenerierung → Kompilierung für mehrere Targets optimierte Kernel zu erzeugen
- Mit PEP-484-Typannotationen wird die Typweitergabe initialisiert, und durch KI-Codegenerierung werden Hunderte von C++-Operatoren automatisch implementiert, um breite Library-Aufrufe wie Numpy, OpenCV und PyTorch abzudecken
- Für dieselbe Python-Funktion werden in großem Umfang verschiedene Implementierungspfade erzeugt und verteilt, und per gemessener Telemetrie wird die schnellste Variante gewählt – eine Strategie der empirischen Performance-Optimierung
- Ziel ist es, kleine, schnelle und portable Binärdateien bereitzustellen, die nicht von Containern abhängen und als Deployment-Einheit für Server, Desktop, Mobile und Web dienen, also überall ausführbar sind
Foreword
- Die Einfachheit und Produktivität von Python sind Stärken, doch bei hochlastigen Workloads gibt es Grenzen bei Performance und Portabilität
- Dieser Beitrag des Gastautors Yusuf Olokoba stellt ein Compiler-Design vor, das schnelle und portable Executables erzeugt, während der ursprüngliche Python-Code erhalten bleibt
- Der Ansatz zielt darauf ab, Kernel-Optimierung durch eine Pipeline zu erreichen – ohne zusätzlichen JIT oder vollständiges Umschreiben in C++
Introduction
- Ziel ist es, unverändertes Python vollständig AOT zu kompilieren, sodass es ohne Interpreter läuft, nahe an C/C++-Geschwindigkeit erreicht und auf allen Plattformen ausführbar ist
- Anders als frühere Ansätze wie Jython, RustPython, Numba, PyTorch oder Mojo setzt dieser Ansatz nicht auf den Austausch von Sprache oder Runtime, sondern auf Code-Transformation und Kernel-Erzeugung
- Diese kompilierten Python-Funktionen werden bereits auf Tausenden von Geräten pro Monat eingesetzt
Containers Are the Wrong Way to Distribute AI
- In realen Deployments bringen Container als überladene Payload Interpreter, Pakete und OS-Snapshots mit, was zu Startverzögerungen und Einschränkungen bei der Portabilität führt
- Die Alternative sind selbstständige ausführbare Dateien, die nur das Modell enthalten, mit kleinerer Größe, schnellerem Start und der Möglichkeit, auf Servern, Desktops, Mobilgeräten und im Web zu laufen
- Der Kernpunkt ist die Strategie, die Deployment-Einheit von einem OS-Snapshot zu einer eigenständig ausführbaren Binärdatei zu machen
Arm64, Apple, and Unity: How It All Began
- Beim Übergang Apples zu arm64 diente Unitys Ansatz als Vorbild, bei dem IL2CPP CIL in C++ umwandelt, sodass es für alle Targets kompiliert werden kann
- Dieselbe Idee soll auf Python angewendet werden, um Codepfade zu schaffen, die überall laufen können
Sketching Out a Python Compiler
- Das grobe Design besteht aus den Schritten Python-Eingabe → symbolischer Trace (IR) → C++-Generierung → Kompilierung für mehrere Targets
- Dass von der IR nicht direkt zu Objektcode gegangen wird, sondern C++ als Zwischenartefakt gewählt wird, hat den Grund, Beschleunigungspfade wie CUDA, MLX, TensorRT und AMX möglichst gut zu nutzen
- Ziel ist ein erweiterbares Design, in das sich hardwareabhängig optimierte Pfade leicht einfügen lassen
Building a Symbolic Tracer for Python
- Das anfängliche Tracing auf Basis von PyTorch FX stieß an Grenzen, weil es Ausführung erfordert und auf PyTorch-Operationen beschränkt ist
- Stattdessen wurde ein symbolischer Tracer auf AST-Parsing-Basis gebaut, der Kontrollfluss und Aufrufauflösung in IR überführt
- Der aktuelle Tracer bietet Funktionen wie statische Analyse, partielle Auswertung und Sandbox-basierte Beobachtung von Live-Werten
Lowering to C++ via Type Propagation
- Um die Brücke von Pythons dynamischer Typisierung zu C++'s statischer Typisierung zu schlagen, wird Typweitergabe verwendet
- Sind die Typen der Eingabeargumente gegeben, lassen sich die Typen von Zwischenvariablen anhand der Operatordefinitionen deterministisch ableiten
- Jede Python-Operation wird auf eine entsprechende C++-Implementierung abgebildet, während sich die Typen über die gesamte Funktion hinweg fortpflanzen
Seeding the Type Propagation Process
- Als Startpunkt für die Typweitergabe dienen PEP-484-Typannotationen
- Das steht zwar im Spannungsverhältnis zum Prinzip, den Originalcode nicht zu verändern, wird aber als vertretbarer Kompromiss für eine knappe Schnittstelle und Kompatibilität bewertet
- Zusätzlich werden Beschränkungen wie eine begrenzte Anzahl von Typen in Funktionssignaturen eingeführt, um eine einfache Consumer-Schnittstelle sicherzustellen
Building a Library of C++ Operators
- Es müssen nicht alle Funktionen direkt in C++ implementiert werden; nur nicht nachverfolgbare Leaf-Operationen erfordern manuelle oder automatische Implementierung
- Da viele Python-Programme aus der Kombination einiger weniger Grundoperationen bestehen, ist die abzudeckende Menge an Operatoren relativ klein
- Mit LLM-basierter Codegenerierung sowie Infrastruktur für Restriktionen, Tests und bedingte Kompilierung wird die Implementierung von Hunderten Funktionen aus Numpy, OpenCV und PyTorch automatisiert
Performance Optimization via Exhaustive Search
- Aus der Erkenntnis heraus, dass Performance-Optimierung immer empirisch ist, werden sämtliche Implementierungsvarianten erzeugt und per Messvergleich die beste ausgewählt
- Beispiel: Allein für Resize auf Apple Silicon werden mehrere Pfade wie Accelerate, vImage, Core Image und Metal generiert, und mehrere Binärdateien mit identischer Funktionalität werden verteilt
- Mit feingranularer Telemetrie werden Latenzen je Pfad gesammelt, und per statistischem Modell wird die schnellste Variante vorhergesagt und ausgewählt
- Der Effekt für Nutzer ist eine Laufzeiterfahrung, die mit der Zeit automatisch schneller wird
Designing a User Interface for the Compiler
- Um die Developer Experience auf eine nahezu null steile Lernkurve zu bringen, wird der PEP-318-Dekorator
@compile als Interface verwendet
- Die CLI nutzt den Dekorator als Entrypoint, durchsucht den abhängigen Codegraphen und kompiliert ihn
- Als Dekoratorargumente werden tag, description, sandbox und metadata angenommen, um Umgebungsreproduktion und Backend-Auswahl zu unterstützen, etwa ONNXRuntime, TensorRT, CoreML, IREE und QNN
Closing Thoughts
- Ausnahmen, Lambdas, Rekursion und Klassen werden nur teilweise oder gar nicht unterstützt; insbesondere bei komplexen Typen und Higher-Order-Typen muss die Typweitergabe erweitert werden
- Die Debugging-Erfahrung bleibt eine Herausforderung, da optimierende Kompilierung Symbolinformationen reduziert und damit das Tracing erschwert
- C++20 mit
std::span, concepts und coroutines bildet die zentrale Grundlage, während C++23 mit std::generator, <stdfloat> und <stacktrace> künftig zu Streaming, half/bfloat16 und Exception-Tracing beitragen soll
- Das Endziel ist, kleine, schnelle und sichere ausführbare Dateien ohne Container als Deployment-Einheit zu etablieren, mit denen sich KI-Workloads wie Embedding und Objekterkennung überall ausführen lassen
1 Kommentare
Ich dachte, es wäre so etwas wie APE, aber das ist es wohl nicht.