バースト実行と高速メッセージ キュー

ニューラル ネットワーク HAL 1.2 では、バースト実行のコンセプトが導入されました。バースト実行とは、カメラ キャプチャのフレームや連続するオーディオ サンプルなど、同じ準備モデルが途切れずに発生する実行シーケンスです。バースト オブジェクトは、一連のバースト実行を制御し、実行間のリソースを保持するのに使用され、実行時のオーバーヘッドの低減を可能にします。バースト オブジェクトでは、次の 3 つの最適化が可能です。

  1. バースト オブジェクトは、一連の実行の前に作成され、シーケンスが終了すると解放されます。このため、バースト オブジェクトの存続期間は、高パフォーマンス状態を維持する必要のある期間のヒントをドライバに与えます。
  2. バースト オブジェクトは、実行間のリソースを保持できます。たとえば、ドライバは最初の実行時にメモリ オブジェクトをマッピングし、後続の実行で再利用するためにバースト オブジェクト内のマッピングをキャッシュできます。キャッシュされたリソースは、バースト オブジェクトが破棄されたとき、またはリソースが不要になったことを NNAPI ランタイムがバースト オブジェクトに通知したときに解放できます。
  3. バースト オブジェクトは、高速メッセージ キュー(FMQ)を使用して、アプリとドライバのプロセス間で通信します。これにより、FMQ が HIDL をバイパスし、共有メモリのアトミック循環 FIFO を通じて別のプロセスに直接データを渡すため、レイテンシを短縮できます。コンシューマ プロセスは、FIFO 内の要素の数をポーリングするか、FMQ のイベントフラグでプロデューサーがシグナルを出すのを待つかのいずれかにより、アイテムをデキューして処理を開始します。このイベントフラグは、高速ユーザー空間ミューテックス(futex)です。

FMQ は低レベルのデータ構造であり、プロセス全体での永久保証も、FMQ の反対側のプロセスが期待どおりに実行されているかどうかを判断する組み込みメカニズムもありません。そのため、FMQ のプロデューサーが終了すると、コンシューマは到着しないデータの待機状態になる可能性があります。この問題の解決策の 1 つは、ドライバが、バースト実行が終了したことを検出するために上位レベルのバースト オブジェクトに FMQ を関連付けることです。

バースト実行は同じ引数で動作し、他の実行パスと同じ結果を返すため、基盤となる FMQ は NNAPI サービス ドライバとの間で同じデータの受け渡しを行う必要があります。ただし、FMQ は plain-old-data タイプのみを転送できます。複雑なデータの転送を実現するには、ネストされたバッファ(ベクター型)のシリアル化やシリアル化解除を FMQ で直接行い、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 値のシリアル化された表現の 1 つの要素で、高速メッセージ キュー全体に送信されます。
  • FmqResultDatum: 実行から返された値(ErrorStatusOutputShapesTiming)のシリアル化表現の 1 つの要素で、高速メッセージ キューを介して返されます。

IBurstContext.hal

IBurstContext.hal は、ニューラル ネットワーク サービスに存在する HIDL インターフェース オブジェクトを定義します。

  • IBurstContext: バーストのリソースを管理するためのコンテキスト オブジェクト。

IBurstCallback.hal

IBurstCallback.hal は、ニューラル ネットワークのランタイムによって作成されたコールバックの HIDL インターフェース オブジェクトを定義し、ニューラル ネットワーク サービスがスロット識別子に対応する hidl_memory オブジェクトを取得するために使用されます。

  • IBurstCallback: メモリ オブジェクトを取得するためにサービスが使用するコールバック オブジェクト。

IPreparedModel.hal

IPreparedModel.hal は、準備されたモデルから IBurstContext オブジェクトを作成するメソッドを備えた HAL 1.2 で拡張されています。

  • configureExecutionBurst: 準備されたモデルで複数の推論を途切れずに実行するために使用するバースト オブジェクトを設定します。

ドライバでのバースト実行のサポート

HIDL NNAPI サービスのバースト オブジェクトをサポートする最も簡単な方法は、ExecutionBurstServer.h にあり、libneuralnetworks_commonlibneuralnetworks_util の静的ライブラリでパッケージ化されたバースト ユーティリティ関数 ::android::nn::ExecutionBurstServer::create を使用することです。この出荷時の関数には 2 つのオーバーロードがあります。

  • 一方のオーバーロードは、IPreparedModel オブジェクトへのポインタを受け入れます。このユーティリティ関数は、IPreparedModel オブジェクトの executeSynchronously メソッドを使用して、モデルを実行します。
  • もう一方のオーバーロードは、複数の実行にまたがって永続化するリソース(hidl_memory マッピングなど)のキャッシュに使用できる、カスタマイズ可能な IBurstExecutorWithCache オブジェクトを受け入れます。

各オーバーロードは、独自の専用のリスナー スレッドを格納して管理する、(バースト オブジェクトを表す)IBurstContext オブジェクトを返します。このスレッドは、requestChannel FMQ からのリクエストを受け取り、推論を行い、resultChannel FMQ を介して結果を返します。IBurstContext オブジェクトに含まれるこのスレッドとその他すべてのリソースは、バーストのクライアントが IBurstContext への参照を失ったときに自動的に解放されます。

また、IPreparedModel::configureExecutionBurst に渡される requestChannelresultChannel の FMQ を経由してメッセージを送受信する方法を理解する IBurstContext の独自の実装を作成することもできます。

バースト ユーティリティ関数は、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();
}