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

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

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

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

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

ממשקי תמונות ברצף

ממשקי ה-burst של Neural Networks HAL נמצאים ב-hardware/interfaces/neuralnetworks/1.2/ והם מתוארים בהמשך. מידע נוסף על ממשקי burst בשכבת 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 לקריאה חוזרת שנוצר על ידי זמן הריצה של Neural Networks, והוא משמש את שירות Neural Networks לאחזור אובייקטים של hidl_memory שמתאימים למזהי משבצות.

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

IPreparedModel.hal

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

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

תמיכה בהפעלות של פרצי נתונים בדרייבר

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

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

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

לחלופין, אתם יכולים ליצור הטמעה משלכם של IBurstContext שמבינה איך לשלוח ולקבל הודעות דרך requestChannel ו-resultChannel FMQ שמועברים אל 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);

בהמשך מוצג יישום ייחוס של ממשק פרץ שנמצא במנהל ההתקן לדוגמה של רשתות עצביות בכתובת 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();
}