Это prometeo, экспериментальный инструмент моделирования встроенных высокопроизводительных вычислений. prometeo предоставляет доменно-ориентированный язык (DSL), основанный на подмножестве языка Python, который позволяет удобно писать научные вычислительные программы на языке высокого уровня (сам Python), который можно легко преобразовать в высокопроизводительный автономный код C. возможность развертывания на встроенных устройствах.
Документацию prometeo можно найти на странице Read the Docs по адресу https://prometeo.readthedocs.io/en/latest/index.html.
Простой пример hello world, показывающий, как запустить тривиальную программу prometeo из Python или перенести ее на C, собрать и запустить, можно найти здесь. Вывод показывает результат анализа использования кучи и время выполнения (в данном случае особо не на что смотреть :p).
Поскольку программы prometeo переводятся в чистый код C, который вызывает высокопроизводительную библиотеку линейной алгебры BLASFEO (публикация: https://arxiv.org/abs/1704.02457, код: https://github.com/giaf/blasfeo), время выполнения может быть сравним с написанным вручную высокопроизводительным кодом. На рисунке ниже показано сравнение времени ЦП, необходимого для выполнения факторизации Риккати с использованием высокооптимизированного рукописного кода C с вызовами BLASFEO, и времени, полученного с помощью транспилированного кода Prometeo из этого примера. Время вычислений, полученное с помощью NumPy и Julia, также добавлено для сравнения — однако обратите внимание, что эти последние две реализации факторизации Риккати не так легко встраиваются, как код C, сгенерированный prometeo, и реализация C, написанная вручную. Все тесты проводились на Dell XPS-9360, оснащенном процессором i7-7560U, работающим на частоте 2,30 ГГц (чтобы избежать колебаний частоты из-за теплового регулирования).
Более того, prometeo может значительно превзойти современные компиляторы Python, такие как Nuitka. В таблице ниже показано время процессора, полученное на тесте Фибоначчи.
парсер/компилятор | Время процессора [с] |
---|---|
Питон 3.7 (CPython) | 11,787 |
Нуитка | 10.039 |
PyPy | 1,78 |
Прометео | 0,657 |
prometeo можно установить через PyPI с помощью pip install prometeo-dsl
. Обратите внимание: поскольку prometeo широко использует подсказки типов для оснащения кода Python статической информацией о типизации, минимальная требуемая версия Python — 3.6.
Если вы хотите установить prometeo со сборкой исходных кодов на свой локальный компьютер, вы можете сделать следующее:
git submodule update --init
чтобы клонировать подмодули.make install_shared
из <prometeo_root>/prometeo/cpmt
чтобы скомпилировать и установить общую библиотеку, связанную с серверной частью C. Обратите внимание, что путь установки по умолчанию — <prometeo_root>/prometeo/cpmt/install
.virtualenv --python=<path_to_python3.6> <path_to_new_virtualenv>
.pip install -e .
из <prometeo_root>
, чтобы установить пакет Python. Наконец, вы можете запустить примеры в <root>/examples
с помощью pmt <example_name>.py --cgen=<True/False>
, где флаг --cgen
определяет, выполняется ли код интерпретатором Python или код C. сгенерированный, скомпилированный и запущенный.
Код 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
может быть запущен стандартным интерпретатором Python (требуется версия >3.6) и будет выполнять описанные операции линейной алгебры с помощью команды pmt simple_example.py --cgen=False
. В то же время код может быть проанализирован prometeo и проанализирован его абстрактным синтаксическим деревом (AST), чтобы сгенерировать следующий высокопроизводительный код C:
#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 ;
}
который основан на высокопроизводительном пакете линейной алгебры BLASFEO. Сгенерированный код будет легко скомпилирован и запущен при запуске pmt simple_example.py --cgen=True
.
Хотя перевод программы, написанной на одном языке, на другой язык с сопоставимым уровнем абстракции может быть значительно проще, чем перевод на язык с совершенно другим уровнем абстракции (особенно если целевой язык имеет гораздо более низкий уровень), перевод программ Python в программы C все еще включает в себя значительный пробел в абстракции, и в целом это непростая задача. Грубо говоря, проблема заключается в необходимости переопределить функции, которые изначально поддерживаются исходным языком, на целевом языке. В частности, при переводе Python на C сложность возникает как из-за разного уровня абстракции двух языков, так и из-за того, что исходный и целевой язык относятся к двум очень разным типам: Python — это интерпретируемый , утино-типизированный и мусорный язык. -collected язык, а C — компилируемый и статически типизированный язык.
Задача переноса Python на C становится еще более сложной, если мы добавим ограничение, согласно которому сгенерированный код C должен быть эффективным (даже для вычислений малого и среднего масштаба) и пригодным для развертывания на встроенном оборудовании. Фактически эти два требования напрямую подразумевают, что сгенерированный код не может использовать: i) сложные библиотеки времени выполнения, например библиотеку времени выполнения Python, которые обычно недоступны на встроенном оборудовании ; ii) динамическое распределение памяти, которое сделало бы выполнение медленным и ненадежным. (исключение составляет память, которая выделяется на этапе установки и размер которой известен априори).
Поскольку преобразование исходного кода, или транспиляция, и, в частности, транспиляция кода Python в код C, не является неизведанной областью, ниже мы упомянем несколько существующих проектов, которые решают эту проблему. При этом мы подчеркиваем, где и как они не удовлетворяют одному из двух требований, изложенных выше, а именно (малого масштаба) эффективности и встраиваемости.
Существует несколько пакетов программного обеспечения, которые в различных формах реализуют перевод с Python на C.
В контексте высокопроизводительных вычислений Numba представляет собой своевременный компилятор числовых функций, написанных на Python. Таким образом, его цель — преобразовать правильно аннотированные функции Python, а не целые программы, в высокопроизводительный код LLVM, чтобы можно было ускорить их выполнение. Numba использует внутреннее представление транслируемого кода и выполняет (потенциально частичный) вывод типа задействованных переменных, чтобы сгенерировать код LLVM, который можно вызывать либо из Python, либо из C/C++. В некоторых случаях, а именно в тех, где полный вывод типа может быть выполнен успешно, может быть сгенерирован код, не полагающийся на C API (с использованием флага nopython ). Однако создаваемый код LLVM по-прежнему будет использовать Numpy для операций BLAS и LAPACK.
Nuitka — это компилятор исходного кода, который может транслировать каждую конструкцию Python в код C, связанный с библиотекой libpython , и, следовательно, способен транспилировать большой класс программ Python. Для этого он опирается на тот факт, что одна из наиболее часто используемых реализаций языка Python, а именно CPython , написана на C. Фактически, Nuitka генерирует код C, содержащий вызовы CPython , которые обычно выполняются парсер Python. Несмотря на привлекательный и общий подход к транспиляции, его нелегко развернуть на встроенном оборудовании из-за его внутренней зависимости от libpython . В то же время, поскольку он довольно близко сопоставляет конструкции Python с их реализацией CPython , можно ожидать ряда проблем с производительностью, когда речь идет о высокопроизводительных вычислениях малого и среднего масштаба. В частности, это связано с тем, что операции, связанные, например, с проверкой типов, выделением памяти и сборкой мусора, которые могут замедлить выполнение, выполняются также и транспилированной программой.
Cython — это язык программирования, цель которого — облегчить написание расширений C для языка Python. В частности, он может транслировать (необязательно) статически типизированный код Python в код C, основанный на CPython . Подобно соображениям, сделанным для Nuitka , это делает его мощным инструментом всякий раз, когда можно положиться на libpython (и когда его накладные расходы незначительны, т. е. когда приходится иметь дело с достаточно крупномасштабными вычислениями), но не в контексте, интересующем нас здесь.
Наконец, хотя в качестве исходного языка он не использует Python, следует отметить, что Julia также компилируется в код LLVM «точно в срок» (и частично с опережением времени). Однако созданный код LLVM опирается на библиотеку времени выполнения Julia , поэтому применимы соображения, аналогичные тем, которые были сделаны для Cython и Nuitka .
Транспиляция программ, написанных с использованием ограниченного подмножества языка Python, в программы на языке C осуществляется с помощью транспилятора prometeo . Этот инструмент преобразования исходного кода анализирует абстрактные синтаксические деревья (AST), связанные с исходными файлами, которые необходимо передать, чтобы создать высокопроизводительный и встраиваемый код C. Для этого к коду Python необходимо применить специальные правила. Это делает возможной чрезвычайно сложную задачу по транспиляции интерпретируемого языка высокого уровня с утиной типизацией в скомпилированный низкоуровневый статически типизированный язык. При этом мы определяем то, что иногда называют встроенным DSL в том смысле, что полученный язык использует синтаксис основного языка (сам Python) и, в случае prometeo , он также может выполняться стандартным интерпретатором 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
Аналогично, приведенный выше код ( example/riccati/riccati_array.py
) может быть запущен стандартным интерпретатором Python с помощью команды pmt riccati_array.py --cgen=False
, а prometeo может генерировать, компилировать и запускать код C, используя вместо этого pmt riccati_array.py --cgen=True
.
Для возможности переноса на C поддерживается только подмножество языка Python. Однако транспилятор Prometeo поддерживает функции, не подобные C, такие как перегрузка функций и классы. Адаптированный пример Riccati ( examples/riccati/riccati_mass_spring_2.py
) ниже показывает, как можно создавать и использовать классы.
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
Отказ от ответственности: prometeo все еще находится на очень предварительной стадии, и на данный момент поддерживаются только несколько операций линейной алгебры и конструкций Python.