OpenCL.NET et Csharp

Publié le : 09-01-2019 08:49

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) qui seront compilées et envoyer à une ou plusieurs cartes graphiques disposant d'un GPU pour parraleliser 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.

Prérequis matériel

Un Pc équipé d'une ou de plusieurs cartes graphiques compatibles CUDA sont nécessaires.

L'import de référence

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 compatibles
  2. Définir la ou les cartes graphiques à 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. Compiler le programme OpenCL-C/C++ à utiliser
  7. Contrôler l'absence d'erreurs
  8. Charger le code dans les noyaux (kernel)
  9. Insérer le noyau dans la file d'attente
  10. Forcer l'exécution
  11. Récupérer les données dans la mémoire du dispositif
  12. Libérer les ressources

Détéction des cartes graphiques compatibles

ErrorCode errorCode;
Platform[] platforms = Cl.GetPlatformIDs(out errorCode);

foreach (Platform platform in platforms)
{
    String Device_Name = Cl.GetPlatformInfo(platform, PlatformInfo.Name, out errorCode).ToString();
    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 à utiliser

Device Gpu_1 = Lst_Devices[Index_Gpu];

Création du contexte

//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

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");
}

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

Event event0;
ErrorCode err;
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);

//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

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

// 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

//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ée.

//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);