CUDA 4 et Architecture Fermi GPUDirect2.0 et UVA Pierre Kunzli - hepia 5 septembre 2012 1 Introduction La version 4.0 de CUDA introduit avec l'architecture Fermi deux nouveautés concernant la gestion de la mémoire sur les devices. GPUDirect2.0 premièrement, qui permet d'échanger des données entre deux devices (fonctionnalité appelée peer-to-peer g.1). Et l'unied Virtual Addressing (g.2), qui simplie la programmation, du fait qu'un espace d'adressage unié est utilisé pour le host et les devices. De plus, la gestion de plusieurs devices est maintenant facilitée. Figure 1 Echange peer-to-peer entre deux devices 2 Pré-requis An de proter de ces fonctionnalités, il faut que le runtime CUDA installé soit en version >= 4.0 et les devices doivent avoir un CUDA Capability Major version >= 2 (architecture Fermi). An de pouvoir utiliser l'uva, l'application doit être compilée en 64 bits et le mode TCC (Tesla Compute Cluster) doit être activé si l'application fonctionne sur Windows 7/Vista (voir [1] section 3.6). 1
Figure 2 Unied Virtual Addressing 3 Mise en oeuvre 3.1 Contrôle des pré-requis Le code suivant permet de s'assurer que le programme s'exécute sur une machine supportant les fonctionnalités UVA et P2P : #define checkcudaerrors(err) checkcudaerrors (err, FILE, LINE ) inline void checkcudaerrors( cudaerror err, const char *file, const int line ) if( cudasuccess!= err) fprintf(stderr, "%s(%i) : CUDA Runtime API error %d: %s.\n", file, line, (int)err, cudageterrorstring( err ) ); inline bool IsAppBuiltAs64() #if defined( x86_64) defined(amd64) defined(_m_amd64) return 1; #else return 0; #endif inline bool IsCuda4() #if CUDART_VERSION >= 4000 return 1; #else return 0; #endif // controler que l'application est compilee en 64 bits if (!IsAppBuiltAs64()) cout<<"uva is only supported on 64-bit OSs and the application must be built as a 64- bit target"<<endl; // controler que cuda4 est disponible if (!IsCuda4()) 2
cout<<"uva needs cuda4 runtime"<<endl; // controler que chaque device supporte p2p et uva int numgpus; checkcudaerrors(cudagetdevicecount(&numgpus)); cudadeviceprop deviceprop; for(int i=0;i<numgpus;i++) checkcudaerrors(cudagetdeviceproperties(&deviceprop, i)); if(!(deviceprop.major>=2)) cout<<"gpu 2.0 (fermi) requied to use UVA/P2P"<<endl; 3.2 UVA Lorsque les conditions sont remplies, l'uva est automatiquement activé. Un seul espace d'adressage est alors utilisé pour le host et pour les devices de compute capability >= 2. Cet espace d'adressage est utilisé pour toute allocation faite sur les devices, mais uniquement pour les allocations faites avec cudahostalloc() ou cudamallochost() pour la mémoire du host. La première conséquence est que lors des cudamemcpy() sur ou depuis n'importe quel device qui utilise l'uva, on peut utiliser la valeur cudamemcpydefault pour spécier le type de copie. Le runtime étant capable de déterminer la source et la destination à partir des pointeurs. Ensuite, lorsque l'uva est utilisé, les pointeurs alloués avec cudahostalloc() ou cudamallochost()peuvent être directement déréférencés depuis les kernels, et donc, la mémoire du host directement accédée. Il faut cependant utiliser cette fonctionalité avec précaution du fait des forts temps de latence qu'elle implique. global void kernel(int* ptr) int i = ptr[0]; ptr[1] = 3; // allouer de la memoire sur le host int* hostptr1; checkcudaerrors(cudamallochost(&hostptr1,10*sizeof(int))); int* hostptr2 = (int*)malloc(10*sizeof(int)); // la memoire allouee avec cudamallochost est directement accessible depuis le kernel kernel<<<dimgrid, dimblock>>>(hostptr1); int* deviceptr; checkcudaerrors(cudamalloc((void**)&deviceptr,10*sizeof(int))); // copier des donnees du host vers le device, inutile de specifier le type de copie // le runtime determine automatiquement la source et la destination checkcudaerrors(cudamemcpy(hostptr2,deviceptr,10*sizeof(int),cudamemcpydefault)); 3
3.3 Peer-to-Peer GPUDirect 2 permet des échanges directs entre deux devices, appelés copies peer-to-peer. Il est possible d'utiliser cette fonctionnalité sans que l'uva soit actif, cependant, cela implique que les données transitent par le host et cette façon de faire ne permet donc pas de gain de performance. Dans ce cas, il faut utiliser les fonctions de copies standard avec le suxe Peer. Par exemple, cudamemcpypeer(), qui prends en paramètre le pointeur destination, le device destination, le pointeur source, le device source, et enn la taille des données à copier. int* device0ptr; checkcudaerrors(cudamalloc((void**)&device0ptr,10*sizeof(int))); // allouer de la memoire sur le device 1 checkcudaerrors(cudasetdevice(1)); int* device1ptr; checkcudaerrors(cudamalloc((void**)&device1ptr,10*sizeof(int))); // copier les donnees du device 0 sur le device 1 checkcudaerrors(cudamemcpypeer(device1ptr, 1, device0ptr, 0, 10*sizeof(int))); 3.4 Peer-to-Peer avec UVA Lorsque l'uva est activé, un device peut accéder directement à la mémoire d'un autre device sans passer par le host. L'accès P2P doit être activé entre chaque device concerné. Une fois le P2P activé, la mise en oeuvre est très simple puisque qu'il sut d'utiliser la fonction cudamemcpy(), le runtime étant capable de déterminer la source et la destination à partir des pointeurs grâce à UVA. Ce type de copie ne transitant pas par le host, les performances sont bien meilleures que sans UVA. int* device0ptr; checkcudaerrors(cudamalloc((void**)&device0ptr,10*sizeof(int))); // activer acces p2p depuis device 0 sur device 1 checkcudaerrors(cudadeviceenablepeeraccess(1, 0)); // allouer de la memoire sur le device 1 checkcudaerrors(cudasetdevice(1)); int* device1ptr; checkcudaerrors(cudamalloc((void**)&device1ptr,10*sizeof(int))); // activer acces p2p depuis device 1 sur device 0 checkcudaerrors(cudadeviceenablepeeraccess(0, 0)); checkcudaerrors(cudamemcpy(device1ptr,device0ptr,10*sizeof(int),cudamemcpydefault)); De plus, lorsque l'uva est activé et que l'accès P2P est activé, un kernel peut accéder directement à la mémoire d'un autre device. Là encore, il faut faire attention aux temps de latence lors de tels accès. global void kernel(int* ptr) int i = ptr[0]; ptr[1] = 3; int* device0ptr; checkcudaerrors(cudamalloc((void**)&device0ptr,10*sizeof(int))); // activer acces p2p depuis device 0 sur device 1 4
checkcudaerrors(cudadeviceenablepeeraccess(1, 0)); // allouer de la memoire sur le device 1 checkcudaerrors(cudasetdevice(1)); int* device1ptr; checkcudaerrors(cudamalloc((void**)&device1ptr,10*sizeof(int))); // activer acces p2p depuis device 1 sur device 0 checkcudaerrors(cudadeviceenablepeeraccess(0, 0)); // le kernel va s'executer sur le device 1 et acceder directement a la memoire du device 2 kernel<<<dimgrid, dimblock>>>(device0ptr); 4 Utilisation de plusieurs devices Comme on peut le voir dans les exemples, un seul processus sur le CPU est capable de prendre le contôle de plusieurs devices. En eet, il est maintenant possible de lancer des exécutions asynchrones à partir d'un thread puis de prendre le contrôle d'un autre device an de lancer d'autres exécutions. Cela facilite dans certains cas la gestions de la programmation multi-devices puisqu'il n'est plus nécessaire d'utiliser plusieurs threads ou processus CPU. Dans l'autre sens, il est également maintenant possible pour plusieurs threads ou processus de prendre le contrôle d'un même device, les kernels sont alors exécutés en concurrence sur le device concerné. Cela permet de faire fonctionner plusieurs programmes CUDA simultanément ou de simplier la conception d'un programme dans certains cas. 5 Débits Des tests de débit ont été eectués. Les tests ont été eectués sur la machine cogito d'hepia, équipée de 4 GPUs Nvidia M2090 connectés à des ports PCIexpress 2.0 16x, permettant en théorie d'obtenir un débit de l'ordre de 8GO par seconde. An de mesurer le débit, on fait transiter 1GO de données : entre deux GPUs en p2p, entre deux GPUs en passant par le host et du host vers un GPU. Les résultats obtenus sont les suivants : type temps débit GPU -> GPU p2p 150 ms 6.7 GO/s GPU -> GPU via host 929 ms 1.07 GO/s HOST -> GPU 862 ms 1.16 GO/s On remarque alors que si les transferts qui transitent via la mémoire du host sont passablement lents par rapport au débit théorique, les transferts directs entre GPUs sont quant à eux beaucoup plus proches du maximum théorique. Références [1] NVidia. Cuda c programming guide 4.1 sections 3.2.6 et 3.2.7, 2012. http://developer.download.nvidia.com/compute/devzone/docs/html/c/doc/cuda_c_ Programming_Guide.pdf. [2] Tim C. Schroeder. Peer-to-peer & unied virtual addressing cuda webinar, 2011. http://developer.download.nvidia.com/cuda/training/cuda_webinars_gpudirect_uva. pdf. 5
A Exemple de code complet #include <cuda_runtime.h> #include <iostream> #include <stdlib.h> using namespace std; /* fonctions d'aide pour cuda */ #define getlastcudaerror(msg) getlastcudaerror (msg, FILE, LINE ) inline void getlastcudaerror( const char *errormessage, const char *file, const int line ) cudaerror_t err = cudagetlasterror(); if( cudasuccess!= err) fprintf(stderr, "%s(%i) : getlastcudaerror() CUDA error : %s : (%d) %s.\n", file, line, errormessage, (int)err, cudageterrorstring( err ) ); #define checkcudaerrors(err) checkcudaerrors (err, FILE, LINE ) inline void checkcudaerrors( cudaerror err, const char *file, const int line ) if( cudasuccess!= err) fprintf(stderr, "%s(%i) : CUDA Runtime API error %d: %s.\n", file, line, (int)err, cudageterrorstring( err ) ); inline bool IsAppBuiltAs64() #if defined( x86_64) defined(amd64) defined(_m_amd64) return 1; #else return 0; #endif inline bool IsCuda4() #if CUDART_VERSION >= 4000 return 1; #else return 0; #endif // nombre de threads par block #define NT 128 6
// kernel basique global void kernel(int *d2, int *d1, int *h1, int size) int mypos = blockidx.x*nt + threadidx.x; if(mypos<size) d2[mypos]=d1[mypos]; h1[size-1]=2; int main( int argc, char** argv) // controler que le systeme possede 2 GPU au moins int numgpus; checkcudaerrors(cudagetdevicecount(&numgpus)); if(numgpus<2) cout<<"this application needs at least 2 devices"<<endl; // controler que l'application est compilee en 64 bits if (!IsAppBuiltAs64()) cout<<"uva is only supported on 64-bit OSs and the application must be built as a 64-bit target"<<endl; // controler que cuda4 est disponible if (!IsCuda4()) cout<<"uva needs cuda4 runtime"<<endl; // controler que les deux premiers devices supporte p2p et uva cudadeviceprop deviceprop; for(int i=0;i<2;i++) checkcudaerrors(cudagetdeviceproperties(&deviceprop, i)); if(!(deviceprop.major>=2)) cout<<"gpu 2.0 (fermi) requied to use UVA/P2P"<<endl; int size = 200; // prendre le controle du device 0 // activer l'acces p2p du device 0 sur le device 1 checkcudaerrors(cudadeviceenablepeeraccess(1, 0)); int *device0ptr; checkcudaerrors(cudamalloc((void**)&device0ptr,size*sizeof(int))); // prendre le controle du device 1 checkcudaerrors(cudasetdevice(1)); // activer l'acces p2p du device 1 sur le device 0 checkcudaerrors(cudadeviceenablepeeraccess(0, 0)); // allouer de la memoire sur le device 1 int *device1ptr; checkcudaerrors(cudamalloc((void**)&device1ptr,size*sizeof(int))); 7
// allouer de la memoire sur le host accessible depuis les devices int *hostptr; checkcudaerrors(cudamallochost(&hostptr,size*sizeof(int))); hostptr[size-1]=0; // affiche 0 cout<<hostptr[size-1]<<endl; // effectuer une copie du device 0 sur le device 1 // (automatiquement p2p, pas besoin de specifier le type de copie) checkcudaerrors(cudamemcpy(device1ptr,device0ptr,size*sizeof(int),cudamemcpydefault)); dim3 dimgrid ((size + NT - 1)/NT,1); dim3 dimblock (NT,1); // executer le kernel sur le device 0 kernel<<<dimgrid, dimblock>>>(device1ptr, device0ptr, hostptr, size); getlastcudaerror("copy() execution failed.\n"); checkcudaerrors(cudathreadsynchronize()); // affiche 2 car la memoire du host a ete modifiee par le kernel cout<<hostptr[size-1]<<endl; 8