Exécutions intensives et files d'attente de messages rapides

La version HAL 1.2 des réseaux de neurones introduit le concept d'exécutions intensives. Rafale les exécutions sont une séquence d'exécutions du même modèle préparé qui ont lieu dans succession rapide (par exemple, celles qui fonctionnent sur les images d'une capture d'appareil photo ou audio successifs exemples. Un objet d'utilisation intensive permet de contrôler un ensemble d'exécutions intensives préservent les ressources entre les exécutions, ce qui permet aux exécutions d'avoir des ou d'autres frais généraux. Les objets en rafale permettent trois optimisations:

  1. Un objet d'utilisation intensive est créé avant une séquence d'exécutions et libéré à la fin de la séquence. La durée de vie de l'utilisation intensive des indications d'objet destinées au conducteur concernant la durée pendant laquelle il doit rester dans un environnement de l'état.
  2. Un objet d'utilisation intensive peut conserver les ressources entre les exécutions. Par exemple, un le pilote peut mapper un objet mémoire lors de la première exécution et mettre en cache le mappage dans l'objet d'utilisation intensive afin de le réutiliser lors d'exécutions ultérieures. Toute ressource mise en cache peut être libérée lorsque l'objet d'utilisation intensive est détruit ou lorsque le réseau NNAPI l'environnement d'exécution informe l'objet d'utilisation intensive que la ressource n'est plus nécessaire.
  3. Un objet en rafale utilise Files d'attente de messages rapides (FMQ) pour la communication entre les processus de l'application et du pilote. Cela peut réduire la latence, car le FMQ contourne le protocole HIDL et transmet les données directement un autre processus par un FIFO circulaire atomique en mémoire partagée. La le processus consommateur sait qu'il doit retirer un élément de la file d'attente et commencer le traitement en interrogeant le nombre d'éléments dans le FIFO ou en attendant l'événement FMQ qui est signalée par le producteur. Cet indicateur d'événement est mutex (futex) de l’espace utilisateur.

Un FMQ est une structure de données de bas niveau qui n'offre aucune garantie à vie et ne dispose pas d'un mécanisme intégré permettant de déterminer si le processus à l'autre extrémité du FMQ fonctionne comme prévu. Par conséquent, si le producteur de le FMQ meurt, le consommateur peut être bloqué à attendre des données qui n'arrivent jamais. Un la solution à ce problème consiste à associer les FMQ aux un objet d'utilisation intensive de niveau supérieur afin de détecter le moment où l'exécution intensive est terminée.

Comme les exécutions intensives fonctionnent sur les mêmes arguments et renvoient les mêmes que les autres chemins d'exécution, les FMQ sous-jacents doivent transmettre les mêmes données et des pilotes de service NNAPI. Toutefois, les FMQ ne peuvent transférer de données standards. Le transfert de données complexes est réalisé par la sérialisation et désérialisation des tampons imbriqués (types de vecteurs) directement dans les FMQ, et à l'aide de Objets de rappel HIDL pour transférer les poignées de pool de mémoire à la demande Le producteur FMQ doit envoyer les messages de requête ou de résultat au client de manière atomique à l'aide de MessageQueue::writeBlocking si la file d'attente est bloquante ; en utilisant MessageQueue::write si la file d'attente est non bloquante.

Interfaces de rafale

Les interfaces d'utilisation intensive des réseaux de neurones HAL se trouvent dans hardware/interfaces/neuralnetworks/1.2/ et sont décrites ci-dessous. Pour en savoir plus sur les interfaces d'utilisation intensive dans le NDK, , consultez frameworks/ml/nn/runtime/include/NeuralNetworks.h

types.hal

types.hal définit le type de données envoyées via le FMQ.

  • FmqRequestDatum: Élément unique d'une représentation sérialisée d'une exécution Request et une valeur MeasureTiming, qui est envoyée via le service de messagerie rapide file d'attente.
  • FmqResultDatum: Élément unique d'une représentation sérialisée des valeurs renvoyées par une exécution (ErrorStatus, OutputShapes et Timing), qui est renvoyées par la file d'attente des messages rapides.

IBurstContext.hal

IBurstContext.hal définit l'objet d'interface HIDL qui se trouve dans le service de réseaux de neurones.

  • IBurstContext: Contexte permettant de gérer les ressources d'une utilisation intensive.

IBurstCallback.hal

IBurstCallback.hal Définit l'objet d'interface HIDL pour un rappel créé par les réseaux de neurones et permet au service de réseaux de neurones de récupérer hidl_memory correspondant aux identifiants d'emplacements.

  • IBurstCallback: Objet de rappel utilisé par un service pour récupérer des objets mémoire.

IPreparedModel.hal

IPreparedModel.hal est étendu dans HAL 1.2 avec une méthode permettant de créer un objet IBurstContext à partir d'un le modèle préparé.

  • configureExecutionBurst: Configure un objet d'utilisation intensive utilisé pour exécuter plusieurs inférences sur un objet à la suite d'un modèle.

Accepter les exécutions intensives dans un pilote

Le moyen le plus simple de prendre en charge les objets en rafale dans un service HIDL NNAPI consiste à utiliser la classe la fonction utilitaire d'utilisation intensive ::android::nn::ExecutionBurstServer::create, trouvé dans ExecutionBurstServer.h et empaquetées dans libneuralnetworks_common et libneuralnetworks_util bibliothèques statiques. Cette fonction de fabrique présente deux surcharges:

  • Une surcharge accepte un pointeur vers un objet IPreparedModel. Ce utilitaire utilise la méthode executeSynchronously dans un IPreparedModel pour exécuter le modèle.
  • Une surcharge accepte un objet IBurstExecutorWithCache personnalisable, permettant de mettre en cache des ressources (telles que les mappages hidl_memory) sur plusieurs exécutions.

Chaque surcharge renvoie un objet IBurstContext (qui représente l'utilisation intensive). ) qui contient et gère son propre thread d'écoute dédié. Ce fil de discussion reçoit les requêtes du FMQ requestChannel, effectue l'inférence, puis renvoie les résultats via le FMQ resultChannel. Ce fil de discussion et tous les autres les ressources contenues dans l'objet IBurstContext sont automatiquement libérées Lorsque le client de la rafale perd sa référence à IBurstContext.

Vous pouvez également créer votre propre implémentation de IBurstContext qui comprenne comment envoyer et recevoir des messages sur requestChannel et resultChannel FMQ transmis à IPreparedModel::configureExecutionBurst.

Les fonctions utilitaires d'utilisation intensive se trouvent 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);

Voici une implémentation de référence d'une interface d'utilisation intensive disponible dans Exemple de pilote de réseaux de neurones : 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();
}