爆發執行作業與快速訊息佇列

類神經網路 HAL 1.2 介紹爆發執行的概念。爆發執行作業是針對同一準備好的模型,快速連續進行的一系列執行作業,例如執行相機所拍攝的影格或連續音訊取樣。爆發物件可用於控制一組爆發執行作業,並在執行作業之間保留資源,讓執行作業的額外負擔降低。爆發物件支援三種最佳化功能:

  1. 爆發物件會在一系列執行作業之前建立,並在序列結束時釋放。因此,爆發物件的生命週期會向駕駛人提示其應處於高效能狀態的時間長度。
  2. 爆發物件可在執行作業之間保留資源。舉例來說,驅動程式可以在第一次執行時對應記憶體物件,並在爆發物件中快取對應項目,以便在後續執行作業中重複使用。當爆發物件遭到銷毀,或 NNAPI 執行階段通知爆發物件不再需要該資源時,系統可以釋出任何快取的資源。
  3. 爆發物件會使用快速訊息佇列 (FMQ) 在應用程式和驅動程式程序之間進行通訊。這麼做可以減少延遲,因為 FMQ 會略過 HIDL,並透過共用記憶體中的原子循環 FIFO 將資料直接傳遞至另一個程序。消費者程序會知道要從佇列中移除項目,並開始處理,方法是輪詢 FIFO 中的元素數量,或是等待 FMQ 的事件標記,由生產者發出信號。這個事件旗標是快速的使用者空間互斥鎖 (futex)。

FMQ 是一種低階資料結構,無法保證各程序之間的生命週期保證,也沒有內建機制可判斷 FMQ 另一端的程序是否正常執行。因此,如果 FMQ 生產者死亡,取用端可能會卡在等待從未送達的資料中。其中一個解決方案是讓駕駛人將 FMQ 與較高等級的爆發物件建立關聯,藉此偵測爆發執行作業已結束的時間。

由於爆發執行作業會在相同的引數上運作,且傳回的結果與其他執行路徑相同,因此基礎 FMQ 必須向 NNAPI 服務驅動程式傳遞相同資料,或從 NNAPI 服務驅動程式傳遞相同資料。不過,FMQ 只能轉移純舊資料類型。傳輸複雜資料的方式,是直接在 FMQ 中序列化和反序列化巢狀緩衝區 (向量類型),並使用 HIDL 回呼物件視需求傳輸記憶體集區句柄。如果佇列處於封鎖狀態,FMQ 生產者必須使用 MessageQueue::writeBlocking 以不可分割的形式傳送要求或結果訊息;如果佇列處於非封鎖狀態,使用 MessageQueue::write 則可透過 MessageQueue::write 以不可分割的形式傳送要求或結果訊息。

爆發介面

類神經網路 HAL 的爆發介面位於 hardware/interfaces/neuralnetworks/1.2/,如下所述。如要進一步瞭解 NDK 層中的爆發介面,請參閱 frameworks/ml/nn/runtime/include/NeuralNetworks.h

types.hal

types.hal 定義 FMQ 傳送的資料類型。

  • FmqRequestDatum:執行 Request 物件的序列化表示法的單一元素和 MeasureTiming 值,可跨快速訊息佇列傳送。
  • FmqResultDatum:從執行作業傳回的值 (ErrorStatusOutputShapesTiming) 序列化表示法的單一元素,該元素會透過快速訊息佇列傳回。

IBurstContext.hal

IBurstContext.hal 會定義類神經網路服務中的 HIDL 介面物件。

  • IBurstContext:用於管理連拍相片資源的內容物件。

IBurstCallback.hal

IBurstCallback.hal 會為類神經網路執行階段建立的回呼定義 HIDL 介面物件,並由類神經網路服務用來擷取與插槽 ID 相對應的 hidl_memory 物件。

  • IBurstCallback:服務使用的回呼物件,用於擷取記憶體物件。

IPreparedModel.hal

IPreparedModel.hal 在 HAL 1.2 中已擴充,其中提供方法可從已準備的模型建立 IBurstContext 物件。

  • configureExecutionBurst:設定快照物件,用於在已準備好的模型上快速連續執行多個推論。

支援在驅動程式中執行爆發作業

如要在 HIDL NNAPI 服務中支援爆發物件,最簡單的方法就是使用在 ExecutionBurstServer.h 中,並封裝在 libneuralnetworks_commonlibneuralnetworks_util 靜態資料庫中的爆發公用程式函式 ::android::nn::ExecutionBurstServer::create。這個工廠函式有兩個超載:

  • 其中一個超載接受指向 IPreparedModel 物件的指標。這個公用函式會使用 IPreparedModel 物件中的 executeSynchronously 方法執行模型。
  • 其中一個超載會接受可自訂的 IBurstExecutorWithCache 物件,可用於快取在多個執行作業中持續存在的資源 (例如 hidl_memory 對應)。

每個超載都會傳回 IBurstContext 物件 (代表 burst 物件),該物件會包含及管理專屬的事件監聽器執行緒。這個執行緒會接收來自 requestChannel FMQ 的要求、執行推論,然後透過 resultChannel FMQ 傳回結果。當 burst 的用戶端失去對 IBurstContext 的參照時,這個執行緒和 IBurstContext 物件中的所有其他資源都會自動釋出。

或者,您也可以自行實作 IBurstContext,瞭解如何透過傳遞至 IPreparedModel::configureExecutionBurstrequestChannelresultChannel FMQ 傳送及接收訊息。

您可以在 ExecutionBurstServer.h 中找到 burst 公用程式函式。

/**
 * 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();
}