Astuces DotNet (Sébastien Courtois)

08/05/2010

[MS Research / DevLabs] Accelerator v2 : Exploiter vos GPU facilement en .NET

Filed under: .NET, DevLabs, Hors Catégorie — Étiquettes : , , , , , , , , — sebastiencourtois @ 17:47

Suite à ma visite sur les sites Internet de Microsoft Research et DevLabs, j’ai décidé de vous parler de ces projets de Microsoft qui seront dans les prochaines versions de .NET (ou pas). Dans les anciens project MS Research que l’on retrouve aujourd’hui au grand public, on pourrait cité WPF/Silverlight,Surface,PFX…

De ce post, nous allons parler d’une technologie dont le nom de code est “Accelerator”.

  • Présentation du contexte

De nos jours, les fabricants d’ordinateur nous fournisse des processeurs toujours plus puissants que ce soit en fréquence d’horloge qu’en nombre de cœurs. Ces nouvelles possibilités commencent à être exploiter correctement (notamment grâce à PFX de .NET 4).

Mais ce qui est moins exploiter est la puissance de calcul des cartes graphiques (GPU). Il faut savoir qu’une carte graphique peut avoir plusieurs processeurs dont chacun peut avoir de nombreux coeurs (jusqu’à 500 pour les plus récentes). Tout ce potentiel n’est utilisé que pour les jeux afin de faire des rendus et des calculs mathématiques afin de rendre des scènes 3D.

Le gros avantages des GPU est sa rapidité de calcul et sa gestion optimisé du parallélisme.

En revanche, les défauts des GPU par rapport aux CPU :

  • Les instructions sont limités aux opérations mathématiques 3D alors que le CPU peut avoir des opérations plus génériques
  • Les instructions dépendent beaucoup du constructeurs de la carte graphique (beaucoup plus que pour un CPU)
  • Il y a très peu d’API permettant d’accéder aux GPU directement (sans passer des API 3D).
  • Qu’est ce qu’Accelerator ?

L’idée du projet Accelerator est de fournir une API (Managé et C++) permettant d’envoyer du code sur ces GPU qu’il l’exécute.

D’autres ont déjà eu l’idée avant Microsoft :

  • Nvidia avec CUDA
  • ATI avec ATI Stream
  • Appel avec OpenCL
  • “Pixel Shaders”

Bien que ces API existent (encore au stade BETA pour la plupart), Microsoft a décidé de créé le projet Accelerator afin de fournir une API de développement pour les GPU indépendant de la plateforme et extensible.

  • Comment cela fonctionne ?

Accelerator se compose de deux gros blogs : Accelerator Library et Target Runtime.

image Structure général d’une application Accelerator

L’”Accelerator Library” est une API de développement pour préparer les données afin d’être exécutés sur le GPU. Cela se base sur un système abstrait à base d’arbre d’expression (nous le verrons plus loin). Cette API est composé d’une partie Native (C++) et d’une partie Managé (wrapper léger sur l’API native).

Le “Target Runtime” est une sorte de driver permettant de faire communiquer l’API avec les couches matériels (GPU … mais aussi CPU Multicore). Ce driver lit les arbres d’expressions d’”Accelarator Library” et envoie les instructions correspondantes aux GPU. Le driver est aussi en charge de la gestion de la charge sur les processeurs/cœurs. Microsoft fournit deux target runtime par défaut : Un DirectX9 (permettant d’accéder indépendamment à l’ensemble des carte graphiques du marché) et un pour les multi-coeurs CPU 64bits. Si vous souhaitez utilisé Accelerator sur d’autres types de processeurs, il suffit de développer un target runtime pour l’architecture cible.

Afin de comprendre le fonctionnement de Accelerator Library, prenons un exemple. Prenons deux tableaux contenant des valeurs à virgules flottantes (float). On souhaite additionner les cases une à une dans un troisième tableaux. Sur un CPU, on aurait une boucle for qui ferait les opération dans l’ordre des cellules et une cellule à la fois.

acceleratorv2 Exécution d’une addition sur un tableau (Sequential Program = CPU / Data Parallel Program = GPU)

Sur le GPU, le tableau est divisé et chaque cœurs à son propre lot de données à exécutés. Après exécution, le tableau est réassemblé à partir des résultats des tableaux de chacun des cœurs.

Afin que les opérations soient abstraites par rapport aux matériels sous jacent, Accelerator fournit un système d’arbre permettant de faciliter la création des instructions GPU. Par exemple, le graph suivant représente l’addition case par case de deux tableau puis la division de chacun des cases résultat par 2.

image Arbre d’expression Accelerator

  • Comment installer Accelerator ?

Accelerator v2 est actuellement disponible sur https://connect.microsoft.com/acceleratorv2.

Une fois installé, vous avez accès à  un document d’introduction et un document contenant l’ensemble des classes du projet. Je vous conseille le document d’introduction qui est très bien fait (j’en reprend un partie dans ce post).

Afin de développer, vous devez ajouter en référence l’assembly Microsoft.Accelerator.dll (contenu dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\Managed).

Attention : Pour l’exécuter, il est nécessaire de placer la dll Accelerator.dll (se trouvant dans le répertoire C:\Program Files\Microsoft\Accelerator v2\bin\x86 ou C:\Program Files\Microsoft\Accelerator v2\bin\x64 selon votre architecture ) dans le répertoire où s’exécute l’application sous peine d’avoir l’exception : “DllNotFoundException : Unable to load DLL ‘Accelerator.dll’: Le module spécifié est introuvable. (Exception from HRESULT: 0x8007007E)”

  • Un Hello world “Accelrator”

En reprenant l’exemple d’addition case à case puis d’une division par 2, on obtient un code C# suivant (le reste du code a été enlevé afin de pas alourdir le code).

On commence par simplifier le code en utilisant des using.

using FPA = Microsoft.ParallelArrays.FloatParallelArray;
using PA = Microsoft.ParallelArrays.ParallelArrays;

FloatParallelArray représente un tableau de float sur la cible (carte graphique dans notre cas).. ParallelArrays regroupe les instruction disponible pour la cible.

DX9Target evalTarget = new DX9Target();

On crée le driver qui sera utilisé pour exécuter les opérations. Afin de faire simple, nous utiliserons le driver DirectX9 afin d’accéder aux GPU.

float[] inputArray1 = new float[arrayLength];
float[] inputArray2 = new float[arrayLength];
float[] stackedArray = new float[arrayLength];
/* Ajout de données dans les tableaux  inputArray1/inputArray2 */

On crée trois tableaux. Deux tableaux contenant les données d’entrée (inputArray1,2) que l’on remplit de valeurs diverses et un tableau pour les résultats.

FPA fpInput1 = new FPA(inputArray1);
FPA fpInput2 = new FPA(inputArray2);

On “transforme” les tableaux afin qu’il soit utilisables pour la cible.

FPA fpStacked = PA.Add(fpInput1, fpInput2);
FPA fpOutput = PA.Divide(fpStacked, 2);

On crée les opérations entre les tableaux. On utilise le résultat de l’addition pour la division. Cela génère l’arbre d’expression en copie d’écran plus haut.

evalTarget.ToArray(fpOutput, out stackedArray);

C’est la partie la plus importante du programme. En effet, jusqu’à maintenant nous avons préparé l’arbre d’expression et les données et c’est sur cette méthode, que l’arbre est évalué et que la cible (GPU dans notre cas) réalise les opérations. Le résultat est ensuite convertir et stocké dans un tableau de float .NET.

  • Et les performances alors ?

Il  est vrai que tout ça est un peu contraignant alors on est en droit de se demander si les performances sont aux rendez vous. Sur l’exemple ci dessus par exemple sur ma machine, les performances étaient meilleures en .NET qu’en GPU. La raison est, selon moi, que le temps de copie et d’éxécution est long pour le peu d’opérations exécutées.

Pour des données plus claires sur le domaine, je vous renvoie sur le PDF de MS Research parlant des performances : Document PDF . On peut voir que les performance peuvent être jusqu’à 70x plus rapide sur certains code par rapport au C++ sur CPU.

Le kit de développement contient aussi des samples (dont un jeu de la vie) qui permettent un peu de se rendre compte de ces performances.

Il est nécessaire de rappeler que ce projet est en alpha et donc loin d’être optimisé.

  • Conclusion

Ce projet est plein de promesses même s’il reste en dessous de technologies comme CUDA qui se rapproche du langage C alors que Accelerator est basé sur des arbres d’expressions / opérations mathématiques de bases. De plus Accelerator étant basé sur le parallélisme, il est nécessaire de rappeler que, comme PFX, tous les algorithmes ne sont pas faits pour ces technologies.

  • Quelques liens utiles :

Site du projet sur Microsoft Research (Attention c’est la v1 sur ce site)

Téléchargement de la v2

Article d’un français sur le sujet : Accelerator & F# / Accelerator : F# vs C#

Publicités

26/04/2010

[Nouveautés .NET 4] Task Parallel Library : Gestion des architectures multicores/multiprocesseurs

Filed under: .NET, C# 4, Débutant, Hors Catégorie, Optimisation — Étiquettes : , , , , , , , , — sebastiencourtois @ 09:55

De nos jours, on ne trouve plus dans le marché de la vente d’ordinateurs que des architectures avec des processeurs double,quad,octo…. cores (et parfois même multi processeurs). Malgré cette débauche de puissance de calculs, il est triste à noter que nos applications ne sont, pour la plupart, pas tellement plus rapide qu’avant. La cause :

  • Les développeurs ne savent généralement pas coder des applications gérant plusieurs threads

PFX1 Ca fait mal d’avoir 4 coeurs et d’en utiliser qu’un seul… à moitié !!!

C’est un fait. Lorsque l’on apprend la programmation, on nous enseigne généralement beaucoup de choses mais rarement comment faire des applications gérant correctement plusieurs threads. L’une des raison est simple : Les API de développement sont généralement complexes et demande des efforts d’architecture/programmation/débug afin de rendre le tout utilisable. WaitHandle,Process,Mutex,lock,Interlocked … autant de mot clé et concepts barbare que le développeur doit maitriser afin de pouvoir décupler la puissance de ses applications. “On a déja du mal à faire une application correspondant aux spécifications dans le temps imparti, on va pas en plus passer des heures à gérer les multi coeurs”.

Partant de ce constat, Microsoft a créé Parallel FX afin d’aider le développeur à gérer facilement les threads au sein de ses applications.

  • Organisation de TPL

TPL se compose de 3 namespaces NET 4 :

  1. System.Threading.Tasks : Ce namespace contient la base de Task Parallel Library (TPL), il contient la classe principale de la technologie : la classe Tasks (sorte de “super Thread”)
  2. System.Collections.Concurrent : Ce namespace contient les collections pouvant être utilisé dans des contextes multithreads (thread safe).
  3. System.Linq : LINQ a été amélioré pour intégrer des aides au multithreading. Connu sous le nom de PLINQ (Parallel LINQ), il permet de paralléliser certaines étapes des requêtes LINQ afin de les rendre plus rapide.
  • Le namespace System.Threading.Task

Ce namespace contient une classe Task. Cette classe représente une opération asynchrone. Ainsi on défini notre tache et, lorsqu’on la démarre, celle-ci est assigné par le TaskScheduler à un des coeurs disponibles de la machine. Ce système marche sous la forme d’une  file (first in / first out) de priorité (il y a des VIP :)). Ce Taskscheduler est aussi capable de distribuer un grand nombre de tâches aux différentes cœurs et de balancer la charge si un cœur est plus occupé qu’un autre (un peu à l’instar des load balancing dans les serveurs web).

La création et l’utilisation des Task est simple :

Task t1 = Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < 200; i++)
        {
            if(i % 2 == 0)
                Console.WriteLine(i+" ");
        }
    });

Task t2 =new Task(() =>
{
    for (int i = 0; i < 200; i++)
    {
        if (i % 2 == 1)
            Console.WriteLine(i + " ");
    }
});
t2.Start();

Il y a,au moins, deux méthodes pour créer une Task. La première en utilisant une fabrique d’objet fournit par la classe Task. La seconde est de créer soi même une classe Task et de lui fournir la méthode à éxécuter puis de le lancer en appelant la méthode Start (un peu de la même façon qu’un thread. Il est à noter que l’on ne peut fournir de méthode retournant de valeurs. De plus, la méthode doit être sans paramètre ou alors avec un seul paramètres de type Object (Action, Action<object>).

L’autre nouveautés très sympathique est la possibilité de paralléliser facilement ses boucles. Si on part des exemples suivants :

foreach (string filename in Directory.GetFiles(MyDirectory, "*"))
    Console.WriteLine(filename);

for (int i = 0; i < 200; i++)
{
    if (i % 2 == 1)
        Console.WriteLine(i + " ");
}

Il est possible d’exécuter les itérations en parallèle en remplacement le for et le foreach par Parallel.For et Parallel.Foreach.

Parallel.ForEach(Directory.GetFiles(MyDirectory, "*"), (filename) =>
{
    Console.WriteLine(filename);
});

Parallel.For(0, 200, (i) =>
{
    if (i % 2 == 1)
        Console.WriteLine(i + " ");
});

Et c’est tout !!! Le taskscheduler s’occupe de la répartition des opérations sur le processeur. On peut ainsi passer d’un code compliqué de copie de fichier multithreadée :

public void SpeedCopyFolder(DirectoryInfo source, DirectoryInfo target, bool overrideExisting)
{
    using (ManualResetEvent mre = new ManualResetEvent(false))
    {
        int threadCount = 0;
        foreach (FileInfo fi in source.GetFiles())
        {
            Interlocked.Increment(ref threadCount);
            // Create a thread for each file
            FileInfo file = new FileInfo(fi.FullName); // Created for the delegate scope
            ThreadPool.QueueUserWorkItem(delegate
            {
                if (File.Exists(Path.Combine(target.ToString(), file.Name)) && !overrideExisting)
                    return;
                fi.CopyTo(Path.Combine(target.ToString(), file.Name), overrideExisting);
                if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
            });
        }
        if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
        mre.WaitOne();
    }
}

à un code beaucoup plus simple :

public static void CopyFiles(string fromFolder, string toFolder)
{
    Parallel.ForEach<string>(Directory.EnumerateFiles(fromFolder, "*"), f =>
    {
        File.Copy(f, toFolder + @"\" + Path.GetFileName(f), true);
    });
}

  • Le namespace System.Collections.Concurrent

Ce namespace contient des collections utilisables dans les cas de multithreading. Ces collections utilisent une interface créé spécialement pour l’occasion : IProducerConsumerCollection<T> qui permet la manipulation de ces collections.

Les collections ainsi ajoutés :

Les collections classiques fonctionnent toujours avec les tasks (sur les exemples que j’ai pu essayer.). Je pense qu’il doit y avoir des différences de performances/d’utilisations possibles qui doivent indiquer quel type utiliser. Je publierais surement un post quand j’en saurais un peu plus sur le sujet.

  • Parallel LINQ

PLINQ est une technologie qui permet de paralléliser vos requêtes LINQ.

Prenons l’exemple suivant : On souhaite récupérer les fichiers dont la première lettre est un ‘a’ puis classer ce résultat selon la dernière lettre du nom du fichier. On aurait là requête LINQ suivante  :

Directory.GetFiles(MyDirectory, "*").Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

Si l’on étudie un peu la façon dont la requête s’éxécute, LINQ va d’abord parcourir la liste des fichiers en recherche de ceux commençant par a puis il va les classer par ordre. Cela est logique pour un traitement mono-core. Toutefois, on pourrait imaginer faire le classement en parralèle du Where. Ainsi lorsque le Where trouve un élément commençant par un ‘a’, il est envoyé au Orderby (se trouvant dans un autre thread) qui réalise un classement en temps réel. Vous devez vous dire que cela doit être complexe à coder. Pourtant voici là façon de faire.

Directory.GetFiles(MyDirectory, "*").AsParallel().Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

Il suffit de rajouter un AsParallel() afin que toutes les expressions suivantes s’exécutent “simultanément”. “C’est pas wonderful ?!?!”

  • C’est bien beau tout ça mais … et les performances ?

Sur un exemple qui est très parlant pour PLINQ (BabyNames disponible sur le training Kit VS 2010), j’obtiens les résultats suivants (sur une machine 4 cœurs avec une sélection du nombre de cœurs utilisés pour l’application).

    Nombre de cœurs LINQ (temps en secondes) PLINQ (Temps en secondes) Amélioration
    1 37,71 38,57 x 0.98
    2 37,46 20,68 x 1.81
    3 37,14 12,82 x 2.90
    4 37,36 10,60 x 3.53

Remarque : La raison pour laquelle on n’arrive pas à 4x plus de performance avant un quad core est tout simplement du au fait que l’OS plus d’autres applications tourne en arrière plan avec des pics imprévisibles. De plus, les mécanismes de synchronisation entre thread prend aussi un peu de la puissance de calculs.

Pour terminer, un petit graph qui fait plaisir :

pfx2

  • Conclusion

Cet technologie va surement réconcilier de nombreux développeurs avec la gestion des applications multithreadés. Toutefois, il est à noter que, dans certains cas, le parallélisme peut nuire au performance de l’application (car cela implique toujours en tâches de fond des techniques de synchronisation). A utiliser avec parcimonie et toujours en testant les performances avec et sans TPL.

Remarque : TPL est disponible en C++ aussi.

Note : Ce post a été fortement inspiré du post de Gal Ratner : http://galratner.com/blogs/net/archive/2010/04/24/a-quick-lap-around-net-4-0-s-parallel-features.aspx

Propulsé par WordPress.com.

%d blogueurs aiment cette page :