A partir do Android 10, a API Neural Networks (NNAPI)
oferece funções para oferecer suporte ao
armazenamento em cache de artefatos de compilação, o que reduz o tempo usado para compilação
quando um app é iniciado. Com essa funcionalidade de armazenamento em cache, o driver não precisa gerenciar nem limpar os arquivos armazenados em cache. Esse é um recurso opcional que
pode ser implementado com a HAL NN 1.2. Para mais informações sobre essa função, consulte ANeuralNetworksCompilation_setCaching
.
O driver também pode implementar o cache de compilação independente da NNAPI. Isso pode ser implementado com ou sem o uso dos recursos de cache da NNAPI NDK e da HAL. O AOSP fornece uma biblioteca de utilitários de baixo nível (um mecanismo de cache). Para mais informações, consulte Implementar um mecanismo de cache.
Visão geral do fluxo de trabalho
Esta seção descreve fluxos de trabalho gerais com o recurso de cache de compilação implementado.
Informações de cache fornecidas e ocorrência em cache
- O app transmite um diretório de cache e um checksum exclusivo para o modelo.
- O tempo de execução da NNAPI procura os arquivos de cache com base no checksum, na preferência de execução e no resultado do particionamento.
- A NNAPI abre os arquivos de cache e passa os identificadores para o driver
com
prepareModelFromCache
. - O driver prepara o modelo diretamente dos arquivos de cache e retorna o modelo preparado.
Informações de cache fornecidas e ausência no cache
- O app transmite um checksum exclusivo para o modelo e um diretório de armazenamento em cache.
- O tempo de execução da NNAPI procura os arquivos de cache com base no checksum, na preferência de execução e no resultado do particionamento, mas não encontra os arquivos de cache.
- A NNAPI cria arquivos de cache vazios com base no checksum, na preferência de execução e no particionamento, abre os arquivos de cache e transmite os identificadores e o modelo ao driver com
prepareModel_1_2
. - O driver compila o modelo, grava informações de cache nos arquivos de cache e retorna o modelo preparado.
Informações de cache não fornecidas
- O app invoca a compilação sem fornecer informações de cache.
- O app não transmite nada relacionado ao armazenamento em cache.
- O ambiente de execução da NNAPI transmite o modelo ao driver com
prepareModel_1_2
. - O driver compila o modelo e retorna o modelo preparado.
Informações de cache
As informações de cache fornecidas a um driver consistem em um token e identificadores de arquivo de cache.
Token
O token é um token de cache de comprimento Constant::BYTE_SIZE_OF_CACHE_TOKEN
que identifica o modelo preparado. O mesmo token é fornecido ao salvar os
arquivos de cache com prepareModel_1_2
e ao recuperar o modelo preparado com
prepareModelFromCache
. O cliente do motorista precisa escolher um token com uma
baixa taxa de colisão. O driver não consegue detectar uma colisão de tokens. Uma colisão resulta em uma execução com falha ou em uma execução bem-sucedida que produz valores de saída incorretos.
Processa arquivos em cache (dois tipos de arquivos em cache)
Os dois tipos de arquivos de cache são cache de dados e cache de modelo.
- Cache de dados:use para armazenar em cache dados constantes, incluindo buffers de tensores pré-processados e transformados. Uma modificação no cache de dados não deve resultar em um efeito pior do que gerar valores de saída ruins no momento da execução.
- Cache de modelo:use para armazenar em cache dados sensíveis à segurança, como código de máquina executável compilado no formato binário nativo do dispositivo. Uma modificação no cache do modelo pode afetar o comportamento de execução do driver, e um cliente malicioso pode usar isso para executar além da permissão concedida. Portanto, o driver precisa verificar se o cache do modelo está corrompido antes de preparar o modelo do cache. Para mais informações, consulte Segurança.
O driver precisa decidir como as informações de cache são distribuídas entre os dois tipos de arquivos de cache e informar quantos arquivos de cache são necessários para cada tipo com getNumberOfCacheFilesNeeded
.
O tempo de execução da NNAPI sempre abre identificadores de arquivos de cache com permissão de leitura e gravação.
Segurança
No cache de compilação, o cache do modelo pode conter dados sensíveis à segurança, como código de máquina executável compilado no formato binário nativo do dispositivo. Se não for protegida adequadamente, uma modificação no cache do modelo poderá afetar o comportamento de execução do driver. Como o conteúdo do cache é armazenado no diretório do app, os arquivos de cache podem ser modificados pelo cliente. Um cliente com bugs pode corromper o cache por acidente, e um cliente malicioso pode usar isso para executar código não verificado no dispositivo. Dependendo das características do dispositivo, isso pode ser um problema de segurança. Assim, o driver precisa detectar uma possível corrupção do cache do modelo antes de preparar o modelo do cache.
Uma maneira de fazer isso é o driver manter um mapa do token para um hash criptográfico do cache do modelo. O driver pode armazenar o token e o hash do cache do modelo ao salvar a compilação em cache. O driver verifica
o novo hash do cache do modelo com o par de token e hash gravado ao
recuperar a compilação do cache. Esse mapeamento precisa ser persistente em reinicializações do sistema. O motorista pode usar o
serviço de keystore do Android, a biblioteca de utilitários em
framework/ml/nn/driver/cache
ou qualquer outro mecanismo adequado para implementar um gerenciador de mapas. Após a atualização do driver, esse gerenciador de mapeamento precisa ser reinicializado para evitar a preparação de arquivos de cache de uma versão anterior.
Para evitar ataques time-of-check to time-of-use (TOCTOU), o driver precisa calcular o hash gravado antes de salvar no arquivo e calcular o novo hash depois de copiar o conteúdo do arquivo para um buffer interno.
Este exemplo de código demonstra como implementar essa lógica.
bool saveToCache(const sp<V1_2::IPreparedModel> preparedModel,
const hidl_vec<hidl_handle>& modelFds, const hidl_vec<hidl_handle>& dataFds,
const HidlToken& token) {
// Serialize the prepared model to internal buffers.
auto buffers = serialize(preparedModel);
// This implementation detail is important: the cache hash must be computed from internal
// buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
auto hash = computeHash(buffers);
// Store the {token, hash} pair to a mapping manager that is persistent across reboots.
CacheManager::get()->store(token, hash);
// Write the cache contents from internal buffers to cache files.
return writeToFds(buffers, modelFds, dataFds);
}
sp<V1_2::IPreparedModel> prepareFromCache(const hidl_vec<hidl_handle>& modelFds,
const hidl_vec<hidl_handle>& dataFds,
const HidlToken& token) {
// Copy the cache contents from cache files to internal buffers.
auto buffers = readFromFds(modelFds, dataFds);
// This implementation detail is important: the cache hash must be computed from internal
// buffers instead of cache files to prevent time-of-check to time-of-use (TOCTOU) attacks.
auto hash = computeHash(buffers);
// Validate the {token, hash} pair by a mapping manager that is persistent across reboots.
if (CacheManager::get()->validate(token, hash)) {
// Retrieve the prepared model from internal buffers.
return deserialize<V1_2::IPreparedModel>(buffers);
} else {
return nullptr;
}
}
Casos de uso avançados
Em alguns casos de uso avançados, um driver precisa acessar o conteúdo do cache (leitura ou gravação) após a chamada de compilação. Exemplos de casos de uso:
- Compilação just-in-time:a compilação é adiada até a primeira execução.
- Compilação em várias etapas:uma compilação rápida é realizada inicialmente, e uma compilação otimizada opcional é realizada posteriormente, dependendo da frequência de uso.
Para acessar o conteúdo do cache (leitura ou gravação) após a chamada de compilação, verifique se o driver:
- Duplica os identificadores de arquivo durante a invocação de
prepareModel_1_2
ouprepareModelFromCache
e lê/atualiza o conteúdo do cache posteriormente. - Implementa a lógica de bloqueio de arquivos fora da chamada de compilação comum para evitar que uma gravação ocorra simultaneamente com uma leitura ou outra gravação.
Implementar um mecanismo de armazenamento em cache
Além da interface de cache de compilação do NN HAL 1.2, você também encontra uma
biblioteca de utilitários de cache no diretório
frameworks/ml/nn/driver/cache
. O subdiretório
nnCache
contém código de armazenamento persistente para o driver implementar
o armazenamento em cache de compilação sem usar os recursos de armazenamento em cache da NNAPI. Essa forma de
armazenamento em cache de compilação pode ser implementada com qualquer versão da HAL de NN. Se o
driver optar por implementar o armazenamento em cache desconectado da interface HAL,
ele será
responsável por liberar os artefatos armazenados em cache quando eles não forem mais necessários.