Pools de mémoire

Cette page décrit les structures de données et les méthodes utilisées pour communiquer efficacement les tampons d'opérande entre le pilote et le framework.

Au moment de la compilation du modèle, le framework fournit les valeurs des opérandes constantes au pilote. Selon la durée de vie de l'opérande constante, ses valeurs sont situées dans un vecteur HIDL ou un pool de mémoire partagé.

  • Si la durée de vie est CONSTANT_COPY, les valeurs sont situées dans le champ operandValues de la structure du modèle. Étant donné que les valeurs du vecteur HIDL sont copiées lors de la communication inter-processus (IPC), celui-ci n'est généralement utilisé que pour contenir une petite quantité de données, telles que les opérandes scalaires (par exemple, le scalaire d'activation dans ADD) et les petits paramètres de Tensor (par exemple, le Tensor de forme dans RESHAPE).
  • Si la durée de vie est CONSTANT_REFERENCE, les valeurs sont situées dans le champ pools de la structure du modèle. Seuls les identifiants des pools de mémoire partagée sont dupliqués lors de l'IPC au lieu de copier les valeurs brutes. Par conséquent, il est plus efficace de conserver une grande quantité de données (par exemple, les paramètres de pondération dans les convolutions) à l'aide de pools de mémoire partagés que les vecteurs HIDL.

Au moment de l'exécution du modèle, le framework fournit les tampons des opérandes d'entrée et de sortie au pilote. Contrairement aux constantes de compilation qui peuvent être envoyées dans un vecteur HIDL, les données d'entrée et de sortie d'une exécution sont toujours communiquées via un ensemble de pools de mémoire.

Le type de données HIDL hidl_memory est utilisé à la fois lors de la compilation et de l'exécution pour représenter un pool de mémoire partagée non mappé. Le pilote doit mapper la mémoire en conséquence pour la rendre utilisable en fonction du nom du type de données hidl_memory. Voici les noms de mémoire acceptés:

  • ashmem: souvenir partagé Android Pour en savoir plus, consultez la section memory.
  • mmap_fd: mémoire partagée sauvegardée par un descripteur de fichier via mmap.
  • hardware_buffer_blob: mémoire partagée sauvegardée par un AHardwareBuffer au format AHARDWARE_BUFFER_FORMAT_BLOB. Disponible dans les réseaux de neurones (NN) HAL 1.2. Pour en savoir plus, consultez AHardwareBuffer.
  • hardware_buffer: mémoire partagée sauvegardée par un AHardwareBuffer général qui n'utilise pas le format AHARDWARE_BUFFER_FORMAT_BLOB. Le tampon matériel en mode non BLOB n'est compatible qu'avec l'exécution du modèle.Disponible à partir de NN HAL 1.2. Pour en savoir plus, consultez AHardwareBuffer.

À partir de NN HAL 1.3, NNAPI accepte les domaines de mémoire qui fournissent des interfaces d'allocation pour les tampons gérés par le pilote. Les tampons gérés par le pilote peuvent également être utilisés comme entrées ou sorties d'exécution. Pour en savoir plus, consultez la section Domaines de mémoire.

Les pilotes NNAPI doivent prendre en charge le mappage des noms de mémoire ashmem et mmap_fd. À partir de NN HAL 1.3, les pilotes doivent également être compatibles avec le mappage de hardware_buffer_blob. La prise en charge du mode général non-BLOB hardware_buffer et des domaines de mémoire est facultative.

AHardwareBuffer

AHardwareBuffer est un type de mémoire partagée qui encapsule un tampon Gralloc. Dans Android 10, l'API Neural Networks (NNAPI) prend en charge l'utilisation de AHardwareBuffer, ce qui permet au pilote d'effectuer des exécutions sans copier les données, ce qui améliore les performances et la consommation d'énergie des applications. Par exemple, une pile HAL d'appareil photo peut transmettre des objets AHardwareBuffer à NNAPI pour les charges de travail de machine learning à l'aide de poignées AHardwareBuffer générées par les API NDK d'appareil photo et Media NDK. Pour en savoir plus, consultez ANeuralNetworksMemory_createFromAHardwareBuffer.

Les objets AHardwareBuffer utilisés dans NNAPI sont transmis au pilote via une structure hidl_memory nommée hardware_buffer ou hardware_buffer_blob. La structure hidl_memory hardware_buffer_blob ne représente que les objets AHardwareBuffer au format AHARDWAREBUFFER_FORMAT_BLOB.

Les informations requises par le framework sont encodées dans le champ hidl_handle de la structure hidl_memory. Le champ hidl_handle encapsule native_handle, qui encode toutes les métadonnées requises concernant le tampon AHardwareBuffer ou le tampon Gralloc.

Le pilote doit décoder correctement le champ hidl_handle fourni et accéder à la mémoire décrite par hidl_handle. Lorsque la méthode getSupportedOperations_1_2, getSupportedOperations_1_1 ou getSupportedOperations est appelée, le pilote doit détecter s'il peut décoder le hidl_handle fourni et accéder à la mémoire décrite par hidl_handle. La préparation du modèle doit échouer si le champ hidl_handle utilisé pour un opérande constant n'est pas accepté. L'exécution doit échouer si le champ hidl_handle utilisé pour un opérande d'entrée ou de sortie de l'exécution n'est pas compatible. Il est recommandé que le pilote renvoie un code d'erreur GENERAL_FAILURE si la préparation ou l'exécution du modèle échoue.

Domaines de mémoire

Pour les appareils équipés d'Android 11 ou version ultérieure, NNAPI est compatible avec les domaines de mémoire qui fournissent des interfaces d'allocation pour les tampons gérés par le pilote. Cela permet de transmettre les mémoires natives de l'appareil lors des exécutions, et d'éviter la copie et la transformation de données inutiles entre les exécutions consécutives d'un même pilote. Ce flux est illustré à la figure 1.

Mettre en mémoire tampon le flux de données avec et sans domaines de mémoire

Figure 1 : Mettre en mémoire tampon le flux de données à l'aide de domaines de mémoire

La fonctionnalité de domaine de mémoire est destinée aux Tensors principalement internes au pilote et qui n'ont pas besoin d'un accès fréquent côté client. Les Tensors d'état des modèles de séquence en sont des exemples. Pour les Tensors qui nécessitent un accès fréquent au processeur côté client, il est préférable d'utiliser des pools de mémoire partagés.

Pour prendre en charge la fonctionnalité de domaine de mémoire, implémentez IDevice::allocate afin de permettre au framework de demander l'allocation de tampon géré par le pilote. Lors de l'allocation, le framework fournit les propriétés et modèles d'utilisation suivants pour le tampon:

  • BufferDesc décrit les propriétés requises du tampon.
  • BufferRole décrit le modèle d'utilisation potentiel du tampon en tant qu'entrée ou sortie d'un modèle préparé. Plusieurs rôles peuvent être spécifiés lors de l'allocation de tampon, et le tampon alloué ne peut être utilisé que comme ces rôles spécifiés.

Le tampon alloué est interne au pilote. Un conducteur peut choisir n'importe quel emplacement de tampon ou mise en page des données. Une fois le tampon alloué, le client du pilote peut le référencer ou interagir avec lui à l'aide du jeton renvoyé ou de l'objet IBuffer.

Le jeton de IDevice::allocate est fourni lorsque vous référencez le tampon comme l'un des objets MemoryPool de la structure Request d'une exécution. Pour empêcher un processus d'essayer d'accéder au tampon alloué dans un autre processus, le pilote doit appliquer la validation appropriée à chaque utilisation du tampon. Le pilote doit vérifier que l'utilisation du tampon est l'un des rôles BufferRole fournis lors de l'allocation et doit échouer immédiatement à l'exécution si l'utilisation est illégale.

L'objet IBuffer est utilisé pour la copie explicite de la mémoire. Dans certains cas, le client du pilote doit initialiser le tampon géré par le pilote à partir d'un pool de mémoire partagée ou le copier dans un pool de mémoire partagée. Voici quelques exemples de cas d'utilisation:

  • Initialisation du Tensor d'état
  • Mettre en cache des résultats intermédiaires
  • Exécution de remplacement sur le processeur

Pour prendre en charge ces cas d'utilisation, le pilote doit implémenter IBuffer::copyTo et IBuffer::copyFrom avec ashmem, mmap_fd et hardware_buffer_blob s'il est compatible avec l'allocation de domaine de mémoire. Le pilote n'est pas compatible avec le mode non-BLOB hardware_buffer.

Lors de l'allocation de tampon, ses dimensions peuvent être déduites des opérandes de modèle correspondants de tous les rôles spécifiés par BufferRole et des dimensions fournies dans BufferDesc. Avec toutes les informations dimensionnelles combinées, le tampon peut avoir des dimensions ou un rang inconnus. Dans ce cas, le tampon est dans un état flexible, où les dimensions sont fixes lorsqu'il est utilisé en tant qu'entrée du modèle et dans un état dynamique lorsqu'il est utilisé comme sortie du modèle. Le même tampon peut être utilisé avec différentes formes de sorties dans différentes exécutions. Le pilote doit gérer correctement le redimensionnement du tampon.

Le domaine de mémoire est une fonctionnalité facultative. Un pilote peut déterminer qu'il ne peut pas prendre en charge une requête d'allocation donnée pour plusieurs raisons. Par exemple :

  • La taille du tampon demandé est dynamique.
  • Le pilote présente des contraintes de mémoire qui l'empêchent de gérer des tampons volumineux.

Plusieurs threads peuvent lire simultanément le tampon géré par le pilote. L'accès simultané au tampon en écriture ou en lecture/écriture n'est pas défini, mais il ne doit pas faire planter le service de pilote ni bloquer l'appelant indéfiniment. Le pilote peut renvoyer une erreur ou laisser le contenu du tampon dans un état indéterminé.