Neural Networks API

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.

איור 1. ארכיטקטורת המערכת של Android Neural Networks API

מודל התכנות של Neural Networks API

כדי לבצע חישובים באמצעות NNAPI, קודם צריך ליצור גרף מנוהל שמגדיר את החישובים שרוצים לבצע. תרשים החישוב הזה, בשילוב עם נתוני הקלט שלכם (למשל, המשקולות וההטיות שמועברות ממסגרת של למידת מכונה) יוצר את המודל להערכת זמן ריצה של NNAPI.

ב-NNAPI נעשה שימוש בארבעה רכיבים מופשטים עיקריים:

  • Model: תרשים חישוב של פעולות מתמטיות והערכים הקבועים שנלמדו בתהליך האימון. הפעולות האלה הן ספציפיות לרשתות נוירונליות. הן כוללות הפעלה דו-ממדית (דו-ממדית) של קונבולציה, הפעלה לוגיסטית (sigmoid), הפעלה ליניארית מאוזן (ReLU) ועוד. יצירת מודל היא פעולה סנכרונית. אחרי שיוצרים את ה-GIF, אפשר להשתמש בו שוב בשרשור ובאוספים. ב-NNAPI, מודל מיוצג כמכונה של ANeuralNetworksModel.
  • הדרכה: מייצגת הגדרה ל-compilation של מודל NNAPI לקוד ברמה נמוכה יותר. יצירת הידור היא פעולה סינכרונית. אחרי שיוצרים אותו, אפשר להשתמש בו שוב בשרשור ובביצועים. ב-NNAPI, כל הידור מיוצג כמכונה של ANeuralNetworksCompilation.
  • זיכרון: מייצג זיכרון משותף, קבצים ממופה לזיכרון ומאגרי זיכרון דומים. שימוש במאגר זיכרון מאפשר לסביבת זמן הריצה של NNAPI להעביר נתונים למנהלי ההתקנים בצורה יעילה יותר. אפליקציה בדרך כלל יוצרת מאגר נתונים זמני משותף אחד שמכיל את כל הטנזור שנדרש כדי להגדיר מודל. אפשר גם להשתמש במאגרי זיכרון כדי לאחסן את הקלט והפלט של מכונה להרצה. ב-NNAPI, כל מאגר זיכרון מיוצג כמכונה של ANeuralNetworksMemory.
  • ביצוע: ממשק להחלה של מודל NNAPI על קבוצת קלט ואיסוף התוצאות. אפשר לבצע את ההפעלה באופן סינכרוני או אסינכרוני.

    בביצוע אסינכרוני, כמה חוטים יכולים להמתין לאותה ביצוע. בסיום הביצוע, כל ה-threads משוחררים.

    ב-NNAPI, כל ביצוע מיוצג כמכונה של ANeuralNetworksExecution.

איור 2 מציג את זרימת התכנות הבסיסית.

איור 2. תהליך התכנות של Android Neural Networks API

בהמשך הקטע הזה מתוארים השלבים להגדרת מודל 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 מייצג מודל עם שתי פעולות: חיבור ואחריו כפל. המודל מקבל טינסור קלט ומפיק טינסור פלט אחד.

איור 3. דוגמה לאופרטורים של מודל NNAPI

לדגם שלמעלה יש שבעה אופרטנדים. האופרנדים האלה מזוהים באופן מרומז על ידי האינדקס של הסדר שבו הם נוספו למודל. למשתנה הראשון שנוסף יש אינדקס 0, למשתנה השני יש אינדקס 1 וכן הלאה. המשתנים 1, 2, 3 ו-5 הם משתני קבוע.

הסדר שבו מוסיפים את האופרנדים לא משנה. לדוגמה, האופרנד של הפלט של המודל יכול להיות הראשון שנוסף. החלק החשוב הוא להשתמש בערך המדד הנכון כשמתייחסים למשתנה.

לגורמים אופרטיביים יש סוגים. הם מצוינים כשהם מתווספים למודל.

לא ניתן להשתמש באופרנד גם כקלט וגם כפלט של מודל.

כל אופרנד חייב להיות קלט של מודל, קבוע או אופרנד פלט של פעולה אחת בלבד.

למידע נוסף על שימוש באופרטורים, ראו מידע נוסף על אופרטורים.

תפעול

פעולה מציינת את החישובים שיש לבצע. כל פעולה מורכבת מהרכיבים הבאים:

  • סוג פעולה (לדוגמה: חיבור, כפל, קונבולציה),
  • רשימה של אינדקסים של האופרנדים שבהם הפעולה משתמשת לקלט,
  • רשימה של אינדקסים של אופרטורים שבהם הפעולה משתמשת כפלט.

הסדר ברשימה הזו חשוב. תוכלו לעיין בחומר העזר בנושא NNAPI API כדי לקבל מידע על הקלט והפלט הצפויים של כל סוג פעולה.

צריך להוסיף את אופרטורי ה-operand שהפעולה צורכת או מייצרת למודל לפני שמוסיפים את הפעולה.

הסדר שבו אתם מוסיפים פעולות לא משנה. NNAPI מסתמך על יחסי התלות שנוצרו על ידי תרשים החישוב של האופרנדים והפעולות כדי לקבוע את הסדר שבו מבוצעות הפעולות.

הפעולות ש-NNAPI תומך בהן מפורטות בטבלה הבאה:

קטגוריה תפעול
פעולות מתמטיות לפי רכיבים
מניפולציה של Tensor
פעולות הקשורות לתמונות
פעולות חיפוש
פעולות נירמול
פעולות עיבוד תמונה
פעולות מאגר
פעולות הפעלה
פעולות אחרות

בעיה ידועה ברמת API 28: כשמעבירים טינסורים של ANEURALNETWORKS_TENSOR_QUANT8_ASYMM לפעולה ANEURALNETWORKS_PAD, שזמינה ב-Android 9 (רמת API 28) ואילך, יכול להיות שהפלט מ-NNAPI לא יתאים לפלט של מסגרות למידה חישובית ברמה גבוהה יותר, כמו TensorFlow Lite. במקום זאת, מומלץ להעביר רק את ANEURALNETWORKS_TENSOR_FLOAT32. הבעיה נפתרה ב-Android 10 (רמת API 29) ואילך.

פיתוח מודלים

בדוגמה הבאה, אנחנו יוצרים את המודל עם שתי הפעולות שמופיע באיור 3.

כדי ליצור את המודל, מבצעים את השלבים הבאים:

  1. קוראים לפונקציה ANeuralNetworksModel_create() כדי להגדיר מודל ריק.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
  2. מוסיפים את המשתנים למודל באמצעות קריאה ל-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
  3. באופרטורים שיש להם ערכים קבועים, כמו משקלים ותכונות הטיה שהאפליקציה מקבלת מתהליך אימון, צריך להשתמש בפונקציות 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));
  4. מוסיפים את הפעולה לכל פעולה בתרשים המכוון שרוצים לחשב. לשם כך, מפעילים את הפונקציה 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);
  5. כדי לזהות את המשתנים שהמודל צריך להתייחס אליהם כקלט ופלט, צריך להפעיל את הפונקציה 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);
  6. אפשר גם לציין אם מותר לחשב את ANEURALNETWORKS_TENSOR_FLOAT32 עם טווח או דיוק נמוכים כמו בפורמט של נקודת צפה באורך 16 ביט של IEEE 754, על ידי קריאה ל-ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. קוראים לפונקציה ANeuralNetworksModel_finish() כדי להשלים את הגדרת המודל. אם אין שגיאות, הפונקציה מחזירה את קוד התוצאה ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksModel_finish(model);

אחרי שיוצרים מודל, אפשר לקמפל אותו כמה פעמים ולבצע כל הידור כמה פעמים.

בקרת זרימה

כדי לשלב את תהליך הבקרה במודל NNAPI:

  1. יוצרים את תת-התרשים התואם של הביצוע (תת-תרשים then ותת-תרשים else לביטוי IF, תת-תרשים condition ותת-תרשים body לולאה WHILE) כמודלים עצמאיים של ANeuralNetworksModel*:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
  2. יוצרים אופרטורים שמפנים למודלים האלה בתוך המודל שמכיל את תהליך הבקרה:

    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);
  3. מוסיפים את הפעולה של תהליך הבקרה:

    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);

קומפילציה

שלב הידור קובע באילו מעבדים המודל יופעל, ומבקש מהדריברים המתאימים להתכונן להפעלה שלו. הפעולות האלה יכולות לכלול את היצירה של קוד מכונה שספציפי למעבדים שבהם המודל יפעל.

כדי לקמפל מודל:

  1. קוראים לפונקציה ANeuralNetworksCompilation_create() כדי ליצור מופע חדש של הידור.

    // Compile the model
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);

    לחלופין, אפשר להשתמש בהקצאת מכשיר כדי לבחור באופן מפורש את המכשירים שבהם הקוד יופעל.

  2. אפשר גם להשפיע על האופן שבו סביבת זמן הריצה מאזנת בין השימוש באנרגיה של הסוללה לבין מהירות הביצוע. אפשר לעשות זאת על ידי התקשרות למספר 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: העדפה להגדלת התפוקה של פריימים עוקבים, למשל כשמעובדים פריימים עוקבים שמגיעים מהמצלמה.
  3. אם רוצים, אפשר להגדיר שמירה במטמון של הידור במטמון על ידי קריאה ל-ANeuralNetworksCompilation_setCaching.

    // Set up compilation caching
    ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);

    משתמשים ב-getCodeCacheDir() עבור ה-cacheDir. הערך של token שמציינים צריך להיות ייחודי לכל מודל באפליקציה.

  4. משלימים את הגדרת הידור באמצעות קריאה ל-ANeuralNetworksCompilation_finish(). אם אין שגיאות, הפונקציה מחזירה את קוד התוצאה ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksCompilation_finish(compilation);

איתור והקצאה של מכשירים

במכשירי Android עם Android 10 (API ברמה 29) ואילך, NNAPI מספק פונקציות שמאפשרות לספריות ולאפליקציות של מסגרת למידת המכונה לקבל מידע על המכשירים הזמינים ולציין את המכשירים שישמשו לביצוע. מתן מידע על המכשירים הזמינים מאפשר לאפליקציות לקבל את הגרסה המדויקת של מנהלי ההתקנים שנמצאים במכשיר, כדי למנוע אי-תאימות ידועה. כשנותנים לאפליקציות את היכולת לציין אילו מכשירים צריכים להפעיל קטעים שונים במודל, אפשר לבצע אופטימיזציה של האפליקציות למכשיר Android שבו הן נפרסות.

גילוי מכשירים

משתמשים ב-ANeuralNetworks_getDeviceCount כדי לקבל את מספר המכשירים הזמינים. לכל מכשיר, משתמשים ב-ANeuralNetworks_getDevice כדי להגדיר מכונה של ANeuralNetworksDevice כמידע על המכשיר הזה.

אחרי שיש לכם הפניה למכשיר, תוכלו למצוא מידע נוסף על המכשיר באמצעות הפונקציות הבאות:

הקצאת מכשיר

אפשר להשתמש ב-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.

ביצוע

בשלב הביצוע, המערכת מחילה את המודל על קבוצת קלט ומאחסנת את תוצאות החישוב במאגר אחד או יותר של משתמשים או במרחבי זיכרון שהוקצו לאפליקציה.

כדי להריץ מודל שעבר הידור:

  1. קוראים לפונקציה ANeuralNetworksExecution_create() כדי ליצור מכונה חדשה לביצוע.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
  2. מציינים איפה האפליקציה קוראת את ערכי הקלט לצורך החישוב. כדי לקרוא ערכים של קלט ממאגר נתונים של משתמש או ממרחב זיכרון שהוקצה, אפשר להפעיל את הפונקציה 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));
  3. מציינים לאן האפליקציה כותבת את ערכי הפלט. האפליקציה יכולה לכתוב ערכי פלט למאגר הנתונים הזמני של המשתמש או לשטח הזיכרון שהוקצה לו, באמצעות קריאה ל-ANeuralNetworksExecution_setOutput() או ל-ANeuralNetworksExecution_setOutputFromMemory() בהתאמה.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
  4. מזמינים את הביצוע להתחיל על ידי קריאה לפונקציה ANeuralNetworksExecution_startCompute(). אם אין שגיאות, הפונקציה מחזירה את קוד התוצאה ANEURALNETWORKS_NO_ERROR.

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
  5. קוראים לפונקציה 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);
  6. לחלופין, אפשר להחיל קבוצה שונה של קלטים על המודל שעבר הידור על ידי שימוש באותה מכונת הידור כדי ליצור מכונה חדשה של 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) בתדירות גבוהה בצד הלקוח, השתמשו במקום זאת במאגרי זיכרון משותפים.

כדי להקצות זיכרון אטום:

  1. קוראים לפונקציה ANeuralNetworksMemoryDesc_create() כדי ליצור מתאר זיכרון חדש:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
  2. מציינים את כל התפקידים המיועדים לקלט ולפלט באמצעות קריאה ל-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);
  3. אפשר גם לציין את מאפייני הזיכרון באמצעות קריאה ל-ANeuralNetworksMemoryDesc_setDimensions().

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
  4. כדי להשלים את הגדרת המתאר, קוראים ל-ANeuralNetworksMemoryDesc_finish().

    ANeuralNetworksMemoryDesc_finish(desc);
  5. כדי להקצות כמה זיכרונות שרוצים, מעבירים את המתאר ל-ANeuralNetworksMemory_createFromDesc().

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
  6. משחררים את מתאר הזיכרון כשאין צורך בו יותר.

    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 Runtime
  • IPC: התקשורת בין תהליכים בין NNAPI Runtime לקוד של הנהג
  • Driver: תהליך מנהל ההתקן של המאיץ.

יצירת נתוני הניתוח של הפרופיל

נניח שבדקתם את עץ המקור של AOSP ב-‎ $ANDROID_BUILD_TOP, והשתמשתם בדוגמה לסיווג תמונות ב-TFLite כאפליקציית היעד. תוכלו ליצור את נתוני הפרופיל של NNAPI באמצעות השלבים הבאים:

  1. מפעילים את 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.

  1. אחרי שמפעילים את האוסף של systrace, מפעילים את האפליקציה ומריצים את בדיקת ההשוואה.

במקרה שלנו, אפשר להפעיל את אפליקציית Image Classification מ-Android Studio או ישירות מממשק המשתמש של טלפון הבדיקה, אם האפליקציה כבר הותקנה. כדי ליצור נתוני NNAPI מסוימים, צריך להגדיר את האפליקציה לשימוש ב-NNAPI על ידי בחירת NNAPI כמכשיר היעד בתיבת הדו-שיח של הגדרת האפליקציה.

  1. כשהבדיקה מסתיימת, מבטלים את systrace בלחיצה על enter בטרמינל של מסוף שפעיל מאז שלב 1.

  2. מריצים את הכלי 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 List recognizeImage(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().

הגדרת מועדים אחרונים

אפליקציות יכולות להגדיר מועדים אחרונים להידור של המודלים וגם להסקת המסקנות.

מידע נוסף על אופרטורים

בקטע הבא נסביר על נושאים מתקדמים בנושא שימוש באופרטורים.

טינסורים מקובצים

טינסור מקודד הוא דרך קומפקטית לייצוג מערך 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_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), עבור אותם דגמים ומערכי נתונים.

כדי להשתמש במדד השוואה, מבצעים את הפעולות הבאות:

  1. מחברים מכשיר Android יעד למחשב, פותחים חלון מסוף ומוודאים שאפשר לגשת למכשיר דרך adb.

  2. אם מחוברים יותר ממכשיר Android אחד, צריך לייצא את משתנה הסביבה ANDROID_SERIAL של מכשיר היעד.

  3. עוברים לספריית המקור ברמה העליונה של Android.

  4. מריצים את הפקודות הבאות:

    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 כדי ליצור פרטי יומן מפורטים.