• Aucun résultat trouvé

4.2 Allocation de ressources sur GPU

4.2.2 Mod` eles de calcul appropri´ es

4.2.2.1 Direct

Les outils de nVidia permettant d’utiliser leur GPUs en mode compute offrent deux niveaux

d’abstraction : le jeu d’instructions PTX (Parallel Thread Execution) [73], permettant d’exprimer

les noyaux en se rapprochant au maximum de la s´equence d’instructions ´evalu´ee par les

multipro-cesseurs

4

, ainsi qu’un langage d´eriv´e du C, CUDA, et permettant d’exprimer les noyaux comme

le code hˆote dans un mˆeme formalisme.

Dans un premier temps, nous allons mimer ces deux niveaux d’abstraction. L’int´erˆet en est

multiple :

– Il est int´eressant d’offrir `a l’utilisateur la possibilit´e d’utiliser ces deux niveaux d’abstraction.

C’est naturel pour le code CUDA, qui est en soi assez facile `a ´ecrire et convient dans la grande

majorit´e des projets. La n´ecessit´e de programmer directement en PTX est plus rare, mais

peut n´eanmoins se justifier dans les cas o`u le nombre de registres utilis´es par un noyau est

un aspect critique du point de vue des performances.

– Il est possible de vouloir int´egrer du code PTX ponctuellement dans un projet ´ecrit par

ailleurs `a un niveau d’abstraction plus ´elev´e (un peu comme on ferait avec le mot-cl´e asm

en C) ; ceci n’est pas possible depuis CUDA, mais rien ne nous empˆeche de rajouter cette

possibilit´e dans les MoCs que nous ´ecrirons.

– Enfin, les moteurs associ´es `a ces MoCs pourront servir de back-end `a des MoCs de niveau

d’abstraction plus ´el´ev´e

Comme on peut le voir sur le listing de la figure 4.11, imiter l’assembleur PTX est assez simple,

et permet de se persuader une fois de plus des capacit´es de Haskell en terme de d´efinition d’EDSL

(Embedded Domain Specific Language). On d´efinit d’abord quelques types concrets permettant de

repr´esenter les objets manipul´es dans le MoC PTX, `a savoir les valeurs imm´ediates

(correspon-dant aux litt´eraux entiers et flottants), les opcodes des instructions, les modificateurs

(permet-tant d’expliciter le comportement exact d’une instruction, par exemple en sp´ecifiant les types et

tailles des op´erandes, le mode d’arrondi souhait´e, les contraintes d’alignement ; la norme PTX

d´efinit rigoureusement quels modificateurs s’appliquent `a quelles instructions) et les op´erandes.

Le mod`ele de calcul permet simplement de rajouter une instruction (instr), d’appliquer un ou

plusieurs pr´edicats `a l’´evaluation d’une instruction (at), et de d´eclarer des identifiants

correspon-dant aux ressources du GPU : registres, m´emoire locale (passage de param`etres au noyau), m´emoire

partag´ee (scratchpad commun `a tous les processeurs d’un multiprocesseur), m´emoire globale et

m´emoire des constantes. Enfin, nous d´efinissons et encourageons l’utilisateur `a employer des alias

pour les instructions. Ces alias utilisent le pattern matching pour s’assurer que les modificateurs

utilis´es avec chaque instruction sont ceux qu’elle admet. En cas d’erreur, le programme ´echouera

`

a l’´elaboration (ce qui est ´equivalent `a un ´echec de la compilation avec ptxas, l’assembleur ptx

fourni par nVidia). Bien que assez simple dans sa structure, ce code d´efinit un sous-langage dont

l’expressivit´e est quasi ´equivalente `a celle de l’ISA PTX dans sa version 1.2 [70]. La figure 4.12

permet de comparer un design exprim´e dans le MoC PTX `a la portion correspondante du fichier

.ptx ´equivalent, tel qu’on le fournirait `a ptxas. On peut constater que notre EDSL ne souffre pas

de lourdeurs ou d’aspects contre-intuitifs sur le plan syntaxique.

Dans l’absolu, on aurait pu faire la mˆeme sorte de travail pour ´ecrire un MoC mimant CUDA

(c’est `a dire un sous-ensemble du C++ avec quelques nouveaux mots cl´e et ´el´ements

syntax-iques propres `a la programmation sur GPU). Cela dit, on peut tout aussi bien repartir du MoC

imp´eratif d´efini `a la section 3.2. Il suffira d’ajouter des primitives pour g´erer les aspects propres

`

a la programmation sur GPU. On utilisera un MoC pour l’hˆote (capable d’allouer de la m´emoire

et de lancer des noyaux) et un pour les noyaux (capable d’utiliser les ressources propres du GPU,

de synchroniser les threads d’un mˆeme warp, d’embarquer du code PTX, etc.). La structure du

code correspondant, ainsi qu’un exemple de design associ´e, est illustr´ee sur les figures 4.13 et

4. Il est `a noter que lebytecodeobtenu par compilation d’un fichier PTX est ensuite traduit par le pilote de la carte graphique en code machine effectivement ex´ecut´e sur la carte ; en cela il ne s’agit pas vraiment de code assembleur

1 −− G e s t i o n d e s v a l e u r s imm´e d i a t e s −−

2 data PtxConst a = PtxConst a | PtxArray [ PtxConst a ]

3 data PtxImm = PtxImmF ( PtxConst Float) | PtxImmI ( PtxConst Integer) 4

5 c l a s s PtxImmediate a where toPtxImm : : a −> PtxImm

6 instance PtxImmediate Float where toPtxImm v a l = PtxImmF ( PtxConst v a l ) 7 instance PtxImmediate Integer where toPtxImm v a l = PtxImmI ( PtxConst v a l ) 8 instance PtxImmediate [Float] where

9 toPtxImm v a l s = PtxImmF ( PtxArray (map PtxConst v a l s ) ) 10 instance I n t e g r a l a => PtxImmediate [ a ] where

11 toPtxImm v a l s = PtxImmI ( PtxArray (map PtxConst (map toInteger v a l s ) ) ) 12

13 −− MoC e t donn ´e e s a s s o c i ´e e s −− 14 data PtxOpcode

15 = Abs | Add | AddC| And | Atom| Bar | Bra | BrkPt | C a l l | CNot| Cos

16 | Cvt | Div | Ex2 | E x i t | Fma | Ld | Lg2 | Mad | Mad24| Max | Membar

17 | Min | Mov | Mul | Mul24| Neg | Not | Or | PMEvent| Rcp | Red | Rem

18 | Ret | R s q r t| Sad | S e l p | S e t | S e t p| ShL | ShR | S i n | S l c t| S q r t

19 | S t | Sub | SubC| Tex | Trap| Vote| Xor

20 data P t x M o d i f i e r

21 = S8 | S16 | S32 | S64 −− t y p e : e n t i e r s s i g n ´e s

22 | U8 | U16 | U32 | U64 −− t y p e : e n t i e r s non−s i g n ´e s

23 | F16 | F32 | F64 −− t y p e : f l o t t a n t s 24 | B8 | B16 | B32 | B64 −− t y p e : b i t s ( c .−`a−d . non t y p ´e ) 25 | Pred −− t y p e : p r e d i c a t s 26 | V2 | V4 −− t y p e : v e c t e u r s 27 | Eq | Ne | Lt | Le | Gt | Ge −− mode : o p a r ´e t i o n de c o m p a r a i s o n 28 | Rn | Rz | Rm | Rp −− mode : a r o n d i d e s f l o t t a n t s

29 | Rni | R z i | Rmi | Rpi −− mode : a r o n d i d e s e n t i e r s

30 | S a t −− mode : p a s d ’ o v e r f l o w ( ” add ” , e t c . )

31 | Cc −− mode : ´e c r i r e dans l e r e g i s t r e CC

32 | Hi | Lo | Wide −− mode : comportement de ” mul ”/”mad”

33 | A l i g n Int −− mode : a l i g n e m e n t du mot ´e c r i t

34 | F u l l | Approx −− mode : p r ´e c i s i o n d e s o p s . f l o t t a n t e s

35 | Ftz −− mode : f l o t t a n t s s o u s−normaux a n n u l ´e s

36 | TexRef | SampleRef | S u r f R e f −− mode : i d e n t i f i a n t s d e s t e x t u r e s

37 | Dim1D | Dim2D | Dim3D −− mode : d i m e n s i o n s d e s t e x t u r e s

38 | Const | G l o b a l | L o c a l −−m´e m o i r e s 39 | Param | S h a r e d −−m´e m o i r e s 40 deriving Eq 41 data PtxOperand 42 = I d e n t String −− i d e n t i f i a n t 43 | Reg Int −− r e g i s t r e o r d i n a i r e 44 | P r e d i c a t e Int −− r e g i s t r e p r e d i c a t

45 | Val PtxImm −− a d r e s s a g e imm´e d i a t

46 | D i r e c t Integer −− a d r e s s a g e d i r e c t

47 | I n d i c e c t Integer −− a d r e s s a g e i n d i r e c t

48

49 c l a s s E x p l o r a b l e em => Ptx em where

50 i n s t r : : [Int] −> PtxOpcode −> [ P t x M o d i f i e r ] −> [ PtxOperand ] −> em ( ) 51 a t : : [ PtxOperand ] −> em ( ) −> em ( )

52 namedReg : : [ P t x M o d i f i e r ] −> String −> em ( PtxOperand )

53 namedLocal : : [ P t x M o d i f i e r ] −> [Int] −> String −> em PtxOperand 54 namedShared : : [ P t x M o d i f i e r ] −> [Int] −> String −> em PtxOperand

55 namedGlobal : : PtxImmediate a =>

56 [ P t x M o d i f i e r ] −> [Int] −> a −> String −> em PtxOperand

57 namedConst : : PtxImmediate a =>

58 [ P t x M o d i f i e r ] −> [Int] −> a −> String −> em PtxOperand 59

60 −− I n s t r u c t i o n s PTX−−

61 abs [ t y ] d s t op | t y ‘elem‘ [ S16 , S32 , S64 ] 62 = i n s t r [ ] Abs [ t y ] [ d s t , op ]

63 add [ t y ] d s t op1 op2 | t y ‘elem‘ [ U16 , U32 , U64 , S16 , S32 , S64 ] 64 = i n s t r [ ] Add [ t y ] [ d s t , op1 , op2 ]

65 add [ Sat , S32 ] dst@ ( Reg ) op1@ ( Reg ) op2@ ( Reg ) 66 = i n s t r [ ] Add [ Sat , S32 ] [ d s t , op1 , op2 ] 67 −− e t c . . .

1 d e s i g n = do 2 e n t r e e <− namedGlobal [ F32 ] ” x ” 3 r e s u l t a t <− namedGlobal [ F32 ] ” r e s u l t a t ” 4 x <− namedReg [ F32 ] ” x ” 5 y <− namedReg [ F32 ] ” y ” 6 z <− namedReg [ F32 ] ” z ” 7 l d [ G l o b a l , F32 ] x ( I n d i r e c t e n t r e e 0 ) −− x := [ e n t r e e ] 8 mov [ F32 ] y 0 x 4 0 e 0 0 0 0 0 −− y := 7 . 0 9 mad [ F32 ] z x x y −− z := x ˆ2 + y 10 s t [ G l o b a l , F32 ] ( I n d i r e c t r e s u l t a t 0 ) z −− [ r e s u l t a t ] := z 11 e x i t −− f i n du noyau 1 . e n t r y d e s i g n 2 { 3 . g l o b a l . f 3 2 e n t r e e ; 4 . g l o b a l . f 3 2 r e s u l t a t ; 5 . r e g . f 3 2 x , y , z ; 6 l d . g l o b a l . f 3 2 x [ e n t r e e ] ; // x := [ e n t r e e ] 7 mov . f 3 2 y , 0 f 4 0 e 0 0 0 0 0 ; // y := 7 8 mad . f 3 2 z , x , x , y ; // z := x ˆ2 + y 9 s t . g l o b a l . f 3 2 [ r e s u l t a t ] , z ; // [ r e s u l t a t ] := z 10 e x i t ; // f i n du noyau 11 }

Figure4.12 – Programme trivial exprim´e dans le MoC PTX (au dessus), compar´e au fichier .ptx

correspondant

4.14. (Il aurait fallu faire quelques accommodations li´ees `a l’utilisation du type Transfer dans

Imperativepour que les primitives de celui-ci puissent vraiment ˆetre utilis´ees de fa¸con

transpar-ente. Par exemple, on aurait pu rendre la classeImperativeparam´etrable par le type monadique

repr´esentant les expressions, de sorte `a pouvoir passer PtxOperand comme arguments `a #=, aux

op´erateurs arithm´etiques, etc.) Pour rester concis, on a suppos´e dans le design donn´e en exemple

que les fonctions r´ealisant le traitement lui-mˆeme (calculerDonnees, chargerBuf, traiterBuf,

ecrireBuf) ont ´et´e pr´ealablement ´ecrites. On voit l`a aussi que le code r´esultant est assez intuitif.

Une des parties les plus int´eressantes des mod`eles de calcul propos´es est la fonctionkernel, qui

prend en argument une fonction d´ecrivant le noyau, et

renvoie

(place dans la monade) une

fonction permettant de le lancer (`a partir de la taille de la grille et des blocs, ainsi que des

argu-ments propres au noyau). On notera ´egalement que la fa¸con dont les pointeurs sont g´er´es (avec

une distinction claire entre les espaces m´emoire hˆote et GPU) empˆeche les probl`emes caus´es par

le d´er´ef´erencement de pointeurs vers la m´emoire du GPU depuis l’hˆote, ouvice versa (ce qui est

encore pire). Il s’agit d’erreurs que nvcc (le compilateur CUDA) est incapable d’intercepter `a la

compilation avec son syst`eme de typage. Cet aspect est d’autant plus important que les erreurs

de ce genre sont souvent graves, se traduisant au mieux par une erreur de segmentation, au pire

par le plantage du driver de la carte vid´eo (qui, tournant `a un niveau de privil`eges ´elev´e, peut

entraˆıner le reste du syst`eme avec lui).

4.2.2.2 Avec ordonnanceur

Les mod`eles de calcul vus pr´ec´edemment peuvent ˆetre ´etendus afin de proposer des primitives

de communication dont CUDA ne dispose pas de fa¸con native (et qu’il n’est pas possible d’y d´efinir

facilement). Nous nous fixons l’objectif suivant : d´efinir les fonctionssendetrecieve, permettant

`

a un grille de threads CUDA d’envoyer et/ou de recevoir des donn´ees via la m´emoire globale du

GPU vers/depuis une autre grille de threads. Ces fonctions seront dans le principe similaires `a

MPI SendetMPI Recvde la biblioth`eque MPI. Elles seront forc´ement bloquantes.

ordon-1 −−−−−−−−−−−−−−−−−−−−−−−−−−− 2 −− G e s t i o n d e s p o i n t e u r s −− 3 −−−−−−−−−−−−−−−−−−−−−−−−−−−

4 data H o s t P t r = H o s t P t r [ P t x M o d i f i e r ] Integer −−m´e moire hˆo t e ( t y p e , a d d r e s s e )

5 data D e v i c e P t r = D e v i c e P t r [ P t x M o d i f i e r ] Integer −−m´e moire GPU g l o b a l e ( t y p e , a d d r e s s e )

6 c l a s s P t r a where r e s o l v e P t r : : a −> Either H o s t P t r D e v i c e P t r 7 instance P t r H o s t P t r where r e s o l v e P t r x = Left x

8 instance P t r D e v i c e P t r where r e s o l v e P t r x = Right x 9 10 −−−−−−−−−− 11 −− MoCs −− 12 −−−−−−−−−− 13 c l a s s I m p e r a t i v e em => H o s t I m p e r a t i v e em where 14 h o s t C a l l o c : : Integer −> [ P t x M o d i f i e r ] −> em ( H o s t P t r ) 15 d e v i c e C a l l o c : : Integer −> [ P t x M o d i f i e r ] −> em ( D e v i c e P t r ) 16 memcpy : : ( P t r a , P t r b ) => a −> b −> Integer −> em ( ) 17 memset : : P t r a => a −> Char−> Integer −> em ( ) 18 f r e e : : P t r a => a −> em ( ) 19 k e r n e l : : ( f o r a l l em ’ . D e v i c e I m p e r a t i v e em ’ => [ D e v i c e P t r ] −> em ’ ( ) ) 20 −> em ( [Integer] −> [Integer] −> [ D e v i c e P t r ] −> em ( ) ) 21 t e x t u r e : : [ P t x M o d i f i e r ] −> [Integer] −> 22 ( f o r a l l em ’ . D e v i c e I m p e r a t i v e em ’ => 23 em ( D e v i c e P t r , [ PtxOperand ] −> PtxOperand −> em ’ ( ) ) ) 24 −− e t c . . . 25 26 c l a s s I m p e r a t i v e em => D e v i c e I m p e r a t i v e em where 27 s h a r e d : : Integer −> [ P t x M o d i f i e r ] −> em PtxOperand 28 r e g i s t e r : : [ P t x M o d i f i e r ] −> em PtxOperand 29 t h r e a d I d x : : em [ PtxOperand ] 30 b l o c k I d x : : em [ PtxOperand ] 31 s y n c t h r e a d s : : em ( ) 32 ptx : : ( f o r a l l em ’ . Ptx em ’ => em ’ ( ) ) −> em ( ) 33 −− e t c . . .

Figure4.13 – Mod`eles de calcul pour la programmation imp´erative sur GPU

1 d e s i g n ’ : : H o s t I m p e r a t i v e em => em ( ) 2 d e s i g n ’ = do 3 k e r 1 <− k e r n e l $ \[ d o n n e e s E n t r e e , d o n n e e s S o r t i e ] −> do 4 bIdx <− b l o c k I d x 5 t I d x <− t h r e a d I d x 6 b u f <− s h a r e d 1024 [ F32 ] 7 c h a r g e r B u f b u f bIdx t I d x d o n n e e s E n t r e e 8 s y n c t h r e a d s 9 t r a i t e r B u f b u f t I d x 10 e c r i r e B u f b u f bIdx t I d x d o n n e e s S o r t i e 11 l e t g r i l l e = [ 6 4 , 6 4 ] 12 l e t b l o c = [ 3 2 , 3 2 ] 13 l e t t a i l l e = product ( g r i l l e ++ b l o c ) 14 d o n n e e s H o t e <− h o s t C a l l o c t a i l l e [ F32 ] 15 i n i t i a l i s e r D o n n e e s d o n n e e s H o t e 16 donneesGpuEntree <− d e v i c e C a l l o c t a i l l e [ F32 ] 17 d o n n e e s G p u S o r t i e <− d e v i c e C a l l o c t a i l l e [ F32 ] 18 memcpy donneesGpuEntree d o n n e e s H o t e ( t a i l l e∗4 ) 19 k e r 1 g r i l l e b l o c [ donneesGpuEntree , d o n n e e s G p u S o r t i e ] 20 memcpy d o n n e e s H o t e d o n n e e s G p u S o r t i e ( t a i l l e∗4 ) 21 s a u v e g a r d e r D o n n e e s d o n n e e s H o t e

1 data ThreadId = ThreadId Integer 2 3 c l a s s WithSendRecv em p t r where 4 s e n d : : p t r −> Integer −> em ThreadId 5 r e c i e v e : : p t r −> Integer −> em ThreadId 6 c l a s s ( WithSendRecv em D e v i c e P t r , D e v i c e I m p e r a t i v e em) => D e v i c e S u p e r t h r e a d em 7 c l a s s ( WithSendRecv em HostPtr , H o s t I m p e r a t i v e em) => H o s t S u p e r t h r e a d em 8 9 c l a s s H o s t I m p e r a t i v e em=> S c h e d u l e r em where 10 newThreadId : : em ThreadId 11 h o s t S u p e r t h r e a d : : ThreadId 12 −> ( f o r a l l em ’ . H o s t S u p e r t h r e a d em ’ => em ’ ( ) ) −> em ( ) 13 d e v i c e S u p e r t h r e a d : : ThreadId 14 −> ( f o r a l l em ’ . D e v i c e S u p e r t h r e a d em ’ => em ’ ( ) ) −> em ( )

Figure4.15 – Mod`ele de calcul pour la programmation sur GPU avec primitives de communication

entre grilles et ordonnancement

nanceur sur l’hˆote capable de lancer les grilles de threads tour `a tour, en fonction des donn´ees que

certaines attendent et que d’autres ´emettent. Chaque grille de threads devra en plus sauvegarder

son ´etat au moment de chaquerecievepour ˆetre capable de reprendre son ex´ecution au point o`u

elle s’est arrˆet´ee. Il est facile de comprendre pourquoi ce genre de chose n’est pas faisable

directe-ment et ´el´egamment avec CUDA : cela n´ecessite un support d’ex´ecution (runtime) situ´e un cran

au dessus de la couche d’abstraction que CUDA propose.

La d´efinition du mod`ele de calcul correspondant est partiellement donn´ee par la figure 4.15.

La classe WithSendRecv est destin´ee `a ˆetre commune `a tous les MoCs dans lesquels envoyer et

recevoir des donn´ees pr´esente un sens. En plus de la monade associ´ee au MoC, elle est param´etr´ee

par ptr, un type repr´esentant les pointeurs dans le MoC en question, et handle, qui y d´esigne

une entit´e capable d’´emettre ou de recevoir des donn´ees. La fonctionsendprend en argument un

pointeur vers les donn´ees `a envoyer (et leur taille) ainsi que le destinataire, tandis que recieve

a pour param`etre l’adresse et la taille du buffer o`u stocker les donn´ees re¸cues. On d´efinit le type

concret SuperthreadId, qui va nous permettre d’identifier nos threads communicants (que l’on

appelle superthreads, donc). Les MoCs utilis´es par les superthreads sont HostSuperthread et

DeviceSuperthread, et sont en fait ni plus ni moins que HostImperativeetDeviceImperative

enrichis de la classe WithSendRecv. La totalit´e de superthreads doit ˆetre d´eclar´ee dans le MoC

Scheduler. Pour chaque superthread, on allouera d’abord un identifiant vianewSuperthreadId,

avant d’en d´efinir le contenu par le biais dehostSuperthreadoudeviceSuperthread. L’int´erˆet de

proc´eder ainsi est d’utilisersendpouvoir communiquer avec des superthreads dont le contenu n’a

pas encore ´et´e d´efini, ce qui n’aurait pas ´et´e possible si on avait, par exemple, renvoy´e l’identifiant

au moment de la d´efinition.

Documents relatifs