OPEN-CL.net c# utiliser la puissance de vos GPUs avec CUDA

Tutoriel sur OPEN-CL en .net avec C# (Csharp) pour utiliser la puissance de calcul de vos GPUs via l'architecture CUDA (Compute Unified Device Architecture)

  Publié le

Et dans les grandes lignes ?

Sommairement, OpenCL est une technique utilisant un code en C ou C++ (qui effectuera des opérations arithmétiques simples, mais relativement lourdes). Ces opérations seront ensuite compilées et envoyées à une ou plusieurs cartes graphiques disposant d'un GPU Cuda pour paralléliser le traitement et alléger la charge au CPU.

En Csharp pour utiliser OpenCL il est possible d'utiliser une couche intermédiaire (wrapper) qui permet de simplifier l'utilisation de la classe native de OpenCL.
Les domaines d'application concrets sont majoritairement l'intelligence artificielle est le pentesting.

Le cas d'école avec OpenCL.NET :

L'exemple suivant montre comment mettre en pratique la librairie OpenCL.NET en C# avec un GPU sous architecture CUDA. Pour effectuer des calculs. Nous n'utiliserons qu'un seul GPU Cuda pour réaliser une simple somme du paramètre d'entrée avec lui-même.

Prérequis matériel

Un Pc équipé d'une ou de plusieurs cartes graphiques compatibles CUDA sont nécessaires pour le fonctionnement de la librairie OpenCL .NET compatible C#.

L'import de référence de OpenCL.NET

Depuis une solution dans visual studio il est nécessaire d'ajouter un import de référence de OpenCL.Net (il est impératif que la solution dispose de la référence OpenCL.Net ici la version 2.2.9.0 - téléchargeable via Nuget)

using OpenCL.Net;

Le fonctionnement général

  1. Détecter la ou les cartes graphiques CUDA compatibles OpenCL.Net.
  2. Définir la ou les cartes graphiques CUDA à utiliser.
  3. Créer le contexte d'exécution.
  4. Préparer une file de commandes.
  5. Allouer et initialiser la mémoire du dispositif (device).
  6. Créer la fonction à exécuter.
  7. Compiler le programme OpenCL-C/C++ à utiliser.
  8. Contrôler l'absence d'erreurs.
  9. Charger le code dans les noyaux (kernel).
  10. Insérer le noyau dans la file d'attente.
  11. Forcer l'exécution du code OPENCL-C.
  12. Récupérer les données dans la mémoire du dispositif.
  13. Libérer les ressources.

Détéction des cartes graphiques compatibles CUDA OPEN-CL.net C#

Nous détéctons les cartes graphiques sous architecture CUDA, compatibles OPEN-CL.net. Nous alimentons la liste Lst_Devices.

ErrorCode errorCode;
Platform[] platforms = Cl.GetPlatformIDs(out errorCode);
var Lst_Devices = new List<Device>();

foreach (Platform platform in platforms)
{
    String Device_Name = Cl.GetPlatformInfo(platform, PlatformInfo.Name, out errorCode).ToString();

    //GUI : List box du formulaire au besoin
    //Lsb_Devices_Names.Items.Add(Device_Name);

    //GPU uniquement 
    foreach (Device device in Cl.GetDeviceIDs(platform, DeviceType.Gpu, out errorCode))
    {
          Lst_Devices.Add(device);
    }
}

//Si aucun equipement 
if (Lst_Devices.Count <= 0)
{
   throw new Exception("Aucun GPU CUDA");
}

Choix du GPU CUDA à utiliser

Nous choisissons ici le GPU CUDA par rapport à sont index dans la liste des GPU CUDA disponible dans Lst_Devices.

//Attention pour les débutants : Index_Gpu n'est pas déclaré est correspond à l'index que vous souhaitez utiliser. 
//Peut être rattaché au besoin à Lsb_Devices_Names.SelectedIndex
Device Gpu_1 = Lst_Devices[Index_Gpu];

Création du contexte du GPU CUDA

Nous créons un contexte d'exécution en lui assignant un tableau d'objet device (GPU) à utiliser.

//Création du contexte (suivant liste des GPU)
Context Gpu_context = Cl.CreateContext(null, 1, new Device[] { Gpu_1 }, null, IntPtr.Zero, out errorCode);
if (errorCode != ErrorCode.Success)
{
  throw new Exception("Impossible de créer le contexte");
}

Initialisation de la file des commandes

Nous initialisons une liste de commandes pour chaque GPU concerné par le contexte d'exécution.

CommandQueue commandQueue = Cl.CreateCommandQueue(Gpu_context, Gpu_1, CommandQueueProperties.OutOfOrderExecModeEnable, out errorCode);
if (errorCode != ErrorCode.Success)
{
     throw new Exception("Impossible de créer la liste de traitement");
}

Création du code à éxécuter sur le GPU Cuda

Ici le point le plus important, la conception du code qui sera interprété sur le GPU (ici une simple somme du paramètre d'entrée avec lui-même). A noter que vous pouvez parfaitement récupérer le code directement depuis un fichier (moyen le plus pertinent qui évite une recompilation de l'application).

String Function_Name = "My_Function";

//Programme à éxécuter en OpenCL-C 
List<String> Lst_Row_Code_Build = new List<string>();
Lst_Row_Code_Build.Add(" __kernel void " + Function_Name + "(__global float* input, __global float* output) ");
Lst_Row_Code_Build.Add("{");
Lst_Row_Code_Build.Add("size_t i = get_global_id(0);");
Lst_Row_Code_Build.Add(" output[i] = input[i] + input[i];");
Lst_Row_Code_Build.Add("};");
String Program_Source_Code = String.Join(System.Environment.NewLine, Lst_Row_Code_Build);

En sortie la variable : Program_Source_Code est égale à :

__kernel void My_Function(__global float* input, __global float* output){
	size_t i = get_global_id(0);
	output[i] = input[i] + input[i];
};

Compilation / Création du programme / Contrôles d'intégrité

Nous créons maintenant le programme en lui donnant la variable Program_Source_Code liée au contexte d'éxécution définit plus haut.

Puis nous compilons le programme et laissons le compilateur nous remonter les erreurs du contrôleur d'intégrité.

Event event0;
ErrorCode err;

//Création du programme 
OpenCL.Net.Program program = Cl.CreateProgramWithSource(Gpu_context, 1, new[] { Program_Source_Code }, null, out err);
Cl.BuildProgram(program, 0, null, string.Empty, null, IntPtr.Zero);

//Récupérations des informations du build
if (Cl.GetProgramBuildInfo(program, Gpu_1, ProgramBuildInfo.Status, out err).CastTo<BuildStatus>() != BuildStatus.Success)
{
	//Affichage si erreurs durant la compilation 
	if (err != ErrorCode.Success)
	{
		Txb_Output.Text += String.Format("ERROR: " + "Cl.GetProgramBuildInfo" + " (" + err.ToString() + ")");
		Txb_Output.Text += String.Format("Cl.GetProgramBuildInfo != Success");
		Txb_Output.Text += Cl.GetProgramBuildInfo(program, Gpu_1, ProgramBuildInfo.Log, out err);
	}
}

Création du noyaux et mise en file d'attente pour le GPU CUDA

Maintenant que le programme est compilé (sans erreurs), nous assignons le programme compilé via la variable program ainsi que la fonction à appeler via la variable Function_Name au noyaux d'exécution.

// Création d'un noyaux pour le programme 
OpenCL.Net.Kernel kernel = Cl.CreateKernel(program, Function_Name, out err);

Nous définissons différents paramètres (le code parle de lui même).

// Allouer des tampons d'entrée et de sortie et remplir l'entrée avec des données
const int count = 2048;
Mem memInput = (Mem)Cl.CreateBuffer(Gpu_context, MemFlags.ReadOnly, sizeof(float) * count, out err);

// Créer une mémoire tampon de sortie pour les résultats
Mem memoutput = (Mem)Cl.CreateBuffer(Gpu_context, MemFlags.WriteOnly, sizeof(float) * count, out err);

// Génération des données de tests aléatoires
var random = new Random();
float[] data = (from i in Enumerable.Range(0, count) select (float)random.NextDouble()).ToArray();

//Copier le tampon hôte de valeurs de tests aléatoires dans le tampon du périphérique d'entrée
Cl.EnqueueWriteBuffer(commandQueue, (IMem)memInput, Bool.True, IntPtr.Zero, new IntPtr(sizeof(float) * count), data, 0, null, out event0);

//Utiliser le nombre maximal d'éléments de travail pris en charge pour ce noyau sur ce périphérique
IntPtr notused;
InfoBuffer local = new InfoBuffer(new IntPtr(4));
Cl.GetKernelWorkGroupInfo(kernel, Gpu_1, KernelWorkGroupInfo.WorkGroupSize, new IntPtr(sizeof(int)), local, out notused);

//Définission des arguments du noyau et mise en file d'attente pour éxécution
Cl.SetKernelArg(kernel, 0, new IntPtr(4), memInput);
Cl.SetKernelArg(kernel, 1, new IntPtr(4), memoutput);
Cl.SetKernelArg(kernel, 2, new IntPtr(4), count);
IntPtr[] workGroupSizePtr = new IntPtr[] { new IntPtr(count) };
Cl.EnqueueNDRangeKernel(commandQueue, kernel, 1, null, workGroupSizePtr, null, 0, null, out event0);

Démarrage du traitement sur le GPU Cuda

Nous démarrons enfin le traitement multithread sur le GPU Cuda.

//Forcer le traitement de la file d'attente de commandes, attendre que toutes les commandes soient terminées
//clFinish (le host attend la fin de la file)
//clWaitForEvent (le host attend la fin d'une commande)
//clEnqueueBarrier (le device attend la fin des commandes antérieures)
//clEnqueueWaitForEvents(le device attend la fin d'une commande)
Cl.Finish(commandQueue);

Récupération des résultats

Les résultats ainsi générés sont ajoutés dans un tableau de float dont la taille correspond à celle des tampons d'entrés.

//Lecture/récupération des résultats 
float[] results = new float[count];
Cl.EnqueueReadBuffer(commandQueue, (IMem)memoutput, Bool.True, IntPtr.Zero, new IntPtr(count * sizeof(float)), results, 0, null, out event0);

Vous savez maintenant utiliser OPEN-CL.net avec C# (csharp) pour bénéficier pleinement de la puissance de calcul de vos cartes graphiques CUDA.