Esecuzioni burst e code di messaggi rapide

Neural Networks HAL 1.2 introduce il concetto di esecuzione a raffica. Le esecuzioni a raffica sono una sequenza di esecuzioni dello stesso modello preparato che avvengono in rapida successione, come quelle che operano sui fotogrammi dell'acquisizione della videocamera o su campioni audio successivi. Un oggetto burst viene utilizzato per controllare un insieme di esecuzioni di burst e per conservare le risorse tra un'esecuzione e l'altra, in modo che le esecuzioni abbiano un overhead inferiore. Gli oggetti Burst consentono tre ottimizzazioni:

  1. Un oggetto burst viene creato prima di una sequenza di esecuzioni e liberato quando la sequenza è terminata. Per questo motivo, la durata dell'oggetto burst indica a un driver per quanto tempo deve rimanere in uno stato ad alte prestazioni.
  2. Un oggetto burst può conservare le risorse tra un'esecuzione e l'altra. Ad esempio, un driver può mappare un oggetto di memoria alla prima esecuzione e memorizzare nella cache la mappatura nell'oggetto di burst per riutilizzarlo nelle esecuzioni successive. Qualsiasi risorsa memorizzata nella cache può essere rilasciata quando l'oggetto di burst viene eliminato o quando il runtime NNAPI comunica all'oggetto di burst che la risorsa non è più necessaria.
  3. Un oggetto burst utilizza le code di messaggi rapide (FMQ) per comunicare tra i processi dell'app e del driver. Ciò può ridurre la latenza perché l'FMQ ignora l'HIDL e passa i dati direttamente a un altro processo tramite un FIFO circolare atomico nella memoria condivisa. Il processo del consumatore è in grado di rimuovere la coda di un articolo e iniziare l'elaborazione tramite il polling del numero di elementi nel file FIFO o attendendo il flag evento di FMQ, che viene segnalato dal produttore. Questo flag evento è un mutex (futex) veloce nello spazio utente.

Un FMQ è una struttura di dati di basso livello che non offre garanzie a vita tra i processi e non ha un meccanismo integrato per determinare se il processo sull'altra estremità dell'FMQ viene eseguito come previsto. Di conseguenza, se il produttore dell'FMQ muore, il consumatore potrebbe restare bloccato in attesa di dati che non arrivano mai. Una soluzione a questo problema è che il driver possa associare gli oggetti FMQ all'oggetto burst di livello superiore per rilevare quando termina l'esecuzione del burst.

Poiché le esecuzioni di burst operano sugli stessi argomenti e restituiscono gli stessi risultati degli altri percorsi di esecuzione, gli FMQ sottostanti devono passare gli stessi dati da e verso i driver di servizio NNAPI. Tuttavia, gli FMQ possono trasferire solo tipi di dati semplici. Il trasferimento di dati complessi viene effettuato serializzando e deserializzando i buffer nidificati (tipi vettoriali) direttamente negli FMQ e utilizzando gli oggetti di callback HIDL per trasferire gli handle del pool di memoria on demand. Il lato producer di FMQ deve inviare i messaggi di richiesta o di risultato al consumer a livello atomico utilizzando MessageQueue::writeBlocking se la coda si blocca oppure utilizzando MessageQueue::write se la coda non blocca.

Interfacce di burst

Le interfacce di burst per l'HAL delle reti neurali si trovano in hardware/interfaces/neuralnetworks/1.2/ e sono descritte di seguito. Per maggiori informazioni sulle interfacce di burst nel livello NDK, consulta frameworks/ml/nn/runtime/include/NeuralNetworks.h.

type.hal

types.hal definisce il tipo di dati che vengono inviati attraverso FMQ.

  • FmqRequestDatum: un singolo elemento di una rappresentazione serializzata di un oggetto Request di esecuzione e un valore MeasureTiming, che viene inviato attraverso la coda di messaggi rapidi.
  • FmqResultDatum: un singolo elemento di una rappresentazione serializzata dei valori restituiti da un'esecuzione (ErrorStatus, OutputShapes e Timing), che viene restituita tramite la coda di messaggi rapidi.

IBurstContext.hal

IBurstContext.hal definisce l'oggetto dell'interfaccia HIDL presente nel servizio Reti neurali.

  • IBurstContext: Oggetto di contesto per gestire le risorse di un burst.

IBurstCallback.hal

IBurstCallback.hal definisce l'oggetto di interfaccia HIDL per un callback creato dal runtime di reti neurali e viene utilizzato dal servizio di reti neurali per recuperare gli oggetti hidl_memory corrispondenti agli identificatori di slot.

  • IBurstCallback: oggetto di callback utilizzato da un servizio per recuperare gli oggetti di memoria.

IPreparedModel.hal

IPreparedModel.hal è stato esteso nell'HAL 1.2 con un metodo per creare un oggetto IBurstContext da un modello preparato.

  • configureExecutionBurst: configura un oggetto burst utilizzato per eseguire più inferenze su un modello preparato in rapida successione.

Sostenere le esecuzioni a raffica in un conducente

Il modo più semplice per supportare gli oggetti di burst in un servizio NNAPI HIDL è utilizzare la funzione di utilità di burst ::android::nn::ExecutionBurstServer::create, disponibile in ExecutionBurstServer.h e pacchettizzata nelle librerie statiche libneuralnetworks_common e libneuralnetworks_util. Questa funzione di fabbrica ha due sovraccarichi:

  • Un sovraccarico accetta un puntatore a un oggetto IPreparedModel. Questa funzione di utilità utilizza il metodo executeSynchronously in un oggetto IPreparedModel per eseguire il modello.
  • Un sovraccarico accetta un oggetto IBurstExecutorWithCache personalizzabile, che può essere utilizzato per memorizzare nella cache risorse (ad esempio le mappature hidl_memory) persistenti in più esecuzioni.

Ogni sovraccarico restituisce un oggetto IBurstContext (che rappresenta l'oggetto burst) che contiene e gestisce il proprio thread listener dedicato. Questo thread riceve richieste dall'FMQ requestChannel, esegue l'inferenza, quindi restituisce i risultati tramite l'FMQ resultChannel. Questo thread e tutte le altre risorse contenute nell'oggetto IBurstContext vengono rilasciati automaticamente quando il client del burst perde il riferimento a IBurstContext.

In alternativa, puoi creare la tua implementazione di IBurstContext che comprende come inviare e ricevere messaggi tramite gli FMQ requestChannel e resultChannel passati a IPreparedModel::configureExecutionBurst.

Le funzioni di utilità di burst si trovano in 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);

Di seguito è riportata un'implementazione di riferimento di un'interfaccia di burst trovata nel driver di esempio delle reti neurali in 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();
}