Neural Networks API

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

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

במכשירי Android ללא מנהל התקן ספציפי של ספק, הבקשות מופעל על המעבד (CPU) בסביבת זמן הריצה של NNAPI.

באיור 1 מוצגת ארכיטקטורת המערכת ברמה גבוהה של NNAPI.

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

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

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

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

  • מודל: תרשים חישוב של פעולות מתמטיות והקבוע והערכים שנלמדו בתהליך האימון. הפעולות האלה ספציפיות נוירונים מלאכותיות. הן כוללות עיבוד נתונים בשכבות עמודות (convolution) דו-מימדי (2D), הפעלה לוגיסטית (sigmoid), הפעלה לינארית מתוקנת (ReLU) ועוד. יצירת מודל היא פעולה סנכרונית. אחרי שתסיימו ליצור אותו, תוכלו להשתמש בו שוב בשרשורים ובאוספים. ב-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: סקלריים ו tensors.

סקלר מייצג ערך יחיד. NNAPI תומך בערכים סקלריים בערכים בוליאניים, נקודה צפה (floating-point), נקודה צפה של 32 ביט, מספר שלם בגרסת 32 ביט, ללא חתימה פורמטים של מספרים שלמים 32 ביט.

רוב הפעולות ב-NNAPI כוללות מעבדי tensor. טינסורים הם מערכי n-ממדים. NNAPI תומך בטכנולוגיית tensors עם נקודה צפה (floating-point) של 16 ביט, נקודה צפה (float) של 32 ביט, 8 ביט כמותית, כמות נתונים של 16 ביט, מספר שלם בגרסת 32 ביט ו-8 ביט ערכים בוליאניים.

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

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

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

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

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

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

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

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

תפעול

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

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

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

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

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

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

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

בעיה ידועה ברמת API 28: בעת העברה ANEURALNETWORKS_TENSOR_QUANT8_ASYMM את Tensor 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 לחישוב בטווח או ברמת דיוק נמוכה כמו פורמט IEEE 754 עם נקודה צפה (floating-point) של 16 ביט באמצעות התקשרות 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() כדי ליצור מכונת הידור (compilation) חדשה.

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

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

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

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

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

אפשר להשתמש ב-ANeuralNetworksModel_getSupportedOperationsForDevices כדי לבדוק אילו פעולות של מודל אפשר להריץ במכשירים ספציפיים.

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

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

בדומה למכשירים אחרים, הטמעת מעבד NNAPI מיוצגת על ידי ANeuralNetworksDevice בשם nnapi-reference ובסוג ANEURALNETWORKS_DEVICE_TYPE_CPU. בשיחה ANeuralNetworksCompilation_createForDevices, ההטמעה של המעבד (CPU) לא שמשמש לטיפול במקרים של כשלים בהידור ובביצוע של מודלים.

האפליקציה אחראית לחלק מודל למודלים משנה שיכולים לפעול במכשירים שצוינו. באפליקציות שלא צריך לבצע בהן חלוקה ידנית למחיצות, כדאי להמשיך להשתמש ב-ANeuralNetworksCompilation_create הפשוט יותר כדי להשתמש בכל המכשירים הזמינים (כולל המעבד) כדי לזרז את המודל. אם לא ניתן היה לתמוך במלואו במודל במכשירים שציינתם באמצעות ANeuralNetworksCompilation_createForDevices, המערכת תחזיר את הערך ANEURALNETWORKS_BAD_DATA.

חלוקת מודלים למחיצות

כשיש כמה מכשירים זמינים למודל, זמן הריצה של NNAPI ומפיץ את העבודה בין המכשירים השונים. לדוגמה, אם צוין יותר ממכשיר אחד ב-ANeuralNetworksCompilation_createForDevices, כל המכשירים שצוינו יילקחו בחשבון בזמן הקצאת העבודה. חשוב לזכור שאם מכשיר המעבד לא מופיע ברשימה, ביצוע המעבד יושבת. כשמשתמשים ב-ANeuralNetworksCompilation_create כל המכשירים הזמינים יובאו בחשבון, כולל מעבד (CPU).

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

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

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

אם לקוח ה-NNAPI מכיל גרסאות שעברו אופטימיזציה של הפעולה (כמו, למשל, TFLite) כדאי להשבית את החלופה של ה-CPU, טיפול בכשלים בהטמעת פעולה אופטימלית של הלקוח.

ב-Android 10, אם ההידור מבוצע באמצעות ANeuralNetworksCompilation_createForDevices, ואז החלופה למעבד (CPU) תושבת.

ב-Android P, ביצוע NNAPI חוזר למעבד (CPU) אם הביצוע במנהל התקן נכשל. זה נכון גם ב-Android 10 כשמשתמשים ב-ANeuralNetworksCompilation_create במקום ב-ANeuralNetworksCompilation_createForDevices.

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

אם חלוקת המשנה או הידור ייכשלו, המערכת תנסה להריץ את המודל כולו ב-CPU.

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

גם אחרי השבתה של החלופה למעבד (CPU), יכול להיות שעדיין יהיו פעולות במודל שנקבעו במעבד. אם המעבד נמצא ברשימת המעבדים שסופקו ל-ANeuralNetworksCompilation_createForDevices, והוא המעבד היחיד שתומך בפעולות האלה או המעבד שמציג את הביצועים הטובים ביותר בפעולות האלה, הוא יבחר כמבצע הראשי (לא כחלופה).

כדי לוודא שאין ביצוע ב-CPU, משתמשים ב-ANeuralNetworksCompilation_createForDevices ומחריגים את nnapi-reference מרשימת המכשירים. החל מ-Android P, אפשר להשבית את החלופה בזמן הביצוע מופעל DEBUG יוצר את גרסת ה-build על ידי הגדרת הנכס debug.nn.partition ל-2.

דומיינים של זיכרון

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

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

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

  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 או BLOB במצב AHardwareBuffer.

כש-NNAPI כבר לא צריך לגשת לאובייקט הזיכרון האטום, צריך לפנות את המכונה המתאימה של ANeuralNetworksMemory:

ANeuralNetworksMemory_free(opaqueMem);

מדידת ביצועים

אפשר להעריך את ביצועי האפליקציה על ידי מדידת זמן הביצוע או לפי פרופיילינג.

זמן הביצוע

כדי לקבוע את משך הזמן הכולל של הביצוע דרך סביבת זמן הריצה, אפשר להשתמש ב-API לביצוע סינכרוני ולמדוד את משך הזמן של הקריאה. כדי לקבוע את משך הזמן הכולל של הביצוע ברמה נמוכה יותר של סטאק התוכנה, אפשר להשתמש ב-ANeuralNetworksExecution_setMeasureTiming וב-ANeuralNetworksExecution_getDuration כדי לקבל את הערכים הבאים:

  • זמן הביצוע במאיץ (לא ב-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 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, מפעילים את האפליקציה ומריצים את של ההשוואה לשוק.

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

הנוסחה היא:

(cellValue - zeroPoint) * scale

כאשר הערך של zeroPoint הוא מספר שלם של 32 ביט והערך של scale הוא מספר צף של 32 ביט.

בהשוואה לערכים של 32 ביט של נקודה צפה (floating-point) בטנסורים, לטנסורים שמוקצנים ב-8 ביט יש שני יתרונות:

  • האפליקציה קטנה יותר, מאחר שהמשקולות המאומנות תופסות רבע מהגודל של img_tensors 32-bit.
  • לעיתים קרובות ניתן לבצע את החישובים מהר יותר. הסיבה לכך היא הכמות הקטנה יותר של נתונים שצריך לאחזר מהזיכרון והיעילות של המעבדים כמו DSP במתמטיקה של מספרים שלמים.

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

ב-NNAPI, מגדירים את סוגי הטנסורים המצטברים על ידי הגדרת שדה הסוג של מבנה הנתונים ANeuralNetworksOperandType לערך ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. בנוסף, צריך לציין את הערך של ה-scale ו-zeroPoint של הטנזור במבנה הנתונים הזה.

בנוסף למכסי img_ quantor_ עדינים א-סימטריים של 8 ביט, NNAPI תומך בדברים הבאים:

אופרנדים אופציונליים

בחלק מהפעולות, כמו ANEURALNETWORKS_LSH_PROJECTION, אפשר להשתמש באופרטורים אופציונליים. כדי לציין במודל שהאופרטור האופציונלי מושמט, צריך לקרוא לפונקציה ANeuralNetworksModel_setOperandValue(), ולהעביר את הערך NULL למאגר הנתונים הזמני ואת הערך 0 לאורך.

אם ההחלטה אם האופרנד קיים או לא משתנה, בכל אחד בנפרד מציינים שהאופרנד מושמט באמצעות הפקודה ANeuralNetworksExecution_setInput() או ANeuralNetworksExecution_setOutput() הפונקציה מעבירה את NULL בשביל מאגר הנתונים הזמני ו-0 בשביל האורך.

Tensors בדרגה לא ידועה

ב-Android 9 (רמת API 28) הוכנסו אופרטורים של מודלים עם מאפיינים לא ידועים אבל עם דירוג ידוע (מספר המאפיינים). הוספה של Android 10 (רמת API 29) Tensors בעלי דירוג לא ידוע, כפי שמוצג ANeuralNetworksOperandType.

בנצ'מרק של 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 של Google.

הפעלת רישום מפורט ביומן 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
  • מעבד CPU
  • הכלי ליצירת פעולות
  • ExecutionBurstController
  • ExecutionBurstServer
  • תוכנית הפעלה
  • פיבונאצ'ידרייבר
  • רפליקה של תרשים
  • IndexedShapeWrapper
  • IonWatcher
  • מנהל
  • זיכרון
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • תפעול
  • כלי תפעול
  • PackageInfo
  • כלי גיבוב (TokenHasher)
  • TypeManager (מנהל סוג)
  • Utils
  • אימותHal
  • 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 כדי ליצור פרטי יומן מפורטים.