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érandes 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. En fonction de la durée de vie de l'opérande constante, ses valeurs se trouvent dans un vecteur HIDL ou un pool de mémoire partagée.

  • 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 se trouvent dans le champ pools de la structure du modèle. Seuls les poignées des pools de mémoire partagée sont dupliquées lors de l'IPC, plutôt que 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 de vecteurs HIDL.

Au moment de l'exécution du modèle, le framework fournit les tampons des opérateurs d'entrée et de sortie au pilote. Contrairement aux constantes au moment de la 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: mémoire partagée Android. Pour en savoir plus, consultez la section Mémoire.
  • mmap_fd: mémoire partagée prise en charge par un descripteur de fichier via mmap.
  • hardware_buffer_blob: mémoire partagée prise en charge 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 prendre en charge le mappage de hardware_buffer_blob. La prise en charge des domaines de mémoire et de hardware_buffer en mode non-BLOB général 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 de données, ce qui améliore les performances et la consommation d'énergie des applications. Par exemple, une pile HAL d'une caméra 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 sur 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 compatible. L'exécution doit échouer si le champ hidl_handle utilisé pour une operande 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 en cas d'échec de la préparation ou de l'exécution du modèle.

Domaines de mémoire

Pour les appareils exécutant 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, ce qui supprime la copie et la transformation inutiles des données entre les exécutions consécutives sur le même pilote. Ce flux est illustré dans 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. Parmi ces tenseurs, citons les tenseurs d'état dans les modèles de séquence. Pour les tenseurs 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 pour permettre au framework de demander une allocation de tampon gérée par le pilote. Lors de l'allocation, le framework fournit les propriétés et les 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é. Vous pouvez spécifier plusieurs rôles lors de l'allocation de la mémoire tampon, et la mémoire tampon allouée ne peut être utilisée que pour les rôles spécifiés.

Le tampon alloué est interne au pilote. Un pilote peut choisir n'importe quel emplacement de tampon ou mise en page de 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 en tant qu'un des objets MemoryPool dans 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 une validation appropriée à chaque utilisation du tampon. Le pilote doit vérifier que l'utilisation de la mémoire tampon correspond à l'un des rôles BufferRole fournis lors de l'allocation et doit échouer immédiatement 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 copier le tampon dans un pool de mémoire partagée. Voici quelques exemples de cas d'utilisation:

  • Initialisation du Tensor d'état
  • Mettre en cache les 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 prend en charge l'allocation de domaines de mémoire. Le pilote peut être compatible avec le mode non-BLOB hardware_buffer.

Lors de l'allocation de la mémoire tampon, les dimensions de la mémoire tampon 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, la mémoire 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'elles sont utilisées comme entrée de modèle et dans un état dynamique lorsqu'elles sont utilisées comme sortie de 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 demande d'allocation donnée pour plusieurs raisons. Exemple :

  • La mémoire tampon demandée a une taille dynamique.
  • Le pilote présente des contraintes de mémoire qui l'empêchent de gérer de grands tampons.

Il est possible que plusieurs threads différents lisent simultanément à partir du 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é.