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 constants au pilote. Selon la durée de vie de l'opérande constante, ses valeurs sont situées soit dans un vecteur HIDL, soit dans 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 interprocessus (IPC), celui-ci n'est généralement utilisé que pour contenir une petite quantité de données telles que des opérandes scalaires (par exemple, le scalaire d'activation dans ADD ) et de petits paramètres tensoriels (par exemple, le tenseur 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 handles des pools de mémoire partagée sont dupliqués 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 poids dans les convolutions) à l'aide de pools de mémoire partagée plutôt que de 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 dans la compilation et dans 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 . Les noms de mémoire pris en charge sont :

  • ashmem : mémoire partagée Android. Pour plus de détails, voir mémoire .
  • mmap_fd : Mémoire partagée soutenue 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 sur les réseaux de neurones (NN) HAL 1.2. Pour plus de détails, 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 est uniquement pris en charge dans l'exécution du modèle. Disponible à partir de NN HAL 1.2. Pour plus de détails, consultez AHardwareBuffer .

À partir de NN HAL 1.3, NNAPI prend en charge 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 plus de détails, consultez 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 hardware_buffer et des domaines de mémoire en mode général non BLOB est facultative.

AMatérielBuffer

AHardwareBuffer est un type de mémoire partagée qui enveloppe un tampon Gralloc . Dans Android 10, l'API Neural Networks (NNAPI) prend en charge l'utilisation de AHardwareBuffer , permettant 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 de caméra peut transmettre des objets AHardwareBuffer au NNAPI pour les charges de travail d'apprentissage automatique à l'aide des descripteurs AHardwareBuffer générés par les API NDK de caméra et NDK multimédia. Pour plus d’informations, 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 représente uniquement les objets AHardwareBuffer au format AHARDWAREBUFFER_FORMAT_BLOB .

Les informations requises par le framework sont codées dans le champ hidl_handle de la structure hidl_memory . Le champ hidl_handle enveloppe native_handle , qui code toutes les métadonnées requises concernant le tampon AHardwareBuffer ou 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 pris en charge. 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 pris en charge. 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 exécutant Android 11 ou version ultérieure, NNAPI prend en charge 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 du périphérique entre les exécutions, supprimant ainsi la copie et la transformation inutiles des données entre des exécutions consécutives sur le même pilote. Ce flux est illustré dans la figure 1.

Flux de données tampon avec et sans domaines de mémoire

Figure 1. Flux de données tampon à l'aide de domaines de mémoire

La fonctionnalité de domaine mémoire est destinée aux tenseurs qui sont pour la plupart internes au pilote et ne nécessitent pas d'accès fréquent du côté client. Des exemples de tels tenseurs incluent 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ée.

Pour prendre en charge la fonctionnalité de domaine mémoire, implémentez IDevice::allocate pour permettre à l'infrastructure de demander une allocation de tampon gérée 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 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 disposition des données. Lorsque le tampon est alloué avec succès, le client du pilote peut référencer ou interagir avec le tampon à l'aide du jeton renvoyé ou de l'objet IBuffer .

Le jeton de IDevice::allocate est fourni lors du référencement du tampon comme l’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 valider que l'utilisation du tampon est l'un des rôles BufferRole fournis lors de l'allocation et doit faire échouer l'exécution immédiatement si l'utilisation est illégale.

L'objet IBuffer est utilisé pour la copie explicite de la mémoire. Dans certaines situations, 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 vers un pool de mémoire partagée. Exemples de cas d'utilisation :

  • Initialisation du tenseur d'état
  • Mise en cache des résultats intermédiaires
  • Exécution de secours sur le CPU

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 domaine mémoire. Il est facultatif que le pilote prenne en charge le mode non-BLOB hardware_buffer .

Lors de l'allocation du tampon, les dimensions du 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, le tampon peut avoir des dimensions ou un rang inconnus. Dans un tel cas, le tampon est dans un état flexible où les dimensions sont fixes lorsqu'il est utilisé comme entrée de modèle et dans un état dynamique lorsqu'il est utilisé comme sortie de modèle. Le même tampon peut être utilisé avec différentes formes de sorties dans différentes exécutions et le pilote doit gérer correctement le redimensionnement du tampon.

Le domaine 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. Par exemple:

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

Il est possible que plusieurs threads différents lisent simultanément le tampon géré par le pilote. L'accès simultané au tampon pour l'écriture ou la lecture/écriture n'est pas défini, mais il ne doit pas faire planter le service du pilote ni bloquer l'appelant indéfiniment. Le pilote peut renvoyer une erreur ou laisser le contenu du tampon dans un état indéterminé.