11. Exemple de boucle C
Calcul de sinus & cosinus en boucle
void scos(
const double* params,
double* out,
int length
){
for (int i = 0; i < length; i++)
{
double param = params[i];
out[2 * i] = sin(param);
out[2 * i + 1] = cos(param);
}
}
11
12. Kernel OpenCL équivalent
Calcul de sinus & cosinus parallèle (simplifié)
#pragma OpenCL EXTENSION cl_khr_fp64 : enable
kernel void scos(
global const double* params,
global double2* out,
int length
){
int i = get_global_id(0); // indice parallèle
if (i >= length) return; // dépassements possibles
{
double param = params[i];
double c, s = sincos(param, &c); // calcul sin & cos rapide
out[i] = (double2)(s, c);
}
}
12
13. Kernels
• Dialecte du C
• Fonctions math vectorielles (SIMD sur float4, int8…)
• Code connait sa position d’exécution
• Lecture / écriture buffers / images
• Synchronisation avec voisins (working group)
13
17. Sous le capot…
OpenCL bindings générés par JNAerator :
BridJ fournit la “colle” à l’exécution
17
18. Plus que des bindings
• Gestion mémoire débrayable
• Cache automatique des binaires
• Inclusion depuis URLs
• Générateur de wrappers typés
• Parallel Reduction (min, max, sum, product)
18
19. Plus que des bindings (2)
• FFT complexe d'un tableau de primitives :
DoubleFFTPow2 fft = new DoubleFFTPow2();
double[] transformed = fft.transform(data);
• Chargement (+ conversion) d'une image :
CLImage2D img = context.createImage2D(Usage.Input, ImageIO.read(file));
• Multiplication de matrices UJMP :
Matrix a = new CLDenseFloatMatrix2D(n, m),
Matrix b = new CLDenseFloatMatrix2D(m, q);
Matrix c = a.times(b);
19
21. Les pièges d’OpenCL
• Coût des transferts RAM / GPU
• Contextes mixtes CPU / GPU :
seulement sur APU AMD
• Branchements conditionnels lents sur GPU
• Besoin de tests d’arrêt
21
22. Encore des pièges
• Peu de formats d’image garantis
• Endianness parfois différente entre hôte & device
• Apple ne supporte qu’OpenCL 1.0
22
23. Chasse à la performance sur GPU
• Groupes d’exécution : memoire locale, barrières…
• Répartition de charge multi-GPU
• Exécutions pyramidales pour réductions associatives
23
24. JavaCL 1.0.0-RC2
• 32 & 64 bits / Windows, Linux, MacOS X
• NVIDIA, AMD, Intel & Apple
• OpenCL 1.0 / 1.1
(1.2 peu répandu)
24
25. Rendre OpenCL plus facile ?
• Éviter d’écrire les kernels en C
• Cacher l’asynchronicité
• Accepter performance sub-optimale
(> 10x plus rapide que Java)
25
26. OpenCL “traduit”
• Apapapi (AMD)
• Java
• Cache beaucoup (trop ?) de détails
• ScalaCL (votre serviteur)
26
29. Quelques lignes de Scala…
val intervalle = (0 until 100000)
val cosSinCol = intervalle.map(i => {
val f = 0.2
(cos(i * f), sin(i * f))
})
val sum = cosSinCol.map({ case (c, s) => c * s }).sum
29
30. Les mêmes lignes sur GPU
import scalacl._
implicit val context = Context.best(DoubleSupport)
val intervalle = (0 until 100000).cl
val cosSinCol = intervalle.map(i => {
val f = 0.2
(cos(i * f), sin(i * f))
})
val sum = cosSinCol.map({ case (c, s) => c * s }).sum
30
33. Des collections asynchrones
• Contrainte : les collections Scala ont l’air synchrones
• Principe : single writer, multiple reader
33
34. Des collections asynchrones
val tab = Array(1, 2, 3, 4).cl
val resultat = tab.map(x => x * 2).map(x – 5)
// resultat pas encore calculé
resultat(i) = 10 // attente avant écriture
34
36. Traduire Scala en OpenCL ?
• Limitations (pas de classes)
• Élimination des tuples
• Élimination du code “objet”
36
37. Ceci est du code objet
for (i <- 0 until 100)
{
…
}
37
38. Une lambda peut en cacher une autre
(0 until 100).foreach(i =>
{
…
})
38
39. Des boucles 5x plus rapides
(0 until 100).foreach(i => var i = 0
{ while (i < 100) {
… …
}) }
39
40. Aller plus vite : flot d’opérations
col.map(f).filter(g).map(h).reduceLeft(i)
•Collections intermédiaires + lambdas
•f, g, h sans effet de bord ?
40
Bonjour à tous Aujourd’hui je suis venu vous parler de GPGPU, c’est à dire de l’utilisation des cartes graphiques pour effectuer des calculs généralistes.
Les cartes graphiques haut de gamme actuelles sont de vrais monstres, Y compris par rapport aux meilleurs CPUs.
Mais en dehors des jeux dernier-cri, les cartes graphiques sont rarement exploitées en mesure avec leurs capacités
Du coup il est temps de mettre les cartes graphiques au travail ! Dans cette courte intervention je vais donc vous présenter les principes d’OpenCL, et comment l’exploiter en Java. Je terminerai en vous présentant ScalaCL, qui ouvre de nouveaux horizons aux paresseux et aux pressés
Tout d’abord un mot sur moi : Développeur C++ / Java avec un background dans la 3D et la finance. Je passe beaucoup de mon temps libre sur des projets Open-Source d’interopérabilité Java / C / C++. J’ai notamment créé JNAerator et BridJ, qui permettent d’obtenir des bindings natifs très facilement. Je me suis basé sur ces deux projets pour créér JavaCL et ScalaCL, qui poussent les limites de l’interopérabilité d’une autre manière.
Alors, pour simplifier à l’extrême, OpenCL c’est comme un OpenGL non graphique, où on ne fait que des shaders.
Pour ceux à qui OpenGL ne parle pas, On peut aussi voir ça comme un environnement d’exécution de code multi-plateforme (la ressemblance avec Java s’arrête là !)
Que gagneriez vous à utiliser OpenCL ? Rendre les parties critiques de vos programmes 10 à 100 fois plus rapides. Exploiter non seulement les GPUs, mais aussi les CPUs (bien plus efficacement qu’en Java !), en particulier en profitant au max du SSE et des multi-coeurs ca peut éventuellement accélérer la visualisation dans votre appli, partage données avec 3D
Donc qu’est-ce qu’on exécute au juste, dans OpenCL ? En gros y’a 2 types de charge utiles : Des taches, qui sont juste des fonctions appelées une fois, classiquement Des kernels, qui sont appelés des milliers / millions de fois, partiellement en parallèle, avec un indice qui varie à chaque appel
Ces tâches et ces kernels sont asynchrones & enchaînables Chaque opération renvoie un évenement de complétion, et peut attendre d’autres événements avant de s’exécuter On peut donc créer un workflow qui implique des lectures, des écritures, et des éxécutions, et être notifié quand les résultats sont prêts
Pour rendre ça un peu plus concret, Prenons un exemple de code C qui calcule des sinus et cosinus de chaque valeur d’un tableau d’entrée, Et les écrit dans un tableau de sortie 2 fois plus grand
La version OpenCL de ce code est très proche La boucle for disparait, la fonction / kernel sera appelée length fois Chaque itération pourra connaître son indice i pour savoir quelle inputs prendre et où mettre son output
Donc on écrit les kernels dans un dialecte du C Dans lequel on dispose de fonctions mathématiques sur des types vectoriels Le kernel connait sa position d’exécution en N-dimensions Et son activité principale, c’est de lire et d’écrire des données Remarquons au passage que ça peut se compliquer car plusieurs exécutions peuvent être regrouper et utiliser de la synchronisation entre eux
Ces kernels doivent être pris en charge et mis en scene par un code hôte, en C ou en Java
JavaCL nous permet d’ écrire ce code hôte facilement Ce sont historiquement les premiers bindings java On a pas mal travaillé sur la doc, et il y a meme un chapitre d’un livre qui lui est consacré
Faisons donc une pause dans le bla-bla, regardons du code
Voila pour un exemple simple. Au passage j’en profite pour dire que JavaCL utilise des bindings autogeneres par JNAerator, Et qu’ils sont linkés dynamiquement par BridJ, qui est compétitif avec JNI.
Il faut savoir que JavaCL fournit plus que de simples bindings
JavaCL fournit en outre quelques briques utiles, telles que Des transformees de fourier rapides, Des conversions de format d’images Du calcul matriciel
Et un éditeur de kernels interactif, que je vais vous montrer rapidement
Bon, maintenant que vous pensez qu’OpenCL est trop facile pour etre vrai, passons en revue certains de ses pieges
Peu de formats d’images garantis, mais JavaCL fait de son mieux ATI Radeon = Big Endian X86 = little endian
Enfin, pour obtenir du code optimal, il faut un peu d’huile de coude
Pour terminer sur JavaCL, Dispo sur les plateformes les plus répandues Compatible avec 1.1 (1.2 pas sous mac)
OpenCL est assez facile à utiliser, mais on aimerait parfois… … Quitte à accepter une performance sub-optimale
Il y a au moins 2 solutions pour la JVM : Aparapi cache cout des transferts de memoire ScalaCL n’a pas (encore) le soutien d’un géant de l’IT
import scalacl._ import math._ implicit val context = Context.best val r = (0 until 100000).cl
Alors comment ça marche ? Plugin de compilateur Collections stockées sur GPU
Y’a une particularité : La contrainte… Donc on respecte le principe Chaque collection maintient une liste d’opérations de lecture et d’écriture en cours
Du coup, dans cet exemple : on obtient un resultat pas encore calculé Et on doit attendre avant de pouvoir y écrire
Une autre particularité est evidemment la conversion de Scala a OpenCL : y’a qq differences
On accepte donc certaines limitations, Et on élimine tout ce qui ne fait pas C : les tuples et les objets en general
Quand je dis objet en general…
Ce code utilise en fait secretement une closure, qui le rend tres lent
En réecrivant les boucles par leur while equivalent, on obtient du code convertible en C Et on rend le scala plus rapide en general !