Programmation CUDA (4) : gestion de la mémoire

Mémoire

Connaissances de base de la mémoire

De manière générale, Registres——Caches——Mémoire principale——Mémoire de disque, la vitesse diminue progressivement et la capacité augmente progressivement.
insérez la description de l'image ici
La mémoire peut être divisée en mémoire programmable et en mémoire non programmable. La mémoire programmable signifie que les utilisateurs peuvent lire et écrire dans cette mémoire, tandis que la mémoire non programmable fait référence à la mémoire qui n'est pas ouverte aux utilisateurs.règles pour accélérer le processus.

Dans la structure de la mémoire du CPU et du GPU, le cache de premier niveau et de second niveau (Cache) sont des périphériques de stockage non programmables.

Structure de la mémoire GPU

Chaque Thread a sa propre somme , resigterset local memorychaque Block en a shared memory. Tous les Threads de ce Block peuvent y accéder, et il y aura constant memory, texture memory, Global memoryet , Cacheetc. entre les Grids, et toutes les Grids pourront y accéder. Différentes mémoires ont des étendues, des durées de vie et des comportements de cache différents.

mémoire Emplacement Que ce soit pour mettre en cache droit d'accès durée de vie variable
enregistrer Ébrécher aucun lecture/écriture de l'appareil Identique au fil
mémoire locale à bord aucun lecture/écriture de l'appareil Identique au fil
La memoire partagée Ébrécher aucun lecture/écriture de l'appareil identique au bloc
mémoire constante à bord ont périphérique en lecture seule, hôte en lecture/écriture peut être maintenu dans le programme
mémoire des textures à bord ont périphérique en lecture seule, hôte en lecture/écriture peut être maintenu dans le programme
mémoire globale à bord aucun périphérique en lecture/écriture, hôte en lecture/écriture peut être maintenu dans le programme

Inscrire les inscrits

Les registres sont l'espace mémoire le plus rapide.Contrairement aux CPU, les GPU ont plus de réserves de registres. Lorsque nous déclarons une variable sans modification dans la fonction noyau, la variable est stockée dans un registre, et le tableau de longueur constante défini dans la fonction noyau se voit également attribuer une adresse dans le registre.

Les registres sont privés pour chaque thread. Les registres stockent généralement les variables privées fréquemment utilisées. Le cycle de vie des variables de registre est le même que celui des fonctions du noyau, du début à la fin de l'opération.

Les registres sont une ressource rare dans SM. Il y a jusqu'à 63 registres par thread dans l'architecture Fermi et jusqu'à 255 registres dans l'architecture Kepler. Si un thread utilise moins de registres, il y aura plus de blocs de threads résidents. Plus il y a de blocs de threads simultanés sur le SM, plus l'efficacité est élevée et plus les performances et le taux d'utilisation sont élevés. Par conséquent, il est préférable d'utiliser moins de registres lors de la programmation. Utilisez moins de registres.

S'il y a trop de variables dans un thread, de sorte que les registres ne suffisent pas du tout, et que les registres débordent à ce moment, la mémoire locale aidera à stocker les variables supplémentaires, ce qui aura un impact très négatif sur l'efficacité.

mémoire locale mémoire locale

Les variables de la fonction noyau qui sont stockées dans des registres mais qui ne peuvent pas entrer dans l'espace de registre alloué par la fonction noyau seront stockées dans l'espace local. Les variables que le compilateur peut stocker dans la mémoire locale sont les suivantes :

  • Un tableau local référencé avec un index inconnu.
  • Grands tableaux locaux ou structures qui peuvent occuper beaucoup d'espace de registre.
  • Toute variable qui ne satisfait pas la qualification du registre du noyau.

La mémoire locale se trouve essentiellement dans la même zone de stockage que la mémoire globale, et son accès se caractérise par une latence élevée et une faible bande passante.

Pour les appareils au-dessus de 2.0, la mémoire locale est stockée sur le cache L1 de chaque SM ou sur le cache L2 de l'appareil.

mémoire partagée mémoire partagée

La mémoire qui utilise les modificateurs suivants dans la fonction noyau est appelée mémoire partagée : __shared__.

Chaque SM dispose d'une certaine quantité de mémoire partagée allouée par des blocs de threads. La mémoire partagée est une mémoire sur puce. Par rapport à la mémoire principale, la vitesse est beaucoup plus rapide, c'est-à-dire que le délai est faible et la bande passante est élevée. Il est similaire au cache L1, mais peut être programmé. Lors de l'utilisation de la mémoire partagée, veillez à ne pas réduire le nombre de déformations actives sur le SM en raison d'une utilisation excessive de la mémoire partagée.

La mémoire partagée est déclarée dans la fonction du noyau, et son cycle de vie est le même que celui du bloc de thread. Lorsque le bloc de thread commence à s'exécuter, la mémoire partagée de ce bloc est allouée, et lorsque le bloc de thread finit de s'exécuter, la mémoire partagée la mémoire est libérée.

La mémoire partagée est visible pour les threads dans le bloc et n'est pas accessible par d'autres blocs de threads, il y a donc un problème de concurrence et la communication peut également être effectuée via la mémoire partagée. Afin d'éviter la concurrence mémoire, vous pouvez utiliser une instruction de synchronisation void __syncthreads();. L'instruction équivaut à un point d'obstacle de chaque thread lorsque le bloc de thread est exécuté. Lorsque tous les threads du bloc sont exécutés jusqu'à ce point d'obstacle, le calcul suivant peut être exécuté. Cependant, une utilisation fréquente affectera l'efficacité de l'exécution du noyau.

La mémoire partagée est divisée en blocs de mémoire de même taille pour un accès parallèle à grande vitesse.
banque : C'est une méthode de division. Dans le cpu, l'accès mémoire consiste à accéder à une certaine adresse pour obtenir les données sur l'adresse, mais ici, il s'agit d'accéder aux adresses du nombre de banques en une seule fois, d'obtenir toutes les données sur ces adresses, et de les mapper logiquement à différentes banques. Semblable au contrôle de lecture de la mémoire.
Afin d'obtenir un accès simultané à la mémoire à large bande passante, la mémoire partagée est divisée en blocs de mémoire de taille égale (banques) auxquels il est possible d'accéder simultanément. Par conséquent, le comportement de lecture et d'écriture de n adresses dans la mémoire peut être effectué à la manière d'un fonctionnement simultané de b banques indépendantes, de sorte que la bande passante effective est augmentée à b fois celle d'une banque.
Si les adresses mémoire demandées par plusieurs threads sont mappées sur la même banque, ces demandes deviennent sérialisées (sérialisées). Le matériel divisera ces requêtes en x séquences de requêtes sans conflits, réduisant la bande passante d'un facteur x. Mais si tous les threads d'un warp accèdent à la même adresse mémoire, une diffusion (boardcast) sera générée et ces requêtes seront complétées en une seule fois. Les appareils avec une capacité de calcul 2.0 et supérieure ont également une capacité de multidiffusion et peuvent répondre aux demandes de certains threads accédant à la même adresse mémoire dans le même warp en même temps.

mémoire constante mémoire constante

La mémoire constante réside dans la mémoire de l'appareil, chaque SM a un cache de mémoire constante dédié, une utilisation constante de la mémoire __constant__.

La mémoire constante est déclarée en dehors de la fonction du noyau et déclarée dans la portée globale. Pour tous les périphériques, seule une certaine quantité de mémoire constante peut être déclarée. La mémoire constante est déclarée statiquement et visible pour toutes les fonctions du noyau dans la même unité de compilation. La mémoire constante ne peut pas être modifiée par la fonction noyau après avoir été initialisée par l'hôte.

mémoire de texture mémoire de texture

La mémoire de texture est une sorte de mémoire en lecture seule dans le GPU. Elle est utilisée pour lier un certain segment de mémoire globale à la mémoire de texture. Ce segment de mémoire globale prend généralement la forme d'un tableau CUDA unidimensionnel/mémoire globale, bidimensionnel ou un tableau CUDA tridimensionnel, puis récupérez les données de la mémoire globale en lisant la mémoire de texture (également appelée récupération de texture). Par rapport à l'accès à la mémoire globale nécessitant un alignement et une fusion, la mémoire de texture a un bon effet d'accélération sur l'accès non aligné et l'accès aléatoire.

mémoire globale mémoire globale

La mémoire globale est une RAM matérielle indépendante du cœur du GPU, c'est-à-dire ce que nous appelons souvent la mémoire vidéo. La majeure partie de l'espace mémoire du GPU est de la mémoire globale. La mémoire globale est le plus grand espace mémoire sur le GPU, a la latence la plus élevée et utilise la mémoire la plus courante. Global fait référence à la portée et au cycle de vie. Il est généralement défini dans le code côté hôte et peut également être défini du côté de l'appareil. Cependant, des modificateurs sont nécessaires. Tant qu'il n'est pas détruit, il appartient à la même durée de vie. cycle que l'application.

cache cache

Le cache GPU est une mémoire non programmable, et il existe 4 types de caches sur le GPU :

  • Cache L1
  • Cache L2
  • cache constant en lecture seule
  • cache de texture en lecture seule

Chaque SM a un cache de premier niveau et tous les SM partagent un cache de second niveau. Les fonctions des antémémoires de premier et deuxième niveaux sont utilisées pour stocker des données dans la mémoire locale et la mémoire globale, y compris les données de dépassement de registre. Chaque SM possède un cache constant en lecture seule et un cache de texture en lecture seule, qui sont utilisés dans la mémoire de l'appareil pour améliorer les performances de lecture à partir de leurs espaces mémoire respectifs.

Différent du CPU, le processus de lecture et d'écriture du CPU peut être mis en cache, mais le processus d'écriture du GPU n'est pas mis en cache, seul le processus de lecture sera mis en cache.

Allocation, libération et transfert de mémoire GPU

Les programmes CUDA utiliseront la mémoire GPU et la mémoire CPU, l'allocation et la libération de la mémoire CPU peuvent utiliser new et delete (C++), malloc, calloc et free (C). L'allocation et la libération de la mémoire GPU sont implémentées à l'aide des fonctions de bibliothèque fournies par CUDA. En même temps, du fait que la mémoire des deux est indépendante l'une de l'autre, il est nécessaire de copier les données sur des mémoires différentes pour réaliser la transmission.

allocation des données de la mémoire

\\ 分配设备上的内存。
cudaError_t cudaMalloc(void** devPtr, size_t size)

cudaMallocCette fonction est utilisée pour allouer de la mémoire sur le périphérique et doit être appelée par l'hôte (c'est-à-dire appelée dans le code exécuté par le CPU). Sa valeur de retour cudaError_test un type énuméré qui énumère toutes les situations d'erreur possibles. Et si l'appel de fonction réussit, il renvoie cudaSuccess. Le type du premier paramètre est void **, pointant vers la première adresse de la mémoire allouée. Le type du deuxième paramètre est size_t, qui spécifie la taille de la mémoire à allouer en octets.

Libération des données de la mémoire

\\ 释放先前在设备上申请的内存空间。
cudaError_t cudaFree(void* devPtr)

cudaFreeCette fonction est utilisée pour libérer l'espace mémoire précédemment alloué sur l'appareil, mais ne peut pas libérer la mémoire allouée via malloc. Le type de retour est toujours cudaError_t. Le paramètre de fonction pointe vers la première adresse de la mémoire de l'appareil qui doit être libérée.

transfert de données en mémoire

Pour terminer la transmission synchrone des données entre la mémoire hôte et la mémoire de l'appareil , vous devez utiliser cudaMemcpyla fonction :

\\ 数据同步拷贝。
cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)

Pour terminer le transfert de données asynchrone entre la mémoire de l'hôte et la mémoire de l'appareil , vous devez utiliser cudaMemcpyAsyncla fonction :

\\ 数据异步拷贝
cudaError_t cudaMemcpyAsync(void* dst, const void* src, size_t count, cudaMemcpyKind kind, cudaStream_t stream = 0)

Si le flux est différent de zéro, il peut se chevaucher avec d'autres opérations de flux.

cudaMemcpyKindIndique le sens de transmission des données, il existe les options suivantes :

  • cudaMemcpyHostToHost
  • cudaMemcpyHostToDevice
  • cudaMemcpyDeviceToHost
  • cudaMemcpyDeviceToDevice

la gestion des erreurs

Étant donné que presque toutes les fonctions de l'API CUDA renvoient une valeur de type cudaError_t, elle est utilisée pour indiquer si l'appel de la fonction a réussi. Lorsque la valeur de retour est cudaSuccess, l'appel de la fonction a réussi. En cas d'échec, la valeur de retour marquera le code spécifique de l'échec et le programmeur peut obtenir le message d'erreur spécifique via la fonction cudaGetErrorString. Par conséquent, afin d'améliorer la robustesse du programme sans perdre la beauté du code et faciliter la correction des erreurs, il est recommandé d'utiliser GPUAssert()des fonctions macro.
Par exemple:

GPUAssert(cudaMalloc(&dev_a, sizeof(int)));

Guess you like

Origin blog.csdn.net/weixin_43603658/article/details/129912525