爆发执行和快速消息队列

神经网络 HAL 1.2 引入了爆发执行的概念。爆发执行是快速连续发生的同一准备好的模型的一系列执行,例如对摄像头拍摄的帧或连续音频样本的执行。爆发对象用于控制一组爆发执行,以及在执行之间保留资源,以便减少执行的开销。爆发对象支持以下三种优化:

  1. 爆发对象在一系列执行开始之前创建,并在其结束之后释放。因此,爆发对象的生命周期可让驱动程序获知它应在多长时间内保持高性能状态。
  2. 爆发对象可以在执行之间保留资源。例如,驱动程序可以在第一次执行时映射内存对象,然后将该映射缓存到爆发对象中,以供在后续执行中重用。当爆发对象被销毁或 NNAPI 运行时通知爆发对象不再需要某项缓存的资源时,相应资源可以被释放。
  3. 爆发对象使用快速消息队列 (FMQ) 在应用和驱动程序进程之间进行通信。这可以减少延迟,因为 FMQ 绕过了 HIDL,通过共享内存中的原子循环 FIFO 直接将数据传递到其他进程。使用方进程通过以下方式之一知道应使某个项出列并开始处理:轮询 FIFO 中的元素数量或等待 FMQ 的事件标记(由生产方发出信号)。该事件标记是一个快速用户空间互斥量 (futex)。

FMQ 是一种低级别数据结构,不提供跨进程的生命周期保证,并且没有内置机制来确定 FMQ 另一端的进程是否按预期运行。因此,如果 FMQ 的生产方终止,使用方可能会陷入等待永远不会到达的数据的困境。解决该问题的一个方法是,让驱动程序将 FMQ 与较高级别的爆发对象相关联,以检测爆发执行何时结束。

由于爆发执行采用相同的参数,并会返回与其他执行路径相同的结果,因此底层 FMQ 必须向 NNAPI 服务驱动程序以及从中传递相同的数据。但是,FMQ 只能传输 plain-old-data 类型。要传输复杂数据,可采用以下方式:直接在 FMQ 中对嵌套式缓冲区(vector 类型)进行序列化和反序列化,然后使用 HIDL 回调对象按需传输内存池句柄。FMQ 的生产方必须通过使用 MessageQueue::writeBlocking(如果队列是阻塞队列)或 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:用于管理爆发的资源的 Context 对象。

IBurstCallback.hal

IBurstCallback.hal 用于定义神经网络运行时创建的回调的 HIDL 接口对象,神经网络服务用它来检索与插槽标识符对应的 hidl_memory 对象。

  • IBurstCallback:由服务用于检索内存对象的回调对象。

IPreparedModel.hal

IPreparedModel.hal 在 HAL 1.2 中进行了扩展,包含用于从准备好的模型创建 IBurstContext 对象的方法。

在驱动程序中支持爆发执行

在 HIDL NNAPI 服务中支持爆发对象的最简单方法是使用爆发效用函数 ::android::nn::ExecutionBurstServer::create,该函数位于 ExecutionBurstServer.h 中,并且打包在 libneuralnetworks_commonlibneuralnetworks_util 静态库中。该工厂函数包含两次过载:

  • 一次过载接受指向 IPreparedModel 对象的指针。该效用函数使用 IPreparedModel 对象中的 executeSynchronously 方法来执行模型。
  • 一次过载接受可自定义的 IBurstExecutorWithCache 对象,该对象可用于缓存在多次执行中持续存在的资源(例如 hidl_memory 映射)。

每次过载都会返回一个 IBurstContext 对象(表示爆发对象),该对象包含并管理自己的专用侦听器线程。该线程会接收来自 requestChannel FMQ 的请求,执行推断,然后通过 resultChannel FMQ 返回结果。当爆发的客户端失去对 IBurstContext 的引用时,该线程和 IBurstContext 对象中包含的所有其他资源均会自动释放。

或者,您可以创建自己的 IBurstContext 实现,使其了解如何通过 requestChannel 以及传递给 IPreparedModel::configureExecutionBurstresultChannel FMQ 发送和接收消息。

爆发效用函数位于 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();
}