هذه هي prometeo، وهي أداة نمذجة تجريبية للحوسبة المدمجة عالية الأداء. يوفر prometeo لغة خاصة بالمجال (DSL) تعتمد على مجموعة فرعية من لغة Python التي تسمح للشخص بكتابة برامج حوسبة علمية بسهولة بلغة عالية المستوى (Python نفسها) والتي يمكن تحويلها بسهولة إلى كود C مستقل عالي الأداء بسهولة قابلة للنشر على الأجهزة المدمجة.
يمكن العثور على وثائق prometeo على قراءة المستندات على https://prometeo.readthedocs.io/en/latest/index.html.
يمكن العثور هنا على مثال بسيط لـ helloworld يوضح كيفية تشغيل برنامج prometeo تافه من Python أو نقله إلى لغة C وإنشائه وتشغيله. يُظهر الإخراج نتيجة تحليل استخدام الكومة ووقت التنفيذ (في هذه الحالة ليس هناك الكثير مما يمكن رؤيته :p).
نظرًا لأن برامج prometeo تنتقل إلى كود C النقي الذي يستدعي مكتبة الجبر الخطي عالية الأداء BLASFEO (المنشور: https://arxiv.org/abs/1704.02457، الكود: https://github.com/giaf/blasfeo)، يمكن أن يكون وقت التنفيذ يمكن مقارنتها بالكود عالي الأداء المكتوب بخط اليد. يوضح الشكل أدناه مقارنة بين وقت وحدة المعالجة المركزية اللازمة لتنفيذ تحليل Riccati باستخدام رمز C مكتوب بخط اليد مُحسَّن للغاية مع استدعاءات BLASFEO وتلك التي تم الحصول عليها باستخدام كود prometeo المنقول من هذا المثال. تمت إضافة أوقات الحساب التي تم الحصول عليها باستخدام NumPy وJulia أيضًا للمقارنة - مع ذلك، لاحظ أن هذين التطبيقين الأخيرين لتحليل Riccati ليسا قابلين للتضمين بسهولة مثل كود C الذي تم إنشاؤه بواسطة prometeo وتنفيذ C المشفر يدويًا. تم تشغيل جميع الاختبارات على جهاز Dell XPS-9360 المجهز بوحدة المعالجة المركزية i7-7560U التي تعمل بسرعة 2.30 جيجا هرتز (لتجنب تقلبات التردد بسبب الاختناق الحراري).
علاوة على ذلك، يمكن أن يتفوق prometeo إلى حد كبير على مترجمي Python الحديثين مثل Nuitka. يوضح الجدول أدناه أوقات وحدة المعالجة المركزية التي تم الحصول عليها وفقًا لمعيار فيبوناتشي.
محلل / مترجم | وقت وحدة المعالجة المركزية [ق] |
---|---|
بايثون 3.7 (كبايثون) | 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. ولدت تجميعها وتشغيلها.
كود بايثون ( 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
.
على الرغم من أن ترجمة برنامج مكتوب بلغة إلى أخرى ذات مستوى مماثل من التجريد يمكن أن تكون أسهل بكثير من الترجمة إلى برنامج ذو مستوى مختلف تمامًا من التجريد (خاصة إذا كانت اللغة المستهدفة ذات مستوى أقل بكثير)، فإن ترجمة برامج بايثون إلى برامج C لا يزال ينطوي على فجوة تجريد كبيرة، فهي ليست مهمة سهلة بشكل عام. بشكل عام، يكمن التحدي في ضرورة إعادة تنفيذ الميزات التي تدعمها اللغة المصدر في اللغة الهدف. على وجه الخصوص، عند ترجمة Python إلى C، تأتي الصعوبة من اختلاف مستوى التجريد في اللغتين ومن حقيقة أن اللغة المصدر واللغة الهدف هما نوعان مختلفان تمامًا: -اللغة المجمعة وC هي لغة مجمعة ومكتوبة بشكل ثابت .
تصبح مهمة نقل Python إلى C أكثر صعوبة إذا أضفنا القيد المتمثل في أن كود C الذي تم إنشاؤه يجب أن يكون فعالاً (حتى بالنسبة للحسابات الصغيرة إلى المتوسطة الحجم) وقابلاً للنشر على الأجهزة المدمجة. في الواقع، يشير هذان المتطلبان بشكل مباشر إلى أن التعليمات البرمجية التي تم إنشاؤها لا يمكنها الاستفادة من: 1) مكتبات وقت التشغيل المعقدة، على سبيل المثال، مكتبة وقت تشغيل Python، والتي لا تتوفر بشكل عام على الأجهزة المضمنة 2) تخصيص الذاكرة الديناميكية الذي من شأنه أن يجعل التنفيذ بطيئًا وغير موثوق به (تم إجراء استثناء للذاكرة المخصصة في مرحلة الإعداد والتي يُعرف حجمها مسبقًا).
نظرًا لأن تحويل التعليمات البرمجية من المصدر إلى المصدر، أو التحويل، وعلى وجه الخصوص تحويل كود Python إلى كود C ليس مجالًا غير مستكشف، في ما يلي، نذكر بعض المشاريع الحالية التي تتناوله. ومن خلال القيام بذلك، نسلط الضوء على أين وكيف أنها لا تلبي أحد المتطلبين الموضحين أعلاه، وهما الكفاءة (على نطاق صغير) وقابلية التضمين.
توجد العديد من حزم البرامج التي تتناول ترجمة Python إلى C بأشكال مختلفة.
في سياق الحوسبة عالية الأداء، يعد Numba مترجمًا فوريًا للوظائف الرقمية المكتوبة بلغة بايثون. على هذا النحو، فإن هدفه هو تحويل وظائف Python المشروحة بشكل صحيح، وليس البرامج بأكملها، إلى كود LLVM عالي الأداء بحيث يمكن تسريع تنفيذها. يستخدم Numba تمثيلًا داخليًا للكود المراد ترجمته ويقوم بإجراء استنتاج نوعي (ربما جزئيًا) على المتغيرات المعنية من أجل إنشاء كود LLVM الذي يمكن استدعاؤه إما من Python أو من C/C++. في بعض الحالات، وتحديدًا الحالات التي يمكن فيها تنفيذ استنتاج النوع الكامل بنجاح، يمكن إنشاء تعليمات برمجية لا تعتمد على واجهة برمجة تطبيقات C (باستخدام علامة nopython ). ومع ذلك، فإن كود LLVM المنبعث سيظل يعتمد على Numpy لعمليات BLAS وLAPACK.
Nuitka هو مترجم من المصدر إلى المصدر يمكنه ترجمة كل بنية Python إلى كود C الذي يرتبط بمكتبة libpython ، وبالتالي فهو قادر على تحويل فئة كبيرة من برامج Python. ومن أجل القيام بذلك، فهو يعتمد على حقيقة أن أحد أكثر تطبيقات لغة بايثون استخدامًا، وهو CPython ، مكتوب بلغة C. في الواقع، يقوم Nuitka بإنشاء كود C يحتوي على استدعاءات لـ CPython والتي يتم تنفيذها عادةً بواسطة محلل بايثون. على الرغم من أسلوب النقل الجذاب والعام، إلا أنه لا يمكن نشره بسهولة على الأجهزة المضمنة نظرًا لاعتماده الجوهري على libpython . في الوقت نفسه، نظرًا لأنه يرسم بشكل وثيق بنيات بايثون لتنفيذ CPython ، يمكن توقع عدد من مشكلات الأداء عندما يتعلق الأمر بالحوسبة عالية الأداء الصغيرة والمتوسطة الحجم. ويرجع ذلك بشكل خاص إلى حقيقة أن العمليات المرتبطة، على سبيل المثال، بفحص النوع وتخصيص الذاكرة وجمع البيانات المهملة التي يمكن أن تؤدي إلى إبطاء التنفيذ، يتم تنفيذها بواسطة البرنامج المنقول أيضًا.
Cython هي لغة برمجة هدفها تسهيل كتابة امتدادات لغة C للغة بايثون. على وجه الخصوص، يمكنه ترجمة (اختياريًا) التعليمات البرمجية المشابهة لـ Python المكتوبة بشكل ثابت إلى تعليمات برمجية C تعتمد على CPython . على غرار الاعتبارات التي تم وضعها لـ Nuitka ، فإن هذا يجعلها أداة قوية عندما يكون من الممكن الاعتماد على libpython (وعندما تكون نفقتها العامة ضئيلة، أي عند التعامل مع حسابات واسعة النطاق بما فيه الكفاية)، ولكن ليس في سياق الاهتمام هنا.
أخيرًا، على الرغم من أنها لا تستخدم بايثون كلغة مصدر، يجب أن نذكر أن جوليا أيضًا تم تجميعها في الوقت المناسب (وجزئيًا مسبقًا) في كود LLVM. ومع ذلك، يعتمد كود LLVM المنبعث على مكتبة وقت التشغيل Julia بحيث يتم تطبيق الاعتبارات المشابهة لتلك التي تم إجراؤها لـ Cython و Nuitka .
يتم تنفيذ ترجمة البرامج المكتوبة باستخدام مجموعة فرعية مقيدة من لغة بايثون إلى برامج C باستخدام محول prometeo . تقوم أداة التحويل من المصدر إلى المصدر هذه بتحليل أشجار بناء الجملة المجردة (AST) المرتبطة بالملفات المصدر المراد نقلها من أجل إصدار كود C عالي الأداء وقابل للتضمين. وللقيام بذلك، يجب فرض قواعد خاصة على كود بايثون. وهذا يجعل المهمة الصعبة للغاية المتمثلة في تحويل لغة مكتوبة على شكل بطة عالية المستوى مفسرة إلى لغة مكتوبة بشكل ثابت منخفضة المستوى ممكنة. ومن خلال القيام بذلك، نحدد ما يشار إليه أحيانًا باسم 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
بدلاً من ذلك. pmt riccati_array.py --cgen=True
.
لكي تتمكن من التحويل إلى لغة C، يتم دعم مجموعة فرعية فقط من لغة Python. ومع ذلك، يتم دعم الميزات غير المشابهة للغة C مثل التحميل الزائد للوظائف والفئات بواسطة جهاز الإرسال الخاص بـ prometeo. يوضح مثال 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 في الوقت الحالي.