Esecuzioni in burst e code di messaggi rapide

Neural Networks HAL 1.2 introduce il concetto di esecuzioni in burst. Le esecuzioni in serie sono una sequenza di esecuzioni dello stesso modello preparato che si verificano in rapida successione, ad esempio quelle che operano sui frame di un'acquisizione della fotocamera o su campioni audio successivi. Un oggetto burst viene utilizzato per controllare un insieme di esecuzioni burst e per conservare le risorse tra un'esecuzione e l'altra, in modo da ridurre l'overhead delle esecuzioni. Gli oggetti Burst consentono tre ottimizzazioni:

  1. Un oggetto burst viene creato prima di una sequenza di esecuzioni e viene liberato al termine della sequenza. Per questo motivo, la durata dell'oggetto burst suggerisce a un driver per quanto tempo deve rimanere in uno stato di prestazioni elevate.
  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 burst per riutilizzarla nelle esecuzioni successive. Qualsiasi risorsa memorizzata nella cache può essere rilasciata quando l'oggetto burst viene distrutto o quando il runtime NNAPI notifica all'oggetto burst che la risorsa non è più necessaria.
  3. Un oggetto burst utilizza code di coda dei messaggi rapidi (FMQ) per comunicare tra i processi dell'app e del driver. In questo modo è possibile ridurre la latenza perché FMQ aggira HIDL e passa i dati direttamente a un altro processo tramite una coda FIFO atomica circolare nella memoria condivisa. Il processo consumer sa rimuovere un elemento dalla coda ed iniziare l'elaborazione o tramite il polling del numero di elementi nella coda FIFO o in attesa del flag evento della coda FMQ, segnalato dal produttore. Questo flag evento è un mutex (futex) dello spazio utente veloce.

Una coda FMQ è una struttura di dati di basso livello che non offre garanzie di durata per tutti i processi e non dispone di un meccanismo integrato per determinare se il processo sull'altra estremità della coda FMQ è in esecuzione come previsto. Di conseguenza, se il produttore per la FMQ si arresta in modo anomalo, il consumatore potrebbe rimanere in attesa di dati che non arrivano mai. Una soluzione a questo problema è che il driver associ le FMQ all' oggetto burst di livello superiore per rilevare quando l'esecuzione del burst è terminata.

Poiché le esecuzioni in batch operano sugli stessi argomenti e restituiscono gli stessi risultati degli altri percorsi di esecuzione, le FMQ sottostanti devono passare gli stessi dati ai e dai driver di servizio NNAPI. Tuttavia, le query dinamiche con più parametri possono trasferire solo tipi di dati di tipo pd. Il trasferimento di dati complessi viene eseguito serializzando e deserializzando gli buffer nidificati (tipi di vettori) direttamente nelle FMQ e utilizzando oggetti di callback HIDL per trasferire gli handle della pool di memoria on demand. Il lato del produttore della coda FMQ deve inviare i messaggi di richiesta o di risultato al consumatore in modo atomico utilizzando MessageQueue::writeBlocking se la coda è bloccante o MessageQueue::write se la coda è non bloccante.

Interfacce burst

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

types.hal

types.hal definisce il tipo di dati inviati tramite la coda FMQ.

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

IBurstContext.hal

IBurstContext.hal definisce l'oggetto dell'interfaccia HIDL che si trova nel servizio Neural Networks.

  • IBurstContext: oggetto di contesto per gestire le risorse di un'esplosione.

IBurstCallback.hal

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

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

IPreparedModel.hal

IPreparedModel.hal viene esteso in 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.

Supporta le esecuzioni in burst in un driver

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

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

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

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

Le funzioni di utilità 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 burst trovata nel driver di esempio delle reti neurali all'indirizzo 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();
}