Il s'agit de prometeo, un outil de modélisation expérimentale pour le calcul haute performance embarqué. prometeo fournit un langage spécifique à un domaine (DSL) basé sur un sous-ensemble du langage Python qui permet d'écrire facilement des programmes de calcul scientifique dans un langage de haut niveau (Python lui-même) qui peut être facilement transpilé en code C autonome hautes performances. déployable sur des appareils embarqués.
La documentation de prometeo est disponible sur Read the Docs à l'adresse https://prometeo.readthedocs.io/en/latest/index.html.
Un exemple simple de Hello World qui montre comment exécuter un programme prometeo trivial à partir de Python ou le transpiler en C, le construire et l'exécuter peut être trouvé ici. La sortie montre le résultat de l'analyse de l'utilisation du tas et le temps d'exécution (dans ce cas, il n'y a pas grand chose à voir :p).
Étant donné que les programmes prometeo sont transpilés en code C pur qui appelle la bibliothèque d'algèbre linéaire haute performance BLASFEO (publication : https://arxiv.org/abs/1704.02457, code : https://github.com/giaf/blasfeo), le temps d'exécution peut être comparable au code haute performance écrit à la main. La figure ci-dessous montre une comparaison du temps CPU nécessaire pour effectuer une factorisation Riccati en utilisant du code C manuscrit hautement optimisé avec des appels à BLASFEO et ceux obtenus avec le code transpilé prometeo de cet exemple. Les temps de calcul obtenus avec NumPy et Julia sont également ajoutés à des fins de comparaison - notez cependant que ces deux dernières implémentations de la factorisation Riccati ne sont pas aussi facilement intégrables que le code C généré par prometeo et l'implémentation C codée à la main. Tous les tests ont été exécutés sur un Dell XPS-9360 équipé d'un processeur i7-7560U fonctionnant à 2,30 GHz (pour éviter les fluctuations de fréquence dues à la limitation thermique).
De plus, prometeo peut largement surpasser les compilateurs Python de pointe tels que Nuitka. Le tableau ci-dessous présente les temps CPU obtenus sur un benchmark Fibonacci.
analyseur/compilateur | Temps CPU [s] |
---|---|
Python 3.7 (CPython) | 11.787 |
Nuitka | 10.039 |
PyPy | 1,78 |
prometéo | 0,657 |
prometeo peut être installé via PyPI avec pip install prometeo-dsl
. Notez que, puisque prometeo utilise largement les indices de type pour équiper le code Python d'informations de typage statique, la version minimale requise de Python est 3.6.
Si vous souhaitez installer prometeo en créant les sources sur votre machine locale, vous pouvez procéder comme suit :
git submodule update --init
pour cloner les sous-modules.make install_shared
depuis <prometeo_root>/prometeo/cpmt
pour compiler et installer la bibliothèque partagée associée au backend C. Notez que le chemin d'installation par défaut est <prometeo_root>/prometeo/cpmt/install
.virtualenv --python=<path_to_python3.6> <path_to_new_virtualenv>
.pip install -e .
depuis <prometeo_root>
pour installer le package Python. Enfin, vous pouvez exécuter les exemples dans <root>/examples
avec pmt <example_name>.py --cgen=<True/False>
, où l'indicateur --cgen
détermine si le code est exécuté par l'interpréteur Python ou si le code C est généré, compilé et exécuté.
Le code Python ( examples/simple_example/simple_example.py
)
from prometeo import *
n : dims = 10
def main () -> int :
A : pmat = pmat ( n , n )
for i in range ( 10 ):
for j in range ( 10 ):
A [ i , j ] = 1.0
B : pmat = pmat ( n , n )
for i in range ( 10 ):
B [ 0 , i ] = 2.0
C : pmat = pmat ( n , n )
C = A * B
pmat_print ( C )
return 0
peut être exécuté par l'interpréteur Python standard (version > 3.6 requise) et il effectuera les opérations d'algèbre linéaire décrites à l'aide de la commande pmt simple_example.py --cgen=False
. Parallèlement, le code peut être analysé par prometeo et son arbre de syntaxe abstraite (AST) analysé afin de générer le code C haute performance suivant :
#include "stdlib.h"
#include "simple_example.h"
void * ___c_pmt_8_heap ;
void * ___c_pmt_64_heap ;
void * ___c_pmt_8_heap_head ;
void * ___c_pmt_64_heap_head ;
#include "prometeo.h"
int main () {
___c_pmt_8_heap = malloc ( 10000 );
___c_pmt_8_heap_head = ___c_pmt_8_heap ;
char * pmem_ptr = ( char * ) ___c_pmt_8_heap ;
align_char_to ( 8 , & pmem_ptr );
___c_pmt_8_heap = pmem_ptr ;
___c_pmt_64_heap = malloc ( 1000000 );
___c_pmt_64_heap_head = ___c_pmt_64_heap ;
pmem_ptr = ( char * ) ___c_pmt_64_heap ;
align_char_to ( 64 , & pmem_ptr );
___c_pmt_64_heap = pmem_ptr ;
void * callee_pmt_8_heap = ___c_pmt_8_heap ;
void * callee_pmt_64_heap = ___c_pmt_64_heap ;
struct pmat * A = c_pmt_create_pmat ( n , n );
for ( int i = 0 ; i < 10 ; i ++ ) {
for ( int j = 0 ; j < 10 ; j ++ ) {
c_pmt_pmat_set_el ( A , i , j , 1.0 );
}
}
struct pmat * B = c_pmt_create_pmat ( n , n );
for ( int i = 0 ; i < 10 ; i ++ ) {
c_pmt_pmat_set_el ( B , 0 , i , 2.0 );
}
struct pmat * C = c_pmt_create_pmat ( n , n );
c_pmt_pmat_fill ( C , 0.0 );
c_pmt_gemm_nn ( A , B , C , C );
c_pmt_pmat_print ( C );
___c_pmt_8_heap = callee_pmt_8_heap ;
___c_pmt_64_heap = callee_pmt_64_heap ;
free ( ___c_pmt_8_heap_head );
free ( ___c_pmt_64_heap_head );
return 0 ;
}
qui s'appuie sur le package d'algèbre linéaire haute performance BLASFEO. Le code généré sera facilement compilé et exécuté lors de l'exécution pmt simple_example.py --cgen=True
.
Bien que traduire un programme écrit dans un langage dans un autre avec un niveau d'abstraction comparable puisse être beaucoup plus facile que de le traduire dans un autre avec un niveau d'abstraction très différent (surtout si le langage cible est d'un niveau bien inférieur), traduire des programmes Python en programmes C implique encore un écart d'abstraction considérable, ce n'est pas une tâche facile en général. En gros, le défi réside dans la nécessité de réimplémenter les fonctionnalités nativement prises en charge par la langue source dans la langue cible. En particulier, lors de la traduction de Python vers C, la difficulté vient à la fois du niveau d'abstraction différent des deux langages et du fait que les langages source et cible sont de deux types très différents : Python est un langage interprété , typé canard et trash. -langage collecté et C est un langage compilé et typé statiquement .
La tâche de transpilation de Python en C devient encore plus difficile si l'on ajoute la contrainte que le code C généré doit être efficace (même pour les calculs à petite et moyenne échelle) et déployable sur du matériel embarqué. En fait, ces deux exigences impliquent directement que le code généré ne peut pas utiliser : i) des bibliothèques d'exécution sophistiquées, par exemple la bibliothèque d'exécution Python, qui ne sont généralement pas disponibles sur le matériel embarqué ii) une allocation de mémoire dynamique qui rendrait l'exécution lente et peu fiable. (exception faite pour la mémoire qui est allouée lors d'une phase d'installation et dont la taille est connue a priori).
La transformation de code source à source, ou transpilation, et en particulier la transpilation de code Python en code C n’étant pas un domaine inexploré, nous mentionnons ci-après quelques projets existants qui y répondent. Ce faisant, nous soulignons où et comment ils ne satisfont pas à l’une des deux exigences décrites ci-dessus, à savoir l’efficacité (à petite échelle) et l’intégrabilité.
Il existe plusieurs progiciels traitant de la traduction Python vers C sous diverses formes.
Dans le contexte du calcul haute performance, Numba est un compilateur juste à temps de fonctions numériques écrites en Python. En tant que tel, son objectif est de convertir des fonctions Python correctement annotées, et non des programmes entiers, en code LLVM hautes performances afin que leur exécution puisse être accélérée. Numba utilise une représentation interne du code à traduire et effectue une inférence de type (potentiellement partielle) sur les variables impliquées afin de générer du code LLVM pouvant être appelé soit depuis Python, soit depuis C/C++. Dans certains cas, notamment ceux où une inférence de type complète peut être effectuée avec succès, du code qui ne repose pas sur l'API C peut être généré (en utilisant l'indicateur nopython ). Cependant, le code LLVM émis s'appuierait toujours sur Numpy pour les opérations BLAS et LAPACK.
Nuitka est un compilateur source à source qui peut traduire chaque construction Python en code C lié à la bibliothèque libpython et il est donc capable de transpiler une grande classe de programmes Python. Pour ce faire, il s'appuie sur le fait que l'une des implémentations les plus utilisées du langage Python, à savoir CPython , est écrite en C. En effet, Nuitka génère du code C qui contient des appels à CPython qui seraient normalement effectués par l'analyseur Python. Malgré son approche de transpilation attrayante et générale, il ne peut pas être facilement déployé sur du matériel embarqué en raison de sa dépendance intrinsèque à libpython . Dans le même temps, comme il mappe assez étroitement les constructions Python à leur implémentation CPython , on peut s'attendre à un certain nombre de problèmes de performances lorsqu'il s'agit de calcul haute performance à petite et moyenne échelle. Cela est particulièrement dû au fait que les opérations associées, par exemple, à la vérification du type, à l'allocation de mémoire et au garbage collection, qui peuvent ralentir l'exécution, sont également effectuées par le programme transpilé.
Cython est un langage de programmation dont le but est de faciliter l'écriture d'extensions C pour le langage Python. En particulier, il peut traduire (éventuellement) du code de type Python typé statiquement en code C qui repose sur CPython . De la même manière que les considérations faites pour Nuitka , cela en fait un outil puissant chaque fois qu'il est possible de s'appuyer sur libpython (et lorsque sa surcharge est négligeable, c'est-à-dire lorsqu'il s'agit de calculs à suffisamment grande échelle), mais pas dans le contexte qui nous intéresse ici.
Enfin, bien qu'il n'utilise pas Python comme langage source, il convient de mentionner que Julia est également compilée juste à temps (et partiellement en avance) dans le code LLVM. Le code LLVM émis s'appuie cependant sur la bibliothèque d'exécution Julia de sorte que des considérations similaires à celles faites pour Cython et Nuitka s'appliquent.
La transpilation de programmes écrits à l'aide d'un sous-ensemble restreint du langage Python en programmes C est réalisée à l'aide du transpileur de prometeo . Cet outil de transformation source-source analyse les arbres de syntaxe abstraite (AST) associés aux fichiers sources à transpiler afin d'émettre du code C performant et embarquable. Pour ce faire, des règles particulières doivent être imposées au code Python. Cela rend possible la tâche par ailleurs extrêmement difficile de transpiler un langage interprété de type canard de haut niveau en un langage compilé de bas niveau typé statiquement. Ce faisant, nous définissons ce que l'on appelle parfois un DSL intégré dans le sens où le langage résultant utilise la syntaxe d'un langage hôte (Python lui-même) et, dans le cas de prometeo , il peut également être exécuté par l'interpréteur Python standard. .
from prometeo import *
nx : dims = 2
nu : dims = 2
nxu : dims = nx + nu
N : dims = 5
def main () -> int :
# number of repetitions for timing
nrep : int = 10000
A : pmat = pmat ( nx , nx )
A [ 0 , 0 ] = 0.8
A [ 0 , 1 ] = 0.1
A [ 1 , 0 ] = 0.3
A [ 1 , 1 ] = 0.8
B : pmat = pmat ( nx , nu )
B [ 0 , 0 ] = 1.0
B [ 1 , 1 ] = 1.0
Q : pmat = pmat ( nx , nx )
Q [ 0 , 0 ] = 1.0
Q [ 1 , 1 ] = 1.0
R : pmat = pmat ( nu , nu )
R [ 0 , 0 ] = 1.0
R [ 1 , 1 ] = 1.0
A : pmat = pmat ( nx , nx )
B : pmat = pmat ( nx , nu )
Q : pmat = pmat ( nx , nx )
R : pmat = pmat ( nu , nu )
RSQ : pmat = pmat ( nxu , nxu )
Lxx : pmat = pmat ( nx , nx )
M : pmat = pmat ( nxu , nxu )
w_nxu_nx : pmat = pmat ( nxu , nx )
BAt : pmat = pmat ( nxu , nx )
BA : pmat = pmat ( nx , nxu )
pmat_hcat ( B , A , BA )
pmat_tran ( BA , BAt )
RSQ [ 0 : nu , 0 : nu ] = R
RSQ [ nu : nu + nx , nu : nu + nx ] = Q
# array-type Riccati factorization
for i in range ( nrep ):
pmt_potrf ( Q , Lxx )
M [ nu : nu + nx , nu : nu + nx ] = Lxx
for i in range ( 1 , N ):
pmt_trmm_rlnn ( Lxx , BAt , w_nxu_nx )
pmt_syrk_ln ( w_nxu_nx , w_nxu_nx , RSQ , M )
pmt_potrf ( M , M )
Lxx [ 0 : nx , 0 : nx ] = M [ nu : nu + nx , nu : nu + nx ]
return 0
De même, le code ci-dessus ( example/riccati/riccati_array.py
) peut être exécuté par l'interpréteur Python standard à l'aide de la commande pmt riccati_array.py --cgen=False
et prometeo peut générer, compiler et exécuter du code C en utilisant à la place pmt riccati_array.py --cgen=True
.
Afin de pouvoir transpiler en C, seul un sous-ensemble du langage Python est pris en charge. Cependant, les fonctionnalités non similaires à celles du C telles que la surcharge de fonctions et les classes sont prises en charge par le transpilateur de prometeo. L'exemple Riccati adapté ( examples/riccati/riccati_mass_spring_2.py
) ci-dessous montre comment les classes peuvent être créées et utilisées.
from prometeo import *
nm : dims = 4
nx : dims = 2 * nm
sizes : dimv = [[ 8 , 8 ], [ 8 , 8 ], [ 8 , 8 ], [ 8 , 8 ], [ 8 , 8 ]]
nu : dims = nm
nxu : dims = nx + nu
N : dims = 5
class qp_data :
A : List = plist ( pmat , sizes )
B : List = plist ( pmat , sizes )
Q : List = plist ( pmat , sizes )
R : List = plist ( pmat , sizes )
P : List = plist ( pmat , sizes )
fact : List = plist ( pmat , sizes )
def factorize ( self ) -> None :
M : pmat = pmat ( nxu , nxu )
Mxx : pmat = pmat ( nx , nx )
L : pmat = pmat ( nxu , nxu )
Q : pmat = pmat ( nx , nx )
R : pmat = pmat ( nu , nu )
BA : pmat = pmat ( nx , nxu )
BAtP : pmat = pmat ( nxu , nx )
pmat_copy ( self . Q [ N - 1 ], self . P [ N - 1 ])
pmat_hcat ( self . B [ N - 1 ], self . A [ N - 1 ], BA )
pmat_copy ( self . Q [ N - 1 ], Q )
pmat_copy ( self . R [ N - 1 ], R )
for i in range ( 1 , N ):
pmat_fill ( BAtP , 0.0 )
pmt_gemm_tn ( BA , self . P [ N - i ], BAtP , BAtP )
pmat_fill ( M , 0.0 )
M [ 0 : nu , 0 : nu ] = R
M [ nu : nu + nx , nu : nu + nx ] = Q
pmt_gemm_nn ( BAtP , BA , M , M )
pmat_fill ( L , 0.0 )
pmt_potrf ( M , L )
Mxx [ 0 : nx , 0 : nx ] = L [ nu : nu + nx , nu : nu + nx ]
# pmat_fill(self.P[N-i-1], 0.0)
pmt_gemm_nt ( Mxx , Mxx , self . P [ N - i - 1 ], self . P [ N - i - 1 ])
# pmat_print(self.P[N-i-1])
return
def main () -> int :
A : pmat = pmat ( nx , nx )
Ac11 : pmat = pmat ( nm , nm )
Ac12 : pmat = pmat ( nm , nm )
for i in range ( nm ):
Ac12 [ i , i ] = 1.0
Ac21 : pmat = pmat ( nm , nm )
for i in range ( nm ):
Ac21 [ i , i ] = - 2.0
for i in range ( nm - 1 ):
Ac21 [ i + 1 , i ] = 1.0
Ac21 [ i , i + 1 ] = 1.0
Ac22 : pmat = pmat ( nm , nm )
for i in range ( nm ):
for j in range ( nm ):
A [ i , j ] = Ac11 [ i , j ]
for i in range ( nm ):
for j in range ( nm ):
A [ i , nm + j ] = Ac12 [ i , j ]
for i in range ( nm ):
for j in range ( nm ):
A [ nm + i , j ] = Ac21 [ i , j ]
for i in range ( nm ):
for j in range ( nm ):
A [ nm + i , nm + j ] = Ac22 [ i , j ]
tmp : float = 0.0
for i in range ( nx ):
tmp = A [ i , i ]
tmp = tmp + 1.0
A [ i , i ] = tmp
B : pmat = pmat ( nx , nu )
for i in range ( nu ):
B [ nm + i , i ] = 1.0
Q : pmat = pmat ( nx , nx )
for i in range ( nx ):
Q [ i , i ] = 1.0
R : pmat = pmat ( nu , nu )
for i in range ( nu ):
R [ i , i ] = 1.0
qp : qp_data = qp_data ()
for i in range ( N ):
qp . A [ i ] = A
for i in range ( N ):
qp . B [ i ] = B
for i in range ( N ):
qp . Q [ i ] = Q
for i in range ( N ):
qp . R [ i ] = R
qp . factorize ()
return 0
Avertissement : prometeo en est encore à un stade très préliminaire et seules quelques opérations d'algèbre linéaire et constructions Python sont prises en charge pour le moment.