这就是 prometeo,一个用于嵌入式高性能计算的实验建模工具。 prometeo 提供了一种基于 Python 语言子集的领域特定语言 (DSL),允许人们方便地用高级语言(Python 本身)编写科学计算程序,并且可以轻松地将其转换为高性能独立的 C 代码可部署在嵌入式设备上。
prometeo 的文档可以在阅读文档中找到:https://prometeo.readthedocs.io/en/latest/index.html。
可以在此处找到一个简单的 hello world 示例,该示例展示了如何从 Python 运行一个简单的 prometeo 程序或将其转换为 C、构建并运行它。输出显示堆使用分析的结果和执行时间(在本例中没有太多可看的:p)。
由于 prometeo 程序转换为纯 C 代码,调用高性能线性代数库 BLASFEO(出版物:https://arxiv.org/abs/1704.02457,代码:https://github.com/giaf/blasfeo),执行时间可以可以与手写的高性能代码相媲美。下图显示了使用高度优化的手写 C 代码调用 BLASFEO 执行 Riccati 分解所需的 CPU 时间与使用本示例中的 prometeo 转译代码获得的 CPU 时间的比较。为了进行比较,还添加了使用 NumPy 和 Julia 获得的计算时间 - 但请注意,Riccati 分解的最后两个实现并不像 prometeo 生成的 C 代码和手动编码的 C 实现那么容易嵌入。所有基准测试均在配备 i7-7560U CPU、运行频率为 2.30 GHz 的 Dell XPS-9360 上运行(以避免因热节流而导致频率波动)。
此外,prometeo 的性能在很大程度上优于 Nuitka 等最先进的 Python 编译器。下表显示了斐波那契基准测试中获得的 CPU 时间。
解析器/编译器 | CPU 时间 [秒] |
---|---|
Python 3.7(CPython) | 11.787 |
努伊特卡 | 10.039 |
吡啶 | 1.78 |
普罗米泰奥 | 0.657 |
prometeo 可以通过 PyPI 使用pip install prometeo-dsl
安装。请注意,由于 prometeo 广泛使用类型提示来为 Python 代码配备静态类型信息,因此所需的最低 Python 版本为 3.6。
如果您想在本地计算机上安装 prometeo 构建源代码,您可以按以下步骤操作:
git submodule update --init
来克隆子模块。<prometeo_root>/prometeo/cpmt
运行make install_shared
以编译并安装与 C 后端关联的共享库。请注意,默认安装路径是<prometeo_root>/prometeo/cpmt/install
。virtualenv --python=<path_to_python3.6> <path_to_new_virtualenv>
设置虚拟环境。pip install -e .
从<prometeo_root>
安装 Python 包。最后,您可以使用pmt <example_name>.py --cgen=<True/False>
运行<root>/examples
中的示例,其中--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 是解释型、鸭子型和垃圾型。 - 收集语言,C 是一种编译型静态类型语言。
如果我们添加生成的 C 代码必须高效(即使对于中小型计算)并且可部署在嵌入式硬件上的约束,那么将 Python 转译为 C 的任务将变得更具挑战性。事实上,这两个要求直接意味着生成的代码无法使用: i)复杂的运行时库,例如Python运行时库,这些库通常在嵌入式硬件上不可用ii)动态内存分配,这会使执行缓慢且不可靠(例外情况是在设置阶段分配的内存,其大小是先验已知的)。
由于源代码到源代码的转换或转译,特别是 Python 代码到 C 代码的转译并不是一个未开发的领域,因此在下文中,我们将提到一些解决该问题的现有项目。在此过程中,我们强调它们在何处以及如何不满足上述两个要求之一,即(小规模)效率和可嵌入性。
现有多个软件包可以以各种形式解决 Python 到 C 的转换问题。
在高性能计算的背景下, Numba是用 Python 编写的数值函数的即时编译器。因此,其目标是将正确注释的 Python 函数(而不是整个程序)转换为高性能 LLVM 代码,以便加快其执行速度。 Numba使用要翻译的代码的内部表示,并对所涉及的变量执行(可能部分)类型推断,以生成可以从 Python 或 C/C++ 调用的 LLVM 代码。在某些情况下,即可以成功执行完整类型推断的情况下,可以生成不依赖于 C API 的代码(使用nopython标志)。然而,发出的 LLVM 代码仍然依赖Numpy进行 BLAS 和 LAPACK 操作。
Nuitka是一个源到源的编译器,可以将每个 Python 构造转换为链接到libpython库的 C 代码,因此它能够转译一大类 Python 程序。为了做到这一点,它依赖于这样一个事实:Python 语言最常用的实现之一,即CPython是用 C 编写的。事实上, Nuitka生成的 C 代码包含对CPython 的调用,这些调用通常由Python 解析器。尽管它具有有吸引力且通用的转译方法,但由于其对libpython的内在依赖,它无法轻松部署在嵌入式硬件上。同时,由于它将 Python 结构与其CPython实现相当紧密地映射在一起,因此在中小型高性能计算方面可能会出现许多性能问题。这尤其是因为与类型检查、内存分配和垃圾收集等相关的操作也会减慢执行速度,这些操作也是由转译的程序执行的。
Cython是一种编程语言,其目标是促进为 Python 语言编写 C 扩展。特别是,它可以将(可选)静态类型的类似 Python 的代码转换为依赖于CPython 的C 代码。与对Nuitka的考虑类似,只要可以依赖libpython (并且当其开销可以忽略不计,即处理足够大规模的计算时),这使其成为一个强大的工具,但不是在此处感兴趣的上下文中。
最后,虽然 Julia 不使用 Python 作为源语言,但我们应该提到的是, Julia也是即时(部分提前)编译为 LLVM 代码的。然而,发出的 LLVM 代码依赖于Julia运行时库,因此适用与Cython和Nuitka类似的注意事项。
使用prometeo的转译器将使用 Python 语言的受限子集编写的程序转译为 C 程序。该源到源转换工具分析与要转译的源文件关联的抽象语法树 (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 可以使用pmt riccati_array.py --cgen=True
生成、编译和运行 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 结构。