- Cet article explique, à partir de cas concrets, une tentative et une conception visant à compiler à l’avance (AOT) du code Python pur pour le transformer en exécutables multiplateformes
- L’idée centrale consiste non pas à créer un nouveau JIT ni à tout réécrire en C++, mais à produire des noyaux optimisés via un pipeline traçage symbolique → IR → génération de code C++ → compilation multi-cible
- Des annotations de type PEP 484 servent à amorcer la propagation de types, et la génération de code par IA permet d’implémenter automatiquement des centaines d’opérateurs C++ pour couvrir un large éventail d’appels de bibliothèques comme Numpy, OpenCV et PyTorch
- La stratégie retenue est une optimisation empirique des performances : générer et déployer en masse plusieurs chemins d’implémentation pour une même fonction Python, puis choisir la variante la plus rapide à partir de télémétrie mesurée
- L’objectif est de fournir de petits binaires portables et rapides ne dépendant pas de conteneurs, afin d’en faire une unité de déploiement exécutable partout : sur serveur, bureau, mobile et web
Foreword
- La simplicité et la productivité de Python sont des atouts, mais il existe des limites de performance et de portabilité pour les charges de travail intensives
- Ce texte, signé par l’auteur invité Yusuf Olokoba, présente la conception d’un compilateur permettant de créer des exécutables rapides et portables tout en conservant le Python d’origine
- L’approche vise à obtenir une optimisation des noyaux en construisant un pipeline sans ajouter de JIT ni procéder à une réécriture complète en C++
Introduction
- L’objectif est de compiler entièrement en AOT du Python non modifié afin qu’il s’exécute sans interpréteur, avec des performances proches du C/C++, et sur toutes les plateformes
- Contrairement à des tentatives existantes (Jython, RustPython, Numba, PyTorch, Mojo, etc.), le choix porte non pas sur un remplacement du langage ou du runtime, mais sur la transformation du code et la génération de noyaux
- Ces fonctions Python compilées sont déjà utilisées sur des milliers d’appareils chaque mois
Containers Are the Wrong Way to Distribute AI
- En production, les conteneurs embarquent une charge utile excessive (interpréteur, paquets, snapshot de l’OS), ce qui entraîne latence au démarrage et contraintes de portabilité
- L’alternative proposée est un exécutable autonome ne contenant que le modèle, offrant un format plus léger, un démarrage plus rapide et la possibilité de fonctionner sur serveur, bureau, mobile et web
- L’idée clé est de faire évoluer l’unité de déploiement : passer d’un snapshot de système d’exploitation à un binaire auto-exécutable
Arm64, Apple, and Unity: How It All Began
- Lors de la transition d’Apple vers arm64, Unity avait rendu possible la compilation vers toutes les cibles en convertissant le CIL en C++ avec IL2CPP ; cette approche a servi de référence
- La vision consiste à appliquer la même idée à Python afin d’obtenir des chemins de code exécutables partout
Sketching Out a Python Compiler
- La conception de haut niveau suit les étapes entrée Python → trace symbolique (IR) → génération C++ → compilation multi-cible
- Le choix de C++ comme artefact intermédiaire, plutôt qu’une génération directe de code objet depuis l’IR, vise à tirer un maximum parti de chemins d’accélération comme CUDA, MLX, TensorRT, AMX, etc.
- Le but est d’obtenir une architecture extensible dans laquelle il est facile d’insérer des chemins optimisés selon le matériel
Building a Symbolic Tracer for Python
- Les premiers essais de traçage basés sur PyTorch FX se heurtaient à des limites liées à la nécessité d’exécuter le code et au fait d’être cantonnés aux opérations PyTorch
- À la place, un traceur symbolique fondé sur le parsing d’AST a été construit pour convertir en IR le flux de contrôle et la résolution des appels
- Le traceur fournit aujourd’hui des fonctions telles que l’analyse statique, l’évaluation partielle et l’observation de valeurs en direct dans un bac à sable
Lowering to C++ via Type Propagation
- Pour faire le pont entre le typage dynamique de Python et le typage statique de C++, l’approche s’appuie sur la propagation de types
- Une fois les types des arguments d’entrée connus, il devient possible d’inférer de manière déterministe les types des variables intermédiaires selon la définition des opérateurs
- Chaque opération Python est mappée vers une implémentation C++ correspondante, tandis que les types sont propagés à l’échelle de toute la fonction
Seeding the Type Propagation Process
- Des annotations de type PEP 484 sont utilisées pour amorcer la propagation de types
- Cela entre certes en tension avec le principe de non-modification du code source, mais c’est présenté comme un compromis acceptable au regard de la simplicité de l’interface et de la compatibilité
- Des contraintes sont également imposées, comme la limitation du nombre de types dans les signatures de fonction, afin de garantir une interface de consommation simple
Building a Library of C++ Operators
- Il n’est pas nécessaire d’implémenter directement toutes les fonctions en C++ : seules les opérations feuilles non traçables exigent une implémentation manuelle ou automatique
- Comme beaucoup de code Python est composé d’une petite combinaison d’opérations de base, l’ensemble des opérateurs à couvrir reste relativement limité
- Grâce à la génération de code pilotée par LLM et à une infrastructure de contraintes, de tests et de compilation conditionnelle, l’implémentation de centaines de fonctions pour Numpy, OpenCV, PyTorch, etc., a pu être automatisée
Performance Optimization via Exhaustive Search
- Partant de l’idée que l’optimisation des performances est toujours empirique, l’approche consiste à générer toutes les variantes d’implémentation, puis à les comparer sur mesures réelles pour retenir la meilleure
- Exemple : sur Apple Silicon, même un simple redimensionnement peut donner lieu à plusieurs chemins — Accelerate, vImage, Core Image, Metal, etc. — et donc à plusieurs binaires offrant la même fonctionnalité
- Une télémétrie fine collecte les latences de chaque chemin, puis un modèle statistique prédit et sélectionne la variante la plus rapide
- Pour l’utilisateur, l’effet obtenu est une exécution qui s’accélère automatiquement avec le temps
Designing a User Interface for the Compiler
- Afin de ramener la courbe d’apprentissage de l’expérience développeur au plus près de zéro, l’interface choisie est le décorateur PEP 318
@compile
- Le CLI utilise ce décorateur comme point d’entrée pour explorer et compiler le graphe de code dépendant
- Les arguments du décorateur incluent tag, description, sandbox et metadata, afin de prendre en charge la reproduction d’environnement et le choix de backend (ONNXRuntime, TensorRT, CoreML, IREE, QNN, etc.)
Closing Thoughts
- La prise en charge des exceptions, lambdas, récursion, classes, etc., reste partielle ou absente ; l’extension de la propagation de types est particulièrement nécessaire pour les types complexes et les types d’ordre supérieur
- L’expérience de débogage constitue aussi un défi, car la compilation optimisante réduit les informations symboliques et rend le traçage plus difficile
std::span, les concepts et les coroutines de C++20 forment une base essentielle, tandis que std::generator, <stdfloat> et <stacktrace> de C++23 devraient contribuer au streaming, au half/bfloat16 et au traçage des exceptions
- L’objectif final est d’établir, sans conteneurs, de petits exécutables rapides et sûrs comme unité de déploiement capable d’exécuter partout des charges de travail IA comme l’embedding ou la détection
1 commentaires
Je pensais que c’était quelque chose comme APE, mais apparemment ce n’est pas ça.