Está en la página 1de 150

Programando la GPU con CUDA

Curso en el Dpto. de Matemticas e Informtica. UIB.


23 al 25 de Junio, 2014.

Manuel Ujaldon
Nvidia CUDA Fellow
Profesor Titular del Dpto. de Arquitectura de Computadores. Universidad de Mlaga.
Conjoint Senior Lecturer. University of Newcastle (Australia)
Contenidos del tutorial [123 diapositivas]
1. Introduccin. [17 diapositivas]
2. Arquitectura. [26]
1. El modelo hardware de CUDA. [3]
2. Primera generacin: Tesla (2007-2009). [3]
3. Segunda generacin: Fermi (2010-2011). [4]
4. Tercera generacin: Kepler (2012-2014). [16]
3. Programacin. [15]
4. Sintaxis. [16]
1. Elementos bsicos. [10]
2. Un par de ejemplos preliminares. [6]
5. Compilacin y herramientas. [12]
6. Ejemplos: VectorAdd, Stencil, MxM. [25]
7. Bibliografa, recursos y herramientas. [12] 2
Prerrequisitos para este tutorial

Se requiere estar familiarizado con el lenguaje C.


No es necesaria experiencia en programacin paralela
(aunque si se tiene, ayuda bastante).
No se necesitan conocimientos sobre la arquitectura de la
GPU: Empezaremos describiendo sus pilares bsicos.
No es necesario tener experiencia en programacin
grfica. Eso era antes cuando GPGPU se basaba en los
shaders y Cg. Con CUDA, no hace falta desenvolverse con
vrtices, pxeles o texturas porque es transparente a todos
estos recursos.

3
I. Introduccin
Bienvenido al mundo de las GPUs

5
Los personajes de esta historia:
La foto de familia de CUDA

6
En apenas 7 aos la programacin CUDA
ha crecido a un ritmo inusitado
Ao 2008 Ao 2014
100.000.000 de 500.000.000 de
GPUs aceptan CUDA GPUs aceptan CUDA

150.000 2.100.000 de
descargas de CUDA descargas CUDA

1 supercomputador 52
supercomputadores

60 780
cursos universitarios cursos

4.000 40.000
artculos cientficos artculos cientficos

Se produce una descarga del SW. de CUDA cada minuto. 7


Distribucin mundial de las 800 universidades
que imparten cursos de CUDA

8
Sntesis evolutiva de la GPU

2001: Primeros chips many-core (en los procesadores


para vrtices y pxeles), mostrando el camino evolutivo.
2003: Esos procesadores son programables (con Cg).
2006: Esos procesadores se unifican en un solo tipo.
2007: Emerge CUDA. Un paso decisivo en la convergencia
de los modelos de programacin para CPU y GPU.
2008: Aritmtica de punto flotante en doble precisin.
2010: Normalizacin de operandos y memoria ECC.
2012: Potenciacin de la computacin irregular.
2014: Unificacin del espacio de direcciones CPU-GPU.
An por mejorar: Robustez en clusters y conexin a disco. 9
Las 3 cualidades que han hecho
de la GPU un procesador nico
Control simplificado.
El control de un hilo se amortiza en otros 31 (warp size = 32).
Escalabilidad.
Aprovechndose del gran volumen de datos que manejan las
aplicaciones, se define un modelo de paralelizacin sostenible.
Productividad.
Se habilitan multitud de mecanismos para que cuando un hilo pase
a realizar operaciones que no permitan su ejecucin veloz, otro
oculte su latencia tomando el procesador de forma inmediata.

Palabras clave esenciales para CUDA:


Warp, SIMD, ocultacin de latencia, conmutacin de contexto gratis.
10
Qu es CUDA?
Compute Unified Device Architecture
Una plataforma diseada conjuntamente a nivel software y
hardware para aprovechar la potencia de una GPU en
aplicaciones de propsito general a tres niveles:
Software: Permite programar la GPU en C con mnimas
pero potentes extensiones SIMD para lograr una ejecucin
eficiente y escalable.
Firmware: Ofrece un driver para la programacin GPGPU
que es compatible con el que utiliza para renderizar. Sencillos
APIs manejan los dispositivos, la memoria, etc.
Hardware: Habilita el paralelismo de la GPU para
programacin de propsito general a travs de un nmero de
multiprocesadores dotados de un conjunto de ncleos
computacionales arropados por una jerarqua de memoria.
11
Lo esencial de CUDA

En general, es lenguaje C con mnimas extensiones:


El programador escribe el programa para un solo hilo (thread), y el
cdigo se instancia de forma automtica sobre miles de hilos.
CUDA define:
Un modelo de arquitectura:
Con multitud de unidades de proceso (cores), agrupadas en multiprocesadores que
comparten una misma unidad de control (ejecucin SIMD).
Un modelo de programacin:
Basado en el paralelismo masivo de datos y en el paralelismo de grano fino.
Escalable: El cdigo se ejecuta sobre cualquier nmero de cores sin recompilar.
Un modelo de gestin de la memoria:
Ms explcita al programador, con control explcito de la memoria cach.
Objetivos:
Construir cdigo escalable a cientos de cores, declarando miles de hilos.
Permitir computacin heterognea en CPU y GPU.
12
Computacin heterognea (1/4)

Terminologa:
Host (el anfitrin): La CPU y la memoria de la placa base [DDR3].
Device (el dispositivo): La tarjeta grfica [GPU + memoria de vdeo]:
GPU: Nvidia GeForce/Tesla.
Memoria de vdeo: GDDR5 en 2013.

Host Device
13
Computacin heterognea (2/4)

CUDA ejecuta un programa sobre un dispositivo (la GPU), que acta como
coprocesador de un anfitrin o host (la CPU).
CUDA puede verse como una librera de funciones que contienen 3 tipos de
componentes:
Host: Control y acceso a los dispositivos.
Dispositivos: Funciones especficas para ellos.
Todos: Tipos de datos vectoriales y un conjunto de rutinas soportadas por ambas partes.

CPU (host)
GPU
Cores
50 GB/s.
Caches (device)

3 canales (192 bits = 24 bytes)


384 bits @ 3 GHz 144 GB/s.
@ 1.333 GHz 32 GB/s.

Memoria principal Memoria de


vdeo
(DDR3)
PCI-e 3.0: 8 GB/s.

(GDDR5) 14
Computacin heterognea (3/4)

El cdigo reescrito en CUDA puede ser inferior al 5%, pero


consumir ms del 50% del tiempo si no migra a la GPU. 15
Computacin heterognea (4/4)

#include <iostream>
#include <algorithm>

CODIGO DEL DISPOSITIVO:


using namespace std;

#define N 1024
#define RADIUS 3
#define BLOCK_SIZE 16

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


__shared__ int temp[BLOCK_SIZE + 2 * RADIUS];
int gindex = threadIdx.x + blockIdx.x * blockDim.x;
int lindex = threadIdx.x + RADIUS;
Funcin paralela
// Read input elements into shared memory
temp[lindex] = in[gindex];
if (threadIdx.x < RADIUS) {
temp[lindex - RADIUS] = in[gindex - RADIUS];
temp[lindex + BLOCK_SIZE] = in[gindex + BLOCK_SIZE];
escrita en CUDA.
}

// Synchronize (ensure all the data is available)


__syncthreads();

// Apply the stencil


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

// Store the result


out[gindex] = result;
}

void fill_ints(int *x, int n) {


fill_n(x, n, 1);
}

int main(void) {
int *in, *out; // host copies of a, b, c

CODIGO DEL HOST:


int *d_in, *d_out; // device copies of a, b, c
int size = (N + 2*RADIUS) * sizeof(int);

// Alloc space for host copies and setup values


in = (int *)malloc(size); fill_ints(in, N + 2*RADIUS);
out = (int *)malloc(size); fill_ints(out, N + 2*RADIUS);

- Cdigo serie.
// Alloc space for device copies
cudaMalloc((void **)&d_in, size);
cudaMalloc((void **)&d_out, size);

// Copy to device
cudaMemcpy(d_in, in, size, cudaMemcpyHostToDevice);

- Cdigo paralelo.
cudaMemcpy(d_out, out, size, cudaMemcpyHostToDevice);

// Launch stencil_1d() kernel on GPU


stencil_1d<<<N/BLOCK_SIZE,BLOCK_SIZE>>>(d_in + RADIUS, d_out + RADIUS);

// Copy result back to host

- Cdigo serie.
cudaMemcpy(out, d_out, size, cudaMemcpyDeviceToHost);

// Cleanup
free(in); free(out);
cudaFree(d_in); cudaFree(d_out);
return 0;
}

16
Un sencillo flujo de procesamiento (1/3)

Bus PCI

1.Copiar los datos de entrada de la


memoria de la CPU a la memoria
de la GPU.

17
Un sencillo flujo de procesamiento (2/3)

PCI Bus

1.Copiar los datos de entrada de la


memoria de la CPU a la memoria
de la GPU.
2.Cargar el programa en la GPU y
ejecutarlo, ubicando datos en
cach para mejorar el
rendimiento.

18
Un sencillo flujo de procesamiento (3/3)

PCI Bus

1.Copiar los datos de entrada de la


memoria de la CPU a la memoria
de la GPU.
2.Cargar el programa en la GPU y
ejecutarlo, ubicando datos en
cach para mejorar el
rendimiento.
3.Transferir los resultados de la
memoria de la GPU a la memoria
de la CPU.
19
El clsico ejemplo

int main(void) { Salida:


printf("Hola mundo!\n");
return 0;
$ nvcc hello.cu
}
$ a.out
Hola mundo!
$

Es cdigo C estndar que se ejecuta en el host.


El compilador nvcc de Nvidia puede utilizarse para
compilar programas que no contengan cdigo para la GPU.

20
Hola mundo! con cdigo para la GPU (1/2)
__global__ void mikernel(void)
{ Dos nuevos elementos
} sintcticos:
int main(void) La palabra clave de CUDA __global__
{ indica una funcin que se ejecuta en la
GPU y se lanza desde la CPU. Por
mikernel<<<1,1>>>();
ejemplo, mikernel<<<1,1>>>.
printf("Hola mundo!\n");
return 0; Eso es todo lo que se requiere
} para ejecutar una funcin en GPU.
nvcc separa el cdigo fuente para la CPU y la GPU.
Las funciones que corresponden a la GPU (como
mikernel()) son procesadas por el compilador de Nvidia.
Las funciones de la CPU (como main()) son procesadas
por su compilador (gcc para Unix, cl.exe para Windows). 21
Hola mundo! con cdigo para la GPU (2/2)
__global__ void mikernel(void)
{ Salida:
}
$ nvcc hello.cu
int main(void) {
$ a.out
mikernel<<<1,1>>>();
Hola mundo!
$
printf("Hola mundo!\n");
return 0;
}

mikernel() no hace nada esta vez.


Los smbolos "<<<" y ">>>" delimitan la llamada desde el cdigo
de la CPU al cdigo de la GPU, tambin denominado lanzamiento de
un kernel.
Los parmetros 1,1 describen el paralelismo (bloques e hilos CUDA).
22
La evolucin de CUDA

En los ltimos 7 aos, Nvidia ha vendido ms de 500 millones de


GPUs que aceptan CUDA para su programacin.
Pero CUDA ha evolucionado en la direccin opuesta a la que estamos
acostumbrados: Partiendo de los investigadores para poco a poco llegar
a usuarios ms generalistas.

Versin de
Usuarios y rasgos ms sobresalientes
CUDA [ao]
1.0 [2007] Muchos investigadores y algunos usuarios madrugadores.
2.0 [2008] Cientficos y aplicaciones para computacin de altas prestaciones.
3.0 [2009] Lderes en la innovacin de aplicaciones.
4.0 [2011] Adopcin mucho ms extensa de desarrolladores.
5.0 [2012] Paralelismo dinmico, enlazado de objetos, Remote DMA.
6.0 [2014] Memoria unificada CPU-GPU
23
II. Arquitectura
II.1. El modelo hardware
de CUDA
Las generaciones hardware de CUDA
GFLOPS en doble precisin por cada vatio consumido

24
22
20 Pascal
Memoria 3D
18 NVLink

16
14
12 Maxwell
Memoria unificada
10 DX12

8
Kepler
6 Paralelismo dinmico

4
Fermi
2 Tesla FP64
CUDA

2008 2010 2012 2014 2016 26


El modelo hardware de CUDA:
Un conjunto de procesadores SIMD
La GPU consta de: GPU
Multiprocesador N
N multiprocesadores, cada uno dotado
de M cores (o procesadores streaming). Multiprocesador 2

Paralelismo masivo: Multiprocesador 1

Aplicado sobre miles de hilos.


Compartiendo datos a diferentes Core 1 Core 2 Core M
Unidad de
Control SIMD

niveles de una jerarqua de memoria.


Computacin heterognea:
GPU:
Intensiva en datos. G80 GT200 GF100 GK110
Paralelismo de grano fino. (Tesla) (Tesla) (Fermi) (Kepler)
CPU: Marco temporal 2006-07 2008-09 2010-11 2012-13
Gestin y control.
N (multiprocs.) 16 30 14-16 13-15
Paralelismo de grano grueso.
M (cores/multip.) 8 8 32 192
Nmero de cores 128 240 448-512 2496-2880
27
Jerarqua de memoria

Cada multiprocesador tiene: GPU

Multiprocesador N
Su banco de registros.
Memoria compartida. Multiprocesador 2
Multiprocesador 1
Una cach de constantes y otra
de texturas, ambas de slo Memoria compartida

lectura y uso marginal. Registros Registros Registros


Unidad

La memoria global es la Procesador 1 Procesador 2 Procesador M


de
Control
SIMD

memoria de vdeo (GDDR5): Cach para


constantes
Tres veces ms rpida que la Cach para
memoria principal de la CPU, texturas

pero... 500 veces ms lenta que


la memoria compartida! (que es Memoria global
SRAM en realidad).
13
28
II.2. La primera generacin:
Tesla (G80 y GT200)
La primera generacin: G80 (GeForce 8800)

GPU G80 (en torno a 600 MHz, frecuencia muy inferior a la de sus cores)
Multiprocesador 16

Multiprocesador 2
Multiprocesador 1 (los bloques de cdigo CUDA se mapean sobre los multipr.)
Memoria compartida (16 KB)

Registros Registros Registros


Unidad de
control
Core 1 Core 8
(emite
Core 2 instrucciones
(1.35 GHz) SIMD)

(los kernels se mapean


Cach de texturas
sobre los cores)

Memoria global (hasta 1.5 GB) (GDDR3 @ 2x 800MHz)


30
La primera generacin: GT200 (GTX 200)

GPU GTX 200 (en torno a 600 MHz)


Multiprocesador 30

Multiprocesador 2
Multiprocesador 1 (los bloques de cdigo CUDA se mapean sobre los multipr.)
Memoria compartida (16 KB)

Registros Registros Registros


Unidad de
control
Core 1 Core 8
(emite
Core 2 instrucciones
(1.30 GHz) SIMD)

(los kernels se mapean


Cach de texturas
sobre los cores)

Memoria global (hasta 4 GB) (GDDR3, 512 bits @ 2x 1.1GHz = 141.7 GB/s)
31
Escalabilidad para futuras generaciones:
Alternativas para su crecimiento futuro
Aumentar el nmero de GPU
Multiprocesador 30
multiprocesadores por pares (escalabilidad en 1 gener.)
(nodo bsico), esto es, crecer Multiprocesador 2
en la dimensin Z. Es lo que Multiprocesador 1
hizo la 1 gener. (de 16 a 30). Memoria compartida
Aumentar el nmero de Registros Registros Registros
procesadores de cada
Core 1 Core 2 Core 8
multiprocesador, o crecer en la (escalabilidad en 2 y 3 gener.)
dimensin X. Es el camino
trazado por la 2 y 3 geners. Cach de texturas

Aumentar el tamao de la
memoria compartida, esto es, Memoria global
crecer en la dimensin Y. 32
II. 3. La segunda generacin:
Fermi (GF100)
El HW de Fermi comparado con los modelos
representativos de la generacin anterior
Arquitectura GPU G80 GT200 GF100 (Fermi)
Nombre comercial GeForce 8800 GTX 200 GTX 580
Ao de lanzamiento 2006 2008 2010
Nmero de transistores 681 millones 1400 millones 3000 millones
Nmero de cores (int y fp32) 128 240 512
Nmero de cores (fp64) 0 30 256
Velocidad de clculo en fp64 Ninguna 30 madds/ciclo 256 madds/ciclo
Planificadores de warps 1 1 2
Memoria compartida 16 KB 16 KB 16 KB + 48 KB
Cach L1 Ninguna Ninguna (o viceversa)
Cach L2 Ninguna Ninguna 768 KB
Correccin de errores
No No S
(DRAM)
Anchura del bus
32 bits 32 bits 64 bits
de direcciones
34
Arquitectura global de Fermi

Hasta 512 cores (16 SMs dotados de 32 cores cada uno).


Doble planificador de hilos en el front-end de cada SM.
64 KB. en cada SM: memoria compartida + cach L1.

35
Mejoras en la aritmtica

Unidades aritmtico-lgicas (ALUs):


Rediseada para optimizar operaciones
sobre enteros de 64 bits.
Admite operaciones de precisin extendida.
La instruccin madd (suma
y producto simultneos):
Est disponible tanto para simple
como para doble precisin.
La FPU (Floating-Point Unit): FP Unit INT Unit

Implementa el formato IEEE-754 en su Load/Store Units x 16


Special Func Units x 4

versin de 2008, aventajando incluso a las


CPUs ms avezadas.

36
La jerarqua de memoria

Fermi es la primera GPU


que ofrece una cach L1, que
combina con la memoria
compartida en proporcin 3:1 o
1:3 para un total de 64 Kbytes
por cada multiprocesador SM.
Tambin incluye una L2 de
768 Kbytes que comparten
todos los multiprocesadores.

13 37
II. 4. La tercera generacin:
Kepler (GK110)
Diagrama de bloques: Kepler GK110

7.100 Mt.
15 multiprocs. SMX.
> 1 TFLOP FP64.
Cach L2 de 1.5 MB.
GDDR5 de 384 bits.
PCI Express Gen3.

39
Escalabilidad de la arquitectura:
Sntesis de sus tres generaciones
Tesla Fermi Kepler
GeForce
GK104 GK110 GK110
Arquitectura G80 GT200 GF100 GF104 GTX
(K10) (K20) (K40)
Titan Z
Marco temporal 2006-07 2008-09 2010 2011 2012 2013 2013-14 2014

CUDA Compute
1.0 1.2 2.0 2.1 3.0 3.5 3.5 3.5
Capability (CCC)

N (multiprocs.) 16 30 16 7 8 14 15 30

M (cores/multip.) 8 8 32 48 192 192 192 192

Nmero de cores 128 240 512 336 1536 2688 2880 5760

40
La nueva GeForce GTX Titan Z

5760 cores (el doble que la K40).


12 Gbytes de memoria de vdeo.
8 TeraFLOPS de rendimiento pico.
Precio inicial: $2999.

41
Evolucin de los multiprocesadores:
Del SM de Femi al SMX de Kepler

42
El multiprocesador SMX
Planificacin y emisin
de instrucciones en warps Front-end

Ejecucin de instrucciones.
512 unidades funcionales:
- 192 para aritmtica entera. Back-end
- 192 para aritmtica s.p.
- 64 para aritmtica d.p.
- 32 para carga/almacen.
- 32 para SFUs (log,sqrt, ...)

Acceso a memoria Interfaz


43
Expresando todo el paralelismo
Tetris (baldosa = warp_instr.): Ejemplo: Kernel con bloques de 384 hilos (12 warps).
- Emite 4 warp_instrs. Bloque 0: Bloque 1:
- Ejecuta hasta 10 warps = sub ...
320 hilos.
- Warp_instrs. son simtricos fmadd ...
y se ejecutan todos en 1 ciclo. fdiv64 ...
load ...
Correspondencia de colores:
instr. sqrt ...
para instrucciones int.
warp
para instrs. float. Kepler:
double. Emite 4 - Emite 4 warps x 2 instrs.
warp_instrs. Fermi: - Ejecuta hasta 16 warp_instrs.
load/store. G80: Tarda - Emite 2. (512 unidades funcionales).
4 ciclos en - Ejecuta
log/sqrt.... ejecutar hasta 5.
cada
warp_instrs.
32 SFU
El jugador planifica los warps!
Se pueden rotar las fichas si no hay 32 LD/ST
dependencias entre sus 4 warps
64 FPU DP

SM en 6x32 = 192 ALUs 192 FPU SP


G80: Fermi:
Ejecuta hasta 10 warp_instrs. 16 U.F. 100 U.F. paralelas. SMX en Kepler: 512 U.F. paralelas. 44
Paralelismo en SMX: A nivel de hilo (TLP)
y a nivel de instruccin (ILP)
Incrementar el paralelismo horizontalmente a travs del TLP:
Ms warps concurrentes (bloques ms grandes y/o ms bloques activos en cada SMX).

Incrementar ...
paralelismo
...
verticalmente
con ILP: ...
Si las instrs.
...
son ms
indepen- ...
dientes

Los SMX pueden potenciar el ILP disponible de forma


intercambiable con el TLP:
Es mucho mejor que Fermi para esto.
Algunas veces es ms fcil incrementar el ILP que el TLP
(por ejemplo, desenrrollar un lazo en un pequeo factor):
El nmero de hilos puede estar limitado por el algoritmo o los lmites HW.
Necesitamos el ILP para lograr un elevado IPC (Instrs. Per Cycle). 45
En las GPUs Kepler concurren todas
las formas de paralelismo. Ejemplo: K40.
1: De tareas (TLP)
2: De instrs. (ILP)

... SMX 0
... La K40 puede
... ...... ejecutar hasta
... ...
... ... 512x15 = 7680
... ...... ...... hilos en un ciclo
... ...
... ...... ... ...... si stos son del
... ... ... ... color adecuado.
... ...... ...... ...... ......
... ... ... ... ...
... ... ... ... ...
... ... ... ... SMX 15
... ... ... ...
... ... ... ...
3:

... ... ...


... ... ...
D

... ... ...


... ...
e

... ...
da

... ...
...
to

...
s

...
(S

4: Vectorial (warp = 32)


IM

La K40 puede planificar y emitir


D)

hasta 64x15 warps en un ciclo:


(todo este volumen representa menos, 60x15 warps) 30720 hilos en 1.14 ns.

Imagina un tetris 3D con 15 cubiletes y hasta 64 baldosas


cayendo simultneamente en cada uno de ellos, porque as
funciona la K40 planificando warps con el mx. paralelismo. 46
Caso estudio: Polinomios de Zernike

Recursos Carga/
ALU FPU 32 bits FPU 64 bits SFU
GPU almacen.
Fermi 32% 32% 16% 16% 4%

Kepler 37.5% 37.5% 12.5% 6.25% 6.25%


Kernel de los
polinomios de 54% 21% 0% 25% 0%
Zernike
Mejor Kepler Fermi Kepler Fermi Fermi

Fermi se encuentra ms equilibrada para este caso.


La distribucin de recursos en Kepler mejora la ejecucin
de la aritmtica entera, pero empeora la de punto flotante y
carga/almacenamiento. El resto no se utilizan. 47
Utilizar el CUDA Visual Profiler para
conocer qu tal se adapta nuestra aplicacin

48
Cmo trabaja el front-end de la GPU:
(1) Planificacin de warps

SM (Fermi) SMX (Kepler) 49


La conexin entre front-end y back-end:
(2) Emisin de warps
En los 5 ciclos ilustrados, hemos podido ejecutar hasta aqu.
En Fermi hay dficit en las SFUs (azul), mientras que en Kepler
lo hay en las unidades de carga/almacenamiento (verde).
Kepler equilibra la doble precisin (rojo) y tiene gran supervit
en computacin int y float, seal de que los cdigos reales
tienen mayor presencia de instrs. naranjas, y sobre todo, amarillas.

SM (Fermi) SMX (Kepler)


50
Cmo trabaja el back-end de la GPU:
(3) Ejecucin de warps
Suponemos que en el momento en que comenzamos a
monitorizar la ejecucin quedaban pendientes de ejecutar:
Un par de warps de punto flotante en simple precisin (naranjas).
Otro par de warps en doble precisin (rojos).
Parece conveniente que el front-end vaya un poco
adelantado respecto al back-end (prebsquedas) con
objeto de maximizar el throughput.

SM (Fermi) SMX (Kepler)


51
Puntualizaciones al modelo tetris

En Fermi, las baldosas rojas no pueden combinarse con otras.


En Kepler, no se pueden tomar 8 warp_instrs. en horizontal,
deben ser de altura 2 como mnimo.
Las instrucciones tienen distinta latencia, as que las que
consuman ms de un ciclo (como los operandos en doble
precisin) deben expandirse verticalmente en proporcin.
Si el warp tiene divergencias, tarda ms de un ciclo: Es la
carencia del vectorial. Se podra representar como el anterior.
Los cdigos tienen ms fichas amarillas (int domina).
Algunas fichas vienen cojas porque el planificador de
warps no puede encontrar un lote de 4x2 sin dependencias.
Las fichas pueden ensamblar baldosas no contiguas. 52
Latencia de los warps

Aunque todas las baldosas tardaran un ciclo en ejecutarse,


la duracin de los warps no sera sa. El tiempo que un warp
gasta en la mquina es la suma de tres:
Tiempo en planificacin.
Tiempo en emisin.
Tiempo en ejecucin.
La planificacin y ejecucin son bastante regulares, pero la
emisin no: Depende de la montaa de baldosas que haya
en el cubilete (estaciones de reserva). De ah la varianza que
experimenta su duracin.

53
El comportamiento de los warps nos ensea
que la GPU no es un procesador regular
Factores impredecibles en tiempo de ejecucin dificultan
un reparto equilibrado de la carga computacional entre los
multiprocesadores. Aqu vemos un ejemplo de la varianza
existente entre los 8 ltimos warps ejecutados en cada SM:

54
III. Programacin
El modelo de programacin CUDA

La GPU (device) ofrece a la CPU (host) la visin de un


coprocesador altamente ramificado en hilos.
Que tiene su propia memoria DRAM.
Donde los hilos se ejecutan en paralelo sobre los ncleos (cores
o stream processors) de un multiprocesador.
GPU
Multiprocesador 1 Multiprocesador 2 Multiprocesador N

Los hilos de CUDA son extremadamente ligeros.


Se crean en un tiempo muy efmero.
La conmutacin de contexto es inmediata.
Objetivo del programador: Declarar miles de hilos, que
la GPU necesita para lograr rendimiento y escalabilidad.
56
Unos pocos parmetros distinguen
los modelos comerciales
Debemos esperar que estas diferencias crezcan entre
modelos asociados a diferentes generaciones.
Las diferencias crecern cuando dos tarjetas pertenezcan
a gamas diferentes:
300-500 para la gama alta.
150-250 para la gama media.
60-120 para la gama baja.
La memoria de vdeo puede diferir tambin cuando
emerge una nueva tecnologa, como el caso de la GDDR5.
Los modelos difieren adems en sus prestaciones grficas,
aunque este tutorial no cubre las mismas.
57
Estructura de un programa CUDA

Cada multiprocesador procesa lotes de bloques, uno


detrs de otro
Bloques activos = los bloques procesados por un multiprocesador
en un lote.
Hilos activos = todos los que provienen de los bloques que se
encuentren activos.
Los registros y la memoria compartida de un multi-
procesador se reparten entre sus hilos activos. Para un
kernel dado, el nmero de bloques activos depende de:
El nmero de registros requeridos por el kernel.
La memoria compartida consumida por el kernel.

58
Conceptos bsicos

Los programadores se enfrentan al reto de exponer el paralelismo para


mltiples cores y mltiples hilos por core. Para ello, deben usar los
siguientes elementos:
Dispositivo = GPU = Conjunto de multiprocesadores.
Multiprocesador = Conjunto de procesadores y memoria compartida.
Kernel = Programa listo para ser ejecutado en la GPU.
Malla (grid) = Conjunto de bloques cuya complecin ejecuta un kernel.
Bloque [de hilos] = Grupo de hilos SIMD que:
Ejecutan un kernel delimitando su dominio de datos segn su threadID
y blockID.
Pueden comunicarse a travs de la memoria compartida del
multiprocesador.
Tamao del warp = 32. Esta es la resolucin del planificador para emitir
hilos por grupos a las unidades de ejecucin.
59
Correspondencia entre los conceptos
hardware y software de CUDA
GPU

Multiprocesador N

Multiprocesador 2
Multiprocesador 1

Memoria compartida

Registros Registros Registros


Unidad


de
Procesador 1 Procesador 2 Procesador M Control
SIMD

Cach para
constantes

Cach para
texturas

Memoria global

60
Recursos y limitaciones segn la GPU
que utilicemos para programar (CCC)
CUDA Compute Capability (CCC)
Limitacin Impacto
1.0, 1.1 1.2, 1.3 2.0, 2.1 3.0, 3.5

Multiprocesadores / GPU 16 30 14-16 14-16 Hardware


Escala-
bilidad
Cores / Multiprocesador 8 8 32 192 Hardware

Hilos / Warp 32 32 32 32 Software Ritmo de


salida de
Bloques / Multiprocesador 8 8 8 16 Software datos

Hilos / Bloque 512 512 1024 1024 Software


Paralelismo
Hilos / Multiprocesador 768 1 024 1 536 2048 Software

Registros de 32 bits / Multiprocesador 8K 16K 32K 64K Hardware Conjunto


de
16K 16K,
Memoria compartida / Multiprocesador 16K 16K 48K 32K, 48K Hardware trabajo
61
Bloques e hilos en GPU

Lmite en Kepler: 1024 hilos por bloque, 2048 hilos por multiprocesador

Los bloques se
asignan a los
multiprocesadores

[Lmite en Kepler:
16 bloques
concurrentes por
Bloque 0 Bloque 1 Bloque 2
cada
multiprocesador]
Malla 0 [Lmite en Kepler: 4G bloques por malla]

Los hilos se asocian a los multiprocesadores en bloques, y


se asignan a los cores en tomos de 32 llamados warps.
Los hilos de un bloque comparten la memoria compartida
y pueden sincronizarse mediante llamadas a synchthreads(). 62
Ejemplos de restricciones de paralelismo en
Kepler al tratar de maximizar concurrencia
Lmites para un multiprocesador: [1] 16 bloques concu-
rrentes, [2] 1024 hilos/bloque y [3] 2048 hilos en total.
1 bloque de 2048 hilos. No lo permite [2].
2 bloques de 1024 hilos. Posible en el mismo multiproc.
4 bloques de 512 hilos. Posible en el mismo multiproc.
4 bloques de 1024 hilos. No lo permite [3] en el mismo
multiprocesador, pero posible usando dos multiprocs.
8 bloques de 256 hilos. Posible en el mismo multiproc.
256 bloques de 8 hilos. No lo permite [1] en el mismo
multiprocesador, posible usando 16 multiprocesadores.
63
La memoria en la GPU: mbito y aplicacin
constantes y texturas

Estos bloques
Tambin se dispone
de una memoria de

BR BR BR BR BR BR BR BR pueden
ejecutarse en
MLLocal memory:
ML ML Off-chip
ML ML ML ML ML un mismo
multiprocesador
Memoria compartida si se cumplen
ciertas
Memoria global: GDDR5 (DRAM) limitaciones de
espacio
Bloque 0 Bloque 1 Bloque 2

Significado: BR: Banco de registros. ML = Memoria local
Memoria GPU: On-chip Off-chip Malla 0

Los hilos de un bloque pueden comunicarse a travs de la


memoria compartida del multiprocesador para trabajar de forma
ms cooperativa y veloz.
La memoria global es la nica visible a hilos, bloques y kernels.
64
Jugando con las restricciones de memoria en
Kepler para maximizar el uso de los recursos
Lmites dentro de un multiprocesador (SMX): 64 Kregs. y
48 KB. de memoria compartida. As:
Para permitir que un segundo bloque se ejecute en el mismo
multiprocesador, cada bloque debe usar a lo sumo 32 Kregs. y 24 KB.
de memoria compartida.
Para permitir que un tercer bloque se ejecute en el mismo multi-
procesador, cada bloque debe usar a lo sumo 21.3 Kregs. y 16 KB. de
memoria compartida.
...y as sucesivamente. Cuanto menor cantidad de
memoria usemos, mayor concurrencia para la ejecucin de
bloques.
El programador debe establecer el compromiso adecuado
entre apostar por memoria o paralelismo en cada cdigo. 65
Pensando el pequeo: Particionamiento 1D
de un vector de 64 elementos
Recordar: Mejor paralelismo de grano fino (asignar un dato
a cada hilo). Despliegue de bloques e hilos:
8 bloques de 8 hilos cada uno. Riesgo en bloques pequeos:
Desperdiciar paralelismo si se alcanza el mximo de 8/16
bloques en cada multiprocesador con pocos hilos en total.

4 bloques de 16 hilos cada uno. Riesgo en bloques grandes:


Estrangular el espacio de cada hilo (la memoria compartida y
el banco de registros se comparte entre todos los hilos).

66
Pensando a lo grande: Particionamiento 1D
de un vector de 64 millones de elementos
Mximo nmero de hilos por bloque: 1K.
Maximo nmero de bloques:
64K en Fermi y 4G en Kepler.
Tamaos ms grandes para las estructuras de datos slo
pueden implementarse mediante un nmero ingente de
bloques (si queremos preservar el paralelismo fino).
Posibilidades a elegir:
64K bloques de 1K hilos cada uno.
128K bloques de 512 hilos cada uno (slo factible en Kepler).
256K bloques de 256 hilos cada uno (slo factible en Kepler).
... y as sucesivamente.
67
Recopilando sobre kernels,
bloques y paralelismo
Los kernels se lanzan en grids.
Malla
Un bloque se ejecuta en un
multiprocesador (SMX). Bloque (0, 0) Bloque (1, 0)
El bloque no migra.
Varios bloques pueden residir
concurrentemente en un SMX. Memoria compartida Memoria compartida

Con las limitaciones de Kepler:


Regs Regs Regs Regs
Hasta 16 bloques concurrentes.
Hasta 1024 hilos en cada bloque.
Hasta 2048 hilos en cada SMX.
Hilo (0, 0) Hilo (1, 0) Hilo (0, 0) Hilo (1, 0)
Otras limitaciones entran en juego
debido al uso conjunto de la
memoria compartida y el banco de
registros segn hemos visto hace 3
diapositivas. Memoria global

68
Escalabilidad transparente

Dado que los bloques no se pueden sincronizar:


El hardware tiene libertad para planificar la ejecucin de un bloque
en cualquier multiprocesador.
Los bloques pueden ejecutarse secuencialmente o
concurrentemente segn la disponibilidad de recursos.
GPU con 2 SMX Malla para el kernel GPU con 4 SMX

SMX 0 SMX 1 Block 0 Block 1 SMX 0 SMX 1 SMX 2 SMX 3


Block 2 Block 3

Block 4 Block 5
Block 0 Block 1 Block 0 Block 1 Block 2 Block 3
Block 6 Block 7

Block 2 Block 3 Block 4 Block 5 Block 6 Block 7

Block 4 Block 5 ! Un kernel se puede expandir a lo largo de cualquier


nmero de procesadores (siempre que hayamos
Block 6 Block 7
declarado un nmero suficiente de bloques). 69
Espacios de memoria

La CPU y la GPU tiene espacios de memoria separados:


Para comunicar ambos procesadores, se utiliza el bus PCI-express.
En la GPU se utilizan funciones para alojar memoria y copiar datos
de la CPU de forma similar a como la CPU procede en lenguaje C
(malloc para alojar y free para liberar).
Los punteros son slo direcciones:
No se puede conocer a travs del valor de un puntero si la direccin
pertenece al espacio de la CPU o al de la GPU.
Hay que ir con mucha cautela a la hora de acceder a los datos a
travs de punteros, ya que si un dato de la CPU trata de accederse
desde la GPU o viceversa, el programa se colgar (esta situacin est
mejorando en el nuevo CUDA 6.0 que unifica la memoria de ambos
procesadores).
70
IV. Sintaxis
IV. 1. Elementos bsicos
CUDA es C con algunas palabras clave ms.
Un ejemplo preliminar
void saxpy_secuencial(int n, float a, float *x, float *y)
{
for (int i = 0; i < n; ++i)

}
y[i] = a*x[i] + y[i]; Cdigo C estndar
// Invocar al kernel SAXPY secuencial
saxpy_secuencial(n, 2.0, x, y);

Cdigo CUDA equivalente de ejecucin paralela en GPU:


__global__ void saxpy_paralelo(int n, float a, float *x,
float *y)
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < n) y[i] = a*x[i] + y[i];
}
// Invocar al kernel SAXPY paralelo con 256 hilos/bloque
int numero_de_bloques = (n + 255) / 256;
saxpy_paralelo<<<numero_de_bloques, 256>>>(n, 2.0, x, y);
73
Lista de extensiones sobre el lenguaje C

Modificadores para las variables __device__ float vector[N];

(type qualifiers): __global__ void filtro (float *image) {

global, device, shared, local, constant. __shared__ float region[M];


...
Palabras clave (keywords):
region[threadIdx.x] = image[i];
threadIdx, blockIdx.
Funciones intrnsecas (intrinsics): __syncthreads() ;
...
__syncthreads }
imagen[j] = result;

API en tiempo de ejecucin: // Alojar memoria en la GPU


void *myimage;
Memoria, smbolos, gestin de la cudaMalloc(&myimage, bytes);
ejecucin.
Funciones kernel para lanzar // 100 bloques de hilos, 10 hilos por bloque
convolucion <<<100, 10>>> (myimage);
cdigo a la GPU desde la CPU. 74
La interaccin entre la CPU y la GPU

CUDA extiende el lenguaje C con un nuevo tipo de funcin,


kernel, que ejecutan en paralelo los hilos activos en GPU.
El resto del cdigo es C nativo que se ejecuta sobre la CPU de
forma convencional.
De esta manera, el tpico main() de C combina la ejecucin
secuencial en CPU y paralela en GPU de kernels CUDA.
Un kernel se lanza siempre de forma asncrona, esto es, el
control regresa de forma inmediata a la CPU.
Cada kernel GPU tiene una barrera implcita a su conclusin,
esto es, no finaliza hasta que no lo hagan todos sus hilos.
Aprovecharemos al mximo el biprocesador CPU-GPU si les
vamos intercalando cdigo con similar carga computacional. 75
La interaccin entre la CPU y la GPU (cont.)
__global__ kernelA(){}
__global__ kernelB(){}
int main()
CPU
Ejecucin

kernelA <<< dimGridA, dimBlockA >>> (params.); GPU


CPU
kernelB <<< dimGridB, dimBlockB >>> (params.); GPU
CPU

Un kernel no comienza hasta que terminan los anteriores.


Emplearemos streams para definir kernels concurrentes. 76
Particin de datos para una matriz 2D [para
un patrn de acceso paralelo, ver ejemplo 2]
Anchura de la matriz

Bloque (0, 0) Bloque ((gridDim.x)-1, 0) - La posicin (hor,ver) para


Hilo (0,0) Hilo (1,0) (blockDim.x-1,0) Hilo (0,0) Hilo (1,0) (blockDim.x-1,0) el bloque dentro de la malla
es (blockIdx.x, blockIdx.y).
- La posicin (hor,ver) para
el hilo dentro del bloque es
(0,blockDim.y-1) (1,blockDim.y-1)
(blockDim.x-1,
blockDim.y-1) (0,blockDim.y-1) (1,blockDim.y-1)
(blockDim.x-1,
blockDim.y-1) (threadIdx.x, threadIdx.y).
Altura de la matriz

Para un caso de 16x16


bloques de 8x8 hilos,
y cada hilo encargado de
Bloque (0, (gridDim.y)-1) Bloque ((gridDim.x)-1, (gridDim.y)-1) una matriz de 4x4 datos:
Hilo (0,0) Hilo (1,0) (blockDim.x-1,0) Hilo (0,0) Hilo (1,0) (blockDim.x-1,0)

Para este hilo:


- blockIdx.x es (gridDim.x)-1, o sea, 15.
- blockIdx.y es 0.
- threadIdx.x es 1.
- threadIdx.y es 0.
(blockDim.x-1, (blockDim.x-1,
(0,blockDim.y-1) (1,blockDim.y-1) blockDim.y-1) (0,blockDim.y-1) (1,blockDim.y-1) blockDim.y-1) Este dato est en la columna:
[blockIdx.x * blockDim.x * 4] +
(threadIdx.x * 4) + 2, esto es,
[15 * 8 * 4] + (1 * 4) + 2 = 486.
Y en la fila:
[blockIdx.y * blockDim.y * 4] +
(threadIdx.y * 4) + 1 = 0 + 0 + 1 = 1. 77
Modificadores para las funciones y
lanzamiento de ejecuciones en GPU
Modificadores para las funciones ejecutadas en la GPU:
__global__ void MyKernel() { } // Invocado por la CPU
__device__ float MyFunc() { } // Invocado por la GPU
Modificadores para las variables que residen en la GPU:
__shared__ float MySharedArray[32]; // Mem. compartida
__constant__ float MyConstantArray[32];
Configuracin de la ejecucin para lanzar kernels:
dim2 gridDim(100,50); // 5000 bloques de hilos
dim3 blockDim(4,8,8); // 256 hilos por bloque
MyKernel <<< gridDim,blockDim >>> (pars.); // Lanzam.
Nota: Opcionalmente, puede haber un tercer parmetro
tras blockDim para indicar la cantidad de memoria
compartida que ser alojada dinmicamente por cada
kernel durante su ejecucin.
78
Variables y funciones intrnsecas

dim3 gridDim; // Dimension(es) de la malla


dim3 blockDim; // Dimension(es) del bloque

uint3 blockIdx; // Indice del bloque dentro de la malla


uint3 threadIdx; // Indice del hilo dentro del bloque

void __syncthreads(); // Sincronizacin entre hilos

El programador debe elegir el tamao del bloque y el


nmero de bloques para explotar al mximo el paralelismo
del cdigo durante su ejecucin.
79
Funciones para conocer en tiempo de
ejecucin con qu recursos contamos
Cada GPU disponible en la capa hardware recibe un
nmero entero que la identifica, comenzando por el 0.
Para conocer el nmero de GPUs disponibles:
cudaGetDeviceCount(int* count);
Para conocer los recursos disponibles en la GPU dev
(cach, registros, frecuencia de reloj, ...):
cudaGetDeviceProperties(struct cudaDeviceProp* prop, int dev);
Para conocer la mejor GPU que rene ciertos requisitos:
cudaChooseDevice(int* dev, const struct cudaDeviceProp* prop);

Para seleccionar una GPU concreta:


cudaSetDevice(int dev);
Para conocer en qu GPU estamos ejecutando el cdigo:
cudaGetDevice(int* dev);
80
Ejemplo: Salida de la funcin
cudaGetDeviceProperties
El programa se encuentra dentro del SDK de Nvidia.

81
Para gestionar la memoria de vdeo

Para reservar y liberar memoria en la GPU:


cudaMalloc(puntero, tamao)
cudaFree(puntero)
Para mover reas de memoria entre CPU y GPU:
En la CPU, declaramos malloc(h_A).
En la GPU, declaramos cudaMalloc(d_A).
Y una vez hecho esto, podemos:
Pasar los datos desde la CPU a la GPU:
cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
Pasar los datos desde la GPU a la CPU:
cudaMemcpy(h_A, d_A, numBytes, cudaMemcpyDeviceToHost);

El prefijo h_ suele usarse para punteros en memoria


principal. Idem d_ para punteros en memoria de vdeo. 82
IV. 2. Un par de ejemplos
Ejemplo 1: Descripcin del cdigo a programar

Alojar N enteros en la memoria de la CPU.


Alojar N enteros en la memoria de la GPU.
Inicializar la memoria de la GPU a cero.
Copiar los valores desde la GPU a la CPU.
Imprimir los valores.

84
Ejemplo 1: Implementacin
[cdigo C en rojo, extensiones CUDA en azul]
int main()
{
int N = 16;
int num_bytes = N*sizeof(int);
int *d_a=0, *h_a=0; // Punteros en dispos. (GPU) y host (CPU)

h_a = (int*) malloc(num_bytes);


cudaMalloc( (void**)&d_a, num_bytes);

if( 0==h_a || 0==d_a ) printf("No pude reservar memoria\n");

cudaMemset( d_a, 0, num_bytes);


cudaMemcpy( h_a, d_a, num_bytes, cudaMemcpyDeviceToHost);

for (int i=0; i<N; i++) printf("%d ", h_a[i]);

free(h_a);
cudaFree(d_a);
}
85
Transferencias de memoria asncronas

Las llamadas a cudaMemcpy() son sncronas, esto es:


No comienzan hasta que no hayan finalizado todas las llamadas
CUDA que le preceden.
El retorno a la CPU no tiene lugar hasta que no se haya realizado la
copia en memoria.
A partir de CUDA Compute Capabilities 1.2 es posible
utilizar la variante cudaMemcpyAsync(), cuyas diferencias
son las siguientes:
El retorno a la CPU tiene lugar de forma inmediata.
Podemos solapar comunicacin y computacin.

86
Ejemplo 2: Incrementar un valor b
a los N elementos de un vector
El kernel CUDA que se ejecuta en GPU,
Programa C en CPU.
seguido del cdigo host para CPU.
Este archivo se compila con gcc
Este archivo se compila con nvcc
__global__ void incremento_en_gpu(float *a, float b, int N)
void incremento_en_cpu(float *a, float b, int N) {
{ int idx = blockIdx.x * blockDim.x + threadIdx.x;
for (int idx = 0; idx<N; idx++) if (idx < N)
a[idx] = a[idx] + b; a[idx] = a[idx] + b;
} }

void main()
{
void main()
..
{
dim3 dimBlock (blocksize);
.....
dim3 dimGrid( ceil( N / (float)blocksize) );
incremento_en_cpu(a, b, N);
incremento_en_gpu<<<dimGrid, dimBlock>>>(a, b, N);
}
}

87
Ejemplo 2: Incrementar un valor b
a los N elementos de un vector

Con N=16 y blockDim=4, tenemos 4 bloques de hilos,


encargndose cada hilo de computar un elemento del vector.
Es lo que queremos: Paralelismo de grano fino en la GPU.
Extensiones
al lenguaje

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


blockDim.x = 4 blockDim.x = 4 blockDim.x = 4 blockDim.x = 4
threadIdx.x = 0,1,2,3 threadIdx.x = 0,1,2,3 threadIdx.x = 0,1,2,3 threadIdx.x = 0,1,2,3
idx = 0,1,2,3 idx = 4,5,6,7 idx = 8,9,10,11 idx = 12,13,14,15

int idx = (blockIdx.x * blockDim.x) + threadIdx.x; Patrn de acceso


comn a todos los
Se mapear del ndice local threadIdx.x al ndice global hilos

Nota: blockDim.x debera ser >= 32 (tamao del warp), esto es slo un ejemplo.
88
Cdigo en CPU para el ejemplo 2
[rojo es C, verde son variables, azul es CUDA]
// Aloja memoria en la CPU
unsigned int numBytes = N * sizeof(float);
float* h_A = (float*) malloc(numBytes);

// Aloja memoria en la GPU


float* d_A = 0; cudaMalloc((void**)&d_A, numbytes);

// Copia los datos de la CPU a la GPU


cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);

// Ejecuta el kernel con un nmero de bloques y tamao de bloque


incremento_en_gpu <<< N/blockSize, blockSize >>> (d_A, b);

// Copia los resultados de la GPU a la CPU


cudaMemcpy(h_A, d_A, numBytes, cudaMemcpyDeviceToHost);

// Libera la memoria de vdeo


cudaFree(d_A); 64 89
V. Compilacin
El proceso global de compilacin

void funcion_en_CPU( )
{
... Kernels Resto del
} CUDA cdigo C
void otras_funcs_CPU(int ...)
{
...
}

void saxpy_serial(float ... ) NVCC Compilador


{ (Open64) de la CPU
for (int i = 0; i < n; ++i)
y[i] = a*x[i] + y[i];
} Identificar
los kernels
void main( ) {
float x; CUDA y rees- Ficheros objeto Ficheros objeto
saxpy_serial(..);
cribirlos para CUDA Enlazador de la CPU
...
} aprovechar
paralelismo
en GPU Ejecutable
CPU-GPU

91
Los diferentes mdulos de compilacin
Aplicacin Cdigo
El cdigo fuente CUDA se CUDA C
fuente
compila con NVCC.
NVCC separa el cdigo que
se ejecuta en CPU del que lo NVCC Cdigo CPU
hace en GPU.
Virtual
La compilacin se realiza Cdigo PTX
en dos etapas:
Virtual: Genera cdigo PTX Fsica
(Parallel Thread eXecution). De PTX al com-
pilador destino
Fsica: Genera el binario para
una GPU especfica (o incluso
para una CPU multicore).
G80 GPU
Cdigo
objeto 92
Compilador nvcc y mquina virtual PTX

Aplicacin float4 me = gx[gtid];


me.x += me.y * me.z;
CUDA C

EDG
Separa cdigo GPU y CPU.
EDG Cdigo CPU Open64
Genera ensamblador PTX.
Parallel Thread eXecution (PTX)
Mquina virtual e ISA.
Open64
Modelo de programacin.
Recursos y estados de ejecucin.

Cdigo PTX

ld.global.v4.f32 {$f1,$f3,$f5,$f7}, [$r9+0];


mad.f32 $f1, $f5, $f3, $f1;
93
NVCC (NVidia CUDA Compiler)

NVCC es un driver del compilador.


Invoca a los compiladores y herramientas, como cudacc, g++, cl, ...
NVCC produce como salida: Proceso de compilacin Linux:
Cdigo C para la CPU, que debe luego
compilarse con el resto de la aplicacin
utilizando otra herramienta.
Cdigo objeto PTX para la GPU.

Proceso de
compilacin
en Windows:

94
Para conocer la utilizacin de los recursos

Compilar el cdigo del kernel con el flag -cubin para


conocer cmo se estn usando los registros.
Alternativa on-line: nvcc ptxas-options=-v
Abrir el archivo de texto .cubin y mirar la seccin code.
architecture {sm_10}
abiversion {0} Memoria local para cada hilo
modname {cubin} (usada por el compilador para
code { volcar contenidos de los registros
name = myGPUcode en memoria)
lmem = 0
smem = 68 Memoria compartida usada
reg = 20 por cada bloque de hilos
bar = 0
bincode { Registros usados
0xa0004205 0x04200780 0x40024c09 0x00200780 por cada hilo

95
Heursticos para la
configuracin de la ejecucin
El nmero de hilos por bloque debe ser un mltiplo de 32.
Para no desperdiciar la ejecucin de warps incompletos.
Debemos declarar ms bloques que multiprocesadores
haya (1), y a ser posible, ser ms del doble (2):
(1) Para que todos ellos tengan al menos un bloque que ejecutar.
(2) Para tener al menos un bloque activo que garantice la actividad del
SMX cuando el bloque en ejecucin sufra un parn debido a un acceso a
memoria, no disponibilidad de UFs, conflictos en bancos, esperas de todos
los hilos en puntos de sincronizacin (__synchthreads()), etc.
Los recursos por bloque (registros y memoria compartida)
deben ser al menos la mitad del total disponible.
De lo contrario, resulta mejor fusionar bloques.

96
Heursticos (cont.)

Reglas generales para que el cdigo sea escalable en futuras


generaciones y para que el flujo de bloques pueda ejecutarse de
forma segmentada (pipeline):
(1) Pensar a lo grande para el nmero de bloques.
(2) Pensar en pequeo para el tamao de los hilos.
Conflicto: Ms hilos por bloque significa mejor ocultacin de
latencia, pero menos registros disponibles para cada hilo.
Sugerencia: Utilizar un mnimo de 64 hilos por bloque, o incluso
mejor, 128 256 hilos (si an disponemos de suficientes registros).
Conflicto: Incrementar la ocupacin no significa necesariamente
aumentar el rendimiento, pero una baja ocupacin del
multiprocesador no permite ocultar latencias en kernels limitados por
el ancho de banda a memoria.
Sugerencia: Vigilar la intensidad aritmtica y el paralelismo.
97
Parametrizacin de una aplicacin

Todo lo que concierne al rendimiento es dependiente de la


aplicacin, por lo que hay que experimentar con ella para
lograr resultados ptimos.
Las GPUs evolucionan muy rpidamente, sobre todo en:
El n de multiprocesadores (SMs) y el nmero de cores por SMs.
El ancho de banda con memoria: Entre 100 y 500 GB/s.
El tamao del banco de registros de cada SM: 8K, 16K, 32K, 64K.
El tamao de la memoria compartida: 16 KB., ampliable a 48 KB. en
Fermi y Kepler.
Hilos: Comprobar el lmite por bloque y el lmite total.
Por bloque: 512 (G80 y GT200), 1024 (Fermi y Kepler).
Total: 768 (G80), 1024 (GT200), 1536 (Fermi), 2048 (Kepler).

98
CUDA Occupancy Calculator

Asesora en la seleccin de los parmetros de configuracin


http://developer.download.nvidia.com/compute/cuda/CUDA_Occupancy_calculator.xls

99
Para alcanzar el mayor grado de paralelismo,
fijarse en el rea naranja del CUDA Occup.(1)
El primer dato es el nmero de hilos por bloque:
El lmite es 1024 en las generaciones Fermi y Kepler.
Las potencias de dos son las mejores elecciones.
Lista de candidatos: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024.
Pondremos 256 como primera estimacin, y el ciclo de desarrollo nos
conducir al valor ptimo aqu, aunque normalmente:
Valores pequeos [2, 4, 8, 16] no explotan el tamao del warp ni los bancos de
memoria compartida.
Valores intermedios [32, 64] comprometen la cooperacin entre hilos y la
escalabilidad en Kepler, Maxwell y futuras generaciones de GPUs.
Valores grandes [512, 1024] nos impiden tener suficiente nmero de bloques
concurrentes en cada multiprocesador, ya que el mximo de hilos por bloque y SMX
son dos valores muy prximos. Adems, la cantidad de registros disponibles para
cada hilo es baja.
100
Para alcanzar el mayor grado de paralelismo,
fijarse en el rea naranja del CUDA Occup.(2)
El segundo dato es el n de registros que usa cada hilo.
Esto lo obtendremos del archivo .cubin.
El lmite es 8K (G80), 16K (GT200), 32K (Fermi), o 64K (Kepler),
as que consumiendo 10 registros/hilo es posible ejecutar:
En G80: 768 hilos/SM, o sea, 3 bloques de 256 hilos [3*256*10=7680] (< 8192).
En Kepler: Alcanzamos el mximo de 2048 hilos por SMX, aunque nos quedamos
muy cortos en el uso de los registros (podramos haber usado hasta 29 regs./hilo):
8 bloques * 256 hilos/bloque * 10 registros/hilo = 22480 regs. (< 65536 mx.).
En el caso de la G80, si hubiramos consumido 11 registros/hilo, ya
no podramos acomodar 3 bloques en el SMX, sino slo 2, por lo que
perderamos 1/3 del paralelismo => Habra que esforzarse en reducir
de 11 a 10 el nmero de registros.

101
Para alcanzar el mayor grado de paralelismo,
fijarse en el rea naranja del CUDA Occup.(3)
El tercer dato es el gasto de memoria compartida en cada
bloque de hilos:
Esto tambin lo obtenemos del archivo .cubin, aunque podemos
llevar una contabilidad manual, ya que todo depende de la declaracin
de variables __shared__ que elegimos como programador.
Lmites: 16 KB (CCC 1.x), 16/48 KB (CCC 2.x), 16/32/48 KB (3.x).
Para el caso anterior sobre la G80, no gastaremos ms de 5 KB de
memoria compartida por cada bloque para que podamos ejecutar el
mximo de 3 bloques en paralelo en cada multiprocesador:
3 bloques x 5 KB./bloque = 15 KB (< 16 KB.)
A partir de 5.33 KB. de memoria compartida usada por cada bloque,
estamos sacrificando un 33% del paralelismo, igual que suceda antes
si no ramos capaces de bajar a 10 registros/kernel.
102
VI. Ejemplos: VectorAdd, Stencil,
ReverseArray, MxM
Pasos para la construccin del cdigo CUDA

1. Identificar las partes del cdigo con mayor potencial para


beneficiarse del paralelismo de datos SIMD de la GPU.
2. Acotar el volumen de datos necesario para realizar dichas
computaciones.
3. Transferir los datos a la GPU.
4. Hacer la llamada al kernel.
5. Establecer las sincronizaciones entre la CPU y la GPU.
6. Transferir los resultados desde la GPU a la CPU.
7. Integrarlos en las estructuras de datos de la CPU.

104
Se requiere cierta coordinacin
en las tareas paralelas
El paralelismo viene expresado por los bloques e hilos.
Los hilos de un bloque pueden requerir sincronizacin si
aparecen dependencias, ya que slo dentro del warp se
garantiza su progresin conjunta (SIMD). Ejemplo:
a[i] = b[i] + 7;
syncthreads();
x[i] = a[i-1]; // El warp 1 lee aqu el valor a[31],
// que debe haber sido escrito ANTES por el warp 0

En las fronteras entre kernels hay barreras implcitas:


Kernel1 <<<nblocks,nthreads>>> (a,b,c);
Kernel2 <<<nblocks,nthreads>>> (a,b);
Bloques pueden coordinarse usando operaciones atmicas.
Ejemplo: Incrementar un contador atomicInc();
105
VI. 1. Suma de dos vectores
Cdigo para GPU y su invocacin desde CPU
// Suma de dos vectores de tamao N: C[1..N] = A[1..N] + B[1..N]
// Cada hilo computa un solo componente del vector resultado C
__global__ void vecAdd(float* A, float* B, float* C) {

int tid = threadIdx.x + (blockDim.x* blockIdx.x);

C[tid] = A[tid] + B[tid]; Cdigo GPU
}
int main() { // Lanza 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 ejecuta


en la GPU (device) y ser llamado desde la CPU (host).
A, B y C son punteros a la memoria de vdeo de la GPU,
por lo que necesitamos:
Alojar/liberar memoria en GPU, usando cudaMalloc/cudaFree.
Estos punteros no pueden ser utilizados desde el cdigo de la CPU. 107
Cdigo CPU para manejar la memoria y
recoger los resultados de 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);
// Copiar los datos de entrada desde la CPU a la GPU
cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, numBytes, cudaMemcpyHostToDevice);
(aqu colocamos la llamada al kernel VecAdd de la pg. anterior)
// Copiar los resultados desde la GPU a la CPU
float* h_C = (float*) malloc(numBytes);
cudaMemcpy(h_C, d_C, numBytes, cudaMemcpyDeviceToHost);
// Liberar la memoria de vdeo
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
64 108
Ejecutando en paralelo
(independientemente de la generacin HW)
vecAdd <<< 1, 1 >>> () GPU
Ejecuta 1 bloque de 1 hilo. Multiprocesador 30
(escalabilidad en 1 gener.)
No hay paralelismo. Multiprocesador 2
Multiprocesador 1
vecAdd <<< B, 1 >>> ()
Ejecuta B bloques de 1 hilo. Memoria compartida

Hay paralelismo entre Registros Registros Registros

multiprocesadores. Core 1 Core 2 Core 8


(escalabilidad en 2 y 3 gener.)
vecAdd <<< B, M >>> ()
Ejecuta B bloques compuestos Cach de texturas
de M hilos. Hay paralelismo
entre multiprocesadores y entre
los cores del multiprocesador. Memoria global
109
Calculando el ndice de acceso a un vector
segn el bloque y el hilo dentro del mismo
Con M hilos por bloque, un ndice unvoco viene dado por:
tid = threadIdx.x+ blockDim.x* blockIdx.x;
Para acceder a un vector en el que cada hilo computa un
solo elemento (queremos paralelismo de grano fino), B=4
bloques de M=8 hilos cada uno:
threadIdx.x threadIdx.x threadIdx.x threadIdx.x
01234567012345670123456701234567
blockIdx.x = 0 blockIdx.x = 1 blockIdx.x = 2 blockIdx.x = 3

Qu hilo computar el vigsimo segundo elemento del


vector?
gridDim.x es 4. blockDim.x es 8. blockIdx.x = 2. threadIdx.x = 5.
tid = 5 + (8 * 2) = 21 (al comenzar en 0, ste es el elemento 22). 110
Manejando vectores de tamao genrico

Los algoritmos reales no suelen tener dimensiones que


sean mltiplos exactos de blockDim.x, as que debemos
desactivar los hilos que computan fuera de rango:
// 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];
}

Y ahora, hay que incluir el bloque "incompleto" de hilos


en el lanzamiento del kernel (redondeo al alza en la div.):

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

111
VI. 2. Kernels patrn (stencils)
Por qu hemos seleccionado este cdigo

En el ejemplo anterior, los hilos aaden complejidad sin


realizar contribuciones reseables.
Sin embargo, los hilos pueden hacer cosas que no estn al
alcance de los bloques paralelos:
Comunicarse (a travs de la memoria compartida).
Sincronizarse (por ejemplo, para salvar los conflictos por
dependencias de datos).
Para ilustrarlo, necesitamos un ejemplo ms sofisticado ...

113
Patrn unidimensional (1D)

Apliquemos un patrn 1D a un vector 1D.


Al finalizar el algoritmo, cada elemento contendr la suma de todos
los elementos dentro de un radio prximo al elemento dado.
Por ejemplo, si el radio es 3, cada elemento agrupar la
suma de 7:

radio radio
De nuevo aplicamos paralelismo de grano fino, por lo que
cada hilo se encargar de obtener el resultado de un solo
elemento del vector resultado.
Los valores del vector de entrada deben leerse varias veces:
Por ejemplo, para un radio de 3, cada elemento se leer 7 veces. 114
Compartiendo datos entre hilos. Ventajas

Los hilos de un mismo bloque pueden compartir datos a


travs de memoria compartida.
La memoria compartida es gestionada de forma explcita
por el programador insertando el prefijo __shared__ en la
declaracin de la variable.
Los datos se alojan para cada uno de los bloques.
La memoria compartida es extremadamente rpida:
500 veces ms rpida que la memoria global (que es memoria de video GDDR5).
La diferencia es tecnolgica: esttica (construida con transistores) frente a dinmica
(construida con mini-condensadores).
El programador puede ver la memoria compartida como una extensin del banco
de registros.
Pero la memoria compartida es ms verstil que los registros, ya
que stos son privados para cada hilo. 115
Compartiendo datos entre hilos. Limitaciones

El uso de los registros y la memoria compartida limita el


paralelismo.
Si dejamos espacio para un segundo bloque en cada multiprocesador,
el banco de registros y la memoria compartida se particionan (aunque
la ejecucin no es simultnea, el cambio de contexto es inmediato).
Ya mostramos ejemplos para Kepler anteriormente. Con un
mximo de 64K registros y 48 Kbytes de memoria compartida
por cada multiprocesador SMX, tenemos:
Para 2 bl./SMX: No superar 32 Kregs. ni 24 KB. de memoria comp.
Para 3 bl./SMX: No superar 21.33 Kregs. ni 16 KB. de memoria comp.
Para 4 bl./SMX: No superar 16 Kregs. ni 12 KB. de memoria comp.
... y as sucesivamente. Usar el CUDA Occupancy Calculator para
asesorarse en la seleccin de la configuracin ms adecuada. 116
Utilizando la memoria compartida

Pasos para cachear los datos en memoria compartida:


Leer (blockDim.x + 2 * radio) elementos de entrada desde la
memoria global a la memoria compartida.
Computar blockDim.x elementos de salida.
Escribir blockDim.x elementos de salida a memoria global.
Cada bloque necesita una extensin de radio elementos
en los extremos del vector.

extensin por extensin por


la izquierda la derecha

blockDim.x elementos de salida


117
Kernel patrn
__global__ void stencil_1d(int *d_in, int *d_out,
int RADIO)
{
__shared__ int temp[BLOCK_SIZE + 2 * RADIO];
int gindex = threadIdx.x
+ blockIdx.x * blockDim.x;
int lindex = threadIdx.x + RADIO;

// Pasar los elementos a memoria compartida


temp[lindex] = d_in[gindex];
if (threadIdx.x < RADIO) {
temp[lindex-RADIO] = d_in[gindex-RADIO];
temp[lindex+BLOCK_SIZE] = d_in[gindex
+BLOCK_SIZE]; Pero hay que prevenir
} condiciones de carrera. Por
ejemplo, el ltimo hilo lee la
// Aplicar el patrn
int result = 0;
extensin antes del que el
for (int offset=-RADIO; offset<=RADIO; offset++) primer hilo haya cargado
result += temp[lindex + offset]; esos valores (en otro warp).
Se hace necesaria una
// Almacenar el resultado sincronizacin entre hilos.
d_out[gindex] = result; 118
}
Sincronizacin entre hilos

Usar __synchthreads();
__global__ void stencil_1d(...) {
para sincronizar todos los hilos de
un bloque: < Declarar variables e ndices>

La barrera tiene que ser alcanzada < Pasar el vector a mem. compartida >
por todos los hilos antes de que stos
puedan continuar. __synchthreads();
Puede utilizarse para prevenir < Aplicar el patrn >
riesgos del tipo RAW / WAR / WAW.
En sentencias condicionales, la < Almacenar el resultado >
codicin debe ser uniforme a lo largo
}
de todo el bloque.

119
Recopilando los conceptos puestos
en prctica en este ejemplo
Lanzar N bloques con M hilos por bloque para ejecutar los hilos
en paralelo. Emplear:
kernel<<<N,M>>>();
Acceder al ndice del bloque dentro de la malla y al ndice del hilo
dentro del bloque. Emplear:
blockIdx.x y threadIdx.x;
Calcular los ndices globales donde cada hilo tenga que trabajar
en un rea de datos diferente segn la particin. Emplear:
int index = threadIdx.x + blockIdx.x * blockDim.x;
Declarar el vector en memoria compartida. Emplear:
__shared__ (como prefijo antecediendo al tipo de dato en la declaracin).
Sincronizar los hilos para prevenir los riesgos de datos. Emplear:
__synchthreads(); 120
VI. 3. Invertir el orden a
los elementos de un vector
Cdigo en GPU para el kernel ReverseArray
(1) utilizando un nico bloque
__global__ void reverseArray(int *in, int *out) {
int index_in = threadIdx.x;
int index_out = blockDim.x 1 threadIdx.x;

// Invertir los contenidos del vector con un solo bloque


out[index_out] = in[index_in];
}

Es una solucin demasiado simplista: No aspira a aplicar


paralelismo masivo porque el mximo tamao de bloque es
1024 hilos, con lo que se sera el mayor vector que este
cdigo podra aceptar como entrada.

122
Cdigo en GPU para el kernel ReverseArray
(2) con mltiples bloques
__global__ void reverseArray(int *in, int *out) {
int in_offset = blockIdx.x * blockDim.x;
int out_offset = (gridDim.x 1 blockIdx.x) * blockDim.x;
int index_in = in_offset + threadIdx.x;
int index_out = out_offset + (blockDim.x 1 threadIdx.x);

// Invertir los contenidos en fragmentos de bloques enteros


out[index_out] = in[index_in];
}

123
Versin utilizando la memoria compartida

124
Cdigo en GPU para el kernel ReverseArray
(3) con mltiples bloques y mem. compartida
__global__ void reverseArray(int *in, int *out) {
__shared__ int temp[BLOCK_SIZE];
int gindex = threadIdx.x + blockIdx.x * blockDim.x;
int lindex = threadIdx.x;

// Colocar los elementos de entrada en memoria compartida


temp[lindex] = in[gindex];
synchthreads();

// Invertir los elementos del vector local a cada bloque


temp[lindex] = temp[blockDim.x-lindex-1];
synchthreads();

// Invertir los contenidos en fragmentos de bloques enteros


out[threadIdx.x + ((N/blockDim.x)-blockIdx.x-1) * blockDim.x] = temp[lindex];
}

125
VI. 4. Producto de matrices
Versin de cdigo CPU escrita en lenguaje C
B
C = A * B.
Matrices cuadradas de tamao N * N.
Linearizadas en vectores para simplificar
el alojamiento de memoria dinmica.
void MxMonCPU(float* A, float* B, float* C, int N);
{
for (int i=0; i<N; i++)
for (int j=0; j<N; j++) A C
{
float sum=0;
for (int k=0; k<N; k++)
{
float a = A[i*N + k];

N
float b = B[k*N + j];
sum += a*b;
}
C[i*N + j] = sum;
} N N
} 127
Versin CUDA para el producto de matrices:
Un primer borrador para el cdigo paralelo
B
__global__ MxMonGPU(float* A, float* B, float* C, int N);
{
float sum=0;
int i, j;
i = threadIdx.x + blockIdx.x * blockDim.x;
j = threadIdx.y + blockIdx.y * blockDim.y;
for (int k=0; k<N; k++)
{
float a = A[i*N + k];
float b = B[k*N + j];
sum += a*b; A C
}
C[i*N + j] = sum;
}

N
N N
128
Versin CUDA para el producto de matrices:
Descripcin de la paralelizacin
Cada hilo computa un elemento de la matriz resultado C.
Las matrices A y B se cargan N veces desde memoria de vdeo.
Los bloques acomodan los hilos en grupos de 1024

N
(limitacin interna en arquitecturas Fermi y Kepler).
As podemos usar bloques 2D de 32x32 hilos cada uno.
Anchura B Anchura A Anchura B
Malla
Bloque

Altura B
Altura A
Altura A
= X
C(x, y)

N
C A B
dim2 dimBlock(BLOCKSIZE, BLOCKSIZE);
Hilo(x,y)
dim2 dimGrid(AnchuraB/BLOCKSIZE, AlturaA/BLOCKSIZE);
...
N
MxMonGPU <<<dimGrid,dimBlock>>> (A, B,N C, N);
129
Versin CUDA para el producto de matrices:
Anlisis
Cada hilo utiliza 10 registros, lo que nos permite alcanzar
el mayor grado de paralelismo en Kepler:
2 bloques de 1024 hilos (32x32) en cada SMX. [2x1024x10 =
20480 registros, que es inferior a la cota de 65536 regs. disponibles].
Problemas:
Baja intensidad aritmtica.
Exigente en el ancho de banda a memoria, que termina erigindose
como el cuello de botella para el rendimiento.
Solucin:
Utilizar la memoria compartida de cada multiprocesador.

130
Utilizando la memoria compartida:
Versin con mosaicos (tiling)
B
La submatriz de Csub de 32x32 datos

M
computada por cada bloque de hilos
utiliza mosaicos de 32x32 elementos de A

N
y B que se alojan de forma reiterativa en
memoria compartida.

M
A y B se cargan slo (N/32) veces
desde memoria global. A C

Logros:
Menos exigente en el Csub

ancho de banda a memoria.

N
Ms intensidad aritmtica.
M M M M

N N
131
Tiling: Detalles de la implementacin

Tenemos que gestionar todos los mosaicos pertenecientes a


cada bloque de hilos:
Se cargan los mosaicos de entrada (A y B) desde memoria global a
memoria compartida en paralelo (todos los hilos contribuyen). Estos
mosaicos reutilizan el espacio de memoria compartida.
__syncthreads() (para asegurarnos que hemos cargado las matrices
completamente antes de comenzar la computacin).
Computar todos los productos y sumas para C utilizando los mosaicos de
memoria compartida.
Cada hilo puede ahora iterar independientemente sobre los elementos del mosaico.
__syncthreads() (para asegurarnos que la computacin con el
mosaico ha acabado antes de cargar, en el mismo espacio de memoria
compartida, dos nuevos mosaicos para A y B en la siguiente iteracin).
Transferir los resultados de C alojados en memoria global.
132
Un truco para evitar conflictos en el acceso
a los bancos de memoria compartida
Recordemos algunos rasgos de CUDA:
La memoria compartida consta de 16 (pre-Fermi) 32 bancos.
Los hilos de un bloque se enumeran en orden "column major", esto
es, la dimensin x es la que ms rpidamente vara.
Si accedemos de la forma habitual a los vectores en
memoria compartida: shData[threadIdx.x][threadIdx.y], los
hilos de un mismo warp leern de la misma columna, esto
es, del mismo banco en memoria compartida.
En cambio, utilizando shData[threadIdx.y][threadIdx.x],
leern de la misma fila, accediendo a un banco diferente.
Por tanto, los mosaicos se almacenan y acceden en
memoria compartida de forma invertida o traspuesta. 133
Tiling: El cdigo CUDA para el kernel en GPU
__global__ void MxMonGPU(float *A, float *B, float *C, int N)
{
int sum=0, tx, ty, i, j;
tx = threadIdx.x; ty = threadIdx.y;
i = tx + blockIdx.x * blockDim.x; j = ty + blockIdx.y * blockDim.y;
__shared__ float As[32][32], float Bs[32][32];

// Recorre los mosaicos de A y B necesarios para computar la submatriz de C


for (int tile=0; tile<(N/32); tile++)
{
// Carga los mosaicos (32x32) de A y B en paralelo (y de forma traspuesta)
As[ty][tx]= A[(i*N) + (ty+(tile*32))];
Bs[ty][tx]= B[((tx+(tile*32))*N) + j];
__syncthreads();
// Computa los resultados para la submatriz de C
for (int k=0; k<32; k++) // Los datos tambin se leern de forma traspuesta
sum += As[k][tx] * Bs[ty][k];
__syncthreads();
}
// Escribe en paralelo todos los resultados obtenidos por el bloque
C[i*N+j] = sum;
}

134
Una optimizacin gracias al compilador:
Desenrrollado de bucles (loop unrolling)
Sin desenrrollar el bucle: Desenrrollando el bucle:
... ...
__syncthreads(); __syncthreads();

// Computar la parte de ese mosaico // Computar la parte de ese mosaico


for (k=0; k<32; k++) sum += As[tx][0]*Bs[0][ty];
sum += As[tx][k]*Bs[k][ty]; sum += As[tx][1]*Bs[1][ty];
sum += As[tx][2]*Bs[2][ty];
__syncthreads(); sum += As[tx][3]*Bs[3][ty];
} sum += As[tx][4]*Bs[4][ty];
C[indexC] = sum; sum += As[tx][5]*Bs[5][ty];
sum += As[tx][6]*Bs[6][ty];
sum += As[tx][7]*Bs[7][ty];
sum += As[tx][8]*Bs[8][ty];


sum += As[tx][31]*Bs[31][ty];
__syncthreads();
}
C[indexC] = sum;
135
Rendimiento con tiling & unrolling en la G80

100

75

GFLOPS
50

25
Slo tiling
Tiling & Unrolling

0
4x4 8x8 12x12 16x16
Tamao del mosaico (32x32 no es factible en la G80)
136
VII. Bibliografa y herramientas
CUDA Zone:
La web bsica del programador CUDA

- Lenguajes (C/C++, Python).


- Libreras (cuBLAS, cuFFT).
- Directivas (OpenACC).
- Templates (thrust).

- Compilador (NVCC).
- Depurador (GDB).
- Profiler (cudaprof y Visual).
- Entorno de desarrollo (Nsight).
- Cdigos de ejemplo.

- Eclipse.
- Matlab.
- CUDA Fortran.
- GPUDirect.
- SDK para el compilador LLVM.

[developer.nvidia.com/cuda-zone] 138
CUDA 6 Production Release. Descarga gratis
para todas las plataformas y usuarios
[developer.nvidia.com/cuda-downloads]

139
Libros sobre CUDA: Desde 2007 hasta 2013

La serie GPU Gems [developer.vidia.com/content/GPUGems3/gpugems3_part01.html]


Una lista de libros de CUDA [www.nvidia.com/object/cuda_books.html]

Sep'07 Feb'10 Jul'10 Abr'11 Oct'11

Nov'11 Dic'12 Jun'13 Oct'13 140


Guas de desarrollo y otros documentos

Para iniciarse con CUDA C: La gua del programador.


[docs.nvidia.com/cuda/cuda-c-programming-guide]
Para optimizadores de cdigo: La gua con los mejores trucos.
[docs.nvidia.com/cuda/cuda-c-best-practices-guide]
La web raz que aglutina todos los documentos ligados a CUDA:
[docs.nvidia.com/cuda]
donde encontramos, adems de las guas anteriores, otras para:
Instalar CUDA en Linux, MacOS y Windows.
Optimizar y mejorar los programas CUDA sobre plataformas Kepler.
Consultar la sintaxis del API de CUDA (runtime, driver y math).
Aprender a usar libreras como cuBLAS, cuFFT, cuRAND, cuSPARSE, ...
Manejar las herramientas bsicas (compilador, depurador, optimizador).
141
Opciones para acelerar tus aplicaciones en
CUDA y material para ensear CUDA
[developer.nvidia.com/cuda-education-training] (accesible
tambin desde la parte inferior izquierda del CUDA Zone)

142
Cursos on-line de acceso gratuito

Realizados por ms de 50.000 programadores de 127 pases


en los ltimos 6 meses. Impartidos por reputados pedagogos:
Prof. Wen-Mei Hwu (Univ. of Illinois).
Prof. John Owens (Univ. of California at Davis).
Dr. David Luebke (Nvidia Research).

Hay dos opciones, igualmente recomendables:


Introduccin a la programacin paralela: [www.udacity.com]
Programacin paralela heterognea: [www.coursera.org]

143
Tutoriales cortos sobre
C/C++, Fortran y Python
Hay que registrarse en la Web de los tutoriales que hay
dados de alta en los servicios en la nube de Amazon EC2:
[nvidia.qwiklab.com]
Suelen ser sesiones de 90 minutos.
Slo se necesita un navegador de Web y una conexin SSH.
Algunos tutoriales son gratuitos, otros requieren tokens de $29.99.

144
Charlas y webinarios

Charlas grabadas @ GTC (Graphics Technology Conference):


383 charlas de la edicin de 2013.
Ms de 500 charlas de la edicin de 2014.
[www.gputechconf.com/gtcnew/on-demand-gtc.php]
Webinarios sobre computacin en GPU:
Listado histrico de charlas en vdeo (mp4/wmv) y diapositivas (PDF).
Listado de prximas charlas on-line a las que poder apuntarse.
[developer.nvidia.com/gpu-computing-webinars]
CUDACasts:
[bit.ly/cudacasts]

145
Ejemplos de webinarios sobre CUDA 6.0

146
Desarrolladores

Para firmar como desarrollador registrado:


[www.nvidia.com/paralleldeveloper]
Acceso a las descargas exclusivas para desarrolladores.
Acceso exclusivo a las versiones ms avanzadas de CUDA.
Actividades exclusivas y ofertas especiales.
Sitio de encuentro con otros muchos desarrolladores:
[www.gpucomputing.net]
Noticias y eventos de GPGPU:
[www.gpgpu.org]
Lanzar cuestiones tcnicas o preguntar dudas on-line:
NVIDIA Developer Forums: [devtalk.nvidia.com]
Busca respuestas o suscribe preguntas: [stackoverflow.com/tags/cuda]
147
Desarrolladores (2)

Listado oficial de GPUs que soportan CUDA:


[developer.nvidia.com/cuda-gpus]
Y una ltima herramienta: El CUDA Occupancy Calculator
[developer.download.nvidia.com/compute/cuda/
CUDA_Occupancy_calculator.xls]

148
Tendencias futuras

El blog de Nvidia, Parallel for all contiene los artculos


ms jugosos y fiables sobre todo lo nuevo que est
apareciendo en torno a CUDA (conviene suscribirse):
[devblogs.nvidia.com/parallelforall]
Algunos artculos especialmente interesantes:
5 Powerful New Features in CUDA 6, por Mark Harris.
CUDA 6.0 Unified Memory, por Mark Ebersole.
CUDA Dynamic Parallelism API and Principles, por Andrew Adinetz
NVLINK, Pascal and Stacked Memory: Feeding the Appetite for Big
Data, por Denis Foley.
CUDA Pro Tip: Increase Application Performance with NVIDIA GPU
Boost, por Mark Harris.
149
Muchas gracias por vuestra atencin

Siempre a vuestra disposicin en el


Departamento de Arquitectura de
Computadores de la Universidad de Mlaga
e-mail: ujaldon@uma.es
Telfono: +34 952 13 28 24.
Pgina Web: http://manuel.ujaldon.es
(versiones en castellano e ingls).
O de forma ms especfica sobre GPUs,
visita mi pgina Web como CUDA Fellow:
http://research.nvidia.com/users/manuel-ujaldon

150

También podría gustarte