متى يجب إنشاء إضافة برنامج التجميع؟
يجب استخدام LiteRT Compiler Plugin عند الحاجة إلى دمج أداة تسريع أجهزة معيّنة مع تبعية برنامج التجميع في إطار عمل LiteRT.
عليك إنشاء مكوّن إضافي للمترجم في الحالات التالية:
- أنّك تستهدف برنامجًا جديدًا للواجهة الخلفية للأجهزة غير متاح.
- تريد نقل عمليات نموذج محدّد إلى مسرّع الأجهزة هذا لتحسين الأداء أو كفاءة استهلاك الطاقة.
- تحتاج إلى دعم للترجمة المسبقة (AOT) (على محطة العمل) أو الترجمة على الجهاز.
يعمل المكوّن الإضافي كجسر، حيث يأخذ أجزاءً من نموذج تعلُّم الآلة ويحوّلها إلى تنسيق يمكن أن ينفّذه الجهاز المستهدف، وذلك باستخدام طلب إلى برنامج الترجمة الخلفي. تجمع LiteRT الرمز الثانوي المخصّص الذي تم إنشاؤه بواسطة المكوّن الإضافي في نموذج .tflite، ما يجعله قابلاً للتنفيذ باستخدام وقت تشغيل LiteRT.
كيف تعمل مكوّنات Compiler الإضافية؟
يستخدم إطار عمل LiteRT إضافة المجمّع أثناء مرحلة تحميل النموذج أو مرحلة المعالجة المسبقة بلا إنترنت لتحديد الرسومات البيانية الفرعية للنموذج وإعدادها للتنفيذ على الجهاز المستهدف.
تتضمّن العملية مرحلتَين رئيسيتَين يديرهما إطار العمل باستخدام الدوال التي تم تصديرها من المكوّن الإضافي:
- التقسيم: تفحص الإضافة الرسم البياني للنموذج بأكمله وتحدّد المجموعات الفرعية من العمليات التي تتوافق معها ويمكنها تسريعها بكفاءة على الأجهزة المستهدَفة. يتم "تقسيم" (وضع علامة) الرسومات البيانية الفرعية المتوافقة هذه لتجميعها وتحديدها.
- التجميع: يعيد إطار عمل LiteRT الرسومات البيانية الفرعية المقسّمة إلى المكوّن الإضافي. بعد ذلك، تستخدم الإضافة منطقها الداخلي وربما سلاسل أدوات خارجية (برامج تجميع) لإنشاء وحدة أو أكثر من وحدات الرمز الثانوي الخاصة بالأجهزة التي تنفّذ الأقسام. وهذا الرمز الثانوي هو ما سيحمّله وقت التشغيل (طبقة تجريد الأجهزة/برنامج التشغيل) على الجهاز المستهدف وينفّذه في النهاية.
يستبدل إطار العمل الرسومات البيانية الفرعية الأصلية بعمليات مخصّصة تستدعي برنامج تشغيل الأجهزة، مع تمرير الرمز الثانوي المجمَّع الذي أنشأته الإضافة.
LiteRT Dispatch هي أداة وقت التشغيل المشابهة لمكوّن Compiler الإضافي. وهي توفّر وسائل استدعاء HAL التي تم إنشاؤها من خلال المترجم. لمزيد من التفاصيل، يُرجى الرجوع إلى مستندات الإرسال.
الترجمة الفورية مقابل الترجمة على الجهاز
يمكن أن تستخدم LiteRT مكوّنات إضافية للمترجم البرمجي من أجل إتاحة الترجمة البرمجية مسبقًا (AOT) من خلال أدواتنا، بالإضافة إلى الترجمة البرمجية على الجهاز. وتتسم الترجمة البرمجية على الجهاز بمرونة أكبر، وهي مدمجة بالكامل في واجهات برمجة التطبيقات لوقت التشغيل في LiteRT، ولا تتطلّب سوى إدارة نموذج واحد. يمكن أن يتيح مسار AOT إمكانية تجميع البيانات عندما يكون استهلاك الموارد كبيرًا جدًا بحيث لا يمكن تشغيلها على الجهاز، وهو ما قد يحدث مع العديد من النماذج الكبيرة الحديثة.
Fallback
تم تصميم LiteRT ليتوافق مع الرسوم البيانية غير المتجانسة. سيتم ترك أي عملية لم يحدّدها المكوّن الإضافي لوحدة المعالجة المركزية أو إتاحتها للتسريع على خلفية أخرى.
تنفيذ مكوّن إضافي لبرنامج تجميع
يتم تنفيذ مكوّن إضافي لمترجم LiteRT كمكتبة مشتركة تصدّر مجموعة محدّدة من دوال C المعرَّفة في واجهة برمجة تطبيقات LiteRT C.
وظائف الواجهة الأساسية
تتمحور الوظيفة الأساسية حول خطوتَين رئيسيتَين للتجميع:
LiteRtCompilerPluginPartition وLiteRtCompilerPluginCompile.
| الوظيفة | الغرض |
|---|---|
| LiteRtCompilerPluginPartition | يختار جميع العمليات المتوافقة ويضع علامة عليها ضمن الرسم البياني الفرعي لنموذج معيّن (خطوة التقسيم). |
| LiteRtCompilerPluginCompile$ | تُنشئ هذه الخطوة الرمز الثانوي الخاص بالأجهزة للأقسام المحدّدة مسبقًا (خطوة التجميع). |
مقتطفات C API
// Name associated with the manufacturer this plugin relates to.
LITERT_CAPI_EXPORT const char* LiteRtGetCompilerPluginSocManufacturer();
// Create and initialize the plugin instance.
LITERT_CAPI_EXPORT LiteRtStatus
LiteRtCreateCompilerPlugin(LiteRtCompilerPlugin* compiler_plugin,
LiteRtEnvironmentOptions env, LiteRtOptions options);
// Choose ops for compilation.
// This is the PARTITION step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
// Prepare result to pass to the runtime for given model containing partitioned
// subgraphs. This is the COMPILE step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
1. دالة التقسيم
توقيع الدالة هو:
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtSubgraph subgraph, LiteRtOpList selected_ops);
وظيفة الدالة partition: هذه هي مرحلة الاختيار. يتكرّر المكوّن الإضافي على العمليات في الإدخال LiteRtSubgraph. بالنسبة إلى كل عملية يتيحها الجهاز المستهدف ويمكن تسريعها، تضيف الإضافة هذه العملية إلى LiteRtOpList$ المقدَّمة في المَعلمة selected_ops. يستخدم إطار عمل LiteRt هذه القائمة لتحديد حدود الأقسام التي سيتم إرسالها إلى خطوة التجميع النهائية.
بشكلٍ تلقائي، ستجمّع LiteRT جميع العمليات المحدّدة في أكبر مخططات فرعية ممكنة من نوع DAG. لتقسيم أكثر دقة، يمكن ربط فهرس عند اختيار العمليات التي تؤدي إلى تقسيم هذه الرسومات البيانية الفرعية بشكل أكبر.
2. دالة التجميع
توقيع الدالة هو:
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result);
مهمة الدالة compile: هذه هي مرحلة الإنشاء. يمثّل الإدخال partitions نموذجًا تم فيه عزل جميع الرسومات البيانية الفرعية المحدّدة. تعالج الإضافة هذه الأقسام، وتستدعي سلسلة الأدوات المحدّدة لإنشاء رمز البايت للأجهزة المستهدَفة. من المتوقّع أن يوفّر الناتج من المكوّن الإضافي نقطة دخول لكل رسم بياني فرعي تم تمريره للتجميع. في معظم الحالات، يكون ذلك إما وحدات رموز بايت فردية لكل رسم بياني فرعي للإدخال، أو وحدة رموز بايت واحدة تتضمّن نقاط إدخال متعددة.
نوع البيانات التي تعرضها الدالة compile: تعرض الدالة LiteRtCompilerPluginCompile
ناتجها باستخدام المَعلمة الخارجية LiteRtCompiledResult.
LiteRtCompiledResult هو معرّف غير شفاف (في ما يتعلق بـ LiteRT) لبنية يديرها البرنامج المساعد. يمثّل هذا الحقل نتيجة عملية التجميع
ويتضمّن جزأين رئيسيين من المعلومات:
- وحدات شفرة البايت: هي مخازن مؤقتة للذاكرة الأولية تحتوي على شفرة البايت التنفيذية الخاصة بالأجهزة (أي التعليمات المجمّعة).
- معلومات المكالمة: البيانات الوصفية لكل قسم يوفر ذلك عملية الربط
من الرسم البياني الفرعي للإدخال
iإلى وحدة رمز بايت للنتيجة ومعرّف نقطة الدخول إلى تلك الوحدة.
مثال على التنفيذ
توضّح المقتطفات التالية كيف يمكن لمكوّن إضافي أساسي تنفيذ الوظائف الأساسية. هذا المثال مأخوذ من مثال يعمل بشكل كامل في
litert/vendors/examples/
تحديد المكوّن الإضافي وإعداده
توفّر هذه الدوال إطار العمل مع معلومات أساسية حول المكوّن الإضافي والأجهزة.
// Define the plugin's internal state structure
struct LiteRtCompilerPluginT {};
// Identify the manufacturer
const char* LiteRtGetCompilerPluginSocManufacturer() {
return "AcmeCorp"; // Example manufacturer name
}
// Specify the supported hardware (in this example, it supports kLiteRtHwAcceleratorNpu)
LiteRtStatus LiteRtGetCompilerPluginSupportedHardware(
LiteRtCompilerPlugin compiler_plugin,
LiteRtHwAccelerators* supported_hardware) {
// ... argument checking ...
*supported_hardware = kLiteRtHwAcceleratorNpu;
return kLiteRtStatusOk;
}
منطق التقسيم (LiteRtCompilerPluginPartition)
يعرض هذا المثال المكوّن الإضافي الذي يختار مجموعة محدودة من العمليات (mul وsub وعملية مركّبة معيّنة) فقط إذا كانت جميع المدخلات والمخرجات عبارة عن أرقام فاصلة عائمة بنظام 32 بت. عادةً ما يتضمّن تحديد ما إذا كان يجب اختيار عملية أم لا استدعاءً لخطاف التحقّق من الصحة في سلسلة أدوات المترجم الخلفية.
LiteRtStatus LiteRtCompilerPluginPartition(LiteRtCompilerPlugin compiler_plugin,
const char* soc_model,
LiteRtSubgraph subgraph,
LiteRtOpList selected_ops) {
// Iterate over ops and check criteria for selection
// (using a C++ wrapper namespace '::litert' for convenience).
// `subgraph` is a single subgraph from the original model, as such
// this function will be called for each subgraph in the original model.
::litert::Subgraph main_subgraph(subgraph);
for (const auto& op : main_subgraph.Ops()) {
// 1. Check a constraint: require all tensors to be Float32
bool only_f32 = true;
// ... logic to check input/output types ...
if (!only_f32) {
continue;
}
// 2. Check op codes and push to selected_ops list
if (op.Code() == kLiteRtOpCodeTflMul) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeTflSub) {
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
} else if (op.Code() == kLiteRtOpCodeShloComposite) {
// Example of checking composite op options
// ... logic to check for "odml.rms_norm" name ...
LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
}
}
return kLiteRtStatusOk;
}
قبل استدعاء عملية التجميع، ستتحقّق LiteRT من صحة جميع العمليات المحدّدة و "تحدّدها" في رسومات بيانية فرعية جديدة ضمن نموذج وسيط جديد. هذا النموذج الوسيط هو ما يتم تمريره إلى عملية التجميع.
منطق التجميع (LiteRtCompilerPluginCompile)
تأخذ هذه الدالة الرسومات البيانية الفرعية المقسّمة وتنشئ LiteRtCompiledResult مخصّصًا. ينشئ هذا المثال وحدة رمز بايت مستقلة لكل قسم سيتم تجميعه. في الحالات الفعلية، يتضمّن ذلك عادةً تحويل عمليات LiteRT إلى أنواع لمكتبة المحوّل البرمجي الخلفية. تنشئ "عملية التحويل البرمجي" الخاصة بمثال المكوّن الإضافي الوظيفي سلسلة يمكن للمستخدم قراءتها وتشفّر الرسم البياني.
// Internal structure defining the compiled output
struct LiteRtCompiledResultT {
std::vector<std::string> byte_code; // The hardware bytecode buffers
std::vector<std::string> per_op_data; // Per-call metadata (CallInfo)
};
LiteRtStatus LiteRtCompilerPluginCompile(
LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
LiteRtModel partitions, LiteRtCompiledResult* compiled_result) {
// 1. Create the internal result structure
auto model = litert::Model::CreateFromNonOwnedHandle(partitions);
const auto num_partitions = model.NumSubgraphs();
auto result = std::make_unique<LiteRtCompiledResultT>();
result->byte_code.resize(num_partitions);
result->per_op_data.resize(num_partitions);
// 2. Iterate and compile each partition
for (auto i = 0; i < num_partitions; ++i) {
// CompileSinglePartition is an internal helper that converts the subgraph
// into the target hardware's format and stores it in result->byte_code.
// In the case of the example this is just a stringification of the graph.
// ... internal call to CompileSinglePartition ...
// Example: result.byte_code[i] = generated_hw_code;
// Example: result.per_op_data[i] = absl::StrFormat("Partition_%d", i);
// The "per_op_data" is a unique identifier associated to the `ith` partition.
// This is analogous to the name of a function in a library.
// This is only meaningful when the plugin is preparing single modules with multiple entry points.
}
// 3. Pass ownership of the result back to the framework
*compiled_result = result.release();
return kLiteRtStatusOk;
}
// Functions to expose the compiled result data to the framework
LiteRtStatus LiteRtGetCompiledResultByteCode(
LiteRtCompiledResult compiled_result, LiteRtParamIndex byte_code_idx,
const void** byte_code, size_t* byte_code_size) {
// ... implementation reads from compiled_result->byte_code ...
}
// ... other LiteRtGetCompiledResult* functions ...
الاستخدام والتحقّق من الصحة
توفّر LiteRT أدوات مختلفة لتطبيق مكوّنات إضافية للمترجم على ملفات النماذج، وتنفيذ النتيجة، والتحقّق من صحتها وقياس أدائها. راجِع مستندات مجموعة اختبارات أدوات التسريع ومستندات قياس الأداء وإنشاء الملفات الشخصية.