API Neural Networks

A API Android Neural Networks (NNAPI) é uma API do Android C desenvolvida para executar operações com uso intenso de recursos computacionais para aprendizado de máquina em dispositivos Android. A NNAPI foi criada para oferecer uma camada básica de funcionalidade para frameworks de machine learning de nível mais alto, como TensorFlow Lite e Caffe2, que compilam e treinam redes neurais. A API está disponível em todos os dispositivos com o Android 8.1 (API de nível 27) ou versões mais recentes.

A NNAPI é compatível com inferências por meio da aplicação de dados de dispositivos Android a modelos definidos pelo desenvolvedor e treinados previamente. Os exemplos de inferências incluem classificação de imagens, previsão do comportamento do usuário e seleção de respostas apropriadas para uma consulta de pesquisa.

Inferências em dispositivos têm muitos benefícios:

  • Latência: não é preciso enviar uma solicitação em uma conexão de rede e aguardar uma resposta. Por exemplo, isso pode ser essencial para aplicativos de vídeo que processam frames sucessivos originados em uma câmera.
  • Disponibilidade: o app é executado mesmo fora da cobertura de rede.
  • Velocidade: o novo hardware específico para o processamento de redes neurais oferece uma computação significativamente mais rápida do que uma CPU de uso geral sozinha.
  • Privacidade: os dados não saem do dispositivo Android.
  • Custo: nenhum grupo de servidores é necessário quando todas as computações são realizadas no dispositivo Android.

Há também algumas desvantagens que o desenvolvedor precisa ter em mente:

  • Utilização do sistema: a avaliação de redes neurais envolve muita computação, o que pode aumentar o consumo da bateria. Monitore a integridade da bateria se essa for uma preocupação para seu app, especialmente para computações de longa execução.
  • Tamanho do aplicativo: preste atenção no tamanho dos seus modelos. Os modelos podem ocupar vários megabytes de espaço. Se o empacotamento de modelos grandes no APK afetar seus usuários de forma negativa, faça o download dos modelos depois da instalação do app, use modelos menores ou execute as computações na nuvem. A NNAPI não oferece funcionalidade para a execução de modelos na nuvem.

Consulte a amostra da API Android Neural Networks (em inglês) para ver um exemplo de como usar a NNAPI.

Entender o tempo de execução da API Neural Networks

A NNAPI precisa ser chamada por bibliotecas de machine learning, frameworks e ferramentas que permitam que os desenvolvedores treinem modelos fora do dispositivo e os implantem em dispositivos Android. Os apps geralmente não usam a NNAPI diretamente, e sim frameworks de machine learning de nível mais alto. Por sua vez, esses frameworks podem usar a NNAPI para realizar operações de inferência aceleradas por hardware em dispositivos compatíveis.

Com base nos requisitos do app e na capacidade de hardware de um dispositivo Android, o tempo de execução das redes neurais do Android pode distribuir de forma eficiente a carga de trabalho de computação nos processadores disponíveis no dispositivo, incluindo hardware de rede neural dedicado, unidades de processamento gráfico (GPUs, na sigla em inglês) e processadores de sinal digital (DSPs, na sigla em inglês).

Para dispositivos Android sem um driver de fornecedor especializado, o tempo de execução da NNAPI executa as solicitações na CPU.

A Figura 1 mostra uma arquitetura de sistema de alto nível para a NNAPI.

Figura 1. Arquitetura de sistema da API Android Neural Networks

Modelo de programação da API Neural Networks

Para executar computações usando a NNAPI, primeiro construa um gráfico direcionado que defina as computações a serem executadas. Esse gráfico de computação, combinado aos seus dados de entrada (por exemplo, os pesos e vieses transmitidos do framework de machine learning), forma o modelo para a avaliação do tempo de execução da NNAPI.

A NNAPI usa quatro abstrações principais:

  • Modelo: gráfico de computação de operações matemáticas e os valores constantes aprendidos por meio de um processo de treinamento. Essas operações são específicas para redes neurais. Elas incluem a convolução bidimensional (2D), a ativação logística (sigmoide), a ativação linear retificada (ReLU, na sigla em inglês) e muito mais (links em inglês). A criação de um modelo é uma operação síncrona. Uma vez criado, ele pode ser reutilizado em linhas de execução e compilações. Na NNAPI, um modelo é representado como uma instância ANeuralNetworksModel.
  • Compilação: representa uma configuração para compilar um modelo de NNAPI em um código de nível mais baixo. A criação de uma compilação é uma operação síncrona. Uma vez criada, ela pode ser reutilizada em linhas de execução e compilações. Na NNAPI, cada compilação é representada como uma instância ANeuralNetworksCompilation.
  • Memória: representa a memória compartilhada, os arquivos mapeados de memória e buffers de memória semelhantes. O uso de um buffer de memória permite que o tempo de execução da NNAPI transfira dados para os drivers de forma mais eficiente. Um app geralmente cria um buffer de memória compartilhado, que contém todos os tensores necessários para definir um modelo. Também é possível usar buffers de memória para armazenar entradas e saídas para uma instância de execução. Na NNAPI, cada buffer de memória é representado como uma instância ANeuralNetworksMemory.
  • Execução: interface para aplicar um modelo de NNAPI a um conjunto de entradas e para coletar resultados. A execução pode ser realizada de forma síncrona ou assíncrona.

    Para a execução assíncrona, várias linhas podem esperar na mesma execução. Quando a execução é concluída, todas as linhas de execução são liberadas

    Na NNAPI, cada execução é representada como uma instância ANeuralNetworksExecution.

A Figura 2 mostra o fluxo de programação básico.

Figura 2. Fluxo de programação para a API Android Neural Networks

O restante desta seção descreve as etapas para configurar seu modelo de NNAPI para realizar a computação, compilar o modelo e executar o modelo compilado.

Fornecer acesso a dados de treinamento

Seus dados de viés e pesos treinados provavelmente são armazenados em um arquivo. Para fornecer acesso a esses dados ao tempo de execução da NNAPI, crie uma instância ANeuralNetworksMemory, chamando a função ANeuralNetworksMemory_createFromFd() e transmitindo o descritor de arquivo do arquivo de dados aberto. Você também pode especificar sinalizações de proteção de memória e um deslocamento em que a região da memória compartilhada começa no arquivo.

// Create a memory buffer from the file that contains the trained data
    ANeuralNetworksMemory* mem1 = NULL;
    int fd = open("training_data", O_RDONLY);
    ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);
    

Apesar de nesse exemplo usarmos apenas uma instância ANeuralNetworksMemory para todos os nossos pesos, é possível usar mais de uma instância ANeuralNetworksMemory para diversos arquivos.

Usar buffers de hardware nativos

É possível usar buffers de hardware nativos para modelar entradas, saídas e valores de operando constantes. Em alguns casos, um acelerador de NNAPI pode acessar objetos AHardwareBuffer sem que o driver precise copiar os dados. AHardwareBuffer tem muitas configurações diferentes, e nem todo acelerador de NNAPI será compatível com todas essas configurações. Devido a essa limitação, consulte as restrições listadas na documentação de referência de ANeuralNetworksMemory_createFromAHardwareBuffer e faça testes com antecedência nos dispositivos em questão para garantir que as compilações e execuções que usam AHardwareBuffer se comportem como esperado, usando a atribuição de dispositivos para especificar o acelerador.

Para permitir que o ambiente de execução NNAPI acesse um objeto AHardwareBuffer, crie uma instância ANeuralNetworksMemory chamando a função ANeuralNetworksMemory_createFromAHardwareBuffer e transmitindo o objeto , conforme mostrado na amostra de código a seguir:

    // Configure and create AHardwareBuffer object
    AHardwareBuffer_Desc desc = ...
    AHardwareBuffer* awhb = nullptr;
    AHardwareBuffer_allocate(&desc, &awhb);

    // Create ANeuralNetworksMemory from AHardwareBuffer
    ANeuralNetworksMemory* mem2 = NULL;
    ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);
    

Quando a NNAPI não precisar mais acessar o objeto AHardwareBuffer, libere a instância ANeuralNetworksMemory correspondente:

    ANeuralNetworksMemory_free(mem2);
    

Observação:

  • É possível usar AHardwareBuffer somente para o buffer inteiro. Não é possível usá-lo com um parâmetro ARect.
  • O ambiente de execução da NNAPI não limpará o buffer. Verifique se os buffers de entrada e saída estão acessíveis antes de programar a execução.
  • Não há compatibilidade para descritores de arquivo de limite de sincronização.
  • Para um AHardwareBuffer com bits de uso e formatos especificados pelo fornecedor, cabe à implementação do fornecedor determinar se o cliente ou o driver será responsável por limpar o cache.

Modelo

Um modelo é a unidade fundamental de computação na NNAPI. Cada modelo é definido por um ou mais operandos e operações.

Operandos

Operandos são objetos de dados usados para definir o gráfico. Eles incluem as entradas e saídas do modelo, os nós intermediários que contêm os dados que fluem de uma operação a outra e as constantes passadas para essas operações.

Há dois tipos de operandos que podem ser adicionados aos modelos da NNAPI: escalares e tensores.

Um escalar representa um valor único. A NNAPI é compatível com valores escalares em formatos booleanos, de ponto flutuante de 16 e de 32 bits, inteiros de 32 bits e inteiros de 32 bits não assinados.

A maioria das operações na NNAPI envolve tensores. Tensores são matrizes de dimensão n. A NNAPI é compatível com tensores com ponto flutuante de 16 e de 32 bits, quantizados de 8 e de 16 bits, inteiros de 32 bits e valores booleanos de 8 bits.

Por exemplo, a Figura 3 abaixo representa um modelo com duas operações: uma adição seguida de uma multiplicação. O modelo comporta um tensor de entrada e produz um tensor de saída.

Figura 3. Exemplo de operandos para um modelo de NNAPI

O modelo acima tem sete operandos. Esses operandos são identificados implicitamente pelo índice da ordem em que são adicionados ao modelo. O primeiro operando adicionado tem um índice de 0, o segundo de 1 e assim por diante. Os operandos 1, 2, 3 e 5 são constantes.

A ordem em que os operandos são adicionados não importa. Por exemplo, o operando de saída do modelo pode ser o primeiro a ser adicionado. O importante é usar o valor de índice correto ao referenciar um operando.

Operandos têm tipos. Eles são especificados quando são adicionados ao modelo.

Um operando não pode ser usado como entrada e saída de um modelo.

Todos os operandos precisam ser uma entrada de modelo, uma constante ou o operando de saída de exatamente uma operação.

Para ver mais informações sobre como usar operandos, consulte Mais informações sobre operandos.

Operações

Uma operação especifica as computações a serem realizadas. Cada operação consiste nos seguintes elementos:

  • Um tipo de operação (por exemplo, adição, multiplicação, convolução)
  • Uma lista de índices dos operandos que a operação usa para entrada
  • Uma lista de índices dos operandos que a operação usa para saída

A ordem nessas listas é importante. Consulte a referência da NNAPI para saber quais são as entradas e saídas esperadas para cada tipo de operação.

Você precisa adicionar ao modelo os operandos que uma operação consome ou produz antes de adicionar a operação.

A ordem em que as operações são adicionadas não importa. A NNAPI usa as dependências estabelecidas pelo gráfico de computação dos operandos e operações para determinar a ordem em que as operações são executadas.

As operações com que a NNAPI é compatível estão resumidas na tabela abaixo.

Categoria Operações
Operações matemáticas com elementos
Manipulação do tensor
Operações de imagem
Operações de busca
Operações de normalização
Operações de convolução
Operações de pooling
Operações de ativação
Outras operações

Problema conhecido na API de nível 28: ao transmitir tensores ANEURALNETWORKS_TENSOR_QUANT8_ASYMM para a operação ANEURALNETWORKS_PAD, que está disponível no Android 9 (API de nível 28) e versões posteriores, a saída da NNAPI pode não corresponder à saída de frameworks de aprendizado de máquina de nível mais alto, como o TensorFlow Lite. Passe apenas ANEURALNETWORKS_TENSOR_FLOAT32. O problema foi resolvido no Android 10 (API de nível 29) e posterior.

Compilar modelos

No exemplo a seguir, criamos o modelo de duas operações encontrado na Figura 3.

Para compilar um modelo, siga estas etapas:

  1. Chame a função ANeuralNetworksModel_create() para definir um modelo vazio.

        ANeuralNetworksModel* model = NULL;
        ANeuralNetworksModel_create(&model);
        
  2. Adicione os operandos ao seu modelo chamando ANeuralNetworks_addOperand(). Os tipos de dados são definidos usando a estrutura de dados ANeuralNetworksOperandType.

        // In our example, all our tensors are matrices of dimension [3][4]
        ANeuralNetworksOperandType tensor3x4Type;
        tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
        tensor3x4Type.scale = 0.f;    // These fields are used for quantized tensors
        tensor3x4Type.zeroPoint = 0;  // These fields are used for quantized tensors
        tensor3x4Type.dimensionCount = 2;
        uint32_t dims[2] = {3, 4};
        tensor3x4Type.dimensions = dims;

    // We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;

    // Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
  3. Para operandos que têm valores constantes, como pesos e vieses que seu app adquire de um processo de treinamento, use as funções ANeuralNetworksModel_setOperandValue() e ANeuralNetworksModel_setOperandValueFromMemory().

    No exemplo a seguir, definimos valores constantes a partir do arquivo de dados de treinamento correspondente ao buffer de memória criado em Fornecer acesso a dados de treinamento.

        // In our example, operands 1 and 3 are constant tensors whose values were
        // established during the training process
        const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize
        ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
        ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);

    // We set the values of the activation operands, in our example operands 2 and 5 int32_t noneValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue)); ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
  4. Para cada operação no gráfico direcionado que você quer computar, adicione a operação ao seu modelo chamando a função ANeuralNetworksModel_addOperation().

    Como parâmetros para essa chamada, seu app precisa fornecer:

    • o tipo de operação;
    • a contagem de valores de entrada;
    • a matriz dos índices para operandos de entrada;
    • a contagem de valores de saída;
    • a matriz dos índices para operandos de saída.

    Um operando não pode ser usado para entrada e saída da mesma operação.

        // We have two operations in our example
        // The first consumes operands 1, 0, 2, and produces operand 4
        uint32_t addInputIndexes[3] = {1, 0, 2};
        uint32_t addOutputIndexes[1] = {4};
        ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);

    // The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
  5. Identifique quais operandos o modelo tratará como entradas e saídas chamando a função ANeuralNetworksModel_identifyInputsAndOutputs().

        // Our model has one input (0) and one output (6)
        uint32_t modelInputIndexes[1] = {0};
        uint32_t modelOutputIndexes[1] = {6};
        ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
        
  6. Opcionalmente, especifique se ANEURALNETWORKS_TENSOR_FLOAT32 pode ser calculado com precisão ou alcance tão baixo quanto o do formato de ponto flutuante de 16 bits IEEE 754 chamando ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Chame ANeuralNetworksModel_finish() para finalizar a definição do seu modelo. Se não houver erros, essa função retornará um código de resultado de ANEURALNETWORKS_NO_ERROR.

        ANeuralNetworksModel_finish(model);
        

Depois de criar um modelo, você pode compilá-lo e executar cada compilação quantas vezes quiser.

Compilação

A etapa de compilação determina em quais processadores seu modelo será executado e solicita que os drivers correspondentes se preparem para a execução. Isso pode incluir a geração de código de máquina específico para os processadores em que seu modelo será executado.

Para compilar um modelo, siga estas etapas:

  1. Chame a função ANeuralNetworksCompilation_create() para criar uma nova instância de compilação.

        // Compile the model
        ANeuralNetworksCompilation* compilation;
        ANeuralNetworksCompilation_create(model, &compilation);
        

    Você também pode usar a atribuição de dispositivos para escolher explicitamente em que dispositivos executar.

  2. Se quiser, você pode influenciar como o tempo de execução afeta o uso da bateria e a velocidade de execução. É possível fazer isso chamando ANeuralNetworksCompilation_setPreference().

        // Ask to optimize for low power consumption
        ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
        

    As preferências que você pode especificar incluem:

  3. Você também pode configurar o armazenamento em cache da compilação chamando ANeuralNetworksCompilation_setCaching.

        // Set up compilation caching
        ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
        

    Use getCodeCacheDir() para o cacheDir. O token especificado precisa ser único para cada modelo no aplicativo.

  4. Finalize a definição de compilação chamando ANeuralNetworksCompilation_finish(). Se não houver erros, essa função retornará um código de resultado de ANEURALNETWORKS_NO_ERROR.

        ANeuralNetworksCompilation_finish(compilation);
        

Descoberta e atribuição de dispositivos

Em dispositivos Android versão 10 (API nível 29) e posterior, a NNAPI oferece funções que permitem que bibliotecas e apps do framework de machine learning recebam informações sobre os dispositivos disponíveis e especifiquem os dispositivos a serem usados para a execução. A disponibilização de informações sobre os dispositivos disponíveis permite que os apps saibam a versão exata dos drivers encontrados em um dispositivo para evitar incompatibilidades conhecidas. Ao conceder aos apps a capacidade de especificar quais dispositivos devem executar seções diferentes de um modelo, os apps podem ser otimizados para o dispositivo Android em que são implantados.

Descoberta de dispositivos

Use ANeuralNetworks_getDeviceCount para ver o número de dispositivos disponíveis. Use ANeuralNetworks_getDevice para cada dispositivo para configurar uma instância ANeuralNetworksDevice para um referência ao dispositivo em questão.

Depois de conseguir uma referência de dispositivo, você pode descobrir mais informações sobre ele usando as seguintes funções:

Atribuição de dispositivos

Use ANeuralNetworksModel_getSupportedOperationsForDevices para descobrir quais operações de um modelo podem ser executadas em dispositivos específicos.

Para controlar quais aceleradores serão usados para a execução, chame ANeuralNetworksCompilation_createForDevices em vez de ANeuralNetworksCompilation_create. Use o objeto ANeuralNetworksCompilationresultante normalmente. A função retornará um erro se o modelo indicado tiver operações incompatíveis com os dispositivos selecionados.

Se vários dispositivos forem especificados, o tempo de execução será responsável pela distribuição do trabalho entre os dispositivos.

Assim como em outros dispositivos, a implementação de CPU com NNAPI é representada por um ANeuralNetworksDevice com o nome nnapi-reference e o tipo ANEURALNETWORKS_DEVICE_TYPE_CPU. Ao chamar ANeuralNetworksCompilation_createForDevices, a implementação da CPU não é usada para processar casos de falha para a compilação e a execução de modelos.

É responsabilidade do app particionar um modelo em submodelos que possam ser executadas nos dispositivos especificados. Os apps que não precisam realizar particionamentos manuais devem continuar chamando o ANeuralNetworksCompilation_create mais simples para usar todos os dispositivos disponíveis (inclusive a CPU) para acelerar o modelo. Se o modelo não for totalmente compatível com os dispositivos especificados usando ANeuralNetworksCompilation_createForDevices, ANEURALNETWORKS_BAD_DATA será retornado.

Particionamento de modelos

Quando vários dispositivos estiverem disponíveis para o modelo, o tempo de execução da NNAPI distribuirá o trabalho entre os dispositivos. Por exemplo, se mais de um dispositivo foi fornecido para ANeuralNetworksCompilation_createForDevices, todos os especificados serão considerados ao alocar o trabalho. Se o dispositivo da CPU não estiver na lista, a execução da CPU será desativada. Ao usar ANeuralNetworksCompilation_create, todos os dispositivos disponíveis serão considerados, incluindo a CPU.

A distribuição é realizada selecionando da lista de dispositivos disponíveis, para cada operação no modelo, o dispositivo que é compatível com a operação e que declara o melhor desempenho, ou seja, o menor tempo de execução ou o menor consumo de energia, dependendo da preferência de execução especificada pelo cliente. Esse algoritmo de particionamento não considera possíveis ineficiências causadas pelo pedido de veiculação entre os diferentes processadores. Portanto, ao especificar vários processadores (explicitamente usando ANeuralNetworksCompilation_createForDevices ou implicitamente usando ANeuralNetworksCompilation_create), é importante definir o perfil do aplicativo resultante.

Para entender como seu modelo foi particionado pela NNAPI, verifique se há uma mensagem nos registros do Android (no nível INFO com a tag ExecutionPlan):

    ModelBuilder::findBestDeviceForEachOperation(op-name): device-index
    

op-name é o nome descritivo da operação no gráfico, e device-index é o índice do dispositivo candidato na lista de dispositivos. Essa lista é a entrada fornecida para ANeuralNetworksCompilation_createForDevices ou, se estiver usando ANeuralNetworksCompilation_createForDevices, a lista de dispositivos retornados ao iterar em todos os dispositivos usando ANeuralNetworks_getDeviceCount e ANeuralNetworks_getDevice.

A mensagem (no nível INFO com tag ExecutionPlan):

    ModelBuilder::partitionTheWork: only one best device: device-name
    

Essa mensagem indica que o gráfico inteiro foi acelerado no dispositivo device-name.

Execução

A etapa de execução aplica o modelo a um conjunto de entradas e armazena as saídas de computação em um ou mais buffers de usuário ou espaços de memória que seu app tenha alocado.

Para executar um modelo compilado, siga estas etapas:

  1. Chame a função ANeuralNetworksExecution_create() para criar uma nova instância de execução.

        // Run the compiled model against a set of inputs
        ANeuralNetworksExecution* run1 = NULL;
        ANeuralNetworksExecution_create(compilation, &run1);
        
  2. Especifique onde seu app lê os valores de entrada para a computação. Seu app pode ler valores de entrada de um buffer de usuário ou de um espaço de memória alocado chamando ANeuralNetworksExecution_setInput() ou ANeuralNetworksExecution_setInputFromMemory() respectivamente.

        // Set the single input to our sample model. Since it is small, we won't use a memory buffer
        float32 myInput[3][4] = { ...the data... };
        ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
        
  3. Especifique onde seu app grava os valores de saída. Seu app pode gravar valores de saída em um buffer de usuário ou em um espaço de memória alocado chamando ANeuralNetworksExecution_setOutput() ou ANeuralNetworksExecution_setOutputFromMemory() respectivamente.

        // Set the output
        float32 myOutput[3][4];
        ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
        
  4. Programe o início da execução chamando a função ANeuralNetworksExecution_startCompute(). Se não houver erros, essa função retornará um código de resultado de ANEURALNETWORKS_NO_ERROR.

        // Starts the work. The work proceeds asynchronously
        ANeuralNetworksEvent* run1_end = NULL;
        ANeuralNetworksExecution_startCompute(run1, &run1_end);
        
  5. Chame a função ANeuralNetworksEvent_wait() para aguardar a execução ser concluída. Se a execução ocorrer corretamente, essa função retornará um código de resultado de ANEURALNETWORKS_NO_ERROR. É possível aguardar em uma linha de execução diferente daquela que inicia a execução.

        // For our example, we have no other work to do and will just wait for the completion
        ANeuralNetworksEvent_wait(run1_end);
        ANeuralNetworksEvent_free(run1_end);
        ANeuralNetworksExecution_free(run1);
        
  6. Opcionalmente, você pode aplicar um conjunto diferente de entradas ao modelo compilado usando a mesma instância de compilação para criar uma nova instância ANeuralNetworksExecution.

        // Apply the compiled model to a different set of inputs
        ANeuralNetworksExecution* run2;
        ANeuralNetworksExecution_create(compilation, &run2);
        ANeuralNetworksExecution_setInput(run2, ...);
        ANeuralNetworksExecution_setOutput(run2, ...);
        ANeuralNetworksEvent* run2_end = NULL;
        ANeuralNetworksExecution_startCompute(run2, &run2_end);
        ANeuralNetworksEvent_wait(run2_end);
        ANeuralNetworksEvent_free(run2_end);
        ANeuralNetworksExecution_free(run2);
        

Execução síncrona

A execução assíncrona demora para gerar e sincronizar linhas de execução. Além disso, a latência pode ser extremamente variável, com os atrasos mais longos atingindo até 500 microssegundos entre o momento em que uma linha de execução é notificada ou iniciada e o momento em que ela é vinculada a um núcleo da CPU.

Para melhorar a latência, você pode direcionar um aplicativo para fazer uma chamada de inferência síncrona para o tempo de execução. Essa chamada só retornará depois que uma inferência for concluída, em vez de retornar depois que a inferência é iniciada. Em vez de chamar ANeuralNetworksExecution_startCompute para uma chamada de inferência assíncrona ao tempo de execução, o app chama ANeuralNetworksExecution_compute para fazer uma chamada síncrona ao tempo de execução. Uma chamada para ANeuralNetworksExecution_compute não leva uma ANeuralNetworksEvent e não é pareada com uma chamada para ANeuralNetworksEvent_wait.

Execuções em burst

Em dispositivos Android versão 10 (API de nível 29) e posteriores, a NNAPI é compatível com execuções em burst por meio do objeto ANeuralNetworksBurst. As execuções em burst são uma sequência de execuções da mesma compilação 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. O uso de objetos ANeuralNetworksBurst pode resultar em execuções mais rápidas, porque eles indicam aos aceleradores que os recursos podem ser reutilizados entre as execuções e que os aceleradores devem permanecer em um estado de alto desempenho durante o burst.

ANeuralNetworksBurst introduz apenas uma pequena mudança no caminho de execução normal. Um objeto burst é criado usando ANeuralNetworksBurst_create, conforme mostrado no snippet de código a seguir:

    // Create burst object to be reused across a sequence of executions
    ANeuralNetworksBurst* burst = NULL;
    ANeuralNetworksBurst_create(compilation, &burst);
    

As execuções burst são síncronas. Entretanto, em vez de usar ANeuralNetworksExecution_compute para realizar a inferência, pareie os diversos objetos ANeuralNetworksExecution com a mesma ANeuralNetworksBurst em chamadas para a função ANeuralNetworksExecution_burstCompute.

    // Create and configure first execution object
    // ...

    // Execute using the burst object
    ANeuralNetworksExecution_burstCompute(execution1, burst);

    // Use results of first execution and free the execution object
    // ...

    // Create and configure second execution object
    // ...

    // Execute using the same burst object
    ANeuralNetworksExecution_burstCompute(execution2, burst);

    // Use results of second execution and free the execution object
    // ...
    

Libere o objeto ANeuralNetworksBurst com ANeuralNetworksBurst_free quando ele não for mais necessário.

    // Cleanup
    ANeuralNetworksBurst_free(burst);
    

Saídas dimensionadas de forma dinâmica

Para oferecer compatibilidade com modelos em que o tamanho da saída depende dos dados da entrada, ou seja, o tamanho não pode ser determinado no tempo de execução do modelo, use ANeuralNetworksExecution_getOutputOperandRank e ANeuralNetworksExecution_getOutputOperandDimensions.

A amostra de código a seguir mostra como fazer isso.

    // Get the rank of the output
    uint32_t myOutputRank = 0;
    ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank);

    // Get the dimensions of the output
    std::vector<uint32_t> myOutputDimensions(myOutputRank);
    ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());
    

Avaliar o desempenho

Quando você quiser determinar o tempo total de execução, use a API de execução síncrona e meça o tempo utilizado pela chamada. Quando quiser determinar o tempo de execução total por um nível inferior da pilha de software, você pode usar ANeuralNetworksExecution_setMeasureTiming e ANeuralNetworksExecution_getDuration para obter:

  • o tempo de execução em um acelerador (não no driver, que é executado no processador host);
  • o tempo de execução no driver, incluindo o tempo no acelerador.

O tempo de execução no driver exclui sobrecargas, como a do próprio ambiente de execução e da IPC, necessária para que o ambiente de execução se comunique com o driver.

Essas APIs medem a duração entre o trabalho enviado e os eventos concluídos, não do tempo que um driver ou acelerador dedica à realização da inferência, possivelmente interrompido pela alternância de contexto.

Por exemplo, se a inferência 1 começar, o driver interromperá o trabalho para realizar a inferência 2 e, em seguida, retomará e concluirá a inferência 1. O tempo de execução da inferência 1 incluirá o momento em que o trabalho foi interrompido para a execução da inferência 2.

Essas informações de tempo podem ser úteis para a implantação de produção de um aplicativo, para coletar dados de telemetria para uso off-line. Você pode usar os dados de tempo para modificar o app, melhorando o desempenho.

Ao usar essa funcionalidade, lembre-se de que:

  • a coleta de informações de tempo pode afetar o desempenho;
  • somente um driver é capaz de computar o tempo gasto nele mesmo ou no acelerador, exceto o tempo gasto no ambiente de execução da NNAPI e na IPC;
  • Você pode usar essas APIs somente com uma ANeuralNetworksExecution que foi criada com ANeuralNetworksCompilation_createForDevices com numDevices = 1.
  • nenhum driver é necessário para relatar informações de tempo.

Limpeza

A etapa de limpeza realiza a liberação de recursos internos usados para sua computação.

    // Cleanup
    ANeuralNetworksCompilation_free(compilation);
    ANeuralNetworksModel_free(model);
    ANeuralNetworksMemory_free(mem1);
    

Gerenciamento de erros e substituição de CPU

Se ocorrer um erro durante o particionamento, se um driver não compilar um modelo (ou parte dele) ou se um driver não executar um modelo compilado (ou parte dele), a NNAPI poderá usar a própria implementação de CPU do modelo ou mais operações.

Se o cliente NNAPI contiver versões otimizadas da operação (como, por exemplo, TFLite), poderá ser vantajoso desativar o substituto da CPU e processar as falhas com a implementação de operação otimizada do cliente.

No Android 10, se a compilação for realizada usando ANeuralNetworksCompilation_createForDevices, o substituto da CPU será desativado.

No Android P, a execução de NNAPI é substituída pela CPU se a execução no driver falhar. Isso também é verdade no Android 10 quando ANeuralNetworksCompilation_create é usado em vez de ANeuralNetworksCompilation_createForDevices.

A primeira execução é substituída pela partição única e se isso ainda falhar, ela repete todo o modelo na CPU.

Se o particionamento ou a compilação falhar, todo o modelo será testado na CPU.

Há casos em que algumas operações não são compatíveis com a CPU e, nessas situações, a compilação ou a execução falharão em vez de serem substituídas.

Mesmo após a desativação do substituto de CPU, ainda pode haver operações no modelo que estão programadas na CPU. Se a CPU estiver na lista de processadores permitidos e for o único processador compatível com essas operações ou for o processador que reivindica o melhor desempenho para essas operações, ela será escolhido como executor principal (não substituto).

Para garantir que não haja execução da CPU, use ANeuralNetworksCompilation_createForDevices ao excluir nnapi-reference da lista de dispositivos. A partir do Android P, é possível desativar a substituição durante a execução nas versões DEBUG, definindo a propriedade debug.nn.partition como 2.

Mais informações sobre operandos

A seção a seguir aborda assuntos avançados sobre o uso de operandos.

Tensores quantizados

Um tensor quantizado é uma forma compacta para representar uma matriz de dimensão n de valores de ponto flutuante.

A NNAPI tem compatibilidade com tensores quantizados assimétricos de 8 bits. Para esses tensores, o valor de cada célula é representado por um número inteiro de 8 bits. Associados ao tensor estão uma escala e um valor de ponto zero. Eles são usados para converter os inteiros de 8 bits nos valores de ponto flutuante que estão sendo representados.

A fórmula é a seguinte:

    (cellValue - zeroPoint) * scale
    

Onde o valor zeroPoint é um número inteiro de 32 bits e a escala é um valor de ponto flutuante de 32 bits.

Em comparação com os tensores de valores de ponto flutuante de 32 bits, os tensores quantizados de 8 bits têm duas vantagens:

  • Seu app será menor, já que os pesos treinados ocuparão um quarto do tamanho dos tensores de 32 bits.
  • Com frequência, as computações podem ser executadas com mais rapidez. Isso se deve à menor quantidade de dados que precisa ser recuperada da memória e à eficiência dos processadores, como DSPs, na matemática de inteiros.

Embora seja possível converter um modelo de ponto flutuante em um quantizado, nossa experiência mostrou que resultados melhores são conseguidos ao treinar um modelo quantizado diretamente. Na prática, a rede neural aprende a compensar pelo aumento de granularidade de cada valor. Para cada tensor quantizado, os valores de escala e zeroPoint são determinados durante o processo de treinamento.

Na NNAPI, defina tipos de tensores quantizados configurando o tipo de campo da estrutura de dados de ANeuralNetworksOperandType como ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. Especifique também o valor de escala e zeroPoint do tensor nessa estrutura de dados.

Além dos tensores quantizados assimétricos de 8 bits, a NNAPI é compatível com:

Operandos opcionais

Algumas operações, como ANEURALNETWORKS_LSH_PROJECTION, usam operandos opcionais. Para indicar no modelo que o operando opcional é omitido, chame a função ANeuralNetworksModel_setOperandValue(), transmitindo NULL para o buffer e 0 para o comprimento.

Se a decisão de incluir ou não o operando variar para cada execução, indique que o operando foi omitido usando as funções ANeuralNetworksExecution_setInput() ou ANeuralNetworksExecution_setOutput(), transmitindo NULL para o buffer e 0 para o comprimento.

Tensores de classificação desconhecida

O Android 9 (API de nível 28) introduziu operandos de modelo de dimensões desconhecidas, mas com classificação conhecida (número de dimensões). O Android 10 (API nível 29) introduziu tensores de classificação desconhecida, como mostrado em ANeuralNetworksOperandType.

Comparativo de mercado da NNAPI

O comparativo de mercado da NNAPI está disponível no AOSP em platform/test/mlts/benchmark (app de comparativo de mercado) e platform/test/mlts/models (modelos e conjuntos de dados).

O comparativo de mercado avalia a latência e a precisão e compara os drivers com o mesmo trabalho realizado usando o Tensorflow Lite em execução na CPU, para os mesmos modelos e conjuntos de dados.

Para usar o comparativo de mercado, faça o seguinte:

  1. Conecte um dispositivo Android de destino ao seu computador, abra uma janela do terminal e verifique se o dispositivo está acessível por meio do adb.

  2. Se mais de um dispositivo Android estiver conectado, exporte a variável de ambiente ANDROID_SERIAL do dispositivo de destino.

  3. Navegue para o diretório original de nível superior do Android.

  4. Execute os seguintes comandos:

        lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available
        ./test/mlts/benchmark/build_and_run_benchmark.sh
        

    No final de uma execução de comparativo de mercado, os resultados serão apresentados como uma página HTML transmitida para xdg-open.

Registros NNAPI

A NNAPI gera informações de diagnóstico úteis nos registros do sistema. Para analisar os registros, use o recurso logcat.

Ative o registro NNAPI detalhado para fases ou componentes específicos definindo a propriedade debug.nn.vlog (usando adb shell) como a seguinte lista de valores, separados por espaço, dois pontos ou vírgula:

  • model: modelismo
  • compilation: geração do plano de execução e compilação do modelo
  • execution: execução do modelo
  • cpuexe: execução de operações usando a implementação da CPU da NNAPI
  • manager: extensões NNAPI, interfaces disponíveis e informações relacionadas aos recursos
  • all ou 1: todos os elementos acima

Por exemplo, para ativar a geração de registros detalhados completos, use o comando adb shell setprop debug.nn.vlog all. Para desativar o registro detalhado, use o comando adb shell setprop debug.nn.vlog '""'.

Depois de ativado, o registro detalhado gera entradas de registro no nível INFO com uma tag definida para o nome da fase ou do componente.

Ao lado das mensagens controladas debug.nn.vlog, os componentes da API NNAPI fornecem outras entradas de registro em vários níveis, cada uma usando uma tag de registro específica.

Para ver uma lista de componentes, pesquise a árvore de origem usando a seguinte expressão:

grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"

No momento, essa expressão retorna as seguintes tags:

  • BurstBuilder
  • Callbacks
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Administrador
  • Memória
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Operações
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Utilitários
  • ValidateHal
  • VersionedInterfaces

Para controlar o nível de mensagens de registro mostradas por logcat, use a variável de ambiente ANDROID_LOG_TAGS.

Para mostrar o conjunto completo de mensagens de registro da NNAPI e desativar outros, configure ANDROID_LOG_TAGS como o seguinte:

    BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.
    

Você pode definir ANDROID_LOG_TAGS usando o seguinte comando:

    export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')
    

Esse é apenas um filtro que se aplica a logcat. Você ainda precisa definir a propriedade debug.nn.vlog como all para gerar informações de registro detalhadas.