• Keine Ergebnisse gefunden

Architektur und Programmierung von Grafik- und Koprozessoren

N/A
N/A
Protected

Academic year: 2022

Aktie "Architektur und Programmierung von Grafik- und Koprozessoren"

Copied!
33
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Koprozessoren

General Purpose Programmierung auf Grafikprozessoren

Stefan Zellmann

Lehrstuhl f¨ur Informatik, Universit¨at zu K¨oln

SS2019

(2)
(3)

Spracherweiterung / Aufrufsyntax Kernels

CUDA Compiler erweitert C++ um spezielle Syntax, um Compute Kernels aufzurufen.

_ _ g l o b a l _ _ v o i d k e r n e l () {

}

int m a i n () {

kernel < < < n u m _ b l o c k s , n u m _ t h r e a d s > > >();

}

Kernel ist Einstiegspunkt des GPU Programms (¨ahnlich wie main()f¨ur Host).

Programmausf¨uhrung kann von dort weiter verzweigen.

Globale Funktionkernel()wird von (#Bl¨ocke ×#Threads pro Block) Threads ausgef¨uhrt.

(4)

1D Grid, 1 Block, N Threads

_ _ g l o b a l _ _ v o i d s a x p y (f l o a t a , f l o a t* x , f l o a t* y ) {

int i = t h r e a d I d x . x ; y [ i ] = a * x [ i ] + y [ i ];

}

int m a i n () {

...

saxpy < < <1 , N > > >( a , x , y );

...

}

(5)

2D Grid, 1 Block, N× N Threads

_ _ g l o b a l _ _ v o i d m a t r i x _ a d d (f l o a t** a , f l o a t** b , f l o a t** c ) {

int i = t h r e a d I d x . x ; int j = t h r e a d I d x . y ;

c [ i ][ j ] = a [ i ][ j ] + b [ i ][ j ];

}

int m a i n () {

...

d i m 3 n u m _ t h r e a d s ( N , N );

m a t r i x _ a d d < < <1 , n u m _ t h r e a d s >( a , b , c );

...

}

(6)

2D Grid unterteilt in Bl¨ocke

_ _ g l o b a l _ _ v o i d m a t r i x _ a d d (f l o a t** a , f l o a t** b , f l o a t** c ) {

int i = b l o c k I d x . x * b l o c k D i m . x + t h r e a d I d x . x ; int j = b l o c k I d x . y * b l o c k D i m . y + t h r e a d I d x . y ; c [ i ][ j ] = a [ i ][ j ] + b [ i ][ j ];

}

int m a i n () {

...

// N /32 B l o e c k e in x - R i c h t u n g , M /32 in y - R i c h t u n g d i m 3 t h r e a d s _ p e r _ b l o c k (32 , 3 2 ) ;

d i m 3 b l o c k s ( r o u n d _ u p ( N / t h r e a d s _ p e r _ b l o c k . x ) , r o u n d _ u p ( M / t h r e a d s _ p e r _ b l o c k . y ));

m a t r i x _ a d d < < < blocks , t h r e a d s _ p e r _ b l o c k >( a , x , y );

...

}

(7)

Optimale Blockgr¨oße

Das ermitteln der optimalen Blockgr¨oße h¨angt von Registerzahl ab, die Kernel ben¨otigt.

Ubersetze Kernel mit¨ nvcc Option -Xptxas -v:

Kepler GK110, Maxwell, Pascal: 64k 32-bit Register File, max. 255 Register pro Thread.

(8)

Optimale Blockgr¨oße

Bestimme Blockgr¨oße mit CUDA Occupancy Calculator.xls

(9)

Threads identifizieren

In CUDA Kernels stehen eingebaute Variablen gridDim.{x|y|z}

blockIdx.{x|y|z}

blockDim.{x|y|z}

threadIdx.{x|y|z}

zur Verf¨ugung. gridDim gibt Anzahl der Bl¨ocke an. ¨Uber blockIdxwird Block identifiziert, ¨uber blockDimdessen Dimensionen und ¨uber threadIdxder lokaleThread Index im Block.

Der globale Thread Index kann mittels

u n s i g n e d i = b l o c k I d x . x * b l o c k D i m . x + t h r e a d I d x . x ; u n s i g n e d j = b l o c k I d x . y * b l o c k D i m . y + t h r e a d I d x . y ; u n s i g n e d k = b l o c k I d x . z * b l o c k D i m . z + t h r e a d I d x . z ;

ausgerechnet werden.

(10)

I Single Source Modell: Host Code und Kernel Code k¨onnen in der gleichen Compilation Unit aufgef¨uhrt werden, teilen sich

#includeDirektiven etc.

I vgl. “Kompilieren mit nvcc”, Code wird sp¨ater von CUDA Compiler aufgeteilt.

I Reduzierter C++ Sprachumfang in Kernels (Speicherallokation, Rekursion etc. nicht oder nur eingeschr¨ankt unterst¨utzt).

I Funktionen k¨onnen “wiederverwendet” werden: host &

device Funktionen.

(11)

Funktionsannotation

// F u n c t i o n can o n l y be u s e d on CPU f l o a t dot ( v e c 3 u , v e c 3 v ) {

r e t u r n u . x * v . x + u . y * v . y + u . z * v . z ; }

// F u n c t i o n can o n l y be u s e d on CPU _ _ h o s t _ _ f l o a t dot ( v e c 3 u , v e c 3 v ) {

r e t u r n u . x * v . x + u . y * v . y + u . z * v . z ; }

// F u n c t i o n can o n l y be u s e d on GPU _ _ d e v i c e _ _ f l o a t dot ( v e c 3 u , v e c 3 v ) {

r e t u r n u . x * v . x + u . y * v . y + u . z * v . z ; }

// F u n c t i o n can be u s e d on CPU and GPU

_ _ h o s t _ _ _ _ d e v i c e _ _ f l o a t dot ( v e c 3 u , v e c 3 v ) { r e t u r n u . x * v . x + u . y * v . y + u . z * v . z ; }

ausgerechnet werden.

(12)

Inlining

Herk¨ommlicher Funktionsaufruf (z. B. CPU) ⇒ Funktions-Stack, Argumente etc. werden in speziellen Registern gespeichert, werden, wenn Funktion zur¨uckkehrt, wieder freigegeben.

GPU und notorischer Registermangel⇒Funktionen fast immer inline.

Daher schlechter bis kein Support f¨ur Rekursion auf GPUs (w¨urde zu unkontrollierbarer Rekursionstiefe f¨uhren).

(13)

Generelle Empfehlungen

1.) Vermeide Divergenz, da Threads in Warp bei dynamischem Branching aufeinander warten.

2.) Koaleszierende Speicherzugriffe: Threads sollten immer von alignierten Speicheradressen lesen.

3.)Wegen Speicherzugriffslatenz: oft lohnt es sich, Berechungen immer wieder zuwiederholen,anstattErgebnisse

zwischenzuspeichern.

4.)Einmal auf der GPU, vermeide Kommunikation mit Host und f¨uhrem¨oglichst viele Berechnungen in Kernel aus.

(14)
(15)

Neben Kernels, die auf der GPU ausgef¨uhrt werden, steht Runtime Bibliothek zur Verf¨ugung, die das Interface zwischen Host und Device steuert.

CUDA Runtime Funktionen haben Pr¨afixcuda, z. B.

cudaMalloc(),cudaMemcpy()etc.

(16)

Runtime Initialisierung

Runtime wirdimplizit initialisiert. Erster CUDA Funktionsaufruf initialisiert Runtime.

I Runtime erstellt CUDA Kontextf¨ur jede installierte, CUDA-kompatible GPU.

I JIT Compilation und Laden von Device Code in GPU Speicher bei Kontexterzeugung.

I Kontext wird von allen CPU-Threads geteilt!

I cudaDeviceReset() zerst¨ort aktuellen Kontext, n¨achster Runtime Funktionsaufruf erstellt neuen Kontext.

(17)

Die meisten CUDA Runtime Funktionen geben Fehlercode zur¨uck:

c u d a E r r o r _ t err = c u d a G e t D e v i c e C o u n t ( . . . ) ; if ( err != c u d a S u c c e s s ) {

...

}

cudaGetLastError()undcudaPeekLastError(): pr¨ufe ob Kernel Fehler ausgel¨ost hat.

// Get l a s t error , r e s e t to c u d a S u c c e s s c u d a E r r o r _ t err = c u d a G e t L a s t E r r o r ();

// P e e k l a s t e r r o r w / o r e s e t

c u d a E r r o r _ t err = c u d a P e e k L a s t E r r o r ();

(18)

Achtung, Kernels werdenasynchron ausgef¨uhrt.LastError bezieht sich wom¨oglich nicht auf den richtigen Kernel⇒ f¨uge Synchronisation / Barrier ein:

// C a l l k e r n e l

kernel < < <1 , N > > >( p a r a m s );

// E r r o r s due to i n v a l i d k e r n e l c o n f i g u r a t i o n ( < < <... > > >) c u d a E r r o r _ t e r r 1 = c u d a G e t L a s t E r r o r ();

// K e r n e l e r r o r s ( e . g . out - of - b o u n d s m e m o r y a c c e s s ) c u d a E r r o r _ t err = c u d a D e v i c e S y n c h r o n i z e ();

Derartig synchronisierte Kernels laufen nicht mehr asynchron.

Fehlerbehandlung sollte nur im Debug Mode durchgef¨uhrt werden.

cudaDeviceSynchronize()ist generelles Synchronisationsprimitiv (falls Applikation Synchronisation erfordert).

(19)

cudaMalloc und cudaFree

I Wie in C/C++ muss DDR Speicher reserviert und sp¨ater wieder freigegeben werden.

I Device Zeiger: deklariere C-style raw pointer, weise mit cudaMalloc Adresse aus GPU Adressraum zu. Dieser Pointer steht auf dem Host zur Verf¨ugung, kann aber nur auf dem Device dereferenziert werden.

int* d _ p o i n t e r ;

c o n s t e x p r int N = 32;

c u d a M a l l o c (& d _ p o i n t e r , s i z e o f(int) * N );

...

c u d a F r e e ( d _ p o i n t e r );

(20)

I Bidirektionale Speichertransfers zwischen Host / Device (VRAM) und Speichertransfers zwischen VRAM

Speicherbereichen:cudaMemcpy().

I Aufruf Semantik wie ANSI-Cmemcpy, mit viertem Parameter, der Richtung angibt.

I enum cudaMemcpyKind:cudaMemcpyHostToHost(wie memcpy()),cudaMemcpyHostToDevice,

cudaMemcpyDeviceToHost,cudaMemcpyDeviceToDevice.

I VRAM wird mit Device Zeiger referenziert.

int h _ a r r a y = { 0 , 1 , 2 , 3 , 4 };

int* d _ a r r a y ;

c u d a M a l l o c (& d_array , s i z e o f(int) * 5);

c u d a M e m c p y ( d_array , h_array , s i z e o f(int) * 5 , c u d a M e m c p y H o s t T o D e v i c e );

(21)

CUDA Saxpy (1/2)

_ _ g l o b a l _ _ v o i d s a x p y (f l o a t a , f l o a t* x , f l o a t* y ) {

int i = t h r e a d I d x . x ; y [ i ] = a * x [ i ] + y [ i ];

}

int m a i n () {

c o n s t e x p r int N = ..;

f l o a t a ;

f l o a t * h_x , * h_y ;

// ... i n i t a , h_x , and h_y f l o a t * d_x , * d_y ;

s i z e _ t S = N * s i z e o f(f l o a t);

c u d a M a l l o c (& d_x , S );

c u d a M a l l o c (& d_y , S );

...

}

(22)

CUDA Saxpy (2/2)

...

c u d a M e m c p y ( d_x , h_x , S , c u d a M e m c p y H o s t T o D e v i c e );

c u d a M e m c p y ( d_y , h_y , S , c u d a M e m c p y H o s t T o D e v i c e );

saxpy < < <1 , N > > >( a , d_x , d_y );

// r e a d b a c k r e s u l t

c u d a M e m c p y ( h_y , d_y , S , c u d a M e m c p y D e v i c e T o H o s t );

c u d a F r e e ( d_x ); c u d a F r e e ( d_y );

...

}

(23)

I Steht limitiert (16 kb, 64 kb etc.) allen gemeinsam ausgef¨uhrten Threads auf SM zur Verf¨ugung.

I Keine direkte Adressierung via Zeiger, sondern spezielle Syntax in Kernel (CUDA Keyword shared ).

I Zwei Arten von Allokation: statischund dynamisch.

(24)

Statische Allokation

_ _ g l o b a l _ _ v o i d k e r n e l (int* da t a ) {

// S t a t i c s i z e s h a r e d m e m o r y _ _ s h a r e d _ _ int s h a r e d _ i n t s [ 6 4 ] ; // A c c e s s w i t h l o c a l t h r e a d ID s h a r e d _ i n t s [ t h r e a d I d x . x ]

= d a t a [ b l o c k I d x . x * b l o c k D i m . x + t h r e a d I d x . x ];

// A c c e s s to s h a r e d m e m o r y m u s t be s y n c h r o n i z e d _ _ s y n c t h r e a d s ();

// Now a c c e s s low - l a t e n c y m e m o r y ...

}

(25)

Dynamische Allokation

_ _ g l o b a l _ _ v o i d k e r n e l (int* da t a ) {

// S h a r e d memory , don ’ t s p e c i f y s i z e _ _ s h a r e d _ _ int s h a r e d _ i n t s [];

}

v o i d c a l l _ k e r n e l () {

// S p e c i f y v a r i a b l e s h a r e d m e m o r y s i z e // w h e n c a l l i n g k e r n e l ( m u s t s t i l l a d h e r e // to p l a t f o r m l i m i t s !)

kernel < < <

n u m _ b l o c k s ,

t h r e a d s _ p e r _ b l o c k s , s h a r e d _ m e m o r y _ s i z e // < < <

> > >( d a t a );

}

(26)

I Shared Memory lohnt sich, wenn Threads aus einer Warp h¨aufig auf Daten der anderen Threads in der Warp zugreifen.

I Je nach Architektur globaler Speicher gecached⇒ f¨ur trivialparallele Applikationen mit einfachen

Speicherzugriffsmustern lohnt sich Shared Memory u. Umst.

nicht.

I Matrix Operationen (z. B. DGEMM) profitieren eher von Shared Memory

I KopiereBlock in Shared Memory und bearbeite lokal.

(27)

Matrix Multiplikation mit Shared Memory C =A×B (vgl. NVIDIA Programming Guide.)

int row = t h r e a d I d x . y , col = t h r e a d I d x . x ; f l o a t cv = 0.0 f ;

for (int m = 0; m < ( A . w i d t h / B L O C K _ S I Z E ); ++ m ) { M a t r i x A s u b = G e t S u b M a t r i x ( A , b l o c k I d x . x , m );

M a t r i x B s u b = G e t S u b M a t r i x ( B , m , b l o c k I d x . y );

_ _ s h a r e d _ _ f l o a t As [ B L O C K _ S I Z E ][ B L O C K _ S I Z E ];

_ _ s h a r e d _ _ f l o a t Bs [ B L O C K _ S I Z E ][ B L O C K _ S I Z E ];

As [ row ][ col ] = A s u b [ row ][ col ];

Bs [ row ][ col ] = B s u b [ row ][ col ];

_ _ s y n c t h r e a d s ();

for (int e = 0; e < B L O C K _ S I Z E ; ++ e ) cv += As [ row ][ e ] * Bs [ e ][ col ];

_ _ s y n c t h r e a d s ();

}

C [ row ][ col ] = cv ;

(28)

Matrix Multiplikation mit Shared Memory

I 2D Grid, Idx.xf¨ur Zeile in Matrix A,Idx.y f¨ur Spalte in Matrix B.

I Iteriere ¨uber Blockgr¨oße, kopiere Submatrizen f¨ur ganzen Block in Shared Memory.

I Skalarproduktionoperation wird von jedem Thread in Shared Memory durchgef¨uhrt.

I Vor und nach Skalarprodukt m¨ussen Threads synchronisiert werden, damit nicht schon andere Threads aus Warp anderen Block in Shared Memory laden.

(29)

Matrix Multiplikation mit Shared Memory

Asub0 Asub1 Bsub1

Bsub0

BLOCK_SIZE BLOCK_SIZE

BLOCK_SIZE BLOCK_SIZE BLOCK_SIZE

BLOCK_SIZEBLOCK_SIZE

Global Thread ID y

Global Thread ID x

A B

C

Warp

Warp

(30)

I Read-Only Speicherbereich.

I Dedizierter Cache nur f¨ur Texturen.

I Dedizierte Hardware f¨ur Filtering (lineare Interpolation).

I Texturspeicher nicht direkt adressierbar, Verwaltung ¨uber spezielle Texturobjekte.

(31)

I Seit Nvidia Kepler Architektur (2012): Bindless Texturen und Texture Objects

I Texturen k¨onnen relativ flexibel erzeugt werden und m¨ussen nicht mehr explizit an Texture Units gebunden werden.

I Vorher: fixe Anzahl an Texturen im Programm.

I Texturspeicher ist nicht direkt adressierbar (keine Zeiger), stattdessen Zugriffsobjekt.

I IHVs halten Organisation des Texturspeichers geheim.

(32)

Texture Objects

Erzeuge Texture Object in Host Code:

c u d a T e x t u r e O b j e c t _ t obj ;

c u d a C r e a t e T e x t u r e O b j e c t (& obj , . . . ) ;

Dedizierte Texturzugriffsfunktionen in Kernel:

_ _ g l o b a l _ _ v o i d k e r n e l ( c u d a T e x t u r e O b j e c t _ t obj1D , c u d a T e x t u r e O b j e c t _ t obj2D , c u d a T e x t u r e O b j e c t _ t o b j 3 D ) {

f l o a t t1 = t e x 1 D ( obj1D , 0.5 f );

f l o a t t2 = t e x 2 D ( obj2D , m a k e _ f l o a t 2 ( 0 . 3 f , 0.4 f ));

f l o a t t3 = t e x 3 D ( obj3D ,

m a k e _ f l o a t 3 ( 0 . 3 f , 0.4 f , 0.5 f ));

}

⇒man kann Texturen nicht direkt adressieren.

(33)

Konfiguration von Texture Objects

Beim Erzeugen von Texture Objects legt man u. a. fest, I welche Dimensionalit¨at die Textur hat (1D, 2D, 3D), I welchen Datentyp die gespeicherten Texel haben, I wie interpoliert wird (n¨achster Nachbar; linear),

I ob Koordinaten normalisiert sind [0..1) oder nicht [0..Width), I sowie den Wrap Modus:

I Clamp I Wrap I Mirror I Border

Referenzen

ÄHNLICHE DOKUMENTE

“Sampling” bestimmt. Dies ist eine in den Prozessor eingebaute Funktionalit¨ at, die, falls aktiviert, die Instruktionspipeline zu definierten Zeitpunkten anh¨ alt und

C++ spezifiziert keine Garbage Collection (der Standard schließt sie nicht explizit aus, aber es gibt keine Implementierungen, die mir bekannt sind). Daher m¨ ussen alle Ressourcen,

I Vulkan / Modernes OpenGL: minimaler Vertex Shader verpflichtend (außer Compute). Ohne minimalen Fragment Shader kein Bild (manchmal Ergebnis

I Desktop GPUs implementieren traditionell Immediate-Mode Rendering: Verarbeitung orientiert sich an Reihenfolge, in der Dreiecke submittiert wurden (nicht zu verwechseln

Die Template Klasse sync queue in der beigef¨ ugten Datei queue.cpp wird von mehreren Threads gleichzeitig als Schlange zum Austauschen von Nachrichten genutzt. Intern verwaltet

(2) Die Bin¨ arrepr¨ asentation der sich ergebenden nat¨ urlichen Zahl k setzt sich immer abwechselnd aus den Bin¨ arziffern der Zahlpaare (i, j) zusammen. Tragen Sie das Ergebnis

Trifft man die vereinfachende Annahme, dass geometrische, aus Dreiecken zusammengesetzte Objekte geschlossen sind und immer nur von außen betrachtet werden, kann man vor dem

Der Ray Tracer geht im Weiteren davon aus, dass totale interne Reflektion aufgetreten ist, wenn refract() einen 0-Vektor zurückgegeben hat und ruft in dem Fall die Funktion