Mise en cache des compilations

À partir d'Android 10, l'API Neural Networks (NNAPI) fournit des fonctions pour prendre en charge la mise en cache des artefacts de compilation, ce qui réduit le temps nécessaire à la compilation au démarrage d'une application. Grâce à cette fonctionnalité de mise en cache, le pilote n'a pas besoin de gérer ou 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 plus d’informations sur cette fonction, consultez ANeuralNetworksCompilation_setCaching .

Le pilote peut également implémenter une mise en cache de compilation indépendante du NNAPI. Cela peut être implémenté que les fonctionnalités de mise en cache NNAPI NDK et HAL soient utilisées ou non. AOSP fournit une bibliothèque d'utilitaires de bas niveau (un moteur de mise en cache). Pour plus d'informations, consultez Implémentation d'un moteur de mise en cache .

Présentation du flux de travail

Cette section décrit les flux de travail généraux avec la fonctionnalité de mise en cache de compilation implémentée.

Informations sur le cache fournies et accès au cache

  1. L'application transmet un répertoire de mise en cache et une somme de contrôle unique au modèle.
  2. Le runtime NNAPI recherche les fichiers de cache en fonction de la somme de contrôle, de la préférence d'exécution et du résultat du partitionnement et trouve les fichiers.
  3. Le NNAPI ouvre les fichiers de cache et transmet les handles au pilote avec prepareModelFromCache .
  4. Le pilote prépare le modèle directement à partir des fichiers cache et renvoie le modèle préparé.

Informations de cache fournies et échec du cache

  1. L'application transmet une somme de contrôle unique au modèle et à un répertoire de mise en cache.
  2. Le runtime NNAPI recherche les fichiers de mise en cache en fonction de la somme de contrôle, de la préférence d'exécution et du résultat du partitionnement et ne trouve pas les fichiers de cache.
  3. Le 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 handles et le modèle au pilote avec prepareModel_1_2 .
  4. Le pilote compile le modèle, écrit les informations de mise en cache dans les fichiers cache et renvoie le modèle préparé.

Informations sur le cache non fournies

  1. L'application appelle la compilation sans fournir aucune information de mise en cache.
  2. L'application ne transmet rien lié à la mise en cache.
  3. Le runtime NNAPI transmet le modèle au pilote avec prepareModel_1_2 .
  4. 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 descripteurs de fichier 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 conducteur doit choisir un jeton avec un faible taux de collision. Le conducteur ne peut pas détecter une collision symbolique. Une collision entraîne un échec d'exécution ou une exécution réussie qui produit des valeurs de sortie incorrectes.

Descripteurs de fichiers cache (deux types de fichiers cache)

Les deux types de fichiers cache sont le cache de données et le cache de modèles .

  • Cache de données : utilisé pour mettre en cache des données constantes, y compris des tampons tenseurs prétraités et transformés. Une modification du cache de données ne devrait pas avoir d'effet pire que la génération de mauvaises valeurs de sortie au moment de l'exécution.
  • Cache de modèle : utilisé pour mettre en cache les données sensibles en matière de sécurité, telles que le code machine exécutable compilé au format binaire natif de l'appareil. Une modification du cache de modèle peut affecter le comportement d'exécution du pilote, et un client malveillant pourrait en profiter pour s'exécuter au-delà de l'autorisation accordée. Ainsi, le pilote doit vérifier si le cache du modèle est corrompu avant de préparer le modèle à partir du cache. Pour plus d'informations, voir Sécurité .

Le pilote doit décider de la manière dont les informations de cache sont réparties entre les deux types de fichiers de cache et indiquer le nombre de fichiers de cache dont il a besoin pour chaque type avec getNumberOfCacheFilesNeeded .

Le runtime NNAPI ouvre toujours les descripteurs de fichiers cache avec des autorisations de lecture et d’écriture.

Sécurité

Dans la mise en cache de compilation, le cache de modèle peut contenir des données sensibles en matière de sécurité, telles que du code machine exécutable compilé au format binaire natif du périphérique. Si elle n'est pas correctement protégée, une modification du cache de 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, les fichiers cache sont modifiables par le client. Un client bogué peut accidentellement corrompre le cache, et un client malveillant pourrait intentionnellement l'utiliser pour exécuter du code non vérifié sur l'appareil. Selon les caractéristiques de l'appareil, cela peut poser un problème de sécurité. Ainsi, le pilote doit être capable de détecter une corruption potentielle du cache du modèle avant de préparer le modèle à partir du cache.

Une façon de procéder consiste pour le pilote à conserver une carte du jeton vers un hachage cryptographique du cache de 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 de modèle avec la paire de jeton et de hachage enregistrée lors de la récupération de la compilation du cache. Ce mappage doit être persistant lors des redémarrages du système. Le pilote peut utiliser le service de magasin de clés 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 de préparer des fichiers de cache à partir d'une version antérieure.

Pour éviter les attaques TOCTOU ( time-of-check to time-of-use ), le pilote doit calculer le hachage enregistré avant de l'enregistrer dans un fichier et 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. Exemples de cas d'utilisation :

  • Compilation juste à temps : la compilation est retardée jusqu'à la première exécution.
  • Compilation en plusieurs étapes : une compilation rapide est effectuée dans un premier temps 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 descripteurs de fichiers lors de l'invocation de prepareModel_1_2 ou prepareModelFromCache et lit/met à jour le contenu du cache ultérieurement.
  • Implémente une logique de verrouillage de fichier en dehors de l'appel de compilation ordinaire pour empêcher une écriture de se produire 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 compilation NN HAL 1.2, vous pouvez également trouver 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 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 de NN HAL. Si le pilote choisit d'implémenter la mise en cache déconnectée de l'interface HAL, il est responsable de libérer les artefacts mis en cache lorsqu'ils ne sont plus nécessaires.