À partir d'Android 10, l'API Neural Networks (NNAPI) fournit des fonctions permettant la mise en cache des artefacts de compilation, ce qui réduit le temps de compilation au démarrage d'une application. Grâce à cette fonctionnalité de mise en cache, le pilote n'a pas besoin de gérer ni de nettoyer les fichiers mis en cache. Il s'agit d'une fonctionnalité facultative qui peut être implémentée avec NN HAL 1.2. Pour en savoir plus sur cette fonction, consultez ANeuralNetworksCompilation_setCaching
.
Le pilote peut également implémenter le cache de compilation indépendamment de la NNAPI. Cela peut être mis en œuvre que les fonctionnalités de mise en cache du NDK NNAPI et du HAL soient utilisées ou non. AOSP fournit une bibliothèque d'utilitaires de bas niveau (un moteur de mise en cache). Pour en savoir plus, consultez la section Implémenter un moteur de mise en cache.
Présentation du workflow
Cette section décrit les workflows généraux avec la fonctionnalité de mise en cache de compilation implémentée.
Informations sur le cache fournies et succès de cache
- L'application transmet un répertoire de mise en cache et une somme de contrôle propre au modèle.
- L'environnement d'exécution NNAPI recherche les fichiers de cache en fonction de la somme de contrôle, des préférences d'exécution et du résultat du partitionnement, puis trouve les fichiers.
- NNAPI ouvre les fichiers de cache et transmet les poignées au pilote avec
prepareModelFromCache
. - Le pilote prépare le modèle directement à partir des fichiers de cache et renvoie le modèle préparé.
Informations sur le cache fournies et défaut de cache
- L'application transmet une somme de contrôle propre au modèle et un répertoire de mise en cache.
- L'environnement d'exécution NNAPI recherche les fichiers de mise en cache en fonction de la somme de contrôle, des préférences d'exécution et du résultat du partitionnement, et ne trouve pas les fichiers de cache.
- La NNAPI crée des fichiers de cache vides en fonction de la somme de contrôle, de la préférence d'exécution et du partitionnement, ouvre les fichiers de cache et transmet les poignées et le modèle au pilote avec
prepareModel_1_2
. - Le pilote compile le modèle, écrit des informations de mise en cache dans les fichiers de cache et renvoie le modèle préparé.
Informations sur le cache non fournies
- L'application appelle la compilation sans fournir d'informations de mise en cache.
- L'application ne transmet rien concernant la mise en cache.
- L'environnement d'exécution NNAPI transmet le modèle au pilote avec
prepareModel_1_2
. - Le pilote compile le modèle et renvoie le modèle préparé.
Informations sur le cache
Les informations de mise en cache fournies à un pilote se composent d'un jeton et de poignées de fichiers de cache.
Jeton
Le jeton est un jeton de mise en cache de longueur Constant::BYTE_SIZE_OF_CACHE_TOKEN
qui identifie le modèle préparé. Le même jeton est fourni lors de l'enregistrement des fichiers de cache avec prepareModel_1_2
et de la récupération du modèle préparé avec prepareModelFromCache
. Le client du pilote doit choisir un jeton avec un faible taux de collision. Le pilote ne peut pas détecter de collision de jetons. Une collision entraîne l'échec de l'exécution ou une exécution réussie qui génère des valeurs de sortie incorrectes.
Poignées de fichiers de cache (deux types de fichiers de cache)
Les deux types de fichiers de cache sont le cache de données et le cache de modèle.
- Cache de données:permet de mettre en cache des données constantes, y compris les tampons de Tensor prétraités et transformés. Une modification du cache de données ne devrait avoir aucun effet pire que la génération de valeurs de sortie incorrectes au moment de l'exécution.
- Cache de modèle : utilisez-le pour mettre en cache des données sensibles à la sécurité telles que le code machine exécutable compilé au format binaire natif de l'appareil. Une modification du cache du modèle peut affecter le comportement d'exécution du pilote, et un client malveillant peut s'en servir pour exécuter des actions au-delà de l'autorisation accordée. Le pilote doit donc vérifier si le cache du modèle est corrompu avant de le préparer à partir du cache. Pour en savoir plus, consultez la section Sécurité.
Le pilote doit décider de la répartition des informations de cache entre les deux types de fichiers de cache et indiquer le nombre de fichiers de cache nécessaires pour chaque type à l'aide de getNumberOfCacheFilesNeeded
.
L'environnement d'exécution NNAPI ouvre toujours les poignées de fichiers de cache avec les autorisations de lecture et d'écriture.
Sécurité
Lors de la mise en cache de la compilation, le cache du modèle peut contenir des données sensibles, telles que du code machine exécutable compilé au format binaire natif de l'appareil. Si elle n'est pas correctement protégée, une modification du cache du modèle peut affecter le comportement d'exécution du pilote. Étant donné que le contenu du cache est stocké dans le répertoire de l'application, le client peut modifier les fichiers de cache. Un client buggé peut corrompre accidentellement le cache, et un client malveillant peut en profiter intentionnellement pour exécuter du code non validé sur l'appareil. Selon les caractéristiques de l'appareil, cela peut constituer un problème de sécurité. Par conséquent, le pilote doit pouvoir détecter une corruption potentielle du cache du modèle avant de préparer le modèle à partir du cache.
Pour ce faire, le pilote doit conserver une carte entre le jeton et un hachage cryptographique du cache du modèle. Le pilote peut stocker le jeton et le hachage de son cache de modèle lors de l'enregistrement de la compilation dans le cache. Le pilote vérifie le nouveau hachage du cache du modèle avec la paire de jetons et de hachage enregistrés lors de la récupération de la compilation à partir du cache. Ce mappage doit être persistant lors des redémarrages du système. Le pilote peut utiliser le service Keystore Android, la bibliothèque d'utilitaires dans framework/ml/nn/driver/cache
ou tout autre mécanisme approprié pour implémenter un gestionnaire de mappage. Lors de la mise à jour du pilote, ce gestionnaire de mappage doit être réinitialisé pour éviter la préparation de fichiers de cache à partir d'une version antérieure.
Pour éviter les attaques par temps de vérification/heure d'utilisation (TOCTOU), le pilote doit calculer le hachage enregistré avant de l'enregistrer dans le fichier, puis calculer le nouveau hachage après avoir copié le contenu du fichier dans un tampon interne.
Cet exemple de code montre comment implémenter cette logique.
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;
}
}
Cas d'utilisation avancés
Dans certains cas d'utilisation avancés, un pilote nécessite un accès au contenu du cache (lecture ou écriture) après l'appel de compilation. Voici quelques exemples de cas d'utilisation:
- Compilation juste à temps:la compilation est retardée jusqu'à la première exécution.
- Compilation à plusieurs étapes:une compilation rapide est initialement effectuée, et une compilation optimisée facultative est effectuée ultérieurement en fonction de la fréquence d'utilisation.
Pour accéder au contenu du cache (lecture ou écriture) après l'appel de compilation, assurez-vous que le pilote :
- Duplique les poignées de fichier lors de l'appel de
prepareModel_1_2
ouprepareModelFromCache
, puis lit/met à jour le contenu du cache ultérieurement. - Implémente la logique de verrouillage de fichier en dehors de l'appel de compilation ordinaire pour éviter qu'une écriture ne se produise simultanément avec une lecture ou une autre écriture.
Implémenter un moteur de mise en cache
En plus de l'interface de mise en cache de la compilation NN HAL 1.2, vous trouverez également une bibliothèque d'utilitaires de mise en cache dans le répertoire frameworks/ml/nn/driver/cache
. Le sous-répertoire nnCache
contient du code de stockage persistant permettant au pilote d'implémenter la mise en cache de la compilation sans utiliser les fonctionnalités de mise en cache NNAPI. Cette forme de mise en cache de compilation peut être implémentée avec n'importe quelle version du HAL NN. Si le pilote choisit d'implémenter la mise en cache déconnecté de l'interface HAL, il est tenu de libérer les artefacts mis en cache lorsqu'ils ne sont plus nécessaires.