Android Neural Networks API (NNAPI) הוא API של Android C שמיועד להרצת פעולות חישוביות שצורכות הרבה מידע על למידת מכונה במכשירי 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 על ידי ספריות, frameworks וכלים של למידת מכונה שמאפשרים למפתחים לאמן את המודלים שלהם מחוץ למכשיר ולפרוס אותם במכשירי Android. בדרך כלל אפליקציות לא משתמשות ב-NNAPI ישירות, אלא במקום זאת משתמשות במסגרות למידת מכונה ברמה גבוהה יותר. בתורו, ה-NNAPI יכול להשתמש במסגרות האלה כדי לבצע פעולות של הסקת מסקנות שמואצלות בחומרה במכשירים נתמכים.
בהתאם לדרישות של האפליקציה וליכולות החומרה במכשיר Android, זמן הריצה ברשת הנוירונים של Android יכול להפיץ ביעילות את עומס העבודה של המחשוב בין מעבדים זמינים במכשיר, כולל חומרת רשת נוירונים ייעודית, יחידות עיבוד גרפיות (GPU) ומעבדי אותות דיגיטליים (DSP).
במכשירי Android שאין להם מנהל התקן של ספק מיוחד, זמן הריצה של NNAPI יריץ את הבקשות במעבד.
באיור 1 מוצגת ארכיטקטורת המערכת ברמה גבוהה של NNAPI.
מודל התכנות של Neural Networks API
כדי לבצע חישובים באמצעות NNAPI, קודם צריך ליצור גרף מנוהל שמגדיר את החישובים שרוצים לבצע. תרשים החישוב הזה, בשילוב עם נתוני הקלט שלכם (למשל, המשקולות וההטיות שמועברות ממסגרת של למידת מכונה) יוצר את המודל להערכת זמן ריצה של NNAPI.
ב-NNAPI נעשה שימוש בארבעה רכיבים מופשטים עיקריים:
- Model: תרשים חישוב של פעולות מתמטיות והערכים הקבועים שנלמדו בתהליך האימון. הפעולות האלה הן ספציפיות לרשתות נוירונליות. הן כוללות הפעלה דו-ממדית (דו-ממדית) של קונבולציה, הפעלה לוגיסטית (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
בכמה קבצים.
שימוש במאגרי חומרה מקומיים
אפשר להשתמש במאגרי חומרה נייטיב לקלט של מודל, לפלט ולערכי אופרנד קבועים. במקרים מסוימים, מאיץ 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 כדי לקבל מידע על הקלט והפלט הצפויים של כל סוג פעולה.
צריך להוסיף את אופרטורי ה-operand שהפעולה צורכת או מייצרת למודל לפני שמוסיפים את הפעולה.
הסדר שבו אתם מוסיפים פעולות לא משנה. 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
שנוצר, כרגיל.
הפונקציה מחזירה שגיאה אם המודל שסופק מכיל פעולות שלא נתמכות במכשירים שנבחרו.
אם מציינים כמה מכשירים, סביבת זמן הריצה אחראית על חלוקת העבודה בין המכשירים.
בדומה למכשירים אחרים, הטמעת ה-CPU של 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)
אם יש שגיאה במהלך החלוקה למחיצות, אם מנהל התקן לא מצליח להדר מודל (חלק של א) או אם מנהל התקן לא מצליח להפעיל מודל הידור (חלק של A), יכול להיות ש-NAPI יחזור להטמעה של המעבד (CPU) של הפעולה אחת או יותר.
אם לקוח NNAPI מכיל גרסאות אופטימליות של הפעולה (למשל, TFLite), יכול להיות שיהיה כדאי להשבית את האפשרות החלופית ל-CPU ולטפל בכשלים באמצעות ההטמעה של הפעולה האופטימלית של הלקוח.
ב-Android 10, אם הידור מתבצע באמצעות ANeuralNetworksCompilation_createForDevices
, האפשרות החלופית של מעבד מושבתת.
ב-Android P, ביצוע NNAPI חוזר למעבד (CPU) אם הביצוע במנהל התקן נכשל.
זה נכון גם ב-Android 10 כשמשתמשים ב-ANeuralNetworksCompilation_create
במקום ב-ANeuralNetworksCompilation_createForDevices
.
הביצוע הראשון חוזר למחיצה היחידה הזו, ואם הוא עדיין נכשל, מתבצע ניסיון חוזר להרצת כל המודל ב-CPU.
אם חלוקה למחיצות (partitioning) או הידור (compilation) נכשלות, המערכת תנסה להשתמש במודל כולו ב-CPU.
יש מקרים שבהם פעולות מסוימות לא נתמכות ב-CPU, ובמצבים כאלה הידור או ביצוע ייכשלו במקום לעבור לאפשרות החלופית.
גם אחרי השבתת האפשרות של מעבר אוטומטי ל-CPU, יכול להיות שעדיין יהיו פעולות במודל שתוזמנו ב-CPU. אם המעבד (CPU) נמצא ברשימת המעבדים שמסופקים ל-ANeuralNetworksCompilation_createForDevices
, והוא המעבד היחיד שתומך בפעולות האלה, או שהוא המעבד שמציג את הביצועים הטובים ביותר בפעולות האלה, הוא ייבחר לרשימת הביצוע הראשית (ללא חלופה).
כדי לוודא שלא תתבצע הפעלה של המעבד (CPU), משתמשים ב-ANeuralNetworksCompilation_createForDevices
ואין לכלול את nnapi-reference
ברשימת המכשירים.
החל מגרסה P של Android, אפשר להשבית את האפשרות של חזרה לגרסה קודמת בזמן הביצוע בגרסאות build לצורכי ניפוי באגים (DEBUG) על ידי הגדרת המאפיין debug.nn.partition
לערך 2.
דומיינים של זיכרון
ב-Android מגרסה 11 ואילך, NNAPI תומך בדומיינים של זיכרון שמספקים ממשקים של הקצאות לזיכרונות אטומים. כך האפליקציות יכולות להעביר בין ביצועים זיכרונות ייעודיים למכשיר, כדי ש-NNAPI לא יקפיא או יטמיע נתונים ללא צורך כשמבצעים ביצועים רצופים באותו מנהל.
מאפיין דומיין הזיכרון מיועד לטנסטורים שהם בעיקר פנימיים לנהג, שלא צריכים גישה לעיתים קרובות לצד הלקוח. דוגמאות למטריצות כאלה הן מטריצות המצב במודלים של רצפים. בשביל טנזורים שזקוקים לגישה למעבד (CPU) בתדירות גבוהה בצד הלקוח, השתמשו במקום זאת במאגרי זיכרון משותפים.
כדי להקצות זיכרון אטום:
קוראים לפונקציה
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 הסינכרוני להפעלה ולמדוד את הזמן שלוקח הקריאה. כשרוצים לקבוע את זמן הביצוע הכולל דרך רמה נמוכה יותר של מקבץ התוכנה, אפשר להשתמש ב-ANeuralNetworksExecution_setMeasureTiming
וב-ANeuralNetworksExecution_getDuration
כדי לקבל:
- את זמן הביצוע במאוץ (לא במנהל ההתקן, שפועל במעבד המארח).
- בזמן הביצוע של הנהג, כולל זמן על המאיץ.
זמן הביצוע ב-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
: מדפיס את כל האירועים שנאספו מהמערכת
- --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. הקטע runInferenceModel
יהיה חלק מאירועי ה-systrace שיעובדו על ידי מנתח ה-nnapi systrace:
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()
.
הגדרת מועדים אחרונים
אפליקציות יכולות להגדיר מועדים אחרונים להידור של המודלים וגם להסקת המסקנות.
- כדי להגדיר את זמן הקצאת הזמן ליצירת הידור, צריך להפעיל את
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
.
בנוסף, מציינים את קנה המידה ואת ערך אפסי הנקודות של הטנזור במבנה הנתונים הזה.
בנוסף למכסי img_קו כמותיים אסימטריים של 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) נוספו טינזורים של דירוג לא ידוע, כפי שמוצג ב-ANeuralOperandnetworkType.
בנצ'מרק של 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
: כל הרכיבים שלמעלה
לדוגמה, כדי להפעיל רישום מפורט מלא ביומן, משתמשים בפקודה 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
- רפליקה של תרשים
- IndexedShapeWrapper
- IonWatcher
- מנהל
- זיכרון
- MemoryUtils
- MetaModel
- ModelArgumentInfo
- ModelBuilder
- רשתות נוירונים
- מעבד מידע
- תפעול
- כלי תפעול
- 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
כדי ליצור פרטי יומן מפורטים.