Android Neural Networks API (NNAPI) הוא ממשק API ל-C ב-Android שמיועד להרצת פעולות עתירות חישובים של למידת מכונה במכשירי Android. NNAPI נועד לספק שכבת פונקציונליות בסיסית למסגרות ברמה גבוהה יותר של למידת מכונה, כמו TensorFlow Lite ו-Caffe2, שמשמשות ליצירה ולהדרכה של רשתות נוירונליות. ה-API זמין בכל מכשירי Android עם Android 8.1 (רמת API 27) ואילך.
NNAPI תומך בהסקת מסקנות על ידי החלת נתונים ממכשירי Android על מודלים שהוגדרו על ידי המפתחים ואומנו בעבר. דוגמאות להסקה כוללות סיווג תמונות, חיזוי התנהגות משתמשים ובחירת תשובות מתאימות לשאילתת חיפוש.
ליצירת ההשלמות במכשיר יש יתרונות רבים:
- זמן אחזור: אין צורך לשלוח בקשה בחיבור לרשת ולהמתין לתגובה. לדוגמה, זה יכול להיות קריטי לאפליקציות וידאו שמעבדות פריימים עוקבים שמגיעים ממצלמה.
- זמינות: האפליקציה פועלת גם מחוץ לתחום הכיסוי של הרשת.
- מהירות: חומרה חדשה שמיועדת ספציפית לעיבוד של רשתות נוירונים מספקת חישוב מהיר יותר באופן משמעותי מאשר מעבד CPU לשימוש כללי בלבד.
- פרטיות: הנתונים לא יוצאים ממכשיר Android.
- עלות: אין צורך בחוות שרתים כשכל החישובים מתבצעים במכשיר Android.
יש גם יתרונות וחסרונות שכל מפתח צריך לזכור:
- ניצול המערכת: הערכת רשתות נוירונליות כוללת הרבה חישובים, שיכולים להגדיל את השימוש באנרגיה מהסוללה. אם זהו איום על האפליקציה שלכם, במיוחד לגבי חישובים ממושכים, כדאי לעקוב אחרי בריאות הסוללה.
- גודל האפליקציה: חשוב לשים לב לגודל המודלים. המודלים עשויים לתפוס כמה מגה-בייט של מקום. אם הוספת מודלים גדולים לחבילת ה-APK תשפיע לרעה על המשתמשים, מומלץ להוריד את המודלים אחרי התקנת האפליקציה, להשתמש במודלים קטנים יותר או להריץ את החישובים בענן. NNAPI לא מספק פונקציונליות להרצת מודלים בענן.
בדוגמה ל-Android Neural Networks API מוסבר איך משתמשים ב-NNAPI.
הסבר על סביבת זמן הריצה של Neural Networks API
NNAPI נועד לקריאה על ידי ספריות, מסגרות וכלים של למידת מכונה שמאפשרים למפתחים לאמן את המודלים שלהם מחוץ למכשיר ולפרוס אותם במכשירי Android. בדרך כלל אפליקציות לא משתמשות ב-NNAPI ישירות, אלא במקום זאת משתמשות במסגרות למידת מכונה ברמה גבוהה יותר. בתורו, ה-NNAPI יכול להשתמש במסגרות האלה כדי לבצע פעולות של הסקת מסקנות שמואצלות בחומרה במכשירים נתמכים.
על סמך הדרישות של האפליקציה ויכולות החומרה במכשיר Android, סביבת זמן הריצה של רשתות העצבים ב-Android יכולה לחלק ביעילות את עומס העבודה של החישוב בין המעבדים הזמינים במכשיר, כולל חומרה ייעודית לרשתות עצבים, יחידות עיבוד גרפיקה (GPU) ומעבדי אותות דיגיטליים (DSP).
במכשירי Android ללא מנהל התקן ספציפי של ספק, הבקשות מופעל על המעבד (CPU) בסביבת זמן הריצה של NNAPI.
באיור 1 מוצגת ארכיטקטורת המערכת ברמה גבוהה של NNAPI.
מודל התכנות של Neural Networks API
כדי לבצע חישובים באמצעות NNAPI, קודם צריך ליצור גרף מנוהל שמגדיר את החישובים שרוצים לבצע. תרשים החישוב הזה, בשילוב עם נתוני הקלט (לדוגמה, המשקלים וההטיות שהועברו ממסגרת למידת מכונה), יוצר את המודל להערכת זמן הריצה של NNAPI.
ב-NNAPI נעשה שימוש בארבעה רכיבים מופשטים עיקריים:
- מודל: גרף חישוב של פעולות מתמטיות והערכים הקבועים שנלמדו בתהליך אימון. הפעולות האלה הן ספציפיות לרשתות נוירונליות. הן כוללות ערבול דו-מימדי (2D), הפעלה לוגיסטית (sigmoid), הפעלה לינארית מתוקנת (ReLU) ועוד. יצירת מודל היא פעולה סינכרונית.
אחרי שיוצרים את ה-GIF, אפשר להשתמש בו שוב בשרשור ובאוספים.
ב-NNAPI, מודל מיוצג כמכונה של
ANeuralNetworksModel
. - הדרכה: מייצגת הגדרה ל-compilation של מודל NNAPI לקוד ברמה נמוכה יותר. יצירת הידור היא פעולה סינכרונית. אחרי שיוצרים אותו, אפשר להשתמש בו שוב בשרשור ובביצועים שונים. ב-NNAPI, כל הידור מיוצג כמכונה של
ANeuralNetworksCompilation
. - זיכרון: מייצג זיכרון משותף, קבצים ממופה לזיכרון ומאגרי זיכרון דומים. שימוש במאגר זיכרון מאפשר לסביבת זמן הריצה של NNAPI להעביר נתונים לנהגים בצורה יעילה יותר. בדרך כלל, אפליקציה יוצרת מאגר אחד של זיכרון משותף שמכיל כל טינסור שנחוץ להגדרת מודל. אפשר גם להשתמש במאגרי זיכרון כדי לאחסן את הקלט והפלט של מכונה להרצה. ב-NNAPI, כל מאגר זיכרון מיוצג כמכונה של
ANeuralNetworksMemory
. ביצוע: ממשק להחלה של מודל NNAPI על קבוצת קלט ואיסוף התוצאות. אפשר לבצע את ההפעלה באופן סינכרוני או אסינכרוני.
בביצוע אסינכרוני, כמה חוטים יכולים להמתין לאותה ביצוע. בסיום הביצוע, כל ה-threads משוחררים.
ב-NNAPI, כל ביצוע מיוצג כמכונה של
ANeuralNetworksExecution
.
תרשים 2 מציג את תהליך התכנות הבסיסי.
בהמשך הקטע הזה מתוארים השלבים להגדרת מודל NNAPI לביצוע חישובים, להדרכת המודל ולהרצת המודל המהדר.
מתן גישה לנתוני האימון
סביר להניח שהנתונים של המשקלים וההטיות שהותאמו מאוחסנים בקובץ. כדי לספק לסביבת זמן הריצה של NNAPI גישה יעילה לנתונים האלה, יוצרים מכונה של ANeuralNetworksMemory
באמצעות קריאה לפונקציה ANeuralNetworksMemory_createFromFd()
ומעבירים את מתאר הקובץ של קובץ הנתונים שנפתח. צריך גם לציין את דגלים להגנה על הזיכרון ואת הזזה שבה מתחיל אזור הזיכרון המשותף בקובץ.
// Create a memory buffer from the file that contains the trained data
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);
בדוגמה הזו אנחנו משתמשים רק במופע אחד של ANeuralNetworksMemory
לכל המשקלים שלנו, אבל אפשר להשתמש ביותר ממופעים אחדים של ANeuralNetworksMemory
בכמה קבצים.
שימוש במאגרי חומרה מקומיים
אפשר להשתמש במאגרי נתונים (buffers) של חומרה מקומית למשתני קלט, פלט וערכים קבועים של אופרטורים במודל. במקרים מסוימים, למאיץ NNAPI יכולה להיות גישה לאובייקטים מסוג AHardwareBuffer
בלי שהנהג יצטרך להעתיק את הנתונים. ל-AHardwareBuffer
יש הרבה הגדרות שונות, ולא בטוח שכל מואץ NNAPI תומך בכל ההגדרות האלה. בגלל המגבלה הזו, כדאי לעיין באילוצים שמפורטים במסמכי העזרה של ANeuralNetworksMemory_createFromAHardwareBuffer
ולבדוק מראש במכשירי היעד כדי לוודא שהקמפיילציות וההפעלות שמשתמשות ב-AHardwareBuffer
פועלות כצפוי, באמצעות הקצאת מכשיר כדי לציין את המאיץ.
כדי לאפשר לסביבת זמן הריצה של NNAPI לגשת לאובייקט AHardwareBuffer
, יוצרים מכונה של ANeuralNetworksMemory
על ידי קריאה לפונקציה ANeuralNetworksMemory_createFromAHardwareBuffer
והעברת האובייקט AHardwareBuffer
, כפי שמתואר בדוגמת הקוד הבאה:
// Configure and create AHardwareBuffer object AHardwareBuffer_Desc desc = ... AHardwareBuffer* ahwb = nullptr; AHardwareBuffer_allocate(&desc, &ahwb); // Create ANeuralNetworksMemory from AHardwareBuffer ANeuralNetworksMemory* mem2 = NULL; ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);
כש-NNAPI כבר לא צריך לגשת לאובייקט AHardwareBuffer
, צריך לפנות את המכונה המתאימה של ANeuralNetworksMemory
:
ANeuralNetworksMemory_free(mem2);
הערה:
- אפשר להשתמש ב-
AHardwareBuffer
רק למאגר כולו, אי אפשר להשתמש בו עם פרמטרARect
. - סביבת זמן הריצה של NNAPI לא תנקה את המאגר. לפני תזמון הביצוע, צריך לוודא שיש גישה למאגרי הקלט והפלט.
- אין תמיכה בתיאור קובצי גדר סנכרון.
- ב-
AHardwareBuffer
עם פורמטים וביטני שימוש ספציפיים לספק, ההטמעה של הספק קובעת אם הלקוח או הנהג אחראים לניקוי המטמון.
דגם
מודל הוא היחידה הבסיסית של חישוב ב-NNAPI. כל מודל מוגדר על ידי אופרטורים ופעולות אחדים או יותר.
אופרטנדים
אופרטנדים הם אובייקטי נתונים שמשמשים להגדרת הגרף. אלה כוללים את הקלט והפלט של המודל, הצמתים הביניים שמכילים את הנתונים שעוברים מפעולה אחת לאחרת ואת הקבועים שמועברים לפעולות האלה.
יש שני סוגים של אופרטנדים שאפשר להוסיף למודלים של NNAPI: סקלריים וטנסורים.
רכיב סקלר מייצג ערך יחיד. NNAPI תומך בערכים סקלריים בפורמטים בוליאני, נקודה צפה של 16 ביט, נקודה צפה של 32 ביט, מספר שלם של 32 ביט ומספר שלם לא חתום של 32 ביט.
רוב הפעולות ב-NNAPI כוללות טינסורים. טינסורים הם מערכי n-ממדים. NNAPI תומך בטנסורים עם ערכים של נקודה צפה (floating-point) של 16 ביט, 32 ביט, 8 ביט מוקצנים, 16 ביט מוקצנים, מספר שלם של 32 ביט וערכים בוליאניים של 8 ביט.
לדוגמה, איור 3 מייצג מודל עם שתי פעולות: חיבור ואחריו כפל. המודל מקבל טינסור קלט ומפיק טינסור פלט אחד.
לדגם שלמעלה יש שבעה אופרטנדים. המשתנים האלה מזוהים באופן משתמע לפי המדד של הסדר שבו הם מתווספים למודל. למשתנה הראשון שנוסף יש אינדקס 0, למשתנה השני יש אינדקס 1 וכן הלאה. המשתנים 1, 2, 3 ו-5 הם משתני קבוע.
אין חשיבות לסדר שבו מוסיפים את המשתנים. לדוגמה, אופרטנד הפלט של המודל יכול להיות הראשון שנוסף. החלק החשוב הוא להשתמש בערך המדד הנכון כשמתייחסים למשתנה.
לגורמים אופרטיביים יש סוגים. הם מצוינים כשהם מתווספים למודל.
אי אפשר להשתמש באופרנד גם כקלט וגם כפלט של מודל.
כל אופרנד חייב להיות קלט של מודל, קבוע או אופרנד פלט של פעולה אחת בלבד.
למידע נוסף על שימוש באופרטורים, ראו מידע נוסף על אופרטורים.
תפעול
פעולה מציינת את החישובים שצריך לבצע. כל פעולה מורכבת מהרכיבים הבאים:
- סוג הפעולה (לדוגמה, חיבור, כפל, עיבוד נתונים),
- רשימה של אינדקסים של אופרטנדים שבהם הפעולה משתמשת כקלט, וגם
- רשימה של אינדקסים של אופרטורים שבהם הפעולה משתמשת כפלט.
הסדר ברשימה הזו חשוב. תוכלו לעיין בחומר העזר בנושא NNAPI API כדי לקבל מידע על הקלט והפלט הצפויים של כל סוג פעולה.
צריך להוסיף את אופרטנדי הפעולה שהיא צורכת או מייצרת למודל לפני שמוסיפים את הפעולה.
אין חשיבות לסדר שבו מוסיפים את הפעולות. NNAPI מסתמך על יחסי התלות שנוצרים על ידי גרף החישוב של אופרטורים ופעולות כדי לקבוע את הסדר שבו הפעולות מתבצעות.
הפעולות ש-NNAPI תומך בהן מפורטות בטבלה הבאה:
בעיה ידועה ברמת API 28: כשמעבירים טינסורים של ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
לפעולה ANEURALNETWORKS_PAD
, שזמינה ב-Android 9 (רמת API 28) ואילך, יכול להיות שהפלט מ-NNAPI לא יתאים לפלט של מסגרות למידה חישובית ברמה גבוהה יותר, כמו TensorFlow Lite. במקום זאת, צריך להעביר רק את ANEURALNETWORKS_TENSOR_FLOAT32
.
הבעיה נפתרה ב-Android 10 (רמת API 29) ואילך.
פיתוח מודלים
בדוגמה הבאה, אנחנו יוצרים את המודל עם שתי הפעולות שמופיע באיור 3.
כדי ליצור את המודל, מבצעים את השלבים הבאים:
קוראים לפונקציה
ANeuralNetworksModel_create()
כדי להגדיר מודל ריק.ANeuralNetworksModel* model = NULL; ANeuralNetworksModel_create(&model);
כדי להוסיף את המשתנים למודל, קוראים ל-
ANeuralNetworks_addOperand()
. סוגי הנתונים שלהם מוגדרים באמצעות מבנה הנתוניםANeuralNetworksOperandType
.// In our example, all our tensors are matrices of dimension [3][4] ANeuralNetworksOperandType tensor3x4Type; tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32; tensor3x4Type.scale = 0.f; // These fields are used for quantized tensors tensor3x4Type.zeroPoint = 0; // These fields are used for quantized tensors tensor3x4Type.dimensionCount = 2; uint32_t dims[2] = {3, 4}; tensor3x4Type.dimensions = dims;
// We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;
// Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6באופרטורים שיש להם ערכים קבועים, כמו משקלים ותכונות הטיה שהאפליקציה מקבלת מתהליך אימון, צריך להשתמש בפונקציות
ANeuralNetworksModel_setOperandValue()
ו-ANeuralNetworksModel_setOperandValueFromMemory()
.בדוגמה הבאה, מגדירים ערכים קבועים מקובץ נתוני האימון שתואמים למאגר הזיכרון שיצרנו בקטע מתן גישה לנתוני האימון.
// In our example, operands 1 and 3 are constant tensors whose values were // established during the training process const int sizeOfTensor = 3 * 4 * 4; // The formula for size calculation is dim0 * dim1 * elementSize ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor); ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);
// We set the values of the activation operands, in our example operands 2 and 5 int32_t noneValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue)); ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));לכל פעולה בתרשים המכוון שרוצים לחשב, מוסיפים את הפעולה למודל באמצעות קריאה לפונקציה
ANeuralNetworksModel_addOperation()
.כפרמטרים לקריאה הזו, האפליקציה צריכה לספק:
- סוג הפעולה
- מספר ערכי הקלט
- המערך של המדדים של אופרטורי הקלט
- מספר ערכי הפלט
- המערך של המדדים של אופרטורי הפלט
חשוב לזכור שאי אפשר להשתמש באופרנד גם כקלט וגם כפלט של אותה פעולה.
// We have two operations in our example // The first consumes operands 1, 0, 2, and produces operand 4 uint32_t addInputIndexes[3] = {1, 0, 2}; uint32_t addOutputIndexes[1] = {4}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);
// The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);כדי לזהות את המשתנים שהמודל צריך להתייחס אליהם כקלט ופלט, צריך להפעיל את הפונקציה
ANeuralNetworksModel_identifyInputsAndOutputs()
.// Our model has one input (0) and one output (6) uint32_t modelInputIndexes[1] = {0}; uint32_t modelOutputIndexes[1] = {6}; ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
אפשר גם לציין אם מותר לחשב את
ANEURALNETWORKS_TENSOR_FLOAT32
עם טווח או דיוק נמוכים כמו בפורמט של נקודת צפה באורך 16 ביט של IEEE 754, על ידי קריאה ל-ANeuralNetworksModel_relaxComputationFloat32toFloat16()
.קוראים ל-
ANeuralNetworksModel_finish()
כדי לסיים את הגדרת המודל. אם אין שגיאות, הפונקציה מחזירה את קוד התוצאהANEURALNETWORKS_NO_ERROR
.ANeuralNetworksModel_finish(model);
אחרי שיוצרים מודל, אפשר לקמפל אותו כמה פעמים ולבצע כל הידור כמה פעמים.
בקרת זרימה
כדי לשלב את תהליך הבקרה במודל NNAPI:
יוצרים את תת-התרשים התואם של הביצוע (תת-תרשים
then
ותת-תרשיםelse
לביטויIF
, תת-תרשיםcondition
ותת-תרשיםbody
לולאהWHILE
) כמודלים עצמאיים שלANeuralNetworksModel*
:ANeuralNetworksModel* thenModel = makeThenModel(); ANeuralNetworksModel* elseModel = makeElseModel();
יוצרים אופרטורים שמפנים למודלים האלה בתוך המודל שמכיל את תהליך הבקרה:
ANeuralNetworksOperandType modelType = { .type = ANEURALNETWORKS_MODEL, }; ANeuralNetworksModel_addOperand(model, &modelType); // kThenOperandIndex ANeuralNetworksModel_addOperand(model, &modelType); // kElseOperandIndex ANeuralNetworksModel_setOperandValueFromModel(model, kThenOperandIndex, &thenModel); ANeuralNetworksModel_setOperandValueFromModel(model, kElseOperandIndex, &elseModel);
מוסיפים את הפעולה של זרימת הבקרה:
uint32_t inputs[] = {kConditionOperandIndex, kThenOperandIndex, kElseOperandIndex, kInput1, kInput2, kInput3}; uint32_t outputs[] = {kOutput1, kOutput2}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF, std::size(inputs), inputs, std::size(output), outputs);
קומפילציה
שלב הידור קובע באילו מעבדים המודל יופעל, ומבקש מהדריברים המתאימים להתכונן להפעלה שלו. זה יכול לכלול יצירת קוד מכונה ספציפי למעבדים שבהם המודל יפעל.
כדי לקמפל מודל:
קוראים לפונקציה
ANeuralNetworksCompilation_create()
כדי ליצור מופע חדש של הידור.// Compile the model ANeuralNetworksCompilation* compilation; ANeuralNetworksCompilation_create(model, &compilation);
לחלופין, אפשר להשתמש בהקצאת מכשיר כדי לבחור באופן מפורש את המכשירים שבהם הקוד יבוצע.
אפשר גם להשפיע על האופן שבו סביבת זמן הריצה מאזנת בין השימוש באנרגיה של הסוללה לבין מהירות הביצוע. אפשר לעשות זאת על ידי התקשרות למספר
ANeuralNetworksCompilation_setPreference()
.// Ask to optimize for low power consumption ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
ההעדפות שאפשר לציין כוללות:
ANEURALNETWORKS_PREFER_LOW_POWER
: עדיף לבצע את הפעולה באופן שממזער את התרוקנות הסוללה. כדאי לעשות זאת לגבי גרסאות הידור שמריצים לעיתים קרובות.ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER
: מומלץ להחזיר תשובה אחת מהר ככל האפשר, גם אם הדבר גורם לצריכת חשמל גבוהה יותר. זוהי ברירת המחדל.ANEURALNETWORKS_PREFER_SUSTAINED_SPEED
: העדפה להגדלת התפוקה של פריימים עוקבים, למשל כשמעובדים פריימים עוקבים שמגיעים מהמצלמה.
אפשר גם להגדיר שמירה במטמון של הידור על ידי קריאה ל-
ANeuralNetworksCompilation_setCaching
.// Set up compilation caching ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
משתמשים ב-
getCodeCacheDir()
עבור ה-cacheDir
. הערך שצוין בשדהtoken
חייב להיות ייחודי לכל מודל באפליקציה.משלימים את הגדרת הידור באמצעות קריאה ל-
ANeuralNetworksCompilation_finish()
. אם אין שגיאות, הפונקציה מחזירה את קוד התוצאהANEURALNETWORKS_NO_ERROR
.ANeuralNetworksCompilation_finish(compilation);
איתור והקצאה של מכשירים
במכשירי Android עם Android 10 (API ברמה 29) ואילך, NNAPI מספק פונקציות שמאפשרות לספריות ולאפליקציות של מסגרת למידת המכונה לקבל מידע על המכשירים הזמינים ולציין את המכשירים שישמשו לביצוע. מתן מידע על המכשירים הזמינים מאפשר לאפליקציות לקבל את הגרסה המדויקת של מנהלי ההתקנים שנמצאים במכשיר, כדי למנוע אי-תאימות ידועה. כשנותנים לאפליקציות את היכולת לציין אילו מכשירים יפעלו קטעים שונים של מודל, אפשר לבצע אופטימיזציה של האפליקציות למכשיר Android שבו הן נפרסו.
גילוי מכשירים
משתמשים ב-ANeuralNetworks_getDeviceCount
כדי לקבל את מספר המכשירים הזמינים. לכל מכשיר, משתמשים ב-ANeuralNetworks_getDevice
כדי להגדיר מכונה של ANeuralNetworksDevice
כמידע על המכשיר הזה.
אחרי שתקבלו את פרטי המכשיר, תוכלו לקבל מידע נוסף עליו באמצעות הפונקציות הבאות:
ANeuralNetworksDevice_getFeatureLevel
ANeuralNetworksDevice_getName
ANeuralNetworksDevice_getType
ANeuralNetworksDevice_getVersion
הקצאת מכשיר
אפשר להשתמש ב-ANeuralNetworksModel_getSupportedOperationsForDevices
כדי לבדוק אילו פעולות של מודל אפשר להריץ במכשירים ספציפיים.
כדי לשלוט במאיצי הביצועים שבהם המערכת תשתמש, צריך להפעיל את ANeuralNetworksCompilation_createForDevices
במקום את ANeuralNetworksCompilation_create
.
משתמשים באובייקט ANeuralNetworksCompilation
שנוצר, כרגיל.
הפונקציה מחזירה שגיאה אם המודל שסופק מכיל פעולות שלא נתמכות במכשירים שנבחרו.
אם מציינים כמה מכשירים, סביבת זמן הריצה אחראית על חלוקת העבודה בין המכשירים.
בדומה למכשירים אחרים, ההטמעה של מעבד NNAPI מיוצגת על ידי ANeuralNetworksDevice
בשם nnapi-reference
ובסוג ANEURALNETWORKS_DEVICE_TYPE_CPU
. כשקוראים ל-ANeuralNetworksCompilation_createForDevices
, ההטמעה של המעבד לא משמשת לטיפול בתרחישי הכשל של הידור והרצה של המודל.
האפליקציה אחראית לחלק מודל למודלים משנה שיכולים לפעול במכשירים שצוינו. באפליקציות שלא צריך לבצע בהן חלוקה ידנית למחיצות, כדאי להמשיך להשתמש ב-ANeuralNetworksCompilation_create
הפשוט יותר כדי להשתמש בכל המכשירים הזמינים (כולל המעבד) כדי לזרז את המודל. אם לא ניתן היה לתמוך במלואו במודל במכשירים שציינתם באמצעות ANeuralNetworksCompilation_createForDevices
, המערכת תחזיר את הערך ANEURALNETWORKS_BAD_DATA
.
חלוקת מודלים למחיצות
כשיש כמה מכשירים זמינים לדגם, סביבת זמן הריצה של NNAPI מחלקת את העבודה בין המכשירים. לדוגמה, אם צוין יותר ממכשיר אחד ל-ANeuralNetworksCompilation_createForDevices
, כל המכשירים שצוינו יילקחו בחשבון בזמן הקצאת העבודה. חשוב לזכור שאם מכשיר המעבד לא מופיע ברשימה, ביצוע המעבד יושבת. כשמשתמשים ב-ANeuralNetworksCompilation_create
, כל המכשירים הזמינים נלקחים בחשבון, כולל המעבד.
כדי לבצע את החלוקה, המערכת בוחרת מתוך רשימת המכשירים הזמינים, לכל אחת מהפעולות במודל, את המכשיר שתומך בפעולה ומציג את הביצועים הטובים ביותר, כלומר זמן הביצוע המהיר ביותר או צריכת האנרגיה הנמוכה ביותר, בהתאם להעדפת הביצוע שצוינה על ידי הלקוח. אלגוריתם החלוקה לא מתייחס לבעיות יעילות אפשריות שנובעות מה-IO בין המעבדים השונים, לכן כשמציינים כמה מעבדים (באופן מפורש באמצעות ANeuralNetworksCompilation_createForDevices
או באופן משתמע באמצעות ANeuralNetworksCompilation_create
), חשוב ליצור פרופיל של האפליקציה שמתקבלת.
כדי להבין איך המודלים שלכם חולקו על ידי NNAPI, תוכלו לחפש ביומני Android הודעה (ברמת INFO עם התג ExecutionPlan
):
ModelBuilder::findBestDeviceForEachOperation(op-name): device-index
op-name
הוא השם התיאורי של הפעולה בתרשים, ו-device-index
הוא המדד של המכשיר המועמד ברשימת המכשירים.
הרשימה הזו היא הקלט שסופק ל-ANeuralNetworksCompilation_createForDevices
, או אם משתמשים ב-ANeuralNetworksCompilation_createForDevices
, רשימת המכשירים שמוחזרת כשמבצעים חזרה על כל המכשירים באמצעות ANeuralNetworks_getDeviceCount
ו-ANeuralNetworks_getDevice
.
ההודעה (ברמת INFO עם התג ExecutionPlan
):
ModelBuilder::partitionTheWork: only one best device: device-name
ההודעה הזו מציינת שהגרף כולו עבר תאוצה במכשיר device-name
.
ביצוע
בשלב הביצוע, המערכת מחילה את המודל על קבוצת קלט ומאחסנת את תוצאות החישוב במאגר אחד או יותר של משתמשים או במרחבי זיכרון שהוקצו לאפליקציה.
כדי להריץ מודל שעבר הידור:
קוראים לפונקציה
ANeuralNetworksExecution_create()
כדי ליצור מופע חדש של ביצוע.// Run the compiled model against a set of inputs ANeuralNetworksExecution* run1 = NULL; ANeuralNetworksExecution_create(compilation, &run1);
מציינים איפה האפליקציה קוראת את ערכי הקלט לצורך החישוב. כדי לקרוא ערכים של קלט ממאגר נתונים של משתמש או ממרחב זיכרון שהוקצה, אפשר להפעיל את הפונקציה
ANeuralNetworksExecution_setInput()
או את הפונקציהANeuralNetworksExecution_setInputFromMemory()
, בהתאמה.// Set the single input to our sample model. Since it is small, we won't use a memory buffer float32 myInput[3][4] = { ...the data... }; ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
מציינים איפה האפליקציה כותבת את ערכי הפלט. האפליקציה יכולה לכתוב ערכי פלט למאגר של משתמש או למרחב זיכרון שהוקצה, על ידי קריאה ל-
ANeuralNetworksExecution_setOutput()
או ל-ANeuralNetworksExecution_setOutputFromMemory()
, בהתאמה.// Set the output float32 myOutput[3][4]; ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
מזמינים את הפונקציה
ANeuralNetworksExecution_startCompute()
כדי לתזמן את תחילת הביצוע. אם אין שגיאות, הפונקציה מחזירה את קוד התוצאהANEURALNETWORKS_NO_ERROR
.// Starts the work. The work proceeds asynchronously ANeuralNetworksEvent* run1_end = NULL; ANeuralNetworksExecution_startCompute(run1, &run1_end);
קוראים לפונקציה
ANeuralNetworksEvent_wait()
כדי להמתין לסיום ההרצה. אם הביצוע בוצע בהצלחה, הפונקציה מחזירה את קוד התוצאהANEURALNETWORKS_NO_ERROR
. אפשר להמתין בשרשור אחר מזה שמתחיל את הביצוע.// For our example, we have no other work to do and will just wait for the completion ANeuralNetworksEvent_wait(run1_end); ANeuralNetworksEvent_free(run1_end); ANeuralNetworksExecution_free(run1);
אפשר גם להחיל קבוצה אחרת של קלטות על המודל המהדר באמצעות אותה מכונה של הידור כדי ליצור מכונה חדשה של
ANeuralNetworksExecution
.// Apply the compiled model to a different set of inputs ANeuralNetworksExecution* run2; ANeuralNetworksExecution_create(compilation, &run2); ANeuralNetworksExecution_setInput(run2, ...); ANeuralNetworksExecution_setOutput(run2, ...); ANeuralNetworksEvent* run2_end = NULL; ANeuralNetworksExecution_startCompute(run2, &run2_end); ANeuralNetworksEvent_wait(run2_end); ANeuralNetworksEvent_free(run2_end); ANeuralNetworksExecution_free(run2);
ביצוע סינכרוני
בביצוע אסינכרוני נדרשת זמן כדי ליצור ולסנכרן את השרשור. בנוסף, זמן האחזור יכול להשתנות מאוד, והעיכובים הארוכים ביותר יכולים להגיע ל-500 מיקרו-שניות בין הזמן שבו הודענו לשרשור או העירנו אותו לבין הזמן שבו הוא מקושר בסופו של דבר לליבת מעבד.
כדי לשפר את זמן האחזור, אפשר להפנות את האפליקציה לבצע במקום זאת קריאה אסינכרונית להסקה בסביבת זמן הריצה. הקריאה הזו תוחזר רק אחרי שההסקה תושלם, ולא אחרי שההסקה תתחיל. במקום לבצע קריאה אסינכרונית להסקה בסביבת זמן הריצה באמצעות ANeuralNetworksExecution_startCompute
, האפליקציה מבצעת קריאה סינכרונית בסביבת זמן הריצה באמצעות ANeuralNetworksExecution_compute
. קריאה ל-ANeuralNetworksExecution_compute
לא מקבלת ANeuralNetworksEvent
ולא מותאמת לקריאה ל-ANeuralNetworksEvent_wait
.
ביצועים בעומס גבוה
במכשירי Android עם Android 10 (API ברמה 29) ואילך, NNAPI תומך בהרצות רצף באמצעות האובייקט ANeuralNetworksBurst
. פעולות מרובות ברצף הן רצף של פעולות של אותה הידור שמתרחשות ברצף מהיר, כמו פעולות שפועלות על פריימים של צילום במצלמה או על דגימות אודיו רצופות. שימוש באובייקטים מסוג ANeuralNetworksBurst
עשוי להוביל לביצועים מהירים יותר, כי הם מציינים למאיצי הביצועים שאפשר לעשות שימוש חוזר במשאבים בין ביצועים, ושמאיצי הביצועים צריכים להישאר במצב של ביצועים גבוהים למשך התקופה של זריקת הנתונים.
ANeuralNetworksBurst
מביא רק לשינוי קטן בנתיב הביצוע הרגיל. יוצרים אובייקט של התפרצות באמצעות ANeuralNetworksBurst_create
, כפי שמתואר בקטע הקוד הבא:
// Create burst object to be reused across a sequence of executions ANeuralNetworksBurst* burst = NULL; ANeuralNetworksBurst_create(compilation, &burst);
ביצוע רצף הפקודות הוא סינכרוני. עם זאת, במקום להשתמש ב-ANeuralNetworksExecution_compute
כדי לבצע כל היסק, צריך להתאים בין האובייקטים השונים של ANeuralNetworksExecution
לאותו ANeuralNetworksBurst
בקריאות לפונקציה ANeuralNetworksExecution_burstCompute
.
// Create and configure first execution object // ... // Execute using the burst object ANeuralNetworksExecution_burstCompute(execution1, burst); // Use results of first execution and free the execution object // ... // Create and configure second execution object // ... // Execute using the same burst object ANeuralNetworksExecution_burstCompute(execution2, burst); // Use results of second execution and free the execution object // ...
כשלא צריך יותר את האובייקט ANeuralNetworksBurst
, משחררים אותו באמצעות ANeuralNetworksBurst_free
.
// Cleanup ANeuralNetworksBurst_free(burst);
תורים של פקודות אסינכרוניות וביצוע מוגבל
ב-Android 11 ואילך, NNAPI תומך בדרך נוספת לתזמן ביצוע אסינכרוני באמצעות השיטה ANeuralNetworksExecution_startComputeWithDependencies()
. כשמשתמשים בשיטה הזו, ההפעלה ממתינה לקבלת אות מכל האירועים התלויים לפני שהיא מתחילה את ההערכה. אחרי שההפעלה תושלם והפלט יהיה מוכן לשימוש, המערכת תאותת על האירוע שהוחזר.
בהתאם למכשירים שמטפלים בהפעלה, יכול להיות שהאירוע יהיה מגובה על ידי גדר סנכרון. צריך להפעיל את ANeuralNetworksEvent_wait()
כדי להמתין לאירוע ולשחרר את המשאבים שבהם השתמש הביצוע. אפשר לייבא מחסומי סנכרון לאובייקט אירוע באמצעות ANeuralNetworksEvent_createFromSyncFenceFd()
, ולייצא מחסומי סנכרון מאובייקט אירוע באמצעות ANeuralNetworksEvent_getSyncFenceFd()
.
פלטים בגודל דינמי
כדי לתמוך במודלים שבהם גודל הפלט תלוי בנתוני הקלט – כלומר, במקרים שבהם אי אפשר לקבוע את הגודל בזמן ביצוע המודל – צריך להשתמש ב-ANeuralNetworksExecution_getOutputOperandRank
וב-ANeuralNetworksExecution_getOutputOperandDimensions
.
דוגמת הקוד הבאה מראה איך לעשות זאת:
// Get the rank of the output uint32_t myOutputRank = 0; ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank); // Get the dimensions of the output std::vector<uint32_t> myOutputDimensions(myOutputRank); ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());
ניקוי
שלב הניקוי מטפל בשחרור המשאבים הפנימיים ששימשו לחישוב.
// Cleanup ANeuralNetworksCompilation_free(compilation); ANeuralNetworksModel_free(model); ANeuralNetworksMemory_free(mem1);
ניהול שגיאות וחלופות למעבד (CPU)
אם מתרחשת שגיאה במהלך חלוקת המחיצות, אם הנהג לא מצליח לקמפל מודל (חלק ממנו) או אם הנהג לא מצליח להריץ מודל (חלק ממנו) שכבר קומפל, יכול להיות ש-NNAPI יחזור להטמעה שלו ב-CPU של הפעולה אחת או יותר.
אם לקוח NNAPI מכיל גרסאות אופטימליות של הפעולה (למשל, TFLite), יכול להיות שיהיה כדאי להשבית את האפשרות החלופית ל-CPU ולטפל בכשלים באמצעות ההטמעה של הפעולה האופטימלית של הלקוח.
ב-Android 10, אם הידור מתבצע באמצעות ANeuralNetworksCompilation_createForDevices
, האפשרות החלופית של מעבד מושבתת.
ב-Android P, אם הביצועים ב-NNAPI נכשלים בהתקן, הם חוזרים ל-CPU.
זה נכון גם ב-Android 10 כשמשתמשים ב-ANeuralNetworksCompilation_create
במקום ב-ANeuralNetworksCompilation_createForDevices
.
הביצוע הראשון חוזר למחיצה היחידה הזו, ואם הוא עדיין נכשל, מתבצע ניסיון חוזר להרצת כל המודל ב-CPU.
אם חלוקת המשנה או הידור ייכשלו, המערכת תנסה להריץ את המודל כולו ב-CPU.
יש מקרים שבהם פעולות מסוימות לא נתמכות ב-CPU, ובמצבים כאלה הידור או ביצוע ייכשלו ולא יתבצעו פעולות חלופיות.
גם אחרי השבתת האפשרות של מעבר אוטומטי ל-CPU, יכול להיות שעדיין יהיו פעולות במודל שתוזמנו ב-CPU. אם המעבד נמצא ברשימת המעבדים שסופקו ל-ANeuralNetworksCompilation_createForDevices
, והוא המעבד היחיד שתומך בפעולות האלה או המעבד שמציג את הביצועים הטובים ביותר בפעולות האלה, הוא יבחר כמבצע הראשי (לא כחלופה).
כדי לוודא שאין ביצוע ב-CPU, משתמשים ב-ANeuralNetworksCompilation_createForDevices
ומחריגים את nnapi-reference
מרשימת המכשירים.
החל מגרסה P של Android, אפשר להשבית את האפשרות של חזרה לגרסה קודמת בזמן הביצוע בגרסאות build לצורכי ניפוי באגים (DEBUG) על ידי הגדרת המאפיין debug.nn.partition
לערך 2.
דומיינים של זיכרון
ב-Android 11 ואילך, NNAPI תומך בדומיינים של זיכרון שמספקים ממשקי הקצאה לזיכרונות אטומים. כך האפליקציות יכולות להעביר בין ביצועים זיכרונות ייעודיים למכשיר, כדי ש-NNAPI לא יקפיא או יטמיע נתונים ללא צורך כשמבצעים ביצועים רצופים באותו מנהל.
התכונה של תחום הזיכרון מיועדת לטינסורים שרובם פנימיים לנהג ולא צריכים גישה תכופה לצד הלקוח. דוגמאות למטריצות כאלה הן מטריצות המצב במודלים של רצפים. במקום זאת, צריך להשתמש במאגרי זיכרון משותפים עבור טינסורים שצריכים גישה תכופה למעבד בצד הלקוח.
כדי להקצות זיכרון אטום:
קוראים לפונקציה
ANeuralNetworksMemoryDesc_create()
כדי ליצור מתאר זיכרון חדש:// Create a memory descriptor ANeuralNetworksMemoryDesc* desc; ANeuralNetworksMemoryDesc_create(&desc);
מציינים את כל התפקידים המיועדים לקלט ולפלט באמצעות קריאה ל-
ANeuralNetworksMemoryDesc_addInputRole()
ול-ANeuralNetworksMemoryDesc_addOutputRole()
.// Specify that the memory may be used as the first input and the first output // of the compilation ANeuralNetworksMemoryDesc_addInputRole(desc, compilation, 0, 1.0f); ANeuralNetworksMemoryDesc_addOutputRole(desc, compilation, 0, 1.0f);
אפשר גם לציין את מאפייני הזיכרון באמצעות קריאה ל-
ANeuralNetworksMemoryDesc_setDimensions()
.// Specify the memory dimensions uint32_t dims[] = {3, 4}; ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
כדי להשלים את הגדרת המתאר, קוראים ל-
ANeuralNetworksMemoryDesc_finish()
.ANeuralNetworksMemoryDesc_finish(desc);
כדי להקצות כמה זיכרונות שרוצים, מעבירים את המתאר ל-
ANeuralNetworksMemory_createFromDesc()
.// Allocate two opaque memories with the descriptor ANeuralNetworksMemory* opaqueMem; ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
משחררים את מתאר הזיכרון כשאין צורך בו יותר.
ANeuralNetworksMemoryDesc_free(desc);
הלקוח יכול להשתמש באובייקט ANeuralNetworksMemory
שנוצר רק עם ANeuralNetworksExecution_setInputFromMemory()
או ANeuralNetworksExecution_setOutputFromMemory()
, בהתאם לתפקידים שצוינו באובייקט ANeuralNetworksMemoryDesc
. צריך להגדיר את הארגומנטים offset ו-length לערך 0, כדי לציין שכל הזיכרון נמצא בשימוש. הלקוח יכול גם להגדיר או לחלץ את תוכן הזיכרון באופן מפורש באמצעות ANeuralNetworksMemory_copy()
.
אפשר ליצור זיכרונות אטומים עם תפקידים של מאפיינים או דרגות לא מוגדרים.
במקרה כזה, יצירת הזיכרון עשויה להיכשל עם הסטטוס ANEURALNETWORKS_OP_FAILED
אם היא לא נתמכת על ידי מנהל ההתקן הבסיסי. מומלץ להקצות למטמון מספיק גדול שמבוסס על Ashmem או על AHardwareBuffer
במצב BLOB, כדי שהלקוח יוכל להטמיע לוגיקה של חלופה חלופית.
כש-NNAPI כבר לא צריך לגשת לאובייקט הזיכרון האטום, צריך לפנות את המכונה המתאימה של ANeuralNetworksMemory
:
ANeuralNetworksMemory_free(opaqueMem);
מדידת ביצועים
אפשר להעריך את ביצועי האפליקציה על ידי מדידת זמן הביצוע או על ידי יצירת פרופיל.
זמן ביצוע
כדי לקבוע את משך הזמן הכולל של הביצוע דרך סביבת זמן הריצה, אפשר להשתמש ב-API לביצוע סינכרוני ולמדוד את משך הזמן של הקריאה. כדי לקבוע את משך הזמן הכולל לביצוע דרך רמה נמוכה יותר ב-stack התוכנה, אפשר להשתמש ב-ANeuralNetworksExecution_setMeasureTiming
וב-ANeuralNetworksExecution_getDuration
כדי לקבל את הערכים הבאים:
- זמן הביצוע במאיץ (לא ב-driver, שפועל במעבד המארח).
- זמן הביצוע ב-driver, כולל הזמן על המצערת.
זמן הביצועים ב-driver לא כולל זמן יתר, כמו זמן הריצה עצמו ו-IPC שנדרש לסביבת זמן הריצה כדי לתקשר עם ה-driver.
ממשקי ה-API האלה מודדים את משך הזמן בין אירועי שליחת העבודה לבין אירועי השלמת העבודה, ולא את הזמן שהדרייבר או המאיץ מקדישים לביצוע ההסקה, שעשוי להיות מופרע על ידי החלפת הקשר.
לדוגמה, אם ההסקה הראשונה מתחילה, הנהג מפסיק את העבודה כדי לבצע את ההסקה השנייה, ואז ממשיך ומשלים את ההסקה הראשונה, זמן הביצוע של ההסקה הראשונה יכלול את הזמן שבו העבודה הופסקה כדי לבצע את ההסקה השנייה.
נתוני התזמון האלה יכולים להיות שימושיים לפריסה בסביבת הייצור של אפליקציה כדי לאסוף נתוני טלמטריה לשימוש אופליין. אפשר להשתמש בנתוני התזמון כדי לשנות את האפליקציה ולשפר את הביצועים שלה.
כשמשתמשים בפונקציונליות הזו, חשוב לזכור את הנקודות הבאות:
- איסוף פרטי תזמון עשוי להשפיע על הביצועים.
- רק הנהג יכול לחשב את הזמן שחלף בעצמו או במאיץ, לא כולל הזמן שחלף בסביבת זמן הריצה של NNAPI וב-IPC.
- אפשר להשתמש בממשקי ה-API האלה רק עם
ANeuralNetworksExecution
שנוצר באמצעותANeuralNetworksCompilation_createForDevices
עםnumDevices = 1
. - אין צורך בנהיגה כדי לדווח על פרטי תזמון.
יצירת פרופיל של האפליקציה באמצעות Android Systrace
החל מ-Android 10, NNAPI יוצר באופן אוטומטי אירועי systrace שאפשר להשתמש בהם כדי ליצור פרופיל של האפליקציה.
המקור של NNAPI מגיע עם השירות parse_systrace
לעיבוד אירועי systrace שנוצרו על ידי האפליקציה, ויצירת תצוגת טבלה שמציגה את הזמן שחלף בשלבים השונים של מחזור החיים של המודל (יצירת מופע, הכנה, ביצוע הידור וסיום) ובשכבות השונות של האפליקציות. השכבות שבהן האפליקציה מחולקת הן:
Application
: קוד האפליקציה הראשיRuntime
: NNAPI RuntimeIPC
: התקשורת בין תהליכים בין NNAPI Runtime לקוד של הנהגDriver
: תהליך מנהל ההתקן של המאיץ.
יצירת נתוני הניתוח של הפרופיל
נניח שבדקתם את עץ המקור של AOSP ב- $ANDROID_BUILD_TOP, והשתמשתם בדוגמה לסיווג תמונות ב-TFLite כאפליקציית היעד. תוכלו ליצור את נתוני הפרופיל של NNAPI באמצעות השלבים הבאים:
- מפעילים את systrace של Android באמצעות הפקודה הבאה:
$ANDROID_BUILD_TOP/external/chromium-trace/systrace.py -o trace.html -a org.tensorflow.lite.examples.classification nnapi hal freq sched idle load binder_driver
הפרמטר -o trace.html
מציין שהמעקבים ייכתבו ב-trace.html
. כשמנתחים אפליקציה משלכם, צריך להחליף את org.tensorflow.lite.examples.classification
בשם התהליך שצוין במניפסט של האפליקציה.
הפקודה הזו תעסיק את אחת ממסוכי המעטפת, אל תפעילו את הפקודה ברקע כי היא ממתינה באופן אינטראקטיבי לסיום של enter
.
- אחרי שמפעילים את האוסף של systrace, מפעילים את האפליקציה ומריצים את בדיקת ההשוואה.
במקרה שלנו, אפשר להפעיל את אפליקציית Image Classification מ-Android Studio או ישירות מממשק המשתמש של טלפון הבדיקה, אם האפליקציה כבר הותקנה. כדי ליצור נתוני NNAPI מסוימים, צריך להגדיר את האפליקציה לשימוש ב-NNAPI על ידי בחירת NNAPI כמכשיר היעד בתיבת הדו-שיח של הגדרת האפליקציה.
כשהבדיקה מסתיימת, מבטלים את systrace בלחיצה על
enter
בטרמינל של מסוף שפעיל מאז שלב 1.מריצים את הכלי
systrace_parser
כדי ליצור נתונים סטטיסטיים מצטברים:
$ANDROID_BUILD_TOP/frameworks/ml/nn/tools/systrace_parser/parse_systrace.py --total-times trace.html
המנתח מקבל את הפרמטרים הבאים:
- --total-times
: הצגת הזמן הכולל שחלף בשכבה, כולל הזמן שחלף בהמתנה לביצוע בקריאה לשכבה בסיסית
- --print-detail
: הדפסת כל האירועים שנאספו מ-systrace
- --per-execution
: הדפסת הביצוע והשלבים המשניים שלו (כמו זמני ביצוע לכל שלב) במקום נתונים סטטיסטיים לכל השלבים
- --json
: הפקת הפלט בפורמט JSON
דוגמה לפלט מופיעה בהמשך:
===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock) Execution
----------------------------------------------------
Initialization Preparation Compilation I/O Compute Results Ex. total Termination Total
-------------- ----------- ----------- ----------- ------------ ----------- ----------- ----------- ----------
Application n/a 19.06 1789.25 n/a n/a 6.70 21.37 n/a 1831.17*
Runtime - 18.60 1787.48 2.93 11.37 0.12 14.42 1.32 1821.81
IPC 1.77 - 1781.36 0.02 8.86 - 8.88 - 1792.01
Driver 1.04 - 1779.21 n/a n/a n/a 7.70 - 1787.95
Total 1.77* 19.06* 1789.25* 2.93* 11.74* 6.70* 21.37* 1.32* 1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers
יכול להיות שהניתוח נכשל אם האירועים שנאספו לא מייצגים מעקב מלא של האפליקציה. באופן ספציפי, יכול להיות שהיא תיכשל אם אירועי systrace שנוצרו כדי לסמן את סיום הקטע נמצאים במעקב בלי אירוע התחלה משויך של הקטע. בדרך כלל, זה קורה אם אירועים מסוימים מסשן פרופיל קודם נוצרים כשמפעילים את האוסף של systrace. במקרה כזה, תצטרכו להריץ שוב את הפרופיל.
הוספת נתונים סטטיסטיים לקוד האפליקציה לפלט של systrace_parser
האפליקציה parse_systrace מבוססת על הפונקציונליות המובנית של systrace ב-Android. אפשר להוסיף מעקב אחר פעולות ספציפיות באפליקציה באמצעות API של systrace (ל-Java, לאפליקציות מקוריות) עם שמות אירועים מותאמים אישית.
כדי לשייך את האירועים המותאמים אישית לשלבים במחזור החיים של האפליקציה, צריך להוסיף לתחילת שם האירוע אחת מהמחרוזות הבאות:
[NN_LA_PI]
: אירוע ברמת האפליקציה לצורך אתחול[NN_LA_PP]
: אירוע ברמת האפליקציה לצורך הכנה[NN_LA_PC]
: אירוע ברמת האפליקציה לצורך הידור[NN_LA_PE]
: אירוע ברמת האפליקציה לביצוע
דוגמה לאופן שבו אפשר לשנות את הקוד לדוגמה של סיווג תמונות ב-TFLite, על ידי הוספת קטע runInferenceModel
לשלב Execution
והשכבה Application
שמכילה עוד קטעים preprocessBitmap
שלא יילקחו בחשבון ב-NNAPI traces. הקטע runInferenceModel
יהיה חלק מהאירועים של systrace שמעובדים על ידי מנתח ה-systrace של nnapi:
Kotlin
/** Runs inference and returns the classification results. */ fun recognizeImage(bitmap: Bitmap): List{ // This section won’t appear in the NNAPI systrace analysis Trace.beginSection("preprocessBitmap") convertBitmapToByteBuffer(bitmap) Trace.endSection() // Run the inference call. // Add this method in to NNAPI systrace analysis. Trace.beginSection("[NN_LA_PE]runInferenceModel") long startTime = SystemClock.uptimeMillis() runInference() long endTime = SystemClock.uptimeMillis() Trace.endSection() ... return recognitions }
Java
/** Runs inference and returns the classification results. */ public ListrecognizeImage(final Bitmap bitmap) { // This section won’t appear in the NNAPI systrace analysis Trace.beginSection("preprocessBitmap"); convertBitmapToByteBuffer(bitmap); Trace.endSection(); // Run the inference call. // Add this method in to NNAPI systrace analysis. Trace.beginSection("[NN_LA_PE]runInferenceModel"); long startTime = SystemClock.uptimeMillis(); runInference(); long endTime = SystemClock.uptimeMillis(); Trace.endSection(); ... Trace.endSection(); return recognitions; }
איכות השירות
ב-Android 11 ואילך, NNAPI מאפשר לשפר את איכות השירות (QoS) על ידי מתן אפשרות לאפליקציה לציין את העדיפויות היחסיות של המודלים שלה, את משך הזמן המקסימלי הצפוי להכנת מודל נתון ואת משך הזמן המקסימלי הצפוי להשלמת חישוב נתון. ב-Android 11 נוספו גם קודי תוצאה נוספים של NNAPI שמאפשרים לאפליקציות להבין כשלים, כמו אי-עמידה בלוחות זמנים לביצוע.
הגדרת העדיפות של עומס עבודה
כדי להגדיר את העדיפות של עומס עבודה ב-NNAPI, צריך להפעיל את ANeuralNetworksCompilation_setPriority()
לפני שמפעילים את ANeuralNetworksCompilation_finish()
.
הגדרת מועדים אחרונים
אפשר להגדיר מועדים אחרונים גם להרצת ה-inference וגם להרצת ה-compilation של המודל.
- כדי להגדיר את זמן הקצאת הזמן ליצירת הידור, צריך להפעיל את
ANeuralNetworksCompilation_setTimeout()
לפני שמפעילים אתANeuralNetworksCompilation_finish()
. - כדי להגדיר את זמן הקצאת הזמן להסקה, צריך להפעיל את הפונקציה
ANeuralNetworksExecution_setTimeout()
לפני התחלת הידור.
מידע נוסף על אופרטורים
בקטע הבא נסקור נושאים מתקדמים לגבי שימוש באופרטורים.
טינסורים מקובצים
טינסור מקודד הוא דרך קומפקטית לייצוג מערך n-ממדי של ערכים של נקודה צפה.
NNAPI תומך ב-tensors אסימטריים של 8 ביט שעברו קצירה. בטנסורים האלה, הערך של כל תא מיוצג על ידי מספר שלם של 8 ביט. משויך לטרנספורמציה סקאלה וערך של נקודת אפס. הם משמשים להמרת המספרים השלמים של 8 ביט לערכי הנקודה הצפה שמיוצגים.
הנוסחה היא:
(cellValue - zeroPoint) * scale
כאשר הערך של zeroPoint הוא מספר שלם של 32 ביט והערך של scale הוא מספר צף של 32 ביט.
בהשוואה לערכים של 32 ביט של נקודה צפה (floating-point) בטנסורים, לטנסורים שמוקצנים ב-8 ביט יש שני יתרונות:
- האפליקציה קטנה יותר, כי המשקלים המאומנים תופסים רבע מהגודל של טינסורים של 32 ביט.
- לעיתים קרובות אפשר לבצע חישובים מהר יותר. הסיבה לכך היא כמות הנתונים הקטנה יותר שצריך לאחזר מהזיכרון והיעילות של מעבדים כמו מעבדי DSP בביצוע פעולות מתמטיות של מספרים שלמים.
אפשר להמיר מודל של נקודה צפה למודל מקודד, אבל הניסיון שלנו הראה שאפשר להשיג תוצאות טובות יותר על ידי אימון של מודל מקודד ישירות. בפועל, רשת העצבים לומדת לפצות על רמת הפירוט המוגברת של כל ערך. לכל טינסור מקודד, הערכים של scale ו-zeroPoint נקבעים במהלך תהליך האימון.
ב-NNAPI, מגדירים את סוגי הטנסורים המצטברים על ידי הגדרת שדה הסוג של מבנה הנתונים ANeuralNetworksOperandType
לערך ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
.
בנוסף, צריך לציין את הערך של ה-scale ו-zeroPoint של הטנזור במבנה הנתונים הזה.
בנוסף ל-tensors אסימטריים של 8 ביט שעברו קצירה, ב-NNAPI יש תמיכה באפשרויות הבאות:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL
שאפשר להשתמש בהם כדי לייצג משקלים לפעולותCONV/DEPTHWISE_CONV/TRANSPOSED_CONV
.ANEURALNETWORKS_TENSOR_QUANT16_ASYMM
שאפשר להשתמש בו למצב הפנימי שלQUANTIZED_16BIT_LSTM
.ANEURALNETWORKS_TENSOR_QUANT8_SYMM
שיכול לשמש כקלט ל-ANEURALNETWORKS_DEQUANTIZE
.
אופרנדים אופציונליים
בחלק מהפעולות, כמו ANEURALNETWORKS_LSH_PROJECTION
, אפשר להשתמש באופרטורים אופציונליים. כדי לציין במודל שהאופרטור האופציונלי מושמט, צריך לקרוא לפונקציה ANeuralNetworksModel_setOperandValue()
, ולהעביר את הערך NULL
למאגר הנתונים הזמני ואת הערך 0 לאורך.
אם ההחלטה לגבי נוכחות המשתנה האופרנד משתנה בכל פעולה, מציינים שהאופרנד מושמט באמצעות הפונקציות ANeuralNetworksExecution_setInput()
או ANeuralNetworksExecution_setOutput()
, ומעבירים את הערך NULL
למאגר ואת הערך 0 לאורך.
טינסורים בדרגה לא ידועה
ב-Android 9 (רמת API 28) הוכנסו אופרטורים של מודלים עם מאפיינים לא ידועים אבל עם דירוג ידוע (מספר המאפיינים). ב-Android 10 (רמת API 29) הושקעו טינסורים של דרגה לא ידועה, כפי שמוצג ב-ANeuralNetworksOperandType.
בנצ'מרק של NNAPI
מדד הביצועים של NNAPI זמין ב-AOSP ב-platform/test/mlts/benchmark
(אפליקציית מדד הביצועים) וב-platform/test/mlts/models
(מודלים ומערכי נתונים).
במדד הביצועים נבדקים זמן האחזור והדיוק, והוא משווה בין הנהגים לבין אותה עבודה שבוצעה באמצעות Tensorflow Lite שפועל ב-CPU, עבור אותם מודלים ומערכי נתונים.
כדי להשתמש במדד השוואה:
מחברים מכשיר Android יעד למחשב, פותחים חלון מסוף ומוודאים שאפשר לגשת למכשיר דרך adb.
אם יש יותר ממכשיר Android אחד שמחובר, מייצאים את משתנה הסביבה
ANDROID_SERIAL
של מכשיר היעד.עוברים לספריית המקור ברמה העליונה של Android.
מריצים את הפקודות הבאות:
lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available ./test/mlts/benchmark/build_and_run_benchmark.sh
בסיום הרצה של בדיקת ביצועים, התוצאות יוצגו כדף HTML שיעבור אל
xdg-open
.
יומני NNAPI
NNAPI יוצר מידע שימושי לגבי אבחון ביומני המערכת. כדי לנתח את היומנים, משתמשים בכלי logcat.
כדי להפעיל רישום מפורט ביומן של NNAPI עבור שלבים או רכיבים ספציפיים, מגדירים את המאפיין debug.nn.vlog
(באמצעות adb shell
) לרשימה הבאה של ערכים, מופרדים באמצעות רווח, נקודה-פסיק או פסיק:
model
: בניית מודלcompilation
: יצירת תוכנית הביצוע של המודל וההדרכהexecution
: ביצוע מודלcpuexe
: ביצוע פעולות באמצעות הטמעת NNAPI במעבד (CPU)manager
: תוספי NNAPI, ממשקים זמינים ויכולות קשורותall
או1
: כל הרכיבים שלמעלה
לדוגמה, כדי להפעיל רישום מפורט (verbose) מלא ביומן, משתמשים בפקודה adb shell setprop debug.nn.vlog all
. כדי להשבית את הרישום המפורט ביומן, משתמשים בפקודה adb shell setprop debug.nn.vlog '""'
.
אחרי ההפעלה, רישום מפורט ביומן יוצר רשומות ביומן ברמת INFO עם תג שמוגדר לשם השלב או הרכיב.
בנוסף להודעות המבוקרות של debug.nn.vlog
, רכיבי ה-API של NNAPI מספקים רשומות אחרות ביומן ברמות שונות, כל אחת עם תג יומן ספציפי.
כדי לקבל רשימה של רכיבים, מחפשים בעץ המקור באמצעות הביטוי הבא:
grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"
הביטוי הזה מחזיר כרגע את התגים הבאים:
- BurstBuilder
- קריאות חזרה
- CompilationBuilder
- CpuExecutor
- ExecutionBuilder
- ExecutionBurstController
- ExecutionBurstServer
- ExecutionPlan
- FibonacciDriver
- GraphDump
- IndexedShapeWrapper
- IonWatcher
- מנהל
- זיכרון
- MemoryUtils
- MetaModel
- ModelArgumentInfo
- ModelBuilder
- NeuralNetworks
- OperationResolver
- תפעול
- OperationsUtils
- PackageInfo
- TokenHasher
- TypeManager
- Utils
- ValidateHal
- VersionedInterfaces
כדי לקבוע את רמת ההודעות ביומן שיוצגו על ידי logcat
, משתמשים במשתנה הסביבה ANDROID_LOG_TAGS
.
כדי להציג את הקבוצה המלאה של הודעות היומן של NNAPI ולהשבית את כל שאר ההודעות, מגדירים את ANDROID_LOG_TAGS
כך:
BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.
אפשר להגדיר את ANDROID_LOG_TAGS
באמצעות הפקודה הבאה:
export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')
חשוב לזכור שזהו רק מסנן שחלה על logcat
. עדיין צריך להגדיר את המאפיין debug.nn.vlog
לערך all
כדי ליצור פרטי יומן מפורטים.