430 likes | 714 Views
OpenCL. Programmation de cartes graphiques. OpenCL. API + langage basé sur le C99 Conçu pour programmer des systèmes parallèles hétérogènes: CPU multi-coeur , GPU, etc. On distingue le processeur hôte des périphériques
E N D
OpenCL Programmation de cartes graphiques
OpenCL • API + langage basé sur le C99 • Conçu pour programmer des systèmes parallèles hétérogènes: CPU multi-coeur, GPU, etc. • On distingue le processeur hôte des périphériques • Un conteneur spécial (context) sert d’interface entre le processeur hôte et les périphériques • Les données et les tâches (kernels) sont transférés à l’aide d’une file d’attente (command queue)
Comparaison: C standard // Addition de 2 vecteurs de taille N // voidvecadd(int *C, int* A, int *B, int N) { for(int i = 0; i < N; i++) { C[i] = A[i] + B[i]; } }
Comparaison: Programmation multithread voidvecadd(int *C, int* A, int *B, int N, int NP, inttid) { intept = N/NP; // nbre d’éléments par thread for(int i = tid*ept; i < (tid+1)*ept; i++) { C[i] = A[i] + B[i]; } }
Comparaison: OpenCL __kernel voidvecadd(__global int *C, __global int* A, __global int *B) { inttid = get_global_id(0); // fonction OpenCL C[tid] = A[tid] + B[tid]; }
Espace des indices • En OpenCL, l’espace des indices des processeurs peut avoir 1, 2 ou 3 dimensions. • Il y a deux niveaux d’indices: • Un indice global unique pour chaque work-item du périphérique (NDRange) • Un indice local unique pour chaque work-item à l’intérieur d’un même workgroup.
Connaître son indice • get_global_id(dim): indice globale du work-item appelant selon la dimension dim=0,1 ou 2 • get_local_id(dim): indice local du work-item appelant • get_group_id(dim): indice du workgroup auquel appartient le work-item appelant • get_local_size(dim): taille de la dimension dim dans le workgroup du work-item appelant.
Relation entre global et local get_global_id(dim) = get_local_size(dim)*get_group_id(dim) + get_local_id(dim)
Plateformes • Une plateforme est une implémentation de OpenCL spécifique à un manufacturier donné • Pour obtenir la liste des plateformes: cl_intclGetPlatformIDs(cl_uintnum_entries, cl_platform_id *platforms, cl_uint *num_platforms) • clGetPlatformInfo pour obtenir de l’information sur une plateforme donné
Exemple 1 cl_intstatus; cl_uintnumPlatforms = 0; cl_platform_id *platforms = NULL; status = clGetPlatformIDs(0, NULL, &numPlatforms); platforms = (cl_platform_id*)malloc( numPlatforms*sizeof(cl_platform_id)); status = clGetPlatformIDs(numPlatforms, platforms, NULL); char Name[1000]; clGetPlatformInfo(platforms[0], CL_PLATFORM_NAME, sizeof(Name), Name, NULL); printf("Name of platform : %s\n", Name);
Périphériques • Pour chacune des plateformes (ex. NVIDIA), on peut obtenir la liste des périphériques associés: cl_intclGetDeviceIDs(cl_platform_idplatform, cl_device_typedevice_type, cl_uintnum_entries, cl_device_id *devices, cl_uint *num_devices)
Exemple 2 cl_uintnumDevices = 0; cl_device_id *devices = NULL; status = clGetDeviceIDs( platforms[0], CL_DEVICE_TYPE_ALL, 0, NULL, &numDevices); devices = (cl_device_id*)malloc( numDevices*sizeof(cl_device_id)); status = clGetDeviceIDs( platforms[0], CL_DEVICE_TYPE_ALL, numDevices, devices, NULL);
Contextes • Un contexte est un type de conteneur permettant de communiquer des données et des instructions à des périphériques • On utilise la fonction: clCreateContext • Le premier paramètre sert à limité la porté du contexte, par exemple en spécifiant une plateforme particulière. • Le « callback » sert a fournir une fonction qui pourra fournir d’avantage d’information sur les erreurs se produisant tout au long de l’utilisation du contexte.
Contextes cl_context clCreateContext ( constcl_context_properties *properties, cl_uintnum_devices, constcl_device_id *devices, void (CL_CALLBACK *pfn_notify)( const char *errinfo, constvoid *private_info, size_t cb, void *user_data), void *user_data, cl_int *errcode_ret)
Exemple 3 cl_contextcontext = NULL; context = clCreateContext( NULL, numDevices, devices, NULL, NULL, &status);
File de commandes • On a déjà dit qu’un contexte est un conteneur permettant de communiquer avec un périphérique. • On ajoute et retire l’information d’un contexte à l’aide d’une file d’attente appelée « file de commande » (command queue). • Toutes les commandes indiquant une communication hôte-périphérique commencent par clEnqueue.
Création d’une file de commandes cl_command_queue clCreateCommandQueue( cl_contextcontext, cl_device_iddevice, cl_command_queue_propertiesproperties, cl_int* errcode_ret) Note: Le paramètre « properties » sert, entre autres, à indiquer si les éléments seront pris dans l’ordre.
Exemple 4 cl_command_queuecmdQueue; cmdQueue = clCreateCommandQueue( context, devices[0], 0, &status);
Objets mémoire • Avant d’être transférés, les données doivent être converties dans un format particulier. • Il y a deux formats: • Les tampons (buffers) • Les images • Les tampons sont l’équivalent des tableaux en C et sont stockés de façon contiguë en mémoire. • Les images sont des objets opaques et sont placées en mémoire de façon optimiser les performances.
Création d’un tampon cl_mem clCreateBuffer( cl_contextcontext, cl_mem_flags flags, size_t size, void *host_ptr, cl_int *errcode_ret) Note: Le paramètre « flags » sert à indiquer si le tampon est en lecture, en écriture ou les deux.
Écrire dans un tampon cl_int clEnqueueWriteBuffer ( cl_command_queuecommand_queue, cl_mem buffer, cl_boolblocking_write, // CL_TRUE pour appel bloquant size_t offset, size_t cb, constvoid *ptr, cl_uintnum_events_in_wait_list, constcl_event *event_wait_list, cl_event *event)
Lire dans un tampon cl_int clEnqueueReadBuffer( cl_command_queuecommand_queue, cl_mem buffer, cl_boolblocking_read, size_t offset, size_t size, void *ptr, cl_uintnum_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)
Exemple 5 cl_membufferA, bufferB, bufferC; bufferA = clCreateBuffer( context, CL_MEM_READ_ONLY, datasize, NULL, &status); bufferB = clCreateBuffer( context, CL_MEM_READ_ONLY, datasize, NULL, &status); bufferC = clCreateBuffer( context, CL_MEM_WRITE_ONLY, datasize, NULL, &status); status = clEnqueueWriteBuffer( cmdQueue, bufferA, CL_FALSE, 0, datasize, A, 0, NULL, NULL); status = clEnqueueWriteBuffer( cmdQueue, bufferB, CL_FALSE, 0, datasize, B, 0, NULL, NULL);
Kernels • Il faut distinguer le programme C de l’ordinateur hôte du programme C OpenCL qui sera exécuté sur un périphérique. • Un programme C OpenCL est une collection de fonctions appelées « kernels» • Le type de retour de ces fonctions doit être void • Les kernels représentent les tâches que l’on peut ordonnancer de façon dynamique sur les périphériques.
Création et exécution d’un kernel • Le code source est stocké sous la forme d’une chaîne de caractères • La chaine est convertie en objet programmes à l’aide de la commande clCreateProgramWithSource() • Un objet programme est compilé à l’aide de la commande clBuildProgram() • Un kernel est obtenu à l’aide de la fonction • Les paramètres du kernel sont affectés à l’aide de la commande clSetKernelArg() • Le kernel est finalement exécuté à l’aide de la commande clEnqueueNDRangeKernel()
Création d’un objet programme cl_program clCreateProgramWithSource ( cl_contextcontext, cl_uint count, const char **strings, const size_t *lengths, cl_int *errcode_ret)
Compiler un objet programme cl_int clBuildProgram ( cl_program program, cl_uintnum_devices, constcl_device_id *device_list, const char *options, void (CL_CALLBACK *pfn_notify)(cl_program program, void *user_data), void *user_data)
Obtenir un kernel cl_kernel clCreateKernel( cl_program program, constchar *kernel_name, cl_int *errcode_ret) Note: *kernel_namecontient le nom d’une fonction dans le programme source .
Affecter les paramètres cl_int clSetKernelArg ( cl_kernelkernel, cl_uintarg_index, size_targ_size, const void *arg_value) Note: Les paramètres demeurent affectés au kernel tant qu’il n’y a pas de modification explicite à l’aide de clSetKernelArg()
Exécuter un kernel cl_int clEnqueueNDRangeKernel( cl_command_queuecommand_queue, cl_kernelkernel, cl_uintwork_dim, const size_t *global_work_offset, const size_t *global_work_size, const size_t *local_work_size, cl_uintnum_events_in_wait_list, const cl_event *event_wait_list, cl_event *event)
Espaces d’adressage Les qualificat • __global: Mémoire partagée par tous les work-items du kernel. • __constant: Mémoire en lecture seule partagée par tous les work-items du kernel. • __local: Mémoire partagée par tous les work-items d’un même work-group. • __private: Mémoire privée pour chaque work-item.
Espace d’adressage par défaut • Pour améliorer la performance, on utilise autant que possible la mémoire __local ou __private. • S’il n’y a pas de qualificatif, les paramètres des fonctions et les variables locales sont __private • Le qualificatif des paramètres qui sont des pointeurs doit être spécifié et ne peut pas être __private. • Les mémoires __private et __local ne sont pas préservées à la fin de l’exécution d’un work-item. • La seule façon de retourner une valeur à l’hôte est via la mémoire __global.