Esta página descreve as estruturas de dados e os métodos usados para comunicar buffers de operandos entre o driver e o framework de maneira eficiente.
No momento da compilação do modelo, o framework fornece os valores dos operandos constantes ao driver. Dependendo do tempo de vida do operando constante, os valores são 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 campooperandValues
da estrutura do modelo. Como os valores no vetor HIDL são copiados durante a comunicação interprocesso (IPC), isso é normalmente usado apenas para armazenar uma pequena quantidade de dados, como operandos escalares (por exemplo, o escalar de ativação emADD
) e pequenos parâmetros de tensor (por exemplo, o tensor de forma emRESHAPE
). - Se a vida útil for
CONSTANT_REFERENCE
, os valores estarão localizados no campopools
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 momento da execução do modelo, o framework fornece os buffers dos operandos de entrada e saída para o 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 com suporte de um descritor de arquivo pormmap
.hardware_buffer_blob
: memória compartilhada apoiada por um AHardwareBuffer com o formatoAHARDWARE_BUFFER_FORMAT_BLOB
. Disponível em HAL de redes neurais (NN) 1.2. Para mais detalhes, consulte AHardwareBuffer.hardware_buffer
: memória compartilhada com suporte de um AHardwareBuffer geral que não usa o formatoAHARDWARE_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 mais detalhes, consulte AHardwareBuffer.
A partir da NN HAL 1.3, a NNAPI oferece suporte a domínios de memória que oferecem interfaces para alocar buffers gerenciados pelo driver. Os buffers gerenciados pelo driver também podem ser usados como entradas ou saídas de execução. Para mais detalhes, consulte Domínios de memória.
Os drivers da NNAPI precisam oferecer suporte ao mapeamento de nomes de memória ashmem
e mmap_fd
. Na
HAL 1.3 de NN, 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
AHardwareBuffer é um tipo de memória compartilhada que envolve um
buffer Gralloc. No Android
10, a API Neural Networks (NNAPI) oferece suporte ao uso do
AHardwareBuffer,
permitindo que o driver realize 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 NDK de câmera e
APIs 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 ele 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
operando de entrada ou saída da execução não tiver suporte. É recomendável
que o driver retorne um código de erro GENERAL_FAILURE
se a preparação ou 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.
Figura 1. Armazenar o fluxo de dados em buffer usando domínios de memória
O recurso de domínio de memória é destinado a tensores que são, na maioria das vezes, internos ao driver e que não precisam de acesso frequente no lado do cliente. Alguns 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 a 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 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,
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 impedir que um processo tente acessar o buffer
alocado em outro processo, o driver precisa aplicar a validação adequada em 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 compartilhada. Estes são alguns exemplos de casos de uso:
- Inicialização do tensor de estado
- Armazenar resultados intermediários em cache
- Execução alternativa 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 ele oferecer suporte à alocação de domínio
de memória. É opcional que o driver ofereça suporte ao modo não BLOB
hardware_buffer
.
Durante a alocação de buffer, as dimensões do buffer podem ser deduzidas dos
operandos de modelo correspondentes de todas as funções especificadas 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 está em um estado flexível em que as dimensões são fixas quando usadas como entrada de modelo e em um estado dinâmico quando usadas como saída de modelo. O mesmo
buffer pode ser usado com diferentes formas 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 pode oferecer suporte a uma determinada solicitação de alocação por vários motivos. Exemplo:
- O buffer solicitado tem um tamanho dinâmico.
- O driver tem restrições de memória que impedem o processamento de buffers grandes.
É possível que várias linhas de execução diferentes leiam do buffer gerenciado pelo driver simultaneamente. O acesso ao buffer simultaneamente para gravação ou leitura/gravação é indefinido, mas não pode causar 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.