Recherche…


Introduction

Avant d'utiliser OpenCL, il faut configurer son code pour l'utiliser. Cette rubrique se concentre sur la façon d’ouvrir OpenClient dans votre projet et d’exécuter un noyau de base. Les exemples sont basés sur le wrapper C # OpenCL.NET mais comme le wrapper n'ajoute aucune abstraction à OpenCL, le code sera probablement exécuté avec très peu de modifications sur C / C ++.

Les appels en C # peuvent ressembler à ceci: "Cl.GetPlatformIDs". Pour OpenCL Ap-Style, vous appelez «clGetPlatformIDs» et pour celui de style C ++ «cl :: GetPlatformIDs»

Remarques

  • NVidia, AMD et Intel ont des implémentations d'OpenCL légèrement différentes, mais les différences connues sont (pour mon expérience) limitées aux exigences de parenthèses et aux conversions implicites. Parfois, NVidia plante votre noyau en essayant de déterminer la surcharge correcte pour une méthode. Dans ce cas, il est utile de proposer une distribution explicite pour aider le GPU. Le problème a été observé pour les noyaux compilés à l'exécution.

  • Pour obtenir plus d'informations sur les appels utilisés dans cette rubrique, il suffit de Google "OpenCL" suivi du nom de la fonction. Le groupe Khronos dispose d'une documentation complète sur tous les paramètres et types de données disponibles sur leur site Web.

Initialisation du périphérique cible

Les noyaux OpenCL peuvent être exécutés sur le GPU ou le processeur. Cela permet des solutions de secours, où le client peut avoir un système très obsolète. Le programmeur peut également choisir de limiter ses fonctionnalités au processeur ou au processeur graphique.

Pour commencer à utiliser OpenCL, vous aurez besoin d'un «contexte» et d'un «périphérique». Les deux sont des structures définies par l'API OpenCL (également appelée cl :: Context ou clContext & ~ Device) et définissent le processeur cible utilisé.

Pour obtenir votre appareil et votre contexte, vous devez interroger une liste de plates-formes disponibles, pouvant héberger plusieurs périphériques. Une plate-forme représente votre GPU physique et votre processeur, tandis qu'un périphérique peut distinguer les unités informatiques contenues. Pour les GPU, la plupart des plates-formes n'auront qu'un seul périphérique. Mais un processeur peut offrir un processeur graphique intégré supplémentaire à côté de ses capacités de processeur.

Le contexte gère la mémoire, les files d'attente de commandes, les différents noyaux et programmes. Un contexte peut être limité à un seul périphérique mais également référencer plusieurs périphériques.

Une note API rapide avant de commencer le codage: presque tous les appels à OpenCL vous donnent une valeur d'erreur, soit en tant que valeur de retour, soit via une valeur ref (pointeur en C). Maintenant, commençons.

ErrorCode err;
var platforms = Cl.GetPlatformIDs(out err);
if(!CheckError(err, "Cl.GetPlatformIDs")) return;
foreach (var platform in platforms) {
    foreach (var device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out err)) {
        if(!CheckError(err, "Cl.GetDeviceIDs")) continue;
        [...]
    }
}

Cet extrait de code interroge tous les périphériques GPU disponibles sur le système. Vous pouvez maintenant les ajouter à une liste ou commencer votre contexte directement avec la première correspondance. La fonction 'CheckError (...)' est un utilitaire simple, qui vérifie si le code d'erreur a la valeur de réussite ou une valeur différente et peut vous offrir une journalisation. Il est recommandé d'utiliser une fonction ou une macro distincte, car vous l'appellerez beaucoup.

ErrorCode est juste un enum sur le type de données cl_int pour C #, C / C ++ peut comparer la valeur int avec des constantes d'erreur prédéfinies comme indiqué ici: https://www.khronos.org/registry/OpenCL/sdk/1.0/docs/man/ xhtml / errors.html

Vous pouvez également vérifier si le périphérique prend en charge toutes les fonctionnalités requises, sinon vos noyaux risquent de tomber en panne à l'exécution. Vous pouvez interroger une fonctionnalité de périphérique avec

Cl.GetDeviceInfo(_device, DeviceInfo.ImageSupport, out err)

Cet exemple demande au périphérique s'il peut exécuter des fonctions d'image. Pour la prochaine et dernière étape, nous devons construire notre contexte à partir des périphériques collectés.

_context = Cl.CreateContext(null, 1, new[] { _device }, ContextNotify, IntPtr.Zero, out err);

Certaines choses se passent ici. Pour les utilisateurs de C / C ++, IntPtr est une adresse de pointeur en C #. Je vais me concentrer sur les parties importantes ici.

  • Le second paramètre définit le nombre de périphériques que vous souhaitez utiliser
  • Le troisième paramètre est un tableau de ces périphériques (ou un pointeur en C / C ++)
  • Et le troisième paramètre est un pointeur de fonction pour une fonction de rappel. Cette fonction sera utilisée chaque fois que des erreurs surviennent dans le contexte.

Pour une utilisation plus poussée, vous devrez conserver vos périphériques utilisés et le contexte quelque part.

Lorsque vous aurez terminé toutes vos interactions OpenCL, vous devrez relancer le contexte avec

Cl.ReleaseContext(_context);

Compiler votre noyau

Les noyaux peuvent être compilés à l'exécution sur le périphérique cible. Pour ce faire, vous avez besoin

  • le code source du noyau
  • le périphérique cible sur lequel compiler
  • un contexte construit avec le périphérique cible

Une mise à jour rapide de la terminologie: un programme contient une collection de noyaux. Vous pouvez considérer un programme comme un fichier source complet C / C ++ / C #, tandis que les noyaux sont les différents membres de la fonction de ce fichier.

Vous devez d'abord créer un programme à partir de votre code source.

var program = Cl.CreateProgramWithSource(_context, 1, new[] { source }, null, out err);

Vous pouvez combiner plusieurs fichiers sources en un seul programme et les compiler ensemble, ce qui vous permet d’avoir des noyaux dans différents fichiers et de les compiler en une seule fois.

Dans l'étape suivante, vous devrez compiler le programme sur votre périphérique cible.

err = Cl.BuildProgram(program, 1, new[] { _device }, string.Empty, null, IntPtr.Zero);

Maintenant, voici une petite mise en garde: le code d'erreur vous indique seulement si l'appel de fonction lui-même a été réussi, mais pas si votre code a réellement été compilé. Pour vérifier cela, nous devons interroger des informations supplémentaires

BuildStatus status;
status = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>();
if (status != BuildStatus.Success) {
    var log = Cl.GetProgramBuildInfo(program, _device, ProgramBuildInfo.Log, out err);
}

Les utilisateurs de C / C ++ peuvent ignorer la distribution à la fin et comparer le nombre entier renvoyé avec la constante correspondante.

Le premier appel vérifie si notre version a été réussie. Sinon, nous pouvons récupérer un journal et voir exactement où les choses ont mal tourné. Voir les remarques concernant certains problèmes courants concernant différentes plates-formes.

Une fois le programme construit, vous devez extraire vos différents noyaux du programme compilé. Pour ce faire, vous créez vos noyaux avec

_kernel = Cl.CreateKernel(_program, kernel, out err);

où 'noyau' est une chaîne du nom du noyau. Lorsque vous avez terminé avec votre noyau, vous devez le publier avec

Cl.ReleaseKernel(_kernel);

Création d'une file d'attente de commandes

Pour lancer toute opération sur vos périphériques, vous devez disposer d'une file d'attente de commandes pour chaque périphérique. La file d'attente garde une trace des différents appels que vous avez effectués sur l'appareil cible et les conserve dans l'ordre. La plupart des commandes peuvent également être exécutées en mode bloquant ou non bloquant.

Créer une file d'attente est assez simple:

_queue = Cl.CreateCommandQueue(_context, _device, CommandQueueProperties.None, out err);

L'interaction de base avec votre file d'attente de commandes consiste à mettre en file d'attente différentes opérations que vous souhaitez effectuer, par exemple, copier des données vers et depuis votre périphérique et lancer un noyau.

Lorsque vous avez fini d'utiliser la file d'attente de commandes, vous devez libérer la file d'attente avec un appel à

Cl.ReleaseCommandQueue(_queue);

Exécuter le noyau

Alors maintenant, nous passons aux choses réelles, en exécutant vos noyaux sur le périphérique parallèle. Veuillez lire les bases du matériel pour bien comprendre le dispatching du noyau.

Vous devez d'abord définir les arguments du noyau avant d'appeler le noyau. Cela se fait via

err = Cl.SetKernelArg(_kernel, $argumentIndex, $argument);

Si vous ne définissez pas chaque argument avant de lancer le noyau, le noyau échouera.

Avant de lancer notre noyau, nous devons calculer la «taille de travail globale» et la «taille de travail locale».

la taille de travail globale est le nombre total de threads qui seront lancés sur votre GPU. La taille de travail locale est le nombre de threads à l'intérieur de chaque bloc de thread. La taille de travail locale peut être omise si le noyau n'a pas besoin d'exigences spéciales. Mais si la taille de travail locale est donnée, la taille de travail globale doit être un multiple de la taille de travail locale.

Les tailles de travail peuvent être unidimensionnelles, bidimensionnelles ou tridimensionnelles. Le choix du nombre de dimensions que vous souhaitez dépend entièrement de vous et vous pouvez choisir ce qui convient le mieux à votre algorithme.

Maintenant que nous avons choisi nos tailles de travail, nous pouvons appeler le noyau.

Event clevent;
err = Cl.EnqueueNDRangeKernel(_queue, _kernel, $dimensions, null, $globalWorkSize, $localWorkSize, 0, null, out clevent);

Les dimensions en $ définissent le nombre de dimensions souhaité, $ globalWorkSize est un tableau de dimensions en taille $ avec la taille de travail globale et la même chose pour $ localWorkSize. Le dernier argument vous donne un objet qui représente votre noyau actuellement exécuté.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow