ביצועים מרובים בבת אחת ותורים מהירים להודעות

ב-Neural Networks HAL 1.2 מוצג המונח 'הרצות רצף'. פעולות רצף הן רצף של פעולות של אותו מודל מוכן שמתרחשות ברצף מהיר, למשל פעולות שפועלות על פריימים של צילום במצלמה או על דגימות אודיו רצופות. אובייקט זרימה משמש לניהול קבוצה של פעולות זרימה, ולשימור משאבים בין פעולות, וכך מאפשר להפחית את התקורה של הפעולות. אובייקטים של Burst מאפשרים לבצע שלוש אופטימיזציות:

  1. אובייקט של רצף פעולות נוצר לפני רצף של פעולות, ומשוחזר כשהרצף מסתיים. לכן, משך החיים של אובייקט ה-burst מספק ל-driver רמז לגבי משך הזמן שבו הוא צריך להישאר במצב של ביצועים גבוהים.
  2. אובייקט זרימה יכול לשמור משאבים בין פעולות. לדוגמה, אפשר למפות אובייקט זיכרון בזמן הביצוע הראשון ולשמור את המיפוי במטמון באובייקט ה-burst לשימוש חוזר בפעולות הבאות. אפשר לשחרר כל משאב שנשמר במטמון כשאובייקט ה-burst נהרס או כשסביבת זמן הריצה של NNAPI מודיעה לאובייקט ה-burst שהמשאב כבר לא נדרש.
  3. אובייקט של התפרצות משתמש בתור הודעות מהיר (FMQ) כדי לתקשר בין תהליכי האפליקציה לבין תהליכי הנהג. כך אפשר לצמצם את זמן האחזור, כי ה-FMQ עוקף את HIDL ומעביר נתונים ישירות לתהליך אחר דרך FIFO אטומי עגול בזיכרון משותף. תהליך הצרכן יודע להסיר פריט מהתור ולהתחיל עיבוד על ידי סקירה של מספר הרכיבים ב-FIFO או על ידי המתנה לדגל האירוע של FMQ, שמקבל אות מהבעלים. דגל האירוע הזה הוא מנעול מהיר למרחב המשתמש (futex).

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

מאחר שהפעלות של רצף פעולות פועלות על אותם ארגומנטים ומחזירות את אותן תוצאות כמו נתיבי ביצוע אחרים, מערכי ה-FMQ הבסיסיים חייבים להעביר את אותם נתונים אל מנהלי השירות של NNAPI ומהם. עם זאת, אפשר להעביר באמצעות FMQ רק סוגים של נתונים רגילים. כדי להעביר נתונים מורכבים, מבצעים שרשור (serialization) וביטול שרשור (deserialization) של מאגרי נתונים בתצוגת עץ (סוגים של וקטורים) ישירות ב-FMQ, ומשתמשים באובייקטים של קריאה חוזרת (callback) של HIDL כדי להעביר כתובות של מאגרי זיכרון על פי דרישה. הצד של הבעלים ב-FMQ חייב לשלוח את בקשות או הודעות התוצאה לצרכנים באופן אטומי באמצעות MessageQueue::writeBlocking אם התור חוסם, או באמצעות MessageQueue::write אם התור לא חוסם.

ממשקי Burst

ממשקי ה-burst של HAL של רשתות העצבים נמצאים ב-hardware/interfaces/neuralnetworks/1.2/, ומתוארים בהמשך. למידע נוסף על ממשקי התפרצות בשכבת NDK, ראו frameworks/ml/nn/runtime/include/NeuralNetworks.h.

types.hal

types.hal מגדיר את סוג הנתונים שנשלחים דרך FMQ.

  • FmqRequestDatum: אלמנט יחיד של ייצוג בסריאליזציה של אובייקט Request לביצוע וערך MeasureTiming, שנשלחים בתור ההודעות המהיר.
  • FmqResultDatum: רכיב יחיד של ייצוג בסדרה של הערכים שמוחזרים מההרצה (ErrorStatus,‏ OutputShapes ו-Timing), שמוחזרים דרך תור ההודעות המהיר.

IBurstContext.hal

IBurstContext.hal מגדיר את אובייקט הממשק של HIDL שנמצא בשירות Neural Networks.

  • IBurstContext: אובייקט הקשר לניהול המשאבים של התפרצות.

IBurstCallback.hal

IBurstCallback.hal מגדיר את אובייקט הממשק של HIDL לקריאה חוזרת (callback) שנוצרה על ידי סביבת זמן הריצה של רשתות העצבים, ומשמש את שירות רשתות העצבים לאחזור אובייקטים מסוג hidl_memory שתואמים למזהי חריצי אחסון.

  • IBurstCallback: אובייקט קריאה חוזרת (callback) ששירות משתמש בו כדי לאחזר אובייקטים בזיכרון.

IPreparedModel.hal

ב-HAL 1.2, IPreparedModel.hal מתרחב עם שיטה ליצירת אובייקט IBurstContext ממודל מוכן.

  • configureExecutionBurst: הגדרת אובייקט 'פרץ' שמשמשים לביצוע מספר מסקנות במודל מוכן ברצף מהיר.

תמיכה בהרצות קצרות (burst) ב-driver

הדרך הפשוטה ביותר לתמוך באובייקטים של זרם בשירות HIDL NNAPI היא להשתמש בפונקציית השירות של הזרם ::android::nn::ExecutionBurstServer::create, שנמצאת ב-ExecutionBurstServer.h ומארזת בספריות הסטטיות libneuralnetworks_common ו-libneuralnetworks_util. לפונקציית ה-factory הזו יש שתי עומסי יתר:

  • אחד מהעומסי יתר מקבל הפניה לאובייקט IPreparedModel. פונקציית השירות הזו משתמשת בשיטה executeSynchronously באובייקט IPreparedModel כדי להריץ את המודל.
  • אחד מהעומסים העודפים מקבל אובייקט IBurstExecutorWithCache שניתן להתאים אישית, שאפשר להשתמש בו כדי לשמור במטמון משאבים (כמו מיפויים של hidl_memory) שנשמרים במהלך מספר פעולות.

כל עומס יתר מחזיר אובייקט IBurstContext (שמייצג את אובייקט ההאצה) שמכיל ומנהל את שרשור המאזין הייעודי שלו. החוט הזה מקבל בקשות מ-FMQ של requestChannel, מבצע את ההסקה ומחזיר את התוצאות דרך FMQ של resultChannel. כשהלקוח של הפיצוץ מאבד את ההפניה ל-IBurstContext, השרשור הזה וכל המשאבים האחרים שמכיל האובייקט IBurstContext משוחררים באופן אוטומטי.

לחלופין, אפשר ליצור הטמעה משלכם של IBurstContext שתבין איך לשלוח ולקבל הודעות דרך ה-FMQs של requestChannel ו-resultChannel שמועברים אל IPreparedModel::configureExecutionBurst.

פונקציות השירות של רצף התמונות נמצאות ב-ExecutionBurstServer.h.

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param executorWithCache Object which maintains a local cache of the
 *     memory pools and executes using the cached memory pools.
 * @result IBurstContext Handle to the burst context.
 */
static sp<ExecutionBurstServer> create(
        const sp<IBurstCallback>& callback, const FmqRequestDescriptor& requestChannel,
        const FmqResultDescriptor& resultChannel,
        std::shared_ptr<IBurstExecutorWithCache> executorWithCache);

/**
 * Create automated context to manage FMQ-based executions.
 *
 * This function is intended to be used by a service to automatically:
 * 1) Receive data from a provided FMQ
 * 2) Execute a model with the given information
 * 3) Send the result to the created FMQ
 *
 * @param callback Callback used to retrieve memories corresponding to
 *     unrecognized slots.
 * @param requestChannel Input FMQ channel through which the client passes the
 *     request to the service.
 * @param resultChannel Output FMQ channel from which the client can retrieve
 *     the result of the execution.
 * @param preparedModel PreparedModel that the burst object was created from.
 *     IPreparedModel::executeSynchronously will be used to perform the
 *     execution.
 * @result IBurstContext Handle to the burst context.
 */
  static sp<ExecutionBurstServer> create(const sp<IBurstCallback>& callback,
                                         const FmqRequestDescriptor& requestChannel,
                                         const FmqResultDescriptor& resultChannel,
                                         IPreparedModel* preparedModel);

בהמשך מופיע יישום ייחוס של ממשק התפרצות (burst) שנמצא ב-Neural Networks sample driver‏ frameworks/ml/nn/driver/sample/SampleDriver.cpp.

Return<void> SamplePreparedModel::configureExecutionBurst(
        const sp<V1_2::IBurstCallback>& callback,
        const MQDescriptorSync<V1_2::FmqRequestDatum>& requestChannel,
        const MQDescriptorSync<V1_2::FmqResultDatum>& resultChannel,
        configureExecutionBurst_cb cb) {
    NNTRACE_FULL(NNTRACE_LAYER_DRIVER, NNTRACE_PHASE_EXECUTION,
                 "SampleDriver::configureExecutionBurst");
    // Alternatively, the burst could be configured via:
    // const sp<V1_2::IBurstContext> burst =
    //         ExecutionBurstServer::create(callback, requestChannel,
    //                                      resultChannel, this);
    //
    // However, this alternative representation does not include a memory map
    // caching optimization, and adds overhead.
    const std::shared_ptr<BurstExecutorWithCache> executorWithCache =
            std::make_shared<BurstExecutorWithCache>(mModel, mDriver, mPoolInfos);
    const sp<V1_2::IBurstContext> burst = ExecutionBurstServer::create(
            callback, requestChannel, resultChannel, executorWithCache);
    if (burst == nullptr) {
        cb(ErrorStatus::GENERAL_FAILURE, {});
    } else {
        cb(ErrorStatus::NONE, burst);
    }
    return Void();
}