Trata-se do prometeo, uma ferramenta de modelagem experimental para computação embarcada de alto desempenho. prometeo fornece uma linguagem de domínio específico (DSL) baseada em um subconjunto da linguagem Python que permite escrever convenientemente programas de computação científica em uma linguagem de alto nível (o próprio Python) que pode ser facilmente transpilada para código C independente de alto desempenho implantável em dispositivos incorporados.
A documentação do prometeo pode ser encontrada em Read the Docs em https://prometeo.readthedocs.io/en/latest/index.html.
Um exemplo simples de hello world que mostra como executar um programa prometeo trivial em Python ou transpilá-lo para C, construí-lo e executá-lo pode ser encontrado aqui. A saída mostra o resultado da análise de uso do heap e o tempo de execução (neste caso não há muito para ver :p).
Como os programas Promeo são transpilados para código C puro que chama a biblioteca de álgebra linear de alto desempenho BLASFEO (publicação: https://arxiv.org/abs/1704.02457, código: https://github.com/giaf/blasfeo), o tempo de execução pode ser comparável ao código de alto desempenho escrito à mão. A figura abaixo mostra uma comparação do tempo de CPU necessário para realizar uma fatoração Riccati usando código C manuscrito altamente otimizado com chamadas ao BLASFEO e aqueles obtidos com o código transpilado prometeo deste exemplo. Os tempos de cálculo obtidos com NumPy e Julia também são adicionados para comparação - observe, entretanto, que essas duas últimas implementações da fatoração Riccati não são tão facilmente incorporáveis quanto o código C gerado por prometeo e a implementação C codificada manualmente. Todos os benchmarks foram executados em um Dell XPS-9360 equipado com uma CPU i7-7560U rodando a 2,30 GHz (para evitar flutuações de frequência devido ao afogamento térmico).
Além disso, o prometeo pode superar em grande parte os compiladores Python de última geração, como o Nuitka. A tabela abaixo mostra os tempos de CPU obtidos em um benchmark Fibonacci.
analisador/compilador | Tempo de CPU[s] |
---|---|
Python 3.7 (CPython) | 11.787 |
Nuitka | 10.039 |
PyPy | 1,78 |
prometeo | 0,657 |
prometeo pode ser instalado através do PyPI com pip install prometeo-dsl
. Observe que, como o prometeo faz uso extensivo de dicas de tipo para equipar o código Python com informações de digitação estática, a versão mínima do Python necessária é 3.6.
Se você deseja instalar o prometeo construindo os fontes em sua máquina local você pode proceder da seguinte forma:
git submodule update --init
para clonar os submódulos.make install_shared
em <prometeo_root>/prometeo/cpmt
para compilar e instalar a biblioteca compartilhada associada ao backend C. Observe que o caminho de instalação padrão é <prometeo_root>/prometeo/cpmt/install
.virtualenv --python=<path_to_python3.6> <path_to_new_virtualenv>
.pip install -e .
de <prometeo_root>
para instalar o pacote Python. Finalmente, você pode executar os exemplos em <root>/examples
com pmt <example_name>.py --cgen=<True/False>
, onde o sinalizador --cgen
determina se o código é executado pelo interpretador Python ou se o código C é gerado, compilado e executado.
O 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
pode ser executado pelo interpretador Python padrão (versão> 3.6 necessária) e executará as operações de álgebra linear descritas usando o comando pmt simple_example.py --cgen=False
. Ao mesmo tempo, o código pode ser analisado pelo prometeo e sua árvore sintática abstrata (AST) analisada para gerar o seguinte código C de alto desempenho:
#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 conta com o pacote de álgebra linear de alto desempenho BLASFEO. O código gerado será prontamente compilado e executado ao executar pmt simple_example.py --cgen=True
.
Embora traduzir um programa escrito em uma linguagem para outra com um nível de abstração comparável possa ser significativamente mais fácil do que traduzir para um com um nível de abstração muito diferente (especialmente se a linguagem alvo for de nível muito inferior), traduzir programas Python em programas C ainda envolve uma lacuna de abstração considerável, não é uma tarefa fácil em geral. Em termos gerais, o desafio reside na necessidade de reimplementar funcionalidades que são suportadas nativamente pela linguagem de origem na linguagem de destino. Em particular, ao traduzir Python para C, a dificuldade vem tanto do diferente nível de abstração das duas linguagens quanto do fato de que a linguagem fonte e a linguagem alvo são de dois tipos muito diferentes: Python é uma linguagem interpretada , do tipo pato e lixo. -linguagem coletada e C é uma linguagem compilada e digitada estaticamente .
A tarefa de transpilar Python para C torna-se ainda mais desafiadora se adicionarmos a restrição de que o código C gerado deve ser eficiente (mesmo para cálculos de pequena e média escala) e implantável em hardware embarcado. Na verdade, esses dois requisitos implicam diretamente que o código gerado não pode fazer uso de: i) bibliotecas de tempo de execução sofisticadas, por exemplo, a biblioteca de tempo de execução Python, que geralmente não estão disponíveis em hardware embarcado ii) alocação dinâmica de memória que tornaria a execução lenta e não confiável (exceção feita para memória alocada em fase de configuração e cujo tamanho é conhecido a priori).
Como a transformação de código fonte para fonte, ou transpilação, e em particular a transpilação de código Python para código C não é um domínio inexplorado, a seguir mencionamos alguns projetos existentes que abordam isso. Ao fazê-lo, destacamos onde e como eles não satisfazem um dos dois requisitos descritos acima, nomeadamente eficiência (em pequena escala) e capacidade de incorporação.
Existem vários pacotes de software que abordam a tradução de Python para C de várias formas.
No contexto da computação de alto desempenho, Numba é um compilador just-in-time para funções numéricas escritas em Python. Como tal, seu objetivo é converter funções Python devidamente anotadas, e não programas inteiros, em código LLVM de alto desempenho, de modo que sua execução possa ser acelerada. Numba usa uma representação interna do código a ser traduzido e realiza uma inferência de tipo (potencialmente parcial) nas variáveis envolvidas para gerar código LLVM que pode ser chamado de Python ou de C/C++. Em alguns casos, nomeadamente aqueles em que uma inferência completa de tipos pode ser realizada com sucesso, pode ser gerado código que não depende da API C (usando a flag nopython ). No entanto, o código LLVM emitido ainda dependeria do Numpy para operações BLAS e LAPACK.
Nuitka é um compilador fonte-a-fonte que pode traduzir cada construção Python em código C vinculado à biblioteca libpython e, portanto, é capaz de transpilar uma grande classe de programas Python. Para tal, baseia-se no facto de uma das implementações mais utilizadas da linguagem Python, nomeadamente CPython , ser escrita em C. Na verdade, Nuitka gera código C que contém chamadas ao CPython que normalmente seriam realizadas por o analisador Python. Apesar de sua abordagem de transpilação atraente e geral, ele não pode ser facilmente implantado em hardware embarcado devido à sua dependência intrínseca do libpython . Ao mesmo tempo, uma vez que mapeia de forma bastante próxima as construções Python para sua implementação CPython , uma série de problemas de desempenho podem ser esperados quando se trata de computação de alto desempenho de pequena e média escala. Isto se deve principalmente ao fato de que as operações associadas, por exemplo, à verificação de tipo, alocação de memória e coleta de lixo que podem retardar a execução também são realizadas pelo programa transpilado.
Cython é uma linguagem de programação cujo objetivo é facilitar a escrita de extensões C para a linguagem Python. Em particular, ele pode traduzir (opcionalmente) código tipo Python digitado estaticamente em código C que depende de CPython . Da mesma forma que as considerações feitas para Nuitka , isso o torna uma ferramenta poderosa sempre que for possível confiar no libpython (e quando sua sobrecarga for insignificante, ou seja, ao lidar com cálculos em escala suficientemente grande), mas não no contexto de interesse aqui.
Finalmente, embora não use Python como linguagem fonte, devemos mencionar que Julia também é compilada just-in-time (e parcialmente antecipadamente) em código LLVM. O código LLVM emitido depende, entretanto, da biblioteca de tempo de execução Julia , de modo que considerações semelhantes às feitas para Cython e Nuitka se aplicam.
A transpilação de programas escritos usando um subconjunto restrito da linguagem Python para programas C é realizada usando o transpiler do prometeo . Esta ferramenta de transformação de fonte para fonte analisa árvores de sintaxe abstratas (AST) associadas aos arquivos de origem a serem transpilados para emitir código C incorporável e de alto desempenho. Para fazer isso, regras especiais precisam ser impostas ao código Python. Isso torna possível a tarefa extremamente desafiadora de transpilar uma linguagem interpretada de tipo pato de alto nível para uma linguagem compilada de tipo estaticamente de baixo nível. Ao fazer isso, definimos o que às vezes é chamado de DSL incorporada no sentido de que a linguagem resultante usa a sintaxe de uma linguagem hospedeira (o próprio Python) e, no caso de prometeo , também pode ser executada pelo interpretador Python padrão .
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
Da mesma forma, o código acima ( example/riccati/riccati_array.py
) pode ser executado pelo interpretador Python padrão usando o comando pmt riccati_array.py --cgen=False
e prometeo pode gerar, compilar e executar código C usando pmt riccati_array.py --cgen=True
.
Para poder transpilar para C, apenas um subconjunto da linguagem Python é suportado. No entanto, recursos não semelhantes ao C, como sobrecarga de funções e classes, são suportados pelo transpiler do prometeo. O exemplo adaptado de Riccati ( examples/riccati/riccati_mass_spring_2.py
) abaixo mostra como as classes podem ser criadas e usadas.
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
Isenção de responsabilidade: prometeo ainda está em um estágio preliminar e apenas algumas operações de álgebra linear e construções Python são suportadas por enquanto.