Vous êtes sur la page 1sur 9

Indice de contenidos

Ejemplos de
programacin CUDA

1. Premisas bsicas para la construccin y desarrollo


de cdigo CUDA. [3 diapositivas]
2. Suma de dos vectores. [5 diapositivas]
3. Kernels de patrn (stencil). [8 diapositivas]
4. Producto de matrices. [12 diapositivas]

Manuel Ujaldn Martnez


Departamento de Arquitectura de Computadores
Universidad de Mlaga

Un breve recordatorio para


la construccin del cdigo

I. Premisas bsicas para


la construccin y desarrollo
de cdigo CUDA

! CUDA est basado en C: Entremezcla programacin


secuencial en CPU con kernels paralelos en GPU.

Se requieren esfuerzos coordinados en


paralelo

Pasos a realizar para


la construccin del cdigo

! El paralelismo lo proporcionan los bloques y los hilos:


! Los hilos dentro de cada bloque pueden requerir una
sincronizacin explcita:

1. Identificar las partes potencialmente paralelas.


2. Delimitar los datos necesarios.
3. Mover los datos a la GPU.
4. Llamar al kernel de computacin.
5. Establecer la sincronizacin apropiada entre GPU y CPU.
6. Transferir los resultados de la GPU a la CPU.
7. Integrar los resultados de la GPU en variables de la CPU.

! Paso 1.
! __syncthreads();
! Paso 2.

! Entre dos kernels, hay barreras implcitas:


! Kernel1 <<<nblocks,nthreads>>> (a,b,c);
! Kernel2 <<<nblocks,nthreads>>> (a,b);

! Los bloques pueden coordinarse utilizando operaciones


atmicas (a partir de CUDA Compute Capabilities 1.2):
! Ejemplo: Incremento de un contador atomicInc();

Cdigo necesario para el kernel en GPU


y su invocacin desde la CPU
// Suma de los vectores C = A+B
// Cada hilo calcula un componente del vector
__global__ void vecAdd(float* A, float* B, float* C) {
!
Int tid = threadIdx.x+ blockDim.x* blockIdx.x;
!
C[tid] = A[tid] + B[tid];
}

II. Suma de dos vectores

Cdigo GPU

int main() { // Lanzamos N/256 bloques de 256 hilos cada uno


!
vecAdd<<< N/256, 256>>>(d_A, d_B, d_C);
Cdigo CPU
}

! El prefijo __global__ indica que vecAdd() se ejecutar


en la GPU, y ser llamado desde el host (CPU).
! A, B y C son punteros a la memoria del dispositivo, as que
necesitaremos:
7

! Alojar/liberar memoria en GPU, usando cudaMalloc()/cudaFree().


! Estos punteros no pueden ser accedidos desde el cdigo del host.
8

Cdigo en CPU para el manejo de memoria


y la recoleccin de resultados desde la GPU
unsigned int numBytes = N * sizeof(float);
// aloja memoria en la CPU
float* h_A = (float*) malloc(numBytes);
float* h_B = (float*) malloc(numBytes);
... inicializa h_A y h_B ...
// aloja memoria en la GPU
float* d_A = 0; cudaMalloc((void**)&d_A, numbytes);
float* d_B = 0; cudaMalloc((void**)&d_B, numbytes);
float* d_C = 0; cudaMalloc((void**)&d_C, numbytes);
// copia los datos de entrada de la CPU en la GPU
cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, numBytes, cudaMemcpyHostToDevice);
... LA LLAMADA AL KERNEL vecAdd ANTERIOR SE REALIZA AQUI ...
// copia los resultados de la GPU en la CPU
float* h_C = (float*) malloc(numBytes);
cudaMemcpy(h_C, d_C, numBytes, cudaMemcpyDeviceToHost);
// libera la memoria de vdeo
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);

Ejecutando en paralelo
(al margen de la generacin hardware)

64

Calculando ndices de acceso a vectores en


funcin de los bloques e hilos declarados

threadIdx.x

threadIdx.x

10

Manipulando vectores de tamao arbitrario


! Los problemas reales no suelen tener una dimensin
mltiplo de blockDim.x, as que debemos prevenir un
eventual acceso ms all del tamao del vector:

! Con M hilos por bloque, el ndice unvoco para cada hilo es:
! tid = threadIdx.x+ blockDim.x* blockIdx.x;
! Para acceder a un vector de un elemento por cada hilo (ya
que buscamos paralelismo de grano fino), con N=4 bloques
de M=8 hilos cada uno, tenemos:
threadIdx.x

GPU
! vecAdd<<< 1, 1 >>>():
Multiprocesador N
Ejecuta 1 bloque compuesto de
(escalabilidad en 2 gener.)
1 hilo - no hay paralelismo.
Multiprocesador 2
Multiprocesador 1
! vecAdd<<< N, 1 >>>():
Memoria compartida
Ejecuta N bloques compuestos
Registros
Registros
Registros
de 1 hilo. Paralelismo intermultiprocesador.
Core 2 Core M
Core 1
(escalabilidad en 3 gener.)
! vecAdd<<< N, M >>>():
Ejecuta N bloques compuestos
Cach de texturas
de M hilos cada uno.
Paralelismo inter- e intraMemoria global
multiprocesador.

// Suma dos vectores de tamao N: C[1..N] = A[1..N] + B[1..N]


__global__ void vecAdd(float* A, float* B, float* C, N) {
int tid = threadIdx.x + (blockDim.x * blockIdx.x);
if (tid < N)
C[tid] = A[tid] + B[tid];
}

threadIdx.x

01234567012345670123456701234567
blockIdx.x = 0 blockIdx.x = 1 blockIdx.x = 2 blockIdx.x = 3

! Qu hilo computar el 22-simo elemento del vector?


! gridDim es 4. blockDim es 8. blockIdx = 2. threadIdx = 5.
! tid = 5 + (8 * 2) = 21 (empezamos a contar desde 0, as que este es
el 22-simo elemento).

11

! Y ahora, actualiza el lanzamiento del kernel para incluir el


bloque de hilos "incompleto":
!

vecAdd<<< (N + M-1)/256, 256>>>(d_A, d_B, d_C, N);


12

Fundamento

III. Kernels patrn


(stencil kernels)

! Tomando como referencia el ejemplo anterior, los hilos


aaden un nivel de complejidad sin contribuir con una
nueva funcionalidad.
! Sin embargo, al compartir un multiprocesador, los hilos
pueden hacer un par de cosas que los bloques no:
! Comunicarse (a travs de la memoria compartida).
! Sincronizarse (por ejemplo, para preservar las dependencias de
datos).

! Para ilustrar todo esto, necesitamos un ejemplo ms


sofisticado. Vamos con ello...

14

13

Las buenas noticias: Los hilos pueden compartir datos y trabajar de forma coordinada

Patrn unidimensional
! Apliquemos un patrn 1D a un vector, donde el resultado
de salida es la suma de los elementos vecinos en un radio.
! Si el radio es 3, la salida es la suma de los 7 elementos
vecinos, incluyendo el propio:

radio

! Los hilos de un bloque pueden compartir datos a travs de


la memoria compartida.
! El usuario gestiona explcitamente la memoria compartida:
Declarada con el prefijo __shared__.
! Los datos se alojan para cada bloque.
! La memoria compartida es extremadamente rpida:
! 500 veces ms rpida que la memoria global (memoria de vdeo GDDR5). La
diferencia es la tecnologa: esttica (transistores) frente a dinmica (capacitores).
! El programador puede verla como una extensin del banco de registros.

radio

! De nuevo, aplicamos paralelismo de grano fino para


procesar la salida de un nico elemento en cada hilo.
! Los elementos del vector se leen en mltiples ocasiones:

! La memoria compartida es ms verstil que los registros:


! Los registros son privados a cada hilo, mientras que la memoria compartida es
privada a cada bloque.

! Concretamente, 7 veces para un radio de 3 elementos.


15

16

Las malas noticias: Cuantos ms datos


compartimos, ms limitamos el paralelismo

Utilizando la memoria compartida

! La memoria compartida y el uso del banco de registros


limita el paralelismo.

! Los datos se cachean en memoria compartida:


! Leer (blockDim.x + 2 * radio) elementos de entrada desde
memoria global para situarlos en memoria compartida.
! Computar blockDim.x elementos de salida.
! Escribir blockDim.x elementos de salida a memoria global.

! Si dejamos espacio para un segundo bloque, el planificador divide


estos dos recursos para que se puedan usar concurrentemente.

! Ejemplos para Fermi (donde hay un mximo de 32768


regs. y 48 KB. de memoria compartida en cada multiproc):
! Para alojar dos bloques en cada multiprocesador: El bloque no
puede sobrepasar 16384 registros y 24 KB. de memoria compartida.
! Para alojar tres bloques en cada multiprocesador: El bloque no
puede sobrepasar 10922 registros y 16 KB. de memoria compartida.
! Para alojar cuatro bloques en cada multiprocesador: El bloque no
puede usar ms de 8192 registros y 16 KB. de memoria compartida.
! ... podemos utilizar el CUDA Occupancy Calculator para resolverlo.

! Cada bloque necesita una extensin de elementos igual al


radio en cada uno de sus bordes.
extensin a la izquierda

blockDim.x elementos de salida


17

El kernel de patrn

! Usar __synchthreads();
para sincronizar todos los
hilos dentro de un bloque:

// Sita los elementos en memoria compartida


temp[lindex] = in[gindex];
if (threadIdx.x < RADIUS) {
temp[lindex-RADIUS] = in[gindex-RADIUS];
temp[lindex+BLOCK_SIZE] = in[gindex+BLOCK_SIZE];
}

// Almacena el resultado
out[gindex] = result;
}

18

Sincronizacin entre los hilos

__global__ void stencil(int *in, int *out) {


__shared__ int temp[BLOCK_SIZE + 2 * RADIUS];
int gindex = threadIdx.x
+ blockIdx.x * blockDim.x;
int lindex = threadIdx.x + RADIUS;

// Aplica el patrn
int result = 0;
for (int offset=-RADIUS; offset<=RADIUS; offset++)
result += temp[lindex + offset];

extensin a la derecha

Debemos prevenir
condiciones de carrera. Por
ejemplo, el ltimo hilo (15)
lee del tramo extendido antes
de que el primero (0) lo haya
trado desde memoria global
a memoria compartida.
Se requiere sincronizar a los
hilos.
19

! Todos los hilos deben alcanzar la


barrera antes de proseguir.
! Se puede usar para prevenir
riesgos del tipo RAW/WAR/WAW.
! En sentencias condicionales, la
condicin debe ser uniforme a lo
largo de todo el bloque.

__global__ void stencil_1d(...) {


< Declarar variables e indices >
< Pasar los elementos de entrada a memoria
compartida>
__synchthreads();
< Aplicar el patrn >
< Almacenar el resultado >
}

20

En resumen...
! Lanzamos N bloques con M hilos por bloque para ejecutar los hilos
en paralelo. Usar:

IV. Producto de matrices

! kernel<<<N,M>>>();

! Acceder al ndice de bloque dentro de su malla y al ndice del hilo


dentro de su bloque:
! blockIdx.x y threadIdx.x;

! Calcular los ndices globales donde cada hilo tiene que trabajar
dependiendo del particionamiento de datos. Usar:
! int index = threadIdx.x + blockIdx.x * blockDim.x;

! Declarar una variable/vector en memoria compartida. Usar:


! __shared__ (anteponindolo al correspondiente tipo de dato).

! Synchronizar los hilos para prevenir riesgos. Usar:


! __synchthreads();

21

Memory layout of a matrix in C language

Ejemplo: El producto de matrices cuadradas


! C = A ! B. Matrices de N x N datos.
! Primera aproximacin con CUDA:

22

! VERIFICAR AQUI QUE CUDA NO ADMITE ARRAYS 2D.


M0,0 M0,1 M0,2 M0,3
M1,0 M1,1 M1,2 M1,3

! Cada hilo computa un elemento de C.


! A y B se cargan N veces de memoria de vdeo.

M2,0 M2,1 M2,2 M2,3


M3,0 M3,1 M3,2 M3,3

! Requiere un gran
ancho de banda.
! Tiene poca intensidad
aritmtica.

M0,0 M0,1 M0,2 M0,3 M1,0 M1,1 M1,2 M1,3 M2,0 M2,1 M2,2 M2,3 M3,0 M3,1 M3,2 M3,3

! Problemas a la vista:

N
23

24

Versin C que se ejecutara en el host

Una primera versin CUDA (ingenua)

! Un solo bloque de hilos


computa la matriz resultado

Grid 1
Block 1

! Cada hilo computa un solo


elemento de la matriz resultado.

Thread
!)2 ,2(

! Cada hilo:
A

! Carga un fila de la matriz A.


! Carga una columna de la matriz B.
! Realiza un producto y una suma por
cada par de elementos de A y B.

void MxMonCPU(float* A, float* B,


float* C, int N);
{
for (int i=0; i<N; i++)
for (int j=0; j<N; j++)
{
float sum=0;
for (int k=0; k<N; k++)
{
float a = M[i*N + k];
float b = N[k*N + j];
sum += a*b;
}
C[i*N + j] = sum;
}
}

48

WIDTH

! Problemas:

N
25

! El ratio entre computacin y acceso a memoria se acerca a 1:1 (bajo).


! El max. tamao de C depende del mximo tamao de bloque.

26

Optimizacin 1: Estructurar el cdigo


para maximizar el paralelismo

Resumen de actuaciones a realizar para


mejorar el rendimiento del cdigo CUDA
CUDA permite lograr excelentes resultados en la
paralelizacin de cdigo si respetamos unas pocas
premisas de rendimiento:

! Declarar muchos threads permite ocultar latencias con memoria global.


! Mxima granularidad del cdigo: Utilizar un nico grid con tantos hilos
como elementos haya en la matriz C, de forma que cada hilo computa un
nico elemento de la matriz resultado C.
! Entre el grid y los hilos se sita el bloque para acomodar los hilos en
grupos de a lo sumo 512 threads (mx. paralelismo en CUDA). Y como 512
no es un cuadrado perfecto, 256 resulta el candidato ideal (256 = 16x16)
para equilibrar el paralelismo en las dos dimensiones de la matriz.

1. Estructurar el cdigo y elegir una configuracin de hilos que


maximice el paralelismo de datos en GPU y minimice la
transferencia de datos CPU ! GPU.
2. Respetar la coherencia al acceder a memoria global (coalescing).

Grid

3. Maximizar el uso de la memoria compartida (tiling).


4. Evitar accesos a memoria compartida con elevado nmero de
conflictos al acceder a sus bancos.
5. Minimizar el uso de warps divergentes.

!!!!!!!!!!!!!!!

!!!!!!!
Th(x,y)

Vamos a aplicar 1 y 3 sobre el producto de matrices.


27

WidthA

WidthB

Bloque

!!!!!!!

HeightA

C(x, y)

=
HeightA

WidthB

X
A

dim2 dimBlock(BLOCK_SIZE, BLOCK_SIZE);


dim2 dimGrid(WidthB/BLOCKSIZE, HeightA/BLOCKSIZE);

28

Optimizacin 3: Tiling aplicado


al producto de matrices

! Cada bloque de hilos se encarga de una


submatriz Csub de MxM elementos de C.

Ctemp=0;
for (i=0; i<widthA; i++){
Ctemp += A[indexA] * B[indexB];
indexA++;
indexB += widthB;
}
C[indexC] = Ctemp ;

! A y B se cargan slo (N/M) veces desde


memoria de vdeo.

! Relaja el ancho
de banda.
! Aumenta la
intensidad
aritmtica.

! Cada hilo necesita 10 regs., por lo que podemos declarar


768 hilos estructurados en 3 bloques de 256 (16x16).
Rendimiento: 10.58 GFLOPS
Mirando el PTX del cdigo, el cuello de botella se sita en
el acceso a memoria global.

Csub

Memoria global

Coalescing en acceso a la matriz B:

! Con tiling:

Optimizacin 2: Garantizar los accesos


coalesced a memoria

M
N

29

Optimizacin 3: Implementacin del tiling

30

Optimizacin 3: Implementacin del tiling


GRID

Tenemos que particionar los datos en subconjuntos (tiles) que


quepan en los 16 Kbytes de memoria compartida
y manipular cada subconjunto desde un bloque de hilos:

!!!!!!!!!!

SM0
As
!!!!
!!!!
!!!!

Bs

!!!!
!!!!
!!!!

8192 registros

! Cargar el subconjunto desde memoria global a memoria compartida.


! __syncthreads()
! Realizar la computacin del subconjunto en memoria compartida.

SP0

SM15

Shared Memory
8 KB constant

!!!!!!!!!!!!!!
SFU0

SP7

SFU1

! Cada hilo puede iterar eficientemente sobre cualquier elemento de datos.

! __syncthreads() (en caso de necesidad por dependencias)


! Copiar los resultados desde memoria compartida a memoria global.

!!!!
!!!!
!!!!

!!!!
!!!!
!!!!

A
31

Memoria global

! Cada hilo carga un elemento


del tile de A y B
Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];
// Load tile (16x16)
As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();
// Compute results from tile
for (i=0; i<16; i++)
Ctemp+=As[ty][i]*Bs[i][tx];
__syncthreads();
}
C[indexC] = Ctemp ;
32

Optimizaciones adicionales:
Loop unrolling

Rendimiento de tiling & unrolling

El compilador tambin puede hacer cosas por nosotros!


Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];

// Load tile (16x16)


As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();

// Load tile (16x16)


As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();

// Compute results from tile


for (i=0; i<16; i++)
Ctemp+=As[ty][i]*Bs[i][tx];
__syncthreads();

// Compute results from tile


Ctemp+=As[ty][0]*Bs[0][tx];
!!!!
Ctemp+=As[ty][15]*Bs[15][tx];
__syncthreads();

}
C[indexC] = Ctemp ;

75
GFLOPS

Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];

100

50

Slo tiling
Tiling & Unrolling

25
0
4x4

}
C[indexC] = Ctemp ;

8x8

12x12

16x16

Tamao del tile


33

34

Vous aimerez peut-être aussi