Notes d'étude CUDA (1)
Tutoriels de référence :
1. QINZHAOYU/CudaSteps
2. Programmation CUDA (1) Notions de base
3. Tutoriel CUDA C/C++ 1 : Accélérer les applications
1. Matériel GPU et outils de développement de programmes CUDA
Dans une plate-forme informatique hétérogène composée de CPU et de GPU, le CPU qui joue un rôle de contrôle est généralement appelé 主机(host)
,
et le GPU qui joue un rôle d'accélération est appelé 设备(device)
.
L'hôte et le périphérique ont tous deux leur propre DRAM, qui sont généralement connectés par un bus PCIe.
La puissance de calcul GPU n'est pas équivalente aux performances de calcul ; un paramètre important pour caractériser les performances de calcul est la valeur maximale des opérations en virgule flottante (FLOPS).
Il existe des points à simple précision et à double précision pour les pics arithmétiques à virgule flottante. Pour les GPU de la série Tesla, le FLOPS en double précision correspond généralement à 1/2 de la simple précision ; pour les GPU de la série GeForce, le FLOPS en double précision correspond généralement à 1/32 de la simple précision.
CUDA fournit deux couches d'API, à savoir CUDA 驱动API
et CUDA 运行时API
.
2. Organisation des threads dans CUDA
1. nvcc compile le code C++
nvcc -o ./bin/hello_cu.exe hello.cu
./bin/hello_cu.exe
nvcc: hello world!
2. Utilisation des fonctions du noyau dans les programmes CUDA
Un programme CUDA qui utilise le GPU a à la fois un code hôte et un code de périphérique.
L'appel de l'hôte à l'appareil est 核函数
réalisé via (fonction noyau).
int main()
{
主机代码
核函数的调用
主机代码
return 0;
}
3. Caractéristiques de la fonction noyau :
1) La limite globale doit être ajoutée ;
2) Le type de retour doit être de type void void
3) La fonction noyau ne prend pas en charge l'iostream de C++
4) La méthode d'appel de la fonction noyau :
hello_from_gpu<<<1, 1>>> # <<<grid size, block size>>>
Lorsque l'hôte appelle une fonction du noyau, il doit indiquer le nombre de threads affectés dans le périphérique. Les threads de la fonction noyau sont souvent organisés en plusieurs blocs de threads :
A. Le premier nombre entre les triples crochets est 线程块的个数
(nombre de blocs de threads) ;
B. Le deuxième nombre entre les triples crochets est 每个线程块中的线程数
(nombre de threads par bloc).
Tous les blocs de threads d'une fonction noyau forment une grille (grid), et le nombre de blocs de threads est appelé la taille de la grille (taille de la grille); chaque bloc de threads contient le même nombre de threads, et ce nombre est appelé la taille du bloc de threads ( taille du bloc).taille).
Par conséquent, le nombre total de threads de la fonction noyau est la taille de la grille * la taille du bloc de threads.
Après avoir appelé la fonction du noyau, appelez la fonction de l'API d'exécution CUDA pour synchroniser l'hôte et le périphérique. Fonction : attendez que le code du périphérique (GPU) ait fini de s'exécuter, puis continuez à s'exécuter sur le processeur.
cudaDeviceSynchronize(); // 与其他并行化的代码类似,核函数启动方式为异步,即 CPU 代码将继续执行而不会等待核函数执行完成;但此行代码:可以让Host 代码(CPU) 等待 Device 代码(GPU) 执行完毕,再在CPU上继续执行。
4. Organisation des threads de CUDA
4.1 Organisation des threads de CUDA
Le nombre total de threads de la fonction noyau doit être au moins égal au nombre de cœurs de calcul (l'ordinateur actuel a 16 cœurs) avant qu'il soit possible d'utiliser pleinement toutes les ressources de calcul du GPU.
hello_from_gpu<<<2, 4>>>
La taille de la grille est de 2, la taille du bloc de threads est de 4 et le nombre total de threads est de 8. Le mode d'exécution du code dans la fonction noyau est “单指令-多线程”
que chaque thread exécute la même chaîne de codes.
À partir de l'architecture Kepler, la taille de grille maximale autorisée est de 2 ^ 31 - 1 (grille unidimensionnelle) et la taille de bloc de thread maximale autorisée est de 2 ^ 10 (1024).
Le nombre total de threads peut être déterminé par deux paramètres :
- gridDim.x, la taille de la grille ;
- blockDim.x, la taille du bloc de thread ;
L'identité de chaque thread peut être déterminée par deux paramètres :
- blockIdx.x, c'est-à-dire l'index de bloc de thread d'un thread dans une grille, [0, gridDm.x);
- threadIdx.x, c'est-à-dire l'index de thread d'un thread dans un bloc de thread, [0, blockDim.x);
La grille et le bloc de filetage peuvent être étendus à une structure tridimensionnelle (chaque axe par défaut est 1) :
- Grille 3D grid_size(gridDim.x, gridDim.y, gridDim.z);
- Bloc de filetage tridimensionnel block_size(blockDim.x, blockDim.y, blockDim.z);
De même, sous la structure tridimensionnelle, les paramètres d'identité de chaque fil :
- ID de bloc de thread (blockIdx.x, blockIdx.y, blockIdx.z) ;
- ID de thread (threadIdx.x, threadIdx.y, threadIdx.z) ;
L'ID du fil de grille multidimensionnel sur le bloc de fil :
tid = threadIdx.z * (blockDim.x * blockDim.y) // 当前线程块上前面的所有线程数
+ threadIdx.y * (blockDim.x) // 当前线程块上当前面上前面行的所有线程数
+ threadIdx.x // 当前线程块上当前面上当前行的线程数
ID du bloc de thread de grille multidimensionnelle sur la grille :
bid = blockIdx.z * (gridDim.x * gridDim.y)
+ blockIdx.y * (gridDim.x)
+ blockIdx.x
Les fils d'un bloc de fils peuvent également être organisés en différentes chaînes de fils, chacune composée de 32 fils consécutifs.
Pour les GPU de l'architecture Kepler à l'architecture Turing, la valeur maximale autorisée de la taille de grille dans les directions x, y, z(2^31 - 1, 2^16 - 1, 2^16 -1)
; la valeur maximale autorisée de la taille de bloc de thread dans les directions x, y, z (1024, 1024, 64)
, tout en nécessitant un thread block Il y a au plus 1024
des threads.
Expansion :
<<<>>> opérateur complétez l'exécution de la forme du paramètre de configuration de la fonction du noyau est<<<Dg, Db, Ns, S>>>
1) Le paramètre Dg est utilisé pour définir la dimension et la taille de l'ensemble de la grille, c'est-à-dire le nombre de blocs d'une grille. Il est de type dim3. Dim3 Dg(Dg.x, Dg.y, 1) signifie que chaque ligne de la grille a des blocs Dg.x, chaque colonne a des blocs Dg.y et la troisième dimension est toujours 1 (actuellement, une fonction noyau n'a qu'un seul grille). Il y a des blocs Dg.x Dg.y dans toute la grille, et la valeur maximale de Dg.x et Dg.y est 65535.
2) Le paramètre Db est utilisé pour définir la dimension et la taille d'un bloc, c'est-à-dire le nombre de fils d'un bloc. Il est de type dim3. Dim3 Db(Db.x, Db.y, Db.z) signifie qu'il y a des threads Db.x dans chaque ligne du bloc entier, des threads Db.y dans chaque colonne et la hauteur est Db.z. La valeur maximale de Db.x et Db.y est de 512 et la valeur maximale de Db.z est de 62. Il y a des threads Db.x Db.y*Db.z dans un bloc . La valeur maximale de ce produit est de 768 pour le matériel avec capacité de calcul 1.0 et 1.1, et la valeur maximale prise en charge par le matériel avec capacité de calcul 1.2 et 1.3 est 1024.
3) Le paramètre Ns est un paramètre facultatif, qui est utilisé pour définir la valeur maximale de chaque bloc à l'exception de la mémoire partagée allouée statiquement, et 能动态分配的shared memory大小
l'unité est l'octet. Lorsque l'allocation dynamique n'est pas requise, la valeur est 0 ou omise.
4) Le paramètre S est un cudaStream_t类型
paramètre facultatif avec une valeur initiale de zéro, indiquant dans quel flux se trouve la fonction noyau.
4.2 Fichiers d'en-tête CUDA
Le suffixe des fichiers d'en-tête CUDA est généralement ".cuh" ;
en même temps, les fichiers d'en-tête c/cpp ".h" et ".hpp" peuvent être inclus, et le compilateur nvcc inclura automatiquement les fichiers d'en-tête cuda nécessaires,
tels que comme <cuda.h >, <cuda_runtime.h>, tandis que le premier inclut également le fichier d'en-tête c++ <stdlib.h>.
4.3 Utiliser nvcc pour compiler le programme CUDA
nvcc séparera d'abord tous les codes source en 主机代码
et 设备代码
;
le code hôte prend entièrement en charge la syntaxe c++, tandis que le code du périphérique ne la prend que partiellement en charge.
Processus de compilation :
nvcc compilera d'abord le code de l'appareil en code de pseudo-assemblage PTX (exécution de thread parallèle), puis le compilera en code objet binaire cubin.
Lors de la compilation en code PTX, l'option est requise -arch=compute_XY
pour spécifier la capacité de calcul d'une architecture virtuelle ; lors de la compilation en code cubin, l'option est requise pour -code=sm_ZW
spécifier la capacité de calcul d'une architecture réelle afin de déterminer le GPU que le fichier exécutable peut utiliser.
La puissance de calcul de l'architecture réelle doit être supérieure ou égale à la puissance de calcul de l'architecture virtuelle, par exemple :
-arch=compute_35 -code=sm_60 (right)
-arch=compute_60 -code=sm_35 (wrong)
4.4 Architecture de la carte graphique et puissance de calcul
Les modules Jetson Orin incluent les éléments suivants :
GPU NVIDIA Ampere Architecture avec jusqu'à 2048 cœurs CUDA et jusqu'à 64 cœurs Tensor
Ampère (CUDA 11 ~présent) SM80 ou SM_80, compute_80 - NVIDIA A100 (n'est plus nommé Tesla - GA100), NVIDIA DGX-A100 SM86
ou SM_86, compute_86 - (à partir de CUDA 11.1) Tesla GA10x, RTX Ampere - RTX 3080 , GA102 – RTX 3090, RTX A6000, RTX A40
3. Le cadre de base d'un programme CUDA simple
1. Pour un programme cuda avec un seul fichier source, le cadre de base est :
包含头文件
定义常量或宏
声明 c++ 自定义函数和 cuda 核函数的原型
int main()
{
1. 分配主机和设备内存
2. 初始化主机中数据
3. 将某些数据从主机复制到设备
4. 调用核函数在设备中计算
5. 将某些数据从设备复制到主机
6. 释放主机和设备内存
}
2. Configuration requise pour les fonctions du noyau CUDA :
1) Le type de retour doit être vide, mais return peut être utilisé dans la fonction (mais ne peut pas renvoyer de valeur) ; 2) Le qualificateur glolbal
doit être utilisé et le qualificateur c++ peut également être ajouté ; 3) La fonction principale prend en charge le mécanisme de surcharge de C++ ; 4) La fonction noyau ne prend pas en charge un nombre variable de listes de paramètres, c'est-à-dire que le nombre de paramètres doit être déterminé ; 5) En général, le tableau (pointeur) passé à la fonction noyau doit pointer vers le mémoire de l'appareil (à l'exception du "mécanisme de programmation de la mémoire unifiée"); 6 ) (se terminant généralement par核函数不可成为一个类的成员
La fonction wrapper appelle la fonction noyau, définissez la fonction wrapper en tant que membre de classe) ;
7) Avant la capacité de calcul 3.5, les fonctions du noyau ne peuvent pas s'appeler ; après cela, elles peuvent être appelées via le mécanisme "parallèle dynamique" ;
8) Qu'elles soient appelées depuis l'hôte ou depuis l'équipement, les fonctions du noyau sont exécutées dans l'équipement ("<<<,>>>" précise la configuration d'exécution).
3. Fonction d'appareil personnalisée
Une fonction du noyau peut appeler une fonction personnalisée sans configuration d'exécution, c'est-à-dire une fonction de périphérique.
La fonction de périphérique est exécutée dans le périphérique et appelée dans le périphérique (gpu), tandis que la fonction du noyau est exécutée dans le périphérique et appelée dans l'hôte (cpu).
Règles de grammaire :
1) __global__
Les fonctions modifiées sont appelées fonctions du noyau, qui sont généralement appelées par l'hôte et exécutées dans le périphérique ;
2) __device__
Les fonctions modifiées sont appelées fonctions du périphérique, qui ne peuvent être appelées que par des fonctions du noyau ou d'autres fonctions du périphérique et exécutées dans le périphérique. périphérique ;
3 ) __host__
pour modifier la fonction c++ ordinaire du segment hôte, qui est appelée et exécutée dans l'hôte, et peut généralement être omise ; 4 )
et peut être utilisée pour modifier la fonction en même temps __host__
, __device__
réduisant ainsi la redondance du code. A ce moment, le compilateur va 5) respectivement dans l'hôte et compiler la fonction sur l'appareil ;
6) Vous ne pouvez pas utiliser __global__
et __device__
pour modifier la fonction en même temps ;
7) Vous ne pouvez pas utiliser __global__
et __host__
pour modifier la fonction en même temps ;
9) Vous pouvez utiliser pour __noinline__
suggérer que le compilateur ne doit pas traiter une fonction de périphérique comme une fonction en ligne ;
10) Vous pouvez utiliser pour __forceinline__
suggérer que le compilateur va Une fonction de périphérique est traitée comme une fonction en ligne.
11) Les fonctions de périphérique peuvent avoir des valeurs de retour.