Pools de memória

Nesta página, descrevemos as estruturas de dados e os métodos usados para comunicar com eficiência os buffers de operando entre o driver e o framework.

No momento da compilação do modelo, o framework fornece os valores dos operandos constantes ao driver. Dependendo da vida útil do operando constante, os valores dele ficam localizados em um vetor HIDL ou em um pool de memória compartilhada.

  • Se a vida útil for CONSTANT_COPY, os valores estarão localizados no campo operandValues da estrutura do modelo. Como os valores no vetor HIDL são copiados durante a comunicação entre processos (IPC), isso geralmente é usado apenas para armazenar uma pequena quantidade de dados, como operandos escalares (por exemplo, o escalar de ativação em ADD) e pequenos parâmetros de tensor (por exemplo, o tensor de forma em RESHAPE).
  • Se a vida útil for CONSTANT_REFERENCE, os valores estarão localizados no campo pools da estrutura do modelo. Somente os identificadores dos pools de memória compartilhada são duplicados durante a IPC, em vez de copiar os valores brutos. Portanto, é mais eficiente armazenar uma grande quantidade de dados (por exemplo, os parâmetros de peso em convoluções) usando pools de memória compartilhados do que vetores HIDL.

No ambiente de execução do modelo, o framework fornece os buffers dos operandos de entrada e saída ao driver. Ao contrário das constantes de tempo de compilação que podem ser enviadas em um vetor HIDL, os dados de entrada e saída de uma execução são sempre comunicados por uma coleção de pools de memória.

O tipo de dados HIDL hidl_memory é usado na compilação e na execução para representar um pool de memória compartilhada não mapeada. O driver precisa mapear a memória adequadamente para torná-la utilizável com base no nome do tipo de dados hidl_memory. Os nomes de memória compatíveis são:

  • ashmem: memória compartilhada do Android. Para mais detalhes, consulte memória.
  • mmap_fd: memória compartilhada apoiada por um descritor de arquivo por meio de mmap.
  • hardware_buffer_blob: memória compartilhada apoiada por um AHardwareBuffer com o formato AHARDWARE_BUFFER_FORMAT_BLOB. Disponível na HAL 1.2 de redes neurais (NN, na sigla em inglês). Para ver mais detalhes, consulte AHardwareBuffer.
  • hardware_buffer: memória compartilhada apoiada por um AHardwareBuffer geral que não usa o formato AHARDWARE_BUFFER_FORMAT_BLOB. O buffer de hardware do modo não BLOB só tem suporte na execução do modelo.Disponível na NN HAL 1.2. Para ver mais detalhes, consulte AHardwareBuffer.

A partir da NN HAL 1.3, a NNAPI oferece suporte a domínios de memória que fornecem interfaces de alocador para buffers gerenciados pelo driver. Os buffers gerenciados pelo driver também podem ser usados como entradas ou saídas de execução. Para ver mais detalhes, consulte Domínios de memória.

Os drivers NNAPI precisam ser compatíveis com o mapeamento de nomes de memória ashmem e mmap_fd. A partir da NN HAL 1.3, os drivers também precisam oferecer suporte ao mapeamento de hardware_buffer_blob. O suporte para o modo geral não BLOB hardware_buffer e domínios de memória é opcional.

AHardwareBuffer

O AHardwareBuffer é um tipo de memória compartilhada que encapsula um buffer Galloc. No Android 10, a API Neural Networks (NNAPI) oferece suporte ao uso de AHardwareBuffer, permitindo que o driver faça execuções sem copiar dados, o que melhora o desempenho e o consumo de energia dos apps. Por exemplo, uma pilha HAL de câmera pode transmitir objetos AHardwareBuffer para a NNAPI para cargas de trabalho de aprendizado de máquina usando identificadores AHardwareBuffer gerados por APIs de NDK da câmera e do NDK de mídia. Para mais informações, consulte ANeuralNetworksMemory_createFromAHardwareBuffer.

Os objetos AHardwareBuffer usados na NNAPI são transmitidos para o driver por uma estrutura hidl_memory chamada hardware_buffer ou hardware_buffer_blob. O struct hardware_buffer_blob do hidl_memory representa apenas objetos AHardwareBuffer com o formato AHARDWAREBUFFER_FORMAT_BLOB.

As informações exigidas pelo framework são codificadas no campo hidl_handle do struct hidl_memory. O campo hidl_handle encapsula native_handle, que codifica todos os metadados necessários sobre o buffer AHardwareBuffer ou Gralloc.

O driver precisa decodificar corretamente o campo hidl_handle fornecido e acessar a memória descrita por hidl_handle. Quando o método getSupportedOperations_1_2, getSupportedOperations_1_1 ou getSupportedOperations é chamado, o driver precisa detectar se pode decodificar o hidl_handle fornecido e acessar a memória descrita por hidl_handle. A preparação do modelo vai falhar se o campo hidl_handle usado para um operando de constante não tiver suporte. A execução vai falhar se o campo hidl_handle usado para um operador de entrada ou saída não tiver suporte. Recomenda-se que o driver retorne um código de erro GENERAL_FAILURE se a preparação ou a execução do modelo falhar.

Domínios de memória

Para dispositivos com o Android 11 ou versões mais recentes, a NNAPI oferece suporte a domínios de memória que oferecem interfaces de alocação para buffers gerenciados pelo driver. Isso permite transmitir as memórias nativas do dispositivo entre as execuções, suprimindo a cópia e transformação de dados desnecessárias entre execuções consecutivas no mesmo driver. Esse fluxo é ilustrado na figura 1.

Fluxo de dados de buffer com e sem domínios de memória

Figura 1. Fluxo de dados de buffer usando domínios de memória

O recurso de domínio da memória é destinado a tensores que são, na maioria, internos ao driver e não precisam de acesso frequente no lado do cliente. Exemplos desses tensores incluem os tensores de estado em modelos sequenciais. Para tensores que precisam de acesso frequente à CPU no lado do cliente, é preferível usar pools de memória compartilhada.

Para oferecer suporte ao recurso de domínio de memória, implemente IDevice::allocate para permitir que o framework solicite a alocação de buffer gerenciada pelo driver. Durante a alocação, o framework fornece as seguintes propriedades e padrões de uso para o buffer:

  • BufferDesc descreve as propriedades necessárias do buffer.
  • BufferRole descreve o possível padrão de uso do buffer como uma entrada ou saída de um modelo preparado. É possível especificar vários papéis durante a alocação do buffer, que pode ser usado somente como esses papéis especificados.

O buffer alocado é interno ao driver. O driver pode escolher qualquer local de buffer ou layout de dados. Quando o buffer é alocado corretamente, o cliente do driver pode referenciar ou interagir com o buffer usando o token retornado ou o objeto IBuffer.

O token de IDevice::allocate é fornecido ao referenciar o buffer como um dos objetos MemoryPool na estrutura Request de uma execução. Para evitar que um processo tente acessar o buffer alocado em outro, o driver precisa aplicar a validação adequada a cada uso do buffer. O driver precisa validar se o uso do buffer é um dos papéis BufferRole fornecidos durante a alocação e falhar na execução imediatamente se o uso for ilegal.

O objeto IBuffer é usado para cópia explícita da memória. Em determinadas situações, o cliente do driver precisa inicializar o buffer gerenciado pelo driver em um pool de memória compartilhada ou copiar o buffer para um pool de memória compartilhado. Estes são alguns exemplos de casos de uso:

  • Inicialização do tensor de estado
  • Armazenar resultados intermediários em cache
  • Execução de substituto na CPU

Para oferecer suporte a esses casos de uso, o driver precisa implementar IBuffer::copyTo e IBuffer::copyFrom com ashmem, mmap_fd e hardware_buffer_blob, se for compatível com a alocação de domínio de memória. É opcional para o driver oferecer suporte ao modo não BLOB hardware_buffer.

Durante a alocação do buffer, as dimensões do buffer podem ser deduzidas dos operandos de modelo correspondentes de todos os papéis especificados por BufferRole e das dimensões fornecidas em BufferDesc. Com todas as informações dimensionais combinadas, o buffer pode ter dimensões ou classificação desconhecidas. Nesse caso, o buffer fica em um estado flexível, em que as dimensões são fixas quando usadas como uma entrada do modelo e em um estado dinâmico quando usado como uma saída de modelo. O mesmo buffer pode ser usado com diferentes formatos de saídas em diferentes execuções, e o driver precisa processar o redimensionamento do buffer corretamente.

O domínio da memória é um recurso opcional. Um driver pode determinar que não é compatível com uma determinada solicitação de alocação por vários motivos. Por exemplo:

  • O buffer solicitado tem um tamanho dinâmico.
  • O driver tem restrições de memória que o impedem de processar grandes buffers.

É possível que várias linhas de execução diferentes leiam o buffer gerenciado pelo driver simultaneamente. O acesso ao buffer simultaneamente para gravação ou leitura/gravação é indefinido, mas não pode causar uma falha no serviço do driver nem bloquear o autor da chamada indefinidamente. O driver pode retornar um erro ou deixar o conteúdo do buffer em um estado indeterminado.