Execuções de burst e filas rápidas de mensagens

A HAL de redes neurais 1.2 apresenta o conceito de execuções em burst. As execuções em burst são uma sequência de execuções do mesmo modelo preparado que ocorrem em uma sucessão rápida, como aquelas que ocorrem em frames de uma captura de câmera ou amostras de áudio sucessivas. Um objeto de burst é usado para controlar um conjunto de execuções de burst e preservar recursos entre as execuções, permitindo que elas tenham menos sobrecarga. Os objetos de burst permitem três otimizações:

  1. Um objeto de burst é criado antes de uma sequência de execuções e liberado quando a sequência termina. Por isso, o tempo de vida do objeto de explosão indica a um driver por quanto tempo ele deve permanecer em um estado de alto desempenho.
  2. Um objeto de burst pode preservar recursos entre execuções. Por exemplo, um driver pode mapear um objeto de memória na primeira execução e armazenar em cache o mapeamento no objeto de burst para reutilização em execuções subsequentes. Qualquer recurso armazenado em cache pode ser liberado quando o objeto de burst é destruído ou quando o tempo de execução da NNAPI notifica o objeto de burst de que o recurso não é mais necessário.
  3. Um objeto de burst usa filas rápidas de mensagens (FMQs) para se comunicar entre processos de app e de driver. Isso pode reduzir a latência porque a FMQ ignora o HIDL e transmite dados diretamente para outro processo por uma FIFO circular atômica na memória compartilhada. O processo consumidor sabe como remover um item da fila e começar o processamento por polling do número de elementos no FIFO ou aguardando a flag de evento do FMQ, que é sinalizada pelo produtor. Esta flag de evento é um mutex rápido do espaço do usuário (futex).

Uma FMQ é uma estrutura de dados de baixo nível que não oferece garantias de ciclo de vida em processos e não tem um mecanismo integrado para determinar se o processo na outra extremidade da FMQ está sendo executado conforme o esperado. Consequentemente, se o produtor da FMQ falhar, o consumidor poderá ficar esperando dados que nunca chegam. Uma solução para esse problema é o driver associar FMQs ao objeto de burst de nível superior para detectar quando a execução do burst termina.

Como as execuções de burst operam nos mesmos argumentos e retornam os mesmos resultados que outros caminhos de execução, as FMQs subjacentes precisam transmitir os mesmos dados para e dos drivers de serviço da NNAPI. No entanto, as FMQs só podem transferir tipos de dados simples. A transferência de dados complexos é realizada serializando e desserializando buffers aninhados (tipos de vetor) diretamente nas FMQs e usando objetos de callback HIDL para transferir identificadores de pool de memória sob demanda. O lado do produtor da FMQ precisa enviar as mensagens de solicitação ou resultado ao consumidor de forma atômica usando MessageQueue::writeBlocking se a fila for de bloqueio ou MessageQueue::write se a fila não for de bloqueio.

Interfaces de burst

As interfaces de burst para a HAL de redes neurais estão em hardware/interfaces/neuralnetworks/1.2/ e são descritas abaixo. Para mais informações sobre interfaces de burst na camada do NDK, consulte frameworks/ml/nn/runtime/include/NeuralNetworks.h.

types.hal

types.hal define o tipo de dados enviados pela FMQ.

  • FmqRequestDatum: um único elemento de uma representação serializada de um objeto Request de execução e um valor MeasureTiming, que é enviado pela fila de mensagens rápidas.
  • FmqResultDatum: um único elemento de uma representação serializada dos valores retornados de uma execução (ErrorStatus, OutputShapes e Timing), que é retornado pela fila de mensagens rápida.

IBurstContext.hal

IBurstContext.hal define o objeto de interface HIDL que reside no serviço de redes neurais.

  • IBurstContext: objeto de contexto para gerenciar os recursos de um burst.

IBurstCallback.hal

IBurstCallback.hal define o objeto de interface HIDL para um callback criado pelo tempo de execução de redes neurais e é usado pelo serviço de redes neurais para recuperar objetos hidl_memory correspondentes a identificadores de slot.

  • IBurstCallback: objeto de callback usado por um serviço para recuperar objetos de memória.

IPreparedModel.hal

IPreparedModel.hal é estendido no HAL 1.2 com um método para criar um objeto IBurstContext de um modelo preparado.

  • configureExecutionBurst: configura um objeto de burst usado para executar várias inferências em um modelo preparado em rápida sucessão.

Aceitar execuções em burst em um driver

A maneira mais simples de oferecer suporte a objetos de burst em um serviço HIDL NNAPI é usar a função utilitária de burst ::android::nn::ExecutionBurstServer::create, que é encontrada em ExecutionBurstServer.h e empacotada nas bibliotecas estáticas libneuralnetworks_common e libneuralnetworks_util. Essa função de fábrica tem duas sobrecargas:

  • Uma sobrecarga aceita um ponteiro para um objeto IPreparedModel. Essa função utilitária usa o método executeSynchronously em um objeto IPreparedModel para executar o modelo.
  • Uma sobrecarga aceita um objeto IBurstExecutorWithCache personalizável, que pode ser usado para armazenar em cache recursos (como mapeamentos hidl_memory) que persistem em várias execuções.

Cada sobrecarga retorna um objeto IBurstContext (que representa o objeto de burst) que contém e gerencia a própria linha de execução de listener dedicada. Essa linha de execução recebe solicitações da FMQ requestChannel, realiza a inferência e retorna os resultados pela FMQ resultChannel. Essa linha de execução e todos os outros recursos contidos no objeto IBurstContext são liberados automaticamente quando o cliente da ação perde a referência a IBurstContext.

Outra opção é criar sua própria implementação de IBurstContext que entenda como enviar e receber mensagens pelas FMQs requestChannel e resultChannel transmitidas para IPreparedModel::configureExecutionBurst.

As funções utilitárias de burst estão em 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);

Confira a seguir uma implementação de referência de uma interface de burst encontrada no driver de exemplo de redes neurais em 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();
}