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. Les exécutions en rafales sont une séquence d'exécutions du même modèle préparé qui se succèdent rapidement, telles que celles exécutées au niveau des trames d'une capture d'échantillons audio successifs tirés d'une caméra. Un objet d'utilisation intensive permet de contrôler un ensemble d'exécutions intensives et de conserver les ressources entre les exécutions, ce qui permet de réduire les frais généraux. Les objets "Burst" 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. Pour cette raison, la durée de vie de l'objet d'utilisation intensive indique au pilote combien de temps il doit rester dans un état hautes performances.
  2. Un objet d'utilisation intensive peut conserver les ressources entre les exécutions. Par exemple, un pilote peut mapper un objet mémoire lors de la première exécution et mettre en cache le mappage dans l'objet en rafale pour le réutiliser lors des exécutions suivantes. Toute ressource mise en cache peut être libérée lorsque l'objet d'utilisation intensive est détruit ou lorsque l'environnement d'exécution NNAPI avertit l'objet d'utilisation intensive que la ressource n'est plus nécessaire.
  3. Un objet de rafale utilise des files d'attente de messages rapides (FMQ) pour communiquer entre les processus d'application et de pilote. Cela peut réduire la latence, car le FMQ contourne HIDL et transmet les données directement à un autre processus via un FIFO circulaire atomique en mémoire partagée. Le processus consommateur sait retirer un élément de la file d'attente et commencer le traitement en interrogeant le nombre d'éléments dans la file d'attente FIFO ou en attendant l'indicateur d'événement de la file d'attente FMQ, qui est signalé par le producteur. Cet indicateur d'événement est un mutex d'espace utilisateur (futex) rapide.

Un FMQ est une structure de données de bas niveau qui n'offre aucune garantie de durée de vie sur l'ensemble des processus et ne possède pas de mécanisme intégré permettant de déterminer si le processus situé à l'autre extrémité du FMQ s'exécute comme prévu. Par conséquent, si le producteur de la file de messages en file d'attente meurt, le consommateur peut être bloqué en attendant des données qui n'arrivent jamais. Une solution à ce problème consiste pour le pilote à associer les FMQ à l'objet d'utilisation intensive de niveau supérieur afin de détecter la fin de l'exécution d'une utilisation intensive.

Étant donné que les exécutions par rafales fonctionnent sur les mêmes arguments et renvoient les mêmes résultats que les autres chemins d'exécution, les FMQ sous-jacents doivent transmettre les mêmes données aux pilotes de service NNAPI et depuis eux. Toutefois, les files de messages de file d'attente ne peuvent transférer que des types de données simples. Le transfert de données complexes s'effectue en sérialisant et en désérialisant des tampons imbriqués (types de vecteurs) directement dans les FMQ, et en utilisant des objets de rappel HIDL pour transférer des poignées de pool de mémoire à la demande. Le côté producteur de la file de messages de requêtes doit envoyer les messages de requête ou de résultat au consommateur de manière atomique à l'aide de MessageQueue::writeBlocking si la file est bloquante, ou à l'aide de MessageQueue::write si la file n'est pas bloquante.

Interfaces de rafale

Les interfaces de rafales pour le HAL des réseaux de neurones 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 la couche 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'un objet Request d'exécution et d'une valeur MeasureTiming, qui est envoyé via la file d'attente des messages rapides.
  • FmqResultDatum : élément unique d'une représentation sérialisée des valeurs renvoyées à partir d'une exécution (ErrorStatus, OutputShapes et Timing), qui est renvoyée via la file d'attente de messages rapide.

IBurstContext.hal

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

  • IBurstContext : objet de contexte permettant de gérer les ressources d'une rafale.

IBurstCallback.hal

IBurstCallback.hal définit l'objet d'interface HIDL pour un rappel créé par l'environnement d'exécution des réseaux de neurones. Le service de réseaux de neurones permet de récupérer les objets hidl_memory correspondant aux identifiants d'emplacement.

  • 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 modèle préparé.

  • configureExecutionBurst : configure un objet "burst" utilisé pour exécuter plusieurs inférences sur un modèle préparé en succession rapide.

Prendre en charge les exécutions en rafale dans un pilote

Le moyen le plus simple de prendre en charge les objets de rafale dans un service NNAPI HIDL consiste à utiliser la fonction utilitaire de rafale ::android::nn::ExecutionBurstServer::create, qui se trouve dans ExecutionBurstServer.h et est empaquetée dans les bibliothèques statiques libneuralnetworks_common et libneuralnetworks_util. Cette fonction de fabrique présente deux surcharges:

  • Une surcharge accepte un pointeur vers un objet IPreparedModel. Cette fonction utilitaire utilise la méthode executeSynchronously dans un objet IPreparedModel pour exécuter le modèle.
  • Une surcharge accepte un objet IBurstExecutorWithCache personnalisable, qui peut être utilisé pour mettre en cache des ressources (telles que les mappages hidl_memory) qui persistent lors de plusieurs exécutions.

Chaque surcharge renvoie un objet IBurstContext (qui représente l'objet d'utilisation intensive) qui contient et gère son propre thread d'écoute dédié. Ce thread reçoit les requêtes de la file de messages de premier plan requestChannel, effectue l'inférence, puis renvoie les résultats via la file de messages de premier plan resultChannel. Ce thread et toutes les autres ressources contenues dans l'objet IBurstContext sont automatiquement libérés lorsque le client de l'éclatement perd sa référence à IBurstContext.

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

Les fonctions utilitaires d'utilisation intensive se trouvent dans 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 l'exemple de pilote de réseaux de neurones à l'adresse 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();
}