ממשק API של רשתות נוירונים

ממשק ה-API של רשתות נוירונים של Android (NNAPI) הוא ממשק API של Android C שמיועד להרצה. פעולות עתירות חישוב של למידת מכונה במכשירי Android. NNAPI נועד לספק שכבת בסיס של פונקציונליות ברמה גבוהה יותר מסגרות של למידת מכונה, TensorFlow Lite ו-Caffe2, שבונים ומאמנים רשתות נוירונים. ה-API זמין בכל מכשירי Android שבהם פועלת גרסת Android 8.1 (רמת API 27) ומעלה.

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

יש הרבה יתרונות להסקת המסקנות במכשיר:

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

יש גם יתרונות וחסרונות שמפתח צריך להביא בחשבון:

  • שימוש במערכת: כדי להעריך רשתות נוירונים, צריך עשויים להגדיל את השימוש בסוללה. כדאי לך לשקול מעקב אחר תקינות הסוללה, במקרה של חששות בנוגע לאפליקציה, במיוחד לביצוע חישובים ממושכים.
  • גודל האפליקציה: חשוב לשים לב לגודל המודלים. מודלים יכולים תופסות כמה מגה-בייט של שטח. אם קיבוץ מודלים גדולים ב-APK עשויה להשפיע על המשתמשים שלך ללא רשות, אולי כדאי להוריד את אחרי התקנת האפליקציה, באמצעות מודלים קטנים יותר או ובענן. NNAPI לא מספק פונקציונליות להרצה שבענן.

לצפייה דוגמה של Android Neural Networks API כדי לראות דוגמה אחת לשימוש ב-NNAPI.

הסבר על זמן הריצה של Neural Networks API

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

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

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

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

איור 1. ארכיטקטורת מערכות לממשק API של רשתות נוירונים ב-Android

מודל תכנות API של רשתות נוירונים

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

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

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

דגם

מודל הוא יחידת החישוב הבסיסית ב-NNAPI. כל מודל מוגדר אופרנד אחד או יותר או פעולה אחת או יותר.

מפרסמים

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

יש שני סוגי אופרנדים שניתן להוסיף למודלים של NNAPI: סקלריים ו tensors.

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

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

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

איור 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 מוחזר.

חלוקה למחיצות (partitioning) של מודל

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

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

אם חלוקה למחיצות (partitioning) או הידור (compilation) נכשלות, המערכת תנסה להשתמש במודל כולו ב-CPU.

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

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

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

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

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

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

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

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

ANeuralNetworksMemory_free(opaqueMem);

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

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

זמן הביצוע

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

  • את זמן הביצוע במאוץ (לא במנהל התקן, שפועל על המארח מעבד מידע).
  • בזמן הביצוע של הנהג, כולל זמן על המאיץ.

זמן הביצוע במנהל התקן לא כולל תקורה, כמו זו של זמן הריצה עצמה ואת ה-IPC שנדרש כדי שזמן הריצה יוכל לתקשר עם הנהג.

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

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

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

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

  • איסוף פרטי התזמון עשוי להיות כרוך בעלות ביצועים.
  • רק נהג יכול לחשב את הזמן שבלה בעצמו או מאיץ, לא כולל הזמן שהוקדש לזמן ריצה של NNAPI וב-IPC.
  • אפשר להשתמש בממשקי ה-API האלה רק עם ANeuralNetworksExecution נוצר באמצעות ANeuralNetworksCompilation_createForDevices עם numDevices = 1.
  • אף נהג לא צריך את האפשרות לדווח על פרטי התזמון.

יצירת פרופיל לאפליקציה באמצעות Android Systrace

החל מ-Android 10, מערכת NNAPI יוצרת באופן אוטומטי אירועי systrace שאפשר להשתמש בהם כדי ליצור פרופיל לקידום האפליקציה.

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

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

האפליקציה parse_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-ממדי של של נקודה צפה (floating-point).

NNAPI תומך במגעים כמותיים אסימטריים ב-8 ביט. עבור הפרמטרים האלה, של כל תא מיוצג על ידי מספר שלם של 8 ביט. משויך אל tensor הוא סולם וערך של אפס נקודות. הם משמשים להמרת 8-bit שלמים לערכי הנקודה הצפה שמיוצגים.

הנוסחה היא:

(cellValue - zeroPoint) * scale

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

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

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

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

ב-NNAPI, מגדירים סוגים של רכיב img_tensor ANeuralNetworksOperandType של מבנה הנתונים ANEURALNETWORKS_TENSOR_QUANT8_ASYMM צריך גם לציין את קנה המידה ואת ערך אפסי של ה-tensor בנתונים האלה שלנו.

בנוסף למכסי 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: ביצוע פעולות באמצעות הטמעת מעבד (CPU) של NNAPI
  • 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
  • מעבד (CPU)
  • הכלי ליצירת פעולות
  • ExecutionBurstController
  • ExecutionBurstServer
  • תוכנית הפעלה
  • פיבונאצ'ידרייבר
  • רפליקה של תרשים
  • IndexedFormWrapper
  • IonWatcher
  • מנהל
  • זיכרון
  • קובצי MemoryUtils
  • מטא-מודל
  • ModelArgumentInfo
  • בונה מודלים
  • רשתות נוירונים
  • מעבד מידע
  • תפעול
  • כלי תפעול
  • פרטי חבילה
  • כלי גיבוב (TokenHasher)
  • TypeManager (מנהל סוג)
  • עזר
  • אימות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 כדי ליצור פרטי יומן מפורטים.