Esto es prometeo, una herramienta de modelado experimental para computación integrada de alto rendimiento. prometeo proporciona un lenguaje de dominio específico (DSL) basado en un subconjunto del lenguaje Python que permite escribir cómodamente programas informáticos científicos en un lenguaje de alto nivel (el propio Python) que se puede transpilar fácilmente a código C autónomo de alto rendimiento. Implementable en dispositivos integrados.
La documentación de prometeo se puede encontrar en Read the Docs en https://prometeo.readthedocs.io/en/latest/index.html.
Aquí se puede encontrar un ejemplo simple de Hola mundo que muestra cómo ejecutar un programa trivial prometeo desde Python o transpilarlo a C, compilarlo y ejecutarlo. El resultado muestra el resultado del análisis de uso del montón y el tiempo de ejecución (en este caso no hay mucho que ver :p).
Dado que los programas prometeo se transpilan a código C puro que llama a la biblioteca de álgebra lineal de alto rendimiento BLASFEO (publicación: https://arxiv.org/abs/1704.02457, código: https://github.com/giaf/blasfeo), el tiempo de ejecución puede ser comparable al código de alto rendimiento escrito a mano. La siguiente figura muestra una comparación del tiempo de CPU necesario para llevar a cabo una factorización de Riccati utilizando código C escrito a mano altamente optimizado con llamadas a BLASFEO y los obtenidos con código transpilado de prometeo de este ejemplo. Los tiempos de cálculo obtenidos con NumPy y Julia también se agregan para comparar; tenga en cuenta, sin embargo, que estas dos últimas implementaciones de la factorización de Riccati no son tan fácilmente integrables como el código C generado por prometeo y la implementación C codificada a mano. Todas las pruebas se ejecutaron en una Dell XPS-9360 equipada con una CPU i7-7560U que funciona a 2,30 GHz (para evitar fluctuaciones de frecuencia debido a la limitación térmica).
Además, prometeo puede superar en gran medida a los compiladores de Python de última generación como Nuitka. La siguiente tabla muestra los tiempos de CPU obtenidos en un punto de referencia de Fibonacci.
analizador/compilador | Tiempo de CPU [s] |
---|---|
Python 3.7 (CPython) | 11.787 |
Nuitka | 10.039 |
PyPy | 1,78 |
prometeo | 0,657 |
prometeo se puede instalar a través de PyPI con pip install prometeo-dsl
. Tenga en cuenta que, dado que prometeo hace un uso extensivo de sugerencias de tipo para equipar el código Python con información de escritura estática, la versión mínima de Python requerida es 3.6.
Si desea instalar prometeo construyendo las fuentes en su máquina local, puede proceder de la siguiente manera:
git submodule update --init
para clonar los submódulos.make install_shared
desde <prometeo_root>/prometeo/cpmt
para compilar e instalar la biblioteca compartida asociada con el backend de C. Tenga en cuenta que la ruta de instalación predeterminada es <prometeo_root>/prometeo/cpmt/install
.virtualenv --python=<path_to_python3.6> <path_to_new_virtualenv>
.pip install -e .
desde <prometeo_root>
para instalar el paquete Python. Finalmente, puede ejecutar los ejemplos en <root>/examples
con pmt <example_name>.py --cgen=<True/False>
, donde el indicador --cgen
determina si el código lo ejecuta el intérprete de Python o el código C. generado, compilado y ejecutado.
El código 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
puede ejecutarse mediante el intérprete estándar de Python (se requiere versión >3.6) y realizará las operaciones de álgebra lineal descritas usando el comando pmt simple_example.py --cgen=False
. Al mismo tiempo, prometeo puede analizar el código y analizar su árbol de sintaxis abstracta (AST) para generar el siguiente código C de alto rendimiento:
#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 ;
}
que se basa en el paquete de álgebra lineal de alto rendimiento BLASFEO. El código generado se compilará y ejecutará fácilmente cuando se ejecute pmt simple_example.py --cgen=True
.
Aunque traducir un programa escrito en un lenguaje a otro con un nivel de abstracción comparable puede ser significativamente más fácil que traducir a uno con un nivel de abstracción muy diferente (especialmente si el lenguaje de destino es de un nivel mucho más bajo), traducir programas Python a programas C todavía implica una considerable brecha de abstracción, no es una tarea fácil en general. En términos generales, el desafío radica en la necesidad de volver a implementar funciones que sean compatibles de forma nativa con el idioma de origen en el idioma de destino. En particular, al traducir Python a C, la dificultad proviene tanto del diferente nivel de abstracción de los dos lenguajes como del hecho de que el lenguaje de origen y el de destino son de dos tipos muy diferentes: Python es un lenguaje interpretado , de tipo pato y basura. -Lenguaje recopilado y C es un lenguaje compilado y tipado estáticamente .
La tarea de transpilar Python a C se vuelve aún más desafiante si agregamos la restricción de que el código C generado debe ser eficiente (incluso para cálculos de pequeña y mediana escala) y desplegable en hardware integrado. De hecho, estos dos requisitos implican directamente que el código generado no puede hacer uso de: i) bibliotecas de tiempo de ejecución sofisticadas, por ejemplo, la biblioteca de tiempo de ejecución de Python, que generalmente no están disponibles en el hardware integrado ii) asignación de memoria dinámica que haría que la ejecución fuera lenta y poco confiable (Se hace una excepción para la memoria que se asigna en una fase de configuración y cuyo tamaño se conoce a priori).
Dado que la transformación o transpilación de código fuente a fuente, y en particular la transpilación de código Python a código C, no es un ámbito inexplorado, a continuación mencionamos algunos proyectos existentes que lo abordan. Al hacerlo, destacamos dónde y cómo no satisfacen uno de los dos requisitos descritos anteriormente, a saber, eficiencia (a pequeña escala) e integrabilidad.
Existen varios paquetes de software que abordan la traducción de Python a C de diversas formas.
En el contexto de la informática de alto rendimiento, Numba es un compilador justo a tiempo para funciones numéricas escritas en Python. Como tal, su objetivo es convertir funciones de Python correctamente anotadas, no programas completos, en código LLVM de alto rendimiento de modo que se pueda acelerar su ejecución. Numba utiliza una representación interna del código a traducir y realiza una inferencia de tipo (potencialmente parcial) en las variables involucradas para generar código LLVM que se puede llamar desde Python o desde C/C++. En algunos casos, concretamente aquellos en los que se puede llevar a cabo con éxito una inferencia de tipos completa, se puede generar código que no depende de la API de C (utilizando el indicador nopython ). Sin embargo, el código LLVM emitido aún dependería de Numpy para las operaciones BLAS y LAPACK.
Nuitka es un compilador de fuente a fuente que puede traducir cada construcción de Python a código C que se vincula con la biblioteca libpython y, por lo tanto, es capaz de transpilar una gran clase de programas de Python. Para hacerlo, se basa en el hecho de que una de las implementaciones más utilizadas del lenguaje Python, CPython , está escrita en C. De hecho, Nuitka genera código C que contiene llamadas a CPython que normalmente serían realizadas por el analizador de Python. A pesar de su atractivo y general enfoque de transpilación, no se puede implementar fácilmente en hardware integrado debido a su dependencia intrínseca de libpython . Al mismo tiempo, dado que asigna bastante estrechamente las construcciones de Python a su implementación de CPython , se pueden esperar una serie de problemas de rendimiento cuando se trata de computación de alto rendimiento a pequeña y mediana escala. Esto se debe particularmente al hecho de que el programa transpilado también lleva a cabo operaciones asociadas con, por ejemplo, verificación de tipos, asignación de memoria y recolección de basura que pueden ralentizar la ejecución.
Cython es un lenguaje de programación cuyo objetivo es facilitar la escritura de extensiones C para el lenguaje Python. En particular, puede traducir (opcionalmente) código tipo Python escrito estáticamente a código C que se basa en CPython . De manera similar a las consideraciones hechas para Nuitka , esto la convierte en una herramienta poderosa siempre que sea posible confiar en libpython (y cuando su sobrecarga sea insignificante, es decir, cuando se trata de cálculos a escala suficientemente grande), pero no en el contexto de interés aquí.
Finalmente, aunque no utiliza Python como lenguaje fuente, debemos mencionar que Julia también se compila justo a tiempo (y parcialmente antes de tiempo) en código LLVM. Sin embargo, el código LLVM emitido depende de la biblioteca de tiempo de ejecución de Julia , por lo que se aplican consideraciones similares a las realizadas para Cython y Nuitka .
La transpilación de programas escritos utilizando un subconjunto restringido del lenguaje Python a programas C se lleva a cabo utilizando el transpilador de prometeo . Esta herramienta de transformación de fuente a fuente analiza árboles de sintaxis abstracta (AST) asociados con los archivos fuente que se transpilarán para emitir código C integrable y de alto rendimiento. Para ello, es necesario imponer reglas especiales al código Python. Esto hace posible la tarea, que de otro modo sería extremadamente desafiante, de transpilar un lenguaje interpretado de tipo pato de alto nivel a uno compilado de tipo estático de bajo nivel. Al hacerlo, definimos lo que a veces se denomina DSL integrado en el sentido de que el lenguaje resultante utiliza la sintaxis de un lenguaje anfitrión (el propio Python) y, en el caso de prometeo , también puede ser ejecutado por el intérprete estándar de Python. .
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 manera similar, el código anterior ( example/riccati/riccati_array.py
) puede ejecutarse mediante el intérprete estándar de Python usando el comando pmt riccati_array.py --cgen=False
y prometeo puede generar, compilar y ejecutar código C usando en su lugar pmt riccati_array.py --cgen=True
.
Para poder transpilar a C, solo se admite un subconjunto del lenguaje Python. Sin embargo, el transpilador de prometeo admite características que no son similares a las de C, como la sobrecarga de funciones y las clases. El ejemplo adaptado de Riccati ( examples/riccati/riccati_mass_spring_2.py
) a continuación muestra cómo se pueden crear y utilizar clases.
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
Descargo de responsabilidad: prometeo aún se encuentra en una etapa muy preliminar y por el momento solo se admiten unas pocas operaciones de álgebra lineal y construcciones de Python.