הרצת מספר משימות רציפות ותורים מהירים של הודעות

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

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

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

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

ממשקי Burst

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

segments.hal

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

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

IBurstContext.hal

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

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

IBurstCallback.hal

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

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

IPreparedModel.hal

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

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

תמיכה בביצוע רצפים במנהל התקן

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

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

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

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

פונקציות השימוש ברצף (burst) נמצאות ב-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);

הדוגמה הבאה היא יישום עזר של ממשק רצף שנמצא במנהל ההתקן לדוגמה של רשת נוירונים חוזרת (Neural Networks) ב-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();
}