這就是 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 編寫的。器。儘管它具有有吸引力且通用的轉譯方法,但由於其對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,僅支援 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 結構。