מתי כדאי ליצור תוסף קומפיילר?
פלאגין LiteRT Compiler נדרש כשרוצים לשלב מאיץ חומרה ספציפי עם תלות בקומפיילר במסגרת LiteRT.
כדאי ליצור פלאגין של קומפיילר אם:
- אתם מטרגטים קצה עורפי חדש של חומרה שלא נתמך.
- אתם רוצים להעביר פעולות ספציפיות של מודל למאיץ החומרה הזה כדי לשפר את הביצועים או את יעילות צריכת החשמל.
- צריך תמיכה בהידור AOT (בתחנת עבודה) או בהידור במכשיר.
הפלאגין משמש כגשר, לוקח חלקים ממודל למידת המכונה וממיר אותם לפורמט שחומרת היעד יכולה להריץ, באמצעות קריאה לקומפיילר של ה-backend. LiteRT מאגד את קוד הבייט המותאם אישית שנוצר על ידי התוסף במודל .tflite, וכך מאפשר להריץ אותו באמצעות זמן הריצה של LiteRT.
איך פלאגינים של קומפיילר עובדים?
מסגרת LiteRT משתמשת בתוסף של הקומפיילר במהלך טעינת המודל או בשלב העיבוד המקדים במצב אופליין, כדי לזהות ולהכין תתי-גרפים של המודל להרצה בחומרה הייעודית.
התהליך כולל שני שלבים עיקריים שמנוהלים על ידי המסגרת באמצעות הפונקציות המיוצאות של התוסף:
- חלוקה למחיצות: התוסף בודק את גרף המודל כולו ומזהה קבוצות משנה של פעולות שהוא תומך בהן ויכול להאיץ ביעילות בחומרה של היעד. התתי-גרפים הנתמכים האלה מחולקים (מסומנים) לצורך קומפילציה ומתוארים בקצרה.
- קומפילציה: מסגרת LiteRT מעבירה את הגרפים המשניים המופרדים בחזרה לפלאגין. לאחר מכן, התוסף משתמש בלוגיקה הפנימית שלו, ואולי בשרשראות כלים (קומפיילרים) חיצוניות, כדי ליצור מודול אחד או יותר של קוד בייט ספציפי לחומרה שמיישם את המחיצות. הבייטקוד הזה הוא מה שזמן הריצה (HAL/driver) של חומרת היעד יטען ויבצע בסופו של דבר.
המסגרת מחליפה את הגרפים המשניים המקוריים בפעולות בהתאמה אישית שמפעילות את מנהל ההתקן של החומרה, ומעבירות את קוד הבייט המהודר שנוצר על ידי התוסף.
LiteRT Dispatch הוא האנלוגיה של זמן הריצה לתוסף של קומפיילר. הם מספקים את האמצעים להתקשרות עם HAL בהינתן פלט של קומפיילר. פרטים נוספים מופיעים במאמרי העזרה בנושא שליחה.
AOT לעומת On-Device
LiteRT יכול להשתמש בתוספים של קומפיילר כדי לתמוך בקומפילציה של AOT באמצעות כלי הפיתוח שלנו, וגם בקומפילציה במכשיר. קומפילציה במכשיר היא גמישה יותר, היא מתבצעת באופן מלא בתוך ה-API של זמן הריצה של LiteRT, ונדרש רק ניהול של מודל יחיד. תהליך ה-AOT יכול לבטל את החסימה של קומפילציה כשהיא דורשת יותר מדי משאבים להרצה במכשיר, וזה יכול לקרות עם הרבה מודלים גדולים בני זמננו.
חלופי
LiteRT מבוסס על תמיכה בגרפים הטרוגניים. כל פעולה שלא נבחרה על ידי הפלאגין תועבר למעבד או תהיה זמינה להאצה בעורף אחר.
הטמעה של פלאגין של קומפיילר
פלאגין של מהדר LiteRT מיושם כספרייה משותפת שמייצאת קבוצה ספציפית של פונקציות C שמוגדרות ב-LiteRT C API.
Essential Interface Functions
הפונקציונליות העיקרית מתבססת על שני שלבי קומפילציה מרכזיים:
LiteRtCompilerPluginPartition ו-LiteRtCompilerPluginCompile.
| פונקציה | מטרה |
|---|---|
| LiteRtCompilerPluginPartition | בחירה וסימון של כל הפעולות הנתמכות בתת-גרף נתון של מודל (השלב Partition). |
| LiteRtCompilerPluginCompile$ | המערכת יוצרת את קוד הביניים הספציפי לחומרה עבור המחיצות שנבחרו מראש (השלב Compile). |
C API Snippets
// 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 יקבץ את כל הפעולות שנבחרו ל-sub-DAGs הגדולים ביותר שאפשר. כדי לבצע חלוקה מפורטת יותר, אפשר לשייך אינדקס כשבוחרים פעולות, וכך לחלק עוד יותר את הגרפים המשניים האלה.
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 לסוגים בספריית הקומפיילר של ה-Backend. הפלאגין של הדוגמה הפונקציונלית יוצר מחרוזת קריאה שמקודדת את הגרף.
// 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 מספק כלי עזר שונים להחלת פלאגינים של קומפיילר על קבצים של מודלים, להפעלת התוצאה ולאימות או להשוואה של ביצועים. מידע נוסף זמין במאמרי העזרה בנושא חבילת הבדיקות של המאיץ ובמאמרי העזרה בנושא השוואה בין ביצועים ויצירת פרופילים.